diff --git a/.vs/slnx.sqlite b/.vs/slnx.sqlite index a0a60e29..a033adcc 100644 Binary files a/.vs/slnx.sqlite and b/.vs/slnx.sqlite differ diff --git a/GTF/project/backend/config/server.js b/GTF/project/backend/config/server.js index 55da0418..9bc478b8 100644 --- a/GTF/project/backend/config/server.js +++ b/GTF/project/backend/config/server.js @@ -1,4 +1,4 @@ -const express = require('express'); +const express = require('express'); const cors = require('cors'); const sql = require('mssql'); require('dotenv').config(); @@ -6,7 +6,7 @@ require('dotenv').config(); const app = express(); const PORT = 3001; -// Configuration base de données +// Configuration base de données const dbConfig = { server: process.env.DB_SERVER, database: process.env.DB_DATABASE, @@ -26,20 +26,34 @@ app.use(cors({ })); app.use(express.json()); -// Log de toutes les requêtes +// Log de toutes les requêtes app.use((req, res, next) => { console.log(`${new Date().toISOString()} - ${req.method} ${req.path} depuis ${req.get('origin') || 'inconnu'}`); + if (req.query.user_email || req.query.email) { + console.log(` - Filtrage pour utilisateur: ${req.query.user_email || req.query.email}`); + } next(); }); -// Variable pour stocker la connexion +// Variable pour stocker la connexion et l'état de la migration let pool = null; +let systemStatus = { + hasFormateurEmailColumn: false, + hasFormateurView: false, + canAccessFormateurView: false, + hasFormateurLocal: false, + operatingMode: 'unknown' +}; -// Fonction pour se connecter à la base +// Fonction pour se connecter à la base async function connectDatabase() { try { pool = await sql.connect(dbConfig); - console.log('Base de données connectée'); + console.log('Base de données connectée'); + + // Diagnostic automatique de la structure et permissions + await checkSystemStatus(); + return true; } catch (error) { console.error('Erreur de connexion :', error.message); @@ -47,116 +61,345 @@ async function connectDatabase() { } } -// Route de test -app.get('/api/test', (req, res) => { - res.json({ - message: 'Le serveur fonctionne !', - timestamp: new Date().toISOString() - }); -}); - -// Route pour tester la base de données -app.get('/api/db-test', async (req, res) => { +// Fonction pour vérifier l'état complet du système +async function checkSystemStatus() { try { - if (!pool) { - return res.status(500).json({ error: 'Base non connectée' }); + // 1. Vérifier si la colonne formateur_email_fk existe + const columnCheck = await pool.request().query(` + SELECT COUNT(*) as count + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_NAME = 'declarations' + AND COLUMN_NAME = 'formateur_email_fk' + `); + systemStatus.hasFormateurEmailColumn = columnCheck.recordset[0].count > 0; + + // 2. Vérifier si la vue Formateurs existe + const viewCheck = await pool.request().query(` + SELECT COUNT(*) as count + FROM INFORMATION_SCHEMA.VIEWS + WHERE TABLE_NAME = 'Formateurs' + `); + systemStatus.hasFormateurView = viewCheck.recordset[0].count > 0; + + // 3. Tester l'accès à la vue Formateurs si elle existe + if (systemStatus.hasFormateurView) { + try { + await pool.request().query(`SELECT TOP 1 userPrincipalName FROM [dbo].[Formateurs]`); + systemStatus.canAccessFormateurView = true; + console.log('✅ Accès à la vue Formateurs: OK'); + } catch (error) { + systemStatus.canAccessFormateurView = false; + console.log('⌠Accès à la vue Formateurs: ERREUR -', error.message); + if (error.message.includes('HP-TO-O365')) { + console.log('💡 Problème de permissions sur la base HP-TO-O365'); + } + } + } + + // 4. Vérifier si la table formateurs_local existe et est accessible + try { + await pool.request().query(`SELECT TOP 1 * FROM formateurs_local`); + systemStatus.hasFormateurLocal = true; + console.log('✅ Table formateurs_local: OK'); + } catch (error) { + systemStatus.hasFormateurLocal = false; + console.log('⌠Table formateurs_local: non accessible'); + } + + // 5. Déterminer le mode de fonctionnement optimal + if (systemStatus.hasFormateurEmailColumn && systemStatus.canAccessFormateurView) { + systemStatus.operatingMode = 'new_with_view'; + } else if (systemStatus.hasFormateurEmailColumn && systemStatus.hasFormateurLocal) { + systemStatus.operatingMode = 'new_with_local'; + } else if (systemStatus.hasFormateurEmailColumn) { + systemStatus.operatingMode = 'new_email_only'; + } else { + systemStatus.operatingMode = 'legacy_hash'; + } + + console.log('📊 État du système:'); + console.log(` - Colonne formateur_email_fk: ${systemStatus.hasFormateurEmailColumn ? '✅' : 'âŒ'}`); + console.log(` - Vue Formateurs: ${systemStatus.hasFormateurView ? '✅' : 'âŒ'}`); + console.log(` - Accès vue Formateurs: ${systemStatus.canAccessFormateurView ? '✅' : 'âŒ'}`); + console.log(` - Table formateurs_local: ${systemStatus.hasFormateurLocal ? '✅' : 'âŒ'}`); + console.log(` - Mode de fonctionnement: ${systemStatus.operatingMode}`); + + } catch (error) { + console.error('Erreur lors du diagnostic:', error.message); + systemStatus.operatingMode = 'legacy_hash'; + } +} + +// Route de diagnostic complet +app.get('/api/diagnostic', async (req, res) => { + try { + await checkSystemStatus(); + + let recommendations = []; + + switch (systemStatus.operatingMode) { + case 'new_with_view': + recommendations.push('✅ Système optimal - toutes les fonctionnalités disponibles'); + break; + case 'new_with_local': + recommendations.push('âš ï¸ Fonctionne avec la table locale - pas d\'accès à la vue distante'); + recommendations.push('💡 Vérifier les permissions sur HP-TO-O365 pour utiliser la vue'); + break; + case 'new_email_only': + recommendations.push('âš ï¸ Mode dégradé - sauvegarde par email mais pas de détails formateurs'); + recommendations.push('💡 Restaurer l\'accès à la vue Formateurs ou table formateurs_local'); + break; + case 'legacy_hash': + recommendations.push('🔄 Mode compatibilité - utilise l\'ancien système de hash'); + recommendations.push('💡 Appliquer la migration avec POST /api/migrate'); + break; } - const result = await pool.request().query('SELECT COUNT(*) as total FROM FormateurSqy'); res.json({ - message: 'Base OK', - formateurs: result.recordset[0].total + systemStatus, + recommendations, + currentMode: systemStatus.operatingMode }); + } catch (error) { res.status(500).json({ error: error.message }); } }); -// Route pour lister les formateurs -app.get('/api/formateurs', async (req, res) => { +// Route pour appliquer la migration +app.post('/api/migrate', async (req, res) => { try { - const result = await pool.request().query(` - SELECT TOP 10 - NUMERO, - NOM_ENS as nom, - PRENOM_ENS as prenom, - EMAIL1 as email - FROM FormateurSqy - WHERE EMAIL1 IS NOT NULL - ORDER BY NOM_ENS - `); + const steps = []; + + // Étape 1: Ajouter la colonne si nécessaire + if (!systemStatus.hasFormateurEmailColumn) { + try { + await pool.request().query(` + ALTER TABLE [dbo].[declarations] + ADD [formateur_email_fk] [nvarchar](255) NULL + `); + steps.push('✅ Colonne formateur_email_fk ajoutée'); + } catch (error) { + if (!error.message.includes('already exists')) { + throw error; + } + steps.push('â„¹ï¸ Colonne formateur_email_fk déjà existante'); + } + } + + // Étape 2: Créer un index + try { + await pool.request().query(` + CREATE NONCLUSTERED INDEX [IX_declarations_formateur_email_fk] + ON [dbo].[declarations] ([formateur_email_fk]) + `); + steps.push('✅ Index créé'); + } catch (error) { + if (error.message.includes('already exists')) { + steps.push('â„¹ï¸ Index déjà existant'); + } else { + steps.push(`âš ï¸ Erreur index: ${error.message}`); + } + } + + // Vérifier à nouveau l'état + await checkSystemStatus(); res.json({ success: true, - data: result.recordset + steps, + newStatus: systemStatus, + message: `Migration appliquée - Mode: ${systemStatus.operatingMode}` }); + } catch (error) { res.status(500).json({ success: false, - error: error.message + error: error.message, + message: 'Erreur lors de la migration' }); } }); -// Route pour sauvegarder une déclaration +// Route de test +app.get('/api/test', (req, res) => { + res.json({ + message: 'Le serveur utilisateur fonctionne !', + timestamp: new Date().toISOString(), + systemStatus + }); +}); + +// Fonction pour générer un hash reproductible depuis un email (mode legacy) +function generateHashFromEmail(email) { + let hash = 0; + for (let i = 0; i < email.length; i++) { + const char = email.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; // Convert to 32-bit integer + } + return Math.abs(hash) % 10000 + 1000; +} + +// Route pour les formateurs (adaptée au mode de fonctionnement) +app.get('/api/formateurs', async (req, res) => { + try { + const { email, search } = req.query; + + switch (systemStatus.operatingMode) { + case 'new_with_view': + // Utiliser la vue Formateurs + let query = ` + SELECT + userPrincipalName as email, + displayName as nom_complet, + givenname as prenom, + surname as nom, + Campus, + departement, + Jobtitle + FROM [dbo].[Formateurs] + `; + + const request = pool.request(); + + if (email) { + query += ` WHERE userPrincipalName = @email`; + request.input('email', sql.NVarChar, email); + } else if (search) { + query += ` WHERE displayName LIKE @search OR userPrincipalName LIKE @search`; + request.input('search', sql.NVarChar, `%${search}%`); + } + + query += ` ORDER BY displayName`; + + const result = await request.query(query); + + res.json({ + success: true, + data: result.recordset, + source: 'vue_formateurs' + }); + break; + + case 'new_with_local': + // Utiliser la table formateurs_local + let localQuery = ` + SELECT + userPrincipalName as email, + displayName as nom_complet, + givenname as prenom, + surname as nom, + Campus, + departement, + Jobtitle + FROM formateurs_local + `; + + const localRequest = pool.request(); + + if (email) { + localQuery += ` WHERE userPrincipalName = @email`; + localRequest.input('email', sql.NVarChar, email); + } else if (search) { + localQuery += ` WHERE displayName LIKE @search OR userPrincipalName LIKE @search`; + localRequest.input('search', sql.NVarChar, `%${search}%`); + } + + localQuery += ` ORDER BY displayName`; + + const localResult = await localRequest.query(localQuery); + + res.json({ + success: true, + data: localResult.recordset, + source: 'table_locale' + }); + break; + + case 'new_email_only': + case 'legacy_hash': + default: + // Mode dégradé - réponse basique + if (email) { + res.json({ + success: true, + data: [{ + email: email, + nom_complet: email.split('@')[0] || 'Utilisateur', + prenom: 'Utilisateur', + nom: email.split('@')[0] || 'Inconnu', + source: 'fallback' + }] + }); + } else { + res.json({ + success: true, + data: [], + source: 'fallback' + }); + } + break; + } + + } catch (error) { + console.error('Erreur recherche formateurs:', error); + // Fallback en cas d'erreur + if (email) { + res.json({ + success: true, + data: [{ + email: email, + nom_complet: email.split('@')[0] || 'Utilisateur', + prenom: 'Utilisateur', + nom: email.split('@')[0] || 'Inconnu', + source: 'error_fallback' + }] + }); + } else { + res.status(500).json({ + success: false, + error: error.message + }); + } + } +}); + +// Conversion simple en string "HH:mm:ss" +const toSQLTime = (timeStr) => { + if (!timeStr) return null; + const [hours, minutes] = timeStr.split(':').map(Number); + const d = new Date(Date.UTC(2000, 0, 1, hours, minutes, 0)); + return d; +}; + +// Route pour sauvegarder une déclaration (ADAPTÉE AU MODE) app.post('/api/save_declaration', async (req, res) => { try { - const { date, activityType, hours, description, user } = req.body; + const { date, activityType, hours, description, user, startTime, endTime } = req.body; + console.log('Données reçues:', { date, activityType, hours, description, user, startTime, endTime }); - console.log('Données reçues:', { date, activityType, hours, description, user }); + const heureDebutSQL = toSQLTime(startTime); + const heureFinSQL = toSQLTime(endTime); - // Validation simple - if (!date || !activityType || !hours) { + // Validation + if (!date || !activityType || !hours || !startTime || !endTime) { return res.status(400).json({ success: false, - error: 'Données manquantes' + error: 'Données manquantes' }); } - let formateurNumero; - - if (user && user.email) { - // Chercher le formateur par email - const formateurResult = await pool.request() - .input('email', sql.VarChar, user.email) - .query('SELECT NUMERO FROM FormateurSqy WHERE EMAIL1 = @email'); - - if (formateurResult.recordset.length > 0) { - // Formateur trouvé - formateurNumero = formateurResult.recordset[0].NUMERO; - console.log('Formateur trouvé par email:', formateurNumero); - } else { - // Créer un nouveau formateur - const maxNumeroResult = await pool.request() - .query('SELECT ISNULL(MAX(NUMERO), 0) + 1 as nextNumero FROM FormateurSqy'); - - const nextNumero = maxNumeroResult.recordset[0].nextNumero; - - await pool.request() - .input('numero', sql.Int, nextNumero) - .input('nom', sql.VarChar, user.nom || 'Inconnu') - .input('prenom', sql.VarChar, user.prenom || 'Inconnu') - .input('email', sql.VarChar, user.email) - .input('department', sql.VarChar, user.department || 'Non défini') - .input('role', sql.VarChar, user.role || 'Employé') - .query(` - INSERT INTO FormateurSqy ( - NUMERO, NOM_ENS, PRENOM_ENS, EMAIL1, [Ecole - Pole], Contrat - ) VALUES ( - @numero, @nom, @prenom, @email, @department, @role - ) - `); - - formateurNumero = nextNumero; - console.log('Nouveau formateur créé:', formateurNumero); - } - } else { - // Fallback : utiliser un formateur par défaut - formateurNumero = 999; - console.log('Utilisation du formateur par défaut:', formateurNumero); + if (!user || !user.email) { + return res.status(400).json({ + success: false, + error: 'Email utilisateur requis' + }); } - // Récupérer l'ID du type de demande + const userEmail = user.email; + + // Récupération du type de demande const typeResult = await pool.request() .input('activityType', sql.VarChar, activityType) .query('SELECT id FROM types_demandes WHERE libelle = @activityType'); @@ -164,56 +407,140 @@ app.post('/api/save_declaration', async (req, res) => { if (typeResult.recordset.length === 0) { return res.status(400).json({ success: false, - error: `Type d'activité invalide: ${activityType}` + error: `Type d'activité invalide: ${activityType}` }); } const typeDemandeId = typeResult.recordset[0].id; - // Vérifier si une déclaration existe déjà pour cette date - const existingResult = await pool.request() - .input('formateurNumero', sql.Int, formateurNumero) - .input('date', sql.Date, date) - .query('SELECT id FROM declarations WHERE formateur_numero = @formateurNumero AND date = @date'); + // Logique selon le mode de fonctionnement + if (systemStatus.operatingMode.startsWith('new_')) { + // NOUVEAU SYSTÈME - par email + console.log(`💾 Sauvegarde avec nouveau système (email) - Mode: ${systemStatus.operatingMode}`); - if (existingResult.recordset.length > 0) { - // Mise à jour - await pool.request() - .input('utilisateurId', sql.Int, formateurNumero) - .input('typeDemandeId', sql.Int, typeDemandeId) - .input('hours', sql.Float, hours) - .input('description', sql.NVarChar, description || null) - .input('formateurNumero', sql.Int, formateurNumero) + // Vérifier si une déclaration existe déjà + const existingResult = await pool.request() + .input('formateurEmail', sql.NVarChar, userEmail) .input('date', sql.Date, date) - .query(` - UPDATE declarations - SET utilisateur_id = @utilisateurId, type_demande_id = @typeDemandeId, duree = @hours, description = @description - WHERE formateur_numero = @formateurNumero AND date = @date - `); + .query('SELECT id FROM declarations WHERE formateur_email_fk = @formateurEmail AND date = @date'); + + const utilisateurId = 1; // À adapter selon votre logique + + if (existingResult.recordset.length > 0) { + // UPDATE + await pool.request() + .input('formateurEmail', sql.NVarChar, userEmail) + .input('typeDemandeId', sql.Int, typeDemandeId) + .input('hours', sql.Float, hours) + .input('description', sql.NVarChar, description || null) + .input('date', sql.Date, date) + .input('heure_debut', sql.Time, heureDebutSQL) + .input('heure_fin', sql.Time, heureFinSQL) + .query(` + UPDATE declarations + SET type_demande_id = @typeDemandeId, + duree = @hours, + description = @description, + heure_debut = @heure_debut, + heure_fin = @heure_fin + WHERE formateur_email_fk = @formateurEmail AND date = @date + `); + console.log('✅ Déclaration mise à jour'); + } else { + // INSERT + await pool.request() + .input('utilisateurId', sql.Int, utilisateurId) + .input('formateurEmail', sql.NVarChar, userEmail) + .input('typeDemandeId', sql.Int, typeDemandeId) + .input('date', sql.Date, date) + .input('hours', sql.Float, hours) + .input('description', sql.NVarChar, description || null) + .input('heure_debut', sql.Time, heureDebutSQL) + .input('heure_fin', sql.Time, heureFinSQL) + .query(` + INSERT INTO declarations ( + utilisateur_id, formateur_email_fk, type_demande_id, + date, duree, description, heure_debut, heure_fin + ) + VALUES ( + @utilisateurId, @formateurEmail, @typeDemandeId, + @date, @hours, @description, @heure_debut, @heure_fin + ) + `); + console.log('✅ Nouvelle déclaration créée'); + } + + res.json({ + success: true, + message: `Déclaration sauvegardée avec succès (${systemStatus.operatingMode})`, + formateurEmail: userEmail + }); - console.log('Déclaration mise à jour'); } else { - // Création - await pool.request() - .input('utilisateurId', sql.Int, formateurNumero) + // ANCIEN SYSTÈME - par hash + console.log('💾 Sauvegarde avec ancien système (hash)'); + + const formateurNumero = generateHashFromEmail(userEmail); + + // Vérifier si une déclaration existe déjà + const existingResult = await pool.request() .input('formateurNumero', sql.Int, formateurNumero) - .input('typeDemandeId', sql.Int, typeDemandeId) .input('date', sql.Date, date) - .input('hours', sql.Float, hours) - .input('description', sql.NVarChar, description || null) - .query(` - INSERT INTO declarations (utilisateur_id, formateur_numero, type_demande_id, date, duree, description) - VALUES (@utilisateurId, @formateurNumero, @typeDemandeId, @date, @hours, @description) - `); + .query('SELECT id FROM declarations WHERE formateur_numero = @formateurNumero AND date = @date'); - console.log('Nouvelle déclaration créée'); + if (existingResult.recordset.length > 0) { + // UPDATE + await pool.request() + .input('utilisateurId', sql.Int, formateurNumero) + .input('typeDemandeId', sql.Int, typeDemandeId) + .input('hours', sql.Float, hours) + .input('description', sql.NVarChar, description || null) + .input('formateurNumero', sql.Int, formateurNumero) + .input('date', sql.Date, date) + .input('heure_debut', sql.Time, heureDebutSQL) + .input('heure_fin', sql.Time, heureFinSQL) + .query(` + UPDATE declarations + SET utilisateur_id = @utilisateurId, + type_demande_id = @typeDemandeId, + duree = @hours, + description = @description, + heure_debut = @heure_debut, + heure_fin = @heure_fin + WHERE formateur_numero = @formateurNumero AND date = @date + `); + console.log('✅ Déclaration mise à jour (legacy)'); + } else { + // INSERT + await pool.request() + .input('utilisateurId', sql.Int, formateurNumero) + .input('formateurNumero', sql.Int, formateurNumero) + .input('typeDemandeId', sql.Int, typeDemandeId) + .input('date', sql.Date, date) + .input('hours', sql.Float, hours) + .input('description', sql.NVarChar, description || null) + .input('heure_debut', sql.Time, heureDebutSQL) + .input('heure_fin', sql.Time, heureFinSQL) + .query(` + INSERT INTO declarations ( + utilisateur_id, formateur_numero, type_demande_id, + date, duree, description, heure_debut, heure_fin + ) + VALUES ( + @utilisateurId, @formateurNumero, @typeDemandeId, + @date, @hours, @description, @heure_debut, @heure_fin + ) + `); + console.log('✅ Nouvelle déclaration créée (legacy)'); + } + + res.json({ + success: true, + message: 'Déclaration sauvegardée avec succès (legacy)', + formateurNumero: formateurNumero + }); } - res.json({ - success: true, - message: 'Déclaration sauvegardée avec succès' - }); - } catch (error) { console.error('Erreur lors de la sauvegarde:', error); res.status(500).json({ @@ -223,60 +550,307 @@ app.post('/api/save_declaration', async (req, res) => { } }); -// Route pour récupérer les déclarations -app.get('/api/get_declarations', async (req, res) => { +// Route pour récupérer par email (ADAPTÉE AU MODE) +app.get('/api/get_declarations_by_email', async (req, res) => { try { - const result = await pool.request().query(` - SELECT - d.id, - d.formateur_numero as utilisateur_id, - td.id as type_demande_id, - d.date, - d.duree, - d.description, - d.formateur_numero, - td.libelle as activityType - FROM declarations d - INNER JOIN types_demandes td ON d.type_demande_id = td.id - ORDER BY d.date DESC - `); + const { email } = req.query; + console.log('DEBUG - Email reçu:', email); - res.json(result.recordset); + if (!email) { + return res.status(400).json({ error: 'Email requis' }); + } + + switch (systemStatus.operatingMode) { + case 'new_with_view': + // Avec vue Formateurs + const resultWithView = await pool.request() + .input('email', sql.NVarChar, email) + .query(` + SELECT + d.id, + d.utilisateur_id, + d.formateur_email_fk as formateur_email, + f.displayName as formateur_nom, + f.Campus, + f.departement, + td.id as type_demande_id, + td.libelle as activityType, + d.date, + d.duree, + d.heure_debut, + d.heure_fin, + d.description, + d.status + FROM declarations d + INNER JOIN types_demandes td ON d.type_demande_id = td.id + LEFT JOIN [dbo].[Formateurs] f ON d.formateur_email_fk = f.userPrincipalName + WHERE d.formateur_email_fk = @email + ORDER BY d.date DESC + `); + res.json(resultWithView.recordset); + break; + + case 'new_with_local': + // Avec table formateurs_local + const resultWithLocal = await pool.request() + .input('email', sql.NVarChar, email) + .query(` + SELECT + d.id, + d.utilisateur_id, + d.formateur_email_fk as formateur_email, + f.displayName as formateur_nom, + f.Campus, + f.departement, + td.id as type_demande_id, + td.libelle as activityType, + d.date, + d.duree, + d.heure_debut, + d.heure_fin, + d.description, + d.status + FROM declarations d + INNER JOIN types_demandes td ON d.type_demande_id = td.id + LEFT JOIN formateurs_local f ON d.formateur_email_fk = f.userPrincipalName + WHERE d.formateur_email_fk = @email + ORDER BY d.date DESC + `); + res.json(resultWithLocal.recordset); + break; + + case 'new_email_only': + // Sans jointure formateur + const resultEmailOnly = await pool.request() + .input('email', sql.NVarChar, email) + .query(` + SELECT + d.id, + d.utilisateur_id, + d.formateur_email_fk as formateur_email, + td.id as type_demande_id, + td.libelle as activityType, + d.date, + d.duree, + d.heure_debut, + d.heure_fin, + d.description, + d.status + FROM declarations d + INNER JOIN types_demandes td ON d.type_demande_id = td.id + WHERE d.formateur_email_fk = @email + ORDER BY d.date DESC + `); + res.json(resultEmailOnly.recordset); + break; + + case 'legacy_hash': + default: + // Ancien système avec hash + const formateurNumero = generateHashFromEmail(email); + console.log(`Email ${email} -> Numéro ${formateurNumero}`); + + const resultLegacy = await pool.request() + .input('formateur_numero', sql.Int, formateurNumero) + .query(` + SELECT + d.id, + d.formateur_numero as utilisateur_id, + td.id as type_demande_id, + d.date, + d.duree, + d.heure_debut, + d.heure_fin, + d.description, + td.libelle as activityType, + d.status + FROM declarations d + INNER JOIN types_demandes td ON d.type_demande_id = td.id + WHERE d.formateur_numero = @formateur_numero + ORDER BY d.date DESC + `); + res.json(resultLegacy.recordset); + break; + } + + console.log(`✅ Déclarations récupérées pour ${email} (mode: ${systemStatus.operatingMode})`); } catch (error) { - console.error('Erreur lors de la récupération:', error); + console.error('Erreur lors de la récupération par email:', error.message); res.status(500).json({ error: error.message }); } }); -// Démarrage du serveur +// Route pour récupérer toutes les déclarations +app.get('/api/get_declarations', async (req, res) => { + try { + const { user_email, admin } = req.query; + + // Utiliser la même logique que pour get_declarations_by_email + if (user_email && !admin) { + // Rediriger vers get_declarations_by_email + req.query = { email: user_email }; + return app._router.handle(req, res); + } + + // Mode admin - récupérer toutes les déclarations + let result; + switch (systemStatus.operatingMode) { + case 'new_with_view': + result = await pool.request().query(` + SELECT + d.id, + d.utilisateur_id, + d.formateur_email_fk as formateur_email, + f.displayName as formateur_nom, + f.Campus, + f.departement, + td.id as type_demande_id, + td.libelle as activityType, + d.date, + d.duree, + d.heure_debut, + d.heure_fin, + d.description, + d.status + FROM declarations d + INNER JOIN types_demandes td ON d.type_demande_id = td.id + LEFT JOIN [dbo].[Formateurs] f ON d.formateur_email_fk = f.userPrincipalName + ORDER BY d.date DESC + `); + break; + + case 'new_with_local': + result = await pool.request().query(` + SELECT + d.id, + d.utilisateur_id, + d.formateur_email_fk as formateur_email, + f.displayName as formateur_nom, + f.Campus, + f.departement, + td.id as type_demande_id, + td.libelle as activityType, + d.date, + d.duree, + d.heure_debut, + d.heure_fin, + d.description, + d.status + FROM declarations d + INNER JOIN types_demandes td ON d.type_demande_id = td.id + LEFT JOIN formateurs_local f ON d.formateur_email_fk = f.userPrincipalName + ORDER BY d.date DESC + `); + break; + + case 'new_email_only': + result = await pool.request().query(` + SELECT + d.id, + d.utilisateur_id, + d.formateur_email_fk as formateur_email, + td.id as type_demande_id, + td.libelle as activityType, + d.date, + d.duree, + d.heure_debut, + d.heure_fin, + d.description, + d.status + FROM declarations d + INNER JOIN types_demandes td ON d.type_demande_id = td.id + ORDER BY d.date DESC + `); + break; + + default: + result = await pool.request().query(` + SELECT + d.id, + d.formateur_numero as utilisateur_id, + td.id as type_demande_id, + d.date, + d.duree, + d.heure_debut, + d.heure_fin, + d.description, + d.formateur_numero, + td.libelle as activityType, + d.status + FROM declarations d + INNER JOIN types_demandes td ON d.type_demande_id = td.id + ORDER BY d.date DESC + `); + break; + } + + res.json(result.recordset); + + } catch (error) { + console.error('Erreur lors de la récupération:', error); + res.status(500).json({ error: error.message }); + } +}); + +// Route de création de formateur (pour compatibilité) +app.post('/api/create_formateur', (req, res) => { + res.json({ + success: true, + message: 'Route de compatibilité - utilisez /api/formateurs' + }); +}); + +// Démarrage du serveur async function startServer() { const dbConnected = await connectDatabase(); if (!dbConnected) { - console.log('Impossible de démarrer sans base de données'); + console.log('Impossible de démarrer sans base de données'); return; } app.listen(PORT, () => { - console.log(`Serveur démarré sur http://localhost:${PORT}`); + console.log(`🚀 Serveur UTILISATEUR démarré sur http://localhost:${PORT}`); + console.log(`📊 Mode de fonctionnement: ${systemStatus.operatingMode}`); + console.log(''); console.log('Routes disponibles :'); + console.log('- GET /api/diagnostic (vérifier l\'état)'); + console.log('- POST /api/migrate (appliquer la migration)'); console.log('- GET /api/test'); - console.log('- GET /api/db-test'); - console.log('- GET /api/formateurs'); + console.log('- GET /api/formateurs?email=test@example.com'); console.log('- POST /api/save_declaration'); console.log('- GET /api/get_declarations'); + console.log('- GET /api/get_declarations_by_email?email=test@example.com'); + console.log(''); + + switch (systemStatus.operatingMode) { + case 'new_with_view': + console.log('✅ Système optimal - utilise la vue Formateurs'); + break; + case 'new_with_local': + console.log('âš ï¸ Mode dégradé - utilise la table formateurs_local'); + console.log('💡 Conseil: Vérifier les permissions sur HP-TO-O365'); + break; + case 'new_email_only': + console.log('âš ï¸ Mode minimal - sauvegarde par email sans détails formateurs'); + break; + case 'legacy_hash': + console.log('🔄 Mode compatibilité - utilise l\'ancien système de hash'); + console.log('💡 Conseil: Appliquer la migration avec POST /api/migrate'); + break; + } }); } -// Arrêt propre +// Arrêt propre process.on('SIGINT', async () => { - console.log('Arrêt du serveur...'); + console.log('Arrêt du serveur utilisateur...'); if (pool) { await pool.close(); } process.exit(0); }); -// Démarrer -startServer(); \ No newline at end of file +// Démarrer +startServer() \ No newline at end of file diff --git a/GTF/project/node_modules/.vite/deps/_metadata.json b/GTF/project/node_modules/.vite/deps/_metadata.json index bbf56146..51ba413a 100644 --- a/GTF/project/node_modules/.vite/deps/_metadata.json +++ b/GTF/project/node_modules/.vite/deps/_metadata.json @@ -1,43 +1,43 @@ { - "hash": "1e9c2bee", - "configHash": "451161ff", + "hash": "5aba48b5", + "configHash": "2088881b", "lockfileHash": "b1ec30a4", - "browserHash": "57e609ed", + "browserHash": "ac0e96b1", "optimized": { "react": { "src": "../../react/index.js", "file": "react.js", - "fileHash": "4348729b", + "fileHash": "9a2d9b21", "needsInterop": true }, "react/jsx-dev-runtime": { "src": "../../react/jsx-dev-runtime.js", "file": "react_jsx-dev-runtime.js", - "fileHash": "9694e297", + "fileHash": "d21e6b25", "needsInterop": true }, "react/jsx-runtime": { "src": "../../react/jsx-runtime.js", "file": "react_jsx-runtime.js", - "fileHash": "4fa96245", + "fileHash": "286c86a9", "needsInterop": true }, "@azure/msal-browser": { "src": "../../@azure/msal-browser/dist/index.mjs", "file": "@azure_msal-browser.js", - "fileHash": "8be31f82", + "fileHash": "90396c8f", "needsInterop": false }, "react-dom/client": { "src": "../../react-dom/client.js", "file": "react-dom_client.js", - "fileHash": "8aa9b52d", + "fileHash": "a16fb88b", "needsInterop": true }, "react-router-dom": { "src": "../../../../node_modules/react-router-dom/dist/index.mjs", "file": "react-router-dom.js", - "fileHash": "f4ac1124", + "fileHash": "a93f0753", "needsInterop": false } }, diff --git a/GTF/project/src/App.tsx b/GTF/project/src/App.tsx index 33527dc9..f7e9f7ab 100644 --- a/GTF/project/src/App.tsx +++ b/GTF/project/src/App.tsx @@ -13,41 +13,57 @@ function App() { const [showProfile, setShowProfile] = useState(false); const [loading, setLoading] = useState(true); - // Fonction pour charger les déclarations depuis la base - const loadDeclarations = async () => { - try { - console.log('Chargement des déclarations...'); - const response = await fetch('http://localhost:3001/api/get_declarations'); + // Fonction pour charger les déclarations filtrées par utilisateur + const loadDeclarations = async () => { + try { + console.log('Chargement des déclarations...'); + + // Vérifier si l'utilisateur est connecté + if (!user?.email) { + console.log('Aucun utilisateur connecté'); + setTimeEntries([]); + setLoading(false); + return; + } - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } + // CORRECTION : Utiliser l'email au lieu de l'ID + const response = await fetch(`http://localhost:3001/api/get_declarations_by_email?email=${encodeURIComponent(user.email)}`); - const data = await response.json(); - console.log('Déclarations reçues:', data); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } - // Convertir les données du serveur au format TimeEntry - const entries: TimeEntry[] = data.map((d: any) => ({ - date: d.date.split('T')[0], // Convertir la date SQL en format YYYY-MM-DD - activityType: d.activityType, - hours: d.duree, - description: d.description || '', - createdAt: new Date(d.date) - })); + const data = await response.json(); + console.log('Déclarations reçues pour l\'email', user.email, ':', data); - setTimeEntries(entries); - console.log('Déclarations converties:', entries); - } catch (error) { - console.error('Erreur lors du chargement des déclarations:', error); - } finally { + // Convertir les données du serveur au format TimeEntry + const entries: TimeEntry[] = data.map((d: any) => ({ + date: d.date.split('T')[0], // Convertir la date SQL en format YYYY-MM-DD + activityType: d.activityType, + hours: d.duree, + description: d.description || '', + createdAt: new Date(d.date) + })); + + setTimeEntries(entries); + console.log('Déclarations converties pour l\'utilisateur:', entries); + } catch (error) { + console.error('Erreur lors du chargement des déclarations:', error); + setTimeEntries([]); + } finally { + setLoading(false); + } +}; + + // Recharger quand l'utilisateur change + useEffect(() => { + if (user?.email) { + loadDeclarations(); + } else { + setTimeEntries([]); setLoading(false); } - }; - - // Charger les déclarations au démarrage - useEffect(() => { - loadDeclarations(); - }, []); + }, [user?.email]); const handleDateClick = (date: string) => setSelectedDate(date); const handleCloseModal = () => setSelectedDate(null); @@ -82,6 +98,17 @@ function App() { .slice(0, 3); }; + // Vérification si utilisateur connecté + if (!user) { + return ( +
+
+
Chargement de la session...
+
+
+ ); + } + return (
{/* Header */} @@ -94,7 +121,9 @@ function App() {

GTF

-

Formateurs - Suivi mensuel

+

+ Formateurs - Suivi mensuel • {user.nom} {user.prenom} +

@@ -136,15 +165,11 @@ function App() { {/* Mobile Profile */} {showProfile && (
- {user ? ( - - ) : ( -
Chargement du profil...
- )} +
)} @@ -161,6 +186,7 @@ function App() { )} @@ -169,15 +195,11 @@ function App() {
{/* Desktop Profile */}
- {user ? ( - - ) : ( -
Chargement du profil...
- )} +
{/* Quick Actions */} @@ -201,6 +223,7 @@ function App() {
@@ -249,7 +272,9 @@ function App() { {!loading && timeEntries.length === 0 && (

Aucune déclaration

-

Commencez par déclarer vos premières heures

+

+ Commencez par déclarer vos premières heures pour {user.prenom} +

diff --git a/GTF/project/src/components/Calendar.tsx b/GTF/project/src/components/Calendar.tsx index 623f4930..2ee34089 100644 --- a/GTF/project/src/components/Calendar.tsx +++ b/GTF/project/src/components/Calendar.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { ChevronLeft, ChevronRight, Clock, Plus } from 'lucide-react'; +import { ChevronLeft, ChevronRight, Clock } from 'lucide-react'; import { TimeEntry } from '../types/TimeEntry'; interface Declaration { @@ -14,6 +14,7 @@ interface Declaration { interface CalendarProps { onDateClick: (date: string) => void; getEntryForDate: (date: string) => TimeEntry | undefined; + currentUserId?: number; } const activityTypeColors = { @@ -45,14 +46,14 @@ const Calendar: React.FC = ({ onDateClick, getEntryForDate }) => .catch((err) => console.error("Erreur fetch:", err)); }, []); - // Fonction de sauvegarde d'une déclaration (à utiliser selon besoin) + // Fonction de sauvegarde d'une déclaration const saveDeclaration = async (newDeclaration: Declaration) => { const res = await fetch("http://localhost:3001/api/get_declarations", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(newDeclaration), }); - // Traiter le retour selon besoin + }; const getMonthData = () => { @@ -174,22 +175,20 @@ const Calendar: React.FC = ({ onDateClick, getEntryForDate }) => return ( - - {/* Error Messages */} - {errors.length > 0 && ( -
-
- -
-

Erreurs de validation

-
    - {errors.map((error, index) => ( -
  • {error}
  • - ))} -
-
-
-
- )} - {/* Form */} -
- {/* Type d'activité */} -
- -
- {(['preparation', 'correction'] as const).map((type) => { - const color = getActivityTypeColor(type); - const isSelected = activityType === type; - return ( -