const express = require('express'); const sql = require('mssql'); const cors = require('cors'); const app = express(); app.use(cors()); app.use(express.json()); const sqlConfig = { user: 'RG-Competences', password: 'P@ssw0rd2026!', server: '192.168.0.3', database: 'RG-Competences', options: { encrypt: true, trustServerCertificate: true, enableArithAbort: true } }; // ═══════════════════════════════════════════════════════ // UTILITAIRE : LOG HISTORIQUE (NOUVELLE VERSION - UNE LIGNE PAR ACTION) // ═══════════════════════════════════════════════════════ async function logHistoryComplete(data) { try { const pool = await sql.connect(sqlConfig); await pool.request() .input('record_id', sql.NVarChar, data.record_id) .input('action_type', sql.NVarChar, data.action_type) .input('action_description', sql.NVarChar, data.action_description) .input('author_email', sql.NVarChar, data.author_email) .input('author_name', sql.NVarChar, data.author_name) .input('author_role', sql.NVarChar, data.author_role) .input('campus', sql.NVarChar, data.campus || null) .input('ip_address', sql.NVarChar, data.ip_address || null) // Nouvelles colonnes .input('auto_evaluation_old', sql.NVarChar, data.auto_evaluation_old || null) .input('auto_evaluation_new', sql.NVarChar, data.auto_evaluation_new || null) .input('evaluation_tuteur_old', sql.NVarChar, data.evaluation_tuteur_old || null) .input('evaluation_tuteur_new', sql.NVarChar, data.evaluation_tuteur_new || null) .input('date_action_old', sql.Date, data.date_action_old || null) .input('date_action_new', sql.Date, data.date_action_new || null) .input('date_action_rre_old', sql.Date, data.date_action_rre_old || null) .input('date_action_rre_new', sql.Date, data.date_action_rre_new || null) .input('date_eval_tuteur_old', sql.Date, data.date_eval_tuteur_old || null) .input('date_eval_tuteur_new', sql.Date, data.date_eval_tuteur_new || null) .input('commentaire_formateur_old', sql.NVarChar, data.commentaire_formateur_old || null) .input('commentaire_formateur_new', sql.NVarChar, data.commentaire_formateur_new || null) .input('commentaire_rre_old', sql.NVarChar, data.commentaire_rre_old || null) .input('commentaire_rre_new', sql.NVarChar, data.commentaire_rre_new || null) .input('commentaire_old', sql.NVarChar, data.commentaire_old || null) .input('commentaire_new', sql.NVarChar, data.commentaire_new || null) .query(` INSERT INTO [dbo].[tbl_Historique_Evaluations] (record_id, action_type, action_description, author_email, author_name, author_role, campus, ip_address, auto_evaluation_old, auto_evaluation_new, evaluation_tuteur_old, evaluation_tuteur_new, date_action_old, date_action_new, date_action_rre_old, date_action_rre_new, date_eval_tuteur_old, date_eval_tuteur_new, commentaire_formateur_old, commentaire_formateur_new, commentaire_rre_old, commentaire_rre_new, commentaire_old, commentaire_new) VALUES (@record_id, @action_type, @action_description, @author_email, @author_name, @author_role, @campus, @ip_address, @auto_evaluation_old, @auto_evaluation_new, @evaluation_tuteur_old, @evaluation_tuteur_new, @date_action_old, @date_action_new, @date_action_rre_old, @date_action_rre_new, @date_eval_tuteur_old, @date_eval_tuteur_new, @commentaire_formateur_old, @commentaire_formateur_new, @commentaire_rre_old, @commentaire_rre_new, @commentaire_old, @commentaire_new) `); console.log('✅ Historique complet enregistré:', data.action_type); } catch (error) { console.error('❌ Erreur log historique:', error); } } // ═══════════════════════════════════════════════════════ // AUTHENTIFICATION // ═══════════════════════════════════════════════════════ async function getUserWithPermissions(userPrincipalName) { try { const pool = await sql.connect(sqlConfig); const superUserResult = await pool.request() .input('userPrincipalName', sql.NVarChar, userPrincipalName) .query(` SELECT TOP 1 userPrincipalName, nom + ' ' + prenom as nom_complet, nom, prenom, campus, COALESCE(role, 'super') as role FROM [dbo].[tbl_SuperUtilisateur] WHERE userPrincipalName = @userPrincipalName AND actif = 1 `); if (superUserResult.recordset.length > 0) { const superUser = superUserResult.recordset[0]; const userRole = superUser.role || 'super'; // Définir les permissions selon le rôle let permissions = {}; if (userRole === 'lecteur') { // Lecteur : peut voir son campus mais rien modifier permissions = { peut_valider_evaluations: false, peut_gerer_formateurs: false, peut_saisir_evaluations: false, peut_voir_tous_campus: false, // ✅ Limité à son campus peut_exporter_donnees: true, // Peut exporter peut_usurper_identite: false, peut_modifier: false // ✅ Ne peut pas modifier }; } else { // Super : peut modifier son campus uniquement permissions = { peut_valider_evaluations: true, peut_gerer_formateurs: true, peut_saisir_evaluations: true, peut_voir_tous_campus: false, // ✅ Limité à son campus peut_exporter_donnees: true, peut_usurper_identite: true, peut_modifier: true // ✅ Peut modifier }; } return { id: superUser.userPrincipalName, email: superUser.userPrincipalName, name: superUser.prenom || superUser.nom, nomComplet: superUser.nom_complet, campus: superUser.campus, role: userRole, permissions: permissions }; } const userResult = await pool.request() .input('userPrincipalName', sql.NVarChar, userPrincipalName) .query(` SELECT TOP 1 userPrincipalName, nom_dip as nom_complet, Nom_brut as nom, givenname as prenom, Campus, 'formateur' as role FROM [dbo].[v_R_Promo_Formateur_Referent] WHERE userPrincipalName = @userPrincipalName UNION ALL SELECT TOP 1 userPrincipalName, nom + ' ' + prenom as nom_complet, nom, prenom, campus as Campus, 'rre' as role FROM [dbo].[tbl_Responsable_RRE] WHERE userPrincipalName = @userPrincipalName AND actif = 1 `); if (userResult.recordset.length === 0) return null; const user = userResult.recordset[0]; const role = user.role || 'rre'; return { id: user.userPrincipalName, email: user.userPrincipalName, name: user.prenom || user.nom, nomComplet: user.nom_complet, campus: user.Campus, role: role, permissions: { peut_valider_evaluations: role === 'rre', peut_gerer_formateurs: role === 'rre', peut_saisir_evaluations: true, peut_voir_tous_campus: role === 'rre', peut_exporter_donnees: role === 'rre', peut_modifier: true } }; } catch (error) { console.error('❌ getUserWithPermissions:', error); throw error; } } // ═══════════════════════════════════════════════════════ // SUPER-UTILISATEUR : LISTE DES FORMATEURS ET RRE // ═══════════════════════════════════════════════════════ app.get('/api/super/users', async (req, res) => { try { const userRole = req.headers['x-user-role']; if (userRole !== 'super') { return res.status(403).json({ error: 'Accès réservé aux super-utilisateurs' }); } const pool = await sql.connect(sqlConfig); const formateurs = await pool.request().query(` SELECT DISTINCT userPrincipalName as id, nom_dip as nom_complet, Nom_brut as nom, givenname as prenom, Campus as campus, 'formateur' as role FROM [dbo].[v_R_Promo_Formateur_Referent] WHERE userPrincipalName IS NOT NULL ORDER BY nom_dip ASC `); const rres = await pool.request().query(` SELECT userPrincipalName as id, nom + ' ' + prenom as nom_complet, nom, prenom, campus, 'rre' as role FROM [dbo].[tbl_Responsable_RRE] WHERE actif = 1 ORDER BY nom ASC, prenom ASC `); const allUsers = [ ...formateurs.recordset.map(f => ({ id: f.id, name: f.nom_complet, email: f.id, campus: f.campus, role: 'formateur' })), ...rres.recordset.map(r => ({ id: r.id, name: r.nom_complet, email: r.id, campus: r.campus, role: 'rre' })) ]; res.json(allUsers); console.log('✅ Liste des utilisateurs pour super-user:', allUsers.length); } catch (error) { console.error('❌ Erreur /api/super/users:', error); res.status(500).json({ error: 'Erreur serveur' }); } }); // ═══════════════════════════════════════════════════════ // SUPER-UTILISATEUR : USURPER UNE IDENTITÉ // ═══════════════════════════════════════════════════════ app.post('/api/super/impersonate', async (req, res) => { try { const userRole = req.headers['x-user-role']; const { targetEmail } = req.body; if (userRole !== 'super') { return res.status(403).json({ error: 'Accès réservé aux super-utilisateurs' }); } if (!targetEmail) { return res.status(400).json({ error: 'Email cible requis' }); } const targetUser = await getUserWithPermissions(targetEmail); if (!targetUser || targetUser.role === 'super' || targetUser.role === 'lecteur') { return res.status(404).json({ error: 'Utilisateur cible non trouvé ou invalide' }); } console.log(`🎭 Super-user usurpe l'identité de: ${targetUser.email} (${targetUser.role})`); res.json({ success: true, impersonatedUser: targetUser }); } catch (error) { console.error('❌ Erreur impersonate:', error); res.status(500).json({ error: 'Erreur serveur' }); } }); app.post('/auth/verify', async (req, res) => { const { email } = req.body; console.log('🔍 Vérification:', email); try { const user = await getUserWithPermissions(email); if (user) { console.log('✅ User:', user.role, user.campus); res.json({ valid: true, user }); } else { console.log('❌ Non trouvé'); res.status(403).json({ valid: false }); } } catch (error) { console.error('❌ Erreur auth:', error); res.status(500).json({ error: 'Erreur serveur' }); } }); // ═══════════════════════════════════════════════════════ // UTILITAIRE : OBTENIR L'UTILISATEUR EFFECTIF // ═══════════════════════════════════════════════════════ function getEffectiveUser(headers) { const realRole = headers['x-user-role']; const impersonatedEmail = headers['x-impersonated-email']; const impersonatedRole = headers['x-impersonated-role']; const impersonatedCampus = headers['x-impersonated-campus']; if (realRole === 'super' && impersonatedEmail) { return { email: impersonatedEmail, role: impersonatedRole, campus: impersonatedCampus, isImpersonating: true, isSuperUser: true }; } return { email: headers['x-user-email'], role: headers['x-user-role'], campus: headers['x-user-campus'], isImpersonating: false, isSuperUser: realRole === 'super', isLecteur: realRole === 'lecteur' }; } // ═══════════════════════════════════════════════════════ // FILTRES - CAMPUS // ═══════════════════════════════════════════════════════ app.get('/api/campus-list', async (req, res) => { try { const effectiveUser = getEffectiveUser(req.headers); const { email, role, campus, isSuperUser, isLecteur } = effectiveUser; const pool = await sql.connect(sqlConfig); let result; // 🔥 SUPER OU LECTEUR SANS USURPATION : SON campus uniquement if ((isSuperUser || isLecteur) && !effectiveUser.isImpersonating) { if (campus) { result = await pool.request() .input('userCampus', sql.NVarChar, campus) .query(` SELECT DISTINCT Campus FROM [dbo].[v_R_Promo_Formateur_Etudiant] WHERE Campus = @userCampus ORDER BY Campus ASC `); } else { // Si pas de campus défini, tous les campus (fallback) result = await pool.request().query(` SELECT DISTINCT Campus FROM [dbo].[v_R_Promo_Formateur_Etudiant] WHERE Campus IS NOT NULL AND Campus != '' ORDER BY Campus ASC `); } } // Formateur : ses campus uniquement else if (role === 'formateur' && email) { result = await pool.request() .input('userEmail', sql.NVarChar, email) .query(` SELECT DISTINCT Campus FROM [dbo].[v_R_Promo_Formateur_Etudiant] WHERE Campus IS NOT NULL AND Campus != '' AND userPrincipalName = @userEmail ORDER BY Campus ASC `); } // RRE : son campus uniquement else if (role === 'rre' && campus) { result = await pool.request() .input('userCampus', sql.NVarChar, campus) .query(` SELECT DISTINCT Campus FROM [dbo].[v_R_Promo_Formateur_Etudiant] WHERE Campus = @userCampus ORDER BY Campus ASC `); } // Fallback : tous les campus else { result = await pool.request().query(` SELECT DISTINCT Campus FROM [dbo].[v_R_Promo_Formateur_Etudiant] WHERE Campus IS NOT NULL AND Campus != '' ORDER BY Campus ASC `); } res.json(result.recordset.map(r => r.Campus)); console.log(`📍 Campus (${role}${isSuperUser ? ' [SUPER]' : ''}${isLecteur ? ' [LECTEUR]' : ''}${effectiveUser.isImpersonating ? ' [USURPÉ]' : ''}):`, result.recordset.length); } catch (error) { console.error('❌ campus-list:', error); res.status(500).json({ error: 'Erreur campus' }); } }); // ═══════════════════════════════════════════════════════ // FILTRES - PROMOTIONS // ═══════════════════════════════════════════════════════ app.get('/api/promo-list', async (req, res) => { try { const { campus } = req.query; const effectiveUser = getEffectiveUser(req.headers); const { email, role, campus: userCampus, isSuperUser, isLecteur } = effectiveUser; const pool = await sql.connect(sqlConfig); const request = pool.request(); let query = ` SELECT DISTINCT NOM_DIP as promo FROM [dbo].[v_R_Promo_Formateur_Etudiant] WHERE NOM_DIP IS NOT NULL AND NOM_DIP != '' `; // 🔥 SUPER OU LECTEUR SANS USURPATION : filtrer par SON campus if ((isSuperUser || isLecteur) && !effectiveUser.isImpersonating) { if (userCampus) { query += ` AND Campus = @userCampus`; request.input('userCampus', sql.NVarChar, userCampus); } } // Formateur : filtrer par son email else if (role === 'formateur' && email) { query += ` AND userPrincipalName = @userEmail`; request.input('userEmail', sql.NVarChar, email); } // RRE : filtrer par son campus else if (role === 'rre' && userCampus) { query += ` AND Campus = @userCampus`; request.input('userCampus', sql.NVarChar, userCampus); } // Filtre par campus sélectionné if (campus && campus !== 'tous') { query += ` AND Campus = @campus`; request.input('campus', sql.NVarChar, campus); } query += ` ORDER BY NOM_DIP ASC`; const result = await request.query(query); res.json(result.recordset.map(r => r.promo)); console.log(`🎓 Promotions (${role}${isSuperUser ? ' [SUPER]' : ''}${isLecteur ? ' [LECTEUR]' : ''}):`, result.recordset.length); } catch (error) { console.error('❌ promo-list:', error); res.status(500).json({ error: 'Erreur promotions' }); } }); // ═══════════════════════════════════════════════════════ // FILTRES - ÉTUDIANTS // ═══════════════════════════════════════════════════════ app.get("/api/student-list", async (req, res) => { try { const { campus, promo } = req.query; const effectiveUser = getEffectiveUser(req.headers); const { email, role, campus: userCampus, isSuperUser, isLecteur } = effectiveUser; const pool = await sql.connect(sqlConfig); const request = pool.request(); let query = ` SELECT LOGin as id, PRENOM + ' ' + NOM as name, Campus, NOM_DIP as formation FROM [dbo].[v_R_Promo_Formateur_Etudiant] WHERE LOGin IS NOT NULL AND LOGin <> '' `; const conditions = []; // 🔥 SUPER OU LECTEUR SANS USURPATION : filtrer par SON campus if ((isSuperUser || isLecteur) && !effectiveUser.isImpersonating) { if (userCampus) { conditions.push("Campus = @userCampus"); request.input("userCampus", sql.NVarChar, userCampus); } } // Formateur : uniquement SES étudiants else if (role === "formateur" && email) { conditions.push("userPrincipalName = @userEmail"); request.input("userEmail", sql.NVarChar, email); } // RRE : uniquement étudiants de SON campus else if (role === "rre" && userCampus) { conditions.push("Campus = @userCampus"); request.input("userCampus", sql.NVarChar, userCampus); } // Filtre par campus sélectionné if (campus && campus !== "tous") { conditions.push("Campus = @campus"); request.input("campus", sql.NVarChar, campus); } // Filtre par promo sélectionnée if (promo && promo !== "tous") { conditions.push("NOM_DIP = @promo"); request.input("promo", sql.NVarChar, promo); } if (conditions.length > 0) { query += " AND " + conditions.join(" AND "); } query += " ORDER BY NOM ASC, PRENOM ASC"; const result = await request.query(query); res.json(result.recordset); console.log( `👥 Étudiants (${role}${isSuperUser ? ' [SUPER]' : ''}${isLecteur ? ' [LECTEUR]' : ''}${effectiveUser.isImpersonating ? ' [USURPÉ]' : ''}):`, result.recordset.length ); } catch (error) { console.error("❌ student-list, error:", error); res.status(500).json({ error: "Erreur étudiants" }); } }); // ═══════════════════════════════════════════════════════ // DONNÉES PROMO // ═══════════════════════════════════════════════════════ app.get('/api/promo-data', async (req, res) => { try { const userRole = req.headers['x-user-role']; const userCampus = req.headers['x-user-campus']; const userEmail = req.headers['x-user-email']; const pool = await sql.connect(sqlConfig); const request = pool.request(); let query = ` SELECT LOGin, NOM, PRENOM, Campus, NOM_DIP, Annee, userPrincipalName as formateurEmail FROM [dbo].[v_R_Promo_Formateur_Etudiant] WHERE LOGin IS NOT NULL `; if (userRole === 'formateur' && userEmail) { query += ` AND userPrincipalName = @userEmail`; request.input('userEmail', sql.NVarChar, userEmail); } else if ((userRole === 'rre' || userRole === 'super' || userRole === 'lecteur') && userCampus) { query += ` AND Campus = @userCampus`; request.input('userCampus', sql.NVarChar, userCampus); } query += ` ORDER BY Campus ASC, NOM_DIP ASC, NOM ASC, PRENOM ASC`; const result = await request.query(query); res.json(result.recordset); console.log(`✅ promo-data (${userRole}):`, result.recordset.length); } catch (error) { console.error('❌ promo-data:', error); res.status(500).json({ error: 'Erreur promo-data' }); } }); // ═══════════════════════════════════════════════════════ // RÉCUPÉRER HISTORIQUE (NOUVELLE VERSION) // ═══════════════════════════════════════════════════════ app.get('/api/history/:studentId', async (req, res) => { try { const { studentId } = req.params; const pool = await sql.connect(sqlConfig); const result = await pool.request() .input('studentId', sql.NVarChar, studentId) .query(` SELECT id, action_type, action_description, author_name, author_email, author_role, campus, created_at, -- Nouvelles colonnes auto_evaluation_old, auto_evaluation_new, evaluation_tuteur_old, evaluation_tuteur_new, date_action_old, date_action_new, date_action_rre_old, date_action_rre_new, date_eval_tuteur_old, date_eval_tuteur_new, commentaire_formateur_old, commentaire_formateur_new, commentaire_rre_old, commentaire_rre_new, commentaire_old, commentaire_new FROM [dbo].[tbl_Historique_Evaluations] WHERE record_id = @studentId ORDER BY created_at DESC `); res.json(result.recordset); console.log(`📜 Historique de ${studentId}:`, result.recordset.length); } catch (error) { console.error('❌ Erreur historique:', error); res.status(500).json({ error: 'Erreur historique' }); } }); // ═══════════════════════════════════════════════════════ // RÉCUPÉRER TOUTES LES ÉVALUATIONS // ═══════════════════════════════════════════════════════ app.get('/api/evaluations', async (req, res) => { try { const effectiveUser = getEffectiveUser(req.headers); const { email, role, campus: userCampus, isSuperUser, isLecteur } = effectiveUser; const { campus, promo, student, status, sortBy, sortOrder } = req.query; const pool = await sql.connect(sqlConfig); const request = pool.request(); let query = ` SELECT e.LOGin as id, e.NOM as nom, e.PRENOM as prenom, e.Campus as campus, e.NOM_DIP as formation, e.NOM_DIP as nomDip, e.Annee as annee, e.userPrincipalName as formateurEmail, COALESCE(ev.auto_evaluation, 'NON') as autoEvaluation, COALESCE(ev.evaluation_tuteur, 'NON') as evaluationTuteur, ev.date_action as dateAction, ev.date_action_rre as dateActionRRE, ev.commentaire_formateur as commentaireFormateur, ev.date_eval_tuteur as dateEvalTuteur, ev.commentaire_rre as commentaireRRE, ev.commentaire, ev.updated_at as lastUpdate FROM [dbo].[v_R_Promo_Formateur_Etudiant] e LEFT JOIN [dbo].[tbl_Evaluations] ev ON e.LOGin = ev.student_id WHERE e.LOGin IS NOT NULL `; const conditions = []; // 🔥 SUPER OU LECTEUR SANS USURPATION : filtrer par SON campus if ((isSuperUser || isLecteur) && !effectiveUser.isImpersonating) { if (userCampus) { conditions.push('e.Campus = @userCampus'); request.input('userCampus', sql.NVarChar, userCampus); console.log(`🔐 ${role.toUpperCase()}: accès limité au campus`, userCampus); } else { console.log(`⚠️ ${role.toUpperCase()}: pas de campus défini, accès à tout`); } } // Filtrage par rôle usurpé ou réel else if (role === 'formateur' && email) { conditions.push('e.userPrincipalName = @userEmail'); request.input('userEmail', sql.NVarChar, email); } else if (role === 'rre' && userCampus) { conditions.push('e.Campus = @userCampus'); request.input('userCampus', sql.NVarChar, userCampus); } // Autres filtres (campus, promo, student sélectionnés dans l'interface) if (campus && campus !== 'tous') { conditions.push('e.Campus = @campus'); request.input('campus', sql.NVarChar, campus); } if (promo && promo !== 'tous') { conditions.push('e.NOM_DIP = @promo'); request.input('promo', sql.NVarChar, promo); } if (student && student !== 'tous') { conditions.push('e.LOGin = @student'); request.input('student', sql.NVarChar, student); } if (conditions.length > 0) { query += ' AND ' + conditions.join(' AND '); } // Filtre par statut if (status && status !== 'tous') { if (status === 'OUI') { query += ` AND (COALESCE(ev.auto_evaluation, 'NON') = 'OUI' AND COALESCE(ev.evaluation_tuteur, 'NON') = 'OUI')`; } else if (status === 'PARTIEL') { query += ` AND (COALESCE(ev.auto_evaluation, 'NON') = 'PARTIEL' OR COALESCE(ev.evaluation_tuteur, 'NON') = 'PARTIEL')`; } else if (status === 'NON') { query += ` AND (COALESCE(ev.auto_evaluation, 'NON') = 'NON' OR COALESCE(ev.evaluation_tuteur, 'NON') = 'NON')`; } } // Tri const validSortColumns = { 'nom': 'e.NOM, e.PRENOM', 'campus': 'e.Campus', 'promo': 'e.NOM_DIP', 'formation': 'e.NOM_DIP', 'date': 'ev.updated_at' }; const orderColumn = validSortColumns[sortBy] || 'e.NOM, e.PRENOM'; const orderDirection = sortOrder === 'desc' ? 'DESC' : 'ASC'; query += ` ORDER BY ${orderColumn} ${orderDirection}`; const result = await request.query(query); const records = result.recordset.map(record => ({ ...record, email: `${record.id}@example.com`, campusPromo: record.campus, history: [] })); res.json({ data: records }); console.log(`✅ Évaluations (${role}${isSuperUser ? ' [SUPER]' : ''}${isLecteur ? ' [LECTEUR]' : ''}${effectiveUser.isImpersonating ? ' [USURPÉ]' : ''}):`, records.length, 'étudiants'); } catch (error) { console.error('❌ Erreur évaluations:', error); res.status(500).json({ error: 'Erreur évaluations' }); } }); // ═══════════════════════════════════════════════════════ // RÉCUPÉRER UNE ÉVALUATION SPÉCIFIQUE // ═══════════════════════════════════════════════════════ app.get('/api/evaluations/:studentId', async (req, res) => { try { const { studentId } = req.params; const userRole = req.headers['x-user-role']; const userEmail = req.headers['x-user-email']; const userCampus = req.headers['x-user-campus']; const pool = await sql.connect(sqlConfig); const request = pool.request(); request.input('studentId', sql.NVarChar, studentId); let query = ` SELECT e.LOGin as id, e.NOM as nom, e.PRENOM as prenom, e.Campus as campus, e.NOM_DIP as formation, e.NOM_DIP as nomDip, e.Annee as annee, e.userPrincipalName as formateurEmail, COALESCE(ev.auto_evaluation, 'NON') as autoEvaluation, COALESCE(ev.evaluation_tuteur, 'NON') as evaluationTuteur, ev.date_action as dateAction, ev.date_action_rre as dateActionRRE, ev.commentaire_formateur as commentaireFormateur, ev.date_eval_tuteur as dateEvalTuteur, ev.commentaire_rre as commentaireRRE, ev.commentaire, ev.updated_at as lastUpdate FROM [dbo].[v_R_Promo_Formateur_Etudiant] e LEFT JOIN [dbo].[tbl_Evaluations] ev ON e.LOGin = ev.student_id WHERE e.LOGin = @studentId `; if (userRole === 'formateur' && userEmail) { query += ` AND e.userPrincipalName = @userEmail`; request.input('userEmail', sql.NVarChar, userEmail); } else if ((userRole === 'rre' || userRole === 'super' || userRole === 'lecteur') && userCampus) { query += ` AND e.Campus = @userCampus`; request.input('userCampus', sql.NVarChar, userCampus); } const evalResult = await request.query(query); if (evalResult.recordset.length === 0) { return res.status(404).json({ error: 'Étudiant non trouvé ou non autorisé' }); } const record = evalResult.recordset[0]; const historyResult = await pool.request() .input('studentId', sql.NVarChar, studentId) .query(` SELECT action_type as action, action_description as details, author_name as author, created_at as date FROM [dbo].[tbl_Historique_Evaluations] WHERE record_id = @studentId ORDER BY created_at DESC `); res.json({ ...record, email: `${record.id}@example.com`, campusPromo: record.campus, history: historyResult.recordset }); console.log('✅ Détail étudiant envoyé:', studentId); } catch (error) { console.error('❌ Erreur détail évaluation:', error); res.status(500).json({ error: 'Erreur détail évaluation' }); } }); // ═══════════════════════════════════════════════════════ // METTRE À JOUR UNE ÉVALUATION (VERSION CORRIGÉE) // Historique : enregistre TOUTES les valeurs à chaque action // ═══════════════════════════════════════════════════════ app.put('/api/evaluations/:studentId', async (req, res) => { try { const { studentId } = req.params; const updates = req.body.updates; const author = req.body.author; const effectiveUser = getEffectiveUser(req.headers); const { email, role, campus, isSuperUser, isLecteur } = effectiveUser; // 🔥 BLOQUER LES LECTEURS if (isLecteur) { console.log(`🚫 Tentative de modification refusée: ${email} (lecteur)`); return res.status(403).json({ error: 'Vous n\'avez pas les droits de modification', message: 'Votre rôle est en lecture seule' }); } const pool = await sql.connect(sqlConfig); // Vérification des accès (sauf super-user) if (!isSuperUser) { if (role === 'formateur' && email) { const accessCheck = await pool.request() .input('studentId', sql.NVarChar, studentId) .input('userEmail', sql.NVarChar, email) .query(` SELECT COUNT(*) as count FROM [dbo].[v_R_Promo_Formateur_Etudiant] WHERE LOGin = @studentId AND userPrincipalName = @userEmail `); if (accessCheck.recordset[0].count === 0) { console.log(`🚫 Accès refusé: ${email} n'a pas accès à ${studentId}`); return res.status(403).json({ error: 'Accès non autorisé à cet étudiant' }); } } else if (role === 'rre' && campus) { const accessCheck = await pool.request() .input('studentId', sql.NVarChar, studentId) .input('userCampus', sql.NVarChar, campus) .query(` SELECT COUNT(*) as count FROM [dbo].[v_R_Promo_Formateur_Etudiant] WHERE LOGin = @studentId AND Campus = @userCampus `); if (accessCheck.recordset[0].count === 0) { console.log(`🚫 Accès refusé: RRE ${email} n'a pas accès à ${studentId}`); return res.status(403).json({ error: 'Accès non autorisé à cet étudiant' }); } } } else { console.log(`🎭 SUPER-USER en action: ${email} modifie ${studentId}`); } // Récupérer l'enregistrement actuel const currentResult = await pool.request() .input('studentId', sql.NVarChar, studentId) .query(`SELECT * FROM [dbo].[tbl_Evaluations] WHERE student_id = @studentId`); const currentRecord = currentResult.recordset[0]; // ═══════════════════════════════════════════════════════ // CAS 1 : CRÉATION // ═══════════════════════════════════════════════════════ if (!currentRecord) { const studentInfo = await pool.request() .input('studentId', sql.NVarChar, studentId) .query(` SELECT TOP 1 LOGin, NOM, PRENOM, Campus, NOM_DIP, Annee FROM [dbo].[v_R_Promo_Formateur_Etudiant] WHERE LOGin = @studentId `); if (studentInfo.recordset.length === 0) { return res.status(404).json({ error: 'Étudiant non trouvé' }); } const student = studentInfo.recordset[0]; const today = new Date().toISOString().split('T')[0]; await pool.request() .input('student_id', sql.NVarChar, studentId) .input('nom', sql.NVarChar, student.NOM || '') .input('prenom', sql.NVarChar, student.PRENOM || '') .input('campus', sql.NVarChar, student.Campus || '') .input('formation', sql.NVarChar, student.NOM_DIP || '') .input('promo_nom_dip', sql.NVarChar, student.NOM_DIP || '') .input('annee', sql.NVarChar, String(student.Annee ?? '')) .input('auto_eval', sql.NVarChar, updates.autoEvaluation || 'NON') .input('eval_tuteur', sql.NVarChar, updates.evaluationTuteur || 'NON') .input('date_action', sql.Date, updates.dateAction || (role === 'formateur' || isSuperUser ? today : null)) .input('date_action_rre', sql.Date, updates.dateActionRRE || (role === 'rre' || isSuperUser ? today : null)) .input('commentaire_formateur', sql.NVarChar, updates.commentaireFormateur || null) .input('date_eval_tuteur', sql.Date, updates.dateEvalTuteur || null) .input('commentaire_rre', sql.NVarChar, updates.commentaireRRE || null) .input('commentaire', sql.NVarChar, updates.commentaire || null) .input('created_by', sql.NVarChar, author.email) .query(` INSERT INTO [dbo].[tbl_Evaluations] (student_id, nom, prenom, campus, formation, promo_nom_dip, annee, auto_evaluation, evaluation_tuteur, date_action, date_action_rre, commentaire_formateur, date_eval_tuteur, commentaire_rre, commentaire, created_by, updated_by) VALUES (@student_id, @nom, @prenom, @campus, @formation, @promo_nom_dip, @annee, @auto_eval, @eval_tuteur, @date_action, @date_action_rre, @commentaire_formateur, @date_eval_tuteur, @commentaire_rre, @commentaire, @created_by, @created_by) `); // Log historique complet pour la création await logHistoryComplete({ record_id: studentId, action_type: 'CREATION', action_description: `Création du dossier d'évaluation${isSuperUser ? ' [SUPER-USER]' : ''}`, author_email: author.email, author_name: author.name, author_role: author.role, campus: student.Campus, auto_evaluation_new: updates.autoEvaluation || 'NON', evaluation_tuteur_new: updates.evaluationTuteur || 'NON', date_action_new: updates.dateAction || (role === 'formateur' || isSuperUser ? today : null), date_action_rre_new: updates.dateActionRRE || (role === 'rre' || isSuperUser ? today : null), commentaire_formateur_new: updates.commentaireFormateur || null, date_eval_tuteur_new: updates.dateEvalTuteur || null, commentaire_rre_new: updates.commentaireRRE || null, commentaire_new: updates.commentaire || null }); const createdResult = await pool.request() .input('studentId', sql.NVarChar, studentId) .query(` SELECT student_id as id, nom, prenom, campus, formation, promo_nom_dip as nomDip, annee, auto_evaluation as autoEvaluation, evaluation_tuteur as evaluationTuteur, date_action as dateAction, date_action_rre as dateActionRRE, commentaire_formateur as commentaireFormateur, date_eval_tuteur as dateEvalTuteur, commentaire_rre as commentaireRRE, commentaire, updated_at as lastUpdate FROM [dbo].[tbl_Evaluations] WHERE student_id = @studentId `); console.log('✅ Nouvelle évaluation créée:', studentId); return res.json({ success: true, message: 'Évaluation créée', data: createdResult.recordset[0] }); } // ═══════════════════════════════════════════════════════ // CAS 2 : MISE À JOUR // ═══════════════════════════════════════════════════════ let updateQuery = 'UPDATE [dbo].[tbl_Evaluations] SET updated_at = GETDATE(), updated_by = @updated_by'; const request = pool.request() .input('studentId', sql.NVarChar, studentId) .input('updated_by', sql.NVarChar, author.email); let hasChanges = false; const canEditFormateur = role === 'formateur' || isSuperUser; const canEditRRE = role === 'rre' || isSuperUser; // ── Construire les valeurs finales (après mise à jour) ── // On part des valeurs actuelles et on applique les modifications let finalAutoEval = currentRecord.auto_evaluation || 'NON'; let finalEvalTuteur = currentRecord.evaluation_tuteur || 'NON'; let finalDateAction = currentRecord.date_action ? currentRecord.date_action.toISOString().split('T')[0] : null; let finalDateActionRRE = currentRecord.date_action_rre ? currentRecord.date_action_rre.toISOString().split('T')[0] : null; let finalDateEvalTuteur = currentRecord.date_eval_tuteur ? currentRecord.date_eval_tuteur.toISOString().split('T')[0] : null; let finalCommentaireFormateur = currentRecord.commentaire_formateur || null; let finalCommentaireRRE = currentRecord.commentaire_rre || null; let finalCommentaire = currentRecord.commentaire || null; // Auto-évaluation if (canEditFormateur && updates.autoEvaluation !== undefined && updates.autoEvaluation !== currentRecord.auto_evaluation) { updateQuery += ', auto_evaluation = @auto_eval'; request.input('auto_eval', sql.NVarChar, updates.autoEvaluation); finalAutoEval = updates.autoEvaluation; hasChanges = true; } // Évaluation tuteur if (canEditFormateur && updates.evaluationTuteur !== undefined && updates.evaluationTuteur !== currentRecord.evaluation_tuteur) { updateQuery += ', evaluation_tuteur = @eval_tuteur'; request.input('eval_tuteur', sql.NVarChar, updates.evaluationTuteur); finalEvalTuteur = updates.evaluationTuteur; hasChanges = true; } // Date action formateur if (canEditFormateur) { if (updates.autoEvaluation !== undefined || updates.evaluationTuteur !== undefined || updates.commentaireFormateur !== undefined || updates.dateEvalTuteur !== undefined || updates.commentaire !== undefined || updates.dateAction !== undefined) { const today = updates.dateAction || new Date().toISOString().split('T')[0]; const currentDateAction = currentRecord.date_action ? currentRecord.date_action.toISOString().split('T')[0] : null; if (currentDateAction !== today) { updateQuery += ', date_action = @date_action'; request.input('date_action', sql.Date, today); finalDateAction = today; hasChanges = true; } } } // Commentaire formateur if (canEditFormateur && updates.commentaireFormateur !== undefined && updates.commentaireFormateur !== currentRecord.commentaire_formateur) { updateQuery += ', commentaire_formateur = @commentaire_formateur'; request.input('commentaire_formateur', sql.NVarChar, updates.commentaireFormateur || null); finalCommentaireFormateur = updates.commentaireFormateur || null; hasChanges = true; } // Date évaluation tuteur if (canEditFormateur && updates.dateEvalTuteur !== undefined) { const currentDateEvalTuteur = currentRecord.date_eval_tuteur ? currentRecord.date_eval_tuteur.toISOString().split('T')[0] : null; const newDateEvalTuteur = updates.dateEvalTuteur || null; if (currentDateEvalTuteur !== newDateEvalTuteur) { updateQuery += ', date_eval_tuteur = @date_eval_tuteur'; request.input('date_eval_tuteur', sql.Date, newDateEvalTuteur); finalDateEvalTuteur = newDateEvalTuteur; hasChanges = true; } } // Date action RRE if (canEditRRE) { if ((updates.commentaireRRE !== undefined && updates.commentaireRRE.trim() !== '') || updates.dateActionRRE !== undefined) { const today = updates.dateActionRRE || new Date().toISOString().split('T')[0]; const currentDateActionRRE = currentRecord.date_action_rre ? currentRecord.date_action_rre.toISOString().split('T')[0] : null; if (currentDateActionRRE !== today) { updateQuery += ', date_action_rre = @date_action_rre'; request.input('date_action_rre', sql.Date, today); finalDateActionRRE = today; hasChanges = true; } } } // Commentaire RRE if (canEditRRE && updates.commentaireRRE !== undefined && updates.commentaireRRE !== currentRecord.commentaire_rre) { updateQuery += ', commentaire_rre = @commentaire_rre'; request.input('commentaire_rre', sql.NVarChar, updates.commentaireRRE || null); finalCommentaireRRE = updates.commentaireRRE || null; hasChanges = true; } // Commentaire général if (updates.commentaire !== undefined && updates.commentaire !== currentRecord.commentaire) { updateQuery += ', commentaire = @commentaire'; request.input('commentaire', sql.NVarChar, updates.commentaire || null); finalCommentaire = updates.commentaire || null; hasChanges = true; } // Exécuter la mise à jour et l'historique if (hasChanges) { updateQuery += ' WHERE student_id = @studentId'; await request.query(updateQuery); // ═══════════════════════════════════════════════════════ // 🔥 HISTORIQUE : on enregistre TOUTES les valeurs finales // (pas seulement les champs modifiés) // Comme ça chaque ligne d'historique est complète // ═══════════════════════════════════════════════════════ await logHistoryComplete({ record_id: studentId, action_type: 'Nouvelle saisie', action_description: `Mise à jour de l'évaluation${isSuperUser ? ' [SUPER-USER]' : ''}`, author_email: author.email, author_name: author.name, author_role: author.role, campus: author.campus, // Toujours remplir TOUTES les colonnes _new avec l'état final auto_evaluation_old: currentRecord.auto_evaluation || null, auto_evaluation_new: finalAutoEval, evaluation_tuteur_old: currentRecord.evaluation_tuteur || null, evaluation_tuteur_new: finalEvalTuteur, date_action_old: currentRecord.date_action || null, date_action_new: finalDateAction, date_action_rre_old: currentRecord.date_action_rre || null, date_action_rre_new: finalDateActionRRE, date_eval_tuteur_old: currentRecord.date_eval_tuteur || null, date_eval_tuteur_new: finalDateEvalTuteur, commentaire_formateur_old: currentRecord.commentaire_formateur || null, commentaire_formateur_new: finalCommentaireFormateur, commentaire_rre_old: currentRecord.commentaire_rre || null, commentaire_rre_new: finalCommentaireRRE, commentaire_old: currentRecord.commentaire || null, commentaire_new: finalCommentaire }); console.log(`✅ Évaluation mise à jour: ${studentId}${isSuperUser ? ' [SUPER-USER]' : ''}`); } else { console.log('ℹ️ Aucune modification détectée pour:', studentId); } // Récupérer et renvoyer l'enregistrement mis à jour const updatedResult = await pool.request() .input('studentId', sql.NVarChar, studentId) .query(` SELECT student_id as id, nom, prenom, campus, formation, promo_nom_dip as nomDip, annee, auto_evaluation as autoEvaluation, evaluation_tuteur as evaluationTuteur, date_action as dateAction, date_action_rre as dateActionRRE, commentaire_formateur as commentaireFormateur, date_eval_tuteur as dateEvalTuteur, commentaire_rre as commentaireRRE, commentaire, updated_at as lastUpdate FROM [dbo].[tbl_Evaluations] WHERE student_id = @studentId `); res.json({ success: true, message: 'Évaluation mise à jour', data: updatedResult.recordset[0] }); } catch (error) { console.error('❌ Erreur mise à jour:', error); res.status(500).json({ error: 'Erreur mise à jour', details: error.message }); } }); // ═══════════════════════════════════════════════════════ // DÉMARRAGE // ═══════════════════════════════════════════════════════ app.listen(3005, () => { console.log('\n🚀 Backend démarré sur port 3005'); console.log('📡 Routes disponibles:'); console.log(' POST /auth/verify'); console.log(' GET /api/campus-list'); console.log(' GET /api/student-list?campus=XXX&promo=XXX'); console.log(' GET /api/promo-list?campus=XXX'); console.log(' GET /api/promo-data'); console.log(' GET /api/evaluations'); console.log(' GET /api/evaluations/:studentId'); console.log(' GET /api/history/:studentId'); console.log(' PUT /api/evaluations/:studentId'); console.log('\n📋 Règles d\'accès:'); console.log(' - Lecteur: voit uniquement SON campus (LECTURE SEULE)'); console.log(' - Super: voit et modifie uniquement SON campus'); console.log(' - Formateur: voit uniquement SES étudiants'); console.log(' - RRE: voit uniquement les étudiants de SON campus'); console.log('\n📜 Historique: UNE LIGNE PAR ACTION avec TOUTES les valeurs\n'); });