Browse Source

Normalisation BDD : séparation données permanentes / inscriptions annuelles

Module écolage complet : configuration, paiement par tranche, suivi intégré

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
master
andrymodeste 1 day ago
parent
commit
6e43b5256c
  1. 167
      README.md
  2. 4
      database/Models/AnneeScolaire.js
  3. 62
      database/Models/ConfigEcolage.js
  4. 602
      database/Models/Etudiants.js
  5. 19
      database/Models/NoteRepechage.js
  6. 55
      database/Models/Notes.js
  7. 7
      database/Models/Parcours.js
  8. 53
      database/database.js
  9. 248
      database/function/System.js
  10. 182
      database/import/Etudiants.js
  11. 184
      resume_modifications.txt
  12. 109
      src/main/index.js
  13. 18
      src/preload/index.js
  14. 5
      src/renderer/src/Routes/Routes.jsx
  15. 2
      src/renderer/src/components/AddNotes.jsx
  16. 317
      src/renderer/src/components/AddStudent.jsx
  17. 113
      src/renderer/src/components/AjoutTranche.jsx
  18. 13
      src/renderer/src/components/AnneeScolaire.jsx
  19. 152
      src/renderer/src/components/ConfigEcolage.jsx
  20. 374
      src/renderer/src/components/Ecolage.jsx
  21. 34
      src/renderer/src/components/Home.jsx
  22. 465
      src/renderer/src/components/Noteclasse.jsx
  23. 381
      src/renderer/src/components/ReleverNotes.jsx
  24. 579
      src/renderer/src/components/Resultat.jsx
  25. 10
      src/renderer/src/components/Sidenav.jsx
  26. 351
      src/renderer/src/components/SingleNotes.jsx
  27. 118
      src/renderer/src/components/Student.jsx
  28. 256
      src/renderer/src/components/TrancheEcolage.jsx
  29. 117
      src/renderer/src/components/UpdateTranche.jsx
  30. 12
      src/renderer/src/components/function/FonctionRelever.js

167
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 ```bash
$ npm install git clone <url-du-repo>
cd c-university
``` ```
### Development ### 2. Installer les dépendances
```bash ```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 ```bash
# For windows npm run dev
$ npm run build:win ```
# For macOS ### Prévisualisation du build
$ npm run build:mac
# For Linux ```bash
$ npm run build:linux 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 | `[email protected]` |
| 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**

4
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 sql = 'UPDATE anneescolaire SET is_current = 0 WHERE id > 0 AND is_current = 1'
const sql2 = 'UPDATE anneescolaire SET is_current = 1 WHERE id = ?' const sql2 = 'UPDATE anneescolaire SET is_current = 1 WHERE id = ?'
pool.query(sql) await pool.query(sql)
try { try {
const [result] = pool.query(sql2, [id]) const [result] = await pool.query(sql2, [id])
console.log(result) console.log(result)
return { return {

62
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
}

602
database/Models/Etudiants.js

@ -1,7 +1,21 @@
const { pool } = require('../database') 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( async function insertEtudiant(
nom, nom,
@ -24,51 +38,95 @@ async function insertEtudiant(
contact, contact,
parcours parcours
) { ) {
const sql = const conn = await pool.getConnection()
'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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
try { try {
let [result] = await pool.query(sql, [ await conn.beginTransaction()
nom,
prenom, // 1. Insert permanent info into etudiants
photos, const [etudiantResult] = await conn.query(
date_de_naissances, `INSERT INTO etudiants
niveau, (nom, prenom, photos, date_de_naissances, num_inscription, sexe, cin,
annee_scolaire, date_delivrance, nationalite, annee_bacc, serie, boursier, domaine, contact)
status, VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
mention_id, [nom, prenom, photos, date_de_naissances, num_inscription, sexe, cin,
num_inscription, date_delivrence, nationaliter, annee_bacc, serie, boursier, domaine, contact]
sexe, )
cin, const etudiantId = etudiantResult.insertId
date_delivrence,
nationaliter, // 2. Get annee_scolaire_id
annee_bacc, const annee_scolaire_id = await getAnneeScolaireId(conn, annee_scolaire)
serie, if (!annee_scolaire_id) {
boursier, await conn.rollback()
domaine, return { success: false, message: 'Année scolaire introuvable: ' + annee_scolaire }
contact, }
parcours
]) // 3. Insert into inscriptions
await conn.query(
return { `INSERT INTO inscriptions
success: true, (etudiant_id, annee_scolaire_id, niveau, mention_id, parcours, status, num_inscription)
id: result.insertId 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()
} }
}
/**
* Get all students filtered by a specific annee_scolaire
*/
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 {
const [rows] = await pool.query(sql, [annee_scolaire])
return rows
} catch (error) { } catch (error) {
return error return error
} }
} }
/** /**
* function to get all etudiants * Get all students with ALL their inscriptions (une ligne par inscription)
*
* @returns JSON
*/ */
async function getAllEtudiants() { 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' 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 { try {
let [rows] = await pool.query(sql) const [rows] = await pool.query(sql)
return rows return rows
} catch (error) { } catch (error) {
return error return error
@ -76,18 +134,21 @@ async function getAllEtudiants() {
} }
/** /**
* function to return a single etudiant * Get a single student with their latest inscription data
* and display it on the screen
*
* @param {int} id
* @returns Promise
*/ */
async function getSingleEtudiant(id) { 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 { try {
const [rows] = await pool.query(sql, [id]) const [rows] = await pool.query(sql, [id])
return rows[0] return rows[0]
} catch (error) { } catch (error) {
return error return error
@ -95,15 +156,25 @@ async function getSingleEtudiant(id) {
} }
/** /**
* function to get all etudiants M2 * Filter students by their latest inscription niveau
*
* @returns JSON
*/ */
async function FilterDataByNiveau(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 { try {
let [rows] = await pool.query(sql, [niveau]) const [rows] = await pool.query(sql, [niveau])
return rows return rows
} catch (error) { } catch (error) {
return error return error
@ -111,18 +182,7 @@ async function FilterDataByNiveau(niveau) {
} }
/** /**
* function to update etudiants * Update a student: permanent fields in etudiants, annual fields in latest inscription
*
* @param {*} nom
* @param {*} prenom
* @param {*} photos
* @param {*} date_de_naissances
* @param {*} niveau
* @param {*} annee_scolaire
* @param {*} status
* @param {*} num_inscription
* @param {*} id
* @returns promise
*/ */
async function updateEtudiant( async function updateEtudiant(
nom, nom,
@ -146,67 +206,88 @@ async function updateEtudiant(
contact, contact,
parcours parcours
) { ) {
const sql = const conn = await pool.getConnection()
'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 = ?'
try { try {
let [result] = await pool.query(sql, [ await conn.beginTransaction()
nom,
prenom, // Update permanent fields (sans num_inscription car il est propre à chaque année)
photos, await conn.query(
date_de_naissances, `UPDATE etudiants SET nom=?, prenom=?, photos=?, date_de_naissances=?,
niveau, sexe=?, cin=?, date_delivrance=?, nationalite=?,
annee_scolaire, annee_bacc=?, serie=?, boursier=?, domaine=?, contact=?
status, WHERE id=?`,
mention_id, [nom, prenom, photos, date_de_naissances, sexe, cin,
num_inscription, date_delivrence, nationalite, annee_bacc, serie, boursier, domaine, contact, id]
sexe, )
cin,
date_delivrence, // Get annee_scolaire_id
nationalite, const annee_scolaire_id = await getAnneeScolaireId(conn, annee_scolaire)
annee_bacc,
serie, if (annee_scolaire_id) {
boursier, // Chercher l'inscription de cette année spécifique
domaine, const [insc] = await conn.query(
contact, 'SELECT id, num_inscription FROM inscriptions WHERE etudiant_id = ? AND annee_scolaire_id = ?',
parcours, [id, annee_scolaire_id]
id )
]) if (insc.length > 0) {
const oldNum = insc[0].num_inscription
if (result.affectedRows === 0) { // Si le num_inscription a changé, sauvegarder l'ancien dans etudiants.num_inscription
return { if (oldNum && oldNum !== num_inscription) {
success: false, const [etudRow] = await conn.query(
message: 'Année Univesitaire non trouvé.' '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
return { const allNums = existingNums ? existingNums.split(',').map(n => n.trim()) : []
success: true, if (!allNums.includes(oldNum)) {
message: 'Année Univesitaire supprimé avec succès.' 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]
)
}
}
await conn.commit()
return { success: true, message: 'Étudiant mis à jour avec succès.' }
} catch (error) { } catch (error) {
await conn.rollback()
return error return error
} finally {
conn.release()
} }
} }
/** /**
* function to return the needed data in dashboard * Get dashboard data
*
* @returns promise
*/ */
async function getDataToDashboard() { async function getDataToDashboard() {
const query = 'SELECT * FROM niveaus' const query = 'SELECT * FROM niveaus'
const query2 = 'SELECT * FROM etudiants' 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 { try {
let [rows] = await pool.query(query) const [niveau] = await pool.query(query)
let niveau = rows const [etudiants] = await pool.query(query2)
;[rows] = await pool.query(query2) const [anne_scolaire] = await pool.query(query3)
let etudiants = rows
;[rows] = await pool.query(query3)
let anne_scolaire = rows
return { niveau, etudiants, anne_scolaire } return { niveau, etudiants, anne_scolaire }
} catch (error) { } catch (error) {
return error return error
@ -215,170 +296,273 @@ async function getDataToDashboard() {
async function changePDP(photos, id) { async function changePDP(photos, id) {
const sql = 'UPDATE etudiants SET photos = ? WHERE id = ?' const sql = 'UPDATE etudiants SET photos = ? WHERE id = ?'
try { try {
let [result] = await pool.query(sql, [photos, id]) const [result] = await pool.query(sql, [photos, id])
if (result.affectedRows === 0) { if (result.affectedRows === 0) {
return { return { success: false, message: 'Étudiant non trouvé.' }
success: false,
message: 'Année Univesitaire non trouvé.'
}
}
return {
success: true,
message: 'Année Univesitaire supprimé avec succès.'
} }
return { success: true, message: 'Photo mise à jour avec succès.' }
} catch (error) { } catch (error) {
return error return error
} }
} }
async function updateParcours(parcours, id) { 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 { try {
let [result] = await pool.query(sql, [parcours, id]) const [result] = await pool.query(sql, [parcours, id, id])
if (result.affectedRows === 0) { if (result.affectedRows === 0) {
return { return { success: false, message: 'Inscription non trouvée.' }
success: false,
message: 'Année Univesitaire non trouvé.'
}
}
return {
success: true,
message: 'Année Univesitaire supprimé avec succès.'
} }
return { success: true, message: 'Parcours mis à jour avec succès.' }
} catch (error) { } catch (error) {
return error return error
} }
} }
async function createTranche(etudiant_id, tranchename, montant) { async function deleteEtudiant(id) {
const sql = 'INSERT INTO trancheecolage (etudiant_id, tranchename, montant) VALUES (?, ?, ?)' const conn = await pool.getConnection()
try { 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 { if (result.affectedRows === 0) {
success: true, return { success: false, message: 'Étudiant non trouvé.' }
id: result.insertId
} }
return { success: true, message: 'Étudiant supprimé avec succès.' }
} catch (error) { } 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 { try {
let [rows] = await pool.query(sql, [id]) // Vérifier si une ligne existe déjà
const [existing] = await pool.query(
return rows '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
}
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) { } 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 { try {
const [result] = await pool.query(sql, [tranchename, montant, id]) const [rows] = await pool.query(sql, [etudiant_id])
console.log('resultat tranche:',result); return rows
} catch (error) {
if (result.affectedRows === 0) { return []
return {
success: false,
message: 'Année Univesitaire non trouvé.'
} }
} }
return { /**
success: true, * Modifier une ligne de tranche (les 2 tranches d'un coup)
message: 'Année Univesitaire supprimé avec succès.' */
} 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) { } catch (error) {
console.log('resultat error:',error); return { success: false, error: error.message }
return error
} }
} }
/**
* Supprimer une ligne de tranche
*/
async function deleteTranche(id) { async function deleteTranche(id) {
const sql = 'DELETE FROM trancheecolage WHERE id = ?'
try { try {
let [result] = await pool.query(sql, [id]) const [result] = await pool.query('DELETE FROM trancheecolage WHERE id = ?', [id])
if (result.affectedRows === 0) return { success: false, message: 'Non trouve.' }
if (result.affectedRows === 0) { return { success: true }
return {
success: false,
message: 'Année Univesitaire non trouvé.'
}
}
return {
success: true,
message: 'Année Univesitaire supprimé avec succès.'
}
} catch (error) { } catch (error) {
return error return { success: false, error: error.message }
} }
} }
async function deleteEtudiant(id) { /**
console.log("id: ", id); * Étudiants ayant payé au moins 1 tranche pour une année scolaire
const sql = 'DELETE FROM etudiants WHERE id = ?'; */
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 { try {
let [result] = await pool.query(sql, [id]); const [rows] = await pool.query(sql, [annee_scolaire_code])
console.log("Résultat DELETE:", result); return rows.map(r => r.etudiant_id)
if (result.affectedRows === 0) {
return {
success: false,
message: 'Etudiant non trouvée.'
};
}
return {
success: true,
message: 'Matière supprimée avec succès.'
};
} catch (error) { } catch (error) {
console.log("err: ",+ error) return []
return { success: false, error: 'Erreur, veuillez réessayer: ' + error };
} }
} }
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 { try {
const [rows] = await pool.query(sql, [id]) await conn.beginTransaction()
return rows[0]
// 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) { } catch (error) {
return error await conn.rollback()
return { success: false, message: error.message }
} finally {
conn.release()
} }
} }
module.exports = { module.exports = {
insertEtudiant, insertEtudiant,
getAllEtudiants, getAllEtudiants,
getAllEtudiantsByAnnee,
FilterDataByNiveau, FilterDataByNiveau,
getSingleEtudiant, getSingleEtudiant,
updateEtudiant, updateEtudiant,
getDataToDashboard, getDataToDashboard,
changePDP, changePDP,
updateParcours, updateParcours,
createTranche, payerTranche,
getTranche, getTranche,
updateTranche, updateTranche,
deleteTranche, deleteTranche,
deleteEtudiant, deleteEtudiant,
getSingleTranche getEtudiantsWithPaidTranche,
reinscribeEtudiant
} }

19
database/Models/NoteRepechage.js

@ -64,17 +64,18 @@ async function getNoteOnline() {
* *
* @returns promise * @returns promise
*/ */
async function getNoteRepech(id, niveau) { async function getNoteRepech(id, niveau, annee_scolaire) {
const query = ` const query = `
SELECT notesrepech.*, matieres.* SELECT notesrepech.*, matieres.*
FROM notesrepech FROM notesrepech
JOIN matieres ON notesrepech.matiere_id = matieres.id JOIN matieres ON notesrepech.matiere_id = matieres.id
WHERE notesrepech.etudiant_id = ? WHERE notesrepech.etudiant_id = ?
AND notesrepech.etudiant_niveau = ? AND notesrepech.etudiant_niveau = ?
AND notesrepech.annee_scolaire = ?
` `
try { try {
const [rows] = await pool.query(query, [id, niveau]) const [rows] = await pool.query(query, [id, niveau, annee_scolaire])
return rows return rows
} catch (error) { } catch (error) {
console.error('Error in getNoteRepech:', error) console.error('Error in getNoteRepech:', error)
@ -140,14 +141,14 @@ async function showMoyenRepech(niveau, scolaire) {
* @param {string} niveau - The student level * @param {string} niveau - The student level
* @returns {Promise} - Promise resolving to the database response or an error * @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 matiere_ids = Object.keys(formData)
const values = Object.values(formData) const values = Object.values(formData)
const query = ` const query = `
UPDATE notesrepech UPDATE notesrepech
SET note = ? SET note = ?
WHERE etudiant_id = ? AND etudiant_niveau = ? AND matiere_id = ? WHERE etudiant_id = ? AND etudiant_niveau = ? AND matiere_id = ? AND annee_scolaire = ?
` `
try { try {
@ -156,17 +157,13 @@ async function updateNoteRepech(formData, niveau, id) {
for (let index = 0; index < matiere_ids.length; index++) { for (let index = 0; index < matiere_ids.length; index++) {
let data = values[index] let data = values[index]
// Convert string number with comma to float, e.g. "12,5" => 12.5
if (typeof data === 'string') { if (typeof data === 'string') {
data = parseFloat(data.replace(',', '.')) data = parseFloat(data.replace(',', '.'))
} else { } else {
data = parseFloat(String(data).replace(',', '.')) data = parseFloat(String(data).replace(',', '.'))
} }
// Optional: console log to verify conversion const [result] = await pool.query(query, [data, id, niveau, matiere_ids[index], annee_scolaire])
console.log(data)
const [result] = await pool.query(query, [data, id, niveau, matiere_ids[index]])
response = result response = result
} }
@ -189,8 +186,8 @@ async function blockShowMoyeneRepech() {
const query2 = ` const query2 = `
SELECT notesrepech.*, SELECT notesrepech.*,
etudiants.id AS etudiantsId, etudiants.id AS etudiantsId,
etudiants.mention_id AS mentionId, notesrepech.mention_id AS mentionId,
etudiants.niveau, notesrepech.etudiant_niveau AS niveau,
matieres.* matieres.*
FROM notesrepech FROM notesrepech
INNER JOIN etudiants ON notesrepech.etudiant_id = etudiants.id INNER JOIN etudiants ON notesrepech.etudiant_id = etudiants.id

55
database/Models/Notes.js

@ -88,34 +88,19 @@ async function getNoteOnline() {
* *
* @returns promise * @returns promise
*/ */
async function getNote(id, niveau) { async function getNote(id, niveau, annee_scolaire) {
let connection let connection
try { try {
connection = await pool.getConnection() 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( const [response2] = await connection.execute(
` `
SELECT notes.*, matieres.* SELECT notes.*, matieres.*
FROM notes FROM notes
JOIN matieres ON notes.matiere_id = matieres.id 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 return response2
@ -164,18 +149,18 @@ async function showMoyen(niveau, scolaire) {
// 2. Prepare the second query // 2. Prepare the second query
const query2 = ` 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 FROM notes
INNER JOIN etudiants ON notes.etudiant_id = etudiants.id INNER JOIN etudiants ON notes.etudiant_id = etudiants.id
INNER JOIN matieres ON notes.matiere_id = matieres.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 // 3. Loop over each student and fetch their notes
for (let index = 0; index < etudiantWithNotes.length; index++) { for (let index = 0; index < etudiantWithNotes.length; index++) {
const etudiantId = etudiantWithNotes[index].etudiant_id const etudiantId = etudiantWithNotes[index].etudiant_id
const [rows] = await pool.query(query2, [etudiantId]) const [rows] = await pool.query(query2, [etudiantId, scolaire])
allEtudiantWithNotes.push(rows) // push just the rows, not [rows, fields] allEtudiantWithNotes.push(rows)
} }
return allEtudiantWithNotes return allEtudiantWithNotes
@ -207,10 +192,10 @@ async function updateNote(formData, niveau, id, mention_id, annee_scolaire) {
data = parseFloat(String(data).replace(',', '.')) 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( const [check] = await pool.query(
'SELECT * FROM notesrepech WHERE etudiant_id = ? AND matiere_id = ? AND etudiant_niveau = ?', 'SELECT * FROM notesrepech WHERE etudiant_id = ? AND matiere_id = ? AND etudiant_niveau = ? AND annee_scolaire = ?',
[id, matiere_id[index], niveau] [id, matiere_id[index], niveau, annee_scolaire]
) )
if (data < 10) { 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( ;[response] = await pool.query(
'UPDATE notes SET note = ? WHERE etudiant_id = ? AND etudiant_niveau = ? AND matiere_id = ?', 'UPDATE notes SET note = ? WHERE etudiant_id = ? AND etudiant_niveau = ? AND matiere_id = ? AND annee_scolaire = ?',
[data, id, niveau, matiere_id[index]] [data, id, niveau, matiere_id[index], annee_scolaire]
) )
} else { } else {
// 4. Remove from notesrepech if note >= 10 // 4. Remove from notesrepech if note >= 10
await pool.query( await pool.query(
'DELETE FROM notesrepech WHERE etudiant_id = ? AND etudiant_niveau = ? AND matiere_id = ?', 'DELETE FROM notesrepech WHERE etudiant_id = ? AND etudiant_niveau = ? AND matiere_id = ? AND annee_scolaire = ?',
[id, niveau, matiere_id[index]] [id, niveau, matiere_id[index], annee_scolaire]
) )
// 5. Update main note // 5. Update main note for the correct year
;[response] = await pool.query( ;[response] = await pool.query(
'UPDATE notes SET note = ? WHERE etudiant_id = ? AND etudiant_niveau = ? AND matiere_id = ?', 'UPDATE notes SET note = ? WHERE etudiant_id = ? AND etudiant_niveau = ? AND matiere_id = ? AND annee_scolaire = ?',
[data, id, niveau, matiere_id[index]] [data, id, niveau, matiere_id[index], annee_scolaire]
) )
} }
} }
@ -258,8 +243,8 @@ async function blockShowMoyene() {
SELECT SELECT
notes.*, notes.*,
etudiants.id AS etudiantsId, etudiants.id AS etudiantsId,
etudiants.mention_id AS mentionId, notes.mention_id AS mentionId,
etudiants.niveau, notes.etudiant_niveau AS niveau,
matieres.* matieres.*
FROM notes FROM notes
INNER JOIN etudiants ON notes.etudiant_id = etudiants.id INNER JOIN etudiants ON notes.etudiant_id = etudiants.id

7
database/Models/Parcours.js

@ -176,8 +176,11 @@ async function extractFiche(matiere_id) {
for (let index = 0; index < newResponse.length; index++) { for (let index = 0; index < newResponse.length; index++) {
const [students] = await connection.query( const [students] = await connection.query(
` `
SELECT * FROM etudiants SELECT e.*, i.niveau, i.mention_id, i.parcours, i.status, a.code AS annee_scolaire
WHERE niveau LIKE ? AND annee_scolaire LIKE ? 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}%`] [`%${newResponse[index]}%`, `%${now}%`]
) )

53
database/database.js

@ -65,13 +65,9 @@ async function createTables() {
prenom VARCHAR(250) DEFAULT NULL, prenom VARCHAR(250) DEFAULT NULL,
photos TEXT DEFAULT NULL, photos TEXT DEFAULT NULL,
date_de_naissances DATE DEFAULT NULL, date_de_naissances DATE DEFAULT NULL,
niveau VARCHAR(250) NOT NULL, num_inscription TEXT DEFAULT NULL,
annee_scolaire VARCHAR(20) NOT NULL,
status INT DEFAULT NULL,
mention_id INT NOT NULL,
num_inscription TEXT UNIQUE NOT NULL,
sexe VARCHAR(20) DEFAULT NULL, sexe VARCHAR(20) DEFAULT NULL,
cin VARCHAR(250) DEFAULT NULL, cin TEXT DEFAULT NULL,
date_delivrance TEXT DEFAULT NULL, date_delivrance TEXT DEFAULT NULL,
nationalite VARCHAR(250) DEFAULT NULL, nationalite VARCHAR(250) DEFAULT NULL,
annee_bacc TEXT DEFAULT NULL, annee_bacc TEXT DEFAULT NULL,
@ -79,11 +75,8 @@ async function createTables() {
boursier VARCHAR(20) DEFAULT NULL, boursier VARCHAR(20) DEFAULT NULL,
domaine VARCHAR(250) DEFAULT NULL, domaine VARCHAR(250) DEFAULT NULL,
contact VARCHAR(20) DEFAULT NULL, contact VARCHAR(20) DEFAULT NULL,
parcours VARCHAR(250) DEFAULT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE 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)
) ENGINE=InnoDB; ) ENGINE=InnoDB;
`) `)
@ -188,6 +181,23 @@ async function createTables() {
) ENGINE=InnoDB; ) 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(` await connection.query(`
CREATE TABLE IF NOT EXISTS traitmentsystem ( CREATE TABLE IF NOT EXISTS traitmentsystem (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
@ -248,8 +258,27 @@ async function createTables() {
CREATE TABLE IF NOT EXISTS trancheecolage ( CREATE TABLE IF NOT EXISTS trancheecolage (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
etudiant_id INT NOT NULL, etudiant_id INT NOT NULL,
tranchename VARCHAR(255) NOT NULL, annee_scolaire_id INT NOT NULL,
montant DOUBLE 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; ) ENGINE=InnoDB;
`) `)

248
database/function/System.js

@ -68,159 +68,161 @@ async function updateStudents() {
const connection = await pool.getConnection() const connection = await pool.getConnection()
try { try {
await connection.beginTransaction() await connection.beginTransaction()
const today = dayjs().format('YYYY-MM-DD')
// Get unfinished years (only one record assumed) // 1. Récupérer toutes les années scolaires triées par debut
const [unfinishedYearsRows] = await connection.query( const [allYears] = await connection.query(
'SELECT * FROM traitmentsystem WHERE is_finished = 0 ORDER BY id ASC LIMIT 1' 'SELECT * FROM anneescolaire ORDER BY debut ASC'
) )
if (unfinishedYearsRows.length === 0) { if (allYears.length < 2) {
await connection.release() await connection.rollback()
return { message: 'No unfinished years found.' } connection.release()
return { message: 'Pas assez d\'années scolaires configurées.' }
} }
const unfinishedYear = unfinishedYearsRows[0]
// Get all students of that unfinished year // 2. Seuils de notes
const [allEtudiants] = await connection.query( const [noteSystemRows] = await connection.query('SELECT * FROM notesystems LIMIT 1')
'SELECT * FROM etudiants WHERE annee_scolaire = ?', const noteSystem = noteSystemRows[0]
[unfinishedYear.code]
)
// Get distinct student IDs with notes let totalProcessed = 0
let etudiantWithNotes = []
for (const etudiant of allEtudiants) { // 3. Pour chaque paire d'années consécutives (annéeN → annéeN+1)
const [results] = await connection.query( for (let i = 0; i < allYears.length - 1; i++) {
'SELECT DISTINCT etudiant_id FROM notes WHERE etudiant_niveau = ? AND annee_scolaire = ?', const prevYear = allYears[i]
[etudiant.niveau, etudiant.annee_scolaire] 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]
) )
etudiantWithNotes.push(...results)
}
// Unique IDs
const uniqueId = [
...new Map(etudiantWithNotes.map((item) => [item.etudiant_id, item])).values()
]
// Get notes details per student if (studentsToProcess.length === 0) continue
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 for (const inscription of studentsToProcess) {
let etudiantWithNotesRepech = [] const inscStatus = parseInt(inscription.status)
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 // Renvoyé → pas de nouvelle inscription
let allEtudiantWithNotesRepech = [] if (inscStatus === 4) continue
for (const student of uniqueIdRepech) {
const [rows] = await connection.query( let newNiveau, newStatus
`SELECT notesrepech.*, etudiants.*, matieres.id, matieres.nom AS nomMat, matieres.credit
FROM notesrepech // Vérifier si l'étudiant a des notes pour l'année précédente
INNER JOIN etudiants ON notesrepech.etudiant_id = etudiants.id const [notesRows] = await connection.query(
INNER JOIN matieres ON notesrepech.matiere_id = matieres.id `SELECT n.note, n.matiere_id, m.credit
WHERE notesrepech.etudiant_id = ?`, FROM notes n
[student.etudiant_id] JOIN matieres m ON n.matiere_id = m.id
WHERE n.etudiant_id = ? AND n.annee_scolaire = ?`,
[inscription.etudiant_id, prevYear.code]
) )
allEtudiantWithNotesRepech.push(rows)
}
// Compute averages and prepare data if (notesRows.length > 0) {
let dataToMap = [] // Calculer la moyenne avec prise en compte du rattrapage
for (let i = 0; i < allEtudiantWithNotes.length; i++) { const [repechRows] = await connection.query(
const notesNormal = allEtudiantWithNotes[i] `SELECT n.note, n.matiere_id, m.credit
const notesRepech = allEtudiantWithNotesRepech[i] || [] 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 total = 0
let totalCredit = 0 let totalCredit = 0
let modelJson = { for (const note of notesRows) {
id: '', const repNote = repechRows.find(r => r.matiere_id === note.matiere_id)
nom: '', const noteToUse = compareSessionNotes(note.note, checkNull(repNote))
prenom: '', total += (noteToUse ?? 0) * note.credit
photos: '', totalCredit += note.credit
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
} }
const moyenne = totalCredit > 0 ? total / totalCredit : 0
modelJson.moyenne = (totalCredit > 0 ? total / totalCredit : 0).toFixed(2)
dataToMap.push(modelJson) 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
} }
// Get note system thresholds
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 { } else {
newNiveau = student.niveau // Pas de notes → utiliser le statut de l'inscription
status = 4 // Fail 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( await connection.query(
'UPDATE etudiants SET niveau = ?, annee_scolaire = ?, status = ? WHERE id = ?', `INSERT INTO inscriptions
[newNiveau, newAnnee, status, student.id] (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]
)
}
} }
// Mark unfinished year as finished totalProcessed++
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() await connection.commit()
connection.release() 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) { } catch (error) {
await connection.rollback() await connection.rollback()
connection.release() connection.release()
console.error(error) console.error('❌ updateStudents error:', error)
throw error throw error
} }
} }

182
database/import/Etudiants.js

@ -9,6 +9,12 @@ const customParseFormat = require('dayjs/plugin/customParseFormat');
dayjs.extend(customParseFormat); dayjs.extend(customParseFormat);
// ---------- Fonctions utilitaires ---------- // ---------- 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) { function fixEncoding(str) {
if (typeof str !== 'string') return str; if (typeof str !== 'string') return str;
return str return str
@ -62,13 +68,8 @@ function convertToISODate(input) {
return null; return null;
} }
// Vérifie année bissextile // ---------- UPDATE étudiant existant ----------
function isLeapYear(year) { async function updateEtudiant(row, conn) {
return (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
}
// ---------- UPDATE étudiant ----------
async function updateEtudiant(row) {
const fields = []; const fields = [];
const params = []; const params = [];
@ -79,13 +80,10 @@ async function updateEtudiant(row) {
} }
} }
// Only permanent fields in etudiants
addFieldIfValue('nom', row.nom); addFieldIfValue('nom', row.nom);
addFieldIfValue('prenom', row.prenom); addFieldIfValue('prenom', row.prenom);
addFieldIfValue('date_de_naissances', convertToISODate(row.date_naissance)); 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('num_inscription', row.num_inscription?.toString());
addFieldIfValue('sexe', row.sexe); addFieldIfValue('sexe', row.sexe);
addFieldIfValue('date_delivrance', convertToISODate(row.date_de_delivrance)); 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' }; if (fields.length === 0) return { success: false, error: 'Aucun champ valide à mettre à jour' };
let sql, whereParams; let sql, whereParams;
if (row.cin && row.cin.toString().trim() !== '') { if (row.cin && row.cin.toString().trim() !== '') {
sql = `UPDATE etudiants SET ${fields.join(', ')} WHERE cin = ?`; sql = `UPDATE etudiants SET ${fields.join(', ')} WHERE cin = ?`;
whereParams = [row.cin]; whereParams = [row.cin];
@ -109,49 +106,44 @@ async function updateEtudiant(row) {
} }
try { try {
const [result] = await pool.query(sql, [...params, ...whereParams]); const [result] = await conn.query(sql, [...params, ...whereParams]);
return { success: true, affectedRows: result.affectedRows };
} catch (error) {
return { success: false, error: error.message };
}
}
// ---------- INSERT multiple étudiants ---------- // Update or create inscription for this year
async function insertMultipleEtudiants(etudiants) { if (result.affectedRows > 0) {
if (!etudiants || etudiants.length === 0) return { success: true, affectedRows: 0 }; // Get the etudiant id
let etudiantId;
const sql = ` if (row.cin && row.cin.toString().trim() !== '') {
INSERT INTO etudiants ( const [et] = await conn.query('SELECT id FROM etudiants WHERE cin = ?', [row.cin]);
nom, prenom, photos, date_de_naissances, niveau, annee_scolaire, status, if (et.length > 0) etudiantId = et[0].id;
mention_id, num_inscription, sexe, cin, date_delivrance, nationalite, } else {
annee_bacc, serie, boursier, domaine, contact, parcours const [et] = await conn.query(
) VALUES ? '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;
}
const values = etudiants.map(row => [ if (etudiantId && row.annee_scolaire_id) {
row.nom, // Check if inscription exists for this year
row.prenom, const [existing] = await conn.query(
getCompressedDefaultImage(), 'SELECT id FROM inscriptions WHERE etudiant_id = ? AND annee_scolaire_id = ?',
convertToISODate(row.date_naissance), [etudiantId, row.annee_scolaire_id]
row.niveau, );
row.annee_scolaire, if (existing.length > 0) {
row.code_redoublement, await conn.query(
row.mention, `UPDATE inscriptions SET niveau=?, mention_id=?, status=?, num_inscription=? WHERE id=?`,
row.num_inscription.toString(), [row.niveau, row.mention, row.code_redoublement, row.num_inscription?.toString(), existing[0].id]
row.sexe, );
row.cin || null, } else {
convertToISODate(row.date_de_delivrance), await conn.query(
row.nationaliter, `INSERT INTO inscriptions (etudiant_id, annee_scolaire_id, niveau, mention_id, status, num_inscription)
parseInt(row.annee_baccalaureat, 10), VALUES (?, ?, ?, ?, ?, ?)`,
row.serie, [etudiantId, row.annee_scolaire_id, row.niveau, row.mention, row.code_redoublement, row.num_inscription?.toString()]
row.boursier, );
fixEncoding(row.domaine), }
row.contact, }
null }
]);
try {
const [result] = await pool.query(sql, [values]);
return { success: true, affectedRows: result.affectedRows }; return { success: true, affectedRows: result.affectedRows };
} catch (error) { } catch (error) {
return { success: false, error: error.message }; return { success: false, error: error.message };
@ -188,8 +180,12 @@ async function importFileToDatabase(filePath) {
} }
} }
const [mentionRows] = await pool.query('SELECT * FROM mentions'); const conn = await pool.getConnection();
const [statusRows] = await pool.query('SELECT * FROM status'); try {
await conn.beginTransaction();
const [mentionRows] = await conn.query('SELECT * FROM mentions');
const [statusRows] = await conn.query('SELECT * FROM status');
const etudiantsToInsert = []; const etudiantsToInsert = [];
const doublons = []; const doublons = [];
@ -204,25 +200,43 @@ async function importFileToDatabase(filePath) {
if (matchedMention) row.mention = matchedMention.id; if (matchedMention) row.mention = matchedMention.id;
// Mapping status // Mapping status
row.code_redoublement = (row.code_redoublement ? row.code_redoublement.trim().substring(0, 1) : 'N'); 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())); const statusMatch = statusRows.find(s => s.nom.toLowerCase().startsWith(row.code_redoublement.toLowerCase()));
if (statusMatch) row.code_redoublement = statusMatch.id; if (statusMatch) row.code_redoublement = statusMatch.id;
// Détection doublons (ignorer CIN vide) // 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; let existing;
if (row.cin && row.cin.toString().trim() !== '') { if (row.cin && row.cin.toString().trim() !== '') {
[existing] = await pool.query('SELECT * FROM etudiants WHERE cin = ?', [row.cin]); [existing] = await conn.query('SELECT id FROM etudiants WHERE cin = ?', [row.cin]);
} else { } else {
[existing] = await pool.query( [existing] = await conn.query(
'SELECT * FROM etudiants WHERE LOWER(TRIM(nom)) = ? AND LOWER(TRIM(prenom)) = ?', 'SELECT id FROM etudiants WHERE LOWER(TRIM(nom)) = ? AND LOWER(TRIM(prenom)) = ?',
[row.nom.toLowerCase().trim(), row.prenom.toLowerCase().trim()] [row.nom.toLowerCase().trim(), row.prenom.toLowerCase().trim()]
); );
} }
if (existing.length > 0) { if (existing.length > 0) {
doublons.push({ nom: row.nom, prenom: row.prenom, cin: row.cin }); doublons.push({ nom: row.nom, prenom: row.prenom, cin: row.cin });
const updateResult = await updateEtudiant(row); const updateResult = await updateEtudiant(row, conn);
if (!updateResult.success) return { error: true, message: `Erreur update ${row.nom} ${row.prenom}: ${updateResult.error}` }; if (!updateResult.success) {
await conn.rollback();
return { error: true, message: `Erreur update ${row.nom} ${row.prenom}: ${updateResult.error}` };
}
} else { } else {
etudiantsToInsert.push(row); etudiantsToInsert.push(row);
} }
@ -231,10 +245,50 @@ async function importFileToDatabase(filePath) {
console.log('✅ Nouveaux à insérer :', etudiantsToInsert.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)); console.log('🔄 Étudiants mis à jour :', doublons.map(e => e.nom + ' ' + e.prenom));
const insertResult = await insertMultipleEtudiants(etudiantsToInsert); // Insert new students
if (!insertResult.success) return { error: true, message: insertResult.error }; for (const row of etudiantsToInsert) {
const [etResult] = await conn.query(
`INSERT INTO etudiants
(nom, prenom, photos, date_de_naissances, num_inscription, sexe, cin,
date_delivrance, nationalite, annee_bacc, serie, boursier, domaine, contact)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
row.nom,
row.prenom,
getCompressedDefaultImage(),
convertToISODate(row.date_naissance),
row.num_inscription.toString(),
row.sexe,
row.cin || null,
convertToISODate(row.date_de_delivrance),
row.nationaliter,
parseInt(row.annee_baccalaureat, 10),
row.serie,
row.boursier,
fixEncoding(row.domaine),
row.contact
]
);
// Create inscription
await conn.query(
`INSERT INTO inscriptions (etudiant_id, annee_scolaire_id, niveau, mention_id, status, num_inscription)
VALUES (?, ?, ?, ?, ?, ?)`,
[etResult.insertId, row.annee_scolaire_id, row.niveau, row.mention, row.code_redoublement, row.num_inscription.toString()]
);
}
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 }; module.exports = { importFileToDatabase };

184
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

109
src/main/index.js

@ -22,11 +22,12 @@ const {
changePDP, changePDP,
deleteEtudiant, deleteEtudiant,
updateParcours, updateParcours,
createTranche, payerTranche,
getTranche, getTranche,
updateTranche, updateTranche,
deleteTranche, deleteTranche,
getSingleTranche getEtudiantsWithPaidTranche,
reinscribeEtudiant
} = require('../../database/Models/Etudiants') } = require('../../database/Models/Etudiants')
const { const {
insertNiveau, insertNiveau,
@ -100,8 +101,14 @@ const {
// Declare mainWindow and tray in the global scope // Declare mainWindow and tray in the global scope
let mainWindow let mainWindow
let tray = null let tray = null
updateCurrentYears() ;(async () => {
updateStudents() try {
await updateCurrentYears()
await updateStudents()
} catch (error) {
console.error('❌ Startup system error:', error)
}
})()
autoUpdater.setFeedURL({ autoUpdater.setFeedURL({
provider: 'generic', provider: 'generic',
@ -368,6 +375,31 @@ ipcMain.handle('insertEtudiant', async (event, credentials) => {
return insert 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 // event for fetching single
ipcMain.handle('getByNiveau', async (event, credentials) => { ipcMain.handle('getByNiveau', async (event, credentials) => {
const { niveau } = credentials const { niveau } = credentials
@ -472,18 +504,18 @@ ipcMain.handle('insertNote', async (event, credentials) => {
// event for get single note // event for get single note
ipcMain.handle('getSingleNote', async (event, credentials) => { 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 return get
}) })
// event for get single note repech // event for get single note repech
ipcMain.handle('getNotesRepech', async (event, credentials) => { 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 return get
}) })
@ -499,9 +531,9 @@ ipcMain.handle('updatetNote', async (event, credentials) => {
// event for updating note repech // event for updating note repech
ipcMain.handle('updatetNoteRepech', async (event, credentials) => { 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 return update
}) })
@ -846,58 +878,61 @@ ipcMain.handle('changeParcours', async (event, credentials) => {
return get return get
}) })
ipcMain.handle('createTranche', async (event, credentials) => { ipcMain.handle('payerTranche', async (event, credentials) => {
const { etudiant_id, tranchename, montant } = credentials const { etudiant_id, annee_scolaire_id, type, montant, num_bordereau } = credentials
// console.log(formData, id); return await payerTranche(etudiant_id, annee_scolaire_id, type, montant, num_bordereau)
const get = createTranche(etudiant_id, tranchename, montant)
return get
}) })
ipcMain.handle('getTranche', async (event, credentials) => { ipcMain.handle('getTranche', async (event, credentials) => {
const { id } = credentials const { id } = credentials
// console.log(formData, id); return await getTranche(id)
const get = getTranche(id)
return get
}) })
ipcMain.handle('updateTranche', async (event, credentials) => { ipcMain.handle('updateTranche', async (event, credentials) => {
const { id, tranchename, montant } = credentials const { id, tranche1_montant, tranche1_bordereau, tranche2_montant, tranche2_bordereau } = credentials
// console.log(formData, id); return await updateTranche(id, tranche1_montant, tranche1_bordereau, tranche2_montant, tranche2_bordereau)
const get = updateTranche(id, tranchename, montant)
return get
}) })
ipcMain.handle('deleteTranche', async (event, credentials) => { ipcMain.handle('deleteTranche', async (event, credentials) => {
const { id } = credentials const { id } = credentials
console.log(id) return await deleteTranche(id)
const get = 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) => { // --- Config Ecolage ---
const { id } = credentials const { getAllConfigEcolage, getConfigEcolageByMentionNiveau, upsertConfigEcolage, deleteConfigEcolage } = require('../../database/Models/ConfigEcolage')
// console.log(formData, id);
const get = getSingleTranche(id)
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) => { ipcMain.handle('createIPConfig', async (event, credentials) => {
const { ipname } = credentials const { ipname } = credentials
// console.log(formData, id);
const get = createConfigIp(ipname) const get = createConfigIp(ipname)
return get return get
}) })
ipcMain.handle('updateIPConfig', async (event, credentials) => { ipcMain.handle('updateIPConfig', async (event, credentials) => {
const { id, ipname } = credentials const { id, ipname } = credentials
// console.log(formData, id);
const get = updateIPConfig(id, ipname) const get = updateIPConfig(id, ipname)
return get return get
}) })

18
src/preload/index.js

@ -2,7 +2,7 @@ import { contextBridge, ipcRenderer } from 'electron'
import { electronAPI } from '@electron-toolkit/preload' import { electronAPI } from '@electron-toolkit/preload'
const { getNessesarytable } = require('../../database/function/System') const { getNessesarytable } = require('../../database/function/System')
const { getAllUsers } = require('../../database/Models/Users') 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 { verifyEtudiantIfHeHasNotes, blockShowMoyene } = require('../../database/Models/Notes')
const { getMatiere, getSemestre, getEnseignants } = require('../../database/Models/Matieres') const { getMatiere, getSemestre, getEnseignants } = require('../../database/Models/Matieres')
const { getSysteme } = require('../../database/Models/NoteSysrem') const { getSysteme } = require('../../database/Models/NoteSysrem')
@ -71,6 +71,7 @@ if (process.contextIsolated) {
contextBridge.exposeInMainWorld('etudiants', { contextBridge.exposeInMainWorld('etudiants', {
insertEtudiant: (credentials) => ipcRenderer.invoke('insertEtudiant', credentials), insertEtudiant: (credentials) => ipcRenderer.invoke('insertEtudiant', credentials),
getEtudiants: () => getAllEtudiants(), getEtudiants: () => getAllEtudiants(),
getEtudiantsByAnnee: (annee) => getAllEtudiantsByAnnee(annee),
FilterDataByNiveau: (credential) => ipcRenderer.invoke('getByNiveau', credential), FilterDataByNiveau: (credential) => ipcRenderer.invoke('getByNiveau', credential),
getSingle: (credential) => ipcRenderer.invoke('single', credential), getSingle: (credential) => ipcRenderer.invoke('single', credential),
updateEtudiants: (credentials) => ipcRenderer.invoke('updateETudiants', credentials), updateEtudiants: (credentials) => ipcRenderer.invoke('updateETudiants', credentials),
@ -78,12 +79,13 @@ if (process.contextIsolated) {
updateEtudiantsPDP: (credentials) => ipcRenderer.invoke('updateETudiantsPDP', credentials), updateEtudiantsPDP: (credentials) => ipcRenderer.invoke('updateETudiantsPDP', credentials),
importExcel: (credentials) => ipcRenderer.invoke('importexcel', credentials), importExcel: (credentials) => ipcRenderer.invoke('importexcel', credentials),
changeParcours: (credentials) => ipcRenderer.invoke('changeParcours', 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), getTranche: (credentials) => ipcRenderer.invoke('getTranche', credentials),
updateTranche: (credentials) => ipcRenderer.invoke('updateTranche', credentials), updateTranche: (credentials) => ipcRenderer.invoke('updateTranche', credentials),
deleteTranche: (credentials) => ipcRenderer.invoke('deleteTranche', credentials), deleteTranche: (credentials) => ipcRenderer.invoke('deleteTranche', credentials),
deleteEtudiant: (id) => ipcRenderer.invoke('deleteEtudiant', id), 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) 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 * contextbridge for status
*/ */

5
src/renderer/src/Routes/Routes.jsx

@ -39,6 +39,7 @@ import Parcours from '../components/Parcours'
import ModalExportFichr from '../components/ModalExportFichr' import ModalExportFichr from '../components/ModalExportFichr'
import Resultat from '../components/Resultat' import Resultat from '../components/Resultat'
import TrancheEcolage from '../components/TrancheEcolage' import TrancheEcolage from '../components/TrancheEcolage'
import ConfigEcolage from '../components/ConfigEcolage'
// Use createHashRouter instead of createBrowserRouter because the desktop app is in local machine // Use createHashRouter instead of createBrowserRouter because the desktop app is in local machine
const Router = createHashRouter([ const Router = createHashRouter([
@ -178,6 +179,10 @@ const Router = createHashRouter([
path: '/resultat/:niveau/:scolaire', path: '/resultat/:niveau/:scolaire',
element: <Resultat /> element: <Resultat />
}, },
{
path: '/configecolage',
element: <ConfigEcolage />
},
{ {
path: '/tranche/:id', path: '/tranche/:id',
element: <TrancheEcolage /> element: <TrancheEcolage />

2
src/renderer/src/components/AddNotes.jsx

@ -139,7 +139,7 @@ const previousFilter = location.state?.selectedNiveau
} }
const handleClose2 = () => { const handleClose2 = () => {
navigate('/student', { state: { selectedNiveau: previousFilter } }) navigate('/student', { state: { selectedNiveau: previousFilter, selectedAnnee: localStorage.getItem('selectedAnnee') || '' } })
setOpen(false) setOpen(false)
} }

317
src/renderer/src/components/AddStudent.jsx

@ -45,37 +45,31 @@ import { MdChangeCircle, MdGrade, MdRule } from 'react-icons/md'
import ModalRecepice from './ModalRecepice' import ModalRecepice from './ModalRecepice'
import { FaLeftLong, FaRightLong } from 'react-icons/fa6' import { FaLeftLong, FaRightLong } from 'react-icons/fa6'
import { Tooltip } from 'react-tooltip' 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 AddStudent = () => {
const navigate = useNavigate()
const [niveaus, setNiveau] = useState([]) const [niveaus, setNiveau] = useState([])
const [status, setStatus] = useState([]) const [status, setStatus] = useState([])
const [scolaire, setScolaire] = useState([]) const [scolaire, setScolaire] = useState([])
const [mention, setMention] = useState([]) const [mention, setMention] = useState([])
const [parcours, setParcours] = useState([]) const [parcours, setParcours] = useState([])
const [isExistingStudent, setIsExistingStudent] = useState(false)
useEffect(() => { // Recherche etudiant existant
window.niveaus.getNiveau().then((response) => { const [searchQuery, setSearchQuery] = useState('')
setNiveau(response) const [searchResults, setSearchResults] = useState([])
}) const [allStudents, setAllStudents] = useState([])
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)
})
}, [])
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 * hook for storing data in the input
@ -102,6 +96,105 @@ const AddStudent = () => {
parcours: '' 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({}) const [dataToSend, setDataToSend] = useState({})
useEffect(() => { useEffect(() => {
@ -181,21 +274,63 @@ const AddStudent = () => {
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
e.preventDefault() e.preventDefault()
// Handle form submission logic let etudiantId = null
const response = await window.etudiants.insertEtudiant(formData)
console.log(response) 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) { if (!response.success) {
setCode(422) setCode(422)
setOpen(true) setOpen(true)
return
}
etudiantId = response.id
}
// 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
})
}
} }
if (response.success) {
imageVisual.current.style.display = 'none' imageVisual.current.style.display = 'none'
imageVisual.current.src = '' imageVisual.current.src = ''
setCode(200) setCode(200)
setOpen(true) setOpen(true)
} setIsExistingStudent(false)
setPaiementType('')
setNumBordereau('')
setMontantPaye('')
} }
const VisuallyHiddenInput = styled('input')({ const VisuallyHiddenInput = styled('input')({
@ -344,11 +479,46 @@ const AddStudent = () => {
p: 4 p: 4
}} }}
> >
<div style={{ textAlign: 'right', fontWeight: 'bold' }}> <div style={{ textAlign: 'right', fontWeight: 'bold', marginBottom: '10px' }}>
<Link to={'/exportetudiant'} style={{ textDecoration: 'none', color: 'orange' }}> <Link to={'/exportetudiant'} style={{ textDecoration: 'none', color: 'orange' }}>
Importer un fichier excel <FaFileExcel /> Importer un fichier excel <FaFileExcel />
</Link> </Link>
</div> </div>
{/* Barre de recherche etudiant existant */}
<div style={{ position: 'relative', marginBottom: '15px' }}>
<TextField
fullWidth
size="small"
color="warning"
placeholder="Rechercher un etudiant existant (nom, prenom, N° inscription)..."
value={searchQuery}
onChange={handleSearch}
sx={{ '& .MuiOutlinedInput-root': { '&:hover fieldset': { borderColor: '#ff9800' } } }}
/>
{searchResults.length > 0 && (
<Paper sx={{
position: 'absolute', top: '100%', left: 0, right: 0, zIndex: 10,
maxHeight: 250, overflow: 'auto', boxShadow: 3
}}>
{searchResults.map((s) => (
<div
key={s.id}
onClick={() => 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'}
>
<span><b>{s.nom}</b> {s.prenom}</span>
<span style={{ color: 'gray', fontSize: '12px' }}>{s.num_inscription} - {s.niveau}</span>
</div>
))}
</Paper>
)}
</div>
<Box <Box
sx={{ sx={{
marginTop: '2%', marginTop: '2%',
@ -1034,6 +1204,95 @@ const AddStudent = () => {
</Grid> </Grid>
</Grid> </Grid>
)} )}
{/* Section Paiement - toujours visible sur page 2 */}
{page2 && (
<Grid item xs={12}>
<Paper sx={{ p: 2, mt: 1, background: '#f9f9f9', border: '1px solid #ff9800' }}>
<Typography variant="subtitle1" sx={{ mb: 1, fontWeight: 'bold' }}>
<FaMoneyBillWave style={{ marginRight: 5 }} />
Paiement Ecolage
{montantConfig
? ` - Droit total : ${Number(montantConfig.montant_total).toLocaleString('fr-FR')} Ar`
: ' - (Configurer les droits dans Admin > Config Ecolage)'}
</Typography>
{montantConfig ? (
<Grid container spacing={2} alignItems="center">
<Grid item xs={12}>
<RadioGroup
row
value={paiementType}
onChange={(e) => {
const val = e.target.value
setPaiementType(val)
if (val === 'complet' && montantConfig) {
setMontantPaye(String(montantConfig.montant_total))
} else {
setMontantPaye('')
}
}}
>
<FormControlLabel value="" control={<Radio color="default" />}
label="Pas de paiement"
/>
<FormControlLabel value="tranche1" control={<Radio color="warning" />}
label="Tranche 1"
/>
<FormControlLabel value="complet" control={<Radio color="warning" />}
label="Tranche Complete"
/>
</RadioGroup>
</Grid>
{paiementType && (
<>
<Grid item xs={12} sm={4}>
<TextField
required
name="num_bordereau"
label="N° Bordereau"
type="text"
fullWidth
size="small"
color="warning"
placeholder="Ex: BRD-2026-001"
value={numBordereau}
onChange={(e) => setNumBordereau(e.target.value)}
sx={{ '& .MuiOutlinedInput-root': { '&:hover fieldset': { borderColor: '#ff9800' } } }}
/>
</Grid>
<Grid item xs={12} sm={4}>
<TextField
required
label="Montant paye"
type="number"
fullWidth
size="small"
color="warning"
value={montantPaye}
onChange={(e) => setMontantPaye(e.target.value)}
InputProps={{ startAdornment: <InputAdornment position="start">Ar</InputAdornment> }}
sx={{ '& .MuiOutlinedInput-root': { '&:hover fieldset': { borderColor: '#ff9800' } } }}
/>
</Grid>
<Grid item xs={12} sm={4}>
<Typography sx={{ fontSize: '13px', color: '#555', lineHeight: 1.4 }}>
Droit total : <b>{Number(montantConfig.montant_total).toLocaleString('fr-FR')} Ar</b><br />
Reste a payer : <b style={{ color: Number(montantPaye || 0) >= montantConfig.montant_total ? 'green' : 'red' }}>
{Number(Math.max(0, montantConfig.montant_total - Number(montantPaye || 0))).toLocaleString('fr-FR')} Ar
</b>
</Typography>
</Grid>
</>
)}
</Grid>
) : (
<Typography sx={{ color: 'gray', fontSize: '14px' }}>
Selectionnez d'abord une mention et un niveau sur la page 1, puis configurez les droits dans Admin &gt; Config Ecolage.
</Typography>
)}
</Paper>
</Grid>
)}
{/* Submit Button */} {/* Submit Button */}
<Grid <Grid
item item
@ -1045,7 +1304,7 @@ const AddStudent = () => {
<FaLeftLong className="pageprecedent" style={{ outline: 'none' }} /> <FaLeftLong className="pageprecedent" style={{ outline: 'none' }} />
</IconButton> </IconButton>
<Tooltip anchorSelect=".pageprecedent" className="custom-tooltip" place="top"> <Tooltip anchorSelect=".pageprecedent" className="custom-tooltip" place="top">
Page précédents Page precedents
</Tooltip> </Tooltip>
<IconButton color="warning" onClick={seePage2}> <IconButton color="warning" onClick={seePage2}>
<FaRightLong className="pagesuivant" style={{ outline: 'none' }} /> <FaRightLong className="pagesuivant" style={{ outline: 'none' }} />

113
src/renderer/src/components/AjoutTranche.jsx

@ -1,4 +1,4 @@
import React, { useState } from 'react' import React, { useState, useEffect } from 'react'
import { import {
Dialog, Dialog,
DialogActions, DialogActions,
@ -6,20 +6,39 @@ import {
DialogTitle, DialogTitle,
TextField, TextField,
Button, Button,
Autocomplete,
InputAdornment, InputAdornment,
Box, Box,
Grid Grid,
FormControl,
InputLabel,
Select,
MenuItem
} from '@mui/material' } from '@mui/material'
import { MdLabelImportantOutline } from 'react-icons/md'
const AjoutTranche = ({ open, onClose, onSubmitSuccess, id }) => { const AjoutTranche = ({ open, onClose, onSubmitSuccess, id }) => {
const [anneesList, setAnneesList] = useState([])
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
etudiant_id: id, etudiant_id: id,
tranchename: '', annee_scolaire_id: '',
montant: '' 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 handleChange = (e) => {
const { name, value } = e.target const { name, value } = e.target
setFormData({ ...formData, [name]: value }) setFormData({ ...formData, [name]: value })
@ -27,69 +46,87 @@ const AjoutTranche = ({ open, onClose, onSubmitSuccess, id }) => {
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
e.preventDefault() e.preventDefault()
let response = await window.etudiants.createTranche(formData) const response = await window.etudiants.payerTranche(formData)
if (response.success) { if (response.success) {
onClose() onClose()
onSubmitSuccess(true) onSubmitSuccess(true)
setFormData({ setFormData({
etudiant_id: id, etudiant_id: id,
tranchename: '', annee_scolaire_id: formData.annee_scolaire_id,
montant: '' type: '',
montant: '',
num_bordereau: ''
}) })
} }
} }
return ( return (
<Dialog open={open} onClose={onClose}> <Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
<form action="" onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<DialogTitle>Ajout tranche</DialogTitle> <DialogTitle>Enregistrer un paiement</DialogTitle>
<DialogContent> <DialogContent>
<Box sx={{ flexGrow: 1 }}> <Box sx={{ flexGrow: 1, mt: 1 }}>
<Grid container spacing={2}> <Grid container spacing={2}>
<Grid item xs={12}>
<FormControl fullWidth color="warning" required size="small">
<InputLabel>Annee scolaire</InputLabel>
<Select
name="annee_scolaire_id"
value={formData.annee_scolaire_id}
onChange={handleChange}
label="Annee scolaire"
>
{anneesList.map((a) => (
<MenuItem value={a.id} key={a.id}>{a.code}</MenuItem>
))}
</Select>
</FormControl>
</Grid>
<Grid item xs={12}>
<FormControl fullWidth color="warning" required size="small">
<InputLabel>Type de paiement</InputLabel>
<Select
name="type"
value={formData.type}
onChange={handleChange}
label="Type de paiement"
>
<MenuItem value="Tranche 1">Tranche 1</MenuItem>
<MenuItem value="Tranche 2">Tranche 2</MenuItem>
<MenuItem value="Tranche Complète">Tranche Complete (1 + 2)</MenuItem>
</Select>
</FormControl>
</Grid>
<Grid item xs={12} sm={6}> <Grid item xs={12} sm={6}>
<TextField <TextField
autoFocus
margin="normal"
required required
name="tranchename" name="num_bordereau"
label="Désignation" label="N° Bordereau"
type="text" type="text"
fullWidth fullWidth
placeholder="Tranche 1" size="small"
placeholder="Ex: BRD-2026-001"
variant="outlined" variant="outlined"
value={formData.tranchename} value={formData.num_bordereau}
color="warning" color="warning"
onChange={handleChange} onChange={handleChange}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<MdLabelImportantOutline />
</InputAdornment>
)
}}
/> />
</Grid> </Grid>
<Grid item xs={12} sm={6}> <Grid item xs={12} sm={6}>
<TextField <TextField
autoFocus
margin="normal"
required required
name="montant" name="montant"
label="Montant" label="Montant"
type="number" type="number"
fullWidth fullWidth
placeholder="Montant" size="small"
variant="outlined" variant="outlined"
value={formData.montant} value={formData.montant}
color="warning" color="warning"
onChange={handleChange} onChange={handleChange}
InputProps={{ InputProps={{
startAdornment: ( startAdornment: <InputAdornment position="start">Ar</InputAdornment>
<InputAdornment position="start">
<MdLabelImportantOutline />
</InputAdornment>
)
}} }}
/> />
</Grid> </Grid>
@ -97,12 +134,8 @@ const AjoutTranche = ({ open, onClose, onSubmitSuccess, id }) => {
</Box> </Box>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={onClose} color="error"> <Button onClick={onClose} color="error">Annuler</Button>
Annuler <Button type="submit" color="warning" variant="contained">Enregistrer</Button>
</Button>
<Button type="submit" color="warning">
Soumettre
</Button>
</DialogActions> </DialogActions>
</form> </form>
</Dialog> </Dialog>

13
src/renderer/src/components/AnneeScolaire.jsx

@ -142,13 +142,12 @@ const AnneeScolaire = () => {
})) }))
const setCurrent = async (id) => { const setCurrent = async (id) => {
// let response = await window.anneescolaire.setCurrent({id}); const response = await window.anneescolaire.setCurrent({ id })
// console.log(response); if (response.success) {
// if (response.changes) { window.anneescolaire.getAnneeScolaire().then((response) => {
// window.anneescolaire.getAnneeScolaire().then((response) => { setAnneeScolaire(response)
// setAnneeScolaire(response); })
// }); }
// }
} }
const deleteButton = async (id) => { const deleteButton = async (id) => {

152
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 (
<div className={classe.mainHome}>
<div className={classeHome.header}>
<div className={classe.h1style}>
<div className={classeHome.blockTitle}>
<h1>
<FaMoneyBillWave style={{ marginRight: 10 }} />
Configuration Ecolage
</h1>
</div>
</div>
</div>
<div style={{ padding: '0 2%' }}>
<Paper sx={{ p: 3 }}>
<Typography variant="subtitle1" sx={{ mb: 2, fontWeight: 'bold' }}>
Definir le montant des droits par mention et niveau
</Typography>
<form onSubmit={handleSubmit}>
<Grid container spacing={2} alignItems="center">
<Grid item xs={12} sm={3}>
<FormControl fullWidth size="small" color="warning" required>
<InputLabel>Mention</InputLabel>
<Select name="mention_id" value={ecolageForm.mention_id} onChange={handleChange} label="Mention">
{mentions.map((m) => (
<MenuItem value={m.id} key={m.id}>{m.nom}</MenuItem>
))}
</Select>
</FormControl>
</Grid>
<Grid item xs={12} sm={3}>
<FormControl fullWidth size="small" color="warning" required>
<InputLabel>Niveau</InputLabel>
<Select name="niveau_id" value={ecolageForm.niveau_id} onChange={handleChange} label="Niveau">
{niveaux.map((n) => (
<MenuItem value={n.id} key={n.id}>{n.nom}</MenuItem>
))}
</Select>
</FormControl>
</Grid>
<Grid item xs={12} sm={3}>
<TextField
required
name="montant_total"
label="Montant total"
type="number"
fullWidth
size="small"
color="warning"
value={ecolageForm.montant_total}
onChange={handleChange}
InputProps={{ startAdornment: <InputAdornment position="start">Ar</InputAdornment> }}
/>
</Grid>
<Grid item xs={12} sm={3}>
<Button type="submit" color="warning" variant="contained" fullWidth>
Enregistrer
</Button>
</Grid>
</Grid>
</form>
<table className="table table-bordered table-striped text-center shadow-sm" style={{ marginTop: '20px' }}>
<thead className="table-secondary">
<tr>
<th>Mention</th>
<th>Niveau</th>
<th>Montant total</th>
<th>Tranche 1</th>
<th>Tranche 2</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{configList.length === 0 ? (
<tr><td colSpan={6} style={{ color: 'gray' }}>Aucune configuration</td></tr>
) : (
configList.map((c) => (
<tr key={c.id}>
<td>{c.mention_nom}</td>
<td>{c.niveau_nom}</td>
<td><b>{Number(c.montant_total).toLocaleString('fr-FR')} Ar</b></td>
<td>{Number(c.montant_total / 2).toLocaleString('fr-FR')} Ar</td>
<td>{Number(c.montant_total / 2).toLocaleString('fr-FR')} Ar</td>
<td style={{ display: 'flex', gap: '8px', justifyContent: 'center' }}>
<Button color="warning" size="small" variant="contained" onClick={() => handleEdit(c)}>
<FaPenToSquare />
</Button>
<Button color="error" size="small" variant="contained" onClick={() => handleDelete(c.id)}>
<FaTrash />
</Button>
</td>
</tr>
))
)}
</tbody>
</table>
</Paper>
</div>
</div>
)
}
export default ConfigEcolage

374
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) => (
<span
style={{
color: params.value > 0 ? 'green' : 'red',
fontWeight: 'bold'
}}
>
{params.value > 0 ? `${params.value} payee(s)` : 'Aucune'}
</span>
)
},
{
field: 'photos',
headerName: 'Image',
width: 80,
renderCell: (params) => (
<img
src={params.value}
alt="pdp"
style={{ width: 40, height: 40, borderRadius: '50%', objectFit: 'cover' }}
/>
)
},
{
field: 'action',
headerName: 'Action',
width: 180,
renderCell: (params) => (
<div style={{ display: 'flex', gap: '10px' }}>
<Link to={`/tranche/${params.value}`}>
<Button
color="warning"
variant="contained"
className={`payer${params.value}`}
>
<MdPayment style={{ fontSize: '20px', color: 'white' }} />
</Button>
<Tooltip
anchorSelect={`.payer${params.value}`}
className="custom-tooltip"
place="top"
>
Gerer le paiement
</Tooltip>
</Link>
</div>
)
}
]
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 (
<div className={classe.mainHome}>
<style>
{`
.custom-tooltip {
font-size: 15px;
border: solid 1px white !important;
z-index: 999;
}
`}
</style>
<div className={classeHome.header}>
<div className={classe.h1style}>
<div className={classeHome.blockTitle}>
<h1>
<FaMoneyBillWave style={{ marginRight: '10px' }} />
Gestion Ecolage
</h1>
</div>
</div>
{/* Filters */}
<div className={classeHome.container}>
<div
style={{
width: '100%',
textAlign: 'right',
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'center'
}}
>
<FormControl
sx={{
m: 1,
width: '25%',
'& .MuiOutlinedInput-root': {
'&:hover fieldset': { borderColor: '#ff9800' }
}
}}
size="small"
variant="outlined"
>
<InputLabel sx={{ color: 'black', fontSize: '18px' }} color="warning">
Annee scolaire
</InputLabel>
<Select
label="Annee scolaire"
color="warning"
value={selectedAnnee}
onChange={FilterByAnnee}
sx={{ background: 'white' }}
>
<MenuItem value="">
<em>Toutes les annees</em>
</MenuItem>
{anneesList.map((a) => (
<MenuItem value={a.code} key={a.id}>
{a.code}
</MenuItem>
))}
</Select>
</FormControl>
<FormControl
sx={{
m: 1,
width: '25%',
'& .MuiOutlinedInput-root': {
'&:hover fieldset': { borderColor: '#ff9800' }
}
}}
size="small"
variant="outlined"
>
<InputLabel sx={{ color: 'black', fontSize: '18px' }} color="warning">
Niveau
</InputLabel>
<Select
label="Niveau"
color="warning"
name="niveau"
value={selectedNiveau}
onChange={FilterData}
startAdornment={
<InputAdornment position="start">
<FaGraduationCap />
</InputAdornment>
}
sx={{ background: 'white' }}
>
<MenuItem value="">
<em>Tous les etudiants</em>
</MenuItem>
{niveaus.map((niveau) => (
<MenuItem value={niveau.nom} key={niveau.id}>
{niveau.nom}
</MenuItem>
))}
</Select>
</FormControl>
</div>
</div>
</div>
<div className={classeHome.boxEtudiantsCard}>
<div style={{ display: 'flex', justifyContent: 'center' }}>
<Paper
sx={{
width: '100%',
height: 'auto',
minHeight: 500,
display: 'flex'
}}
>
<ThemeProvider theme={theme}>
<div style={{ display: 'flex', flexDirection: 'column', width: '100%' }}>
<DataGrid
rows={dataRow}
columns={columns}
filterModel={filterModel}
onFilterModelChange={(newModel) => {
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}
/>
</div>
</ThemeProvider>
</Paper>
</div>
</div>
</div>
)
}
export default Ecolage

34
src/renderer/src/components/Home.jsx

@ -29,12 +29,15 @@ const Home = () => {
const currentYear = dayjs().year() const currentYear = dayjs().year()
useEffect(() => { useEffect(() => {
// Fetch data and update state window.niveaus.getNiveau().then((response) => {
window.etudiants.getDataToDashboards().then((response) => { setNiveau(response || [])
setEtudiants(response.etudiants) })
setOriginalEtudiants(response.etudiants) window.anneescolaire.getAnneeScolaire().then((response) => {
setNiveau(response.niveau) setAnnee_scolaire(response || [])
setAnnee_scolaire(response.anne_scolaire) })
window.etudiants.getEtudiants().then((response) => {
setEtudiants(response || [])
setOriginalEtudiants(response || [])
}) })
}, []) }, [])
@ -60,14 +63,13 @@ const Home = () => {
// Find the maximum value using Math.max // Find the maximum value using Math.max
const maxStudentCount = Math.max(...studentCounts) const maxStudentCount = Math.max(...studentCounts)
const FilterAnneeScolaire = (e) => { const FilterAnneeScolaire = async (e) => {
let annee_scolaire = e.target.value const annee = e.target.value
const filteredEtudiants = originalEtudiants.filter( if (annee === 'general') {
(etudiant) => etudiant.annee_scolaire === annee_scolaire
)
setEtudiants(filteredEtudiants)
if (annee_scolaire == 'general') {
setEtudiants(originalEtudiants) setEtudiants(originalEtudiants)
} else {
const filtered = await window.etudiants.getEtudiantsByAnnee(annee)
setEtudiants(filtered || [])
} }
} }
// end filter all data // end filter all data
@ -173,9 +175,9 @@ const Home = () => {
<MenuItem value="general" selected> <MenuItem value="general" selected>
<em>Géneral</em> <em>Géneral</em>
</MenuItem> </MenuItem>
{annee_scolaire.map((niveau) => ( {annee_scolaire.map((a) => (
<MenuItem value={niveau.annee_scolaire} key={niveau.annee_scolaire}> <MenuItem value={a.code} key={a.id}>
{niveau.annee_scolaire} {a.code}
</MenuItem> </MenuItem>
))} ))}
</Select> </Select>

465
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 { Tooltip } from 'react-tooltip'
import ReleverNotes from './ReleverNotes' import ReleverNotes from './ReleverNotes'
import { FaDownload } from 'react-icons/fa' import { FaDownload } from 'react-icons/fa'
import getSemestre from './function/GetSemestre'
const Noteclasse = () => { const Noteclasse = () => {
const { niveau, scolaire } = useParams() const { niveau, scolaire } = useParams()
const [etudiants, setEtudiants] = useState([]) const [etudiants, setEtudiants] = useState([])
const [mention, setMention] = useState([]) const [mention, setMention] = useState([])
const [session, setSession] = useState([]) const [moyennesCalculees, setMoyennesCalculees] = useState([])
const formData = { const formData = { niveau, scolaire }
niveau,
scolaire
}
useEffect(() => { useEffect(() => {
window.notes.getMoyenne(formData).then((response) => { const fetchData = async () => {
setEtudiants(response) try {
}) // Récupérer la liste des étudiants
window.noteRepech.getMoyenneRepech(formData).then((response) => { const etudiantsData = await window.notes.getMoyenne(formData)
setSession(response) setEtudiants(etudiantsData)
})
window.mention.getMention().then((response) => { // Récupérer les mentions
setMention(response) 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
}) })
}, [])
let dataToMap = [] 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])
)
function returnmention(id) { return {
let mentions ...matiere,
for (let index = 0; index < mention.length; index++) { semestre: matchedSemestre ? matchedSemestre.nom : null
if (mention[index].id == id) {
mentions = mention[index].nom
} }
})
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
} }
return mentions })
}
// 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
})
function checkNull(params) { // Calculer la moyenne pondérée
console.log(params); const moyenne = calculerMoyennePonderee(updatedMatieres)
if (params == null || params == undefined) {
return null 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)
})
} }
return params } catch (error) {
console.error(`Erreur pour l'étudiant ${etudiantId}:`, error)
} }
// 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
} }
function compareSessionNotes(session1, session2) { setMoyennesCalculees(moyennes)
let notes console.log('=== MOYENNES CALCULÉES ===')
if (session2) { console.log('Nombre d\'étudiants:', moyennes.length)
if (session1 < session2.note) { if (moyennes.length > 0) {
notes = session2.note console.log('Premier étudiant:', moyennes[0])
} else {
notes = session1
} }
} else { } catch (error) {
notes = session1 console.error('Erreur lors du chargement des données:', error)
} }
return notes
} }
for (let index = 0; index < etudiants.length; index++) { fetchData()
let total = 0 }, [niveau, scolaire])
let note = 0
let totalCredit = 0 function returnmention(id) {
const found = mention.find((m) => m.id === id)
// Create a new object for each student return found ? found.nom : ''
let modelJson = {
id: '',
nom: '',
prenom: '',
photos: '',
moyenne: '',
mention: '',
anneescolaire: ''
} }
for (let j = 0; j < etudiants[index].length; j++) { // ========================================
modelJson.id = etudiants[index][j].etudiant_id // FONCTION CENTRALISÉE POUR CALCUL DE MOYENNE PONDÉRÉE
modelJson.nom = etudiants[index][j].nom // ========================================
modelJson.prenom = etudiants[index][j].prenom const calculerMoyennePonderee = (matieres) => {
modelJson.photos = etudiants[index][j].photos let totalNotesPonderees = 0
modelJson.mention = etudiants[index][j].mention_id let totalCredits = 0
modelJson.anneescolaire = etudiants[index][j].annee_scolaire
matieres.forEach((matiere) => {
// MODIFICATION: Utiliser la meilleure note (rattrapage si existe) pour la moyenne générale let noteFinale
if (session[index]) {
note += // IMPORTANT: Traiter 0 comme null (pas de rattrapage)
compareSessionNotesForAverage(etudiants[index][j].note, checkNull(session[index][j])) * const noteRepechValide = matiere.noterepech !== null
etudiants[index][j].credit && 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 { } else {
note += etudiants[index][j].note * etudiants[index][j].credit noteFinale = Number(matiere.note)
}
totalCredit += etudiants[index][j].credit
} }
total = note / totalCredit if (noteFinale != null && !isNaN(noteFinale)) {
modelJson.moyenne = total.toFixed(2) totalNotesPonderees += noteFinale * Number(matiere.credit)
totalCredits += Number(matiere.credit)
}
})
// Add the new object to the array return totalCredits > 0 ? totalNotesPonderees / totalCredits : 0
dataToMap.push(modelJson)
} }
function checkNumberSession(id) { function checkNumberSession(id) {
let sessionNumber = 1 const etudiant = moyennesCalculees.find(e => e.id === id)
for (let index = 0; index < session.length; index++) { return etudiant && etudiant.aRattrapage ? 2 : 1
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 theme = createTheme({ const theme = createTheme({
components: { components: {
MuiIconButton: { MuiIconButton: { styleOverrides: { root: { color: 'gray' } } },
styleOverrides: { MuiButton: { styleOverrides: { root: { color: '#121212' } } }
root: {
color: 'gray' // Change the color of toolbar icons
}
}
},
MuiButton: {
styleOverrides: {
root: {
color: '#121212' // Change the color of toolbar icons
}
}
}
} }
}) })
const paginationModel = { page: 0, pageSize: 5 } const paginationModel = { page: 0, pageSize: 5 }
// États pour le menu déroulant
const [anchorEl, setAnchorEl] = useState(null) const [anchorEl, setAnchorEl] = useState(null)
const [selectedStudentId, setSelectedStudentId] = useState(null) const [selectedStudentId, setSelectedStudentId] = useState(null)
const open = Boolean(anchorEl) const open = Boolean(anchorEl)
@ -167,135 +182,54 @@ const Noteclasse = () => {
setAnchorEl(event.currentTarget) setAnchorEl(event.currentTarget)
setSelectedStudentId(studentId) setSelectedStudentId(studentId)
} }
const handleMenuClose = () => { const handleMenuClose = () => {
setAnchorEl(null) setAnchorEl(null)
setSelectedStudentId(null) setSelectedStudentId(null)
} }
const handleSessionTypeSelect = (sessionType) => { const handleSessionTypeSelect = (sessionType) => {
sendData(selectedStudentId, sessionType) sendData(selectedStudentId, sessionType)
handleMenuClose() handleMenuClose()
} }
const columns = [
{ field: 'nom', headerName: 'Nom', width: 170 },
{ field: 'prenom', headerName: 'Prénom', width: 160 },
{ field: 'session', headerName: 'Nombre de Session', width: 180 },
{ field: 'mention', headerName: 'Mention', width: 180 },
{ field: 'moyenne', headerName: 'Moyenne Général', width: 160 },
{
field: 'photos',
headerName: 'Photos',
width: 100,
renderCell: (params) => (
<img
src={params.value} // Correct the access to the image source
alt={'image pdp'}
style={{ width: 50, height: 50, borderRadius: '50%', objectFit: 'cover' }}
/>
)
},
{
field: 'action',
headerName: 'Action',
flex: 1,
renderCell: (params) => (
<div style={{ display: 'flex', gap: '10px' }}>
<Button
color="warning"
variant="contained"
className={`update${params.value}`}
onClick={(event) => handleMenuClick(event, params.value)}
>
<IoNewspaperOutline style={{ fontSize: '20px', color: 'white' }} />
</Button>
<Tooltip
anchorSelect={`.update${params.value}`}
style={{ fontSize: '13px', zIndex: 22 }}
place="bottom-end"
>
Imprimer un relevé de notes
</Tooltip>
</div>
)
}
]
const dataTable = dataToMap.map((data) => ({
id: data.id,
nom: data.nom,
prenom: data.prenom,
photos: data.photos,
mention: returnmention(data.mention),
session: checkNumberSession(data.id),
moyenne: data.moyenne,
action: data.id
}))
const [openCard, setOpenCart] = useState(false) const [openCard, setOpenCart] = useState(false)
const [bolll, setBolll] = useState(false) const [downloadTrigger, setDownloadTrigger] = useState(false)
const [form, setForm] = useState({ const [form, setForm] = useState({ id: '', niveau: '', anneescolaire: '', sessionType: 'ensemble' })
id: '', const [selectedId, setSelectedId] = useState(null)
niveau: '',
anneescolaire: '',
sessionType: 'ensemble' // Par défaut
})
const [selectedId, setSelectedId] = useState(null) // Store id dynamically
const sendData = (id, sessionType = 'ensemble') => { const sendData = (id, sessionType = 'ensemble') => {
setSelectedId(id) setSelectedId(id)
setForm(prevForm => ({ setForm((prev) => ({ ...prev, sessionType }))
...prevForm,
sessionType: sessionType
}))
setOpenCart(true) setOpenCart(true)
} }
useEffect(() => { useEffect(() => {
if (selectedId !== null) { if (selectedId !== null) {
const foundData = dataToMap.find((item) => item.id === selectedId) const foundData = moyennesCalculees.find((item) => item.id === selectedId)
if (foundData) { if (foundData) {
setForm((prevForm) => ({ setForm((prev) => ({
...prevForm, ...prev,
id: foundData.id, id: foundData.id,
anneescolaire: foundData.anneescolaire, anneescolaire: foundData.anneescolaire,
niveau: niveau niveau
})) // Update form with the found object }))
} }
} }
}, [openCard, selectedId]) }, [openCard, selectedId])
console.log(form)
const downloadButton = () => {
setBolll(true)
}
/**
* function to close modal
*/
const handleCloseCart = () => { const handleCloseCart = () => {
setBolll(false) setDownloadTrigger(false)
setOpenCart(false) setOpenCart(false)
} }
const modalReleverNotes = () => { const modalReleverNotes = () => (
return ( <Modal open={openCard} onClose={handleCloseCart} aria-labelledby="modal-title">
<Modal
open={openCard}
onClose={handleCloseCart}
aria-labelledby="modal-title"
aria-describedby="modal-description"
sx={{
overflow: 'auto'
}}
>
<Box <Box
sx={{ sx={{
boxShadow: 24, boxShadow: 24,
p: 4, p: 4,
overflow: 'auto', pl: 0,
maxHeight: '100vh',
overflowY: 'auto',
position: 'relative' position: 'relative'
}} }}
> >
@ -304,12 +238,12 @@ const Noteclasse = () => {
anneescolaire={scolaire} anneescolaire={scolaire}
niveau={form.niveau} niveau={form.niveau}
sessionType={form.sessionType} sessionType={form.sessionType}
refs={bolll} refs={downloadTrigger}
/> />
<Button <Button
color="warning" color="warning"
variant="contained" variant="contained"
onClick={downloadButton} onClick={() => setDownloadTrigger(true)}
sx={{ position: 'absolute', top: '2%', right: '10%' }} sx={{ position: 'absolute', top: '2%', right: '10%' }}
> >
<FaDownload /> <FaDownload />
@ -322,8 +256,9 @@ const Noteclasse = () => {
right: '23%', right: '23%',
border: 'none', border: 'none',
background: 'orange', background: 'orange',
outline: 'none', borderRadius: '100px',
borderRadius: '100px' padding: '5px 10px',
cursor: 'pointer'
}} }}
onClick={handleCloseCart} onClick={handleCloseCart}
> >
@ -332,40 +267,78 @@ const Noteclasse = () => {
</Box> </Box>
</Modal> </Modal>
) )
const columns = [
{ field: 'nom', headerName: 'Nom', width: 170 },
{ field: 'prenom', headerName: 'Prénom', width: 160 },
{ field: 'session', headerName: 'Nombre de Session', width: 180 },
{ field: 'mention', headerName: 'Mention', width: 180 },
{ field: 'moyenne', headerName: 'Moyenne Général', width: 160 },
{
field: 'photos',
headerName: 'Photos',
width: 100,
renderCell: (params) => (
<img
src={params.value}
alt={'image pdp'}
style={{ width: 50, height: 50, borderRadius: '50%', objectFit: 'cover' }}
/>
)
},
{
field: 'action',
headerName: 'Action',
flex: 1,
renderCell: (params) => (
<div style={{ display: 'flex', gap: '10px' }}>
<Button
color="warning"
variant="contained"
className={`update${params.value}`}
onClick={(event) => handleMenuClick(event, params.value)}
>
<IoNewspaperOutline style={{ fontSize: '20px', color: 'white' }} />
</Button>
<Tooltip
anchorSelect={`.update${params.value}`}
style={{ fontSize: '13px', zIndex: 22 }}
place="bottom-end"
>
Imprimer un relevé de notes
</Tooltip>
</div>
)
} }
]
const dataTable = moyennesCalculees.map((data) => ({
id: data.id,
nom: data.nom,
prenom: data.prenom,
photos: data.photos,
mention: returnmention(data.mention),
session: checkNumberSession(data.id),
moyenne: data.moyenne,
action: data.id
}))
return ( return (
<div className={classe.mainHome}> <div className={classe.mainHome}>
{modalReleverNotes()} {modalReleverNotes()}
{/* Menu pour sélectionner le type de session */} <Menu anchorEl={anchorEl} open={open} onClose={handleMenuClose}>
<Menu <MenuItem onClick={() => handleSessionTypeSelect('normale')}>Session Normale</MenuItem>
anchorEl={anchorEl} <MenuItem onClick={() => handleSessionTypeSelect('ensemble')}>Session Rattrapage</MenuItem>
open={open}
onClose={handleMenuClose}
MenuListProps={{
'aria-labelledby': 'session-button',
}}
>
<MenuItem onClick={() => handleSessionTypeSelect('normale')}>
Session Normale
</MenuItem>
<MenuItem onClick={() => handleSessionTypeSelect('ensemble')}>
Session Rattrapage
</MenuItem>
</Menu> </Menu>
<div className={classeHome.header}> <div className={classeHome.header}>
<div className={classe.h1style}> <div className={classe.h1style}>
<div className={classeHome.blockTitle}> <div className={classeHome.blockTitle}>
<h1> <h1>Notes des {niveau} en {scolaire}</h1>
Notes des {niveau} en {scolaire}
</h1>
<div style={{ display: 'flex', gap: '10px' }}> <div style={{ display: 'flex', gap: '10px' }}>
<Link to={`/resultat/${niveau}/${scolaire}`}> <Link to={`/resultat/${niveau}/${scolaire}`}>
<Button color="warning" variant="contained"> <Button color="warning" variant="contained">Resultat</Button>
Resultat
</Button>
</Link> </Link>
<Link to={'#'} onClick={() => window.history.back()}> <Link to={'#'} onClick={() => window.history.back()}>
<Button color="warning" variant="contained"> <Button color="warning" variant="contained">
@ -377,52 +350,24 @@ const Noteclasse = () => {
</div> </div>
</div> </div>
<div className={classeHome.boxEtudiantsCard}> <div className={classeHome.boxEtudiantsCard} style={{ maxHeight: '70vh', overflowY: 'auto' }}>
<div style={{ display: 'flex', justifyContent: 'center' }}> <Paper sx={{ height: 'auto', width: '100%', minHeight: 500 }}>
<Paper
sx={{
height: 'auto', // Auto height to make the grid responsive
width: '100%',
minHeight: 500, // Ensures a minimum height
display: 'flex'
}}
>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '100%', // Make it responsive to full width
flexDirection: 'column' // Stacks content vertically on smaller screens
}}
>
<DataGrid <DataGrid
rows={dataTable} rows={dataTable}
columns={columns} columns={columns}
initialState={{ pagination: { paginationModel } }} initialState={{ pagination: { paginationModel } }}
pageSizeOptions={[5, 10]} pageSizeOptions={[5, 10, 20]}
sx={{
border: 0,
width: '100%', // Ensures the DataGrid takes full width
height: '50%', // Ensures it grows to fit content
minHeight: 400, // Minimum height for the DataGrid
display: 'flex',
justifyContent: 'center',
'@media (max-width: 600px)': {
width: '100%', // 100% width on small screens
height: 'auto' // Allow height to grow with content
}
}}
slots={{ toolbar: GridToolbar }} slots={{ toolbar: GridToolbar }}
localeText={frFR.components.MuiDataGrid.defaultProps.localeText} localeText={frFR.components.MuiDataGrid.defaultProps.localeText}
autoHeight={false}
sx={{ minHeight: 500 }}
loading={moyennesCalculees.length === 0}
/> />
</div>
</ThemeProvider> </ThemeProvider>
</Paper> </Paper>
</div> </div>
</div> </div>
</div>
) )
} }

381
src/renderer/src/components/ReleverNotes.jsx

@ -14,55 +14,46 @@ const ReleverNotes = ({ id, anneescolaire, niveau, sessionType = 'ensemble', ref
const [etudiant, setEtudiant] = useState([]) const [etudiant, setEtudiant] = useState([])
const [matieres, setMatieres] = useState([]) const [matieres, setMatieres] = useState([])
const [notes, setNotes] = useState([]) const [notes, setNotes] = useState([])
const [noteSysteme, setNoteSysteme] = useState(null)
// Fonction pour vérifier si les crédits doivent être affichés
const shouldShowCredits = () => {
return niveau !== 'L1' && niveau !== 'L2'
}
const handleDownloadPDF = async () => { const handleDownloadPDF = async () => {
const input = Telever.current const input = Telever.current
// Set a high scale for better quality
const scale = 3 const scale = 3
html2Canvas(input, { html2Canvas(input, {
scale, // Increase resolution scale,
useCORS: true, // Handle cross-origin images useCORS: true,
allowTaint: true allowTaint: true
}).then((canvas) => { }).then((canvas) => {
const imgData = canvas.toDataURL('image/png') const imgData = canvas.toDataURL('image/png')
// Create a PDF with dimensions matching the captured content
const pdf = new jsPDF({ const pdf = new jsPDF({
orientation: 'portrait', orientation: 'portrait',
unit: 'mm', unit: 'mm',
format: 'a4' format: 'a4'
}) })
const imgWidth = 210 // A4 width in mm const pageWidth = 210
const pageHeight = 297 // A4 height in mm const pageHeight = 297
const imgHeight = (canvas.height * imgWidth) / canvas.width const margin = 5
const printWidth = pageWidth - margin * 2
let position = 0 const imgHeight = (canvas.height * printWidth) / canvas.width
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight, '', 'FAST') if (imgHeight > pageHeight - margin * 2) {
const ratio = (pageHeight - margin * 2) / imgHeight
// Handle multi-page case const adjustedWidth = printWidth * ratio
while (position + imgHeight >= pageHeight) { const adjustedHeight = pageHeight - margin * 2
position -= pageHeight const xOffset = (pageWidth - adjustedWidth) / 2
pdf.addPage() pdf.addImage(imgData, 'PNG', xOffset, margin, adjustedWidth, adjustedHeight, '', 'FAST')
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight, '', 'FAST') } else {
pdf.addImage(imgData, 'PNG', margin, margin, printWidth, imgHeight, '', 'FAST')
} }
pdf.save('document.pdf') pdf.save('releve_de_notes.pdf')
}) })
} }
useEffect(() => { useEffect(() => {
if (!id) { if (!id) {
// id doesn't exist, you might want to retry, or do nothing
// For example, refetch later or show an error
return return
} }
window.etudiants.getSingle({ id }).then((response) => { 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) => { window.notes.noteRelerer({ id, anneescolaire, niveau }).then((response) => {
setNotes(response) setNotes(response)
}) })
window.notesysteme.getSysteme().then((response) => {
if (response) setNoteSysteme(response)
})
}, [id]) }, [id])
const Telever = useRef() const Telever = useRef()
@ -90,42 +85,35 @@ const ReleverNotes = ({ id, anneescolaire, niveau, sessionType = 'ensemble', ref
const [matiereWithSemestreRepech, setMatiereWithSemestreRepech] = useState([]) const [matiereWithSemestreRepech, setMatiereWithSemestreRepech] = useState([])
useEffect(() => { 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) => { const updatedMatieres = notes.noteNormal.map((matiere) => {
// Get the semesters based on the student's niveau
const semesters = getSemestre(matiere.etudiant_niveau) const semesters = getSemestre(matiere.etudiant_niveau)
// Find the matched semestre based on the conditions
const matchedSemestre = notes.semestre.find( const matchedSemestre = notes.semestre.find(
(sem) => (sem) =>
sem.matiere_id === matiere.matiere_id && sem.matiere_id === matiere.matiere_id &&
sem.mention_id === matiere.mention_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 { return {
...matiere, ...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) => { const updatedMatieresRepech = notes.noteRepech.map((matiere) => {
// Get the semesters based on the student's niveau
const semesters = getSemestre(matiere.etudiant_niveau) const semesters = getSemestre(matiere.etudiant_niveau)
// Find the matched semestre based on the conditions
const matchedSemestre = notes.semestre.find( const matchedSemestre = notes.semestre.find(
(sem) => (sem) =>
sem.matiere_id === matiere.matiere_id && sem.matiere_id === matiere.matiere_id &&
sem.mention_id === matiere.mention_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 { return {
...matiere, ...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) { function compareMention(mentionID) {
let statusText let statusText
matieres.map((statu) => { matieres.map((statu) => {
if (mentionID == statu.id) { if (mentionID == statu.id) {
statusText = statu.nom statusText = statu.nom
} }
}) })
return statusText ? statusText.charAt(0).toUpperCase() + statusText.slice(1) : statusText return statusText ? statusText.charAt(0).toUpperCase() + statusText.slice(1) : statusText
} }
// data are finaly get and ready for the traitement below // Fusion des notes normales et de rattrapage
// Merging the arrays based on matiere_id
matiereWithSemestre.forEach((item1) => { matiereWithSemestre.forEach((item1) => {
// Find the corresponding item in array2 based on matiere_id
let matchingItem = matiereWithSemestreRepech.find( let matchingItem = matiereWithSemestreRepech.find(
(item2) => item2.matiere_id === item1.matiere_id (item2) => item2.matiere_id === item1.matiere_id
) )
item1.noterepech = matchingItem ? matchingItem.note : null
// If there's a match, add noterepech from array2, otherwise use the note from array1
item1.noterepech = matchingItem ? matchingItem.note : item1.note
}) })
// step 1 group all by semestre
const groupedDataBySemestre = matiereWithSemestre.reduce((acc, matiere) => { const groupedDataBySemestre = matiereWithSemestre.reduce((acc, matiere) => {
const { semestre } = matiere const { semestre } = matiere
if (!acc[semestre]) { if (!acc[semestre]) {
acc[semestre] = [] acc[semestre] = []
} }
acc[semestre].push(matiere) acc[semestre].push(matiere)
return acc return acc
}, {}) }, {})
// MODIFICATION: Fonction compareMoyenne mise à jour // ========================================
const compareMoyenne = (normal, rattrapage, sessionType) => { // FONCTION CENTRALISÉE POUR CALCUL DE MOYENNE PONDÉRÉE
if (sessionType === 'normale') { // ========================================
// Pour session normale: toujours évaluer selon la note normale uniquement const calculerMoyennePonderee = (matieres, utiliserRattrapage = true) => {
return Number(normal) >= 10 ? 'Admis' : 'Ajourné' let totalNotesPonderees = 0
} else if (sessionType === 'rattrapage') { let totalCredits = 0
// Pour session rattrapage: évaluer selon la note de rattrapage
return Number(rattrapage) >= 10 ? 'Admis' : 'Ajourné' 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 { } else {
// Pour session ensemble: prendre la meilleure des deux notes // Sinon, prendre uniquement la note normale
const bestNote = Math.max(Number(normal), Number(rattrapage)) noteFinale = Number(matiere.note)
return bestNote >= 10 ? 'Admis' : 'Ajourné' }
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 = () => { const TbodyContent = () => {
return ( return (
<> <>
{Object.entries(groupedDataBySemestre).map(([semestre, matieres]) => { {Object.entries(groupedDataBySemestre).map(([semestre, matieres]) => {
// Group by unite_enseignement inside each semestre
const groupedByUnite = matieres.reduce((acc, matiere) => { const groupedByUnite = matieres.reduce((acc, matiere) => {
if (!acc[matiere.ue]) { if (!acc[matiere.ue]) {
acc[matiere.ue] = [] acc[matiere.ue] = []
@ -205,7 +216,6 @@ const ReleverNotes = ({ id, anneescolaire, niveau, sessionType = 'ensemble', ref
<> <>
{matieres.map((matiere, matiereIndex) => ( {matieres.map((matiere, matiereIndex) => (
<tr key={matiere.id} style={{ border: 'none' }}> <tr key={matiere.id} style={{ border: 'none' }}>
{/* Display 'semestre' only for the first row of the first unite_enseignement */}
{uniteIndex === 0 && matiereIndex === 0 && ( {uniteIndex === 0 && matiereIndex === 0 && (
<td <td
rowSpan={ rowSpan={
@ -225,7 +235,6 @@ const ReleverNotes = ({ id, anneescolaire, niveau, sessionType = 'ensemble', ref
</td> </td>
)} )}
{/* Display 'unite_enseignement' only for the first row of each group */}
{matiereIndex === 0 && ( {matiereIndex === 0 && (
<td <td
rowSpan={matieres.length} rowSpan={matieres.length}
@ -240,12 +249,11 @@ const ReleverNotes = ({ id, anneescolaire, niveau, sessionType = 'ensemble', ref
</td> </td>
)} )}
{/* Matiere Data */} <td style={{ borderRight: 'solid 1px black', borderTop: 'solid 1px black', textAlign: 'left', paddingLeft: '4px' }}>
<td style={{ borderRight: 'solid 1px black', borderTop: 'solid 1px black' }}>
{matiere.nom} {matiere.nom}
</td> </td>
{/* Affichage conditionnel des colonnes selon le type de session */} {/* SECTION NORMALE */}
{sessionType !== 'rattrapage' && ( {sessionType !== 'rattrapage' && (
<> <>
<td style={{ borderRight: 'solid 1px black', borderTop: 'solid 1px black' }}> <td style={{ borderRight: 'solid 1px black', borderTop: 'solid 1px black' }}>
@ -254,7 +262,6 @@ const ReleverNotes = ({ id, anneescolaire, niveau, sessionType = 'ensemble', ref
<td style={{ borderRight: 'solid 1px black', borderTop: 'solid 1px black' }}> <td style={{ borderRight: 'solid 1px black', borderTop: 'solid 1px black' }}>
{matiere.note} {matiere.note}
</td> </td>
{/* Moyenne UE pour session normale */}
{matiereIndex === 0 && ( {matiereIndex === 0 && (
<td <td
rowSpan={matieres.length} rowSpan={matieres.length}
@ -266,24 +273,22 @@ const ReleverNotes = ({ id, anneescolaire, niveau, sessionType = 'ensemble', ref
}} }}
className="moyenneUENormale" className="moyenneUENormale"
> >
{( {calculerMoyennePonderee(matieres, false).toFixed(2)}
matieres.reduce((total, matiere) => total + matiere.note, 0) /
matieres.length
).toFixed(2)}
</td> </td>
)} )}
</> </>
)} )}
{/* SECTION RATTRAPAGE */}
{sessionType !== 'normale' && ( {sessionType !== 'normale' && (
<> <>
<td style={{ borderRight: 'solid 1px black', borderTop: 'solid 1px black' }}> <td style={{ borderRight: 'solid 1px black', borderTop: 'solid 1px black' }}>
{matiere.credit} {matiere.noterepech !== null &&
</td> matiere.noterepech !== undefined &&
<td style={{ borderRight: 'solid 1px black', borderTop: 'solid 1px black' }}> Number(matiere.noterepech) > 0
{matiere.noterepech} ? matiere.noterepech
: matiere.note}
</td> </td>
{/* Moyenne UE pour session rattrapage */}
{matiereIndex === 0 && ( {matiereIndex === 0 && (
<td <td
rowSpan={matieres.length} rowSpan={matieres.length}
@ -295,16 +300,13 @@ const ReleverNotes = ({ id, anneescolaire, niveau, sessionType = 'ensemble', ref
}} }}
className="moyenneUERattrapage" className="moyenneUERattrapage"
> >
{( {calculerMoyennePonderee(matieres, true).toFixed(2)}
matieres.reduce((total, matiere) => total + matiere.noterepech, 0) /
matieres.length
).toFixed(2)}
</td> </td>
)} )}
</> </>
)} )}
{/* Display the comparison value only once */} {/* OBSERVATION */}
{matiereIndex === 0 && ( {matiereIndex === 0 && (
<td <td
rowSpan={matieres.length} rowSpan={matieres.length}
@ -315,26 +317,14 @@ const ReleverNotes = ({ id, anneescolaire, niveau, sessionType = 'ensemble', ref
borderTop: 'solid 1px black' borderTop: 'solid 1px black'
}} }}
> >
{compareMoyenne( {compareMoyenne(matieres, sessionType)}
(
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
)}
</td> </td>
)} )}
</tr> </tr>
))} ))}
{/* Add Total Row for 'unite_enseignement' */} {/* Total Row */}
<tr <tr style={{ border: 'none', borderLeft: 'solid 1px black' }}>
style={{ border: 'none', borderLeft: 'solid 1px black' }}
>
<td <td
colSpan={2} colSpan={2}
style={{ style={{
@ -359,7 +349,7 @@ const ReleverNotes = ({ id, anneescolaire, niveau, sessionType = 'ensemble', ref
borderTop: 'solid 1px black' borderTop: 'solid 1px black'
}} }}
> >
{matieres.reduce((total, matiere) => total + matiere.credit, 0)} {matieres.reduce((total, matiere) => total + Number(matiere.credit), 0)}
</td> </td>
<td <td
colSpan={2} colSpan={2}
@ -383,7 +373,6 @@ const ReleverNotes = ({ id, anneescolaire, niveau, sessionType = 'ensemble', ref
borderRight: 'solid 1px black' borderRight: 'solid 1px black'
}} }}
> >
{matieres.reduce((total, matiere) => total + matiere.credit, 0)}
</td> </td>
<td <td
colSpan={2} colSpan={2}
@ -415,52 +404,80 @@ const ReleverNotes = ({ id, anneescolaire, niveau, sessionType = 'ensemble', ref
) )
} }
// MODIFICATION: Fonction totalNotes mise à jour pour utiliser les nouvelles classes // ========================================
// Remplacer la fonction totalNotes() dans ReleverNotes.jsx par celle-ci : // CALCUL DE LA MOYENNE GÉNÉRALE
// TOUJOURS avec les meilleures notes (rattrapage si existe)
// ========================================
const totalNotes = () => { const totalNotes = () => {
// Calculer la moyenne pondérée par les crédits selon le type de session // Session NORMALE uniquement note normale
let totalNotesPonderees = 0
let totalCredits = 0
if (sessionType === 'normale') { if (sessionType === 'normale') {
// Utiliser uniquement les notes normales return calculerMoyennePonderee(matiereWithSemestre, false)
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
}
})
} }
return totalCredits > 0 ? totalNotesPonderees / totalCredits : 0 // Session RATTRAPAGE ou ENSEMBLE meilleure note
return calculerMoyennePonderee(matiereWithSemestre, true)
} }
const [note, setNote] = useState(0) const [note, setNote] = useState(0)
useEffect(() => { useEffect(() => {
setNote(totalNotes()) const moyenne = totalNotes()
}, [TbodyContent, sessionType]) 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 ( return (
<div className={classe.mainHome}> <div className={classe.mainHome}>
@ -468,17 +485,21 @@ const totalNotes = () => {
<div style={{ display: 'flex', justifyContent: 'center' }}> <div style={{ display: 'flex', justifyContent: 'center' }}>
<Paper <Paper
sx={{ sx={{
height: 'auto', width: '210mm',
minHeight: 500, minHeight: '297mm',
display: 'flex', display: 'flex',
padding: '1%', padding: '10mm 12mm',
width: '70%',
marginTop: '2%', marginTop: '2%',
justifyContent: 'center' justifyContent: 'center',
'@media print': {
boxShadow: 'none',
margin: 0,
padding: '8mm 10mm'
}
}} }}
ref={Telever} ref={Telever}
> >
<div style={{ width: '80%' }}> <div style={{ width: '100%' }}>
<div <div
style={{ style={{
display: 'flex', display: 'flex',
@ -490,16 +511,29 @@ const totalNotes = () => {
<img src={logoRelerev2} alt="logo gauche" width={90} /> <img src={logoRelerev2} alt="logo gauche" width={90} />
<div style={{ flex: 1, margin: '0 20px' }}> <div style={{ flex: 1, margin: '0 20px' }}>
<h5 style={{ margin: 0, fontWeight: 'bold', textTransform: 'uppercase',fontSize: '16px' }}> <h5
style={{
margin: 0,
fontWeight: 'bold',
textTransform: 'uppercase',
fontSize: '16px'
}}
>
REPOBLIKAN'I MADAGASIKARA REPOBLIKAN'I MADAGASIKARA
</h5> </h5>
<p style={{ margin: 0, fontStyle: 'italic',fontSize: '11px' }}>Fitiavana Tanindrazana Fandrosoana</p> <p style={{ margin: 0, fontStyle: 'italic', fontSize: '11px' }}>
Fitiavana Tanindrazana Fandrosoana
</p>
<p style={{ margin: 0, fontWeight: 'bold', fontSize: '11px' }}> <p style={{ margin: 0, fontWeight: 'bold', fontSize: '11px' }}>
MINISTÈRE DE L'ENSEIGNEMENT SUPÉRIEUR <br /> MINISTÈRE DE L'ENSEIGNEMENT SUPÉRIEUR <br />
ET DE LA RECHERCHE SCIENTIFIQUE ET DE LA RECHERCHE SCIENTIFIQUE
</p> </p>
<p style={{ margin: 0, fontWeight: 'bold',fontSize: '16px' }}>UNIVERSITÉ DE TOAMASINA</p> <p style={{ margin: 0, fontWeight: 'bold', fontSize: '16px' }}>
<p style={{ margin: 0, fontWeight: 'bold',fontSize: '16px' }}>ÉCOLE SUPÉRIEURE POLYTECHNIQUE</p> UNIVERSITÉ DE TOAMASINA
</p>
<p style={{ margin: 0, fontWeight: 'bold', fontSize: '16px' }}>
ÉCOLE SUPÉRIEURE POLYTECHNIQUE
</p>
</div> </div>
<img src={logoRelerev1} alt="logo droite" width={90} /> <img src={logoRelerev1} alt="logo droite" width={90} />
@ -510,11 +544,10 @@ const totalNotes = () => {
Releve de notes Releve de notes
</h4> </h4>
<hr style={{ margin: 0, border: 'solid 1px black' }} /> <hr style={{ margin: 0, border: 'solid 1px black' }} />
{/* block info */} {/* block info */}
<div style={{ marginTop: '2px', display: 'flex' }}> <div style={{ marginTop: '2px', display: 'flex' }}>
{/* gauche */}
<div style={{ display: 'flex', width: '60%' }}> <div style={{ display: 'flex', width: '60%' }}>
{/* gauche gauche */}
<div style={{ width: '40%' }}> <div style={{ width: '40%' }}>
<span> <span>
<b>Nom</b> <b>Nom</b>
@ -532,7 +565,6 @@ const totalNotes = () => {
<b>Codage</b> <b>Codage</b>
</span> </span>
</div> </div>
{/* gauche droite */}
<div style={{ width: '60%' }}> <div style={{ width: '60%' }}>
<span>: {etudiant.nom}</span> <span>: {etudiant.nom}</span>
<br /> <br />
@ -543,9 +575,7 @@ const totalNotes = () => {
<span>: {etudiant.num_inscription}</span> <span>: {etudiant.num_inscription}</span>
</div> </div>
</div> </div>
{/* droite */}
<div style={{ display: 'flex', width: '40%' }}> <div style={{ display: 'flex', width: '40%' }}>
{/* droite gauche */}
<div style={{ width: '30%' }}> <div style={{ width: '30%' }}>
<span> <span>
<b>Annee Sco</b> <b>Annee Sco</b>
@ -559,7 +589,6 @@ const totalNotes = () => {
<b>Parcours</b> <b>Parcours</b>
</span> </span>
</div> </div>
{/* droite droite */}
<div style={{ width: '70%' }}> <div style={{ width: '70%' }}>
<span>: {etudiant.annee_scolaire}</span> <span>: {etudiant.annee_scolaire}</span>
<br /> <br />
@ -571,7 +600,7 @@ const totalNotes = () => {
</div> </div>
{/* table */} {/* table */}
<table style={{ marginTop: '5px', borderCollapse: 'collapse' }}> <table style={{ marginTop: '5px', borderCollapse: 'collapse', width: '100%', fontSize: '14px' }}>
<thead> <thead>
<tr style={{ borderTop: 'solid 1px black', textAlign: 'center' }}> <tr style={{ borderTop: 'solid 1px black', textAlign: 'center' }}>
<th colSpan={3}></th> <th colSpan={3}></th>
@ -592,19 +621,13 @@ const totalNotes = () => {
<th style={{ borderLeft: 'solid 1px black' }}></th> <th style={{ borderLeft: 'solid 1px black' }}></th>
{sessionType !== 'rattrapage' && ( {sessionType !== 'rattrapage' && (
<th <th colSpan={3} style={{ borderLeft: 'solid 1px black' }}>
colSpan={3}
style={{ borderLeft: 'solid 1px black' }}
>
Normale Normale
</th> </th>
)} )}
{sessionType !== 'normale' && ( {sessionType !== 'normale' && (
<th <th colSpan={3} style={{ borderLeft: 'solid 1px black' }}>
colSpan={3}
style={{ borderLeft: 'solid 1px black' }}
>
Rattrapage Rattrapage
</th> </th>
)} )}
@ -617,14 +640,26 @@ const totalNotes = () => {
style={{ style={{
borderTop: 'solid 1px black', borderTop: 'solid 1px black',
textAlign: 'center', textAlign: 'center',
padding:'20px', padding: '20px'
}}
>
<th
style={{
padding: '1%',
borderLeft: 'solid 1px black',
borderBottom: 'solid 1px black'
}} }}
> >
<th style={{padding: '1%', borderLeft: 'solid 1px black', borderBottom: 'solid 1px black' }}>
semestre semestre
</th> </th>
<th style={{ borderLeft: 'solid 1px black' }}>Unités <br /> d'Enseignement <br />(UE) </th> <th style={{ borderLeft: 'solid 1px black' }}>
<th style={{ borderLeft: 'solid 1px black' }}>Éléments <br /> constitutifs <br />(EC)</th> Unités <br /> d'Enseignement <br />
(UE){' '}
</th>
<th style={{ borderLeft: 'solid 1px black' }}>
Éléments <br /> constitutifs <br />
(EC)
</th>
{sessionType !== 'rattrapage' && ( {sessionType !== 'rattrapage' && (
<> <>
@ -636,7 +671,6 @@ const totalNotes = () => {
{sessionType !== 'normale' && ( {sessionType !== 'normale' && (
<> <>
<th style={{ borderLeft: 'solid 1px black', padding: '0 5px' }}>crédit</th>
<th style={{ borderLeft: 'solid 1px black', padding: '0 5px' }}>Notes</th> <th style={{ borderLeft: 'solid 1px black', padding: '0 5px' }}>Notes</th>
<th style={{ borderLeft: 'solid 1px black', padding: '0 5px' }}>Moyenne</th> <th style={{ borderLeft: 'solid 1px black', padding: '0 5px' }}>Moyenne</th>
</> </>
@ -673,11 +707,13 @@ const totalNotes = () => {
paddingLeft: '2%' paddingLeft: '2%'
}} }}
> >
Mention:{' '} Mention: <span style={{ marginLeft: '3%' }}>{getmentionAfterNotes(note)}</span>
<span style={{ marginLeft: '3%' }}>{getmentionAfterNotes(note)}</span>
</td> </td>
<td colSpan={sessionType === 'ensemble' ? 6 : 4} style={{ textAlign: 'left', paddingLeft: '1%' }}> <td
Décision du Jury:{' '} colSpan={sessionType === 'ensemble' ? 6 : 4}
style={{ textAlign: 'left', paddingLeft: '1%' }}
>
Décision du Jury: <span style={{ marginLeft: '5px' }}>{descisionJury(note, etudiant.niveau, noteSysteme)}</span>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@ -686,7 +722,6 @@ const totalNotes = () => {
<p> <p>
<b>Toamasine le</b> <b>Toamasine le</b>
</p> </p>
{/* texte hidden for place in signature */}
<p style={{ visibility: 'hidden' }}> <p style={{ visibility: 'hidden' }}>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Blanditiis delectus Lorem ipsum dolor sit amet consectetur adipisicing elit. Blanditiis delectus
perspiciatis nisi aliquid eos adipisci cumque amet ratione error voluptatum. perspiciatis nisi aliquid eos adipisci cumque amet ratione error voluptatum.

579
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 classe from '../assets/AllStyleComponents.module.css'
import classeHome from '../assets/Home.module.css' import classeHome from '../assets/Home.module.css'
import Paper from '@mui/material/Paper' 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 { IoMdReturnRight } from 'react-icons/io'
import jsPDF from 'jspdf' import jsPDF from 'jspdf'
import autoTable from 'jspdf-autotable' import autoTable from 'jspdf-autotable'
import { FaDownload } from 'react-icons/fa' import { FaDownload } from 'react-icons/fa'
import logoRelerev1 from '../assets/logorelever.png' import logoRelerev1 from '../assets/logorelever.png'
import logoRelerev2 from '../assets/logorelever2.png' import logoRelerev2 from '../assets/logorelever2.png'
import getSemestre from './function/GetSemestre'
const Resultat = () => { const Resultat = () => {
const { niveau, scolaire } = useParams() const { niveau, scolaire } = useParams()
const formData = { const formData = { niveau, scolaire }
niveau,
scolaire
}
const [etudiants, setEtudiants] = useState([]) const [etudiants, setEtudiants] = useState([])
const [mention, setMention] = useState([]) const [mention, setMention] = useState([])
const [session, setSession] = useState([]) const [session, setSession] = useState([])
const [tabValue, setTabValue] = useState(0) const [tabValue, setTabValue] = useState(0)
// États pour les sélections
const [selectedMatiere, setSelectedMatiere] = useState('') const [selectedMatiere, setSelectedMatiere] = useState('')
const [selectedUE, setSelectedUE] = useState('') const [selectedUE, setSelectedUE] = useState('')
const [selectedMentionId, setSelectedMentionId] = useState('')
const [availableMatieres, setAvailableMatieres] = useState([]) const [availableMatieres, setAvailableMatieres] = useState([])
const [availableUEs, setAvailableUEs] = useState([]) const [availableUEs, setAvailableUEs] = useState([])
const [moyennesRattrapage, setMoyennesRattrapage] = useState([])
useEffect(() => { useEffect(() => {
window.notes.getMoyenne(formData).then((response) => { const fetchData = async () => {
setEtudiants(response) try {
extractMatieresAndUEs(response) 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
}) })
window.noteRepech.getMoyenneRepech(formData).then((response) => { if (notesData && notesData.noteNormal && notesData.noteRepech && notesData.semestre) {
setSession(response) 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 }
}) })
window.mention.getMention().then((response) => { const updatedMatieresRepech = notesData.noteRepech.map((matiere) => {
setMention(response) 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 extractMatieresAndUEs = (data) => {
const matieres = new Set() const matieres = new Map() // key=nomMat, value=mention_id
const ues = new Set() const ues = new Set()
for (let index = 0; index < data.length; index++) { for (let index = 0; index < data.length; index++) {
for (let j = 0; j < data[index].length; j++) { 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}` 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) ues.add(ue)
} }
} }
setAvailableMatieres(Array.from(matieres.keys()))
setAvailableMatieres(Array.from(matieres))
setAvailableUEs(Array.from(ues)) setAvailableUEs(Array.from(ues))
if (matieres.size > 0) setSelectedMatiere(Array.from(matieres.keys())[0])
// Sélectionner la première matière et UE par défaut
if (matieres.size > 0) setSelectedMatiere(Array.from(matieres)[0])
if (ues.size > 0) setSelectedUE(Array.from(ues)[0]) if (ues.size > 0) setSelectedUE(Array.from(ues)[0])
} }
let dataToMap = []
function returnmention(id) { function returnmention(id) {
let mentions const found = mention.find((m) => m.id === id)
for (let index = 0; index < mention.length; index++) { return found ? found.nom : ''
if (mention[index].id == id) {
mentions = mention[index].nom
}
}
return mentions
} }
// Fonction pour déterminer la mention selon la moyenne
function getMentionFromMoyenne(moyenne) { function getMentionFromMoyenne(moyenne) {
const moy = parseFloat(moyenne) const moy = parseFloat(moyenne)
if (moy >= 18) return 'Excellent' if (moy >= 18) return 'Excellent'
@ -87,102 +162,86 @@ const Resultat = () => {
return 'Remise à la famille' return 'Remise à la famille'
} }
function checkNull(params) {
if (params == null || params == undefined) {
return null
}
return params
}
function compareSessionNotes(session1, session2) { function compareSessionNotes(session1, session2) {
let notes
if (session2) { if (session2) {
if (session1 < session2.note) { return session1 < session2.note ? session2.note : session1
notes = session2.note
} else {
notes = session1
} }
} else { return session1
notes = session1
}
return notes
} }
// Traitement des données pour résultat définitif - INCLUANT TOUS LES ÉTUDIANTS const calculerMoyennesSessionNormale = () => {
const moyennes = []
for (let index = 0; index < etudiants.length; index++) { for (let index = 0; index < etudiants.length; index++) {
let total = 0 if (etudiants[index] && etudiants[index][0]) {
let note = 0 let totalNotesPonderees = 0
let totalCredit = 0 let totalCredits = 0
let hasValidNotes = false let hasValidNotes = false
let modelJson = { let modelJson = {
id: '', id: etudiants[index][0].etudiant_id,
nom: '', nom: etudiants[index][0].nom,
prenom: '', prenom: etudiants[index][0].prenom,
photos: '', photos: etudiants[index][0].photos,
moyenne: '', mention: etudiants[index][0].mention_id,
mention: '', anneescolaire: etudiants[index][0].annee_scolaire,
anneescolaire: '' moyenne: 'N/A'
} }
for (let j = 0; j < etudiants[index].length; j++) { for (let j = 0; j < etudiants[index].length; j++) {
modelJson.id = etudiants[index][j].etudiant_id const noteNormale = Number(etudiants[index][j].note)
modelJson.nom = etudiants[index][j].nom const credit = Number(etudiants[index][j].credit)
modelJson.prenom = etudiants[index][j].prenom if (noteNormale != null && !isNaN(noteNormale)) {
modelJson.photos = etudiants[index][j].photos totalNotesPonderees += noteNormale * credit
modelJson.mention = etudiants[index][j].mention_id totalCredits += credit
modelJson.anneescolaire = etudiants[index][j].annee_scolaire
let currentNote = etudiants[index][j].note
if (session[index]) {
currentNote = compareSessionNotes(etudiants[index][j].note, checkNull(session[index][j]))
}
// 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 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
})
}
// Calculer la moyenne même si certaines notes manquent const sortedStudents = calculerMoyennesSessionNormale()
if (hasValidNotes && totalCredit > 0) {
total = note / totalCredit const getResultsRattrapageAdmis = () => {
modelJson.moyenne = total.toFixed(2) return moyennesRattrapage.filter(e => e.mention == selectedMentionId && e.admis).sort((a, b) => parseFloat(b.moyenne) - parseFloat(a.moyenne))
} else {
modelJson.moyenne = 'N/A'
} }
dataToMap.push(modelJson) const getResultsRattrapageNonAdmis = () => {
return moyennesRattrapage.filter(e => e.mention == selectedMentionId && !e.admis).sort((a, b) => parseFloat(b.moyenne) - parseFloat(a.moyenne))
}
const getResultsByMention = () => {
return sortedStudents.filter(s => s.mention == selectedMentionId)
} }
// Fonction pour obtenir les résultats par matière sélectionnée
const getResultsByMatiere = () => { const getResultsByMatiere = () => {
const results = [] const results = []
for (let index = 0; index < etudiants.length; index++) { for (let index = 0; index < etudiants.length; index++) {
for (let j = 0; j < etudiants[index].length; j++) { for (let j = 0; j < etudiants[index].length; j++) {
const matiere = etudiants[index][j].matiere || `Matière ${j + 1}` const nomMat = etudiants[index][j].nomMat || `Matière ${j + 1}`
const mentionId = etudiants[index][j].mention_id
if (matiere === selectedMatiere) { if (nomMat === selectedMatiere && mentionId == selectedMentionId) {
let finalNote = etudiants[index][j].note let finalNote = etudiants[index][j].note
if (session[index] && session[index][j]) { if (session[index] && session[index][j]) {
finalNote = compareSessionNotes(etudiants[index][j].note, session[index][j]) finalNote = compareSessionNotes(etudiants[index][j].note, session[index][j])
} }
results.push({ results.push({
id: etudiants[index][j].etudiant_id, id: etudiants[index][j].etudiant_id,
nom: etudiants[index][j].nom, nom: etudiants[index][j].nom,
prenom: etudiants[index][j].prenom, prenom: etudiants[index][j].prenom,
note: finalNote != null ? finalNote.toFixed(2) : 'N/A', note: finalNote != null ? finalNote.toFixed(2) : 'N/A',
credit: etudiants[index][j].credit, credit: etudiants[index][j].credit
mention: returnmention(etudiants[index][j].mention_id)
}) })
} }
} }
} }
return results.sort((a, b) => { return results.sort((a, b) => {
const noteA = a.note === 'N/A' ? -1 : parseFloat(a.note) const noteA = a.note === 'N/A' ? -1 : parseFloat(a.note)
const noteB = b.note === 'N/A' ? -1 : parseFloat(b.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 getResultsByUE = () => {
const groupedStudents = {} const groupedStudents = {}
const matieresInUE = new Set() 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 index = 0; index < etudiants.length; index++) {
for (let j = 0; j < etudiants[index].length; j++) { for (let j = 0; j < etudiants[index].length; j++) {
const ue = etudiants[index][j].ue || `UE${Math.floor(j / 2) + 1}` const ue = etudiants[index][j].ue || `UE${Math.floor(j / 2) + 1}`
const matiere = etudiants[index][j].matiere || `Matière ${j + 1}` const matiere = etudiants[index][j].nomMat || `Matière ${j + 1}`
const mentionId = etudiants[index][j].mention_id
if (ue === selectedUE) { if (ue === selectedUE && mentionId == selectedMentionId) {
matieresInUE.add(matiere) matieresInUE.add(matiere)
const etudiantId = etudiants[index][j].etudiant_id const etudiantId = etudiants[index][j].etudiant_id
if (!groupedStudents[etudiantId]) { if (!groupedStudents[etudiantId]) {
groupedStudents[etudiantId] = { groupedStudents[etudiantId] = {
id: etudiantId, id: etudiantId,
@ -216,12 +271,10 @@ const Resultat = () => {
hasValidNotes: false hasValidNotes: false
} }
} }
let finalNote = etudiants[index][j].note let finalNote = etudiants[index][j].note
if (session[index] && session[index][j]) { if (session[index] && session[index][j]) {
finalNote = compareSessionNotes(etudiants[index][j].note, session[index][j]) finalNote = compareSessionNotes(etudiants[index][j].note, session[index][j])
} }
if (finalNote != null && finalNote != undefined && !isNaN(finalNote)) { if (finalNote != null && finalNote != undefined && !isNaN(finalNote)) {
groupedStudents[etudiantId].matieres[matiere] = finalNote.toFixed(2) groupedStudents[etudiantId].matieres[matiere] = finalNote.toFixed(2)
groupedStudents[etudiantId].totalNote += finalNote * etudiants[index][j].credit groupedStudents[etudiantId].totalNote += finalNote * etudiants[index][j].credit
@ -233,14 +286,10 @@ const Resultat = () => {
} }
} }
} }
const results = Object.values(groupedStudents).map(student => ({ const results = Object.values(groupedStudents).map(student => ({
...student, ...student,
moyenneUE: student.hasValidNotes && student.totalCredit > 0 moyenneUE: student.hasValidNotes && student.totalCredit > 0 ? (student.totalNote / student.totalCredit).toFixed(2) : 'N/A'
? (student.totalNote / student.totalCredit).toFixed(2)
: 'N/A'
})) }))
return { return {
students: results.sort((a, b) => { students: results.sort((a, b) => {
const moyA = a.moyenneUE === 'N/A' ? -1 : parseFloat(a.moyenneUE) 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) => { const handleTabChange = (event, newValue) => {
setTabValue(newValue) setTabValue(newValue)
} }
@ -264,15 +307,9 @@ const Resultat = () => {
const print = () => { const print = () => {
const generatePDF = () => { const generatePDF = () => {
try { try {
const pdf = new jsPDF({ const pdf = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' })
orientation: 'portrait',
unit: 'mm',
format: 'a4'
})
pdf.addImage(logoRelerev1, 'PNG', 175, 5, 32, 30) pdf.addImage(logoRelerev1, 'PNG', 175, 5, 32, 30)
pdf.addImage(logoRelerev2, 'PNG', 10, 5, 40, 30) pdf.addImage(logoRelerev2, 'PNG', 10, 5, 40, 30)
pdf.setFontSize(10) pdf.setFontSize(10)
pdf.text('REPOBLIKAN\'I MADAGASIKARA', 105, 10, { align: 'center' }) pdf.text('REPOBLIKAN\'I MADAGASIKARA', 105, 10, { align: 'center' })
pdf.text('Fitiavana-Tanindrazana-Fandrosoana', 105, 14, { 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('********************', 105, 30, { align: 'center' })
pdf.text('UNIVERSITÉ DE TOAMASINA', 105, 34, { align: 'center' }) pdf.text('UNIVERSITÉ DE TOAMASINA', 105, 34, { align: 'center' })
pdf.text('ÉCOLE SUPÉRIEURE POLYTECHNIQUE', 105, 38, { align: 'center' }) pdf.text('ÉCOLE SUPÉRIEURE POLYTECHNIQUE', 105, 38, { align: 'center' })
const tableId = tabValue === 0 ? '#mentionTable' : tabValue === 1 ? '#rattrapageAdmisTable' : tabValue === 2 ? '#rattrapageNonAdmisTable' : tabValue === 3 ? '#subjectTable' : '#ueTable'
const tableId = tabValue === 0 ? '#resultTable' : tabValue === 1 ? '#subjectTable' : '#ueTable'
autoTable(pdf, { autoTable(pdf, {
html: tableId, html: tableId,
startY: 50, startY: 50,
theme: 'grid', theme: 'grid',
headStyles: { headStyles: { fillColor: [255, 255, 255], halign: 'center', fontStyle: 'bold', textColor: [0, 0, 0], lineColor: [0, 0, 0], lineWidth: 0.5 },
fillColor: [255, 255, 255], // Fond blanc styles: { fontSize: 8, cellPadding: 2, halign: 'center', lineColor: [0, 0, 0], lineWidth: 0.5 },
halign: 'center', bodyStyles: { lineColor: [0, 0, 0], lineWidth: 0.5 }
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
}
}) })
if (tabValue === 1) {
const suffix = tabValue === 0 ? 'definitif' : const admis = getResultsRattrapageAdmis()
tabValue === 1 ? `par-matiere-${selectedMatiere}` : const finalY = pdf.lastAutoTable.finalY || 50
`par-ue-${selectedUE}` 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`) pdf.save(`Resultat-${suffix}-${niveau}-${scolaire}.pdf`)
} catch (error) { } catch (error) {
console.error('Error generating PDF:', error) console.error('Error generating PDF:', error)
@ -322,27 +346,9 @@ const Resultat = () => {
} }
const renderHeader = () => ( const renderHeader = () => (
<div <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', marginBottom: '20px', position: 'relative' }}>
style={{
display: 'flex',
alignItems: 'flex-start',
justifyContent: 'space-between',
marginBottom: '20px',
position: 'relative'
}}
>
<img src={logoRelerev2} alt="Logo gauche" width={90} height={90} /> <img src={logoRelerev2} alt="Logo gauche" width={90} height={90} />
<div style={{ position: 'absolute', left: '50%', transform: 'translateX(-50%)', textAlign: 'center', fontSize: '10px', lineHeight: '1.2' }}>
<div
style={{
position: 'absolute',
left: '50%',
transform: 'translateX(-50%)',
textAlign: 'center',
fontSize: '10px',
lineHeight: '1.2'
}}
>
<div style={{ fontWeight: 'bold' }}>REPOBLIKAN'I MADAGASIKARA</div> <div style={{ fontWeight: 'bold' }}>REPOBLIKAN'I MADAGASIKARA</div>
<div style={{ fontStyle: 'italic' }}>Fitiavana-Tanindrazana-Fandrosoana</div> <div style={{ fontStyle: 'italic' }}>Fitiavana-Tanindrazana-Fandrosoana</div>
<div>********************</div> <div>********************</div>
@ -352,23 +358,28 @@ const Resultat = () => {
<div style={{ fontWeight: 'bold' }}>UNIVERSITÉ DE TOAMASINA</div> <div style={{ fontWeight: 'bold' }}>UNIVERSITÉ DE TOAMASINA</div>
<div style={{ fontWeight: 'bold' }}>ÉCOLE SUPÉRIEURE POLYTECHNIQUE</div> <div style={{ fontWeight: 'bold' }}>ÉCOLE SUPÉRIEURE POLYTECHNIQUE</div>
</div> </div>
<img src={logoRelerev1} alt="Logo droite" width={110} height={90} /> <img src={logoRelerev1} alt="Logo droite" width={110} height={90} />
</div> </div>
) )
const renderResultDefinitif = () => ( const renderResultParMention = () => {
<table const results = getResultsByMention()
className="table table-bordered table-striped text-center shadow-sm" const selectedMentionName = returnmention(selectedMentionId)
id="resultTable" return (
style={{ fontSize: '12px' }} <>
> <div style={{ marginBottom: '20px' }}>
<FormControl fullWidth>
<InputLabel>Sélectionner une mention</InputLabel>
<Select value={selectedMentionId} onChange={(e) => setSelectedMentionId(e.target.value)} label="Sélectionner une mention">
{mention.map((m) => (<MenuItem key={m.id} value={m.id}>{m.nom}</MenuItem>))}
</Select>
</FormControl>
</div>
<table className="table table-bordered table-striped text-center shadow-sm" id="mentionTable" style={{ fontSize: '12px' }}>
<thead className="table-secondary"> <thead className="table-secondary">
<tr> <tr>
<td colSpan={5} className="py-3" style={{ backgroundColor: '#f8f9fa' }}> <td colSpan={5} className="py-3" style={{ backgroundColor: '#f8f9fa' }}>
<h6 style={{ margin: 0, fontWeight: 'bold' }}> <h6 style={{ margin: 0, fontWeight: 'bold' }}>Session Normale - Résultat {niveau} pour la mention : {selectedMentionName} ({results.length} étudiant{results.length > 1 ? 's' : ''})</h6>
Résultat Définitif : {niveau} admis en {niveau === 'L1' ? 'L2' : niveau === 'L2' ? 'L3' : 'Master'} par ordre de mérite
</h6>
</td> </td>
</tr> </tr>
<tr style={{ backgroundColor: '#e9ecef' }}> <tr style={{ backgroundColor: '#e9ecef' }}>
@ -380,54 +391,134 @@ const Resultat = () => {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{sortedStudents.map((sorted, index) => ( {results.length > 0 ? (results.map((sorted, index) => (
<tr key={sorted.id}> <tr key={sorted.id}>
<td style={{ fontWeight: 'bold' }}>{index + 1}.</td> <td style={{ fontWeight: 'bold' }}>{index + 1}.</td>
<td style={{ textAlign: 'left', paddingLeft: '10px', fontWeight: 'bold' }}>{sorted.nom}</td> <td style={{ textAlign: 'left', paddingLeft: '10px', fontWeight: 'bold' }}>{sorted.nom}</td>
<td style={{ textAlign: 'left', paddingLeft: '10px' }}>{sorted.prenom}</td> <td style={{ textAlign: 'left', paddingLeft: '10px' }}>{sorted.prenom}</td>
<td style={{ fontWeight: 'bold' }}>{sorted.moyenne}</td> <td style={{ fontWeight: 'bold' }}>{sorted.moyenne}</td>
<td style={{ fontWeight: 'bold' }}> <td style={{ fontWeight: 'bold' }}>{sorted.moyenne !== 'N/A' ? getMentionFromMoyenne(sorted.moyenne) : 'N/A'}</td>
{sorted.moyenne !== 'N/A' ? getMentionFromMoyenne(sorted.moyenne) : 'N/A'} </tr>
))) : (<tr><td colSpan={5} style={{ textAlign: 'center', padding: '20px', fontStyle: 'italic' }}>Aucun étudiant avec la mention "{selectedMentionName}"</td></tr>)}
</tbody>
</table>
</>
)
}
const renderRattrapageAdmis = () => {
const results = getResultsRattrapageAdmis()
const selectedMentionName = returnmention(selectedMentionId)
return (
<>
<div style={{ marginBottom: '20px' }}>
<FormControl fullWidth>
<InputLabel>Sélectionner une mention</InputLabel>
<Select value={selectedMentionId} onChange={(e) => setSelectedMentionId(e.target.value)} label="Sélectionner une mention">
{mention.map((m) => (<MenuItem key={m.id} value={m.id}>{m.nom}</MenuItem>))}
</Select>
</FormControl>
</div>
<table className="table table-bordered table-striped text-center shadow-sm" id="rattrapageAdmisTable" style={{ fontSize: '12px' }}>
<thead className="table-secondary">
<tr>
<td colSpan={5} className="py-3" style={{ backgroundColor: '#f8f9fa' }}>
<h6 style={{ margin: 0, fontWeight: 'bold' }}>Session de Rattrapage - Étudiants ADMIS {niveau} pour la mention : {selectedMentionName} ({results.length} étudiant{results.length > 1 ? 's' : ''})</h6>
</td> </td>
</tr> </tr>
))} <tr style={{ backgroundColor: '#e9ecef' }}>
<th style={{ width: '10%', fontWeight: 'bold' }}>RANG</th>
<th style={{ width: '25%', fontWeight: 'bold' }}>NOMS</th>
<th style={{ width: '30%', fontWeight: 'bold' }}>PRÉNOMS</th>
<th style={{ width: '15%', fontWeight: 'bold' }}>Moyenne</th>
<th style={{ width: '20%', fontWeight: 'bold' }}>Mention</th>
</tr>
</thead>
<tbody>
{results.length > 0 ? (results.map((item, index) => (
<tr key={item.id}>
<td style={{ fontWeight: 'bold' }}>{index + 1}.</td>
<td style={{ textAlign: 'left', paddingLeft: '10px', fontWeight: 'bold' }}>{item.nom}</td>
<td style={{ textAlign: 'left', paddingLeft: '10px' }}>{item.prenom}</td>
<td style={{ fontWeight: 'bold' }}>{item.moyenne}</td>
<td style={{ fontWeight: 'bold' }}>{getMentionFromMoyenne(item.moyenne)}</td>
</tr>
))) : (<tr><td colSpan={5} style={{ textAlign: 'center', padding: '20px', fontStyle: 'italic' }}>Aucun étudiant admis pour cette mention</td></tr>)}
</tbody>
</table>
{results.length > 0 && (<div style={{ textAlign: 'center', marginTop: '20px', fontSize: '14px', fontWeight: 'bold', padding: '10px', backgroundColor: '#d4edda', borderRadius: '5px' }}>Arrêt la liste des étudiants admis à {results.length}</div>)}
</>
)
}
const renderRattrapageNonAdmis = () => {
const results = getResultsRattrapageNonAdmis()
const selectedMentionName = returnmention(selectedMentionId)
return (
<>
<div style={{ marginBottom: '20px' }}>
<FormControl fullWidth>
<InputLabel>Sélectionner une mention</InputLabel>
<Select value={selectedMentionId} onChange={(e) => setSelectedMentionId(e.target.value)} label="Sélectionner une mention">
{mention.map((m) => (<MenuItem key={m.id} value={m.id}>{m.nom}</MenuItem>))}
</Select>
</FormControl>
</div>
<table className="table table-bordered table-striped text-center shadow-sm" id="rattrapageNonAdmisTable" style={{ fontSize: '12px' }}>
<thead className="table-secondary">
<tr>
<td colSpan={5} className="py-3" style={{ backgroundColor: '#f8f9fa' }}>
<h6 style={{ margin: 0, fontWeight: 'bold' }}>Session de Rattrapage - Étudiants NON ADMIS {niveau} pour la mention : {selectedMentionName} ({results.length} étudiant{results.length > 1 ? 's' : ''})</h6>
</td>
</tr>
<tr style={{ backgroundColor: '#e9ecef' }}>
<th style={{ width: '10%', fontWeight: 'bold' }}>RANG</th>
<th style={{ width: '25%', fontWeight: 'bold' }}>NOMS</th>
<th style={{ width: '30%', fontWeight: 'bold' }}>PRÉNOMS</th>
<th style={{ width: '15%', fontWeight: 'bold' }}>Moyenne</th>
<th style={{ width: '20%', fontWeight: 'bold' }}>Mention</th>
</tr>
</thead>
<tbody>
{results.length > 0 ? (results.map((item, index) => (
<tr key={item.id}>
<td style={{ fontWeight: 'bold' }}>{index + 1}.</td>
<td style={{ textAlign: 'left', paddingLeft: '10px', fontWeight: 'bold' }}>{item.nom}</td>
<td style={{ textAlign: 'left', paddingLeft: '10px' }}>{item.prenom}</td>
<td style={{ fontWeight: 'bold' }}>{item.moyenne}</td>
<td style={{ fontWeight: 'bold' }}>{getMentionFromMoyenne(item.moyenne)}</td>
</tr>
))) : (<tr><td colSpan={5} style={{ textAlign: 'center', padding: '20px', fontStyle: 'italic' }}>Aucun étudiant non admis pour cette mention</td></tr>)}
</tbody> </tbody>
</table> </table>
</>
) )
}
const renderResultParMatiere = () => { const renderResultParMatiere = () => {
const results = getResultsByMatiere() const results = getResultsByMatiere()
const selectedMentionName = returnmention(selectedMentionId)
return ( return (
<> <>
<div style={{ marginBottom: '20px' }}> <div style={{ marginBottom: '20px', display: 'flex', gap: '16px' }}>
<FormControl fullWidth>
<InputLabel>Sélectionner une mention</InputLabel>
<Select value={selectedMentionId} onChange={(e) => setSelectedMentionId(e.target.value)} label="Sélectionner une mention">
{mention.map((m) => (<MenuItem key={m.id} value={m.id}>{m.nom}</MenuItem>))}
</Select>
</FormControl>
<FormControl fullWidth> <FormControl fullWidth>
<InputLabel>Sélectionner une matière</InputLabel> <InputLabel>Sélectionner une matière</InputLabel>
<Select <Select value={selectedMatiere} onChange={(e) => setSelectedMatiere(e.target.value)} label="Sélectionner une matière">
value={selectedMatiere} {availableMatieres.map((matiere) => (<MenuItem key={matiere} value={matiere}>{matiere}</MenuItem>))}
onChange={(e) => setSelectedMatiere(e.target.value)}
label="Sélectionner une matière"
>
{availableMatieres.map((matiere) => (
<MenuItem key={matiere} value={matiere}>
{matiere}
</MenuItem>
))}
</Select> </Select>
</FormControl> </FormControl>
</div> </div>
<table className="table table-bordered table-striped text-center shadow-sm" id="subjectTable" style={{ fontSize: '12px' }}>
<table
className="table table-bordered table-striped text-center shadow-sm"
id="subjectTable"
style={{ fontSize: '12px' }}
>
<thead className="table-secondary"> <thead className="table-secondary">
<tr> <tr>
<td colSpan={4} className="py-3" style={{ backgroundColor: '#f8f9fa' }}> <td colSpan={4} className="py-3" style={{ backgroundColor: '#f8f9fa' }}>
<h6 style={{ margin: 0, fontWeight: 'bold' }}> <h6 style={{ margin: 0, fontWeight: 'bold' }}>Résultat {niveau} Mention : {selectedMentionName} Matière : {selectedMatiere}</h6>
Résultat pour la matière : {selectedMatiere}
</h6>
</td> </td>
</tr> </tr>
<tr style={{ backgroundColor: '#e9ecef' }}> <tr style={{ backgroundColor: '#e9ecef' }}>
@ -454,46 +545,35 @@ const Resultat = () => {
const renderResultParUE = () => { const renderResultParUE = () => {
const { students, matieres } = getResultsByUE() const { students, matieres } = getResultsByUE()
const selectedMentionName = returnmention(selectedMentionId)
return ( return (
<> <>
<div style={{ marginBottom: '20px' }}> <div style={{ marginBottom: '20px', display: 'flex', gap: '16px' }}>
<FormControl fullWidth>
<InputLabel>Sélectionner une mention</InputLabel>
<Select value={selectedMentionId} onChange={(e) => setSelectedMentionId(e.target.value)} label="Sélectionner une mention">
{mention.map((m) => (<MenuItem key={m.id} value={m.id}>{m.nom}</MenuItem>))}
</Select>
</FormControl>
<FormControl fullWidth> <FormControl fullWidth>
<InputLabel>Sélectionner une UE</InputLabel> <InputLabel>Sélectionner une UE</InputLabel>
<Select <Select value={selectedUE} onChange={(e) => setSelectedUE(e.target.value)} label="Sélectionner une UE">
value={selectedUE} {availableUEs.map((ue) => (<MenuItem key={ue} value={ue}>{ue}</MenuItem>))}
onChange={(e) => setSelectedUE(e.target.value)}
label="Sélectionner une UE"
>
{availableUEs.map((ue) => (
<MenuItem key={ue} value={ue}>
{ue}
</MenuItem>
))}
</Select> </Select>
</FormControl> </FormControl>
</div> </div>
<table className="table table-bordered table-striped text-center shadow-sm" id="ueTable" style={{ fontSize: '12px' }}>
<table
className="table table-bordered table-striped text-center shadow-sm"
id="ueTable"
style={{ fontSize: '12px' }}
>
<thead className="table-secondary"> <thead className="table-secondary">
<tr> <tr>
<td colSpan={3 + matieres.length} className="py-3" style={{ backgroundColor: '#f8f9fa' }}> <td colSpan={3 + matieres.length} className="py-3" style={{ backgroundColor: '#f8f9fa' }}>
<h6 style={{ margin: 0, fontWeight: 'bold' }}> <h6 style={{ margin: 0, fontWeight: 'bold' }}>Résultat {niveau} Mention : {selectedMentionName} UE : {selectedUE}</h6>
Résultat pour l'UE : {selectedUE}
</h6>
</td> </td>
</tr> </tr>
<tr style={{ backgroundColor: '#e9ecef' }}> <tr style={{ backgroundColor: '#e9ecef' }}>
<th style={{ fontWeight: 'bold' }}>RANG</th> <th style={{ fontWeight: 'bold' }}>RANG</th>
<th style={{ fontWeight: 'bold' }}>NOMS</th> <th style={{ fontWeight: 'bold' }}>NOMS</th>
<th style={{ fontWeight: 'bold' }}>PRÉNOMS</th> <th style={{ fontWeight: 'bold' }}>PRÉNOMS</th>
{matieres.map((matiere) => ( {matieres.map((matiere) => (<th key={matiere} style={{ fontWeight: 'bold' }}>{matiere}</th>))}
<th key={matiere} style={{ fontWeight: 'bold' }}>{matiere}</th>
))}
<th style={{ fontWeight: 'bold' }}>MOYENNE UE</th> <th style={{ fontWeight: 'bold' }}>MOYENNE UE</th>
</tr> </tr>
</thead> </thead>
@ -503,11 +583,7 @@ const Resultat = () => {
<td style={{ fontWeight: 'bold' }}>{index + 1}.</td> <td style={{ fontWeight: 'bold' }}>{index + 1}.</td>
<td style={{ textAlign: 'left', paddingLeft: '10px', fontWeight: 'bold' }}>{student.nom}</td> <td style={{ textAlign: 'left', paddingLeft: '10px', fontWeight: 'bold' }}>{student.nom}</td>
<td style={{ textAlign: 'left', paddingLeft: '10px' }}>{student.prenom}</td> <td style={{ textAlign: 'left', paddingLeft: '10px' }}>{student.prenom}</td>
{matieres.map((matiere) => ( {matieres.map((matiere) => (<td key={matiere} style={{ fontWeight: 'bold' }}>{student.matieres[matiere] || 'N/A'}</td>))}
<td key={matiere} style={{ fontWeight: 'bold' }}>
{student.matieres[matiere] || 'N/A'}
</td>
))}
<td style={{ fontWeight: 'bold', backgroundColor: '#fff3cd' }}>{student.moyenneUE}</td> <td style={{ fontWeight: 'bold', backgroundColor: '#fff3cd' }}>{student.moyenneUE}</td>
</tr> </tr>
))} ))}
@ -522,9 +598,7 @@ const Resultat = () => {
<div className={classeHome.header}> <div className={classeHome.header}>
<div className={classe.h1style}> <div className={classe.h1style}>
<div className={classeHome.blockTitle}> <div className={classeHome.blockTitle}>
<h1> <h1>Resultat des {niveau} en {scolaire}</h1>
Resultat des {niveau} en {scolaire}
</h1>
<div style={{ display: 'flex', gap: '10px' }}> <div style={{ display: 'flex', gap: '10px' }}>
<Link to={'#'} onClick={print}> <Link to={'#'} onClick={print}>
<Button color="warning" variant="contained"> <Button color="warning" variant="contained">
@ -541,39 +615,26 @@ const Resultat = () => {
</div> </div>
</div> </div>
</div> </div>
<div className={classeHome.boxEtudiantsCard}> <div className={classeHome.boxEtudiantsCard}>
<Paper <Paper sx={{ height: 'auto', width: '100%', display: 'flex', flexDirection: 'column', padding: '2%' }}>
sx={{
height: 'auto',
width: '100%',
display: 'flex',
flexDirection: 'column',
padding: '2%'
}}
>
{renderHeader()} {renderHeader()}
<div style={{ marginBottom: '15px', fontSize: '12px' }}> <div style={{ marginBottom: '15px', fontSize: '12px' }}>
<div><strong>Parcours :</strong> GC</div> <div><strong>Parcours :</strong> GC</div>
<div><strong>Niveau :</strong> {niveau}</div> <div><strong>Niveau :</strong> {niveau}</div>
<div><strong>Année Universitaire :</strong> {scolaire}</div> <div><strong>Année Universitaire :</strong> {scolaire}</div>
</div> </div>
<Tabs value={tabValue} onChange={handleTabChange} centered sx={{ marginBottom: '20px' }}>
<Tabs <Tab label="Session Normale" />
value={tabValue} <Tab label="Rattrapage - Admis" />
onChange={handleTabChange} <Tab label="Rattrapage - Non Admis" />
centered
sx={{ marginBottom: '20px' }}
>
<Tab label="Résultat Définitif" />
<Tab label="Par Matière" /> <Tab label="Par Matière" />
<Tab label="Par UE" /> <Tab label="Par UE" />
</Tabs> </Tabs>
{tabValue === 0 && renderResultParMention()}
{tabValue === 0 && renderResultDefinitif()} {tabValue === 1 && renderRattrapageAdmis()}
{tabValue === 1 && renderResultParMatiere()} {tabValue === 2 && renderRattrapageNonAdmis()}
{tabValue === 2 && renderResultParUE()} {tabValue === 3 && renderResultParMatiere()}
{tabValue === 4 && renderResultParUE()}
</Paper> </Paper>
</div> </div>
</div> </div>

10
src/renderer/src/components/Sidenav.jsx

@ -18,6 +18,7 @@ import { BsCalendar2Date } from 'react-icons/bs'
import { SiVitest } from 'react-icons/si' import { SiVitest } from 'react-icons/si'
import { GrManual } from 'react-icons/gr' import { GrManual } from 'react-icons/gr'
import { FaClipboardList } from 'react-icons/fa6' import { FaClipboardList } from 'react-icons/fa6'
import { FaMoneyBillWave } from 'react-icons/fa'
const Sidenav = () => { const Sidenav = () => {
const [anchorEl, setAnchorEl] = useState(null) const [anchorEl, setAnchorEl] = useState(null)
@ -268,6 +269,15 @@ const isAdmin = () => {
<MdAdminPanelSettings /> Admin <MdAdminPanelSettings /> Admin
</Link> </Link>
</MenuItem> </MenuItem>
<MenuItem>
<Link
to="/configecolage"
style={{ color: 'black', textDecoration: 'none' }}
onClick={handleClose}
>
<FaMoneyBillWave /> Config Ecolage
</Link>
</MenuItem>
<MenuItem> <MenuItem>
<Link <Link
to="/para" to="/para"

351
src/renderer/src/components/SingleNotes.jsx

@ -1,71 +1,69 @@
import React, { useEffect, useRef, useState } from 'react' import React, { useEffect, useRef, useState } from 'react'
import { useParams, Link } from 'react-router-dom' import { useParams, Link } from 'react-router-dom'
import { Box, InputAdornment, Typography, Modal, TextField, Grid, Button } from '@mui/material'
import Paper from '@mui/material/Paper' import Paper from '@mui/material/Paper'
import classe from '../assets/AllStyleComponents.module.css'
import classeHome from '../assets/Home.module.css'
import { IoMdReturnRight } from 'react-icons/io' import { IoMdReturnRight } from 'react-icons/io'
import { Button } from '@mui/material'
import { Box, InputAdornment, Typography, Modal, TextField, Grid } from '@mui/material'
import { CgNotes } from 'react-icons/cg' import { CgNotes } from 'react-icons/cg'
import classe from '../assets/AllStyleComponents.module.css'
import classeHome from '../assets/Home.module.css'
import svgSuccess from '../assets/success.svg' import svgSuccess from '../assets/success.svg'
import svgError from '../assets/error.svg' import svgError from '../assets/error.svg'
const SingleNotes = () => { const SingleNotes = () => {
let { id, niveau, scolaire } = useParams() const { id, niveau, scolaire } = useParams()
const [notes, setNotes] = useState([]) const [notes, setNotes] = useState([])
const [notesRepech, setNotesRepech] = useState([]) const [notesRepech, setNotesRepech] = useState([])
const [formData, setFormData] = useState({}) const [formData, setFormData] = useState({})
const [formData2, setFormData2] = useState({}) const [formData2, setFormData2] = useState({})
const [etudiant, setEtudiant] = useState([]) const [etudiant, setEtudiant] = useState({})
let annee_scolaire = scolaire
const [screenRattrapage, setScreenRattrapage] = useState(false) 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(() => { useEffect(() => {
window.etudiants.getSingle({ id }).then((response) => { window.etudiants.getSingle({ id }).then(setEtudiant)
setEtudiant(response)
})
}, []) }, [])
useEffect(() => { useEffect(() => {
let mention_id = etudiant.mention_id if (!etudiant?.mention_id) return
window.notes.getNotes({ id, niveau, mention_id }).then((response) => {
setNotes(response)
})
window.noteRepech.getNotesRepech({ id, niveau, mention_id }).then((response) => { window.notes.getNotes({ id, niveau, mention_id: etudiant.mention_id, annee_scolaire: scolaire }).then(setNotes)
setNotesRepech(response) window.noteRepech
}) .getNotesRepech({ id, niveau, mention_id: etudiant.mention_id, annee_scolaire: scolaire })
.then(setNotesRepech)
}, [etudiant]) }, [etudiant])
console.log(notes) /* ===================== INIT FORM ===================== */
/**
* Update formData whenever matieres change
*/
useEffect(() => { useEffect(() => {
const initialFormData = notes.reduce((acc, mat) => { const init = {}
acc[mat.id] = mat.note // Initialize each key with an empty string notes.forEach((n) => (init[n.id] = n.note))
return acc setFormData(init)
}, {}) }, [notes])
setFormData(initialFormData)
}, [notes]) // Dependency array ensures this runs whenever `matieres` is updated
/**
* Update formData2 whenever matieres change
*/
useEffect(() => { useEffect(() => {
const initialFormData = notesRepech.reduce((acc, mat) => { const init = {}
acc[mat.id] = mat.note // Initialize each key with an empty string notesRepech.forEach((n) => (init[n.id] = n.note))
return acc setFormData2(init)
}, {}) }, [notesRepech])
setFormData2(initialFormData)
}, [notesRepech]) // Dependency array ensures this runs whenever `matieres` is updated /* ===================== SUBMIT NORMALE ===================== */
const submitForm = async (e) => { const submitForm = async (e) => {
e.preventDefault() e.preventDefault()
try {
let mention_id = etudiant.mention_id let mention_id = etudiant.mention_id
console.log('normal submited') let annee_scolaire = scolaire // utiliser l'année de l'URL, pas de l'étudiant
let annee_scolaire = etudiant.annee_scolaire
let response = await window.notes.updateNote({ const response = await window.notes.updateNote({
formData, formData,
niveau, niveau,
id, id,
@ -73,177 +71,119 @@ const SingleNotes = () => {
annee_scolaire annee_scolaire
}) })
if (response.changes) {
setMessage('Modification des notes terminer avec succès')
setStatus(200) setStatus(200)
setMessage('Notes enregistrées avec succès')
setOpen(true) setOpen(true)
window.noteRepech.getNotesRepech({ id, niveau, mention_id }).then((response) => {
setNotesRepech(response)
})
window.notes.getNotes({ id, niveau, mention_id }).then((response) => { // rechargement avec l'année correcte
setNotes(response) 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) => { const submitForm2 = async (e) => {
e.preventDefault() e.preventDefault()
try {
let mention_id = etudiant.mention_id let mention_id = etudiant.mention_id
console.log('rattrapage submited')
let response = await window.noteRepech.updateNoteRepech({ formData2, niveau, id })
console.log(response) await window.noteRepech.updateNoteRepech({
if (response.changes) { formData2,
setMessage('Modification des notes terminer avec succès') niveau,
id,
annee_scolaire: scolaire
})
setStatus(200) setStatus(200)
setMessage('Notes de rattrapage enregistrées avec succès')
setOpen(true) setOpen(true)
window.noteRepech.getNotesRepech({ id, niveau, mention_id }).then((response) => {
setNotesRepech(response)
})
window.notes.getNotes({ id, niveau, mention_id }).then((response) => { const repechData = await window.noteRepech.getNotesRepech({ id, niveau, mention_id })
setNotes(response) setNotesRepech(repechData)
})
} catch (error) {
console.error(error)
setStatus(500)
setMessage("Échec de l'enregistrement des notes de rattrapage")
setOpen(true)
} }
} }
const [status, setStatus] = useState(200)
const [message, setMessage] = useState('')
/** /* ===================== MODAL ===================== */
* hook to open modal
*/
const [open, setOpen] = useState(false)
/** const modalMessage = () => (
* function to close modal <Modal open={open} onClose={handleClose}>
*/
const handleClose = () => setOpen(false)
/**
* function to return the view Modal
*
* @returns {JSX}
*/
const modals = () => (
<Modal
open={open}
onClose={handleClose}
aria-labelledby="modal-title"
aria-describedby="modal-description"
>
<Box <Box
sx={{ sx={{
position: 'absolute', position: 'absolute',
top: '50%', top: '50%',
left: '50%', left: '50%',
transform: 'translate(-50%, -50%)', transform: 'translate(-50%, -50%)',
width: 450, width: 420,
bgcolor: 'background.paper', bgcolor: 'background.paper',
boxShadow: 24, boxShadow: 24,
p: 4 p: 4,
}} textAlign: 'center'
>
{status === 200 ? (
<Typography
style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '10px' }}
>
<img src={svgSuccess} alt="" width={50} height={50} /> <span>{message}</span>
</Typography>
) : (
<Typography
style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '10px' }}
>
<img src={svgError} alt="" width={50} height={50} /> <span>{message}</span>
</Typography>
)}
<Box
sx={{
marginTop: '2%',
display: 'flex',
gap: '20px',
alignItems: 'end',
justifyContent: 'flex-end'
}} }}
> >
<Button onClick={handleClose} color="warning" variant="contained"> <img src={status === 200 ? svgSuccess : svgError} alt="" width={60} />
<Typography sx={{ mt: 2 }}>{message}</Typography>
<Button sx={{ mt: 3 }} color="warning" variant="contained" onClick={handleClose}>
OK OK
</Button> </Button>
</Box> </Box>
</Box>
</Modal> </Modal>
) )
const nom = useRef() /* ===================== UI ===================== */
const changeScreen = () => {
setScreenRattrapage(!screenRattrapage)
}
return ( return (
<div className={classe.mainHome}> <div className={classe.mainHome}>
{modals()} {modalMessage()}
<div className={classeHome.header}> <div className={classeHome.header}>
<div className={classe.h1style}> <div className={classe.h1style}>
<div className={classeHome.blockTitle}> <div className={classeHome.blockTitle}>
<h1>Mise a jour des notes</h1> <h1>Mise à jour des notes</h1>
<div style={{ display: 'flex', gap: '20px' }}>
<Link onClick={() => window.history.back()}> <Link onClick={() => window.history.back()}>
<Button color="warning" variant="contained"> <Button color="warning" variant="contained">
<IoMdReturnRight style={{ fontSize: '20px' }} /> <IoMdReturnRight />
</Button> </Button>
</Link> </Link>
</div> </div>
</div> </div>
</div> </div>
</div>
{/* displaying the form */}
<div className={classeHome.boxEtudiantsCard}> <div className={classeHome.boxEtudiantsCard}>
<Box <Paper sx={{ p: 4, width: 700, margin: 'auto', mt: 5 }}>
sx={{
position: 'absolute',
top: '55%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 700,
borderRadius: '2%',
bgcolor: 'background.paper',
boxShadow: 24,
overflowY: 'auto',
p: 4
}}
>
<Box
sx={{
marginTop: '2%',
display: 'flex',
width: '100%',
height: '70vh',
gap: '20px',
alignItems: 'start',
justifyContent: 'center'
}}
>
{!screenRattrapage ? ( {!screenRattrapage ? (
<form action="" onSubmit={submitForm}> <form onSubmit={submitForm}>
<h4 style={{ textAlign: 'center' }}>Mise a jour des notes</h4> <h4 style={{ textAlign: 'center' }}>Session normale</h4>
{/* {/* map the all matiere and note to the form */}
<Grid container spacing={2}> <Grid container spacing={2}>
{notes.map((note) => ( {notes.map((n) => (
<Grid item xs={12} sm={3} key={note.nom}> <Grid item xs={12} sm={3} key={n.id}>
<TextField <TextField
label={note.nom} label={n.nom}
name={note.matiere_id} value={formData[n.id] || ''}
color="warning" onChange={(e) =>
fullWidth setFormData({ ...formData, [n.id]: e.target.value })
placeholder="point séparateur"
className="inputToValidateExport"
value={formData[note.matiere_id] || ''} // Access the correct value from formData
onChange={
(e) => setFormData({ ...formData, [note.id]: e.target.value }) // Update the specific key
} }
fullWidth
color="warning"
InputProps={{ InputProps={{
startAdornment: ( startAdornment: (
<InputAdornment position="start"> <InputAdornment position="start">
@ -251,66 +191,42 @@ const SingleNotes = () => {
</InputAdornment> </InputAdornment>
) )
}} }}
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
}
}}
/> />
</Grid> </Grid>
))} ))}
</Grid> </Grid>
<Grid
item <Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 3, gap: 2 }}>
xs={12} <Button onClick={() => setScreenRattrapage(true)} color="warning" variant="contained">
style={{ Rattrapage
display: 'flex',
gap: '30px',
justifyContent: 'flex-end',
marginTop: '2%'
}}
>
<Button type="button" color="warning" variant="contained" onClick={changeScreen}>
Voir les notes de rattrapage
</Button> </Button>
<Button type="submit" color="warning" variant="contained"> <Button type="submit" color="warning" variant="contained">
Enregister Enregistrer
</Button> </Button>
</Grid> </Box>
</form> </form>
) : ( ) : (
<form action="" onSubmit={submitForm2}> <form onSubmit={submitForm2}>
<h4 style={{ textAlign: 'center' }}>Mise a jour des notes de Rattrapage</h4> <h4 style={{ textAlign: 'center' }}>Session rattrapage</h4>
{/* {/* map the all matiere and note to the form */}
<Grid container spacing={2}> <Grid container spacing={2}>
{notesRepech.length === 0 ? ( {notesRepech.length === 0 ? (
// Show this message if notesRepech is empty
<Grid item xs={12}> <Grid item xs={12}>
<h4 style={{ textAlign: 'center', color: 'green' }}> <Typography color="green" textAlign="center">
L'étudiant a validé tous les crédits. L'étudiant a validé tous les crédits.
</h4> </Typography>
</Grid> </Grid>
) : ( ) : (
// Render form fields if notesRepech contains data notesRepech.map((n) => (
notesRepech.map((note) => ( <Grid item xs={12} sm={4} key={n.id}>
<Grid item xs={12} sm={4} key={note.nom}>
<TextField <TextField
label={note.nom} label={n.nom}
name={note.matiere_id} value={formData2[n.id] || ''}
color="warning" onChange={(e) =>
fullWidth setFormData2({ ...formData2, [n.id]: e.target.value })
placeholder="point séparateur"
className="inputToValidateExport"
value={formData2[note.matiere_id] || ''} // Access the correct value from formData2
onChange={
(e) => setFormData2({ ...formData2, [note.id]: e.target.value }) // Update the specific key
} }
fullWidth
color="warning"
InputProps={{ InputProps={{
startAdornment: ( startAdornment: (
<InputAdornment position="start"> <InputAdornment position="start">
@ -318,44 +234,23 @@ const SingleNotes = () => {
</InputAdornment> </InputAdornment>
) )
}} }}
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
}
}}
/> />
</Grid> </Grid>
)) ))
)} )}
</Grid> </Grid>
<Grid <Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 3, gap: 2 }}>
item <Button onClick={() => setScreenRattrapage(false)} color="warning" variant="contained">
xs={12} Normale
style={{
display: 'flex',
gap: '30px',
justifyContent: 'flex-end',
marginTop: '2%'
}}
>
<Button type="button" color="warning" variant="contained" onClick={changeScreen}>
Voir les notes session normale
</Button> </Button>
<Button type="submit" color="warning" variant="contained"> <Button type="submit" color="warning" variant="contained">
Enregister Enregistrer
</Button> </Button>
</Grid> </Box>
</form> </form>
)} )}
</Box> </Paper>
</Box>
</div> </div>
</div> </div>
) )

118
src/renderer/src/components/Student.jsx

@ -64,16 +64,22 @@ const Student = () => {
const [sortModel, setSortModel] = useState([]) const [sortModel, setSortModel] = useState([])
const location = useLocation() const location = useLocation()
const savedFilter = localStorage.getItem('selectedNiveau') || '' const savedFilter = localStorage.getItem('selectedNiveau') || ''
const savedAnnee = localStorage.getItem('selectedAnnee') || ''
const initialFilter = location.state?.selectedNiveau || savedFilter const initialFilter = location.state?.selectedNiveau || savedFilter
const initialAnnee = location.state?.selectedAnnee || savedAnnee
const [selectedNiveau, setSelectedNiveau] = useState(initialFilter) const [selectedNiveau, setSelectedNiveau] = useState(initialFilter)
const [selectedAnnee, setSelectedAnnee] = useState(initialAnnee)
const [anneesList, setAnneesList] = useState([])
useEffect(() => { useEffect(() => {
if (initialFilter) { if (initialFilter) {
setSelectedNiveau(initialFilter) setSelectedNiveau(initialFilter)
FilterData({ target: { value: initialFilter } }) // applique le filtre initial
} }
}, [initialFilter]) if (initialAnnee) {
setSelectedAnnee(initialAnnee)
}
}, [])
@ -84,21 +90,58 @@ const Student = () => {
const [etudiants, setEtudiants] = useState([]) const [etudiants, setEtudiants] = useState([])
const [notes, setNotes] = useState([]) const [notes, setNotes] = useState([])
// Charger la liste des années scolaires disponibles
useEffect(() => { useEffect(() => {
window.etudiants.getEtudiants().then((response) => { window.anneescolaire.getAnneeScolaire().then((response) => {
setAllEtudiants(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)
}
})
}, [])
if (selectedNiveau && selectedNiveau !== '') { // Recharger les étudiants depuis la DB quand l'année sélectionnée change
setEtudiants(response.filter(e => e.niveau === selectedNiveau)) // 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 { } else {
setEtudiants(response) response = []
} }
}) } catch (err) {
console.error('Erreur filtre tranche:', err)
}
} else {
response = await window.etudiants.getEtudiants()
}
setAllEtudiants(response || [])
}
loadEtudiants()
window.notes.getMoyenneVerify().then((response) => { window.notes.getMoyenneVerify().then((response) => {
setNotes(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(() => { useEffect(() => {
const savedFilters = localStorage.getItem('datagridFilters') const savedFilters = localStorage.getItem('datagridFilters')
@ -522,7 +565,7 @@ const Student = () => {
// Ensure that the array is flat (not wrapped in another array) // Ensure that the array is flat (not wrapped in another array)
const dataRow = etudiants.map((etudiant) => ({ 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, nom: etudiant.nom,
prenom: etudiant.prenom, prenom: etudiant.prenom,
niveau: etudiant.niveau, niveau: etudiant.niveau,
@ -544,7 +587,7 @@ const Student = () => {
mention_id: etudiant.mention_id, mention_id: etudiant.mention_id,
mentionUnite: etudiant.mentionUnite, mentionUnite: etudiant.mentionUnite,
nomMention: etudiant.nomMention, nomMention: etudiant.nomMention,
action: etudiant.id // Ensure this is a valid URL for the image action: etudiant.id
})) }))
function comparestatut(statutID) { 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 FilterData = (e) => {
const niveau = e.target.value const niveau = e.target.value
setSelectedNiveau(niveau) setSelectedNiveau(niveau)
localStorage.setItem('selectedNiveau', niveau)
if (niveau === '') { setPaginationModel(prev => ({ ...prev, page: 0 }))
setEtudiants(allEtudiants)
} else {
const filtered = allEtudiants.filter(student => student.niveau === niveau)
setEtudiants(filtered)
} }
/**
* 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 })) setPaginationModel(prev => ({ ...prev, page: 0 }))
} }
@ -664,12 +712,42 @@ const Student = () => {
</div> </div>
{/* bare des filtre */} {/* bare des filtre */}
<div className={classeHome.container}> <div className={classeHome.container}>
{/* filtre par année scolaire + niveau */}
<div style={{ width: '100%', textAlign: 'right', display: 'flex', justifyContent: 'flex-end', alignItems: 'center' }}>
{/* filtre par année scolaire */}
<FormControl
sx={{
m: 1,
width: '25%',
'& .MuiOutlinedInput-root': {
'&:hover fieldset': { borderColor: '#ff9800' }
}
}}
size="small"
variant="outlined"
>
<InputLabel sx={{ color: 'black', fontSize: '18px' }} color="warning">
Année scolaire
</InputLabel>
<Select
label="Année scolaire"
color="warning"
value={selectedAnnee}
onChange={FilterByAnnee}
sx={{ background: 'white' }}
>
<MenuItem value=""><em>Toutes les années</em></MenuItem>
{anneesList.map((a) => (
<MenuItem value={a.code} key={a.id}>{a.code}</MenuItem>
))}
</Select>
</FormControl>
{/* filtre par niveau */} {/* filtre par niveau */}
<div style={{ width: '100%', textAlign: 'right' }}>
<FormControl <FormControl
sx={{ sx={{
m: 1, m: 1,
width: '30%', width: '25%',
'& .MuiOutlinedInput-root': { '& .MuiOutlinedInput-root': {
'&:hover fieldset': { '&:hover fieldset': {
borderColor: '#ff9800' // Set the border color on hover borderColor: '#ff9800' // Set the border color on hover

256
src/renderer/src/components/TrancheEcolage.jsx

@ -3,96 +3,130 @@ import { useParams, Link } from 'react-router-dom'
import classe from '../assets/AllStyleComponents.module.css' import classe from '../assets/AllStyleComponents.module.css'
import classeHome from '../assets/Home.module.css' import classeHome from '../assets/Home.module.css'
import Paper from '@mui/material/Paper' import Paper from '@mui/material/Paper'
import { Button, Modal, Box } from '@mui/material' import { Button, Box, Typography, Modal } from '@mui/material'
import { IoMdReturnRight } from 'react-icons/io' import { IoMdReturnRight } from 'react-icons/io'
import AjoutTranche from './AjoutTranche' import AjoutTranche from './AjoutTranche'
import { Tooltip } from 'react-tooltip' import { Tooltip } from 'react-tooltip'
import { FaPenToSquare } from 'react-icons/fa6'
import { FaTrash } from 'react-icons/fa' import { FaTrash } from 'react-icons/fa'
import { FaPenToSquare } from 'react-icons/fa6'
import { MdPayment } from 'react-icons/md'
import UpdateTranche from './UpdateTranche' import UpdateTranche from './UpdateTranche'
import DeleteTranche from './DeleteTranche' import warning from '../assets/warning.svg'
import success from '../assets/success.svg'
const TrancheEcolage = () => { const TrancheEcolage = () => {
const { id } = useParams() const { id } = useParams()
const [tranche, setTranche] = useState([]) const [tranches, setTranches] = useState([])
const [etudiant, setEtudiant] = useState({}) const [etudiant, setEtudiant] = useState({})
const [montantConfig, setMontantConfig] = useState(null)
useEffect(() => { const loadData = () => {
window.etudiants.getTranche({ id }).then((response) => { window.etudiants.getTranche({ id }).then((response) => {
setTranche(response) setTranches(response || [])
}) })
window.etudiants.getSingle({ id }).then((response) => { window.etudiants.getSingle({ id }).then((response) => {
setEtudiant(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 [openAdd, setOpenAdd] = useState(false)
const onCloseAdd = () => setOpenAdd(false)
const openAddFunction = () => {
setOpenAdd(true)
}
const [isSubmited, setIsSubmited] = useState(false) const [isSubmited, setIsSubmited] = useState(false)
const handleFormSubmit = (status) => { const handleFormSubmit = (status) => {
setIsSubmited(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(() => { useEffect(() => {
if (isSubmited) { if (isSubmited) {
window.etudiants.getTranche({ id }).then((response) => { loadData()
setTranche(response)
})
setIsSubmited(false) setIsSubmited(false)
} }
}, [isSubmited]) }, [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 = () => (
<Modal open={openDeleteModal} onClose={() => { setOpenDeleteModal(false); setIsDeleted(false) }}>
<Box sx={{
position: 'absolute', top: '50%', left: '50%',
transform: 'translate(-50%, -50%)', width: 400,
bgcolor: 'background.paper', boxShadow: 24, p: 4
}}>
{isDeleted ? (
<Typography style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '10px' }}>
<img src={success} alt="" width={50} height={50} />
<span>Supprime avec succes</span>
</Typography>
) : (
<Typography style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '10px' }}>
<img src={warning} alt="" width={50} height={50} />
<span>Voulez-vous supprimer ce paiement ?</span>
</Typography>
)}
<Box sx={{ mt: 2, display: 'flex', gap: '10px', justifyContent: 'flex-end' }}>
{isDeleted ? (
<Button onClick={() => { setOpenDeleteModal(false); setIsDeleted(false) }} color="warning" variant="contained">OK</Button>
) : (
<>
<Button onClick={handleDelete} color="error" variant="contained">Oui</Button>
<Button onClick={() => setOpenDeleteModal(false)} color="warning" variant="contained">Non</Button>
</>
)}
</Box>
</Box>
</Modal>
)
return ( return (
<div className={classe.mainHome}> <div className={classe.mainHome}>
<AjoutTranche <AjoutTranche
id={id} id={id}
onClose={onCloseAdd} onClose={() => setOpenAdd(false)}
onSubmitSuccess={handleFormSubmit} onSubmitSuccess={handleFormSubmit}
open={openAdd} open={openAdd}
/> />
{deleteModal()}
<UpdateTranche <UpdateTranche
onClose={onCloseUpdate}
onSubmitSuccess={handleFormSubmit}
open={openUpdate} open={openUpdate}
id={idToSend} onClose={() => setOpenUpdate(false)}
/>
<DeleteTranche
id={idToSend2}
onClose={onCloseDelete}
onSubmitSuccess={handleFormSubmit} onSubmitSuccess={handleFormSubmit}
open={openDelete} tranche={trancheToEdit}
/> />
<div className={classeHome.header}> <div className={classeHome.header}>
<div className={classe.h1style}> <div className={classe.h1style}>
<div className={classeHome.blockTitle}> <div className={classeHome.blockTitle}>
<h1>Tranche d'Ecolage</h1> <h1>Frais de Formation</h1>
<div style={{ display: 'flex', gap: '10px' }}> <div style={{ display: 'flex', gap: '10px' }}>
<Link to={'#'} onClick={openAddFunction}> <Link to={'#'} onClick={() => setOpenAdd(true)}>
<Button color="warning" variant="contained"> <Button color="warning" variant="contained">
Ajouter <MdPayment style={{ fontSize: '20px', marginRight: 5 }} /> Ajouter un paiement
</Button> </Button>
</Link> </Link>
<Link to={'#'} onClick={() => window.history.back()}> <Link to={'#'} onClick={() => window.history.back()}>
@ -106,83 +140,111 @@ const TrancheEcolage = () => {
</div> </div>
<div className={classeHome.boxEtudiantsCard}> <div className={classeHome.boxEtudiantsCard}>
<Paper <Paper sx={{ height: 'auto', width: '100%', display: 'flex', padding: '2%' }}>
sx={{ <table className="table table-bordered table-striped text-center shadow-sm">
height: 'auto', // Auto height to make the grid responsive
width: '100%',
// minHeight: 500, // Ensures a minimum height
display: 'flex',
padding: '2%'
}}
>
<table className="table table-bordered table-striped text-center shadow-sm" id="myTable2">
<thead className="table-secondary"> <thead className="table-secondary">
<tr> <tr>
<td colSpan={4} className="py-3"> <td colSpan={8} className="py-3">
<h6> <h6>
Evolution d'écolage de {etudiant.nom} {etudiant.prenom} Evolution d'ecolage de <b>{etudiant.nom} {etudiant.prenom}</b> - N°: {etudiant.num_inscription}
</h6> </h6>
</td> </td>
</tr> </tr>
<tr> <tr>
<th>Tranche N°</th> <th>Annee scolaire</th>
<th>Désignation</th> <th>Tranche 1 - N° Bordereau</th>
<th>Montant</th> <th>Tranche 1 - Montant</th>
<th>Tranche 2 - N° Bordereau</th>
<th>Tranche 2 - Montant</th>
<th>Reste a payer</th>
<th>Statut</th>
<th>Action</th> <th>Action</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{tranche.map((tranch, index) => ( {tranches.length === 0 ? (
<tr key={tranch.id}> <tr>
<td>{index + 1}</td> <td colSpan={8} style={{ color: 'gray', padding: '20px' }}>Aucun paiement enregistre</td>
<td>{tranch.tranchename}</td> </tr>
<td>{Number(tranch.montant).toLocaleString(0, 3)}</td> ) : (
<td tranches.map((t) => {
className="fw-bold" const total = (t.tranche1_montant || 0) + (t.tranche2_montant || 0)
style={{ const hasTranche1 = t.tranche1_montant > 0
display: 'flex', const hasTranche2 = t.tranche2_montant > 0
gap: '10px',
alignItems: 'center', return (
justifyContent: 'center' <tr key={t.id}>
}} <td><b>{t.annee_scolaire_code}</b></td>
> <td>{t.tranche1_bordereau || <span style={{ color: '#ccc' }}>-</span>}</td>
<td>
{hasTranche1 ? (
<span style={{ color: 'green', fontWeight: 'bold' }}>
{Number(t.tranche1_montant).toLocaleString('fr-FR')} Ar
</span>
) : (
<span style={{ color: '#ccc' }}>Non paye</span>
)}
</td>
<td>{t.tranche2_bordereau || <span style={{ color: '#ccc' }}>-</span>}</td>
<td>
{hasTranche2 ? (
<span style={{ color: 'green', fontWeight: 'bold' }}>
{Number(t.tranche2_montant).toLocaleString('fr-FR')} Ar
</span>
) : (
<span style={{ color: '#ccc' }}>Non paye</span>
)}
</td>
<td>
{(() => {
const droitTotal = montantConfig ? montantConfig.montant_total : 0
const reste = Math.max(0, droitTotal - total)
return (
<b style={{ color: reste === 0 ? 'green' : 'red' }}>
{Number(reste).toLocaleString('fr-FR')} Ar
</b>
)
})()}
</td>
<td>
{hasTranche1 && hasTranche2 ? (
<span style={{ color: 'green', fontWeight: 'bold' }}>Complet</span>
) : hasTranche1 || hasTranche2 ? (
<span style={{ color: 'orange', fontWeight: 'bold' }}>Partiel</span>
) : (
<span style={{ color: 'red', fontWeight: 'bold' }}>Non paye</span>
)}
</td>
<td style={{ display: 'flex', gap: '8px', justifyContent: 'center' }}>
<Button <Button
variant="contained" variant="contained"
color="warning" color="warning"
onClick={() => openUpdateFunction(tranch.id)} size="small"
> onClick={() => { setTrancheToEdit(t); setOpenUpdate(true) }}
<FaPenToSquare className={`edit${t.id}`}
style={{ fontSize: '20px', color: 'white' }}
className={`update${tranch.id}`}
/>
<Tooltip
anchorSelect={`.update${tranch.id}`}
className="custom-tooltip"
place="top"
> >
<FaPenToSquare style={{ fontSize: '16px', color: 'white' }} />
</Button>
<Tooltip anchorSelect={`.edit${t.id}`} className="custom-tooltip" place="top">
Modifier Modifier
</Tooltip> </Tooltip>
</Button>
<Button <Button
variant="contained" variant="contained"
color="error" color="error"
onClick={() => openDeleteFunction(tranch.id)} size="small"
> onClick={() => { setIdToDelete(t.id); setOpenDeleteModal(true) }}
<FaTrash className={`del${t.id}`}
style={{ fontSize: '20px', color: 'white' }}
className={`delete${tranch.id}`}
/>
<Tooltip
anchorSelect={`.delete${tranch.id}`}
className="custom-tooltip"
place="top"
> >
<FaTrash style={{ fontSize: '16px', color: 'white' }} />
</Button>
<Tooltip anchorSelect={`.del${t.id}`} className="custom-tooltip" place="top">
Supprimer Supprimer
</Tooltip> </Tooltip>
</Button>
</td> </td>
</tr> </tr>
))} )
})
)}
</tbody> </tbody>
</table> </table>
</Paper> </Paper>

117
src/renderer/src/components/UpdateTranche.jsx

@ -6,38 +6,30 @@ import {
DialogTitle, DialogTitle,
TextField, TextField,
Button, Button,
Autocomplete,
InputAdornment, InputAdornment,
Box, Box,
Grid Grid
} from '@mui/material' } 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({ const [formData, setFormData] = useState({
id: id, id: '',
tranchename: '', tranche1_montant: '',
montant: '' tranche1_bordereau: '',
tranche2_montant: '',
tranche2_bordereau: ''
}) })
const [tranche, setTranche] = useState([])
useEffect(() => { useEffect(() => {
if (id !== null) { if (tranche) {
window.etudiants.getSingleTranche({ id }).then((response) => {
setTranche(response)
})
setFormData({ 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]) }, [tranche])
const handleChange = (e) => { const handleChange = (e) => {
@ -47,64 +39,81 @@ const UpdateTranche = ({ open, onClose, onSubmitSuccess, id }) => {
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
e.preventDefault() e.preventDefault()
let response = await window.etudiants.updateTranche(formData) const response = await window.etudiants.updateTranche(formData)
if (response.success) {
if (response.changes) {
onClose() onClose()
onSubmitSuccess(true) onSubmitSuccess(true)
} }
} }
return ( return (
<Dialog open={open} onClose={onClose}> <Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
<form action="" onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<DialogTitle>Ajout tranche</DialogTitle> <DialogTitle>Modifier le paiement</DialogTitle>
<DialogContent> <DialogContent>
<Box sx={{ flexGrow: 1 }}> <Box sx={{ flexGrow: 1, mt: 1 }}>
<Grid container spacing={2}> <Grid container spacing={2}>
<Grid item xs={12}>
<b>Tranche 1</b>
</Grid>
<Grid item xs={12} sm={6}> <Grid item xs={12} sm={6}>
<TextField <TextField
autoFocus name="tranche1_bordereau"
margin="normal" label="N° Bordereau"
required
name="tranchename"
label="Désignation"
type="text" type="text"
fullWidth fullWidth
placeholder="Tranche 1" size="small"
variant="outlined" variant="outlined"
value={formData.tranchename} value={formData.tranche1_bordereau}
color="warning"
onChange={handleChange}
/>
</Grid>
<Grid item xs={12} sm={6}>
<TextField
name="tranche1_montant"
label="Montant"
type="number"
fullWidth
size="small"
variant="outlined"
value={formData.tranche1_montant}
color="warning" color="warning"
onChange={handleChange} onChange={handleChange}
InputProps={{ InputProps={{
startAdornment: ( startAdornment: <InputAdornment position="start">Ar</InputAdornment>
<InputAdornment position="start">
<MdLabelImportantOutline />
</InputAdornment>
)
}} }}
/> />
</Grid> </Grid>
<Grid item xs={12} sx={{ mt: 1 }}>
<b>Tranche 2</b>
</Grid>
<Grid item xs={12} sm={6}> <Grid item xs={12} sm={6}>
<TextField <TextField
autoFocus name="tranche2_bordereau"
margin="normal" label="N° Bordereau"
required type="text"
name="montant" fullWidth
size="small"
variant="outlined"
value={formData.tranche2_bordereau}
color="warning"
onChange={handleChange}
/>
</Grid>
<Grid item xs={12} sm={6}>
<TextField
name="tranche2_montant"
label="Montant" label="Montant"
type="number" type="number"
fullWidth fullWidth
placeholder="Montant" size="small"
variant="outlined" variant="outlined"
value={formData.montant} value={formData.tranche2_montant}
color="warning" color="warning"
onChange={handleChange} onChange={handleChange}
InputProps={{ InputProps={{
startAdornment: ( startAdornment: <InputAdornment position="start">Ar</InputAdornment>
<InputAdornment position="start">
<MdLabelImportantOutline />
</InputAdornment>
)
}} }}
/> />
</Grid> </Grid>
@ -112,12 +121,8 @@ const UpdateTranche = ({ open, onClose, onSubmitSuccess, id }) => {
</Box> </Box>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={onClose} color="error"> <Button onClick={onClose} color="error">Annuler</Button>
Annuler <Button type="submit" color="warning" variant="contained">Enregistrer</Button>
</Button>
<Button type="submit" color="warning">
Soumettre
</Button>
</DialogActions> </DialogActions>
</form> </form>
</Dialog> </Dialog>

12
src/renderer/src/components/function/FonctionRelever.js

@ -32,10 +32,14 @@ function nextLevel(niveau) {
} }
} }
export const descisionJury = (notes, niveau) => { export const descisionJury = (notes, niveau, systeme) => {
if (notes >= 10) { if (!systeme) return ''
return `Admis en ${nextLevel(niveau)}`
if (notes >= systeme.admis) {
return `Admis`
} else if (notes > systeme.renvoyer) {
return `Redoublant`
} else { } else {
return 'Vous redoublez' return `Renvoyé`
} }
} }

Loading…
Cancel
Save