diff --git a/GTA_P_V2/project/DockerfileGTA.frontend b/GTA_P_V2/project/DockerfileGTA.frontend index 341818c..7b3e7ce 100644 --- a/GTA_P_V2/project/DockerfileGTA.frontend +++ b/GTA_P_V2/project/DockerfileGTA.frontend @@ -26,12 +26,12 @@ export default defineConfig({ }, server: { host: '0.0.0.0', - port: 90, + port: 80, strictPort: true, - allowedHosts: ['mygta-dev.ensup-adm.net', 'localhost'], + allowedHosts: ['mygta.ensup-adm.net', 'localhost'], proxy: { '/api': { - target: 'http://backend:3004', + target: 'http://backend:3000', changeOrigin: true, secure: false, configure: (proxy, options) => { @@ -39,7 +39,7 @@ export default defineConfig({ console.log('Proxy error:', err); }); proxy.on('proxyReq', (proxyReq, req, res) => { - console.log('Proxying:', req.method, req.url, '-> http://backend:3004'); + console.log('Proxying:', req.method, req.url, '-> http://backend:3000'); }); } } @@ -48,6 +48,6 @@ export default defineConfig({ }); VITECONFIG -EXPOSE 90 +EXPOSE 80 -CMD ["npx", "vite", "--host", "0.0.0.0", "--port", "90"] +CMD ["npx", "vite", "--host", "0.0.0.0", "--port", "80"] diff --git a/GTA_P_V2/project/public/Backend/DockerfileGTA.backend b/GTA_P_V2/project/public/Backend/DockerfileGTA.backend index 8889208..5a14dfd 100644 --- a/GTA_P_V2/project/public/Backend/DockerfileGTA.backend +++ b/GTA_P_V2/project/public/Backend/DockerfileGTA.backend @@ -18,7 +18,7 @@ COPY . . RUN mkdir -p /app/uploads/medical # Expose the port -EXPOSE 3004 +EXPOSE 3000 # Start the server CMD ["node", "server.js"] diff --git a/GTA_P_V2/project/public/Backend/server.js b/GTA_P_V2/project/public/Backend/server.js index e6d2e37..6bc4b5d 100644 --- a/GTA_P_V2/project/public/Backend/server.js +++ b/GTA_P_V2/project/public/Backend/server.js @@ -1,4 +1,4 @@ -// ============================================================================ +// ============================================================================ // 🚀 GTA - GESTION DES TEMPS ET ABSENCES // ============================================================================ // Serveur Backend Node.js avec SQL Server @@ -25,7 +25,7 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const app = express(); -const PORT = 3004; +const PORT = 3000; const webhookManager = new WebhookManager(WEBHOOKS.SECRET_KEY); const sseClientsCollab = new Set(); @@ -41,7 +41,7 @@ process.on('unhandledRejection', (reason, promise) => { }); app.use(cors({ - origin: ['http://localhost:3013', 'http://localhost:80', 'https://mygta-dev.ensup-adm.net'], + origin: ['http://localhost:3013', 'http://localhost:80', 'https://mygta.ensup-adm.net'], credentials: true })); @@ -160,6 +160,270 @@ async function getSoldeFige(conn, collaborateurId, typeCongeId, annee) { return soldes.length > 0 ? soldes[0] : null; } +// ============================================================ +// 📧 WORKER : scrute RappelsCRAPending toutes les 60 secondes +// et envoie les emails via Microsoft Graph +// ============================================================ + +// Template email unifié (quotidien ou hebdomadaire) +function buildRappelCRAHtml(prenom, joursArray, type, moisLabel = null) { + const nb = joursArray.length; + + function grouperEnPlages(jours) { + if (!jours || jours.length === 0) return []; + const sorted = [...jours].sort(); + const plages = []; + let debut = sorted[0]; + let fin = sorted[0]; + for (let i = 1; i < sorted.length; i++) { + const prev = new Date(sorted[i - 1] + 'T00:00:00'); + const curr = new Date(sorted[i] + 'T00:00:00'); + const diffJ = (curr - prev) / 86400000; + if (diffJ <= 3) { + fin = sorted[i]; + } else { + plages.push({ debut, fin }); + debut = sorted[i]; + fin = sorted[i]; + } + } + plages.push({ debut, fin }); + return plages; + } + + function formatPlage(debut, fin) { + const opts = { weekday: 'long', day: 'numeric', month: 'long' }; + const d = new Date(debut + 'T00:00:00').toLocaleDateString('fr-FR', opts); + const f = new Date(fin + 'T00:00:00').toLocaleDateString('fr-FR', opts); + return debut === fin ? d : `Du ${d} au ${f}`; + } + + const plages = grouperEnPlages(joursArray); + const nbPlages = plages.length; + + const plagesHtml = plages.slice(0, 8).map(({ debut, fin }) => { + const label = formatPlage(debut, fin); + const nbJoursPlage = joursArray.filter(j => j >= debut && j <= fin).length; + const badge = nbJoursPlage > 1 + ? `${nbJoursPlage}j` + : ''; + return ` +
+ … et ${nbPlages - 8} autre${nbPlages - 8 > 1 ? 's' : ''} + période${nbPlages - 8 > 1 ? 's' : ''}. +
` + : ''; + + // ✅ Config selon le type de rappel + const cfg = + type === 'quotidien' ? { + couleur: '#f59e0b', + emoji: '📅', + titre: `${nb} jour${nb > 1 ? 's' : ''} non saisi${nb > 1 ? 's' : ''} dans votre CRA`, + intro: `Vous avez ${nb} jour${nb > 1 ? 's' : ''} ouvrable${nb > 1 ? 's' : ''} + non saisi${nb > 1 ? 's' : ''} dans votre Compte-Rendu d'Activités.` + } + : type === 'mensuel' ? { + couleur: '#8b5cf6', + emoji: '📋', + titre: `CRA de ${moisLabel} non saisi`, + intro: `Votre Compte-Rendu d'Activités du mois de + ${moisLabel} n'a pas encore été saisi. + Il contient ${nb} jour${nb > 1 ? 's' : ''} + ouvrable${nb > 1 ? 's' : ''} à renseigner.` + } + : { + couleur: '#3b82f6', + emoji: '🔔', + titre: `${nb} jour${nb > 1 ? 's' : ''} non saisi${nb > 1 ? 's' : ''} dans votre CRA`, + intro: `Vous avez ${nb} jour${nb > 1 ? 's' : ''} ouvrable${nb > 1 ? 's' : ''} + non saisi${nb > 1 ? 's' : ''} dans votre Compte-Rendu d'Activités.` + }; + + return ` + `; +} + +// Worker principal +async function traiterRappelsCRAPending() { + let rappels; + try { + const [rows] = await pool.query(` + SELECT TOP 20 + Id, CollabId, Email, Prenom, NomComplet, + TypeRappel, JoursJson, NbJours, + Annee, Mois + FROM RappelsCRAPending + WHERE Statut = 'pending' + ORDER BY DateCreation ASC + `); + rappels = rows; + } catch (err) { + console.error('❌ Worker CRA — lecture table:', err.message); + return; + } + + if (!rappels || rappels.length === 0) return; + + console.log(`📬 Worker CRA : ${rappels.length} rappel(s) à traiter`); + + const accessToken = await getGraphToken(); + if (!accessToken) { + console.error('❌ Worker CRA — token Graph indisponible'); + return; + } + + // Tableau des mois en français + const moisNoms = [ + 'janvier', 'février', 'mars', 'avril', 'mai', 'juin', + 'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre' + ]; + + for (const rappel of rappels) { + try { + const joursArray = JSON.parse(rappel.JoursJson).map(j => j.d); + + // ✅ Label mois pour le type mensuel + const moisLabel = (rappel.Mois && rappel.Annee) + ? `${moisNoms[rappel.Mois - 1]} ${rappel.Annee}` + : null; + + // ✅ Sujet selon le type + const sujet = + rappel.TypeRappel === 'quotidien' + ? `📅 Rappel CRA — hier non saisi` + : rappel.TypeRappel === 'mensuel' + ? `📋 Rappel CRA — ${moisLabel} non saisi` + : `🔔 Rappel CRA — ${rappel.NbJours} jour${rappel.NbJours > 1 ? 's' : ''} non saisi${rappel.NbJours > 1 ? 's' : ''}`; + + // ✅ HTML avec moisLabel passé en paramètre + const html = buildRappelCRAHtml( + rappel.Prenom, + joursArray, + rappel.TypeRappel, + moisLabel + ); + + const envoye = await sendMailGraph( + accessToken, + 'gtanoreply@ensup.eu', + rappel.Email, + sujet, + html + ); + + await pool.query(` + UPDATE RappelsCRAPending + SET Statut = ?, + DateTraitement = GETDATE() + WHERE Id = ? + `, [envoye ? 'traite' : 'erreur', rappel.Id]); + + if (envoye) { + console.log(` ✅ ${rappel.NomComplet} (${rappel.TypeRappel}) → ${rappel.Email}`); + } else { + console.error(` ❌ Échec envoi → ${rappel.Email}`); + } + + await new Promise(r => setTimeout(r, 400)); + + } catch (err) { + console.error(` ❌ Erreur traitement rappel ID ${rappel.Id}:`, err.message); + await pool.query(` + UPDATE RappelsCRAPending + SET Statut = 'erreur', DateTraitement = GETDATE() + WHERE Id = ? + `, [rappel.Id]); + } + } +} +// Démarrer le worker (60 secondes entre chaque passage) +const workerCRA = setInterval(traiterRappelsCRAPending, 60 * 1000); + +// Nettoyage optionnel des lignes traitées > 30 jours (évite que la table grossisse) +setInterval(async () => { + try { + await pool.query(` + DELETE FROM RappelsCRAPending + WHERE Statut IN ('traite', 'erreur') + AND DateCreation < DATEADD(DAY, -30, GETDATE()) + `); + } catch (e) { /* silencieux */ } +}, 24 * 60 * 60 * 1000); // une fois par jour + +console.log('⏰ Worker CRA démarré — scrute RappelsCRAPending toutes les 60s'); + +async function calculerAcquisitionRTTSmart(conn, collaborateurId, dateReference) { + try { + const result = await calculerAcquisitionRTT(conn, collaborateurId, dateReference); + return result; // { acquisition, moisTravailles, config, typeContrat } + } catch (error) { + console.error('❌ calculerAcquisitionRTTSmart erreur:', error.message); + return { acquisition: 0, moisTravailles: 0, config: { joursAnnuels: 0, acquisitionMensuelle: 0 }, typeContrat: '37h' }; + } +} + async function calculerAcquisitionDepuisArrete(conn, collaborateurId, typeConge, dateReference = new Date()) { const dernierArrete = await getDernierArrete(conn); const anneeRef = dateReference.getFullYear(); @@ -524,6 +788,17 @@ function getUserAccesTransversal(userEmail) { return acces; } +// Helper à centraliser UNE SEULE FOIS dans server.js +function getAnneesCourantes(dateReference = new Date()) { + const currentYear = dateReference.getFullYear(); + const dateBascule = new Date(`${currentYear}-06-01`); + const apresBasculement = dateReference >= dateBascule; + return { + anneeN: apresBasculement ? currentYear + 1 : currentYear, // ex: 2027 + anneeN1: apresBasculement ? currentYear : currentYear - 1, // ex: 2026 (report) + apresBasculement + }; +} const uploadMedical = multer({ @@ -799,8 +1074,12 @@ app.post('/api/webhook/receive', async (req, res) => { statut: data.statut, typeConge: data.typeConge, couleurHex: data.couleurHex || '#d946ef', - date: data.date, - periode: data.periode, + date: data.date || data.dateDebut, // 🔧 AJOUTÉ + dateDebut: data.dateDebut || data.date, // 🔧 AJOUTÉ + dateFin: data.dateFin || data.date, // 🔧 AJOUTÉ + periode: data.periode || data.periodeJournee || 'Journée entière', // 🔧 AJOUTÉ + periodeJournee: data.periodeJournee || data.periode, // 🔧 AJOUTÉ + nombreJours: data.nombreJours, // 🔧 AJOUTÉ collaborateurId: data.collaborateurId, timestamp: new Date().toISOString() }, data.collaborateurId); @@ -1657,6 +1936,7 @@ function getMonthName(mois) { return mois_names[mois] || mois; } + async function getGraphToken() { try { const params = new URLSearchParams({ @@ -1725,7 +2005,7 @@ function formatDate(date) { return `${day}/${month}/${year}`; } -function getExerciceCP(date = new Date()) { +/*function getExerciceCP(date = new Date()) { const d = new Date(date); const annee = d.getFullYear(); const mois = d.getMonth() + 1; @@ -1734,7 +2014,7 @@ function getExerciceCP(date = new Date()) { } return `${annee}-${annee + 1}`; } - +*/ function getMoisTravaillesCP(date = new Date(), dateEntree = null) { const d = new Date(date); d.setHours(0, 0, 0, 0); @@ -1765,6 +2045,41 @@ function getMoisTravaillesCP(date = new Date(), dateEntree = null) { return Math.max(0, Math.min(12, moisTravailles)); } +//Pour CP Anticipés + +// ───────────────────────────────────────────────────────────── +// HELPER EXERCICE CP +// Annee = année de début de plage : 2025 → exercice 01/06/2025-31/05/2026 +// ───────────────────────────────────────────────────────────── +function getExerciceCP(date) { + const d = new Date(date); + const mois = d.getMonth() + 1; // 1-12 + const anneeDebut = mois >= 6 ? d.getFullYear() : d.getFullYear() - 1; + return { + annee: anneeDebut, // INT → JOIN avec CompteurConges.Annee + label: `${anneeDebut}-${anneeDebut + 1}` // VARCHAR → audit lisible + }; +} + +/** + * Détermine le mode (NORMAL ou ANTICIPATION) et les exercices concernés. + * NORMAL : congés sur le MÊME exercice que la demande → N-1 prioritaire puis N + * ANTICIPATION : congés sur l'exercice SUIVANT → CP N uniquement + */ +function getInfosDemandeCP(dateDebut, dateDemande = new Date()) { + const exerciceDemande = getExerciceCP(dateDemande); + const exerciceConges = getExerciceCP(dateDebut); + const isAnticipe = exerciceConges.annee > exerciceDemande.annee; + + return { + isAnticipe, + mode: isAnticipe ? 'ANTICIPATION' : 'NORMAL', + exerciceDemande, // { annee: 2025, label: '2025-2026' } + exerciceConges, // { annee: 2026, label: '2026-2027' } + anneeN: exerciceDemande.annee, // compteur CP N (toujours utilisé) + anneeN1: isAnticipe ? null : exerciceDemande.annee - 1 // CP N-1 seulement en NORMAL + }; +} function getMoisTravaillesRTT(date = new Date(), dateEntree = null) { @@ -2820,22 +3135,53 @@ app.get('/api/getDetailedLeaveCounters', async (req, res) => { const typeContrat = user.TypeContrat || '37h'; const today = new Date(); - const currentYear = today.getFullYear(); + const currentYearSys = today.getFullYear(); + + // ── Récupérer le TypeCongeId CP avant la détection de bascule ── + const [cpType] = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1`, ['Congé payé']); + + if (cpType.length === 0) { + conn.release(); + return res.json({ success: false, message: 'Type de congé CP non trouvé' }); + } + + // ── Détection dynamique des années post-bascule ──────────────── + // On lit la base plutôt que la date système, car la bascule peut + // être faite manuellement avant le 01/06. + const [check2027] = await conn.query(` + SELECT TOP 1 Id + FROM CompteurConges + WHERE CollaborateurADId = ? + AND TypeCongeId = ? + AND Annee = ? + AND SoldeReporte > 0 + `, [userId, cpType[0].Id, currentYearSys + 1]); + + const basculeFaite = check2027.length > 0; + + const dateBascule = new Date(`${currentYearSys}-06-01`); + const apresBasculement = today >= dateBascule; + + const currentYear = basculeFaite + ? currentYearSys + 1 + : (apresBasculement ? currentYearSys + 1 : currentYearSys); + const previousYear = currentYear - 1; + const anneeN = currentYear; + console.log('\n📊 === CALCUL COMPTEURS ==='); console.log('User:', user.prenom, user.nom, '(ID:', userId, ')'); console.log('Date référence:', today.toLocaleDateString('fr-FR')); + console.log(`📅 Années: N=${currentYear}, N-1=${previousYear} (basculeFaite=${basculeFaite})`); // ═══════════════════════════════════════════════════════════ // 1️⃣ CALCUL AVEC LES FORMULES INTELLIGENTES // ═══════════════════════════════════════════════════════════ - // CP N const acquisCP = calculerAcquisitionCP_Smart(today, dateEntree); console.log('🧮 CP calculé:', acquisCP.toFixed(2) + 'j'); - // RTT N let acquisRTT = 0; let rttTypeId = null; const [rttType] = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1`, ['RTT']); @@ -2851,14 +3197,6 @@ app.get('/api/getDetailedLeaveCounters', async (req, res) => { // 2️⃣ RÉCUPÉRER INFOS POUR CALCUL DES SOLDES // ═══════════════════════════════════════════════════════════ - const [cpType] = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1`, ['Congé payé']); - - if (cpType.length === 0) { - conn.release(); - return res.json({ success: false, message: 'Type de congé CP non trouvé' }); - } - - // Solde reporté CP let soldeReporte = 0; const [compteurCP] = await conn.query(` SELECT SoldeReporte FROM CompteurConges @@ -2869,7 +3207,6 @@ app.get('/api/getDetailedLeaveCounters', async (req, res) => { soldeReporte = parseFloat(compteurCP[0].SoldeReporte) || 0; } - // Consommé CP const [consommeCP] = await conn.query(` SELECT COALESCE(SUM(dd.JoursUtilises), 0) as total FROM DeductionDetails dd @@ -2886,7 +3223,6 @@ app.get('/api/getDetailedLeaveCounters', async (req, res) => { console.log('💰 CP - Acquis:', acquisCP.toFixed(2), '+ Reporté:', soldeReporte.toFixed(2), '- Consommé:', totalConsommeCP.toFixed(2), '= Solde:', nouveauSoldeCP.toFixed(2)); - // Consommé RTT let totalConsommeRTT = 0; let nouveauSoldeRTT = 0; if (rttTypeId) { @@ -2954,7 +3290,6 @@ app.get('/api/getDetailedLeaveCounters', async (req, res) => { } } catch (syncError) { console.error('❌ Erreur synchronisation GTA-RH:', syncError.message); - // On continue même si la sync échoue } // ═══════════════════════════════════════════════════════════ @@ -2963,7 +3298,6 @@ app.get('/api/getDetailedLeaveCounters', async (req, res) => { console.log('\n📖 Lecture base après synchronisation...'); - // Ancienneté const ancienneteMs = today - new Date(dateEntree || today); const ancienneteMois = Math.floor(ancienneteMs / (1000 * 60 * 60 * 24 * 30.44)); @@ -2987,7 +3321,7 @@ app.get('/api/getDetailedLeaveCounters', async (req, res) => { }, dateReference: today.toISOString().split('T')[0], exerciceCP: getExerciceCP(today), - anneeRTT: currentYear, + anneeRTT: currentYearSys, cpN1: null, cpN: null, rttN: null, @@ -3010,7 +3344,7 @@ app.get('/api/getDetailedLeaveCounters', async (req, res) => { counters.cpN1 = { annee: previousYear, - exercice: `${previousYear}-${previousYear + 1}`, + exercice: `${previousYear - 1}-${previousYear}`, reporte: parseFloat(totalAcquis.toFixed(2)), pris: parseFloat(pris.toFixed(2)), solde: parseFloat(soldeReporte.toFixed(2)), @@ -3018,12 +3352,12 @@ app.get('/api/getDetailedLeaveCounters', async (req, res) => { }; counters.totalDisponible.cp += counters.cpN1.solde; - console.log('✅ CP N-1 BASE: Acquis=' + totalAcquis + 'j, Solde=' + soldeReporte + 'j'); + console.log('✅ CP N-1 BASE: Annee=' + previousYear + ' Acquis=' + totalAcquis + 'j, Solde=' + soldeReporte + 'j'); } // ✅ CP N - LECTURE BASE const [cpNData] = await conn.query(` - SELECT Total, Solde, SoldeReporte + SELECT Total, Solde, SoldeReporte, SoldeAnticipe, IsAnticipe FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, [userId, cpType[0].Id, currentYear] @@ -3033,30 +3367,39 @@ app.get('/api/getDetailedLeaveCounters', async (req, res) => { const totalAcquis = parseFloat(cpNData[0].Total) || 0; const soldeBDD = parseFloat(cpNData[0].Solde) || 0; const soldeReporteBDD = parseFloat(cpNData[0].SoldeReporte) || 0; + const soldeAnticipe = parseFloat(cpNData[0].SoldeAnticipe) || 0; + const isAnticipe = (cpNData[0].IsAnticipe ?? 0) === 1; const soldeReel = Math.max(0, soldeBDD - soldeReporteBDD); const pris = Math.max(0, totalAcquis - soldeReel); counters.cpN = { annee: currentYear, - exercice: getExerciceCP(today), + exercice: `${currentYear - 1}-${currentYear}`, totalAnnuel: 25.00, acquis: parseFloat(totalAcquis.toFixed(2)), pris: parseFloat(pris.toFixed(2)), solde: parseFloat(soldeReel.toFixed(2)), - pourcentageUtilise: totalAcquis > 0 ? parseFloat((pris / totalAcquis * 100).toFixed(1)) : 0 + pourcentageUtilise: totalAcquis > 0 ? parseFloat((pris / totalAcquis * 100).toFixed(1)) : 0, + soldeAnticipe: parseFloat(soldeAnticipe.toFixed(2)), + isAnticipe: isAnticipe, + anneeAnticipe: anneeN }; counters.totalDisponible.cp += counters.cpN.solde; - console.log('✅ CP N BASE: Acquis=' + totalAcquis + 'j, Solde=' + soldeReel + 'j'); + console.log('✅ CP N BASE: Annee=' + currentYear + ' Acquis=' + totalAcquis + 'j, Solde=' + soldeReel + 'j'); } // ✅ RTT N - LECTURE BASE + // ✅ RTT N - LECTURE BASE + // Le RTT est annuel (01/01→31/12), il ne bascule pas comme le CP if (rttType.length > 0 && user.role !== 'Apprenti') { + const anneeRTT = currentYearSys; // toujours 2026, pas currentYear=2027 + const [rttNData] = await conn.query(` - SELECT Total, Solde - FROM CompteurConges - WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, - [userId, rttTypeId, currentYear] + SELECT Total, Solde + FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, + [userId, rttTypeId, anneeRTT] ); if (rttNData.length > 0) { @@ -3064,10 +3407,10 @@ app.get('/api/getDetailedLeaveCounters', async (req, res) => { const soldeBDD = parseFloat(rttNData[0].Solde) || 0; const pris = Math.max(0, totalAcquis - soldeBDD); - const rttConfig = await getConfigurationRTT(conn, currentYear, typeContrat); + const rttConfig = await getConfigurationRTT(conn, anneeRTT, typeContrat); counters.rttN = { - annee: currentYear, + annee: anneeRTT, typeContrat: typeContrat, totalAnnuel: parseFloat(rttConfig.joursAnnuels.toFixed(2)), acquis: parseFloat(totalAcquis.toFixed(2)), @@ -3077,30 +3420,31 @@ app.get('/api/getDetailedLeaveCounters', async (req, res) => { }; counters.totalDisponible.rtt += counters.rttN.solde; - console.log('✅ RTT BASE: Acquis=' + totalAcquis + 'j, Solde=' + soldeBDD + 'j'); + console.log('✅ RTT BASE: Annee=' + anneeRTT + ' Acquis=' + totalAcquis + 'j, Solde=' + soldeBDD + 'j'); } } // ✅ Récup - LECTURE BASE + // ✅ Récup - LECTURE BASE + // La récup est annuelle, elle ne bascule pas comme le CP const [recupType] = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1`, ['Récupération']); if (recupType.length > 0) { - const [recupData] = await conn.query(` - SELECT - COALESCE(SUM(CASE WHEN dd.TypeDeduction IN ('Accum Récup', 'Accum Recup') THEN dd.JoursUtilises ELSE 0 END), 0) as acquis, - COALESCE(SUM(CASE WHEN dd.TypeDeduction = 'Récup Dose' THEN dd.JoursUtilises ELSE 0 END), 0) as pris - FROM DeductionDetails dd - JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id - WHERE dc.CollaborateurADId = ? AND dd.TypeCongeId = ? AND dd.Annee = ? AND dc.Statut != 'Refusé'`, - [userId, recupType[0].Id, currentYear] + const anneeRecup = currentYearSys; // toujours 2026, pas currentYear=2027 + + const [compteurRecup] = await conn.query(` + SELECT Total, Solde + FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, + [userId, recupType[0].Id, anneeRecup] ); - if (recupData.length > 0) { - const acquis = parseFloat(recupData[0].acquis) || 0; - const pris = parseFloat(recupData[0].pris) || 0; - const solde = Math.max(0, acquis - pris); + if (compteurRecup.length > 0) { + const acquis = parseFloat(compteurRecup[0].Total || 0); + const solde = parseFloat(compteurRecup[0].Solde || 0); + const pris = Math.max(0, acquis - solde); counters.recupN = { - annee: currentYear, + annee: anneeRecup, acquis: parseFloat(acquis.toFixed(2)), pris: parseFloat(pris.toFixed(2)), solde: parseFloat(solde.toFixed(2)), @@ -3108,11 +3452,10 @@ app.get('/api/getDetailedLeaveCounters', async (req, res) => { }; counters.totalDisponible.recup = counters.recupN.solde; - console.log('✅ RÉCUP BASE: Acquis=' + acquis + 'j, Solde=' + solde + 'j'); + console.log('✅ RÉCUP BASE: Annee=' + anneeRecup + ' Acquis=' + acquis + 'j, Solde=' + solde + 'j'); } } - // Total disponible counters.totalDisponible.total = counters.totalDisponible.cp + counters.totalDisponible.rtt + @@ -3121,7 +3464,8 @@ app.get('/api/getDetailedLeaveCounters', async (req, res) => { conn.release(); console.log('\n✅ Réponse envoyée au frontend'); - console.log(' CP disponible:', counters.totalDisponible.cp + 'j'); + console.log(' CP N-1 (Annee=' + previousYear + '):', counters.cpN1?.solde + 'j'); + console.log(' CP N (Annee=' + currentYear + '):', counters.cpN?.solde + 'j'); console.log(' RTT disponible:', counters.totalDisponible.rtt + 'j'); console.log(' RÉCUP disponible:', counters.totalDisponible.recup + 'j'); console.log(' TOTAL disponible:', counters.totalDisponible.total + 'j'); @@ -3194,15 +3538,16 @@ async function deductLeaveBalanceWithTracking(conn, collaborateurId, typeCongeId SET SoldeReporte = CASE WHEN (SoldeReporte - ?) < 0 THEN 0 ELSE (SoldeReporte - ?) END, Solde = CASE WHEN (Solde - ?) < 0 THEN 0 ELSE (Solde - ?) END WHERE Id = ?`, - [aDeduireN1, aDeduireN1, compteurN1[0].Id] + [aDeduireN1, aDeduireN1, 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]); + // Sauvegarde du détail de la déduction N-1 + await conn.query( + `INSERT INTO DeductionDetails + (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) + VALUES (?, ?, ?, 'Reporté N-1', ?)`, + [demandeCongeId, typeCongeId, previousYear, aDeduireN1] + ); deductions.push({ annee: previousYear, @@ -3233,7 +3578,7 @@ async function deductLeaveBalanceWithTracking(conn, collaborateurId, typeCongeId `UPDATE CompteurConges SET Solde = CASE WHEN (Solde - ?) < 0 THEN 0 ELSE (Solde - ?) END WHERE Id = ?`, - [aDeduireN, compteurN[0].Id] + [aDeduireN, aDeduireN, compteurN[0].Id] ); // Sauvegarde du détail de la déduction @@ -3271,7 +3616,7 @@ async function restoreLeaveBalance(conn, demandeCongeId, collaborateurId) { console.log(`Collaborateur ID: ${collaborateurId}`); const [deductions] = await conn.query( - `SELECT dd.TypeCongeId, dd.Annee, dd.TypeDeduction, dd.JoursUtilises, tc.Nom as TypeNom + `SELECT dd.Id AS DeductionId, 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 = ? @@ -3289,11 +3634,11 @@ async function restoreLeaveBalance(conn, demandeCongeId, collaborateurId) { const restorations = []; for (const deduction of deductions) { - const { TypeCongeId, Annee, TypeDeduction, JoursUtilises, TypeNom } = deduction; + const { DeductionId, TypeCongeId, Annee, TypeDeduction, JoursUtilises, TypeNom } = deduction; console.log(`\n🔍 Traitement: ${TypeNom} - ${TypeDeduction} - ${JoursUtilises}j (Année: ${Annee})`); - // ⭐ NOUVEAU : Gestion des Récup posées + // ── Récup posée ─────────────────────────────────────────── if (TypeDeduction === 'Récup Posée') { console.log(`🔄 Restauration Récup posée: +${JoursUtilises}j`); @@ -3309,154 +3654,72 @@ async function restoreLeaveBalance(conn, demandeCongeId, collaborateurId) { await conn.query( `UPDATE CompteurConges - SET Solde = ?, - DerniereMiseAJour = GETDATE() + SET Solde = ?, DerniereMiseAJour = GETDATE() WHERE Id = ?`, [nouveauSolde, compteur[0].Id] ); - restorations.push({ - type: TypeNom, - annee: Annee, - typeDeduction: TypeDeduction, - joursRestores: JoursUtilises - }); + 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`); + // ── Anticipé N+1 ────────────────────────────────────────── + // ✅ Plus de SoldeAnticipe → on supprime uniquement la ligne DeductionDetails + // Le solde réel de la ligne CompteurConges Annee=anneeN n'a jamais été touché + if (TypeDeduction === 'Anticipé N+1') { + console.log(`🔄 Restauration Anticipé N+1: suppression de ${JoursUtilises}j dans DeductionDetails`); + await conn.query( + `DELETE FROM DeductionDetails WHERE Id = ?`, + [DeductionId] + ); + + restorations.push({ type: TypeNom, annee: Annee, typeDeduction: TypeDeduction, joursRestores: JoursUtilises }); + console.log(`✅ Anticipé N+1 restauré: ligne DeductionDetails supprimée`); + continue; + } + + // ── Ancien TypeDeduction 'N Anticipé' (rétrocompatibilité) ─ + // Pour les demandes posées avant la migration vers 'Anticipé N+1' + if (TypeDeduction === 'N Anticipé' || TypeDeduction === 'N+1 Anticipé') { + console.log(`🔄 Restauration ${TypeDeduction} (legacy): suppression de ${JoursUtilises}j`); + + // Si la ligne CompteurConges avait été modifiée (ancienne logique), + // on restitue le Solde en base pour être sûr const [compteur] = await conn.query( - `SELECT Id, SoldeAnticipe FROM CompteurConges + `SELECT Id, Solde 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); - + // Rembourser dans Solde au cas où l'ancienne logique l'avait déduit await conn.query( `UPDATE CompteurConges - SET SoldeAnticipe = ?, + SET Solde = Solde + ?, + SoldeAnticipe = CASE WHEN COALESCE(SoldeAnticipe, 0) - ? < 0 THEN 0 ELSE COALESCE(SoldeAnticipe, 0) - ? END, + IsAnticipe = 0, DerniereMiseAJour = GETDATE() WHERE Id = ?`, - [nouveauSolde, compteur[0].Id] + [JoursUtilises, JoursUtilises, JoursUtilises, 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, ?, GETDATE())`, - [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] + // Supprimer la ligne DeductionDetails dans tous les cas + await conn.query( + `DELETE FROM DeductionDetails WHERE Id = ?`, + [DeductionId] ); - if (compteur.length > 0) { - const ancienSolde = parseFloat(compteur[0].SoldeAnticipe || 0); - const nouveauSolde = ancienSolde + parseFloat(JoursUtilises); - - await conn.query( - `UPDATE CompteurConges - SET SoldeAnticipe = ?, - DerniereMiseAJour = GETDATE() - 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, ?, GETDATE())`, - [collaborateurId, TypeCongeId, Annee, JoursUtilises] - ); - - restorations.push({ - type: TypeNom, - annee: Annee, - typeDeduction: TypeDeduction, - joursRestores: JoursUtilises - }); - console.log(`✅ Compteur N créé avec ${JoursUtilises}j anticipés`); - } + restorations.push({ type: TypeNom, annee: Annee, typeDeduction: TypeDeduction, joursRestores: JoursUtilises }); + console.log(`✅ ${TypeDeduction} (legacy) restauré`); continue; } - // 🔹 Reporté N-1 + // ── 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 = GETDATE() - 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 = ?`, @@ -3469,27 +3732,153 @@ async function restoreLeaveBalance(conn, demandeCongeId, collaborateurId) { await conn.query( `UPDATE CompteurConges - SET Solde = Solde + ?, + SET SoldeReporte = SoldeReporte + ?, + Solde = Solde + ?, + DerniereMiseAJour = GETDATE() + WHERE Id = ?`, + [JoursUtilises, JoursUtilises, compteur[0].Id] + ); + + restorations.push({ type: TypeNom, annee: Annee, typeDeduction: TypeDeduction, joursRestores: JoursUtilises }); + console.log(`✅ Reporté N-1 restauré: ${ancienSolde} → ${nouveauSolde}`); + } + continue; + } + + // ── Année N (déduction normale) ─────────────────────────── + if (TypeDeduction === 'Année N' || TypeDeduction === 'Anne 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 = GETDATE() WHERE Id = ?`, [JoursUtilises, compteur[0].Id] ); - restorations.push({ - type: TypeNom, - annee: Annee, - typeDeduction: TypeDeduction, - joursRestores: JoursUtilises - }); + restorations.push({ type: TypeNom, annee: Annee, typeDeduction: TypeDeduction, joursRestores: JoursUtilises }); console.log(`✅ Année N restaurée: ${ancienSolde} → ${nouveauSolde}`); } + continue; } + + // ── Année N-1 (déduction normale sur exercice précédent) ── + if (TypeDeduction === 'Année N-1' || TypeDeduction === 'Anne N-1') { + 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 = GETDATE() + WHERE Id = ?`, + [JoursUtilises, compteur[0].Id] + ); + + restorations.push({ type: TypeNom, annee: Annee, typeDeduction: TypeDeduction, joursRestores: JoursUtilises }); + console.log(`✅ Année N-1 restaurée: ${ancienSolde} → ${nouveauSolde}`); + } + continue; + } + + // ── Anticip (jours réels déduits du solde CP N) ────────────────────── + if (TypeDeduction === 'Anticip') { + 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 = parseFloat((ancienSolde + parseFloat(JoursUtilises)).toFixed(2)); + await conn.query( + `UPDATE CompteurConges SET Solde = ?, DerniereMiseAJour = GETDATE() WHERE Id = ?`, + [nouveauSolde, compteur[0].Id] + ); + console.log(`✅ Anticip restauré: Solde ${ancienSolde} → ${nouveauSolde}j`); + } + await conn.query(`DELETE FROM DeductionDetails WHERE Id = ?`, [DeductionId]); + restorations.push({ type: TypeNom, annee: Annee, typeDeduction: TypeDeduction, joursRestores: JoursUtilises }); + continue; + } + + // ── AnticipFutur (tracé uniquement, solde non touché) ──────────────── + if (TypeDeduction === 'AnticipFutur') { + await conn.query(`DELETE FROM DeductionDetails WHERE Id = ?`, [DeductionId]); + restorations.push({ type: TypeNom, annee: Annee, typeDeduction: TypeDeduction, joursRestores: JoursUtilises }); + console.log(`✅ AnticipFutur supprimé: ${JoursUtilises}j tracé retiré (aucun solde à remettre)`); + continue; + } + + // ── AnticipRTT (solde RTT N réel déduit) → remettre ───────────────── + if (TypeDeduction === 'AnticipRTT') { + const [compteur] = await conn.query( + `SELECT Id, Solde FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, + [collaborateurId, TypeCongeId, Annee] + ); + if (compteur.length > 0) { + const nouveauSolde = parseFloat((parseFloat(compteur[0].Solde) + parseFloat(JoursUtilises)).toFixed(2)); + await conn.query( + `UPDATE CompteurConges SET Solde = ?, DerniereMiseAJour = GETDATE() WHERE Id = ?`, + [nouveauSolde, compteur[0].Id] + ); + console.log(`✅ AnticipRTT restauré: Solde +${JoursUtilises}j → ${nouveauSolde}j`); + } + await conn.query(`DELETE FROM DeductionDetails WHERE Id = ?`, [DeductionId]); + restorations.push({ type: TypeNom, annee: Annee, typeDeduction: TypeDeduction, joursRestores: JoursUtilises }); + continue; + } + + // ── AnticipFuturRTT (tracé uniquement, solde non touché) ──────────── + if (TypeDeduction === 'AnticipFuturRTT') { + await conn.query(`DELETE FROM DeductionDetails WHERE Id = ?`, [DeductionId]); + restorations.push({ type: TypeNom, annee: Annee, typeDeduction: TypeDeduction, joursRestores: JoursUtilises }); + console.log(`✅ AnticipFuturRTT supprimé: ${JoursUtilises}j tracé retiré (aucun solde à remettre)`); + continue; + } + + // ── AnticipNp1RTT (déduit du Solde compteur RTT N+1) → remettre ───── + if (TypeDeduction === 'AnticipNp1RTT') { + const [compteur] = await conn.query( + `SELECT Id, Solde FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, + [collaborateurId, TypeCongeId, Annee] + ); + if (compteur.length > 0) { + const nouveauSolde = parseFloat((parseFloat(compteur[0].Solde) + parseFloat(JoursUtilises)).toFixed(2)); + await conn.query( + `UPDATE CompteurConges SET Solde = ?, DerniereMiseAJour = GETDATE() WHERE Id = ?`, + [nouveauSolde, compteur[0].Id] + ); + console.log(`✅ AnticipNp1RTT restauré: Solde RTT N+1 +${JoursUtilises}j → ${nouveauSolde}j`); + } + await conn.query(`DELETE FROM DeductionDetails WHERE Id = ?`, [DeductionId]); + restorations.push({ type: TypeNom, annee: Annee, typeDeduction: TypeDeduction, joursRestores: JoursUtilises }); + continue; + } + + // ── TypeDeduction non reconnu → log warning ─────────────── + console.warn(`⚠️ TypeDeduction non géré: "${TypeDeduction}" — ignoré`); } - // ⭐ IMPORTANT : Recalculer les soldes anticipés après restauration - console.log(`\n🔄 Recalcul des soldes anticipés...`); - await updateSoldeAnticipe(conn, collaborateurId); - + // ✅ Suppression de updateSoldeAnticipe — plus nécessaire sans SoldeAnticipe console.log(`\n✅ Restauration terminée: ${restorations.length} opérations\n`); return { @@ -3503,6 +3892,7 @@ async function restoreLeaveBalance(conn, demandeCongeId, collaborateurId) { throw error; } } + app.get('/api/testProrata', async (req, res) => { try { const userId = parseInt(req.query.user_id || 0); @@ -3799,38 +4189,40 @@ app.get('/api/getEmploye', async (req, res) => { 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 id = parseInt(req.query.id) || 0; + if (!id) 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]); + const result = await pool.request() + .input('collabId', sql.Int, id) + .query(` + SELECT + dc.Id, + dc.DateDebut, + dc.DateFin, + dc.NombreJours AS days, + dc.Statut AS status, + dc.DateDemande, + STRING_AGG(tc.Nom, ', ') WITHIN GROUP (ORDER BY tc.Nom) AS type, + CASE + WHEN dc.DateDebut = dc.DateFin + THEN FORMAT(dc.DateDebut, 'dd/MM/yyyy') + ELSE FORMAT(dc.DateDebut, 'dd/MM/yyyy') + ' - ' + FORMAT(dc.DateFin, 'dd/MM/yyyy') + END AS datedisplay + FROM DemandeConge dc + LEFT JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId + LEFT JOIN TypeConge tc ON dct.TypeCongeId = tc.Id + WHERE dc.CollaborateurADId = @collabId + GROUP BY dc.Id, dc.DateDebut, dc.DateFin, dc.NombreJours, dc.Statut, dc.DateDemande + ORDER BY dc.DateDemande DESC + `); - res.json({ + return res.json({ success: true, - requests: rows + requests: result.recordset }); - } catch (error) { - console.error('❌ Erreur getEmployeRequest:', error); - res.status(500).json({ + console.error('Erreur getEmployeRequest', error); + return res.status(500).json({ success: false, message: 'Erreur DB', error: error.message @@ -3838,6 +4230,8 @@ app.get('/api/getEmployeRequest', async (req, res) => { } }); + + app.get('/api/getRequests', async (req, res) => { try { const userId = req.query.user_id; @@ -3849,6 +4243,7 @@ app.get('/api/getRequests', async (req, res) => { let mainRequest = pool.request(); let whereClause; + let collaborateurId; if (isUUID) { // Si UUID, chercher d'abord le CollaborateurADId @@ -3870,20 +4265,19 @@ app.get('/api/getRequests', async (req, res) => { }); } - const collaborateurId = userLookup.recordset[0].id; + collaborateurId = userLookup.recordset[0].id; console.log(`✅ CollaborateurADId trouvé: ${collaborateurId}`); - // Créer une nouvelle request pour la requête principale mainRequest.input('collaborateurId', collaborateurId); whereClause = 'dc.CollaborateurADId = @collaborateurId'; } else { - // Si ID numérique, utiliser directement - mainRequest.input('userId', parseInt(userId)); + collaborateurId = parseInt(userId); + mainRequest.input('userId', collaborateurId); whereClause = '(dc.EmployeeId = @userId OR dc.CollaborateurADId = @userId)'; } - // ✅ REQUÊTE CORRIGÉE pour MSSQL avec la table de liaison + // ✅ REQUÊTE CORRIGÉE - AJOUT de PeriodeJournee et CouleurHex const result = await mainRequest.query(` SELECT dc.Id, @@ -3895,6 +4289,7 @@ app.get('/api/getRequests', async (req, res) => { dc.CommentaireValidation, dc.Validateur, dc.DocumentJoint, + dc.NombreJours, ( SELECT STRING_AGG(Nom, ', ') WITHIN GROUP (ORDER BY Nom) FROM ( @@ -3908,7 +4303,50 @@ app.get('/api/getRequests', async (req, res) => { SELECT SUM(dct2.NombreJours) FROM DemandeCongeType dct2 WHERE dct2.DemandeCongeId = dc.Id - ) AS NombreJoursTotal + ) AS NombreJoursTotal, + -- ⭐ AJOUT: Récupérer les couleurs des types de congé + ( + SELECT STRING_AGG(tc3.CouleurHex, ',') WITHIN GROUP (ORDER BY tc3.Nom) + FROM ( + SELECT DISTINCT tc2.CouleurHex, tc2.Nom + FROM DemandeCongeType dct2 + JOIN TypeConge tc2 ON dct2.TypeCongeId = tc2.Id + WHERE dct2.DemandeCongeId = dc.Id + ) AS tc3 + ) AS Couleurs, + -- ⭐ AJOUT: Récupérer la première couleur (principale) + ( + SELECT TOP 1 tc2.CouleurHex + FROM DemandeCongeType dct2 + JOIN TypeConge tc2 ON dct2.TypeCongeId = tc2.Id + WHERE dct2.DemandeCongeId = dc.Id + ORDER BY dct2.NombreJours DESC + ) AS CouleurPrincipale, + -- ⭐ AJOUT: Récupérer la période de journée + ( + SELECT TOP 1 COALESCE(dct2.PeriodeJournee, 'Journée entière') + FROM DemandeCongeType dct2 + WHERE dct2.DemandeCongeId = dc.Id + ) AS PeriodeJournee, + -- ⭐ AJOUT: Détails JSON des congés (type, jours, période, couleur) + ( + SELECT CONCAT( + '[', + STRING_AGG( + CONCAT( + '{"type":"', tc2.Nom, + '","jours":', dct2.NombreJours, + ',"periode":"', COALESCE(dct2.PeriodeJournee, 'Journée entière'), + '","couleur":"', COALESCE(tc2.CouleurHex, '#6b7280'), '"}' + ), + ',' + ), + ']' + ) + FROM DemandeCongeType dct2 + JOIN TypeConge tc2 ON dct2.TypeCongeId = tc2.Id + WHERE dct2.DemandeCongeId = dc.Id + ) AS DetailsCongesJSON FROM DemandeConge dc WHERE ${whereClause} ORDER BY dc.DateDemande DESC @@ -3928,20 +4366,35 @@ app.get('/api/getRequests', async (req, res) => { fileUrl = `/uploads/${path.basename(row.DocumentJoint)}`; } + // ⭐ Parser les détails JSON si disponible + let detailsConges = []; + if (row.DetailsCongesJSON) { + try { + detailsConges = JSON.parse(row.DetailsCongesJSON); + } catch (e) { + console.warn('Erreur parsing DetailsCongesJSON:', e); + } + } + return { id: row.Id, - type: row.TypeConges || 'Non défini', // ✅ Gérer le cas null + type: row.TypeConges || 'Non défini', startDate: row.DateDebut, endDate: row.DateFin, dateDisplay, - days: row.NombreJoursTotal || workingDays, // ✅ Utiliser le total de la table de liaison + days: row.NombreJoursTotal || row.NombreJours || workingDays, status: row.Statut, reason: row.Commentaire || 'Aucun commentaire', submittedAt: row.DateDemande, submittedDisplay: formatDate(row.DateDemande), validator: row.Validateur || null, validationComment: row.CommentaireValidation || null, - fileUrl + fileUrl, + // ⭐ NOUVEAUX CHAMPS POUR LE CALENDRIER + couleur: row.CouleurPrincipale || '#6b7280', + couleurs: row.Couleurs ? row.Couleurs.split(',') : ['#6b7280'], + periodeJournee: row.PeriodeJournee || 'Journée entière', + detailsConges: detailsConges }; }); @@ -3963,99 +4416,309 @@ app.get('/api/getRequests', async (req, res) => { 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) })); + // ✅ CORRIGÉ : manager_id (cohérent avec frontend) + const managerId = req.query.manager_id; + if (!managerId) return res.json({ success: false, message: 'Paramètre manager_id manquant' }); + + const request = pool.request(); + request.input('managerId', managerId); + + // Récupérer les infos du manager + const managerResult = await request.query(` + SELECT ServiceId, CampusId, role + FROM CollaborateurAD + WHERE id = @managerId + `); + + if (managerResult.recordset.length === 0) { + return res.json({ success: false, message: 'Manager non trouvé' }); + } + + const managerRow = managerResult.recordset[0]; + const serviceId = managerRow.ServiceId; + const campusId = managerRow.CampusId; + const role = normalizeRole(managerRow.role); + + console.log(`🔍 getAllTeamRequests - Manager: ${managerId}, Role: ${role}`); + + let rows; + + // VALIDATEURS ET DIRECTEURS : Service + Campus (vue complète) + if (role === 'validateur' || role === 'directeur de campus') { + const queryRequest = pool.request(); + queryRequest.input('serviceId', serviceId); + queryRequest.input('campusId', campusId); + queryRequest.input('managerId', managerId); + + const result = await queryRequest.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, + STRING_AGG(tc.Nom, ', ') as type + FROM DemandeConge dc + JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id + JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId + JOIN TypeConge tc ON dct.TypeCongeId = tc.Id + WHERE ca.ServiceId = @serviceId + AND ca.CampusId = @campusId + AND ca.id != @managerId + GROUP BY dc.Id, dc.DateDebut, dc.DateFin, dc.Statut, dc.DateDemande, + dc.Commentaire, dc.DocumentJoint, dc.CollaborateurADId, + ca.Prenom, ca.Nom, ca.Email + ORDER BY dc.DateDemande DESC + `); + + rows = result.recordset; + console.log(` ✅ ${rows.length} demande(s) historiques (service/campus)`); + } + // RH : Service + Campus + // RH : Toutes les demandes (tous services et campus) + else if (role === 'rh') { + const queryRequest = pool.request(); + queryRequest.input('managerId', managerId); + + const result = await queryRequest.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, + STRING_AGG(tc.Nom, ', ') as type + FROM DemandeConge dc + JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id + JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId + JOIN TypeConge tc ON dct.TypeCongeId = tc.Id + WHERE ca.id != @managerId + GROUP BY dc.Id, dc.DateDebut, dc.DateFin, dc.Statut, dc.DateDemande, + dc.Commentaire, dc.DocumentJoint, dc.CollaborateurADId, + ca.Prenom, ca.Nom, ca.Email + ORDER BY dc.DateDemande DESC + `); + + rows = result.recordset; + console.log(` ✅ ${rows.length} demande(s) historiques (RH - toutes)`); + } + // ADMIN/PRESIDENT : Toutes + else { + const result = await pool.request().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, + STRING_AGG(tc.Nom, ', ') as type + FROM DemandeConge dc + JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id + JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId + JOIN TypeConge tc ON dct.TypeCongeId = tc.Id + GROUP BY dc.Id, dc.DateDebut, dc.DateFin, dc.Statut, dc.DateDemande, + dc.Commentaire, dc.DocumentJoint, dc.CollaborateurADId, + ca.Prenom, ca.Nom, ca.Email + ORDER BY dc.DateDemande DESC + `); + + rows = result.recordset; + console.log(` ✅ ${rows.length} demande(s) historiques (Admin)`); + } + + 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 || 'Non défini', + 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 }); + console.error('❌ Erreur getAllTeamRequests:', error); + res.status(500).json({ + success: false, + message: 'Erreur DB', + error: error.message + }); } }); + + app.get('/api/getPendingRequests', async (req, res) => { try { - const validatorId = req.query.validator_id; + // ✅ CORRIGÉ : manager_id + const managerId = req.query.manager_id; + if (!managerId) return res.json({ success: false, message: 'ID manager manquant' }); - if (!validatorId) { - return res.json({ success: false, message: 'ID validateur manquant' }); + const request = pool.request(); + request.input('managerId', managerId); + + const managerResult = await request.query(` + SELECT ServiceId, CampusId, role + FROM CollaborateurAD + WHERE id = @managerId + `); + + if (managerResult.recordset.length === 0) { + return res.json({ success: false, message: 'Manager non trouvé' }); } - const conn = await pool.getConnection(); + const managerRow = managerResult.recordset[0]; + const serviceId = managerRow.ServiceId; + const campusId = managerRow.CampusId; + const role = normalizeRole(managerRow.role); - // Récupérer les infos du validateur - const [managerRows] = await conn.query( - 'SELECT TOP 1 ServiceId, CampusId, role FROM CollaborateurAD WHERE id = ?', - [validatorId] - ); + console.log(`🔍 getPendingRequests - Manager: ${managerId}, Role: ${role}`); - if (managerRows.length === 0) { - conn.release(); - return res.json({ success: false, message: 'Validateur non trouvé' }); - } + let rows; - const serviceId = managerRows[0].ServiceId; - const campusId = managerRows[0].CampusId; - const role = normalizeRole(managerRows[0].role); + // VALIDATEURS, DIRECTEURS ET PRÉSIDENTS : Leurs collaborateurs directs (hiérarchie) + if (role === 'validateur' || role === 'directeur de campus' || role === 'president') { + const queryRequest = pool.request(); + queryRequest.input('managerId', managerId); - let requests; - - if (role === 'admin' || role === 'president' || role === 'rh') { - // Admin/President/RH : toutes les demandes en attente - [requests] = await conn.query(` + const result = await queryRequest.query(` SELECT dc.Id, - dc.CollaborateurADId, dc.DateDebut, dc.DateFin, - dc.NombreJours, dc.Statut, - dc.Commentaire, - tc.Nom as TypeConge, - ca.prenom, - ca.nom, - s.Nom as service_name, - camp.Nom as campus_name + dc.DateDemande, + dc.Commentaire, + dc.CollaborateurADId, + CONCAT(ca.prenom, ' ', ca.nom) as employee_name, + ca.email as employee_email, + STRING_AGG(tc.Nom, ', ') as types, + SUM(dct.NombreJours) as NombreJoursTotal FROM DemandeConge dc - JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.Id - JOIN TypeConge tc ON dc.TypeCongeId = tc.Id - LEFT JOIN Services s ON ca.ServiceId = s.Id - LEFT JOIN Campus camp ON ca.CampusId = camp.Id - WHERE dc.Statut = 'En attente' - ORDER BY dc.DateCreation DESC - `); - } else if (role === 'validateur' || role === 'directeur de campus') { - // Validateur/Directeur : leurs collaborateurs directs via hiérarchie - [requests] = await conn.query(` - SELECT DISTINCT - dc.Id, - dc.CollaborateurADId, - dc.DateDebut, - dc.DateFin, - dc.NombreJours, - dc.Statut, - dc.Commentaire, - tc.Nom as TypeConge, - ca.prenom, - ca.nom, - s.Nom as service_name, - camp.Nom as campus_name - FROM DemandeConge dc - JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.Id + JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id JOIN HierarchieValidationAD hv ON ca.id = hv.CollaborateurId - JOIN TypeConge tc ON dc.TypeCongeId = tc.Id - LEFT JOIN Services s ON ca.ServiceId = s.Id - LEFT JOIN Campus camp ON ca.CampusId = camp.Id + LEFT JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId + LEFT JOIN TypeConge tc ON dct.TypeCongeId = tc.Id + WHERE hv.SuperieurId = @managerId + AND dc.Statut = 'En attente' + 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 + `); + + rows = result.recordset; + console.log(` ✅ ${rows.length} demande(s) à valider (hiérarchie - ${role})`); + } + // RH : Service + Campus + else if (role === 'rh') { + const queryRequest = pool.request(); + queryRequest.input('serviceId', serviceId); + queryRequest.input('campusId', campusId); + queryRequest.input('managerId', managerId); + + const result = await queryRequest.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, + STRING_AGG(tc.Nom, ', ') as types, + SUM(dct.NombreJours) as NombreJoursTotal + 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 + WHERE ca.ServiceId = @serviceId + AND ca.CampusId = @campusId + AND dc.Statut = 'En attente' + AND ca.id != @managerId + 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 + `); + + rows = result.recordset; + console.log(` ✅ ${rows.length} demande(s) à valider (RH)`); + } + // ADMIN : Toutes les demandes en attente + else if (role === 'admin') { + const result = await pool.request().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, + STRING_AGG(tc.Nom, ', ') as types, + SUM(dct.NombreJours) as NombreJoursTotal + 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 WHERE dc.Statut = 'En attente' - AND hv.SuperieurId = ? - ORDER BY dc.DateCreation DESC - `, [validatorId]); + 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 + `); + + rows = result.recordset; + console.log(` ✅ ${rows.length} demande(s) à valider (Admin)`); } else { - conn.release(); - return res.json({ success: false, message: 'Rôle non autorisé pour validation' }); + return res.json({ success: false, message: 'Rôle non autorisé pour la validation' }); } - conn.release(); + 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 || 'Non défini', + start_date: row.DateDebut, + end_date: row.DateFin, + date_display: row.DateDebut === row.DateFin + ? formatDate(row.DateDebut) + : `${formatDate(row.DateDebut)} - ${formatDate(row.DateFin)}`, + days: row.NombreJoursTotal || getWorkingDays(row.DateDebut, row.DateFin), + status: row.Statut, + reason: row.Commentaire || '', + submitted_at: row.DateDemande, + submitted_display: formatDate(row.DateDemande) + })); res.json({ success: true, @@ -4076,6 +4739,8 @@ app.get('/api/getPendingRequests', async (req, res) => { }); + + // Route getTeamMembers - Membres de l'équipe app.get('/api/getTeamMembers', async (req, res) => { try { @@ -4400,8 +5065,8 @@ app.post('/api/saisirRecupJour', async (req, res) => { // SAUVEGARDER DANS DemandeCongeType await conn.query(` INSERT INTO DemandeCongeType - (DemandeCongeId, TypeCongeId, NombreJours, PeriodeJournee) - VALUES (?, ?, ?, 'Journée entière') + (DemandeCongeId, TypeCongeId, NombreJours, PeriodeJournee, DateDebut, DateFin) + VALUES (?, ?, ?, 'Journée entière',?, ?) `, [demandeId, recupTypeId, nombre_heures]); // ACCUMULER DANS LE COMPTEUR @@ -4513,14 +5178,640 @@ app.get('/api/getMesSamedis', async (req, res) => { } }); -async function checkLeaveBalanceWithAnticipation(conn, collaborateurId, repartition, dateDebut) { - const dateDebutObj = new Date(dateDebut); - const currentYear = dateDebutObj.getFullYear(); + +/** + * Déduit les jours d'un compteur avec gestion de l'anticipation + * Ordre de déduction : N-1 → N → N Anticip + */ + +// ======================================== +// 💰 FONCTION DE DÉDUCTION AVEC ANTICIPATION (SANS ID MANUEL) +// ======================================== + +// ============================================================================ +// 🔄 FONCTION DE DÉDUCTION AVEC ORDRE CORRIGÉ : N-1 → N → ANTICIPÉ +// ============================================================================ + +// ============================================================================ +// 🔄 FONCTION DE DÉDUCTION CORRIGÉE POUR SQL SERVER +// Ordre : N-1 → N → ANTICIPÉ +// ============================================================================ + +// ============================================================================ +// 🔄 FONCTION DE DÉDUCTION CORRIGÉE POUR SQL SERVER (avec syntaxe ?) +// Ordre : N-1 → N → ANTICIPÉ +// ============================================================================ + +// FONCTION CORRIGÉE : deductLeaveBalanceWithAnticipation +// Ordre de déduction : N-1 → N → N Anticipé +// FONCTION CORRIGÉE pour SQL Server +// FONCTION CORRIGÉE pour votre wrapper MySQL +// FONCTION CORRIGÉE POUR SQL SERVER - BASÉE SUR VOTRE ANCIEN CODE MYSQL +// ✅ VERSION SQL SERVER - CONVERSION DE VOTRE VERSION MYSQL QUI FONCTIONNE +async function deductLeaveBalanceWithAnticipation(conn, collaborateurId, typeCongeId, nombreJours, demandeCongeId, dateDebut) { + const today = new Date(); + const currentYear = today.getFullYear(); const previousYear = currentYear - 1; - console.log('\n🔍 === CHECK SOLDES AVEC ANTICIPATION ==='); - console.log(`📅 Date demande: ${dateDebut}`); - console.log(`📅 Année demande: ${currentYear}`); + let joursRestants = nombreJours; + const deductions = []; + + console.log("📊 DÉDUCTION AVEC PRIORITÉ N-1"); + console.log(`Jours à déduire: ${nombreJours}j`); + + // ======================================== + // 🔍 Vérifier si c'est un CP (pour gérer N-1) + // ======================================== + const [typeInfo] = await conn.query( + `SELECT Nom FROM TypeConge WHERE Id = ?`, + [typeCongeId] + ); + + const isCP = typeInfo.length > 0 && typeInfo[0].Nom === 'Congé payé'; + + // ======================================== + // 1️⃣ Déduire du REPORT N-1 (CP uniquement) + // ======================================== + if (isCP) { + 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 = CASE WHEN Solde - ? < 0 THEN 0 ELSE Solde - ? END, + SoldeReporte = CASE WHEN SoldeReporte - ? < 0 THEN 0 ELSE SoldeReporte - ? END, + DerniereMiseAJour = GETDATE() + WHERE Id=?`, + [aDeduireN1, aDeduireN1, 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 + }); + + joursRestants -= aDeduireN1; + console.log(`✅ Report N-1: ${aDeduireN1.toFixed(2)}j déduits (reste ${joursRestants.toFixed(2)}j)`); + } else { + console.log(`⚠️ Aucun solde N-1 disponible`); + } + } else { + console.log(`⚠️ Pas de compteur N-1 trouvé`); + } + } + + // ======================================== + // 2️⃣ Déduire du SOLDE N (acquis 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 soldeTotal = parseFloat(compteurN[0].Solde || 0); + const soldeReporte = parseFloat(compteurN[0].SoldeReporte || 0); + + // ⭐ CORRECTION : Le solde N est le solde total MOINS le report + // Car le Solde contient déjà le SoldeReporte + const soldeNAcquis = isCP ? Math.max(0, soldeTotal - soldeReporte) : soldeTotal; + const aDeduireN = Math.min(soldeNAcquis, joursRestants); + + if (aDeduireN > 0) { + await conn.query( + `UPDATE CompteurConges + SET Solde = CASE WHEN Solde - ? < 0 THEN 0 ELSE Solde - ? END, + DerniereMiseAJour = GETDATE() + WHERE Id=?`, + [aDeduireN, 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 + }); + + joursRestants -= aDeduireN; + console.log(`✅ Année N: ${aDeduireN.toFixed(2)}j déduits (reste ${joursRestants.toFixed(2)}j)`); + } + } + } + + if (joursRestants > 0) { + console.log(`❌ ERREUR: ${joursRestants.toFixed(2)}j non couverts`); + } + + console.log(`✅ Déduction terminée - Total déduit: ${(nombreJours - joursRestants).toFixed(2)}j`); + + return { + success: joursRestants === 0, + joursDeduitsTotal: nombreJours - joursRestants, + joursNonDeduits: joursRestants, + details: deductions + }; +} +/** + * Calcule l'acquisition CP projetée à une date donnée + * Règle : 2.5j/mois travaillé depuis la date d'entrée (ou juin N-1 si déjà là) + * Plafond exercice : 25j (juin N → mai N+1) + */ +function calculerAcquisitionCPSmart(dateReference, dateEntree) { + if (!dateEntree) return 0; + + const ref = new Date(dateReference); + const entree = new Date(dateEntree); + + // Début de l'exercice en cours (1er juin de l'année N-1 ou N selon mois) + const moisRef = ref.getMonth() + 1; // 1-12 + const anneeRef = ref.getFullYear(); + const debutExercice = moisRef >= 6 + ? new Date(anneeRef, 5, 1) // 1er juin anneeRef + : new Date(anneeRef - 1, 5, 1); // 1er juin anneeRef-1 + + // Le décompte commence au plus tard entre entrée et début exercice + const dateDepart = entree > debutExercice ? entree : debutExercice; + + if (ref <= dateDepart) return 0; + + // Nombre de mois complets travaillés entre dateDepart et dateReference + let mois = 0; + const d = new Date(dateDepart); + + while (true) { + const next = new Date(d.getFullYear(), d.getMonth() + 1, d.getDate()); + if (next > ref) break; + mois++; + d.setMonth(d.getMonth() + 1); + } + + // 2.5j par mois, plafonné à 25j + return Math.min(25, mois * 2.5); +} + +/** + * Vérifie si le collaborateur a assez de CP pour la demande. + * Respecte les règles N-1 prioritaire (NORMAL) / N seulement (ANTICIPATION). + */ +// ✅ checkLeaveBalanceCP CORRIGÉE +// ========================================== +// FONCTION CORRIGÉE - deductLeaveBalanceCP +// ========================================== + +async function deductLeaveBalanceCP(conn, collaborateurId, cpTypeId, nombreJours, demandeId, dateDebut) { + const today = new Date(); + const currentYear = today.getFullYear(); + + const dateBascule = new Date(currentYear, 5, 1); // 01/06/currentYear + dateBascule.setHours(0, 0, 0, 0); + const apresBasculement = today >= dateBascule; + + // Exercice en cours (Annee = année de FIN) + const anneeExCourant = apresBasculement ? currentYear + 1 : currentYear; + const anneeExPrecedent = anneeExCourant - 1; + + // Exercice CP du congé demandé + const dateDebutConge = new Date(dateDebut); + const moisConge = dateDebutConge.getMonth() + 1; + const anneeConge = dateDebutConge.getFullYear(); + const anneeExConge = moisConge >= 6 ? anneeConge + 1 : anneeConge; + + // Cas anticipé : le congé est sur l'exercice SUIVANT + const isAnticipe = anneeExConge > anneeExCourant; + + console.log(` CP — exerciceCourant=${anneeExCourant} | exerciceConge=${anneeExConge} | anticipé=${isAnticipe}`); + + let joursRestants = nombreJours; + const deductions = []; + + if (!isAnticipe) { + // ── CAS NORMAL ────────────────────────────────────────────────── + // Priorité 1 : CP N-1 + const [compteurN1] = await conn.query( + `SELECT Id, Solde FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, + [collaborateurId, cpTypeId, anneeExPrecedent] + ); + + if (compteurN1.length > 0) { + const soldeN1 = Math.max(0, parseFloat(compteurN1[0].Solde) || 0); + console.log(` 💰 CP N-1 (Annee=${anneeExPrecedent}): ${soldeN1.toFixed(2)}j`); + + const aDeduireN1 = Math.min(soldeN1, joursRestants); + if (aDeduireN1 > 0) { + await conn.query( + `UPDATE CompteurConges + SET Solde = CASE WHEN Solde - ? < 0 THEN 0 ELSE Solde - ? END, + DerniereMiseAJour = GETDATE() + WHERE Id = ?`, + [aDeduireN1, aDeduireN1, compteurN1[0].Id] + ); + await conn.query( + `INSERT INTO DeductionDetails + (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) + VALUES (?, ?, ?, 'Report N-1', ?)`, + [demandeId, cpTypeId, anneeExPrecedent, aDeduireN1] + ); + deductions.push({ annee: anneeExPrecedent, type: 'Report N-1', joursUtilises: aDeduireN1 }); + joursRestants -= aDeduireN1; + console.log(` ✅ CP N-1 déduit: ${aDeduireN1}j (reste ${joursRestants}j)`); + } + } else { + console.log(` ℹ️ Pas de compteur CP N-1 (Annee=${anneeExPrecedent})`); + } + + // Priorité 2 : CP N courant + if (joursRestants > 0) { + const [compteurN] = await conn.query( + `SELECT Id, Solde, SoldeReporte FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, + [collaborateurId, cpTypeId, anneeExCourant] + ); + + 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); + console.log(` 💰 CP N (Annee=${anneeExCourant}): ${soldeTotal}j - ${soldeReporte}j reporté = ${soldeN.toFixed(2)}j`); + + const aDeduireN = Math.min(soldeN, joursRestants); + if (aDeduireN > 0) { + await conn.query( + `UPDATE CompteurConges + SET Solde = CASE WHEN Solde - ? < 0 THEN 0 ELSE Solde - ? END, + DerniereMiseAJour = GETDATE() + WHERE Id = ?`, + [aDeduireN, aDeduireN, compteurN[0].Id] + ); + await conn.query( + `INSERT INTO DeductionDetails + (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) + VALUES (?, ?, ?, 'Année N', ?)`, + [demandeId, cpTypeId, anneeExCourant, aDeduireN] + ); + deductions.push({ annee: anneeExCourant, type: 'Année N', joursUtilises: aDeduireN }); + joursRestants -= aDeduireN; + console.log(` ✅ CP N déduit: ${aDeduireN}j (reste ${joursRestants}j)`); + } + } else { + console.log(` ⚠️ Pas de compteur CP N (Annee=${anneeExCourant})`); + } + } + + return { + success: joursRestants === 0, + mode: 'NORMAL', + joursDeduitsTotal: nombreJours - joursRestants, + joursNonDeduits: joursRestants, + details: deductions + }; + + } else { + // ── CAS ANTICIPÉ ───────────────────────────────────────────────── + const TAUX_CP = 25 / 360; + const anneeN_anticipe = anneeExCourant; + + // CORRECTION 2+3 : Lire Solde, SoldeReporte ET Total dans la même requête + const [cNRows] = await conn.query( + `SELECT Id, Solde, COALESCE(SoldeReporte, 0) AS SoldeReporte, COALESCE(Total, 0) AS Total + FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, + [collaborateurId, cpTypeId, anneeN_anticipe] + ); + + // CORRECTION 2 : Solde N = Solde brut - SoldeReporte + const soldeNBrut = cNRows.length > 0 ? Math.max(0, parseFloat(cNRows[0].Solde || 0)) : 0; + const soldeNReporte = cNRows.length > 0 ? parseFloat(cNRows[0].SoldeReporte || 0) : 0; + const soldeNActuel = Math.max(0, soldeNBrut - soldeNReporte); + + // CORRECTION 3 : Plafond calculé avec Total acquis (pas Solde) + const totalDejaAcquisN = cNRows.length > 0 ? parseFloat(cNRows[0].Total || 0) : 0; + + // Projection jusqu'au 31/05/anneeN_anticipe + // Projection jusqu'à la DATE DU CONGÉ (pas la fin d'exercice) + const dateDebutCongeObj = new Date(dateDebut); + dateDebutCongeObj.setHours(0, 0, 0, 0); + const joursJusquAuConge = Math.max(0, Math.floor((dateDebutCongeObj - today) / (1000 * 60 * 60 * 24))); + const plafondRestantN = Math.max(0, 25 - totalDejaAcquisN); + const acquisJusquAuConge = parseFloat(Math.min(plafondRestantN, joursJusquAuConge * TAUX_CP).toFixed(2)); + const soldeNProj = parseFloat(Math.min(25, soldeNActuel + acquisJusquAuConge).toFixed(2)); + + console.log(` 📊 Projection CP N jusqu'au ${dateDebut}: soldeActuel=${soldeNActuel}j, plafond=${plafondRestantN}j, jours d'ici le congé=${joursJusquAuConge}j, acquis d'ici=${acquisJusquAuConge}j, solde projeté=${soldeNProj}j`); + + // Déjà consommé sur Annee=anneeN_anticipe (Anticip + AnticipFutur des demandes précédentes) + const dejaConsoNRows = 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 IN ('Anticip', 'AnticipFutur') + AND dc.Statut NOT IN ('Refusé', 'Refusée', 'Annulé', 'Annulée') + AND dc.DateDebut <= ?`, + [collaborateurId, cpTypeId, anneeN_anticipe, dateDebut] + ); + const dejaConsoN = parseFloat(dejaConsoNRows[0]?.total || 0); // ← CETTE LIGNE + const dispoN = parseFloat(Math.max(0, soldeNProj - dejaConsoN).toFixed(2)); + console.log(` 💰 CP N (${anneeN_anticipe}): actuel=${soldeNActuel.toFixed(2)}j + acquis d'ici=${acquisJusquAuConge.toFixed(2)}j = projeté ${soldeNProj.toFixed(2)}j - consommé anticipé ${dejaConsoN.toFixed(2)}j = dispo ${dispoN.toFixed(2)}j`); + // CP N+1 prorata depuis 01/06/anneeN_anticipe jusqu'à dateDebutConge + const debutNp1 = new Date(anneeN_anticipe, 5, 1); // 01/06/anneeN_anticipe + debutNp1.setHours(0, 0, 0, 0); + const diffJoursNp1 = Math.max(0, Math.floor((dateDebutConge - debutNp1) / (1000 * 60 * 60 * 24))); + const acquisNp1 = parseFloat(Math.min(25, diffJoursNp1 * TAUX_CP).toFixed(2)); + const dejaConsoNp1Rows = 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 dc.Statut NOT IN ('Refusé', 'Refusée', 'Annulé', 'Annulée') + AND dc.DateDebut <= ?`, + [collaborateurId, cpTypeId, anneeExConge, dateDebut] + ); + const dejaConsoNp1 = parseFloat(dejaConsoNp1Rows[0]?.total || 0); + const dispoNp1 = parseFloat(Math.max(0, acquisNp1 - dejaConsoNp1).toFixed(2)); + console.log(` 💰 CP N+1 (${anneeExConge}): ${diffJoursNp1}j × ${TAUX_CP.toFixed(4)} = ${acquisNp1}j - consommé ${dejaConsoNp1}j = dispo ${dispoNp1}j`); + + const totalDispo = parseFloat((dispoN + dispoNp1).toFixed(2)); + console.log(` 📊 Total anticipé: N=${dispoN.toFixed(2)}j + N+1=${dispoNp1.toFixed(2)}j = ${totalDispo.toFixed(2)}j | demandés=${nombreJours}j`); + + if (totalDispo < nombreJours) { + console.warn(` ❌ Anticipé bloqué — ${totalDispo.toFixed(2)}j dispo < ${nombreJours}j demandés`); + return { + success: false, + mode: 'ANTICIPÉ', + joursDeduitsTotal: 0, + joursNonDeduits: nombreJours, + soldeNProj, dejaConsoN, dispoN, acquisNp1, dejaConsoNp1, dispoNp1, totalDispo, + details: [], + message: `Solde CP anticipé insuffisant : ${totalDispo.toFixed(2)}j disponibles (N: ${dispoN.toFixed(2)}j + N+1: ${dispoNp1.toFixed(2)}j), ${nombreJours}j demandés` + }; + } + + // ── Déduction ──────────────────────────────────────────────────── + let joursRestants = nombreJours; + + // 1️⃣ Déduire du solde réel actuel (ce qui est vraiment en base) + if (joursRestants > 0 && soldeNActuel > 0 && cNRows.length > 0) { + const aDeduire = parseFloat(Math.min(soldeNActuel, joursRestants).toFixed(2)); + await conn.query( + `UPDATE CompteurConges SET Solde = Solde - ?, DerniereMiseAJour = GETDATE() WHERE Id = ?`, + [aDeduire, cNRows[0].Id] + ); + await conn.query( + `INSERT INTO DeductionDetails (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) + VALUES (?, ?, ?, 'Anticip', ?)`, + [demandeId, cpTypeId, anneeN_anticipe, aDeduire] + ); + deductions.push({ annee: anneeN_anticipe, type: 'Anticip', joursUtilises: aDeduire }); + joursRestants = parseFloat((joursRestants - aDeduire).toFixed(2)); + console.log(` ✅ CP N (${anneeN_anticipe}) Anticip: ${aDeduire.toFixed(2)}j sur solde réel (reste futur: ${joursRestants.toFixed(2)}j)`); + } + + // 2️⃣ Jours restants dans CP N (acquis futurs jusqu'au 31/05) → AnticipFutur + if (joursRestants > 0 && dispoN > 0) { + const aTracerN = parseFloat(Math.min(dispoN, joursRestants).toFixed(2)); + await conn.query( + `INSERT INTO DeductionDetails (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) + VALUES (?, ?, ?, 'AnticipFutur', ?)`, + [demandeId, cpTypeId, anneeN_anticipe, aTracerN] + ); + deductions.push({ annee: anneeN_anticipe, type: 'AnticipFutur', joursUtilises: aTracerN }); + joursRestants = parseFloat((joursRestants - aTracerN).toFixed(2)); + console.log(` ✅ CP N (${anneeN_anticipe}) AnticipFutur: ${aTracerN.toFixed(2)}j tracé — reste: ${joursRestants.toFixed(2)}j`); + } + + // 3️⃣ Jours restants = prorata CP N+1 → AnticipNp1 + if (joursRestants > 0 && dispoNp1 > 0) { + const aTracerNp1 = parseFloat(Math.min(dispoNp1, joursRestants).toFixed(2)); + const [cNp1Rows] = await conn.query( + `SELECT Id, Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, + [collaborateurId, cpTypeId, anneeExConge] + ); + if (cNp1Rows.length === 0) { + const [newIdRows] = await conn.query(`SELECT ISNULL(MAX(Id), 0) + 1 AS nextId FROM CompteurConges`); + const newId = (newIdRows[0]?.nextId) || 1; + await conn.query( + `INSERT INTO CompteurConges (Id, CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour) VALUES (?, ?, ?, ?, 0, ?, 0, GETDATE())`, + [newId, collaborateurId, cpTypeId, anneeExConge, -aTracerNp1] + ); + console.log(` ➕ CompteurConges Annee=${anneeExConge} créé Solde=${-aTracerNp1}j`); + } else { + await conn.query( + `UPDATE CompteurConges SET Solde = Solde - ?, DerniereMiseAJour = GETDATE() WHERE Id = ?`, + [aTracerNp1, cNp1Rows[0].Id] + ); + console.log(` ✏️ CompteurConges Annee=${anneeExConge} Solde - ${aTracerNp1}j`); + } + await conn.query( + `INSERT INTO DeductionDetails (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) + VALUES (?, ?, ?, 'AnticipNp1', ?)`, + [demandeId, cpTypeId, anneeExConge, aTracerNp1] + ); + deductions.push({ annee: anneeExConge, type: 'AnticipNp1', joursUtilises: aTracerNp1 }); + joursRestants = parseFloat((joursRestants - aTracerNp1).toFixed(2)); + console.log(` ✅ CP N+1 (${anneeExConge}) AnticipNp1: ${aTracerNp1.toFixed(2)}j tracé`); + } + + return { + success: true, mode: 'ANTICIPÉ', + joursDeduitsTotal: nombreJours, joursNonDeduits: 0, + soldeNProj, dispoN, details: deductions + }; + } +} + + +// ========================================== +// FONCTION CORRIGÉE - checkLeaveBalanceCP +// ========================================== + +async function checkLeaveBalanceCP(conn, collaborateurId, joursNecessaires, dateDebut) { + const today = new Date(); + today.setHours(0, 0, 0, 0); + const currentYear = today.getFullYear(); + const userIdInt = parseInt(collaborateurId, 10); + const dateDebutObj = new Date(dateDebut); + dateDebutObj.setHours(0, 0, 0, 0); + + const TAUX_CP = 25 / 360; + + const dateBascule = new Date(currentYear, 5, 1); + dateBascule.setHours(0, 0, 0, 0); + const apresBasculement = today >= dateBascule; + const anneeExCourant = apresBasculement ? currentYear + 1 : currentYear; + const anneeExPrecedent = anneeExCourant - 1; + + const finExerciceCourant = new Date(anneeExCourant, 4, 31); + finExerciceCourant.setHours(23, 59, 59, 0); + const isAnticipe = dateDebutObj > finExerciceCourant; + + const moisDebut = dateDebutObj.getMonth() + 1; + const anneeDebut = dateDebutObj.getFullYear(); + const anneeExConge = moisDebut >= 6 ? anneeDebut + 1 : anneeDebut; + + console.log(`\n🔍 CHECK CP BALANCE`); + console.log(` Collaborateur: ${userIdInt}`); + console.log(` Jours nécessaires: ${joursNecessaires}j`); + console.log(` Exercice courant: ${anneeExCourant} | Fin exercice: ${finExerciceCourant.toLocaleDateString('fr-FR')}`); + console.log(` Date congé: ${dateDebut} | Mode: ${isAnticipe ? 'ANTICIPÉ 🔮' : 'NORMAL'}`); + + const [cpTypeRows] = await conn.query( + `SELECT TOP 1 Id FROM TypeConge WHERE Nom = ?`, + ['Congé payé'] + ); + if (cpTypeRows.length === 0) throw new Error('Type CP introuvable'); + const cpTypeId = cpTypeRows[0].Id; + + // ── Lire solde ET total acquis ───────────────────────────────────────── + const [cNRows] = await conn.query( + `SELECT TOP 1 COALESCE(Solde, 0) AS Solde, COALESCE(SoldeReporte, 0) AS SoldeReporte, + COALESCE(Total, 0) AS Total + FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? + ORDER BY Id DESC`, + [userIdInt, cpTypeId, anneeExCourant] + ); + const soldeNActuel = cNRows.length > 0 + ? Math.max(0, parseFloat(cNRows[0].Solde) - parseFloat(cNRows[0].SoldeReporte)) + : 0; + const totalDejaAcquis = cNRows.length > 0 ? parseFloat(cNRows[0].Total) : 0; + + // ── Projection CP N jusqu'à la date du congé ────────────────────────── + const joursJusquAuDebut = Math.max(0, Math.floor((dateDebutObj - today) / (1000 * 60 * 60 * 24))); + const acquisDiciDebut = parseFloat( + Math.min(25 - totalDejaAcquis, joursJusquAuDebut * TAUX_CP).toFixed(2) + ); + const soldeNProjecte = parseFloat((soldeNActuel + acquisDiciDebut).toFixed(2)); + + // ── Déjà consommé Anticip+AnticipFutur sur anneeExCourant ───────────── + const dejaConsoNRows = 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 IN ('Anticip', 'AnticipFutur') + AND dc.Statut != 'Refusé' + AND dc.DateDebut <= ? -- ✅ AJOUT +`, [userIdInt, cpTypeId, anneeExCourant, dateDebut]); + const dejaConsoN = parseFloat(dejaConsoNRows[0]?.total || 0); + const soldeNNet = parseFloat(Math.max(0, soldeNProjecte - dejaConsoN).toFixed(2)); + + console.log(` CP N (${anneeExCourant}): actuel=${soldeNActuel}j + ${joursJusquAuDebut}j × ${TAUX_CP.toFixed(4)} = +${acquisDiciDebut}j → projeté ${soldeNProjecte}j - consommé ${dejaConsoN}j = net ${soldeNNet}j`); + + if (isAnticipe) { + // ── MODE ANTICIPÉ : CP N net + CP N+1 prorata jusqu'à dateDebutObj ── + const debutNp1 = new Date(anneeExCourant, 5, 1); + debutNp1.setHours(0, 0, 0, 0); + const diffJoursNp1 = Math.max(0, Math.floor((dateDebutObj - debutNp1) / (1000 * 60 * 60 * 24))); + const acquisNp1 = parseFloat(Math.min(25, diffJoursNp1 * TAUX_CP).toFixed(2)); + + const dejaConsoNp1Rows = 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 dc.Statut != 'Refusé' + AND dc.DateDebut <= ? -- ✅ AJOUT +`, [userIdInt, cpTypeId, anneeExConge, dateDebut]); + const dejaConsoNp1 = parseFloat(dejaConsoNp1Rows[0]?.total || 0); + const dispoNp1 = parseFloat(Math.max(0, acquisNp1 - dejaConsoNp1).toFixed(2)); + + const soldeTotalDisponible = parseFloat((soldeNNet + dispoNp1).toFixed(2)); + const suffisant = soldeTotalDisponible >= joursNecessaires; + const deficit = suffisant ? 0 : parseFloat((joursNecessaires - soldeTotalDisponible).toFixed(2)); + + console.log(` CP N+1 (${anneeExConge}): ${diffJoursNp1}j × ${TAUX_CP.toFixed(4)} = ${acquisNp1}j - consommé ${dejaConsoNp1}j = dispo ${dispoNp1}j`); + console.log(` 📊 Total anticipé: ${soldeTotalDisponible}j | besoin=${joursNecessaires}j`); + console.log(` ${suffisant ? '✅ AUTORISÉ' : '❌ BLOQUÉ'}`); + + return { + valide: suffisant, + mode: 'ANTICIPATION', + isAnticipe: true, + anneeN: anneeExCourant, + anneeNp1: anneeExConge, + soldeNProjecte, dejaConsoN, soldeNNet, + acquisNp1, dejaConsoNp1, dispoNp1, + soldeTotalDisponible, deficit, + message: suffisant ? undefined + : `Solde CP insuffisant (⏩ Anticipé) Maximum disponible : ${soldeTotalDisponible.toFixed(2)}j — Demandé : ${joursNecessaires}j — Déficit : ${deficit.toFixed(2)}j` + }; + + } else { + // ── MODE NORMAL : N-1 + N projeté ───────────────────────────────── + const [cN1Rows] = await conn.query( + `SELECT TOP 1 COALESCE(Solde, 0) AS Solde + FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? + ORDER BY Id DESC`, + [userIdInt, cpTypeId, anneeExPrecedent] + ); + const soldeN1 = cN1Rows.length > 0 ? Math.max(0, parseFloat(cN1Rows[0].Solde)) : 0; + + const soldeTotalDisponible = parseFloat((soldeN1 + soldeNNet).toFixed(2)); + const suffisant = soldeTotalDisponible >= joursNecessaires; + const deficit = suffisant ? 0 : parseFloat((joursNecessaires - soldeTotalDisponible).toFixed(2)); + + console.log(` CP N-1 (${anneeExPrecedent}): ${soldeN1}j`); + console.log(` Total disponible: N-1=${soldeN1}j + N net=${soldeNNet}j = ${soldeTotalDisponible}j`); + console.log(` ${suffisant ? '✅ AUTORISÉ' : '❌ BLOQUÉ'} — besoin=${joursNecessaires}j`); + + return { + valide: suffisant, + mode: 'NORMAL', + isAnticipe: false, + soldeN1, + soldeNActuel, + soldeNProjecte, + soldeNNet, + soldeTotalDisponible, + deficit, + message: suffisant ? undefined + : `Solde CP insuffisant. Maximum disponible : ${soldeTotalDisponible.toFixed(2)}j — Demandé : ${joursNecessaires}j — Déficit : ${deficit.toFixed(2)}j` + }; + } +} +// ============================================================================ +// 🔍 FONCTION DE VÉRIFICATION DES SOLDES (sans anticipation) +// ============================================================================ +async function checkLeaveBalanceWithAnticipation(conn, collaborateurId, repartition, dateDebut) { + const today = new Date(); + today.setHours(0, 0, 0, 0); + + const currentYear = today.getFullYear(); + const previousYear = currentYear - 1; + + console.log('\n🔍 === CHECK SOLDES (ANTICIPATION DÉSACTIVÉE) ==='); const verification = []; @@ -4532,376 +5823,218 @@ async function checkLeaveBalanceWithAnticipation(conn, collaborateurId, repartit continue; } - const typeName = typeCode === 'CP' ? 'Congé payé' : typeCode === 'RTT' ? 'RTT' : typeCode; + const typeName = typeCode === 'CP' ? 'Congé payé' : + typeCode === 'RTT' ? 'RTT' : + typeCode === 'Récup' ? 'Récupération' : typeCode; + const [typeRow] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', [typeName]); - if (typeRow.length === 0) { - continue; - } + if (typeRow.length === 0) continue; const typeCongeId = typeRow[0].Id; // ==================================== - // 1️⃣ Récupérer les infos du collaborateur + // CP - SANS ANTICIPATION // ==================================== - 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_Smart(dateDebutObj, dateEntree); - budgetAnnuel = 25; - console.log(`💰 Acquisition CP à la date ${dateDebut}: ${acquisALaDate.toFixed(2)}j`); - } else if (typeCode === 'RTT' && !isApprenti) { - const rttData = await calculerAcquisitionRTT_Smart(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') { + // Solde N-1 (reporté) const [compteurN1] = await conn.query(` - SELECT Solde, SoldeReporte - FROM CompteurConges + SELECT Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? `, [collaborateurId, typeCongeId, previousYear]); - if (compteurN1.length > 0) { - reporteN1 = parseFloat(compteurN1[0].Solde || 0); - } - } + const soldeN1 = compteurN1.length > 0 ? parseFloat(compteurN1[0].Solde || 0) : 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 - */ - -// ======================================== -// 💰 FONCTION DE DÉDUCTION AVEC ANTICIPATION (SANS ID MANUEL) -// ======================================== - -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(`💳 === 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}`); - - // ===== ÉTAPE 1 : Déduire du REPORT N-1 ===== - try { - 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 || 0); - const aDeduireN1 = Math.min(soldeN1, joursRestants); - - if (aDeduireN1 > 0) { - await conn.query(` - UPDATE CompteurConges - SET SoldeReporte = SoldeReporte - ?, - Solde = Solde - ?, - DerniereMiseAJour = GETDATE() - WHERE Id = ? - `, [aDeduireN1, aDeduireN1, compteurN1[0].Id]); - - // ⭐ SANS SPÉCIFIER L'ID - await conn.query(` - INSERT INTO DeductionDetails - (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) - VALUES (?, ?, ?, ?, ?) - `, [demandeCongeId, typeCongeId, previousYear, 'Report N-1', aDeduireN1]); - - deductions.push({ - annee: previousYear, - type: 'Report N-1', - joursUtilises: aDeduireN1 - }); - - joursRestants -= aDeduireN1; - console.log(` ✅ Report N-1: ${aDeduireN1.toFixed(2)}j déduits - reste ${joursRestants}j`); - } - } - } catch (error) { - console.error('❌ Erreur déduction N-1:', error.message); - throw error; - } - - // ===== ÉTAPE 2 : Déduire du SOLDE N ===== - if (joursRestants > 0) { - try { + // Solde N (actuel) const [compteurN] = await conn.query(` - SELECT Id, Solde, SoldeReporte, Total - FROM CompteurConges + SELECT 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); + const soldeNTotal = compteurN.length > 0 ? parseFloat(compteurN[0].Solde || 0) : 0; + const soldeReporte = compteurN.length > 0 ? parseFloat(compteurN[0].SoldeReporte || 0) : 0; + const soldeN = Math.max(0, soldeNTotal - soldeReporte); - if (aDeduireN > 0) { - await conn.query(` - UPDATE CompteurConges - SET Solde = Solde - ?, - DerniereMiseAJour = GETDATE() - WHERE Id = ? - `, [aDeduireN, compteurN[0].Id]); + // ⭐ TOTAL SANS ANTICIPATION + const soldeTotal = soldeN1 + soldeN; - // ⭐ SANS SPÉCIFIER L'ID - await conn.query(` - INSERT INTO DeductionDetails - (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) - VALUES (?, ?, ?, ?, ?) - `, [demandeCongeId, typeCongeId, currentYear, 'Année N', aDeduireN]); + console.log(`💰 CP: N-1=${soldeN1.toFixed(2)}j + N=${soldeN.toFixed(2)}j = ${soldeTotal.toFixed(2)}j`); - deductions.push({ - annee: currentYear, - type: 'Année N', - joursUtilises: aDeduireN - }); + const suffisant = soldeTotal >= joursNecessaires; - joursRestants -= aDeduireN; - console.log(` ✅ Solde N: ${aDeduireN.toFixed(2)}j déduits - reste ${joursRestants}j`); - } - } - } catch (error) { - console.error('❌ Erreur déduction N:', error.message); - throw error; - } - } + verification.push({ + type: typeName, + joursNecessaires, + soldeN1, + soldeN, + soldeTotal, + anticipeDisponible: 0, + suffisant, + deficit: Math.max(0, joursNecessaires - soldeTotal) + }); - // ===== ÉTAPE 3 : ANTICIPÉ ===== - if (joursRestants > 0) { - console.log(` 💳 Il reste ${joursRestants.toFixed(2)}j à déduire → Anticipé`); - - try { - const [collabInfo] = await conn.query(` - SELECT DateEntree, TypeContrat, role - FROM CollaborateurAD - WHERE id = ? - `, [collaborateurId]); - - const dateEntree = collabInfo[0]?.DateEntree || null; - - const [typeInfo] = await conn.query(` - SELECT Nom FROM TypeConge WHERE Id = ? - `, [typeCongeId]); - - const typeNom = typeInfo[0]?.Nom; - let budgetAnnuel = 0; - let acquisALaDate = 0; - - if (typeNom === 'Congé payé') { - acquisALaDate = calculerAcquisitionCP_Smart(dateDebutObj, dateEntree); - budgetAnnuel = 25; - } else if (typeNom === 'RTT') { - const rttData = await calculerAcquisitionRTT_Smart(conn, collaborateurId, dateDebutObj); - acquisALaDate = rttData.acquisition; - budgetAnnuel = rttData.config.joursAnnuels; - } - - const anticipableMax = Math.max(0, budgetAnnuel - acquisALaDate); - - 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 dejaPrisAnticipe = parseFloat(anticipeUtilise[0]?.total || 0); - const anticipeDisponible = Math.max(0, anticipableMax - dejaPrisAnticipe); - - console.log(` 💳 Anticipable max: ${anticipableMax.toFixed(2)}j`); - console.log(` 💳 Déjà pris: ${dejaPrisAnticipe.toFixed(2)}j`); - console.log(` 💳 Disponible: ${anticipeDisponible.toFixed(2)}j`); - - if (anticipeDisponible >= joursRestants) { - // ⭐ SANS SPÉCIFIER L'ID - await conn.query(` - INSERT INTO DeductionDetails - (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) - VALUES (?, ?, ?, ?, ?) - `, [demandeCongeId, typeCongeId, currentYear, 'N Anticip', joursRestants]); - - deductions.push({ - annee: currentYear, - type: 'N Anticip', - joursUtilises: joursRestants - }); - - console.log(` ✅ Anticipé: ${joursRestants.toFixed(2)}j`); - joursRestants = 0; - } else { + if (!suffisant) { return { - success: false, - joursDeduitsTotal: nombreJours - joursRestants, - joursNonDeduits: joursRestants, - details: deductions, - error: `Solde insuffisant (manque ${joursRestants.toFixed(2)}j)` + valide: false, + message: `Solde CP insuffisant : ${soldeTotal.toFixed(2)}j disponibles, ${joursNecessaires}j demandés`, + details: verification, + insuffisants: verification.filter(v => !v.suffisant) + }; + } + } + + // ==================================== + // RTT - SANS ANTICIPATION + // ==================================== + else if (typeCode === 'RTT') { + const [compteurN] = await conn.query(` + SELECT Solde FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? + `, [collaborateurId, typeCongeId, currentYear]); + + const soldeN = compteurN.length > 0 ? parseFloat(compteurN[0].Solde || 0) : 0; + + console.log(`💰 RTT: ${soldeN.toFixed(2)}j disponibles`); + + const suffisant = soldeN >= joursNecessaires; + + verification.push({ + type: typeName, + joursNecessaires, + soldeN, + soldeTotal: soldeN, + anticipeDisponible: 0, + suffisant, + deficit: Math.max(0, joursNecessaires - soldeN) + }); + + if (!suffisant) { + return { + valide: false, + message: `Solde RTT insuffisant : ${soldeN.toFixed(2)}j disponibles, ${joursNecessaires}j demandés`, + details: verification, + insuffisants: verification.filter(v => !v.suffisant) + }; + } + } + + // ==================================== + // RÉCUP + // ==================================== + 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 [compteur] = await conn.query(` + SELECT Solde FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? + `, [collaborateurId, recupType[0].Id, currentYear]); + + const soldeRecup = compteur.length > 0 ? parseFloat(compteur[0].Solde || 0) : 0; + + const suffisant = soldeRecup >= joursNecessaires; + + verification.push({ + type: 'Récupération', + joursNecessaires, + soldeTotal: soldeRecup, + suffisant, + deficit: Math.max(0, joursNecessaires - soldeRecup) + }); + + if (!suffisant) { + return { + valide: false, + message: `Solde Récupération insuffisant : ${soldeRecup.toFixed(2)}j disponibles, ${joursNecessaires}j demandés`, + details: verification, + insuffisants: verification.filter(v => !v.suffisant) }; } - } catch (error) { - console.error('❌ Erreur anticipé:', error.message); - throw error; } } - console.log(` ✅ Déduction OK - Total: ${(nombreJours - joursRestants).toFixed(2)}j`); - return { - success: joursRestants === 0, - joursDeduitsTotal: nombreJours - joursRestants, - joursNonDeduits: joursRestants, - details: deductions + valide: true, + details: verification, + insuffisants: [] }; } // ======================================== // 🔧 FONCTION HELPER - GÉNÉRATION D'ID // ======================================== // ✅ VERSION CORRIGÉE +// ✅ getNextId — NE PREND PLUS conn EN PARAMÈTRE +// ✅ getNextId — pool.request() direct, sans conn async function getNextId(connection, tableName) { - try { - const [result] = await connection.query( - `SELECT ISNULL(MAX(Id), 0) + 1 AS NextId FROM ${tableName}` - ); - return result[0].NextId; - } catch (error) { - console.error(`❌ Erreur génération ID pour ${tableName}:`, error.message); - throw error; - } + const request = pool.request(); + const result = await request.query( + `SELECT ISNULL(MAX(Id), 0) + 1 AS NextId FROM [${tableName}]` + ); + const id = result.recordset[0].NextId; + console.log(`🔑 getNextId(${tableName}) = ${id}`); + return id; } + + +// ========================================== +// CODE CORRIGÉ - submitLeaveRequest +// ========================================== + +// ✅ Helper à placer UNE FOIS en haut du fichier (après la définition du pool) +function extractValue(row, ...keys) { + for (const key of keys) { + const val = row[key] ?? row[key.toLowerCase()] ?? row[key.toUpperCase()]; + if (val !== undefined && val !== null) { + if (typeof val === 'object') return Object.values(val)[0] ?? null; + return val; + } + } + for (const val of Object.values(row)) { + if (val !== null && val !== undefined && typeof val !== 'object') return val; + } + return null; +} + + +// ✅ Helper à ajouter UNE FOIS en haut du fichier +function getRows(result) { + // Gère : result, result.recordset, result[0], result.recordset[0] + if (!result) return []; + if (Array.isArray(result)) { + // Si c'est un tableau de tableaux [[{...}]] + if (result.length > 0 && Array.isArray(result[0])) return result[0]; + return result; + } + if (result.recordset) { + const rs = result.recordset; + if (Array.isArray(rs) && rs.length > 0 && Array.isArray(rs[0])) return rs[0]; + return rs; + } + return []; +} + +// ════════════════════════════════════════════════════════════════════════ +// POST /api/submitLeaveRequest +// ════════════════════════════════════════════════════════════════════════ app.post('/api/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), async (req, res) => { const conn = await pool.getConnection(); try { await conn.beginTransaction(); + console.log('🚀 Transaction démarrée'); - const currentYear = new Date().getFullYear(); + const today = new Date(); + const currentYear = today.getFullYear(); + + // ── CP : basculement 1er juin ────────────────────────────────── + const dateBasculeCP = new Date(`${currentYear}-06-01`); + const apresBasc = today >= dateBasculeCP; + const anneeN = apresBasc ? currentYear + 1 : currentYear; + const anneeN1 = apresBasc ? currentYear : currentYear - 1; - // ✅ 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 = req.body.DateDebut; const DateFin = req.body.DateFin; const NombreJours = parseFloat(req.body.NombreJours); @@ -4910,495 +6043,1490 @@ app.post('/api/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), const Commentaire = req.body.Commentaire || ''; const statut = req.body.statut || null; - // ✅ Parser la répartition (elle arrive en string depuis FormData) let Repartition; - try { - Repartition = JSON.parse(req.body.Repartition || '[]'); - } catch (parseError) { - console.error('❌ Erreur parsing Repartition:', parseError); - if (req.files) { - req.files.forEach(file => { - if (fs.existsSync(file.path)) { - fs.unlinkSync(file.path); - } - }); + try { Repartition = JSON.parse(req.body.Repartition); } + catch (e) { console.error('❌ Erreur parsing Repartition:', e); Repartition = []; } + + // ── Helpers ─────────────────────────────────────────────────── + const toTypeName = (code) => ({ + 'CP': 'Congé payé', 'RTT': 'RTT', 'ABS': 'Congé maladie', + 'Formation': 'Formation', 'Récup': 'Récupération' + }[code] || code); + + const getRows = (result) => { + if (!result) return []; + if (Array.isArray(result)) { + if (result.length > 0 && Array.isArray(result[0])) return result[0]; + return result; + } + if (result.recordset) { + const rs = result.recordset; + if (Array.isArray(rs) && rs.length > 0 && Array.isArray(rs[0])) return rs[0]; + return rs; + } + return []; + }; + + const readNum = (row, key) => { + if (!row) return null; + const v = row[key] ?? row[key.toLowerCase()] ?? row[key.toUpperCase()]; + if (v === undefined || v === null) return null; + const n = Number(v); + return isNaN(n) ? null : n; + }; + + // ── Validation PeriodeJournee ───────────────────────────────── + const validPeriodes = ['Journée entière', 'Matin', 'Après-midi']; + for (const rep of Repartition) { + const periodeDebut = rep.PeriodeJourneeDebut || 'Journée entière'; + const periodeFin = rep.PeriodeJourneeFin || 'Journée entière'; + + if (!validPeriodes.includes(periodeDebut)) { + await conn.rollback(); conn.release(); + return res.json({ success: false, message: `PeriodeJourneeDebut invalide: ${periodeDebut}` }); + } + if (!validPeriodes.includes(periodeFin)) { + await conn.rollback(); conn.release(); + return res.json({ success: false, message: `PeriodeJourneeFin invalide: ${periodeFin}` }); + } + // Vérification cohérence demi-journée (même jour) + const isDemiJournee = periodeDebut === periodeFin && periodeDebut !== 'Journée entière'; + if (isDemiJournee && parseFloat(rep.NombreJours) !== 0.5) { + await conn.rollback(); conn.release(); + return res.json({ success: false, message: `Demi-journée doit valoir 0.5j (reçu: ${rep.NombreJours}j)` }); } - return res.status(400).json({ - success: false, - message: 'Erreur de format de la répartition' - }); } - 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' }); + if (!Repartition || Repartition.length === 0) { + await conn.rollback(); conn.release(); + return res.json({ success: false, message: 'Répartition manquante' }); } - // ✅ 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(); + const totalJoursDemandes = Repartition.reduce((sum, r) => sum + parseFloat(r.NombreJours || 0), 0); + if (totalJoursDemandes > 25) { + await conn.rollback(); conn.release(); return res.json({ success: false, - message: 'Un justificatif médical est obligatoire pour un arrêt maladie' + message: `Vous ne pouvez pas poser plus de 25 jours ouvrés par demande (demandé : ${totalJoursDemandes}j)` }); } - // ⭐ 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' + // ── ÉTAPE 1 : Trouver le collaborateur ──────────────────────── + console.log('🔍 ÉTAPE 1 - Recherche collaborateur:', Email); + const collabADRaw = await conn.query( + `SELECT TOP 1 CAST(id AS INT) AS collab_id FROM CollaborateurAD WHERE email = ?`, + [Email] ); - - 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 collabAD = getRows(collabADRaw); const isAD = collabAD.length > 0; - const collaborateurId = isAD ? collabAD[0].id : null; + const collaborateurId = isAD ? readNum(collabAD[0], 'collab_id') : null; + console.log('👤 CollaborateurAD trouvé:', isAD, '| ID:', collaborateurId); - 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; + if (!isAD || !collaborateurId) { + await conn.rollback(); conn.release(); + return res.json({ success: false, message: `Collaborateur introuvable: ${Email}` }); } - // ======================================== - // É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); + // ── Trouver le validateur ───────────────────────────────────── + let validateurId = null; + let validateurADId = null; + const hierRaw = await conn.query( + `SELECT TOP 1 CAST(SuperieurId AS INT) AS sup_id + FROM HierarchieValidationAD WHERE CollaborateurId = ?`, + [collaborateurId] + ); + const hier = getRows(hierRaw); + if (hier.length > 0) validateurADId = readNum(hier[0], 'sup_id'); + console.log('👔 ValidateurADId:', validateurADId); - const [userRole] = await conn.query('SELECT role FROM CollaborateurAD WHERE id = ?', [collaborateurId]); - const isApprenti = userRole.length > 0 && userRole[0].role === 'Apprenti'; - - const checkResult = await checkLeaveBalanceWithAnticipation( - conn, - collaborateurId, - Repartition, - DateDebut - ); - - if (!checkResult.valide) { - uploadedFiles.forEach(file => { - if (fs.existsSync(file.path)) fs.unlinkSync(file.path); - }); - await conn.rollback(); - conn.release(); - - 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 (AVEC GÉNÉRATION MANUELLE D'ID) - // ======================================== - console.log('\n📝 Création de la demande...'); + const isFormationOnly = Repartition.every(r => r.TypeConge === 'Formation'); + const statutDemande = isFormationOnly ? 'Validé' : (statut || 'En attente'); + console.log('📋 isFormationOnly:', isFormationOnly, '| Statut:', statutDemande); + // ── TypeCongeId CSV ─────────────────────────────────────────── const typeIds = []; for (const rep of Repartition) { - const code = rep.TypeConge; - - 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); - } - - 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(','); - - // 🔥 GÉNÉRATION MANUELLE DE L'ID (CONTOURNEMENT IDENTITY) - const [maxIdResult] = await conn.query('SELECT ISNULL(MAX(Id), 0) + 1 AS NextId FROM DemandeConge'); - const demandeId = maxIdResult[0].NextId; - - console.log(`🆔 ID généré manuellement: ${demandeId}`); - - // 🔥 INSERT AVEC ID EXPLICITE - await conn.query(` - INSERT INTO DemandeConge - (Id, CollaborateurADId, DateDebut, DateFin, TypeCongeId, Statut, DateDemande, Commentaire, Validateur, NombreJours) - VALUES (?, ?, ?, ?, ?, ?, GETDATE(), ?, ?, ?) - `, [ - demandeId, - collaborateurId, - DateDebut, - DateFin, - typeCongeIdCsv, - statutDemande, - Commentaire || '', - '', - NombreJours - ]); - - 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 (?, ?, ?, ?, ?, GETDATE()) - `, [demandeId, file.originalname, file.path, file.mimetype, file.size]); - console.log(` ✓ ${file.originalname}`); - } - } - - // ======================================== - // ÉTAPE 4 : Sauvegarder la répartition - // ======================================== - // ======================================== - // ÉTAPE 4 : Sauvegarder la répartition - // ======================================== - // 5️⃣ 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] + const name = toTypeName(rep.TypeConge); + const tRaw = await conn.query( + `SELECT TOP 1 CAST(Id AS INT) AS type_id FROM TypeConge WHERE Nom = ?`, [name] ); + const tRow = getRows(tRaw); + if (tRow.length > 0) { + const tid = readNum(tRow[0], 'type_id'); + if (tid && !typeIds.includes(tid)) typeIds.push(tid); + } + } + const typeCongeIdCsv = typeIds.join(','); + console.log('🏷 TypeCongeIdCsv:', typeCongeIdCsv); + + if (!typeCongeIdCsv) { + await conn.rollback(); conn.release(); + return res.json({ success: false, message: 'Type de congé introuvable en base' }); + } + + // ── ÉTAPE 2 : INSERT DemandeConge ───────────────────────────── + console.log('📝 ÉTAPE 2 - Création DemandeConge'); + const nextDcRaw = await conn.query( + `SELECT CAST(ISNULL(MAX(Id), 0) + 1 AS INT) AS next_id FROM DemandeConge` + ); + const demandeId = readNum(getRows(nextDcRaw)[0], 'next_id'); + console.log('🆔 Prochain ID DemandeConge:', demandeId); + + await conn.query( + `INSERT INTO DemandeConge + (Id, CollaborateurADId, DateDebut, DateFin, TypeCongeId, Statut, DateDemande, + Commentaire, Validateur, NombreJours, ValidateurId, ValidateurADId) + VALUES (?, ?, ?, ?, ?, ?, GETDATE(), ?, ?, ?, ?, ?)`, + [demandeId, collaborateurId, DateDebut, DateFin, typeCongeIdCsv, + statutDemande, Commentaire, '', NombreJours, validateurId, validateurADId] + ); + console.log('✅ Demande créée ID:', demandeId, '| Statut:', statutDemande); + + // ── ÉTAPE 3 : Fichiers médicaux ─────────────────────────────── + if (uploadedFiles.length > 0) { + console.log('📎 ÉTAPE 3 - Sauvegarde fichiers médicaux'); + for (const file of uploadedFiles) { + const nextDocRaw = await conn.query( + `SELECT CAST(ISNULL(MAX(Id), 0) + 1 AS INT) AS next_id FROM DocumentsMedicaux` + ); + const docId = readNum(getRows(nextDocRaw)[0], 'next_id'); + await conn.query( + `INSERT INTO DocumentsMedicaux + (Id, DemandeCongeId, NomFichier, CheminFichier, TypeMime, TailleFichier, DateUpload) + VALUES (?, ?, ?, ?, ?, ?, GETDATE())`, + [docId, demandeId, file.originalname, file.path, file.mimetype, file.size] + ); + console.log('📄 Fichier sauvegardé:', file.originalname); + } + } + + // ── ÉTAPE 4 : DemandeCongeType ──────────────────────────────── + // ✅ CORRECTION : on stocke PeriodeJournee (début) ET PeriodeJourneeFin séparément + // Prérequis SQL : ALTER TABLE DemandeCongeType ADD PeriodeJourneeFin NVARCHAR(50) NULL; + console.log('📋 ÉTAPE 4 - Sauvegarde répartition'); + for (const rep of Repartition) { + const name = toTypeName(rep.TypeConge); + const typeRowRaw = await conn.query( + `SELECT TOP 1 CAST(Id AS INT) AS type_id FROM TypeConge WHERE Nom = ?`, [name] + ); + const typeRow = getRows(typeRowRaw); if (typeRow.length > 0) { - // ⭐ GÉNÉRER L'ID MANUELLEMENT - const demandeCongeTypeId = await getNextId(conn, 'DemandeCongeType'); + const typeCongeId = readNum(typeRow[0], 'type_id'); - await conn.query(` - INSERT INTO DemandeCongeType - (Id, DemandeCongeId, TypeCongeId, NombreJours, PeriodeJournee) - VALUES (?, ?, ?, ?, ?) - `, [ - demandeCongeTypeId, - demandeId, - typeRow[0].Id, - rep.NombreJours, - rep.PeriodeJournee || 'Journée entière' - ]); - console.log(` ✓ ${name}: ${rep.NombreJours}j (${rep.PeriodeJournee || 'Journée entière'})`); + const periodeDebut = rep.PeriodeJourneeDebut || 'Journée entière'; + const periodeFin = rep.PeriodeJourneeFin || 'Journée entière'; + + const segmentDateDebut = rep.DateDebut + ? new Date(rep.DateDebut).toISOString().split('T')[0] + : DateDebut; + const segmentDateFin = rep.DateFin + ? new Date(rep.DateFin).toISOString().split('T')[0] + : DateFin; + + const nextDctRaw = await conn.query( + `SELECT CAST(ISNULL(MAX(Id), 0) + 1 AS INT) AS next_id FROM DemandeCongeType` + ); + const dctId = readNum(getRows(nextDctRaw)[0], 'next_id'); + + await conn.query( + `INSERT INTO DemandeCongeType + (Id, DemandeCongeId, TypeCongeId, NombreJours, PeriodeJournee, PeriodeJourneeFin, DateDebut, DateFin) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, + [dctId, demandeId, typeCongeId, rep.NombreJours, periodeDebut, periodeFin, + segmentDateDebut, segmentDateFin] + ); + console.log(`✅ ${name} → ${rep.NombreJours}j | ${segmentDateDebut} → ${segmentDateFin} | début:${periodeDebut} fin:${periodeFin}`); + } else { + console.warn(`⚠️ TypeConge introuvable: ${name}`); } } - // ======================================== - // ÉTAPE 5 : Déduction des compteurs CP/RTT/RÉCUP (AVEC ANTICIPATION N+1) - // ======================================== + + // ── ÉTAPE 5 : Déduction des compteurs ───────────────────────── + console.log('💰 ÉTAPE 5 - Déduction compteurs'); 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)`); + console.log(`⏭ Skip déduction: ${rep.TypeConge}`); continue; } - // ⭐ TRAITEMENT SPÉCIAL POUR RÉCUP + // ── Récup ───────────────────────────────────────────── if (rep.TypeConge === 'Récup') { - const [recupType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Récupération']); + const recupRaw = await conn.query( + `SELECT TOP 1 CAST(Id AS INT) AS type_id FROM TypeConge WHERE Nom = ?`, + ['Récupération'] + ); + const recupRow = getRows(recupRaw); + if (!recupRow.length) throw new Error('Type Récupération introuvable'); + const recupTypeId = readNum(recupRow[0], 'type_id'); - if (recupType.length > 0) { - await conn.query(` - UPDATE CompteurConges - SET Solde = CASE WHEN Solde - ? < 0 THEN 0 ELSE Solde - ? END, - DerniereMiseAJour = GETDATE() - WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? - `, [rep.NombreJours, 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`); - } + await conn.query( + `UPDATE CompteurConges + SET Solde = CASE WHEN Solde - ? < 0 THEN 0 ELSE Solde - ? END, + DerniereMiseAJour = GETDATE() + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, + [rep.NombreJours, rep.NombreJours, collaborateurId, recupTypeId, currentYear] + ); + await conn.query( + `INSERT INTO DeductionDetails + (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) + VALUES (?, ?, ?, 'Récup Posé', ?)`, + [demandeId, recupTypeId, currentYear, rep.NombreJours] + ); + console.log(`✅ Récup déduit: ${rep.NombreJours}j`); 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 deductResult = await deductLeaveBalanceWithAnticipation( - conn, - collaborateurId, - typeRow[0].Id, - rep.NombreJours, - demandeId, - DateDebut + // ── CP ──────────────────────────────────────────────── + if (rep.TypeConge === 'CP') { + const cpRaw = await conn.query( + `SELECT TOP 1 CAST(Id AS INT) AS type_id FROM TypeConge WHERE Nom = ?`, + ['Congé payé'] ); + const cpRow = getRows(cpRaw); + if (!cpRow.length) throw new Error('Type Congé payé introuvable'); + const cpTypeId = readNum(cpRow[0], 'type_id'); - console.log(` ✓ ${name}: ${rep.NombreJours}j déduits`); - if (deductResult.details && deductResult.details.length > 0) { - deductResult.details.forEach(d => { - console.log(` - ${d.type} (${d.annee}): ${d.joursUtilises}j`); - }); + console.log('🔄 Appel deductLeaveBalanceCP...'); + const result = await deductLeaveBalanceCP( + conn, collaborateurId, cpTypeId, + parseFloat(rep.NombreJours), demandeId, DateDebut + ); + console.log('📊 Résultat déduction CP:', result); + + if (!result.success) { + if (result.mode === 'ANTICIPÉ POST-BASCULEMENT') { + console.warn(`⚠️ Anticipé post-basculement — ${result.joursNonDeduits.toFixed(2)}j non couverts`); + await conn.query( + `INSERT INTO DeductionDetails + (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) + VALUES (?, ?, ?, 'Anticipé Toléré', ?)`, + [demandeId, cpTypeId, anneeN, result.joursNonDeduits] + ); + } else { + throw new Error(`Déduction CP échouée — ${result.joursNonDeduits.toFixed(2)}j non couverts`); + } } + console.log(`✅ CP déduits: ${result.joursDeduitsTotal.toFixed(2)}j | Mode: ${result.mode}`); + continue; + } + + // ── RTT ─────────────────────────────────────────────── + if (rep.TypeConge === 'RTT') { + const rttRaw = await conn.query( + `SELECT TOP 1 CAST(Id AS INT) AS type_id FROM TypeConge WHERE Nom = ?`, + ['RTT'] + ); + const rttRow = getRows(rttRaw); + if (!rttRow.length) throw new Error('Type RTT introuvable'); + const rttTypeId = readNum(rttRow[0], 'type_id'); + + const anneeRTT = new Date(DateDebut).getFullYear(); + const isAnticipeRTT = anneeRTT > currentYear; + + console.log(` RTT — anneeRTT=${anneeRTT} (${isAnticipeRTT ? 'anticipé' : 'normal'})`); + + if (!isAnticipeRTT) { + const existingRTT = await conn.query( + `SELECT TOP 1 Id FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, + [collaborateurId, rttTypeId, anneeRTT] + ); + if (!getRows(existingRTT).length) { + const nextRttIdRaw = await conn.query( + `SELECT CAST(ISNULL(MAX(Id), 0) + 1 AS INT) AS next_id FROM CompteurConges` + ); + const nextRttId = readNum(getRows(nextRttIdRaw)[0], 'next_id'); + const conn2 = await pool.getConnection(); + let acquisRTT = 0; + try { + const finEx = new Date(anneeRTT, 11, 31); + const rttData = await calculerAcquisitionRTTSmart(conn2, collaborateurId, finEx); + acquisRTT = rttData.acquisition; + } finally { conn2.release(); } + await conn.query( + `INSERT INTO CompteurConges + (Id, CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour) + VALUES (?, ?, ?, ?, ?, 0, 0, GETDATE())`, + [nextRttId, collaborateurId, rttTypeId, anneeRTT, acquisRTT] + ); + console.log(` ✅ Compteur RTT ${anneeRTT} créé (Total=${acquisRTT.toFixed(2)}j)`); + } + await conn.query( + `UPDATE CompteurConges + SET Solde = CASE WHEN Solde - ? < 0 THEN 0 ELSE Solde - ? END, + DerniereMiseAJour = GETDATE() + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, + [rep.NombreJours, rep.NombreJours, collaborateurId, rttTypeId, anneeRTT] + ); + await conn.query( + `INSERT INTO DeductionDetails + (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) + VALUES (?, ?, ?, 'Année N', ?)`, + [demandeId, rttTypeId, anneeRTT, rep.NombreJours] + ); + console.log(`✅ RTT normal déduit: ${rep.NombreJours}j sur Annee=${anneeRTT}`); + + } else { + const anneeRTTCourant = currentYear; + const nombreJoursRTT = parseFloat(rep.NombreJours); + + const conn2rtt = await pool.getConnection(); + let joursAnnuelsRTTN = 10; + try { + const cfgN = await getConfigurationRTT(conn2rtt, anneeRTTCourant, typeContrat); + joursAnnuelsRTTN = cfgN.joursAnnuels; + } finally { conn2rtt.release(); } + const tauxRTTN = joursAnnuelsRTTN / 360; + + const conn3rtt = await pool.getConnection(); + let joursAnnuelsRTTNp1 = 10; + try { + const cfgNp1 = await getConfigurationRTT(conn3rtt, anneeRTT, typeContrat); + joursAnnuelsRTTNp1 = cfgNp1.joursAnnuels; + } finally { conn3rtt.release(); } + const tauxRTTNp1 = joursAnnuelsRTTNp1 / 360; + + const [cRTTNRows] = await conn.query( + `SELECT Id, Solde FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, + [collaborateurId, rttTypeId, anneeRTTCourant] + ); + const soldeRTTNActuel = cRTTNRows.length > 0 ? Math.max(0, parseFloat(cRTTNRows[0].Solde || 0)) : 0; + + const finExRTTN = new Date(anneeRTTCourant, 11, 31); finExRTTN.setHours(23, 59, 59, 0); + const joursRestantsRTTN = Math.max(0, Math.floor((finExRTTN - today) / (1000 * 60 * 60 * 24))); + const acquisRestantsRTTN = parseFloat(Math.min(joursAnnuelsRTTN - soldeRTTNActuel, joursRestantsRTTN * tauxRTTN).toFixed(2)); + const soldeRTTNProj = parseFloat(Math.min(joursAnnuelsRTTN, soldeRTTNActuel + acquisRestantsRTTN).toFixed(2)); + + const [dejaConsoRTTNRows] = 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 IN ('AnticipRTT', 'AnticipFuturRTT') + AND dc.Statut != 'Refusé'`, + [collaborateurId, rttTypeId, anneeRTTCourant] + ); + const dejaConsoRTTN = parseFloat(dejaConsoRTTNRows[0]?.total || 0); + const dispoRTTN = parseFloat(Math.max(0, soldeRTTNProj - dejaConsoRTTN).toFixed(2)); + + const dateDebutCongeRTT = new Date(DateDebut); dateDebutCongeRTT.setHours(0, 0, 0, 0); + const debutNp1RTT = new Date(anneeRTT, 0, 1); debutNp1RTT.setHours(0, 0, 0, 0); + const diffJoursRTTNp1 = Math.max(0, Math.floor((dateDebutCongeRTT - debutNp1RTT) / (1000 * 60 * 60 * 24))); + const acquisRTTNp1 = parseFloat(Math.min(joursAnnuelsRTTNp1, diffJoursRTTNp1 * tauxRTTNp1).toFixed(2)); + const [dejaConsoRTTNp1Rows] = 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 dc.Statut != 'Refusé'`, + [collaborateurId, rttTypeId, anneeRTT] + ); + const dejaConsoRTTNp1 = parseFloat(dejaConsoRTTNp1Rows[0]?.total || 0); + const dispoRTTNp1 = parseFloat(Math.max(0, acquisRTTNp1 - dejaConsoRTTNp1).toFixed(2)); + const totalDispoRTT = parseFloat((dispoRTTN + dispoRTTNp1).toFixed(2)); + + console.log(` 💰 RTT N (${anneeRTTCourant}): actuel=${soldeRTTNActuel.toFixed(2)}j + restants=${acquisRestantsRTTN.toFixed(2)}j = projeté ${soldeRTTNProj.toFixed(2)}j - consommé ${dejaConsoRTTN.toFixed(2)}j = dispo ${dispoRTTN.toFixed(2)}j`); + console.log(` 💰 RTT N+1 (${anneeRTT}): ${diffJoursRTTNp1}j × ${tauxRTTNp1.toFixed(4)} = ${acquisRTTNp1.toFixed(2)}j - consommé ${dejaConsoRTTNp1.toFixed(2)}j = dispo ${dispoRTTNp1.toFixed(2)}j`); + console.log(` 📊 RTT total anticipé: ${totalDispoRTT.toFixed(2)}j | demandés=${nombreJoursRTT}j`); + + if (totalDispoRTT < nombreJoursRTT) { + throw new Error(`Solde RTT anticipé insuffisant : ${totalDispoRTT.toFixed(2)}j disponibles (N: ${dispoRTTN.toFixed(2)}j + N+1: ${dispoRTTNp1.toFixed(2)}j), ${nombreJoursRTT}j demandés`); + } + + let joursRestantsRTT2 = nombreJoursRTT; + + if (joursRestantsRTT2 > 0 && soldeRTTNActuel > 0 && cRTTNRows.length > 0) { + const aDeduireRTT = parseFloat(Math.min(soldeRTTNActuel, joursRestantsRTT2).toFixed(2)); + await conn.query( + `UPDATE CompteurConges SET Solde = Solde - ?, DerniereMiseAJour = GETDATE() WHERE Id = ?`, + [aDeduireRTT, cRTTNRows[0].Id] + ); + await conn.query( + `INSERT INTO DeductionDetails (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) + VALUES (?, ?, ?, 'AnticipRTT', ?)`, + [demandeId, rttTypeId, anneeRTTCourant, aDeduireRTT] + ); + joursRestantsRTT2 = parseFloat((joursRestantsRTT2 - aDeduireRTT).toFixed(2)); + console.log(` ✅ RTT AnticipRTT: ${aDeduireRTT.toFixed(2)}j déduit → reste: ${joursRestantsRTT2.toFixed(2)}j`); + } + + if (joursRestantsRTT2 > 0 && dispoRTTN > 0) { + const aTracerRTTN = parseFloat(Math.min(dispoRTTN, joursRestantsRTT2).toFixed(2)); + await conn.query( + `INSERT INTO DeductionDetails (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) + VALUES (?, ?, ?, 'AnticipFuturRTT', ?)`, + [demandeId, rttTypeId, anneeRTTCourant, aTracerRTTN] + ); + joursRestantsRTT2 = parseFloat((joursRestantsRTT2 - aTracerRTTN).toFixed(2)); + console.log(` ✅ RTT AnticipFuturRTT: ${aTracerRTTN.toFixed(2)}j tracé → reste: ${joursRestantsRTT2.toFixed(2)}j`); + } + + if (joursRestantsRTT2 > 0 && dispoRTTNp1 > 0) { + const aTracerRTTNp1 = parseFloat(Math.min(dispoRTTNp1, joursRestantsRTT2).toFixed(2)); + const [cRTTNp1Rows] = await conn.query( + `SELECT Id, Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, + [collaborateurId, rttTypeId, anneeRTT] + ); + if (cRTTNp1Rows.length === 0) { + const [newIdRTTRows] = await conn.query(`SELECT ISNULL(MAX(Id), 0) + 1 AS nextId FROM CompteurConges`); + const newIdRTT = newIdRTTRows[0]?.nextId || 1; + await conn.query( + `INSERT INTO CompteurConges (Id, CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour) VALUES (?, ?, ?, ?, 0, ?, 0, GETDATE())`, + [newIdRTT, collaborateurId, rttTypeId, anneeRTT, -aTracerRTTNp1] + ); + console.log(` ➕ CompteurConges RTT Annee=${anneeRTT} créé Solde=${-aTracerRTTNp1}j`); + } else { + await conn.query( + `UPDATE CompteurConges SET Solde = Solde - ?, DerniereMiseAJour = GETDATE() WHERE Id = ?`, + [aTracerRTTNp1, cRTTNp1Rows[0].Id] + ); + console.log(` ✏️ CompteurConges RTT Annee=${anneeRTT} Solde - ${aTracerRTTNp1}j`); + } + await conn.query( + `INSERT INTO DeductionDetails (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) + VALUES (?, ?, ?, 'AnticipNp1RTT', ?)`, + [demandeId, rttTypeId, anneeRTT, aTracerRTTNp1] + ); + joursRestantsRTT2 = parseFloat((joursRestantsRTT2 - aTracerRTTNp1).toFixed(2)); + console.log(` ✅ RTT AnticipNp1RTT: ${aTracerRTTNp1.toFixed(2)}j tracé sur N+1`); + } + + if (joursRestantsRTT2 > 0.01) { + throw new Error(`Déduction RTT anticipée incomplète: ${joursRestantsRTT2.toFixed(2)}j non couverts`); + } + console.log(`✅ RTT anticipé déduit: ${nombreJoursRTT}j | AnticipRTT+AnticipFuturRTT+AnticipNp1RTT`); + } + continue; } } - - await updateSoldeAnticipe(conn, collaborateurId); - console.log('✅ Déductions terminées\n'); + console.log('✅ Déductions compteurs terminées'); } - // ======================================== - // É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}`; + // ── ÉTAPE 6 : Notifications ─────────────────────────────────── + console.log('🔔 ÉTAPE 6 - Notifications'); + const dateDebutFmt = new Date(DateDebut).toLocaleDateString('fr-FR'); + const dateFinFmt = new Date(DateFin).toLocaleDateString('fr-FR'); + const datesPeriode = dateDebutFmt === dateFinFmt + ? dateDebutFmt + : `du ${dateDebutFmt} au ${dateFinFmt}`; - if (isFormationOnly && isAD && collaborateurId) { - await conn.query(` - INSERT INTO Notifications (CollaborateurADId, Type, Titre, Message, DemandeCongeId, DateCreation, Lu) - VALUES (?, ?, ?, ?, ?, GETDATE(), 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'); + if (isFormationOnly && collaborateurId) { + const maxNotifRaw = await conn.query( + `SELECT CAST(ISNULL(MAX(Id), 0) + 1 AS INT) AS next_id FROM Notifications` + ); + const notifId = readNum(getRows(maxNotifRaw)[0], 'next_id'); + await conn.query( + `INSERT INTO Notifications + (Id, CollaborateurADId, Type, Titre, Message, DemandeCongeId, DateCreation, Lu) + VALUES (?, ?, 'Success', 'Formation validée automatiquement', ?, ?, GETDATE(), 0)`, + [notifId, collaborateurId, + `Votre période de formation ${datesPeriode} a été validée automatiquement.`, + demandeId] + ); + console.log('🔔 Notification formation créée, ID:', notifId); } - // ======================================== - // É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); - } + // ── ÉTAPE 7 : Récupérer les managers ───────────────────────── + console.log('👔 ÉTAPE 7 - Récupération managers'); + const managersRaw = await conn.query( + `SELECT c.email AS manager_email + FROM HierarchieValidationAD hv + JOIN CollaborateurAD c ON hv.SuperieurId = c.id + WHERE hv.CollaborateurId = ?`, + [collaborateurId] + ); + const managers = getRows(managersRaw) + .map(r => r?.manager_email ?? r?.MANAGER_EMAIL) + .filter(Boolean); + console.log('👥 Managers trouvés:', managers); + // ── COMMIT ──────────────────────────────────────────────────── await conn.commit(); - console.log('\n🎉 Transaction validée\n'); + console.log('✅ Transaction validée avec succès'); - // ======================================== - // ÉTAPE 8 : Notifier les clients SSE - // ======================================== - if (isFormationOnly && isAD && collaborateurId) { + // ── ÉTAPE 8 : SSE ───────────────────────────────────────────── + if (isFormationOnly && collaborateurId) { notifyCollabClients({ type: 'demande-validated', - demandeId: parseInt(demandeId), - statut: 'Validée', + demandeId, + statut: 'Validé', timestamp: new Date().toISOString() }, collaborateurId); } - // ======================================== - // ENVOI DES EMAILS - // ======================================== + // ── ÉTAPE 9 : Emails (après commit) ─────────────────────────── + console.log('📧 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(' | '); + const typesConges = Repartition.map(r => + `${toTypeName(r.TypeConge)} (${r.NombreJours}j)` + ).join(', '); if (isFormationOnly) { - const subjectCollab = '✅ Formation enregistrée et validée'; - const bodyCollab = ` -Bonjour ${Nom},
-Votre période de formation a été automatiquement validée.
-Période : ${datesPeriode}
-Durée : ${NombreJours} jour(s)
-Bonjour ${Nom},
-Votre demande de congé a bien été enregistrée.
-Type : ${typesConges}
-Période : ${datesPeriode}
-Durée : ${NombreJours} jour(s)
-${Nom} a soumis une nouvelle demande.
+Bonjour ${Nom},
+Votre période de formation a été automatiquement validée.
+Période : ${datesPeriode}
+Durée : ${NombreJours} jours
+Bonjour ${Nom},
+Votre demande a bien été enregistrée et est en attente de validation.
Type : ${typesConges}
Période : ${datesPeriode}
+Durée : ${NombreJours} jours
${Nom} a soumis une nouvelle demande de congé.
+| Type | +${typesConges} |
| Période | +${datesPeriode} |
| Durée | +${NombreJours} jours |
| Commentaire | +${Commentaire} |
Bonjour ${collaborateurNom},
-Votre demande de ${request.TypeConge} pour ${request.NombreJours} jour(s) ${datesPeriode} a été ${action === 'approve' ? 'approuvée' : 'refusée'} par ${validateurNom}.
+Votre demande de ${request.TypeConge} pour ${request.NombreJours} jour(s) ${datesPeriode} a été ${action === 'approve' ? 'approuvée' : 'refusée'} par ${validateurNom}.
${comment ? `Commentaire: ${comment}
` : ''} @@ -5611,7 +7800,7 @@ app.post('/api/validateRequest', async (req, res) => { `; const emailSent = await sendMailGraph(accessToken, fromEmail, request.collaborateur_email, subject, emailHtml); - console.log('4. Email envoyé ?', emailSent ? 'OUI ✅' : 'NON ❌'); + console.log('📧 Email envoyé ?', emailSent ? 'OUI ✅' : 'NON ❌'); } res.json({ @@ -5628,7 +7817,6 @@ app.post('/api/validateRequest', async (req, res) => { } }); - app.get('/api/testRestoration', async (req, res) => { const conn = await pool.getConnection(); try { @@ -5769,13 +7957,12 @@ app.get('/api/getCampusBySociete', async (req, res) => { } }); - - - - // ======================================== // ROUTE getTeamLeaves COMPLÈTE // ======================================== +// ======================================== +// ROUTE getTeamLeaves COMPLÈTE ET OPTIMISÉE +// ======================================== app.get('/api/getTeamLeaves', async (req, res) => { try { let { user_id: userIdParam, role: roleParam, selectedCampus, selectedSociete, selectedService } = req.query; @@ -5847,13 +8034,31 @@ app.get('/api/getTeamLeaves', async (req, res) => { console.log(` - SocieteId: ${societeId}`); console.log(` - Role normalisé: ${role}`); + // ✅ Fragment SQL réutilisable pour le JSON detailsconges (tous les CAS) + const detailscongesSQL = ` + CONCAT( + '[', + STRING_AGG( + CONCAT( + '{"type":"', tc.Nom, + '","jours":', dct.NombreJours, + ',"periodeDebut":"', COALESCE(dct.PeriodeJournee, 'Journée entière'), + '","periodeFin":"', COALESCE(dct.PeriodeJourneeFin, dct.PeriodeJournee, 'Journée entière'), + '","dateDebut":"', CONVERT(VARCHAR(10), dct.DateDebut, 23), + '","dateFin":"', CONVERT(VARCHAR(10), dct.DateFin, 23), + '"}' + ), + ',' + ), + ']' + ) AS detailsconges`; + const filters = {}; // ======================================== // CAS 1: PRESIDENT, ADMIN, RH, DIRECTEUR DE CAMPUS - // ⚠️ SANS VALIDATEUR - C'est la correction principale ! // ======================================== - if (role === 'president' || role === 'admin' || role === 'rh' || role === 'directeur de campus') { + if (role === 'president' || role === 'admin' || 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}`); @@ -6012,18 +8217,7 @@ app.get('/api/getTeamLeaves', async (req, res) => { JOIN TypeConge tc2 ON dct2.TypeCongeId = tc2.Id WHERE dct2.DemandeCongeId = dc.Id) AS DistinctTypes ) AS type, - CONCAT( - '[', - STRING_AGG( - CONCAT( - '{"type":"', tc.Nom, - '","jours":', dct.NombreJours, - ',"periode":"', COALESCE(dct.PeriodeJournee, 'Journée entière'), '"}' - ), - ',' - ), - ']' - ) AS detailsconges, + ${detailscongesSQL}, MAX(tc.CouleurHex) AS color, dc.Statut AS statut, s.Nom AS servicenom, @@ -6045,9 +8239,7 @@ app.get('/api/getTeamLeaves', async (req, res) => { console.log(`🔍 Query finale WHERE:`, whereConditions.join(' AND ')); const leavesResult = await queryRequest.query(query); - const formattedLeaves = leavesResult.recordset.map(leave => ({ - ...leave - })); + const formattedLeaves = leavesResult.recordset.map(leave => ({ ...leave })); console.log(`✅ ${formattedLeaves.length} congés trouvés`); @@ -6060,13 +8252,11 @@ app.get('/api/getTeamLeaves', async (req, res) => { } // ======================================== - // CAS 2: VALIDATEUR - Vue équipe via HierarchieValidationAD - // ⚠️ C'est ici que le validateur doit tomber ! + // CAS 2: VALIDATEUR // ======================================== else if (role === 'validateur') { - console.log("CAS 2: Validateur - Vue de l'équipe via HierarchieValidationAD"); - - console.log(`📍 Validateur: Service=${serviceNom}, Campus=${campusNom}, Société=${societeNom}`); + console.log("CAS 2: Validateur - Vue identique au collaborateur"); + console.log(`📍 Filtres reçus du frontend: Société=${selectedSociete}, Campus=${selectedCampus}, Service=${selectedService}`); const isFirstLoad = !selectedCampus && !selectedService && !selectedSociete; @@ -6077,7 +8267,7 @@ app.get('/api/getTeamLeaves', async (req, res) => { selectedSociete = societeNom; } - console.log(`📍 Filtres appliqués: Société=${selectedSociete}, Campus=${selectedCampus}, Service=${selectedService}`); + console.log(`📍 Filtres appliqués finaux: Société=${selectedSociete}, Campus=${selectedCampus}, Service=${selectedService}`); // Sociétés disponibles const societesRequest = pool.request(); @@ -6135,7 +8325,7 @@ app.get('/api/getTeamLeaves', async (req, res) => { filters.defaultService = serviceNom; filters.defaultSociete = societeNom; - // ⭐ LISTE DES EMPLOYÉS - UNIQUEMENT CEUX DE L'ÉQUIPE DU VALIDATEUR + // ⭐ LISTE DES EMPLOYÉS let employeesQuery = ` SELECT CONCAT(ca.prenom, ' ', ca.nom) AS fullname, @@ -6146,13 +8336,10 @@ app.get('/api/getTeamLeaves', async (req, res) => { JOIN Services s ON ca.ServiceId = s.Id JOIN Campus c ON ca.CampusId = c.Id JOIN Societe so ON ca.SocieteId = so.Id - JOIN HierarchieValidationAD h ON ca.id = h.CollaborateurId - WHERE h.SuperieurId = @userId - AND (ca.actif = 1 OR ca.actif IS NULL) + WHERE (ca.actif = 1 OR ca.actif IS NULL) `; let employeesRequest = pool.request(); - employeesRequest.input('userId', userInfo.id); let employeesConditions = []; if (selectedSociete && selectedSociete !== 'all') { @@ -6185,16 +8372,11 @@ app.get('/api/getTeamLeaves', async (req, res) => { service: emp.servicenom })); - console.log(`👥 Équipe du validateur: ${filters.employees.length} personnes`); - - // ⭐ QUERY DES CONGÉS - UNIQUEMENT LES COLLABORATEURS DU VALIDATEUR - let queryConditions = `WHERE dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente') - AND ca.id IN ( - SELECT CollaborateurId FROM HierarchieValidationAD WHERE SuperieurId = @userId - )`; + console.log(`👥 Employés trouvés: ${filters.employees.length}`); + // QUERY DES CONGÉS + let queryConditions = `WHERE dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente')`; let queryRequest = pool.request(); - queryRequest.input('userId', userInfo.id); let congesConditions = []; if (selectedSociete && selectedSociete !== 'all') { @@ -6228,18 +8410,7 @@ app.get('/api/getTeamLeaves', async (req, res) => { JOIN TypeConge tc2 ON dct2.TypeCongeId = tc2.Id WHERE dct2.DemandeCongeId = dc.Id) AS DistinctTypes ) AS type, - CONCAT( - '[', - STRING_AGG( - CONCAT( - '{"type":"', tc.Nom, - '","jours":', dct.NombreJours, - ',"periode":"', COALESCE(dct.PeriodeJournee, 'Journée entière'), '"}' - ), - ',' - ), - ']' - ) AS detailsconges, + ${detailscongesSQL}, MAX(tc.CouleurHex) AS color, dc.Statut AS statut, s.Nom AS servicenom, @@ -6258,14 +8429,12 @@ app.get('/api/getTeamLeaves', async (req, res) => { ORDER BY s.Nom, dc.DateDebut ASC `; - console.log(`🔍 Query WHERE final validateur:`, queryConditions); + console.log(`🔍 Query WHERE final:`, queryConditions); const leavesResult = await queryRequest.query(query); - const formattedLeaves = leavesResult.recordset.map(leave => ({ - ...leave - })); + const formattedLeaves = leavesResult.recordset.map(leave => ({ ...leave })); - console.log(`✅ ${formattedLeaves.length} congés trouvés pour l'équipe du validateur`); + console.log(`✅ ${formattedLeaves.length} congés trouvés`); return res.json({ success: true, @@ -6278,9 +8447,8 @@ app.get('/api/getTeamLeaves', async (req, res) => { // ======================================== // CAS 3: COLLABORATEUR / APPRENTI // ======================================== - else if (role === 'collaborateur' || role === 'apprenti') { + else if (role === 'collaborateur' || role === 'apprenti' || role === 'rh') { console.log("CAS 3: Collaborateur/Apprenti avec filtres avancés"); - console.log(`📍 Filtres reçus du frontend: Société=${selectedSociete}, Campus=${selectedCampus}, Service=${selectedService}`); const isFirstLoad = !selectedCampus && !selectedService && !selectedSociete; @@ -6435,18 +8603,7 @@ app.get('/api/getTeamLeaves', async (req, res) => { JOIN TypeConge tc2 ON dct2.TypeCongeId = tc2.Id WHERE dct2.DemandeCongeId = dc.Id) AS DistinctTypes ) AS type, - CONCAT( - '[', - STRING_AGG( - CONCAT( - '{"type":"', tc.Nom, - '","jours":', dct.NombreJours, - ',"periode":"', COALESCE(dct.PeriodeJournee, 'Journée entière'), '"}' - ), - ',' - ), - ']' - ) AS detailsconges, + ${detailscongesSQL}, MAX(tc.CouleurHex) AS color, dc.Statut AS statut, s.Nom AS servicenom, @@ -6468,9 +8625,7 @@ app.get('/api/getTeamLeaves', async (req, res) => { console.log(`🔍 Query WHERE final:`, queryConditions); const leavesResult = await queryRequest.query(query); - const formattedLeaves = leavesResult.recordset.map(leave => ({ - ...leave - })); + const formattedLeaves = leavesResult.recordset.map(leave => ({ ...leave })); console.log(`✅ ${formattedLeaves.length} congés trouvés`); @@ -6489,10 +8644,7 @@ app.get('/api/getTeamLeaves', async (req, res) => { console.log("CAS 4: Autres rôles - Fallback service/campus"); if (!serviceId) { - return res.json({ - success: false, - message: 'ServiceId manquant' - }); + return res.json({ success: false, message: 'ServiceId manquant' }); } const checkServiceRequest = pool.request(); @@ -6502,7 +8654,6 @@ app.get('/api/getTeamLeaves', async (req, res) => { const isAdminFinancier = serviceNomCheck === "Administratif & Financier"; if (isAdminFinancier) { - // Service multi-campus const employeesRequest = pool.request(); employeesRequest.input('serviceId', serviceId); const employeesResult = await employeesRequest.query(` @@ -6542,18 +8693,7 @@ app.get('/api/getTeamLeaves', async (req, res) => { JOIN TypeConge tc2 ON dct2.TypeCongeId = tc2.Id WHERE dct2.DemandeCongeId = dc.Id) AS DistinctTypes ) AS type, - CONCAT( - '[', - STRING_AGG( - CONCAT( - '{"type":"', tc.Nom, - '","jours":', dct.NombreJours, - ',"periode":"', COALESCE(dct.PeriodeJournee, 'Journée entière'), '"}' - ), - ',' - ), - ']' - ) AS detailsconges, + ${detailscongesSQL}, MAX(tc.CouleurHex) AS color, dc.Statut AS statut, s.Nom AS servicenom, @@ -6574,9 +8714,7 @@ app.get('/api/getTeamLeaves', async (req, res) => { `; const leavesResult = await queryRequest.query(query); - const formattedLeaves = leavesResult.recordset.map(leave => ({ - ...leave - })); + const formattedLeaves = leavesResult.recordset.map(leave => ({ ...leave })); console.log(`✅ ${formattedLeaves.length} congés trouvés`); @@ -6588,7 +8726,6 @@ app.get('/api/getTeamLeaves', async (req, res) => { }); } else { - // Service + Campus const employeesRequest = pool.request(); employeesRequest.input('serviceId', serviceId); employeesRequest.input('campusId', campusId); @@ -6631,18 +8768,7 @@ app.get('/api/getTeamLeaves', async (req, res) => { JOIN TypeConge tc2 ON dct2.TypeCongeId = tc2.Id WHERE dct2.DemandeCongeId = dc.Id) AS DistinctTypes ) AS type, - CONCAT( - '[', - STRING_AGG( - CONCAT( - '{"type":"', tc.Nom, - '","jours":', dct.NombreJours, - ',"periode":"', COALESCE(dct.PeriodeJournee, 'Journée entière'), '"}' - ), - ',' - ), - ']' - ) AS detailsconges, + ${detailscongesSQL}, MAX(tc.CouleurHex) AS color, dc.Statut AS statut, s.Nom AS servicenom, @@ -6664,9 +8790,7 @@ app.get('/api/getTeamLeaves', async (req, res) => { `; const leavesResult = await queryRequest.query(query); - const formattedLeaves = leavesResult.recordset.map(leave => ({ - ...leave - })); + const formattedLeaves = leavesResult.recordset.map(leave => ({ ...leave })); console.log(`✅ ${formattedLeaves.length} congés trouvés`); @@ -6687,8 +8811,7 @@ app.get('/api/getTeamLeaves', async (req, res) => { error: error.message }); } -}); - +});; // ======================================== // SYNCHRONISATION ENTRA ID CORRIGÉE // À remplacer dans server.js à partir de la ligne ~3700 @@ -7207,203 +9330,289 @@ app.post('/api/updateRequest', upload.array('medicalDocuments', 5), async (req, console.log('Fichiers reçus:', req.files?.length || 0); const { - requestId, - leaveType, - startDate, - endDate, - reason, - businessDays, - userId, - userEmail, - userName, - accessToken + requestId, leaveType, startDate, endDate, + reason, businessDays, userId, userEmail, userName, accessToken } = req.body; - // ⭐ PARSER LA RÉPARTITION (CRITIQUE) let Repartition; try { Repartition = JSON.parse(req.body.Repartition || '[]'); console.log('📊 Répartition parsée:', JSON.stringify(Repartition, null, 2)); } catch (parseError) { console.error('❌ Erreur parsing Repartition:', parseError); - if (req.files) { - req.files.forEach(file => { - if (fs.existsSync(file.path)) { - fs.unlinkSync(file.path); - } - }); - } - return res.status(400).json({ - success: false, - message: 'Erreur de format de la répartition' - }); + req.files?.forEach(file => { if (fs.existsSync(file.path)) fs.unlinkSync(file.path); }); + return res.status(400).json({ success: false, message: 'Erreur de format de la répartition' }); } - // Validation if (!requestId || !leaveType || !startDate || !endDate || !businessDays || !userId) { - if (req.files) { - req.files.forEach(file => { - if (fs.existsSync(file.path)) { - fs.unlinkSync(file.path); - } - }); - } - return res.status(400).json({ - success: false, - message: '❌ Données manquantes' - }); + req.files?.forEach(file => { if (fs.existsSync(file.path)) fs.unlinkSync(file.path); }); + return res.status(400).json({ success: false, message: '❌ Données manquantes' }); } connection = await pool.getConnection(); + + const q = async (sqlText, params = {}, usePoolDirectly = false) => { + const request = (usePoolDirectly || !connection || typeof connection.request !== 'function') + ? pool.request() + : connection.request(); + for (const [key, value] of Object.entries(params)) request.input(key, value); + const result = await request.query(sqlText); + return result.recordset || []; + }; + await connection.beginTransaction(); const uploadedFiles = req.files || []; - console.log(`Demande ID: ${requestId}, User ID: ${userId}`); - // 1️⃣ RÉCUPÉRER LA DEMANDE ORIGINALE - const [originalRequest] = await connection.query( - 'SELECT * FROM DemandeConge WHERE Id = ? AND CollaborateurADId = ?', - [requestId, userId] + // ── Calcul anneeN dynamique (CP et RTT) ────────────────────── + const today = new Date(); + const currentYear = today.getFullYear(); + const dateBascule = new Date(`${currentYear}-06-01`); + const apresBasculement = today >= dateBascule; + const anneeN = apresBasculement ? currentYear + 1 : currentYear; + const anneeN1 = apresBasculement ? currentYear : currentYear - 1; + + // TypeContrat du collaborateur (nécessaire pour taux RTT anticipé) + const collabInfoUpdate = await q( + 'SELECT TOP 1 TypeContrat FROM CollaborateurAD WHERE id = @userId', + { userId } + ); + const typeContrat = collabInfoUpdate[0]?.TypeContrat || '37h'; + + // 1️⃣ RÉCUPÉRER LA DEMANDE ORIGINALE ────────────────────────── + const originalRequest = await q( + 'SELECT * FROM DemandeConge WHERE Id = @requestId AND CollaborateurADId = @userId', + { requestId, userId } ); - if (originalRequest.length === 0) { + if (!originalRequest || originalRequest.length === 0) { await connection.rollback(); - if (req.files) { - req.files.forEach(file => { - if (fs.existsSync(file.path)) { - fs.unlinkSync(file.path); - } - }); - } - return res.status(404).json({ - success: false, - message: '❌ Demande introuvable ou non autorisée' - }); + req.files?.forEach(file => { if (fs.existsSync(file.path)) fs.unlinkSync(file.path); }); + return res.status(404).json({ success: false, message: '❌ Demande introuvable ou non autorisée' }); } const original = originalRequest[0]; const oldStatus = original.Statut; - console.log(`📋 Demande originale: Statut=${oldStatus}`); - // ⭐ VALIDATION : Si arrêt maladie, il faut au moins 1 fichier - const hasABS = Repartition.some(r => r.TypeConge === 'ABS'); - if (hasABS && uploadedFiles.length === 0) { + const aujourdhui = new Date(); + aujourdhui.setHours(0, 0, 0, 0); + const dateDebutOriginale = original.DateDebut ? new Date(original.DateDebut) : null; + if (dateDebutOriginale) dateDebutOriginale.setHours(0, 0, 0, 0); + + if (dateDebutOriginale && dateDebutOriginale <= aujourdhui) { await connection.rollback(); - return res.json({ + req.files?.forEach(file => { if (fs.existsSync(file.path)) fs.unlinkSync(file.path); }); + return res.status(400).json({ success: false, - message: 'Un justificatif médical est obligatoire pour un arrêt maladie' + message: '❌ Impossible de modifier : la date de début est déjà passée ou c\'est aujourd\'hui', + dateDebut: dateDebutOriginale.toISOString().split('T')[0] }); } - // ⭐ VALIDATION DE LA RÉPARTITION - console.log('📊 Validation répartition...'); - console.log('Nombre de jours total:', businessDays); - console.log('Répartition reçue:', JSON.stringify(Repartition, null, 2)); + const hasABS = Repartition.some(r => r.TypeConge === 'ABS'); + if (hasABS && uploadedFiles.length === 0) { + await connection.rollback(); + return res.json({ success: false, message: 'Un justificatif médical est obligatoire pour un arrêt maladie' }); + } - // 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); - } + if (['CP', 'RTT', 'Récup'].includes(r.TypeConge)) 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' - ); - + const hasCountableLeave = Repartition.some(r => ['CP', 'RTT', 'Récup'].includes(r.TypeConge)); if (hasCountableLeave && Math.abs(sommeRepartition - businessDays) > 0.01) { - console.error('❌ ERREUR : Répartition incohérente !'); - - if (req.files) { - req.files.forEach(file => { - if (fs.existsSync(file.path)) { - fs.unlinkSync(file.path); - } - }); - } - + req.files?.forEach(file => { if (fs.existsSync(file.path)) fs.unlinkSync(file.path); }); await connection.rollback(); - return res.json({ success: false, message: `Erreur de répartition : la somme (${sommeRepartition.toFixed(2)}j) ne correspond pas au total (${businessDays}j)` }); } - console.log('✅ Validation répartition OK'); - - // 2️⃣ REMBOURSER L'ANCIENNE DEMANDE (via DeductionDetails) + // 2️⃣ REMBOURSER L'ANCIENNE DEMANDE ─────────────────────────── + // ✅ Plus de SoldeAnticipe — on rembourse uniquement Solde + // ✅ Pour 'Anticipé N+1' : juste suppression DeductionDetails (rien n'avait été déduit du Solde) let restorationStats = { count: 0, details: [] }; if (oldStatus !== 'Refusée' && oldStatus !== 'Annulée' && original.TypeCongeId !== 3) { console.log(`🔄 Remboursement de l'ancienne demande...`); - const [oldDeductions] = await connection.query( - 'SELECT * FROM DeductionDetails WHERE DemandeCongeId = ?', - [requestId] + const oldDeductions = await q( + 'SELECT * FROM DeductionDetails WHERE DemandeCongeId = @requestId', + { requestId } ); - if (oldDeductions.length > 0) { + if (oldDeductions && oldDeductions.length > 0) { for (const d of oldDeductions) { - const [compteur] = await connection.query( - 'SELECT Id, Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?', - [userId, d.TypeCongeId, d.Annee] + + // ✅ Anticipé Toléré → skip (jamais déduit du Solde) + if (d.TypeDeduction === 'Anticipé Toléré') { + console.log(` ⏭️ SKIP: ${d.JoursUtilises}j "Anticipé Toléré" (non acquis)`); + continue; + } + + // ✅ Anticipé N+1 (nouvelle logique) → rien à rembourser en Solde + // La ligne DeductionDetails sera supprimée en bloc plus bas + if (d.TypeDeduction === 'Anticipé N+1') { + console.log(` ⏭️ SKIP Solde pour Anticipé N+1 (${d.JoursUtilises}j) — sera supprimé avec DELETE`); + restorationStats.count++; + restorationStats.details.push({ + typeCongeId: d.TypeCongeId, + annee: d.Annee, + joursRendus: parseFloat(d.JoursUtilises), + isAnticipe: true, + typeDeduction: d.TypeDeduction + }); + continue; + } + + // ✅ AnticipFutur → jamais déduit du Solde, juste suppression DeductionDetails + if (d.TypeDeduction === 'AnticipFutur') { + console.log(` ⏭️ SKIP Solde pour AnticipFutur (${d.JoursUtilises}j) — tracé uniquement, sera supprimé avec DELETE`); + restorationStats.count++; + restorationStats.details.push({ + typeCongeId: d.TypeCongeId, + annee: d.Annee, + joursRendus: 0, + isAnticipe: true, + typeDeduction: d.TypeDeduction + }); + continue; + } + + // ✅ AnticipNp1 (CP) → déduit du Solde CompteurConges N+1 → remettre + if (d.TypeDeduction === 'AnticipNp1') { + const compteurNp1 = await q( + `SELECT Id, Solde FROM CompteurConges WHERE CollaborateurADId = @userId AND TypeCongeId = @typeCongeId AND Annee = @annee`, + { userId, typeCongeId: d.TypeCongeId, annee: d.Annee } + ); + if (compteurNp1?.length > 0) { + const newSolde = parseFloat((parseFloat(compteurNp1[0].Solde) + parseFloat(d.JoursUtilises)).toFixed(2)); + await q( + 'UPDATE CompteurConges SET Solde = @solde, DerniereMiseAJour = GETDATE() WHERE Id = @id', + { solde: newSolde, id: compteurNp1[0].Id } + ); + console.log(` ✅ AnticipNp1 restauré: Solde N+1 +${d.JoursUtilises}j → ${newSolde}j`); + } + restorationStats.count++; + restorationStats.details.push({ typeCongeId: d.TypeCongeId, annee: d.Annee, joursRendus: parseFloat(d.JoursUtilises), typeDeduction: d.TypeDeduction }); + continue; + } + + // ✅ AnticipRTT → solde RTT N réel déduit → remettre + if (d.TypeDeduction === 'AnticipRTT') { + const compteur = await q( + `SELECT Id, Solde FROM CompteurConges WHERE CollaborateurADId = @userId AND TypeCongeId = @typeCongeId AND Annee = @annee`, + { userId, typeCongeId: d.TypeCongeId, annee: d.Annee } + ); + if (compteur?.length > 0) { + const newSolde = parseFloat((parseFloat(compteur[0].Solde) + parseFloat(d.JoursUtilises)).toFixed(2)); + await q('UPDATE CompteurConges SET Solde = @solde, DerniereMiseAJour = GETDATE() WHERE Id = @id', { solde: newSolde, id: compteur[0].Id }); + console.log(` ✅ AnticipRTT restauré: Solde +${d.JoursUtilises}j → ${newSolde}j`); + } + restorationStats.count++; + restorationStats.details.push({ typeCongeId: d.TypeCongeId, annee: d.Annee, joursRendus: parseFloat(d.JoursUtilises), typeDeduction: d.TypeDeduction }); + continue; + } + + // ✅ AnticipFuturRTT → tracé uniquement, aucun solde à remettre + if (d.TypeDeduction === 'AnticipFuturRTT') { + console.log(` ⏭️ SKIP Solde pour AnticipFuturRTT (${d.JoursUtilises}j) — tracé uniquement`); + restorationStats.count++; + restorationStats.details.push({ typeCongeId: d.TypeCongeId, annee: d.Annee, joursRendus: 0, typeDeduction: d.TypeDeduction }); + continue; + } + + // ✅ AnticipNp1RTT (RTT N+1) → déduit du Solde CompteurConges RTT N+1 → remettre + if (d.TypeDeduction === 'AnticipNp1RTT') { + const compteur = await q( + `SELECT Id, Solde FROM CompteurConges WHERE CollaborateurADId = @userId AND TypeCongeId = @typeCongeId AND Annee = @annee`, + { userId, typeCongeId: d.TypeCongeId, annee: d.Annee } + ); + if (compteur?.length > 0) { + const newSolde = parseFloat((parseFloat(compteur[0].Solde) + parseFloat(d.JoursUtilises)).toFixed(2)); + await q('UPDATE CompteurConges SET Solde = @solde, DerniereMiseAJour = GETDATE() WHERE Id = @id', { solde: newSolde, id: compteur[0].Id }); + console.log(` ✅ AnticipNp1RTT restauré: Solde RTT N+1 +${d.JoursUtilises}j → ${newSolde}j`); + } + restorationStats.count++; + restorationStats.details.push({ typeCongeId: d.TypeCongeId, annee: d.Annee, joursRendus: parseFloat(d.JoursUtilises), typeDeduction: d.TypeDeduction }); + continue; + } + + // ✅ Legacy 'N Anticipé' / 'N+1 Anticipé' (ancienne logique) + // Le Solde avait été déduit → on le rembourse + const isAncienAnticipe = d.TypeDeduction && ( + d.TypeDeduction === 'N Anticipé' || + d.TypeDeduction === 'N+1 Anticipé' ); - if (compteur.length > 0) { - const newSolde = parseFloat(compteur[0].Solde) + parseFloat(d.JoursUtilises); - await connection.query( - 'UPDATE CompteurConges SET Solde = ?, DerniereMiseAJour = GETDATE() WHERE Id = ?', - [newSolde, compteur[0].Id] + const compteur = await q( + `SELECT Id, Solde FROM CompteurConges + WHERE CollaborateurADId = @userId AND TypeCongeId = @typeCongeId AND Annee = @annee`, + { userId, typeCongeId: d.TypeCongeId, annee: d.Annee } + ); + + if (compteur && compteur.length > 0) { + const joursUtilises = parseFloat(d.JoursUtilises); + const newSolde = parseFloat(compteur[0].Solde) + joursUtilises; + + await q( + 'UPDATE CompteurConges SET Solde = @solde, DerniereMiseAJour = GETDATE() WHERE Id = @id', + { solde: newSolde, id: compteur[0].Id } ); + + console.log(` ✅ Remboursé ${joursUtilises}j (Solde) — Type=${d.TypeDeduction}, Année=${d.Annee}${isAncienAnticipe ? ' [legacy]' : ''}`); + restorationStats.count++; - restorationStats.details.push(`${d.JoursUtilises}j rendus (Type ${d.TypeCongeId}, Année ${d.Annee})`); - console.log(` ✅ Remboursé ${d.JoursUtilises}j au compteur TypeId=${d.TypeCongeId}, Annee=${d.Annee}`); + restorationStats.details.push({ + typeCongeId: d.TypeCongeId, + annee: d.Annee, + joursRendus: joursUtilises, + isAnticipe: isAncienAnticipe, + typeDeduction: d.TypeDeduction + }); } } - // Supprimer les anciennes déductions - await connection.query('DELETE FROM DeductionDetails WHERE DemandeCongeId = ?', [requestId]); + // Supprime toutes les déductions de cette demande en une fois + await q( + 'DELETE FROM DeductionDetails WHERE DemandeCongeId = @requestId', + { requestId } + ); console.log(` 🧹 ${oldDeductions.length} déduction(s) supprimée(s)`); } } - // 3️⃣ METTRE À JOUR LA DEMANDE (⭐ SQL SERVER : GETDATE + FORMAT) + // 3️⃣ METTRE À JOUR LA DEMANDE ──────────────────────────────── console.log('📝 Mise à jour de la demande...'); - - // Si elle était validée, on la repasse en "En attente" const newStatus = (oldStatus === 'Validée' || oldStatus === 'Validé') ? 'En attente' : oldStatus; - await connection.query( + await q( `UPDATE DemandeConge - SET TypeCongeId = ?, - DateDebut = ?, - DateFin = ?, - Commentaire = ?, - NombreJours = ?, - Statut = ?, + SET TypeCongeId = @leaveType, + DateDebut = @startDate, + DateFin = @endDate, + Commentaire = @reason, + NombreJours = @businessDays, + Statut = @newStatus, DateValidation = GETDATE(), CommentaireValidation = COALESCE(CommentaireValidation, '') + CHAR(10) + '[Modifiée le ' + - FORMAT(GETDATE(), 'dd/MM/yyyy à HH:mm', 'fr-FR') + - ']' - WHERE Id = ?`, - [leaveType, startDate, endDate, reason || '', businessDays, newStatus, requestId] + FORMAT(GETDATE(), 'dd/MM/yyyy à HH:mm', 'fr-FR') + ']' + WHERE Id = @requestId`, + { leaveType, startDate, endDate, reason: reason || '', businessDays, newStatus, requestId } ); - console.log(`✅ Demande ${requestId} modifiée - Statut: ${newStatus}`); - // 4️⃣ SUPPRIMER L'ANCIENNE RÉPARTITION DANS DemandeCongeType - await connection.query('DELETE FROM DemandeCongeType WHERE DemandeCongeId = ?', [requestId]); + // 4️⃣ SUPPRIMER L'ANCIENNE RÉPARTITION ──────────────────────── + await q('DELETE FROM DemandeCongeType WHERE DemandeCongeId = @requestId', { requestId }); - // 5️⃣ Sauvegarder la nouvelle répartition AVEC GÉNÉRATION MANUELLE D'ID + // 5️⃣ SAUVEGARDER LA NOUVELLE RÉPARTITION ───────────────────── console.log('\n📊 Sauvegarde de la nouvelle répartition en base...'); + for (const rep of Repartition) { const code = rep.TypeConge; const name = code === 'CP' ? 'Congé payé' : @@ -7412,35 +9621,26 @@ app.post('/api/updateRequest', upload.array('medicalDocuments', 5), async (req, code === 'Formation' ? 'Formation' : code === 'Récup' ? 'Récupération' : code; - const [typeRow] = await connection.query( - 'SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', - [name] + const typeRows = await q('SELECT TOP 1 Id FROM TypeConge WHERE Nom = @nom', { nom: name }); + if (!typeRows || typeRows.length === 0) { console.warn(` ⚠️ Type introuvable: ${name}`); continue; } + + const typeCongeId = typeRows[0].Id; + const dctId = await getNextId(connection, 'DemandeCongeType'); + + await q( + `INSERT INTO DemandeCongeType (Id, DemandeCongeId, TypeCongeId, NombreJours, PeriodeJournee, DateDebut, DateFin) + VALUES (@id, @demandeCongeId, @typeCongeId, @nbJours, @periode, @dateDebut, @dateFin)`, + { + id: dctId, demandeCongeId: requestId, typeCongeId, + nbJours: rep.NombreJours, periode: rep.PeriodeJournee || 'Journée entière', + dateDebut: rep.DateDebut || startDate, dateFin: rep.DateFin || endDate + } ); - - if (typeRow.length > 0) { - // ⭐ GÉNÉRER L'ID MANUELLEMENT - const demandeCongeTypeId = await getNextId(connection, 'DemandeCongeType'); - - await connection.query(` - INSERT INTO DemandeCongeType - (Id, DemandeCongeId, TypeCongeId, NombreJours, PeriodeJournee) - VALUES (?, ?, ?, ?, ?) - `, [ - demandeCongeTypeId, - requestId, - typeRow[0].Id, - rep.NombreJours, - rep.PeriodeJournee || 'Journée entière' - ]); - console.log(` ✓ ${name}: ${rep.NombreJours}j (${rep.PeriodeJournee || 'Journée entière'})`); - } + console.log(` ✓ ${name}: ${rep.NombreJours}j (${rep.PeriodeJournee || 'Journée entière'})`); } - // 6️⃣ CALCULER ET APPLIQUER LA NOUVELLE DÉDUCTION + // 6️⃣ CALCULER ET APPLIQUER LA NOUVELLE DÉDUCTION ───────────── let newRepartition = []; - const currentYear = new Date().getFullYear(); - const previousYear = currentYear - 1; - const isFormationOnly = Repartition.length === 1 && Repartition[0].TypeConge === 'Formation'; if (!isFormationOnly) { @@ -7452,240 +9652,268 @@ app.post('/api/updateRequest', upload.array('medicalDocuments', 5), async (req, continue; } - // ⭐ TRAITEMENT SPÉCIAL POUR RÉCUP + // ── Récup ───────────────────────────────────────────── if (rep.TypeConge === 'Récup') { - const [recupType] = await connection.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Récupération']); + const recupTypeRows = await q('SELECT TOP 1 Id FROM TypeConge WHERE Nom = @nom', { nom: 'Récupération' }); + if (!recupTypeRows?.length) { console.warn(' ⚠️ Type Récupération introuvable'); continue; } - if (recupType.length > 0) { - await connection.query(` - UPDATE CompteurConges - SET Solde = CASE WHEN Solde - ? < 0 THEN 0 ELSE Solde - ? END, - DerniereMiseAJour = GETDATE() - WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? - `, [rep.NombreJours, rep.NombreJours, userId, recupType[0].Id, currentYear]); + const recupTypeId = recupTypeRows[0].Id; + const nbJours = parseFloat(rep.NombreJours || 0); - await connection.query(` - INSERT INTO DeductionDetails - (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) - VALUES (?, ?, ?, 'Récup Posée', ?) - `, [requestId, recupType[0].Id, currentYear, rep.NombreJours]); - - newRepartition.push({ - typeCongeId: recupType[0].Id, - annee: currentYear, - jours: rep.NombreJours, - typeDeduction: 'Récup Posée' - }); - - console.log(` ✓ Récup: ${rep.NombreJours}j déduits`); - } + await q( + `UPDATE CompteurConges + SET Solde = CASE WHEN Solde - @nbJours < 0 THEN 0 ELSE Solde - @nbJours END, + DerniereMiseAJour = GETDATE() + WHERE CollaborateurADId = @userId AND TypeCongeId = @typeCongeId AND Annee = @annee`, + { nbJours, userId, typeCongeId: recupTypeId, annee: currentYear } + ); + await q( + `INSERT INTO DeductionDetails (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) + VALUES (@requestId, @typeCongeId, @annee, 'Récup Posée', @jours)`, + { requestId, typeCongeId: recupTypeId, annee: currentYear, jours: nbJours } + ); + newRepartition.push({ typeCongeId: recupTypeId, annee: currentYear, jours: rep.NombreJours, typeDeduction: 'Récup Posée' }); + console.log(` ✓ Récup: ${nbJours}j déduits`); continue; } - // ⭐ CP et RTT : déduction normale - const name = rep.TypeConge === 'CP' ? 'Congé payé' : 'RTT'; - const [typeRow] = await connection.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', [name]); + // ── CP ──────────────────────────────────────────────── + if (rep.TypeConge === 'CP') { + const cpTypeRows = await q('SELECT TOP 1 Id FROM TypeConge WHERE Nom = @nom', { nom: 'Congé payé' }); + if (!cpTypeRows?.length) { console.warn(' ⚠️ Type Congé payé introuvable'); continue; } - if (typeRow.length > 0) { - let joursRestants = parseFloat(rep.NombreJours); + const cpTypeId = cpTypeRows[0].Id; - // A. Essayer N-1 (CP uniquement) - if (rep.TypeConge === 'CP') { - const [compteurN1] = await connection.query( - 'SELECT Id, Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?', - [userId, typeRow[0].Id, previousYear] - ); + console.log('🔄 Appel deductLeaveBalanceCP pour modification...'); + const result = await deductLeaveBalanceCP( + connection, userId, cpTypeId, + parseFloat(rep.NombreJours), requestId, startDate + ); + console.log('📊 Résultat déduction CP:', result); - if (compteurN1.length > 0 && compteurN1[0].Solde > 0) { - const disponibleN1 = parseFloat(compteurN1[0].Solde); - const aPrendreN1 = Math.min(disponibleN1, joursRestants); - - await connection.query( - 'UPDATE CompteurConges SET Solde = Solde - ?, DerniereMiseAJour = GETDATE() WHERE Id = ?', - [aPrendreN1, compteurN1[0].Id] + if (!result.success) { + // ✅ Tolérance uniquement pour anticipé post-bascule + // (les jours seront acquis avant la date de départ) + if (result.mode === 'ANTICIPÉ POST-BASCULEMENT') { + console.warn(`⚠️ Anticipé post-basculement — ${result.joursNonDeduits.toFixed(2)}j non couverts (toléré)`); + await q( + `INSERT INTO DeductionDetails (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) + VALUES (@requestId, @typeCongeId, @annee, 'Anticipé Toléré', @jours)`, + { requestId, typeCongeId: cpTypeId, annee: anneeN, jours: result.joursNonDeduits } ); - - await connection.query(` - INSERT INTO DeductionDetails - (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) - VALUES (?, ?, ?, 'Année N-1', ?) - `, [requestId, typeRow[0].Id, previousYear, aPrendreN1]); - - newRepartition.push({ - typeCongeId: typeRow[0].Id, - annee: previousYear, - jours: aPrendreN1, - typeDeduction: 'Année N-1' - }); - - joursRestants -= aPrendreN1; - console.log(` ✓ ${name} N-1: ${aPrendreN1}j déduits (reste: ${joursRestants}j)`); + } else { + throw new Error(`Déduction CP échouée — ${result.joursNonDeduits.toFixed(2)}j non couverts`); } } - // B. Essayer N - if (joursRestants > 0) { - const [compteurN] = await connection.query( - 'SELECT Id, Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?', - [userId, typeRow[0].Id, currentYear] + console.log(`✅ CP déduits: ${result.joursDeduitsTotal.toFixed(2)}j | Mode: ${result.mode}`); + continue; + } + + // ── RTT ─────────────────────────────────────────────── + // ✅ Même logique anticipé que le submit : AnticipRTT / AnticipFuturRTT / AnticipNp1RTT + if (rep.TypeConge === 'RTT') { + const rttTypeRows = await q('SELECT TOP 1 Id FROM TypeConge WHERE Nom = @nom', { nom: 'RTT' }); + if (!rttTypeRows?.length) { console.warn(' ⚠️ Type RTT introuvable'); continue; } + + const rttTypeId = rttTypeRows[0].Id; + const nbJours = parseFloat(rep.NombreJours || 0); + + // anneeRTT = année civile de la date de début du congé + const anneeRTT = new Date(startDate).getFullYear(); + const isAnticipeRTT = anneeRTT > currentYear; + + console.log(` RTT — anneeRTT=${anneeRTT} (${isAnticipeRTT ? 'anticipé' : 'normal'})`); + + if (!isAnticipeRTT) { + // ── MODE NORMAL ────────────────────────────────────────── + const existingRTT = await q( + `SELECT Id FROM CompteurConges WHERE CollaborateurADId = @userId AND TypeCongeId = @typeCongeId AND Annee = @annee`, + { userId, typeCongeId: rttTypeId, annee: anneeRTT } ); - - if (compteurN.length > 0) { - const disponibleN = parseFloat(compteurN[0].Solde); - const aPrendreN = Math.min(disponibleN, joursRestants); - - await connection.query( - 'UPDATE CompteurConges SET Solde = Solde - ?, DerniereMiseAJour = GETDATE() WHERE Id = ?', - [aPrendreN, compteurN[0].Id] + if (!existingRTT?.length) { + const nextId = await getNextId(connection, 'CompteurConges'); + const conn2 = await pool.getConnection(); + let acquisRTT = 0; + try { + const finEx = new Date(anneeRTT, 11, 31); + const rttData = await calculerAcquisitionRTTSmart(conn2, userId, finEx); + acquisRTT = rttData.acquisition; + } finally { conn2.release(); } + await q( + `INSERT INTO CompteurConges (Id, CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour) VALUES (@id, @userId, @typeCongeId, @annee, @total, 0, 0, GETDATE())`, + { id: nextId, userId, typeCongeId: rttTypeId, annee: anneeRTT, total: acquisRTT } ); - - await connection.query(` - INSERT INTO DeductionDetails - (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) - VALUES (?, ?, ?, 'Année N', ?) - `, [requestId, typeRow[0].Id, currentYear, aPrendreN]); - - newRepartition.push({ - typeCongeId: typeRow[0].Id, - annee: currentYear, - jours: aPrendreN, - typeDeduction: 'Année N' - }); - - joursRestants -= aPrendreN; - console.log(` ✓ ${name} N: ${aPrendreN}j déduits (reste: ${joursRestants}j)`); + console.log(` ✅ Compteur RTT ${anneeRTT} créé (Total=${acquisRTT.toFixed(2)}j)`); } - } - - // C. Si il reste des jours → Anticipé - if (joursRestants > 0) { - const [compteurN] = await connection.query( - 'SELECT Id FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?', - [userId, typeRow[0].Id, currentYear] + await q( + `UPDATE CompteurConges SET Solde = CASE WHEN Solde - @nbJours < 0 THEN 0 ELSE Solde - @nbJours END, DerniereMiseAJour = GETDATE() WHERE CollaborateurADId = @userId AND TypeCongeId = @typeCongeId AND Annee = @annee`, + { nbJours, userId, typeCongeId: rttTypeId, annee: anneeRTT } ); + await q( + `INSERT INTO DeductionDetails (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) VALUES (@requestId, @typeCongeId, @annee, 'Année N', @jours)`, + { requestId, typeCongeId: rttTypeId, annee: anneeRTT, jours: nbJours } + ); + console.log(` ✓ RTT normal: ${nbJours}j déduits sur Annee=${anneeRTT}`); - if (compteurN.length > 0) { - await connection.query( - 'UPDATE CompteurConges SET Solde = Solde - ?, DerniereMiseAJour = GETDATE() WHERE Id = ?', - [joursRestants, compteurN[0].Id] - ); + } else { + // ── MODE ANTICIPÉ RTT ───────────────────────────────────── + const anneeRTTCourant = currentYear; - await connection.query(` - INSERT INTO DeductionDetails - (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) - VALUES (?, ?, ?, 'N Anticip', ?) - `, [requestId, typeRow[0].Id, currentYear, joursRestants]); + // Taux RTT N et N+1 + const conn2rtt = await pool.getConnection(); + let joursAnnuelsRTTN = 10; + try { const cfg = await getConfigurationRTT(conn2rtt, anneeRTTCourant, typeContrat || '37h'); joursAnnuelsRTTN = cfg.joursAnnuels; } finally { conn2rtt.release(); } + const conn3rtt = await pool.getConnection(); + let joursAnnuelsRTTNp1 = 10; + try { const cfg = await getConfigurationRTT(conn3rtt, anneeRTT, typeContrat || '37h'); joursAnnuelsRTTNp1 = cfg.joursAnnuels; } finally { conn3rtt.release(); } + const tauxRTTN = joursAnnuelsRTTN / 360; + const tauxRTTNp1 = joursAnnuelsRTTNp1 / 360; - newRepartition.push({ - typeCongeId: typeRow[0].Id, - annee: currentYear, - jours: joursRestants, - typeDeduction: 'N Anticip' - }); + // Solde RTT N réel + const cRTTN = await q( + `SELECT Id, Solde FROM CompteurConges WHERE CollaborateurADId = @userId AND TypeCongeId = @typeCongeId AND Annee = @annee`, + { userId, typeCongeId: rttTypeId, annee: anneeRTTCourant } + ); + const soldeRTTNActuel = cRTTN?.length > 0 ? Math.max(0, parseFloat(cRTTN[0].Solde || 0)) : 0; - console.log(` ⚠️ ${name} Anticipé: ${joursRestants}j`); + // Projection RTT N jusqu'au 31/12 + const finExRTTN = new Date(anneeRTTCourant, 11, 31); finExRTTN.setHours(23, 59, 59, 0); + const joursRestantsRTTN = Math.max(0, Math.floor((finExRTTN - today) / 86400000)); + const acquisRestantsRTTN = parseFloat(Math.min(joursAnnuelsRTTN - soldeRTTNActuel, joursRestantsRTTN * tauxRTTN).toFixed(2)); + const soldeRTTNProj = parseFloat(Math.min(joursAnnuelsRTTN, soldeRTTNActuel + acquisRestantsRTTN).toFixed(2)); + + // Déjà consommé RTT N (AnticipRTT + AnticipFuturRTT) + const dejaConsoRTTNRows = await q( + `SELECT CAST(COALESCE(SUM(dd.JoursUtilises), 0) AS FLOAT) AS total FROM DeductionDetails dd JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id WHERE dc.CollaborateurADId = @userId AND dd.TypeCongeId = @typeCongeId AND dd.Annee = @annee AND dd.TypeDeduction IN ('AnticipRTT', 'AnticipFuturRTT') AND dc.Statut != 'Refusé'`, + { userId, typeCongeId: rttTypeId, annee: anneeRTTCourant } + ); + const dejaConsoRTTN = parseFloat(dejaConsoRTTNRows[0]?.total || 0); + const dispoRTTN = parseFloat(Math.max(0, soldeRTTNProj - dejaConsoRTTN).toFixed(2)); + + // Prorata RTT N+1 depuis 01/01/anneeRTT + const dateDebutCongeRTT = new Date(startDate); dateDebutCongeRTT.setHours(0, 0, 0, 0); + const debutNp1RTT = new Date(anneeRTT, 0, 1); debutNp1RTT.setHours(0, 0, 0, 0); + const diffJoursRTTNp1 = Math.max(0, Math.floor((dateDebutCongeRTT - debutNp1RTT) / 86400000)); + const acquisRTTNp1 = parseFloat(Math.min(joursAnnuelsRTTNp1, diffJoursRTTNp1 * tauxRTTNp1).toFixed(2)); + const dejaConsoRTTNp1Rows = await q( + `SELECT CAST(COALESCE(SUM(dd.JoursUtilises), 0) AS FLOAT) AS total FROM DeductionDetails dd JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id WHERE dc.CollaborateurADId = @userId AND dd.TypeCongeId = @typeCongeId AND dd.Annee = @annee AND dc.Statut != 'Refusé'`, + { userId, typeCongeId: rttTypeId, annee: anneeRTT } + ); + const dejaConsoRTTNp1 = parseFloat(dejaConsoRTTNp1Rows[0]?.total || 0); + const dispoRTTNp1 = parseFloat(Math.max(0, acquisRTTNp1 - dejaConsoRTTNp1).toFixed(2)); + const totalDispoRTT = parseFloat((dispoRTTN + dispoRTTNp1).toFixed(2)); + + console.log(` 💰 RTT N (${anneeRTTCourant}): projeté ${soldeRTTNProj.toFixed(2)}j - consommé ${dejaConsoRTTN.toFixed(2)}j = dispo ${dispoRTTN.toFixed(2)}j`); + console.log(` 💰 RTT N+1 (${anneeRTT}): ${acquisRTTNp1.toFixed(2)}j - consommé ${dejaConsoRTTNp1.toFixed(2)}j = dispo ${dispoRTTNp1.toFixed(2)}j`); + console.log(` 📊 RTT total: ${totalDispoRTT.toFixed(2)}j | demandés=${nbJours}j`); + + if (totalDispoRTT < nbJours) { + throw new Error(`Solde RTT anticipé insuffisant : ${totalDispoRTT.toFixed(2)}j disponibles, ${nbJours}j demandés`); } + + let joursRestantsRTT = nbJours; + + // 1️⃣ Solde RTT N réel → AnticipRTT + if (joursRestantsRTT > 0 && soldeRTTNActuel > 0 && cRTTN?.length > 0) { + const aDeduireRTT = parseFloat(Math.min(soldeRTTNActuel, joursRestantsRTT).toFixed(2)); + await q('UPDATE CompteurConges SET Solde = Solde - @jours, DerniereMiseAJour = GETDATE() WHERE Id = @id', { jours: aDeduireRTT, id: cRTTN[0].Id }); + await q(`INSERT INTO DeductionDetails (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) VALUES (@requestId, @typeCongeId, @annee, 'AnticipRTT', @jours)`, { requestId, typeCongeId: rttTypeId, annee: anneeRTTCourant, jours: aDeduireRTT }); + joursRestantsRTT = parseFloat((joursRestantsRTT - aDeduireRTT).toFixed(2)); + console.log(` ✅ AnticipRTT: ${aDeduireRTT.toFixed(2)}j → reste ${joursRestantsRTT.toFixed(2)}j`); + } + + // 2️⃣ Acquisition future RTT N (jusqu'au 31/12) → AnticipFuturRTT + if (joursRestantsRTT > 0 && dispoRTTN > 0) { + const aTracerRTTN = parseFloat(Math.min(dispoRTTN, joursRestantsRTT).toFixed(2)); + await q(`INSERT INTO DeductionDetails (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) VALUES (@requestId, @typeCongeId, @annee, 'AnticipFuturRTT', @jours)`, { requestId, typeCongeId: rttTypeId, annee: anneeRTTCourant, jours: aTracerRTTN }); + joursRestantsRTT = parseFloat((joursRestantsRTT - aTracerRTTN).toFixed(2)); + console.log(` ✅ AnticipFuturRTT: ${aTracerRTTN.toFixed(2)}j tracé → reste ${joursRestantsRTT.toFixed(2)}j`); + } + + // 3️⃣ Prorata RTT N+1 → AnticipNp1RTT + if (joursRestantsRTT > 0 && dispoRTTNp1 > 0) { + const aTracerRTTNp1 = parseFloat(Math.min(dispoRTTNp1, joursRestantsRTT).toFixed(2)); + const cRTTNp1 = await q(`SELECT Id, Solde FROM CompteurConges WHERE CollaborateurADId = @userId AND TypeCongeId = @typeCongeId AND Annee = @annee`, { userId, typeCongeId: rttTypeId, annee: anneeRTT }); + if (!cRTTNp1?.length) { + const nextId = await getNextId(connection, 'CompteurConges'); + await q(`INSERT INTO CompteurConges (Id, CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour) VALUES (@id, @userId, @typeCongeId, @annee, 0, @solde, 0, GETDATE())`, { id: nextId, userId, typeCongeId: rttTypeId, annee: anneeRTT, solde: -aTracerRTTNp1 }); + console.log(` ➕ CompteurConges RTT Annee=${anneeRTT} créé Solde=${-aTracerRTTNp1}j`); + } else { + await q('UPDATE CompteurConges SET Solde = Solde - @jours, DerniereMiseAJour = GETDATE() WHERE Id = @id', { jours: aTracerRTTNp1, id: cRTTNp1[0].Id }); + } + await q(`INSERT INTO DeductionDetails (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) VALUES (@requestId, @typeCongeId, @annee, 'AnticipNp1RTT', @jours)`, { requestId, typeCongeId: rttTypeId, annee: anneeRTT, jours: aTracerRTTNp1 }); + joursRestantsRTT = parseFloat((joursRestantsRTT - aTracerRTTNp1).toFixed(2)); + console.log(` ✅ AnticipNp1RTT: ${aTracerRTTNp1.toFixed(2)}j tracé sur RTT ${anneeRTT}`); + } + + if (joursRestantsRTT > 0.01) { + throw new Error(`Déduction RTT anticipée incomplète: ${joursRestantsRTT.toFixed(2)}j non couverts`); + } + console.log(` ✓ RTT anticipé: ${nbJours}j déduits | AnticipRTT+AnticipFuturRTT+AnticipNp1RTT`); } + continue; } } + + // ✅ updateSoldeAnticipe supprimé — plus nécessaire + console.log('✅ Déductions appliquées'); } await connection.commit(); console.log(`✅ Demande ${requestId} modifiée avec succès`); - // 7️⃣ ENVOI DES EMAILS (Asynchrone) + // 7️⃣ ENVOI DES EMAILS (après commit) ───────────────────────── const graphToken = await getGraphToken(); if (graphToken) { - const [managerInfo] = await connection.query( + const managerInfo = await q( `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] + WHERE c.id = @userId`, + { userId }, + true // usePoolDirectly après commit ); - if (managerInfo.length > 0) { + if (managerInfo?.length > 0) { const manager = managerInfo[0]; - // Email au manager - sendMailGraph( - graphToken, - 'gtanoreply@ensup.eu', - manager.Email, + sendMailGraph(graphToken, 'gtanoreply@ensup.eu', manager.Email, '🔄 Modification de demande de congé', - ` -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) | -
- ✅ Les compteurs ont été automatiquement recalculés (${restorationStats.count} opération(s)). -
-Merci de valider ou refuser cette demande dans l'application.
+${userName} a modifié sa demande de congé :
+| Type : | ${getLeaveTypeName(leaveType)} |
| Dates : | du ${formatDateFR(startDate)} au ${formatDateFR(endDate)} |
| Durée : | ${businessDays} jour(s) |
✅ Compteurs recalculés (${restorationStats.count} opération(s)).
` : ''}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.
+Bonjour ${userName.split(' ')[0]}, votre demande 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.
Bonjour ${collabName},
- -- Votre demande de congé a bien été annulée. -
- -| Période : | -${datesPeriode} | -
| Durée totale : | -${request.NombreJours} jour(s) | -
| - Répartition : - | -|
| Période : | ${datesPeriode} |
| Durée totale : | ${request.NombreJours} jour(s) |
| Répartition : | |
- ✅ Vos compteurs ont été restaurés
- ${restorationStats.count} opération(s) de remboursement effectuée(s).
-
- 📧 Cet email est envoyé automatiquement, merci de ne pas y répondre. -
-✅ Vos compteurs ont été restaurés
${restorationStats.count} opération(s) effectuée(s).
📧 Cet email est envoyé automatiquement, merci de ne pas y répondre.
Bonjour ${managerName},
- -- ${collabName} a annulé ${isValidated ? 'son congé validé' : 'sa demande de congé'}. -
- -| Statut initial : | -${requestStatus} | -
| Période : | -${datesPeriode} | -
| Durée totale : | -${request.NombreJours} jour(s) | -
| - Répartition : - | -|
| Statut initial : | ${requestStatus} |
| Période : | ${datesPeriode} |
| Durée totale : | ${request.NombreJours} jour(s) |
| Répartition : | |
- ✅ Les compteurs ont été automatiquement restaurés (${restorationStats.count} opération(s)). -
-- 📧 Cet email est envoyé automatiquement, merci de ne pas y répondre. -
-✅ Les compteurs ont été automatiquement restaurés (${restorationStats.count} opération(s)).
+📧 Cet email est envoyé automatiquement, merci de ne pas y répondre.
Bonjour ${managerName},
++ Un non-respect des temps de repos obligatoires a été déclaré par ${collabName}. +
+| Collaborateur : | +${collabName} | +
| Date : | +${dateFormatted} | +
💬 Commentaire :
+${commentaire}
++ ℹ️ Rappel : Le Code du travail impose un repos quotidien minimum de 11 heures consécutives + et un repos hebdomadaire de 35 heures consécutives (24h + 11h). +
++ Cet email est envoyé automatiquement depuis l'application GTA. +
+Lance une vérification pour voir les données...
+