diff --git a/README.md b/README.md index 0534b92..29b5a27 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,169 @@ -# package.json +# C-University -An Electron application with React +Application de gestion universitaire desktop, développée avec **Electron + React + Vite**. Elle permet la gestion complète des étudiants, des notes, des matières, des mentions et des années scolaires d'un établissement universitaire. -## Recommended IDE Setup +> Développé par **CPAY COMPANY FOR MADAGASCAR** -- [VSCode](https://code.visualstudio.com/) + [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) + [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) +--- -## Project Setup +## Fonctionnalités -### Install +- **Gestion des étudiants** — inscription, consultation, modification, export PDF/Excel, import CSV +- **Gestion des notes** — saisie, notes de rattrapage (repêchage), calcul automatique des moyennes +- **Résultats** — admis / redoublant / renvoyé selon un système de seuils configurables +- **Mentions & Parcours** — gestion des filières et des spécialisations +- **Matières** — création, affectation aux mentions et aux semestres, import CSV +- **Niveaux académiques** — gestion des niveaux d'étude (L1, L2, M1, M2, etc.) +- **Années scolaires** — gestion multi-années avec année courante active +- **Tranche d'écolage** — suivi des paiements de scolarité par étudiant +- **Export de fiches** — génération de fiches matières avec QR code et PDF +- **Statistiques & graphiques** — visualisation via Chart.js +- **Gestion des utilisateurs** — authentification sécurisée, rôles admin +- **Mise à jour automatique** — via `electron-updater` + +--- + +## Stack technique + +| Couche | Technologies | +|--------|-------------| +| Desktop | Electron 31 | +| Frontend | React 18, Vite, React Router DOM v6 | +| UI | Material UI (MUI v6), Bootstrap 5, React-Bootstrap | +| État | TanStack React Query, Context API | +| Base de données | MySQL (mysql2), better-sqlite3 | +| API locale | Express.js | +| PDF | jsPDF, jsPDF-AutoTable, pdf-lib, html2canvas | +| Excel/CSV | xlsx, xlsx-populate, PapaParse, csv-parse | +| Authentification | bcryptjs | +| Graphiques | Chart.js, react-chartjs-2 | +| QR Code | qrcode | +| Logs | electron-log | + +--- + +## Prérequis + +- [Node.js](https://nodejs.org/) >= 18 +- [MySQL](https://www.mysql.com/) (serveur local actif) +- npm >= 10 + +--- + +## Installation + +### 1. Cloner le dépôt ```bash -$ npm install +git clone +cd c-university ``` -### Development +### 2. Installer les dépendances ```bash -$ npm run dev +npm install ``` -### Build +### 3. Configurer la base de données + +Assurez-vous que MySQL est en cours d'exécution. La configuration par défaut dans `database/database.js` est : + +``` +Host : 127.0.0.1 +User : root +Password : (vide) +Database : university +``` + +> Les tables sont créées automatiquement au premier lancement. Un compte **admin** par défaut est également créé. + +--- + +## Lancement + +### Mode développement ```bash -# For windows -$ npm run build:win +npm run dev +``` -# For macOS -$ npm run build:mac +### Prévisualisation du build -# For Linux -$ npm run build:linux +```bash +npm run start ``` + +--- + +## Build & Distribution + +| Commande | Description | +|----------|-------------| +| `npm run build` | Build de l'application | +| `npm run build:win` | Installateur Windows (.exe via NSIS) | +| `npm run build:mac` | Package macOS | +| `npm run build:linux` | Package Linux | +| `npm run build:unpack` | Build sans package (dossier non compressé) | + +--- + +## Compte par défaut + +| Champ | Valeur | +|-------|--------| +| Utilisateur | `admin` | +| Email | `admin@example.com` | +| Mot de passe | `123456789` | + +> Il est fortement recommandé de changer le mot de passe après le premier login. + +--- + +## Structure du projet + +``` +c-university/ +├── database/ +│ └── database.js # Connexion MySQL + création des tables +├── src/ +│ ├── main/ # Processus principal Electron +│ ├── preload/ # Scripts de préchargement +│ └── renderer/src/ # Interface React +│ ├── components/ # Composants de l'application +│ ├── Routes/ # Configuration du routeur +│ ├── contexts/ # Contextes React (Auth, Moyenne) +│ ├── layouts/ # Layouts (Default, Login) +│ └── assets/ # Images et ressources +├── resources/ # Icônes et ressources Electron +├── electron.vite.config.mjs # Configuration Electron-Vite +├── electron-builder.yml # Configuration du packaging +└── package.json +``` + +--- + +## Schéma de base de données (résumé) + +| Table | Description | +|-------|-------------| +| `users` | Comptes utilisateurs avec rôles | +| `etudiants` | Données complètes des étudiants | +| `mentions` | Filières / mentions | +| `parcours` | Parcours au sein d'une mention | +| `niveaux` | Niveaux académiques | +| `matieres` | Matières / UE | +| `semestres` | Semestres | +| `notes` | Notes des étudiants | +| `notesrepech` | Notes de rattrapage | +| `notesystems` | Seuils admis / redoublant / renvoyé | +| `anneescolaire` | Années scolaires | +| `trancheecolage` | Tranches de paiement de scolarité | +| `matiereEnseignants` | Enseignants par matière | +| `status` | Statuts étudiants (Nouveau, Passant, Redoublant…) | + +--- + +## Version + +**v4.1.0** diff --git a/database/Models/AnneeScolaire.js b/database/Models/AnneeScolaire.js index e204f1a..3a9cd14 100644 --- a/database/Models/AnneeScolaire.js +++ b/database/Models/AnneeScolaire.js @@ -110,10 +110,10 @@ async function setCurrent(id) { const sql = 'UPDATE anneescolaire SET is_current = 0 WHERE id > 0 AND is_current = 1' const sql2 = 'UPDATE anneescolaire SET is_current = 1 WHERE id = ?' - pool.query(sql) + await pool.query(sql) try { - const [result] = pool.query(sql2, [id]) + const [result] = await pool.query(sql2, [id]) console.log(result) return { diff --git a/database/Models/ConfigEcolage.js b/database/Models/ConfigEcolage.js new file mode 100644 index 0000000..73fbe27 --- /dev/null +++ b/database/Models/ConfigEcolage.js @@ -0,0 +1,62 @@ +const { pool } = require('../database') + +async function getAllConfigEcolage() { + const sql = ` + SELECT c.*, m.nom AS mention_nom, m.uniter AS mention_uniter, n.nom AS niveau_nom + FROM configecolage c + LEFT JOIN mentions m ON c.mention_id = m.id + LEFT JOIN niveaus n ON c.niveau_id = n.id + ORDER BY n.nom, m.nom + ` + try { + const [rows] = await pool.query(sql) + return rows + } catch (error) { + return [] + } +} + +async function getConfigEcolageByMentionNiveau(mention_id, niveau_nom) { + const sql = ` + SELECT c.montant_total + FROM configecolage c + LEFT JOIN niveaus n ON c.niveau_id = n.id + WHERE c.mention_id = ? AND n.nom = ? + ` + try { + const [rows] = await pool.query(sql, [mention_id, niveau_nom]) + return rows.length > 0 ? rows[0] : null + } catch (error) { + return null + } +} + +async function upsertConfigEcolage(mention_id, niveau_id, montant_total) { + const sql = ` + INSERT INTO configecolage (mention_id, niveau_id, montant_total) + VALUES (?, ?, ?) + ON DUPLICATE KEY UPDATE montant_total = ? + ` + try { + await pool.query(sql, [mention_id, niveau_id, montant_total, montant_total]) + return { success: true } + } catch (error) { + return { success: false, error: error.message } + } +} + +async function deleteConfigEcolage(id) { + try { + await pool.query('DELETE FROM configecolage WHERE id = ?', [id]) + return { success: true } + } catch (error) { + return { success: false, error: error.message } + } +} + +module.exports = { + getAllConfigEcolage, + getConfigEcolageByMentionNiveau, + upsertConfigEcolage, + deleteConfigEcolage +} diff --git a/database/Models/Etudiants.js b/database/Models/Etudiants.js index 73423bf..e2938e2 100644 --- a/database/Models/Etudiants.js +++ b/database/Models/Etudiants.js @@ -1,7 +1,21 @@ const { pool } = require('../database') +// Helper: get annee_scolaire_id from code string +async function getAnneeScolaireId(conn, code) { + const [rows] = await conn.query('SELECT id FROM anneescolaire WHERE code = ?', [code]) + return rows.length > 0 ? rows[0].id : null +} + +// Helper: SQL to get latest inscription fields per student +const inscriptionJoin = ` + LEFT JOIN inscriptions i ON e.id = i.etudiant_id + AND i.id = (SELECT MAX(ii.id) FROM inscriptions ii WHERE ii.etudiant_id = e.id) + LEFT JOIN anneescolaire a ON i.annee_scolaire_id = a.id + LEFT JOIN mentions m ON i.mention_id = m.id +` + /** - * function to insert etudiant into databases + * Insert a new student + create their first inscription */ async function insertEtudiant( nom, @@ -24,51 +38,95 @@ async function insertEtudiant( contact, parcours ) { - 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 conn = await pool.getConnection() try { - let [result] = await pool.query(sql, [ - nom, - prenom, - photos, - date_de_naissances, - niveau, - annee_scolaire, - status, - mention_id, - num_inscription, - sexe, - cin, - date_delivrence, - nationaliter, - annee_bacc, - serie, - boursier, - domaine, - contact, - parcours - ]) - - return { - success: true, - id: result.insertId + await conn.beginTransaction() + + // 1. Insert permanent info into etudiants + const [etudiantResult] = 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [nom, prenom, photos, date_de_naissances, num_inscription, sexe, cin, + date_delivrence, nationaliter, annee_bacc, serie, boursier, domaine, contact] + ) + const etudiantId = etudiantResult.insertId + + // 2. Get annee_scolaire_id + const annee_scolaire_id = await getAnneeScolaireId(conn, annee_scolaire) + if (!annee_scolaire_id) { + await conn.rollback() + return { success: false, message: 'Année scolaire introuvable: ' + annee_scolaire } } + + // 3. Insert into inscriptions + await conn.query( + `INSERT INTO inscriptions + (etudiant_id, annee_scolaire_id, niveau, mention_id, parcours, status, num_inscription) + VALUES (?, ?, ?, ?, ?, ?, ?)`, + [etudiantId, annee_scolaire_id, niveau, mention_id, parcours, status, num_inscription] + ) + + await conn.commit() + return { success: true, id: etudiantId } } catch (error) { + await conn.rollback() return error + } finally { + conn.release() } } /** - * function to get all etudiants - * - * @returns JSON + * Get all students filtered by a specific annee_scolaire */ -async function getAllEtudiants() { - 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' +async function getAllEtudiantsByAnnee(annee_scolaire) { + const sql = ` + SELECT e.id, e.nom, e.prenom, e.photos, e.date_de_naissances, e.sexe, e.cin, + e.date_delivrance, e.nationalite, e.annee_bacc, e.serie, e.boursier, e.domaine, e.contact, + i.num_inscription, i.niveau, a.code AS annee_scolaire, + CAST(i.status AS SIGNED) AS status, + i.mention_id, i.parcours, i.id AS inscription_id, + m.uniter AS mentionUnite, m.nom AS nomMention + FROM etudiants e + INNER JOIN inscriptions i ON e.id = i.etudiant_id + INNER JOIN anneescolaire a ON i.annee_scolaire_id = a.id AND a.code = ? + LEFT JOIN mentions m ON i.mention_id = m.id + ORDER BY e.nom + ` try { - let [rows] = await pool.query(sql) + const [rows] = await pool.query(sql, [annee_scolaire]) + return rows + } catch (error) { + return error + } +} +/** + * Get all students with ALL their inscriptions (une ligne par inscription) + */ +async function getAllEtudiants() { + const sql = ` + SELECT e.id, e.nom, e.prenom, e.photos, e.date_de_naissances, e.sexe, e.cin, + e.date_delivrance, e.nationalite, e.annee_bacc, e.serie, e.boursier, e.domaine, e.contact, + i.num_inscription, i.id AS inscription_id, + a.code AS annee_scolaire, + /* Champs actuels depuis la dernière inscription */ + cur.niveau, CAST(cur.status AS SIGNED) AS status, + cur.mention_id, cur.parcours, + m_cur.uniter AS mentionUnite, m_cur.nom AS nomMention + FROM etudiants e + INNER JOIN inscriptions i ON e.id = i.etudiant_id + LEFT JOIN anneescolaire a ON i.annee_scolaire_id = a.id + /* Dernière inscription pour les champs actuels */ + LEFT JOIN inscriptions cur ON e.id = cur.etudiant_id + AND cur.id = (SELECT MAX(ii.id) FROM inscriptions ii WHERE ii.etudiant_id = e.id) + LEFT JOIN mentions m_cur ON cur.mention_id = m_cur.id + ORDER BY e.nom, a.code DESC + ` + try { + const [rows] = await pool.query(sql) return rows } catch (error) { return error @@ -76,18 +134,21 @@ async function getAllEtudiants() { } /** - * function to return a single etudiant - * and display it on the screen - * - * @param {int} id - * @returns Promise + * Get a single student with their latest inscription data */ async function getSingleEtudiant(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 = ?' - + const sql = ` + SELECT e.*, + i.num_inscription, i.niveau, a.code AS annee_scolaire, + CAST(i.status AS SIGNED) AS status, + i.mention_id, i.parcours, + m.uniter AS mentionUnite, m.nom AS nomMention + FROM etudiants e + ${inscriptionJoin} + WHERE e.id = ? + ` try { const [rows] = await pool.query(sql, [id]) - return rows[0] } catch (error) { return error @@ -95,15 +156,25 @@ async function getSingleEtudiant(id) { } /** - * function to get all etudiants M2 - * - * @returns JSON + * Filter students by their latest inscription niveau */ async function FilterDataByNiveau(niveau) { - 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' + const sql = ` + SELECT e.*, + i.num_inscription, i.niveau, a.code AS annee_scolaire, + CAST(i.status AS SIGNED) AS status, + i.mention_id, i.parcours, + m.uniter AS mentionUnite, m.nom AS nomMention + FROM etudiants e + INNER JOIN inscriptions i ON e.id = i.etudiant_id + AND i.id = (SELECT MAX(ii.id) FROM inscriptions ii WHERE ii.etudiant_id = e.id) + LEFT JOIN anneescolaire a ON i.annee_scolaire_id = a.id + LEFT JOIN mentions m ON i.mention_id = m.id + WHERE i.niveau = ? + ORDER BY a.code DESC + ` try { - let [rows] = await pool.query(sql, [niveau]) - + const [rows] = await pool.query(sql, [niveau]) return rows } catch (error) { return error @@ -111,18 +182,7 @@ async function FilterDataByNiveau(niveau) { } /** - * function to update etudiants - * - * @param {*} nom - * @param {*} prenom - * @param {*} photos - * @param {*} date_de_naissances - * @param {*} niveau - * @param {*} annee_scolaire - * @param {*} status - * @param {*} num_inscription - * @param {*} id - * @returns promise + * Update a student: permanent fields in etudiants, annual fields in latest inscription */ async function updateEtudiant( nom, @@ -146,67 +206,88 @@ async function updateEtudiant( contact, parcours ) { - const sql = - 'UPDATE etudiants SET 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 = ? WHERE id = ?' - + const conn = await pool.getConnection() try { - let [result] = await pool.query(sql, [ - nom, - prenom, - photos, - date_de_naissances, - niveau, - annee_scolaire, - status, - mention_id, - num_inscription, - sexe, - cin, - date_delivrence, - nationalite, - annee_bacc, - serie, - boursier, - domaine, - contact, - parcours, - id - ]) - - if (result.affectedRows === 0) { - return { - success: false, - message: 'Année Univesitaire non trouvé.' + await conn.beginTransaction() + + // Update permanent fields (sans num_inscription car il est propre à chaque année) + await conn.query( + `UPDATE etudiants SET nom=?, prenom=?, photos=?, date_de_naissances=?, + sexe=?, cin=?, date_delivrance=?, nationalite=?, + annee_bacc=?, serie=?, boursier=?, domaine=?, contact=? + WHERE id=?`, + [nom, prenom, photos, date_de_naissances, sexe, cin, + date_delivrence, nationalite, annee_bacc, serie, boursier, domaine, contact, id] + ) + + // Get annee_scolaire_id + const annee_scolaire_id = await getAnneeScolaireId(conn, annee_scolaire) + + if (annee_scolaire_id) { + // Chercher l'inscription de cette année spécifique + const [insc] = await conn.query( + 'SELECT id, num_inscription FROM inscriptions WHERE etudiant_id = ? AND annee_scolaire_id = ?', + [id, annee_scolaire_id] + ) + if (insc.length > 0) { + const oldNum = insc[0].num_inscription + // Si le num_inscription a changé, sauvegarder l'ancien dans etudiants.num_inscription + if (oldNum && oldNum !== num_inscription) { + const [etudRow] = await conn.query( + 'SELECT num_inscription FROM etudiants WHERE id = ?', [id] + ) + const existingNums = etudRow[0]?.num_inscription || '' + // Ajouter l'ancien numéro s'il n'est pas déjà dans l'historique + const allNums = existingNums ? existingNums.split(',').map(n => n.trim()) : [] + if (!allNums.includes(oldNum)) { + allNums.push(oldNum) + } + // S'assurer que le nouveau est aussi dedans + if (!allNums.includes(num_inscription)) { + allNums.push(num_inscription) + } + await conn.query( + 'UPDATE etudiants SET num_inscription = ? WHERE id = ?', + [allNums.join(','), id] + ) + } + await conn.query( + `UPDATE inscriptions SET niveau=?, mention_id=?, parcours=?, status=?, + num_inscription=? WHERE id=?`, + [niveau, mention_id, parcours, status, num_inscription, insc[0].id] + ) + } else { + await conn.query( + `INSERT INTO inscriptions + (etudiant_id, annee_scolaire_id, niveau, mention_id, parcours, status, num_inscription) + VALUES (?, ?, ?, ?, ?, ?, ?)`, + [id, annee_scolaire_id, niveau, mention_id, parcours, status, num_inscription] + ) } } - return { - success: true, - message: 'Année Univesitaire supprimé avec succès.' - } + await conn.commit() + return { success: true, message: 'Étudiant mis à jour avec succès.' } } catch (error) { + await conn.rollback() return error + } finally { + conn.release() } } /** - * function to return the needed data in dashboard - * - * @returns promise + * Get dashboard data */ async function getDataToDashboard() { const query = 'SELECT * FROM niveaus' const query2 = 'SELECT * FROM etudiants' - const query3 = 'SELECT DISTINCT annee_scolaire FROM etudiants' // get all Année Univesitaire sans doublan + const query3 = 'SELECT DISTINCT a.code AS annee_scolaire FROM inscriptions i JOIN anneescolaire a ON i.annee_scolaire_id = a.id' try { - let [rows] = await pool.query(query) - let niveau = rows - ;[rows] = await pool.query(query2) - let etudiants = rows - ;[rows] = await pool.query(query3) - let anne_scolaire = rows - + const [niveau] = await pool.query(query) + const [etudiants] = await pool.query(query2) + const [anne_scolaire] = await pool.query(query3) return { niveau, etudiants, anne_scolaire } } catch (error) { return error @@ -215,170 +296,273 @@ async function getDataToDashboard() { async function changePDP(photos, id) { const sql = 'UPDATE etudiants SET photos = ? WHERE id = ?' - try { - let [result] = await pool.query(sql, [photos, id]) - + const [result] = await pool.query(sql, [photos, id]) if (result.affectedRows === 0) { - return { - success: false, - message: 'Année Univesitaire non trouvé.' - } - } - - return { - success: true, - message: 'Année Univesitaire supprimé avec succès.' + return { success: false, message: 'Étudiant non trouvé.' } } + return { success: true, message: 'Photo mise à jour avec succès.' } } catch (error) { return error } } async function updateParcours(parcours, id) { - const sql = 'UPDATE etudiants SET parcours = ? WHERE id = ?' - + // Update parcours in the latest inscription + const sql = ` + UPDATE inscriptions SET parcours = ? + WHERE etudiant_id = ? AND id = (SELECT MAX(ii.id) FROM (SELECT id FROM inscriptions WHERE etudiant_id = ?) ii) + ` try { - let [result] = await pool.query(sql, [parcours, id]) - + const [result] = await pool.query(sql, [parcours, id, id]) if (result.affectedRows === 0) { - return { - success: false, - message: 'Année Univesitaire non trouvé.' - } - } - - return { - success: true, - message: 'Année Univesitaire supprimé avec succès.' + return { success: false, message: 'Inscription non trouvée.' } } + return { success: true, message: 'Parcours mis à jour avec succès.' } } catch (error) { return error } } -async function createTranche(etudiant_id, tranchename, montant) { - const sql = 'INSERT INTO trancheecolage (etudiant_id, tranchename, montant) VALUES (?, ?, ?)' - +async function deleteEtudiant(id) { + const conn = await pool.getConnection() try { - let [result] = await pool.query(sql, [etudiant_id, tranchename, montant]) + await conn.beginTransaction() + // Delete in correct order to respect FK constraints + await conn.query('DELETE FROM notes WHERE etudiant_id = ?', [id]) + await conn.query('DELETE FROM notesrepech WHERE etudiant_id = ?', [id]) + await conn.query('DELETE FROM trancheecolage WHERE etudiant_id = ?', [id]) + await conn.query('DELETE FROM inscriptions WHERE etudiant_id = ?', [id]) + const [result] = await conn.query('DELETE FROM etudiants WHERE id = ?', [id]) + await conn.commit() - return { - success: true, - id: result.insertId + if (result.affectedRows === 0) { + return { success: false, message: 'Étudiant non trouvé.' } } + return { success: true, message: 'Étudiant supprimé avec succès.' } } catch (error) { - return error + await conn.rollback() + return { success: false, error: 'Erreur, veuillez réessayer: ' + error } + } finally { + conn.release() } } -async function getTranche(id) { - const sql = 'SELECT * FROM trancheecolage WHERE etudiant_id = ?' - +/** + * Payer une tranche (Tranche 1, Tranche 2, ou Complète) + * 1 seule ligne par étudiant par année scolaire + */ +async function payerTranche(etudiant_id, annee_scolaire_id, type, montant, num_bordereau) { try { - let [rows] = await pool.query(sql, [id]) + // Vérifier si une ligne existe déjà + const [existing] = await pool.query( + 'SELECT * FROM trancheecolage WHERE etudiant_id = ? AND annee_scolaire_id = ?', + [etudiant_id, annee_scolaire_id] + ) + + if (existing.length === 0) { + // Créer une nouvelle ligne + let data = { tranche1_montant: 0, tranche1_bordereau: null, tranche2_montant: 0, tranche2_bordereau: null } + + if (type === 'Tranche 1') { + data.tranche1_montant = montant + data.tranche1_bordereau = num_bordereau + } else if (type === 'Tranche 2') { + data.tranche2_montant = montant + data.tranche2_bordereau = num_bordereau + } else { + // Tranche Complète : le montant saisi = total, on partage en 2 + const half = Math.round(Number(montant) / 2) + data.tranche1_montant = half + data.tranche1_bordereau = num_bordereau + data.tranche2_montant = Number(montant) - half + data.tranche2_bordereau = num_bordereau + } - return rows + const is_paid = (data.tranche1_montant > 0 || data.tranche2_montant > 0) ? 1 : 0 + await pool.query( + 'INSERT INTO trancheecolage (etudiant_id, annee_scolaire_id, tranche1_montant, tranche1_bordereau, tranche2_montant, tranche2_bordereau, is_paid) VALUES (?, ?, ?, ?, ?, ?, ?)', + [etudiant_id, annee_scolaire_id, data.tranche1_montant, data.tranche1_bordereau, data.tranche2_montant, data.tranche2_bordereau, is_paid] + ) + return { success: true } + } else { + // Mettre à jour la ligne existante + const row = existing[0] + let updates = {} + + if (type === 'Tranche 1') { + updates.tranche1_montant = montant + updates.tranche1_bordereau = num_bordereau + } else if (type === 'Tranche 2') { + updates.tranche2_montant = montant + updates.tranche2_bordereau = num_bordereau + } else { + // Tranche Complète : le montant saisi = total, on partage en 2 + const half = Math.round(Number(montant) / 2) + updates.tranche1_montant = half + updates.tranche1_bordereau = num_bordereau + updates.tranche2_montant = Number(montant) - half + updates.tranche2_bordereau = num_bordereau + } + + const t1 = updates.tranche1_montant !== undefined ? updates.tranche1_montant : row.tranche1_montant + const t2 = updates.tranche2_montant !== undefined ? updates.tranche2_montant : row.tranche2_montant + const b1 = updates.tranche1_bordereau !== undefined ? updates.tranche1_bordereau : row.tranche1_bordereau + const b2 = updates.tranche2_bordereau !== undefined ? updates.tranche2_bordereau : row.tranche2_bordereau + const is_paid = (t1 > 0 || t2 > 0) ? 1 : 0 + + await pool.query( + 'UPDATE trancheecolage SET tranche1_montant = ?, tranche1_bordereau = ?, tranche2_montant = ?, tranche2_bordereau = ?, is_paid = ? WHERE id = ?', + [t1, b1, t2, b2, is_paid, row.id] + ) + return { success: true } + } } catch (error) { - return error + return { success: false, error: error.message } } } -async function updateTranche(id, tranchename, montant) { - const sql = 'UPDATE trancheecolage SET tranchename = ?, montant = ? WHERE id = ?' - +/** + * Récupérer la ligne de tranche d'un étudiant (toutes les années) + */ +async function getTranche(etudiant_id) { + const sql = ` + SELECT t.*, a.code AS annee_scolaire_code + FROM trancheecolage t + LEFT JOIN anneescolaire a ON t.annee_scolaire_id = a.id + WHERE t.etudiant_id = ? + ORDER BY a.code DESC + ` try { - const [result] = await pool.query(sql, [tranchename, montant, id]) - console.log('resultat tranche:',result); - - if (result.affectedRows === 0) { - return { - success: false, - message: 'Année Univesitaire non trouvé.' - } - } + const [rows] = await pool.query(sql, [etudiant_id]) + return rows + } catch (error) { + return [] + } +} - return { - success: true, - message: 'Année Univesitaire supprimé avec succès.' - } +/** + * Modifier une ligne de tranche (les 2 tranches d'un coup) + */ +async function updateTranche(id, tranche1_montant, tranche1_bordereau, tranche2_montant, tranche2_bordereau) { + const t1 = Number(tranche1_montant) || 0 + const t2 = Number(tranche2_montant) || 0 + const is_paid = (t1 > 0 || t2 > 0) ? 1 : 0 + try { + const [result] = await pool.query( + 'UPDATE trancheecolage SET tranche1_montant = ?, tranche1_bordereau = ?, tranche2_montant = ?, tranche2_bordereau = ?, is_paid = ? WHERE id = ?', + [t1, tranche1_bordereau || null, t2, tranche2_bordereau || null, is_paid, id] + ) + if (result.affectedRows === 0) return { success: false, message: 'Non trouve.' } + return { success: true } } catch (error) { - console.log('resultat error:',error); - return error + return { success: false, error: error.message } } } +/** + * Supprimer une ligne de tranche + */ async function deleteTranche(id) { - const sql = 'DELETE FROM trancheecolage WHERE id = ?' - try { - let [result] = await pool.query(sql, [id]) - - if (result.affectedRows === 0) { - return { - success: false, - message: 'Année Univesitaire non trouvé.' - } - } - - return { - success: true, - message: 'Année Univesitaire supprimé avec succès.' - } + const [result] = await pool.query('DELETE FROM trancheecolage WHERE id = ?', [id]) + if (result.affectedRows === 0) return { success: false, message: 'Non trouve.' } + return { success: true } } catch (error) { - return error + return { success: false, error: error.message } } } -async function deleteEtudiant(id) { - console.log("id: ", id); - const sql = 'DELETE FROM etudiants WHERE id = ?'; - +/** + * Étudiants ayant payé au moins 1 tranche pour une année scolaire + */ +async function getEtudiantsWithPaidTranche(annee_scolaire_code) { + const sql = ` + SELECT t.etudiant_id + FROM trancheecolage t + INNER JOIN anneescolaire a ON t.annee_scolaire_id = a.id + WHERE a.code = ? AND t.is_paid = 1 + ` try { - let [result] = await pool.query(sql, [id]); - console.log("Résultat DELETE:", result); - - if (result.affectedRows === 0) { - return { - success: false, - message: 'Etudiant non trouvée.' - }; - } - - return { - success: true, - message: 'Matière supprimée avec succès.' - }; + const [rows] = await pool.query(sql, [annee_scolaire_code]) + return rows.map(r => r.etudiant_id) } catch (error) { - console.log("err: ",+ error) - return { success: false, error: 'Erreur, veuillez réessayer: ' + error }; - + return [] } } -async function getSingleTranche(id) { - const sql = 'SELECT * FROM trancheecolage WHERE id = ?' +/** + * Réinscription d'un étudiant existant pour une nouvelle année scolaire. + * Crée une NOUVELLE inscription sans modifier les inscriptions précédentes. + */ +async function reinscribeEtudiant( + etudiant_id, + niveau, + annee_scolaire, + status, + num_inscription, + mention_id, + parcours +) { + const conn = await pool.getConnection() try { - const [rows] = await pool.query(sql, [id]) - return rows[0] + await conn.beginTransaction() + + // 1. Get annee_scolaire_id + const annee_scolaire_id = await getAnneeScolaireId(conn, annee_scolaire) + if (!annee_scolaire_id) { + await conn.rollback() + return { success: false, message: 'Année scolaire introuvable: ' + annee_scolaire } + } + + // 2. Vérifier si une inscription existe déjà pour cet étudiant et cette année + const [existing] = await conn.query( + 'SELECT id FROM inscriptions WHERE etudiant_id = ? AND annee_scolaire_id = ?', + [etudiant_id, annee_scolaire_id] + ) + + if (existing.length > 0) { + // Même année scolaire : mettre à jour l'inscription existante + await conn.query( + `UPDATE inscriptions SET niveau=?, mention_id=?, parcours=?, status=?, num_inscription=? + WHERE id=?`, + [niveau, mention_id, parcours, status, num_inscription, existing[0].id] + ) + } else { + // Nouvelle année scolaire : créer une nouvelle inscription + await conn.query( + `INSERT INTO inscriptions + (etudiant_id, annee_scolaire_id, niveau, mention_id, parcours, status, num_inscription) + VALUES (?, ?, ?, ?, ?, ?, ?)`, + [etudiant_id, annee_scolaire_id, niveau, mention_id, parcours, status, num_inscription] + ) + } + + await conn.commit() + return { success: true, id: etudiant_id } } catch (error) { - return error + await conn.rollback() + return { success: false, message: error.message } + } finally { + conn.release() } } module.exports = { insertEtudiant, getAllEtudiants, + getAllEtudiantsByAnnee, FilterDataByNiveau, getSingleEtudiant, updateEtudiant, getDataToDashboard, changePDP, updateParcours, - createTranche, + payerTranche, getTranche, updateTranche, deleteTranche, deleteEtudiant, - getSingleTranche + getEtudiantsWithPaidTranche, + reinscribeEtudiant } diff --git a/database/Models/NoteRepechage.js b/database/Models/NoteRepechage.js index 8cd2dd7..29d5b68 100644 --- a/database/Models/NoteRepechage.js +++ b/database/Models/NoteRepechage.js @@ -64,17 +64,18 @@ async function getNoteOnline() { * * @returns promise */ -async function getNoteRepech(id, niveau) { +async function getNoteRepech(id, niveau, annee_scolaire) { const query = ` SELECT notesrepech.*, matieres.* FROM notesrepech JOIN matieres ON notesrepech.matiere_id = matieres.id WHERE notesrepech.etudiant_id = ? AND notesrepech.etudiant_niveau = ? + AND notesrepech.annee_scolaire = ? ` try { - const [rows] = await pool.query(query, [id, niveau]) + const [rows] = await pool.query(query, [id, niveau, annee_scolaire]) return rows } catch (error) { console.error('Error in getNoteRepech:', error) @@ -140,14 +141,14 @@ async function showMoyenRepech(niveau, scolaire) { * @param {string} niveau - The student level * @returns {Promise} - Promise resolving to the database response or an error */ -async function updateNoteRepech(formData, niveau, id) { +async function updateNoteRepech(formData, niveau, id, annee_scolaire) { const matiere_ids = Object.keys(formData) const values = Object.values(formData) const query = ` UPDATE notesrepech SET note = ? - WHERE etudiant_id = ? AND etudiant_niveau = ? AND matiere_id = ? + WHERE etudiant_id = ? AND etudiant_niveau = ? AND matiere_id = ? AND annee_scolaire = ? ` try { @@ -156,17 +157,13 @@ async function updateNoteRepech(formData, niveau, id) { for (let index = 0; index < matiere_ids.length; index++) { let data = values[index] - // Convert string number with comma to float, e.g. "12,5" => 12.5 if (typeof data === 'string') { data = parseFloat(data.replace(',', '.')) } else { data = parseFloat(String(data).replace(',', '.')) } - // Optional: console log to verify conversion - console.log(data) - - const [result] = await pool.query(query, [data, id, niveau, matiere_ids[index]]) + const [result] = await pool.query(query, [data, id, niveau, matiere_ids[index], annee_scolaire]) response = result } @@ -189,8 +186,8 @@ async function blockShowMoyeneRepech() { const query2 = ` SELECT notesrepech.*, etudiants.id AS etudiantsId, - etudiants.mention_id AS mentionId, - etudiants.niveau, + notesrepech.mention_id AS mentionId, + notesrepech.etudiant_niveau AS niveau, matieres.* FROM notesrepech INNER JOIN etudiants ON notesrepech.etudiant_id = etudiants.id diff --git a/database/Models/Notes.js b/database/Models/Notes.js index 93e998e..b875317 100644 --- a/database/Models/Notes.js +++ b/database/Models/Notes.js @@ -88,34 +88,19 @@ async function getNoteOnline() { * * @returns promise */ -async function getNote(id, niveau) { +async function getNote(id, niveau, annee_scolaire) { let connection try { connection = await pool.getConnection() - // 1. Get all notes joined with matieres - const [response] = await connection.execute( - ` - SELECT notes.*, matieres.* - FROM notes - JOIN matieres ON notes.matiere_id = matieres.id - WHERE notes.etudiant_id = ? AND notes.etudiant_niveau = ? - `, - [id, niveau] - ) - - // 2. Optional: Build list of matiere_id (not used here but kept from original) - const arrayResponseIdMatiere = response.map((note) => note.matiere_id) - - // 3. Same query again (as in your original) — this is redundant unless changed const [response2] = await connection.execute( ` SELECT notes.*, matieres.* FROM notes JOIN matieres ON notes.matiere_id = matieres.id - WHERE notes.etudiant_id = ? AND notes.etudiant_niveau = ? + WHERE notes.etudiant_id = ? AND notes.etudiant_niveau = ? AND notes.annee_scolaire = ? `, - [id, niveau] + [id, niveau, annee_scolaire] ) return response2 @@ -164,18 +149,18 @@ async function showMoyen(niveau, scolaire) { // 2. Prepare the second query const query2 = ` - SELECT notes.*, etudiants.*, matieres.id AS matiere_id, matieres.nom AS nomMat, matieres.credit + SELECT notes.*, etudiants.*, matieres.id AS matiere_id, matieres.nom AS nomMat, matieres.credit, matieres.ue FROM notes INNER JOIN etudiants ON notes.etudiant_id = etudiants.id INNER JOIN matieres ON notes.matiere_id = matieres.id - WHERE notes.etudiant_id = ? + WHERE notes.etudiant_id = ? AND notes.annee_scolaire = ? ` // 3. Loop over each student and fetch their notes for (let index = 0; index < etudiantWithNotes.length; index++) { const etudiantId = etudiantWithNotes[index].etudiant_id - const [rows] = await pool.query(query2, [etudiantId]) - allEtudiantWithNotes.push(rows) // push just the rows, not [rows, fields] + const [rows] = await pool.query(query2, [etudiantId, scolaire]) + allEtudiantWithNotes.push(rows) } return allEtudiantWithNotes @@ -207,10 +192,10 @@ async function updateNote(formData, niveau, id, mention_id, annee_scolaire) { data = parseFloat(String(data).replace(',', '.')) } - // 1. Check if already in notesrepech + // 1. Check if already in notesrepech for this year const [check] = await pool.query( - 'SELECT * FROM notesrepech WHERE etudiant_id = ? AND matiere_id = ? AND etudiant_niveau = ?', - [id, matiere_id[index], niveau] + 'SELECT * FROM notesrepech WHERE etudiant_id = ? AND matiere_id = ? AND etudiant_niveau = ? AND annee_scolaire = ?', + [id, matiere_id[index], niveau, annee_scolaire] ) if (data < 10) { @@ -223,22 +208,22 @@ async function updateNote(formData, niveau, id, mention_id, annee_scolaire) { ) } - // 3. Update main note anyway + // 3. Update main note for the correct year ;[response] = await pool.query( - 'UPDATE notes SET note = ? WHERE etudiant_id = ? AND etudiant_niveau = ? AND matiere_id = ?', - [data, id, niveau, matiere_id[index]] + 'UPDATE notes SET note = ? WHERE etudiant_id = ? AND etudiant_niveau = ? AND matiere_id = ? AND annee_scolaire = ?', + [data, id, niveau, matiere_id[index], annee_scolaire] ) } else { // 4. Remove from notesrepech if note >= 10 await pool.query( - 'DELETE FROM notesrepech WHERE etudiant_id = ? AND etudiant_niveau = ? AND matiere_id = ?', - [id, niveau, matiere_id[index]] + 'DELETE FROM notesrepech WHERE etudiant_id = ? AND etudiant_niveau = ? AND matiere_id = ? AND annee_scolaire = ?', + [id, niveau, matiere_id[index], annee_scolaire] ) - // 5. Update main note + // 5. Update main note for the correct year ;[response] = await pool.query( - 'UPDATE notes SET note = ? WHERE etudiant_id = ? AND etudiant_niveau = ? AND matiere_id = ?', - [data, id, niveau, matiere_id[index]] + 'UPDATE notes SET note = ? WHERE etudiant_id = ? AND etudiant_niveau = ? AND matiere_id = ? AND annee_scolaire = ?', + [data, id, niveau, matiere_id[index], annee_scolaire] ) } } @@ -258,8 +243,8 @@ async function blockShowMoyene() { SELECT notes.*, etudiants.id AS etudiantsId, - etudiants.mention_id AS mentionId, - etudiants.niveau, + notes.mention_id AS mentionId, + notes.etudiant_niveau AS niveau, matieres.* FROM notes INNER JOIN etudiants ON notes.etudiant_id = etudiants.id diff --git a/database/Models/Parcours.js b/database/Models/Parcours.js index 9f7309f..e6ffcfe 100644 --- a/database/Models/Parcours.js +++ b/database/Models/Parcours.js @@ -176,8 +176,11 @@ async function extractFiche(matiere_id) { for (let index = 0; index < newResponse.length; index++) { const [students] = await connection.query( ` - SELECT * FROM etudiants - WHERE niveau LIKE ? AND annee_scolaire LIKE ? + SELECT e.*, i.niveau, i.mention_id, i.parcours, i.status, a.code AS annee_scolaire + FROM etudiants e + INNER JOIN inscriptions i ON e.id = i.etudiant_id + INNER JOIN anneescolaire a ON i.annee_scolaire_id = a.id + WHERE i.niveau LIKE ? AND a.code LIKE ? `, [`%${newResponse[index]}%`, `%${now}%`] ) diff --git a/database/database.js b/database/database.js index c10b0dd..0a38265 100644 --- a/database/database.js +++ b/database/database.js @@ -65,13 +65,9 @@ async function createTables() { prenom VARCHAR(250) DEFAULT NULL, photos TEXT DEFAULT NULL, date_de_naissances DATE DEFAULT NULL, - niveau VARCHAR(250) NOT NULL, - annee_scolaire VARCHAR(20) NOT NULL, - status INT DEFAULT NULL, - mention_id INT NOT NULL, - num_inscription TEXT UNIQUE NOT NULL, + num_inscription TEXT DEFAULT NULL, sexe VARCHAR(20) DEFAULT NULL, - cin VARCHAR(250) DEFAULT NULL, + cin TEXT DEFAULT NULL, date_delivrance TEXT DEFAULT NULL, nationalite VARCHAR(250) DEFAULT NULL, annee_bacc TEXT DEFAULT NULL, @@ -79,11 +75,8 @@ async function createTables() { boursier VARCHAR(20) DEFAULT NULL, domaine VARCHAR(250) DEFAULT NULL, contact VARCHAR(20) DEFAULT NULL, - parcours VARCHAR(250) DEFAULT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (status) REFERENCES status(id), - FOREIGN KEY (mention_id) REFERENCES mentions(id) + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB; `) @@ -188,6 +181,23 @@ async function createTables() { ) ENGINE=InnoDB; `) + await connection.query(` + CREATE TABLE IF NOT EXISTS inscriptions ( + id INT AUTO_INCREMENT PRIMARY KEY, + etudiant_id INT NOT NULL, + annee_scolaire_id INT NOT NULL, + niveau VARCHAR(10) NOT NULL, + mention_id INT DEFAULT NULL, + parcours VARCHAR(50) DEFAULT NULL, + status VARCHAR(20) DEFAULT NULL, + num_inscription VARCHAR(50) DEFAULT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (etudiant_id) REFERENCES etudiants(id), + FOREIGN KEY (annee_scolaire_id) REFERENCES anneescolaire(id), + UNIQUE KEY unique_num_inscription_annee (num_inscription, annee_scolaire_id) + ) ENGINE=InnoDB; + `) + await connection.query(` CREATE TABLE IF NOT EXISTS traitmentsystem ( id INT AUTO_INCREMENT PRIMARY KEY, @@ -248,8 +258,27 @@ async function createTables() { CREATE TABLE IF NOT EXISTS trancheecolage ( id INT AUTO_INCREMENT PRIMARY KEY, etudiant_id INT NOT NULL, - tranchename VARCHAR(255) NOT NULL, - montant DOUBLE NOT NULL + annee_scolaire_id INT NOT NULL, + tranche1_montant DOUBLE DEFAULT 0, + tranche1_bordereau VARCHAR(255) DEFAULT NULL, + tranche2_montant DOUBLE DEFAULT 0, + tranche2_bordereau VARCHAR(255) DEFAULT NULL, + is_paid TINYINT(1) DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY unique_etudiant_annee (etudiant_id, annee_scolaire_id) + ) ENGINE=InnoDB; + `) + + await connection.query(` + CREATE TABLE IF NOT EXISTS configecolage ( + id INT AUTO_INCREMENT PRIMARY KEY, + mention_id INT NOT NULL, + niveau_id INT NOT NULL, + montant_total DOUBLE NOT NULL DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY unique_mention_niveau (mention_id, niveau_id) ) ENGINE=InnoDB; `) diff --git a/database/function/System.js b/database/function/System.js index c6b40fd..f78fe47 100644 --- a/database/function/System.js +++ b/database/function/System.js @@ -68,159 +68,161 @@ async function updateStudents() { const connection = await pool.getConnection() try { await connection.beginTransaction() + const today = dayjs().format('YYYY-MM-DD') - // Get unfinished years (only one record assumed) - const [unfinishedYearsRows] = await connection.query( - 'SELECT * FROM traitmentsystem WHERE is_finished = 0 ORDER BY id ASC LIMIT 1' + // 1. Récupérer toutes les années scolaires triées par debut + const [allYears] = await connection.query( + 'SELECT * FROM anneescolaire ORDER BY debut ASC' ) - if (unfinishedYearsRows.length === 0) { - await connection.release() - return { message: 'No unfinished years found.' } + if (allYears.length < 2) { + await connection.rollback() + connection.release() + return { message: 'Pas assez d\'années scolaires configurées.' } } - const unfinishedYear = unfinishedYearsRows[0] - // Get all students of that unfinished year - const [allEtudiants] = await connection.query( - 'SELECT * FROM etudiants WHERE annee_scolaire = ?', - [unfinishedYear.code] - ) - - // Get distinct student IDs with notes - let etudiantWithNotes = [] - for (const etudiant of allEtudiants) { - const [results] = await connection.query( - 'SELECT DISTINCT etudiant_id FROM notes WHERE etudiant_niveau = ? AND annee_scolaire = ?', - [etudiant.niveau, etudiant.annee_scolaire] - ) - etudiantWithNotes.push(...results) - } - // Unique IDs - const uniqueId = [ - ...new Map(etudiantWithNotes.map((item) => [item.etudiant_id, item])).values() - ] - - // Get notes details per student - let allEtudiantWithNotes = [] - for (const student of uniqueId) { - const [rows] = await connection.query( - `SELECT notes.*, etudiants.*, matieres.id, matieres.nom AS nomMat, matieres.credit - FROM notes - LEFT JOIN etudiants ON notes.etudiant_id = etudiants.id - LEFT JOIN matieres ON notes.matiere_id = matieres.id - WHERE notes.etudiant_id = ?`, - [student.etudiant_id] - ) - allEtudiantWithNotes.push(rows) - } - - // Get distinct student IDs with notesrepech - let etudiantWithNotesRepech = [] - for (const etudiant of allEtudiants) { - const [results] = await connection.query( - 'SELECT DISTINCT etudiant_id FROM notesrepech WHERE etudiant_niveau = ? AND annee_scolaire = ?', - [etudiant.niveau, etudiant.annee_scolaire] - ) - etudiantWithNotesRepech.push(...results) - } - // Unique IDs for repech - const uniqueIdRepech = [ - ...new Map(etudiantWithNotesRepech.map((item) => [item.etudiant_id, item])).values() - ] - - // Get notesrepech details per student - let allEtudiantWithNotesRepech = [] - for (const student of uniqueIdRepech) { - const [rows] = await connection.query( - `SELECT notesrepech.*, etudiants.*, matieres.id, matieres.nom AS nomMat, matieres.credit - FROM notesrepech - INNER JOIN etudiants ON notesrepech.etudiant_id = etudiants.id - INNER JOIN matieres ON notesrepech.matiere_id = matieres.id - WHERE notesrepech.etudiant_id = ?`, - [student.etudiant_id] - ) - allEtudiantWithNotesRepech.push(rows) - } - - // Compute averages and prepare data - let dataToMap = [] - for (let i = 0; i < allEtudiantWithNotes.length; i++) { - const notesNormal = allEtudiantWithNotes[i] - const notesRepech = allEtudiantWithNotesRepech[i] || [] - - let total = 0 - let totalCredit = 0 - let modelJson = { - id: '', - nom: '', - prenom: '', - photos: '', - moyenne: '', - mention: '', - niveau: '', - annee_scolaire: '' - } - - for (let j = 0; j < notesNormal.length; j++) { - const normalNote = notesNormal[j] - modelJson.id = normalNote.etudiant_id - modelJson.nom = normalNote.nom - modelJson.prenom = normalNote.prenom - modelJson.photos = normalNote.photos - modelJson.mention = normalNote.mention_id - modelJson.niveau = normalNote.niveau - modelJson.annee_scolaire = normalNote.annee_scolaire - - // Find repech note for same matiere if exists - const repechNoteObj = notesRepech.find((r) => r.matiere_id === normalNote.matiere_id) - const noteToUse = compareSessionNotes(normalNote.note, checkNull(repechNoteObj)) - - total += (noteToUse ?? 0) * normalNote.credit - totalCredit += normalNote.credit - } - - modelJson.moyenne = (totalCredit > 0 ? total / totalCredit : 0).toFixed(2) - dataToMap.push(modelJson) - } - - // Get note system thresholds + // 2. Seuils de notes const [noteSystemRows] = await connection.query('SELECT * FROM notesystems LIMIT 1') const noteSystem = noteSystemRows[0] - // Update etudiants based on moyenne - for (const student of dataToMap) { - let status, newNiveau, newAnnee - newAnnee = updateSchoolYear(student.annee_scolaire) - - if (student.moyenne >= noteSystem.admis) { - newNiveau = nextLevel(student.niveau) - status = 2 // Passed - } else if (student.moyenne >= noteSystem.redouble) { - newNiveau = student.niveau - status = 3 // Repeat - } else { - newNiveau = student.niveau - status = 4 // Fail - } - - await connection.query( - 'UPDATE etudiants SET niveau = ?, annee_scolaire = ?, status = ? WHERE id = ?', - [newNiveau, newAnnee, status, student.id] + let totalProcessed = 0 + + // 3. Pour chaque paire d'années consécutives (annéeN → annéeN+1) + for (let i = 0; i < allYears.length - 1; i++) { + const prevYear = allYears[i] + const nextYear = allYears[i + 1] + + // Traiter uniquement si l'année suivante a déjà commencé + if (nextYear.debut > today) continue + + // Trouver les étudiants inscrits en annéeN mais PAS encore en annéeN+1 + const [studentsToProcess] = await connection.query( + `SELECT i.*, e.nom, e.prenom, e.photos + FROM inscriptions i + JOIN etudiants e ON i.etudiant_id = e.id + WHERE i.annee_scolaire_id = ? + AND i.etudiant_id NOT IN ( + SELECT etudiant_id FROM inscriptions WHERE annee_scolaire_id = ? + )`, + [prevYear.id, nextYear.id] ) + + if (studentsToProcess.length === 0) continue + + for (const inscription of studentsToProcess) { + const inscStatus = parseInt(inscription.status) + + // Renvoyé → pas de nouvelle inscription + if (inscStatus === 4) continue + + let newNiveau, newStatus + + // Vérifier si l'étudiant a des notes pour l'année précédente + const [notesRows] = await connection.query( + `SELECT n.note, n.matiere_id, m.credit + FROM notes n + JOIN matieres m ON n.matiere_id = m.id + WHERE n.etudiant_id = ? AND n.annee_scolaire = ?`, + [inscription.etudiant_id, prevYear.code] + ) + + if (notesRows.length > 0) { + // Calculer la moyenne avec prise en compte du rattrapage + const [repechRows] = await connection.query( + `SELECT n.note, n.matiere_id, m.credit + FROM notesrepech n + JOIN matieres m ON n.matiere_id = m.id + WHERE n.etudiant_id = ? AND n.annee_scolaire = ?`, + [inscription.etudiant_id, prevYear.code] + ) + + let total = 0 + let totalCredit = 0 + for (const note of notesRows) { + const repNote = repechRows.find(r => r.matiere_id === note.matiere_id) + const noteToUse = compareSessionNotes(note.note, checkNull(repNote)) + total += (noteToUse ?? 0) * note.credit + totalCredit += note.credit + } + const moyenne = totalCredit > 0 ? total / totalCredit : 0 + + if (moyenne >= noteSystem.admis) { + newNiveau = nextLevel(inscription.niveau) + newStatus = 2 // Passant + } else if (moyenne >= noteSystem.renvoyer) { + newNiveau = inscription.niveau + newStatus = 3 // Redoublant + } else { + newNiveau = inscription.niveau + newStatus = 4 // Renvoyé + continue // Pas de nouvelle inscription pour les renvoyés + } + } else { + // Pas de notes → utiliser le statut de l'inscription + if (inscStatus === 2) { + // Passant → niveau supérieur + newNiveau = nextLevel(inscription.niveau) + newStatus = 2 + } else { + // Redoublant, Nouveau, Ancien → même niveau + newNiveau = inscription.niveau + newStatus = 3 + } + } + + // Créer l'inscription pour l'année suivante + await connection.query( + `INSERT INTO inscriptions + (etudiant_id, annee_scolaire_id, niveau, mention_id, parcours, status, num_inscription) + VALUES (?, ?, ?, ?, ?, ?, ?)`, + [ + inscription.etudiant_id, nextYear.id, newNiveau, + inscription.mention_id, inscription.parcours, newStatus, + inscription.num_inscription + ] + ) + + // Pour les redoublants, copier les notes de l'année précédente (seulement si pas encore copiées) + if (newStatus === 3) { + const [existingNotes] = await connection.query( + `SELECT COUNT(*) as cnt FROM notes WHERE etudiant_id = ? AND annee_scolaire = ?`, + [inscription.etudiant_id, nextYear.code] + ) + if (existingNotes[0].cnt === 0) { + await connection.query( + `INSERT INTO notes (etudiant_id, matiere_id, etudiant_niveau, mention_id, note, annee_scolaire) + SELECT etudiant_id, matiere_id, etudiant_niveau, mention_id, note, ? + FROM notes + WHERE etudiant_id = ? AND annee_scolaire = ?`, + [nextYear.code, inscription.etudiant_id, prevYear.code] + ) + await connection.query( + `INSERT INTO notesrepech (etudiant_id, matiere_id, etudiant_niveau, mention_id, note, annee_scolaire) + SELECT etudiant_id, matiere_id, etudiant_niveau, mention_id, note, ? + FROM notesrepech + WHERE etudiant_id = ? AND annee_scolaire = ?`, + [nextYear.code, inscription.etudiant_id, prevYear.code] + ) + } + } + + totalProcessed++ + } } - // Mark unfinished year as finished - await connection.query('UPDATE traitmentsystem SET is_finished = 1 WHERE id = ?', [ - unfinishedYear.id - ]) + // 4. Mettre à jour traitmentsystem (nettoyage) + await connection.query( + 'UPDATE traitmentsystem SET is_finished = 1 WHERE is_finished = 0' + ) await connection.commit() connection.release() - return { success: true, message: 'Students updated successfully' } + console.log(`✅ updateStudents: ${totalProcessed} inscription(s) créée(s).`) + return { success: true, message: `${totalProcessed} inscription(s) créée(s).`, totalProcessed } } catch (error) { await connection.rollback() connection.release() - console.error(error) + console.error('❌ updateStudents error:', error) throw error } } diff --git a/database/import/Etudiants.js b/database/import/Etudiants.js index 8ae1420..911999c 100644 --- a/database/import/Etudiants.js +++ b/database/import/Etudiants.js @@ -9,6 +9,12 @@ 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 @@ -62,13 +68,8 @@ function convertToISODate(input) { return null; } -// Vérifie année bissextile -function isLeapYear(year) { - return (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0); -} - -// ---------- UPDATE étudiant ---------- -async function updateEtudiant(row) { +// ---------- UPDATE étudiant existant ---------- +async function updateEtudiant(row, conn) { const fields = []; const params = []; @@ -79,13 +80,10 @@ async function updateEtudiant(row) { } } + // Only permanent fields in etudiants 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)); @@ -99,7 +97,6 @@ async function updateEtudiant(row) { 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]; @@ -109,49 +106,44 @@ async function updateEtudiant(row) { } try { - const [result] = await pool.query(sql, [...params, ...whereParams]); - return { success: true, affectedRows: result.affectedRows }; - } catch (error) { - return { success: false, error: error.message }; - } -} - -// ---------- 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, - 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 || null, - convertToISODate(row.date_de_delivrance), - row.nationaliter, - parseInt(row.annee_baccalaureat, 10), - row.serie, - row.boursier, - fixEncoding(row.domaine), - row.contact, - null - ]); + 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()] + ); + } + } + } - try { - const [result] = await pool.query(sql, [values]); return { success: true, affectedRows: result.affectedRows }; } catch (error) { return { success: false, error: error.message }; @@ -188,53 +180,115 @@ async function importFileToDatabase(filePath) { } } - const [mentionRows] = await pool.query('SELECT * FROM mentions'); - const [statusRows] = await pool.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 - 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; - - // 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()] + 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); + } } - if (existing.length > 0) { - doublons.push({ nom: row.nom, prenom: row.prenom, cin: row.cin }); - const updateResult = await updateEtudiant(row); - if (!updateResult.success) 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)); + 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 + ] + ); - const insertResult = await insertMultipleEtudiants(etudiantsToInsert); - if (!insertResult.success) return { error: true, message: insertResult.error }; + // 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()] + ); + } - return { error: false, message: `Importation réussie. ${etudiantsToInsert.length} nouvel(le)(s) inséré(s), ${doublons.length} mis à jour.` }; + 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 }; diff --git a/resume_modifications.txt b/resume_modifications.txt new file mode 100644 index 0000000..7d81a52 --- /dev/null +++ b/resume_modifications.txt @@ -0,0 +1,184 @@ +============================================================ + RÉSUMÉ DES MODIFICATIONS - C-UNIVERSITY +============================================================ + +1. BASE DE DONNÉES / SCHÉMA (database/database.js) +------------------------------------------------------------ +- Refonte de la table "etudiants" : les champs annuels (niveau, année scolaire, status, mention, parcours) ont été retirés +- Nouvelle table "inscriptions" : stocke chaque inscription annuelle d'un étudiant (niveau, mention, parcours, status, num_inscription) +- Nouvelle table "configecolage" : configure le montant d'écolage par mention/niveau +- Restructuration de "trancheecolage" : passe à 2 tranches par ligne (tranche1_montant, tranche1_bordereau, tranche2_montant, tranche2_bordereau, is_paid) + + +2. MODELS +------------------------------------------------------------ + + database/Models/Etudiants.js + - Refonte complète : toutes les fonctions utilisent maintenant des transactions et la table "inscriptions" + - Nouvelles fonctions : reinscribeEtudiant, payerTranche, getEtudiantsWithPaidTranche, getAllEtudiantsByAnnee + - insertEtudiant : insère dans "etudiants" puis crée une inscription dans "inscriptions" (transaction) + - updateEtudiant : met à jour les données permanentes + cherche/crée l'inscription de l'année + - deleteEtudiant : suppression en cascade (notes, notesrepech, trancheecolage, inscriptions, etudiants) + - payerTranche (remplace createTranche) : gère Tranche 1, Tranche 2, Tranche Complète avec bordereau + - getTranche : jointure avec anneescolaire, tri par année décroissante + - updateTranche : signature étendue à 4 paramètres (tranche1_montant, tranche1_bordereau, tranche2_montant, tranche2_bordereau) + + database/Models/AnneeScolaire.js + - Correction : ajout des "await" manquants dans setCurrent (bug critique de condition de course) + + database/Models/Notes.js + - Ajout du filtre par annee_scolaire dans toutes les requêtes (getNote, showMoyen, updateNote, blockShowMoyene) + - blockShowMoyene : utilise notes.mention_id et notes.etudiant_niveau au lieu des champs supprimés de etudiants + + database/Models/NoteRepechage.js + - Ajout du filtre par annee_scolaire dans getNoteRepech, updateNoteRepech, blockShowMoyenRepech + + database/Models/Parcours.js + - extractFiche : jointure avec inscriptions et anneescolaire au lieu de champs directs sur etudiants + + database/Models/ConfigEcolage.js (NOUVEAU) + - getAllConfigEcolage() : liste toutes les configurations avec noms de mention et niveau + - getConfigEcolageByMentionNiveau() : retourne le montant pour une combinaison mention/niveau + - upsertConfigEcolage() : INSERT ou UPDATE selon existence + - deleteConfigEcolage() : suppression d'une configuration + + +3. BACKEND (main process / preload) +------------------------------------------------------------ + + src/main/index.js + - Nouveaux handlers IPC : getAllConfigEcolage, getConfigEcolageByMentionNiveau, upsertConfigEcolage, deleteConfigEcolage + - Remplacement de createTranche par payerTranche + - Ajout de getEtudiantsWithPaidTranche et reinscribeEtudiant + - Handlers de notes/rattrapage reçoivent maintenant annee_scolaire en paramètre + - Démarrage : updateCurrentYears() et updateStudents() enveloppés dans async/await avec gestion d'erreur + + src/preload/index.js + - Exposition de getEtudiantsByAnnee sur window.etudiants + - Remplacement de createTranche par payerTranche + - Remplacement de getSingleTranche par getEtudiantsWithPaidTranche et reinscribeEtudiant + - Nouvel objet window.configecolage avec 4 méthodes (getAll, getByMentionNiveau, upsert, delete) + + +4. FRONTEND - COMPOSANTS MODIFIÉS +------------------------------------------------------------ + + src/renderer/src/Routes/Routes.jsx + - Ajout de la route /configecolage vers le composant ConfigEcolage + + src/renderer/src/components/AddNotes.jsx + - Navigation retour transmet selectedAnnee depuis localStorage + + src/renderer/src/components/AddStudent.jsx (refonte majeure) + - Ajout barre de recherche d'étudiant existant (par nom, prénom ou numéro d'inscription) + - Réinscription : si étudiant existant sélectionné, appel reinscribeEtudiant au lieu de insertEtudiant + - Section paiement écolage intégrée (type de paiement, bordereau, montant, montant restant) + - Chargement automatique du montant configuré selon mention/niveau + + src/renderer/src/components/AjoutTranche.jsx + - Formulaire repensé : sélection année scolaire, type de paiement (Tranche 1/2/Complète), numéro de bordereau + - Appel payerTranche au lieu de createTranche + + src/renderer/src/components/AnneeScolaire.jsx + - Fonction setCurrent rétablie (était commentée) + + src/renderer/src/components/Home.jsx + - Chargement par 3 appels séparés au lieu de getDataToDashboards + - Filtre par année via getEtudiantsByAnnee + + src/renderer/src/components/Noteclasse.jsx (refonte calcul) + - Nouvelle fonction calculerMoyennePonderee centralisée + - Gestion correcte du rattrapage (0 = pas de rattrapage, prend la meilleure note si rattrapage > 0) + - Remplacement de l'état "session" par "moyennesCalculees" + + src/renderer/src/components/ReleverNotes.jsx + - PDF amélioré : format A4 (210mm x 297mm), redimensionnement automatique + - Calcul de moyenne pondérée corrigé avec rattrapage + - Décision du jury basée sur le seuil du système de notes (noteSysteme.admis) + - Correction noterepech : null si pas de rattrapage (au lieu de la note normale) + + src/renderer/src/components/Resultat.jsx + - Ajout des moyennes avec rattrapage (moyennesRattrapage) + - calculerMoyennePonderee dupliquée localement + - Correction extractMatieresAndUEs : utilise nomMat et Map + + src/renderer/src/components/SingleNotes.jsx + - Transmission de annee_scolaire aux appels de notes et notes de rattrapage + + src/renderer/src/components/Student.jsx + - Adapté au nouveau modèle avec inscriptions + + src/renderer/src/components/TrancheEcolage.jsx + - Adaptation au nouveau format 2 tranches avec bordereau + + src/renderer/src/components/UpdateTranche.jsx + - Adaptation au nouveau format (tranche1_montant, tranche1_bordereau, tranche2_montant, tranche2_bordereau) + + src/renderer/src/components/Sidenav.jsx + - Ajout du lien vers la page Config Ecolage + + src/renderer/src/components/function/FonctionRelever.js + - Récupération du système de notes intégrée dans les calculs du relevé + + +5. FRONTEND - NOUVEAUX COMPOSANTS +------------------------------------------------------------ + + src/renderer/src/components/ConfigEcolage.jsx (NOUVEAU) + - Interface d'administration des montants d'écolage par mention et par niveau + - Formulaire : sélection mention, niveau, saisie montant total + - Tableau : affiche Mention, Niveau, Montant total, Tranche 1 (moitié), Tranche 2 (moitié) + - Actions : modifier (pré-remplit le formulaire) et supprimer + + src/renderer/src/components/Ecolage.jsx (NOUVEAU) + - Tableau de suivi des paiements (DataGrid MUI) + - Filtres par année scolaire et par niveau + - Colonnes : Nom, Prénom, N° inscription, Niveau, Mention, Année, Statut, Contact, Tranches payées, Photo + - Indicateur visuel : vert si tranche payée, rouge sinon + + +6. SYSTÈME D'IMPORT (database/import/Etudiants.js) +------------------------------------------------------------ +- Import enveloppé dans une transaction complète +- Insertions une par une dans "etudiants" + "inscriptions" (au lieu de batch INSERT) +- Résolution de annee_scolaire_id depuis le code d'année +- Auto-progression du niveau pour les passants (L1->L2->...->M2->D1->...->PHD) +- Rollback automatique en cas d'erreur + + +7. SYSTÈME DE TRAITEMENT (database/function/System.js) +------------------------------------------------------------ +- Refonte de updateStudents : + * Récupère toutes les années scolaires triées par date + * Pour chaque paire d'années consécutives, traite les étudiants non encore inscrits en année N+1 + * Calcule la moyenne avec rattrapage pour déterminer le statut + * Crée une nouvelle inscription pour l'année suivante + * Copie les notes pour les redoublants + * Les étudiants renvoyés (status=4) ne reçoivent pas de nouvelle inscription + + +8. AUTRES +------------------------------------------------------------ + + README.md + - Mise à jour de la documentation + + +============================================================ + SYNTHÈSE GLOBALE +============================================================ + +Ces modifications représentent une REFONTE ARCHITECTURALE MAJEURE avec 2 axes : + +1. NORMALISATION DE LA BASE DE DONNÉES + Séparation des données permanentes d'un étudiant et de ses inscriptions + annuelles dans deux tables distinctes, permettant de gérer l'historique + complet sur plusieurs années sans dupliquer les données personnelles. + +2. MODULE ÉCOLAGE COMPLET + Configuration des montants par mention/niveau, paiement par tranche + avec numéro de bordereau, suivi intégré, et paiement possible dès + l'inscription d'un étudiant. + +Statistiques : 26 fichiers modifiés, 3 nouveaux fichiers + +2755 lignes ajoutées, -1926 lignes supprimées diff --git a/src/main/index.js b/src/main/index.js index 5396e39..b8ac072 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -22,11 +22,12 @@ const { changePDP, deleteEtudiant, updateParcours, - createTranche, + payerTranche, getTranche, updateTranche, deleteTranche, - getSingleTranche + getEtudiantsWithPaidTranche, + reinscribeEtudiant } = require('../../database/Models/Etudiants') const { insertNiveau, @@ -100,8 +101,14 @@ const { // Declare mainWindow and tray in the global scope let mainWindow let tray = null -updateCurrentYears() -updateStudents() +;(async () => { + try { + await updateCurrentYears() + await updateStudents() + } catch (error) { + console.error('❌ Startup system error:', error) + } +})() autoUpdater.setFeedURL({ provider: 'generic', @@ -368,6 +375,31 @@ ipcMain.handle('insertEtudiant', async (event, credentials) => { return insert }) +// event for reinscription (new inscription for existing student) +ipcMain.handle('reinscribeEtudiant', async (event, credentials) => { + const { + etudiant_id, + niveau, + annee_scolaire, + status, + num_inscription, + mention_id, + parcours + } = credentials + + const result = await reinscribeEtudiant( + etudiant_id, + niveau, + annee_scolaire, + status, + num_inscription, + mention_id, + parcours + ) + + return result +}) + // event for fetching single ipcMain.handle('getByNiveau', async (event, credentials) => { const { niveau } = credentials @@ -472,18 +504,18 @@ ipcMain.handle('insertNote', async (event, credentials) => { // event for get single note ipcMain.handle('getSingleNote', async (event, credentials) => { - const { id, niveau, mention_id } = credentials + const { id, niveau, mention_id, annee_scolaire } = credentials - const get = await getNote(id, niveau, mention_id) + const get = await getNote(id, niveau, annee_scolaire) return get }) // event for get single note repech ipcMain.handle('getNotesRepech', async (event, credentials) => { - const { id, niveau, mention_id } = credentials + const { id, niveau, mention_id, annee_scolaire } = credentials - const get = await getNoteRepech(id, niveau, mention_id) + const get = await getNoteRepech(id, niveau, annee_scolaire) return get }) @@ -499,9 +531,9 @@ ipcMain.handle('updatetNote', async (event, credentials) => { // event for updating note repech ipcMain.handle('updatetNoteRepech', async (event, credentials) => { - const { formData2, niveau, id } = credentials + const { formData2, niveau, id, annee_scolaire } = credentials - const update = await updateNoteRepech(formData2, niveau, id) + const update = await updateNoteRepech(formData2, niveau, id, annee_scolaire) return update }) @@ -846,58 +878,61 @@ ipcMain.handle('changeParcours', async (event, credentials) => { return get }) -ipcMain.handle('createTranche', async (event, credentials) => { - const { etudiant_id, tranchename, montant } = credentials - // console.log(formData, id); - const get = createTranche(etudiant_id, tranchename, montant) - - return get +ipcMain.handle('payerTranche', async (event, credentials) => { + const { etudiant_id, annee_scolaire_id, type, montant, num_bordereau } = credentials + return await payerTranche(etudiant_id, annee_scolaire_id, type, montant, num_bordereau) }) ipcMain.handle('getTranche', async (event, credentials) => { const { id } = credentials - // console.log(formData, id); - const get = getTranche(id) - - return get + return await getTranche(id) }) ipcMain.handle('updateTranche', async (event, credentials) => { - const { id, tranchename, montant } = credentials - // console.log(formData, id); - const get = updateTranche(id, tranchename, montant) - - return get + const { id, tranche1_montant, tranche1_bordereau, tranche2_montant, tranche2_bordereau } = credentials + return await updateTranche(id, tranche1_montant, tranche1_bordereau, tranche2_montant, tranche2_bordereau) }) ipcMain.handle('deleteTranche', async (event, credentials) => { const { id } = credentials - console.log(id) - const get = deleteTranche(id) + return await deleteTranche(id) +}) - return get +ipcMain.handle('getEtudiantsWithPaidTranche', async (event, credentials) => { + const { annee_scolaire } = credentials + return await getEtudiantsWithPaidTranche(annee_scolaire) }) -ipcMain.handle('getSingleTranche', async (event, credentials) => { - const { id } = credentials - // console.log(formData, id); - const get = getSingleTranche(id) +// --- Config Ecolage --- +const { getAllConfigEcolage, getConfigEcolageByMentionNiveau, upsertConfigEcolage, deleteConfigEcolage } = require('../../database/Models/ConfigEcolage') - return get +ipcMain.handle('getAllConfigEcolage', async () => { + return await getAllConfigEcolage() +}) + +ipcMain.handle('getConfigEcolageByMentionNiveau', async (event, credentials) => { + const { mention_id, niveau_nom } = credentials + return await getConfigEcolageByMentionNiveau(mention_id, niveau_nom) +}) + +ipcMain.handle('upsertConfigEcolage', async (event, credentials) => { + const { mention_id, niveau_id, montant_total } = credentials + return await upsertConfigEcolage(mention_id, niveau_id, montant_total) +}) + +ipcMain.handle('deleteConfigEcolage', async (event, credentials) => { + const { id } = credentials + return await deleteConfigEcolage(id) }) ipcMain.handle('createIPConfig', async (event, credentials) => { const { ipname } = credentials - // console.log(formData, id); const get = createConfigIp(ipname) - return get }) ipcMain.handle('updateIPConfig', async (event, credentials) => { const { id, ipname } = credentials - // console.log(formData, id); const get = updateIPConfig(id, ipname) - return get }) diff --git a/src/preload/index.js b/src/preload/index.js index 184005b..2206e64 100644 --- a/src/preload/index.js +++ b/src/preload/index.js @@ -2,7 +2,7 @@ import { contextBridge, ipcRenderer } from 'electron' import { electronAPI } from '@electron-toolkit/preload' const { getNessesarytable } = require('../../database/function/System') const { getAllUsers } = require('../../database/Models/Users') -const { getAllEtudiants, getDataToDashboard } = require('../../database/Models/Etudiants') +const { getAllEtudiants, getAllEtudiantsByAnnee, getDataToDashboard } = require('../../database/Models/Etudiants') const { verifyEtudiantIfHeHasNotes, blockShowMoyene } = require('../../database/Models/Notes') const { getMatiere, getSemestre, getEnseignants } = require('../../database/Models/Matieres') const { getSysteme } = require('../../database/Models/NoteSysrem') @@ -71,6 +71,7 @@ if (process.contextIsolated) { contextBridge.exposeInMainWorld('etudiants', { insertEtudiant: (credentials) => ipcRenderer.invoke('insertEtudiant', credentials), getEtudiants: () => getAllEtudiants(), + getEtudiantsByAnnee: (annee) => getAllEtudiantsByAnnee(annee), FilterDataByNiveau: (credential) => ipcRenderer.invoke('getByNiveau', credential), getSingle: (credential) => ipcRenderer.invoke('single', credential), updateEtudiants: (credentials) => ipcRenderer.invoke('updateETudiants', credentials), @@ -78,12 +79,13 @@ if (process.contextIsolated) { updateEtudiantsPDP: (credentials) => ipcRenderer.invoke('updateETudiantsPDP', credentials), importExcel: (credentials) => ipcRenderer.invoke('importexcel', credentials), changeParcours: (credentials) => ipcRenderer.invoke('changeParcours', credentials), - createTranche: (credentials) => ipcRenderer.invoke('createTranche', credentials), + payerTranche: (credentials) => ipcRenderer.invoke('payerTranche', credentials), getTranche: (credentials) => ipcRenderer.invoke('getTranche', credentials), updateTranche: (credentials) => ipcRenderer.invoke('updateTranche', credentials), deleteTranche: (credentials) => ipcRenderer.invoke('deleteTranche', credentials), deleteEtudiant: (id) => ipcRenderer.invoke('deleteEtudiant', id), - getSingleTranche: (credentials) => ipcRenderer.invoke('getSingleTranche', credentials) + getEtudiantsWithPaidTranche: (credentials) => ipcRenderer.invoke('getEtudiantsWithPaidTranche', credentials), + reinscribeEtudiant: (credentials) => ipcRenderer.invoke('reinscribeEtudiant', credentials) }) /** @@ -168,6 +170,16 @@ if (process.contextIsolated) { updateIPConfig: (credentials) => ipcRenderer.invoke('updateIPConfig', credentials) }) + /** + * contextbridge for configecolage + */ + contextBridge.exposeInMainWorld('configecolage', { + getAll: () => ipcRenderer.invoke('getAllConfigEcolage'), + getByMentionNiveau: (credentials) => ipcRenderer.invoke('getConfigEcolageByMentionNiveau', credentials), + upsert: (credentials) => ipcRenderer.invoke('upsertConfigEcolage', credentials), + delete: (credentials) => ipcRenderer.invoke('deleteConfigEcolage', credentials) + }) + /** * contextbridge for status */ diff --git a/src/renderer/src/Routes/Routes.jsx b/src/renderer/src/Routes/Routes.jsx index cafd33b..f2185cc 100644 --- a/src/renderer/src/Routes/Routes.jsx +++ b/src/renderer/src/Routes/Routes.jsx @@ -39,6 +39,7 @@ import Parcours from '../components/Parcours' import ModalExportFichr from '../components/ModalExportFichr' import Resultat from '../components/Resultat' import TrancheEcolage from '../components/TrancheEcolage' +import ConfigEcolage from '../components/ConfigEcolage' // Use createHashRouter instead of createBrowserRouter because the desktop app is in local machine const Router = createHashRouter([ @@ -178,6 +179,10 @@ const Router = createHashRouter([ path: '/resultat/:niveau/:scolaire', element: }, + { + path: '/configecolage', + element: + }, { path: '/tranche/:id', element: diff --git a/src/renderer/src/components/AddNotes.jsx b/src/renderer/src/components/AddNotes.jsx index f7a19bb..42249c9 100644 --- a/src/renderer/src/components/AddNotes.jsx +++ b/src/renderer/src/components/AddNotes.jsx @@ -139,7 +139,7 @@ const previousFilter = location.state?.selectedNiveau } const handleClose2 = () => { - navigate('/student', { state: { selectedNiveau: previousFilter } }) + navigate('/student', { state: { selectedNiveau: previousFilter, selectedAnnee: localStorage.getItem('selectedAnnee') || '' } }) setOpen(false) } diff --git a/src/renderer/src/components/AddStudent.jsx b/src/renderer/src/components/AddStudent.jsx index 7ab3abe..ab65665 100644 --- a/src/renderer/src/components/AddStudent.jsx +++ b/src/renderer/src/components/AddStudent.jsx @@ -45,37 +45,31 @@ import { MdChangeCircle, MdGrade, MdRule } from 'react-icons/md' import ModalRecepice from './ModalRecepice' import { FaLeftLong, FaRightLong } from 'react-icons/fa6' import { Tooltip } from 'react-tooltip' +import Radio from '@mui/material/Radio' +import RadioGroup from '@mui/material/RadioGroup' +import FormControlLabel from '@mui/material/FormControlLabel' +import Paper from '@mui/material/Paper' const AddStudent = () => { + const navigate = useNavigate() + const [niveaus, setNiveau] = useState([]) const [status, setStatus] = useState([]) const [scolaire, setScolaire] = useState([]) const [mention, setMention] = useState([]) const [parcours, setParcours] = useState([]) + const [isExistingStudent, setIsExistingStudent] = useState(false) - useEffect(() => { - window.niveaus.getNiveau().then((response) => { - setNiveau(response) - }) - - window.statuss.getStatus().then((response) => { - setStatus(response) - }) - - window.anneescolaire.getAnneeScolaire().then((response) => { - setScolaire(response) - }) - - window.mention.getMention().then((response) => { - setMention(response) - }) + // Recherche etudiant existant + const [searchQuery, setSearchQuery] = useState('') + const [searchResults, setSearchResults] = useState([]) + const [allStudents, setAllStudents] = useState([]) - window.notesysteme.getParcours().then((response) => { - setParcours(response) - }) - }, []) - - const navigate = useNavigate() + // Paiement + const [paiementType, setPaiementType] = useState('') + const [numBordereau, setNumBordereau] = useState('') + const [montantPaye, setMontantPaye] = useState('') + const [montantConfig, setMontantConfig] = useState(null) /** * hook for storing data in the input @@ -102,6 +96,105 @@ const AddStudent = () => { parcours: '' }) + useEffect(() => { + window.niveaus.getNiveau().then((response) => setNiveau(response)) + window.statuss.getStatus().then((response) => setStatus(response)) + window.anneescolaire.getAnneeScolaire().then((response) => setScolaire(response)) + window.mention.getMention().then((response) => setMention(response)) + window.notesysteme.getParcours().then((response) => setParcours(response)) + window.etudiants.getEtudiants().then((response) => setAllStudents(response || [])).catch(() => setAllStudents([])) + }, []) + + // Charger le montant quand mention_id et niveau changent + useEffect(() => { + if (formData.mention_id && formData.niveau && window.configecolage) { + window.configecolage.getByMentionNiveau({ + mention_id: formData.mention_id, + niveau_nom: formData.niveau + }).then((res) => { + setMontantConfig(res) + }).catch(() => setMontantConfig(null)) + } else { + setMontantConfig(null) + } + }, [formData.mention_id, formData.niveau]) + + // Recherche etudiant + const handleSearch = (e) => { + const query = e.target.value.toLowerCase().trim() + setSearchQuery(e.target.value) + if (query.length < 2) { setSearchResults([]); return } + + // Vérifier si la recherche est un numéro + const isNumeric = /^\d+$/.test(query) + + const filtered = allStudents.filter((s) => { + // Recherche par nom/prénom (toujours par inclusion) + if (s.nom && s.nom.toLowerCase().includes(query)) return true + if (s.prenom && s.prenom.toLowerCase().includes(query)) return true + + if (isNumeric) { + // Pour les numéros : correspondance exacte parmi les num_inscription + const nums = [] + if (s.num_inscription) nums.push(s.num_inscription.trim()) + if (s.all_num_inscriptions) { + s.all_num_inscriptions.split(',').forEach(n => nums.push(n.trim())) + } + return nums.some(n => n === query) + } else { + // Pour le texte : recherche par inclusion dans num_inscription + if (s.num_inscription && s.num_inscription.toLowerCase().includes(query)) return true + if (s.all_num_inscriptions && s.all_num_inscriptions.toLowerCase().includes(query)) return true + } + return false + }) + setSearchResults(filtered.slice(0, 8)) + } + + // Helper pour formater les dates en toute securite + const formatDate = (val) => { + if (!val) return '' + try { + const str = String(val) + if (str.includes('T')) return str.split('T')[0] + if (str.includes('-') && str.length >= 10) return str.substring(0, 10) + return str + } catch { return '' } + } + + const selectStudent = (student) => { + setIsExistingStudent(true) + setSearchQuery('') + setSearchResults([]) + console.log('Student sélectionné:', JSON.stringify(student, null, 2)) + setFormData({ + nom: student.nom || '', + prenom: student.prenom || '', + photos: student.photos || null, + date_de_naissances: formatDate(student.date_de_naissances), + niveau: student.niveau || '', + annee_scolaire: (scolaire.find((s) => s.is_current === 1) || {}).code || '', + status: student.status || '', + num_inscription: student.num_inscription || '', + mention_id: student.mention_id || '', + sexe: student.sexe || 'Garçon', + nationaliter: student.nationalite || '', + cin: student.cin || '', + date_delivrence: formatDate(student.date_delivrance), + annee_bacc: formatDate(student.annee_bacc), + serie: student.serie || '', + boursier: student.boursier || 'oui', + domaine: student.domaine || '', + contact: student.contact || '', + parcours: student.parcours || '', + existingId: student.id + }) + if (imageVisual.current && student.photos) { + imageVisual.current.style.display = 'block' + imageVisual.current.src = student.photos + } + } + const [dataToSend, setDataToSend] = useState({}) useEffect(() => { @@ -181,21 +274,63 @@ const AddStudent = () => { const handleSubmit = async (e) => { e.preventDefault() - // Handle form submission logic - const response = await window.etudiants.insertEtudiant(formData) + let etudiantId = null - console.log(response) - if (!response.success) { - setCode(422) - setOpen(true) + if (isExistingStudent && formData.existingId) { + // Etudiant existant : réinscription pour la nouvelle année scolaire + const reinscriptionData = { + etudiant_id: formData.existingId, + niveau: formData.niveau, + annee_scolaire: formData.annee_scolaire, + status: formData.status, + num_inscription: formData.num_inscription, + mention_id: formData.mention_id, + parcours: formData.parcours + } + console.log('Reinscription envoyée:', reinscriptionData) + const response = await window.etudiants.reinscribeEtudiant(reinscriptionData) + console.log('Reinscription réponse:', response) + if (!response || !response.success) { + console.error('Erreur reinscription:', response) + setCode(422) + setOpen(true) + return + } + etudiantId = formData.existingId + } else { + // Nouvel etudiant : insertion + const response = await window.etudiants.insertEtudiant(formData) + console.log('Insert:', response) + if (!response.success) { + setCode(422) + setOpen(true) + return + } + etudiantId = response.id } - if (response.success) { - imageVisual.current.style.display = 'none' - imageVisual.current.src = '' - setCode(200) - setOpen(true) + // Enregistrer le paiement si un type est selectionne + if (paiementType && numBordereau && montantPaye && etudiantId) { + const annee = scolaire.find((s) => s.code === formData.annee_scolaire) + if (annee) { + await window.etudiants.payerTranche({ + etudiant_id: etudiantId, + annee_scolaire_id: annee.id, + type: paiementType === 'complet' ? 'Tranche Complète' : 'Tranche 1', + montant: Number(montantPaye), + num_bordereau: numBordereau + }) + } } + + imageVisual.current.style.display = 'none' + imageVisual.current.src = '' + setCode(200) + setOpen(true) + setIsExistingStudent(false) + setPaiementType('') + setNumBordereau('') + setMontantPaye('') } const VisuallyHiddenInput = styled('input')({ @@ -344,11 +479,46 @@ const AddStudent = () => { p: 4 }} > -
+
Importer un fichier excel
+ + {/* Barre de recherche etudiant existant */} +
+ + {searchResults.length > 0 && ( + + {searchResults.map((s) => ( +
selectStudent(s)} + style={{ + padding: '8px 12px', cursor: 'pointer', borderBottom: '1px solid #eee', + display: 'flex', justifyContent: 'space-between', alignItems: 'center' + }} + onMouseEnter={(e) => e.target.style.background = '#fff3e0'} + onMouseLeave={(e) => e.target.style.background = 'white'} + > + {s.nom} {s.prenom} + {s.num_inscription} - {s.niveau} +
+ ))} +
+ )} +
{ )} + {/* Section Paiement - toujours visible sur page 2 */} + {page2 && ( + + + + + Paiement Ecolage + {montantConfig + ? ` - Droit total : ${Number(montantConfig.montant_total).toLocaleString('fr-FR')} Ar` + : ' - (Configurer les droits dans Admin > Config Ecolage)'} + + {montantConfig ? ( + + + { + const val = e.target.value + setPaiementType(val) + if (val === 'complet' && montantConfig) { + setMontantPaye(String(montantConfig.montant_total)) + } else { + setMontantPaye('') + } + }} + > + } + label="Pas de paiement" + /> + } + label="Tranche 1" + /> + } + label="Tranche Complete" + /> + + + {paiementType && ( + <> + + setNumBordereau(e.target.value)} + sx={{ '& .MuiOutlinedInput-root': { '&:hover fieldset': { borderColor: '#ff9800' } } }} + /> + + + setMontantPaye(e.target.value)} + InputProps={{ startAdornment: Ar }} + sx={{ '& .MuiOutlinedInput-root': { '&:hover fieldset': { borderColor: '#ff9800' } } }} + /> + + + + Droit total : {Number(montantConfig.montant_total).toLocaleString('fr-FR')} Ar
+ Reste a payer : = montantConfig.montant_total ? 'green' : 'red' }}> + {Number(Math.max(0, montantConfig.montant_total - Number(montantPaye || 0))).toLocaleString('fr-FR')} Ar + +
+
+ + )} +
+ ) : ( + + Selectionnez d'abord une mention et un niveau sur la page 1, puis configurez les droits dans Admin > Config Ecolage. + + )} +
+
+ )} + {/* Submit Button */} { - Page précédents + Page precedents diff --git a/src/renderer/src/components/AjoutTranche.jsx b/src/renderer/src/components/AjoutTranche.jsx index 5f66207..7034006 100644 --- a/src/renderer/src/components/AjoutTranche.jsx +++ b/src/renderer/src/components/AjoutTranche.jsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import React, { useState, useEffect } from 'react' import { Dialog, DialogActions, @@ -6,20 +6,39 @@ import { DialogTitle, TextField, Button, - Autocomplete, InputAdornment, Box, - Grid + Grid, + FormControl, + InputLabel, + Select, + MenuItem } from '@mui/material' -import { MdLabelImportantOutline } from 'react-icons/md' const AjoutTranche = ({ open, onClose, onSubmitSuccess, id }) => { + const [anneesList, setAnneesList] = useState([]) const [formData, setFormData] = useState({ etudiant_id: id, - tranchename: '', - montant: '' + annee_scolaire_id: '', + type: '', + montant: '', + num_bordereau: '' }) + useEffect(() => { + window.anneescolaire.getAnneeScolaire().then((response) => { + setAnneesList(response || []) + const currentYear = (response || []).find((a) => a.is_current === 1 || a.is_current === true) + if (currentYear) { + setFormData((prev) => ({ ...prev, annee_scolaire_id: currentYear.id })) + } + }) + }, []) + + useEffect(() => { + setFormData((prev) => ({ ...prev, etudiant_id: id })) + }, [id]) + const handleChange = (e) => { const { name, value } = e.target setFormData({ ...formData, [name]: value }) @@ -27,69 +46,87 @@ const AjoutTranche = ({ open, onClose, onSubmitSuccess, id }) => { const handleSubmit = async (e) => { e.preventDefault() - let response = await window.etudiants.createTranche(formData) + const response = await window.etudiants.payerTranche(formData) if (response.success) { onClose() onSubmitSuccess(true) setFormData({ etudiant_id: id, - tranchename: '', - montant: '' + annee_scolaire_id: formData.annee_scolaire_id, + type: '', + montant: '', + num_bordereau: '' }) } } return ( - -
- Ajout tranche + + + Enregistrer un paiement - + + + + Annee scolaire + + + + + + Type de paiement + + + - - - ) - }} /> - - - ) + startAdornment: Ar }} /> @@ -97,12 +134,8 @@ const AjoutTranche = ({ open, onClose, onSubmitSuccess, id }) => { - - + +
diff --git a/src/renderer/src/components/AnneeScolaire.jsx b/src/renderer/src/components/AnneeScolaire.jsx index 55e0ca9..2aa4dca 100644 --- a/src/renderer/src/components/AnneeScolaire.jsx +++ b/src/renderer/src/components/AnneeScolaire.jsx @@ -142,13 +142,12 @@ const AnneeScolaire = () => { })) const setCurrent = async (id) => { - // let response = await window.anneescolaire.setCurrent({id}); - // console.log(response); - // if (response.changes) { - // window.anneescolaire.getAnneeScolaire().then((response) => { - // setAnneeScolaire(response); - // }); - // } + const response = await window.anneescolaire.setCurrent({ id }) + if (response.success) { + window.anneescolaire.getAnneeScolaire().then((response) => { + setAnneeScolaire(response) + }) + } } const deleteButton = async (id) => { diff --git a/src/renderer/src/components/ConfigEcolage.jsx b/src/renderer/src/components/ConfigEcolage.jsx new file mode 100644 index 0000000..252c8ce --- /dev/null +++ b/src/renderer/src/components/ConfigEcolage.jsx @@ -0,0 +1,152 @@ +import React, { useState, useEffect } from 'react' +import { + Box, Button, InputAdornment, TextField, Grid, Typography, + FormControl, InputLabel, Select, MenuItem, Paper +} from '@mui/material' +import { FaTrash, FaMoneyBillWave } from 'react-icons/fa' +import { FaPenToSquare } from 'react-icons/fa6' +import classe from '../assets/AllStyleComponents.module.css' +import classeHome from '../assets/Home.module.css' + +const ConfigEcolage = () => { + const [configList, setConfigList] = useState([]) + const [mentions, setMentions] = useState([]) + const [niveaux, setNiveaux] = useState([]) + const [ecolageForm, setEcolageForm] = useState({ mention_id: '', niveau_id: '', montant_total: '' }) + + const loadConfig = () => { + window.configecolage.getAll().then((res) => setConfigList(res || [])) + } + + useEffect(() => { + loadConfig() + window.mention.getMention().then((res) => setMentions(res || [])) + window.niveaus.getNiveau().then((res) => setNiveaux(res || [])) + }, []) + + const handleChange = (e) => { + setEcolageForm({ ...ecolageForm, [e.target.name]: e.target.value }) + } + + const handleSubmit = async (e) => { + e.preventDefault() + const res = await window.configecolage.upsert(ecolageForm) + if (res.success) { + loadConfig() + setEcolageForm({ mention_id: '', niveau_id: '', montant_total: '' }) + } + } + + const handleDelete = async (id) => { + const res = await window.configecolage.delete({ id }) + if (res.success) loadConfig() + } + + const handleEdit = (c) => { + setEcolageForm({ mention_id: c.mention_id, niveau_id: c.niveau_id, montant_total: c.montant_total }) + } + + return ( +
+
+
+
+

+ + Configuration Ecolage +

+
+
+
+ +
+ + + Definir le montant des droits par mention et niveau + +
+ + + + Mention + + + + + + Niveau + + + + + Ar }} + /> + + + + + +
+ + + + + + + + + + + + + + {configList.length === 0 ? ( + + ) : ( + configList.map((c) => ( + + + + + + + + + )) + )} + +
MentionNiveauMontant totalTranche 1Tranche 2Action
Aucune configuration
{c.mention_nom}{c.niveau_nom}{Number(c.montant_total).toLocaleString('fr-FR')} Ar{Number(c.montant_total / 2).toLocaleString('fr-FR')} Ar{Number(c.montant_total / 2).toLocaleString('fr-FR')} Ar + + +
+
+
+
+ ) +} + +export default ConfigEcolage diff --git a/src/renderer/src/components/Ecolage.jsx b/src/renderer/src/components/Ecolage.jsx new file mode 100644 index 0000000..7243e89 --- /dev/null +++ b/src/renderer/src/components/Ecolage.jsx @@ -0,0 +1,374 @@ +import React, { useEffect, useState } from 'react' +import { Link } from 'react-router-dom' +import classe from '../assets/AllStyleComponents.module.css' +import classeHome from '../assets/Home.module.css' +import Paper from '@mui/material/Paper' +import { Button, InputAdornment } from '@mui/material' +import { DataGrid, GridToolbar } from '@mui/x-data-grid' +import { frFR } from '@mui/x-data-grid/locales' +import dayjs from 'dayjs' +import { createTheme, ThemeProvider } from '@mui/material/styles' +import InputLabel from '@mui/material/InputLabel' +import MenuItem from '@mui/material/MenuItem' +import FormControl from '@mui/material/FormControl' +import Select from '@mui/material/Select' +import { Tooltip } from 'react-tooltip' +import { FaGraduationCap, FaMoneyBillWave } from 'react-icons/fa' +import { MdPayment } from 'react-icons/md' +import { useLocation } from 'react-router-dom' + +const Ecolage = () => { + const theme = createTheme({ + components: { + MuiIconButton: { + styleOverrides: { root: { color: 'gray' } } + }, + MuiButton: { + styleOverrides: { root: { color: '#121212' } } + } + } + }) + + const [status, setStatus] = useState([]) + const [filterModel, setFilterModel] = useState({ items: [] }) + const [sortModel, setSortModel] = useState([]) + const location = useLocation() + + const savedFilter = localStorage.getItem('ecolageNiveau') || '' + const savedAnnee = localStorage.getItem('ecolageAnnee') || '' + const initialFilter = location.state?.selectedNiveau || savedFilter + const initialAnnee = location.state?.selectedAnnee || savedAnnee + + const [selectedNiveau, setSelectedNiveau] = useState(initialFilter) + const [selectedAnnee, setSelectedAnnee] = useState(initialAnnee) + const [anneesList, setAnneesList] = useState([]) + const [allEtudiants, setAllEtudiants] = useState([]) + const [etudiants, setEtudiants] = useState([]) + const [niveaus, setNiveau] = useState([]) + + const [paginationModel, setPaginationModel] = useState({ page: 0, pageSize: 20 }) + const [pageSizeOptions, setPageSizeOptions] = useState([20, 40, 60]) + + useEffect(() => { + if (initialFilter) setSelectedNiveau(initialFilter) + if (initialAnnee) setSelectedAnnee(initialAnnee) + }, []) + + // Load academic years + useEffect(() => { + window.anneescolaire.getAnneeScolaire().then((response) => { + setAnneesList(response || []) + const currentYear = (response || []).find((a) => a.is_current === 1 || a.is_current === true) + if (currentYear && !savedAnnee) { + setSelectedAnnee(currentYear.code) + localStorage.setItem('ecolageAnnee', currentYear.code) + } + }) + }, []) + + // Load students when year changes + useEffect(() => { + const loadEtudiants = async () => { + if (selectedAnnee && selectedAnnee !== '') { + const response = await window.etudiants.getAllEtudiantsForEcolage({ + annee_scolaire: selectedAnnee + }) + setAllEtudiants(response || []) + } else { + setAllEtudiants([]) + } + } + loadEtudiants() + }, [selectedAnnee]) + + // Filter by niveau + useEffect(() => { + if (selectedNiveau && selectedNiveau !== '') { + setEtudiants(allEtudiants.filter((e) => e.niveau === selectedNiveau)) + } else { + setEtudiants(allEtudiants) + } + }, [allEtudiants, selectedNiveau]) + + // Load saved datagrid state + useEffect(() => { + const savedFilters = localStorage.getItem('ecolageGridFilters') + const savedSort = localStorage.getItem('ecolageGridSort') + const savedPagination = localStorage.getItem('ecolageGridPagination') + + if (savedFilters) setFilterModel(JSON.parse(savedFilters)) + if (savedSort) setSortModel(JSON.parse(savedSort)) + if (savedPagination) setPaginationModel(JSON.parse(savedPagination)) + }, []) + + useEffect(() => { + window.niveaus.getNiveau().then((response) => setNiveau(response)) + window.statuss.getStatus().then((response) => setStatus(response)) + }, []) + + function comparestatut(statutID) { + let statusText + status.map((statu) => { + if (statutID == statu.id) statusText = statu.nom + }) + return statusText + } + + const handlePaginationModelChange = (newModel) => { + setPaginationModel(newModel) + const maxOption = Math.max(...pageSizeOptions) + if (newModel.pageSize === maxOption) { + setPageSizeOptions((prev) => [...prev, maxOption + 20]) + } + } + + const columns = [ + { field: 'nom', headerName: 'Nom', width: 180 }, + { field: 'prenom', headerName: 'Prenom', width: 180 }, + { field: 'num_inscription', headerName: "N° d'inscription", width: 160 }, + { field: 'niveau', headerName: 'Niveau', width: 80 }, + { field: 'nomMention', headerName: 'Mention', width: 180 }, + { field: 'annee_scolaire', headerName: 'Annee universitaire', width: 130 }, + { field: 'status', headerName: 'Status', width: 140 }, + { field: 'contact', headerName: 'Contact', width: 150 }, + { + field: 'tranches_payees', + headerName: 'Tranches payees', + width: 130, + renderCell: (params) => ( + 0 ? 'green' : 'red', + fontWeight: 'bold' + }} + > + {params.value > 0 ? `${params.value} payee(s)` : 'Aucune'} + + ) + }, + { + field: 'photos', + headerName: 'Image', + width: 80, + renderCell: (params) => ( + pdp + ) + }, + { + field: 'action', + headerName: 'Action', + width: 180, + renderCell: (params) => ( +
+ + + + Gerer le paiement + + +
+ ) + } + ] + + const dataRow = etudiants.map((etudiant) => ({ + id: etudiant.id, + nom: etudiant.nom, + prenom: etudiant.prenom, + niveau: etudiant.niveau, + annee_scolaire: etudiant.annee_scolaire, + status: comparestatut(etudiant.status), + num_inscription: etudiant.num_inscription, + nomMention: etudiant.nomMention, + contact: etudiant.contact, + photos: etudiant.photos, + tranches_payees: etudiant.tranches_payees || 0, + action: etudiant.id + })) + + const FilterData = (e) => { + const niveau = e.target.value + setSelectedNiveau(niveau) + localStorage.setItem('ecolageNiveau', niveau) + setPaginationModel((prev) => ({ ...prev, page: 0 })) + } + + const FilterByAnnee = (e) => { + const annee = e.target.value + setSelectedAnnee(annee) + localStorage.setItem('ecolageAnnee', annee) + setSelectedNiveau('') + localStorage.setItem('ecolageNiveau', '') + setPaginationModel((prev) => ({ ...prev, page: 0 })) + } + + return ( +
+ +
+
+
+

+ + Gestion Ecolage +

+
+
+ {/* Filters */} +
+
+ + + Annee scolaire + + + + + + + Niveau + + + +
+
+
+ +
+
+ + +
+ { + setFilterModel(newModel) + localStorage.setItem('ecolageGridFilters', JSON.stringify(newModel)) + }} + sortModel={sortModel} + onSortModelChange={(newModel) => { + setSortModel(newModel) + localStorage.setItem('ecolageGridSort', JSON.stringify(newModel)) + }} + paginationModel={paginationModel} + onPaginationModelChange={(newModel) => { + handlePaginationModelChange(newModel) + localStorage.setItem('ecolageGridPagination', JSON.stringify(newModel)) + }} + pageSizeOptions={pageSizeOptions} + sx={{ + border: 0, + width: 'auto', + height: '50%', + minHeight: 400, + display: 'flex', + justifyContent: 'center' + }} + slots={{ toolbar: GridToolbar }} + localeText={frFR.components.MuiDataGrid.defaultProps.localeText} + /> +
+
+
+
+
+
+ ) +} + +export default Ecolage diff --git a/src/renderer/src/components/Home.jsx b/src/renderer/src/components/Home.jsx index f14cc5d..bbfa41a 100644 --- a/src/renderer/src/components/Home.jsx +++ b/src/renderer/src/components/Home.jsx @@ -29,12 +29,15 @@ const Home = () => { const currentYear = dayjs().year() useEffect(() => { - // Fetch data and update state - window.etudiants.getDataToDashboards().then((response) => { - setEtudiants(response.etudiants) - setOriginalEtudiants(response.etudiants) - setNiveau(response.niveau) - setAnnee_scolaire(response.anne_scolaire) + window.niveaus.getNiveau().then((response) => { + setNiveau(response || []) + }) + window.anneescolaire.getAnneeScolaire().then((response) => { + setAnnee_scolaire(response || []) + }) + window.etudiants.getEtudiants().then((response) => { + setEtudiants(response || []) + setOriginalEtudiants(response || []) }) }, []) @@ -60,14 +63,13 @@ const Home = () => { // Find the maximum value using Math.max const maxStudentCount = Math.max(...studentCounts) - const FilterAnneeScolaire = (e) => { - let annee_scolaire = e.target.value - const filteredEtudiants = originalEtudiants.filter( - (etudiant) => etudiant.annee_scolaire === annee_scolaire - ) - setEtudiants(filteredEtudiants) - if (annee_scolaire == 'general') { + const FilterAnneeScolaire = async (e) => { + const annee = e.target.value + if (annee === 'general') { setEtudiants(originalEtudiants) + } else { + const filtered = await window.etudiants.getEtudiantsByAnnee(annee) + setEtudiants(filtered || []) } } // end filter all data @@ -173,9 +175,9 @@ const Home = () => { Géneral - {annee_scolaire.map((niveau) => ( - - {niveau.annee_scolaire} + {annee_scolaire.map((a) => ( + + {a.code} ))} diff --git a/src/renderer/src/components/Noteclasse.jsx b/src/renderer/src/components/Noteclasse.jsx index 491600a..4541518 100644 --- a/src/renderer/src/components/Noteclasse.jsx +++ b/src/renderer/src/components/Noteclasse.jsx @@ -12,153 +12,168 @@ import { Button, Modal, Box, Menu, MenuItem } from '@mui/material' import { Tooltip } from 'react-tooltip' import ReleverNotes from './ReleverNotes' import { FaDownload } from 'react-icons/fa' +import getSemestre from './function/GetSemestre' const Noteclasse = () => { const { niveau, scolaire } = useParams() const [etudiants, setEtudiants] = useState([]) const [mention, setMention] = useState([]) - const [session, setSession] = useState([]) + const [moyennesCalculees, setMoyennesCalculees] = useState([]) - const formData = { - niveau, - scolaire - } + const formData = { niveau, scolaire } useEffect(() => { - window.notes.getMoyenne(formData).then((response) => { - setEtudiants(response) - }) - window.noteRepech.getMoyenneRepech(formData).then((response) => { - setSession(response) - }) - window.mention.getMention().then((response) => { - setMention(response) - }) - }, []) - - let dataToMap = [] - - function returnmention(id) { - let mentions - for (let index = 0; index < mention.length; index++) { - if (mention[index].id == id) { - mentions = mention[index].nom + const fetchData = async () => { + try { + // Récupérer la liste des étudiants + const etudiantsData = await window.notes.getMoyenne(formData) + setEtudiants(etudiantsData) + + // Récupérer les mentions + const mentionData = await window.mention.getMention() + setMention(mentionData) + + // Pour chaque étudiant, récupérer TOUTES ses notes avec noteRelerer + const moyennes = [] + + for (let i = 0; i < etudiantsData.length; i++) { + if (etudiantsData[i] && etudiantsData[i][0]) { + const etudiantId = etudiantsData[i][0].etudiant_id + const etudiantInfo = etudiantsData[i][0] + + try { + // Utiliser noteRelerer pour avoir TOUTES les matières + const notesData = await window.notes.noteRelerer({ + id: etudiantId, + anneescolaire: scolaire, + niveau: niveau + }) + + if (notesData && notesData.noteNormal && notesData.noteRepech && notesData.semestre) { + // Fusionner les notes normales avec les semestres + const updatedMatieres = notesData.noteNormal.map((matiere) => { + const semesters = getSemestre(matiere.etudiant_niveau) + const matchedSemestre = notesData.semestre.find( + (sem) => + sem.matiere_id === matiere.matiere_id && + sem.mention_id === matiere.mention_id && + (sem.nom === semesters[0] || sem.nom === semesters[1]) + ) + + return { + ...matiere, + semestre: matchedSemestre ? matchedSemestre.nom : null + } + }) + + const updatedMatieresRepech = notesData.noteRepech.map((matiere) => { + const semesters = getSemestre(matiere.etudiant_niveau) + const matchedSemestre = notesData.semestre.find( + (sem) => + sem.matiere_id === matiere.matiere_id && + sem.mention_id === matiere.mention_id && + (sem.nom === semesters[0] || sem.nom === semesters[1]) + ) + + return { + ...matiere, + semestre: matchedSemestre ? matchedSemestre.nom : null + } + }) + + // Fusionner notes normales et rattrapage + updatedMatieres.forEach((item1) => { + let matchingItem = updatedMatieresRepech.find( + (item2) => item2.matiere_id === item1.matiere_id + ) + item1.noterepech = matchingItem ? matchingItem.note : null + }) + + // Calculer la moyenne pondérée + const moyenne = calculerMoyennePonderee(updatedMatieres) + + moyennes.push({ + id: etudiantId, + nom: etudiantInfo.nom, + prenom: etudiantInfo.prenom, + photos: etudiantInfo.photos, + mention: etudiantInfo.mention_id, + anneescolaire: etudiantInfo.annee_scolaire, + moyenne: moyenne.toFixed(2), + aRattrapage: updatedMatieres.some(m => m.noterepech !== null && Number(m.noterepech) > 0) + }) + } + } catch (error) { + console.error(`Erreur pour l'étudiant ${etudiantId}:`, error) + } + } + } + + setMoyennesCalculees(moyennes) + console.log('=== MOYENNES CALCULÉES ===') + console.log('Nombre d\'étudiants:', moyennes.length) + if (moyennes.length > 0) { + console.log('Premier étudiant:', moyennes[0]) + } + } catch (error) { + console.error('Erreur lors du chargement des données:', error) } } - return mentions - } - - function checkNull(params) { - console.log(params); - if (params == null || params == undefined) { - return null - } - return params - } - // MODIFICATION: Nouvelle fonction pour calculer la moyenne avec rattrapage - function compareSessionNotesForAverage(session1, session2) { - // Si il y a une session de rattrapage, utiliser la meilleure note - if (session2) { - return Math.max(session1, session2.note) - } - // Sinon utiliser la note normale - return session1 - } + fetchData() + }, [niveau, scolaire]) - function compareSessionNotes(session1, session2) { - let notes - if (session2) { - if (session1 < session2.note) { - notes = session2.note - } else { - notes = session1 - } - } else { - notes = session1 - } - return notes + function returnmention(id) { + const found = mention.find((m) => m.id === id) + return found ? found.nom : '' } - for (let index = 0; index < etudiants.length; index++) { - let total = 0 - let note = 0 - let totalCredit = 0 - - // Create a new object for each student - let modelJson = { - id: '', - nom: '', - prenom: '', - photos: '', - moyenne: '', - mention: '', - anneescolaire: '' - } + // ======================================== + // FONCTION CENTRALISÉE POUR CALCUL DE MOYENNE PONDÉRÉE + // ======================================== + const calculerMoyennePonderee = (matieres) => { + let totalNotesPonderees = 0 + let totalCredits = 0 - for (let j = 0; j < etudiants[index].length; j++) { - modelJson.id = etudiants[index][j].etudiant_id - modelJson.nom = etudiants[index][j].nom - modelJson.prenom = etudiants[index][j].prenom - modelJson.photos = etudiants[index][j].photos - modelJson.mention = etudiants[index][j].mention_id - modelJson.anneescolaire = etudiants[index][j].annee_scolaire - - // MODIFICATION: Utiliser la meilleure note (rattrapage si existe) pour la moyenne générale - if (session[index]) { - note += - compareSessionNotesForAverage(etudiants[index][j].note, checkNull(session[index][j])) * - etudiants[index][j].credit + matieres.forEach((matiere) => { + let noteFinale + + // IMPORTANT: Traiter 0 comme null (pas de rattrapage) + const noteRepechValide = matiere.noterepech !== null + && matiere.noterepech !== undefined + && Number(matiere.noterepech) > 0 + + // TOUJOURS prendre la meilleure note si rattrapage VALIDE existe + if (noteRepechValide) { + noteFinale = Math.max(Number(matiere.note), Number(matiere.noterepech)) } else { - note += etudiants[index][j].note * etudiants[index][j].credit + noteFinale = Number(matiere.note) } - totalCredit += etudiants[index][j].credit - } - total = note / totalCredit - modelJson.moyenne = total.toFixed(2) + if (noteFinale != null && !isNaN(noteFinale)) { + totalNotesPonderees += noteFinale * Number(matiere.credit) + totalCredits += Number(matiere.credit) + } + }) - // Add the new object to the array - dataToMap.push(modelJson) + return totalCredits > 0 ? totalNotesPonderees / totalCredits : 0 } function checkNumberSession(id) { - let sessionNumber = 1 - for (let index = 0; index < session.length; index++) { - for (let j = 0; j < session[index].length; j++) { - if (session[index][j].etudiant_id == id) { - sessionNumber = 2 - break - } - } - if (sessionNumber === 2) break - } - return sessionNumber + const etudiant = moyennesCalculees.find(e => e.id === id) + return etudiant && etudiant.aRattrapage ? 2 : 1 } const theme = createTheme({ components: { - MuiIconButton: { - styleOverrides: { - root: { - color: 'gray' // Change the color of toolbar icons - } - } - }, - MuiButton: { - styleOverrides: { - root: { - color: '#121212' // Change the color of toolbar icons - } - } - } + MuiIconButton: { styleOverrides: { root: { color: 'gray' } } }, + MuiButton: { styleOverrides: { root: { color: '#121212' } } } } }) const paginationModel = { page: 0, pageSize: 5 } - // États pour le menu déroulant const [anchorEl, setAnchorEl] = useState(null) const [selectedStudentId, setSelectedStudentId] = useState(null) const open = Boolean(anchorEl) @@ -167,17 +182,92 @@ const Noteclasse = () => { setAnchorEl(event.currentTarget) setSelectedStudentId(studentId) } - const handleMenuClose = () => { setAnchorEl(null) setSelectedStudentId(null) } - const handleSessionTypeSelect = (sessionType) => { sendData(selectedStudentId, sessionType) handleMenuClose() } + const [openCard, setOpenCart] = useState(false) + const [downloadTrigger, setDownloadTrigger] = useState(false) + const [form, setForm] = useState({ id: '', niveau: '', anneescolaire: '', sessionType: 'ensemble' }) + const [selectedId, setSelectedId] = useState(null) + + const sendData = (id, sessionType = 'ensemble') => { + setSelectedId(id) + setForm((prev) => ({ ...prev, sessionType })) + setOpenCart(true) + } + + useEffect(() => { + if (selectedId !== null) { + const foundData = moyennesCalculees.find((item) => item.id === selectedId) + if (foundData) { + setForm((prev) => ({ + ...prev, + id: foundData.id, + anneescolaire: foundData.anneescolaire, + niveau + })) + } + } + }, [openCard, selectedId]) + + const handleCloseCart = () => { + setDownloadTrigger(false) + setOpenCart(false) + } + + const modalReleverNotes = () => ( + + + + + + + + ) + const columns = [ { field: 'nom', headerName: 'Nom', width: 170 }, { field: 'prenom', headerName: 'Prénom', width: 160 }, @@ -190,7 +280,7 @@ const Noteclasse = () => { width: 100, renderCell: (params) => ( {'image @@ -202,17 +292,17 @@ const Noteclasse = () => { flex: 1, renderCell: (params) => (
- - Imprimer un relevé de notes @@ -222,7 +312,7 @@ const Noteclasse = () => { } ] - const dataTable = dataToMap.map((data) => ({ + const dataTable = moyennesCalculees.map((data) => ({ id: data.id, nom: data.nom, prenom: data.prenom, @@ -233,139 +323,22 @@ const Noteclasse = () => { action: data.id })) - const [openCard, setOpenCart] = useState(false) - const [bolll, setBolll] = useState(false) - const [form, setForm] = useState({ - id: '', - niveau: '', - anneescolaire: '', - sessionType: 'ensemble' // Par défaut - }) - const [selectedId, setSelectedId] = useState(null) // Store id dynamically - - const sendData = (id, sessionType = 'ensemble') => { - setSelectedId(id) - setForm(prevForm => ({ - ...prevForm, - sessionType: sessionType - })) - setOpenCart(true) - } - - useEffect(() => { - if (selectedId !== null) { - const foundData = dataToMap.find((item) => item.id === selectedId) - if (foundData) { - setForm((prevForm) => ({ - ...prevForm, - id: foundData.id, - anneescolaire: foundData.anneescolaire, - niveau: niveau - })) // Update form with the found object - } - } - }, [openCard, selectedId]) - - console.log(form) - - const downloadButton = () => { - setBolll(true) - } - - /** - * function to close modal - */ - const handleCloseCart = () => { - setBolll(false) - setOpenCart(false) - } - - const modalReleverNotes = () => { - return ( - - - - - - - - ) - } - return (
{modalReleverNotes()} - - {/* Menu pour sélectionner le type de session */} - - handleSessionTypeSelect('normale')}> - Session Normale - - handleSessionTypeSelect('ensemble')}> - Session Rattrapage - + + + handleSessionTypeSelect('normale')}>Session Normale + handleSessionTypeSelect('ensemble')}>Session Rattrapage
-

- Notes des {niveau} en {scolaire} -

+

Notes des {niveau} en {scolaire}

- + window.history.back()}>
-
-
- - -
- -
-
-
-
+
+ + + + +
) diff --git a/src/renderer/src/components/ReleverNotes.jsx b/src/renderer/src/components/ReleverNotes.jsx index 2d0757e..aa95c43 100644 --- a/src/renderer/src/components/ReleverNotes.jsx +++ b/src/renderer/src/components/ReleverNotes.jsx @@ -14,55 +14,46 @@ const ReleverNotes = ({ id, anneescolaire, niveau, sessionType = 'ensemble', ref const [etudiant, setEtudiant] = useState([]) const [matieres, setMatieres] = useState([]) const [notes, setNotes] = useState([]) - - // Fonction pour vérifier si les crédits doivent être affichés - const shouldShowCredits = () => { - return niveau !== 'L1' && niveau !== 'L2' - } + const [noteSysteme, setNoteSysteme] = useState(null) const handleDownloadPDF = async () => { const input = Telever.current - - // Set a high scale for better quality const scale = 3 html2Canvas(input, { - scale, // Increase resolution - useCORS: true, // Handle cross-origin images + scale, + useCORS: true, allowTaint: true }).then((canvas) => { const imgData = canvas.toDataURL('image/png') - - // Create a PDF with dimensions matching the captured content const pdf = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' }) - const imgWidth = 210 // A4 width in mm - const pageHeight = 297 // A4 height in mm - const imgHeight = (canvas.height * imgWidth) / canvas.width - - let position = 0 - - pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight, '', 'FAST') - - // Handle multi-page case - while (position + imgHeight >= pageHeight) { - position -= pageHeight - pdf.addPage() - pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight, '', 'FAST') + const pageWidth = 210 + const pageHeight = 297 + const margin = 5 + const printWidth = pageWidth - margin * 2 + const imgHeight = (canvas.height * printWidth) / canvas.width + + if (imgHeight > pageHeight - margin * 2) { + const ratio = (pageHeight - margin * 2) / imgHeight + const adjustedWidth = printWidth * ratio + const adjustedHeight = pageHeight - margin * 2 + const xOffset = (pageWidth - adjustedWidth) / 2 + pdf.addImage(imgData, 'PNG', xOffset, margin, adjustedWidth, adjustedHeight, '', 'FAST') + } else { + pdf.addImage(imgData, 'PNG', margin, margin, printWidth, imgHeight, '', 'FAST') } - pdf.save('document.pdf') + pdf.save('releve_de_notes.pdf') }) } useEffect(() => { if (!id) { - // id doesn't exist, you might want to retry, or do nothing - // For example, refetch later or show an error return } window.etudiants.getSingle({ id }).then((response) => { @@ -76,6 +67,10 @@ const ReleverNotes = ({ id, anneescolaire, niveau, sessionType = 'ensemble', ref window.notes.noteRelerer({ id, anneescolaire, niveau }).then((response) => { setNotes(response) }) + + window.notesysteme.getSysteme().then((response) => { + if (response) setNoteSysteme(response) + }) }, [id]) const Telever = useRef() @@ -90,42 +85,35 @@ const ReleverNotes = ({ id, anneescolaire, niveau, sessionType = 'ensemble', ref const [matiereWithSemestreRepech, setMatiereWithSemestreRepech] = useState([]) useEffect(() => { - if (!notes.noteNormal || !notes.semestre || !notes.noteRepech) return // Ensure data exists + if (!notes.noteNormal || !notes.semestre || !notes.noteRepech) return const updatedMatieres = notes.noteNormal.map((matiere) => { - // Get the semesters based on the student's niveau const semesters = getSemestre(matiere.etudiant_niveau) - - // Find the matched semestre based on the conditions const matchedSemestre = notes.semestre.find( (sem) => sem.matiere_id === matiere.matiere_id && sem.mention_id === matiere.mention_id && - (sem.nom === semesters[0] || sem.nom === semesters[1]) // Check if the semester matches + (sem.nom === semesters[0] || sem.nom === semesters[1]) ) return { ...matiere, - semestre: matchedSemestre ? matchedSemestre.nom : null // Add 'semestre' or set null if no match + semestre: matchedSemestre ? matchedSemestre.nom : null } }) const updatedMatieresRepech = notes.noteRepech.map((matiere) => { - // Get the semesters based on the student's niveau const semesters = getSemestre(matiere.etudiant_niveau) - - // Find the matched semestre based on the conditions const matchedSemestre = notes.semestre.find( (sem) => sem.matiere_id === matiere.matiere_id && sem.mention_id === matiere.mention_id && - (sem.nom === semesters[0] || sem.nom === semesters[1]) // Check if the semester matches + (sem.nom === semesters[0] || sem.nom === semesters[1]) ) - // Return the updated matiere with the matched semestre or null if no match return { ...matiere, - semestre: matchedSemestre ? matchedSemestre.nom : null // Add 'semestre' or set null if no match + semestre: matchedSemestre ? matchedSemestre.nom : null } }) @@ -135,62 +123,85 @@ const ReleverNotes = ({ id, anneescolaire, niveau, sessionType = 'ensemble', ref function compareMention(mentionID) { let statusText - matieres.map((statu) => { if (mentionID == statu.id) { statusText = statu.nom } }) - return statusText ? statusText.charAt(0).toUpperCase() + statusText.slice(1) : statusText } - // data are finaly get and ready for the traitement below - - // Merging the arrays based on matiere_id + // Fusion des notes normales et de rattrapage matiereWithSemestre.forEach((item1) => { - // Find the corresponding item in array2 based on matiere_id let matchingItem = matiereWithSemestreRepech.find( (item2) => item2.matiere_id === item1.matiere_id ) - - // If there's a match, add noterepech from array2, otherwise use the note from array1 - item1.noterepech = matchingItem ? matchingItem.note : item1.note + item1.noterepech = matchingItem ? matchingItem.note : null }) - // step 1 group all by semestre const groupedDataBySemestre = matiereWithSemestre.reduce((acc, matiere) => { const { semestre } = matiere - if (!acc[semestre]) { acc[semestre] = [] } - acc[semestre].push(matiere) - return acc }, {}) - // MODIFICATION: Fonction compareMoyenne mise à jour - const compareMoyenne = (normal, rattrapage, sessionType) => { - if (sessionType === 'normale') { - // Pour session normale: toujours évaluer selon la note normale uniquement - return Number(normal) >= 10 ? 'Admis' : 'Ajourné' - } else if (sessionType === 'rattrapage') { - // Pour session rattrapage: évaluer selon la note de rattrapage - return Number(rattrapage) >= 10 ? 'Admis' : 'Ajourné' - } else { - // Pour session ensemble: prendre la meilleure des deux notes - const bestNote = Math.max(Number(normal), Number(rattrapage)) - return bestNote >= 10 ? 'Admis' : 'Ajourné' + // ======================================== + // FONCTION CENTRALISÉE POUR CALCUL DE MOYENNE PONDÉRÉE + // ======================================== + const calculerMoyennePonderee = (matieres, utiliserRattrapage = true) => { + let totalNotesPonderees = 0 + let totalCredits = 0 + + matieres.forEach((matiere) => { + let noteFinale + + // IMPORTANT: Traiter 0 comme null (pas de rattrapage) + const noteRepechValide = matiere.noterepech !== null + && matiere.noterepech !== undefined + && Number(matiere.noterepech) > 0 + + // Si on doit utiliser le rattrapage ET qu'il existe, prendre la meilleure note + if (utiliserRattrapage && noteRepechValide) { + noteFinale = Math.max(Number(matiere.note), Number(matiere.noterepech)) + } else { + // Sinon, prendre uniquement la note normale + noteFinale = Number(matiere.note) + } + + if (noteFinale != null && !isNaN(noteFinale)) { + totalNotesPonderees += noteFinale * Number(matiere.credit) + totalCredits += Number(matiere.credit) + } + }) + + return totalCredits > 0 ? totalNotesPonderees / totalCredits : 0 + } + + // Fonction pour obtenir la meilleure note entre normale et rattrapage + const getBestNote = (noteNormale, noteRattrapage) => { + // Traiter 0 comme null (pas de rattrapage valide) + if (noteRattrapage === null || noteRattrapage === undefined || Number(noteRattrapage) <= 0) { + return noteNormale } + return Math.max(Number(noteNormale), Number(noteRattrapage)) } + // Fonction pour déterminer Admis/Ajourné +// Fonction pour déterminer Admis/Ajourné +const compareMoyenne = (matieres, sessionType) => { + const utiliserRattrapage = sessionType === 'rattrapage' || sessionType === 'ensemble' + const moyenne = calculerMoyennePonderee(matieres, utiliserRattrapage) + if (!noteSysteme) return '' + return moyenne >= noteSysteme.admis ? 'Admis' : 'Ajourné' +} + const TbodyContent = () => { return ( <> {Object.entries(groupedDataBySemestre).map(([semestre, matieres]) => { - // Group by unite_enseignement inside each semestre const groupedByUnite = matieres.reduce((acc, matiere) => { if (!acc[matiere.ue]) { acc[matiere.ue] = [] @@ -205,7 +216,6 @@ const ReleverNotes = ({ id, anneescolaire, niveau, sessionType = 'ensemble', ref <> {matieres.map((matiere, matiereIndex) => ( - {/* Display 'semestre' only for the first row of the first unite_enseignement */} {uniteIndex === 0 && matiereIndex === 0 && ( )} - {/* Display 'unite_enseignement' only for the first row of each group */} {matiereIndex === 0 && ( )} - {/* Matiere Data */} - + {matiere.nom} - - {/* Affichage conditionnel des colonnes selon le type de session */} + + {/* SECTION NORMALE */} {sessionType !== 'rattrapage' && ( <> @@ -254,7 +262,6 @@ const ReleverNotes = ({ id, anneescolaire, niveau, sessionType = 'ensemble', ref {matiere.note} - {/* Moyenne UE pour session normale */} {matiereIndex === 0 && ( - {( - matieres.reduce((total, matiere) => total + matiere.note, 0) / - matieres.length - ).toFixed(2)} + {calculerMoyennePonderee(matieres, false).toFixed(2)} )} )} - + + {/* SECTION RATTRAPAGE */} {sessionType !== 'normale' && ( <> - {matiere.credit} - - - {matiere.noterepech} + {matiere.noterepech !== null && + matiere.noterepech !== undefined && + Number(matiere.noterepech) > 0 + ? matiere.noterepech + : matiere.note} - {/* Moyenne UE pour session rattrapage */} {matiereIndex === 0 && ( - {( - matieres.reduce((total, matiere) => total + matiere.noterepech, 0) / - matieres.length - ).toFixed(2)} + {calculerMoyennePonderee(matieres, true).toFixed(2)} )} )} - {/* Display the comparison value only once */} + {/* OBSERVATION */} {matiereIndex === 0 && ( - {compareMoyenne( - ( - matieres.reduce((total, matiere) => total + matiere.note, 0) / - matieres.length - ).toFixed(2), - ( - matieres.reduce((total, matiere) => total + matiere.noterepech, 0) / - matieres.length - ).toFixed(2), - sessionType - )} + {compareMoyenne(matieres, sessionType)} )} ))} - {/* Add Total Row for 'unite_enseignement' */} - + {/* Total Row */} + Total de Credit - + {sessionType !== 'rattrapage' && ( <> - {matieres.reduce((total, matiere) => total + matiere.credit, 0)} + {matieres.reduce((total, matiere) => total + Number(matiere.credit), 0)} )} - + {sessionType !== 'normale' && ( <> - {matieres.reduce((total, matiere) => total + matiere.credit, 0)} )} - + { - // Calculer la moyenne pondérée par les crédits selon le type de session - let totalNotesPonderees = 0 - let totalCredits = 0 - - if (sessionType === 'normale') { - // Utiliser uniquement les notes normales - matiereWithSemestre.forEach((matiere) => { - if (matiere.note != null && !isNaN(matiere.note)) { - totalNotesPonderees += matiere.note * matiere.credit - totalCredits += matiere.credit - } - }) - } else if (sessionType === 'rattrapage') { - // Utiliser uniquement les notes de rattrapage - matiereWithSemestre.forEach((matiere) => { - if (matiere.noterepech != null && !isNaN(matiere.noterepech)) { - totalNotesPonderees += matiere.noterepech * matiere.credit - totalCredits += matiere.credit - } - }) - } else { - // Pour 'ensemble', prendre la meilleure note entre normale et rattrapage pour chaque matière - matiereWithSemestre.forEach((matiere) => { - const noteNormale = matiere.note != null && !isNaN(matiere.note) ? matiere.note : 0 - const noteRattrapage = matiere.noterepech != null && !isNaN(matiere.noterepech) ? matiere.noterepech : 0 - const bestNote = Math.max(noteNormale, noteRattrapage) - - if (bestNote > 0) { - totalNotesPonderees += bestNote * matiere.credit - totalCredits += matiere.credit - } - }) + // ======================================== + // CALCUL DE LA MOYENNE GÉNÉRALE + // TOUJOURS avec les meilleures notes (rattrapage si existe) + // ======================================== + const totalNotes = () => { + // Session NORMALE → uniquement note normale + if (sessionType === 'normale') { + return calculerMoyennePonderee(matiereWithSemestre, false) + } + + // Session RATTRAPAGE ou ENSEMBLE → meilleure note + return calculerMoyennePonderee(matiereWithSemestre, true) } - return totalCredits > 0 ? totalNotesPonderees / totalCredits : 0 -} - const [note, setNote] = useState(0) useEffect(() => { - setNote(totalNotes()) - }, [TbodyContent, sessionType]) + const moyenne = totalNotes() + console.log('=== DEBUG RELEVER NOTES ===') + console.log('SessionType:', sessionType) + console.log('Nombre de matières:', matiereWithSemestre.length) + + // Log détaillé de toutes les matières + if (matiereWithSemestre.length > 0) { + console.log('Première matière:', matiereWithSemestre[0]) + console.log('Détail des matières:') + + // Calcul pour session NORMALE (sans rattrapage) + let totalPondereNormal = 0 + let totalCreditNormal = 0 + + // Calcul pour session RATTRAPAGE (avec meilleures notes) + let totalPondereRattrapage = 0 + let totalCreditRattrapage = 0 + + matiereWithSemestre.forEach((mat, idx) => { + const noteRepechValide = mat.noterepech !== null + && mat.noterepech !== undefined + && Number(mat.noterepech) > 0 + const meilleureNote = noteRepechValide + ? Math.max(Number(mat.note), Number(mat.noterepech)) + : Number(mat.note) + + console.log(`Matière ${idx + 1}:`, { + nom: mat.nom, + note: mat.note, + noterepech: mat.noterepech, + credit: mat.credit, + meilleure: meilleureNote + }) + + // Pour session normale : uniquement note normale + totalPondereNormal += Number(mat.note) * Number(mat.credit) + totalCreditNormal += Number(mat.credit) + + // Pour session rattrapage : meilleure note + totalPondereRattrapage += meilleureNote * Number(mat.credit) + totalCreditRattrapage += Number(mat.credit) + }) + + console.log('--- SESSION NORMALE ---') + console.log('Total notes pondérées:', totalPondereNormal) + console.log('Total crédits:', totalCreditNormal) + console.log('Moyenne session normale:', (totalPondereNormal / totalCreditNormal).toFixed(2)) + + console.log('--- SESSION RATTRAPAGE ---') + console.log('Total notes pondérées:', totalPondereRattrapage) + console.log('Total crédits:', totalCreditRattrapage) + console.log('Moyenne session rattrapage:', (totalPondereRattrapage / totalCreditRattrapage).toFixed(2)) + } + + console.log('Moyenne générale (toujours avec rattrapage):', moyenne) + setNote(moyenne) + }, [matiereWithSemestre, sessionType]) return (
@@ -468,53 +485,69 @@ const totalNotes = () => {
-
-
- logo gauche - -
-
- REPOBLIKAN'I MADAGASIKARA -
-

Fitiavana – Tanindrazana – Fandrosoana

-

- MINISTÈRE DE L'ENSEIGNEMENT SUPÉRIEUR
- ET DE LA RECHERCHE SCIENTIFIQUE -

-

UNIVERSITÉ DE TOAMASINA

-

ÉCOLE SUPÉRIEURE POLYTECHNIQUE

-
+
+
+ logo gauche + +
+
+ REPOBLIKAN'I MADAGASIKARA +
+

+ Fitiavana – Tanindrazana – Fandrosoana +

+

+ MINISTÈRE DE L'ENSEIGNEMENT SUPÉRIEUR
+ ET DE LA RECHERCHE SCIENTIFIQUE +

+

+ UNIVERSITÉ DE TOAMASINA +

+

+ ÉCOLE SUPÉRIEURE POLYTECHNIQUE +

+
- logo droite -
+ logo droite +

Releve de notes


+ {/* block info */}
- {/* gauche */}
- {/* gauche gauche */}
Nom @@ -532,7 +565,6 @@ const totalNotes = () => { Codage
- {/* gauche droite */}
: {etudiant.nom}
@@ -543,9 +575,7 @@ const totalNotes = () => { : {etudiant.num_inscription}
- {/* droite */}
- {/* droite gauche */}
Annee Sco @@ -559,7 +589,6 @@ const totalNotes = () => { Parcours
- {/* droite droite */}
: {etudiant.annee_scolaire}
@@ -571,7 +600,7 @@ const totalNotes = () => {
{/* table */} - +
@@ -590,25 +619,19 @@ const totalNotes = () => { - + {sessionType !== 'rattrapage' && ( - )} - + {sessionType !== 'normale' && ( - )} - + @@ -617,15 +640,27 @@ const totalNotes = () => { style={{ borderTop: 'solid 1px black', textAlign: 'center', - padding:'20px', + padding: '20px' }} > - - - - + + + {sessionType !== 'rattrapage' && ( <> @@ -633,15 +668,14 @@ const totalNotes = () => { )} - + {sessionType !== 'normale' && ( <> - )} - + @@ -673,11 +707,13 @@ const totalNotes = () => { paddingLeft: '2%' }} > - Mention:{' '} - {getmentionAfterNotes(note)} + Mention: {getmentionAfterNotes(note)} - @@ -686,7 +722,6 @@ const totalNotes = () => {

Toamasine le

- {/* texte hidden for place in signature */}

Lorem ipsum dolor sit amet consectetur adipisicing elit. Blanditiis delectus perspiciatis nisi aliquid eos adipisci cumque amet ratione error voluptatum. diff --git a/src/renderer/src/components/Resultat.jsx b/src/renderer/src/components/Resultat.jsx index 9d1d30c..a92742c 100644 --- a/src/renderer/src/components/Resultat.jsx +++ b/src/renderer/src/components/Resultat.jsx @@ -3,79 +3,154 @@ import { useParams, Link } from 'react-router-dom' import classe from '../assets/AllStyleComponents.module.css' import classeHome from '../assets/Home.module.css' import Paper from '@mui/material/Paper' -import { Button, Modal, Box, Tabs, Tab, Select, MenuItem, FormControl, InputLabel } from '@mui/material' +import { Button, Tabs, Tab, Select, MenuItem, FormControl, InputLabel } from '@mui/material' import { IoMdReturnRight } from 'react-icons/io' import jsPDF from 'jspdf' import autoTable from 'jspdf-autotable' import { FaDownload } from 'react-icons/fa' import logoRelerev1 from '../assets/logorelever.png' import logoRelerev2 from '../assets/logorelever2.png' +import getSemestre from './function/GetSemestre' const Resultat = () => { const { niveau, scolaire } = useParams() - const formData = { - niveau, - scolaire - } + const formData = { niveau, scolaire } const [etudiants, setEtudiants] = useState([]) const [mention, setMention] = useState([]) const [session, setSession] = useState([]) const [tabValue, setTabValue] = useState(0) - - // États pour les sélections const [selectedMatiere, setSelectedMatiere] = useState('') const [selectedUE, setSelectedUE] = useState('') + const [selectedMentionId, setSelectedMentionId] = useState('') const [availableMatieres, setAvailableMatieres] = useState([]) const [availableUEs, setAvailableUEs] = useState([]) + const [moyennesRattrapage, setMoyennesRattrapage] = useState([]) useEffect(() => { - window.notes.getMoyenne(formData).then((response) => { - setEtudiants(response) - extractMatieresAndUEs(response) - }) - window.noteRepech.getMoyenneRepech(formData).then((response) => { - setSession(response) - }) - window.mention.getMention().then((response) => { - setMention(response) + const fetchData = async () => { + try { + const etudiantsData = await window.notes.getMoyenne(formData) + setEtudiants(etudiantsData) + extractMatieresAndUEs(etudiantsData) + const sessionData = await window.noteRepech.getMoyenneRepech(formData) + setSession(sessionData) + const mentionData = await window.mention.getMention() + setMention(mentionData) + if (mentionData.length > 0) { + setSelectedMentionId(mentionData[0].id) + } + await calculerMoyennesRattrapage(etudiantsData) + } catch (error) { + console.error('Erreur lors du chargement des données:', error) + } + } + fetchData() + }, [niveau, scolaire]) + + const calculerMoyennesRattrapage = async (etudiantsData) => { + const moyennes = [] + for (let i = 0; i < etudiantsData.length; i++) { + if (etudiantsData[i] && etudiantsData[i][0]) { + const etudiantId = etudiantsData[i][0].etudiant_id + const etudiantInfo = etudiantsData[i][0] + try { + const notesData = await window.notes.noteRelerer({ + id: etudiantId, + anneescolaire: scolaire, + niveau: niveau + }) + if (notesData && notesData.noteNormal && notesData.noteRepech && notesData.semestre) { + const updatedMatieres = notesData.noteNormal.map((matiere) => { + const semesters = getSemestre(matiere.etudiant_niveau) + const matchedSemestre = notesData.semestre.find( + (sem) => + sem.matiere_id === matiere.matiere_id && + sem.mention_id === matiere.mention_id && + (sem.nom === semesters[0] || sem.nom === semesters[1]) + ) + return { ...matiere, semestre: matchedSemestre ? matchedSemestre.nom : null } + }) + const updatedMatieresRepech = notesData.noteRepech.map((matiere) => { + const semesters = getSemestre(matiere.etudiant_niveau) + const matchedSemestre = notesData.semestre.find( + (sem) => + sem.matiere_id === matiere.matiere_id && + sem.mention_id === matiere.mention_id && + (sem.nom === semesters[0] || sem.nom === semesters[1]) + ) + return { ...matiere, semestre: matchedSemestre ? matchedSemestre.nom : null } + }) + let aRattrapage = false + updatedMatieres.forEach((item1) => { + let matchingItem = updatedMatieresRepech.find( + (item2) => item2.matiere_id === item1.matiere_id + ) + item1.noterepech = matchingItem ? matchingItem.note : null + if (item1.noterepech !== null && item1.noterepech !== undefined && Number(item1.noterepech) > 0) { + aRattrapage = true + } + }) + if (aRattrapage) { + const moyenne = calculerMoyennePonderee(updatedMatieres) + moyennes.push({ + id: etudiantId, + nom: etudiantInfo.nom, + prenom: etudiantInfo.prenom, + mention: etudiantInfo.mention_id, + moyenne: moyenne.toFixed(2), + admis: moyenne >= 10 + }) + } + } + } catch (error) { + console.error(`Erreur pour l'étudiant ${etudiantId}:`, error) + } + } + } + setMoyennesRattrapage(moyennes) + } + + const calculerMoyennePonderee = (matieres) => { + let totalNotesPonderees = 0 + let totalCredits = 0 + matieres.forEach((matiere) => { + let noteFinale + const noteRepechValide = matiere.noterepech !== null && matiere.noterepech !== undefined && Number(matiere.noterepech) > 0 + if (noteRepechValide) { + noteFinale = Math.max(Number(matiere.note), Number(matiere.noterepech)) + } else { + noteFinale = Number(matiere.note) + } + if (noteFinale != null && !isNaN(noteFinale)) { + totalNotesPonderees += noteFinale * Number(matiere.credit) + totalCredits += Number(matiere.credit) + } }) - }, []) + return totalCredits > 0 ? totalNotesPonderees / totalCredits : 0 + } - // Fonction pour extraire les matières et UEs disponibles const extractMatieresAndUEs = (data) => { - const matieres = new Set() + const matieres = new Map() // key=nomMat, value=mention_id const ues = new Set() - for (let index = 0; index < data.length; index++) { for (let j = 0; j < data[index].length; j++) { - const matiere = data[index][j].matiere || `Matière ${j + 1}` + const nomMat = data[index][j].nomMat || `Matière ${j + 1}` const ue = data[index][j].ue || `UE${Math.floor(j / 2) + 1}` - matieres.add(matiere) + if (!matieres.has(nomMat)) matieres.set(nomMat, data[index][j].mention_id) ues.add(ue) } } - - setAvailableMatieres(Array.from(matieres)) + setAvailableMatieres(Array.from(matieres.keys())) setAvailableUEs(Array.from(ues)) - - // Sélectionner la première matière et UE par défaut - if (matieres.size > 0) setSelectedMatiere(Array.from(matieres)[0]) + if (matieres.size > 0) setSelectedMatiere(Array.from(matieres.keys())[0]) if (ues.size > 0) setSelectedUE(Array.from(ues)[0]) } - let dataToMap = [] - function returnmention(id) { - let mentions - for (let index = 0; index < mention.length; index++) { - if (mention[index].id == id) { - mentions = mention[index].nom - } - } - return mentions + const found = mention.find((m) => m.id === id) + return found ? found.nom : '' } - // Fonction pour déterminer la mention selon la moyenne function getMentionFromMoyenne(moyenne) { const moy = parseFloat(moyenne) if (moy >= 18) return 'Excellent' @@ -87,102 +162,86 @@ const Resultat = () => { return 'Remise à la famille' } - function checkNull(params) { - if (params == null || params == undefined) { - return null - } - return params - } - function compareSessionNotes(session1, session2) { - let notes if (session2) { - if (session1 < session2.note) { - notes = session2.note - } else { - notes = session1 - } - } else { - notes = session1 + return session1 < session2.note ? session2.note : session1 } - return notes + return session1 } - // Traitement des données pour résultat définitif - INCLUANT TOUS LES ÉTUDIANTS - for (let index = 0; index < etudiants.length; index++) { - let total = 0 - let note = 0 - let totalCredit = 0 - let hasValidNotes = false - - let modelJson = { - id: '', - nom: '', - prenom: '', - photos: '', - moyenne: '', - mention: '', - anneescolaire: '' + const calculerMoyennesSessionNormale = () => { + const moyennes = [] + for (let index = 0; index < etudiants.length; index++) { + if (etudiants[index] && etudiants[index][0]) { + let totalNotesPonderees = 0 + let totalCredits = 0 + let hasValidNotes = false + let modelJson = { + id: etudiants[index][0].etudiant_id, + nom: etudiants[index][0].nom, + prenom: etudiants[index][0].prenom, + photos: etudiants[index][0].photos, + mention: etudiants[index][0].mention_id, + anneescolaire: etudiants[index][0].annee_scolaire, + moyenne: 'N/A' + } + for (let j = 0; j < etudiants[index].length; j++) { + const noteNormale = Number(etudiants[index][j].note) + const credit = Number(etudiants[index][j].credit) + if (noteNormale != null && !isNaN(noteNormale)) { + totalNotesPonderees += noteNormale * credit + totalCredits += credit + hasValidNotes = true + } + } + if (hasValidNotes && totalCredits > 0) { + modelJson.moyenne = (totalNotesPonderees / totalCredits).toFixed(2) + } + moyennes.push(modelJson) + } } + return moyennes.sort((a, b) => { + const moyA = a.moyenne === 'N/A' ? -1 : parseFloat(a.moyenne) + const moyB = b.moyenne === 'N/A' ? -1 : parseFloat(b.moyenne) + return moyB - moyA + }) + } - for (let j = 0; j < etudiants[index].length; j++) { - modelJson.id = etudiants[index][j].etudiant_id - modelJson.nom = etudiants[index][j].nom - modelJson.prenom = etudiants[index][j].prenom - modelJson.photos = etudiants[index][j].photos - modelJson.mention = etudiants[index][j].mention_id - modelJson.anneescolaire = etudiants[index][j].annee_scolaire + const sortedStudents = calculerMoyennesSessionNormale() - let currentNote = etudiants[index][j].note - if (session[index]) { - currentNote = compareSessionNotes(etudiants[index][j].note, checkNull(session[index][j])) - } + const getResultsRattrapageAdmis = () => { + return moyennesRattrapage.filter(e => e.mention == selectedMentionId && e.admis).sort((a, b) => parseFloat(b.moyenne) - parseFloat(a.moyenne)) + } - // Vérifier si l'étudiant a des notes valides - if (currentNote != null && currentNote != undefined && !isNaN(currentNote)) { - note += currentNote * etudiants[index][j].credit - totalCredit += etudiants[index][j].credit - hasValidNotes = true - } - } + const getResultsRattrapageNonAdmis = () => { + return moyennesRattrapage.filter(e => e.mention == selectedMentionId && !e.admis).sort((a, b) => parseFloat(b.moyenne) - parseFloat(a.moyenne)) + } - // Calculer la moyenne même si certaines notes manquent - if (hasValidNotes && totalCredit > 0) { - total = note / totalCredit - modelJson.moyenne = total.toFixed(2) - } else { - modelJson.moyenne = 'N/A' - } - - dataToMap.push(modelJson) + const getResultsByMention = () => { + return sortedStudents.filter(s => s.mention == selectedMentionId) } - // Fonction pour obtenir les résultats par matière sélectionnée const getResultsByMatiere = () => { const results = [] - for (let index = 0; index < etudiants.length; index++) { for (let j = 0; j < etudiants[index].length; j++) { - const matiere = etudiants[index][j].matiere || `Matière ${j + 1}` - - if (matiere === selectedMatiere) { + const nomMat = etudiants[index][j].nomMat || `Matière ${j + 1}` + const mentionId = etudiants[index][j].mention_id + if (nomMat === selectedMatiere && mentionId == selectedMentionId) { let finalNote = etudiants[index][j].note if (session[index] && session[index][j]) { finalNote = compareSessionNotes(etudiants[index][j].note, session[index][j]) } - results.push({ id: etudiants[index][j].etudiant_id, nom: etudiants[index][j].nom, prenom: etudiants[index][j].prenom, note: finalNote != null ? finalNote.toFixed(2) : 'N/A', - credit: etudiants[index][j].credit, - mention: returnmention(etudiants[index][j].mention_id) + credit: etudiants[index][j].credit }) } } } - return results.sort((a, b) => { const noteA = a.note === 'N/A' ? -1 : parseFloat(a.note) const noteB = b.note === 'N/A' ? -1 : parseFloat(b.note) @@ -190,21 +249,17 @@ const Resultat = () => { }) } - // Fonction pour obtenir les résultats par UE sélectionnée const getResultsByUE = () => { const groupedStudents = {} const matieresInUE = new Set() - - // Grouper les étudiants et collecter les matières de l'UE for (let index = 0; index < etudiants.length; index++) { for (let j = 0; j < etudiants[index].length; j++) { const ue = etudiants[index][j].ue || `UE${Math.floor(j / 2) + 1}` - const matiere = etudiants[index][j].matiere || `Matière ${j + 1}` - - if (ue === selectedUE) { + const matiere = etudiants[index][j].nomMat || `Matière ${j + 1}` + const mentionId = etudiants[index][j].mention_id + if (ue === selectedUE && mentionId == selectedMentionId) { matieresInUE.add(matiere) const etudiantId = etudiants[index][j].etudiant_id - if (!groupedStudents[etudiantId]) { groupedStudents[etudiantId] = { id: etudiantId, @@ -216,12 +271,10 @@ const Resultat = () => { hasValidNotes: false } } - let finalNote = etudiants[index][j].note if (session[index] && session[index][j]) { finalNote = compareSessionNotes(etudiants[index][j].note, session[index][j]) } - if (finalNote != null && finalNote != undefined && !isNaN(finalNote)) { groupedStudents[etudiantId].matieres[matiere] = finalNote.toFixed(2) groupedStudents[etudiantId].totalNote += finalNote * etudiants[index][j].credit @@ -233,14 +286,10 @@ const Resultat = () => { } } } - const results = Object.values(groupedStudents).map(student => ({ ...student, - moyenneUE: student.hasValidNotes && student.totalCredit > 0 - ? (student.totalNote / student.totalCredit).toFixed(2) - : 'N/A' + moyenneUE: student.hasValidNotes && student.totalCredit > 0 ? (student.totalNote / student.totalCredit).toFixed(2) : 'N/A' })) - return { students: results.sort((a, b) => { const moyA = a.moyenneUE === 'N/A' ? -1 : parseFloat(a.moyenneUE) @@ -251,12 +300,6 @@ const Resultat = () => { } } - const sortedStudents = dataToMap.sort((a, b) => { - const moyA = a.moyenne === 'N/A' ? -1 : parseFloat(a.moyenne) - const moyB = b.moyenne === 'N/A' ? -1 : parseFloat(b.moyenne) - return moyB - moyA - }) - const handleTabChange = (event, newValue) => { setTabValue(newValue) } @@ -264,15 +307,9 @@ const Resultat = () => { const print = () => { const generatePDF = () => { try { - const pdf = new jsPDF({ - orientation: 'portrait', - unit: 'mm', - format: 'a4' - }) - - pdf.addImage(logoRelerev1, 'PNG', 175, 5, 32, 30) - pdf.addImage(logoRelerev2, 'PNG', 10, 5, 40, 30) - + const pdf = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' }) + pdf.addImage(logoRelerev1, 'PNG', 175, 5, 32, 30) + pdf.addImage(logoRelerev2, 'PNG', 10, 5, 40, 30) pdf.setFontSize(10) pdf.text('REPOBLIKAN\'I MADAGASIKARA', 105, 10, { align: 'center' }) pdf.text('Fitiavana-Tanindrazana-Fandrosoana', 105, 14, { align: 'center' }) @@ -282,37 +319,24 @@ const Resultat = () => { pdf.text('********************', 105, 30, { align: 'center' }) pdf.text('UNIVERSITÉ DE TOAMASINA', 105, 34, { align: 'center' }) pdf.text('ÉCOLE SUPÉRIEURE POLYTECHNIQUE', 105, 38, { align: 'center' }) - - const tableId = tabValue === 0 ? '#resultTable' : tabValue === 1 ? '#subjectTable' : '#ueTable' - + const tableId = tabValue === 0 ? '#mentionTable' : tabValue === 1 ? '#rattrapageAdmisTable' : tabValue === 2 ? '#rattrapageNonAdmisTable' : tabValue === 3 ? '#subjectTable' : '#ueTable' autoTable(pdf, { html: tableId, startY: 50, theme: 'grid', - headStyles: { - fillColor: [255, 255, 255], // Fond blanc - halign: 'center', - fontStyle: 'bold', - textColor: [0, 0, 0], // Texte noir - lineColor: [0, 0, 0], // Bordure noire - lineWidth: 0.5 - }, - styles: { - fontSize: 8, - cellPadding: 2, - halign: 'center', - lineColor: [0, 0, 0], // Bordure noire pour toutes les cellules - lineWidth: 0.5 - }, - bodyStyles: { - lineColor: [0, 0, 0], // Bordure noire pour le corps du tableau - lineWidth: 0.5 - } + headStyles: { fillColor: [255, 255, 255], halign: 'center', fontStyle: 'bold', textColor: [0, 0, 0], lineColor: [0, 0, 0], lineWidth: 0.5 }, + styles: { fontSize: 8, cellPadding: 2, halign: 'center', lineColor: [0, 0, 0], lineWidth: 0.5 }, + bodyStyles: { lineColor: [0, 0, 0], lineWidth: 0.5 } }) - - const suffix = tabValue === 0 ? 'definitif' : - tabValue === 1 ? `par-matiere-${selectedMatiere}` : - `par-ue-${selectedUE}` + if (tabValue === 1) { + const admis = getResultsRattrapageAdmis() + const finalY = pdf.lastAutoTable.finalY || 50 + pdf.setFontSize(11) + pdf.setFont(undefined, 'bold') + pdf.text(`Arrêt la liste des étudiants admis à ${admis.length}`, 105, finalY + 15, { align: 'center' }) + } + const selectedMentionName = returnmention(selectedMentionId) + const suffix = tabValue === 0 ? `session-normale-${selectedMentionName}` : tabValue === 1 ? `rattrapage-admis-${selectedMentionName}` : tabValue === 2 ? `rattrapage-non-admis-${selectedMentionName}` : tabValue === 3 ? `par-matiere-${selectedMatiere}` : `par-ue-${selectedUE}` pdf.save(`Resultat-${suffix}-${niveau}-${scolaire}.pdf`) } catch (error) { console.error('Error generating PDF:', error) @@ -322,27 +346,9 @@ const Resultat = () => { } const renderHeader = () => ( -

+
Logo gauche - -
+
REPOBLIKAN'I MADAGASIKARA
Fitiavana-Tanindrazana-Fandrosoana
********************
@@ -352,82 +358,167 @@ const Resultat = () => {
UNIVERSITÉ DE TOAMASINA
ÉCOLE SUPÉRIEURE POLYTECHNIQUE
- Logo droite
) - const renderResultDefinitif = () => ( -
+ Normale + Rattrapage + semestre Unités
d'Enseignement
(UE)
Éléments
constitutifs
(EC)
+ Unités
d'Enseignement
+ (UE){' '} +
+ Éléments
constitutifs
+ (EC) +
créditMoyennecrédit Notes Moyenne Observation - Décision du Jury:{' '} + + Décision du Jury: {descisionJury(note, etudiant.niveau, noteSysteme)}
- - - - - - - - - - - - - - {sortedStudents.map((sorted, index) => ( - - - - - - - - ))} - -
-
- Résultat Définitif : {niveau} admis en {niveau === 'L1' ? 'L2' : niveau === 'L2' ? 'L3' : 'Master'} par ordre de mérite -
-
RANGNOMSPRÉNOMSMoyenneMention
{index + 1}.{sorted.nom}{sorted.prenom}{sorted.moyenne} - {sorted.moyenne !== 'N/A' ? getMentionFromMoyenne(sorted.moyenne) : 'N/A'} -
- ) + const renderResultParMention = () => { + const results = getResultsByMention() + const selectedMentionName = returnmention(selectedMentionId) + return ( + <> +
+ + Sélectionner une mention + + +
+ + + + + + + + + + + + + + + {results.length > 0 ? (results.map((sorted, index) => ( + + + + + + + + ))) : ()} + +
+
Session Normale - Résultat {niveau} pour la mention : {selectedMentionName} ({results.length} étudiant{results.length > 1 ? 's' : ''})
+
RANGNOMSPRÉNOMSMoyenneMention
{index + 1}.{sorted.nom}{sorted.prenom}{sorted.moyenne}{sorted.moyenne !== 'N/A' ? getMentionFromMoyenne(sorted.moyenne) : 'N/A'}
Aucun étudiant avec la mention "{selectedMentionName}"
+ + ) + } + + const renderRattrapageAdmis = () => { + const results = getResultsRattrapageAdmis() + const selectedMentionName = returnmention(selectedMentionId) + return ( + <> +
+ + Sélectionner une mention + + +
+ + + + + + + + + + + + + + + {results.length > 0 ? (results.map((item, index) => ( + + + + + + + + ))) : ()} + +
+
Session de Rattrapage - Étudiants ADMIS {niveau} pour la mention : {selectedMentionName} ({results.length} étudiant{results.length > 1 ? 's' : ''})
+
RANGNOMSPRÉNOMSMoyenneMention
{index + 1}.{item.nom}{item.prenom}{item.moyenne}{getMentionFromMoyenne(item.moyenne)}
Aucun étudiant admis pour cette mention
+ {results.length > 0 && (
Arrêt la liste des étudiants admis à {results.length}
)} + + ) + } + + const renderRattrapageNonAdmis = () => { + const results = getResultsRattrapageNonAdmis() + const selectedMentionName = returnmention(selectedMentionId) + return ( + <> +
+ + Sélectionner une mention + + +
+ + + + + + + + + + + + + + + {results.length > 0 ? (results.map((item, index) => ( + + + + + + + + ))) : ()} + +
+
Session de Rattrapage - Étudiants NON ADMIS {niveau} pour la mention : {selectedMentionName} ({results.length} étudiant{results.length > 1 ? 's' : ''})
+
RANGNOMSPRÉNOMSMoyenneMention
{index + 1}.{item.nom}{item.prenom}{item.moyenne}{getMentionFromMoyenne(item.moyenne)}
Aucun étudiant non admis pour cette mention
+ + ) + } const renderResultParMatiere = () => { const results = getResultsByMatiere() - + const selectedMentionName = returnmention(selectedMentionId) return ( <> -
+
+ + Sélectionner une mention + + Sélectionner une matière - setSelectedMatiere(e.target.value)} label="Sélectionner une matière"> + {availableMatieres.map((matiere) => ({matiere}))}
- - +
@@ -454,46 +545,35 @@ const Resultat = () => { const renderResultParUE = () => { const { students, matieres } = getResultsByUE() - + const selectedMentionName = returnmention(selectedMentionId) return ( <> -
+
+ + Sélectionner une mention + + Sélectionner une UE - setSelectedUE(e.target.value)} label="Sélectionner une UE"> + {availableUEs.map((ue) => ({ue}))}
- -
-
- Résultat pour la matière : {selectedMatiere} -
+
Résultat {niveau} — Mention : {selectedMentionName} — Matière : {selectedMatiere}
+
- {matieres.map((matiere) => ( - - ))} + {matieres.map((matiere) => ())} @@ -503,11 +583,7 @@ const Resultat = () => { - {matieres.map((matiere) => ( - - ))} + {matieres.map((matiere) => ())} ))} @@ -522,9 +598,7 @@ const Resultat = () => {
-

- Resultat des {niveau} en {scolaire} -

+

Resultat des {niveau} en {scolaire}

-
- + {renderHeader()} -
Parcours : GC
Niveau : {niveau}
Année Universitaire : {scolaire}
- - - + + + + - - {tabValue === 0 && renderResultDefinitif()} - {tabValue === 1 && renderResultParMatiere()} - {tabValue === 2 && renderResultParUE()} + {tabValue === 0 && renderResultParMention()} + {tabValue === 1 && renderRattrapageAdmis()} + {tabValue === 2 && renderRattrapageNonAdmis()} + {tabValue === 3 && renderResultParMatiere()} + {tabValue === 4 && renderResultParUE()}
diff --git a/src/renderer/src/components/Sidenav.jsx b/src/renderer/src/components/Sidenav.jsx index f8feaa0..d96ff79 100644 --- a/src/renderer/src/components/Sidenav.jsx +++ b/src/renderer/src/components/Sidenav.jsx @@ -18,6 +18,7 @@ import { BsCalendar2Date } from 'react-icons/bs' import { SiVitest } from 'react-icons/si' import { GrManual } from 'react-icons/gr' import { FaClipboardList } from 'react-icons/fa6' +import { FaMoneyBillWave } from 'react-icons/fa' const Sidenav = () => { const [anchorEl, setAnchorEl] = useState(null) @@ -268,6 +269,15 @@ const isAdmin = () => { Admin + + + Config Ecolage + + { - let { id, niveau, scolaire } = useParams() + const { id, niveau, scolaire } = useParams() + const [notes, setNotes] = useState([]) const [notesRepech, setNotesRepech] = useState([]) const [formData, setFormData] = useState({}) const [formData2, setFormData2] = useState({}) - const [etudiant, setEtudiant] = useState([]) - let annee_scolaire = scolaire + const [etudiant, setEtudiant] = useState({}) const [screenRattrapage, setScreenRattrapage] = useState(false) + /* ===================== MESSAGE MODAL ===================== */ + const [open, setOpen] = useState(false) + const [status, setStatus] = useState(200) + const [message, setMessage] = useState('') + + const handleClose = () => setOpen(false) + + /* ===================== DATA ===================== */ + useEffect(() => { - window.etudiants.getSingle({ id }).then((response) => { - setEtudiant(response) - }) + window.etudiants.getSingle({ id }).then(setEtudiant) }, []) useEffect(() => { - let mention_id = etudiant.mention_id - window.notes.getNotes({ id, niveau, mention_id }).then((response) => { - setNotes(response) - }) + if (!etudiant?.mention_id) return - window.noteRepech.getNotesRepech({ id, niveau, mention_id }).then((response) => { - setNotesRepech(response) - }) + window.notes.getNotes({ id, niveau, mention_id: etudiant.mention_id, annee_scolaire: scolaire }).then(setNotes) + window.noteRepech + .getNotesRepech({ id, niveau, mention_id: etudiant.mention_id, annee_scolaire: scolaire }) + .then(setNotesRepech) }, [etudiant]) - console.log(notes) - /** - * Update formData whenever matieres change - */ + /* ===================== INIT FORM ===================== */ + useEffect(() => { - const initialFormData = notes.reduce((acc, mat) => { - acc[mat.id] = mat.note // Initialize each key with an empty string - return acc - }, {}) - setFormData(initialFormData) - }, [notes]) // Dependency array ensures this runs whenever `matieres` is updated + const init = {} + notes.forEach((n) => (init[n.id] = n.note)) + setFormData(init) + }, [notes]) - /** - * Update formData2 whenever matieres change - */ useEffect(() => { - const initialFormData = notesRepech.reduce((acc, mat) => { - acc[mat.id] = mat.note // Initialize each key with an empty string - return acc - }, {}) - setFormData2(initialFormData) - }, [notesRepech]) // Dependency array ensures this runs whenever `matieres` is updated + const init = {} + notesRepech.forEach((n) => (init[n.id] = n.note)) + setFormData2(init) + }, [notesRepech]) + + /* ===================== SUBMIT NORMALE ===================== */ const submitForm = async (e) => { e.preventDefault() - let mention_id = etudiant.mention_id - console.log('normal submited') - let annee_scolaire = etudiant.annee_scolaire - let response = await window.notes.updateNote({ - formData, - niveau, - id, - mention_id, - annee_scolaire - }) + try { + let mention_id = etudiant.mention_id + let annee_scolaire = scolaire // utiliser l'année de l'URL, pas de l'étudiant + + const response = await window.notes.updateNote({ + formData, + niveau, + id, + mention_id, + annee_scolaire + }) - if (response.changes) { - setMessage('Modification des notes terminer avec succès') setStatus(200) + setMessage('Notes enregistrées avec succès') setOpen(true) - window.noteRepech.getNotesRepech({ id, niveau, mention_id }).then((response) => { - setNotesRepech(response) - }) - window.notes.getNotes({ id, niveau, mention_id }).then((response) => { - setNotes(response) - }) + // rechargement avec l'année correcte + const notesData = await window.notes.getNotes({ id, niveau, mention_id, annee_scolaire: scolaire }) + setNotes(notesData) + + const repechData = await window.noteRepech.getNotesRepech({ id, niveau, mention_id, annee_scolaire: scolaire }) + setNotesRepech(repechData) + + } catch (error) { + console.error(error) + setStatus(500) + setMessage("Échec de l'enregistrement des notes") + setOpen(true) } } + + + /* ===================== SUBMIT RATTRAPAGE ===================== */ const submitForm2 = async (e) => { e.preventDefault() - let mention_id = etudiant.mention_id - console.log('rattrapage submited') - let response = await window.noteRepech.updateNoteRepech({ formData2, niveau, id }) - - console.log(response) - if (response.changes) { - setMessage('Modification des notes terminer avec succès') + try { + let mention_id = etudiant.mention_id + + await window.noteRepech.updateNoteRepech({ + formData2, + niveau, + id, + annee_scolaire: scolaire + }) + setStatus(200) + setMessage('Notes de rattrapage enregistrées avec succès') + setOpen(true) + + const repechData = await window.noteRepech.getNotesRepech({ id, niveau, mention_id }) + setNotesRepech(repechData) + + } catch (error) { + console.error(error) + setStatus(500) + setMessage("Échec de l'enregistrement des notes de rattrapage") setOpen(true) - window.noteRepech.getNotesRepech({ id, niveau, mention_id }).then((response) => { - setNotesRepech(response) - }) - - window.notes.getNotes({ id, niveau, mention_id }).then((response) => { - setNotes(response) - }) } } + - const [status, setStatus] = useState(200) - const [message, setMessage] = useState('') - - /** - * hook to open modal - */ - const [open, setOpen] = useState(false) - - /** - * function to close modal - */ - const handleClose = () => setOpen(false) + /* ===================== MODAL ===================== */ - /** - * function to return the view Modal - * - * @returns {JSX} - */ - const modals = () => ( - + const modalMessage = () => ( + - {status === 200 ? ( - - {message} - - ) : ( - - {message} - - )} - - - + + {message} + + ) - const nom = useRef() - - const changeScreen = () => { - setScreenRattrapage(!screenRattrapage) - } + /* ===================== UI ===================== */ return (
- {modals()} + {modalMessage()} +
-

Mise a jour des notes

-
- window.history.back()}> - - -
+

Mise à jour des notes

+ window.history.back()}> + +
- {/* displaying the form */}
- - - {!screenRattrapage ? ( -
-

Mise a jour des notes

- {/* {/* map the all matiere and note to the form */} - - {notes.map((note) => ( - + + {!screenRattrapage ? ( + +

Session normale

+ + + {notes.map((n) => ( + + + setFormData({ ...formData, [n.id]: e.target.value }) + } + fullWidth + color="warning" + InputProps={{ + startAdornment: ( + + + + ) + }} + /> + + ))} + + + + + + + + ) : ( +
+

Session rattrapage

+ + + {notesRepech.length === 0 ? ( + + + L'étudiant a validé tous les crédits. + + + ) : ( + notesRepech.map((n) => ( + setFormData({ ...formData, [note.id]: e.target.value }) // Update the specific key + label={n.nom} + value={formData2[n.id] || ''} + onChange={(e) => + setFormData2({ ...formData2, [n.id]: e.target.value }) } + fullWidth + color="warning" InputProps={{ startAdornment: ( @@ -251,111 +234,23 @@ const SingleNotes = () => { ) }} - inputRef={nom} - sx={{ - '& .MuiOutlinedInput-root': { - '&:hover fieldset': { - borderColor: '#ff9800' // Set the border color on hover - } - }, - '& .MuiInputBase-input::placeholder': { - fontSize: '11px' // Set the placeholder font size - } - }} /> - ))} - - - - - - - ) : ( -
-

Mise a jour des notes de Rattrapage

- {/* {/* map the all matiere and note to the form */} - - {notesRepech.length === 0 ? ( - // Show this message if notesRepech is empty - -

- L'étudiant a validé tous les crédits. -

-
- ) : ( - // Render form fields if notesRepech contains data - notesRepech.map((note) => ( - - setFormData2({ ...formData2, [note.id]: e.target.value }) // Update the specific key - } - InputProps={{ - startAdornment: ( - - - - ) - }} - inputRef={nom} - sx={{ - '& .MuiOutlinedInput-root': { - '&:hover fieldset': { - borderColor: '#ff9800' // Set the border color on hover - } - }, - '& .MuiInputBase-input::placeholder': { - fontSize: '11px' // Set the placeholder font size - } - }} - /> - - )) - )} -
+ )) + )} +
- - - - - - )} -
-
+ + + + + + )} +
) diff --git a/src/renderer/src/components/Student.jsx b/src/renderer/src/components/Student.jsx index 9064b67..6774449 100644 --- a/src/renderer/src/components/Student.jsx +++ b/src/renderer/src/components/Student.jsx @@ -64,41 +64,84 @@ const Student = () => { const [sortModel, setSortModel] = useState([]) const location = useLocation() const savedFilter = localStorage.getItem('selectedNiveau') || '' + const savedAnnee = localStorage.getItem('selectedAnnee') || '' const initialFilter = location.state?.selectedNiveau || savedFilter - + const initialAnnee = location.state?.selectedAnnee || savedAnnee + const [selectedNiveau, setSelectedNiveau] = useState(initialFilter) - + const [selectedAnnee, setSelectedAnnee] = useState(initialAnnee) + const [anneesList, setAnneesList] = useState([]) + useEffect(() => { if (initialFilter) { setSelectedNiveau(initialFilter) - FilterData({ target: { value: initialFilter } }) // applique le filtre initial } - }, [initialFilter]) + if (initialAnnee) { + setSelectedAnnee(initialAnnee) + } + }, []) /** * hook for displaying the students */ - const [allEtudiants, setAllEtudiants] = useState([]) + const [allEtudiants, setAllEtudiants] = useState([]) const [etudiants, setEtudiants] = useState([]) const [notes, setNotes] = useState([]) + // Charger la liste des années scolaires disponibles useEffect(() => { - window.etudiants.getEtudiants().then((response) => { - setAllEtudiants(response) - - if (selectedNiveau && selectedNiveau !== '') { - setEtudiants(response.filter(e => e.niveau === selectedNiveau)) - } else { - setEtudiants(response) + window.anneescolaire.getAnneeScolaire().then((response) => { + setAnneesList(response || []) + const currentYear = (response || []).find(a => a.is_current === 1 || a.is_current === true) + if (currentYear) { + setSelectedAnnee(currentYear.code) + localStorage.setItem('selectedAnnee', currentYear.code) } }) - + }, []) + + // Recharger les étudiants depuis la DB quand l'année sélectionnée change + // Filtrer pour n'afficher que ceux qui ont payé au moins une tranche + useEffect(() => { + const loadEtudiants = async () => { + let response + if (selectedAnnee && selectedAnnee !== '') { + response = await window.etudiants.getEtudiantsByAnnee(selectedAnnee) + try { + const paidIds = await window.etudiants.getEtudiantsWithPaidTranche({ + annee_scolaire: selectedAnnee + }) + console.log('Etudiants total:', (response || []).length, '| Etudiants ayant payé:', paidIds) + if (Array.isArray(paidIds) && paidIds.length > 0) { + response = (response || []).filter((e) => paidIds.includes(e.id)) + } else { + response = [] + } + } catch (err) { + console.error('Erreur filtre tranche:', err) + } + } else { + response = await window.etudiants.getEtudiants() + } + setAllEtudiants(response || []) + } + loadEtudiants() + window.notes.getMoyenneVerify().then((response) => { setNotes(response) }) - }, [selectedNiveau]) + }, [selectedAnnee]) + + // Filtrer par niveau quand allEtudiants ou selectedNiveau change + useEffect(() => { + if (selectedNiveau && selectedNiveau !== '') { + setEtudiants(allEtudiants.filter(e => e.niveau === selectedNiveau)) + } else { + setEtudiants(allEtudiants) + } + }, [allEtudiants, selectedNiveau]) useEffect(() => { const savedFilters = localStorage.getItem('datagridFilters') @@ -522,7 +565,7 @@ const Student = () => { // Ensure that the array is flat (not wrapped in another array) const dataRow = etudiants.map((etudiant) => ({ - id: etudiant.id, // Ensure this exists and is unique for each etudiant + id: etudiant.inscription_id || etudiant.id, nom: etudiant.nom, prenom: etudiant.prenom, niveau: etudiant.niveau, @@ -544,7 +587,7 @@ const Student = () => { mention_id: etudiant.mention_id, mentionUnite: etudiant.mentionUnite, nomMention: etudiant.nomMention, - action: etudiant.id // Ensure this is a valid URL for the image + action: etudiant.id })) function comparestatut(statutID) { @@ -559,19 +602,24 @@ const Student = () => { } /** - * ✅ Fonction de filtrage avec reset de pagination + * ✅ Fonction de filtrage par niveau avec reset de pagination */ const FilterData = (e) => { const niveau = e.target.value setSelectedNiveau(niveau) - - if (niveau === '') { - setEtudiants(allEtudiants) - } else { - const filtered = allEtudiants.filter(student => student.niveau === niveau) - setEtudiants(filtered) - } - + localStorage.setItem('selectedNiveau', niveau) + setPaginationModel(prev => ({ ...prev, page: 0 })) + } + + /** + * Fonction de filtrage par année scolaire + */ + const FilterByAnnee = (e) => { + const annee = e.target.value + setSelectedAnnee(annee) + localStorage.setItem('selectedAnnee', annee) + setSelectedNiveau('') + localStorage.setItem('selectedNiveau', '') setPaginationModel(prev => ({ ...prev, page: 0 })) } @@ -664,12 +712,42 @@ const Student = () => { {/* bare des filtre */}
- {/* filtre par niveau */} -
+ {/* filtre par année scolaire + niveau */} +
+ {/* filtre par année scolaire */} + + + Année scolaire + + + + + {/* filtre par niveau */} { const { id } = useParams() - const [tranche, setTranche] = useState([]) + const [tranches, setTranches] = useState([]) const [etudiant, setEtudiant] = useState({}) + const [montantConfig, setMontantConfig] = useState(null) - useEffect(() => { + const loadData = () => { window.etudiants.getTranche({ id }).then((response) => { - setTranche(response) + setTranches(response || []) }) - window.etudiants.getSingle({ id }).then((response) => { setEtudiant(response) + // Charger la config ecolage pour cette mention + niveau + if (response && response.mention_id && response.niveau) { + window.configecolage.getByMentionNiveau({ + mention_id: response.mention_id, + niveau_nom: response.niveau + }).then((res) => setMontantConfig(res)).catch(() => setMontantConfig(null)) + } }) + } + + useEffect(() => { + loadData() }, []) + // Modal ajout const [openAdd, setOpenAdd] = useState(false) - const onCloseAdd = () => setOpenAdd(false) - - const openAddFunction = () => { - setOpenAdd(true) - } - const [isSubmited, setIsSubmited] = useState(false) + const handleFormSubmit = (status) => { setIsSubmited(status) } - const [openUpdate, setOpenUpdate] = useState(false) - const onCloseUpdate = () => setOpenUpdate(false) - const [idToSend, setIdToSend] = useState(null) - const [idToSend2, setIdToSend2] = useState(null) - - const openUpdateFunction = (id) => { - setOpenUpdate(true) - setIdToSend(id) - } - - const [openDelete, setOpenDelete] = useState(false) - const onCloseDelete = () => setOpenDelete(false) - const openDeleteFunction = (id) => { - setOpenDelete(true) - setIdToSend2(id) - } - useEffect(() => { if (isSubmited) { - window.etudiants.getTranche({ id }).then((response) => { - setTranche(response) - }) + loadData() setIsSubmited(false) } }, [isSubmited]) + // Modal modification + const [openUpdate, setOpenUpdate] = useState(false) + const [trancheToEdit, setTrancheToEdit] = useState(null) + + // Modal suppression + const [openDeleteModal, setOpenDeleteModal] = useState(false) + const [isDeleted, setIsDeleted] = useState(false) + const [idToDelete, setIdToDelete] = useState(null) + + const handleDelete = async () => { + const response = await window.etudiants.deleteTranche({ id: idToDelete }) + if (response.success) { + setIsDeleted(true) + loadData() + } + } + + const deleteModal = () => ( + { setOpenDeleteModal(false); setIsDeleted(false) }}> + + {isDeleted ? ( + + + Supprime avec succes + + ) : ( + + + Voulez-vous supprimer ce paiement ? + + )} + + {isDeleted ? ( + + ) : ( + <> + + + + )} + + + + ) + return (
setOpenAdd(false)} onSubmitSuccess={handleFormSubmit} open={openAdd} /> + {deleteModal()} - setOpenUpdate(false)} onSubmitSuccess={handleFormSubmit} - open={openDelete} + tranche={trancheToEdit} />
-

Tranche d'Ecolage

+

Frais de Formation

- + setOpenAdd(true)}> window.history.back()}> @@ -106,83 +140,111 @@ const TrancheEcolage = () => {
- -
-
- Résultat pour l'UE : {selectedUE} -
+
Résultat {niveau} — Mention : {selectedMentionName} — UE : {selectedUE}
RANG NOMS PRÉNOMS{matiere}{matiere}MOYENNE UE
{index + 1}. {student.nom} {student.prenom} - {student.matieres[matiere] || 'N/A'} - {student.matieres[matiere] || 'N/A'}{student.moyenneUE}
+ +
- - - - + + + + + + + - {tranche.map((tranch, index) => ( - - - - - + {tranches.length === 0 ? ( + + - ))} + ) : ( + tranches.map((t) => { + const total = (t.tranche1_montant || 0) + (t.tranche2_montant || 0) + const hasTranche1 = t.tranche1_montant > 0 + const hasTranche2 = t.tranche2_montant > 0 + + return ( + + + + + + + + + + + ) + }) + )}
+
- Evolution d'écolage de {etudiant.nom} {etudiant.prenom} + Evolution d'ecolage de {etudiant.nom} {etudiant.prenom} - N°: {etudiant.num_inscription}
Tranche N°DésignationMontantAnnee scolaireTranche 1 - N° BordereauTranche 1 - MontantTranche 2 - N° BordereauTranche 2 - MontantReste a payerStatut Action
{index + 1}{tranch.tranchename}{Number(tranch.montant).toLocaleString(0, 3)} - - -
Aucun paiement enregistre
{t.annee_scolaire_code}{t.tranche1_bordereau || -} + {hasTranche1 ? ( + + {Number(t.tranche1_montant).toLocaleString('fr-FR')} Ar + + ) : ( + Non paye + )} + {t.tranche2_bordereau || -} + {hasTranche2 ? ( + + {Number(t.tranche2_montant).toLocaleString('fr-FR')} Ar + + ) : ( + Non paye + )} + + {(() => { + const droitTotal = montantConfig ? montantConfig.montant_total : 0 + const reste = Math.max(0, droitTotal - total) + return ( + + {Number(reste).toLocaleString('fr-FR')} Ar + + ) + })()} + + {hasTranche1 && hasTranche2 ? ( + Complet + ) : hasTranche1 || hasTranche2 ? ( + Partiel + ) : ( + Non paye + )} + + + + Modifier + + + + Supprimer + +
diff --git a/src/renderer/src/components/UpdateTranche.jsx b/src/renderer/src/components/UpdateTranche.jsx index 21a993e..9981528 100644 --- a/src/renderer/src/components/UpdateTranche.jsx +++ b/src/renderer/src/components/UpdateTranche.jsx @@ -6,38 +6,30 @@ import { DialogTitle, TextField, Button, - Autocomplete, InputAdornment, Box, Grid } from '@mui/material' -import { MdLabelImportantOutline } from 'react-icons/md' -const UpdateTranche = ({ open, onClose, onSubmitSuccess, id }) => { +const UpdateTranche = ({ open, onClose, onSubmitSuccess, tranche }) => { const [formData, setFormData] = useState({ - id: id, - tranchename: '', - montant: '' + id: '', + tranche1_montant: '', + tranche1_bordereau: '', + tranche2_montant: '', + tranche2_bordereau: '' }) - const [tranche, setTranche] = useState([]) useEffect(() => { - if (id !== null) { - window.etudiants.getSingleTranche({ id }).then((response) => { - setTranche(response) - }) + if (tranche) { setFormData({ - id: id + id: tranche.id || '', + tranche1_montant: tranche.tranche1_montant || '', + tranche1_bordereau: tranche.tranche1_bordereau || '', + tranche2_montant: tranche.tranche2_montant || '', + tranche2_bordereau: tranche.tranche2_bordereau || '' }) } - }, [id]) - - useEffect(() => { - setFormData((prev) => ({ - ...prev, - tranchename: tranche.tranchename || '', - montant: tranche.montant || '' - })) }, [tranche]) const handleChange = (e) => { @@ -47,64 +39,81 @@ const UpdateTranche = ({ open, onClose, onSubmitSuccess, id }) => { const handleSubmit = async (e) => { e.preventDefault() - let response = await window.etudiants.updateTranche(formData) - - if (response.changes) { + const response = await window.etudiants.updateTranche(formData) + if (response.success) { onClose() onSubmitSuccess(true) } } return ( - -
- Ajout tranche + + + Modifier le paiement - + + + Tranche 1 + + + + - - - ) + startAdornment: Ar }} /> + + Tranche 2 + + + + - - - ) + startAdornment: Ar }} /> @@ -112,12 +121,8 @@ const UpdateTranche = ({ open, onClose, onSubmitSuccess, id }) => { - - + +
diff --git a/src/renderer/src/components/function/FonctionRelever.js b/src/renderer/src/components/function/FonctionRelever.js index 19b23d8..8a67646 100644 --- a/src/renderer/src/components/function/FonctionRelever.js +++ b/src/renderer/src/components/function/FonctionRelever.js @@ -32,10 +32,14 @@ function nextLevel(niveau) { } } -export const descisionJury = (notes, niveau) => { - if (notes >= 10) { - return `Admis en ${nextLevel(niveau)}` +export const descisionJury = (notes, niveau, systeme) => { + if (!systeme) return '' + + if (notes >= systeme.admis) { + return `Admis` + } else if (notes > systeme.renvoyer) { + return `Redoublant` } else { - return 'Vous redoublez' + return `Renvoyé` } }