diff --git a/database/Models/Etudiants.backup.js b/database/Models/Etudiants.backup.js index 40a5b52..b8435b2 100644 --- a/database/Models/Etudiants.backup.js +++ b/database/Models/Etudiants.backup.js @@ -62,7 +62,7 @@ async function insertEtudiant( * @returns JSON */ async function getAllEtudiants() { - const query = database.prepare('SELECT * FROM etudiants ORDER BY annee_scolaire DESC') + const query = database.prepare('SELECT e.*, m.uniter AS mentionUnite, m.nom As nomMention FROM etudiants e JOIN mentions m ON e.mention_id = m.id ORDER BY annee_scolaire DESC') try { let response = await query.all() diff --git a/database/Models/Etudiants.js b/database/Models/Etudiants.js index dc35667..73423bf 100644 --- a/database/Models/Etudiants.js +++ b/database/Models/Etudiants.js @@ -65,7 +65,7 @@ async function insertEtudiant( * @returns JSON */ async function getAllEtudiants() { - const sql = 'SELECT * FROM etudiants ORDER BY annee_scolaire DESC' + const sql = 'SELECT e.*, m.uniter AS mentionUnite, m.nom As nomMention FROM etudiants e JOIN mentions m ON e.mention_id = m.id ORDER BY annee_scolaire DESC' try { let [rows] = await pool.query(sql) @@ -83,7 +83,7 @@ async function getAllEtudiants() { * @returns Promise */ async function getSingleEtudiant(id) { - const sql = 'SELECT e.*, m.uniter AS mentionUnite FROM etudiants e JOIN mentions m ON e.mention_id = m.id WHERE e.id = ?' + const sql = 'SELECT e.*, m.uniter AS mentionUnite, m.nom As nomMention FROM etudiants e JOIN mentions m ON e.mention_id = m.id WHERE e.id = ?' try { const [rows] = await pool.query(sql, [id]) @@ -100,7 +100,7 @@ async function getSingleEtudiant(id) { * @returns JSON */ async function FilterDataByNiveau(niveau) { - const sql = 'SELECT * FROM etudiants WHERE niveau = ? ORDER BY annee_scolaire DESC' + const sql = 'SELECT e.*, m.uniter AS mentionUnite, m.nom As nomMention FROM etudiants e JOIN mentions m ON e.mention_id = m.id WHERE niveau = ? ORDER BY annee_scolaire DESC' try { let [rows] = await pool.query(sql, [niveau]) diff --git a/database/database.js b/database/database.js index 4260948..c10b0dd 100644 --- a/database/database.js +++ b/database/database.js @@ -1,14 +1,14 @@ const mysql = require('mysql2/promise') const bcrypt = require('bcryptjs') - const pool = mysql.createPool({ - host: '192.168.200.200', - user: 'root', - password: 'stephane1313', - database: 'university', - waitForConnections: true, - connectionLimit: 10, - queueLimit: 0 +const pool = mysql.createPool({ + host: '192.168.200.200', + user: 'root', + password: 'stephane1313', + database: 'university', +waitForConnections: true, +connectionLimit: 10, +queueLimit: 0 }) async function createTables() { diff --git a/database/import/Etudiants.js b/database/import/Etudiants.js index d4a351c..8ae1420 100644 --- a/database/import/Etudiants.js +++ b/database/import/Etudiants.js @@ -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 (fileExtension === '.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 }; diff --git a/src/renderer/src/assets/logorelever.png b/src/renderer/src/assets/logorelever.png index 952d3a2..9993821 100644 Binary files a/src/renderer/src/assets/logorelever.png and b/src/renderer/src/assets/logorelever.png differ diff --git a/src/renderer/src/components/AddNotes.jsx b/src/renderer/src/components/AddNotes.jsx index 73da243..f7a19bb 100644 --- a/src/renderer/src/components/AddNotes.jsx +++ b/src/renderer/src/components/AddNotes.jsx @@ -10,6 +10,7 @@ import svgSuccess from '../assets/success.svg' import svgError from '../assets/error.svg' import validateAddNote from './validation/AddNote' import ModalUpdateParcoursEtudiant from './ModalUpdateParcoursEtudiant' +import { useLocation } from 'react-router-dom' const AddNotes = () => { const { id, niveau, mention_id, parcours } = useParams() @@ -22,6 +23,9 @@ const AddNotes = () => { setOpenModal1(false) } + const location = useLocation() +const previousFilter = location.state?.selectedNiveau + /** * Fetching the matieres */ @@ -83,7 +87,7 @@ const AddNotes = () => { let valid = validateAddNote() let annee_scolaire = AnneeScolaireEtudiant let mention_id = etudiants.mention_id - + if (valid) { let response = await window.notes.insertNote({ etudiant_id, @@ -92,21 +96,33 @@ const AddNotes = () => { formData, annee_scolaire }) + if (response.success) { + // ✅ Ici on sauvegarde le filtre avant d'ouvrir le modal + if (previousFilter) { + localStorage.setItem('selectedNiveau', previousFilter) + } + setOpen(true) setStatut(200) setDisabled(true) + + // Reset du formulaire const resetFormData = matieres.reduce((acc, mat) => { - acc[mat.id] = '' // Reset each field to an empty string + acc[mat.id] = '' return acc }, {}) setFormData(resetFormData) + } else { + setOpen(true) + setStatut(400) } } else { setOpen(true) setStatut(400) } } + const [statut, setStatut] = useState(200) @@ -123,9 +139,10 @@ const AddNotes = () => { } const handleClose2 = () => { - navigate('/notes') + navigate('/student', { state: { selectedNiveau: previousFilter } }) setOpen(false) } + /** * function to return the view Modal diff --git a/src/renderer/src/components/ReleverNotes.jsx b/src/renderer/src/components/ReleverNotes.jsx index 4cde1f4..e525797 100644 --- a/src/renderer/src/components/ReleverNotes.jsx +++ b/src/renderer/src/components/ReleverNotes.jsx @@ -192,10 +192,10 @@ const ReleverNotes = ({ id, anneescolaire, niveau, sessionType = 'ensemble', ref {Object.entries(groupedDataBySemestre).map(([semestre, matieres]) => { // Group by unite_enseignement inside each semestre const groupedByUnite = matieres.reduce((acc, matiere) => { - if (!acc[matiere.unite_enseignement]) { - acc[matiere.unite_enseignement] = [] + if (!acc[matiere.ue]) { + acc[matiere.ue] = [] } - acc[matiere.unite_enseignement].push(matiere) + acc[matiere.ue].push(matiere) return acc }, {}) @@ -218,7 +218,6 @@ const ReleverNotes = ({ id, anneescolaire, niveau, sessionType = 'ensemble', ref paddingTop: '8px', borderRight: 'solid 1px black', borderBottom: 'solid 1px black', - background: '#bdbcbc', borderLeft: 'solid 1px black' }} > @@ -255,6 +254,24 @@ const ReleverNotes = ({ id, anneescolaire, niveau, sessionType = 'ensemble', ref
Fitiavana – Tanindrazana – Fandrosoana
+
+ MINISTÈRE DE L'ENSEIGNEMENT SUPÉRIEUR
+ ET DE LA RECHERCHE SCIENTIFIQUE
+
UNIVERSITÉ DE TOAMASINA
+ÉCOLE SUPÉRIEURE POLYTECHNIQUE