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); // ---------- 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] } 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, '-'); } function convertToISODate(input) { if (!input) return null; if (input instanceof Date && !isNaN(input)) { return dayjs(input).format('YYYY-MM-DD'); } if (typeof input === 'number') { return dayjs(new Date((input - 25569) * 86400 * 1000)).format('YYYY-MM-DD'); } if (typeof input === 'string') { const cleanInput = input.trim(); const versMatch = cleanInput.match(/vers\s*(\d{4})/i); if (versMatch) return `${versMatch[1]}-01-01`; const formats = [ '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', 'MM/DD/YY', 'M/D/YY', 'DD-MM-YY', 'D-M-YY', 'MM-DD-YY', 'M-D-YY' ]; for (const fmt of formats) { const parsed = dayjs(cleanInput, fmt, true); if (parsed.isValid()) return parsed.format('YYYY-MM-DD'); } const freeParse = dayjs(cleanInput); if (freeParse.isValid()) return freeParse.format('YYYY-MM-DD'); } return null; } // ---------- UPDATE étudiant existant ---------- async function updateEtudiant(row, conn) { const fields = []; const params = []; function addFieldIfValue(field, value) { if (value !== undefined && value !== null && value !== '') { fields.push(`${field} = ?`); params.push(value); } } // Only permanent fields in etudiants 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()]; } 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()] ); } } } return { success: true, affectedRows: result.affectedRows }; } catch (error) { return { success: false, error: error.message }; } } // ---------- IMPORT fichier ---------- async function importFileToDatabase(filePath) { let records; const ext = path.extname(filePath).toLowerCase(); if (ext === '.xlsx') { const workbook = XLSX.readFile(filePath); const worksheet = workbook.Sheets[workbook.SheetNames[0]]; 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é' }; } // 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()) { for (const f of requiredFields) { if (!row[f]) return { error: true, message: `Le champ '${f}' est manquant à la ligne ${i + 2}` }; } } 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() ); 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); } } 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 ] ); // 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()] ); } 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(); } } module.exports = { importFileToDatabase };