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); // ✅ Fonction de correction d'encodage 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; console.log('🔍 Input original:', input, 'Type:', typeof input); // Si input est un objet Date valide if (input instanceof Date && !isNaN(input)) { const result = dayjs(input).format('YYYY-MM-DD'); console.log('📅 Date object convertie:', result); return result; } // Si input est un nombre (numéro de série Excel) if (typeof input === 'number') { // Formule Excel: (numéro - 25569) * 86400 * 1000 const excelDate = new Date((input - 25569) * 86400 * 1000); const result = dayjs(excelDate).format('YYYY-MM-DD'); console.log('📊 Numéro Excel', input, 'converti en:', result); return result; } // Si input est une chaîne if (typeof input === 'string') { const cleanInput = input.trim(); // Cas spécial "vers YYYY" const versMatch = cleanInput.match(/vers\s*(\d{4})/i); if (versMatch) { const result = `${versMatch[1]}-01-01`; console.log('📝 "Vers" détecté:', result); return result; } // Formats à tester dans l'ordre de priorité const formats = [ 'DD/MM/YYYY', 'D/M/YYYY', // Format français prioritaire 'YYYY-MM-DD', // Format ISO 'DD-MM-YYYY', 'D-M-YYYY', // Format français avec tirets 'MM/DD/YYYY', 'M/D/YYYY', // Format américain 'MM-DD-YYYY', 'M-D-YYYY', // Format américain avec tirets 'DD/MM/YY', 'D/M/YY', // Années courtes 'MM/DD/YY', 'M/D/YY', 'DD-MM-YY', 'D-M-YY', 'MM-DD-YY', 'M-D-YY' ]; // Test avec parsing strict pour éviter les interprétations erronées for (const format of formats) { const parsedDate = dayjs(cleanInput, format, true); // true = strict parsing if (parsedDate.isValid()) { const result = parsedDate.format('YYYY-MM-DD'); console.log(`✅ Format "${format}" réussi:`, cleanInput, '->', result); // Vérification supplémentaire pour les dates invalides comme 29/02 en année non-bissextile if (format.includes('DD/MM') || format.includes('D/M')) { const day = parsedDate.date(); const month = parsedDate.month() + 1; // dayjs month is 0-indexed const year = parsedDate.year(); // Vérifier si c'est le 29 février d'une année non-bissextile if (month === 2 && day === 29 && !isLeapYear(year)) { console.warn('⚠️ Date invalide détectée: 29 février en année non-bissextile'); return null; // ou retourner une date par défaut } } return result; } } // Si aucun format strict ne fonctionne, essayer le parsing libre en dernier recours const freeParseDate = dayjs(cleanInput); if (freeParseDate.isValid()) { const result = freeParseDate.format('YYYY-MM-DD'); console.log('🆓 Parsing libre réussi:', cleanInput, '->', result); return result; } } console.error('❌ Impossible de convertir:', input); return null; } // Fonction utilitaire pour vérifier les années bissextiles function isLeapYear(year) { return (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0); } // ✅ Mise à jour d'un étudiant existant async function updateEtudiant(row) { const sql = ` UPDATE etudiants SET nom = ?, prenom = ?, photos = ?, date_de_naissances = ?, niveau = ?, annee_scolaire = ?, status = ?, mention_id = ?, num_inscription = ?, sexe = ?, date_delivrance = ?, nationalite = ?, annee_bacc = ?, serie = ?, boursier = ?, domaine = ?, contact = ?, parcours = ? WHERE cin = ? OR (LOWER(nom) = ? AND LOWER(prenom) = ?) `; const params = [ row.nom, row.prenom, getCompressedDefaultImage(), convertToISODate(row.date_naissance), row.niveau, row.annee_scolaire, row.code_redoublement, row.mention, row.num_inscription.toString(), row.sexe, convertToISODate(row.date_de_delivrance), row.nationaliter, parseInt(row.annee_baccalaureat, 10), row.serie, row.boursier, fixEncoding(row.domaine), row.contact, null, row.cin, row.nom.toLowerCase().trim(), row.prenom.toLowerCase().trim() ]; try { const [result] = await pool.query(sql, params); console.log(`Update effectué pour CIN ${row.cin} ou nom ${row.nom} ${row.prenom}, affectedRows=${result.affectedRows}`); return { success: true, affectedRows: result.affectedRows }; } catch (error) { console.error('❌ Erreur MySQL update :', error.message); return { success: false, error: error.message }; } } // ✅ Insertion réelle multiple async function insertMultipleEtudiants(etudiants) { const sql = ` INSERT INTO etudiants ( nom, prenom, photos, date_de_naissances, niveau, annee_scolaire, status, mention_id, num_inscription, sexe, cin, date_delivrance, nationalite, annee_bacc, serie, boursier, domaine, contact, parcours ) VALUES ? `; const values = etudiants.map(row => [ row.nom, row.prenom, getCompressedDefaultImage(), convertToISODate(row.date_naissance), row.niveau, row.annee_scolaire, row.code_redoublement, row.mention, row.num_inscription.toString(), row.sexe, row.cin, convertToISODate(row.date_de_delivrance), row.nationaliter, parseInt(row.annee_baccalaureat, 10), row.serie, row.boursier, fixEncoding(row.domaine), row.contact, null ]); try { const [result] = await pool.query(sql, [values]); return { success: true, affectedRows: result.affectedRows }; } catch (error) { console.error('❌ Erreur MySQL :', error.message); return { success: false, error: error.message }; } } // ✅ Import fichier vers base async function importFileToDatabase(filePath) { const fileExtension = path.extname(filePath).toLowerCase(); let records; if (fileExtension === '.xlsx') { const workbook = XLSX.readFile(filePath); const worksheet = workbook.Sheets[workbook.SheetNames[0]]; // raw: true pour garder les valeurs brutes, surtout pour les dates records = XLSX.utils.sheet_to_json(worksheet, { defval: ''}); } else if (fileExtension === '.csv') { const fileContent = fs.readFileSync(filePath, 'utf8'); records = parse(fileContent, { columns: true, skip_empty_lines: true }); }else { console.error('Unsupported file format.'); return { error: true, message: 'Format de fichier non supporté.' }; } console.log(`📄 Nombre de lignes : ${records.length}`); // 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 field of requiredFields) { if (!row[field]) { const msg = `Le champ '${field}' est manquant à la ligne ${i + 2}`; console.error(msg); return { error: true, message: msg }; } } } const [mentionRows] = await pool.query('SELECT * FROM mentions'); const [statusRows] = await pool.query('SELECT * FROM status'); const etudiantsToInsert = []; const doublons = []; console.log(records); for (const row of records) { // Mapping mention console.log('Avant conversion date_naissance:', row.date_naissance); row.date_naissance = convertToISODate(row.date_naissance); console.log('Après conversion date_naissance:', row.date_naissance); const matchedMention = mentionRows.find( m => m.nom.toUpperCase() === row.mention.toUpperCase() || m.uniter.toUpperCase() === row.mention.toUpperCase() ); if (matchedMention) row.mention = matchedMention.id; // Gestion code_redoublement -> status id if (row.code_redoublement) { row.code_redoublement = row.code_redoublement.trim().substring(0, 1); } else { row.code_redoublement = 'N'; } const statusMatch = statusRows.find( s => s.nom.toLowerCase().startsWith(row.code_redoublement.toLowerCase()) ); if (statusMatch) row.code_redoublement = statusMatch.id; // Vérification doublons (extraction complet) const nomComplet = (row.nom + ' ' + row.prenom).toLowerCase().trim(); const [existing] = await pool.query( 'SELECT * FROM etudiants WHERE LOWER(CONCAT(nom, " ", prenom)) = ? OR cin = ?', [nomComplet, row.cin] ); if (existing.length > 0) { doublons.push({ nom: row.nom, prenom: row.prenom, cin: row.cin }); // Mise à jour const updateResult = await updateEtudiant(row); if (!updateResult.success) { return { error: true, message: `Erreur lors de la mise à jour de ${row.nom} ${row.prenom} : ${updateResult.error}` }; } continue; } etudiantsToInsert.push(row); } console.log(etudiantsToInsert); // Insertion des nouveaux let insertResult = { success: true, affectedRows: 0 }; if (etudiantsToInsert.length > 0) { insertResult = await insertMultipleEtudiants(etudiantsToInsert); if (!insertResult.success) { return { error: true, message: `Erreur lors de l'insertion : ${insertResult.error}` }; } } let msg = `Importation réussie. ${etudiantsToInsert.length} nouvel(le)(s) étudiant(s) inséré(s). ${doublons.length} étudiant(s) mis à jour.`; return { error: false, message: msg }; } module.exports = { importFileToDatabase };