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() {
- • Lundi à vendredi uniquement
- • Description optionnelle
+
@@ -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 (