Files
RGC/SuiviREForamteur/suivireforamteur/public/backend/server.js
2026-02-11 13:57:54 +01:00

1222 lines
54 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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');
});