You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

295 lines
10 KiB

8 months ago
const fs = require('fs');
const path = require('path');
const XLSX = require('xlsx');
const { getCompressedDefaultImage } = require('../function/GetImageDefaault');
const { parse } = require('csv-parse/sync');
const { pool } = require('../database');
const dayjs = require('dayjs');
const customParseFormat = require('dayjs/plugin/customParseFormat');
dayjs.extend(customParseFormat);
4 months ago
// ---------- Fonctions utilitaires ----------
function nextLevel(niveau) {
const levels = ['L1', 'L2', 'L3', 'M1', 'M2', 'D1', 'D2', 'D3', 'PHD']
const idx = levels.indexOf(niveau)
return idx === -1 || idx === levels.length - 1 ? niveau : levels[idx + 1]
}
8 months ago
function fixEncoding(str) {
if (typeof str !== 'string') return str;
return str
.replace(/├®/g, 'é')
.replace(/├à/g, 'à')
.replace(/├©/g, 'é')
.replace(/├ô/g, 'ô')
.replace(/├ù/g, 'ù')
.replace(/’/g, "'")
.replace(/â€/g, '…')
.replace(/â€/g, '-');
}
4 months ago
function convertToISODate(input) {
8 months ago
if (!input) return null;
if (input instanceof Date && !isNaN(input)) {
4 months ago
return dayjs(input).format('YYYY-MM-DD');
}
8 months ago
if (typeof input === 'number') {
4 months ago
return dayjs(new Date((input - 25569) * 86400 * 1000)).format('YYYY-MM-DD');
}
8 months ago
if (typeof input === 'string') {
const cleanInput = input.trim();
const versMatch = cleanInput.match(/vers\s*(\d{4})/i);
4 months ago
if (versMatch) return `${versMatch[1]}-01-01`;
8 months ago
const formats = [
4 months ago
'DD/MM/YYYY', 'D/M/YYYY',
'YYYY-MM-DD',
'DD-MM-YYYY', 'D-M-YYYY',
'MM/DD/YYYY', 'M/D/YYYY',
'MM-DD-YYYY', 'M-D-YYYY',
'DD/MM/YY', 'D/M/YY',
8 months ago
'MM/DD/YY', 'M/D/YY',
'DD-MM-YY', 'D-M-YY',
'MM-DD-YY', 'M-D-YY'
];
4 months ago
for (const fmt of formats) {
const parsed = dayjs(cleanInput, fmt, true);
if (parsed.isValid()) return parsed.format('YYYY-MM-DD');
8 months ago
}
4 months ago
const freeParse = dayjs(cleanInput);
if (freeParse.isValid()) return freeParse.format('YYYY-MM-DD');
}
8 months ago
return null;
}
// ---------- UPDATE étudiant existant ----------
async function updateEtudiant(row, conn) {
4 months ago
const fields = [];
const params = [];
8 months ago
4 months ago
function addFieldIfValue(field, value) {
if (value !== undefined && value !== null && value !== '') {
fields.push(`${field} = ?`);
params.push(value);
}
}
// Only permanent fields in etudiants
4 months ago
addFieldIfValue('nom', row.nom);
addFieldIfValue('prenom', row.prenom);
addFieldIfValue('date_de_naissances', convertToISODate(row.date_naissance));
addFieldIfValue('num_inscription', row.num_inscription?.toString());
addFieldIfValue('sexe', row.sexe);
addFieldIfValue('date_delivrance', convertToISODate(row.date_de_delivrance));
addFieldIfValue('nationalite', row.nationaliter);
addFieldIfValue('annee_bacc', parseInt(row.annee_baccalaureat, 10));
addFieldIfValue('serie', row.serie);
addFieldIfValue('boursier', row.boursier);
addFieldIfValue('domaine', fixEncoding(row.domaine));
addFieldIfValue('contact', row.contact);
if (fields.length === 0) return { success: false, error: 'Aucun champ valide à mettre à jour' };
let sql, whereParams;
if (row.cin && row.cin.toString().trim() !== '') {
sql = `UPDATE etudiants SET ${fields.join(', ')} WHERE cin = ?`;
whereParams = [row.cin];
} else {
sql = `UPDATE etudiants SET ${fields.join(', ')} WHERE LOWER(TRIM(nom)) = ? AND LOWER(TRIM(prenom)) = ?`;
whereParams = [row.nom.toLowerCase().trim(), row.prenom.toLowerCase().trim()];
}
8 months ago
try {
const [result] = await conn.query(sql, [...params, ...whereParams]);
// Update or create inscription for this year
if (result.affectedRows > 0) {
// Get the etudiant id
let etudiantId;
if (row.cin && row.cin.toString().trim() !== '') {
const [et] = await conn.query('SELECT id FROM etudiants WHERE cin = ?', [row.cin]);
if (et.length > 0) etudiantId = et[0].id;
} else {
const [et] = await conn.query(
'SELECT id FROM etudiants WHERE LOWER(TRIM(nom)) = ? AND LOWER(TRIM(prenom)) = ?',
[row.nom.toLowerCase().trim(), row.prenom.toLowerCase().trim()]
);
if (et.length > 0) etudiantId = et[0].id;
}
if (etudiantId && row.annee_scolaire_id) {
// Check if inscription exists for this year
const [existing] = await conn.query(
'SELECT id FROM inscriptions WHERE etudiant_id = ? AND annee_scolaire_id = ?',
[etudiantId, row.annee_scolaire_id]
);
if (existing.length > 0) {
await conn.query(
`UPDATE inscriptions SET niveau=?, mention_id=?, status=?, num_inscription=? WHERE id=?`,
[row.niveau, row.mention, row.code_redoublement, row.num_inscription?.toString(), existing[0].id]
);
} else {
await conn.query(
`INSERT INTO inscriptions (etudiant_id, annee_scolaire_id, niveau, mention_id, status, num_inscription)
VALUES (?, ?, ?, ?, ?, ?)`,
[etudiantId, row.annee_scolaire_id, row.niveau, row.mention, row.code_redoublement, row.num_inscription?.toString()]
);
}
}
}
8 months ago
return { success: true, affectedRows: result.affectedRows };
8 months ago
} catch (error) {
8 months ago
return { success: false, error: error.message };
8 months ago
}
}
4 months ago
// ---------- IMPORT fichier ----------
async function importFileToDatabase(filePath) {
8 months ago
let records;
4 months ago
const ext = path.extname(filePath).toLowerCase();
4 months ago
if (ext === '.xlsx') {
8 months ago
const workbook = XLSX.readFile(filePath);
const worksheet = workbook.Sheets[workbook.SheetNames[0]];
4 months ago
records = XLSX.utils.sheet_to_json(worksheet, { defval: '' });
} else if (ext === '.csv') {
const content = fs.readFileSync(filePath, 'utf8');
records = parse(content, { columns: true, skip_empty_lines: true });
} else {
return { error: true, message: 'Format de fichier non supporté' };
}
8 months ago
// Vérifier champs obligatoires
const requiredFields = [
'nom', 'date_naissance', 'niveau', 'annee_scolaire',
'mention', 'num_inscription', 'nationaliter', 'sexe',
'annee_baccalaureat', 'serie', 'code_redoublement',
'boursier', 'domaine'
];
for (const [i, row] of records.entries()) {
4 months ago
for (const f of requiredFields) {
if (!row[f]) return { error: true, message: `Le champ '${f}' est manquant à la ligne ${i + 2}` };
}
8 months ago
}
const conn = await pool.getConnection();
try {
await conn.beginTransaction();
const [mentionRows] = await conn.query('SELECT * FROM mentions');
const [statusRows] = await conn.query('SELECT * FROM status');
const etudiantsToInsert = [];
const doublons = [];
for (const row of records) {
row.date_naissance = convertToISODate(row.date_naissance);
// Mapping mention
const matchedMention = mentionRows.find(
m => m.nom.toUpperCase() === row.mention.toUpperCase() || m.uniter.toUpperCase() === row.mention.toUpperCase()
4 months ago
);
if (matchedMention) row.mention = matchedMention.id;
// Mapping status
const codeLettre = (row.code_redoublement ? row.code_redoublement.trim().substring(0, 1) : 'N');
row.code_redoublement = codeLettre;
const statusMatch = statusRows.find(s => s.nom.toLowerCase().startsWith(row.code_redoublement.toLowerCase()));
if (statusMatch) row.code_redoublement = statusMatch.id;
// Auto-progression du niveau selon le statut
// Passant (id=2) → niveau supérieur | Redoublant/Nouveau/Renvoyé/Ancien → niveau inchangé
if (row.code_redoublement === 2) {
row.niveau = nextLevel(row.niveau)
}
// Get annee_scolaire_id
const [anneeRows] = await conn.query('SELECT id FROM anneescolaire WHERE code = ?', [row.annee_scolaire]);
if (anneeRows.length === 0) {
await conn.rollback();
return { error: true, message: `Année scolaire '${row.annee_scolaire}' introuvable dans la base de données.` };
}
row.annee_scolaire_id = anneeRows[0].id;
// Détection doublons
let existing;
if (row.cin && row.cin.toString().trim() !== '') {
[existing] = await conn.query('SELECT id FROM etudiants WHERE cin = ?', [row.cin]);
} else {
[existing] = await conn.query(
'SELECT id FROM etudiants WHERE LOWER(TRIM(nom)) = ? AND LOWER(TRIM(prenom)) = ?',
[row.nom.toLowerCase().trim(), row.prenom.toLowerCase().trim()]
);
}
if (existing.length > 0) {
doublons.push({ nom: row.nom, prenom: row.prenom, cin: row.cin });
const updateResult = await updateEtudiant(row, conn);
if (!updateResult.success) {
await conn.rollback();
return { error: true, message: `Erreur update ${row.nom} ${row.prenom}: ${updateResult.error}` };
}
} else {
etudiantsToInsert.push(row);
}
4 months ago
}
8 months ago
console.log('✅ Nouveaux à insérer :', etudiantsToInsert.map(e => e.nom + ' ' + e.prenom));
console.log('🔄 Étudiants mis à jour :', doublons.map(e => e.nom + ' ' + e.prenom));
// Insert new students
for (const row of etudiantsToInsert) {
const [etResult] = await conn.query(
`INSERT INTO etudiants
(nom, prenom, photos, date_de_naissances, num_inscription, sexe, cin,
date_delivrance, nationalite, annee_bacc, serie, boursier, domaine, contact)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
row.nom,
row.prenom,
getCompressedDefaultImage(),
convertToISODate(row.date_naissance),
row.num_inscription.toString(),
row.sexe,
row.cin || null,
convertToISODate(row.date_de_delivrance),
row.nationaliter,
parseInt(row.annee_baccalaureat, 10),
row.serie,
row.boursier,
fixEncoding(row.domaine),
row.contact
]
);
8 months ago
// Create inscription
await conn.query(
`INSERT INTO inscriptions (etudiant_id, annee_scolaire_id, niveau, mention_id, status, num_inscription)
VALUES (?, ?, ?, ?, ?, ?)`,
[etResult.insertId, row.annee_scolaire_id, row.niveau, row.mention, row.code_redoublement, row.num_inscription.toString()]
);
}
8 months ago
await conn.commit();
return {
error: false,
message: `Importation réussie. ${etudiantsToInsert.length} nouvel(le)(s) inséré(s), ${doublons.length} mis à jour.`
};
} catch (error) {
await conn.rollback();
return { error: true, message: error.message };
} finally {
conn.release();
}
}
8 months ago
module.exports = { importFileToDatabase };