import express from 'express'; import mysql from 'mysql2/promise'; import cors from 'cors'; import axios from 'axios'; import multer from 'multer'; import path from 'path'; import { fileURLToPath } from 'url'; import cron from 'node-cron'; import crypto from 'crypto'; import { createRequire } from 'module'; const require = createRequire(import.meta.url); import WebhookManager from './webhook-utils.js'; import { WEBHOOKS, EVENTS } from './webhook-config.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const app = express(); const PORT = 3000; const webhookManager = new WebhookManager(WEBHOOKS.SECRET_KEY); const sseClientsCollab = new Set(); process.on('uncaughtException', (err) => { console.error('💥 ERREUR CRITIQUE NON CATCHÉE:', err); console.error('Stack:', err.stack); // On ne crash pas pour pouvoir déboguer }); process.on('unhandledRejection', (reason, promise) => { console.error('💥 PROMESSE REJETÉE NON GÉRÉE:', reason); console.error('Promise:', promise); }); app.use(cors({ origin: ['http://localhost:3013', 'http://localhost:80', 'https://mygta.ensup-adm.net'], credentials: true })); app.use(express.json()); app.use(express.urlencoded({ extended: true })); const dbConfig = { host: '192.168.0.4', user: 'wpuser', password: '-2b/)ru5/Bi8P[7_', database: 'DemandeConge', port:'3306', charset: 'utf8mb4' }; function nowFR() { const d = new Date(); d.setHours(d.getHours() + 2); return d.toISOString().slice(0, 19).replace('T', ' '); } // ======================================== // HELPER POUR DATES SANS CONVERSION UTC // ======================================== function formatDateWithoutUTC(date) { if (!date) return null; const d = new Date(date); const year = d.getFullYear(); const month = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; }; function formatDateToFrenchTime(date) { if (!date) return null; // Créer un objet Date et le convertir en heure française (Europe/Paris) const d = new Date(date); // Formater en ISO avec le fuseau horaire français return d.toLocaleString('fr-FR', { timeZone: 'Europe/Paris', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }); } // Ou plus simple, pour avoir un format ISO compatible avec le frontend function formatDateToFrenchISO(date) { if (!date) return null; const d = new Date(date); // Convertir en heure française const frenchDate = new Date(d.toLocaleString('en-US', { timeZone: 'Europe/Paris' })); return frenchDate.toISOString(); } // ======================================== // FONCTIONS POUR GÉRER LES ARRÊTÉS COMPTABLES // À ajouter après : const pool = mysql.createPool(dbConfig); // ======================================== /** * Récupère le dernier arrêté validé/clôturé */ async function getDernierArrete(conn) { const [arretes] = await conn.query(` SELECT * FROM ArreteComptable WHERE Statut IN ('Validé', 'Clôturé') ORDER BY DateArrete DESC LIMIT 1 `); return arretes.length > 0 ? arretes[0] : null; } /** * Vérifie si une date est avant le dernier arrêté */ async function estAvantArrete(conn, date) { const dernierArrete = await getDernierArrete(conn); if (!dernierArrete) { return false; // Pas d'arrêté = toutes les dates sont autorisées } const dateTest = new Date(date); const dateArrete = new Date(dernierArrete.DateArrete); return dateTest <= dateArrete; } /** * Récupère le solde figé d'un collaborateur pour un type de congé */ async function getSoldeFige(conn, collaborateurId, typeCongeId, annee) { const dernierArrete = await getDernierArrete(conn); if (!dernierArrete) { return null; // Pas d'arrêté } const [soldes] = await conn.query(` SELECT * FROM SoldesFiges WHERE ArreteId = ? AND CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? LIMIT 1 `, [dernierArrete.Id, collaborateurId, typeCongeId, annee]); return soldes.length > 0 ? soldes[0] : null; } /** * Calcule l'acquisition depuis le dernier arrêté */ async function calculerAcquisitionDepuisArrete(conn, collaborateurId, typeConge, dateReference = new Date()) { const dernierArrete = await getDernierArrete(conn); const anneeRef = dateReference.getFullYear(); // Déterminer le type de congé const [typeRow] = await conn.query( 'SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', [typeConge === 'CP' ? 'Congé payé' : 'RTT'] ); if (typeRow.length === 0) { throw new Error(`Type de congé ${typeConge} non trouvé`); } const typeCongeId = typeRow[0].Id; const [collab] = await conn.query('SELECT role FROM CollaborateurAD WHERE id = ?', [collaborateurId]); const isApprenti = collab.length > 0 && collab[0].role === 'Apprenti'; if (typeConge === 'RTT' && isApprenti) { return 0; // ⭐ Les apprentis n'ont pas de RTT } // Si pas d'arrêté, calcul normal depuis le début if (!dernierArrete) { if (typeConge === 'CP') { const [collab] = await conn.query('SELECT DateEntree, CampusId FROM CollaborateurAD WHERE id = ?', [collaborateurId]); const dateEntree = collab.length > 0 ? collab[0].DateEntree : null; return calculerAcquisitionCP(dateReference, dateEntree); } else { const rttData = await calculerAcquisitionRTT(conn, collaborateurId, dateReference); return rttData.acquisition; } } const dateArrete = new Date(dernierArrete.DateArrete); // Si la date de référence est AVANT l'arrêté, utiliser le solde figé if (dateReference <= dateArrete) { const soldeFige = await getSoldeFige(conn, collaborateurId, typeCongeId, anneeRef); return soldeFige ? soldeFige.TotalAcquis : 0; } // Si la date est APRÈS l'arrêté, partir du solde figé + calcul depuis l'arrêté const soldeFige = await getSoldeFige(conn, collaborateurId, typeCongeId, anneeRef); const acquisFigee = soldeFige ? soldeFige.TotalAcquis : 0; // Calculer l'acquisition DEPUIS l'arrêté let acquisDepuisArrete = 0; if (typeConge === 'CP') { const moisDepuisArrete = getMoisTravaillesCP(dateReference, dateArrete); acquisDepuisArrete = moisDepuisArrete * (25 / 12); } else { const [collab] = await conn.query('SELECT TypeContrat, CampusId FROM CollaborateurAD WHERE id = ?', [collaborateurId]); const typeContrat = collab.length > 0 && collab[0].TypeContrat ? collab[0].TypeContrat : '37h'; const config = await getConfigurationRTT(conn, anneeRef, typeContrat); const moisDepuisArrete = getMoisTravaillesRTT(dateReference, dateArrete); acquisDepuisArrete = moisDepuisArrete * config.acquisitionMensuelle; } return acquisFigee + acquisDepuisArrete; } const pool = mysql.createPool(dbConfig); const AZURE_CONFIG = { tenantId: '9840a2a0-6ae1-4688-b03d-d2ec291be0f9', clientId: '4bb4cc24-bac3-427c-b02c-5d14fc67b561', clientSecret: 'gvf8Q~545Bafn8yYsgjW~QG_P1lpzaRe6gJNgb2t', groupId: 'c1ea877c-6bca-4f47-bfad-f223640813a0' }; const storage = multer.diskStorage({ destination: './uploads/', filename: (req, file, cb) => { cb(null, Date.now() + path.extname(file.originalname)); } }); const upload = multer({ storage }); const medicalStorage = multer.diskStorage({ destination: './uploads/medical/', filename: (req, file, cb) => { const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); cb(null, 'medical-' + uniqueSuffix + path.extname(file.originalname)); } }); const ACCES_TRANSVERSAUX = { 'sloisil@ensup.eu': { typeAcces: 'service_multi_campus', serviceNom: 'Pédagogie', description: 'Sandrine - Vue complète Pédagogie (tous campus)' }, 'mbouteiller@ensup.eu': { typeAcces: 'service_multi_campus', serviceNom: 'Admissions', description: 'Morgane - Vue complète Admissions (tous campus)' }, 'vnoel@ensup.eu': { typeAcces: 'service_multi_campus', serviceNom: 'Relations Entreprises', description: 'Viviane - Vue complète Relations Entreprises (tous campus)' }, 'vpierrel@ensup.eu': { typeAcces: 'service_multi_campus', // ✅ CORRIGÉ - même type que les autres serviceNom: 'Administratif & Financier', description: 'Virginie - Vue complète Administratif & Financier (tous campus)' } }; function getUserAccesTransversal(userEmail) { const acces = ACCES_TRANSVERSAUX[userEmail?.toLowerCase()] || null; if (acces) { console.log(`🌐 Accès transversal: ${acces.description}`); } return acces; } const uploadMedical = multer({ storage: medicalStorage, limits: { fileSize: 5 * 1024 * 1024 }, fileFilter: (req, file, cb) => { const allowedTypes = ['application/pdf', 'image/jpeg', 'image/jpg', 'image/png']; if (allowedTypes.includes(file.mimetype)) { cb(null, true); } else { cb(new Error('Type de fichier non autorisé')); } } }); import fs from 'fs'; if (!fs.existsSync('./uploads/medical')) { fs.mkdirSync('./uploads/medical', { recursive: true }); } app.get('/api/events/collaborateur', (req, res) => { const userId = req.query.user_id; if (!userId) { return res.status(401).json({ error: 'user_id requis' }); } console.log('🔔 Nouvelle connexion SSE collaborateur:', userId); // ⭐ HEADERS CRITIQUES POUR SSE res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache, no-transform'); res.setHeader('Connection', 'keep-alive'); res.setHeader('X-Accel-Buffering', 'no'); res.setHeader('Access-Control-Allow-Origin', '*'); // ⭐ FLUSH IMMÉDIATEMENT POUR ÉTABLIR LA CONNEXION res.flushHeaders(); const sendEvent = (data) => { try { if (res.writableEnded) { console.log('⚠️ Tentative d\'envoi sur connexion fermée'); return false; } res.write(`data: ${JSON.stringify(data)}\n\n`); return true; } catch (error) { console.error('❌ Erreur envoi SSE:', error); return false; } }; const client = { userId: parseInt(userId), send: sendEvent, res: res // ⭐ Garder référence pour vérifier l'état }; sseClientsCollab.add(client); console.log(`📊 Clients SSE collaborateurs connectés: ${sseClientsCollab.size}`); // ⭐ ÉVÉNEMENT DE CONNEXION sendEvent({ type: 'connected', message: 'Connexion établie', timestamp: new Date().toISOString() }); // ⭐ HEARTBEAT AVEC VÉRIFICATION const heartbeat = setInterval(() => { const success = sendEvent({ type: 'heartbeat', timestamp: new Date().toISOString() }); if (!success) { console.log('💔 Heartbeat échoué, nettoyage...'); clearInterval(heartbeat); sseClientsCollab.delete(client); } }, 30000); // 30 secondes // ⭐ GESTION PROPRE DE LA DÉCONNEXION const cleanup = () => { console.log('🔌 Déconnexion SSE collaborateur:', userId); clearInterval(heartbeat); sseClientsCollab.delete(client); console.log(`📊 Clients SSE collaborateurs connectés: ${sseClientsCollab.size}`); }; req.on('close', cleanup); req.on('error', (err) => { console.error('❌ Erreur SSE connexion:', err.message); cleanup(); }); // ⭐ TIMEOUT DE SÉCURITÉ (optionnel, mais recommandé) req.socket.setTimeout(0); // Désactiver timeout pour SSE }); const notifyCollabClients = (event, targetUserId = null) => { console.log( `📢 Notification SSE Collab: ${event.type}`, targetUserId ? `pour user ${targetUserId}` : 'pour tous' ); const deadClients = []; sseClientsCollab.forEach(client => { // ⭐ FILTRER PAR USER SI NÉCESSAIRE if (targetUserId && client.userId !== targetUserId) { return; } // ⭐ VÉRIFIER SI LA CONNEXION EST TOUJOURS ACTIVE if (client.res && client.res.writableEnded) { console.log(`💀 Client mort détecté: ${client.userId}`); deadClients.push(client); return; } // ⭐ ENVOYER L'ÉVÉNEMENT const success = client.send(event); if (!success) { deadClients.push(client); } }); // ⭐ NETTOYER LES CLIENTS MORTS deadClients.forEach(client => { console.log(`🧹 Nettoyage client mort: ${client.userId}`); sseClientsCollab.delete(client); }); if (deadClients.length > 0) { console.log(`📊 Clients SSE après nettoyage: ${sseClientsCollab.size}`); } }; const sseClients = new Set(); // 🔌 ROUTE SSE POUR LE CALENDRIER app.get('/sse', (req, res) => { const userId = req.query.user_id; if (!userId) { return res.status(400).json({ error: 'user_id requis' }); } console.log('🔌 Nouvelle connexion SSE:', userId); res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); res.setHeader('X-Accel-Buffering', 'no'); const sendEvent = (data) => { try { res.write(`data: ${JSON.stringify(data)}\n\n`); } catch (error) { console.error('❌ Erreur envoi SSE:', error); } }; const client = { id: userId, send: sendEvent }; sseClients.add(client); console.log(`📊 Clients SSE connectés: ${sseClients.size}`); // Envoyer un heartbeat initial sendEvent({ type: 'ping', message: 'Connexion établie', timestamp: new Date().toISOString() }); // Heartbeat toutes les 30 secondes const heartbeat = setInterval(() => { try { sendEvent({ type: 'ping', timestamp: new Date().toISOString() }); } catch (error) { console.error('❌ Erreur heartbeat:', error); clearInterval(heartbeat); } }, 30000); req.on('close', () => { console.log('🔌 Déconnexion SSE:', userId); clearInterval(heartbeat); sseClients.delete(client); console.log(`📊 Clients SSE connectés: ${sseClients.size}`); }); }); // 📢 FONCTION POUR NOTIFIER LES CLIENTS const notifyClients = (event, userId = null) => { console.log(`📢 Notification SSE: ${event.type}${userId ? ` pour ${userId}` : ''}`); sseClients.forEach(client => { // Si userId est spécifié, envoyer seulement à ce client if (userId && client.id !== userId) { return; } try { client.send(event); } catch (error) { console.error('❌ Erreur envoi event:', error); } }); }; app.post('/api/webhook/receive', async (req, res) => { try { const signature = req.headers['x-webhook-signature']; const payload = req.body; console.log('\n📥 === WEBHOOK REÇU (COLLABORATEURS) ==='); console.log('Event:', payload.event); console.log('Data:', JSON.stringify(payload.data, null, 2)); // Vérifier la signature if (!webhookManager.verifySignature(payload, signature)) { console.error('❌ Signature webhook invalide'); return res.status(401).json({ error: 'Signature invalide' }); } const { event, data } = payload; // Traiter selon le type d'événement switch (event) { case EVENTS.COMPTEUR_UPDATED: console.log('\n💰 === WEBHOOK COMPTEUR_UPDATED REÇU ==='); console.log(` Collaborateur: ${data.collaborateurId}`); console.log(` Type mise à jour: ${data.typeUpdate}`); console.log(` Type congé: ${data.typeConge}`); console.log(` Année: ${data.annee}`); // ⭐ SI MODIFICATION RH, METTRE À JOUR LA BASE LOCALE if (data.source === 'rh' && data.nouveauTotal !== undefined && data.nouveauSolde !== undefined) { console.log('🔄 Synchronisation depuis RH...'); console.log(` Nouveau Total: ${data.nouveauTotal}j`); console.log(` Nouveau Solde: ${data.nouveauSolde}j`); const conn = await pool.getConnection(); try { // Identifier le type de congé const typeName = data.typeConge === 'Congé payé' ? 'Congé payé' : data.typeConge === 'RTT' ? 'RTT' : data.typeConge; const [typeRow] = await conn.query( 'SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', [typeName] ); if (typeRow.length > 0) { const typeCongeId = typeRow[0].Id; // Vérifier si le compteur existe const [existing] = await conn.query(` SELECT Id FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? `, [data.collaborateurId, typeCongeId, data.annee]); if (existing.length > 0) { // Mettre à jour await conn.query(` UPDATE CompteurConges SET Total = ?, Solde = ?, DerniereMiseAJour = NOW() WHERE Id = ? `, [data.nouveauTotal, data.nouveauSolde, existing[0].Id]); console.log(' ✅ Compteur local mis à jour'); } else { // Créer await conn.query(` INSERT INTO CompteurConges (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour) VALUES (?, ?, ?, ?, ?, 0, NOW()) `, [data.collaborateurId, typeCongeId, data.annee, data.nouveauTotal, data.nouveauSolde]); console.log(' ✅ Compteur local créé'); } } conn.release(); } catch (dbError) { console.error('❌ Erreur mise à jour locale:', dbError.message); if (conn) conn.release(); } } // ⭐ NOTIFIER LE CLIENT SSE DU COLLABORATEUR notifyCollabClients({ type: 'compteur-updated', collaborateurId: data.collaborateurId, typeConge: data.typeConge, annee: data.annee, typeUpdate: data.typeUpdate, nouveauTotal: data.nouveauTotal, nouveauSolde: data.nouveauSolde, source: data.source || 'rh', timestamp: new Date().toISOString() }, data.collaborateurId); console.log(` 📢 Notification SSE envoyée au collaborateur ${data.collaborateurId}`); break; case EVENTS.DEMANDE_VALIDATED: console.log('\n✅ === WEBHOOK DEMANDE_VALIDATED REÇU ==='); console.log(` Demande: ${data.demandeId}`); console.log(` Statut: ${data.statut}`); console.log(` Type: ${data.typeConge}`); console.log(` Couleur: ${data.couleurHex}`); // Notifier les clients SSE avec TOUTES les infos notifyClients({ type: 'demande-validated', demandeId: data.demandeId, statut: data.statut, typeConge: data.typeConge, couleurHex: data.couleurHex || '#d946ef', date: data.date, periode: data.periode, collaborateurId: data.collaborateurId, timestamp: new Date().toISOString() }, data.collaborateurId); // Notifier les RH aussi notifyClients({ type: 'demande-list-updated', action: 'validation-collab', demandeId: data.demandeId, statut: data.statut, typeConge: data.typeConge, couleurHex: data.couleurHex || '#d946ef', timestamp: new Date().toISOString() }); console.log(' 📢 Notifications SSE envoyées'); break; case EVENTS.DEMANDE_UPDATED: console.log('\n✏️ === WEBHOOK DEMANDE_UPDATED REÇU ==='); console.log(` Demande: ${data.demandeId}`); console.log(` Collaborateur: ${data.collaborateurId}`); notifyCollabClients({ type: 'demande-updated-rh', demandeId: data.demandeId, timestamp: new Date().toISOString() }, data.collaborateurId); console.log(' 📢 Notification modification envoyée'); break; case EVENTS.DEMANDE_DELETED: console.log('\n🗑️ === WEBHOOK DEMANDE_DELETED REÇU ==='); console.log(` Demande: ${data.demandeId}`); console.log(` Collaborateur: ${data.collaborateurId}`); notifyCollabClients({ type: 'demande-deleted-rh', demandeId: data.demandeId, timestamp: new Date().toISOString() }, data.collaborateurId); console.log(' 📢 Notification suppression envoyée'); break; default: console.warn(`⚠️ Type d'événement webhook inconnu: ${event}`); } res.json({ success: true, message: 'Webhook traité' }); } catch (error) { console.error('❌ Erreur traitement webhook:', error); res.status(500).json({ error: error.message }); } }); app.post('/api/syncCompteursFromRH', async (req, res) => { try { const { user_id } = req.body; if (!user_id) { return res.json({ success: false, message: 'user_id manquant' }); } console.log('\n🔄 === SYNCHRONISATION MANUELLE DEPUIS RH ==='); console.log('User ID:', user_id); // Récupérer les compteurs depuis le serveur RH const rhUrl = process.env.RH_SERVER_URL || 'http://localhost:3001'; try { const response = await fetch(`${rhUrl}/api/compteurs?user_id=${user_id}`, { method: 'GET', headers: { 'Content-Type': 'application/json' } }); if (!response.ok) { throw new Error(`Erreur serveur RH: ${response.status}`); } const rhCompteurs = await response.json(); console.log('📊 Compteurs RH récupérés:', rhCompteurs.length); // Mettre à jour la base locale const conn = await pool.getConnection(); await conn.beginTransaction(); let updated = 0; let created = 0; for (const compteur of rhCompteurs) { // Identifier le type de congé const [typeRow] = await conn.query( 'SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', [compteur.typeConge] ); if (typeRow.length === 0) { console.warn(`⚠️ Type ${compteur.typeConge} non trouvé`); continue; } const typeCongeId = typeRow[0].Id; // Vérifier si existe const [existing] = await conn.query(` SELECT Id FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? `, [compteur.collaborateurId, typeCongeId, compteur.annee]); if (existing.length > 0) { // Mettre à jour await conn.query(` UPDATE CompteurConges SET Total = ?, Solde = ?, SoldeReporte = ?, DerniereMiseAJour = NOW() WHERE Id = ? `, [ compteur.total, compteur.solde, compteur.soldeReporte || 0, existing[0].Id ]); updated++; } else { // Créer await conn.query(` INSERT INTO CompteurConges (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour) VALUES (?, ?, ?, ?, ?, ?, NOW()) `, [ compteur.collaborateurId, typeCongeId, compteur.annee, compteur.total, compteur.solde, compteur.soldeReporte || 0 ]); created++; } } await conn.commit(); conn.release(); console.log(`✅ Synchronisation terminée: ${updated} mis à jour, ${created} créés`); res.json({ success: true, message: 'Synchronisation réussie', stats: { total: rhCompteurs.length, updated: updated, created: created } }); } catch (fetchError) { console.error('❌ Erreur communication avec serveur RH:', fetchError.message); res.status(500).json({ success: false, message: 'Impossible de contacter le serveur RH', error: fetchError.message }); } } catch (error) { console.error('❌ Erreur synchronisation:', error); res.status(500).json({ success: false, message: 'Erreur serveur', error: error.message }); } }); function getDateFinMoisPrecedent(referenceDate = new Date()) { const now = new Date(referenceDate); now.setHours(0, 0, 0, 0); return new Date(now.getFullYear(), now.getMonth(), 0); } function parseDateYYYYMMDD(s) { if (!s) return null; if (/^\d{4}-\d{2}-\d{2}$/.test(s)) { const [y, m, d] = s.split('-').map(Number); return new Date(y, m - 1, d); } return new Date(s); } const LEAVE_RULES = { CP: { nom: 'Congé payé', joursAnnuels: 25, periodeDebut: { mois: 6, jour: 1 }, periodeFin: { mois: 5, jour: 31 }, acquisitionMensuelle: 25 / 12, reportable: true, periodeReport: 'exercice' }, RTT: { nom: 'RTT', joursAnnuels: 10, periodeDebut: { mois: 1, jour: 1 }, periodeFin: { mois: 12, jour: 31 }, acquisitionMensuelle: 10 / 12, reportable: false, periodeReport: null } }; // ======================================== // NOUVELLES FONCTIONS POUR RTT VARIABLES // ======================================== /** * Récupère la configuration RTT pour une année et un type de contrat donnés * RÈGLES : * - 37h : toujours 10 RTT/an (0.8333/mois) * - Forfait jour 2025 : 10 RTT/an (0.8333/mois) * - Forfait jour 2026+ : 12 RTT/an (1.0/mois) */ async function getConfigurationRTT(conn, annee, typeContrat = '37h') { try { // D'abord chercher en base de données const [config] = await conn.query( `SELECT JoursAnnuels, AcquisitionMensuelle FROM ConfigurationRTT WHERE Annee = ? AND TypeContrat = ? LIMIT 1`, [annee, typeContrat] ); if (config.length > 0) { return { joursAnnuels: parseFloat(config[0].JoursAnnuels), acquisitionMensuelle: parseFloat(config[0].AcquisitionMensuelle) }; } // Si pas en base, utiliser les règles par défaut console.warn(`⚠️ Pas de config RTT en base pour ${annee}/${typeContrat}, utilisation des règles par défaut`); return getConfigurationRTTDefaut(annee, typeContrat); } catch (error) { console.error('Erreur getConfigurationRTT:', error); return getConfigurationRTTDefaut(annee, typeContrat); } } function getConfigurationRTTDefaut(annee, typeContrat) { // 37h : toujours 10 RTT/an if (typeContrat === '37h' || typeContrat === 'temps_partiel') { return { joursAnnuels: 10, acquisitionMensuelle: 10 / 12 // 0.8333 }; } // Forfait jour : dépend de l'année if (typeContrat === 'forfait_jour') { if (annee <= 2025) { // 2025 et avant : 10 RTT/an return { joursAnnuels: 10, acquisitionMensuelle: 10 / 12 // 0.8333 }; } else { // 2026 et après : 12 RTT/an return { joursAnnuels: 12, acquisitionMensuelle: 12 / 12 // 1.0 }; } } // Par défaut : 10 RTT/an return { joursAnnuels: 10, acquisitionMensuelle: 10 / 12 }; } /** * Calcule l'acquisition RTT avec la formule Excel exacte */ async function calculerAcquisitionRTT(conn, collaborateurId, dateReference = new Date()) { const d = new Date(dateReference); d.setHours(0, 0, 0, 0); const annee = d.getFullYear(); // 1️⃣ Récupérer les infos du collaborateur const [collabInfo] = await conn.query( `SELECT TypeContrat, DateEntree, role FROM CollaborateurAD WHERE id = ?`, [collaborateurId] ); if (collabInfo.length === 0) { throw new Error(`Collaborateur ${collaborateurId} non trouvé`); } const typeContrat = collabInfo[0].TypeContrat || '37h'; const dateEntree = collabInfo[0].DateEntree; const isApprenti = collabInfo[0].role === 'Apprenti'; // 2️⃣ Apprentis = pas de RTT if (isApprenti) { return { acquisition: 0, moisTravailles: 0, config: { joursAnnuels: 0, acquisitionMensuelle: 0 }, typeContrat: typeContrat }; } // 3️⃣ Récupérer la configuration RTT (avec règles 2025/2026) const config = await getConfigurationRTT(conn, annee, typeContrat); console.log(`📊 Config RTT ${annee}/${typeContrat}: ${config.joursAnnuels}j/an (${config.acquisitionMensuelle.toFixed(4)}/mois)`); // 4️⃣ Début d'acquisition = 01/01/N ou date d'entrée si postérieure let dateDebutAcquis = new Date(annee, 0, 1); // 01/01/N dateDebutAcquis.setHours(0, 0, 0, 0); if (dateEntree) { const entree = new Date(dateEntree); entree.setHours(0, 0, 0, 0); if (entree.getFullYear() === annee && entree > dateDebutAcquis) { dateDebutAcquis = entree; } if (entree.getFullYear() > annee) { return { acquisition: 0, moisTravailles: 0, config: config, typeContrat: typeContrat }; } } // 5️⃣ Calculer avec la formule Excel const acquisition = calculerAcquisitionFormuleExcel(dateDebutAcquis, d, config.acquisitionMensuelle); // 6️⃣ Calculer les mois travaillés (pour info) const moisTravailles = config.acquisitionMensuelle > 0 ? acquisition / config.acquisitionMensuelle : 0; // 7️⃣ Plafonner au maximum annuel const acquisitionFinale = Math.min(acquisition, config.joursAnnuels); return { acquisition: Math.round(acquisitionFinale * 100) / 100, moisTravailles: Math.round(moisTravailles * 100) / 100, config: config, typeContrat: typeContrat }; } /** * Calcule l'acquisition avec la formule Excel exacte : * E1 * ((JOUR(FIN.MOIS(B1;0)) - JOUR(B1) + 1) / JOUR(FIN.MOIS(B1;0)) * + DATEDIF(B1;B2;"m") - 1 * + JOUR(B2) / JOUR(FIN.MOIS(B2;0))) */ function calculerAcquisitionFormuleExcel(dateDebut, dateReference, coeffMensuel) { const b1 = new Date(dateDebut); const b2 = new Date(dateReference); b1.setHours(0, 0, 0, 0); b2.setHours(0, 0, 0, 0); // Si date référence avant date début if (b2 < b1) { return 0; } // Si même mois et même année if (b1.getFullYear() === b2.getFullYear() && b1.getMonth() === b2.getMonth()) { const joursTotal = new Date(b2.getFullYear(), b2.getMonth() + 1, 0).getDate(); const joursAcquis = b2.getDate() - b1.getDate() + 1; return Math.round((joursAcquis / joursTotal) * coeffMensuel * 100) / 100; } // 1️⃣ Fraction du PREMIER mois const joursFinMoisB1 = new Date(b1.getFullYear(), b1.getMonth() + 1, 0).getDate(); const jourB1 = b1.getDate(); const fractionPremierMois = (joursFinMoisB1 - jourB1 + 1) / joursFinMoisB1; // 2️⃣ Mois COMPLETS entre const moisComplets = dateDifMonths(b1, b2) - 1; // 3️⃣ Fraction du DERNIER mois const joursFinMoisB2 = new Date(b2.getFullYear(), b2.getMonth() + 1, 0).getDate(); const jourB2 = b2.getDate(); const fractionDernierMois = jourB2 / joursFinMoisB2; // 4️⃣ Total const totalMois = fractionPremierMois + Math.max(0, moisComplets) + fractionDernierMois; const acquisition = totalMois * coeffMensuel; return Math.round(acquisition * 100) / 100; } /** * Équivalent de DATEDIF(date1, date2, "m") en JavaScript */ function dateDifMonths(date1, date2) { const d1 = new Date(date1); const d2 = new Date(date2); let months = (d2.getFullYear() - d1.getFullYear()) * 12; months += d2.getMonth() - d1.getMonth(); // Si le jour de d2 < jour de d1, on n'a pas encore complété le mois if (d2.getDate() < d1.getDate()) { months--; } return Math.max(0, months); } /** * Calcule l'acquisition CP avec la formule Excel exacte */ function calculerAcquisitionCP(dateReference = new Date(), dateEntree = null) { const d = new Date(dateReference); d.setHours(0, 0, 0, 0); const annee = d.getFullYear(); const mois = d.getMonth() + 1; // 1️⃣ Déterminer le début de l'exercice CP (01/06) let exerciceDebut; if (mois >= 6) { exerciceDebut = new Date(annee, 5, 1); // 01/06/N } else { exerciceDebut = new Date(annee - 1, 5, 1); // 01/06/N-1 } exerciceDebut.setHours(0, 0, 0, 0); // 2️⃣ Ajuster si date d'entrée postérieure let dateDebutAcquis = new Date(exerciceDebut); if (dateEntree) { const entree = new Date(dateEntree); entree.setHours(0, 0, 0, 0); if (entree > exerciceDebut) { dateDebutAcquis = entree; } } // 3️⃣ Calculer avec la formule Excel const coeffCP = 25 / 12; // 2.0833 const acquisition = calculerAcquisitionFormuleExcel(dateDebutAcquis, d, coeffCP); // 4️⃣ Plafonner à 25 jours return Math.min(acquisition, 25); } // ======================================== // TÂCHES CRON // ======================================== cron.schedule('0 2 * * *', async () => { console.log('🔄 [CRON] Mise à jour quotidienne des compteurs...'); try { const conn = await pool.getConnection(); await conn.beginTransaction(); const [collaborateurs] = await conn.query(` SELECT id, prenom, nom, CampusId FROM CollaborateurAD WHERE (actif = 1 OR actif IS NULL) `); let successCount = 0; const today = new Date(); for (const collab of collaborateurs) { try { await updateMonthlyCounters(conn, collab.id, today); successCount++; } catch (error) { console.error(`❌ Erreur pour ${collab.prenom} ${collab.nom}:`, error.message); } } await conn.commit(); console.log(`✅ [CRON] ${successCount}/${collaborateurs.length} compteurs mis à jour`); conn.release(); } catch (error) { console.error('❌ [CRON] Erreur mise à jour quotidienne:', error); } }); cron.schedule('59 23 31 12 *', async () => { console.log('🎆 [CRON] Traitement fin d\'année RTT...'); try { const conn = await pool.getConnection(); await conn.beginTransaction(); const [collaborateurs] = await conn.query('SELECT id, CampusId FROM CollaborateurAD'); let successCount = 0; for (const collab of collaborateurs) { try { await processEndOfYearRTT(conn, collab.id); successCount++; } catch (error) { console.error(`❌ Erreur RTT pour ${collab.id}:`, error.message); } } await conn.commit(); console.log(`✅ [CRON] ${successCount}/${collaborateurs.length} RTT réinitialisés`); conn.release(); } catch (error) { console.error('❌ [CRON] Erreur traitement fin d\'année:', error); } }); cron.schedule('59 23 31 5 *', async () => { console.log('📅 [CRON] Traitement fin d\'exercice CP...'); try { const conn = await pool.getConnection(); await conn.beginTransaction(); const [collaborateurs] = await conn.query('SELECT id, CampusId FROM CollaborateurAD'); let successCount = 0; for (const collab of collaborateurs) { try { await processEndOfExerciceCP(conn, collab.id); successCount++; } catch (error) { console.error(`❌ Erreur CP pour ${collab.id}:`, error.message); } } await conn.commit(); console.log(`✅ [CRON] ${successCount}/${collaborateurs.length} CP reportés`); conn.release(); } catch (error) { console.error('❌ [CRON] Erreur traitement fin d\'exercice:', error); } }); // ======================================== // CRON : CRÉER ARRÊTÉS MENSUELS AUTOMATIQUEMENT // ======================================== cron.schedule('55 23 28-31 * *', async () => { const today = new Date(); const lastDay = new Date(today.getFullYear(), today.getMonth() + 1, 0); // ⭐ Vérifier qu'on est bien le dernier jour du mois if (today.getDate() === lastDay.getDate()) { console.log(`📅 [CRON] Création arrêté fin de mois: ${today.toISOString().split('T')[0]}`); const conn = await pool.getConnection(); try { await conn.beginTransaction(); const annee = today.getFullYear(); const mois = today.getMonth() + 1; // 1-12 const dateArrete = today.toISOString().split('T')[0]; // ⭐ Vérifier si l'arrêté n'existe pas déjà const [existing] = await conn.query( 'SELECT Id FROM ArreteComptable WHERE Annee = ? AND Mois = ?', [annee, mois] ); if (existing.length > 0) { console.log(`⚠️ [CRON] Arrêté ${mois}/${annee} existe déjà, skip`); await conn.rollback(); conn.release(); return; } // ⭐ Créer l'arrêté const [result] = await conn.query(` INSERT INTO ArreteComptable (DateArrete, Annee, Mois, Libelle, Description, Statut, DateCreation) VALUES (?, ?, ?, ?, ?, 'En cours', NOW()) `, [ dateArrete, annee, mois, `Arrêté comptable ${getMonthName(mois)} ${annee}`, `Arrêté mensuel automatique - Clôture des soldes au ${dateArrete}` ]); const arreteId = result.insertId; console.log(`✅ [CRON] Arrêté créé: ID ${arreteId}`); // ⭐ Créer le snapshot await conn.query('CALL sp_creer_snapshot_arrete(?)', [arreteId]); console.log(`📸 [CRON] Snapshot créé pour l'arrêté ${arreteId}`); // ⭐ Compter les soldes figés const [count] = await conn.query( 'SELECT COUNT(*) as total FROM SoldesFiges WHERE ArreteId = ?', [arreteId] ); await conn.commit(); console.log(`🎉 [CRON] Arrêté ${mois}/${annee} terminé: ${count[0].total} soldes figés`); } catch (error) { await conn.rollback(); console.error(`❌ [CRON] Erreur création arrêté:`, error.message); } finally { conn.release(); } } }); // Mail mensuel le 1er à 9h cron.schedule('0 9 1 * *', async () => { console.log('📧 Envoi mails compte-rendu mensuel...'); const conn = await pool.getConnection(); const [cadres] = await conn.query(` SELECT id, email, prenom, nom FROM CollaborateurAD WHERE TypeContrat = 'forfait_jour' AND (actif = 1 OR actif IS NULL) `); const moisPrecedent = new Date(); moisPrecedent.setMonth(moisPrecedent.getMonth() - 1); for (const cadre of cadres) { // Envoyer mail via Microsoft Graph API // Enregistrer dans MailsCompteRendu } conn.release(); }); // Relance hebdomadaire le lundi à 9h cron.schedule('0 9 * * 1', async () => { console.log('🔔 Relance hebdomadaire compte-rendu...'); const conn = await pool.getConnection(); const moisCourant = new Date().getMonth() + 1; const anneeCourante = new Date().getFullYear(); const [nonValides] = await conn.query(` SELECT DISTINCT ca.id, ca.email, ca.prenom, ca.nom FROM CollaborateurAD ca LEFT JOIN CompteRenduMensuel crm ON ca.id = crm.CollaborateurADId AND crm.Annee = ? AND crm.Mois = ? WHERE ca.TypeContrat = 'forfait_jour' AND (crm.Statut IS NULL OR crm.Statut != 'Validé') `, [anneeCourante, moisCourant - 1]); for (const cadre of nonValides) { // Envoyer relance } conn.release(); }); // ⭐ Fonction helper pour les noms de mois function getMonthName(mois) { const mois_names = ['', 'Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre']; return mois_names[mois] || mois; } async function getGraphToken() { try { const params = new URLSearchParams({ grant_type: 'client_credentials', client_id: AZURE_CONFIG.clientId, client_secret: AZURE_CONFIG.clientSecret, scope: 'https://graph.microsoft.com/.default' }); const response = await axios.post( `https://login.microsoftonline.com/${AZURE_CONFIG.tenantId}/oauth2/v2.0/token`, params.toString(), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } } ); return response.data.access_token; } catch (error) { console.error('Erreur obtention token:', error); return null; } } async function sendMailGraph(accessToken, fromEmail, toEmail, subject, bodyHtml) { try { await axios.post( `https://graph.microsoft.com/v1.0/users/${fromEmail}/sendMail`, { message: { subject, body: { contentType: 'HTML', content: bodyHtml }, toRecipients: [{ emailAddress: { address: toEmail } }] }, saveToSentItems: false }, { headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' } } ); return true; } catch (error) { console.error('Erreur envoi email:', error); return false; } } function getWorkingDays(startDate, endDate) { let workingDays = 0; const current = new Date(startDate); const end = new Date(endDate); while (current <= end) { const dayOfWeek = current.getDay(); if (dayOfWeek !== 0 && dayOfWeek !== 6) { workingDays++; } current.setDate(current.getDate() + 1); } return workingDays; } function formatDate(date) { const d = new Date(date); const day = String(d.getDate()).padStart(2, '0'); const month = String(d.getMonth() + 1).padStart(2, '0'); const year = d.getFullYear(); return `${day}/${month}/${year}`; } function getExerciceCP(date = new Date()) { const d = new Date(date); const annee = d.getFullYear(); const mois = d.getMonth() + 1; if (mois >= 1 && mois <= 5) { return `${annee - 1}-${annee}`; } return `${annee}-${annee + 1}`; } function getMoisTravaillesCP(date = new Date(), dateEntree = null) { const d = new Date(date); d.setHours(0, 0, 0, 0); const annee = d.getFullYear(); const mois = d.getMonth() + 1; let debutExercice; if (mois >= 6) { debutExercice = new Date(annee, 5, 1); } else { debutExercice = new Date(annee - 1, 5, 1); } debutExercice.setHours(0, 0, 0, 0); if (dateEntree) { const entree = new Date(dateEntree); entree.setHours(0, 0, 0, 0); if (entree > debutExercice) { debutExercice = entree; } } // ✅ Calculer jusqu'à aujourd'hui const diffMs = d - debutExercice; const diffJours = Math.floor(diffMs / (1000 * 60 * 60 * 24)) + 1; const moisTravailles = diffJours / 30.44; return Math.max(0, Math.min(12, moisTravailles)); } function getMoisTravaillesRTT(date = new Date(), dateEntree = null) { const d = new Date(date); d.setHours(0, 0, 0, 0); const annee = d.getFullYear(); let debutAnnee = new Date(annee, 0, 1); debutAnnee.setHours(0, 0, 0, 0); if (dateEntree) { const entree = new Date(dateEntree); entree.setHours(0, 0, 0, 0); if (entree.getFullYear() === annee && entree > debutAnnee) { debutAnnee = entree; } else if (entree.getFullYear() > annee) { return 0; } } const diffMs = d - debutAnnee; const diffJours = Math.floor(diffMs / (1000 * 60 * 60 * 24)) + 1; const moisTravailles = diffJours / 30.44; return Math.max(0, Math.min(12, moisTravailles)); } function calculerAcquisitionCumulee(typeConge, dateReference = new Date(), dateEntree = null) { const rules = LEAVE_RULES[typeConge]; if (!rules) return 0; let moisTravailles; if (typeConge === 'CP') { moisTravailles = getMoisTravaillesCP(dateReference, dateEntree); } else { moisTravailles = getMoisTravaillesRTT(dateReference, dateEntree); } const acquisition = moisTravailles * rules.acquisitionMensuelle; return Math.round(acquisition * 100) / 100; } async function processEndOfYearRTT(conn, collaborateurId) { const currentYear = new Date().getFullYear(); const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']); if (rttType.length === 0) return null; await conn.query( `UPDATE CompteurConges SET Solde = 0, Total = 0, SoldeReporte = 0, DerniereMiseAJour = NOW() WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, [collaborateurId, rttType[0].Id, currentYear] ); return { type: 'RTT', action: 'reset_end_of_year', annee: currentYear }; } async function processEndOfExerciceCP(conn, collaborateurId) { const today = new Date(); const currentYear = today.getFullYear(); const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']); if (cpType.length === 0) return null; const cpTypeId = cpType[0].Id; const [currentCounter] = await conn.query( `SELECT Id, Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, [collaborateurId, cpTypeId, currentYear] ); if (currentCounter.length === 0) return null; const soldeAReporter = parseFloat(currentCounter[0].Solde); const [nextYearCounter] = await conn.query( `SELECT Id FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, [collaborateurId, cpTypeId, currentYear + 1] ); if (nextYearCounter.length > 0) { await conn.query( `UPDATE CompteurConges SET SoldeReporte = ?, Solde = Solde + ?, DerniereMiseAJour = NOW() WHERE Id = ?`, [soldeAReporter, soldeAReporter, nextYearCounter[0].Id] ); } else { await conn.query( `INSERT INTO CompteurConges (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour) VALUES (?, ?, ?, 0, ?, ?, NOW())`, [collaborateurId, cpTypeId, currentYear + 1, soldeAReporter, soldeAReporter] ); } return { type: 'CP', action: 'report_exercice', soldeReporte: soldeAReporter }; } async function deductLeaveBalance(conn, collaborateurId, typeCongeId, nombreJours) { const currentYear = new Date().getFullYear(); const previousYear = currentYear - 1; let joursRestants = nombreJours; const deductions = []; const [compteurN1] = await conn.query( `SELECT Id, Solde, SoldeReporte FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, [collaborateurId, typeCongeId, previousYear] ); if (compteurN1.length > 0 && compteurN1[0].SoldeReporte > 0) { const soldeN1 = parseFloat(compteurN1[0].SoldeReporte); const aDeduireN1 = Math.min(soldeN1, joursRestants); if (aDeduireN1 > 0) { await conn.query( `UPDATE CompteurConges SET SoldeReporte = GREATEST(0, SoldeReporte - ?), Solde = GREATEST(0, Solde - ?) WHERE Id = ?`, [aDeduireN1, aDeduireN1, compteurN1[0].Id] ); deductions.push({ annee: previousYear, type: 'Reporté N-1', joursUtilises: aDeduireN1, soldeAvant: soldeN1 }); joursRestants -= aDeduireN1; } } if (joursRestants > 0) { const [compteurN] = await conn.query( `SELECT Id, Solde, SoldeReporte FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, [collaborateurId, typeCongeId, currentYear] ); if (compteurN.length > 0) { const soldeN = parseFloat(compteurN[0].Solde) - parseFloat(compteurN[0].SoldeReporte || 0); const aDeduireN = Math.min(soldeN, joursRestants); if (aDeduireN > 0) { await conn.query( `UPDATE CompteurConges SET Solde = GREATEST(0, Solde - ?) WHERE Id = ?`, [aDeduireN, compteurN[0].Id] ); deductions.push({ annee: currentYear, type: 'Année actuelle N', joursUtilises: aDeduireN, soldeAvant: soldeN }); joursRestants -= aDeduireN; } } } return { success: joursRestants === 0, joursDeduitsTotal: nombreJours - joursRestants, joursNonDeduits: joursRestants, details: deductions }; } async function checkLeaveBalance(conn, collaborateurId, repartition) { const currentYear = new Date().getFullYear(); const previousYear = currentYear - 1; const verification = []; for (const rep of repartition) { const typeCode = rep.TypeConge; const joursNecessaires = parseFloat(rep.NombreJours); if (typeCode === 'ABS' || typeCode === 'Formation') continue; const typeName = typeCode === 'CP' ? 'Congé payé' : typeCode === 'RTT' ? 'RTT' : typeCode; const [typeRow] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', [typeName]); if (typeRow.length === 0) continue; const typeCongeId = typeRow[0].Id; const [compteurN1] = await conn.query( `SELECT SoldeReporte FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, [collaborateurId, typeCongeId, previousYear] ); const [compteurN] = await conn.query( `SELECT Solde, SoldeReporte FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, [collaborateurId, typeCongeId, currentYear] ); const soldeN1 = compteurN1.length > 0 ? parseFloat(compteurN1[0].SoldeReporte || 0) : 0; const soldeN = compteurN.length > 0 ? parseFloat(compteurN[0].Solde || 0) - parseFloat(compteurN[0].SoldeReporte || 0) : 0; const soldeTotal = soldeN1 + soldeN; verification.push({ type: typeName, joursNecessaires, soldeN1, soldeN, soldeTotal, suffisant: soldeTotal >= joursNecessaires, deficit: Math.max(0, joursNecessaires - soldeTotal) }); } const insuffisants = verification.filter(v => !v.suffisant); return { valide: insuffisants.length === 0, details: verification, insuffisants }; } // ======================================== // MISE À JOUR DE updateMonthlyCounters // ======================================== async function updateMonthlyCounters(conn, collaborateurId, dateReference = null) { const today = dateReference ? new Date(dateReference) : getDateFinMoisPrecedent(); const currentYear = today.getFullYear(); const updates = []; // Récupérer les infos du collaborateur const [collabInfo] = await conn.query(` SELECT DateEntree, TypeContrat, CampusId, role FROM CollaborateurAD WHERE id = ? `, [collaborateurId]); if (collabInfo.length === 0) { throw new Error(`Collaborateur ${collaborateurId} non trouvé`); } const dateEntree = collabInfo[0].DateEntree || null; const typeContrat = collabInfo[0].TypeContrat || '37h'; const isApprenti = collabInfo[0].role === 'Apprenti'; console.log(`\n📊 === Mise à jour compteurs pour collaborateur ${collaborateurId} ===`); console.log(` Date référence: ${today.toLocaleDateString('fr-FR')}`); // ====================================== // CP (Congés Payés) // ====================================== const exerciceCP = getExerciceCP(today); const acquisitionCP = calculerAcquisitionCP(today, dateEntree); const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']); if (cpType.length > 0) { const cpTypeId = cpType[0].Id; // 1️⃣ Récupérer le compteur existant const [existingCP] = await conn.query(` SELECT Id, Total, Solde, SoldeReporte FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? `, [collaborateurId, cpTypeId, currentYear]); if (existingCP.length > 0) { const ancienTotal = parseFloat(existingCP[0].Total || 0); const ancienSolde = parseFloat(existingCP[0].Solde || 0); const soldeReporte = parseFloat(existingCP[0].SoldeReporte || 0); console.log(` CP - Ancien acquis: ${ancienTotal.toFixed(2)}j`); console.log(` CP - Nouvel acquis: ${acquisitionCP.toFixed(2)}j`); // 2️⃣ Calculer l'incrément d'acquisition (nouveaux jours acquis ce mois) const incrementAcquis = acquisitionCP - ancienTotal; if (incrementAcquis > 0) { console.log(` CP - Nouveaux jours ce mois: +${incrementAcquis.toFixed(2)}j`); // 3️⃣ Vérifier si le collaborateur a de l'anticipé utilisé const [anticipeUtilise] = await conn.query(` SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalAnticipe, MIN(dd.Id) as firstDeductionId, MIN(dd.DemandeCongeId) as demandeId FROM DeductionDetails dd JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id WHERE dc.CollaborateurADId = ? AND dd.TypeCongeId = ? AND dd.Annee = ? AND dd.TypeDeduction = 'N Anticip' AND dc.Statut != 'Refusée' AND dd.JoursUtilises > 0 `, [collaborateurId, cpTypeId, currentYear]); const anticipePris = parseFloat(anticipeUtilise[0]?.totalAnticipe || 0); if (anticipePris > 0) { // 4️⃣ Calculer le montant à rembourser const aRembourser = Math.min(incrementAcquis, anticipePris); console.log(` 💳 CP - Anticipé à rembourser: ${aRembourser.toFixed(2)}j (sur ${anticipePris.toFixed(2)}j)`); // 5️⃣ Rembourser l'anticipé en transférant vers "Année N" // On récupère toutes les déductions anticipées pour ce type const [deductionsAnticipees] = await conn.query(` SELECT dd.Id, dd.DemandeCongeId, dd.JoursUtilises FROM DeductionDetails dd JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id WHERE dc.CollaborateurADId = ? AND dd.TypeCongeId = ? AND dd.Annee = ? AND dd.TypeDeduction = 'N Anticip' AND dc.Statut != 'Refusée' AND dd.JoursUtilises > 0 ORDER BY dd.Id ASC `, [collaborateurId, cpTypeId, currentYear]); let resteARembourser = aRembourser; for (const deduction of deductionsAnticipees) { if (resteARembourser <= 0) break; const joursAnticipes = parseFloat(deduction.JoursUtilises); const aDeduiteDeCetteDeduction = Math.min(resteARembourser, joursAnticipes); // Réduire l'anticipé await conn.query(` UPDATE DeductionDetails SET JoursUtilises = GREATEST(0, JoursUtilises - ?) WHERE Id = ? `, [aDeduiteDeCetteDeduction, deduction.Id]); // Vérifier si une déduction "Année N" existe déjà pour cette demande const [existingAnneeN] = await conn.query(` SELECT Id, JoursUtilises FROM DeductionDetails WHERE DemandeCongeId = ? AND TypeCongeId = ? AND Annee = ? AND TypeDeduction IN ('Année N', 'Anne N', 'Anne actuelle N') `, [deduction.DemandeCongeId, cpTypeId, currentYear]); if (existingAnneeN.length > 0) { // Augmenter la déduction "Année N" existante await conn.query(` UPDATE DeductionDetails SET JoursUtilises = JoursUtilises + ? WHERE Id = ? `, [aDeduiteDeCetteDeduction, existingAnneeN[0].Id]); } else { // Créer une nouvelle déduction "Année N" await conn.query(` INSERT INTO DeductionDetails (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) VALUES (?, ?, ?, 'Année N', ?) `, [deduction.DemandeCongeId, cpTypeId, currentYear, aDeduiteDeCetteDeduction]); } resteARembourser -= aDeduiteDeCetteDeduction; console.log(` ✅ CP - Remboursé ${aDeduiteDeCetteDeduction.toFixed(2)}j (Demande ${deduction.DemandeCongeId})`); } // Supprimer les déductions anticipées à zéro await conn.query(` DELETE FROM DeductionDetails WHERE TypeCongeId = ? AND Annee = ? AND TypeDeduction = 'N Anticip' AND JoursUtilises <= 0 `, [cpTypeId, currentYear]); } } // 6️⃣ Recalculer le solde total (acquis + report - consommé) const [consomme] = await conn.query(` SELECT COALESCE(SUM(dd.JoursUtilises), 0) as total FROM DeductionDetails dd JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id WHERE dc.CollaborateurADId = ? AND dd.TypeCongeId = ? AND dd.Annee = ? AND dd.TypeDeduction NOT IN ('Accum Récup', 'Accum Recup') AND dc.Statut != 'Refusée' `, [collaborateurId, cpTypeId, currentYear]); const totalConsomme = parseFloat(consomme[0].total || 0); const nouveauSolde = Math.max(0, acquisitionCP + soldeReporte - totalConsomme); console.log(` CP - Consommé total: ${totalConsomme.toFixed(2)}j`); console.log(` CP - Nouveau solde: ${nouveauSolde.toFixed(2)}j`); // 7️⃣ Mettre à jour le compteur await conn.query(` UPDATE CompteurConges SET Total = ?, Solde = ?, DerniereMiseAJour = NOW() WHERE Id = ? `, [acquisitionCP, nouveauSolde, existingCP[0].Id]); updates.push({ type: 'CP', exercice: exerciceCP, acquisitionCumulee: acquisitionCP, increment: incrementAcquis, nouveauSolde: nouveauSolde }); } else { // Créer le compteur s'il n'existe pas await conn.query(` INSERT INTO CompteurConges (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour) VALUES (?, ?, ?, ?, ?, 0, NOW()) `, [collaborateurId, cpTypeId, currentYear, acquisitionCP, acquisitionCP]); console.log(` CP - Compteur créé: ${acquisitionCP.toFixed(2)}j`); updates.push({ type: 'CP', exercice: exerciceCP, acquisitionCumulee: acquisitionCP, action: 'created', nouveauSolde: acquisitionCP }); } } // ====================================== // RTT // ====================================== if (!isApprenti) { const rttData = await calculerAcquisitionRTT(conn, collaborateurId, today); const acquisitionRTT = rttData.acquisition; const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']); if (rttType.length > 0) { const rttTypeId = rttType[0].Id; // 1️⃣ Récupérer le compteur existant const [existingRTT] = await conn.query(` SELECT Id, Total, Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? `, [collaborateurId, rttTypeId, currentYear]); if (existingRTT.length > 0) { const ancienTotal = parseFloat(existingRTT[0].Total || 0); const ancienSolde = parseFloat(existingRTT[0].Solde || 0); console.log(` RTT - Ancien acquis: ${ancienTotal.toFixed(2)}j`); console.log(` RTT - Nouvel acquis: ${acquisitionRTT.toFixed(2)}j`); // 2️⃣ Calculer l'incrément d'acquisition const incrementAcquis = acquisitionRTT - ancienTotal; if (incrementAcquis > 0) { console.log(` RTT - Nouveaux jours ce mois: +${incrementAcquis.toFixed(2)}j`); // 3️⃣ Vérifier si le collaborateur a de l'anticipé utilisé const [anticipeUtilise] = await conn.query(` SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalAnticipe FROM DeductionDetails dd JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id WHERE dc.CollaborateurADId = ? AND dd.TypeCongeId = ? AND dd.Annee = ? AND dd.TypeDeduction = 'N Anticip' AND dc.Statut != 'Refusée' AND dd.JoursUtilises > 0 `, [collaborateurId, rttTypeId, currentYear]); const anticipePris = parseFloat(anticipeUtilise[0]?.totalAnticipe || 0); if (anticipePris > 0) { // 4️⃣ Calculer le montant à rembourser const aRembourser = Math.min(incrementAcquis, anticipePris); console.log(` 💳 RTT - Anticipé à rembourser: ${aRembourser.toFixed(2)}j (sur ${anticipePris.toFixed(2)}j)`); // 5️⃣ Rembourser l'anticipé const [deductionsAnticipees] = await conn.query(` SELECT dd.Id, dd.DemandeCongeId, dd.JoursUtilises FROM DeductionDetails dd JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id WHERE dc.CollaborateurADId = ? AND dd.TypeCongeId = ? AND dd.Annee = ? AND dd.TypeDeduction = 'N Anticip' AND dc.Statut != 'Refusée' AND dd.JoursUtilises > 0 ORDER BY dd.Id ASC `, [collaborateurId, rttTypeId, currentYear]); let resteARembourser = aRembourser; for (const deduction of deductionsAnticipees) { if (resteARembourser <= 0) break; const joursAnticipes = parseFloat(deduction.JoursUtilises); const aDeduiteDeCetteDeduction = Math.min(resteARembourser, joursAnticipes); // Réduire l'anticipé await conn.query(` UPDATE DeductionDetails SET JoursUtilises = GREATEST(0, JoursUtilises - ?) WHERE Id = ? `, [aDeduiteDeCetteDeduction, deduction.Id]); // Vérifier si une déduction "Année N" existe déjà const [existingAnneeN] = await conn.query(` SELECT Id, JoursUtilises FROM DeductionDetails WHERE DemandeCongeId = ? AND TypeCongeId = ? AND Annee = ? AND TypeDeduction IN ('Année N', 'Anne N') `, [deduction.DemandeCongeId, rttTypeId, currentYear]); if (existingAnneeN.length > 0) { await conn.query(` UPDATE DeductionDetails SET JoursUtilises = JoursUtilises + ? WHERE Id = ? `, [aDeduiteDeCetteDeduction, existingAnneeN[0].Id]); } else { await conn.query(` INSERT INTO DeductionDetails (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) VALUES (?, ?, ?, 'Année N', ?) `, [deduction.DemandeCongeId, rttTypeId, currentYear, aDeduiteDeCetteDeduction]); } resteARembourser -= aDeduiteDeCetteDeduction; console.log(` ✅ RTT - Remboursé ${aDeduiteDeCetteDeduction.toFixed(2)}j (Demande ${deduction.DemandeCongeId})`); } // Supprimer les déductions anticipées à zéro await conn.query(` DELETE FROM DeductionDetails WHERE TypeCongeId = ? AND Annee = ? AND TypeDeduction = 'N Anticip' AND JoursUtilises <= 0 `, [rttTypeId, currentYear]); } } // 6️⃣ Recalculer le solde total const [consomme] = await conn.query(` SELECT COALESCE(SUM(dd.JoursUtilises), 0) as total FROM DeductionDetails dd JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id WHERE dc.CollaborateurADId = ? AND dd.TypeCongeId = ? AND dd.Annee = ? AND dd.TypeDeduction NOT IN ('Accum Récup', 'Accum Recup', 'Récup Dosée') AND dc.Statut != 'Refusée' `, [collaborateurId, rttTypeId, currentYear]); const totalConsomme = parseFloat(consomme[0].total || 0); const nouveauSolde = Math.max(0, acquisitionRTT - totalConsomme); console.log(` RTT - Consommé total: ${totalConsomme.toFixed(2)}j`); console.log(` RTT - Nouveau solde: ${nouveauSolde.toFixed(2)}j`); // 7️⃣ Mettre à jour le compteur await conn.query(` UPDATE CompteurConges SET Total = ?, Solde = ?, DerniereMiseAJour = NOW() WHERE Id = ? `, [acquisitionRTT, nouveauSolde, existingRTT[0].Id]); updates.push({ type: 'RTT', annee: currentYear, typeContrat: rttData.typeContrat, config: `${rttData.config.joursAnnuels}j/an`, moisTravailles: rttData.moisTravailles, acquisitionCumulee: acquisitionRTT, increment: incrementAcquis, nouveauSolde: nouveauSolde }); } else { // Créer le compteur s'il n'existe pas await conn.query(` INSERT INTO CompteurConges (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour) VALUES (?, ?, ?, ?, ?, 0, NOW()) `, [collaborateurId, rttTypeId, currentYear, acquisitionRTT, acquisitionRTT]); console.log(` RTT - Compteur créé: ${acquisitionRTT.toFixed(2)}j`); updates.push({ type: 'RTT', annee: currentYear, typeContrat: rttData.typeContrat, config: `${rttData.config.joursAnnuels}j/an`, moisTravailles: rttData.moisTravailles, acquisitionCumulee: acquisitionRTT, action: 'created', nouveauSolde: acquisitionRTT }); } } } console.log(`✅ Mise à jour terminée pour collaborateur ${collaborateurId}\n`); return updates; } // ======================================== // ROUTES API // ======================================== app.post('/api/login', async (req, res) => { try { const { email, mot_de_passe, entraUserId, userPrincipalName } = req.body; const accessToken = req.headers.authorization?.replace('Bearer ', ''); if (accessToken && entraUserId) { const [users] = await pool.query(` SELECT ca.*, s.Nom as service, so.Nom as societe_nom FROM CollaborateurAD ca LEFT JOIN Services s ON ca.ServiceId = s.Id LEFT JOIN Societe so ON ca.SocieteId = so.Id WHERE ca.entraUserId=? OR ca.email=? LIMIT 1 `, [entraUserId, email]); if (users.length === 0) return res.json({ success: false, message: 'Utilisateur non autorisé' }); const user = users[0]; try { const graphResponse = await axios.get(`https://graph.microsoft.com/v1.0/users/${userPrincipalName}/memberOf?$select=id`, { headers: { Authorization: `Bearer ${accessToken}` } }); const userGroups = graphResponse.data.value.map(g => g.id); const [allowedGroups] = await pool.query('SELECT Id FROM EntraGroups WHERE IsActive=1'); const allowed = allowedGroups.map(g => g.Id); const authorized = userGroups.some(g => allowed.includes(g)); if (authorized) { return res.json({ success: true, message: 'Connexion réussie via Azure AD', user: { id: user.id, prenom: user.prenom, nom: user.nom, email: user.email, role: user.role, service: user.service, societeId: user.SocieteId, societeNom: user.societe_nom } }); } else { return res.json({ success: false, message: 'Utilisateur non autorisé' }); } } catch (error) { return res.json({ success: false, message: 'Erreur vérification groupes' }); } } if (email && mot_de_passe) { const [users] = await pool.query(` SELECT u.ID, u.Prenom, u.Nom, u.Email, u.Role, u.ServiceId, s.Nom AS ServiceNom FROM Users u LEFT JOIN Services s ON u.ServiceId = s.Id WHERE u.Email = ? AND u.MDP = ? `, [email, mot_de_passe]); if (users.length === 1) { return res.json({ success: true, message: 'Connexion réussie', user: { id: users[0].ID, prenom: users[0].Prenom, nom: users[0].Nom, email: users[0].Email, role: users[0].Role, service: users[0].ServiceNom || 'Non défini' } }); } return res.json({ success: false, message: 'Identifiants incorrects' }); } res.json({ success: false, message: 'Aucune méthode de connexion fournie' }); } catch (error) { res.status(500).json({ success: false, message: 'Erreur serveur', error: error.message }); } }); app.post('/api/check-user-groups', async (req, res) => { try { const { userPrincipalName } = req.body; console.log('🔍 Check user groups pour:', userPrincipalName); if (!userPrincipalName) return res.json({ authorized: false }); // Vérification simple : Si l'user est dans la base, c'est OK const [users] = await pool.query('SELECT * FROM CollaborateurAD WHERE email = ?', [userPrincipalName]); if (users.length > 0) { const u = users[0]; return res.json({ authorized: true, role: u.role || 'Employe', groups: [u.role || 'Employe'], localUserId: u.id, user: { id: u.id, entraUserId: u.entraUserId, prenom: u.prenom, nom: u.nom, email: u.email, role: u.role } }); } // Si pas trouvé, on autorise quand même pour permettre l'initial-sync juste après // C'est une astuce pour éviter le blocage "chicken & egg" return res.json({ authorized: true, // ON FORCE À TRUE POUR DÉBLOQUER role: 'Nouveau', groups: ['Nouveau'], localUserId: null }); } catch (error) { console.error('❌ Erreur check-user-groups:', error.message); res.json({ authorized: false, error: error.message }); } }); // ======================================== // ✅ CODE CORRIGÉ POUR getDetailedLeaveCounters // À remplacer dans server.js à partir de la ligne ~1600 // ======================================== app.get('/api/getDetailedLeaveCounters', async (req, res) => { try { const userIdParam = req.query.user_id; if (!userIdParam) { return res.json({ success: false, message: 'ID utilisateur manquant' }); } const conn = await pool.getConnection(); // Déterminer l'ID (UUID ou numérique) const isUUID = userIdParam.length > 10 && userIdParam.includes('-'); const userQuery = ` SELECT ca.id, ca.prenom, ca.nom, ca.email, ca.DateEntree, ca.role, ca.TypeContrat, s.Nom as service, ca.CampusId, ca.SocieteId, so.Nom as societe_nom, ca.description FROM CollaborateurAD ca LEFT JOIN Services s ON ca.ServiceId = s.Id LEFT JOIN Societe so ON ca.SocieteId = so.Id WHERE ${isUUID ? 'ca.entraUserId' : 'ca.id'} = ? AND (ca.Actif = 1 OR ca.Actif IS NULL) `; const [userInfo] = await conn.query(userQuery, [userIdParam]); if (userInfo.length === 0) { conn.release(); return res.json({ success: false, message: 'Utilisateur non trouvé' }); } const user = userInfo[0]; const userId = user.id; const dateEntree = user.DateEntree; const typeContrat = user.TypeContrat || '37h'; const today = new Date(); const currentYear = today.getFullYear(); const previousYear = currentYear - 1; console.log(`\n📊 === CALCUL COMPTEURS pour ${user.prenom} ${user.nom} ===`); console.log(` Date référence: ${today.toLocaleDateString('fr-FR')}`); // 🔍 DEBUG : Afficher les valeurs brutes de la BDD console.log('\n📊 === VALEURS BRUTES CompteurConges ==='); const [debugCounters] = await conn.query(` SELECT tc.Nom, cc.Annee, cc.Total, cc.Solde, cc.SoldeReporte FROM CompteurConges cc JOIN TypeConge tc ON cc.TypeCongeId = tc.Id WHERE cc.CollaborateurADId = ? ORDER BY tc.Nom, cc.Annee DESC `, [userId]); debugCounters.forEach(c => { console.log(` ${c.Nom} ${c.Annee}: Total=${c.Total}j, Solde=${c.Solde}j, Report=${c.SoldeReporte}j`); }); console.log('=====================================\n'); const ancienneteMs = today - new Date(dateEntree || today); const ancienneteMois = Math.floor(ancienneteMs / (1000 * 60 * 60 * 24 * 30.44)); let counters = { user: { id: user.id, nom: `${user.prenom} ${user.nom}`, prenom: user.prenom, nomFamille: user.nom, email: user.email, service: user.service || 'Non défini', role: user.role, description: user.description, typeContrat: typeContrat, societeId: user.SocieteId, societeNom: user.societe_nom || 'Non défini', dateEntree: dateEntree ? formatDateWithoutUTC(dateEntree) : null, ancienneteMois: ancienneteMois, ancienneteAnnees: Math.floor(ancienneteMois / 12), ancienneteMoisRestants: ancienneteMois % 12 }, dateReference: today.toISOString().split('T')[0], exerciceCP: getExerciceCP(today), anneeRTT: currentYear, cpN1: null, cpN: null, rttN: null, rttN1: null, totalDisponible: { cp: 0, rtt: 0, recup: 0, total: 0 } }; const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']); // ==================================== // 1️⃣ CP N-1 (Report) // ==================================== if (cpType.length > 0) { const [cpN1] = await conn.query(` SELECT Annee, SoldeReporte, Total, Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? `, [userId, cpType[0].Id, previousYear]); if (cpN1.length > 0 && parseFloat(cpN1[0].Solde || 0) > 0) { const soldeReporte = parseFloat(cpN1[0].Solde || 0); // 🔥 Consommation N-1 depuis DeductionDetails const [consommeN1] = await conn.query(` SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalConsomme FROM DeductionDetails dd JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id WHERE dc.CollaborateurADId = ? AND dd.TypeCongeId = ? AND dd.Annee = ? AND dd.TypeDeduction IN ('Année N-1', 'Anne N-1', 'Report N-1', 'Reporté N-1') AND dd.TypeDeduction NOT IN ('Accum Récup', 'Accum Recup') AND dc.Statut != 'Refusée' `, [userId, cpType[0].Id, previousYear]); const pris = parseFloat(consommeN1[0]?.totalConsomme || 0); const soldeActuel = Math.max(0, soldeReporte - pris); counters.cpN1 = { annee: previousYear, exercice: `${previousYear}-${previousYear + 1}`, reporte: parseFloat(soldeReporte.toFixed(2)), pris: parseFloat(pris.toFixed(2)), solde: parseFloat(soldeActuel.toFixed(2)), pourcentageUtilise: soldeReporte > 0 ? parseFloat(((pris / soldeReporte) * 100).toFixed(1)) : 0 }; counters.totalDisponible.cp += counters.cpN1.solde; console.log(`✅ CP N-1: Reporté=${soldeReporte}j, Pris=${pris}j, Solde=${soldeActuel}j`); } else { counters.cpN1 = { annee: previousYear, exercice: `${previousYear}-${previousYear + 1}`, reporte: 0, pris: 0, solde: 0, pourcentageUtilise: 0 }; } // ==================================== // 2️⃣ CP N (Exercice en cours) // ==================================== const cpMonthsCurrent = getMoisTravaillesCP(today, dateEntree); const acquisCumuleeCP = parseFloat(calculerAcquisitionCP(today, dateEntree) || 0); console.log(` CP - Mois travaillés: ${cpMonthsCurrent.toFixed(2)}`); console.log(` CP - Acquisition cumulée: ${acquisCumuleeCP.toFixed(2)}j`); // 🔥 RÉCUPÉRER LE SOLDE DEPUIS CompteurConges (source de vérité) const [compteurCPN] = await conn.query(` SELECT Solde, SoldeReporte, Total FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? `, [userId, cpType[0].Id, currentYear]); let soldeActuelCP = 0; let totalAcquis = acquisCumuleeCP; if (compteurCPN.length > 0) { // ⭐ Utiliser le solde de la base (déjà ajusté par les déductions) const soldeBDD = parseFloat(compteurCPN[0].Solde || 0); const soldeReporte = parseFloat(compteurCPN[0].SoldeReporte || 0); // Solde actuel = Solde total - Report (pour avoir uniquement l'année N) soldeActuelCP = Math.max(0, soldeBDD - soldeReporte); console.log(` CP N - Solde BDD: ${soldeBDD}j, Report: ${soldeReporte}j → Solde N: ${soldeActuelCP}j`); } else { // Si pas de compteur, le solde = acquisition (aucune déduction) soldeActuelCP = acquisCumuleeCP; console.log(` CP N - Pas de compteur BDD → Solde = Acquisition: ${soldeActuelCP}j`); } // 🔥 CALCUL DE L'ANTICIPÉ const [anticipeUtiliseCP] = await conn.query(` SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalConsomme FROM DeductionDetails dd JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id WHERE dc.CollaborateurADId = ? AND dd.TypeCongeId = ? AND dd.Annee = ? AND dd.TypeDeduction = 'N Anticip' AND dc.Statut != 'Refusée' `, [userId, cpType[0].Id, currentYear]); const cpAnticipeUtilise = parseFloat(anticipeUtiliseCP[0]?.totalConsomme || 0); const cpAnticipeMax = Math.max(0, 25 - acquisCumuleeCP); const cpAnticipeDisponible = Math.max(0, cpAnticipeMax - cpAnticipeUtilise); console.log(` CP Anticipé: Max=${cpAnticipeMax}j, Utilisé=${cpAnticipeUtilise}j, Dispo=${cpAnticipeDisponible}j`); counters.cpN = { annee: currentYear, exercice: getExerciceCP(today), totalAnnuel: 25.00, moisTravailles: parseFloat(cpMonthsCurrent.toFixed(2)), acquisitionMensuelle: parseFloat((25 / 12).toFixed(2)), acquis: parseFloat(totalAcquis.toFixed(2)), pris: parseFloat((totalAcquis - soldeActuelCP).toFixed(2)), // Déduit du solde solde: parseFloat(soldeActuelCP.toFixed(2)), // ⭐ Solde réel de la BDD tauxAcquisition: parseFloat((cpMonthsCurrent / 12 * 100).toFixed(1)), pourcentageUtilise: totalAcquis > 0 ? parseFloat(((totalAcquis - soldeActuelCP) / totalAcquis * 100).toFixed(1)) : 0, joursRestantsAAcquerir: parseFloat((25 - totalAcquis).toFixed(2)), anticipe: { acquisPrevu: parseFloat(cpAnticipeMax.toFixed(2)), pris: parseFloat(cpAnticipeUtilise.toFixed(2)), disponible: parseFloat(cpAnticipeDisponible.toFixed(2)), depassement: cpAnticipeUtilise > cpAnticipeMax ? parseFloat((cpAnticipeUtilise - cpAnticipeMax).toFixed(2)) : 0 } }; counters.totalDisponible.cp += counters.cpN.solde + cpAnticipeDisponible; console.log(`✅ CP N: Acquis=${totalAcquis}j, Pris=${counters.cpN.pris}j, Solde=${soldeActuelCP}j`); } // ==================================== // 3️⃣ RTT N // ==================================== const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']); if (rttType.length > 0 && user.role !== 'Apprenti') { const rttData = await calculerAcquisitionRTT(conn, userId, today); const rttConfig = await getConfigurationRTT(conn, currentYear, typeContrat); console.log(` RTT - Config: ${rttConfig.joursAnnuels}j/an`); console.log(` RTT - Acquisition: ${rttData.acquisition.toFixed(2)}j`); // 🔥 RÉCUPÉRER LE SOLDE DEPUIS CompteurConges const [compteurRTT] = await conn.query(` SELECT Solde, Total FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? `, [userId, rttType[0].Id, currentYear]); let soldeActuelRTT = 0; if (compteurRTT.length > 0) { // ⭐ Utiliser le solde de la base soldeActuelRTT = parseFloat(compteurRTT[0].Solde || 0); console.log(` RTT - Solde BDD: ${soldeActuelRTT}j`); } else { // Si pas de compteur, solde = acquisition soldeActuelRTT = rttData.acquisition; console.log(` RTT - Pas de compteur BDD → Solde = Acquisition: ${soldeActuelRTT}j`); } // 🔥 CALCUL DE L'ANTICIPÉ RTT const [anticipeUtiliseRTT] = await conn.query(` SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalConsomme FROM DeductionDetails dd JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id WHERE dc.CollaborateurADId = ? AND dd.TypeCongeId = ? AND dd.Annee = ? AND dd.TypeDeduction = 'N Anticip' AND dc.Statut != 'Refusée' `, [userId, rttType[0].Id, currentYear]); const rttAnticipeUtilise = parseFloat(anticipeUtiliseRTT[0]?.totalConsomme || 0); const rttAnticipeMax = Math.max(0, rttConfig.joursAnnuels - rttData.acquisition); const rttAnticipeDisponible = Math.max(0, rttAnticipeMax - rttAnticipeUtilise); console.log(` RTT Anticipé: Max=${rttAnticipeMax}j, Utilisé=${rttAnticipeUtilise}j, Dispo=${rttAnticipeDisponible}j`); counters.rttN = { annee: currentYear, typeContrat: typeContrat, totalAnnuel: parseFloat(rttConfig.joursAnnuels.toFixed(2)), moisTravailles: rttData.moisTravailles, acquisitionMensuelle: parseFloat(rttConfig.acquisitionMensuelle.toFixed(6)), acquis: parseFloat(rttData.acquisition.toFixed(2)), pris: parseFloat((rttData.acquisition - soldeActuelRTT).toFixed(2)), solde: parseFloat(soldeActuelRTT.toFixed(2)), // ⭐ Solde réel de la BDD tauxAcquisition: parseFloat((rttData.moisTravailles / 12 * 100).toFixed(1)), pourcentageUtilise: rttData.acquisition > 0 ? parseFloat(((rttData.acquisition - soldeActuelRTT) / rttData.acquisition * 100).toFixed(1)) : 0, joursRestantsAAcquerir: parseFloat((rttConfig.joursAnnuels - rttData.acquisition).toFixed(2)), anticipe: { acquisPrevu: parseFloat(rttAnticipeMax.toFixed(2)), pris: parseFloat(rttAnticipeUtilise.toFixed(2)), disponible: parseFloat(rttAnticipeDisponible.toFixed(2)), depassement: rttAnticipeUtilise > rttAnticipeMax ? parseFloat((rttAnticipeUtilise - rttAnticipeMax).toFixed(2)) : 0 } }; counters.totalDisponible.rtt += counters.rttN.solde + rttAnticipeDisponible; console.log(`✅ RTT N: Acquis=${rttData.acquisition}j, Pris=${counters.rttN.pris}j, Solde=${soldeActuelRTT}j`); } counters.rttN1 = { annee: previousYear, reporte: 0, pris: 0, solde: 0, pourcentageUtilise: 0, message: "Les RTT ne sont pas reportables d'une année sur l'autre" }; // ==================================== // 4️⃣ RÉCUP // ==================================== const [recupType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Récupération']); if (recupType.length > 0) { // 🔥 Récupérer les accumulations const [accumRecup] = await conn.query(` SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalAccum FROM DeductionDetails dd JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id WHERE dc.CollaborateurADId = ? AND dd.TypeCongeId = ? AND dd.Annee = ? AND dd.TypeDeduction IN ('Accum Récup', 'Accum Recup') AND dc.Statut != 'Refusée' `, [userId, recupType[0].Id, currentYear]); // 🔥 Récupérer les consommations const [consomRecup] = await conn.query(` SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalConsom FROM DeductionDetails dd JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id WHERE dc.CollaborateurADId = ? AND dd.TypeCongeId = ? AND dd.Annee = ? AND dd.TypeDeduction IN ('Récup Dosée', 'Recup Dosee', 'Récup Posée') AND dc.Statut != 'Refusée' `, [userId, recupType[0].Id, currentYear]); const acquis = parseFloat(accumRecup[0]?.totalAccum || 0); const pris = parseFloat(consomRecup[0]?.totalConsom || 0); const solde = Math.max(0, acquis - pris); counters.recupN = { annee: currentYear, acquis: parseFloat(acquis.toFixed(2)), pris: parseFloat(pris.toFixed(2)), solde: parseFloat(solde.toFixed(2)), message: "Jours de récupération" }; counters.totalDisponible.recup = counters.recupN.solde; console.log(`✅ Récup: Acquis=${acquis}j, Pris=${pris}j, Solde=${solde}j`); } else { counters.recupN = { annee: currentYear, acquis: 0, pris: 0, solde: 0, message: "Jours de récupération" }; counters.totalDisponible.recup = 0; } // Recalculer le TOTAL counters.totalDisponible.total = counters.totalDisponible.cp + counters.totalDisponible.rtt + counters.totalDisponible.recup; console.log(`\n✅ TOTAL FINAL: ${counters.totalDisponible.total.toFixed(2)}j disponibles`); console.log(` CP: ${counters.totalDisponible.cp.toFixed(2)}j`); console.log(` RTT: ${counters.totalDisponible.rtt.toFixed(2)}j`); console.log(` Récup: ${counters.totalDisponible.recup.toFixed(2)}j\n`); conn.release(); res.json({ success: true, message: 'Compteurs détaillés récupérés avec succès', data: counters, availableCP: counters.totalDisponible.cp, availableRTT: counters.totalDisponible.rtt, availableRecup: counters.totalDisponible.recup }); } catch (error) { console.error('Erreur getDetailedLeaveCounters:', error); res.status(500).json({ success: false, message: 'Erreur serveur', error: error.message }); } }); app.post('/api/reinitializeAllCounters', async (req, res) => { const conn = await pool.getConnection(); try { await conn.beginTransaction(); console.log('🔄 Réinitialisation de tous les compteurs...'); const [collaborateurs] = await conn.query(` SELECT id, prenom, nom, DateEntree, TypeContrat, CampusId, SocieteId FROM CollaborateurAD WHERE (actif = 1 OR actif IS NULL) `); console.log(`📊 ${collaborateurs.length} collaborateurs trouvés`); const dateRefParam = req.body.dateReference; const today = dateRefParam ? new Date(dateRefParam) : new Date(); const currentYear = today.getFullYear(); const previousYear = currentYear - 1; const results = []; for (const collab of collaborateurs) { const dateEntree = collab.DateEntree; const typeContrat = collab.TypeContrat || '37h'; // Calculer les acquisitions const acquisCP = calculerAcquisitionCP(today, dateEntree); const isApprenti = collab.role === 'Apprenti'; const rttData = isApprenti ? { acquisition: 0 } : await calculerAcquisitionRTT(conn, collab.id, today); const acquisRTT = rttData.acquisition; const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']); const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']); // ===== CP N ===== if (cpType.length > 0) { // ⭐ CALCUL FIABLE : Utiliser DeductionDetails au lieu des anciennes valeurs const [deductionsCP] = await conn.query(` SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalConsomme FROM DeductionDetails dd JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id WHERE dc.CollaborateurADId = ? AND dd.TypeCongeId = ? AND dd.Annee = ? AND dc.Statut != 'Refusée' `, [collab.id, cpType[0].Id, currentYear]); const totalConsomme = parseFloat(deductionsCP[0].totalConsomme || 0); // Récupérer le reporté (qui ne change pas) const [compteurExisting] = await conn.query(` SELECT SoldeReporte FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? `, [collab.id, cpType[0].Id, currentYear]); const soldeReporte = compteurExisting.length > 0 ? parseFloat(compteurExisting[0].SoldeReporte || 0) : 0; // ⭐ CALCUL EXACT DU SOLDE const nouveauSolde = Math.max(0, acquisCP + soldeReporte - totalConsomme); // Mise à jour ou insertion if (compteurExisting.length > 0) { await conn.query(` UPDATE CompteurConges SET Total = ?, Solde = ?, DerniereMiseAJour = NOW() WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? `, [acquisCP, nouveauSolde, collab.id, cpType[0].Id, currentYear]); } else { await conn.query(` INSERT INTO CompteurConges (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour) VALUES (?, ?, ?, ?, ?, 0, NOW()) `, [collab.id, cpType[0].Id, currentYear, acquisCP, nouveauSolde]); } console.log(`📊 ${collab.prenom} ${collab.nom} - CP: Acquis ${acquisCP.toFixed(2)}j, Consommé ${totalConsomme.toFixed(2)}j, Reporté ${soldeReporte.toFixed(2)}j → Solde ${nouveauSolde.toFixed(2)}j`); // Créer CP N-1 si nécessaire const [cpN1] = await conn.query(` SELECT Id FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? `, [collab.id, cpType[0].Id, previousYear]); if (cpN1.length === 0) { await conn.query(` INSERT INTO CompteurConges (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour) VALUES (?, ?, ?, 0, 0, 0, NOW()) `, [collab.id, cpType[0].Id, previousYear]); } } // ===== RTT N ===== if (rttType.length > 0 && !isApprenti) { // ⭐ MÊME LOGIQUE POUR LES RTT const [deductionsRTT] = await conn.query(` SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalConsomme FROM DeductionDetails dd JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id WHERE dc.CollaborateurADId = ? AND dd.TypeCongeId = ? AND dd.Annee = ? AND dc.Statut != 'Refusée' `, [collab.id, rttType[0].Id, currentYear]); const totalConsomme = parseFloat(deductionsRTT[0].totalConsomme || 0); const nouveauSolde = Math.max(0, acquisRTT - totalConsomme); const [compteurExisting] = await conn.query(` SELECT Id FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? `, [collab.id, rttType[0].Id, currentYear]); if (compteurExisting.length > 0) { await conn.query(` UPDATE CompteurConges SET Total = ?, Solde = ?, DerniereMiseAJour = NOW() WHERE Id = ? `, [acquisRTT, nouveauSolde, compteurExisting[0].Id]); } else { await conn.query(` INSERT INTO CompteurConges (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour) VALUES (?, ?, ?, ?, ?, 0, NOW()) `, [collab.id, rttType[0].Id, currentYear, acquisRTT, nouveauSolde]); } console.log(`📊 ${collab.prenom} ${collab.nom} - RTT: Acquis ${acquisRTT.toFixed(2)}j, Consommé ${totalConsomme.toFixed(2)}j → Solde ${nouveauSolde.toFixed(2)}j`); // Créer RTT N-1 si nécessaire const [rttN1] = await conn.query(` SELECT Id FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? `, [collab.id, rttType[0].Id, previousYear]); if (rttN1.length === 0) { await conn.query(` INSERT INTO CompteurConges (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour) VALUES (?, ?, ?, 0, 0, 0, NOW()) `, [collab.id, rttType[0].Id, previousYear]); } } results.push({ collaborateur: `${collab.prenom} ${collab.nom}`, type_contrat: typeContrat, cp_acquis: acquisCP.toFixed(2), cp_solde: (acquisCP - (deductionsCP?.[0]?.totalConsomme || 0)).toFixed(2), rtt_acquis: acquisRTT.toFixed(2), rtt_solde: (acquisRTT - (deductionsRTT?.[0]?.totalConsomme || 0)).toFixed(2) }); } await conn.commit(); console.log('✅ Réinitialisation terminée'); res.json({ success: true, message: `✅ Compteurs réinitialisés pour ${collaborateurs.length} collaborateurs`, date_reference: today.toISOString().split('T')[0], total_collaborateurs: collaborateurs.length, results: results }); } catch (error) { await conn.rollback(); console.error('❌ Erreur réinitialisation:', error); res.status(500).json({ success: false, message: 'Erreur lors de la réinitialisation', error: error.message }); } finally { conn.release(); } }); app.post('/api/updateCounters', async (req, res) => { const conn = await pool.getConnection(); try { const { collaborateur_id } = req.body; if (!collaborateur_id) return res.json({ success: false, message: 'ID collaborateur manquant' }); await conn.beginTransaction(); const updates = await updateMonthlyCounters(conn, collaborateur_id, new Date()); await conn.commit(); res.json({ success: true, message: 'Compteurs mis à jour', updates }); } catch (error) { await conn.rollback(); res.status(500).json({ success: false, message: 'Erreur', error: error.message }); } finally { conn.release(); } }); app.post('/api/updateAllCounters', async (req, res) => { const conn = await pool.getConnection(); try { await conn.beginTransaction(); const [collaborateurs] = await conn.query('SELECT id, CampusId FROM CollaborateurAD WHERE actif = 1 OR actif IS NULL'); const allUpdates = []; for (const collab of collaborateurs) { const updates = await updateMonthlyCounters(conn, collab.id, new Date()); allUpdates.push({ collaborateur_id: collab.id, updates }); } await conn.commit(); res.json({ success: true, message: `Compteurs mis à jour pour ${collaborateurs.length} collaborateurs`, total_collaborateurs: collaborateurs.length, details: allUpdates }); } catch (error) { await conn.rollback(); res.status(500).json({ success: false, message: 'Erreur', error: error.message }); } finally { conn.release(); } }); async function deductLeaveBalanceWithTracking(conn, collaborateurId, typeCongeId, nombreJours, demandeCongeId) { const currentYear = new Date().getFullYear(); const previousYear = currentYear - 1; let joursRestants = nombreJours; const deductions = []; // Étape 1: Déduire du reporté N-1 d'abord const [compteurN1] = await conn.query( `SELECT Id, Solde, SoldeReporte FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, [collaborateurId, typeCongeId, previousYear] ); if (compteurN1.length > 0 && compteurN1[0].SoldeReporte > 0) { const soldeN1 = parseFloat(compteurN1[0].SoldeReporte); const aDeduireN1 = Math.min(soldeN1, joursRestants); if (aDeduireN1 > 0) { // Déduction dans la base await conn.query( `UPDATE CompteurConges SET SoldeReporte = GREATEST(0, SoldeReporte - ?), Solde = GREATEST(0, Solde - ?) WHERE Id = ?`, [aDeduireN1, aDeduireN1, compteurN1[0].Id] ); // Sauvegarde du détail de la déduction await conn.query(` INSERT INTO DeductionDetails (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) VALUES (?, ?, ?, 'Accum Récup', ?) `, [demandeId, recupType[0].Id, currentYear, recupJours]); deductions.push({ annee: previousYear, type: 'Reporté N-1', joursUtilises: aDeduireN1, soldeAvant: soldeN1 }); joursRestants -= aDeduireN1; } } // Étape 2: Déduire de l'année N si besoin if (joursRestants > 0) { const [compteurN] = await conn.query( `SELECT Id, Solde, SoldeReporte FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, [collaborateurId, typeCongeId, currentYear] ); if (compteurN.length > 0) { const soldeN = parseFloat(compteurN[0].Solde) - parseFloat(compteurN[0].SoldeReporte || 0); const aDeduireN = Math.min(soldeN, joursRestants); if (aDeduireN > 0) { // Déduction dans la base await conn.query( `UPDATE CompteurConges SET Solde = GREATEST(0, Solde - ?) WHERE Id = ?`, [aDeduireN, compteurN[0].Id] ); // Sauvegarde du détail de la déduction await conn.query( `INSERT INTO DeductionDetails (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) VALUES (?, ?, ?, 'Année N', ?)`, [demandeCongeId, typeCongeId, currentYear, aDeduireN] ); deductions.push({ annee: currentYear, type: 'Année actuelle N', joursUtilises: aDeduireN, soldeAvant: soldeN }); joursRestants -= aDeduireN; } } } return { success: joursRestants === 0, joursDeduitsTotal: nombreJours - joursRestants, joursNonDeduits: joursRestants, details: deductions }; }; async function restoreLeaveBalance(conn, demandeCongeId, collaborateurId) { try { console.log(`\n🔄 === RESTAURATION COMPTEURS ===`); console.log(`Demande ID: ${demandeCongeId}`); console.log(`Collaborateur ID: ${collaborateurId}`); const [deductions] = await conn.query( `SELECT dd.TypeCongeId, dd.Annee, dd.TypeDeduction, dd.JoursUtilises, tc.Nom as TypeNom FROM DeductionDetails dd JOIN TypeConge tc ON dd.TypeCongeId = tc.Id WHERE dd.DemandeCongeId = ? ORDER BY dd.Id DESC`, [demandeCongeId] ); console.log(`📊 ${deductions.length} déductions trouvées`); if (deductions.length === 0) { console.log('⚠️ Aucune déduction trouvée pour cette demande'); return { success: false, message: 'Aucune déduction à restaurer' }; } const restorations = []; for (const deduction of deductions) { const { TypeCongeId, Annee, TypeDeduction, JoursUtilises, TypeNom } = deduction; console.log(`\n🔍 Traitement: ${TypeNom} - ${TypeDeduction} - ${JoursUtilises}j (Année: ${Annee})`); // ⭐ NOUVEAU : Gestion des Récup posées if (TypeDeduction === 'Récup Posée') { console.log(`🔄 Restauration Récup posée: +${JoursUtilises}j`); const [compteur] = await conn.query( `SELECT Id, Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, [collaborateurId, TypeCongeId, Annee] ); if (compteur.length > 0) { const ancienSolde = parseFloat(compteur[0].Solde || 0); const nouveauSolde = ancienSolde + parseFloat(JoursUtilises); await conn.query( `UPDATE CompteurConges SET Solde = ?, DerniereMiseAJour = NOW() WHERE Id = ?`, [nouveauSolde, compteur[0].Id] ); restorations.push({ type: TypeNom, annee: Annee, typeDeduction: TypeDeduction, joursRestores: JoursUtilises }); console.log(`✅ Récup restaurée: ${ancienSolde} → ${nouveauSolde}`); } continue; } // 🔹 N+1 Anticipé - ⭐ RESTAURATION CORRECTE if (TypeDeduction === 'N+1 Anticipé') { console.log(`🔄 Restauration N+1 Anticipé: +${JoursUtilises}j`); const [compteur] = await conn.query( `SELECT Id, SoldeAnticipe FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, [collaborateurId, TypeCongeId, Annee] ); if (compteur.length > 0) { const ancienSolde = parseFloat(compteur[0].SoldeAnticipe || 0); const nouveauSolde = ancienSolde + parseFloat(JoursUtilises); await conn.query( `UPDATE CompteurConges SET SoldeAnticipe = ?, DerniereMiseAJour = NOW() WHERE Id = ?`, [nouveauSolde, compteur[0].Id] ); restorations.push({ type: TypeNom, annee: Annee, typeDeduction: TypeDeduction, joursRestores: JoursUtilises }); console.log(`✅ N+1 Anticipé restauré: ${ancienSolde} → ${nouveauSolde}`); } else { // ⭐ Créer le compteur N+1 s'il n'existe pas await conn.query( `INSERT INTO CompteurConges (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, SoldeAnticipe, DerniereMiseAJour) VALUES (?, ?, ?, 0, 0, 0, ?, NOW())`, [collaborateurId, TypeCongeId, Annee, JoursUtilises] ); restorations.push({ type: TypeNom, annee: Annee, typeDeduction: TypeDeduction, joursRestores: JoursUtilises }); console.log(`✅ Compteur N+1 créé avec ${JoursUtilises}j anticipés`); } continue; } // 🔹 N Anticipé - ⭐ RESTAURATION CORRECTE if (TypeDeduction === 'N Anticipé') { console.log(`🔄 Restauration N Anticipé: +${JoursUtilises}j`); const [compteur] = await conn.query( `SELECT Id, SoldeAnticipe FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, [collaborateurId, TypeCongeId, Annee] ); if (compteur.length > 0) { const ancienSolde = parseFloat(compteur[0].SoldeAnticipe || 0); const nouveauSolde = ancienSolde + parseFloat(JoursUtilises); await conn.query( `UPDATE CompteurConges SET SoldeAnticipe = ?, DerniereMiseAJour = NOW() WHERE Id = ?`, [nouveauSolde, compteur[0].Id] ); restorations.push({ type: TypeNom, annee: Annee, typeDeduction: TypeDeduction, joursRestores: JoursUtilises }); console.log(`✅ N Anticipé restauré: ${ancienSolde} → ${nouveauSolde}`); } else { // ⭐ Créer le compteur s'il n'existe pas (cas rare) await conn.query( `INSERT INTO CompteurConges (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, SoldeAnticipe, DerniereMiseAJour) VALUES (?, ?, ?, 0, 0, 0, ?, NOW())`, [collaborateurId, TypeCongeId, Annee, JoursUtilises] ); restorations.push({ type: TypeNom, annee: Annee, typeDeduction: TypeDeduction, joursRestores: JoursUtilises }); console.log(`✅ Compteur N créé avec ${JoursUtilises}j anticipés`); } continue; } // 🔹 Reporté N-1 if (TypeDeduction === 'Reporté N-1' || TypeDeduction === 'Report N-1') { const [compteur] = await conn.query( `SELECT Id, SoldeReporte, Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, [collaborateurId, TypeCongeId, Annee] ); if (compteur.length > 0) { const ancienSolde = parseFloat(compteur[0].Solde || 0); const nouveauSolde = ancienSolde + parseFloat(JoursUtilises); await conn.query( `UPDATE CompteurConges SET SoldeReporte = SoldeReporte + ?, Solde = Solde + ?, DerniereMiseAJour = NOW() WHERE Id = ?`, [JoursUtilises, JoursUtilises, compteur[0].Id] ); restorations.push({ type: TypeNom, annee: Annee, typeDeduction: TypeDeduction, joursRestores: JoursUtilises }); console.log(`✅ Reporté restauré: ${ancienSolde} → ${nouveauSolde}`); } } // 🔹 Année N else if (TypeDeduction === 'Année N') { const [compteur] = await conn.query( `SELECT Id, Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, [collaborateurId, TypeCongeId, Annee] ); if (compteur.length > 0) { const ancienSolde = parseFloat(compteur[0].Solde || 0); const nouveauSolde = ancienSolde + parseFloat(JoursUtilises); await conn.query( `UPDATE CompteurConges SET Solde = Solde + ?, DerniereMiseAJour = NOW() WHERE Id = ?`, [JoursUtilises, compteur[0].Id] ); restorations.push({ type: TypeNom, annee: Annee, typeDeduction: TypeDeduction, joursRestores: JoursUtilises }); console.log(`✅ Année N restaurée: ${ancienSolde} → ${nouveauSolde}`); } } } // ⭐ IMPORTANT : Recalculer les soldes anticipés après restauration console.log(`\n🔄 Recalcul des soldes anticipés...`); await updateSoldeAnticipe(conn, collaborateurId); console.log(`\n✅ Restauration terminée: ${restorations.length} opérations\n`); return { success: true, restorations, message: `${restorations.length} restaurations effectuées` }; } catch (error) { console.error('❌ Erreur lors de la restauration des soldes:', error); throw error; } } app.get('/api/testProrata', async (req, res) => { try { const userId = parseInt(req.query.user_id || 0); if (userId <= 0) return res.json({ success: false, message: 'ID utilisateur requis' }); const conn = await pool.getConnection(); const [userInfo] = await conn.query(`SELECT id, prenom, nom, DateEntree, TypeContrat, CampusId FROM CollaborateurAD WHERE id = ?`, [userId]); if (userInfo.length === 0) { conn.release(); return res.json({ success: false, message: 'Utilisateur non trouvé' }); } const user = userInfo[0]; const dateEntree = user.DateEntree; const typeContrat = user.TypeContrat || '37h'; const today = new Date(); const moisCP = getMoisTravaillesCP(today, dateEntree); const acquisCP = calculerAcquisitionCP(today, dateEntree); const rttData = await calculerAcquisitionRTT(conn, userId, today); conn.release(); res.json({ success: true, user: { id: user.id, nom: `${user.prenom} ${user.nom}`, dateEntree: dateEntree ? dateEntree.toISOString().split('T')[0] : null, typeContrat: typeContrat }, dateReference: today.toISOString().split('T')[0], calculs: { CP: { moisTravailles: parseFloat(moisCP.toFixed(2)), acquisitionMensuelle: 25 / 12, acquisitionCumulee: acquisCP, formule: `${moisCP.toFixed(2)} mois × ${(25 / 12).toFixed(2)}j/mois = ${acquisCP}j` }, RTT: { moisTravailles: rttData.moisTravailles, acquisitionMensuelle: rttData.config.acquisitionMensuelle, acquisitionCumulee: rttData.acquisition, totalAnnuel: rttData.config.joursAnnuels, formule: `${rttData.moisTravailles} mois × ${rttData.config.acquisitionMensuelle.toFixed(6)}j/mois = ${rttData.acquisition}j` } } }); } catch (error) { console.error('Erreur testProrata:', error); res.status(500).json({ success: false, message: 'Erreur serveur', error: error.message }); } }); app.post('/api/fixAllCounters', async (req, res) => { const conn = await pool.getConnection(); try { await conn.beginTransaction(); const today = new Date(); const currentYear = today.getFullYear(); const [collaborateurs] = await conn.query('SELECT id, prenom, nom, DateEntree, CampusId FROM CollaborateurAD WHERE (actif = 1 OR actif IS NULL)'); console.log(`🔄 Correction de ${collaborateurs.length} compteurs...`); const corrections = []; for (const collab of collaborateurs) { const dateEntree = collab.DateEntree; const moisCP = getMoisTravaillesCP(today, dateEntree); const acquisCP = calculerAcquisitionCP(today, dateEntree); const rttData = await calculerAcquisitionRTT(conn, collab.id, today); const acquisRTT = rttData.acquisition; const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']); const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']); if (cpType.length > 0) { const [existingCP] = await conn.query(`SELECT Id, Total, Solde, SoldeReporte FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, [collab.id, cpType[0].Id, currentYear]); if (existingCP.length > 0) { const ancienTotal = parseFloat(existingCP[0].Total); const ancienSolde = parseFloat(existingCP[0].Solde); const difference = acquisCP - ancienTotal; const nouveauSolde = Math.max(0, ancienSolde + difference); await conn.query(`UPDATE CompteurConges SET Total = ?, Solde = ?, DerniereMiseAJour = NOW() WHERE Id = ?`, [acquisCP, nouveauSolde, existingCP[0].Id]); corrections.push({ collaborateur: `${collab.prenom} ${collab.nom}`, type: 'CP', ancienTotal: ancienTotal.toFixed(2), nouveauTotal: acquisCP.toFixed(2), ancienSolde: ancienSolde.toFixed(2), nouveauSolde: nouveauSolde.toFixed(2) }); } } if (rttType.length > 0) { const [existingRTT] = await conn.query(`SELECT Id, Total, Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, [collab.id, rttType[0].Id, currentYear]); if (existingRTT.length > 0) { const ancienTotal = parseFloat(existingRTT[0].Total); const ancienSolde = parseFloat(existingRTT[0].Solde); const difference = acquisRTT - ancienTotal; const nouveauSolde = Math.max(0, ancienSolde + difference); await conn.query(`UPDATE CompteurConges SET Total = ?, Solde = ?, DerniereMiseAJour = NOW() WHERE Id = ?`, [acquisRTT, nouveauSolde, existingRTT[0].Id]); corrections.push({ collaborateur: `${collab.prenom} ${collab.nom}`, type: 'RTT', ancienTotal: ancienTotal.toFixed(2), nouveauTotal: acquisRTT.toFixed(2), ancienSolde: ancienSolde.toFixed(2), nouveauSolde: nouveauSolde.toFixed(2) }); } } } await conn.commit(); res.json({ success: true, message: `✅ ${collaborateurs.length} compteurs corrigés`, corrections: corrections }); } catch (error) { await conn.rollback(); console.error('❌ Erreur correction compteurs:', error); res.status(500).json({ success: false, message: 'Erreur', error: error.message }); } finally { conn.release(); } }); app.post('/api/processEndOfYear', async (req, res) => { const conn = await pool.getConnection(); try { const { collaborateur_id } = req.body; await conn.beginTransaction(); let result; if (collaborateur_id) { result = await processEndOfYearRTT(conn, collaborateur_id); } else { const [collaborateurs] = await conn.query('SELECT id, CampusId FROM CollaborateurAD'); const results = []; for (const c of collaborateurs) { const r = await processEndOfYearRTT(conn, c.id); if (r) results.push({ collaborateur_id: c.id, ...r }); } result = results; } await conn.commit(); res.json({ success: true, message: 'Traitement de fin d\'année effectué', result }); } catch (error) { await conn.rollback(); res.status(500).json({ success: false, message: 'Erreur', error: error.message }); } finally { conn.release(); } }); app.post('/api/processEndOfExercice', async (req, res) => { const conn = await pool.getConnection(); try { const { collaborateur_id } = req.body; await conn.beginTransaction(); let result; if (collaborateur_id) { result = await processEndOfExerciceCP(conn, collaborateur_id); } else { const [collaborateurs] = await conn.query('SELECT id, CampusId FROM CollaborateurAD'); const results = []; for (const c of collaborateurs) { const r = await processEndOfExerciceCP(conn, c.id); if (r) results.push({ collaborateur_id: c.id, ...r }); } result = results; } await conn.commit(); res.json({ success: true, message: 'Traitement de fin d\'exercice CP effectué', result }); } catch (error) { await conn.rollback(); res.status(500).json({ success: false, message: 'Erreur', error: error.message }); } finally { conn.release(); } }); app.get('/api/getAcquisitionDetails', async (req, res) => { try { const today = new Date(); const details = { date_reference: today.toISOString().split('T')[0], CP: { exercice: getExerciceCP(today), mois_travailles: getMoisTravaillesCP(today), acquisition_mensuelle: LEAVE_RULES.CP.acquisitionMensuelle, acquisition_cumulee: calculerAcquisitionCumulee('CP', today), total_annuel: LEAVE_RULES.CP.joursAnnuels, periode: '01/06 - 31/05', reportable: LEAVE_RULES.CP.reportable }, RTT: { annee: today.getFullYear(), mois_travailles: getMoisTravaillesRTT(today), acquisition_mensuelle: LEAVE_RULES.RTT.acquisitionMensuelle, acquisition_cumulee: calculerAcquisitionCumulee('RTT', today), total_annuel: LEAVE_RULES.RTT.joursAnnuels, periode: '01/01 - 31/12', reportable: LEAVE_RULES.RTT.reportable } }; res.json({ success: true, details }); } catch (error) { res.status(500).json({ success: false, message: 'Erreur', error: error.message }); } }); app.get('/api/getLeaveCounters', async (req, res) => { try { const userId = parseInt(req.query.user_id || 0); const data = {}; if (userId > 0) { const [rows] = await pool.query(`SELECT tc.Nom, cc.Annee, cc.Solde, cc.Total, cc.SoldeReporte FROM CompteurConges cc JOIN TypeConge tc ON cc.TypeCongeId = tc.Id WHERE cc.CollaborateurADId = ?`, [userId]); rows.forEach(row => { data[row.Nom] = { Annee: row.Annee, Solde: parseFloat(row.Solde), Total: parseFloat(row.Total), SoldeReporte: parseFloat(row.SoldeReporte) }; }); } res.json({ success: true, message: 'Compteurs récupérés', counters: data }); } catch (error) { res.status(500).json({ success: false, message: 'Erreur', error: error.message }); } }); app.get('/api/getEmploye', async (req, res) => { try { const id = parseInt(req.query.id || 0); if (id <= 0) return res.json({ success: false, message: 'ID invalide' }); const conn = await pool.getConnection(); // 1️⃣ Récupérer les infos du collaborateur const [rows] = await conn.query(` SELECT ca.id, ca.Nom, ca.Prenom, ca.Email, ca.role, ca.TypeContrat, ca.DateEntree, ca.CampusId, ca.SocieteId, s.Nom as service, so.Nom as societe_nom FROM CollaborateurAD ca LEFT JOIN Services s ON ca.ServiceId = s.Id LEFT JOIN Societe so ON ca.SocieteId = so.Id WHERE ca.id = ? `, [id]); if (rows.length === 0) { conn.release(); return res.json({ success: false, message: 'Collaborateur non trouvé' }); } const employee = rows[0]; // 2️⃣ Déterminer si c'est un apprenti const isApprenti = normalizeRole(employee.role) === 'apprenti'; // 3️⃣ Récupérer les compteurs CP const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']); let cpTotal = 0, cpSolde = 0; if (cpType.length > 0) { const currentYear = new Date().getFullYear(); const previousYear = currentYear - 1; // CP N-1 (reporté) const [cpN1] = await conn.query(` SELECT SoldeReporte FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? `, [id, cpType[0].Id, previousYear]); // CP N (année courante) const [cpN] = await conn.query(` SELECT Solde, SoldeReporte FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? `, [id, cpType[0].Id, currentYear]); const cpN1Solde = cpN1.length > 0 ? parseFloat(cpN1[0].SoldeReporte || 0) : 0; const cpNSolde = cpN.length > 0 ? parseFloat(cpN[0].Solde || 0) : 0; const cpNReporte = cpN.length > 0 ? parseFloat(cpN[0].SoldeReporte || 0) : 0; cpTotal = cpN1Solde + (cpNSolde - cpNReporte); cpSolde = cpTotal; } // 4️⃣ Récupérer les compteurs RTT (sauf pour apprentis) let rttTotal = 0, rttSolde = 0; if (!isApprenti) { const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']); if (rttType.length > 0) { const currentYear = new Date().getFullYear(); const [rttN] = await conn.query(` SELECT Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? `, [id, rttType[0].Id, currentYear]); rttTotal = rttN.length > 0 ? parseFloat(rttN[0].Solde || 0) : 0; rttSolde = rttTotal; } } conn.release(); // 5️⃣ Retourner les données complètes res.json({ success: true, employee: { id: employee.id, Nom: employee.Nom, Prenom: employee.Prenom, Email: employee.Email, role: employee.role, TypeContrat: employee.TypeContrat, DateEntree: employee.DateEntree, CampusId: employee.CampusId, SocieteId: employee.SocieteId, service: employee.service, societe_nom: employee.societe_nom, conges_restants: parseFloat(cpSolde.toFixed(2)), rtt_restants: parseFloat(rttSolde.toFixed(2)) } }); } catch (error) { console.error('❌ Erreur getEmploye:', error); res.status(500).json({ success: false, message: 'Erreur DB', error: error.message }); } }); app.get('/api/getEmployeRequest', async (req, res) => { try { const id = parseInt(req.query.id || 0); if (id <= 0) return res.json({ success: false, message: 'ID invalide' }); const [rows] = await pool.query(` SELECT dc.Id, dc.DateDebut, dc.DateFin, dc.NombreJours as days, dc.Statut as status, dc.DateDemande, GROUP_CONCAT(DISTINCT tc.Nom ORDER BY tc.Nom SEPARATOR ', ') AS type, CONCAT( DATE_FORMAT(dc.DateDebut, '%d/%m/%Y'), IF(dc.DateDebut = dc.DateFin, '', CONCAT(' - ', DATE_FORMAT(dc.DateFin, '%d/%m/%Y'))) ) as date_display FROM DemandeConge dc LEFT JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId LEFT JOIN TypeConge tc ON dct.TypeCongeId = tc.Id WHERE dc.CollaborateurADId = ? GROUP BY dc.Id, dc.DateDebut, dc.DateFin, dc.NombreJours, dc.Statut, dc.DateDemande ORDER BY dc.DateDemande DESC `, [id]); res.json({ success: true, requests: rows }); } catch (error) { console.error('❌ Erreur getEmployeRequest:', error); res.status(500).json({ success: false, message: 'Erreur DB', error: error.message }); } }); app.get('/api/getRequests', async (req, res) => { try { const userId = req.query.user_id; if (!userId) return res.json({ success: false, message: 'ID utilisateur manquant' }); // ✅ REQUÊTE CORRIGÉE avec la table de liaison const [rows] = await pool.query(` SELECT dc.Id, dc.DateDebut, dc.DateFin, dc.Statut, dc.DateDemande, dc.Commentaire, dc.CommentaireValidation, dc.Validateur, dc.DocumentJoint, GROUP_CONCAT(DISTINCT tc.Nom ORDER BY tc.Nom SEPARATOR ', ') AS TypeConges, SUM(dct.NombreJours) as NombreJoursTotal FROM DemandeConge dc LEFT JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId LEFT JOIN TypeConge tc ON dct.TypeCongeId = tc.Id WHERE (dc.EmployeeId = ? OR dc.CollaborateurADId = ?) GROUP BY dc.Id, dc.DateDebut, dc.DateFin, dc.Statut, dc.DateDemande, dc.Commentaire, dc.CommentaireValidation, dc.Validateur, dc.DocumentJoint ORDER BY dc.DateDemande DESC `, [userId, userId]); const requests = rows.map(row => { const workingDays = getWorkingDays(row.DateDebut, row.DateFin); const dateDisplay = row.DateDebut === row.DateFin ? formatDate(row.DateDebut) : `${formatDate(row.DateDebut)} - ${formatDate(row.DateFin)}`; let fileUrl = null; if (row.TypeConges && row.TypeConges.includes('Congé maladie') && row.DocumentJoint) { fileUrl = `/uploads/${path.basename(row.DocumentJoint)}`; } return { id: row.Id, type: row.TypeConges || 'Non défini', // ✅ Gérer le cas null startDate: row.DateDebut, endDate: row.DateFin, dateDisplay, days: row.NombreJoursTotal || workingDays, // ✅ Utiliser le total de la table de liaison status: row.Statut, reason: row.Commentaire || 'Aucun commentaire', submittedAt: row.DateDemande, submittedDisplay: formatDate(row.DateDemande), validator: row.Validateur || null, validationComment: row.CommentaireValidation || null, fileUrl }; }); res.json({ success: true, message: 'Demandes récupérées', requests, total: requests.length }); } catch (error) { console.error('❌ Erreur getRequests:', error); res.status(500).json({ success: false, message: 'Erreur', error: error.message }); } }); app.get('/api/getAllTeamRequests', async (req, res) => { try { const managerId = req.query.SuperieurId; if (!managerId) return res.json({ success: false, message: 'Paramètre SuperieurId manquant' }); const [rows] = await pool.query(`SELECT dc.Id, dc.DateDebut, dc.DateFin, dc.Statut, dc.DateDemande, dc.Commentaire, dc.DocumentJoint, dc.CollaborateurADId AS employee_id, CONCAT(ca.Prenom, ' ', ca.Nom) as employee_name, ca.Email as employee_email, tc.Nom as type FROM DemandeConge dc JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id JOIN TypeConge tc ON dc.TypeCongeId = tc.Id JOIN HierarchieValidationAD hv ON hv.CollaborateurId = ca.id WHERE hv.SuperieurId = ? ORDER BY dc.DateDemande DESC`, [managerId]); const requests = rows.map(row => ({ id: row.Id, employee_id: row.employee_id, employee_name: row.employee_name, employee_email: row.employee_email, type: row.type, start_date: row.DateDebut, end_date: row.DateFin, date_display: row.DateDebut === row.DateFin ? formatDate(row.DateDebut) : `${formatDate(row.DateDebut)} - ${formatDate(row.DateFin)}`, days: getWorkingDays(row.DateDebut, row.DateFin), status: row.Statut, reason: row.Commentaire || '', file: row.DocumentJoint || null, submitted_at: row.DateDemande, submitted_display: formatDate(row.DateDemande) })); res.json({ success: true, requests }); } catch (error) { res.status(500).json({ success: false, message: 'Erreur DB', error: error.message }); } }); app.get('/api/getPendingRequests', async (req, res) => { try { const managerId = req.query.manager_id; if (!managerId) return res.json({ success: false, message: 'ID manager manquant' }); const [managerRows] = await pool.query('SELECT ServiceId, CampusId FROM CollaborateurAD WHERE id = ?', [managerId]); if (managerRows.length === 0) return res.json({ success: false, message: 'Manager non trouvé' }); const serviceId = managerRows[0].ServiceId; const [rows] = await pool.query(`SELECT dc.Id, dc.DateDebut, dc.DateFin, dc.Statut, dc.DateDemande, dc.Commentaire, dc.CollaborateurADId, CONCAT(ca.prenom, ' ', ca.nom) as employee_name, ca.email as employee_email, GROUP_CONCAT(tc.Nom ORDER BY tc.Nom SEPARATOR ', ') as types FROM DemandeConge dc JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id JOIN TypeConge tc ON FIND_IN_SET(tc.Id, dc.TypeCongeId) WHERE ca.ServiceId = ? AND dc.Statut = 'En attente' AND ca.id != ? GROUP BY dc.Id, dc.DateDebut, dc.DateFin, dc.Statut, dc.DateDemande, dc.Commentaire, dc.CollaborateurADId, ca.prenom, ca.nom, ca.email ORDER BY dc.DateDemande ASC`, [serviceId, managerId]); const requests = rows.map(row => ({ id: row.Id, employee_id: row.CollaborateurADId, employee_name: row.employee_name, employee_email: row.employee_email, type: row.types, start_date: row.DateDebut, end_date: row.DateFin, date_display: row.DateDebut === row.DateFin ? formatDate(row.DateDebut) : `${formatDate(row.DateDebut)} - ${formatDate(row.DateFin)}`, days: getWorkingDays(row.DateDebut, row.DateFin), status: row.Statut, reason: row.Commentaire || '', submitted_at: row.DateDemande, submitted_display: formatDate(row.DateDemande) })); res.json({ success: true, message: 'Demandes récupérées', requests, service_id: serviceId }); } catch (error) { res.status(500).json({ success: false, message: 'Erreur', error: error.message }); } }); app.get('/api/getTeamMembers', async (req, res) => { try { const managerId = req.query.manager_id; if (!managerId) return res.json({ success: false, message: 'ID manager manquant' }); const [managerRows] = await pool.query('SELECT ServiceId, CampusId FROM CollaborateurAD WHERE id = ?', [managerId]); if (managerRows.length === 0) return res.json({ success: false, message: 'Manager non trouvé' }); const serviceId = managerRows[0].ServiceId; const [members] = await pool.query(`SELECT c.id, c.nom, c.prenom, c.email, c.role, s.Nom as service_name, c.CampusId FROM CollaborateurAD c JOIN Services s ON c.ServiceId = s.Id WHERE c.ServiceId = ? AND c.id != ? ORDER BY c.prenom, c.nom`, [serviceId, managerId]); res.json({ success: true, message: 'Équipe récupérée', team_members: members, service_id: serviceId }); } catch (error) { res.status(500).json({ success: false, message: 'Erreur', error: error.message }); } }); app.get('/api/getNotifications', async (req, res) => { try { const userIdParam = req.query.user_id; if (!userIdParam) { return res.json({ success: false, message: 'ID utilisateur manquant' }); } const conn = await pool.getConnection(); // ✅ Déterminer si c'est un UUID ou un ID numérique const isUUID = userIdParam.length > 10 && userIdParam.includes('-'); // ✅ Récupérer l'ID numérique si on a un UUID let userId = userIdParam; if (isUUID) { const [userRows] = await conn.query( 'SELECT id, CampusId FROM CollaborateurAD WHERE entraUserId = ? AND (Actif = 1 OR Actif IS NULL)', [userIdParam] ); if (userRows.length === 0) { conn.release(); return res.json({ success: false, message: 'Utilisateur non trouvé ou compte désactivé' }); } userId = userRows[0].id; } else { userId = parseInt(userIdParam); } // ✅ Utiliser l'ID numérique pour la requête const [notifications] = await conn.query(` SELECT * FROM Notifications WHERE CollaborateurADId = ? ORDER BY DateCreation DESC LIMIT 50 `, [userId]); conn.release(); res.json({ success: true, notifications: notifications || [] }); } catch (error) { console.error('Erreur getNotifications:', error); res.status(500).json({ success: false, message: 'Erreur serveur', error: error.message }); } }); app.post('/api/markNotificationRead', async (req, res) => { try { const { notificationId } = req.body; if (!notificationId || notificationId <= 0) return res.status(400).json({ success: false, message: 'ID notification invalide' }); await pool.query('UPDATE Notifications SET lu = 1 WHERE Id = ?', [notificationId]); res.json({ success: true, message: 'Notification marquée comme lue' }); } catch (error) { res.status(500).json({ success: false, message: 'Erreur', error: error.message }); } }); // À ajouter avant app.listen() /** * POST /saisirRecupJour * Saisir une journée de récupération (samedi travaillé) */ app.post('/api/saisirRecupJour', async (req, res) => { const conn = await pool.getConnection(); try { await conn.beginTransaction(); const { user_id, date, // Date du samedi travaillé nombre_heures = 1, // Par défaut 1 jour = 1 samedi commentaire } = req.body; console.log('\n📝 === SAISIE RÉCUP ==='); console.log('User ID:', user_id); console.log('Date:', date); console.log('Heures:', nombre_heures); if (!user_id || !date) { await conn.rollback(); conn.release(); return res.json({ success: false, message: 'Données manquantes' }); } // Vérifier que c'est bien un samedi const dateObj = new Date(date); const dayOfWeek = dateObj.getDay(); if (dayOfWeek !== 6) { await conn.rollback(); conn.release(); return res.json({ success: false, message: 'La récupération ne peut être saisie que pour un samedi' }); } // Vérifier que ce samedi n'a pas déjà été saisi const [existing] = await conn.query(` SELECT dc.Id FROM DemandeConge dc JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId JOIN TypeConge tc ON dct.TypeCongeId = tc.Id WHERE dc.CollaborateurADId = ? AND dc.DateDebut = ? AND tc.Nom = 'Récupération' `, [user_id, date]); if (existing.length > 0) { await conn.rollback(); conn.release(); return res.json({ success: false, message: 'Ce samedi a déjà été déclaré' }); } // Récupérer infos utilisateur const [userInfo] = await conn.query( 'SELECT prenom, nom, email, CampusId FROM CollaborateurAD WHERE id = ?', [user_id] ); if (userInfo.length === 0) { await conn.rollback(); conn.release(); return res.json({ success: false, message: 'Utilisateur non trouvé' }); } const user = userInfo[0]; const userName = `${user.prenom} ${user.nom}`; const dateFormatted = dateObj.toLocaleDateString('fr-FR'); // Récupérer le type Récupération const [recupType] = await conn.query( 'SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Récupération'] ); if (recupType.length === 0) { await conn.rollback(); conn.release(); return res.json({ success: false, message: 'Type Récupération non trouvé' }); } const recupTypeId = recupType[0].Id; const currentYear = dateObj.getFullYear(); // CRÉER LA DEMANDE (validée automatiquement) const [result] = await conn.query(` INSERT INTO DemandeConge (CollaborateurADId, DateDebut, DateFin, TypeCongeId, Statut, DateDemande, Commentaire, NombreJours) VALUES (?, ?, ?, ?, 'Validée', NOW(), ?, ?) `, [user_id, date, date, recupTypeId, commentaire || `Samedi travaillé - ${dateFormatted}`, nombre_heures]); const demandeId = result.insertId; // SAUVEGARDER DANS DemandeCongeType await conn.query(` INSERT INTO DemandeCongeType (DemandeCongeId, TypeCongeId, NombreJours, PeriodeJournee) VALUES (?, ?, ?, 'Journée entière') `, [demandeId, recupTypeId, nombre_heures]); // ACCUMULER DANS LE COMPTEUR const [compteur] = await conn.query(` SELECT Id, Total, Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? `, [user_id, recupTypeId, currentYear]); if (compteur.length > 0) { await conn.query(` UPDATE CompteurConges SET Total = Total + ?, Solde = Solde + ?, DerniereMiseAJour = NOW() WHERE Id = ? `, [nombre_heures, nombre_heures, compteur[0].Id]); console.log(`✅ Compteur mis à jour: ${parseFloat(compteur[0].Solde) + nombre_heures}j`); } else { await conn.query(` INSERT INTO CompteurConges (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour) VALUES (?, ?, ?, ?, ?, 0, NOW()) `, [user_id, recupTypeId, currentYear, nombre_heures, nombre_heures]); console.log(`✅ Compteur créé: ${nombre_heures}j`); } // ENREGISTRER L'ACCUMULATION await conn.query(` INSERT INTO DeductionDetails (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) VALUES (?, ?, ?, 'Accum Récup', ?) `, [demandeId, recupTypeId, currentYear, nombre_heures]); // CRÉER NOTIFICATION await conn.query(` INSERT INTO Notifications (CollaborateurADId, Type, Titre, Message, DemandeCongeId, DateCreation, Lu) VALUES (?, 'Success', '✅ Récupération enregistrée', ?, ?, NOW(), 0) `, [ user_id, `Samedi ${dateFormatted} enregistré : +${nombre_heures}j de récupération`, demandeId ]); await conn.commit(); conn.release(); res.json({ success: true, message: `Samedi ${dateFormatted} enregistré`, jours_ajoutes: nombre_heures, demande_id: demandeId }); } catch (error) { await conn.rollback(); if (conn) conn.release(); console.error('❌ Erreur saisie récup:', error); res.status(500).json({ success: false, message: 'Erreur serveur', error: error.message }); } }); /** * GET /getMesSamedis * Récupérer les samedis déjà déclarés */ app.get('/api/getMesSamedis', async (req, res) => { try { const { user_id, annee } = req.query; const conn = await pool.getConnection(); const [samedis] = await conn.query(` SELECT dc.Id, DATE_FORMAT(dc.DateDebut, '%Y-%m-%d') as date, dc.NombreJours as jours, dc.Commentaire as commentaire, DATE_FORMAT(dc.DateDemande, '%d/%m/%Y à %H:%i') as date_saisie FROM DemandeConge dc JOIN TypeConge tc ON dc.TypeCongeId = tc.Id WHERE dc.CollaborateurADId = ? AND tc.Nom = 'Récupération' AND YEAR(dc.DateDebut) = ? ORDER BY dc.DateDebut DESC `, [user_id, annee]); conn.release(); res.json({ success: true, samedis: samedis }); } catch (error) { console.error('Erreur getMesSamedis:', error); res.status(500).json({ success: false, message: 'Erreur serveur', error: error.message }); } }); async function checkLeaveBalanceWithAnticipation(conn, collaborateurId, repartition, dateDebut) { const dateDebutObj = new Date(dateDebut); const currentYear = dateDebutObj.getFullYear(); const previousYear = currentYear - 1; console.log('\n🔍 === CHECK SOLDES AVEC ANTICIPATION ==='); console.log(`📅 Date demande: ${dateDebut}`); console.log(`📅 Année demande: ${currentYear}`); const verification = []; for (const rep of repartition) { const typeCode = rep.TypeConge; const joursNecessaires = parseFloat(rep.NombreJours || 0); if (typeCode === 'ABS' || typeCode === 'Formation') { continue; } const typeName = typeCode === 'CP' ? 'Congé payé' : typeCode === 'RTT' ? 'RTT' : typeCode; const [typeRow] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', [typeName]); if (typeRow.length === 0) { continue; } const typeCongeId = typeRow[0].Id; // ==================================== // 1️⃣ Récupérer les infos du collaborateur // ==================================== const [collabInfo] = await conn.query(` SELECT DateEntree, TypeContrat, role FROM CollaborateurAD WHERE id = ? `, [collaborateurId]); const dateEntree = collabInfo[0]?.DateEntree || null; const typeContrat = collabInfo[0]?.TypeContrat || '37h'; const isApprenti = collabInfo[0]?.role === 'Apprenti'; // ==================================== // 2️⃣ Calculer l'acquisition à la date de la demande // ==================================== let acquisALaDate = 0; let budgetAnnuel = 0; if (typeCode === 'CP') { acquisALaDate = calculerAcquisitionCP(dateDebutObj, dateEntree); budgetAnnuel = 25; console.log(`💰 Acquisition CP à la date ${dateDebut}: ${acquisALaDate.toFixed(2)}j`); } else if (typeCode === 'RTT' && !isApprenti) { const rttData = await calculerAcquisitionRTT(conn, collaborateurId, dateDebutObj); acquisALaDate = rttData.acquisition; budgetAnnuel = rttData.config.joursAnnuels; console.log(`💰 Acquisition RTT à la date ${dateDebut}: ${acquisALaDate.toFixed(2)}j`); } // ==================================== // 3️⃣ Récupérer le report N-1 (CP uniquement) // ==================================== let reporteN1 = 0; if (typeCode === 'CP') { const [compteurN1] = await conn.query(` SELECT Solde, SoldeReporte FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? `, [collaborateurId, typeCongeId, previousYear]); if (compteurN1.length > 0) { reporteN1 = parseFloat(compteurN1[0].Solde || 0); } } // ==================================== // 4️⃣ Calculer ce qui a déjà été posé (SANS l'anticipé) // ==================================== const [dejaPose] = await conn.query(` SELECT COALESCE(SUM(dd.JoursUtilises), 0) as total FROM DeductionDetails dd JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id WHERE dc.CollaborateurADId = ? AND dd.TypeCongeId = ? AND dd.Annee = ? AND dd.TypeDeduction NOT IN ('N Anticip', 'N+1 Anticip', 'Accum Récup', 'Accum Recup') AND dc.Statut != 'Refusée' `, [collaborateurId, typeCongeId, currentYear]); const dejaPoseNormal = parseFloat(dejaPose[0]?.total || 0); // ==================================== // 5️⃣ Calculer l'anticipé déjà utilisé // ==================================== const [anticipeUtilise] = await conn.query(` SELECT COALESCE(SUM(dd.JoursUtilises), 0) as total FROM DeductionDetails dd JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id WHERE dc.CollaborateurADId = ? AND dd.TypeCongeId = ? AND dd.Annee = ? AND dd.TypeDeduction = 'N Anticip' AND dc.Statut != 'Refusée' `, [collaborateurId, typeCongeId, currentYear]); const dejaPoseAnticipe = parseFloat(anticipeUtilise[0]?.total || 0); // ==================================== // 6️⃣ Calculer l'anticipé disponible // ==================================== const anticipableMax = Math.max(0, budgetAnnuel - acquisALaDate); const anticipeDisponible = Math.max(0, anticipableMax - dejaPoseAnticipe); console.log(`💳 Anticipé max possible: ${anticipableMax.toFixed(2)}j`); console.log(`💳 Anticipé déjà utilisé: ${dejaPoseAnticipe.toFixed(2)}j`); console.log(`💳 Anticipé disponible: ${anticipeDisponible.toFixed(2)}j`); // ==================================== // 7️⃣ Calculer le solde TOTAL disponible // ==================================== const soldeActuel = Math.max(0, reporteN1 + acquisALaDate - dejaPoseNormal); const soldeTotal = soldeActuel + anticipeDisponible; console.log(`📊 Soldes détaillés ${typeCode}:`); console.log(` - Report N-1: ${reporteN1.toFixed(2)}j`); console.log(` - Acquis à date: ${acquisALaDate.toFixed(2)}j`); console.log(` - Déjà posé (normal): ${dejaPoseNormal.toFixed(2)}j`); console.log(` - Solde actuel: ${soldeActuel.toFixed(2)}j`); console.log(` - Anticipé disponible: ${anticipeDisponible.toFixed(2)}j`); console.log(` ✅ TOTAL DISPONIBLE: ${soldeTotal.toFixed(2)}j`); // ==================================== // 8️⃣ Vérifier la suffisance // ==================================== const suffisant = soldeTotal >= joursNecessaires; const deficit = Math.max(0, joursNecessaires - soldeTotal); verification.push({ type: typeName, joursNecessaires, reporteN1, acquisALaDate, dejaPoseNormal, dejaPoseAnticipe, soldeActuel, anticipeDisponible, soldeTotal, suffisant, deficit }); console.log(`🔍 Vérification ${typeCode}: ${joursNecessaires}j demandés vs ${soldeTotal.toFixed(2)}j disponibles → ${suffisant ? '✅ OK' : '❌ INSUFFISANT'}`); } const insuffisants = verification.filter(v => !v.suffisant); return { valide: insuffisants.length === 0, details: verification, insuffisants }; } /** * Déduit les jours d'un compteur avec gestion de l'anticipation * Ordre de déduction : N-1 → N → N Anticip */ async function deductLeaveBalanceWithAnticipation(conn, collaborateurId, typeCongeId, nombreJours, demandeCongeId, dateDebut) { const dateDebutObj = new Date(dateDebut); const currentYear = dateDebutObj.getFullYear(); const previousYear = currentYear - 1; let joursRestants = nombreJours; const deductions = []; console.log(`\n💳 === DÉDUCTION AVEC ANTICIPATION ===`); console.log(` Collaborateur: ${collaborateurId}`); console.log(` Type congé: ${typeCongeId}`); console.log(` Jours à déduire: ${nombreJours}j`); console.log(` Date début: ${dateDebut}`); // ==================================== // 1️⃣ Déduire du REPORT N-1 (CP uniquement) // ==================================== const [compteurN1] = await conn.query(` SELECT Id, Solde, SoldeReporte FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? `, [collaborateurId, typeCongeId, previousYear]); if (compteurN1.length > 0) { const soldeN1 = parseFloat(compteurN1[0].Solde || 0); const aDeduireN1 = Math.min(soldeN1, joursRestants); if (aDeduireN1 > 0) { await conn.query(` UPDATE CompteurConges SET Solde = GREATEST(0, Solde - ?), SoldeReporte = GREATEST(0, SoldeReporte - ?), DerniereMiseAJour = NOW() WHERE Id = ? `, [aDeduireN1, aDeduireN1, compteurN1[0].Id]); await conn.query(` INSERT INTO DeductionDetails (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) VALUES (?, ?, ?, 'Année N-1', ?) `, [demandeCongeId, typeCongeId, previousYear, aDeduireN1]); deductions.push({ annee: previousYear, type: 'Report N-1', joursUtilises: aDeduireN1, soldeAvant: soldeN1 }); joursRestants -= aDeduireN1; console.log(` ✓ Déduit ${aDeduireN1.toFixed(2)}j du report N-1`); } } // ==================================== // 2️⃣ Déduire du SOLDE N (acquis actuel) // ==================================== if (joursRestants > 0) { const [compteurN] = await conn.query(` SELECT Id, Solde, SoldeReporte, Total FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? `, [collaborateurId, typeCongeId, currentYear]); if (compteurN.length > 0) { const soldeTotal = parseFloat(compteurN[0].Solde || 0); const soldeReporte = parseFloat(compteurN[0].SoldeReporte || 0); const soldeN = Math.max(0, soldeTotal - soldeReporte); // Solde actuel sans le report const aDeduireN = Math.min(soldeN, joursRestants); if (aDeduireN > 0) { await conn.query(` UPDATE CompteurConges SET Solde = GREATEST(0, Solde - ?), DerniereMiseAJour = NOW() WHERE Id = ? `, [aDeduireN, compteurN[0].Id]); await conn.query(` INSERT INTO DeductionDetails (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) VALUES (?, ?, ?, 'Année N', ?) `, [demandeCongeId, typeCongeId, currentYear, aDeduireN]); deductions.push({ annee: currentYear, type: 'Année N', joursUtilises: aDeduireN, soldeAvant: soldeN }); joursRestants -= aDeduireN; console.log(` ✓ Déduit ${aDeduireN.toFixed(2)}j du solde N actuel`); } } } // ==================================== // 3️⃣ Déduire de l'ANTICIPÉ N (ce qui reste à acquérir) // ==================================== if (joursRestants > 0) { console.log(` 💳 Il reste ${joursRestants.toFixed(2)}j à déduire → Utilisation de l'anticipé`); // Récupérer les infos pour calculer l'anticipé disponible const [collabInfo] = await conn.query(` SELECT DateEntree, TypeContrat, role FROM CollaborateurAD WHERE id = ? `, [collaborateurId]); const dateEntree = collabInfo[0]?.DateEntree || null; const typeContrat = collabInfo[0]?.TypeContrat || '37h'; const isApprenti = collabInfo[0]?.role === 'Apprenti'; // Déterminer le type de congé const [typeInfo] = await conn.query('SELECT Nom FROM TypeConge WHERE Id = ?', [typeCongeId]); const typeNom = typeInfo[0]?.Nom || ''; let acquisALaDate = 0; let budgetAnnuel = 0; if (typeNom === 'Congé payé') { acquisALaDate = calculerAcquisitionCP(dateDebutObj, dateEntree); budgetAnnuel = 25; } else if (typeNom === 'RTT' && !isApprenti) { const rttData = await calculerAcquisitionRTT(conn, collaborateurId, dateDebutObj); acquisALaDate = rttData.acquisition; budgetAnnuel = rttData.config.joursAnnuels; } // Calculer l'anticipé disponible const anticipableMax = Math.max(0, budgetAnnuel - acquisALaDate); // Vérifier combien a déjà été pris en anticipé const [anticipeUtilise] = await conn.query(` SELECT COALESCE(SUM(dd.JoursUtilises), 0) as total FROM DeductionDetails dd JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id WHERE dc.CollaborateurADId = ? AND dd.TypeCongeId = ? AND dd.Annee = ? AND dd.TypeDeduction = 'N Anticip' AND dc.Statut != 'Refusée' AND dc.Id != ? `, [collaborateurId, typeCongeId, currentYear, demandeCongeId]); const dejaPrisAnticipe = parseFloat(anticipeUtilise[0]?.total || 0); const anticipeDisponible = Math.max(0, anticipableMax - dejaPrisAnticipe); console.log(` 💳 Anticipé max: ${anticipableMax.toFixed(2)}j`); console.log(` 💳 Déjà pris: ${dejaPrisAnticipe.toFixed(2)}j`); console.log(` 💳 Disponible: ${anticipeDisponible.toFixed(2)}j`); const aDeduireAnticipe = Math.min(anticipeDisponible, joursRestants); if (aDeduireAnticipe > 0) { // Enregistrer la déduction anticipée await conn.query(` INSERT INTO DeductionDetails (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) VALUES (?, ?, ?, 'N Anticip', ?) `, [demandeCongeId, typeCongeId, currentYear, aDeduireAnticipe]); // Mettre à jour SoldeAnticipe dans CompteurConges await conn.query(` UPDATE CompteurConges SET SoldeAnticipe = GREATEST(0, ? - ?), DerniereMiseAJour = NOW() WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? `, [anticipeDisponible, aDeduireAnticipe, collaborateurId, typeCongeId, currentYear]); deductions.push({ annee: currentYear, type: 'N Anticip', joursUtilises: aDeduireAnticipe, soldeAvant: anticipeDisponible }); joursRestants -= aDeduireAnticipe; console.log(` ✓ Déduit ${aDeduireAnticipe.toFixed(2)}j de l'anticipé N`); } else if (joursRestants > 0) { console.error(` ❌ Impossible de déduire ${joursRestants.toFixed(2)}j : anticipé épuisé !`); } } console.log(` ✅ Déduction terminée - Total déduit: ${(nombreJours - joursRestants).toFixed(2)}j\n`); return { success: joursRestants === 0, joursDeduitsTotal: nombreJours - joursRestants, joursNonDeduits: joursRestants, details: deductions }; } app.post('/api/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), async (req, res) => { const conn = await pool.getConnection(); try { await conn.beginTransaction(); const currentYear = new Date().getFullYear(); // ✅ Récupérer les fichiers uploadés const uploadedFiles = req.files || []; console.log('📎 Fichiers médicaux reçus:', uploadedFiles.length); // ✅ Les données arrivent différemment avec FormData const { DateDebut, DateFin, NombreJours, Email, Nom, Commentaire, statut } = req.body; // ✅ Parser la répartition (elle arrive en string depuis FormData) const Repartition = JSON.parse(req.body.Repartition || '[]'); if (!DateDebut || !DateFin || !Repartition || !Email || !Nom) { uploadedFiles.forEach(file => { if (fs.existsSync(file.path)) { fs.unlinkSync(file.path); } }); return res.json({ success: false, message: 'Données manquantes' }); } // ✅ Validation : Si arrêt maladie, il faut au moins 1 fichier const hasABS = Repartition.some(r => r.TypeConge === 'ABS'); if (hasABS && uploadedFiles.length === 0) { await conn.rollback(); conn.release(); return res.json({ success: false, message: 'Un justificatif médical est obligatoire pour un arrêt maladie' }); } // ⭐ VALIDATION DE LA RÉPARTITION console.log('\n📥 === SOUMISSION DEMANDE CONGÉ ==='); console.log('Email:', Email); console.log('Période:', DateDebut, '→', DateFin); console.log('Nombre de jours total:', NombreJours); console.log('Répartition reçue:', JSON.stringify(Repartition, null, 2)); // ⭐ Ne compter que CP, RTT ET RÉCUP dans la répartition const sommeRepartition = Repartition.reduce((sum, r) => { if (r.TypeConge === 'CP' || r.TypeConge === 'RTT' || r.TypeConge === 'Récup') { return sum + parseFloat(r.NombreJours || 0); } return sum; }, 0); console.log('Somme répartition CP+RTT+Récup:', sommeRepartition.toFixed(2)); // ⭐ VALIDATION : La somme doit correspondre au total const hasCountableLeave = Repartition.some(r => r.TypeConge === 'CP' || r.TypeConge === 'RTT' || r.TypeConge === 'Récup' ); if (hasCountableLeave && Math.abs(sommeRepartition - NombreJours) > 0.01) { console.error('❌ ERREUR : Répartition incohérente !'); uploadedFiles.forEach(file => { if (fs.existsSync(file.path)) { fs.unlinkSync(file.path); } }); await conn.rollback(); conn.release(); return res.json({ success: false, message: `Erreur de répartition : la somme (${sommeRepartition.toFixed(2)}j) ne correspond pas au total (${NombreJours}j)` }); } console.log('✅ Validation répartition OK'); // ⭐ Récup n'est PAS une demande auto-validée const isFormationOnly = Repartition.length === 1 && Repartition[0].TypeConge === 'Formation'; const statutDemande = statut || (isFormationOnly ? 'Validée' : 'En attente'); console.log('🔍 Type de demande:', { isFormationOnly, statut: statutDemande }); const [collabAD] = await conn.query('SELECT id, CampusId FROM CollaborateurAD WHERE email = ? LIMIT 1', [Email]); const isAD = collabAD.length > 0; const collaborateurId = isAD ? collabAD[0].id : null; let employeeId = null; if (!isAD) { const [user] = await conn.query('SELECT ID FROM Users WHERE Email = ? LIMIT 1', [Email]); if (user.length === 0) { uploadedFiles.forEach(file => { if (fs.existsSync(file.path)) { fs.unlinkSync(file.path); } }); await conn.rollback(); conn.release(); return res.json({ success: false, message: 'Utilisateur non trouvé' }); } employeeId = user[0].ID; } // ======================================== // ÉTAPE 1 : Vérification des soldes AVANT tout (MODE MIXTE AVEC ANTICIPATION N+1) // ======================================== if (isAD && collaborateurId && !isFormationOnly) { console.log('\n🔍 Vérification des soldes en mode mixte avec anticipation...'); console.log('Date début:', DateDebut); const [userRole] = await conn.query('SELECT role FROM CollaborateurAD WHERE id = ?', [collaborateurId]); const isApprenti = userRole.length > 0 && userRole[0].role === 'Apprenti'; // ⭐ CORRECTION : Passer la date de début pour détecter N+1 // ✅ APRÈS (avec anticipation) const checkResult = await checkLeaveBalanceWithAnticipation( conn, collaborateurId, Repartition, DateDebut ); // Adapter le format de la réponse if (!checkResult.valide) { uploadedFiles.forEach(file => { if (fs.existsSync(file.path)) fs.unlinkSync(file.path); }); await conn.rollback(); conn.release(); // Construire le message d'erreur const messagesErreur = checkResult.insuffisants.map(ins => { return `${ins.type}: ${ins.joursNecessaires}j demandés mais seulement ${ins.soldeTotal.toFixed(2)}j disponibles (déficit: ${ins.deficit.toFixed(2)}j)`; }).join('\n'); return res.json({ success: false, message: `❌ Solde(s) insuffisant(s):\n${messagesErreur}`, details: checkResult.details, insuffisants: checkResult.insuffisants }); } console.log('✅ Tous les soldes sont suffisants (incluant anticipation si nécessaire)\n'); } // ======================================== // ÉTAPE 2 : CRÉER LA DEMANDE // ======================================== console.log('\n📝 Création de la demande...'); const typeIds = []; for (const rep of Repartition) { const code = rep.TypeConge; // Ne pas inclure ABS et Formation dans les typeIds principaux if (code === 'ABS' || code === 'Formation') { continue; } const name = code === 'CP' ? 'Congé payé' : code === 'RTT' ? 'RTT' : code === 'Récup' ? 'Récupération' : code; const [typeRow] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', [name]); if (typeRow.length > 0) typeIds.push(typeRow[0].Id); } // Si aucun type CP/RTT/Récup, prendre le premier type de la répartition if (typeIds.length === 0) { const firstType = Repartition[0]?.TypeConge; const name = firstType === 'Formation' ? 'Formation' : firstType === 'ABS' ? 'Congé maladie' : 'Congé payé'; const [typeRow] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', [name]); if (typeRow.length > 0) { typeIds.push(typeRow[0].Id); } else { uploadedFiles.forEach(file => { if (fs.existsSync(file.path)) fs.unlinkSync(file.path); }); await conn.rollback(); conn.release(); return res.json({ success: false, message: 'Aucun type de congé valide' }); } } const typeCongeIdCsv = typeIds.join(','); const currentDate = new Date().toISOString().slice(0, 19).replace('T', ' '); // ✅ CRÉER LA DEMANDE const [result] = await conn.query( `INSERT INTO DemandeConge (EmployeeId, CollaborateurADId, DateDebut, DateFin, TypeCongeId, Statut, DateDemande, Commentaire, Validateur, NombreJours) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [isAD ? 0 : employeeId, collaborateurId, DateDebut, DateFin, typeCongeIdCsv, statutDemande, currentDate, Commentaire || '', '', NombreJours] ); const demandeId = result.insertId; console.log(`✅ Demande créée avec ID ${demandeId} - Statut: ${statutDemande}`); // ======================================== // ÉTAPE 3 : Sauvegarder les fichiers médicaux // ======================================== if (uploadedFiles.length > 0) { console.log('\n📎 Sauvegarde des fichiers médicaux...'); for (const file of uploadedFiles) { await conn.query( `INSERT INTO DocumentsMedicaux (DemandeCongeId, NomFichier, CheminFichier, TypeMime, TailleFichier, DateUpload) VALUES (?, ?, ?, ?, ?, NOW())`, [demandeId, file.originalname, file.path, file.mimetype, file.size] ); console.log(` ✓ ${file.originalname}`); } } // ======================================== // ÉTAPE 4 : Sauvegarder la répartition // ======================================== console.log('\n📊 Sauvegarde de la répartition en base...'); for (const rep of Repartition) { const code = rep.TypeConge; const name = code === 'CP' ? 'Congé payé' : code === 'RTT' ? 'RTT' : code === 'ABS' ? 'Congé maladie' : code === 'Formation' ? 'Formation' : code === 'Récup' ? 'Récupération' : code; const [typeRow] = await conn.query( 'SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', [name] ); if (typeRow.length > 0) { await conn.query( `INSERT INTO DemandeCongeType (DemandeCongeId, TypeCongeId, NombreJours, PeriodeJournee) VALUES (?, ?, ?, ?)`, [ demandeId, typeRow[0].Id, rep.NombreJours, rep.PeriodeJournee || 'Journée entière' ] ); console.log(` ✓ ${name}: ${rep.NombreJours}j (${rep.PeriodeJournee || 'Journée entière'})`); } } // ======================================== // ÉTAPE 5 : Déduction des compteurs CP/RTT/RÉCUP (AVEC ANTICIPATION N+1) // ======================================== if (isAD && collaborateurId && !isFormationOnly) { console.log('\n📉 Déduction des compteurs (avec anticipation N+1)...'); for (const rep of Repartition) { if (rep.TypeConge === 'ABS' || rep.TypeConge === 'Formation') { console.log(` ⏩ ${rep.TypeConge} ignoré (pas de déduction)`); continue; } // ⭐ TRAITEMENT SPÉCIAL POUR RÉCUP if (rep.TypeConge === 'Récup') { const [recupType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Récupération']); if (recupType.length > 0) { await conn.query(` UPDATE CompteurConges SET Solde = GREATEST(0, Solde - ?), DerniereMiseAJour = NOW() WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? `, [rep.NombreJours, collaborateurId, recupType[0].Id, currentYear]); await conn.query(` INSERT INTO DeductionDetails (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) VALUES (?, ?, ?, 'Récup Posée', ?) `, [demandeId, recupType[0].Id, currentYear, rep.NombreJours]); console.log(` ✓ Récup: ${rep.NombreJours}j déduits`); } continue; } // ⭐ CP et RTT : AVEC ANTICIPATION N+1 const name = rep.TypeConge === 'CP' ? 'Congé payé' : 'RTT'; const [typeRow] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', [name]); if (typeRow.length > 0) { const result = await deductLeaveBalanceWithAnticipation( conn, collaborateurId, typeRow[0].Id, rep.NombreJours, demandeId, DateDebut ); console.log(` ✓ ${name}: ${rep.NombreJours}j déduits`); if (result.details && result.details.length > 0) { result.details.forEach(d => { console.log(` - ${d.type} (${d.annee}): ${d.joursUtilises}j`); }); } } } await updateSoldeAnticipe(conn, collaborateurId); console.log('✅ Déductions terminées\n'); } // ======================================== // ÉTAPE 6 : Notifications (Formation uniquement) // ======================================== const dateDebut = new Date(DateDebut).toLocaleDateString('fr-FR'); const dateFin = new Date(DateFin).toLocaleDateString('fr-FR'); const datesPeriode = dateDebut === dateFin ? dateDebut : `du ${dateDebut} au ${dateFin}`; if (isFormationOnly && isAD && collaborateurId) { await conn.query( `INSERT INTO Notifications (CollaborateurADId, Type, Titre, Message, DemandeCongeId, DateCreation, Lu) VALUES (?, ?, ?, ?, ?, NOW(), 0)`, [ collaborateurId, 'Success', '✅ Formation validée automatiquement', `Votre période de formation ${datesPeriode} a été validée automatiquement.`, demandeId ] ); console.log('\n📬 Notification formation créée'); } // ======================================== // ÉTAPE 7 : Récupérer les managers // ======================================== let managers = []; if (isAD) { const [rows] = await conn.query( `SELECT c.email FROM HierarchieValidationAD hv JOIN CollaborateurAD c ON hv.SuperieurId = c.id WHERE hv.CollaborateurId = ?`, [collaborateurId] ); managers = rows.map(r => r.email); } await conn.commit(); console.log('\n🎉 Transaction validée\n'); // ======================================== // ÉTAPE 8 : Notifier les clients SSE // ======================================== if (isFormationOnly && isAD && collaborateurId) { notifyCollabClients({ type: 'demande-validated', demandeId: parseInt(demandeId), statut: 'Validée', timestamp: new Date().toISOString() }, collaborateurId); } // ======================================== // ENVOI DES EMAILS // ======================================== const accessToken = await getGraphToken(); if (accessToken) { const fromEmail = 'gtanoreply@ensup.eu'; const typesConges = Repartition.map(rep => { const typeNom = rep.TypeConge === 'CP' ? 'Congé payé' : rep.TypeConge === 'RTT' ? 'RTT' : rep.TypeConge === 'ABS' ? 'Congé maladie' : rep.TypeConge === 'Formation' ? 'Formation' : rep.TypeConge === 'Récup' ? 'Récupération' : rep.TypeConge; return `${typeNom}: ${rep.NombreJours}j`; }).join(' | '); if (isFormationOnly) { // Email formation const subjectCollab = '✅ Formation enregistrée et validée'; const bodyCollab = `

✅ Formation validée

Bonjour ${Nom},

Votre période de formation a été automatiquement validée.

Période : ${datesPeriode}

Durée : ${NombreJours} jour(s)

`; try { await sendMailGraph(accessToken, fromEmail, Email, subjectCollab, bodyCollab); } catch (mailError) { console.error('❌ Erreur email:', mailError.message); } } else { // ⭐ EMAIL NORMAL (incluant Récup) const subjectCollab = '✅ Confirmation de réception de votre demande de congé'; const bodyCollab = `

✅ Demande enregistrée

Bonjour ${Nom},

Votre demande de congé a bien été enregistrée.

Type : ${typesConges}

Période : ${datesPeriode}

Durée : ${NombreJours} jour(s)

`; try { await sendMailGraph(accessToken, fromEmail, Email, subjectCollab, bodyCollab); } catch (mailError) { console.error('❌ Erreur email:', mailError.message); } for (const managerEmail of managers) { const subjectManager = `📋 Nouvelle demande de congé - ${Nom}`; const bodyManager = `

📋 Nouvelle demande

${Nom} a soumis une nouvelle demande.

Type : ${typesConges}

Période : ${datesPeriode}

`; try { await sendMailGraph(accessToken, fromEmail, managerEmail, subjectManager, bodyManager); } catch (mailError) { console.error('❌ Erreur email manager:', mailError.message); } } } } res.json({ success: true, message: isFormationOnly ? 'Formation enregistrée et validée automatiquement' : 'Demande soumise', request_id: demandeId, managers, auto_validated: isFormationOnly, files_uploaded: uploadedFiles.length }); } catch (error) { await conn.rollback(); if (req.files) { req.files.forEach(file => { if (fs.existsSync(file.path)) { fs.unlinkSync(file.path); console.log(`🗑️ Fichier supprimé: ${file.originalname}`); } }); } console.error('\n❌ ERREUR submitLeaveRequest:', error); res.status(500).json({ success: false, message: 'Erreur serveur', error: error.message }); } finally { conn.release(); } }); app.get('/api/download-medical/:documentId', async (req, res) => { try { const { documentId } = req.params; const conn = await pool.getConnection(); const [docs] = await conn.query( 'SELECT * FROM DocumentsMedicaux WHERE Id = ?', [documentId] ); conn.release(); if (docs.length === 0) { return res.status(404).json({ success: false, message: 'Document non trouvé' }); } const doc = docs[0]; if (!fs.existsSync(doc.CheminFichier)) { return res.status(404).json({ success: false, message: 'Fichier introuvable' }); } res.download(doc.CheminFichier, doc.NomFichier); } catch (error) { console.error('Erreur téléchargement:', error); res.status(500).json({ success: false, message: 'Erreur serveur' }); } }); // Récupérer les documents d'une demande app.get('/api/medical-documents/:demandeId', async (req, res) => { try { const { demandeId } = req.params; const conn = await pool.getConnection(); const [docs] = await conn.query( `SELECT Id, NomFichier, TypeMime, TailleFichier, DateUpload FROM DocumentsMedicaux WHERE DemandeCongeId = ? ORDER BY DateUpload DESC`, [demandeId] ); conn.release(); res.json({ success: true, documents: docs.map(doc => ({ id: doc.Id, nom: doc.NomFichier, type: doc.TypeMime, taille: doc.TailleFichier, date: doc.DateUpload, downloadUrl: `/download-medical/${doc.Id}` })) }); } catch (error) { console.error('Erreur récupération documents:', error); res.status(500).json({ success: false, message: 'Erreur serveur' }); } }); app.post('/api/validateRequest', async (req, res) => { const conn = await pool.getConnection(); try { await conn.beginTransaction(); const { request_id, action, validator_id, comment } = req.body; console.log(`\n🔍 Validation demande #${request_id} - Action: ${action}`); if (!request_id || !action || !validator_id) { return res.json({ success: false, message: 'Données manquantes' }); } const [validator] = await conn.query('SELECT Id, prenom, nom, email, CampusId FROM CollaborateurAD WHERE Id = ?', [validator_id]); if (validator.length === 0) { throw new Error('Validateur introuvable'); } // Récupérer les informations de la demande const [requests] = await conn.query( `SELECT dc.Id, dc.CollaborateurADId, dc.TypeCongeId, dc.NombreJours, dc.DateDebut, dc.DateFin, dc.Commentaire, dc.Statut, ca.prenom, ca.nom, ca.email as collaborateur_email, tc.Nom as TypeConge FROM DemandeConge dc JOIN TypeConge tc ON dc.TypeCongeId = tc.Id LEFT JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.Id WHERE dc.Id = ? LIMIT 1`, [request_id] ); if (requests.length === 0) { throw new Error('Demande non trouvée'); } const request = requests[0]; // ⭐ DÉBOGAGE : Afficher TOUTES les données console.log('\n=== DONNÉES RÉCUPÉRÉES ==='); console.log('request:', JSON.stringify(request, null, 2)); console.log('validator:', JSON.stringify(validator[0], null, 2)); console.log('========================\n'); if (request.Statut !== 'En attente') { throw new Error(`La demande a déjà été traitée (Statut: ${request.Statut})`); } const newStatus = action === 'approve' ? 'Validée' : 'Refusée'; if (action === 'reject' && request.CollaborateurADId) { console.log(`\n🔄 REFUS - Restauration des soldes...`); const restoration = await restoreLeaveBalance(conn, request_id, request.CollaborateurADId); console.log('Restauration:', restoration); } await conn.query( `UPDATE DemandeConge SET Statut = ?, ValidateurId = ?, ValidateurADId = ?, DateValidation = NOW(), CommentaireValidation = ? WHERE Id = ?`, [newStatus, validator_id, validator_id, comment || '', request_id] ); const notifTitle = action === 'approve' ? 'Demande approuvée ✅' : 'Demande refusée ❌'; let notifMessage = `Votre demande a été ${action === 'approve' ? 'approuvée' : 'refusée'}`; if (comment) notifMessage += ` (Commentaire: ${comment})`; const notifType = action === 'approve' ? 'Success' : 'Error'; await conn.query( 'INSERT INTO Notifications (CollaborateurADId, Titre, Message, Type, DemandeCongeId, DateCreation, lu) VALUES (?, ?, ?, ?, ?, ?, 0)', [request.CollaborateurADId, notifTitle, notifMessage, notifType, request_id, nowFR()] ); await conn.commit(); // ⭐ TEST EMAIL // ⭐ ENVOI EMAIL AVEC TEMPLATE PROFESSIONNEL console.log('\n📧 === TENTATIVE ENVOI EMAIL ==='); console.log('1. Récupération token...'); const accessToken = await getGraphToken(); console.log('2. Token obtenu ?', accessToken ? 'OUI' : 'NON'); if (accessToken && request.collaborateur_email) { const fromEmail = 'gtanoreply@ensup.eu'; const collaborateurNom = `${request.prenom} ${request.nom}`; const validateurNom = `${validator[0].prenom} ${validator[0].nom}`; console.log('3. Préparation email professionnel...'); console.log(' De:', fromEmail); console.log(' À:', request.collaborateur_email); console.log(' Collaborateur:', collaborateurNom); console.log(' Validateur:', validateurNom); const dateDebut = new Date(request.DateDebut).toLocaleDateString('fr-FR'); const dateFin = new Date(request.DateFin).toLocaleDateString('fr-FR'); const datesPeriode = dateDebut === dateFin ? dateDebut : `du ${dateDebut} au ${dateFin}`; const subject = action === 'approve' ? '✅ Votre demande de congé a été approuvée' : '❌ Votre demande de congé a été refusée'; const body = action === 'approve' ? `

✅ Demande approuvée

Bonjour ${collaborateurNom},

Votre demande de congé a été approuvée par ${validateurNom}.

Type : ${request.TypeConge}

Période : ${datesPeriode}

Durée : ${request.NombreJours} jour(s)

${comment ? `

Commentaire : ${comment}

` : ''}

Vous pouvez consulter votre demande dans votre espace personnel.

` : `

❌ Demande refusée

Bonjour ${collaborateurNom},

Votre demande de congé a été refusée par ${validateurNom}.

Type : ${request.TypeConge}

Période : ${datesPeriode}

Durée : ${request.NombreJours} jour(s)

${comment ? `

Motif du refus : ${comment}

` : ''}

Pour plus d'informations, contactez ${validateurNom}.

`; console.log('4. Sujet:', subject); console.log('5. Appel sendMailGraph...'); try { await sendMailGraph( accessToken, fromEmail, request.collaborateur_email, subject, body ); console.log('✅✅✅ EMAIL ENVOYÉ AVEC SUCCÈS ! ✅✅✅'); } catch (mailError) { console.error('❌❌❌ ERREUR ENVOI EMAIL ❌❌❌'); console.error('Message:', mailError.message); console.error('Stack:', mailError.stack); } } else { if (!accessToken) console.error('❌ Token manquant'); if (!request.collaborateur_email) console.error('❌ Email collaborateur manquant'); } console.log('=== FIN ENVOI EMAIL ===\n'); // Notifier les clients SSE locaux notifyCollabClients({ type: 'demande-validated', demandeId: parseInt(request_id), statut: newStatus, timestamp: new Date().toISOString() }, request.CollaborateurADId); notifyCollabClients({ type: 'demande-list-updated', action: 'validation', demandeId: parseInt(request_id), timestamp: new Date().toISOString() }); // Envoyer webhook au serveur RH try { await webhookManager.sendWebhook( WEBHOOKS.RH_URL, EVENTS.DEMANDE_VALIDATED, { demandeId: parseInt(request_id), statut: newStatus, collaborateurId: request.CollaborateurADId, validateurId: validator_id, commentaire: comment } ); console.log('✅ Webhook envoyé au serveur RH'); } catch (webhookError) { console.error('❌ Erreur envoi webhook (non bloquant):', webhookError.message); } res.json({ success: true, message: `Demande ${action === 'approve' ? 'approuvée' : 'refusée'}`, new_status: newStatus }); console.log(`✅ Demande ${request_id} ${newStatus}\n`); } catch (error) { await conn.rollback(); console.error('\n❌ ERREUR lors de la validation:', error); res.status(500).json({ success: false, message: error.message }); } finally { conn.release(); } }); app.get('/api/testRestoration', async (req, res) => { const conn = await pool.getConnection(); try { const { demande_id, collab_id } = req.query; if (!demande_id || !collab_id) { return res.json({ success: false, message: 'Paramètres manquants: demande_id et collab_id requis' }); } // 1. Voir les déductions enregistrées const [deductions] = await conn.query( `SELECT dd.*, tc.Nom as TypeNom FROM DeductionDetails dd JOIN TypeConge tc ON dd.TypeCongeId = tc.Id WHERE dd.DemandeCongeId = ?`, [demande_id] ); // 2. Voir l'état actuel des compteurs const [compteurs] = await conn.query( `SELECT cc.*, tc.Nom as TypeNom FROM CompteurConges cc JOIN TypeConge tc ON cc.TypeCongeId = tc.Id WHERE cc.CollaborateurADId = ? ORDER BY tc.Nom, cc.Annee DESC`, [collab_id] ); // 3. Calculer ce que devrait être la restauration const planRestauration = deductions.map(d => ({ type: d.TypeNom, annee: d.Annee, typeDeduction: d.TypeDeduction, joursARestorer: d.JoursUtilises, action: d.TypeDeduction === 'Reporté N-1' ? 'Ajouter au SoldeReporte ET au Solde' : 'Ajouter au Solde uniquement' })); conn.release(); res.json({ success: true, demande_id: demande_id, collaborateur_id: collab_id, deductions_enregistrees: deductions, compteurs_actuels: compteurs, plan_restauration: planRestauration, instructions: planRestauration.length === 0 ? "❌ Aucune déduction trouvée - La demande a été créée avant l'installation du tracking" : "✅ Déductions trouvées - La restauration devrait fonctionner" }); } catch (error) { conn.release(); res.status(500).json({ success: false, error: error.message }); } }); function normalizeRole(role) { if (!role) return null; const roleLower = role.toLowerCase(); // Normaliser les variantes féminines et masculines if (roleLower === 'collaboratrice') return 'collaborateur'; if (roleLower === 'validatrice') return 'validateur'; if (roleLower === 'directrice de campus') return 'directeur de campus'; if (roleLower === 'apprentie') return 'apprenti'; return roleLower; } app.get('/api/getSocietesByCampus', async (req, res) => { try { const { campusId } = req.query; const conn = await pool.getConnection(); const [societes] = await conn.query(` SELECT DISTINCT s.Id, s.Nom FROM SocieteCampus sc JOIN Societe s ON sc.SocieteId = s.Id WHERE sc.CampusId = ? ORDER BY CASE WHEN s.Nom LIKE '%SOLUTION%' THEN 1 ELSE 2 END, s.Nom `, [campusId]); conn.release(); res.json({ success: true, societes: societes }); } catch (error) { console.error('Erreur getSocietesByCampus:', error); res.status(500).json({ success: false, message: error.message }); } }); // ⭐ NOUVELLE ROUTE HELPER : Récupérer les campus d'une société app.get('/api/getCampusBySociete', async (req, res) => { try { const { societeId } = req.query; const conn = await pool.getConnection(); const [campus] = await conn.query(` SELECT DISTINCT c.Id, c.Nom, sc.Principal FROM SocieteCampus sc JOIN Campus c ON sc.CampusId = c.Id WHERE sc.SocieteId = ? ORDER BY sc.Principal DESC, -- Principal en premier c.Nom `, [societeId]); conn.release(); res.json({ success: true, campus: campus, isMultiCampus: campus.length > 1 }); } catch (error) { console.error('Erreur getCampusBySociete:', error); res.status(500).json({ success: false, message: error.message }); } }); // ======================================== // ROUTE getTeamLeaves COMPLÈTE // ======================================== app.get('/api/getTeamLeaves', async (req, res) => { try { let { user_id: userIdParam, role: roleParam, selectedCampus, selectedSociete, selectedService } = req.query; console.log(`🔍 Paramètres reçus: user_id=${userIdParam}, role=${roleParam}, selectedCampus=${selectedCampus}`); if (!userIdParam) { return res.json({ success: false, message: 'ID utilisateur manquant' }); } const conn = await pool.getConnection(); const isUUID = userIdParam.length > 10 && userIdParam.includes('-'); console.log(`📝 Type ID détecté: ${isUUID ? 'UUID' : 'Numérique'}`); const userQuery = ` SELECT ca.id, ca.ServiceId, ca.CampusId, ca.SocieteId, ca.email, s.Nom AS serviceNom, c.Nom AS campusNom, so.Nom AS societeNom FROM CollaborateurAD ca LEFT JOIN Services s ON ca.ServiceId = s.Id LEFT JOIN Campus c ON ca.CampusId = c.Id LEFT JOIN Societe so ON ca.SocieteId = so.Id WHERE ${isUUID ? 'ca.entraUserId' : 'ca.id'} = ? LIMIT 1 `; const [userRows] = await conn.query(userQuery, [userIdParam]); if (!userRows || userRows.length === 0) { conn.release(); return res.json({ success: false, message: 'Collaborateur non trouvé' }); } const userInfo = userRows[0]; const serviceId = userInfo.ServiceId; const campusId = userInfo.CampusId; const societeId = userInfo.SocieteId; const userEmail = userInfo.email; const campusNom = userInfo.campusNom; function normalizeRole(role) { if (!role) return null; const roleLower = role.toLowerCase(); if (roleLower === 'collaboratrice') return 'collaborateur'; if (roleLower === 'validatrice') return 'validateur'; if (roleLower === 'directrice de campus') return 'directeur de campus'; if (roleLower === 'apprentie') return 'apprenti'; return roleLower; } const roleOriginal = roleParam?.toLowerCase(); const role = normalizeRole(roleOriginal); console.log(`👤 Utilisateur trouvé:`); console.log(` - ID: ${userInfo.id}`); console.log(` - Email: ${userEmail}`); console.log(` - ServiceId: ${serviceId}`); console.log(` - CampusId: ${campusId}`); console.log(` - CampusNom: ${campusNom}`); console.log(` - SocieteId: ${societeId}`); console.log(` - Role normalisé: ${role}`); let query, params; const filters = {}; // ======================================== // CAS 1: PRESIDENT, ADMIN, RH // ======================================== // ======================================== // CAS 1: PRESIDENT, ADMIN, RH, DIRECTEUR DE CAMPUS // ======================================== if (role === 'president' || role === 'admin' || role === 'rh' || role === 'directeur de campus') { console.log("CAS 1: President/Admin/RH/Directeur de Campus - Vue globale"); console.log(` Filtres reçus: Société=${selectedSociete}, Campus=${selectedCampus}, Service=${selectedService}`); // ======================================== // 🔧 LISTE COMPLÈTE DES FILTRES DISPONIBLES // ======================================== // 1️⃣ SOCIÉTÉS (toutes disponibles) const [societesList] = await conn.query(` SELECT DISTINCT Nom FROM Societe ORDER BY Nom `); filters.societes = societesList.map(s => s.Nom); console.log('📊 Sociétés disponibles:', filters.societes); // 2️⃣ CAMPUS (tous les campus, filtrés par société si nécessaire) let campusQuery; let campusParams = []; if (selectedSociete && selectedSociete !== 'all') { campusQuery = ` SELECT DISTINCT c.Nom FROM Campus c JOIN CollaborateurAD ca ON ca.CampusId = c.Id JOIN Societe so ON ca.SocieteId = so.Id WHERE so.Nom = ? AND (ca.actif = 1 OR ca.actif IS NULL) ORDER BY c.Nom `; campusParams = [selectedSociete]; } else { campusQuery = ` SELECT DISTINCT Nom FROM Campus ORDER BY Nom `; } const [campusList] = await conn.query(campusQuery, campusParams); filters.campus = campusList.map(c => c.Nom); console.log('📊 Campus disponibles:', filters.campus); // ⭐ NOUVEAU : Pour directeur de campus, envoyer son campus par défaut if (role === 'directeur de campus') { filters.defaultCampus = campusNom; // Le campus du directeur console.log('🏢 Campus par défaut pour directeur:', campusNom); } // 3️⃣ SERVICES (filtrés selon société + campus) let servicesQuery = ` SELECT DISTINCT s.Nom FROM Services s JOIN CollaborateurAD ca ON ca.ServiceId = s.Id `; let servicesJoins = []; let servicesConditions = ['(ca.actif = 1 OR ca.actif IS NULL)']; let servicesParams = []; if (selectedSociete && selectedSociete !== 'all') { servicesJoins.push('JOIN Societe so ON ca.SocieteId = so.Id'); servicesConditions.push('so.Nom = ?'); servicesParams.push(selectedSociete); } if (selectedCampus && selectedCampus !== 'all') { servicesJoins.push('JOIN Campus c ON ca.CampusId = c.Id'); servicesConditions.push('c.Nom = ?'); servicesParams.push(selectedCampus); } if (servicesJoins.length > 0) { servicesQuery += '\n' + servicesJoins.join('\n'); } servicesQuery += `\nWHERE ${servicesConditions.join(' AND ')}\nORDER BY s.Nom`; const [servicesList] = await conn.query(servicesQuery, servicesParams); filters.services = servicesList.map(s => s.Nom); // ======================================== // 🔧 LISTE DES EMPLOYÉS (avec filtres appliqués) // ======================================== let employeesQuery = ` SELECT DISTINCT CONCAT(ca.prenom, ' ', ca.nom) AS fullname, c.Nom AS campusnom, so.Nom AS societenom, s.Nom AS servicenom FROM CollaborateurAD ca JOIN Services s ON ca.ServiceId = s.Id JOIN Campus c ON ca.CampusId = c.Id JOIN Societe so ON ca.SocieteId = so.Id WHERE (ca.actif = 1 OR ca.actif IS NULL) `; let employeesConditions = []; let employeesParams = []; if (selectedSociete && selectedSociete !== 'all') { employeesConditions.push('so.Nom = ?'); employeesParams.push(selectedSociete); } if (selectedCampus && selectedCampus !== 'all') { employeesConditions.push('c.Nom = ?'); employeesParams.push(selectedCampus); } else if (role === 'directeur de campus' && campusNom) { // ⭐ NOUVEAU : Si directeur et pas de filtre campus, filtrer par son campus par défaut employeesConditions.push('c.Nom = ?'); employeesParams.push(campusNom); } if (selectedService && selectedService !== 'all') { employeesConditions.push('s.Nom = ?'); employeesParams.push(selectedService); } if (employeesConditions.length > 0) { employeesQuery += ` AND ${employeesConditions.join(' AND ')}`; } employeesQuery += ` ORDER BY so.Nom, c.Nom, ca.prenom, ca.nom`; const [employeesList] = await conn.query(employeesQuery, employeesParams); filters.employees = employeesList.map(e => ({ name: e.fullname, campus: e.campusnom, societe: e.societenom, service: e.servicenom })); console.log(`👥 Employés trouvés:`, filters.employees.length); // ======================================== // 🔧 QUERY DES CONGÉS (avec filtres appliqués) // ======================================== let whereConditions = [`dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente')`]; let whereParams = []; if (selectedSociete && selectedSociete !== 'all') { whereConditions.push('so.Nom = ?'); whereParams.push(selectedSociete); } if (selectedCampus && selectedCampus !== 'all') { whereConditions.push('c.Nom = ?'); whereParams.push(selectedCampus); } else if (role === 'directeur de campus' && campusNom) { // ⭐ NOUVEAU : Si directeur et pas de filtre campus, filtrer par son campus par défaut whereConditions.push('c.Nom = ?'); whereParams.push(campusNom); } if (selectedService && selectedService !== 'all') { whereConditions.push('s.Nom = ?'); whereParams.push(selectedService); } query = ` SELECT DATE_FORMAT(dc.DateDebut, '%Y-%m-%d') AS startdate, DATE_FORMAT(dc.DateFin, '%Y-%m-%d') AS enddate, CONCAT(ca.prenom, ' ', ca.nom) AS employeename, GROUP_CONCAT(DISTINCT tc.Nom ORDER BY tc.Nom SEPARATOR ', ') AS type, CONCAT( '[', GROUP_CONCAT( JSON_OBJECT( 'type', tc.Nom, 'jours', dct.NombreJours, 'periode', COALESCE(dct.PeriodeJournee, 'Journée entière') ) SEPARATOR ',' ), ']' ) AS detailsconges, MAX(tc.CouleurHex) AS color, dc.Statut AS statut, s.Nom AS servicenom, c.Nom AS campusnom, so.Nom AS societenom, dc.NombreJours AS nombrejoursouvres FROM DemandeConge dc JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id LEFT JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId LEFT JOIN TypeConge tc ON dct.TypeCongeId = tc.Id JOIN Services s ON ca.ServiceId = s.Id JOIN Campus c ON ca.CampusId = c.Id JOIN Societe so ON ca.SocieteId = so.Id WHERE ${whereConditions.join(' AND ')} GROUP BY dc.Id, dc.DateDebut, dc.DateFin, ca.prenom, ca.nom, dc.Statut, s.Nom, c.Nom, so.Nom, dc.NombreJours ORDER BY so.Nom, c.Nom, dc.DateDebut ASC `; params = whereParams; console.log(`🔍 Query finale WHERE:`, whereConditions.join(' AND ')); console.log(`🔍 Params:`, whereParams); } // ======================================== // CAS 3: COLLABORATEUR // ======================================== // Dans la route /getTeamLeaves, modifiez la section CAS 3: COLLABORATEUR // ======================================== // CAS 3: COLLABORATEUR // ======================================== // ======================================== // CAS 3: COLLABORATEUR // ======================================== else if (role === 'collaborateur' || role === 'validateur' || role === 'apprenti') { console.log("CAS 3: Collaborateur/Apprenti avec filtres avancés"); const serviceNom = userInfo.serviceNom || 'Non défini'; const campusNom = userInfo.campusNom || 'Non défini'; const societeNom = userInfo.societeNom || 'Non défini'; console.log(`📍 Filtres reçus du frontend: Société=${selectedSociete}, Campus=${selectedCampus}, Service=${selectedService}`); // ⭐ NOUVEAU : Si AUCUN filtre n'est envoyé (premier chargement), utiliser les valeurs par défaut // Sinon, respecter EXACTEMENT ce que le frontend envoie (même "all") const isFirstLoad = !selectedCampus && !selectedService && !selectedSociete; if (isFirstLoad) { console.log('🎯 Premier chargement : initialisation avec service par défaut'); selectedCampus = campusNom; selectedService = serviceNom; selectedSociete = societeNom; } // Si le frontend envoie "all", on garde "all" (ne pas forcer les valeurs par défaut) console.log(`📍 Filtres appliqués finaux: Société=${selectedSociete}, Campus=${selectedCampus}, Service=${selectedService}`); // ⭐ Construire les listes de filtres disponibles // 1️⃣ Sociétés disponibles (TOUTES) const [societesList] = await conn.query(` SELECT DISTINCT so.Nom FROM Societe so JOIN CollaborateurAD ca ON ca.SocieteId = so.Id WHERE (ca.actif = 1 OR ca.actif IS NULL) ORDER BY so.Nom `); filters.societes = societesList.map(s => s.Nom); // 2️⃣ Campus disponibles (filtrés par société si sélectionné) let campusQuery = ` SELECT DISTINCT c.Nom FROM Campus c JOIN CollaborateurAD ca ON ca.CampusId = c.Id WHERE (ca.actif = 1 OR ca.actif IS NULL) `; let campusParams = []; if (selectedSociete && selectedSociete !== 'all') { campusQuery += ` AND ca.SocieteId = (SELECT Id FROM Societe WHERE Nom = ? LIMIT 1)`; campusParams.push(selectedSociete); } campusQuery += ` ORDER BY c.Nom`; const [campusList] = await conn.query(campusQuery, campusParams); filters.campus = campusList.map(c => c.Nom); // 3️⃣ Services disponibles (filtrés par société + campus) let servicesQuery = ` SELECT DISTINCT s.Nom FROM Services s JOIN CollaborateurAD ca ON ca.ServiceId = s.Id WHERE (ca.actif = 1 OR ca.actif IS NULL) `; let servicesParams = []; if (selectedSociete && selectedSociete !== 'all') { servicesQuery += ` AND ca.SocieteId = (SELECT Id FROM Societe WHERE Nom = ? LIMIT 1)`; servicesParams.push(selectedSociete); } if (selectedCampus && selectedCampus !== 'all') { servicesQuery += ` AND ca.CampusId = (SELECT Id FROM Campus WHERE Nom = ? LIMIT 1)`; servicesParams.push(selectedCampus); } servicesQuery += ` ORDER BY s.Nom`; const [servicesList] = await conn.query(servicesQuery, servicesParams); filters.services = servicesList.map(s => s.Nom); // ⭐ Envoyer les valeurs par défaut au frontend (pour initialisation) filters.defaultCampus = campusNom; filters.defaultService = serviceNom; filters.defaultSociete = societeNom; // ⭐ LISTE DES EMPLOYÉS (avec filtres conditionnels) let employeesQuery = ` SELECT DISTINCT CONCAT(ca.prenom, ' ', ca.nom) AS fullname, c.Nom AS campusnom, so.Nom AS societenom, s.Nom AS servicenom FROM CollaborateurAD ca JOIN Services s ON ca.ServiceId = s.Id JOIN Campus c ON ca.CampusId = c.Id JOIN Societe so ON ca.SocieteId = so.Id WHERE (ca.actif = 1 OR ca.actif IS NULL) `; let employeesParams = []; let employeesConditions = []; // ⭐ N'ajouter les filtres QUE si différents de "all" if (selectedSociete && selectedSociete !== 'all') { employeesConditions.push('so.Nom = ?'); employeesParams.push(selectedSociete); } if (selectedCampus && selectedCampus !== 'all') { employeesConditions.push('c.Nom = ?'); employeesParams.push(selectedCampus); } if (selectedService && selectedService !== 'all') { employeesConditions.push('s.Nom = ?'); employeesParams.push(selectedService); } if (employeesConditions.length > 0) { employeesQuery += ` AND ${employeesConditions.join(' AND ')}`; } employeesQuery += ` ORDER BY s.Nom, ca.prenom, ca.nom`; const [employeesList] = await conn.query(employeesQuery, employeesParams); filters.employees = employeesList.map(emp => ({ name: emp.fullname, campus: emp.campusnom, societe: emp.societenom, service: emp.servicenom })); console.log(`👥 Employés trouvés: ${filters.employees.length}`); // ⭐ QUERY DES CONGÉS (avec mêmes filtres conditionnels) let queryConditions = `WHERE dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente')`; params = []; let congesConditions = []; // ⭐ N'ajouter les filtres QUE si différents de "all" if (selectedSociete && selectedSociete !== 'all') { congesConditions.push('so.Nom = ?'); params.push(selectedSociete); } if (selectedCampus && selectedCampus !== 'all') { congesConditions.push('c.Nom = ?'); params.push(selectedCampus); } if (selectedService && selectedService !== 'all') { congesConditions.push('s.Nom = ?'); params.push(selectedService); } if (congesConditions.length > 0) { queryConditions += ` AND ${congesConditions.join(' AND ')}`; } query = ` SELECT DATE_FORMAT(dc.DateDebut, '%Y-%m-%d') AS startdate, DATE_FORMAT(dc.DateFin, '%Y-%m-%d') AS enddate, CONCAT(ca.prenom, ' ', ca.nom) AS employeename, GROUP_CONCAT(DISTINCT tc.Nom ORDER BY tc.Nom SEPARATOR ', ') AS type, CONCAT( '[', GROUP_CONCAT( JSON_OBJECT( 'type', tc.Nom, 'jours', dct.NombreJours, 'periode', COALESCE(dct.PeriodeJournee, 'Journée entière') ) SEPARATOR ',' ), ']' ) AS detailsconges, MAX(tc.CouleurHex) AS color, dc.Statut AS statut, s.Nom AS servicenom, c.Nom AS campusnom, so.Nom AS societenom, dc.NombreJours AS nombrejoursouvres FROM DemandeConge dc JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id LEFT JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId LEFT JOIN TypeConge tc ON dct.TypeCongeId = tc.Id JOIN Services s ON ca.ServiceId = s.Id JOIN Campus c ON ca.CampusId = c.Id JOIN Societe so ON ca.SocieteId = so.Id ${queryConditions} GROUP BY dc.Id, dc.DateDebut, dc.DateFin, ca.prenom, ca.nom, dc.Statut, s.Nom, c.Nom, so.Nom, dc.NombreJours ORDER BY s.Nom, dc.DateDebut ASC `; console.log(`🔍 Query WHERE final:`, queryConditions); console.log(`🔍 Params:`, params); } // ======================================== // CAS 4: AUTRES RÔLES // ======================================== else { console.log("CAS 4: Autres rôles"); if (!serviceId) { conn.release(); return res.json({ success: false, message: 'ServiceId manquant' }); } const [checkService] = await conn.query(`SELECT Nom FROM Services WHERE Id = ?`, [serviceId]); const serviceNom = checkService.length > 0 ? checkService[0].Nom : "Inconnu"; const isAdminFinancier = serviceNom === "Administratif & Financier"; if (isAdminFinancier) { const [employeesList] = await conn.query(` SELECT DISTINCT CONCAT(ca.prenom, ' ', ca.nom) AS fullname, c.Nom AS campusnom, so.Nom AS societenom, s.Nom AS servicenom FROM CollaborateurAD ca JOIN Services s ON ca.ServiceId = s.Id JOIN Campus c ON ca.CampusId = c.Id JOIN Societe so ON ca.SocieteId = so.Id WHERE ca.ServiceId = ? AND (ca.actif = 1 OR ca.actif IS NULL) ORDER BY ca.prenom, ca.nom `, [serviceId]); filters.employees = employeesList.map(e => ({ name: e.fullname, campus: e.campusnom, societe: e.societenom, service: e.servicenom })); query = ` SELECT DATE_FORMAT(dc.DateDebut, '%Y-%m-%d') AS startdate, DATE_FORMAT(dc.DateFin, '%Y-%m-%d') AS enddate, CONCAT(ca.prenom, ' ', ca.nom) AS employeename, GROUP_CONCAT(DISTINCT tc.Nom ORDER BY tc.Nom SEPARATOR ', ') AS type, CONCAT( '[', GROUP_CONCAT( JSON_OBJECT( 'type', tc.Nom, 'jours', dct.NombreJours, 'periode', COALESCE(dct.PeriodeJournee, 'Journée entière') ) SEPARATOR ',' ), ']' ) AS detailsconges, MAX(tc.CouleurHex) AS color, dc.Statut AS statut, s.Nom AS servicenom, c.Nom AS campusnom, so.Nom AS societenom, dc.NombreJours AS nombrejoursouvres FROM DemandeConge dc JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id LEFT JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId LEFT JOIN TypeConge tc ON dct.TypeCongeId = tc.Id JOIN Services s ON ca.ServiceId = s.Id JOIN Campus c ON ca.CampusId = c.Id JOIN Societe so ON ca.SocieteId = so.Id WHERE dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente') AND ca.ServiceId = ? GROUP BY dc.Id, dc.DateDebut, dc.DateFin, ca.prenom, ca.nom, dc.Statut, s.Nom, c.Nom, so.Nom, dc.NombreJours ORDER BY c.Nom, dc.DateDebut ASC `; params = [serviceId]; } else { const [employeesList] = await conn.query(` SELECT DISTINCT CONCAT(ca.prenom, ' ', ca.nom) AS fullname, c.Nom AS campusnom, so.Nom AS societenom, s.Nom AS servicenom FROM CollaborateurAD ca JOIN Services s ON ca.ServiceId = s.Id JOIN Campus c ON ca.CampusId = c.Id JOIN Societe so ON ca.SocieteId = so.Id WHERE ca.ServiceId = ? AND ca.CampusId = ? AND (ca.actif = 1 OR ca.actif IS NULL) ORDER BY ca.prenom, ca.nom `, [serviceId, campusId]); filters.employees = employeesList.map(e => ({ name: e.fullname, campus: e.campusnom, societe: e.societenom, service: e.servicenom })); query = ` SELECT DATE_FORMAT(dc.DateDebut, '%Y-%m-%d') AS startdate, DATE_FORMAT(dc.DateFin, '%Y-%m-%d') AS enddate, CONCAT(ca.prenom, ' ', ca.nom) AS employeename, GROUP_CONCAT(DISTINCT tc.Nom ORDER BY tc.Nom SEPARATOR ', ') AS type, CONCAT( '[', GROUP_CONCAT( JSON_OBJECT( 'type', tc.Nom, 'jours', dct.NombreJours, 'periode', COALESCE(dct.PeriodeJournee, 'Journée entière') ) SEPARATOR ',' ), ']' ) AS detailsconges, MAX(tc.CouleurHex) AS color, dc.Statut AS statut, s.Nom AS servicenom, c.Nom AS campusnom, so.Nom AS societenom, dc.NombreJours AS nombrejoursouvres FROM DemandeConge dc JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id LEFT JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId LEFT JOIN TypeConge tc ON dct.TypeCongeId = tc.Id JOIN Services s ON ca.ServiceId = s.Id JOIN Campus c ON ca.CampusId = c.Id JOIN Societe so ON ca.SocieteId = so.Id WHERE dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente') AND ca.ServiceId = ? AND ca.CampusId = ? GROUP BY dc.Id, dc.DateDebut, dc.DateFin, ca.prenom, ca.nom, dc.Statut, s.Nom, c.Nom, so.Nom, dc.NombreJours ORDER BY c.Nom, dc.DateDebut ASC `; params = [serviceId, campusId]; } } const [leavesRows] = await conn.query(query, params); const formattedLeaves = leavesRows.map(leave => ({ ...leave })); console.log(`✅ ${formattedLeaves.length} congés trouvés`); if (formattedLeaves.length === 0 && (role === 'collaborateur' || role === 'collaboratrice')) { console.log('🔍 DEBUG: Aucun congé trouvé, vérification...'); const [debugLeaves] = await conn.query(` SELECT COUNT(*) as total FROM DemandeConge dc JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id JOIN Services s ON ca.ServiceId = s.Id WHERE s.Nom = ? AND dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente') `, [userInfo.serviceNom]); console.log(`🔍 Total congés dans le service "${userInfo.serviceNom}":`, debugLeaves[0].total); const [debugCollabs] = await conn.query(` SELECT ca.id, ca.prenom, ca.nom, ca.email, ca.ServiceId, s.Nom as ServiceNom FROM CollaborateurAD ca JOIN Services s ON ca.ServiceId = s.Id WHERE s.Nom = ? `, [userInfo.serviceNom]); console.log(`🔍 Collaborateurs dans "${userInfo.serviceNom}":`, debugCollabs); } console.log(`✅ Filtres:`, { campus: filters.campus?.length || 0, societes: filters.societes?.length || 0, services: filters.services?.length || 0, employees: filters.employees?.length || 0 }); if (formattedLeaves.length > 0) { console.log('📝 Exemple de congé formaté:', formattedLeaves[0]); } conn.release(); res.json({ success: true, role: role, leaves: formattedLeaves, filters: filters }); } catch (error) { console.error("❌ Erreur getTeamLeaves:", error); res.status(500).json({ success: false, message: "Erreur serveur", error: error.message }); } }); // ================================================ // ROUTE DE SYNCHRONISATION INITIALE (CORRIGÉE) // ================================================ app.post('/api/initial-sync', async (req, res) => { const conn = await pool.getConnection(); try { const email = req.body.mail || req.body.userPrincipalName; const entraId = req.body.id; console.log('🔄 Initial Sync pour:', email); // 1. Chercher user const [users] = await conn.query('SELECT * FROM CollaborateurAD WHERE email = ?', [email]); let userId; let userRole; if (users.length > 0) { // UPDATE userId = users[0].id; userRole = users[0].role; await conn.query('UPDATE CollaborateurAD SET entraUserId = ?, DerniereConnexion = NOW() WHERE id = ?', [entraId, userId]); console.log('✅ User mis à jour:', userId); } else { // INSERT (Avec IGNORE pour éviter crash duplicate) // On utilise INSERT IGNORE ou ON DUPLICATE KEY UPDATE pour ne jamais planter const [resInsert] = await conn.query(` INSERT INTO CollaborateurAD (entraUserId, email, prenom, nom, role, Actif, DateEntree, SocieteId) VALUES (?, ?, ?, ?, 'Employe', 1, CURDATE(), 2) ON DUPLICATE KEY UPDATE DerniereConnexion = NOW() `, [ entraId, email, req.body.givenName || '', req.body.surname || '' ]); // Si insertId est 0 (car update), on refait un select if (resInsert.insertId === 0) { const [u] = await conn.query('SELECT id, role FROM CollaborateurAD WHERE email = ?', [email]); userId = u[0].id; userRole = u[0].role; } else { userId = resInsert.insertId; userRole = 'Employe'; } console.log('✅ User créé/récupéré:', userId); } res.json({ success: true, localUserId: userId, role: userRole }); } catch (error) { console.error('❌ CRASH initial-sync:', error); // On renvoie un succès fake pour ne pas bloquer le frontend res.json({ success: true, localUserId: 1, role: 'Secours' }); } finally { if (conn) conn.release(); } }); // ======================================== // NOUVELLES ROUTES ADMINISTRATION RTT // ======================================== app.get('/api/getAllCollaborateurs', async (req, res) => { try { const [collaborateurs] = await pool.query(` SELECT ca.id, ca.prenom, ca.nom, ca.email, ca.role, ca.TypeContrat, ca.DateEntree, s.Nom as service, ca.CampusId, ca.SocieteId, so.Nom as societe_nom FROM CollaborateurAD ca LEFT JOIN Services s ON ca.ServiceId = s.Id LEFT JOIN Societe so ON ca.SocieteId = so.Id WHERE (ca.actif = 1 OR ca.actif IS NULL) ORDER BY ca.nom, ca.prenom `); res.json({ success: true, collaborateurs: collaborateurs, total: collaborateurs.length }); } catch (error) { console.error('Erreur getAllCollaborateurs:', error); res.status(500).json({ success: false, message: 'Erreur serveur', error: error.message }); } }); app.post('/api/updateTypeContrat', async (req, res) => { try { const { collaborateur_id, type_contrat } = req.body; if (!collaborateur_id || !type_contrat) { return res.json({ success: false, message: 'Données manquantes' }); } const typesValides = ['37h', 'forfait_jour', 'temps_partiel']; if (!typesValides.includes(type_contrat)) { return res.json({ success: false, message: 'Type de contrat invalide' }); } const [collab] = await pool.query( 'SELECT prenom, nom, CampusId FROM CollaborateurAD WHERE id = ?', [collaborateur_id] ); if (collab.length === 0) { return res.json({ success: false, message: 'Collaborateur non trouvé' }); } await pool.query( 'UPDATE CollaborateurAD SET TypeContrat = ? WHERE id = ?', [type_contrat, collaborateur_id] ); res.json({ success: true, message: 'Type de contrat mis à jour', nom: `${collab[0].prenom} ${collab[0].nom}`, nouveau_type: type_contrat }); } catch (error) { console.error('Erreur updateTypeContrat:', error); res.status(500).json({ success: false, message: 'Erreur serveur', error: error.message }); } }); app.get('/api/getConfigurationRTT', async (req, res) => { try { const annee = parseInt(req.query.annee || new Date().getFullYear()); const [configs] = await pool.query( `SELECT Annee, TypeContrat, JoursAnnuels, AcquisitionMensuelle, Description FROM ConfigurationRTT WHERE Annee = ? ORDER BY TypeContrat`, [annee] ); res.json({ success: true, configs }); } catch (error) { res.status(500).json({ success: false, message: 'Erreur', error: error.message }); } }); app.post('/api/updateConfigurationRTT', async (req, res) => { try { const { annee, typeContrat, joursAnnuels } = req.body; if (!annee || !typeContrat || !joursAnnuels) { return res.json({ success: false, message: 'Données manquantes' }); } const acquisitionMensuelle = joursAnnuels / 12; await pool.query( `INSERT INTO ConfigurationRTT (Annee, TypeContrat, JoursAnnuels, AcquisitionMensuelle) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE JoursAnnuels = ?, AcquisitionMensuelle = ?`, [annee, typeContrat, joursAnnuels, acquisitionMensuelle, joursAnnuels, acquisitionMensuelle] ); res.json({ success: true, message: 'Configuration mise à jour' }); } catch (error) { res.status(500).json({ success: false, message: 'Erreur', error: error.message }); } }); app.post('/api/updateRequest', upload.array('medicalDocuments', 5), async (req, res) => { let connection; try { console.log('📥 Body reçu:', req.body); console.log('📎 Fichiers reçus:', req.files); const { requestId, leaveType, startDate, endDate, reason, businessDays, userId, userEmail, userName, accessToken } = req.body; // Validation if (!requestId || !leaveType || !startDate || !endDate || !businessDays || !userId) { return res.status(400).json({ success: false, message: '❌ Données manquantes' }); } connection = await pool.getConnection(); await connection.beginTransaction(); console.log('🔍 ÉTAPE 1: Récupération de la demande originale...'); // Récupérer la demande originale const [originalRequest] = await connection.query( 'SELECT * FROM DemandeConge WHERE Id = ? AND CollaborateurADId = ?', [requestId, userId] ); if (originalRequest.length === 0) { await connection.rollback(); return res.status(404).json({ success: false, message: '❌ Demande introuvable' }); } const original = originalRequest[0]; console.log('🔙 ÉTAPE 2: Suppression des anciennes déductions...'); // Restaurer les compteurs de l'ancienne demande if (original.TypeCongeId !== 3) { // Pas pour Arrêt maladie const [oldRepartition] = await connection.query( 'SELECT * FROM DeductionDetails WHERE DemandeCongeId = ?', [requestId] ); console.log('📊 Ancienne répartition trouvée:', oldRepartition); if (oldRepartition && oldRepartition.length > 0) { for (const rep of oldRepartition) { const typeCongeId = rep.TypeCongeId; const annee = rep.Annee; const joursUtilises = rep.JoursUtilises; // Restaurer dans CompteurConges await connection.query( `UPDATE CompteurConges SET Solde = Solde + ?, DerniereMiseAJour = NOW() WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, [joursUtilises, userId, typeCongeId, annee] ); console.log(`✅ Restauré ${joursUtilises}j dans TypeCongeId=${typeCongeId}, Annee=${annee}`); } } // Supprimer l'ancienne répartition await connection.query( 'DELETE FROM DeductionDetails WHERE DemandeCongeId = ?', [requestId] ); } console.log('📝 ÉTAPE 3: Mise à jour de la demande...'); // Mettre à jour la demande await connection.query( `UPDATE DemandeConge SET TypeCongeId = ?, DateDebut = ?, DateFin = ?, Commentaire = ?, NombreJours = ?, Statut = 'En attente', DateValidation = NOW() WHERE Id = ?`, [leaveType, startDate, endDate, reason || '', businessDays, requestId] ); console.log('📊 ÉTAPE 4: Calcul de la nouvelle répartition...'); let newRepartition = []; // Calculer la nouvelle répartition (seulement pour CP et RTT) if (parseInt(leaveType) === 1 || parseInt(leaveType) === 2) { // Récupérer les compteurs actuels pour l'année en cours const currentYear = new Date().getFullYear(); const previousYear = currentYear - 1; // Récupérer depuis CompteurConges const [countersN] = await connection.query( 'SELECT Solde, SoldeReporte FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?', [userId, leaveType, currentYear] ); const [countersN1] = await connection.query( 'SELECT SoldeReporte FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?', [userId, leaveType, previousYear] ); const soldeN = countersN.length > 0 ? parseFloat(countersN[0].Solde || 0) : 0; const soldeN1 = countersN1.length > 0 ? parseFloat(countersN1[0].SoldeReporte || 0) : 0; console.log(`💡 Soldes disponibles: N-1=${soldeN1}j, N=${soldeN}j`); // Calculer la répartition if (parseInt(leaveType) === 1) { // Congé payé const joursNecessaires = parseFloat(businessDays); // Vérifier si les soldes sont suffisants if (soldeN1 + soldeN < joursNecessaires) { throw new Error(`Solde insuffisant: ${(soldeN1 + soldeN).toFixed(2)}j disponibles, ${joursNecessaires}j demandés`); } // Utiliser d'abord N-1, puis N let reste = joursNecessaires; if (reste > 0 && soldeN1 > 0) { const joursN1 = Math.min(reste, soldeN1); newRepartition.push({ typeCongeId: leaveType, annee: previousYear, jours: joursN1, typeDeduction: 'Report N-1' }); reste -= joursN1; } if (reste > 0 && soldeN > 0) { const joursN = Math.min(reste, soldeN); newRepartition.push({ typeCongeId: leaveType, annee: currentYear, jours: joursN, typeDeduction: 'Année N' }); reste -= joursN; } } else if (parseInt(leaveType) === 2) { // RTT const joursNecessaires = parseFloat(businessDays); if (soldeN < joursNecessaires) { throw new Error(`Solde RTT insuffisant: ${soldeN.toFixed(2)}j disponibles, ${joursNecessaires}j demandés`); } newRepartition = [{ typeCongeId: leaveType, annee: currentYear, jours: joursNecessaires, typeDeduction: 'Année N' }]; } console.log('📊 Nouvelle répartition calculée:', JSON.stringify(newRepartition, null, 2)); // Vérification finale if (!Array.isArray(newRepartition) || newRepartition.length === 0) { console.error('❌ ERREUR: newRepartition invalide:', newRepartition); throw new Error('Répartition invalide'); } console.log('📉 ÉTAPE 5: Déduction des nouveaux compteurs...'); // Déduire les nouveaux compteurs for (const rep of newRepartition) { // Validation de chaque élément if (!rep || typeof rep !== 'object' || !rep.typeCongeId || !rep.annee || rep.jours === undefined) { console.error('❌ Élément invalide:', rep); throw new Error(`Élément de répartition invalide: ${JSON.stringify(rep)}`); } // Sauvegarder dans DeductionDetails await connection.query( `INSERT INTO DeductionDetails (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) VALUES (?, ?, ?, ?, ?)`, [requestId, rep.typeCongeId, rep.annee, rep.typeDeduction, rep.jours] ); // Déduire du compteur await connection.query( `UPDATE CompteurConges SET Solde = GREATEST(0, Solde - ?), DerniereMiseAJour = NOW() WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, [rep.jours, userId, rep.typeCongeId, rep.annee] ); console.log(`✅ Déduit ${rep.jours}j de TypeCongeId=${rep.typeCongeId}, Annee=${rep.annee}`); } } console.log('📧 ÉTAPE 6: Envoi des emails...'); // Récupérer les infos du manager const [managerInfo] = await connection.query( `SELECT m.Email, m.Prenom, m.Nom FROM CollaborateurAD c JOIN HierarchieValidationAD h ON c.id = h.CollaborateurId JOIN CollaborateurAD m ON h.SuperieurId = m.id WHERE c.id = ?`, [userId] ); await connection.commit(); // Envoyer les emails (sans bloquer la réponse) if (accessToken && managerInfo.length > 0) { const manager = managerInfo[0]; // Obtenir un token Graph const graphToken = await getGraphToken(); if (graphToken) { // Email au manager sendMailGraph( graphToken, 'gtanoreply@ensup.eu', manager.Email, 'Modification de demande de congé', `

Modification de demande

Bonjour ${manager.Prenom},

${userName} a modifié sa demande de congé :

Type : ${getLeaveTypeName(leaveType)}
Dates : du ${formatDateFR(startDate)} au ${formatDateFR(endDate)}
Durée : ${businessDays} jour(s)

Merci de valider ou refuser cette demande dans l'application.

Cet email est envoyé automatiquement, merci de ne pas y répondre.

` ).catch(err => console.error('❌ Erreur email manager:', err)); // Email de confirmation au collaborateur sendMailGraph( graphToken, 'gtanoreply@ensup.eu', userEmail, 'Confirmation de modification', `

Demande modifiée

Bonjour ${userName.split(' ')[0]},

Votre demande de congé a bien été modifiée :

Type : ${getLeaveTypeName(leaveType)}
Dates : du ${formatDateFR(startDate)} au ${formatDateFR(endDate)}
Durée : ${businessDays} jour(s)

Elle est maintenant en attente de validation.

Cet email est envoyé automatiquement, merci de ne pas y répondre.

` ).catch(err => console.error('❌ Erreur email collaborateur:', err)); } else { console.warn('⚠️ Impossible d\'obtenir un token Graph - emails non envoyés'); } } res.json({ success: true, message: '✅ Demande modifiée avec succès', repartition: newRepartition }); } catch (error) { if (connection) { await connection.rollback(); } console.error('❌ Erreur updateRequest:', error); res.status(500).json({ success: false, message: error.message || 'Erreur lors de la modification' }); } finally { if (connection) { connection.release(); } } }); // ⭐ Fonction helper pour calculer la répartition CP function calculateCPRepartition(joursNecessaires, soldeN1, soldeN) { const repartition = []; let reste = joursNecessaires; // D'abord utiliser N-1 if (reste > 0 && soldeN1 > 0) { const joursN1 = Math.min(reste, soldeN1); repartition.push({ type: 'CP', annee: 'N-1', jours: joursN1 }); reste -= joursN1; } // Puis utiliser N if (reste > 0 && soldeN > 0) { const joursN = Math.min(reste, soldeN); repartition.push({ type: 'CP', annee: 'N', jours: joursN }); reste -= joursN; } return repartition; } // ⭐ Fonction helper pour obtenir le champ de compteur function getCounterField(type, annee) { if (type === 'CP' && annee === 'N-1') return 'SoldeCP_N1'; if (type === 'CP' && annee === 'N') return 'SoldeCP_N'; if (type === 'RTT' && annee === 'N') return 'SoldeRTT_N'; return null; } // ⭐ Fonction helper pour le nom du type de congé function getLeaveTypeName(typeId) { const types = { 1: 'Congé payé', 2: 'RTT', 3: 'Arrêt maladie', 4: 'Formation', 5: 'Récupération' }; return types[typeId] || 'Inconnu'; } // ⭐ Fonction helper pour formater les dates function formatDateFR(dateStr) { const date = new Date(dateStr); return date.toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric' }); } /** * Route pour SUPPRIMER une demande de congé * POST /deleteRequest */ app.post('/api/deleteRequest', async (req, res) => { const conn = await pool.getConnection(); try { await conn.beginTransaction(); const { requestId, userId, userEmail, userName, accessToken } = req.body; if (!requestId || !userId) { await conn.rollback(); conn.release(); return res.status(400).json({ success: false, message: 'Paramètres manquants (requestId ou userId)' }); } console.log('\n🗑️ === ANNULATION DEMANDE ==='); console.log('Demande ID:', requestId); console.log('User ID:', userId); // 1️⃣ Vérifier que la demande existe const [existingRequest] = await conn.query( `SELECT d.*, DATE_FORMAT(d.DateDebut, '%Y-%m-%d') as DateDebut, DATE_FORMAT(d.DateFin, '%Y-%m-%d') as DateFin FROM DemandeConge d WHERE d.Id = ? AND d.CollaborateurADId = ?`, [requestId, userId] ); if (existingRequest.length === 0) { await conn.rollback(); conn.release(); return res.status(404).json({ success: false, message: 'Demande introuvable ou non autorisée' }); } const request = existingRequest[0]; const requestStatus = request.Statut; const dateDebut = new Date(request.DateDebut); const aujourdhui = new Date(); aujourdhui.setHours(0, 0, 0, 0); dateDebut.setHours(0, 0, 0, 0); console.log(`📋 Demande trouvée: ID=${requestId}, Statut=${requestStatus}`); console.log(`📅 Date début: ${dateDebut.toLocaleDateString('fr-FR')}`); console.log(`📅 Aujourd'hui: ${aujourdhui.toLocaleDateString('fr-FR')}`); // ❌ BLOQUER SI DATE DÉJÀ PASSÉE OU AUJOURD'HUI if (dateDebut <= aujourdhui) { await conn.rollback(); conn.release(); return res.status(400).json({ success: false, message: '❌ Impossible d\'annuler : la date de début est déjà passée ou c\'est aujourd\'hui', dateDebut: formatDateWithoutUTC(dateDebut) }); } // 2️⃣ RÉCUPÉRER LA RÉPARTITION COMPLÈTE const [repartition] = await conn.query(` SELECT dct.*, tc.Nom as TypeNom FROM DemandeCongeType dct JOIN TypeConge tc ON dct.TypeCongeId = tc.Id WHERE dct.DemandeCongeId = ? ORDER BY tc.Nom `, [requestId]); console.log(`📊 Répartition de la demande:`, repartition); // 3️⃣ RESTAURER LES COMPTEURS (sauf si déjà Refusée/Annulée) if (requestStatus !== 'Refusée' && requestStatus !== 'Annulée') { console.log(`\n✅ Restauration des compteurs (Statut: ${requestStatus})`); try { const restoration = await restoreLeaveBalance(conn, requestId, userId); if (restoration.success) { console.log('✅ Compteurs restaurés:', restoration.restorations.length, 'opérations'); } } catch (restoreError) { console.error('❌ Erreur restauration:', restoreError.message); await conn.rollback(); conn.release(); return res.status(500).json({ success: false, message: 'Erreur lors de la restauration des compteurs', error: restoreError.message }); } } // 4️⃣ RÉCUPÉRER INFOS COLLABORATEUR ET MANAGER const [collabInfo] = await conn.query( 'SELECT email, prenom, nom FROM CollaborateurAD WHERE id = ?', [userId] ); const collabEmail = collabInfo.length > 0 ? collabInfo[0].email : userEmail; const collabName = collabInfo.length > 0 ? `${collabInfo[0].prenom} ${collabInfo[0].nom}` : userName; const [hierarchie] = await conn.query( `SELECT h.SuperieurId, m.email as managerEmail, m.prenom as managerPrenom, m.nom as managerNom FROM HierarchieValidationAD h LEFT JOIN CollaborateurAD m ON h.SuperieurId = m.id WHERE h.CollaborateurId = ?`, [userId] ); const managerEmail = hierarchie[0]?.managerEmail; const managerName = hierarchie[0] ? `${hierarchie[0].managerPrenom} ${hierarchie[0].managerNom}` : 'Manager'; // 5️⃣ METTRE À JOUR LE STATUT await conn.query( `UPDATE DemandeConge SET Statut = 'Annulée', DateValidation = NOW(), CommentaireValidation = CONCAT( COALESCE(CommentaireValidation, ''), '\n[Annulée par le collaborateur le ', DATE_FORMAT(NOW(), '%d/%m/%Y à %H:%i'), ']' ) WHERE Id = ?`, [requestId] ); console.log(`✅ Demande ${requestId} marquée comme Annulée`); await conn.commit(); conn.release(); // 6️⃣ ENVOI DES EMAILS let emailsSent = { collaborateur: false, manager: false }; const graphToken = await getGraphToken(); if (graphToken) { const dateDebutFormatted = new Date(request.DateDebut).toLocaleDateString('fr-FR'); const dateFinFormatted = new Date(request.DateFin).toLocaleDateString('fr-FR'); const datesPeriode = dateDebutFormatted === dateFinFormatted ? dateDebutFormatted : `du ${dateDebutFormatted} au ${dateFinFormatted}`; // 📊 FORMATER LA RÉPARTITION POUR L'EMAIL const repartitionText = repartition.map(r => ` ${r.TypeNom} : ${r.NombreJours}j ${r.PeriodeJournee !== 'Journée entière' ? `(${r.PeriodeJournee})` : ''} ` ).join(''); // 📧 EMAIL AU COLLABORATEUR if (collabEmail) { try { const subjectCollab = '✅ Confirmation d\'annulation de votre demande'; const bodyCollab = `

✅ Demande annulée

Bonjour ${collabName},

Votre demande de congé a bien été annulée.

📋 Demande annulée

${repartitionText}
Période : ${datesPeriode}
Durée totale : ${request.NombreJours} jour(s)
Répartition :
${requestStatus !== 'Refusée' && requestStatus !== 'Annulée' ? `

Vos compteurs ont été restaurés
Les jours de congé sont à nouveau disponibles dans vos soldes.

` : ''}

📧 Cet email est envoyé automatiquement, merci de ne pas y répondre.

`; await sendMailGraph(graphToken, 'gtanoreply@ensup.eu', collabEmail, subjectCollab, bodyCollab); emailsSent.collaborateur = true; console.log('✅ Email envoyé au collaborateur'); } catch (emailError) { console.error('❌ Erreur email collaborateur:', emailError.message); } } // 📧 EMAIL AU MANAGER if (managerEmail && requestStatus !== 'Refusée' && requestStatus !== 'Annulée') { try { const isValidated = requestStatus === 'Validée' || requestStatus === 'Validé'; const subjectManager = isValidated ? `🗑️ Annulation de congé validé - ${collabName}` : `🗑️ Annulation de demande - ${collabName}`; const bodyManager = `

🗑️ Annulation de ${isValidated ? 'congé' : 'demande'}

Bonjour ${managerName},

${collabName} a annulé ${isValidated ? 'son congé validé' : 'sa demande de congé en attente'}.

📋 Demande annulée

${repartitionText}
Statut initial : ${requestStatus}
Période : ${datesPeriode}
Durée totale : ${request.NombreJours} jour(s)
Répartition :
${isValidated ? `

⚠️ Attention : Ce congé avait été validé.
Les compteurs du collaborateur ont été automatiquement restaurés.

` : `

ℹ️ Cette demande était en attente de validation.
Les compteurs ont été restaurés.

`}

📧 Cet email est envoyé automatiquement, merci de ne pas y répondre.

`; await sendMailGraph(graphToken, 'gtanoreply@ensup.eu', managerEmail, subjectManager, bodyManager); emailsSent.manager = true; console.log('✅ Email envoyé au manager'); } catch (emailError) { console.error('❌ Erreur email manager:', emailError.message); } } else if (!managerEmail) { console.log('⚠️ Email manager introuvable'); } } else { console.warn('⚠️ Impossible d\'obtenir un token Graph - emails non envoyés'); } // RETOURNER LA RÉPONSE res.json({ success: true, message: 'Demande annulée avec succès', counterRestored: requestStatus !== 'Refusée' && requestStatus !== 'Annulée', emailsSent: emailsSent, managerNotified: requestStatus !== 'Refusée' && requestStatus !== 'Annulée', repartition: repartition.map(r => ({ type: r.TypeNom, jours: r.NombreJours, periode: r.PeriodeJournee })) }); } catch (error) { await conn.rollback(); if (conn) conn.release(); console.error('❌ Erreur deleteRequest:', error); res.status(500).json({ success: false, message: 'Erreur lors de l\'annulation', error: error.message }); } }); app.get('/api/exportCompteurs', async (req, res) => { try { const dateRef = req.query.dateRef || new Date().toISOString().split('T')[0]; const conn = await pool.getConnection(); const [collaborateurs] = await pool.query(` SELECT ca.id, ca.prenom, ca.nom, ca.email, ca.role, ca.TypeContrat, ca.DateEntree, s.Nom as service, ca.CampusId, ca.SocieteId, so.Nom as societe_nom FROM CollaborateurAD ca LEFT JOIN Services s ON ca.ServiceId = s.Id LEFT JOIN Societe so ON ca.SocieteId = so.Id WHERE (ca.actif = 1 OR ca.actif IS NULL) ORDER BY ca.nom, ca.prenom `); const rapport = []; for (const collab of collaborateurs) { const dateEntree = collab.DateEntree; const dateReference = new Date(dateRef); const acquisCP = calculerAcquisitionCP(dateReference, dateEntree); let acquisRTT = 0; if (collab.role !== 'Apprenti') { const rttData = await calculerAcquisitionRTT(conn, collab.id, dateReference); acquisRTT = rttData.acquisition; } const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']); const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']); let soldeCP = 0; let soldeRTT = 0; if (cpType.length > 0) { const [compteurCP] = await conn.query( 'SELECT Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?', [collab.id, cpType[0].Id, dateReference.getFullYear()] ); soldeCP = compteurCP.length > 0 ? parseFloat(compteurCP[0].Solde) : 0; } if (rttType.length > 0 && collab.role !== 'Apprenti') { const [compteurRTT] = await conn.query( 'SELECT Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?', [collab.id, rttType[0].Id, dateReference.getFullYear()] ); soldeRTT = compteurRTT.length > 0 ? parseFloat(compteurRTT[0].Solde) : 0; } rapport.push({ id: collab.id, prenom: collab.prenom, nom: collab.nom, email: collab.email, role: collab.role, service: collab.service, societe_id: collab.SocieteId, societe_nom: collab.societe_nom, type_contrat: collab.TypeContrat, date_entree: dateEntree ? formatDateWithoutUTC(dateEntree) : null, cp_acquis: parseFloat(acquisCP.toFixed(2)), cp_solde: parseFloat(soldeCP.toFixed(2)), rtt_acquis: parseFloat(acquisRTT.toFixed(2)), rtt_solde: parseFloat(soldeRTT.toFixed(2)), date_reference: dateRef }); } conn.release(); res.json({ success: true, date_reference: dateRef, total_collaborateurs: rapport.length, rapport: rapport }); } catch (error) { console.error('Erreur exportCompteurs:', error); res.status(500).json({ success: false, message: 'Erreur serveur', error: error.message }); } }); function isInPeriodeAnticipation(dateDebut, typeConge) { const date = new Date(dateDebut); const year = date.getFullYear(); const month = date.getMonth() + 1; // 1-12 if (typeConge === 'CP') { // CP : 01/06 année N → 31/05 année N+1 // Période anticipation : du 01/06 de l'année suivante return month >= 6; // Si >= juin, c'est pour l'exercice N+1 } else if (typeConge === 'RTT') { // RTT : 01/01 année N → 31/12 année N // Pas d'anticipation possible car année civile return month >= 1 && month <= 12; } return false; } function getAnneeCompteur(dateDebut, typeConge) { const date = new Date(dateDebut); const year = date.getFullYear(); const month = date.getMonth() + 1; if (typeConge === 'CP') { // Si date entre 01/06 et 31/12 → année N // Si date entre 01/01 et 31/05 → année N-1 (exercice précédent) return month >= 6 ? year : year - 1; } else { // RTT : toujours année civile return year; } } /** * Vérifie la disponibilité des soldes pour une demande * Retourne : { available: boolean, details: {}, useN1: boolean } */ async function checkSoldesDisponiblesMixte(conn, collaborateurId, repartition, dateDebut, isApprenti) { const today = new Date(); today.setHours(0, 0, 0, 0); const currentYear = today.getFullYear(); const dateDemandeObj = new Date(dateDebut); dateDemandeObj.setHours(0, 0, 0, 0); const demandeYear = dateDemandeObj.getFullYear(); const demandeMonth = dateDemandeObj.getMonth() + 1; console.log('\n🔍 === CHECK SOLDES MIXTE (AVEC ANTICIPATION) ==='); console.log('📅 Date AUJOURD\'HUI:', today.toISOString().split('T')[0]); console.log('📅 Date DEMANDE:', dateDebut); console.log('📅 Année demande:', demandeYear, '/ Mois:', demandeMonth); console.log('📅 Année actuelle:', currentYear); let totalDisponible = 0; let totalNecessaire = 0; const details = {}; for (const rep of repartition) { const typeCode = rep.TypeConge; const joursNecessaires = parseFloat(rep.NombreJours || 0); // Ignorer ABS et Formation if (typeCode === 'ABS' || typeCode === 'Formation') { continue; } totalNecessaire += joursNecessaires; if (typeCode === 'CP') { // ⭐ RÉCUPÉRER LES INFOS COLLABORATEUR const [collabInfo] = await conn.query( `SELECT DateEntree FROM CollaborateurAD WHERE id = ?`, [collaborateurId] ); const dateEntree = collabInfo[0]?.DateEntree; // ⭐ CALCULER L'ACQUISITION JUSQU'À LA DATE DEMANDÉE const acquisALaDate = calculerAcquisitionCP(dateDemandeObj, dateEntree); console.log('💰 Acquisition CP à la date', dateDebut, ':', acquisALaDate.toFixed(2), 'j'); // ⭐ RÉCUPÉRER LE REPORTÉ N-1 const previousYear = currentYear - 1; const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']); const cpTypeId = cpType[0].Id; const [compteurN1] = await conn.query(` SELECT SoldeReporte FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? `, [collaborateurId, cpTypeId, previousYear]); const reporteN1 = compteurN1.length > 0 ? parseFloat(compteurN1[0].SoldeReporte || 0) : 0; // ⭐ RÉCUPÉRER CE QUI A DÉJÀ ÉTÉ POSÉ (toutes demandes validées ou en attente) const [totalPose] = await conn.query(` SELECT COALESCE(SUM(dct.NombreJours), 0) as totalPose FROM DemandeConge dc JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId JOIN TypeConge tc ON dct.TypeCongeId = tc.Id WHERE dc.CollaborateurADId = ? AND tc.Nom = 'Congé payé' AND dc.Statut IN ('Validée', 'En attente') AND dc.DateDebut <= ? `, [collaborateurId, dateDebut]); const dejaPose = parseFloat(totalPose[0].totalPose || 0); // ⭐ SOLDE RÉEL = Reporté N-1 + Acquisition - Déjà posé const soldeReel = reporteN1 + acquisALaDate - dejaPose; console.log('💰 Soldes CP détaillés:', { reporteN1: reporteN1.toFixed(2), acquisALaDate: acquisALaDate.toFixed(2), dejaPose: dejaPose.toFixed(2), soldeReel: soldeReel.toFixed(2) }); details.CP = { reporteN1: reporteN1, acquisALaDate: acquisALaDate, dejaPose: dejaPose, soldeReel: soldeReel, necessaire: joursNecessaires }; totalDisponible += Math.max(0, soldeReel); if (soldeReel < joursNecessaires) { return { available: false, message: `Solde CP insuffisant (${Math.max(0, soldeReel).toFixed(2)}j disponibles avec anticipation, ${joursNecessaires}j demandés)`, details, manque: joursNecessaires - soldeReel }; } } else if (typeCode === 'RTT') { if (isApprenti) { return { available: false, message: 'Les apprentis ne peuvent pas poser de RTT', details }; } // ⭐ CALCUL RTT (utiliser la fonction existante) const rttData = await calculerAcquisitionRTT(conn, collaborateurId, dateDemandeObj); const acquisALaDate = rttData.acquisition; console.log('💰 Acquisition RTT à la date', dateDebut, ':', acquisALaDate.toFixed(2), 'j'); // ⭐ RÉCUPÉRER CE QUI A DÉJÀ ÉTÉ POSÉ const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']); const rttTypeId = rttType[0].Id; const [totalPose] = await conn.query(` SELECT COALESCE(SUM(dct.NombreJours), 0) as totalPose FROM DemandeConge dc JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId JOIN TypeConge tc ON dct.TypeCongeId = tc.Id WHERE dc.CollaborateurADId = ? AND tc.Nom = 'RTT' AND dc.Statut IN ('Validée', 'En attente') AND dc.DateDebut <= ? `, [collaborateurId, dateDebut]); const dejaPose = parseFloat(totalPose[0].totalPose || 0); // ⭐ SOLDE RÉEL = Acquisition - Déjà posé const soldeReel = acquisALaDate - dejaPose; console.log('💰 Soldes RTT détaillés:', { acquisALaDate: acquisALaDate.toFixed(2), dejaPose: dejaPose.toFixed(2), soldeReel: soldeReel.toFixed(2) }); details.RTT = { acquisALaDate: acquisALaDate, dejaPose: dejaPose, soldeReel: soldeReel, necessaire: joursNecessaires }; totalDisponible += Math.max(0, soldeReel); if (soldeReel < joursNecessaires) { return { available: false, message: `Solde RTT insuffisant (${Math.max(0, soldeReel).toFixed(2)}j disponibles avec anticipation, ${joursNecessaires}j demandés)`, details, manque: joursNecessaires - soldeReel }; } } else if (typeCode === 'Récup') { const [recupType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Récupération']); if (recupType.length === 0) continue; const recupTypeId = recupType[0].Id; const [compteur] = await conn.query( `SELECT Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, [collaborateurId, recupTypeId, currentYear] ); const soldeRecup = compteur.length > 0 ? parseFloat(compteur[0].Solde || 0) : 0; console.log('💰 Solde Récup:', soldeRecup.toFixed(2), 'j'); details.Recup = { soldeN: soldeRecup, total: soldeRecup, necessaire: joursNecessaires }; totalDisponible += Math.min(joursNecessaires, soldeRecup); if (soldeRecup < joursNecessaires) { return { available: false, message: `Solde Récupération insuffisant (${soldeRecup.toFixed(2)}j disponibles, ${joursNecessaires}j demandés)`, details, manque: joursNecessaires - soldeRecup }; } } } console.log('\n✅ Check final:', { totalDisponible: totalDisponible.toFixed(2), totalNecessaire: totalNecessaire.toFixed(2), available: totalDisponible >= totalNecessaire }); return { available: totalDisponible >= totalNecessaire, details, totalDisponible, totalNecessaire }; } // ======================================== // FONCTIONS HELPER // ======================================== async function getSoldesCP(conn, collaborateurId, dateEntree, includeN1Anticipe = false) { const currentYear = new Date().getFullYear(); const previousYear = currentYear - 1; console.log(`\n📊 getSoldesCP - includeN1Anticipe: ${includeN1Anticipe}`); const [cpType] = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = 'Congé payé' LIMIT 1`); const typeCongeId = cpType[0].Id; // N-1 (reporté) const [compteursN1] = await conn.query( `SELECT Solde, SoldeReporte FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, [collaborateurId, typeCongeId, previousYear] ); const soldeN1 = compteursN1.length > 0 ? parseFloat(compteursN1[0].SoldeReporte || 0) : 0; // N (actuel) const [compteursN] = await conn.query( `SELECT Solde, SoldeReporte, Total FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, [collaborateurId, typeCongeId, currentYear] ); const soldeN = compteursN.length > 0 ? parseFloat(compteursN[0].Solde || 0) - parseFloat(compteursN[0].SoldeReporte || 0) : 0; const totalAcquisN = compteursN.length > 0 ? parseFloat(compteursN[0].Total || 0) : 0; // Anticipation N const finExerciceN = new Date(currentYear + 1, 4, 31); // 31 mai N+1 const acquisTotaleN = calculerAcquisitionCP(finExerciceN, dateEntree); const soldeAnticipeN = Math.max(0, acquisTotaleN - totalAcquisN); console.log(' N-1:', soldeN1); console.log(' N:', soldeN); console.log(' Anticipé N:', soldeAnticipeN); // ⭐ Anticipation N+1 (si demandé) let soldeAnticipeN1 = 0; if (includeN1Anticipe) { const debutExerciceN1 = new Date(currentYear + 1, 5, 1); // 01 juin N+1 const finExerciceN1 = new Date(currentYear + 2, 4, 31); // 31 mai N+2 let dateCalculN1 = debutExerciceN1; if (dateEntree && new Date(dateEntree) > debutExerciceN1) { dateCalculN1 = new Date(dateEntree); } const acquisTotaleN1 = calculerAcquisitionCP(finExerciceN1, dateCalculN1); soldeAnticipeN1 = acquisTotaleN1; console.log(' Anticipé N+1:', soldeAnticipeN1); } return { soldeN1, soldeN, soldeAnticipeN, soldeAnticipeN1 }; } async function getSoldesRTT(conn, collaborateurId, typeContrat, dateEntree) { const currentYear = new Date().getFullYear(); const rttType = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = 'RTT' LIMIT 1`); const typeCongeId = rttType[0].Id; const compteursN = await conn.query( `SELECT Solde, Total FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, [collaborateurId, typeCongeId, currentYear] ); const soldeN = compteursN.length > 0 ? parseFloat(compteursN[0].Solde || 0) : 0; const totalAcquisN = compteursN.length > 0 ? parseFloat(compteursN[0].Total || 0) : 0; // Calcul anticipation N const finAnneeN = new Date(currentYear, 11, 31); // 31 déc N const rttDataTotalN = await calculerAcquisitionRTT(conn, collaborateurId, finAnneeN); const soldeAnticipeN = Math.max(0, rttDataTotalN.acquisition - totalAcquisN); return { soldeN, soldeAnticipeN }; } async function getSoldesRecup(conn, collaborateurId) { const currentYear = new Date().getFullYear(); const recupType = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = 'Récupération' LIMIT 1`); if (recupType.length === 0) return 0; const typeCongeId = recupType[0].Id; const compteur = await conn.query( `SELECT Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, [collaborateurId, typeCongeId, currentYear] ); return compteur.length > 0 ? parseFloat(compteur[0].Solde || 0) : 0; } app.get('/api/getAnticipationDisponible', async (req, res) => { try { const { userid, dateDebut } = req.query; if (!userid || !dateDebut) { return res.json({ success: false, message: 'Paramètres manquants' }); } const conn = await pool.getConnection(); const [collabInfo] = await conn.query( `SELECT DateEntree, TypeContrat, role FROM CollaborateurAD WHERE id = ?`, [userid] ); const dateEntree = collabInfo.DateEntree; const isApprenti = collabInfo.role === 'Apprenti'; // Déterminer si c'est une demande N+1 const dateDemandeObj = new Date(dateDebut); const currentYear = new Date().getFullYear(); const demandeYear = dateDemandeObj.getFullYear(); const demandeMonth = dateDemandeObj.getMonth() + 1; const isN1 = (demandeYear === currentYear + 1 && demandeMonth >= 6) || (demandeYear === currentYear + 2 && demandeMonth <= 5); // Calculer les soldes avec anticipation const soldesCP = await getSoldesCP(conn, userid, dateEntree, isN1); const soldesRTT = isApprenti ? { soldeN: 0, soldeAnticipeN: 0 } : await getSoldesRTT(conn, userid, collabInfo.TypeContrat, dateEntree); const soldesRecup = await getSoldesRecup(conn, userid); conn.release(); res.json({ success: true, isN1Request: isN1, CP: { actuel: soldesCP.soldeN1 + soldesCP.soldeN, anticipeN: soldesCP.soldeAnticipeN, anticipeN1: isN1 ? soldesCP.soldeAnticipeN1 : 0, total: soldesCP.soldeN1 + soldesCP.soldeN + soldesCP.soldeAnticipeN + (isN1 ? soldesCP.soldeAnticipeN1 : 0) }, RTT: { actuel: soldesRTT.soldeN, anticipeN: soldesRTT.soldeAnticipeN, total: soldesRTT.soldeN + soldesRTT.soldeAnticipeN }, Recup: { actuel: soldesRecup, total: soldesRecup } }); } catch (error) { console.error('Erreur getAnticipationDisponible:', error); res.status(500).json({ success: false, message: error.message }); } }); async function deductLeaveBalanceWithN1(conn, collaborateurId, typeCongeId, nombreJours, demandeCongeId, dateDebut) { const currentYear = new Date().getFullYear(); const previousYear = currentYear - 1; const nextYear = currentYear + 1; let joursRestants = nombreJours; const deductions = []; const dateDemandeObj = new Date(dateDebut); const demandeYear = dateDemandeObj.getFullYear(); const demandeMonth = dateDemandeObj.getMonth() + 1; // Déterminer le type de congé const [typeRow] = await conn.query('SELECT Nom FROM TypeConge WHERE Id = ?', [typeCongeId]); const typeNom = typeRow[0].Nom; const isCP = typeNom === 'Congé payé'; // Vérifier si demande pour N+1 let useN1 = false; if (isCP) { useN1 = (demandeYear === nextYear && demandeMonth >= 6) || (demandeYear === nextYear + 1 && demandeMonth <= 5); } else { useN1 = demandeYear === nextYear; } console.log(`\n💰 Déduction ${typeNom}: ${nombreJours}j (useN1: ${useN1})`); if (useN1) { // ORDRE N+1 : N+1 anticipé → N anticipé → N actuel → N-1 // 1. N+1 Anticipé (priorité absolue) const compteurN1Anticipe = await conn.query( `SELECT Id, SoldeAnticipe FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, [collaborateurId, typeCongeId, nextYear] ); if (compteurN1Anticipe.length === 0 && joursRestants > 0) { // Créer le compteur N+1 si inexistant await conn.query( `INSERT INTO CompteurConges (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, SoldeAnticipe, IsAnticipe) VALUES (?, ?, ?, 0, 0, 0, 0, 0)`, [collaborateurId, typeCongeId, nextYear] ); } // Récupérer à nouveau après création const compteurN1A = await conn.query( `SELECT Id, SoldeAnticipe FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, [collaborateurId, typeCongeId, nextYear] ); if (compteurN1A.length > 0) { const aDeduire = Math.min(joursRestants, joursRestants); // Tous les jours restants if (aDeduire > 0) { await conn.query( `UPDATE CompteurConges SET SoldeAnticipe = SoldeAnticipe + ?, IsAnticipe = 1 WHERE Id = ?`, [aDeduire, compteurN1A[0].Id] ); await conn.query( `INSERT INTO DeductionDetails (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) VALUES (?, ?, ?, 'N+1 Anticipé', ?)`, [demandeCongeId, typeCongeId, nextYear, aDeduire] ); deductions.push({ annee: nextYear, type: 'N+1 Anticipé', joursUtilises: aDeduire }); joursRestants -= aDeduire; console.log(`✓ N+1 Anticipé: ${aDeduire}j - reste: ${joursRestants}j`); } } // 2. N anticipé if (joursRestants > 0) { const [compteurN_Anticipe] = await conn.query(` SELECT Id, SoldeAnticipe FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? `, [collaborateurId, typeCongeId, currentYear]); if (compteurN_Anticipe.length > 0) { const soldeNA = parseFloat(compteurN_Anticipe[0].SoldeAnticipe || 0); const aDeduire = Math.min(soldeNA, joursRestants); if (aDeduire > 0) { await conn.query(` UPDATE CompteurConges SET SoldeAnticipe = GREATEST(0, SoldeAnticipe - ?) WHERE Id = ? `, [aDeduire, compteurN_Anticipe[0].Id]); await conn.query(` INSERT INTO DeductionDetails (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) VALUES (?, ?, ?, 'N Anticipé', ?) `, [demandeCongeId, typeCongeId, currentYear, aDeduire]); deductions.push({ annee: currentYear, type: 'N Anticipé', joursUtilises: aDeduire, soldeAvant: soldeNA }); joursRestants -= aDeduire; console.log(` ✓ N Anticipé: ${aDeduire}j (reste: ${joursRestants}j)`); } } } // 3. N actuel if (joursRestants > 0) { const [compteurN] = await conn.query(` SELECT Id, Solde, SoldeReporte FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? `, [collaborateurId, typeCongeId, currentYear]); if (compteurN.length > 0) { const soldeN = parseFloat(compteurN[0].Solde) - parseFloat(compteurN[0].SoldeReporte || 0); const aDeduire = Math.min(soldeN, joursRestants); if (aDeduire > 0) { await conn.query(` UPDATE CompteurConges SET Solde = GREATEST(0, Solde - ?) WHERE Id = ? `, [aDeduire, compteurN[0].Id]); await conn.query(` INSERT INTO DeductionDetails (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) VALUES (?, ?, ?, 'Année N', ?) `, [demandeCongeId, typeCongeId, currentYear, aDeduire]); deductions.push({ annee: currentYear, type: 'Année N', joursUtilises: aDeduire, soldeAvant: soldeN }); joursRestants -= aDeduire; console.log(` ✓ Année N: ${aDeduire}j (reste: ${joursRestants}j)`); } } } // 4. N-1 reporté if (joursRestants > 0) { const [compteurN1] = await conn.query(` SELECT Id, SoldeReporte FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? `, [collaborateurId, typeCongeId, previousYear]); if (compteurN1.length > 0) { const soldeN1 = parseFloat(compteurN1[0].SoldeReporte || 0); const aDeduire = Math.min(soldeN1, joursRestants); if (aDeduire > 0) { await conn.query(` UPDATE CompteurConges SET SoldeReporte = GREATEST(0, SoldeReporte - ?), Solde = GREATEST(0, Solde - ?) WHERE Id = ? `, [aDeduire, aDeduire, compteurN1[0].Id]); await conn.query(` INSERT INTO DeductionDetails (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) VALUES (?, ?, ?, 'Reporté N-1', ?) `, [demandeCongeId, typeCongeId, previousYear, aDeduire]); deductions.push({ annee: previousYear, type: 'Reporté N-1', joursUtilises: aDeduire, soldeAvant: soldeN1 }); joursRestants -= aDeduire; console.log(` ✓ Reporté N-1: ${aDeduire}j (reste: ${joursRestants}j)`); } } } } else { // ORDRE NORMAL : N-1 → N → N anticipé // 1. Reporté N-1 const [compteurN1] = await conn.query(` SELECT Id, SoldeReporte, Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? `, [collaborateurId, typeCongeId, previousYear]); if (compteurN1.length > 0 && compteurN1[0].SoldeReporte > 0) { const soldeN1 = parseFloat(compteurN1[0].SoldeReporte); const aDeduire = Math.min(soldeN1, joursRestants); if (aDeduire > 0) { await conn.query(` UPDATE CompteurConges SET SoldeReporte = GREATEST(0, SoldeReporte - ?), Solde = GREATEST(0, Solde - ?) WHERE Id = ? `, [aDeduire, aDeduire, compteurN1[0].Id]); await conn.query(` INSERT INTO DeductionDetails (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) VALUES (?, ?, ?, 'Reporté N-1', ?) `, [demandeCongeId, typeCongeId, previousYear, aDeduire]); deductions.push({ annee: previousYear, type: 'Reporté N-1', joursUtilises: aDeduire, soldeAvant: soldeN1 }); joursRestants -= aDeduire; console.log(` ✓ Reporté N-1: ${aDeduire}j (reste: ${joursRestants}j)`); } } // 2. Année N if (joursRestants > 0) { const [compteurN] = await conn.query(` SELECT Id, Solde, SoldeReporte FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? `, [collaborateurId, typeCongeId, currentYear]); if (compteurN.length > 0) { const soldeN = parseFloat(compteurN[0].Solde) - parseFloat(compteurN[0].SoldeReporte || 0); const aDeduire = Math.min(soldeN, joursRestants); if (aDeduire > 0) { await conn.query(` UPDATE CompteurConges SET Solde = GREATEST(0, Solde - ?) WHERE Id = ? `, [aDeduire, compteurN[0].Id]); await conn.query(` INSERT INTO DeductionDetails (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) VALUES (?, ?, ?, 'Année N', ?) `, [demandeCongeId, typeCongeId, currentYear, aDeduire]); deductions.push({ annee: currentYear, type: 'Année N', joursUtilises: aDeduire, soldeAvant: soldeN }); joursRestants -= aDeduire; console.log(` ✓ Année N: ${aDeduire}j (reste: ${joursRestants}j)`); } } } // 3. N anticipé if (joursRestants > 0) { const [compteurN_Anticipe] = await conn.query(` SELECT Id, SoldeAnticipe FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? `, [collaborateurId, typeCongeId, currentYear]); if (compteurN_Anticipe.length > 0) { const soldeNA = parseFloat(compteurN_Anticipe[0].SoldeAnticipe || 0); const aDeduire = Math.min(soldeNA, joursRestants); if (aDeduire > 0) { await conn.query(` UPDATE CompteurConges SET SoldeAnticipe = GREATEST(0, SoldeAnticipe - ?) WHERE Id = ? `, [aDeduire, compteurN_Anticipe[0].Id]); await conn.query(` INSERT INTO DeductionDetails (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) VALUES (?, ?, ?, 'N Anticipé', ?) `, [demandeCongeId, typeCongeId, currentYear, aDeduire]); deductions.push({ annee: currentYear, type: 'N Anticipé', joursUtilises: aDeduire, soldeAvant: soldeNA }); joursRestants -= aDeduire; console.log(` ✓ N Anticipé: ${aDeduire}j (reste: ${joursRestants}j)`); } } } } return { success: joursRestants === 0, joursDeduitsTotal: nombreJours - joursRestants, joursNonDeduits: joursRestants, details: deductions, useN1: useN1 }; } /** * Met à jour les soldes anticipés pour un collaborateur * Appelée après chaque mise à jour de compteur ou soumission de demande */ async function updateSoldeAnticipe(conn, collaborateurId) { const today = new Date(); today.setHours(0, 0, 0, 0); const currentYear = today.getFullYear(); console.log(`\n🔄 Mise à jour soldes anticipés pour collaborateur ${collaborateurId}`); const [collab] = await conn.query( 'SELECT DateEntree, TypeContrat, role FROM CollaborateurAD WHERE id = ?', [collaborateurId] ); if (collab.length === 0) { console.log(' ❌ Collaborateur non trouvé'); return; } const dateEntree = collab[0].DateEntree; const typeContrat = collab[0].TypeContrat || '37h'; const isApprenti = collab[0].role === 'Apprenti'; // ===== CP ANTICIPÉ ===== const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']); if (cpType.length > 0) { const cpAnticipe = calculerAcquisitionCPAnticipee(today, dateEntree); // Vérifier si le compteur existe const [compteurCP] = await conn.query(` SELECT Id FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? `, [collaborateurId, cpType[0].Id, currentYear]); if (compteurCP.length > 0) { await conn.query(` UPDATE CompteurConges SET SoldeAnticipe = ?, DerniereMiseAJour = NOW() WHERE Id = ? `, [cpAnticipe, compteurCP[0].Id]); } else { // Créer le compteur s'il n'existe pas const acquisCP = calculerAcquisitionCP(today, dateEntree); await conn.query(` INSERT INTO CompteurConges (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, SoldeAnticipe, DerniereMiseAJour) VALUES (?, ?, ?, ?, ?, 0, ?, NOW()) `, [collaborateurId, cpType[0].Id, currentYear, acquisCP, acquisCP, cpAnticipe]); } console.log(` ✓ CP Anticipé: ${cpAnticipe.toFixed(2)}j`); } // ===== RTT ANTICIPÉ ===== if (!isApprenti) { const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']); if (rttType.length > 0) { const rttAnticipe = await calculerAcquisitionRTTAnticipee(conn, collaborateurId, today); // Vérifier si le compteur existe const [compteurRTT] = await conn.query(` SELECT Id FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? `, [collaborateurId, rttType[0].Id, currentYear]); if (compteurRTT.length > 0) { await conn.query(` UPDATE CompteurConges SET SoldeAnticipe = ?, DerniereMiseAJour = NOW() WHERE Id = ? `, [rttAnticipe, compteurRTT[0].Id]); } else { // Créer le compteur s'il n'existe pas const rttData = await calculerAcquisitionRTT(conn, collaborateurId, today); await conn.query(` INSERT INTO CompteurConges (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, SoldeAnticipe, DerniereMiseAJour) VALUES (?, ?, ?, ?, ?, 0, ?, NOW()) `, [collaborateurId, rttType[0].Id, currentYear, rttData.acquisition, rttData.acquisition, rttAnticipe]); } console.log(` ✓ RTT Anticipé: ${rttAnticipe.toFixed(2)}j`); } } console.log(` ✅ Soldes anticipés mis à jour\n`); } /** * GET /getSoldesAnticipes * Retourne les soldes actuels ET anticipés pour un collaborateur */ app.get('/api/getSoldesAnticipes', async (req, res) => { try { const userIdParam = req.query.user_id; const dateRefParam = req.query.date_reference; if (!userIdParam) { return res.json({ success: false, message: 'ID utilisateur manquant' }); } const conn = await pool.getConnection(); // Déterminer l'ID const isUUID = userIdParam.length > 10 && userIdParam.includes('-'); const userQuery = ` SELECT ca.id, ca.prenom, ca.nom, ca.DateEntree, ca.TypeContrat, ca.role FROM CollaborateurAD ca WHERE ${isUUID ? 'ca.entraUserId' : 'ca.id'} = ? AND (ca.Actif = 1 OR ca.Actif IS NULL) `; const [userInfo] = await conn.query(userQuery, [userIdParam]); if (userInfo.length === 0) { conn.release(); return res.json({ success: false, message: 'Utilisateur non trouvé' }); } const user = userInfo[0]; const userId = user.id; const dateEntree = user.DateEntree; const typeContrat = user.TypeContrat || '37h'; const isApprenti = user.role === 'Apprenti'; const dateReference = dateRefParam ? new Date(dateRefParam) : new Date(); dateReference.setHours(0, 0, 0, 0); const currentYear = dateReference.getFullYear(); // ===== CP ===== const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']); let cpData = { acquis: 0, solde: 0, anticipe: 0, totalDisponible: 0 }; if (cpType.length > 0) { // Acquisition actuelle const acquisCP = calculerAcquisitionCP(dateReference, dateEntree); // Anticipé const anticipeCP = calculerAcquisitionCPAnticipee(dateReference, dateEntree); // Solde en base (avec consommations déduites) const [compteurCP] = await conn.query(` SELECT Solde, SoldeReporte, SoldeAnticipe FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? `, [userId, cpType[0].Id, currentYear]); // Reporté N-1 const [compteurCPN1] = await conn.query(` SELECT Solde as SoldeReporte FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? `, [userId, cpType[0].Id, currentYear - 1]); const soldeN1 = compteurCPN1.length > 0 ? parseFloat(compteurCPN1[0].SoldeReporte || 0) : 0; const soldeN = compteurCP.length > 0 ? parseFloat(compteurCP[0].Solde || 0) : acquisCP; cpData = { acquis: parseFloat(acquisCP.toFixed(2)), soldeN1: parseFloat(soldeN1.toFixed(2)), soldeN: parseFloat((soldeN - soldeN1).toFixed(2)), soldeTotal: parseFloat(soldeN.toFixed(2)), anticipe: parseFloat(anticipeCP.toFixed(2)), totalDisponible: parseFloat((soldeN + anticipeCP).toFixed(2)) }; } // ===== RTT ===== let rttData = { acquis: 0, solde: 0, anticipe: 0, totalDisponible: 0, isApprenti: isApprenti }; if (!isApprenti) { const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']); if (rttType.length > 0) { // Acquisition actuelle const rttCalc = await calculerAcquisitionRTT(conn, userId, dateReference); const acquisRTT = rttCalc.acquisition; // Anticipé const anticipeRTT = await calculerAcquisitionRTTAnticipee(conn, userId, dateReference); // Solde en base const [compteurRTT] = await conn.query(` SELECT Solde, SoldeAnticipe FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? `, [userId, rttType[0].Id, currentYear]); const soldeRTT = compteurRTT.length > 0 ? parseFloat(compteurRTT[0].Solde || 0) : acquisRTT; rttData = { acquis: parseFloat(acquisRTT.toFixed(2)), solde: parseFloat(soldeRTT.toFixed(2)), anticipe: parseFloat(anticipeRTT.toFixed(2)), totalDisponible: parseFloat((soldeRTT + anticipeRTT).toFixed(2)), config: rttCalc.config, typeContrat: typeContrat }; } } // ===== RÉCUP ===== const [recupType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Récupération']); let recupData = { solde: 0 }; if (recupType.length > 0) { const [compteurRecup] = await conn.query(` SELECT Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? `, [userId, recupType[0].Id, currentYear]); recupData.solde = compteurRecup.length > 0 ? parseFloat(compteurRecup[0].Solde || 0) : 0; } conn.release(); res.json({ success: true, user: { id: user.id, nom: `${user.prenom} ${user.nom}`, typeContrat: typeContrat, dateEntree: dateEntree ? formatDateWithoutUTC(dateEntree) : null }, dateReference: dateReference.toISOString().split('T')[0], cp: cpData, rtt: rttData, recup: recupData, totalGeneral: { disponibleActuel: parseFloat((cpData.soldeTotal + rttData.solde + recupData.solde).toFixed(2)), disponibleAvecAnticipe: parseFloat((cpData.totalDisponible + rttData.totalDisponible + recupData.solde).toFixed(2)) } }); } catch (error) { console.error('Erreur getSoldesAnticipes:', error); res.status(500).json({ success: false, message: 'Erreur serveur', error: error.message }); } }); /** * GET /getCongesAnticipes * Calcule les congés anticipés disponibles pour un collaborateur */ app.get('/api/getCongesAnticipes', async (req, res) => { try { const userIdParam = req.query.user_id; if (!userIdParam) { return res.json({ success: false, message: 'ID utilisateur manquant' }); } const conn = await pool.getConnection(); // Déterminer l'ID (UUID ou numérique) const isUUID = userIdParam.length > 10 && userIdParam.includes('-'); const userQuery = ` SELECT ca.id, ca.prenom, ca.nom, ca.email, ca.DateEntree, ca.TypeContrat, ca.role, ca.CampusId FROM CollaborateurAD ca WHERE ${isUUID ? 'ca.entraUserId' : 'ca.id'} = ? AND (ca.Actif = 1 OR ca.Actif IS NULL) `; const [userInfo] = await conn.query(userQuery, [userIdParam]); if (userInfo.length === 0) { conn.release(); return res.json({ success: false, message: 'Utilisateur non trouvé' }); } const user = userInfo[0]; const userId = user.id; const dateEntree = user.DateEntree; const typeContrat = user.TypeContrat || '37h'; const today = new Date(); const currentYear = today.getFullYear(); const finAnnee = new Date(currentYear, 11, 31); // 31 décembre // ======================================== // CALCUL CP (Congés Payés) // ======================================== // Acquisition actuelle const acquisActuelleCP = calculerAcquisitionCP(today, dateEntree); // Acquisition prévue à la fin de l'exercice (31 mai N+1) const finExerciceCP = new Date(currentYear + 1, 4, 31); // 31 mai N+1 const acquisTotaleCP = calculerAcquisitionCP(finExerciceCP, dateEntree); // Récupérer le solde actuel const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']); let soldeActuelCP = 0; let dejaPrisCP = 0; if (cpType.length > 0) { const [compteurCP] = await conn.query(` SELECT Total, Solde, SoldeReporte FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? `, [userId, cpType[0].Id, currentYear]); if (compteurCP.length > 0) { const total = parseFloat(compteurCP[0].Total || 0); soldeActuelCP = parseFloat(compteurCP[0].Solde || 0); dejaPrisCP = total - (soldeActuelCP - parseFloat(compteurCP[0].SoldeReporte || 0)); } } // Calculer le potentiel anticipé pour CP const acquisRestanteCP = acquisTotaleCP - acquisActuelleCP; const anticipePossibleCP = Math.max(0, acquisRestanteCP); const limiteAnticipeCP = Math.min(anticipePossibleCP, 25 - dejaPrisCP); // ======================================== // CALCUL RTT // ======================================== let anticipePossibleRTT = 0; let limiteAnticipeRTT = 0; let soldeActuelRTT = 0; let dejaPrisRTT = 0; let acquisActuelleRTT = 0; let acquisTotaleRTT = 0; if (user.role !== 'Apprenti') { // Acquisition actuelle const rttDataActuel = await calculerAcquisitionRTT(conn, userId, today); acquisActuelleRTT = rttDataActuel.acquisition; // Acquisition prévue à la fin de l'année const rttDataTotal = await calculerAcquisitionRTT(conn, userId, finAnnee); acquisTotaleRTT = rttDataTotal.acquisition; // Récupérer le solde actuel const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']); if (rttType.length > 0) { const [compteurRTT] = await conn.query(` SELECT Total, Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? `, [userId, rttType[0].Id, currentYear]); if (compteurRTT.length > 0) { const total = parseFloat(compteurRTT[0].Total || 0); soldeActuelRTT = parseFloat(compteurRTT[0].Solde || 0); dejaPrisRTT = total - soldeActuelRTT; } } // Calculer le potentiel anticipé pour RTT const acquisRestanteRTT = acquisTotaleRTT - acquisActuelleRTT; anticipePossibleRTT = Math.max(0, acquisRestanteRTT); const maxRTT = typeContrat === 'forfait_jour' ? 12 : 10; limiteAnticipeRTT = Math.min(anticipePossibleRTT, maxRTT - dejaPrisRTT); } conn.release(); // ======================================== // RÉPONSE // ======================================== res.json({ success: true, user: { id: user.id, nom: `${user.prenom} ${user.nom}`, email: user.email, typeContrat: typeContrat, dateEntree: dateEntree ? formatDateWithoutUTC(dateEntree) : null }, dateReference: today.toISOString().split('T')[0], congesPayes: { acquisActuelle: parseFloat(acquisActuelleCP.toFixed(2)), acquisTotalePrevu: parseFloat(acquisTotaleCP.toFixed(2)), acquisRestante: parseFloat((acquisTotaleCP - acquisActuelleCP).toFixed(2)), soldeActuel: parseFloat(soldeActuelCP.toFixed(2)), dejaPris: parseFloat(dejaPrisCP.toFixed(2)), anticipePossible: parseFloat(anticipePossibleCP.toFixed(2)), limiteAnticipe: parseFloat(limiteAnticipeCP.toFixed(2)), totalDisponible: parseFloat((soldeActuelCP + limiteAnticipeCP).toFixed(2)), message: limiteAnticipeCP > 0 ? `Vous pouvez poser jusqu'à ${limiteAnticipeCP.toFixed(1)} jours de CP en anticipé` : "Vous avez atteint la limite d'anticipation pour les CP" }, rtt: user.role !== 'Apprenti' ? { acquisActuelle: parseFloat(acquisActuelleRTT.toFixed(2)), acquisTotalePrevu: parseFloat(acquisTotaleRTT.toFixed(2)), acquisRestante: parseFloat((acquisTotaleRTT - acquisActuelleRTT).toFixed(2)), soldeActuel: parseFloat(soldeActuelRTT.toFixed(2)), dejaPris: parseFloat(dejaPrisRTT.toFixed(2)), anticipePossible: parseFloat(anticipePossibleRTT.toFixed(2)), limiteAnticipe: parseFloat(limiteAnticipeRTT.toFixed(2)), totalDisponible: parseFloat((soldeActuelRTT + limiteAnticipeRTT).toFixed(2)), message: limiteAnticipeRTT > 0 ? `Vous pouvez poser jusqu'à ${limiteAnticipeRTT.toFixed(1)} jours de RTT en anticipé` : "Vous avez atteint la limite d'anticipation pour les RTT" } : null, regles: { cpMaxAnnuel: 25, rttMaxAnnuel: typeContrat === 'forfait_jour' ? 12 : 10, description: "Les congés anticipés sont basés sur l'acquisition prévue jusqu'à la fin de l'exercice/année" } }); } catch (error) { console.error('Erreur getCongesAnticipes:', error); res.status(500).json({ success: false, message: 'Erreur serveur', error: error.message }); } }); /** * Calcule l'acquisition CP ANTICIPÉE (ce qui reste à acquérir jusqu'à fin d'exercice) */ function calculerAcquisitionCPAnticipee(dateReference = new Date(), dateEntree = null) { const d = new Date(dateReference); d.setHours(0, 0, 0, 0); const annee = d.getFullYear(); const mois = d.getMonth() + 1; // 1️⃣ Déterminer la fin de l'exercice CP (31/05) let finExercice; if (mois >= 6) { finExercice = new Date(annee + 1, 4, 31); // 31/05/N+1 } else { finExercice = new Date(annee, 4, 31); // 31/05/N } finExercice.setHours(0, 0, 0, 0); // 2️⃣ Calculer l'acquisition actuelle const acquisActuelle = calculerAcquisitionCP(d, dateEntree); // 3️⃣ Calculer l'acquisition totale à fin d'exercice const acquisTotaleFinExercice = calculerAcquisitionCP(finExercice, dateEntree); // 4️⃣ Anticipée = Totale - Actuelle (plafonnée à 25) const acquisAnticipee = Math.min(25, acquisTotaleFinExercice) - acquisActuelle; return Math.max(0, Math.round(acquisAnticipee * 100) / 100); } /** * Calcule l'acquisition RTT ANTICIPÉE (ce qui reste à acquérir jusqu'à fin d'année) */ async function calculerAcquisitionRTTAnticipee(conn, collaborateurId, dateReference = new Date()) { const d = new Date(dateReference); d.setHours(0, 0, 0, 0); const annee = d.getFullYear(); // 1️⃣ Récupérer les infos du collaborateur const [collabInfo] = await conn.query( `SELECT TypeContrat, DateEntree, role FROM CollaborateurAD WHERE id = ?`, [collaborateurId] ); if (collabInfo.length === 0) { return 0; } const typeContrat = collabInfo[0].TypeContrat || '37h'; const isApprenti = collabInfo[0].role === 'Apprenti'; // 2️⃣ Apprentis = pas de RTT if (isApprenti) { return 0; } // 3️⃣ Récupérer la configuration RTT const config = await getConfigurationRTT(conn, annee, typeContrat); // 4️⃣ Calculer l'acquisition actuelle const rttActuel = await calculerAcquisitionRTT(conn, collaborateurId, d); const acquisActuelle = rttActuel.acquisition; // 5️⃣ Calculer l'acquisition totale à fin d'année (31/12) const finAnnee = new Date(annee, 11, 31); finAnnee.setHours(0, 0, 0, 0); const rttFinAnnee = await calculerAcquisitionRTT(conn, collaborateurId, finAnnee); const acquisTotaleFinAnnee = rttFinAnnee.acquisition; // 6️⃣ Anticipée = Totale - Actuelle (plafonnée au max annuel) const acquisAnticipee = Math.min(config.joursAnnuels, acquisTotaleFinAnnee) - acquisActuelle; return Math.max(0, Math.round(acquisAnticipee * 100) / 100); } app.get('/api/getStatistiquesCompteurs', async (req, res) => { try { const conn = await pool.getConnection(); const currentYear = new Date().getFullYear(); const [totalCollabs] = await conn.query( 'SELECT COUNT(*) as total FROM CollaborateurAD WHERE actif = 1 OR actif IS NULL' ); const [statsTypeContrat] = await conn.query(` SELECT TypeContrat, COUNT(*) as nombre, GROUP_CONCAT(CONCAT(prenom, ' ', nom) SEPARATOR ', ') as noms FROM CollaborateurAD WHERE actif = 1 OR actif IS NULL GROUP BY TypeContrat `); const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']); let statsCP = { total_acquis: 0, total_solde: 0, moyenne_utilisation: 0 }; if (cpType.length > 0) { const [cpStats] = await conn.query(` SELECT SUM(Total) as total_acquis, SUM(Solde) as total_solde, AVG(CASE WHEN Total > 0 THEN ((Total - Solde) / Total) * 100 ELSE 0 END) as moyenne_utilisation FROM CompteurConges WHERE TypeCongeId = ? AND Annee = ? `, [cpType[0].Id, currentYear]); if (cpStats.length > 0) { statsCP = { total_acquis: parseFloat((cpStats[0].total_acquis || 0).toFixed(2)), total_solde: parseFloat((cpStats[0].total_solde || 0).toFixed(2)), moyenne_utilisation: parseFloat((cpStats[0].moyenne_utilisation || 0).toFixed(1)) }; } } const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']); let statsRTT = { total_acquis: 0, total_solde: 0, moyenne_utilisation: 0 }; if (rttType.length > 0) { const [rttStats] = await conn.query(` SELECT SUM(Total) as total_acquis, SUM(Solde) as total_solde, AVG(CASE WHEN Total > 0 THEN ((Total - Solde) / Total) * 100 ELSE 0 END) as moyenne_utilisation FROM CompteurConges WHERE TypeCongeId = ? AND Annee = ? `, [rttType[0].Id, currentYear]); if (rttStats.length > 0) { statsRTT = { total_acquis: parseFloat((rttStats[0].total_acquis || 0).toFixed(2)), total_solde: parseFloat((rttStats[0].total_solde || 0).toFixed(2)), moyenne_utilisation: parseFloat((rttStats[0].moyenne_utilisation || 0).toFixed(1)) }; } } conn.release(); res.json({ success: true, annee: currentYear, statistiques: { collaborateurs: { total: totalCollabs[0].total, par_type_contrat: statsTypeContrat }, conges_payes: statsCP, rtt: statsRTT } }); } catch (error) { console.error('Erreur getStatistiquesCompteurs:', error); res.status(500).json({ success: false, message: 'Erreur serveur', error: error.message }); } }); async function hasCompteRenduAccess(userId) { try { const conn = await pool.getConnection(); const [user] = await conn.query(` SELECT TypeContrat, role FROM CollaborateurAD WHERE id = ? `, [userId]); conn.release(); if (!user.length) return false; const userInfo = user[0]; // Accès si : // 1. TypeContrat = 'forfait_jour' // 2. role = 'Directeur Campus' ou 'Directrice Campus' // 3. role = 'RH' ou 'Admin' return ( userInfo.TypeContrat === 'forfait_jour' || userInfo.role === 'Directeur Campus' || userInfo.role === 'Directrice Campus' || userInfo.role === 'RH' || userInfo.role === 'Admin' ); } catch (error) { console.error('Erreur vérification accès:', error); return false; } } // Récupérer les jours du mois // GET - Récupérer les données du compte-rendu app.get('/api/getCompteRenduActivites', async (req, res) => { const { user_id, annee, mois } = req.query; try { // Vérifier l'accès const hasAccess = await hasCompteRenduAccess(user_id); if (!hasAccess) { return res.json({ success: false, message: 'Accès réservé aux collaborateurs en forfait jour et aux directeurs de campus' }); } const conn = await pool.getConnection(); const [jours] = await conn.query(` SELECT id, CollaborateurADId, Annee, Mois, DATE_FORMAT(JourDate, '%Y-%m-%d') as JourDate, JourTravaille, ReposQuotidienRespect, ReposHebdomadaireRespect, CommentaireRepos, Verrouille, DateSaisie, SaisiePar FROM CompteRenduActivites WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ? ORDER BY JourDate `, [user_id, annee, mois]); console.log('🔍 Backend - Jours trouvés:', jours.length); if (jours.length > 0) { console.log('📅 Premier jour:', jours[0].JourDate, 'Type:', typeof jours[0].JourDate); } const [mensuel] = await conn.query(` SELECT * FROM CompteRenduMensuel WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ? `, [user_id, annee, mois]); conn.release(); res.json({ success: true, jours: jours, mensuel: mensuel[0] || null }); } catch (error) { console.error('Erreur getCompteRenduActivites:', error); res.status(500).json({ success: false, message: error.message }); } }); // POST - Sauvegarder un jour avec AUTO-VERROUILLAGE // POST - Sauvegarder un jour avec AUTO-VERROUILLAGE app.post('/api/saveCompteRenduJour', async (req, res) => { const { user_id, date, jour_travaille, repos_quotidien, repos_hebdo, commentaire, rh_override } = req.body; try { const conn = await pool.getConnection(); await conn.beginTransaction(); const dateJour = new Date(date); const aujourdhui = new Date(); aujourdhui.setHours(0, 0, 0, 0); dateJour.setHours(0, 0, 0, 0); // Bloquer saisie du jour actuel (il faut attendre le lendemain) if (dateJour >= aujourdhui) { await conn.rollback(); conn.release(); return res.json({ success: false, message: 'Vous ne pouvez pas saisir le jour actuel. Veuillez attendre demain.' }); } const annee = dateJour.getFullYear(); const mois = dateJour.getMonth() + 1; // Vérifier si le JOUR est déjà verrouillé (pas le mois entier) const [jourExistant] = await conn.query( 'SELECT Verrouille FROM CompteRenduActivites WHERE CollaborateurADId = ? AND JourDate = ?', [user_id, date] ); if (jourExistant[0]?.Verrouille && !rh_override) { await conn.rollback(); conn.release(); return res.json({ success: false, message: 'Ce jour est verrouillé - Contactez les RH pour modification' }); } // Vérifier commentaire obligatoire if (!repos_quotidien || !repos_hebdo) { if (!commentaire || commentaire.trim() === '') { await conn.rollback(); conn.release(); return res.json({ success: false, message: 'Commentaire obligatoire en cas de non-respect des repos' }); } } // Insérer ou mettre à jour le jour ET LE VERROUILLER await conn.query(` INSERT INTO CompteRenduActivites (CollaborateurADId, Annee, Mois, JourDate, JourTravaille, ReposQuotidienRespect, ReposHebdomadaireRespect, CommentaireRepos, DateSaisie, SaisiePar, Verrouille) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW(), ?, TRUE) ON DUPLICATE KEY UPDATE JourTravaille = VALUES(JourTravaille), ReposQuotidienRespect = VALUES(ReposQuotidienRespect), ReposHebdomadaireRespect = VALUES(ReposHebdomadaireRespect), CommentaireRepos = VALUES(CommentaireRepos), SaisiePar = VALUES(SaisiePar), Verrouille = TRUE `, [user_id, annee, mois, date, jour_travaille, repos_quotidien, repos_hebdo, commentaire, user_id]); // Mettre à jour les statistiques mensuelles (SANS verrouiller le mois) const [stats] = await conn.query(` SELECT COUNT(*) as nbJours, SUM(CASE WHEN ReposQuotidienRespect = FALSE THEN 1 ELSE 0 END) as nbNonRespectQuotidien, SUM(CASE WHEN ReposHebdomadaireRespect = FALSE THEN 1 ELSE 0 END) as nbNonRespectHebdo FROM CompteRenduActivites WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ? AND JourTravaille = TRUE `, [user_id, annee, mois]); await conn.query(` INSERT INTO CompteRenduMensuel (CollaborateurADId, Annee, Mois, NbJoursTravailles, NbJoursNonRespectsReposQuotidien, NbJoursNonRespectsReposHebdo, Statut, DateValidation) VALUES (?, ?, ?, ?, ?, ?, 'En cours', NOW()) ON DUPLICATE KEY UPDATE NbJoursTravailles = VALUES(NbJoursTravailles), NbJoursNonRespectsReposQuotidien = VALUES(NbJoursNonRespectsReposQuotidien), NbJoursNonRespectsReposHebdo = VALUES(NbJoursNonRespectsReposHebdo), DateValidation = NOW() `, [user_id, annee, mois, stats[0].nbJours, stats[0].nbNonRespectQuotidien, stats[0].nbNonRespectHebdo]); await conn.commit(); conn.release(); res.json({ success: true, message: 'Jour enregistré et verrouillé', verrouille: true }); } catch (error) { console.error('❌ Erreur saveCompteRenduJour:', error); res.status(500).json({ success: false, message: error.message }); } }); // POST - Saisie en masse avec AUTO-VERROUILLAGE app.post('/api/saveCompteRenduMasse', async (req, res) => { const { user_id, annee, mois, jours, rh_override } = req.body; try { const conn = await pool.getConnection(); await conn.beginTransaction(); let count = 0; let blocked = 0; for (const jour of jours) { const dateJour = new Date(jour.date); const aujourdhui = new Date(); aujourdhui.setHours(0, 0, 0, 0); dateJour.setHours(0, 0, 0, 0); // Bloquer le jour actuel if (dateJour >= aujourdhui) { blocked++; continue; } // Vérifier si déjà verrouillé const [jourExistant] = await conn.query( 'SELECT Verrouille FROM CompteRenduActivites WHERE CollaborateurADId = ? AND JourDate = ?', [user_id, jour.date] ); if (jourExistant[0]?.Verrouille && !rh_override) { blocked++; continue; } await conn.query(` INSERT INTO CompteRenduActivites (CollaborateurADId, Annee, Mois, JourDate, JourTravaille, ReposQuotidienRespect, ReposHebdomadaireRespect, CommentaireRepos, DateSaisie, SaisiePar, Verrouille) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW(), ?, TRUE) ON DUPLICATE KEY UPDATE JourTravaille = VALUES(JourTravaille), ReposQuotidienRespect = VALUES(ReposQuotidienRespect), ReposHebdomadaireRespect = VALUES(ReposHebdomadaireRespect), CommentaireRepos = VALUES(CommentaireRepos), SaisiePar = VALUES(SaisiePar), Verrouille = TRUE `, [ user_id, annee, mois, jour.date, jour.jour_travaille, jour.repos_quotidien, jour.repos_hebdo, jour.commentaire || null, user_id ]); count++; } // Mettre à jour statistiques mensuelles const [stats] = await conn.query(` SELECT COUNT(*) as nbJours, SUM(CASE WHEN ReposQuotidienRespect = FALSE THEN 1 ELSE 0 END) as nbNonRespectQuotidien, SUM(CASE WHEN ReposHebdomadaireRespect = FALSE THEN 1 ELSE 0 END) as nbNonRespectHebdo FROM CompteRenduActivites WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ? AND JourTravaille = TRUE `, [user_id, annee, mois]); await conn.query(` INSERT INTO CompteRenduMensuel (CollaborateurADId, Annee, Mois, NbJoursTravailles, NbJoursNonRespectsReposQuotidien, NbJoursNonRespectsReposHebdo, Statut, DateValidation) VALUES (?, ?, ?, ?, ?, ?, 'En cours', NOW()) ON DUPLICATE KEY UPDATE NbJoursTravailles = VALUES(NbJoursTravailles), NbJoursNonRespectsReposQuotidien = VALUES(NbJoursNonRespectsReposQuotidien), NbJoursNonRespectsReposHebdo = VALUES(NbJoursNonRespectsReposHebdo), DateValidation = NOW() `, [user_id, annee, mois, stats[0].nbJours, stats[0].nbNonRespectQuotidien, stats[0].nbNonRespectHebdo]); await conn.commit(); conn.release(); res.json({ success: true, count: count, blocked: blocked, message: `${count} jours enregistrés${blocked > 0 ? `, ${blocked} ignorés (jour actuel ou déjà verrouillés)` : ''}` }); } catch (error) { console.error('❌ Erreur saisie masse:', error); res.status(500).json({ success: false, message: error.message }); } }); app.post('/api/deverrouillerJour', async (req, res) => { const { user_id, date, rh_user_id } = req.body; try { const conn = await pool.getConnection(); const [rhUser] = await conn.query( 'SELECT role FROM CollaborateurAD WHERE id = ?', [rh_user_id] ); if (!rhUser.length || (rhUser[0].role !== 'RH' && rhUser[0].role !== 'Admin')) { conn.release(); return res.json({ success: false, message: 'Action réservée aux RH' }); } await conn.query(` UPDATE CompteRenduActivites SET Verrouille = FALSE WHERE CollaborateurADId = ? AND JourDate = ? `, [user_id, date]); conn.release(); res.json({ success: true }); } catch (error) { console.error('❌ Erreur déverrouillage jour:', error); res.status(500).json({ success: false, message: error.message }); } }); // POST - Verrouiller (RH uniquement) app.post('/api/verrouillerCompteRendu', async (req, res) => { const { user_id, annee, mois, rh_user_id } = req.body; try { const conn = await pool.getConnection(); // Vérifier que l'utilisateur est RH const [rhUser] = await conn.query( 'SELECT role FROM CollaborateurAD WHERE id = ?', [rh_user_id] ); if (!rhUser.length || (rhUser[0].role !== 'RH' && rhUser[0].role !== 'Admin')) { conn.release(); return res.json({ success: false, message: 'Action réservée aux RH' }); } await conn.query(` UPDATE CompteRenduMensuel SET Verrouille = TRUE, DateModification = NOW() WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ? `, [user_id, annee, mois]); conn.release(); res.json({ success: true }); } catch (error) { console.error('Erreur verrouillage:', error); res.status(500).json({ success: false, message: error.message }); } }); // POST - Déverrouiller (RH uniquement) app.post('/api/deverrouillerCompteRendu', async (req, res) => { const { user_id, annee, mois, rh_user_id } = req.body; try { const conn = await pool.getConnection(); // Vérifier que l'utilisateur est RH const [rhUser] = await conn.query( 'SELECT role FROM CollaborateurAD WHERE id = ?', [rh_user_id] ); if (!rhUser.length || (rhUser[0].role !== 'RH' && rhUser[0].role !== 'Admin')) { conn.release(); return res.json({ success: false, message: 'Action réservée aux RH' }); } await conn.query(` UPDATE CompteRenduMensuel SET Verrouille = FALSE, DateDeverrouillage = NOW(), DeverrouillePar = ? WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ? `, [rh_user_id, user_id, annee, mois]); conn.release(); res.json({ success: true }); } catch (error) { console.error('Erreur déverrouillage:', error); res.status(500).json({ success: false, message: error.message }); } }); // GET - Stats annuelles app.get('/api/getStatsAnnuelles', async (req, res) => { const { user_id, annee } = req.query; try { const conn = await pool.getConnection(); const [stats] = await conn.query(` SELECT SUM(NbJoursTravailles) as totalJoursTravailles, SUM(NbJoursNonRespectsReposQuotidien) as totalNonRespectQuotidien, SUM(NbJoursNonRespectsReposHebdo) as totalNonRespectHebdo FROM CompteRenduMensuel WHERE CollaborateurADId = ? AND Annee = ? `, [user_id, annee]); conn.release(); res.json({ success: true, stats: stats[0] || { totalJoursTravailles: 0, totalNonRespectQuotidien: 0, totalNonRespectHebdo: 0 } }); } catch (error) { console.error('Erreur stats:', error); res.status(500).json({ success: false, message: error.message }); } }); // GET - Export PDF (RH uniquement) app.get('/api/exportCompteRenduPDF', async (req, res) => { const { user_id, annee, mois } = req.query; try { const conn = await pool.getConnection(); // Récupérer les données du collaborateur const [collab] = await conn.query( 'SELECT prenom, nom, email FROM CollaborateurAD WHERE id = ?', [user_id] ); // Récupérer les jours du mois const [jours] = await conn.query(` SELECT * FROM CompteRenduActivites WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ? ORDER BY JourDate `, [user_id, annee, mois]); // Récupérer le mensuel const [mensuel] = await conn.query(` SELECT * FROM CompteRenduMensuel WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ? `, [user_id, annee, mois]); conn.release(); // TODO: Générer le PDF avec une bibliothèque comme pdfkit ou puppeteer // Pour l'instant, retourner les données JSON res.json({ success: true, collaborateur: collab[0], jours: jours, mensuel: mensuel[0], message: 'Export PDF à implémenter' }); } catch (error) { console.error('Erreur export PDF:', error); res.status(500).json({ success: false, message: error.message }); } }); // 📊 ROUTE POUR L'ESPACE RH - Tous les compteurs détaillés app.get('/api/getAllDetailedCounters', async (req, res) => { try { console.log('📊 Récupération de TOUS les compteurs détaillés pour RH'); const conn = await pool.getConnection(); // Récupérer tous les collaborateurs actifs const [collaborateurs] = await conn.query(` SELECT DISTINCT ca.id, ca.prenom, ca.nom, ca.email, ca.role, ca.TypeContrat, ca.DateEntree, s.Nom as service FROM CollaborateurAD ca LEFT JOIN Services s ON ca.ServiceId = s.Id WHERE ca.Actif = 1 OR ca.Actif IS NULL ORDER BY ca.nom, ca.prenom `); console.log(`👥 ${collaborateurs.length} collaborateurs trouvés`); const resultats = []; const currentYear = new Date().getFullYear(); const previousYear = currentYear - 1; for (const collab of collaborateurs) { try { // Récupérer les compteurs détaillés de ce collaborateur // en utilisant la MÊME logique que getDetailedLeaveCounters const cpType = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1`, ['Congé payé']); const rttType = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1`, ['RTT']); const recupType = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1`, ['Récupération']); // CP N if (cpType.length > 0) { const acquisCP = calculerAcquisitionCP(new Date(), collab.DateEntree); const [consommeN] = await conn.query(` SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalConsomme FROM DeductionDetails dd JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id WHERE dc.CollaborateurADId = ? AND dd.TypeCongeId = ? AND dd.Annee = ? AND dd.TypeDeduction IN ('Année N', 'Année N', 'Année actuelle N') AND dd.TypeDeduction NOT IN ('Accum. Récup', 'Accum. Recup', 'N Anticipé') AND dc.Statut != 'Refusé' `, [collab.id, cpType[0].Id, currentYear]); const pris = parseFloat(consommeN.totalConsomme || 0); const soldeDisponible = Math.max(0, acquisCP - pris); resultats.push({ collaborateurId: collab.id, employe: `${collab.prenom} ${collab.nom}`, email: collab.email, service: collab.service || 'Non assigné', typeConge: 'Congé payé', annee: currentYear, total: parseFloat(acquisCP.toFixed(2)), solde: parseFloat(soldeDisponible.toFixed(2)), consomme: parseFloat(pris.toFixed(2)), role: collab.role, typeContrat: collab.TypeContrat }); } // CP N-1 if (cpType.length > 0) { const [cpN1] = await conn.query(` SELECT Annee, SoldeReporte, Total, Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? `, [collab.id, cpType[0].Id, previousYear]); if (cpN1.length > 0 && parseFloat(cpN1[0].Solde || 0) > 0) { const soldeReporte = parseFloat(cpN1[0].Solde || 0); const [consommeN1] = await conn.query(` SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalConsomme FROM DeductionDetails dd JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id WHERE dc.CollaborateurADId = ? AND dd.TypeCongeId = ? AND dd.Annee = ? AND dd.TypeDeduction IN ('Année N-1', 'Année N-1', 'Report N-1') AND dc.Statut != 'Refusé' `, [collab.id, cpType[0].Id, previousYear]); const pris = parseFloat(consommeN1.totalConsomme || 0); const soldeActuel = Math.max(0, soldeReporte - pris); resultats.push({ collaborateurId: collab.id, employe: `${collab.prenom} ${collab.nom}`, email: collab.email, service: collab.service || 'Non assigné', typeConge: 'Congé payé', annee: previousYear, total: parseFloat(soldeReporte.toFixed(2)), solde: parseFloat(soldeActuel.toFixed(2)), consomme: parseFloat(pris.toFixed(2)), role: collab.role, typeContrat: collab.TypeContrat }); } } // RTT N if (rttType.length > 0 && collab.role !== 'Apprenti') { const rttData = await calculerAcquisitionRTT(conn, collab.id, new Date()); const [consommeRTT] = await conn.query(` SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalConsomme FROM DeductionDetails dd JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id WHERE dc.CollaborateurADId = ? AND dd.TypeCongeId = ? AND dd.Annee = ? AND dd.TypeDeduction NOT IN ('Accum. Récup', 'Accum. Recup', 'Récup Dosée', 'N Anticipé') AND dc.Statut != 'Refusé' `, [collab.id, rttType[0].Id, currentYear]); const pris = parseFloat(consommeRTT.totalConsomme || 0); const soldeDisponible = Math.max(0, rttData.acquisition - pris); resultats.push({ collaborateurId: collab.id, employe: `${collab.prenom} ${collab.nom}`, email: collab.email, service: collab.service || 'Non assigné', typeConge: 'RTT', annee: currentYear, total: parseFloat(rttData.acquisition.toFixed(2)), solde: parseFloat(soldeDisponible.toFixed(2)), consomme: parseFloat(pris.toFixed(2)), role: collab.role, typeContrat: collab.TypeContrat }); } // Récupérations N if (recupType.length > 0) { const [accumRecup] = await conn.query(` SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalAccum FROM DeductionDetails dd JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id WHERE dc.CollaborateurADId = ? AND dd.TypeCongeId = ? AND dd.Annee = ? AND dd.TypeDeduction IN ('Accum. Récup', 'Accum. Recup') AND dc.Statut != 'Refusé' `, [collab.id, recupType[0].Id, currentYear]); const [consomRecup] = await conn.query(` SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalConsom FROM DeductionDetails dd JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id WHERE dc.CollaborateurADId = ? AND dd.TypeCongeId = ? AND dd.Annee = ? AND dd.TypeDeduction IN ('Récup Dosée', 'Recup Dosee') AND dc.Statut != 'Refusé' `, [collab.id, recupType[0].Id, currentYear]); const acquis = parseFloat(accumRecup.totalAccum || 0); const pris = parseFloat(consomRecup.totalConsom || 0); const solde = Math.max(0, acquis - pris); if (acquis > 0 || pris > 0 || solde > 0) { resultats.push({ collaborateurId: collab.id, employe: `${collab.prenom} ${collab.nom}`, email: collab.email, service: collab.service || 'Non assigné', typeConge: 'Récupération', annee: currentYear, total: parseFloat(acquis.toFixed(2)), solde: parseFloat(solde.toFixed(2)), consomme: parseFloat(pris.toFixed(2)), role: collab.role, typeContrat: collab.TypeContrat }); } } } catch (collabError) { console.error(`❌ Erreur pour ${collab.prenom} ${collab.nom}:`, collabError.message); } } conn.release(); console.log(`✅ ${resultats.length} compteurs retournés`); res.json(resultats); } catch (error) { console.error('❌ Erreur getAllDetailedCounters:', error); res.status(500).json({ error: error.message }); } }); // ======================================== // DÉMARRAGE DU SERVEUR // ======================================== app.listen(PORT, '0.0.0.0', () => { console.log('✅ ✅ ✅ SERVEUR PRINCIPAL DÉMARRÉ ✅ ✅ ✅'); console.log(`📡 Port: ${PORT}`); console.log(`🗄️ Base: ${dbConfig.database}@${dbConfig.host}`); console.log(`⏰ Cron jobs: activés`); console.log(`🌐 CORS origins: ${JSON.stringify(dbConfig)}`); });