@ -8,7 +8,7 @@ const dayjs = require('dayjs');
const customParseFormat = require ( 'dayjs/plugin/customParseFormat' ) ;
dayjs . extend ( customParseFormat ) ;
// ✅ Fonction de correction d'encodage
// ---------- Fonctions utilitaires ----------
function fixEncoding ( str ) {
if ( typeof str !== 'string' ) return str ;
return str
@ -21,156 +21,105 @@ function fixEncoding(str) {
. 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 ;
return dayjs ( input ) . format ( 'YYYY-MM-DD' ) ;
}
// 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 ;
return dayjs ( new Date ( ( input - 25569 ) * 86400 * 1000 ) ) . format ( 'YYYY-MM-DD' ) ;
}
// 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 ;
}
if ( versMatch ) return ` ${ versMatch [ 1 ] } -01-01 ` ;
// 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
'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'
] ;
// 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 ;
}
for ( const fmt of formats ) {
const parsed = dayjs ( cleanInput , fmt , true ) ;
if ( parsed . isValid ( ) ) return parsed . format ( 'YYYY-MM-DD' ) ;
}
// 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 ;
}
const freeParse = dayjs ( cleanInput ) ;
if ( freeParse . isValid ( ) ) return freeParse . format ( 'YYYY-MM-DD' ) ;
}
console . error ( '❌ Impossible de convertir:' , input ) ;
return null ;
}
// Fonction utilitaire pour vérifier les années bissextiles
// Vérifie année bissextile
function isLeapYear ( year ) {
return ( year % 4 === 0 && year % 100 !== 0 ) || ( year % 400 === 0 ) ;
}
// ✅ Mise à jour d'un étudiant existant
// ---------- UPDATE étudiant ----------
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 fields = [ ] ;
const params = [ ] ;
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 ( )
] ;
function addFieldIfValue ( field , value ) {
if ( value !== undefined && value !== null && value !== '' ) {
fields . push ( ` ${ field } = ? ` ) ;
params . push ( value ) ;
}
}
addFieldIfValue ( 'nom' , row . nom ) ;
addFieldIfValue ( 'prenom' , row . prenom ) ;
addFieldIfValue ( 'date_de_naissances' , convertToISODate ( row . date_naissance ) ) ;
addFieldIfValue ( 'niveau' , row . niveau ) ;
addFieldIfValue ( 'annee_scolaire' , row . annee_scolaire ) ;
addFieldIfValue ( 'status' , row . code_redoublement ) ;
addFieldIfValue ( 'mention_id' , row . mention ) ;
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 pool . query ( sql , params ) ;
console . log ( ` Update effectué pour CIN ${ row . cin } ou nom ${ row . nom } ${ row . prenom } , affectedRows= ${ result . affectedRows } ` ) ;
const [ result ] = await pool . query ( sql , [ ... params , ... whereParams ] ) ;
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
// ---------- INSERT multiple étudiants ----------
async function insertMultipleEtudiants ( etudiants ) {
if ( ! etudiants || etudiants . length === 0 ) return { success : true , affectedRows : 0 } ;
const sql = `
INSERT INTO etudiants (
nom , prenom , photos , date_de_naissances , niveau , annee_scolaire , status ,
@ -190,7 +139,7 @@ async function insertMultipleEtudiants(etudiants) {
row . mention ,
row . num_inscription . toString ( ) ,
row . sexe ,
row . cin ,
row . cin || null ,
convertToISODate ( row . date_de_delivrance ) ,
row . nationaliter ,
parseInt ( row . annee_baccalaureat , 10 ) ,
@ -205,31 +154,26 @@ async function insertMultipleEtudiants(etudiants) {
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
// ---------- IMPORT fichier ----------
async function importFileToDatabase ( filePath ) {
const fileExtension = path . extname ( filePath ) . toLowerCase ( ) ;
let records ;
const ext = path . extname ( filePath ) . toLowerCase ( ) ;
if ( fil eE xtension === '.xlsx' ) {
if ( ext === '.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é.' } ;
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é' } ;
}
console . log ( ` 📄 Nombre de lignes : ${ records . length } ` ) ;
// Vérifier champs obligatoires
const requiredFields = [
'nom' , 'date_naissance' , 'niveau' , 'annee_scolaire' ,
@ -239,12 +183,8 @@ async function importFileToDatabase(filePath) {
] ;
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 } ;
}
for ( const f of requiredFields ) {
if ( ! row [ f ] ) return { error : true , message : ` Le champ ' ${ f } ' est manquant à la ligne ${ i + 2 } ` } ;
}
}
@ -253,64 +193,48 @@ async function importFileToDatabase(filePath) {
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 ) ;
// Mapping mention
const matchedMention = mentionRows . find (
m => m . nom . toUpperCase ( ) === row . mention . toUpperCase ( ) ||
m . uniter . toUpperCase ( ) === row . mention . toUpperCase ( )
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 ( ) )
) ;
// Mapping status
row . code_redoublement = ( row . code_redoublement ? row . code_redoublement . trim ( ) . substring ( 0 , 1 ) : '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 ]
) ;
// Détection doublons (ignorer CIN vide)
let existing ;
if ( row . cin && row . cin . toString ( ) . trim ( ) !== '' ) {
[ existing ] = await pool . query ( 'SELECT * FROM etudiants WHERE cin = ?' , [ row . cin ] ) ;
} else {
[ existing ] = await pool . query (
'SELECT * 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 } ) ;
// 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 ;
if ( ! updateResult . success ) return { error : true , message : ` Erreur update ${ row . nom } ${ row . prenom } : ${ updateResult . error } ` } ;
} else {
etudiantsToInsert . push ( row ) ;
}
etudiantsToInsert . push ( row ) ;
}
console . log ( etudiantsToInsert ) ;
console . log ( '✅ Nouveaux à insérer :' , etudiantsToInsert . map ( e => e . nom + ' ' + e . prenom ) ) ;
console . log ( '🔄 Étudiants mis à jour :' , doublons . map ( e => e . nom + ' ' + e . prenom ) ) ;
// 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 } ` } ;
}
}
const insertResult = await insertMultipleEtudiants ( etudiantsToInsert ) ;
if ( ! insertResult . success ) return { error : true , message : 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 } ;
return { error : false , message : ` Importation réussie. ${ etudiantsToInsert . length } nouvel(le)(s) inséré(s), ${ doublons . length } mis à jour. ` } ;
}
module . exports = { importFileToDatabase } ;