Files
GTA/project/public/Backend/server.js
2025-11-28 16:55:45 +01:00

8801 lines
351 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import express from 'express';
import mysql from 'mysql2/promise';
import cors from 'cors';
import axios from 'axios';
import multer from 'multer';
import path from 'path';
import { fileURLToPath } from 'url';
import cron from 'node-cron';
import crypto from 'crypto';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
import WebhookManager from './webhook-utils.js';
import { WEBHOOKS, EVENTS } from './webhook-config.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = express();
const PORT = 3000;
const webhookManager = new WebhookManager(WEBHOOKS.SECRET_KEY);
const sseClientsCollab = new Set();
process.on('uncaughtException', (err) => {
console.error('💥 ERREUR CRITIQUE NON CATCHÉE:', err);
console.error('Stack:', err.stack);
// On ne crash pas pour pouvoir déboguer
});
process.on('unhandledRejection', (reason, promise) => {
console.error('💥 PROMESSE REJETÉE NON GÉRÉE:', reason);
console.error('Promise:', promise);
});
app.use(cors({
origin: ['http://localhost:3013', 'http://localhost:80', 'https://mygta.ensup-adm.net'],
credentials: true
}));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
const dbConfig = {
host: '192.168.0.4',
user: 'wpuser',
password: '-2b/)ru5/Bi8P[7_',
database: 'DemandeConge',
port:'3306',
charset: 'utf8mb4'
};
function nowFR() {
const d = new Date();
d.setHours(d.getHours() + 2);
return d.toISOString().slice(0, 19).replace('T', ' ');
}
// ========================================
// HELPER POUR DATES SANS CONVERSION UTC
// ========================================
function formatDateWithoutUTC(date) {
if (!date) return null;
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
function formatDateToFrenchTime(date) {
if (!date) return null;
// Créer un objet Date et le convertir en heure française (Europe/Paris)
const d = new Date(date);
// Formater en ISO avec le fuseau horaire français
return d.toLocaleString('fr-FR', {
timeZone: 'Europe/Paris',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
});
}
// Ou plus simple, pour avoir un format ISO compatible avec le frontend
function formatDateToFrenchISO(date) {
if (!date) return null;
const d = new Date(date);
// Convertir en heure française
const frenchDate = new Date(d.toLocaleString('en-US', { timeZone: 'Europe/Paris' }));
return frenchDate.toISOString();
}
// ========================================
// FONCTIONS POUR GÉRER LES ARRÊTÉS COMPTABLES
// À ajouter après : const pool = mysql.createPool(dbConfig);
// ========================================
/**
* Récupère le dernier arrêté validé/clôturé
*/
async function getDernierArrete(conn) {
const [arretes] = await conn.query(`
SELECT * FROM ArreteComptable
WHERE Statut IN ('Validé', 'Clôturé')
ORDER BY DateArrete DESC
LIMIT 1
`);
return arretes.length > 0 ? arretes[0] : null;
}
/**
* Vérifie si une date est avant le dernier arrêté
*/
async function estAvantArrete(conn, date) {
const dernierArrete = await getDernierArrete(conn);
if (!dernierArrete) {
return false; // Pas d'arrêté = toutes les dates sont autorisées
}
const dateTest = new Date(date);
const dateArrete = new Date(dernierArrete.DateArrete);
return dateTest <= dateArrete;
}
/**
* Récupère le solde figé d'un collaborateur pour un type de congé
*/
async function getSoldeFige(conn, collaborateurId, typeCongeId, annee) {
const dernierArrete = await getDernierArrete(conn);
if (!dernierArrete) {
return null; // Pas d'arrêté
}
const [soldes] = await conn.query(`
SELECT * FROM SoldesFiges
WHERE ArreteId = ?
AND CollaborateurADId = ?
AND TypeCongeId = ?
AND Annee = ?
LIMIT 1
`, [dernierArrete.Id, collaborateurId, typeCongeId, annee]);
return soldes.length > 0 ? soldes[0] : null;
}
/**
* Calcule l'acquisition depuis le dernier arrêté
*/
async function calculerAcquisitionDepuisArrete(conn, collaborateurId, typeConge, dateReference = new Date()) {
const dernierArrete = await getDernierArrete(conn);
const anneeRef = dateReference.getFullYear();
// Déterminer le type de congé
const [typeRow] = await conn.query(
'SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1',
[typeConge === 'CP' ? 'Congé payé' : 'RTT']
);
if (typeRow.length === 0) {
throw new Error(`Type de congé ${typeConge} non trouvé`);
}
const typeCongeId = typeRow[0].Id;
const [collab] = await conn.query('SELECT role FROM CollaborateurAD WHERE id = ?', [collaborateurId]);
const isApprenti = collab.length > 0 && collab[0].role === 'Apprenti';
if (typeConge === 'RTT' && isApprenti) {
return 0; // ⭐ Les apprentis n'ont pas de RTT
}
// Si pas d'arrêté, calcul normal depuis le début
if (!dernierArrete) {
if (typeConge === 'CP') {
const [collab] = await conn.query('SELECT DateEntree, CampusId FROM CollaborateurAD WHERE id = ?', [collaborateurId]);
const dateEntree = collab.length > 0 ? collab[0].DateEntree : null;
return calculerAcquisitionCP(dateReference, dateEntree);
} else {
const rttData = await calculerAcquisitionRTT(conn, collaborateurId, dateReference);
return rttData.acquisition;
}
}
const dateArrete = new Date(dernierArrete.DateArrete);
// Si la date de référence est AVANT l'arrêté, utiliser le solde figé
if (dateReference <= dateArrete) {
const soldeFige = await getSoldeFige(conn, collaborateurId, typeCongeId, anneeRef);
return soldeFige ? soldeFige.TotalAcquis : 0;
}
// Si la date est APRÈS l'arrêté, partir du solde figé + calcul depuis l'arrêté
const soldeFige = await getSoldeFige(conn, collaborateurId, typeCongeId, anneeRef);
const acquisFigee = soldeFige ? soldeFige.TotalAcquis : 0;
// Calculer l'acquisition DEPUIS l'arrêté
let acquisDepuisArrete = 0;
if (typeConge === 'CP') {
const moisDepuisArrete = getMoisTravaillesCP(dateReference, dateArrete);
acquisDepuisArrete = moisDepuisArrete * (25 / 12);
} else {
const [collab] = await conn.query('SELECT TypeContrat, CampusId FROM CollaborateurAD WHERE id = ?', [collaborateurId]);
const typeContrat = collab.length > 0 && collab[0].TypeContrat ? collab[0].TypeContrat : '37h';
const config = await getConfigurationRTT(conn, anneeRef, typeContrat);
const moisDepuisArrete = getMoisTravaillesRTT(dateReference, dateArrete);
acquisDepuisArrete = moisDepuisArrete * config.acquisitionMensuelle;
}
return acquisFigee + acquisDepuisArrete;
}
const pool = mysql.createPool(dbConfig);
const AZURE_CONFIG = {
tenantId: '9840a2a0-6ae1-4688-b03d-d2ec291be0f9',
clientId: '4bb4cc24-bac3-427c-b02c-5d14fc67b561',
clientSecret: 'gvf8Q~545Bafn8yYsgjW~QG_P1lpzaRe6gJNgb2t',
groupId: 'c1ea877c-6bca-4f47-bfad-f223640813a0'
};
const storage = multer.diskStorage({
destination: './uploads/',
filename: (req, file, cb) => {
cb(null, Date.now() + path.extname(file.originalname));
}
});
const upload = multer({ storage });
const medicalStorage = multer.diskStorage({
destination: './uploads/medical/',
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, 'medical-' + uniqueSuffix + path.extname(file.originalname));
}
});
const ACCES_TRANSVERSAUX = {
'sloisil@ensup.eu': {
typeAcces: 'service_multi_campus',
serviceNom: 'Pédagogie',
description: 'Sandrine - Vue complète Pédagogie (tous campus)'
},
'mbouteiller@ensup.eu': {
typeAcces: 'service_multi_campus',
serviceNom: 'Admissions',
description: 'Morgane - Vue complète Admissions (tous campus)'
},
'vnoel@ensup.eu': {
typeAcces: 'service_multi_campus',
serviceNom: 'Relations Entreprises',
description: 'Viviane - Vue complète Relations Entreprises (tous campus)'
},
'vpierrel@ensup.eu': {
typeAcces: 'service_multi_campus', // ✅ CORRIGÉ - même type que les autres
serviceNom: 'Administratif & Financier',
description: 'Virginie - Vue complète Administratif & Financier (tous campus)'
}
};
function getUserAccesTransversal(userEmail) {
const acces = ACCES_TRANSVERSAUX[userEmail?.toLowerCase()] || null;
if (acces) {
console.log(`🌐 Accès transversal: ${acces.description}`);
}
return acces;
}
const uploadMedical = multer({
storage: medicalStorage,
limits: { fileSize: 5 * 1024 * 1024 },
fileFilter: (req, file, cb) => {
const allowedTypes = ['application/pdf', 'image/jpeg', 'image/jpg', 'image/png'];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('Type de fichier non autorisé'));
}
}
});
import fs from 'fs';
if (!fs.existsSync('./uploads/medical')) {
fs.mkdirSync('./uploads/medical', { recursive: true });
}
app.get('/api/events/collaborateur', (req, res) => {
const userId = req.query.user_id;
if (!userId) {
return res.status(401).json({ error: 'user_id requis' });
}
console.log('🔔 Nouvelle connexion SSE collaborateur:', userId);
// ⭐ HEADERS CRITIQUES POUR SSE
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache, no-transform');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no');
res.setHeader('Access-Control-Allow-Origin', '*');
// ⭐ FLUSH IMMÉDIATEMENT POUR ÉTABLIR LA CONNEXION
res.flushHeaders();
const sendEvent = (data) => {
try {
if (res.writableEnded) {
console.log('⚠️ Tentative d\'envoi sur connexion fermée');
return false;
}
res.write(`data: ${JSON.stringify(data)}\n\n`);
return true;
} catch (error) {
console.error('❌ Erreur envoi SSE:', error);
return false;
}
};
const client = {
userId: parseInt(userId),
send: sendEvent,
res: res // ⭐ Garder référence pour vérifier l'état
};
sseClientsCollab.add(client);
console.log(`📊 Clients SSE collaborateurs connectés: ${sseClientsCollab.size}`);
// ⭐ ÉVÉNEMENT DE CONNEXION
sendEvent({
type: 'connected',
message: 'Connexion établie',
timestamp: new Date().toISOString()
});
// ⭐ HEARTBEAT AVEC VÉRIFICATION
const heartbeat = setInterval(() => {
const success = sendEvent({
type: 'heartbeat',
timestamp: new Date().toISOString()
});
if (!success) {
console.log('💔 Heartbeat échoué, nettoyage...');
clearInterval(heartbeat);
sseClientsCollab.delete(client);
}
}, 30000); // 30 secondes
// ⭐ GESTION PROPRE DE LA DÉCONNEXION
const cleanup = () => {
console.log('🔌 Déconnexion SSE collaborateur:', userId);
clearInterval(heartbeat);
sseClientsCollab.delete(client);
console.log(`📊 Clients SSE collaborateurs connectés: ${sseClientsCollab.size}`);
};
req.on('close', cleanup);
req.on('error', (err) => {
console.error('❌ Erreur SSE connexion:', err.message);
cleanup();
});
// ⭐ TIMEOUT DE SÉCURITÉ (optionnel, mais recommandé)
req.socket.setTimeout(0); // Désactiver timeout pour SSE
});
const notifyCollabClients = (event, targetUserId = null) => {
console.log(
`📢 Notification SSE Collab: ${event.type}`,
targetUserId ? `pour user ${targetUserId}` : 'pour tous'
);
const deadClients = [];
sseClientsCollab.forEach(client => {
// ⭐ FILTRER PAR USER SI NÉCESSAIRE
if (targetUserId && client.userId !== targetUserId) {
return;
}
// ⭐ VÉRIFIER SI LA CONNEXION EST TOUJOURS ACTIVE
if (client.res && client.res.writableEnded) {
console.log(`💀 Client mort détecté: ${client.userId}`);
deadClients.push(client);
return;
}
// ⭐ ENVOYER L'ÉVÉNEMENT
const success = client.send(event);
if (!success) {
deadClients.push(client);
}
});
// ⭐ NETTOYER LES CLIENTS MORTS
deadClients.forEach(client => {
console.log(`🧹 Nettoyage client mort: ${client.userId}`);
sseClientsCollab.delete(client);
});
if (deadClients.length > 0) {
console.log(`📊 Clients SSE après nettoyage: ${sseClientsCollab.size}`);
}
};
const sseClients = new Set();
// 🔌 ROUTE SSE POUR LE CALENDRIER
app.get('/sse', (req, res) => {
const userId = req.query.user_id;
if (!userId) {
return res.status(400).json({ error: 'user_id requis' });
}
console.log('🔌 Nouvelle connexion SSE:', userId);
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no');
const sendEvent = (data) => {
try {
res.write(`data: ${JSON.stringify(data)}\n\n`);
} catch (error) {
console.error('❌ Erreur envoi SSE:', error);
}
};
const client = { id: userId, send: sendEvent };
sseClients.add(client);
console.log(`📊 Clients SSE connectés: ${sseClients.size}`);
// Envoyer un heartbeat initial
sendEvent({
type: 'ping',
message: 'Connexion établie',
timestamp: new Date().toISOString()
});
// Heartbeat toutes les 30 secondes
const heartbeat = setInterval(() => {
try {
sendEvent({ type: 'ping', timestamp: new Date().toISOString() });
} catch (error) {
console.error('❌ Erreur heartbeat:', error);
clearInterval(heartbeat);
}
}, 30000);
req.on('close', () => {
console.log('🔌 Déconnexion SSE:', userId);
clearInterval(heartbeat);
sseClients.delete(client);
console.log(`📊 Clients SSE connectés: ${sseClients.size}`);
});
});
// 📢 FONCTION POUR NOTIFIER LES CLIENTS
const notifyClients = (event, userId = null) => {
console.log(`📢 Notification SSE: ${event.type}${userId ? ` pour ${userId}` : ''}`);
sseClients.forEach(client => {
// Si userId est spécifié, envoyer seulement à ce client
if (userId && client.id !== userId) {
return;
}
try {
client.send(event);
} catch (error) {
console.error('❌ Erreur envoi event:', error);
}
});
};
app.post('/api/webhook/receive', async (req, res) => {
try {
const signature = req.headers['x-webhook-signature'];
const payload = req.body;
console.log('\n📥 === WEBHOOK REÇU (COLLABORATEURS) ===');
console.log('Event:', payload.event);
console.log('Data:', JSON.stringify(payload.data, null, 2));
// Vérifier la signature
if (!webhookManager.verifySignature(payload, signature)) {
console.error('❌ Signature webhook invalide');
return res.status(401).json({ error: 'Signature invalide' });
}
const { event, data } = payload;
// Traiter selon le type d'événement
switch (event) {
case EVENTS.COMPTEUR_UPDATED:
console.log('\n💰 === WEBHOOK COMPTEUR_UPDATED REÇU ===');
console.log(` Collaborateur: ${data.collaborateurId}`);
console.log(` Type mise à jour: ${data.typeUpdate}`);
console.log(` Type congé: ${data.typeConge}`);
console.log(` Année: ${data.annee}`);
// ⭐ SI MODIFICATION RH, METTRE À JOUR LA BASE LOCALE
if (data.source === 'rh' && data.nouveauTotal !== undefined && data.nouveauSolde !== undefined) {
console.log('🔄 Synchronisation depuis RH...');
console.log(` Nouveau Total: ${data.nouveauTotal}j`);
console.log(` Nouveau Solde: ${data.nouveauSolde}j`);
const conn = await pool.getConnection();
try {
// Identifier le type de congé
const typeName = data.typeConge === 'Congé payé' ? 'Congé payé' :
data.typeConge === 'RTT' ? 'RTT' : data.typeConge;
const [typeRow] = await conn.query(
'SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1',
[typeName]
);
if (typeRow.length > 0) {
const typeCongeId = typeRow[0].Id;
// Vérifier si le compteur existe
const [existing] = await conn.query(`
SELECT Id FROM CompteurConges
WHERE CollaborateurADId = ?
AND TypeCongeId = ?
AND Annee = ?
`, [data.collaborateurId, typeCongeId, data.annee]);
if (existing.length > 0) {
// Mettre à jour
await conn.query(`
UPDATE CompteurConges
SET Total = ?,
Solde = ?,
DerniereMiseAJour = NOW()
WHERE Id = ?
`, [data.nouveauTotal, data.nouveauSolde, existing[0].Id]);
console.log(' ✅ Compteur local mis à jour');
} else {
// Créer
await conn.query(`
INSERT INTO CompteurConges
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour)
VALUES (?, ?, ?, ?, ?, 0, NOW())
`, [data.collaborateurId, typeCongeId, data.annee, data.nouveauTotal, data.nouveauSolde]);
console.log(' ✅ Compteur local créé');
}
}
conn.release();
} catch (dbError) {
console.error('❌ Erreur mise à jour locale:', dbError.message);
if (conn) conn.release();
}
}
// ⭐ NOTIFIER LE CLIENT SSE DU COLLABORATEUR
notifyCollabClients({
type: 'compteur-updated',
collaborateurId: data.collaborateurId,
typeConge: data.typeConge,
annee: data.annee,
typeUpdate: data.typeUpdate,
nouveauTotal: data.nouveauTotal,
nouveauSolde: data.nouveauSolde,
source: data.source || 'rh',
timestamp: new Date().toISOString()
}, data.collaborateurId);
console.log(` 📢 Notification SSE envoyée au collaborateur ${data.collaborateurId}`);
break;
case EVENTS.DEMANDE_VALIDATED:
console.log('\n✅ === WEBHOOK DEMANDE_VALIDATED REÇU ===');
console.log(` Demande: ${data.demandeId}`);
console.log(` Statut: ${data.statut}`);
console.log(` Type: ${data.typeConge}`);
console.log(` Couleur: ${data.couleurHex}`);
// Notifier les clients SSE avec TOUTES les infos
notifyClients({
type: 'demande-validated',
demandeId: data.demandeId,
statut: data.statut,
typeConge: data.typeConge,
couleurHex: data.couleurHex || '#d946ef',
date: data.date,
periode: data.periode,
collaborateurId: data.collaborateurId,
timestamp: new Date().toISOString()
}, data.collaborateurId);
// Notifier les RH aussi
notifyClients({
type: 'demande-list-updated',
action: 'validation-collab',
demandeId: data.demandeId,
statut: data.statut,
typeConge: data.typeConge,
couleurHex: data.couleurHex || '#d946ef',
timestamp: new Date().toISOString()
});
console.log(' 📢 Notifications SSE envoyées');
break;
case EVENTS.DEMANDE_UPDATED:
console.log('\n✏ === WEBHOOK DEMANDE_UPDATED REÇU ===');
console.log(` Demande: ${data.demandeId}`);
console.log(` Collaborateur: ${data.collaborateurId}`);
notifyCollabClients({
type: 'demande-updated-rh',
demandeId: data.demandeId,
timestamp: new Date().toISOString()
}, data.collaborateurId);
console.log(' 📢 Notification modification envoyée');
break;
case EVENTS.DEMANDE_DELETED:
console.log('\n🗑 === WEBHOOK DEMANDE_DELETED REÇU ===');
console.log(` Demande: ${data.demandeId}`);
console.log(` Collaborateur: ${data.collaborateurId}`);
notifyCollabClients({
type: 'demande-deleted-rh',
demandeId: data.demandeId,
timestamp: new Date().toISOString()
}, data.collaborateurId);
console.log(' 📢 Notification suppression envoyée');
break;
default:
console.warn(`⚠️ Type d'événement webhook inconnu: ${event}`);
}
res.json({ success: true, message: 'Webhook traité' });
} catch (error) {
console.error('❌ Erreur traitement webhook:', error);
res.status(500).json({ error: error.message });
}
});
app.post('/api/syncCompteursFromRH', async (req, res) => {
try {
const { user_id } = req.body;
if (!user_id) {
return res.json({ success: false, message: 'user_id manquant' });
}
console.log('\n🔄 === SYNCHRONISATION MANUELLE DEPUIS RH ===');
console.log('User ID:', user_id);
// Récupérer les compteurs depuis le serveur RH
const rhUrl = process.env.RH_SERVER_URL || 'http://localhost:3001';
try {
const response = await fetch(`${rhUrl}/api/compteurs?user_id=${user_id}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`Erreur serveur RH: ${response.status}`);
}
const rhCompteurs = await response.json();
console.log('📊 Compteurs RH récupérés:', rhCompteurs.length);
// Mettre à jour la base locale
const conn = await pool.getConnection();
await conn.beginTransaction();
let updated = 0;
let created = 0;
for (const compteur of rhCompteurs) {
// Identifier le type de congé
const [typeRow] = await conn.query(
'SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1',
[compteur.typeConge]
);
if (typeRow.length === 0) {
console.warn(`⚠️ Type ${compteur.typeConge} non trouvé`);
continue;
}
const typeCongeId = typeRow[0].Id;
// Vérifier si existe
const [existing] = await conn.query(`
SELECT Id FROM CompteurConges
WHERE CollaborateurADId = ?
AND TypeCongeId = ?
AND Annee = ?
`, [compteur.collaborateurId, typeCongeId, compteur.annee]);
if (existing.length > 0) {
// Mettre à jour
await conn.query(`
UPDATE CompteurConges
SET Total = ?,
Solde = ?,
SoldeReporte = ?,
DerniereMiseAJour = NOW()
WHERE Id = ?
`, [
compteur.total,
compteur.solde,
compteur.soldeReporte || 0,
existing[0].Id
]);
updated++;
} else {
// Créer
await conn.query(`
INSERT INTO CompteurConges
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour)
VALUES (?, ?, ?, ?, ?, ?, NOW())
`, [
compteur.collaborateurId,
typeCongeId,
compteur.annee,
compteur.total,
compteur.solde,
compteur.soldeReporte || 0
]);
created++;
}
}
await conn.commit();
conn.release();
console.log(`✅ Synchronisation terminée: ${updated} mis à jour, ${created} créés`);
res.json({
success: true,
message: 'Synchronisation réussie',
stats: {
total: rhCompteurs.length,
updated: updated,
created: created
}
});
} catch (fetchError) {
console.error('❌ Erreur communication avec serveur RH:', fetchError.message);
res.status(500).json({
success: false,
message: 'Impossible de contacter le serveur RH',
error: fetchError.message
});
}
} catch (error) {
console.error('❌ Erreur synchronisation:', error);
res.status(500).json({
success: false,
message: 'Erreur serveur',
error: error.message
});
}
});
function getDateFinMoisPrecedent(referenceDate = new Date()) {
const now = new Date(referenceDate);
now.setHours(0, 0, 0, 0);
return new Date(now.getFullYear(), now.getMonth(), 0);
}
function parseDateYYYYMMDD(s) {
if (!s) return null;
if (/^\d{4}-\d{2}-\d{2}$/.test(s)) {
const [y, m, d] = s.split('-').map(Number);
return new Date(y, m - 1, d);
}
return new Date(s);
}
const LEAVE_RULES = {
CP: {
nom: 'Congé payé',
joursAnnuels: 25,
periodeDebut: { mois: 6, jour: 1 },
periodeFin: { mois: 5, jour: 31 },
acquisitionMensuelle: 25 / 12,
reportable: true,
periodeReport: 'exercice'
},
RTT: {
nom: 'RTT',
joursAnnuels: 10,
periodeDebut: { mois: 1, jour: 1 },
periodeFin: { mois: 12, jour: 31 },
acquisitionMensuelle: 10 / 12,
reportable: false,
periodeReport: null
}
};
// ========================================
// NOUVELLES FONCTIONS POUR RTT VARIABLES
// ========================================
/**
* Récupère la configuration RTT pour une année et un type de contrat donnés
* RÈGLES :
* - 37h : toujours 10 RTT/an (0.8333/mois)
* - Forfait jour 2025 : 10 RTT/an (0.8333/mois)
* - Forfait jour 2026+ : 12 RTT/an (1.0/mois)
*/
async function getConfigurationRTT(conn, annee, typeContrat = '37h') {
try {
// D'abord chercher en base de données
const [config] = await conn.query(
`SELECT JoursAnnuels, AcquisitionMensuelle
FROM ConfigurationRTT
WHERE Annee = ? AND TypeContrat = ?
LIMIT 1`,
[annee, typeContrat]
);
if (config.length > 0) {
return {
joursAnnuels: parseFloat(config[0].JoursAnnuels),
acquisitionMensuelle: parseFloat(config[0].AcquisitionMensuelle)
};
}
// Si pas en base, utiliser les règles par défaut
console.warn(`⚠️ Pas de config RTT en base pour ${annee}/${typeContrat}, utilisation des règles par défaut`);
return getConfigurationRTTDefaut(annee, typeContrat);
} catch (error) {
console.error('Erreur getConfigurationRTT:', error);
return getConfigurationRTTDefaut(annee, typeContrat);
}
}
function getConfigurationRTTDefaut(annee, typeContrat) {
// 37h : toujours 10 RTT/an
if (typeContrat === '37h' || typeContrat === 'temps_partiel') {
return {
joursAnnuels: 10,
acquisitionMensuelle: 10 / 12 // 0.8333
};
}
// Forfait jour : dépend de l'année
if (typeContrat === 'forfait_jour') {
if (annee <= 2025) {
// 2025 et avant : 10 RTT/an
return {
joursAnnuels: 10,
acquisitionMensuelle: 10 / 12 // 0.8333
};
} else {
// 2026 et après : 12 RTT/an
return {
joursAnnuels: 12,
acquisitionMensuelle: 12 / 12 // 1.0
};
}
}
// Par défaut : 10 RTT/an
return {
joursAnnuels: 10,
acquisitionMensuelle: 10 / 12
};
}
/**
* Calcule l'acquisition RTT avec la formule Excel exacte
*/
async function calculerAcquisitionRTT(conn, collaborateurId, dateReference = new Date()) {
const d = new Date(dateReference);
d.setHours(0, 0, 0, 0);
const annee = d.getFullYear();
// 1⃣ Récupérer les infos du collaborateur
const [collabInfo] = await conn.query(
`SELECT TypeContrat, DateEntree, role FROM CollaborateurAD WHERE id = ?`,
[collaborateurId]
);
if (collabInfo.length === 0) {
throw new Error(`Collaborateur ${collaborateurId} non trouvé`);
}
const typeContrat = collabInfo[0].TypeContrat || '37h';
const dateEntree = collabInfo[0].DateEntree;
const isApprenti = collabInfo[0].role === 'Apprenti';
// 2⃣ Apprentis = pas de RTT
if (isApprenti) {
return {
acquisition: 0,
moisTravailles: 0,
config: { joursAnnuels: 0, acquisitionMensuelle: 0 },
typeContrat: typeContrat
};
}
// 3⃣ Récupérer la configuration RTT (avec règles 2025/2026)
const config = await getConfigurationRTT(conn, annee, typeContrat);
console.log(`📊 Config RTT ${annee}/${typeContrat}: ${config.joursAnnuels}j/an (${config.acquisitionMensuelle.toFixed(4)}/mois)`);
// 4⃣ Début d'acquisition = 01/01/N ou date d'entrée si postérieure
let dateDebutAcquis = new Date(annee, 0, 1); // 01/01/N
dateDebutAcquis.setHours(0, 0, 0, 0);
if (dateEntree) {
const entree = new Date(dateEntree);
entree.setHours(0, 0, 0, 0);
if (entree.getFullYear() === annee && entree > dateDebutAcquis) {
dateDebutAcquis = entree;
}
if (entree.getFullYear() > annee) {
return {
acquisition: 0,
moisTravailles: 0,
config: config,
typeContrat: typeContrat
};
}
}
// 5⃣ Calculer avec la formule Excel
const acquisition = calculerAcquisitionFormuleExcel(dateDebutAcquis, d, config.acquisitionMensuelle);
// 6⃣ Calculer les mois travaillés (pour info)
const moisTravailles = config.acquisitionMensuelle > 0
? acquisition / config.acquisitionMensuelle
: 0;
// 7⃣ Plafonner au maximum annuel
const acquisitionFinale = Math.min(acquisition, config.joursAnnuels);
return {
acquisition: Math.round(acquisitionFinale * 100) / 100,
moisTravailles: Math.round(moisTravailles * 100) / 100,
config: config,
typeContrat: typeContrat
};
}
/**
* Calcule l'acquisition avec la formule Excel exacte :
* E1 * ((JOUR(FIN.MOIS(B1;0)) - JOUR(B1) + 1) / JOUR(FIN.MOIS(B1;0))
* + DATEDIF(B1;B2;"m") - 1
* + JOUR(B2) / JOUR(FIN.MOIS(B2;0)))
*/
function calculerAcquisitionFormuleExcel(dateDebut, dateReference, coeffMensuel) {
const b1 = new Date(dateDebut);
const b2 = new Date(dateReference);
b1.setHours(0, 0, 0, 0);
b2.setHours(0, 0, 0, 0);
// Si date référence avant date début
if (b2 < b1) {
return 0;
}
// Si même mois et même année
if (b1.getFullYear() === b2.getFullYear() && b1.getMonth() === b2.getMonth()) {
const joursTotal = new Date(b2.getFullYear(), b2.getMonth() + 1, 0).getDate();
const joursAcquis = b2.getDate() - b1.getDate() + 1;
return Math.round((joursAcquis / joursTotal) * coeffMensuel * 100) / 100;
}
// 1⃣ Fraction du PREMIER mois
const joursFinMoisB1 = new Date(b1.getFullYear(), b1.getMonth() + 1, 0).getDate();
const jourB1 = b1.getDate();
const fractionPremierMois = (joursFinMoisB1 - jourB1 + 1) / joursFinMoisB1;
// 2⃣ Mois COMPLETS entre
const moisComplets = dateDifMonths(b1, b2) - 1;
// 3⃣ Fraction du DERNIER mois
const joursFinMoisB2 = new Date(b2.getFullYear(), b2.getMonth() + 1, 0).getDate();
const jourB2 = b2.getDate();
const fractionDernierMois = jourB2 / joursFinMoisB2;
// 4⃣ Total
const totalMois = fractionPremierMois + Math.max(0, moisComplets) + fractionDernierMois;
const acquisition = totalMois * coeffMensuel;
return Math.round(acquisition * 100) / 100;
}
/**
* Équivalent de DATEDIF(date1, date2, "m") en JavaScript
*/
function dateDifMonths(date1, date2) {
const d1 = new Date(date1);
const d2 = new Date(date2);
let months = (d2.getFullYear() - d1.getFullYear()) * 12;
months += d2.getMonth() - d1.getMonth();
// Si le jour de d2 < jour de d1, on n'a pas encore complété le mois
if (d2.getDate() < d1.getDate()) {
months--;
}
return Math.max(0, months);
}
/**
* Calcule l'acquisition CP avec la formule Excel exacte
*/
function calculerAcquisitionCP(dateReference = new Date(), dateEntree = null) {
const d = new Date(dateReference);
d.setHours(0, 0, 0, 0);
const annee = d.getFullYear();
const mois = d.getMonth() + 1;
// 1⃣ Déterminer le début de l'exercice CP (01/06)
let exerciceDebut;
if (mois >= 6) {
exerciceDebut = new Date(annee, 5, 1); // 01/06/N
} else {
exerciceDebut = new Date(annee - 1, 5, 1); // 01/06/N-1
}
exerciceDebut.setHours(0, 0, 0, 0);
// 2⃣ Ajuster si date d'entrée postérieure
let dateDebutAcquis = new Date(exerciceDebut);
if (dateEntree) {
const entree = new Date(dateEntree);
entree.setHours(0, 0, 0, 0);
if (entree > exerciceDebut) {
dateDebutAcquis = entree;
}
}
// 3⃣ Calculer avec la formule Excel
const coeffCP = 25 / 12; // 2.0833
const acquisition = calculerAcquisitionFormuleExcel(dateDebutAcquis, d, coeffCP);
// 4⃣ Plafonner à 25 jours
return Math.min(acquisition, 25);
}
// ========================================
// TÂCHES CRON
// ========================================
cron.schedule('0 2 * * *', async () => {
console.log('🔄 [CRON] Mise à jour quotidienne des compteurs...');
try {
const conn = await pool.getConnection();
await conn.beginTransaction();
const [collaborateurs] = await conn.query(`
SELECT id, prenom, nom, CampusId
FROM CollaborateurAD
WHERE (actif = 1 OR actif IS NULL)
`);
let successCount = 0;
const today = new Date();
for (const collab of collaborateurs) {
try {
await updateMonthlyCounters(conn, collab.id, today);
successCount++;
} catch (error) {
console.error(`❌ Erreur pour ${collab.prenom} ${collab.nom}:`, error.message);
}
}
await conn.commit();
console.log(`✅ [CRON] ${successCount}/${collaborateurs.length} compteurs mis à jour`);
conn.release();
} catch (error) {
console.error('❌ [CRON] Erreur mise à jour quotidienne:', error);
}
});
cron.schedule('59 23 31 12 *', async () => {
console.log('🎆 [CRON] Traitement fin d\'année RTT...');
try {
const conn = await pool.getConnection();
await conn.beginTransaction();
const [collaborateurs] = await conn.query('SELECT id, CampusId FROM CollaborateurAD');
let successCount = 0;
for (const collab of collaborateurs) {
try {
await processEndOfYearRTT(conn, collab.id);
successCount++;
} catch (error) {
console.error(`❌ Erreur RTT pour ${collab.id}:`, error.message);
}
}
await conn.commit();
console.log(`✅ [CRON] ${successCount}/${collaborateurs.length} RTT réinitialisés`);
conn.release();
} catch (error) {
console.error('❌ [CRON] Erreur traitement fin d\'année:', error);
}
});
cron.schedule('59 23 31 5 *', async () => {
console.log('📅 [CRON] Traitement fin d\'exercice CP...');
try {
const conn = await pool.getConnection();
await conn.beginTransaction();
const [collaborateurs] = await conn.query('SELECT id, CampusId FROM CollaborateurAD');
let successCount = 0;
for (const collab of collaborateurs) {
try {
await processEndOfExerciceCP(conn, collab.id);
successCount++;
} catch (error) {
console.error(`❌ Erreur CP pour ${collab.id}:`, error.message);
}
}
await conn.commit();
console.log(`✅ [CRON] ${successCount}/${collaborateurs.length} CP reportés`);
conn.release();
} catch (error) {
console.error('❌ [CRON] Erreur traitement fin d\'exercice:', error);
}
});
// ========================================
// CRON : CRÉER ARRÊTÉS MENSUELS AUTOMATIQUEMENT
// ========================================
cron.schedule('55 23 28-31 * *', async () => {
const today = new Date();
const lastDay = new Date(today.getFullYear(), today.getMonth() + 1, 0);
// ⭐ Vérifier qu'on est bien le dernier jour du mois
if (today.getDate() === lastDay.getDate()) {
console.log(`📅 [CRON] Création arrêté fin de mois: ${today.toISOString().split('T')[0]}`);
const conn = await pool.getConnection();
try {
await conn.beginTransaction();
const annee = today.getFullYear();
const mois = today.getMonth() + 1; // 1-12
const dateArrete = today.toISOString().split('T')[0];
// ⭐ Vérifier si l'arrêté n'existe pas déjà
const [existing] = await conn.query(
'SELECT Id FROM ArreteComptable WHERE Annee = ? AND Mois = ?',
[annee, mois]
);
if (existing.length > 0) {
console.log(`⚠️ [CRON] Arrêté ${mois}/${annee} existe déjà, skip`);
await conn.rollback();
conn.release();
return;
}
// ⭐ Créer l'arrêté
const [result] = await conn.query(`
INSERT INTO ArreteComptable
(DateArrete, Annee, Mois, Libelle, Description, Statut, DateCreation)
VALUES (?, ?, ?, ?, ?, 'En cours', NOW())
`, [
dateArrete,
annee,
mois,
`Arrêté comptable ${getMonthName(mois)} ${annee}`,
`Arrêté mensuel automatique - Clôture des soldes au ${dateArrete}`
]);
const arreteId = result.insertId;
console.log(`✅ [CRON] Arrêté créé: ID ${arreteId}`);
// ⭐ Créer le snapshot
await conn.query('CALL sp_creer_snapshot_arrete(?)', [arreteId]);
console.log(`📸 [CRON] Snapshot créé pour l'arrêté ${arreteId}`);
// ⭐ Compter les soldes figés
const [count] = await conn.query(
'SELECT COUNT(*) as total FROM SoldesFiges WHERE ArreteId = ?',
[arreteId]
);
await conn.commit();
console.log(`🎉 [CRON] Arrêté ${mois}/${annee} terminé: ${count[0].total} soldes figés`);
} catch (error) {
await conn.rollback();
console.error(`❌ [CRON] Erreur création arrêté:`, error.message);
} finally {
conn.release();
}
}
});
// Mail mensuel le 1er à 9h
cron.schedule('0 9 1 * *', async () => {
console.log('📧 Envoi mails compte-rendu mensuel...');
const conn = await pool.getConnection();
const [cadres] = await conn.query(`
SELECT id, email, prenom, nom
FROM CollaborateurAD
WHERE TypeContrat = 'forfait_jour' AND (actif = 1 OR actif IS NULL)
`);
const moisPrecedent = new Date();
moisPrecedent.setMonth(moisPrecedent.getMonth() - 1);
for (const cadre of cadres) {
// Envoyer mail via Microsoft Graph API
// Enregistrer dans MailsCompteRendu
}
conn.release();
});
// Relance hebdomadaire le lundi à 9h
cron.schedule('0 9 * * 1', async () => {
console.log('🔔 Relance hebdomadaire compte-rendu...');
const conn = await pool.getConnection();
const moisCourant = new Date().getMonth() + 1;
const anneeCourante = new Date().getFullYear();
const [nonValides] = await conn.query(`
SELECT DISTINCT ca.id, ca.email, ca.prenom, ca.nom
FROM CollaborateurAD ca
LEFT JOIN CompteRenduMensuel crm ON ca.id = crm.CollaborateurADId
AND crm.Annee = ? AND crm.Mois = ?
WHERE ca.TypeContrat = 'forfait_jour'
AND (crm.Statut IS NULL OR crm.Statut != 'Validé')
`, [anneeCourante, moisCourant - 1]);
for (const cadre of nonValides) {
// Envoyer relance
}
conn.release();
});
// ⭐ Fonction helper pour les noms de mois
function getMonthName(mois) {
const mois_names = ['', 'Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin',
'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'];
return mois_names[mois] || mois;
}
async function getGraphToken() {
try {
const params = new URLSearchParams({
grant_type: 'client_credentials',
client_id: AZURE_CONFIG.clientId,
client_secret: AZURE_CONFIG.clientSecret,
scope: 'https://graph.microsoft.com/.default'
});
const response = await axios.post(
`https://login.microsoftonline.com/${AZURE_CONFIG.tenantId}/oauth2/v2.0/token`,
params.toString(),
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
);
return response.data.access_token;
} catch (error) {
console.error('Erreur obtention token:', error);
return null;
}
}
async function sendMailGraph(accessToken, fromEmail, toEmail, subject, bodyHtml) {
try {
await axios.post(
`https://graph.microsoft.com/v1.0/users/${fromEmail}/sendMail`,
{
message: {
subject,
body: { contentType: 'HTML', content: bodyHtml },
toRecipients: [{ emailAddress: { address: toEmail } }]
},
saveToSentItems: false
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json'
}
}
);
return true;
} catch (error) {
console.error('Erreur envoi email:', error);
return false;
}
}
function getWorkingDays(startDate, endDate) {
let workingDays = 0;
const current = new Date(startDate);
const end = new Date(endDate);
while (current <= end) {
const dayOfWeek = current.getDay();
if (dayOfWeek !== 0 && dayOfWeek !== 6) {
workingDays++;
}
current.setDate(current.getDate() + 1);
}
return workingDays;
}
function formatDate(date) {
const d = new Date(date);
const day = String(d.getDate()).padStart(2, '0');
const month = String(d.getMonth() + 1).padStart(2, '0');
const year = d.getFullYear();
return `${day}/${month}/${year}`;
}
function getExerciceCP(date = new Date()) {
const d = new Date(date);
const annee = d.getFullYear();
const mois = d.getMonth() + 1;
if (mois >= 1 && mois <= 5) {
return `${annee - 1}-${annee}`;
}
return `${annee}-${annee + 1}`;
}
function getMoisTravaillesCP(date = new Date(), dateEntree = null) {
const d = new Date(date);
d.setHours(0, 0, 0, 0);
const annee = d.getFullYear();
const mois = d.getMonth() + 1;
let debutExercice;
if (mois >= 6) {
debutExercice = new Date(annee, 5, 1);
} else {
debutExercice = new Date(annee - 1, 5, 1);
}
debutExercice.setHours(0, 0, 0, 0);
if (dateEntree) {
const entree = new Date(dateEntree);
entree.setHours(0, 0, 0, 0);
if (entree > debutExercice) {
debutExercice = entree;
}
}
// ✅ Calculer jusqu'à aujourd'hui
const diffMs = d - debutExercice;
const diffJours = Math.floor(diffMs / (1000 * 60 * 60 * 24)) + 1;
const moisTravailles = diffJours / 30.44;
return Math.max(0, Math.min(12, moisTravailles));
}
function getMoisTravaillesRTT(date = new Date(), dateEntree = null) {
const d = new Date(date);
d.setHours(0, 0, 0, 0);
const annee = d.getFullYear();
let debutAnnee = new Date(annee, 0, 1);
debutAnnee.setHours(0, 0, 0, 0);
if (dateEntree) {
const entree = new Date(dateEntree);
entree.setHours(0, 0, 0, 0);
if (entree.getFullYear() === annee && entree > debutAnnee) {
debutAnnee = entree;
} else if (entree.getFullYear() > annee) {
return 0;
}
}
const diffMs = d - debutAnnee;
const diffJours = Math.floor(diffMs / (1000 * 60 * 60 * 24)) + 1;
const moisTravailles = diffJours / 30.44;
return Math.max(0, Math.min(12, moisTravailles));
}
function calculerAcquisitionCumulee(typeConge, dateReference = new Date(), dateEntree = null) {
const rules = LEAVE_RULES[typeConge];
if (!rules) return 0;
let moisTravailles;
if (typeConge === 'CP') {
moisTravailles = getMoisTravaillesCP(dateReference, dateEntree);
} else {
moisTravailles = getMoisTravaillesRTT(dateReference, dateEntree);
}
const acquisition = moisTravailles * rules.acquisitionMensuelle;
return Math.round(acquisition * 100) / 100;
}
async function processEndOfYearRTT(conn, collaborateurId) {
const currentYear = new Date().getFullYear();
const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']);
if (rttType.length === 0) return null;
await conn.query(
`UPDATE CompteurConges SET Solde = 0, Total = 0, SoldeReporte = 0, DerniereMiseAJour = NOW() WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
[collaborateurId, rttType[0].Id, currentYear]
);
return { type: 'RTT', action: 'reset_end_of_year', annee: currentYear };
}
async function processEndOfExerciceCP(conn, collaborateurId) {
const today = new Date();
const currentYear = today.getFullYear();
const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']);
if (cpType.length === 0) return null;
const cpTypeId = cpType[0].Id;
const [currentCounter] = await conn.query(
`SELECT Id, Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
[collaborateurId, cpTypeId, currentYear]
);
if (currentCounter.length === 0) return null;
const soldeAReporter = parseFloat(currentCounter[0].Solde);
const [nextYearCounter] = await conn.query(
`SELECT Id FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
[collaborateurId, cpTypeId, currentYear + 1]
);
if (nextYearCounter.length > 0) {
await conn.query(
`UPDATE CompteurConges SET SoldeReporte = ?, Solde = Solde + ?, DerniereMiseAJour = NOW() WHERE Id = ?`,
[soldeAReporter, soldeAReporter, nextYearCounter[0].Id]
);
} else {
await conn.query(
`INSERT INTO CompteurConges (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour) VALUES (?, ?, ?, 0, ?, ?, NOW())`,
[collaborateurId, cpTypeId, currentYear + 1, soldeAReporter, soldeAReporter]
);
}
return { type: 'CP', action: 'report_exercice', soldeReporte: soldeAReporter };
}
async function deductLeaveBalance(conn, collaborateurId, typeCongeId, nombreJours) {
const currentYear = new Date().getFullYear();
const previousYear = currentYear - 1;
let joursRestants = nombreJours;
const deductions = [];
const [compteurN1] = await conn.query(
`SELECT Id, Solde, SoldeReporte FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
[collaborateurId, typeCongeId, previousYear]
);
if (compteurN1.length > 0 && compteurN1[0].SoldeReporte > 0) {
const soldeN1 = parseFloat(compteurN1[0].SoldeReporte);
const aDeduireN1 = Math.min(soldeN1, joursRestants);
if (aDeduireN1 > 0) {
await conn.query(
`UPDATE CompteurConges SET SoldeReporte = GREATEST(0, SoldeReporte - ?), Solde = GREATEST(0, Solde - ?) WHERE Id = ?`,
[aDeduireN1, aDeduireN1, compteurN1[0].Id]
);
deductions.push({ annee: previousYear, type: 'Reporté N-1', joursUtilises: aDeduireN1, soldeAvant: soldeN1 });
joursRestants -= aDeduireN1;
}
}
if (joursRestants > 0) {
const [compteurN] = await conn.query(
`SELECT Id, Solde, SoldeReporte FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
[collaborateurId, typeCongeId, currentYear]
);
if (compteurN.length > 0) {
const soldeN = parseFloat(compteurN[0].Solde) - parseFloat(compteurN[0].SoldeReporte || 0);
const aDeduireN = Math.min(soldeN, joursRestants);
if (aDeduireN > 0) {
await conn.query(
`UPDATE CompteurConges SET Solde = GREATEST(0, Solde - ?) WHERE Id = ?`,
[aDeduireN, compteurN[0].Id]
);
deductions.push({ annee: currentYear, type: 'Année actuelle N', joursUtilises: aDeduireN, soldeAvant: soldeN });
joursRestants -= aDeduireN;
}
}
}
return { success: joursRestants === 0, joursDeduitsTotal: nombreJours - joursRestants, joursNonDeduits: joursRestants, details: deductions };
}
async function checkLeaveBalance(conn, collaborateurId, repartition) {
const currentYear = new Date().getFullYear();
const previousYear = currentYear - 1;
const verification = [];
for (const rep of repartition) {
const typeCode = rep.TypeConge;
const joursNecessaires = parseFloat(rep.NombreJours);
if (typeCode === 'ABS' || typeCode === 'Formation') continue;
const typeName = typeCode === 'CP' ? 'Congé payé' : typeCode === 'RTT' ? 'RTT' : typeCode;
const [typeRow] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', [typeName]);
if (typeRow.length === 0) continue;
const typeCongeId = typeRow[0].Id;
const [compteurN1] = await conn.query(
`SELECT SoldeReporte FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
[collaborateurId, typeCongeId, previousYear]
);
const [compteurN] = await conn.query(
`SELECT Solde, SoldeReporte FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
[collaborateurId, typeCongeId, currentYear]
);
const soldeN1 = compteurN1.length > 0 ? parseFloat(compteurN1[0].SoldeReporte || 0) : 0;
const soldeN = compteurN.length > 0 ? parseFloat(compteurN[0].Solde || 0) - parseFloat(compteurN[0].SoldeReporte || 0) : 0;
const soldeTotal = soldeN1 + soldeN;
verification.push({ type: typeName, joursNecessaires, soldeN1, soldeN, soldeTotal, suffisant: soldeTotal >= joursNecessaires, deficit: Math.max(0, joursNecessaires - soldeTotal) });
}
const insuffisants = verification.filter(v => !v.suffisant);
return { valide: insuffisants.length === 0, details: verification, insuffisants };
}
// ========================================
// MISE À JOUR DE updateMonthlyCounters
// ========================================
async function updateMonthlyCounters(conn, collaborateurId, dateReference = null) {
const today = dateReference ? new Date(dateReference) : getDateFinMoisPrecedent();
const currentYear = today.getFullYear();
const updates = [];
// Récupérer les infos du collaborateur
const [collabInfo] = await conn.query(`
SELECT DateEntree, TypeContrat, CampusId, role
FROM CollaborateurAD WHERE id = ?
`, [collaborateurId]);
if (collabInfo.length === 0) {
throw new Error(`Collaborateur ${collaborateurId} non trouvé`);
}
const dateEntree = collabInfo[0].DateEntree || null;
const typeContrat = collabInfo[0].TypeContrat || '37h';
const isApprenti = collabInfo[0].role === 'Apprenti';
console.log(`\n📊 === Mise à jour compteurs pour collaborateur ${collaborateurId} ===`);
console.log(` Date référence: ${today.toLocaleDateString('fr-FR')}`);
// ======================================
// CP (Congés Payés)
// ======================================
const exerciceCP = getExerciceCP(today);
const acquisitionCP = calculerAcquisitionCP(today, dateEntree);
const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']);
if (cpType.length > 0) {
const cpTypeId = cpType[0].Id;
// 1⃣ Récupérer le compteur existant
const [existingCP] = await conn.query(`
SELECT Id, Total, Solde, SoldeReporte
FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
`, [collaborateurId, cpTypeId, currentYear]);
if (existingCP.length > 0) {
const ancienTotal = parseFloat(existingCP[0].Total || 0);
const ancienSolde = parseFloat(existingCP[0].Solde || 0);
const soldeReporte = parseFloat(existingCP[0].SoldeReporte || 0);
console.log(` CP - Ancien acquis: ${ancienTotal.toFixed(2)}j`);
console.log(` CP - Nouvel acquis: ${acquisitionCP.toFixed(2)}j`);
// 2⃣ Calculer l'incrément d'acquisition (nouveaux jours acquis ce mois)
const incrementAcquis = acquisitionCP - ancienTotal;
if (incrementAcquis > 0) {
console.log(` CP - Nouveaux jours ce mois: +${incrementAcquis.toFixed(2)}j`);
// 3⃣ Vérifier si le collaborateur a de l'anticipé utilisé
const [anticipeUtilise] = await conn.query(`
SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalAnticipe,
MIN(dd.Id) as firstDeductionId,
MIN(dd.DemandeCongeId) as demandeId
FROM DeductionDetails dd
JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
WHERE dc.CollaborateurADId = ?
AND dd.TypeCongeId = ?
AND dd.Annee = ?
AND dd.TypeDeduction = 'N Anticip'
AND dc.Statut != 'Refusée'
AND dd.JoursUtilises > 0
`, [collaborateurId, cpTypeId, currentYear]);
const anticipePris = parseFloat(anticipeUtilise[0]?.totalAnticipe || 0);
if (anticipePris > 0) {
// 4⃣ Calculer le montant à rembourser
const aRembourser = Math.min(incrementAcquis, anticipePris);
console.log(` 💳 CP - Anticipé à rembourser: ${aRembourser.toFixed(2)}j (sur ${anticipePris.toFixed(2)}j)`);
// 5⃣ Rembourser l'anticipé en transférant vers "Année N"
// On récupère toutes les déductions anticipées pour ce type
const [deductionsAnticipees] = await conn.query(`
SELECT dd.Id, dd.DemandeCongeId, dd.JoursUtilises
FROM DeductionDetails dd
JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
WHERE dc.CollaborateurADId = ?
AND dd.TypeCongeId = ?
AND dd.Annee = ?
AND dd.TypeDeduction = 'N Anticip'
AND dc.Statut != 'Refusée'
AND dd.JoursUtilises > 0
ORDER BY dd.Id ASC
`, [collaborateurId, cpTypeId, currentYear]);
let resteARembourser = aRembourser;
for (const deduction of deductionsAnticipees) {
if (resteARembourser <= 0) break;
const joursAnticipes = parseFloat(deduction.JoursUtilises);
const aDeduiteDeCetteDeduction = Math.min(resteARembourser, joursAnticipes);
// Réduire l'anticipé
await conn.query(`
UPDATE DeductionDetails
SET JoursUtilises = GREATEST(0, JoursUtilises - ?)
WHERE Id = ?
`, [aDeduiteDeCetteDeduction, deduction.Id]);
// Vérifier si une déduction "Année N" existe déjà pour cette demande
const [existingAnneeN] = await conn.query(`
SELECT Id, JoursUtilises
FROM DeductionDetails
WHERE DemandeCongeId = ?
AND TypeCongeId = ?
AND Annee = ?
AND TypeDeduction IN ('Année N', 'Anne N', 'Anne actuelle N')
`, [deduction.DemandeCongeId, cpTypeId, currentYear]);
if (existingAnneeN.length > 0) {
// Augmenter la déduction "Année N" existante
await conn.query(`
UPDATE DeductionDetails
SET JoursUtilises = JoursUtilises + ?
WHERE Id = ?
`, [aDeduiteDeCetteDeduction, existingAnneeN[0].Id]);
} else {
// Créer une nouvelle déduction "Année N"
await conn.query(`
INSERT INTO DeductionDetails
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
VALUES (?, ?, ?, 'Année N', ?)
`, [deduction.DemandeCongeId, cpTypeId, currentYear, aDeduiteDeCetteDeduction]);
}
resteARembourser -= aDeduiteDeCetteDeduction;
console.log(` ✅ CP - Remboursé ${aDeduiteDeCetteDeduction.toFixed(2)}j (Demande ${deduction.DemandeCongeId})`);
}
// Supprimer les déductions anticipées à zéro
await conn.query(`
DELETE FROM DeductionDetails
WHERE TypeCongeId = ?
AND Annee = ?
AND TypeDeduction = 'N Anticip'
AND JoursUtilises <= 0
`, [cpTypeId, currentYear]);
}
}
// 6⃣ Recalculer le solde total (acquis + report - consommé)
const [consomme] = await conn.query(`
SELECT COALESCE(SUM(dd.JoursUtilises), 0) as total
FROM DeductionDetails dd
JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
WHERE dc.CollaborateurADId = ?
AND dd.TypeCongeId = ?
AND dd.Annee = ?
AND dd.TypeDeduction NOT IN ('Accum Récup', 'Accum Recup')
AND dc.Statut != 'Refusée'
`, [collaborateurId, cpTypeId, currentYear]);
const totalConsomme = parseFloat(consomme[0].total || 0);
const nouveauSolde = Math.max(0, acquisitionCP + soldeReporte - totalConsomme);
console.log(` CP - Consommé total: ${totalConsomme.toFixed(2)}j`);
console.log(` CP - Nouveau solde: ${nouveauSolde.toFixed(2)}j`);
// 7⃣ Mettre à jour le compteur
await conn.query(`
UPDATE CompteurConges
SET Total = ?, Solde = ?, DerniereMiseAJour = NOW()
WHERE Id = ?
`, [acquisitionCP, nouveauSolde, existingCP[0].Id]);
updates.push({
type: 'CP',
exercice: exerciceCP,
acquisitionCumulee: acquisitionCP,
increment: incrementAcquis,
nouveauSolde: nouveauSolde
});
} else {
// Créer le compteur s'il n'existe pas
await conn.query(`
INSERT INTO CompteurConges
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour)
VALUES (?, ?, ?, ?, ?, 0, NOW())
`, [collaborateurId, cpTypeId, currentYear, acquisitionCP, acquisitionCP]);
console.log(` CP - Compteur créé: ${acquisitionCP.toFixed(2)}j`);
updates.push({
type: 'CP',
exercice: exerciceCP,
acquisitionCumulee: acquisitionCP,
action: 'created',
nouveauSolde: acquisitionCP
});
}
}
// ======================================
// RTT
// ======================================
if (!isApprenti) {
const rttData = await calculerAcquisitionRTT(conn, collaborateurId, today);
const acquisitionRTT = rttData.acquisition;
const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']);
if (rttType.length > 0) {
const rttTypeId = rttType[0].Id;
// 1⃣ Récupérer le compteur existant
const [existingRTT] = await conn.query(`
SELECT Id, Total, Solde
FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
`, [collaborateurId, rttTypeId, currentYear]);
if (existingRTT.length > 0) {
const ancienTotal = parseFloat(existingRTT[0].Total || 0);
const ancienSolde = parseFloat(existingRTT[0].Solde || 0);
console.log(` RTT - Ancien acquis: ${ancienTotal.toFixed(2)}j`);
console.log(` RTT - Nouvel acquis: ${acquisitionRTT.toFixed(2)}j`);
// 2⃣ Calculer l'incrément d'acquisition
const incrementAcquis = acquisitionRTT - ancienTotal;
if (incrementAcquis > 0) {
console.log(` RTT - Nouveaux jours ce mois: +${incrementAcquis.toFixed(2)}j`);
// 3⃣ Vérifier si le collaborateur a de l'anticipé utilisé
const [anticipeUtilise] = await conn.query(`
SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalAnticipe
FROM DeductionDetails dd
JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
WHERE dc.CollaborateurADId = ?
AND dd.TypeCongeId = ?
AND dd.Annee = ?
AND dd.TypeDeduction = 'N Anticip'
AND dc.Statut != 'Refusée'
AND dd.JoursUtilises > 0
`, [collaborateurId, rttTypeId, currentYear]);
const anticipePris = parseFloat(anticipeUtilise[0]?.totalAnticipe || 0);
if (anticipePris > 0) {
// 4⃣ Calculer le montant à rembourser
const aRembourser = Math.min(incrementAcquis, anticipePris);
console.log(` 💳 RTT - Anticipé à rembourser: ${aRembourser.toFixed(2)}j (sur ${anticipePris.toFixed(2)}j)`);
// 5⃣ Rembourser l'anticipé
const [deductionsAnticipees] = await conn.query(`
SELECT dd.Id, dd.DemandeCongeId, dd.JoursUtilises
FROM DeductionDetails dd
JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
WHERE dc.CollaborateurADId = ?
AND dd.TypeCongeId = ?
AND dd.Annee = ?
AND dd.TypeDeduction = 'N Anticip'
AND dc.Statut != 'Refusée'
AND dd.JoursUtilises > 0
ORDER BY dd.Id ASC
`, [collaborateurId, rttTypeId, currentYear]);
let resteARembourser = aRembourser;
for (const deduction of deductionsAnticipees) {
if (resteARembourser <= 0) break;
const joursAnticipes = parseFloat(deduction.JoursUtilises);
const aDeduiteDeCetteDeduction = Math.min(resteARembourser, joursAnticipes);
// Réduire l'anticipé
await conn.query(`
UPDATE DeductionDetails
SET JoursUtilises = GREATEST(0, JoursUtilises - ?)
WHERE Id = ?
`, [aDeduiteDeCetteDeduction, deduction.Id]);
// Vérifier si une déduction "Année N" existe déjà
const [existingAnneeN] = await conn.query(`
SELECT Id, JoursUtilises
FROM DeductionDetails
WHERE DemandeCongeId = ?
AND TypeCongeId = ?
AND Annee = ?
AND TypeDeduction IN ('Année N', 'Anne N')
`, [deduction.DemandeCongeId, rttTypeId, currentYear]);
if (existingAnneeN.length > 0) {
await conn.query(`
UPDATE DeductionDetails
SET JoursUtilises = JoursUtilises + ?
WHERE Id = ?
`, [aDeduiteDeCetteDeduction, existingAnneeN[0].Id]);
} else {
await conn.query(`
INSERT INTO DeductionDetails
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
VALUES (?, ?, ?, 'Année N', ?)
`, [deduction.DemandeCongeId, rttTypeId, currentYear, aDeduiteDeCetteDeduction]);
}
resteARembourser -= aDeduiteDeCetteDeduction;
console.log(` ✅ RTT - Remboursé ${aDeduiteDeCetteDeduction.toFixed(2)}j (Demande ${deduction.DemandeCongeId})`);
}
// Supprimer les déductions anticipées à zéro
await conn.query(`
DELETE FROM DeductionDetails
WHERE TypeCongeId = ?
AND Annee = ?
AND TypeDeduction = 'N Anticip'
AND JoursUtilises <= 0
`, [rttTypeId, currentYear]);
}
}
// 6⃣ Recalculer le solde total
const [consomme] = await conn.query(`
SELECT COALESCE(SUM(dd.JoursUtilises), 0) as total
FROM DeductionDetails dd
JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
WHERE dc.CollaborateurADId = ?
AND dd.TypeCongeId = ?
AND dd.Annee = ?
AND dd.TypeDeduction NOT IN ('Accum Récup', 'Accum Recup', 'Récup Dosée')
AND dc.Statut != 'Refusée'
`, [collaborateurId, rttTypeId, currentYear]);
const totalConsomme = parseFloat(consomme[0].total || 0);
const nouveauSolde = Math.max(0, acquisitionRTT - totalConsomme);
console.log(` RTT - Consommé total: ${totalConsomme.toFixed(2)}j`);
console.log(` RTT - Nouveau solde: ${nouveauSolde.toFixed(2)}j`);
// 7⃣ Mettre à jour le compteur
await conn.query(`
UPDATE CompteurConges
SET Total = ?, Solde = ?, DerniereMiseAJour = NOW()
WHERE Id = ?
`, [acquisitionRTT, nouveauSolde, existingRTT[0].Id]);
updates.push({
type: 'RTT',
annee: currentYear,
typeContrat: rttData.typeContrat,
config: `${rttData.config.joursAnnuels}j/an`,
moisTravailles: rttData.moisTravailles,
acquisitionCumulee: acquisitionRTT,
increment: incrementAcquis,
nouveauSolde: nouveauSolde
});
} else {
// Créer le compteur s'il n'existe pas
await conn.query(`
INSERT INTO CompteurConges
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour)
VALUES (?, ?, ?, ?, ?, 0, NOW())
`, [collaborateurId, rttTypeId, currentYear, acquisitionRTT, acquisitionRTT]);
console.log(` RTT - Compteur créé: ${acquisitionRTT.toFixed(2)}j`);
updates.push({
type: 'RTT',
annee: currentYear,
typeContrat: rttData.typeContrat,
config: `${rttData.config.joursAnnuels}j/an`,
moisTravailles: rttData.moisTravailles,
acquisitionCumulee: acquisitionRTT,
action: 'created',
nouveauSolde: acquisitionRTT
});
}
}
}
console.log(`✅ Mise à jour terminée pour collaborateur ${collaborateurId}\n`);
return updates;
}
// ========================================
// ROUTES API
// ========================================
app.post('/api/login', async (req, res) => {
try {
const { email, mot_de_passe, entraUserId, userPrincipalName } = req.body;
const accessToken = req.headers.authorization?.replace('Bearer ', '');
if (accessToken && entraUserId) {
const [users] = await pool.query(`
SELECT ca.*, s.Nom as service, so.Nom as societe_nom
FROM CollaborateurAD ca
LEFT JOIN Services s ON ca.ServiceId = s.Id
LEFT JOIN Societe so ON ca.SocieteId = so.Id
WHERE ca.entraUserId=? OR ca.email=?
LIMIT 1
`, [entraUserId, email]);
if (users.length === 0) return res.json({ success: false, message: 'Utilisateur non autorisé' });
const user = users[0];
try {
const graphResponse = await axios.get(`https://graph.microsoft.com/v1.0/users/${userPrincipalName}/memberOf?$select=id`, { headers: { Authorization: `Bearer ${accessToken}` } });
const userGroups = graphResponse.data.value.map(g => g.id);
const [allowedGroups] = await pool.query('SELECT Id FROM EntraGroups WHERE IsActive=1');
const allowed = allowedGroups.map(g => g.Id);
const authorized = userGroups.some(g => allowed.includes(g));
if (authorized) {
return res.json({
success: true,
message: 'Connexion réussie via Azure AD',
user: {
id: user.id,
prenom: user.prenom,
nom: user.nom,
email: user.email,
role: user.role,
service: user.service,
societeId: user.SocieteId,
societeNom: user.societe_nom
}
});
} else {
return res.json({ success: false, message: 'Utilisateur non autorisé' });
}
} catch (error) {
return res.json({ success: false, message: 'Erreur vérification groupes' });
}
}
if (email && mot_de_passe) {
const [users] = await pool.query(`
SELECT u.ID, u.Prenom, u.Nom, u.Email, u.Role, u.ServiceId, s.Nom AS ServiceNom
FROM Users u
LEFT JOIN Services s ON u.ServiceId = s.Id
WHERE u.Email = ? AND u.MDP = ?
`, [email, mot_de_passe]);
if (users.length === 1) {
return res.json({
success: true,
message: 'Connexion réussie',
user: {
id: users[0].ID,
prenom: users[0].Prenom,
nom: users[0].Nom,
email: users[0].Email,
role: users[0].Role,
service: users[0].ServiceNom || 'Non défini'
}
});
}
return res.json({ success: false, message: 'Identifiants incorrects' });
}
res.json({ success: false, message: 'Aucune méthode de connexion fournie' });
} catch (error) {
res.status(500).json({ success: false, message: 'Erreur serveur', error: error.message });
}
});
app.post('/api/check-user-groups', async (req, res) => {
try {
const { userPrincipalName } = req.body;
console.log('🔍 Check user groups pour:', userPrincipalName);
if (!userPrincipalName) return res.json({ authorized: false });
// Vérification simple : Si l'user est dans la base, c'est OK
const [users] = await pool.query('SELECT * FROM CollaborateurAD WHERE email = ?', [userPrincipalName]);
if (users.length > 0) {
const u = users[0];
return res.json({
authorized: true,
role: u.role || 'Employe',
groups: [u.role || 'Employe'],
localUserId: u.id,
user: {
id: u.id,
entraUserId: u.entraUserId,
prenom: u.prenom,
nom: u.nom,
email: u.email,
role: u.role
}
});
}
// Si pas trouvé, on autorise quand même pour permettre l'initial-sync juste après
// C'est une astuce pour éviter le blocage "chicken & egg"
return res.json({
authorized: true, // ON FORCE À TRUE POUR DÉBLOQUER
role: 'Nouveau',
groups: ['Nouveau'],
localUserId: null
});
} catch (error) {
console.error('❌ Erreur check-user-groups:', error.message);
res.json({ authorized: false, error: error.message });
}
});
// ========================================
// ✅ CODE CORRIGÉ POUR getDetailedLeaveCounters
// À remplacer dans server.js à partir de la ligne ~1600
// ========================================
app.get('/api/getDetailedLeaveCounters', async (req, res) => {
try {
const userIdParam = req.query.user_id;
if (!userIdParam) {
return res.json({ success: false, message: 'ID utilisateur manquant' });
}
const conn = await pool.getConnection();
// Déterminer l'ID (UUID ou numérique)
const isUUID = userIdParam.length > 10 && userIdParam.includes('-');
const userQuery = `
SELECT ca.id, ca.prenom, ca.nom, ca.email, ca.DateEntree, ca.role,
ca.TypeContrat, s.Nom as service, ca.CampusId, ca.SocieteId,
so.Nom as societe_nom, ca.description
FROM CollaborateurAD ca
LEFT JOIN Services s ON ca.ServiceId = s.Id
LEFT JOIN Societe so ON ca.SocieteId = so.Id
WHERE ${isUUID ? 'ca.entraUserId' : 'ca.id'} = ?
AND (ca.Actif = 1 OR ca.Actif IS NULL)
`;
const [userInfo] = await conn.query(userQuery, [userIdParam]);
if (userInfo.length === 0) {
conn.release();
return res.json({ success: false, message: 'Utilisateur non trouvé' });
}
const user = userInfo[0];
const userId = user.id;
const dateEntree = user.DateEntree;
const typeContrat = user.TypeContrat || '37h';
const today = new Date();
const currentYear = today.getFullYear();
const previousYear = currentYear - 1;
console.log(`\n📊 === CALCUL COMPTEURS pour ${user.prenom} ${user.nom} ===`);
console.log(` Date référence: ${today.toLocaleDateString('fr-FR')}`);
// 🔍 DEBUG : Afficher les valeurs brutes de la BDD
console.log('\n📊 === VALEURS BRUTES CompteurConges ===');
const [debugCounters] = await conn.query(`
SELECT tc.Nom, cc.Annee, cc.Total, cc.Solde, cc.SoldeReporte
FROM CompteurConges cc
JOIN TypeConge tc ON cc.TypeCongeId = tc.Id
WHERE cc.CollaborateurADId = ?
ORDER BY tc.Nom, cc.Annee DESC
`, [userId]);
debugCounters.forEach(c => {
console.log(` ${c.Nom} ${c.Annee}: Total=${c.Total}j, Solde=${c.Solde}j, Report=${c.SoldeReporte}j`);
});
console.log('=====================================\n');
const ancienneteMs = today - new Date(dateEntree || today);
const ancienneteMois = Math.floor(ancienneteMs / (1000 * 60 * 60 * 24 * 30.44));
let counters = {
user: {
id: user.id,
nom: `${user.prenom} ${user.nom}`,
prenom: user.prenom,
nomFamille: user.nom,
email: user.email,
service: user.service || 'Non défini',
role: user.role,
description: user.description,
typeContrat: typeContrat,
societeId: user.SocieteId,
societeNom: user.societe_nom || 'Non défini',
dateEntree: dateEntree ? formatDateWithoutUTC(dateEntree) : null,
ancienneteMois: ancienneteMois,
ancienneteAnnees: Math.floor(ancienneteMois / 12),
ancienneteMoisRestants: ancienneteMois % 12
},
dateReference: today.toISOString().split('T')[0],
exerciceCP: getExerciceCP(today),
anneeRTT: currentYear,
cpN1: null,
cpN: null,
rttN: null,
rttN1: null,
totalDisponible: { cp: 0, rtt: 0, recup: 0, total: 0 }
};
const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']);
// ====================================
// 1⃣ CP N-1 (Report)
// ====================================
if (cpType.length > 0) {
const [cpN1] = await conn.query(`
SELECT Annee, SoldeReporte, Total, Solde
FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
`, [userId, cpType[0].Id, previousYear]);
if (cpN1.length > 0 && parseFloat(cpN1[0].Solde || 0) > 0) {
const soldeReporte = parseFloat(cpN1[0].Solde || 0);
// 🔥 Consommation N-1 depuis DeductionDetails
const [consommeN1] = await conn.query(`
SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalConsomme
FROM DeductionDetails dd
JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
WHERE dc.CollaborateurADId = ?
AND dd.TypeCongeId = ?
AND dd.Annee = ?
AND dd.TypeDeduction IN ('Année N-1', 'Anne N-1', 'Report N-1', 'Reporté N-1')
AND dd.TypeDeduction NOT IN ('Accum Récup', 'Accum Recup')
AND dc.Statut != 'Refusée'
`, [userId, cpType[0].Id, previousYear]);
const pris = parseFloat(consommeN1[0]?.totalConsomme || 0);
const soldeActuel = Math.max(0, soldeReporte - pris);
counters.cpN1 = {
annee: previousYear,
exercice: `${previousYear}-${previousYear + 1}`,
reporte: parseFloat(soldeReporte.toFixed(2)),
pris: parseFloat(pris.toFixed(2)),
solde: parseFloat(soldeActuel.toFixed(2)),
pourcentageUtilise: soldeReporte > 0 ? parseFloat(((pris / soldeReporte) * 100).toFixed(1)) : 0
};
counters.totalDisponible.cp += counters.cpN1.solde;
console.log(`✅ CP N-1: Reporté=${soldeReporte}j, Pris=${pris}j, Solde=${soldeActuel}j`);
} else {
counters.cpN1 = {
annee: previousYear,
exercice: `${previousYear}-${previousYear + 1}`,
reporte: 0,
pris: 0,
solde: 0,
pourcentageUtilise: 0
};
}
// ====================================
// 2⃣ CP N (Exercice en cours)
// ====================================
const cpMonthsCurrent = getMoisTravaillesCP(today, dateEntree);
const acquisCumuleeCP = parseFloat(calculerAcquisitionCP(today, dateEntree) || 0);
console.log(` CP - Mois travaillés: ${cpMonthsCurrent.toFixed(2)}`);
console.log(` CP - Acquisition cumulée: ${acquisCumuleeCP.toFixed(2)}j`);
// 🔥 RÉCUPÉRER LE SOLDE DEPUIS CompteurConges (source de vérité)
const [compteurCPN] = await conn.query(`
SELECT Solde, SoldeReporte, Total
FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
`, [userId, cpType[0].Id, currentYear]);
let soldeActuelCP = 0;
let totalAcquis = acquisCumuleeCP;
if (compteurCPN.length > 0) {
// ⭐ Utiliser le solde de la base (déjà ajusté par les déductions)
const soldeBDD = parseFloat(compteurCPN[0].Solde || 0);
const soldeReporte = parseFloat(compteurCPN[0].SoldeReporte || 0);
// Solde actuel = Solde total - Report (pour avoir uniquement l'année N)
soldeActuelCP = Math.max(0, soldeBDD - soldeReporte);
console.log(` CP N - Solde BDD: ${soldeBDD}j, Report: ${soldeReporte}j → Solde N: ${soldeActuelCP}j`);
} else {
// Si pas de compteur, le solde = acquisition (aucune déduction)
soldeActuelCP = acquisCumuleeCP;
console.log(` CP N - Pas de compteur BDD → Solde = Acquisition: ${soldeActuelCP}j`);
}
// 🔥 CALCUL DE L'ANTICIPÉ
const [anticipeUtiliseCP] = await conn.query(`
SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalConsomme
FROM DeductionDetails dd
JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
WHERE dc.CollaborateurADId = ?
AND dd.TypeCongeId = ?
AND dd.Annee = ?
AND dd.TypeDeduction = 'N Anticip'
AND dc.Statut != 'Refusée'
`, [userId, cpType[0].Id, currentYear]);
const cpAnticipeUtilise = parseFloat(anticipeUtiliseCP[0]?.totalConsomme || 0);
const cpAnticipeMax = Math.max(0, 25 - acquisCumuleeCP);
const cpAnticipeDisponible = Math.max(0, cpAnticipeMax - cpAnticipeUtilise);
console.log(` CP Anticipé: Max=${cpAnticipeMax}j, Utilisé=${cpAnticipeUtilise}j, Dispo=${cpAnticipeDisponible}j`);
counters.cpN = {
annee: currentYear,
exercice: getExerciceCP(today),
totalAnnuel: 25.00,
moisTravailles: parseFloat(cpMonthsCurrent.toFixed(2)),
acquisitionMensuelle: parseFloat((25 / 12).toFixed(2)),
acquis: parseFloat(totalAcquis.toFixed(2)),
pris: parseFloat((totalAcquis - soldeActuelCP).toFixed(2)), // Déduit du solde
solde: parseFloat(soldeActuelCP.toFixed(2)), // ⭐ Solde réel de la BDD
tauxAcquisition: parseFloat((cpMonthsCurrent / 12 * 100).toFixed(1)),
pourcentageUtilise: totalAcquis > 0 ? parseFloat(((totalAcquis - soldeActuelCP) / totalAcquis * 100).toFixed(1)) : 0,
joursRestantsAAcquerir: parseFloat((25 - totalAcquis).toFixed(2)),
anticipe: {
acquisPrevu: parseFloat(cpAnticipeMax.toFixed(2)),
pris: parseFloat(cpAnticipeUtilise.toFixed(2)),
disponible: parseFloat(cpAnticipeDisponible.toFixed(2)),
depassement: cpAnticipeUtilise > cpAnticipeMax ? parseFloat((cpAnticipeUtilise - cpAnticipeMax).toFixed(2)) : 0
}
};
counters.totalDisponible.cp += counters.cpN.solde + cpAnticipeDisponible;
console.log(`✅ CP N: Acquis=${totalAcquis}j, Pris=${counters.cpN.pris}j, Solde=${soldeActuelCP}j`);
}
// ====================================
// 3⃣ RTT N
// ====================================
const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']);
if (rttType.length > 0 && user.role !== 'Apprenti') {
const rttData = await calculerAcquisitionRTT(conn, userId, today);
const rttConfig = await getConfigurationRTT(conn, currentYear, typeContrat);
console.log(` RTT - Config: ${rttConfig.joursAnnuels}j/an`);
console.log(` RTT - Acquisition: ${rttData.acquisition.toFixed(2)}j`);
// 🔥 RÉCUPÉRER LE SOLDE DEPUIS CompteurConges
const [compteurRTT] = await conn.query(`
SELECT Solde, Total
FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
`, [userId, rttType[0].Id, currentYear]);
let soldeActuelRTT = 0;
if (compteurRTT.length > 0) {
// ⭐ Utiliser le solde de la base
soldeActuelRTT = parseFloat(compteurRTT[0].Solde || 0);
console.log(` RTT - Solde BDD: ${soldeActuelRTT}j`);
} else {
// Si pas de compteur, solde = acquisition
soldeActuelRTT = rttData.acquisition;
console.log(` RTT - Pas de compteur BDD → Solde = Acquisition: ${soldeActuelRTT}j`);
}
// 🔥 CALCUL DE L'ANTICIPÉ RTT
const [anticipeUtiliseRTT] = await conn.query(`
SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalConsomme
FROM DeductionDetails dd
JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
WHERE dc.CollaborateurADId = ?
AND dd.TypeCongeId = ?
AND dd.Annee = ?
AND dd.TypeDeduction = 'N Anticip'
AND dc.Statut != 'Refusée'
`, [userId, rttType[0].Id, currentYear]);
const rttAnticipeUtilise = parseFloat(anticipeUtiliseRTT[0]?.totalConsomme || 0);
const rttAnticipeMax = Math.max(0, rttConfig.joursAnnuels - rttData.acquisition);
const rttAnticipeDisponible = Math.max(0, rttAnticipeMax - rttAnticipeUtilise);
console.log(` RTT Anticipé: Max=${rttAnticipeMax}j, Utilisé=${rttAnticipeUtilise}j, Dispo=${rttAnticipeDisponible}j`);
counters.rttN = {
annee: currentYear,
typeContrat: typeContrat,
totalAnnuel: parseFloat(rttConfig.joursAnnuels.toFixed(2)),
moisTravailles: rttData.moisTravailles,
acquisitionMensuelle: parseFloat(rttConfig.acquisitionMensuelle.toFixed(6)),
acquis: parseFloat(rttData.acquisition.toFixed(2)),
pris: parseFloat((rttData.acquisition - soldeActuelRTT).toFixed(2)),
solde: parseFloat(soldeActuelRTT.toFixed(2)), // ⭐ Solde réel de la BDD
tauxAcquisition: parseFloat((rttData.moisTravailles / 12 * 100).toFixed(1)),
pourcentageUtilise: rttData.acquisition > 0 ? parseFloat(((rttData.acquisition - soldeActuelRTT) / rttData.acquisition * 100).toFixed(1)) : 0,
joursRestantsAAcquerir: parseFloat((rttConfig.joursAnnuels - rttData.acquisition).toFixed(2)),
anticipe: {
acquisPrevu: parseFloat(rttAnticipeMax.toFixed(2)),
pris: parseFloat(rttAnticipeUtilise.toFixed(2)),
disponible: parseFloat(rttAnticipeDisponible.toFixed(2)),
depassement: rttAnticipeUtilise > rttAnticipeMax ? parseFloat((rttAnticipeUtilise - rttAnticipeMax).toFixed(2)) : 0
}
};
counters.totalDisponible.rtt += counters.rttN.solde + rttAnticipeDisponible;
console.log(`✅ RTT N: Acquis=${rttData.acquisition}j, Pris=${counters.rttN.pris}j, Solde=${soldeActuelRTT}j`);
}
counters.rttN1 = {
annee: previousYear,
reporte: 0,
pris: 0,
solde: 0,
pourcentageUtilise: 0,
message: "Les RTT ne sont pas reportables d'une année sur l'autre"
};
// ====================================
// 4⃣ RÉCUP
// ====================================
const [recupType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Récupération']);
if (recupType.length > 0) {
// 🔥 Récupérer les accumulations
const [accumRecup] = await conn.query(`
SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalAccum
FROM DeductionDetails dd
JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
WHERE dc.CollaborateurADId = ?
AND dd.TypeCongeId = ?
AND dd.Annee = ?
AND dd.TypeDeduction IN ('Accum Récup', 'Accum Recup')
AND dc.Statut != 'Refusée'
`, [userId, recupType[0].Id, currentYear]);
// 🔥 Récupérer les consommations
const [consomRecup] = await conn.query(`
SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalConsom
FROM DeductionDetails dd
JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
WHERE dc.CollaborateurADId = ?
AND dd.TypeCongeId = ?
AND dd.Annee = ?
AND dd.TypeDeduction IN ('Récup Dosée', 'Recup Dosee', 'Récup Posée')
AND dc.Statut != 'Refusée'
`, [userId, recupType[0].Id, currentYear]);
const acquis = parseFloat(accumRecup[0]?.totalAccum || 0);
const pris = parseFloat(consomRecup[0]?.totalConsom || 0);
const solde = Math.max(0, acquis - pris);
counters.recupN = {
annee: currentYear,
acquis: parseFloat(acquis.toFixed(2)),
pris: parseFloat(pris.toFixed(2)),
solde: parseFloat(solde.toFixed(2)),
message: "Jours de récupération"
};
counters.totalDisponible.recup = counters.recupN.solde;
console.log(`✅ Récup: Acquis=${acquis}j, Pris=${pris}j, Solde=${solde}j`);
} else {
counters.recupN = {
annee: currentYear,
acquis: 0,
pris: 0,
solde: 0,
message: "Jours de récupération"
};
counters.totalDisponible.recup = 0;
}
// Recalculer le TOTAL
counters.totalDisponible.total = counters.totalDisponible.cp + counters.totalDisponible.rtt + counters.totalDisponible.recup;
console.log(`\n✅ TOTAL FINAL: ${counters.totalDisponible.total.toFixed(2)}j disponibles`);
console.log(` CP: ${counters.totalDisponible.cp.toFixed(2)}j`);
console.log(` RTT: ${counters.totalDisponible.rtt.toFixed(2)}j`);
console.log(` Récup: ${counters.totalDisponible.recup.toFixed(2)}j\n`);
conn.release();
res.json({
success: true,
message: 'Compteurs détaillés récupérés avec succès',
data: counters,
availableCP: counters.totalDisponible.cp,
availableRTT: counters.totalDisponible.rtt,
availableRecup: counters.totalDisponible.recup
});
} catch (error) {
console.error('Erreur getDetailedLeaveCounters:', error);
res.status(500).json({
success: false,
message: 'Erreur serveur',
error: error.message
});
}
});
app.post('/api/reinitializeAllCounters', async (req, res) => {
const conn = await pool.getConnection();
try {
await conn.beginTransaction();
console.log('🔄 Réinitialisation de tous les compteurs...');
const [collaborateurs] = await conn.query(`
SELECT id, prenom, nom, DateEntree, TypeContrat, CampusId, SocieteId
FROM CollaborateurAD
WHERE (actif = 1 OR actif IS NULL)
`);
console.log(`📊 ${collaborateurs.length} collaborateurs trouvés`);
const dateRefParam = req.body.dateReference;
const today = dateRefParam ? new Date(dateRefParam) : new Date();
const currentYear = today.getFullYear();
const previousYear = currentYear - 1;
const results = [];
for (const collab of collaborateurs) {
const dateEntree = collab.DateEntree;
const typeContrat = collab.TypeContrat || '37h';
// Calculer les acquisitions
const acquisCP = calculerAcquisitionCP(today, dateEntree);
const isApprenti = collab.role === 'Apprenti';
const rttData = isApprenti ? { acquisition: 0 } : await calculerAcquisitionRTT(conn, collab.id, today);
const acquisRTT = rttData.acquisition;
const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']);
const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']);
// ===== CP N =====
if (cpType.length > 0) {
// ⭐ CALCUL FIABLE : Utiliser DeductionDetails au lieu des anciennes valeurs
const [deductionsCP] = await conn.query(`
SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalConsomme
FROM DeductionDetails dd
JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
WHERE dc.CollaborateurADId = ?
AND dd.TypeCongeId = ?
AND dd.Annee = ?
AND dc.Statut != 'Refusée'
`, [collab.id, cpType[0].Id, currentYear]);
const totalConsomme = parseFloat(deductionsCP[0].totalConsomme || 0);
// Récupérer le reporté (qui ne change pas)
const [compteurExisting] = await conn.query(`
SELECT SoldeReporte FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
`, [collab.id, cpType[0].Id, currentYear]);
const soldeReporte = compteurExisting.length > 0
? parseFloat(compteurExisting[0].SoldeReporte || 0)
: 0;
// ⭐ CALCUL EXACT DU SOLDE
const nouveauSolde = Math.max(0, acquisCP + soldeReporte - totalConsomme);
// Mise à jour ou insertion
if (compteurExisting.length > 0) {
await conn.query(`
UPDATE CompteurConges
SET Total = ?, Solde = ?, DerniereMiseAJour = NOW()
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
`, [acquisCP, nouveauSolde, collab.id, cpType[0].Id, currentYear]);
} else {
await conn.query(`
INSERT INTO CompteurConges
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour)
VALUES (?, ?, ?, ?, ?, 0, NOW())
`, [collab.id, cpType[0].Id, currentYear, acquisCP, nouveauSolde]);
}
console.log(`📊 ${collab.prenom} ${collab.nom} - CP: Acquis ${acquisCP.toFixed(2)}j, Consommé ${totalConsomme.toFixed(2)}j, Reporté ${soldeReporte.toFixed(2)}j → Solde ${nouveauSolde.toFixed(2)}j`);
// Créer CP N-1 si nécessaire
const [cpN1] = await conn.query(`
SELECT Id FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
`, [collab.id, cpType[0].Id, previousYear]);
if (cpN1.length === 0) {
await conn.query(`
INSERT INTO CompteurConges
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour)
VALUES (?, ?, ?, 0, 0, 0, NOW())
`, [collab.id, cpType[0].Id, previousYear]);
}
}
// ===== RTT N =====
if (rttType.length > 0 && !isApprenti) {
// ⭐ MÊME LOGIQUE POUR LES RTT
const [deductionsRTT] = await conn.query(`
SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalConsomme
FROM DeductionDetails dd
JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
WHERE dc.CollaborateurADId = ?
AND dd.TypeCongeId = ?
AND dd.Annee = ?
AND dc.Statut != 'Refusée'
`, [collab.id, rttType[0].Id, currentYear]);
const totalConsomme = parseFloat(deductionsRTT[0].totalConsomme || 0);
const nouveauSolde = Math.max(0, acquisRTT - totalConsomme);
const [compteurExisting] = await conn.query(`
SELECT Id FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
`, [collab.id, rttType[0].Id, currentYear]);
if (compteurExisting.length > 0) {
await conn.query(`
UPDATE CompteurConges
SET Total = ?, Solde = ?, DerniereMiseAJour = NOW()
WHERE Id = ?
`, [acquisRTT, nouveauSolde, compteurExisting[0].Id]);
} else {
await conn.query(`
INSERT INTO CompteurConges
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour)
VALUES (?, ?, ?, ?, ?, 0, NOW())
`, [collab.id, rttType[0].Id, currentYear, acquisRTT, nouveauSolde]);
}
console.log(`📊 ${collab.prenom} ${collab.nom} - RTT: Acquis ${acquisRTT.toFixed(2)}j, Consommé ${totalConsomme.toFixed(2)}j → Solde ${nouveauSolde.toFixed(2)}j`);
// Créer RTT N-1 si nécessaire
const [rttN1] = await conn.query(`
SELECT Id FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
`, [collab.id, rttType[0].Id, previousYear]);
if (rttN1.length === 0) {
await conn.query(`
INSERT INTO CompteurConges
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour)
VALUES (?, ?, ?, 0, 0, 0, NOW())
`, [collab.id, rttType[0].Id, previousYear]);
}
}
results.push({
collaborateur: `${collab.prenom} ${collab.nom}`,
type_contrat: typeContrat,
cp_acquis: acquisCP.toFixed(2),
cp_solde: (acquisCP - (deductionsCP?.[0]?.totalConsomme || 0)).toFixed(2),
rtt_acquis: acquisRTT.toFixed(2),
rtt_solde: (acquisRTT - (deductionsRTT?.[0]?.totalConsomme || 0)).toFixed(2)
});
}
await conn.commit();
console.log('✅ Réinitialisation terminée');
res.json({
success: true,
message: `✅ Compteurs réinitialisés pour ${collaborateurs.length} collaborateurs`,
date_reference: today.toISOString().split('T')[0],
total_collaborateurs: collaborateurs.length,
results: results
});
} catch (error) {
await conn.rollback();
console.error('❌ Erreur réinitialisation:', error);
res.status(500).json({
success: false,
message: 'Erreur lors de la réinitialisation',
error: error.message
});
} finally {
conn.release();
}
});
app.post('/api/updateCounters', async (req, res) => {
const conn = await pool.getConnection();
try {
const { collaborateur_id } = req.body;
if (!collaborateur_id) return res.json({ success: false, message: 'ID collaborateur manquant' });
await conn.beginTransaction();
const updates = await updateMonthlyCounters(conn, collaborateur_id, new Date());
await conn.commit();
res.json({ success: true, message: 'Compteurs mis à jour', updates });
} catch (error) {
await conn.rollback();
res.status(500).json({ success: false, message: 'Erreur', error: error.message });
} finally {
conn.release();
}
});
app.post('/api/updateAllCounters', async (req, res) => {
const conn = await pool.getConnection();
try {
await conn.beginTransaction();
const [collaborateurs] = await conn.query('SELECT id, CampusId FROM CollaborateurAD WHERE actif = 1 OR actif IS NULL');
const allUpdates = [];
for (const collab of collaborateurs) {
const updates = await updateMonthlyCounters(conn, collab.id, new Date());
allUpdates.push({ collaborateur_id: collab.id, updates });
}
await conn.commit();
res.json({ success: true, message: `Compteurs mis à jour pour ${collaborateurs.length} collaborateurs`, total_collaborateurs: collaborateurs.length, details: allUpdates });
} catch (error) {
await conn.rollback();
res.status(500).json({ success: false, message: 'Erreur', error: error.message });
} finally {
conn.release();
}
});
async function deductLeaveBalanceWithTracking(conn, collaborateurId, typeCongeId, nombreJours, demandeCongeId) {
const currentYear = new Date().getFullYear();
const previousYear = currentYear - 1;
let joursRestants = nombreJours;
const deductions = [];
// Étape 1: Déduire du reporté N-1 d'abord
const [compteurN1] = await conn.query(
`SELECT Id, Solde, SoldeReporte FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
[collaborateurId, typeCongeId, previousYear]
);
if (compteurN1.length > 0 && compteurN1[0].SoldeReporte > 0) {
const soldeN1 = parseFloat(compteurN1[0].SoldeReporte);
const aDeduireN1 = Math.min(soldeN1, joursRestants);
if (aDeduireN1 > 0) {
// Déduction dans la base
await conn.query(
`UPDATE CompteurConges
SET SoldeReporte = GREATEST(0, SoldeReporte - ?),
Solde = GREATEST(0, Solde - ?)
WHERE Id = ?`,
[aDeduireN1, aDeduireN1, compteurN1[0].Id]
);
// Sauvegarde du détail de la déduction
await conn.query(`
INSERT INTO DeductionDetails
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
VALUES (?, ?, ?, 'Accum Récup', ?)
`, [demandeId, recupType[0].Id, currentYear, recupJours]);
deductions.push({
annee: previousYear,
type: 'Reporté N-1',
joursUtilises: aDeduireN1,
soldeAvant: soldeN1
});
joursRestants -= aDeduireN1;
}
}
// Étape 2: Déduire de l'année N si besoin
if (joursRestants > 0) {
const [compteurN] = await conn.query(
`SELECT Id, Solde, SoldeReporte FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
[collaborateurId, typeCongeId, currentYear]
);
if (compteurN.length > 0) {
const soldeN = parseFloat(compteurN[0].Solde) - parseFloat(compteurN[0].SoldeReporte || 0);
const aDeduireN = Math.min(soldeN, joursRestants);
if (aDeduireN > 0) {
// Déduction dans la base
await conn.query(
`UPDATE CompteurConges
SET Solde = GREATEST(0, Solde - ?)
WHERE Id = ?`,
[aDeduireN, compteurN[0].Id]
);
// Sauvegarde du détail de la déduction
await conn.query(
`INSERT INTO DeductionDetails
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
VALUES (?, ?, ?, 'Année N', ?)`,
[demandeCongeId, typeCongeId, currentYear, aDeduireN]
);
deductions.push({
annee: currentYear,
type: 'Année actuelle N',
joursUtilises: aDeduireN,
soldeAvant: soldeN
});
joursRestants -= aDeduireN;
}
}
}
return {
success: joursRestants === 0,
joursDeduitsTotal: nombreJours - joursRestants,
joursNonDeduits: joursRestants,
details: deductions
};
};
async function restoreLeaveBalance(conn, demandeCongeId, collaborateurId) {
try {
console.log(`\n🔄 === RESTAURATION COMPTEURS ===`);
console.log(`Demande ID: ${demandeCongeId}`);
console.log(`Collaborateur ID: ${collaborateurId}`);
const [deductions] = await conn.query(
`SELECT dd.TypeCongeId, dd.Annee, dd.TypeDeduction, dd.JoursUtilises, tc.Nom as TypeNom
FROM DeductionDetails dd
JOIN TypeConge tc ON dd.TypeCongeId = tc.Id
WHERE dd.DemandeCongeId = ?
ORDER BY dd.Id DESC`,
[demandeCongeId]
);
console.log(`📊 ${deductions.length} déductions trouvées`);
if (deductions.length === 0) {
console.log('⚠️ Aucune déduction trouvée pour cette demande');
return { success: false, message: 'Aucune déduction à restaurer' };
}
const restorations = [];
for (const deduction of deductions) {
const { TypeCongeId, Annee, TypeDeduction, JoursUtilises, TypeNom } = deduction;
console.log(`\n🔍 Traitement: ${TypeNom} - ${TypeDeduction} - ${JoursUtilises}j (Année: ${Annee})`);
// ⭐ NOUVEAU : Gestion des Récup posées
if (TypeDeduction === 'Récup Posée') {
console.log(`🔄 Restauration Récup posée: +${JoursUtilises}j`);
const [compteur] = await conn.query(
`SELECT Id, Solde FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
[collaborateurId, TypeCongeId, Annee]
);
if (compteur.length > 0) {
const ancienSolde = parseFloat(compteur[0].Solde || 0);
const nouveauSolde = ancienSolde + parseFloat(JoursUtilises);
await conn.query(
`UPDATE CompteurConges
SET Solde = ?,
DerniereMiseAJour = NOW()
WHERE Id = ?`,
[nouveauSolde, compteur[0].Id]
);
restorations.push({
type: TypeNom,
annee: Annee,
typeDeduction: TypeDeduction,
joursRestores: JoursUtilises
});
console.log(`✅ Récup restaurée: ${ancienSolde}${nouveauSolde}`);
}
continue;
}
// 🔹 N+1 Anticipé - ⭐ RESTAURATION CORRECTE
if (TypeDeduction === 'N+1 Anticipé') {
console.log(`🔄 Restauration N+1 Anticipé: +${JoursUtilises}j`);
const [compteur] = await conn.query(
`SELECT Id, SoldeAnticipe FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
[collaborateurId, TypeCongeId, Annee]
);
if (compteur.length > 0) {
const ancienSolde = parseFloat(compteur[0].SoldeAnticipe || 0);
const nouveauSolde = ancienSolde + parseFloat(JoursUtilises);
await conn.query(
`UPDATE CompteurConges
SET SoldeAnticipe = ?,
DerniereMiseAJour = NOW()
WHERE Id = ?`,
[nouveauSolde, compteur[0].Id]
);
restorations.push({
type: TypeNom,
annee: Annee,
typeDeduction: TypeDeduction,
joursRestores: JoursUtilises
});
console.log(`✅ N+1 Anticipé restauré: ${ancienSolde}${nouveauSolde}`);
} else {
// ⭐ Créer le compteur N+1 s'il n'existe pas
await conn.query(
`INSERT INTO CompteurConges
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, SoldeAnticipe, DerniereMiseAJour)
VALUES (?, ?, ?, 0, 0, 0, ?, NOW())`,
[collaborateurId, TypeCongeId, Annee, JoursUtilises]
);
restorations.push({
type: TypeNom,
annee: Annee,
typeDeduction: TypeDeduction,
joursRestores: JoursUtilises
});
console.log(`✅ Compteur N+1 créé avec ${JoursUtilises}j anticipés`);
}
continue;
}
// 🔹 N Anticipé - ⭐ RESTAURATION CORRECTE
if (TypeDeduction === 'N Anticipé') {
console.log(`🔄 Restauration N Anticipé: +${JoursUtilises}j`);
const [compteur] = await conn.query(
`SELECT Id, SoldeAnticipe FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
[collaborateurId, TypeCongeId, Annee]
);
if (compteur.length > 0) {
const ancienSolde = parseFloat(compteur[0].SoldeAnticipe || 0);
const nouveauSolde = ancienSolde + parseFloat(JoursUtilises);
await conn.query(
`UPDATE CompteurConges
SET SoldeAnticipe = ?,
DerniereMiseAJour = NOW()
WHERE Id = ?`,
[nouveauSolde, compteur[0].Id]
);
restorations.push({
type: TypeNom,
annee: Annee,
typeDeduction: TypeDeduction,
joursRestores: JoursUtilises
});
console.log(`✅ N Anticipé restauré: ${ancienSolde}${nouveauSolde}`);
} else {
// ⭐ Créer le compteur s'il n'existe pas (cas rare)
await conn.query(
`INSERT INTO CompteurConges
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, SoldeAnticipe, DerniereMiseAJour)
VALUES (?, ?, ?, 0, 0, 0, ?, NOW())`,
[collaborateurId, TypeCongeId, Annee, JoursUtilises]
);
restorations.push({
type: TypeNom,
annee: Annee,
typeDeduction: TypeDeduction,
joursRestores: JoursUtilises
});
console.log(`✅ Compteur N créé avec ${JoursUtilises}j anticipés`);
}
continue;
}
// 🔹 Reporté N-1
if (TypeDeduction === 'Reporté N-1' || TypeDeduction === 'Report N-1') {
const [compteur] = await conn.query(
`SELECT Id, SoldeReporte, Solde FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
[collaborateurId, TypeCongeId, Annee]
);
if (compteur.length > 0) {
const ancienSolde = parseFloat(compteur[0].Solde || 0);
const nouveauSolde = ancienSolde + parseFloat(JoursUtilises);
await conn.query(
`UPDATE CompteurConges
SET SoldeReporte = SoldeReporte + ?,
Solde = Solde + ?,
DerniereMiseAJour = NOW()
WHERE Id = ?`,
[JoursUtilises, JoursUtilises, compteur[0].Id]
);
restorations.push({
type: TypeNom,
annee: Annee,
typeDeduction: TypeDeduction,
joursRestores: JoursUtilises
});
console.log(`✅ Reporté restauré: ${ancienSolde}${nouveauSolde}`);
}
}
// 🔹 Année N
else if (TypeDeduction === 'Année N') {
const [compteur] = await conn.query(
`SELECT Id, Solde FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
[collaborateurId, TypeCongeId, Annee]
);
if (compteur.length > 0) {
const ancienSolde = parseFloat(compteur[0].Solde || 0);
const nouveauSolde = ancienSolde + parseFloat(JoursUtilises);
await conn.query(
`UPDATE CompteurConges
SET Solde = Solde + ?,
DerniereMiseAJour = NOW()
WHERE Id = ?`,
[JoursUtilises, compteur[0].Id]
);
restorations.push({
type: TypeNom,
annee: Annee,
typeDeduction: TypeDeduction,
joursRestores: JoursUtilises
});
console.log(`✅ Année N restaurée: ${ancienSolde}${nouveauSolde}`);
}
}
}
// ⭐ IMPORTANT : Recalculer les soldes anticipés après restauration
console.log(`\n🔄 Recalcul des soldes anticipés...`);
await updateSoldeAnticipe(conn, collaborateurId);
console.log(`\n✅ Restauration terminée: ${restorations.length} opérations\n`);
return {
success: true,
restorations,
message: `${restorations.length} restaurations effectuées`
};
} catch (error) {
console.error('❌ Erreur lors de la restauration des soldes:', error);
throw error;
}
}
app.get('/api/testProrata', async (req, res) => {
try {
const userId = parseInt(req.query.user_id || 0);
if (userId <= 0) return res.json({ success: false, message: 'ID utilisateur requis' });
const conn = await pool.getConnection();
const [userInfo] = await conn.query(`SELECT id, prenom, nom, DateEntree, TypeContrat, CampusId FROM CollaborateurAD WHERE id = ?`, [userId]);
if (userInfo.length === 0) { conn.release(); return res.json({ success: false, message: 'Utilisateur non trouvé' }); }
const user = userInfo[0];
const dateEntree = user.DateEntree;
const typeContrat = user.TypeContrat || '37h';
const today = new Date();
const moisCP = getMoisTravaillesCP(today, dateEntree);
const acquisCP = calculerAcquisitionCP(today, dateEntree);
const rttData = await calculerAcquisitionRTT(conn, userId, today);
conn.release();
res.json({
success: true,
user: {
id: user.id,
nom: `${user.prenom} ${user.nom}`,
dateEntree: dateEntree ? dateEntree.toISOString().split('T')[0] : null,
typeContrat: typeContrat
},
dateReference: today.toISOString().split('T')[0],
calculs: {
CP: {
moisTravailles: parseFloat(moisCP.toFixed(2)),
acquisitionMensuelle: 25 / 12,
acquisitionCumulee: acquisCP,
formule: `${moisCP.toFixed(2)} mois × ${(25 / 12).toFixed(2)}j/mois = ${acquisCP}j`
},
RTT: {
moisTravailles: rttData.moisTravailles,
acquisitionMensuelle: rttData.config.acquisitionMensuelle,
acquisitionCumulee: rttData.acquisition,
totalAnnuel: rttData.config.joursAnnuels,
formule: `${rttData.moisTravailles} mois × ${rttData.config.acquisitionMensuelle.toFixed(6)}j/mois = ${rttData.acquisition}j`
}
}
});
} catch (error) {
console.error('Erreur testProrata:', error);
res.status(500).json({ success: false, message: 'Erreur serveur', error: error.message });
}
});
app.post('/api/fixAllCounters', async (req, res) => {
const conn = await pool.getConnection();
try {
await conn.beginTransaction();
const today = new Date();
const currentYear = today.getFullYear();
const [collaborateurs] = await conn.query('SELECT id, prenom, nom, DateEntree, CampusId FROM CollaborateurAD WHERE (actif = 1 OR actif IS NULL)');
console.log(`🔄 Correction de ${collaborateurs.length} compteurs...`);
const corrections = [];
for (const collab of collaborateurs) {
const dateEntree = collab.DateEntree;
const moisCP = getMoisTravaillesCP(today, dateEntree);
const acquisCP = calculerAcquisitionCP(today, dateEntree);
const rttData = await calculerAcquisitionRTT(conn, collab.id, today);
const acquisRTT = rttData.acquisition;
const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']);
const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']);
if (cpType.length > 0) {
const [existingCP] = await conn.query(`SELECT Id, Total, Solde, SoldeReporte FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, [collab.id, cpType[0].Id, currentYear]);
if (existingCP.length > 0) {
const ancienTotal = parseFloat(existingCP[0].Total);
const ancienSolde = parseFloat(existingCP[0].Solde);
const difference = acquisCP - ancienTotal;
const nouveauSolde = Math.max(0, ancienSolde + difference);
await conn.query(`UPDATE CompteurConges SET Total = ?, Solde = ?, DerniereMiseAJour = NOW() WHERE Id = ?`, [acquisCP, nouveauSolde, existingCP[0].Id]);
corrections.push({ collaborateur: `${collab.prenom} ${collab.nom}`, type: 'CP', ancienTotal: ancienTotal.toFixed(2), nouveauTotal: acquisCP.toFixed(2), ancienSolde: ancienSolde.toFixed(2), nouveauSolde: nouveauSolde.toFixed(2) });
}
}
if (rttType.length > 0) {
const [existingRTT] = await conn.query(`SELECT Id, Total, Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, [collab.id, rttType[0].Id, currentYear]);
if (existingRTT.length > 0) {
const ancienTotal = parseFloat(existingRTT[0].Total);
const ancienSolde = parseFloat(existingRTT[0].Solde);
const difference = acquisRTT - ancienTotal;
const nouveauSolde = Math.max(0, ancienSolde + difference);
await conn.query(`UPDATE CompteurConges SET Total = ?, Solde = ?, DerniereMiseAJour = NOW() WHERE Id = ?`, [acquisRTT, nouveauSolde, existingRTT[0].Id]);
corrections.push({ collaborateur: `${collab.prenom} ${collab.nom}`, type: 'RTT', ancienTotal: ancienTotal.toFixed(2), nouveauTotal: acquisRTT.toFixed(2), ancienSolde: ancienSolde.toFixed(2), nouveauSolde: nouveauSolde.toFixed(2) });
}
}
}
await conn.commit();
res.json({ success: true, message: `${collaborateurs.length} compteurs corrigés`, corrections: corrections });
} catch (error) {
await conn.rollback();
console.error('❌ Erreur correction compteurs:', error);
res.status(500).json({ success: false, message: 'Erreur', error: error.message });
} finally {
conn.release();
}
});
app.post('/api/processEndOfYear', async (req, res) => {
const conn = await pool.getConnection();
try {
const { collaborateur_id } = req.body;
await conn.beginTransaction();
let result;
if (collaborateur_id) {
result = await processEndOfYearRTT(conn, collaborateur_id);
} else {
const [collaborateurs] = await conn.query('SELECT id, CampusId FROM CollaborateurAD');
const results = [];
for (const c of collaborateurs) {
const r = await processEndOfYearRTT(conn, c.id);
if (r) results.push({ collaborateur_id: c.id, ...r });
}
result = results;
}
await conn.commit();
res.json({ success: true, message: 'Traitement de fin d\'année effectué', result });
} catch (error) {
await conn.rollback();
res.status(500).json({ success: false, message: 'Erreur', error: error.message });
} finally {
conn.release();
}
});
app.post('/api/processEndOfExercice', async (req, res) => {
const conn = await pool.getConnection();
try {
const { collaborateur_id } = req.body;
await conn.beginTransaction();
let result;
if (collaborateur_id) {
result = await processEndOfExerciceCP(conn, collaborateur_id);
} else {
const [collaborateurs] = await conn.query('SELECT id, CampusId FROM CollaborateurAD');
const results = [];
for (const c of collaborateurs) {
const r = await processEndOfExerciceCP(conn, c.id);
if (r) results.push({ collaborateur_id: c.id, ...r });
}
result = results;
}
await conn.commit();
res.json({ success: true, message: 'Traitement de fin d\'exercice CP effectué', result });
} catch (error) {
await conn.rollback();
res.status(500).json({ success: false, message: 'Erreur', error: error.message });
} finally {
conn.release();
}
});
app.get('/api/getAcquisitionDetails', async (req, res) => {
try {
const today = new Date();
const details = { date_reference: today.toISOString().split('T')[0], CP: { exercice: getExerciceCP(today), mois_travailles: getMoisTravaillesCP(today), acquisition_mensuelle: LEAVE_RULES.CP.acquisitionMensuelle, acquisition_cumulee: calculerAcquisitionCumulee('CP', today), total_annuel: LEAVE_RULES.CP.joursAnnuels, periode: '01/06 - 31/05', reportable: LEAVE_RULES.CP.reportable }, RTT: { annee: today.getFullYear(), mois_travailles: getMoisTravaillesRTT(today), acquisition_mensuelle: LEAVE_RULES.RTT.acquisitionMensuelle, acquisition_cumulee: calculerAcquisitionCumulee('RTT', today), total_annuel: LEAVE_RULES.RTT.joursAnnuels, periode: '01/01 - 31/12', reportable: LEAVE_RULES.RTT.reportable } };
res.json({ success: true, details });
} catch (error) {
res.status(500).json({ success: false, message: 'Erreur', error: error.message });
}
});
app.get('/api/getLeaveCounters', async (req, res) => {
try {
const userId = parseInt(req.query.user_id || 0);
const data = {};
if (userId > 0) {
const [rows] = await pool.query(`SELECT tc.Nom, cc.Annee, cc.Solde, cc.Total, cc.SoldeReporte FROM CompteurConges cc JOIN TypeConge tc ON cc.TypeCongeId = tc.Id WHERE cc.CollaborateurADId = ?`, [userId]);
rows.forEach(row => { data[row.Nom] = { Annee: row.Annee, Solde: parseFloat(row.Solde), Total: parseFloat(row.Total), SoldeReporte: parseFloat(row.SoldeReporte) }; });
}
res.json({ success: true, message: 'Compteurs récupérés', counters: data });
} catch (error) {
res.status(500).json({ success: false, message: 'Erreur', error: error.message });
}
});
app.get('/api/getEmploye', async (req, res) => {
try {
const id = parseInt(req.query.id || 0);
if (id <= 0) return res.json({ success: false, message: 'ID invalide' });
const conn = await pool.getConnection();
// 1⃣ Récupérer les infos du collaborateur
const [rows] = await conn.query(`
SELECT
ca.id,
ca.Nom,
ca.Prenom,
ca.Email,
ca.role,
ca.TypeContrat,
ca.DateEntree,
ca.CampusId,
ca.SocieteId,
s.Nom as service,
so.Nom as societe_nom
FROM CollaborateurAD ca
LEFT JOIN Services s ON ca.ServiceId = s.Id
LEFT JOIN Societe so ON ca.SocieteId = so.Id
WHERE ca.id = ?
`, [id]);
if (rows.length === 0) {
conn.release();
return res.json({ success: false, message: 'Collaborateur non trouvé' });
}
const employee = rows[0];
// 2⃣ Déterminer si c'est un apprenti
const isApprenti = normalizeRole(employee.role) === 'apprenti';
// 3⃣ Récupérer les compteurs CP
const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']);
let cpTotal = 0, cpSolde = 0;
if (cpType.length > 0) {
const currentYear = new Date().getFullYear();
const previousYear = currentYear - 1;
// CP N-1 (reporté)
const [cpN1] = await conn.query(`
SELECT SoldeReporte
FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
`, [id, cpType[0].Id, previousYear]);
// CP N (année courante)
const [cpN] = await conn.query(`
SELECT Solde, SoldeReporte
FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
`, [id, cpType[0].Id, currentYear]);
const cpN1Solde = cpN1.length > 0 ? parseFloat(cpN1[0].SoldeReporte || 0) : 0;
const cpNSolde = cpN.length > 0 ? parseFloat(cpN[0].Solde || 0) : 0;
const cpNReporte = cpN.length > 0 ? parseFloat(cpN[0].SoldeReporte || 0) : 0;
cpTotal = cpN1Solde + (cpNSolde - cpNReporte);
cpSolde = cpTotal;
}
// 4⃣ Récupérer les compteurs RTT (sauf pour apprentis)
let rttTotal = 0, rttSolde = 0;
if (!isApprenti) {
const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']);
if (rttType.length > 0) {
const currentYear = new Date().getFullYear();
const [rttN] = await conn.query(`
SELECT Solde
FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
`, [id, rttType[0].Id, currentYear]);
rttTotal = rttN.length > 0 ? parseFloat(rttN[0].Solde || 0) : 0;
rttSolde = rttTotal;
}
}
conn.release();
// 5⃣ Retourner les données complètes
res.json({
success: true,
employee: {
id: employee.id,
Nom: employee.Nom,
Prenom: employee.Prenom,
Email: employee.Email,
role: employee.role,
TypeContrat: employee.TypeContrat,
DateEntree: employee.DateEntree,
CampusId: employee.CampusId,
SocieteId: employee.SocieteId,
service: employee.service,
societe_nom: employee.societe_nom,
conges_restants: parseFloat(cpSolde.toFixed(2)),
rtt_restants: parseFloat(rttSolde.toFixed(2))
}
});
} catch (error) {
console.error('❌ Erreur getEmploye:', error);
res.status(500).json({
success: false,
message: 'Erreur DB',
error: error.message
});
}
});
app.get('/api/getEmployeRequest', async (req, res) => {
try {
const id = parseInt(req.query.id || 0);
if (id <= 0) return res.json({ success: false, message: 'ID invalide' });
const [rows] = await pool.query(`
SELECT
dc.Id,
dc.DateDebut,
dc.DateFin,
dc.NombreJours as days,
dc.Statut as status,
dc.DateDemande,
GROUP_CONCAT(DISTINCT tc.Nom ORDER BY tc.Nom SEPARATOR ', ') AS type,
CONCAT(
DATE_FORMAT(dc.DateDebut, '%d/%m/%Y'),
IF(dc.DateDebut = dc.DateFin, '', CONCAT(' - ', DATE_FORMAT(dc.DateFin, '%d/%m/%Y')))
) as date_display
FROM DemandeConge dc
LEFT JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId
LEFT JOIN TypeConge tc ON dct.TypeCongeId = tc.Id
WHERE dc.CollaborateurADId = ?
GROUP BY dc.Id, dc.DateDebut, dc.DateFin, dc.NombreJours, dc.Statut, dc.DateDemande
ORDER BY dc.DateDemande DESC
`, [id]);
res.json({
success: true,
requests: rows
});
} catch (error) {
console.error('❌ Erreur getEmployeRequest:', error);
res.status(500).json({
success: false,
message: 'Erreur DB',
error: error.message
});
}
});
app.get('/api/getRequests', async (req, res) => {
try {
const userId = req.query.user_id;
if (!userId) return res.json({ success: false, message: 'ID utilisateur manquant' });
// ✅ REQUÊTE CORRIGÉE avec la table de liaison
const [rows] = await pool.query(`
SELECT
dc.Id,
dc.DateDebut,
dc.DateFin,
dc.Statut,
dc.DateDemande,
dc.Commentaire,
dc.CommentaireValidation,
dc.Validateur,
dc.DocumentJoint,
GROUP_CONCAT(DISTINCT tc.Nom ORDER BY tc.Nom SEPARATOR ', ') AS TypeConges,
SUM(dct.NombreJours) as NombreJoursTotal
FROM DemandeConge dc
LEFT JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId
LEFT JOIN TypeConge tc ON dct.TypeCongeId = tc.Id
WHERE (dc.EmployeeId = ? OR dc.CollaborateurADId = ?)
GROUP BY dc.Id, dc.DateDebut, dc.DateFin, dc.Statut, dc.DateDemande, dc.Commentaire, dc.CommentaireValidation, dc.Validateur, dc.DocumentJoint
ORDER BY dc.DateDemande DESC
`, [userId, userId]);
const requests = rows.map(row => {
const workingDays = getWorkingDays(row.DateDebut, row.DateFin);
const dateDisplay = row.DateDebut === row.DateFin
? formatDate(row.DateDebut)
: `${formatDate(row.DateDebut)} - ${formatDate(row.DateFin)}`;
let fileUrl = null;
if (row.TypeConges && row.TypeConges.includes('Congé maladie') && row.DocumentJoint) {
fileUrl = `/uploads/${path.basename(row.DocumentJoint)}`;
}
return {
id: row.Id,
type: row.TypeConges || 'Non défini', // ✅ Gérer le cas null
startDate: row.DateDebut,
endDate: row.DateFin,
dateDisplay,
days: row.NombreJoursTotal || workingDays, // ✅ Utiliser le total de la table de liaison
status: row.Statut,
reason: row.Commentaire || 'Aucun commentaire',
submittedAt: row.DateDemande,
submittedDisplay: formatDate(row.DateDemande),
validator: row.Validateur || null,
validationComment: row.CommentaireValidation || null,
fileUrl
};
});
res.json({
success: true,
message: 'Demandes récupérées',
requests,
total: requests.length
});
} catch (error) {
console.error('❌ Erreur getRequests:', error);
res.status(500).json({
success: false,
message: 'Erreur',
error: error.message
});
}
});
app.get('/api/getAllTeamRequests', async (req, res) => {
try {
const managerId = req.query.SuperieurId;
if (!managerId) return res.json({ success: false, message: 'Paramètre SuperieurId manquant' });
const [rows] = await pool.query(`SELECT dc.Id, dc.DateDebut, dc.DateFin, dc.Statut, dc.DateDemande, dc.Commentaire, dc.DocumentJoint, dc.CollaborateurADId AS employee_id, CONCAT(ca.Prenom, ' ', ca.Nom) as employee_name, ca.Email as employee_email, tc.Nom as type FROM DemandeConge dc JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id JOIN TypeConge tc ON dc.TypeCongeId = tc.Id JOIN HierarchieValidationAD hv ON hv.CollaborateurId = ca.id WHERE hv.SuperieurId = ? ORDER BY dc.DateDemande DESC`, [managerId]);
const requests = rows.map(row => ({ id: row.Id, employee_id: row.employee_id, employee_name: row.employee_name, employee_email: row.employee_email, type: row.type, start_date: row.DateDebut, end_date: row.DateFin, date_display: row.DateDebut === row.DateFin ? formatDate(row.DateDebut) : `${formatDate(row.DateDebut)} - ${formatDate(row.DateFin)}`, days: getWorkingDays(row.DateDebut, row.DateFin), status: row.Statut, reason: row.Commentaire || '', file: row.DocumentJoint || null, submitted_at: row.DateDemande, submitted_display: formatDate(row.DateDemande) }));
res.json({ success: true, requests });
} catch (error) {
res.status(500).json({ success: false, message: 'Erreur DB', error: error.message });
}
});
app.get('/api/getPendingRequests', async (req, res) => {
try {
const managerId = req.query.manager_id;
if (!managerId) return res.json({ success: false, message: 'ID manager manquant' });
const [managerRows] = await pool.query('SELECT ServiceId, CampusId FROM CollaborateurAD WHERE id = ?', [managerId]);
if (managerRows.length === 0) return res.json({ success: false, message: 'Manager non trouvé' });
const serviceId = managerRows[0].ServiceId;
const [rows] = await pool.query(`SELECT dc.Id, dc.DateDebut, dc.DateFin, dc.Statut, dc.DateDemande, dc.Commentaire, dc.CollaborateurADId, CONCAT(ca.prenom, ' ', ca.nom) as employee_name, ca.email as employee_email, GROUP_CONCAT(tc.Nom ORDER BY tc.Nom SEPARATOR ', ') as types FROM DemandeConge dc JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id JOIN TypeConge tc ON FIND_IN_SET(tc.Id, dc.TypeCongeId) WHERE ca.ServiceId = ? AND dc.Statut = 'En attente' AND ca.id != ? GROUP BY dc.Id, dc.DateDebut, dc.DateFin, dc.Statut, dc.DateDemande, dc.Commentaire, dc.CollaborateurADId, ca.prenom, ca.nom, ca.email ORDER BY dc.DateDemande ASC`, [serviceId, managerId]);
const requests = rows.map(row => ({ id: row.Id, employee_id: row.CollaborateurADId, employee_name: row.employee_name, employee_email: row.employee_email, type: row.types, start_date: row.DateDebut, end_date: row.DateFin, date_display: row.DateDebut === row.DateFin ? formatDate(row.DateDebut) : `${formatDate(row.DateDebut)} - ${formatDate(row.DateFin)}`, days: getWorkingDays(row.DateDebut, row.DateFin), status: row.Statut, reason: row.Commentaire || '', submitted_at: row.DateDemande, submitted_display: formatDate(row.DateDemande) }));
res.json({ success: true, message: 'Demandes récupérées', requests, service_id: serviceId });
} catch (error) {
res.status(500).json({ success: false, message: 'Erreur', error: error.message });
}
});
app.get('/api/getTeamMembers', async (req, res) => {
try {
const managerId = req.query.manager_id;
if (!managerId) return res.json({ success: false, message: 'ID manager manquant' });
const [managerRows] = await pool.query('SELECT ServiceId, CampusId FROM CollaborateurAD WHERE id = ?', [managerId]);
if (managerRows.length === 0) return res.json({ success: false, message: 'Manager non trouvé' });
const serviceId = managerRows[0].ServiceId;
const [members] = await pool.query(`SELECT c.id, c.nom, c.prenom, c.email, c.role, s.Nom as service_name, c.CampusId FROM CollaborateurAD c JOIN Services s ON c.ServiceId = s.Id WHERE c.ServiceId = ? AND c.id != ? ORDER BY c.prenom, c.nom`, [serviceId, managerId]);
res.json({ success: true, message: 'Équipe récupérée', team_members: members, service_id: serviceId });
} catch (error) {
res.status(500).json({ success: false, message: 'Erreur', error: error.message });
}
});
app.get('/api/getNotifications', async (req, res) => {
try {
const userIdParam = req.query.user_id;
if (!userIdParam) {
return res.json({ success: false, message: 'ID utilisateur manquant' });
}
const conn = await pool.getConnection();
// ✅ Déterminer si c'est un UUID ou un ID numérique
const isUUID = userIdParam.length > 10 && userIdParam.includes('-');
// ✅ Récupérer l'ID numérique si on a un UUID
let userId = userIdParam;
if (isUUID) {
const [userRows] = await conn.query(
'SELECT id, CampusId FROM CollaborateurAD WHERE entraUserId = ? AND (Actif = 1 OR Actif IS NULL)',
[userIdParam]
);
if (userRows.length === 0) {
conn.release();
return res.json({
success: false,
message: 'Utilisateur non trouvé ou compte désactivé'
});
}
userId = userRows[0].id;
} else {
userId = parseInt(userIdParam);
}
// ✅ Utiliser l'ID numérique pour la requête
const [notifications] = await conn.query(`
SELECT * FROM Notifications
WHERE CollaborateurADId = ?
ORDER BY DateCreation DESC
LIMIT 50
`, [userId]);
conn.release();
res.json({
success: true,
notifications: notifications || []
});
} catch (error) {
console.error('Erreur getNotifications:', error);
res.status(500).json({
success: false,
message: 'Erreur serveur',
error: error.message
});
}
});
app.post('/api/markNotificationRead', async (req, res) => {
try {
const { notificationId } = req.body;
if (!notificationId || notificationId <= 0) return res.status(400).json({ success: false, message: 'ID notification invalide' });
await pool.query('UPDATE Notifications SET lu = 1 WHERE Id = ?', [notificationId]);
res.json({ success: true, message: 'Notification marquée comme lue' });
} catch (error) {
res.status(500).json({ success: false, message: 'Erreur', error: error.message });
}
});
// À ajouter avant app.listen()
/**
* POST /saisirRecupJour
* Saisir une journée de récupération (samedi travaillé)
*/
app.post('/api/saisirRecupJour', async (req, res) => {
const conn = await pool.getConnection();
try {
await conn.beginTransaction();
const {
user_id,
date, // Date du samedi travaillé
nombre_heures = 1, // Par défaut 1 jour = 1 samedi
commentaire
} = req.body;
console.log('\n📝 === SAISIE RÉCUP ===');
console.log('User ID:', user_id);
console.log('Date:', date);
console.log('Heures:', nombre_heures);
if (!user_id || !date) {
await conn.rollback();
conn.release();
return res.json({
success: false,
message: 'Données manquantes'
});
}
// Vérifier que c'est bien un samedi
const dateObj = new Date(date);
const dayOfWeek = dateObj.getDay();
if (dayOfWeek !== 6) {
await conn.rollback();
conn.release();
return res.json({
success: false,
message: 'La récupération ne peut être saisie que pour un samedi'
});
}
// Vérifier que ce samedi n'a pas déjà été saisi
const [existing] = await conn.query(`
SELECT dc.Id
FROM DemandeConge dc
JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId
JOIN TypeConge tc ON dct.TypeCongeId = tc.Id
WHERE dc.CollaborateurADId = ?
AND dc.DateDebut = ?
AND tc.Nom = 'Récupération'
`, [user_id, date]);
if (existing.length > 0) {
await conn.rollback();
conn.release();
return res.json({
success: false,
message: 'Ce samedi a déjà été déclaré'
});
}
// Récupérer infos utilisateur
const [userInfo] = await conn.query(
'SELECT prenom, nom, email, CampusId FROM CollaborateurAD WHERE id = ?',
[user_id]
);
if (userInfo.length === 0) {
await conn.rollback();
conn.release();
return res.json({
success: false,
message: 'Utilisateur non trouvé'
});
}
const user = userInfo[0];
const userName = `${user.prenom} ${user.nom}`;
const dateFormatted = dateObj.toLocaleDateString('fr-FR');
// Récupérer le type Récupération
const [recupType] = await conn.query(
'SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1',
['Récupération']
);
if (recupType.length === 0) {
await conn.rollback();
conn.release();
return res.json({
success: false,
message: 'Type Récupération non trouvé'
});
}
const recupTypeId = recupType[0].Id;
const currentYear = dateObj.getFullYear();
// CRÉER LA DEMANDE (validée automatiquement)
const [result] = await conn.query(`
INSERT INTO DemandeConge
(CollaborateurADId, DateDebut, DateFin, TypeCongeId,
Statut, DateDemande, Commentaire, NombreJours)
VALUES (?, ?, ?, ?, 'Validée', NOW(), ?, ?)
`, [user_id, date, date, recupTypeId, commentaire || `Samedi travaillé - ${dateFormatted}`, nombre_heures]);
const demandeId = result.insertId;
// SAUVEGARDER DANS DemandeCongeType
await conn.query(`
INSERT INTO DemandeCongeType
(DemandeCongeId, TypeCongeId, NombreJours, PeriodeJournee)
VALUES (?, ?, ?, 'Journée entière')
`, [demandeId, recupTypeId, nombre_heures]);
// ACCUMULER DANS LE COMPTEUR
const [compteur] = await conn.query(`
SELECT Id, Total, Solde
FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
`, [user_id, recupTypeId, currentYear]);
if (compteur.length > 0) {
await conn.query(`
UPDATE CompteurConges
SET Total = Total + ?,
Solde = Solde + ?,
DerniereMiseAJour = NOW()
WHERE Id = ?
`, [nombre_heures, nombre_heures, compteur[0].Id]);
console.log(`✅ Compteur mis à jour: ${parseFloat(compteur[0].Solde) + nombre_heures}j`);
} else {
await conn.query(`
INSERT INTO CompteurConges
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour)
VALUES (?, ?, ?, ?, ?, 0, NOW())
`, [user_id, recupTypeId, currentYear, nombre_heures, nombre_heures]);
console.log(`✅ Compteur créé: ${nombre_heures}j`);
}
// ENREGISTRER L'ACCUMULATION
await conn.query(`
INSERT INTO DeductionDetails
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
VALUES (?, ?, ?, 'Accum Récup', ?)
`, [demandeId, recupTypeId, currentYear, nombre_heures]);
// CRÉER NOTIFICATION
await conn.query(`
INSERT INTO Notifications
(CollaborateurADId, Type, Titre, Message, DemandeCongeId, DateCreation, Lu)
VALUES (?, 'Success', '✅ Récupération enregistrée', ?, ?, NOW(), 0)
`, [
user_id,
`Samedi ${dateFormatted} enregistré : +${nombre_heures}j de récupération`,
demandeId
]);
await conn.commit();
conn.release();
res.json({
success: true,
message: `Samedi ${dateFormatted} enregistré`,
jours_ajoutes: nombre_heures,
demande_id: demandeId
});
} catch (error) {
await conn.rollback();
if (conn) conn.release();
console.error('❌ Erreur saisie récup:', error);
res.status(500).json({
success: false,
message: 'Erreur serveur',
error: error.message
});
}
});
/**
* GET /getMesSamedis
* Récupérer les samedis déjà déclarés
*/
app.get('/api/getMesSamedis', async (req, res) => {
try {
const { user_id, annee } = req.query;
const conn = await pool.getConnection();
const [samedis] = await conn.query(`
SELECT
dc.Id,
DATE_FORMAT(dc.DateDebut, '%Y-%m-%d') as date,
dc.NombreJours as jours,
dc.Commentaire as commentaire,
DATE_FORMAT(dc.DateDemande, '%d/%m/%Y à %H:%i') as date_saisie
FROM DemandeConge dc
JOIN TypeConge tc ON dc.TypeCongeId = tc.Id
WHERE dc.CollaborateurADId = ?
AND tc.Nom = 'Récupération'
AND YEAR(dc.DateDebut) = ?
ORDER BY dc.DateDebut DESC
`, [user_id, annee]);
conn.release();
res.json({
success: true,
samedis: samedis
});
} catch (error) {
console.error('Erreur getMesSamedis:', error);
res.status(500).json({
success: false,
message: 'Erreur serveur',
error: error.message
});
}
});
async function checkLeaveBalanceWithAnticipation(conn, collaborateurId, repartition, dateDebut) {
const dateDebutObj = new Date(dateDebut);
const currentYear = dateDebutObj.getFullYear();
const previousYear = currentYear - 1;
console.log('\n🔍 === CHECK SOLDES AVEC ANTICIPATION ===');
console.log(`📅 Date demande: ${dateDebut}`);
console.log(`📅 Année demande: ${currentYear}`);
const verification = [];
for (const rep of repartition) {
const typeCode = rep.TypeConge;
const joursNecessaires = parseFloat(rep.NombreJours || 0);
if (typeCode === 'ABS' || typeCode === 'Formation') {
continue;
}
const typeName = typeCode === 'CP' ? 'Congé payé' : typeCode === 'RTT' ? 'RTT' : typeCode;
const [typeRow] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', [typeName]);
if (typeRow.length === 0) {
continue;
}
const typeCongeId = typeRow[0].Id;
// ====================================
// 1⃣ Récupérer les infos du collaborateur
// ====================================
const [collabInfo] = await conn.query(`
SELECT DateEntree, TypeContrat, role
FROM CollaborateurAD
WHERE id = ?
`, [collaborateurId]);
const dateEntree = collabInfo[0]?.DateEntree || null;
const typeContrat = collabInfo[0]?.TypeContrat || '37h';
const isApprenti = collabInfo[0]?.role === 'Apprenti';
// ====================================
// 2⃣ Calculer l'acquisition à la date de la demande
// ====================================
let acquisALaDate = 0;
let budgetAnnuel = 0;
if (typeCode === 'CP') {
acquisALaDate = calculerAcquisitionCP(dateDebutObj, dateEntree);
budgetAnnuel = 25;
console.log(`💰 Acquisition CP à la date ${dateDebut}: ${acquisALaDate.toFixed(2)}j`);
} else if (typeCode === 'RTT' && !isApprenti) {
const rttData = await calculerAcquisitionRTT(conn, collaborateurId, dateDebutObj);
acquisALaDate = rttData.acquisition;
budgetAnnuel = rttData.config.joursAnnuels;
console.log(`💰 Acquisition RTT à la date ${dateDebut}: ${acquisALaDate.toFixed(2)}j`);
}
// ====================================
// 3⃣ Récupérer le report N-1 (CP uniquement)
// ====================================
let reporteN1 = 0;
if (typeCode === 'CP') {
const [compteurN1] = await conn.query(`
SELECT Solde, SoldeReporte
FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
`, [collaborateurId, typeCongeId, previousYear]);
if (compteurN1.length > 0) {
reporteN1 = parseFloat(compteurN1[0].Solde || 0);
}
}
// ====================================
// 4⃣ Calculer ce qui a déjà été posé (SANS l'anticipé)
// ====================================
const [dejaPose] = await conn.query(`
SELECT COALESCE(SUM(dd.JoursUtilises), 0) as total
FROM DeductionDetails dd
JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
WHERE dc.CollaborateurADId = ?
AND dd.TypeCongeId = ?
AND dd.Annee = ?
AND dd.TypeDeduction NOT IN ('N Anticip', 'N+1 Anticip', 'Accum Récup', 'Accum Recup')
AND dc.Statut != 'Refusée'
`, [collaborateurId, typeCongeId, currentYear]);
const dejaPoseNormal = parseFloat(dejaPose[0]?.total || 0);
// ====================================
// 5⃣ Calculer l'anticipé déjà utilisé
// ====================================
const [anticipeUtilise] = await conn.query(`
SELECT COALESCE(SUM(dd.JoursUtilises), 0) as total
FROM DeductionDetails dd
JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
WHERE dc.CollaborateurADId = ?
AND dd.TypeCongeId = ?
AND dd.Annee = ?
AND dd.TypeDeduction = 'N Anticip'
AND dc.Statut != 'Refusée'
`, [collaborateurId, typeCongeId, currentYear]);
const dejaPoseAnticipe = parseFloat(anticipeUtilise[0]?.total || 0);
// ====================================
// 6⃣ Calculer l'anticipé disponible
// ====================================
const anticipableMax = Math.max(0, budgetAnnuel - acquisALaDate);
const anticipeDisponible = Math.max(0, anticipableMax - dejaPoseAnticipe);
console.log(`💳 Anticipé max possible: ${anticipableMax.toFixed(2)}j`);
console.log(`💳 Anticipé déjà utilisé: ${dejaPoseAnticipe.toFixed(2)}j`);
console.log(`💳 Anticipé disponible: ${anticipeDisponible.toFixed(2)}j`);
// ====================================
// 7⃣ Calculer le solde TOTAL disponible
// ====================================
const soldeActuel = Math.max(0, reporteN1 + acquisALaDate - dejaPoseNormal);
const soldeTotal = soldeActuel + anticipeDisponible;
console.log(`📊 Soldes détaillés ${typeCode}:`);
console.log(` - Report N-1: ${reporteN1.toFixed(2)}j`);
console.log(` - Acquis à date: ${acquisALaDate.toFixed(2)}j`);
console.log(` - Déjà posé (normal): ${dejaPoseNormal.toFixed(2)}j`);
console.log(` - Solde actuel: ${soldeActuel.toFixed(2)}j`);
console.log(` - Anticipé disponible: ${anticipeDisponible.toFixed(2)}j`);
console.log(` ✅ TOTAL DISPONIBLE: ${soldeTotal.toFixed(2)}j`);
// ====================================
// 8⃣ Vérifier la suffisance
// ====================================
const suffisant = soldeTotal >= joursNecessaires;
const deficit = Math.max(0, joursNecessaires - soldeTotal);
verification.push({
type: typeName,
joursNecessaires,
reporteN1,
acquisALaDate,
dejaPoseNormal,
dejaPoseAnticipe,
soldeActuel,
anticipeDisponible,
soldeTotal,
suffisant,
deficit
});
console.log(`🔍 Vérification ${typeCode}: ${joursNecessaires}j demandés vs ${soldeTotal.toFixed(2)}j disponibles → ${suffisant ? '✅ OK' : '❌ INSUFFISANT'}`);
}
const insuffisants = verification.filter(v => !v.suffisant);
return {
valide: insuffisants.length === 0,
details: verification,
insuffisants
};
}
/**
* Déduit les jours d'un compteur avec gestion de l'anticipation
* Ordre de déduction : N-1 → N → N Anticip
*/
async function deductLeaveBalanceWithAnticipation(conn, collaborateurId, typeCongeId, nombreJours, demandeCongeId, dateDebut) {
const dateDebutObj = new Date(dateDebut);
const currentYear = dateDebutObj.getFullYear();
const previousYear = currentYear - 1;
let joursRestants = nombreJours;
const deductions = [];
console.log(`\n💳 === DÉDUCTION AVEC ANTICIPATION ===`);
console.log(` Collaborateur: ${collaborateurId}`);
console.log(` Type congé: ${typeCongeId}`);
console.log(` Jours à déduire: ${nombreJours}j`);
console.log(` Date début: ${dateDebut}`);
// ====================================
// 1⃣ Déduire du REPORT N-1 (CP uniquement)
// ====================================
const [compteurN1] = await conn.query(`
SELECT Id, Solde, SoldeReporte
FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
`, [collaborateurId, typeCongeId, previousYear]);
if (compteurN1.length > 0) {
const soldeN1 = parseFloat(compteurN1[0].Solde || 0);
const aDeduireN1 = Math.min(soldeN1, joursRestants);
if (aDeduireN1 > 0) {
await conn.query(`
UPDATE CompteurConges
SET Solde = GREATEST(0, Solde - ?),
SoldeReporte = GREATEST(0, SoldeReporte - ?),
DerniereMiseAJour = NOW()
WHERE Id = ?
`, [aDeduireN1, aDeduireN1, compteurN1[0].Id]);
await conn.query(`
INSERT INTO DeductionDetails
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
VALUES (?, ?, ?, 'Année N-1', ?)
`, [demandeCongeId, typeCongeId, previousYear, aDeduireN1]);
deductions.push({
annee: previousYear,
type: 'Report N-1',
joursUtilises: aDeduireN1,
soldeAvant: soldeN1
});
joursRestants -= aDeduireN1;
console.log(` ✓ Déduit ${aDeduireN1.toFixed(2)}j du report N-1`);
}
}
// ====================================
// 2⃣ Déduire du SOLDE N (acquis actuel)
// ====================================
if (joursRestants > 0) {
const [compteurN] = await conn.query(`
SELECT Id, Solde, SoldeReporte, Total
FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
`, [collaborateurId, typeCongeId, currentYear]);
if (compteurN.length > 0) {
const soldeTotal = parseFloat(compteurN[0].Solde || 0);
const soldeReporte = parseFloat(compteurN[0].SoldeReporte || 0);
const soldeN = Math.max(0, soldeTotal - soldeReporte); // Solde actuel sans le report
const aDeduireN = Math.min(soldeN, joursRestants);
if (aDeduireN > 0) {
await conn.query(`
UPDATE CompteurConges
SET Solde = GREATEST(0, Solde - ?),
DerniereMiseAJour = NOW()
WHERE Id = ?
`, [aDeduireN, compteurN[0].Id]);
await conn.query(`
INSERT INTO DeductionDetails
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
VALUES (?, ?, ?, 'Année N', ?)
`, [demandeCongeId, typeCongeId, currentYear, aDeduireN]);
deductions.push({
annee: currentYear,
type: 'Année N',
joursUtilises: aDeduireN,
soldeAvant: soldeN
});
joursRestants -= aDeduireN;
console.log(` ✓ Déduit ${aDeduireN.toFixed(2)}j du solde N actuel`);
}
}
}
// ====================================
// 3⃣ Déduire de l'ANTICIPÉ N (ce qui reste à acquérir)
// ====================================
if (joursRestants > 0) {
console.log(` 💳 Il reste ${joursRestants.toFixed(2)}j à déduire → Utilisation de l'anticipé`);
// Récupérer les infos pour calculer l'anticipé disponible
const [collabInfo] = await conn.query(`
SELECT DateEntree, TypeContrat, role
FROM CollaborateurAD
WHERE id = ?
`, [collaborateurId]);
const dateEntree = collabInfo[0]?.DateEntree || null;
const typeContrat = collabInfo[0]?.TypeContrat || '37h';
const isApprenti = collabInfo[0]?.role === 'Apprenti';
// Déterminer le type de congé
const [typeInfo] = await conn.query('SELECT Nom FROM TypeConge WHERE Id = ?', [typeCongeId]);
const typeNom = typeInfo[0]?.Nom || '';
let acquisALaDate = 0;
let budgetAnnuel = 0;
if (typeNom === 'Congé payé') {
acquisALaDate = calculerAcquisitionCP(dateDebutObj, dateEntree);
budgetAnnuel = 25;
} else if (typeNom === 'RTT' && !isApprenti) {
const rttData = await calculerAcquisitionRTT(conn, collaborateurId, dateDebutObj);
acquisALaDate = rttData.acquisition;
budgetAnnuel = rttData.config.joursAnnuels;
}
// Calculer l'anticipé disponible
const anticipableMax = Math.max(0, budgetAnnuel - acquisALaDate);
// Vérifier combien a déjà été pris en anticipé
const [anticipeUtilise] = await conn.query(`
SELECT COALESCE(SUM(dd.JoursUtilises), 0) as total
FROM DeductionDetails dd
JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
WHERE dc.CollaborateurADId = ?
AND dd.TypeCongeId = ?
AND dd.Annee = ?
AND dd.TypeDeduction = 'N Anticip'
AND dc.Statut != 'Refusée'
AND dc.Id != ?
`, [collaborateurId, typeCongeId, currentYear, demandeCongeId]);
const dejaPrisAnticipe = parseFloat(anticipeUtilise[0]?.total || 0);
const anticipeDisponible = Math.max(0, anticipableMax - dejaPrisAnticipe);
console.log(` 💳 Anticipé max: ${anticipableMax.toFixed(2)}j`);
console.log(` 💳 Déjà pris: ${dejaPrisAnticipe.toFixed(2)}j`);
console.log(` 💳 Disponible: ${anticipeDisponible.toFixed(2)}j`);
const aDeduireAnticipe = Math.min(anticipeDisponible, joursRestants);
if (aDeduireAnticipe > 0) {
// Enregistrer la déduction anticipée
await conn.query(`
INSERT INTO DeductionDetails
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
VALUES (?, ?, ?, 'N Anticip', ?)
`, [demandeCongeId, typeCongeId, currentYear, aDeduireAnticipe]);
// Mettre à jour SoldeAnticipe dans CompteurConges
await conn.query(`
UPDATE CompteurConges
SET SoldeAnticipe = GREATEST(0, ? - ?),
DerniereMiseAJour = NOW()
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
`, [anticipeDisponible, aDeduireAnticipe, collaborateurId, typeCongeId, currentYear]);
deductions.push({
annee: currentYear,
type: 'N Anticip',
joursUtilises: aDeduireAnticipe,
soldeAvant: anticipeDisponible
});
joursRestants -= aDeduireAnticipe;
console.log(` ✓ Déduit ${aDeduireAnticipe.toFixed(2)}j de l'anticipé N`);
} else if (joursRestants > 0) {
console.error(` ❌ Impossible de déduire ${joursRestants.toFixed(2)}j : anticipé épuisé !`);
}
}
console.log(` ✅ Déduction terminée - Total déduit: ${(nombreJours - joursRestants).toFixed(2)}j\n`);
return {
success: joursRestants === 0,
joursDeduitsTotal: nombreJours - joursRestants,
joursNonDeduits: joursRestants,
details: deductions
};
}
app.post('/api/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), async (req, res) => {
const conn = await pool.getConnection();
try {
await conn.beginTransaction();
const currentYear = new Date().getFullYear();
// ✅ Récupérer les fichiers uploadés
const uploadedFiles = req.files || [];
console.log('📎 Fichiers médicaux reçus:', uploadedFiles.length);
// ✅ Les données arrivent différemment avec FormData
const { DateDebut, DateFin, NombreJours, Email, Nom, Commentaire, statut } = req.body;
// ✅ Parser la répartition (elle arrive en string depuis FormData)
const Repartition = JSON.parse(req.body.Repartition || '[]');
if (!DateDebut || !DateFin || !Repartition || !Email || !Nom) {
uploadedFiles.forEach(file => {
if (fs.existsSync(file.path)) {
fs.unlinkSync(file.path);
}
});
return res.json({ success: false, message: 'Données manquantes' });
}
// ✅ Validation : Si arrêt maladie, il faut au moins 1 fichier
const hasABS = Repartition.some(r => r.TypeConge === 'ABS');
if (hasABS && uploadedFiles.length === 0) {
await conn.rollback();
conn.release();
return res.json({
success: false,
message: 'Un justificatif médical est obligatoire pour un arrêt maladie'
});
}
// ⭐ VALIDATION DE LA RÉPARTITION
console.log('\n📥 === SOUMISSION DEMANDE CONGÉ ===');
console.log('Email:', Email);
console.log('Période:', DateDebut, '→', DateFin);
console.log('Nombre de jours total:', NombreJours);
console.log('Répartition reçue:', JSON.stringify(Repartition, null, 2));
// ⭐ Ne compter que CP, RTT ET RÉCUP dans la répartition
const sommeRepartition = Repartition.reduce((sum, r) => {
if (r.TypeConge === 'CP' || r.TypeConge === 'RTT' || r.TypeConge === 'Récup') {
return sum + parseFloat(r.NombreJours || 0);
}
return sum;
}, 0);
console.log('Somme répartition CP+RTT+Récup:', sommeRepartition.toFixed(2));
// ⭐ VALIDATION : La somme doit correspondre au total
const hasCountableLeave = Repartition.some(r =>
r.TypeConge === 'CP' || r.TypeConge === 'RTT' || r.TypeConge === 'Récup'
);
if (hasCountableLeave && Math.abs(sommeRepartition - NombreJours) > 0.01) {
console.error('❌ ERREUR : Répartition incohérente !');
uploadedFiles.forEach(file => {
if (fs.existsSync(file.path)) {
fs.unlinkSync(file.path);
}
});
await conn.rollback();
conn.release();
return res.json({
success: false,
message: `Erreur de répartition : la somme (${sommeRepartition.toFixed(2)}j) ne correspond pas au total (${NombreJours}j)`
});
}
console.log('✅ Validation répartition OK');
// ⭐ Récup n'est PAS une demande auto-validée
const isFormationOnly = Repartition.length === 1 && Repartition[0].TypeConge === 'Formation';
const statutDemande = statut || (isFormationOnly ? 'Validée' : 'En attente');
console.log('🔍 Type de demande:', { isFormationOnly, statut: statutDemande });
const [collabAD] = await conn.query('SELECT id, CampusId FROM CollaborateurAD WHERE email = ? LIMIT 1', [Email]);
const isAD = collabAD.length > 0;
const collaborateurId = isAD ? collabAD[0].id : null;
let employeeId = null;
if (!isAD) {
const [user] = await conn.query('SELECT ID FROM Users WHERE Email = ? LIMIT 1', [Email]);
if (user.length === 0) {
uploadedFiles.forEach(file => {
if (fs.existsSync(file.path)) {
fs.unlinkSync(file.path);
}
});
await conn.rollback();
conn.release();
return res.json({ success: false, message: 'Utilisateur non trouvé' });
}
employeeId = user[0].ID;
}
// ========================================
// ÉTAPE 1 : Vérification des soldes AVANT tout (MODE MIXTE AVEC ANTICIPATION N+1)
// ========================================
if (isAD && collaborateurId && !isFormationOnly) {
console.log('\n🔍 Vérification des soldes en mode mixte avec anticipation...');
console.log('Date début:', DateDebut);
const [userRole] = await conn.query('SELECT role FROM CollaborateurAD WHERE id = ?', [collaborateurId]);
const isApprenti = userRole.length > 0 && userRole[0].role === 'Apprenti';
// ⭐ CORRECTION : Passer la date de début pour détecter N+1
// ✅ APRÈS (avec anticipation)
const checkResult = await checkLeaveBalanceWithAnticipation(
conn,
collaborateurId,
Repartition,
DateDebut
);
// Adapter le format de la réponse
if (!checkResult.valide) {
uploadedFiles.forEach(file => {
if (fs.existsSync(file.path)) fs.unlinkSync(file.path);
});
await conn.rollback();
conn.release();
// Construire le message d'erreur
const messagesErreur = checkResult.insuffisants.map(ins => {
return `${ins.type}: ${ins.joursNecessaires}j demandés mais seulement ${ins.soldeTotal.toFixed(2)}j disponibles (déficit: ${ins.deficit.toFixed(2)}j)`;
}).join('\n');
return res.json({
success: false,
message: `❌ Solde(s) insuffisant(s):\n${messagesErreur}`,
details: checkResult.details,
insuffisants: checkResult.insuffisants
});
}
console.log('✅ Tous les soldes sont suffisants (incluant anticipation si nécessaire)\n');
}
// ========================================
// ÉTAPE 2 : CRÉER LA DEMANDE
// ========================================
console.log('\n📝 Création de la demande...');
const typeIds = [];
for (const rep of Repartition) {
const code = rep.TypeConge;
// Ne pas inclure ABS et Formation dans les typeIds principaux
if (code === 'ABS' || code === 'Formation') {
continue;
}
const name = code === 'CP' ? 'Congé payé' :
code === 'RTT' ? 'RTT' :
code === 'Récup' ? 'Récupération' : code;
const [typeRow] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', [name]);
if (typeRow.length > 0) typeIds.push(typeRow[0].Id);
}
// Si aucun type CP/RTT/Récup, prendre le premier type de la répartition
if (typeIds.length === 0) {
const firstType = Repartition[0]?.TypeConge;
const name = firstType === 'Formation' ? 'Formation' :
firstType === 'ABS' ? 'Congé maladie' : 'Congé payé';
const [typeRow] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', [name]);
if (typeRow.length > 0) {
typeIds.push(typeRow[0].Id);
} else {
uploadedFiles.forEach(file => {
if (fs.existsSync(file.path)) fs.unlinkSync(file.path);
});
await conn.rollback();
conn.release();
return res.json({ success: false, message: 'Aucun type de congé valide' });
}
}
const typeCongeIdCsv = typeIds.join(',');
const currentDate = new Date().toISOString().slice(0, 19).replace('T', ' ');
// ✅ CRÉER LA DEMANDE
const [result] = await conn.query(
`INSERT INTO DemandeConge
(EmployeeId, CollaborateurADId, DateDebut, DateFin, TypeCongeId, Statut, DateDemande, Commentaire, Validateur, NombreJours)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[isAD ? 0 : employeeId, collaborateurId, DateDebut, DateFin, typeCongeIdCsv, statutDemande, currentDate, Commentaire || '', '', NombreJours]
);
const demandeId = result.insertId;
console.log(`✅ Demande créée avec ID ${demandeId} - Statut: ${statutDemande}`);
// ========================================
// ÉTAPE 3 : Sauvegarder les fichiers médicaux
// ========================================
if (uploadedFiles.length > 0) {
console.log('\n📎 Sauvegarde des fichiers médicaux...');
for (const file of uploadedFiles) {
await conn.query(
`INSERT INTO DocumentsMedicaux
(DemandeCongeId, NomFichier, CheminFichier, TypeMime, TailleFichier, DateUpload)
VALUES (?, ?, ?, ?, ?, NOW())`,
[demandeId, file.originalname, file.path, file.mimetype, file.size]
);
console.log(`${file.originalname}`);
}
}
// ========================================
// ÉTAPE 4 : Sauvegarder la répartition
// ========================================
console.log('\n📊 Sauvegarde de la répartition en base...');
for (const rep of Repartition) {
const code = rep.TypeConge;
const name = code === 'CP' ? 'Congé payé' :
code === 'RTT' ? 'RTT' :
code === 'ABS' ? 'Congé maladie' :
code === 'Formation' ? 'Formation' :
code === 'Récup' ? 'Récupération' : code;
const [typeRow] = await conn.query(
'SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1',
[name]
);
if (typeRow.length > 0) {
await conn.query(
`INSERT INTO DemandeCongeType
(DemandeCongeId, TypeCongeId, NombreJours, PeriodeJournee)
VALUES (?, ?, ?, ?)`,
[
demandeId,
typeRow[0].Id,
rep.NombreJours,
rep.PeriodeJournee || 'Journée entière'
]
);
console.log(`${name}: ${rep.NombreJours}j (${rep.PeriodeJournee || 'Journée entière'})`);
}
}
// ========================================
// ÉTAPE 5 : Déduction des compteurs CP/RTT/RÉCUP (AVEC ANTICIPATION N+1)
// ========================================
if (isAD && collaborateurId && !isFormationOnly) {
console.log('\n📉 Déduction des compteurs (avec anticipation N+1)...');
for (const rep of Repartition) {
if (rep.TypeConge === 'ABS' || rep.TypeConge === 'Formation') {
console.log(`${rep.TypeConge} ignoré (pas de déduction)`);
continue;
}
// ⭐ TRAITEMENT SPÉCIAL POUR RÉCUP
if (rep.TypeConge === 'Récup') {
const [recupType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Récupération']);
if (recupType.length > 0) {
await conn.query(`
UPDATE CompteurConges
SET Solde = GREATEST(0, Solde - ?),
DerniereMiseAJour = NOW()
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
`, [rep.NombreJours, collaborateurId, recupType[0].Id, currentYear]);
await conn.query(`
INSERT INTO DeductionDetails
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
VALUES (?, ?, ?, 'Récup Posée', ?)
`, [demandeId, recupType[0].Id, currentYear, rep.NombreJours]);
console.log(` ✓ Récup: ${rep.NombreJours}j déduits`);
}
continue;
}
// ⭐ CP et RTT : AVEC ANTICIPATION N+1
const name = rep.TypeConge === 'CP' ? 'Congé payé' : 'RTT';
const [typeRow] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', [name]);
if (typeRow.length > 0) {
const result = await deductLeaveBalanceWithAnticipation(
conn,
collaborateurId,
typeRow[0].Id,
rep.NombreJours,
demandeId,
DateDebut
);
console.log(`${name}: ${rep.NombreJours}j déduits`);
if (result.details && result.details.length > 0) {
result.details.forEach(d => {
console.log(` - ${d.type} (${d.annee}): ${d.joursUtilises}j`);
});
}
}
}
await updateSoldeAnticipe(conn, collaborateurId);
console.log('✅ Déductions terminées\n');
}
// ========================================
// ÉTAPE 6 : Notifications (Formation uniquement)
// ========================================
const dateDebut = new Date(DateDebut).toLocaleDateString('fr-FR');
const dateFin = new Date(DateFin).toLocaleDateString('fr-FR');
const datesPeriode = dateDebut === dateFin ? dateDebut : `du ${dateDebut} au ${dateFin}`;
if (isFormationOnly && isAD && collaborateurId) {
await conn.query(
`INSERT INTO Notifications (CollaborateurADId, Type, Titre, Message, DemandeCongeId, DateCreation, Lu)
VALUES (?, ?, ?, ?, ?, NOW(), 0)`,
[
collaborateurId,
'Success',
'✅ Formation validée automatiquement',
`Votre période de formation ${datesPeriode} a été validée automatiquement.`,
demandeId
]
);
console.log('\n📬 Notification formation créée');
}
// ========================================
// ÉTAPE 7 : Récupérer les managers
// ========================================
let managers = [];
if (isAD) {
const [rows] = await conn.query(
`SELECT c.email FROM HierarchieValidationAD hv
JOIN CollaborateurAD c ON hv.SuperieurId = c.id
WHERE hv.CollaborateurId = ?`,
[collaborateurId]
);
managers = rows.map(r => r.email);
}
await conn.commit();
console.log('\n🎉 Transaction validée\n');
// ========================================
// ÉTAPE 8 : Notifier les clients SSE
// ========================================
if (isFormationOnly && isAD && collaborateurId) {
notifyCollabClients({
type: 'demande-validated',
demandeId: parseInt(demandeId),
statut: 'Validée',
timestamp: new Date().toISOString()
}, collaborateurId);
}
// ========================================
// ENVOI DES EMAILS
// ========================================
const accessToken = await getGraphToken();
if (accessToken) {
const fromEmail = 'gtanoreply@ensup.eu';
const typesConges = Repartition.map(rep => {
const typeNom = rep.TypeConge === 'CP' ? 'Congé payé' :
rep.TypeConge === 'RTT' ? 'RTT' :
rep.TypeConge === 'ABS' ? 'Congé maladie' :
rep.TypeConge === 'Formation' ? 'Formation' :
rep.TypeConge === 'Récup' ? 'Récupération' : rep.TypeConge;
return `${typeNom}: ${rep.NombreJours}j`;
}).join(' | ');
if (isFormationOnly) {
// Email formation
const subjectCollab = '✅ Formation enregistrée et validée';
const bodyCollab = `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<div style="background-color: #10b981; color: white; padding: 20px;">
<h2 style="margin: 0;">✅ Formation validée</h2>
</div>
<div style="padding: 20px;">
<p>Bonjour <strong>${Nom}</strong>,</p>
<p>Votre période de formation a été automatiquement validée.</p>
<p><strong>Période :</strong> ${datesPeriode}</p>
<p><strong>Durée :</strong> ${NombreJours} jour(s)</p>
</div>
</div>
`;
try {
await sendMailGraph(accessToken, fromEmail, Email, subjectCollab, bodyCollab);
} catch (mailError) {
console.error('❌ Erreur email:', mailError.message);
}
} else {
// ⭐ EMAIL NORMAL (incluant Récup)
const subjectCollab = '✅ Confirmation de réception de votre demande de congé';
const bodyCollab = `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<div style="background-color: #3b82f6; color: white; padding: 20px;">
<h2 style="margin: 0;">✅ Demande enregistrée</h2>
</div>
<div style="padding: 20px;">
<p>Bonjour <strong>${Nom}</strong>,</p>
<p>Votre demande de congé a bien été enregistrée.</p>
<p><strong>Type :</strong> ${typesConges}</p>
<p><strong>Période :</strong> ${datesPeriode}</p>
<p><strong>Durée :</strong> ${NombreJours} jour(s)</p>
</div>
</div>
`;
try {
await sendMailGraph(accessToken, fromEmail, Email, subjectCollab, bodyCollab);
} catch (mailError) {
console.error('❌ Erreur email:', mailError.message);
}
for (const managerEmail of managers) {
const subjectManager = `📋 Nouvelle demande de congé - ${Nom}`;
const bodyManager = `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<div style="background-color: #f59e0b; color: white; padding: 20px;">
<h2 style="margin: 0;">📋 Nouvelle demande</h2>
</div>
<div style="padding: 20px;">
<p><strong>${Nom}</strong> a soumis une nouvelle demande.</p>
<p><strong>Type :</strong> ${typesConges}</p>
<p><strong>Période :</strong> ${datesPeriode}</p>
</div>
</div>
`;
try {
await sendMailGraph(accessToken, fromEmail, managerEmail, subjectManager, bodyManager);
} catch (mailError) {
console.error('❌ Erreur email manager:', mailError.message);
}
}
}
}
res.json({
success: true,
message: isFormationOnly ? 'Formation enregistrée et validée automatiquement' : 'Demande soumise',
request_id: demandeId,
managers,
auto_validated: isFormationOnly,
files_uploaded: uploadedFiles.length
});
} catch (error) {
await conn.rollback();
if (req.files) {
req.files.forEach(file => {
if (fs.existsSync(file.path)) {
fs.unlinkSync(file.path);
console.log(`🗑️ Fichier supprimé: ${file.originalname}`);
}
});
}
console.error('\n❌ ERREUR submitLeaveRequest:', error);
res.status(500).json({
success: false,
message: 'Erreur serveur',
error: error.message
});
} finally {
conn.release();
}
});
app.get('/api/download-medical/:documentId', async (req, res) => {
try {
const { documentId } = req.params;
const conn = await pool.getConnection();
const [docs] = await conn.query(
'SELECT * FROM DocumentsMedicaux WHERE Id = ?',
[documentId]
);
conn.release();
if (docs.length === 0) {
return res.status(404).json({ success: false, message: 'Document non trouvé' });
}
const doc = docs[0];
if (!fs.existsSync(doc.CheminFichier)) {
return res.status(404).json({ success: false, message: 'Fichier introuvable' });
}
res.download(doc.CheminFichier, doc.NomFichier);
} catch (error) {
console.error('Erreur téléchargement:', error);
res.status(500).json({ success: false, message: 'Erreur serveur' });
}
});
// Récupérer les documents d'une demande
app.get('/api/medical-documents/:demandeId', async (req, res) => {
try {
const { demandeId } = req.params;
const conn = await pool.getConnection();
const [docs] = await conn.query(
`SELECT Id, NomFichier, TypeMime, TailleFichier, DateUpload
FROM DocumentsMedicaux
WHERE DemandeCongeId = ?
ORDER BY DateUpload DESC`,
[demandeId]
);
conn.release();
res.json({
success: true,
documents: docs.map(doc => ({
id: doc.Id,
nom: doc.NomFichier,
type: doc.TypeMime,
taille: doc.TailleFichier,
date: doc.DateUpload,
downloadUrl: `/download-medical/${doc.Id}`
}))
});
} catch (error) {
console.error('Erreur récupération documents:', error);
res.status(500).json({ success: false, message: 'Erreur serveur' });
}
});
app.post('/api/validateRequest', async (req, res) => {
const conn = await pool.getConnection();
try {
await conn.beginTransaction();
const { request_id, action, validator_id, comment } = req.body;
console.log(`\n🔍 Validation demande #${request_id} - Action: ${action}`);
if (!request_id || !action || !validator_id) {
return res.json({ success: false, message: 'Données manquantes' });
}
const [validator] = await conn.query('SELECT Id, prenom, nom, email, CampusId FROM CollaborateurAD WHERE Id = ?', [validator_id]);
if (validator.length === 0) {
throw new Error('Validateur introuvable');
}
// Récupérer les informations de la demande
const [requests] = await conn.query(
`SELECT
dc.Id,
dc.CollaborateurADId,
dc.TypeCongeId,
dc.NombreJours,
dc.DateDebut,
dc.DateFin,
dc.Commentaire,
dc.Statut,
ca.prenom,
ca.nom,
ca.email as collaborateur_email,
tc.Nom as TypeConge
FROM DemandeConge dc
JOIN TypeConge tc ON dc.TypeCongeId = tc.Id
LEFT JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.Id
WHERE dc.Id = ?
LIMIT 1`,
[request_id]
);
if (requests.length === 0) {
throw new Error('Demande non trouvée');
}
const request = requests[0];
// ⭐ DÉBOGAGE : Afficher TOUTES les données
console.log('\n=== DONNÉES RÉCUPÉRÉES ===');
console.log('request:', JSON.stringify(request, null, 2));
console.log('validator:', JSON.stringify(validator[0], null, 2));
console.log('========================\n');
if (request.Statut !== 'En attente') {
throw new Error(`La demande a déjà été traitée (Statut: ${request.Statut})`);
}
const newStatus = action === 'approve' ? 'Validée' : 'Refusée';
if (action === 'reject' && request.CollaborateurADId) {
console.log(`\n🔄 REFUS - Restauration des soldes...`);
const restoration = await restoreLeaveBalance(conn, request_id, request.CollaborateurADId);
console.log('Restauration:', restoration);
}
await conn.query(
`UPDATE DemandeConge SET Statut = ?, ValidateurId = ?, ValidateurADId = ?, DateValidation = NOW(), CommentaireValidation = ? WHERE Id = ?`,
[newStatus, validator_id, validator_id, comment || '', request_id]
);
const notifTitle = action === 'approve' ? 'Demande approuvée ✅' : 'Demande refusée ❌';
let notifMessage = `Votre demande a été ${action === 'approve' ? 'approuvée' : 'refusée'}`;
if (comment) notifMessage += ` (Commentaire: ${comment})`;
const notifType = action === 'approve' ? 'Success' : 'Error';
await conn.query(
'INSERT INTO Notifications (CollaborateurADId, Titre, Message, Type, DemandeCongeId, DateCreation, lu) VALUES (?, ?, ?, ?, ?, ?, 0)',
[request.CollaborateurADId, notifTitle, notifMessage, notifType, request_id, nowFR()]
);
await conn.commit();
// ⭐ TEST EMAIL
// ⭐ ENVOI EMAIL AVEC TEMPLATE PROFESSIONNEL
console.log('\n📧 === TENTATIVE ENVOI EMAIL ===');
console.log('1. Récupération token...');
const accessToken = await getGraphToken();
console.log('2. Token obtenu ?', accessToken ? 'OUI' : 'NON');
if (accessToken && request.collaborateur_email) {
const fromEmail = 'gtanoreply@ensup.eu';
const collaborateurNom = `${request.prenom} ${request.nom}`;
const validateurNom = `${validator[0].prenom} ${validator[0].nom}`;
console.log('3. Préparation email professionnel...');
console.log(' De:', fromEmail);
console.log(' À:', request.collaborateur_email);
console.log(' Collaborateur:', collaborateurNom);
console.log(' Validateur:', validateurNom);
const dateDebut = new Date(request.DateDebut).toLocaleDateString('fr-FR');
const dateFin = new Date(request.DateFin).toLocaleDateString('fr-FR');
const datesPeriode = dateDebut === dateFin ? dateDebut : `du ${dateDebut} au ${dateFin}`;
const subject = action === 'approve'
? '✅ Votre demande de congé a été approuvée'
: '❌ Votre demande de congé a été refusée';
const body = action === 'approve'
? `<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<div style="background-color: #10b981; color: white; padding: 20px; border-radius: 8px 8px 0 0;">
<h2 style="margin: 0;">✅ Demande approuvée</h2>
</div>
<div style="background-color: #f9fafb; padding: 20px; border: 1px solid #e5e7eb;">
<p style="font-size: 16px;">Bonjour <strong>${collaborateurNom}</strong>,</p>
<p>Votre demande de congé a été <strong style="color: #10b981;">approuvée</strong> par ${validateurNom}.</p>
<div style="background-color: white; border-left: 4px solid #10b981; padding: 15px; margin: 20px 0;">
<p><strong>Type :</strong> ${request.TypeConge}</p>
<p><strong>Période :</strong> ${datesPeriode}</p>
<p><strong>Durée :</strong> ${request.NombreJours} jour(s)</p>
${comment ? `<p><strong>Commentaire :</strong> ${comment}</p>` : ''}
</div>
<p style="color: #6b7280;">Vous pouvez consulter votre demande dans votre espace personnel.</p>
</div>
</div>`
: `<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<div style="background-color: #ef4444; color: white; padding: 20px; border-radius: 8px 8px 0 0;">
<h2 style="margin: 0;">❌ Demande refusée</h2>
</div>
<div style="background-color: #f9fafb; padding: 20px; border: 1px solid #e5e7eb;">
<p style="font-size: 16px;">Bonjour <strong>${collaborateurNom}</strong>,</p>
<p>Votre demande de congé a été <strong style="color: #ef4444;">refusée</strong> par ${validateurNom}.</p>
<div style="background-color: white; border-left: 4px solid #ef4444; padding: 15px; margin: 20px 0;">
<p><strong>Type :</strong> ${request.TypeConge}</p>
<p><strong>Période :</strong> ${datesPeriode}</p>
<p><strong>Durée :</strong> ${request.NombreJours} jour(s)</p>
${comment ? `<p><strong>Motif du refus :</strong> ${comment}</p>` : ''}
</div>
<p style="color: #6b7280;">Pour plus d'informations, contactez ${validateurNom}.</p>
</div>
</div>`;
console.log('4. Sujet:', subject);
console.log('5. Appel sendMailGraph...');
try {
await sendMailGraph(
accessToken,
fromEmail,
request.collaborateur_email,
subject,
body
);
console.log('✅✅✅ EMAIL ENVOYÉ AVEC SUCCÈS ! ✅✅✅');
} catch (mailError) {
console.error('❌❌❌ ERREUR ENVOI EMAIL ❌❌❌');
console.error('Message:', mailError.message);
console.error('Stack:', mailError.stack);
}
} else {
if (!accessToken) console.error('❌ Token manquant');
if (!request.collaborateur_email) console.error('❌ Email collaborateur manquant');
}
console.log('=== FIN ENVOI EMAIL ===\n');
// Notifier les clients SSE locaux
notifyCollabClients({
type: 'demande-validated',
demandeId: parseInt(request_id),
statut: newStatus,
timestamp: new Date().toISOString()
}, request.CollaborateurADId);
notifyCollabClients({
type: 'demande-list-updated',
action: 'validation',
demandeId: parseInt(request_id),
timestamp: new Date().toISOString()
});
// Envoyer webhook au serveur RH
try {
await webhookManager.sendWebhook(
WEBHOOKS.RH_URL,
EVENTS.DEMANDE_VALIDATED,
{
demandeId: parseInt(request_id),
statut: newStatus,
collaborateurId: request.CollaborateurADId,
validateurId: validator_id,
commentaire: comment
}
);
console.log('✅ Webhook envoyé au serveur RH');
} catch (webhookError) {
console.error('❌ Erreur envoi webhook (non bloquant):', webhookError.message);
}
res.json({
success: true,
message: `Demande ${action === 'approve' ? 'approuvée' : 'refusée'}`,
new_status: newStatus
});
console.log(`✅ Demande ${request_id} ${newStatus}\n`);
} catch (error) {
await conn.rollback();
console.error('\n❌ ERREUR lors de la validation:', error);
res.status(500).json({
success: false,
message: error.message
});
} finally {
conn.release();
}
});
app.get('/api/testRestoration', async (req, res) => {
const conn = await pool.getConnection();
try {
const { demande_id, collab_id } = req.query;
if (!demande_id || !collab_id) {
return res.json({
success: false,
message: 'Paramètres manquants: demande_id et collab_id requis'
});
}
// 1. Voir les déductions enregistrées
const [deductions] = await conn.query(
`SELECT dd.*, tc.Nom as TypeNom
FROM DeductionDetails dd
JOIN TypeConge tc ON dd.TypeCongeId = tc.Id
WHERE dd.DemandeCongeId = ?`,
[demande_id]
);
// 2. Voir l'état actuel des compteurs
const [compteurs] = await conn.query(
`SELECT cc.*, tc.Nom as TypeNom
FROM CompteurConges cc
JOIN TypeConge tc ON cc.TypeCongeId = tc.Id
WHERE cc.CollaborateurADId = ?
ORDER BY tc.Nom, cc.Annee DESC`,
[collab_id]
);
// 3. Calculer ce que devrait être la restauration
const planRestauration = deductions.map(d => ({
type: d.TypeNom,
annee: d.Annee,
typeDeduction: d.TypeDeduction,
joursARestorer: d.JoursUtilises,
action: d.TypeDeduction === 'Reporté N-1'
? 'Ajouter au SoldeReporte ET au Solde'
: 'Ajouter au Solde uniquement'
}));
conn.release();
res.json({
success: true,
demande_id: demande_id,
collaborateur_id: collab_id,
deductions_enregistrees: deductions,
compteurs_actuels: compteurs,
plan_restauration: planRestauration,
instructions: planRestauration.length === 0
? "❌ Aucune déduction trouvée - La demande a été créée avant l'installation du tracking"
: "✅ Déductions trouvées - La restauration devrait fonctionner"
});
} catch (error) {
conn.release();
res.status(500).json({
success: false,
error: error.message
});
}
});
function normalizeRole(role) {
if (!role) return null;
const roleLower = role.toLowerCase();
// Normaliser les variantes féminines et masculines
if (roleLower === 'collaboratrice') return 'collaborateur';
if (roleLower === 'validatrice') return 'validateur';
if (roleLower === 'directrice de campus') return 'directeur de campus';
if (roleLower === 'apprentie') return 'apprenti';
return roleLower;
}
app.get('/api/getSocietesByCampus', async (req, res) => {
try {
const { campusId } = req.query;
const conn = await pool.getConnection();
const [societes] = await conn.query(`
SELECT DISTINCT s.Id, s.Nom
FROM SocieteCampus sc
JOIN Societe s ON sc.SocieteId = s.Id
WHERE sc.CampusId = ?
ORDER BY
CASE WHEN s.Nom LIKE '%SOLUTION%' THEN 1 ELSE 2 END,
s.Nom
`, [campusId]);
conn.release();
res.json({
success: true,
societes: societes
});
} catch (error) {
console.error('Erreur getSocietesByCampus:', error);
res.status(500).json({ success: false, message: error.message });
}
});
// ⭐ NOUVELLE ROUTE HELPER : Récupérer les campus d'une société
app.get('/api/getCampusBySociete', async (req, res) => {
try {
const { societeId } = req.query;
const conn = await pool.getConnection();
const [campus] = await conn.query(`
SELECT DISTINCT c.Id, c.Nom, sc.Principal
FROM SocieteCampus sc
JOIN Campus c ON sc.CampusId = c.Id
WHERE sc.SocieteId = ?
ORDER BY
sc.Principal DESC, -- Principal en premier
c.Nom
`, [societeId]);
conn.release();
res.json({
success: true,
campus: campus,
isMultiCampus: campus.length > 1
});
} catch (error) {
console.error('Erreur getCampusBySociete:', error);
res.status(500).json({ success: false, message: error.message });
}
});
// ========================================
// ROUTE getTeamLeaves COMPLÈTE
// ========================================
app.get('/api/getTeamLeaves', async (req, res) => {
try {
let { user_id: userIdParam, role: roleParam, selectedCampus, selectedSociete, selectedService } = req.query;
console.log(`🔍 Paramètres reçus: user_id=${userIdParam}, role=${roleParam}, selectedCampus=${selectedCampus}`);
if (!userIdParam) {
return res.json({ success: false, message: 'ID utilisateur manquant' });
}
const conn = await pool.getConnection();
const isUUID = userIdParam.length > 10 && userIdParam.includes('-');
console.log(`📝 Type ID détecté: ${isUUID ? 'UUID' : 'Numérique'}`);
const userQuery = `
SELECT
ca.id,
ca.ServiceId,
ca.CampusId,
ca.SocieteId,
ca.email,
s.Nom AS serviceNom,
c.Nom AS campusNom,
so.Nom AS societeNom
FROM CollaborateurAD ca
LEFT JOIN Services s ON ca.ServiceId = s.Id
LEFT JOIN Campus c ON ca.CampusId = c.Id
LEFT JOIN Societe so ON ca.SocieteId = so.Id
WHERE ${isUUID ? 'ca.entraUserId' : 'ca.id'} = ?
LIMIT 1
`;
const [userRows] = await conn.query(userQuery, [userIdParam]);
if (!userRows || userRows.length === 0) {
conn.release();
return res.json({ success: false, message: 'Collaborateur non trouvé' });
}
const userInfo = userRows[0];
const serviceId = userInfo.ServiceId;
const campusId = userInfo.CampusId;
const societeId = userInfo.SocieteId;
const userEmail = userInfo.email;
const campusNom = userInfo.campusNom;
function normalizeRole(role) {
if (!role) return null;
const roleLower = role.toLowerCase();
if (roleLower === 'collaboratrice') return 'collaborateur';
if (roleLower === 'validatrice') return 'validateur';
if (roleLower === 'directrice de campus') return 'directeur de campus';
if (roleLower === 'apprentie') return 'apprenti';
return roleLower;
}
const roleOriginal = roleParam?.toLowerCase();
const role = normalizeRole(roleOriginal);
console.log(`👤 Utilisateur trouvé:`);
console.log(` - ID: ${userInfo.id}`);
console.log(` - Email: ${userEmail}`);
console.log(` - ServiceId: ${serviceId}`);
console.log(` - CampusId: ${campusId}`);
console.log(` - CampusNom: ${campusNom}`);
console.log(` - SocieteId: ${societeId}`);
console.log(` - Role normalisé: ${role}`);
let query, params;
const filters = {};
// ========================================
// CAS 1: PRESIDENT, ADMIN, RH
// ========================================
// ========================================
// CAS 1: PRESIDENT, ADMIN, RH, DIRECTEUR DE CAMPUS
// ========================================
if (role === 'president' || role === 'admin' || role === 'rh' || role === 'directeur de campus') {
console.log("CAS 1: President/Admin/RH/Directeur de Campus - Vue globale");
console.log(` Filtres reçus: Société=${selectedSociete}, Campus=${selectedCampus}, Service=${selectedService}`);
// ========================================
// 🔧 LISTE COMPLÈTE DES FILTRES DISPONIBLES
// ========================================
// 1⃣ SOCIÉTÉS (toutes disponibles)
const [societesList] = await conn.query(`
SELECT DISTINCT Nom
FROM Societe
ORDER BY Nom
`);
filters.societes = societesList.map(s => s.Nom);
console.log('📊 Sociétés disponibles:', filters.societes);
// 2⃣ CAMPUS (tous les campus, filtrés par société si nécessaire)
let campusQuery;
let campusParams = [];
if (selectedSociete && selectedSociete !== 'all') {
campusQuery = `
SELECT DISTINCT c.Nom
FROM Campus c
JOIN CollaborateurAD ca ON ca.CampusId = c.Id
JOIN Societe so ON ca.SocieteId = so.Id
WHERE so.Nom = ?
AND (ca.actif = 1 OR ca.actif IS NULL)
ORDER BY c.Nom
`;
campusParams = [selectedSociete];
} else {
campusQuery = `
SELECT DISTINCT Nom
FROM Campus
ORDER BY Nom
`;
}
const [campusList] = await conn.query(campusQuery, campusParams);
filters.campus = campusList.map(c => c.Nom);
console.log('📊 Campus disponibles:', filters.campus);
// ⭐ NOUVEAU : Pour directeur de campus, envoyer son campus par défaut
if (role === 'directeur de campus') {
filters.defaultCampus = campusNom; // Le campus du directeur
console.log('🏢 Campus par défaut pour directeur:', campusNom);
}
// 3⃣ SERVICES (filtrés selon société + campus)
let servicesQuery = `
SELECT DISTINCT s.Nom
FROM Services s
JOIN CollaborateurAD ca ON ca.ServiceId = s.Id
`;
let servicesJoins = [];
let servicesConditions = ['(ca.actif = 1 OR ca.actif IS NULL)'];
let servicesParams = [];
if (selectedSociete && selectedSociete !== 'all') {
servicesJoins.push('JOIN Societe so ON ca.SocieteId = so.Id');
servicesConditions.push('so.Nom = ?');
servicesParams.push(selectedSociete);
}
if (selectedCampus && selectedCampus !== 'all') {
servicesJoins.push('JOIN Campus c ON ca.CampusId = c.Id');
servicesConditions.push('c.Nom = ?');
servicesParams.push(selectedCampus);
}
if (servicesJoins.length > 0) {
servicesQuery += '\n' + servicesJoins.join('\n');
}
servicesQuery += `\nWHERE ${servicesConditions.join(' AND ')}\nORDER BY s.Nom`;
const [servicesList] = await conn.query(servicesQuery, servicesParams);
filters.services = servicesList.map(s => s.Nom);
// ========================================
// 🔧 LISTE DES EMPLOYÉS (avec filtres appliqués)
// ========================================
let employeesQuery = `
SELECT DISTINCT
CONCAT(ca.prenom, ' ', ca.nom) AS fullname,
c.Nom AS campusnom,
so.Nom AS societenom,
s.Nom AS servicenom
FROM CollaborateurAD ca
JOIN Services s ON ca.ServiceId = s.Id
JOIN Campus c ON ca.CampusId = c.Id
JOIN Societe so ON ca.SocieteId = so.Id
WHERE (ca.actif = 1 OR ca.actif IS NULL)
`;
let employeesConditions = [];
let employeesParams = [];
if (selectedSociete && selectedSociete !== 'all') {
employeesConditions.push('so.Nom = ?');
employeesParams.push(selectedSociete);
}
if (selectedCampus && selectedCampus !== 'all') {
employeesConditions.push('c.Nom = ?');
employeesParams.push(selectedCampus);
} else if (role === 'directeur de campus' && campusNom) {
// ⭐ NOUVEAU : Si directeur et pas de filtre campus, filtrer par son campus par défaut
employeesConditions.push('c.Nom = ?');
employeesParams.push(campusNom);
}
if (selectedService && selectedService !== 'all') {
employeesConditions.push('s.Nom = ?');
employeesParams.push(selectedService);
}
if (employeesConditions.length > 0) {
employeesQuery += ` AND ${employeesConditions.join(' AND ')}`;
}
employeesQuery += ` ORDER BY so.Nom, c.Nom, ca.prenom, ca.nom`;
const [employeesList] = await conn.query(employeesQuery, employeesParams);
filters.employees = employeesList.map(e => ({
name: e.fullname,
campus: e.campusnom,
societe: e.societenom,
service: e.servicenom
}));
console.log(`👥 Employés trouvés:`, filters.employees.length);
// ========================================
// 🔧 QUERY DES CONGÉS (avec filtres appliqués)
// ========================================
let whereConditions = [`dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente')`];
let whereParams = [];
if (selectedSociete && selectedSociete !== 'all') {
whereConditions.push('so.Nom = ?');
whereParams.push(selectedSociete);
}
if (selectedCampus && selectedCampus !== 'all') {
whereConditions.push('c.Nom = ?');
whereParams.push(selectedCampus);
} else if (role === 'directeur de campus' && campusNom) {
// ⭐ NOUVEAU : Si directeur et pas de filtre campus, filtrer par son campus par défaut
whereConditions.push('c.Nom = ?');
whereParams.push(campusNom);
}
if (selectedService && selectedService !== 'all') {
whereConditions.push('s.Nom = ?');
whereParams.push(selectedService);
}
query = `
SELECT
DATE_FORMAT(dc.DateDebut, '%Y-%m-%d') AS startdate,
DATE_FORMAT(dc.DateFin, '%Y-%m-%d') AS enddate,
CONCAT(ca.prenom, ' ', ca.nom) AS employeename,
GROUP_CONCAT(DISTINCT tc.Nom ORDER BY tc.Nom SEPARATOR ', ') AS type,
CONCAT(
'[',
GROUP_CONCAT(
JSON_OBJECT(
'type', tc.Nom,
'jours', dct.NombreJours,
'periode', COALESCE(dct.PeriodeJournee, 'Journée entière')
)
SEPARATOR ','
),
']'
) AS detailsconges,
MAX(tc.CouleurHex) AS color,
dc.Statut AS statut,
s.Nom AS servicenom,
c.Nom AS campusnom,
so.Nom AS societenom,
dc.NombreJours AS nombrejoursouvres
FROM DemandeConge dc
JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id
LEFT JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId
LEFT JOIN TypeConge tc ON dct.TypeCongeId = tc.Id
JOIN Services s ON ca.ServiceId = s.Id
JOIN Campus c ON ca.CampusId = c.Id
JOIN Societe so ON ca.SocieteId = so.Id
WHERE ${whereConditions.join(' AND ')}
GROUP BY dc.Id, dc.DateDebut, dc.DateFin, ca.prenom, ca.nom, dc.Statut, s.Nom, c.Nom, so.Nom, dc.NombreJours
ORDER BY so.Nom, c.Nom, dc.DateDebut ASC
`;
params = whereParams;
console.log(`🔍 Query finale WHERE:`, whereConditions.join(' AND '));
console.log(`🔍 Params:`, whereParams);
}
// ========================================
// CAS 3: COLLABORATEUR
// ========================================
// Dans la route /getTeamLeaves, modifiez la section CAS 3: COLLABORATEUR
// ========================================
// CAS 3: COLLABORATEUR
// ========================================
// ========================================
// CAS 3: COLLABORATEUR
// ========================================
else if (role === 'collaborateur' || role === 'validateur' || role === 'apprenti') {
console.log("CAS 3: Collaborateur/Apprenti avec filtres avancés");
const serviceNom = userInfo.serviceNom || 'Non défini';
const campusNom = userInfo.campusNom || 'Non défini';
const societeNom = userInfo.societeNom || 'Non défini';
console.log(`📍 Filtres reçus du frontend: Société=${selectedSociete}, Campus=${selectedCampus}, Service=${selectedService}`);
// ⭐ NOUVEAU : Si AUCUN filtre n'est envoyé (premier chargement), utiliser les valeurs par défaut
// Sinon, respecter EXACTEMENT ce que le frontend envoie (même "all")
const isFirstLoad = !selectedCampus && !selectedService && !selectedSociete;
if (isFirstLoad) {
console.log('🎯 Premier chargement : initialisation avec service par défaut');
selectedCampus = campusNom;
selectedService = serviceNom;
selectedSociete = societeNom;
}
// Si le frontend envoie "all", on garde "all" (ne pas forcer les valeurs par défaut)
console.log(`📍 Filtres appliqués finaux: Société=${selectedSociete}, Campus=${selectedCampus}, Service=${selectedService}`);
// ⭐ Construire les listes de filtres disponibles
// 1⃣ Sociétés disponibles (TOUTES)
const [societesList] = await conn.query(`
SELECT DISTINCT so.Nom
FROM Societe so
JOIN CollaborateurAD ca ON ca.SocieteId = so.Id
WHERE (ca.actif = 1 OR ca.actif IS NULL)
ORDER BY so.Nom
`);
filters.societes = societesList.map(s => s.Nom);
// 2⃣ Campus disponibles (filtrés par société si sélectionné)
let campusQuery = `
SELECT DISTINCT c.Nom
FROM Campus c
JOIN CollaborateurAD ca ON ca.CampusId = c.Id
WHERE (ca.actif = 1 OR ca.actif IS NULL)
`;
let campusParams = [];
if (selectedSociete && selectedSociete !== 'all') {
campusQuery += ` AND ca.SocieteId = (SELECT Id FROM Societe WHERE Nom = ? LIMIT 1)`;
campusParams.push(selectedSociete);
}
campusQuery += ` ORDER BY c.Nom`;
const [campusList] = await conn.query(campusQuery, campusParams);
filters.campus = campusList.map(c => c.Nom);
// 3⃣ Services disponibles (filtrés par société + campus)
let servicesQuery = `
SELECT DISTINCT s.Nom
FROM Services s
JOIN CollaborateurAD ca ON ca.ServiceId = s.Id
WHERE (ca.actif = 1 OR ca.actif IS NULL)
`;
let servicesParams = [];
if (selectedSociete && selectedSociete !== 'all') {
servicesQuery += ` AND ca.SocieteId = (SELECT Id FROM Societe WHERE Nom = ? LIMIT 1)`;
servicesParams.push(selectedSociete);
}
if (selectedCampus && selectedCampus !== 'all') {
servicesQuery += ` AND ca.CampusId = (SELECT Id FROM Campus WHERE Nom = ? LIMIT 1)`;
servicesParams.push(selectedCampus);
}
servicesQuery += ` ORDER BY s.Nom`;
const [servicesList] = await conn.query(servicesQuery, servicesParams);
filters.services = servicesList.map(s => s.Nom);
// ⭐ Envoyer les valeurs par défaut au frontend (pour initialisation)
filters.defaultCampus = campusNom;
filters.defaultService = serviceNom;
filters.defaultSociete = societeNom;
// ⭐ LISTE DES EMPLOYÉS (avec filtres conditionnels)
let employeesQuery = `
SELECT DISTINCT
CONCAT(ca.prenom, ' ', ca.nom) AS fullname,
c.Nom AS campusnom,
so.Nom AS societenom,
s.Nom AS servicenom
FROM CollaborateurAD ca
JOIN Services s ON ca.ServiceId = s.Id
JOIN Campus c ON ca.CampusId = c.Id
JOIN Societe so ON ca.SocieteId = so.Id
WHERE (ca.actif = 1 OR ca.actif IS NULL)
`;
let employeesParams = [];
let employeesConditions = [];
// ⭐ N'ajouter les filtres QUE si différents de "all"
if (selectedSociete && selectedSociete !== 'all') {
employeesConditions.push('so.Nom = ?');
employeesParams.push(selectedSociete);
}
if (selectedCampus && selectedCampus !== 'all') {
employeesConditions.push('c.Nom = ?');
employeesParams.push(selectedCampus);
}
if (selectedService && selectedService !== 'all') {
employeesConditions.push('s.Nom = ?');
employeesParams.push(selectedService);
}
if (employeesConditions.length > 0) {
employeesQuery += ` AND ${employeesConditions.join(' AND ')}`;
}
employeesQuery += ` ORDER BY s.Nom, ca.prenom, ca.nom`;
const [employeesList] = await conn.query(employeesQuery, employeesParams);
filters.employees = employeesList.map(emp => ({
name: emp.fullname,
campus: emp.campusnom,
societe: emp.societenom,
service: emp.servicenom
}));
console.log(`👥 Employés trouvés: ${filters.employees.length}`);
// ⭐ QUERY DES CONGÉS (avec mêmes filtres conditionnels)
let queryConditions = `WHERE dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente')`;
params = [];
let congesConditions = [];
// ⭐ N'ajouter les filtres QUE si différents de "all"
if (selectedSociete && selectedSociete !== 'all') {
congesConditions.push('so.Nom = ?');
params.push(selectedSociete);
}
if (selectedCampus && selectedCampus !== 'all') {
congesConditions.push('c.Nom = ?');
params.push(selectedCampus);
}
if (selectedService && selectedService !== 'all') {
congesConditions.push('s.Nom = ?');
params.push(selectedService);
}
if (congesConditions.length > 0) {
queryConditions += ` AND ${congesConditions.join(' AND ')}`;
}
query = `
SELECT
DATE_FORMAT(dc.DateDebut, '%Y-%m-%d') AS startdate,
DATE_FORMAT(dc.DateFin, '%Y-%m-%d') AS enddate,
CONCAT(ca.prenom, ' ', ca.nom) AS employeename,
GROUP_CONCAT(DISTINCT tc.Nom ORDER BY tc.Nom SEPARATOR ', ') AS type,
CONCAT(
'[',
GROUP_CONCAT(
JSON_OBJECT(
'type', tc.Nom,
'jours', dct.NombreJours,
'periode', COALESCE(dct.PeriodeJournee, 'Journée entière')
)
SEPARATOR ','
),
']'
) AS detailsconges,
MAX(tc.CouleurHex) AS color,
dc.Statut AS statut,
s.Nom AS servicenom,
c.Nom AS campusnom,
so.Nom AS societenom,
dc.NombreJours AS nombrejoursouvres
FROM DemandeConge dc
JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id
LEFT JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId
LEFT JOIN TypeConge tc ON dct.TypeCongeId = tc.Id
JOIN Services s ON ca.ServiceId = s.Id
JOIN Campus c ON ca.CampusId = c.Id
JOIN Societe so ON ca.SocieteId = so.Id
${queryConditions}
GROUP BY dc.Id, dc.DateDebut, dc.DateFin, ca.prenom, ca.nom, dc.Statut, s.Nom, c.Nom, so.Nom, dc.NombreJours
ORDER BY s.Nom, dc.DateDebut ASC
`;
console.log(`🔍 Query WHERE final:`, queryConditions);
console.log(`🔍 Params:`, params);
}
// ========================================
// CAS 4: AUTRES RÔLES
// ========================================
else {
console.log("CAS 4: Autres rôles");
if (!serviceId) {
conn.release();
return res.json({
success: false,
message: 'ServiceId manquant'
});
}
const [checkService] = await conn.query(`SELECT Nom FROM Services WHERE Id = ?`, [serviceId]);
const serviceNom = checkService.length > 0 ? checkService[0].Nom : "Inconnu";
const isAdminFinancier = serviceNom === "Administratif & Financier";
if (isAdminFinancier) {
const [employeesList] = await conn.query(`
SELECT DISTINCT
CONCAT(ca.prenom, ' ', ca.nom) AS fullname,
c.Nom AS campusnom,
so.Nom AS societenom,
s.Nom AS servicenom
FROM CollaborateurAD ca
JOIN Services s ON ca.ServiceId = s.Id
JOIN Campus c ON ca.CampusId = c.Id
JOIN Societe so ON ca.SocieteId = so.Id
WHERE ca.ServiceId = ?
AND (ca.actif = 1 OR ca.actif IS NULL)
ORDER BY ca.prenom, ca.nom
`, [serviceId]);
filters.employees = employeesList.map(e => ({
name: e.fullname,
campus: e.campusnom,
societe: e.societenom,
service: e.servicenom
}));
query = `
SELECT
DATE_FORMAT(dc.DateDebut, '%Y-%m-%d') AS startdate,
DATE_FORMAT(dc.DateFin, '%Y-%m-%d') AS enddate,
CONCAT(ca.prenom, ' ', ca.nom) AS employeename,
GROUP_CONCAT(DISTINCT tc.Nom ORDER BY tc.Nom SEPARATOR ', ') AS type,
CONCAT(
'[',
GROUP_CONCAT(
JSON_OBJECT(
'type', tc.Nom,
'jours', dct.NombreJours,
'periode', COALESCE(dct.PeriodeJournee, 'Journée entière')
)
SEPARATOR ','
),
']'
) AS detailsconges,
MAX(tc.CouleurHex) AS color,
dc.Statut AS statut,
s.Nom AS servicenom,
c.Nom AS campusnom,
so.Nom AS societenom,
dc.NombreJours AS nombrejoursouvres
FROM DemandeConge dc
JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id
LEFT JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId
LEFT JOIN TypeConge tc ON dct.TypeCongeId = tc.Id
JOIN Services s ON ca.ServiceId = s.Id
JOIN Campus c ON ca.CampusId = c.Id
JOIN Societe so ON ca.SocieteId = so.Id
WHERE dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente')
AND ca.ServiceId = ?
GROUP BY dc.Id, dc.DateDebut, dc.DateFin, ca.prenom, ca.nom, dc.Statut, s.Nom, c.Nom, so.Nom, dc.NombreJours
ORDER BY c.Nom, dc.DateDebut ASC
`;
params = [serviceId];
} else {
const [employeesList] = await conn.query(`
SELECT DISTINCT
CONCAT(ca.prenom, ' ', ca.nom) AS fullname,
c.Nom AS campusnom,
so.Nom AS societenom,
s.Nom AS servicenom
FROM CollaborateurAD ca
JOIN Services s ON ca.ServiceId = s.Id
JOIN Campus c ON ca.CampusId = c.Id
JOIN Societe so ON ca.SocieteId = so.Id
WHERE ca.ServiceId = ?
AND ca.CampusId = ?
AND (ca.actif = 1 OR ca.actif IS NULL)
ORDER BY ca.prenom, ca.nom
`, [serviceId, campusId]);
filters.employees = employeesList.map(e => ({
name: e.fullname,
campus: e.campusnom,
societe: e.societenom,
service: e.servicenom
}));
query = `
SELECT
DATE_FORMAT(dc.DateDebut, '%Y-%m-%d') AS startdate,
DATE_FORMAT(dc.DateFin, '%Y-%m-%d') AS enddate,
CONCAT(ca.prenom, ' ', ca.nom) AS employeename,
GROUP_CONCAT(DISTINCT tc.Nom ORDER BY tc.Nom SEPARATOR ', ') AS type,
CONCAT(
'[',
GROUP_CONCAT(
JSON_OBJECT(
'type', tc.Nom,
'jours', dct.NombreJours,
'periode', COALESCE(dct.PeriodeJournee, 'Journée entière')
)
SEPARATOR ','
),
']'
) AS detailsconges,
MAX(tc.CouleurHex) AS color,
dc.Statut AS statut,
s.Nom AS servicenom,
c.Nom AS campusnom,
so.Nom AS societenom,
dc.NombreJours AS nombrejoursouvres
FROM DemandeConge dc
JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id
LEFT JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId
LEFT JOIN TypeConge tc ON dct.TypeCongeId = tc.Id
JOIN Services s ON ca.ServiceId = s.Id
JOIN Campus c ON ca.CampusId = c.Id
JOIN Societe so ON ca.SocieteId = so.Id
WHERE dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente')
AND ca.ServiceId = ?
AND ca.CampusId = ?
GROUP BY dc.Id, dc.DateDebut, dc.DateFin, ca.prenom, ca.nom, dc.Statut, s.Nom, c.Nom, so.Nom, dc.NombreJours
ORDER BY c.Nom, dc.DateDebut ASC
`;
params = [serviceId, campusId];
}
}
const [leavesRows] = await conn.query(query, params);
const formattedLeaves = leavesRows.map(leave => ({
...leave
}));
console.log(`${formattedLeaves.length} congés trouvés`);
if (formattedLeaves.length === 0 && (role === 'collaborateur' || role === 'collaboratrice')) {
console.log('🔍 DEBUG: Aucun congé trouvé, vérification...');
const [debugLeaves] = await conn.query(`
SELECT COUNT(*) as total
FROM DemandeConge dc
JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id
JOIN Services s ON ca.ServiceId = s.Id
WHERE s.Nom = ?
AND dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente')
`, [userInfo.serviceNom]);
console.log(`🔍 Total congés dans le service "${userInfo.serviceNom}":`, debugLeaves[0].total);
const [debugCollabs] = await conn.query(`
SELECT ca.id, ca.prenom, ca.nom, ca.email, ca.ServiceId, s.Nom as ServiceNom
FROM CollaborateurAD ca
JOIN Services s ON ca.ServiceId = s.Id
WHERE s.Nom = ?
`, [userInfo.serviceNom]);
console.log(`🔍 Collaborateurs dans "${userInfo.serviceNom}":`, debugCollabs);
}
console.log(`✅ Filtres:`, {
campus: filters.campus?.length || 0,
societes: filters.societes?.length || 0,
services: filters.services?.length || 0,
employees: filters.employees?.length || 0
});
if (formattedLeaves.length > 0) {
console.log('📝 Exemple de congé formaté:', formattedLeaves[0]);
}
conn.release();
res.json({
success: true,
role: role,
leaves: formattedLeaves,
filters: filters
});
} catch (error) {
console.error("❌ Erreur getTeamLeaves:", error);
res.status(500).json({
success: false,
message: "Erreur serveur",
error: error.message
});
}
});
// ================================================
// ROUTE DE SYNCHRONISATION INITIALE (CORRIGÉE)
// ================================================
app.post('/api/initial-sync', async (req, res) => {
const conn = await pool.getConnection();
try {
const email = req.body.mail || req.body.userPrincipalName;
const entraId = req.body.id;
console.log('🔄 Initial Sync pour:', email);
// 1. Chercher user
const [users] = await conn.query('SELECT * FROM CollaborateurAD WHERE email = ?', [email]);
let userId;
let userRole;
if (users.length > 0) {
// UPDATE
userId = users[0].id;
userRole = users[0].role;
await conn.query('UPDATE CollaborateurAD SET entraUserId = ?, DerniereConnexion = NOW() WHERE id = ?', [entraId, userId]);
console.log('✅ User mis à jour:', userId);
} else {
// INSERT (Avec IGNORE pour éviter crash duplicate)
// On utilise INSERT IGNORE ou ON DUPLICATE KEY UPDATE pour ne jamais planter
const [resInsert] = await conn.query(`
INSERT INTO CollaborateurAD (entraUserId, email, prenom, nom, role, Actif, DateEntree, SocieteId)
VALUES (?, ?, ?, ?, 'Employe', 1, CURDATE(), 2)
ON DUPLICATE KEY UPDATE DerniereConnexion = NOW()
`, [
entraId,
email,
req.body.givenName || '',
req.body.surname || ''
]);
// Si insertId est 0 (car update), on refait un select
if (resInsert.insertId === 0) {
const [u] = await conn.query('SELECT id, role FROM CollaborateurAD WHERE email = ?', [email]);
userId = u[0].id;
userRole = u[0].role;
} else {
userId = resInsert.insertId;
userRole = 'Employe';
}
console.log('✅ User créé/récupéré:', userId);
}
res.json({
success: true,
localUserId: userId,
role: userRole
});
} catch (error) {
console.error('❌ CRASH initial-sync:', error);
// On renvoie un succès fake pour ne pas bloquer le frontend
res.json({
success: true,
localUserId: 1,
role: 'Secours'
});
} finally {
if (conn) conn.release();
}
});
// ========================================
// NOUVELLES ROUTES ADMINISTRATION RTT
// ========================================
app.get('/api/getAllCollaborateurs', async (req, res) => {
try {
const [collaborateurs] = await pool.query(`
SELECT
ca.id,
ca.prenom,
ca.nom,
ca.email,
ca.role,
ca.TypeContrat,
ca.DateEntree,
s.Nom as service,
ca.CampusId,
ca.SocieteId,
so.Nom as societe_nom
FROM CollaborateurAD ca
LEFT JOIN Services s ON ca.ServiceId = s.Id
LEFT JOIN Societe so ON ca.SocieteId = so.Id
WHERE (ca.actif = 1 OR ca.actif IS NULL)
ORDER BY ca.nom, ca.prenom
`);
res.json({
success: true,
collaborateurs: collaborateurs,
total: collaborateurs.length
});
} catch (error) {
console.error('Erreur getAllCollaborateurs:', error);
res.status(500).json({
success: false,
message: 'Erreur serveur',
error: error.message
});
}
});
app.post('/api/updateTypeContrat', async (req, res) => {
try {
const { collaborateur_id, type_contrat } = req.body;
if (!collaborateur_id || !type_contrat) {
return res.json({
success: false,
message: 'Données manquantes'
});
}
const typesValides = ['37h', 'forfait_jour', 'temps_partiel'];
if (!typesValides.includes(type_contrat)) {
return res.json({
success: false,
message: 'Type de contrat invalide'
});
}
const [collab] = await pool.query(
'SELECT prenom, nom, CampusId FROM CollaborateurAD WHERE id = ?',
[collaborateur_id]
);
if (collab.length === 0) {
return res.json({
success: false,
message: 'Collaborateur non trouvé'
});
}
await pool.query(
'UPDATE CollaborateurAD SET TypeContrat = ? WHERE id = ?',
[type_contrat, collaborateur_id]
);
res.json({
success: true,
message: 'Type de contrat mis à jour',
nom: `${collab[0].prenom} ${collab[0].nom}`,
nouveau_type: type_contrat
});
} catch (error) {
console.error('Erreur updateTypeContrat:', error);
res.status(500).json({
success: false,
message: 'Erreur serveur',
error: error.message
});
}
});
app.get('/api/getConfigurationRTT', async (req, res) => {
try {
const annee = parseInt(req.query.annee || new Date().getFullYear());
const [configs] = await pool.query(
`SELECT Annee, TypeContrat, JoursAnnuels, AcquisitionMensuelle, Description
FROM ConfigurationRTT
WHERE Annee = ?
ORDER BY TypeContrat`,
[annee]
);
res.json({ success: true, configs });
} catch (error) {
res.status(500).json({ success: false, message: 'Erreur', error: error.message });
}
});
app.post('/api/updateConfigurationRTT', async (req, res) => {
try {
const { annee, typeContrat, joursAnnuels } = req.body;
if (!annee || !typeContrat || !joursAnnuels) {
return res.json({ success: false, message: 'Données manquantes' });
}
const acquisitionMensuelle = joursAnnuels / 12;
await pool.query(
`INSERT INTO ConfigurationRTT (Annee, TypeContrat, JoursAnnuels, AcquisitionMensuelle)
VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
JoursAnnuels = ?, AcquisitionMensuelle = ?`,
[annee, typeContrat, joursAnnuels, acquisitionMensuelle, joursAnnuels, acquisitionMensuelle]
);
res.json({ success: true, message: 'Configuration mise à jour' });
} catch (error) {
res.status(500).json({ success: false, message: 'Erreur', error: error.message });
}
});
app.post('/api/updateRequest', upload.array('medicalDocuments', 5), async (req, res) => {
let connection;
try {
console.log('📥 Body reçu:', req.body);
console.log('📎 Fichiers reçus:', req.files);
const {
requestId,
leaveType,
startDate,
endDate,
reason,
businessDays,
userId,
userEmail,
userName,
accessToken
} = req.body;
// Validation
if (!requestId || !leaveType || !startDate || !endDate || !businessDays || !userId) {
return res.status(400).json({
success: false,
message: '❌ Données manquantes'
});
}
connection = await pool.getConnection();
await connection.beginTransaction();
console.log('🔍 ÉTAPE 1: Récupération de la demande originale...');
// Récupérer la demande originale
const [originalRequest] = await connection.query(
'SELECT * FROM DemandeConge WHERE Id = ? AND CollaborateurADId = ?',
[requestId, userId]
);
if (originalRequest.length === 0) {
await connection.rollback();
return res.status(404).json({
success: false,
message: '❌ Demande introuvable'
});
}
const original = originalRequest[0];
console.log('🔙 ÉTAPE 2: Suppression des anciennes déductions...');
// Restaurer les compteurs de l'ancienne demande
if (original.TypeCongeId !== 3) { // Pas pour Arrêt maladie
const [oldRepartition] = await connection.query(
'SELECT * FROM DeductionDetails WHERE DemandeCongeId = ?',
[requestId]
);
console.log('📊 Ancienne répartition trouvée:', oldRepartition);
if (oldRepartition && oldRepartition.length > 0) {
for (const rep of oldRepartition) {
const typeCongeId = rep.TypeCongeId;
const annee = rep.Annee;
const joursUtilises = rep.JoursUtilises;
// Restaurer dans CompteurConges
await connection.query(
`UPDATE CompteurConges
SET Solde = Solde + ?,
DerniereMiseAJour = NOW()
WHERE CollaborateurADId = ?
AND TypeCongeId = ?
AND Annee = ?`,
[joursUtilises, userId, typeCongeId, annee]
);
console.log(`✅ Restauré ${joursUtilises}j dans TypeCongeId=${typeCongeId}, Annee=${annee}`);
}
}
// Supprimer l'ancienne répartition
await connection.query(
'DELETE FROM DeductionDetails WHERE DemandeCongeId = ?',
[requestId]
);
}
console.log('📝 ÉTAPE 3: Mise à jour de la demande...');
// Mettre à jour la demande
await connection.query(
`UPDATE DemandeConge
SET TypeCongeId = ?,
DateDebut = ?,
DateFin = ?,
Commentaire = ?,
NombreJours = ?,
Statut = 'En attente',
DateValidation = NOW()
WHERE Id = ?`,
[leaveType, startDate, endDate, reason || '', businessDays, requestId]
);
console.log('📊 ÉTAPE 4: Calcul de la nouvelle répartition...');
let newRepartition = [];
// Calculer la nouvelle répartition (seulement pour CP et RTT)
if (parseInt(leaveType) === 1 || parseInt(leaveType) === 2) {
// Récupérer les compteurs actuels pour l'année en cours
const currentYear = new Date().getFullYear();
const previousYear = currentYear - 1;
// Récupérer depuis CompteurConges
const [countersN] = await connection.query(
'SELECT Solde, SoldeReporte FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?',
[userId, leaveType, currentYear]
);
const [countersN1] = await connection.query(
'SELECT SoldeReporte FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?',
[userId, leaveType, previousYear]
);
const soldeN = countersN.length > 0 ? parseFloat(countersN[0].Solde || 0) : 0;
const soldeN1 = countersN1.length > 0 ? parseFloat(countersN1[0].SoldeReporte || 0) : 0;
console.log(`💡 Soldes disponibles: N-1=${soldeN1}j, N=${soldeN}j`);
// Calculer la répartition
if (parseInt(leaveType) === 1) { // Congé payé
const joursNecessaires = parseFloat(businessDays);
// Vérifier si les soldes sont suffisants
if (soldeN1 + soldeN < joursNecessaires) {
throw new Error(`Solde insuffisant: ${(soldeN1 + soldeN).toFixed(2)}j disponibles, ${joursNecessaires}j demandés`);
}
// Utiliser d'abord N-1, puis N
let reste = joursNecessaires;
if (reste > 0 && soldeN1 > 0) {
const joursN1 = Math.min(reste, soldeN1);
newRepartition.push({
typeCongeId: leaveType,
annee: previousYear,
jours: joursN1,
typeDeduction: 'Report N-1'
});
reste -= joursN1;
}
if (reste > 0 && soldeN > 0) {
const joursN = Math.min(reste, soldeN);
newRepartition.push({
typeCongeId: leaveType,
annee: currentYear,
jours: joursN,
typeDeduction: 'Année N'
});
reste -= joursN;
}
} else if (parseInt(leaveType) === 2) { // RTT
const joursNecessaires = parseFloat(businessDays);
if (soldeN < joursNecessaires) {
throw new Error(`Solde RTT insuffisant: ${soldeN.toFixed(2)}j disponibles, ${joursNecessaires}j demandés`);
}
newRepartition = [{
typeCongeId: leaveType,
annee: currentYear,
jours: joursNecessaires,
typeDeduction: 'Année N'
}];
}
console.log('📊 Nouvelle répartition calculée:', JSON.stringify(newRepartition, null, 2));
// Vérification finale
if (!Array.isArray(newRepartition) || newRepartition.length === 0) {
console.error('❌ ERREUR: newRepartition invalide:', newRepartition);
throw new Error('Répartition invalide');
}
console.log('📉 ÉTAPE 5: Déduction des nouveaux compteurs...');
// Déduire les nouveaux compteurs
for (const rep of newRepartition) {
// Validation de chaque élément
if (!rep || typeof rep !== 'object' || !rep.typeCongeId || !rep.annee || rep.jours === undefined) {
console.error('❌ Élément invalide:', rep);
throw new Error(`Élément de répartition invalide: ${JSON.stringify(rep)}`);
}
// Sauvegarder dans DeductionDetails
await connection.query(
`INSERT INTO DeductionDetails (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
VALUES (?, ?, ?, ?, ?)`,
[requestId, rep.typeCongeId, rep.annee, rep.typeDeduction, rep.jours]
);
// Déduire du compteur
await connection.query(
`UPDATE CompteurConges
SET Solde = GREATEST(0, Solde - ?),
DerniereMiseAJour = NOW()
WHERE CollaborateurADId = ?
AND TypeCongeId = ?
AND Annee = ?`,
[rep.jours, userId, rep.typeCongeId, rep.annee]
);
console.log(`✅ Déduit ${rep.jours}j de TypeCongeId=${rep.typeCongeId}, Annee=${rep.annee}`);
}
}
console.log('📧 ÉTAPE 6: Envoi des emails...');
// Récupérer les infos du manager
const [managerInfo] = await connection.query(
`SELECT m.Email, m.Prenom, m.Nom
FROM CollaborateurAD c
JOIN HierarchieValidationAD h ON c.id = h.CollaborateurId
JOIN CollaborateurAD m ON h.SuperieurId = m.id
WHERE c.id = ?`,
[userId]
);
await connection.commit();
// Envoyer les emails (sans bloquer la réponse)
if (accessToken && managerInfo.length > 0) {
const manager = managerInfo[0];
// Obtenir un token Graph
const graphToken = await getGraphToken();
if (graphToken) {
// Email au manager
sendMailGraph(
graphToken,
'gtanoreply@ensup.eu',
manager.Email,
'Modification de demande de congé',
`
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<div style="background: linear-gradient(135deg, #3b82f6 0%, #1e40af 100%); padding: 30px; text-align: center; border-radius: 10px 10px 0 0;">
<h1 style="color: white; margin: 0;">Modification de demande</h1>
</div>
<div style="background-color: #f8f9fa; padding: 30px; border-radius: 0 0 10px 10px;">
<p style="font-size: 16px; color: #333;">Bonjour <strong>${manager.Prenom}</strong>,</p>
<p style="font-size: 16px; color: #333;"><strong>${userName}</strong> a modifié sa demande de congé :</p>
<div style="background-color: white; padding: 20px; border-radius: 8px; margin: 20px 0; border-left: 4px solid #3b82f6;">
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 8px 0; color: #666;"><strong>Type :</strong></td>
<td style="padding: 8px 0; color: #333;">${getLeaveTypeName(leaveType)}</td>
</tr>
<tr>
<td style="padding: 8px 0; color: #666;"><strong>Dates :</strong></td>
<td style="padding: 8px 0; color: #333;">du ${formatDateFR(startDate)} au ${formatDateFR(endDate)}</td>
</tr>
<tr>
<td style="padding: 8px 0; color: #666;"><strong>Durée :</strong></td>
<td style="padding: 8px 0; color: #333;">${businessDays} jour(s)</td>
</tr>
</table>
</div>
<p style="font-size: 16px; color: #333;">Merci de valider ou refuser cette demande dans l'application.</p>
<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #e5e7eb;">
<p style="font-size: 14px; color: #666; margin: 0;">Cet email est envoyé automatiquement, merci de ne pas y répondre.</p>
</div>
</div>
</div>
`
).catch(err => console.error('❌ Erreur email manager:', err));
// Email de confirmation au collaborateur
sendMailGraph(
graphToken,
'gtanoreply@ensup.eu',
userEmail,
'Confirmation de modification',
`
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<div style="background: linear-gradient(135deg, #10b981 0%, #059669 100%); padding: 30px; text-align: center; border-radius: 10px 10px 0 0;">
<h1 style="color: white; margin: 0;">Demande modifiée</h1>
</div>
<div style="background-color: #f8f9fa; padding: 30px; border-radius: 0 0 10px 10px;">
<p style="font-size: 16px; color: #333;">Bonjour <strong>${userName.split(' ')[0]}</strong>,</p>
<p style="font-size: 16px; color: #333;">Votre demande de congé a bien été modifiée :</p>
<div style="background-color: white; padding: 20px; border-radius: 8px; margin: 20px 0; border-left: 4px solid #10b981;">
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 8px 0; color: #666;"><strong>Type :</strong></td>
<td style="padding: 8px 0; color: #333;">${getLeaveTypeName(leaveType)}</td>
</tr>
<tr>
<td style="padding: 8px 0; color: #666;"><strong>Dates :</strong></td>
<td style="padding: 8px 0; color: #333;">du ${formatDateFR(startDate)} au ${formatDateFR(endDate)}</td>
</tr>
<tr>
<td style="padding: 8px 0; color: #666;"><strong>Durée :</strong></td>
<td style="padding: 8px 0; color: #333;">${businessDays} jour(s)</td>
</tr>
</table>
</div>
<p style="font-size: 16px; color: #333;">Elle est maintenant <strong style="color: #f59e0b;">en attente de validation</strong>.</p>
<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #e5e7eb;">
<p style="font-size: 14px; color: #666; margin: 0;">Cet email est envoyé automatiquement, merci de ne pas y répondre.</p>
</div>
</div>
</div>
`
).catch(err => console.error('❌ Erreur email collaborateur:', err));
} else {
console.warn('⚠️ Impossible d\'obtenir un token Graph - emails non envoyés');
}
}
res.json({
success: true,
message: '✅ Demande modifiée avec succès',
repartition: newRepartition
});
} catch (error) {
if (connection) {
await connection.rollback();
}
console.error('❌ Erreur updateRequest:', error);
res.status(500).json({
success: false,
message: error.message || 'Erreur lors de la modification'
});
} finally {
if (connection) {
connection.release();
}
}
});
// ⭐ Fonction helper pour calculer la répartition CP
function calculateCPRepartition(joursNecessaires, soldeN1, soldeN) {
const repartition = [];
let reste = joursNecessaires;
// D'abord utiliser N-1
if (reste > 0 && soldeN1 > 0) {
const joursN1 = Math.min(reste, soldeN1);
repartition.push({
type: 'CP',
annee: 'N-1',
jours: joursN1
});
reste -= joursN1;
}
// Puis utiliser N
if (reste > 0 && soldeN > 0) {
const joursN = Math.min(reste, soldeN);
repartition.push({
type: 'CP',
annee: 'N',
jours: joursN
});
reste -= joursN;
}
return repartition;
}
// ⭐ Fonction helper pour obtenir le champ de compteur
function getCounterField(type, annee) {
if (type === 'CP' && annee === 'N-1') return 'SoldeCP_N1';
if (type === 'CP' && annee === 'N') return 'SoldeCP_N';
if (type === 'RTT' && annee === 'N') return 'SoldeRTT_N';
return null;
}
// ⭐ Fonction helper pour le nom du type de congé
function getLeaveTypeName(typeId) {
const types = {
1: 'Congé payé',
2: 'RTT',
3: 'Arrêt maladie',
4: 'Formation',
5: 'Récupération'
};
return types[typeId] || 'Inconnu';
}
// ⭐ Fonction helper pour formater les dates
function formatDateFR(dateStr) {
const date = new Date(dateStr);
return date.toLocaleDateString('fr-FR', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
});
}
/**
* Route pour SUPPRIMER une demande de congé
* POST /deleteRequest
*/
app.post('/api/deleteRequest', async (req, res) => {
const conn = await pool.getConnection();
try {
await conn.beginTransaction();
const { requestId, userId, userEmail, userName, accessToken } = req.body;
if (!requestId || !userId) {
await conn.rollback();
conn.release();
return res.status(400).json({
success: false,
message: 'Paramètres manquants (requestId ou userId)'
});
}
console.log('\n🗑 === ANNULATION DEMANDE ===');
console.log('Demande ID:', requestId);
console.log('User ID:', userId);
// 1⃣ Vérifier que la demande existe
const [existingRequest] = await conn.query(
`SELECT d.*,
DATE_FORMAT(d.DateDebut, '%Y-%m-%d') as DateDebut,
DATE_FORMAT(d.DateFin, '%Y-%m-%d') as DateFin
FROM DemandeConge d
WHERE d.Id = ? AND d.CollaborateurADId = ?`,
[requestId, userId]
);
if (existingRequest.length === 0) {
await conn.rollback();
conn.release();
return res.status(404).json({
success: false,
message: 'Demande introuvable ou non autorisée'
});
}
const request = existingRequest[0];
const requestStatus = request.Statut;
const dateDebut = new Date(request.DateDebut);
const aujourdhui = new Date();
aujourdhui.setHours(0, 0, 0, 0);
dateDebut.setHours(0, 0, 0, 0);
console.log(`📋 Demande trouvée: ID=${requestId}, Statut=${requestStatus}`);
console.log(`📅 Date début: ${dateDebut.toLocaleDateString('fr-FR')}`);
console.log(`📅 Aujourd'hui: ${aujourdhui.toLocaleDateString('fr-FR')}`);
// ❌ BLOQUER SI DATE DÉJÀ PASSÉE OU AUJOURD'HUI
if (dateDebut <= aujourdhui) {
await conn.rollback();
conn.release();
return res.status(400).json({
success: false,
message: '❌ Impossible d\'annuler : la date de début est déjà passée ou c\'est aujourd\'hui',
dateDebut: formatDateWithoutUTC(dateDebut)
});
}
// 2⃣ RÉCUPÉRER LA RÉPARTITION COMPLÈTE
const [repartition] = await conn.query(`
SELECT dct.*, tc.Nom as TypeNom
FROM DemandeCongeType dct
JOIN TypeConge tc ON dct.TypeCongeId = tc.Id
WHERE dct.DemandeCongeId = ?
ORDER BY tc.Nom
`, [requestId]);
console.log(`📊 Répartition de la demande:`, repartition);
// 3⃣ RESTAURER LES COMPTEURS (sauf si déjà Refusée/Annulée)
if (requestStatus !== 'Refusée' && requestStatus !== 'Annulée') {
console.log(`\n✅ Restauration des compteurs (Statut: ${requestStatus})`);
try {
const restoration = await restoreLeaveBalance(conn, requestId, userId);
if (restoration.success) {
console.log('✅ Compteurs restaurés:', restoration.restorations.length, 'opérations');
}
} catch (restoreError) {
console.error('❌ Erreur restauration:', restoreError.message);
await conn.rollback();
conn.release();
return res.status(500).json({
success: false,
message: 'Erreur lors de la restauration des compteurs',
error: restoreError.message
});
}
}
// 4⃣ RÉCUPÉRER INFOS COLLABORATEUR ET MANAGER
const [collabInfo] = await conn.query(
'SELECT email, prenom, nom FROM CollaborateurAD WHERE id = ?',
[userId]
);
const collabEmail = collabInfo.length > 0 ? collabInfo[0].email : userEmail;
const collabName = collabInfo.length > 0
? `${collabInfo[0].prenom} ${collabInfo[0].nom}`
: userName;
const [hierarchie] = await conn.query(
`SELECT h.SuperieurId, m.email as managerEmail,
m.prenom as managerPrenom, m.nom as managerNom
FROM HierarchieValidationAD h
LEFT JOIN CollaborateurAD m ON h.SuperieurId = m.id
WHERE h.CollaborateurId = ?`,
[userId]
);
const managerEmail = hierarchie[0]?.managerEmail;
const managerName = hierarchie[0]
? `${hierarchie[0].managerPrenom} ${hierarchie[0].managerNom}`
: 'Manager';
// 5⃣ METTRE À JOUR LE STATUT
await conn.query(
`UPDATE DemandeConge
SET Statut = 'Annulée',
DateValidation = NOW(),
CommentaireValidation = CONCAT(
COALESCE(CommentaireValidation, ''),
'\n[Annulée par le collaborateur le ',
DATE_FORMAT(NOW(), '%d/%m/%Y à %H:%i'),
']'
)
WHERE Id = ?`,
[requestId]
);
console.log(`✅ Demande ${requestId} marquée comme Annulée`);
await conn.commit();
conn.release();
// 6⃣ ENVOI DES EMAILS
let emailsSent = { collaborateur: false, manager: false };
const graphToken = await getGraphToken();
if (graphToken) {
const dateDebutFormatted = new Date(request.DateDebut).toLocaleDateString('fr-FR');
const dateFinFormatted = new Date(request.DateFin).toLocaleDateString('fr-FR');
const datesPeriode = dateDebutFormatted === dateFinFormatted
? dateDebutFormatted
: `du ${dateDebutFormatted} au ${dateFinFormatted}`;
// 📊 FORMATER LA RÉPARTITION POUR L'EMAIL
const repartitionText = repartition.map(r =>
`<tr>
<td style="padding: 8px 0; color: #666;"><strong>${r.TypeNom} :</strong></td>
<td style="padding: 8px 0; color: #333;">${r.NombreJours}j ${r.PeriodeJournee !== 'Journée entière' ? `(${r.PeriodeJournee})` : ''}</td>
</tr>`
).join('');
// 📧 EMAIL AU COLLABORATEUR
if (collabEmail) {
try {
const subjectCollab = '✅ Confirmation d\'annulation de votre demande';
const bodyCollab = `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<div style="background: linear-gradient(135deg, #10b981 0%, #059669 100%); padding: 30px; text-align: center; border-radius: 10px 10px 0 0;">
<h1 style="color: white; margin: 0;">✅ Demande annulée</h1>
</div>
<div style="background-color: #f8f9fa; padding: 30px; border-radius: 0 0 10px 10px;">
<p style="font-size: 16px; color: #333;">Bonjour <strong>${collabName}</strong>,</p>
<p style="font-size: 16px; color: #333;">
Votre demande de congé a bien été <strong style="color: #10b981;">annulée</strong>.
</p>
<div style="background-color: white; padding: 20px; border-radius: 8px; margin: 20px 0; border-left: 4px solid #10b981;">
<h3 style="color: #10b981; margin-top: 0;">📋 Demande annulée</h3>
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 8px 0; color: #666;"><strong>Période :</strong></td>
<td style="padding: 8px 0; color: #333;">${datesPeriode}</td>
</tr>
<tr>
<td style="padding: 8px 0; color: #666;"><strong>Durée totale :</strong></td>
<td style="padding: 8px 0; color: #333;">${request.NombreJours} jour(s)</td>
</tr>
<tr>
<td colspan="2" style="padding: 12px 0 4px 0; color: #666; border-top: 1px solid #e5e7eb;">
<strong>Répartition :</strong>
</td>
</tr>
${repartitionText}
</table>
</div>
${requestStatus !== 'Refusée' && requestStatus !== 'Annulée' ? `
<div style="background-color: #dbeafe; padding: 15px; border-radius: 8px; margin-top: 20px;">
<p style="margin: 0; color: #1e40af;">
✅ <strong>Vos compteurs ont été restaurés</strong><br>
Les jours de congé sont à nouveau disponibles dans vos soldes.
</p>
</div>
` : ''}
<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #e5e7eb;">
<p style="font-size: 14px; color: #666; margin: 0;">
📧 Cet email est envoyé automatiquement, merci de ne pas y répondre.
</p>
</div>
</div>
</div>
`;
await sendMailGraph(graphToken, 'gtanoreply@ensup.eu', collabEmail, subjectCollab, bodyCollab);
emailsSent.collaborateur = true;
console.log('✅ Email envoyé au collaborateur');
} catch (emailError) {
console.error('❌ Erreur email collaborateur:', emailError.message);
}
}
// 📧 EMAIL AU MANAGER
if (managerEmail && requestStatus !== 'Refusée' && requestStatus !== 'Annulée') {
try {
const isValidated = requestStatus === 'Validée' || requestStatus === 'Validé';
const subjectManager = isValidated
? `🗑️ Annulation de congé validé - ${collabName}`
: `🗑️ Annulation de demande - ${collabName}`;
const bodyManager = `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<div style="background: linear-gradient(135deg, ${isValidated ? '#ef4444' : '#f59e0b'} 0%, ${isValidated ? '#991b1b' : '#d97706'} 100%); padding: 30px; text-align: center; border-radius: 10px 10px 0 0;">
<h1 style="color: white; margin: 0;">🗑️ Annulation de ${isValidated ? 'congé' : 'demande'}</h1>
</div>
<div style="background-color: #f8f9fa; padding: 30px; border-radius: 0 0 10px 10px;">
<p style="font-size: 16px; color: #333;">Bonjour ${managerName},</p>
<p style="font-size: 16px; color: #333;">
<strong>${collabName}</strong> a annulé ${isValidated ? 'son congé <strong>validé</strong>' : 'sa demande de congé <strong>en attente</strong>'}.
</p>
<div style="background-color: white; padding: 20px; border-radius: 8px; margin: 20px 0; border-left: 4px solid ${isValidated ? '#ef4444' : '#f59e0b'};">
<h3 style="color: ${isValidated ? '#ef4444' : '#f59e0b'}; margin-top: 0;">📋 Demande annulée</h3>
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 8px 0; color: #666;"><strong>Statut initial :</strong></td>
<td style="padding: 8px 0; color: #333;">${requestStatus}</td>
</tr>
<tr>
<td style="padding: 8px 0; color: #666;"><strong>Période :</strong></td>
<td style="padding: 8px 0; color: #333;">${datesPeriode}</td>
</tr>
<tr>
<td style="padding: 8px 0; color: #666;"><strong>Durée totale :</strong></td>
<td style="padding: 8px 0; color: #333;">${request.NombreJours} jour(s)</td>
</tr>
<tr>
<td colspan="2" style="padding: 12px 0 4px 0; color: #666; border-top: 1px solid #e5e7eb;">
<strong>Répartition :</strong>
</td>
</tr>
${repartitionText}
</table>
</div>
${isValidated ? `
<div style="background-color: #fee2e2; padding: 15px; border-radius: 8px; margin-top: 20px;">
<p style="margin: 0; color: #991b1b;">
⚠️ <strong>Attention :</strong> Ce congé avait été validé.<br>
Les compteurs du collaborateur ont été automatiquement restaurés.
</p>
</div>
` : `
<div style="background-color: #fef3c7; padding: 15px; border-radius: 8px; margin-top: 20px;">
<p style="margin: 0; color: #92400e;">
Cette demande était en attente de validation.<br>
Les compteurs ont été restaurés.
</p>
</div>
`}
<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #e5e7eb;">
<p style="font-size: 14px; color: #666; margin: 0;">
📧 Cet email est envoyé automatiquement, merci de ne pas y répondre.
</p>
</div>
</div>
</div>
`;
await sendMailGraph(graphToken, 'gtanoreply@ensup.eu', managerEmail, subjectManager, bodyManager);
emailsSent.manager = true;
console.log('✅ Email envoyé au manager');
} catch (emailError) {
console.error('❌ Erreur email manager:', emailError.message);
}
} else if (!managerEmail) {
console.log('⚠️ Email manager introuvable');
}
} else {
console.warn('⚠️ Impossible d\'obtenir un token Graph - emails non envoyés');
}
// RETOURNER LA RÉPONSE
res.json({
success: true,
message: 'Demande annulée avec succès',
counterRestored: requestStatus !== 'Refusée' && requestStatus !== 'Annulée',
emailsSent: emailsSent,
managerNotified: requestStatus !== 'Refusée' && requestStatus !== 'Annulée',
repartition: repartition.map(r => ({
type: r.TypeNom,
jours: r.NombreJours,
periode: r.PeriodeJournee
}))
});
} catch (error) {
await conn.rollback();
if (conn) conn.release();
console.error('❌ Erreur deleteRequest:', error);
res.status(500).json({
success: false,
message: 'Erreur lors de l\'annulation',
error: error.message
});
}
});
app.get('/api/exportCompteurs', async (req, res) => {
try {
const dateRef = req.query.dateRef || new Date().toISOString().split('T')[0];
const conn = await pool.getConnection();
const [collaborateurs] = await pool.query(`
SELECT
ca.id,
ca.prenom,
ca.nom,
ca.email,
ca.role,
ca.TypeContrat,
ca.DateEntree,
s.Nom as service,
ca.CampusId,
ca.SocieteId,
so.Nom as societe_nom
FROM CollaborateurAD ca
LEFT JOIN Services s ON ca.ServiceId = s.Id
LEFT JOIN Societe so ON ca.SocieteId = so.Id
WHERE (ca.actif = 1 OR ca.actif IS NULL)
ORDER BY ca.nom, ca.prenom
`);
const rapport = [];
for (const collab of collaborateurs) {
const dateEntree = collab.DateEntree;
const dateReference = new Date(dateRef);
const acquisCP = calculerAcquisitionCP(dateReference, dateEntree);
let acquisRTT = 0;
if (collab.role !== 'Apprenti') {
const rttData = await calculerAcquisitionRTT(conn, collab.id, dateReference);
acquisRTT = rttData.acquisition;
}
const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']);
const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']);
let soldeCP = 0;
let soldeRTT = 0;
if (cpType.length > 0) {
const [compteurCP] = await conn.query(
'SELECT Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?',
[collab.id, cpType[0].Id, dateReference.getFullYear()]
);
soldeCP = compteurCP.length > 0 ? parseFloat(compteurCP[0].Solde) : 0;
}
if (rttType.length > 0 && collab.role !== 'Apprenti') {
const [compteurRTT] = await conn.query(
'SELECT Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?',
[collab.id, rttType[0].Id, dateReference.getFullYear()]
);
soldeRTT = compteurRTT.length > 0 ? parseFloat(compteurRTT[0].Solde) : 0;
}
rapport.push({
id: collab.id,
prenom: collab.prenom,
nom: collab.nom,
email: collab.email,
role: collab.role,
service: collab.service,
societe_id: collab.SocieteId,
societe_nom: collab.societe_nom,
type_contrat: collab.TypeContrat,
date_entree: dateEntree ? formatDateWithoutUTC(dateEntree) : null,
cp_acquis: parseFloat(acquisCP.toFixed(2)),
cp_solde: parseFloat(soldeCP.toFixed(2)),
rtt_acquis: parseFloat(acquisRTT.toFixed(2)),
rtt_solde: parseFloat(soldeRTT.toFixed(2)),
date_reference: dateRef
});
}
conn.release();
res.json({
success: true,
date_reference: dateRef,
total_collaborateurs: rapport.length,
rapport: rapport
});
} catch (error) {
console.error('Erreur exportCompteurs:', error);
res.status(500).json({
success: false,
message: 'Erreur serveur',
error: error.message
});
}
});
function isInPeriodeAnticipation(dateDebut, typeConge) {
const date = new Date(dateDebut);
const year = date.getFullYear();
const month = date.getMonth() + 1; // 1-12
if (typeConge === 'CP') {
// CP : 01/06 année N → 31/05 année N+1
// Période anticipation : du 01/06 de l'année suivante
return month >= 6; // Si >= juin, c'est pour l'exercice N+1
} else if (typeConge === 'RTT') {
// RTT : 01/01 année N → 31/12 année N
// Pas d'anticipation possible car année civile
return month >= 1 && month <= 12;
}
return false;
}
function getAnneeCompteur(dateDebut, typeConge) {
const date = new Date(dateDebut);
const year = date.getFullYear();
const month = date.getMonth() + 1;
if (typeConge === 'CP') {
// Si date entre 01/06 et 31/12 → année N
// Si date entre 01/01 et 31/05 → année N-1 (exercice précédent)
return month >= 6 ? year : year - 1;
} else {
// RTT : toujours année civile
return year;
}
}
/**
* Vérifie la disponibilité des soldes pour une demande
* Retourne : { available: boolean, details: {}, useN1: boolean }
*/
async function checkSoldesDisponiblesMixte(conn, collaborateurId, repartition, dateDebut, isApprenti) {
const today = new Date();
today.setHours(0, 0, 0, 0);
const currentYear = today.getFullYear();
const dateDemandeObj = new Date(dateDebut);
dateDemandeObj.setHours(0, 0, 0, 0);
const demandeYear = dateDemandeObj.getFullYear();
const demandeMonth = dateDemandeObj.getMonth() + 1;
console.log('\n🔍 === CHECK SOLDES MIXTE (AVEC ANTICIPATION) ===');
console.log('📅 Date AUJOURD\'HUI:', today.toISOString().split('T')[0]);
console.log('📅 Date DEMANDE:', dateDebut);
console.log('📅 Année demande:', demandeYear, '/ Mois:', demandeMonth);
console.log('📅 Année actuelle:', currentYear);
let totalDisponible = 0;
let totalNecessaire = 0;
const details = {};
for (const rep of repartition) {
const typeCode = rep.TypeConge;
const joursNecessaires = parseFloat(rep.NombreJours || 0);
// Ignorer ABS et Formation
if (typeCode === 'ABS' || typeCode === 'Formation') {
continue;
}
totalNecessaire += joursNecessaires;
if (typeCode === 'CP') {
// ⭐ RÉCUPÉRER LES INFOS COLLABORATEUR
const [collabInfo] = await conn.query(
`SELECT DateEntree FROM CollaborateurAD WHERE id = ?`,
[collaborateurId]
);
const dateEntree = collabInfo[0]?.DateEntree;
// ⭐ CALCULER L'ACQUISITION JUSQU'À LA DATE DEMANDÉE
const acquisALaDate = calculerAcquisitionCP(dateDemandeObj, dateEntree);
console.log('💰 Acquisition CP à la date', dateDebut, ':', acquisALaDate.toFixed(2), 'j');
// ⭐ RÉCUPÉRER LE REPORTÉ N-1
const previousYear = currentYear - 1;
const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']);
const cpTypeId = cpType[0].Id;
const [compteurN1] = await conn.query(`
SELECT SoldeReporte
FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
`, [collaborateurId, cpTypeId, previousYear]);
const reporteN1 = compteurN1.length > 0 ? parseFloat(compteurN1[0].SoldeReporte || 0) : 0;
// ⭐ RÉCUPÉRER CE QUI A DÉJÀ ÉTÉ POSÉ (toutes demandes validées ou en attente)
const [totalPose] = await conn.query(`
SELECT COALESCE(SUM(dct.NombreJours), 0) as totalPose
FROM DemandeConge dc
JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId
JOIN TypeConge tc ON dct.TypeCongeId = tc.Id
WHERE dc.CollaborateurADId = ?
AND tc.Nom = 'Congé payé'
AND dc.Statut IN ('Validée', 'En attente')
AND dc.DateDebut <= ?
`, [collaborateurId, dateDebut]);
const dejaPose = parseFloat(totalPose[0].totalPose || 0);
// ⭐ SOLDE RÉEL = Reporté N-1 + Acquisition - Déjà posé
const soldeReel = reporteN1 + acquisALaDate - dejaPose;
console.log('💰 Soldes CP détaillés:', {
reporteN1: reporteN1.toFixed(2),
acquisALaDate: acquisALaDate.toFixed(2),
dejaPose: dejaPose.toFixed(2),
soldeReel: soldeReel.toFixed(2)
});
details.CP = {
reporteN1: reporteN1,
acquisALaDate: acquisALaDate,
dejaPose: dejaPose,
soldeReel: soldeReel,
necessaire: joursNecessaires
};
totalDisponible += Math.max(0, soldeReel);
if (soldeReel < joursNecessaires) {
return {
available: false,
message: `Solde CP insuffisant (${Math.max(0, soldeReel).toFixed(2)}j disponibles avec anticipation, ${joursNecessaires}j demandés)`,
details,
manque: joursNecessaires - soldeReel
};
}
} else if (typeCode === 'RTT') {
if (isApprenti) {
return {
available: false,
message: 'Les apprentis ne peuvent pas poser de RTT',
details
};
}
// ⭐ CALCUL RTT (utiliser la fonction existante)
const rttData = await calculerAcquisitionRTT(conn, collaborateurId, dateDemandeObj);
const acquisALaDate = rttData.acquisition;
console.log('💰 Acquisition RTT à la date', dateDebut, ':', acquisALaDate.toFixed(2), 'j');
// ⭐ RÉCUPÉRER CE QUI A DÉJÀ ÉTÉ POSÉ
const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']);
const rttTypeId = rttType[0].Id;
const [totalPose] = await conn.query(`
SELECT COALESCE(SUM(dct.NombreJours), 0) as totalPose
FROM DemandeConge dc
JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId
JOIN TypeConge tc ON dct.TypeCongeId = tc.Id
WHERE dc.CollaborateurADId = ?
AND tc.Nom = 'RTT'
AND dc.Statut IN ('Validée', 'En attente')
AND dc.DateDebut <= ?
`, [collaborateurId, dateDebut]);
const dejaPose = parseFloat(totalPose[0].totalPose || 0);
// ⭐ SOLDE RÉEL = Acquisition - Déjà posé
const soldeReel = acquisALaDate - dejaPose;
console.log('💰 Soldes RTT détaillés:', {
acquisALaDate: acquisALaDate.toFixed(2),
dejaPose: dejaPose.toFixed(2),
soldeReel: soldeReel.toFixed(2)
});
details.RTT = {
acquisALaDate: acquisALaDate,
dejaPose: dejaPose,
soldeReel: soldeReel,
necessaire: joursNecessaires
};
totalDisponible += Math.max(0, soldeReel);
if (soldeReel < joursNecessaires) {
return {
available: false,
message: `Solde RTT insuffisant (${Math.max(0, soldeReel).toFixed(2)}j disponibles avec anticipation, ${joursNecessaires}j demandés)`,
details,
manque: joursNecessaires - soldeReel
};
}
} else if (typeCode === 'Récup') {
const [recupType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Récupération']);
if (recupType.length === 0) continue;
const recupTypeId = recupType[0].Id;
const [compteur] = await conn.query(
`SELECT Solde FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
[collaborateurId, recupTypeId, currentYear]
);
const soldeRecup = compteur.length > 0 ? parseFloat(compteur[0].Solde || 0) : 0;
console.log('💰 Solde Récup:', soldeRecup.toFixed(2), 'j');
details.Recup = {
soldeN: soldeRecup,
total: soldeRecup,
necessaire: joursNecessaires
};
totalDisponible += Math.min(joursNecessaires, soldeRecup);
if (soldeRecup < joursNecessaires) {
return {
available: false,
message: `Solde Récupération insuffisant (${soldeRecup.toFixed(2)}j disponibles, ${joursNecessaires}j demandés)`,
details,
manque: joursNecessaires - soldeRecup
};
}
}
}
console.log('\n✅ Check final:', {
totalDisponible: totalDisponible.toFixed(2),
totalNecessaire: totalNecessaire.toFixed(2),
available: totalDisponible >= totalNecessaire
});
return {
available: totalDisponible >= totalNecessaire,
details,
totalDisponible,
totalNecessaire
};
}
// ========================================
// FONCTIONS HELPER
// ========================================
async function getSoldesCP(conn, collaborateurId, dateEntree, includeN1Anticipe = false) {
const currentYear = new Date().getFullYear();
const previousYear = currentYear - 1;
console.log(`\n📊 getSoldesCP - includeN1Anticipe: ${includeN1Anticipe}`);
const [cpType] = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = 'Congé payé' LIMIT 1`);
const typeCongeId = cpType[0].Id;
// N-1 (reporté)
const [compteursN1] = await conn.query(
`SELECT Solde, SoldeReporte FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
[collaborateurId, typeCongeId, previousYear]
);
const soldeN1 = compteursN1.length > 0 ? parseFloat(compteursN1[0].SoldeReporte || 0) : 0;
// N (actuel)
const [compteursN] = await conn.query(
`SELECT Solde, SoldeReporte, Total FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
[collaborateurId, typeCongeId, currentYear]
);
const soldeN = compteursN.length > 0
? parseFloat(compteursN[0].Solde || 0) - parseFloat(compteursN[0].SoldeReporte || 0)
: 0;
const totalAcquisN = compteursN.length > 0 ? parseFloat(compteursN[0].Total || 0) : 0;
// Anticipation N
const finExerciceN = new Date(currentYear + 1, 4, 31); // 31 mai N+1
const acquisTotaleN = calculerAcquisitionCP(finExerciceN, dateEntree);
const soldeAnticipeN = Math.max(0, acquisTotaleN - totalAcquisN);
console.log(' N-1:', soldeN1);
console.log(' N:', soldeN);
console.log(' Anticipé N:', soldeAnticipeN);
// ⭐ Anticipation N+1 (si demandé)
let soldeAnticipeN1 = 0;
if (includeN1Anticipe) {
const debutExerciceN1 = new Date(currentYear + 1, 5, 1); // 01 juin N+1
const finExerciceN1 = new Date(currentYear + 2, 4, 31); // 31 mai N+2
let dateCalculN1 = debutExerciceN1;
if (dateEntree && new Date(dateEntree) > debutExerciceN1) {
dateCalculN1 = new Date(dateEntree);
}
const acquisTotaleN1 = calculerAcquisitionCP(finExerciceN1, dateCalculN1);
soldeAnticipeN1 = acquisTotaleN1;
console.log(' Anticipé N+1:', soldeAnticipeN1);
}
return { soldeN1, soldeN, soldeAnticipeN, soldeAnticipeN1 };
}
async function getSoldesRTT(conn, collaborateurId, typeContrat, dateEntree) {
const currentYear = new Date().getFullYear();
const rttType = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = 'RTT' LIMIT 1`);
const typeCongeId = rttType[0].Id;
const compteursN = await conn.query(
`SELECT Solde, Total FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
[collaborateurId, typeCongeId, currentYear]
);
const soldeN = compteursN.length > 0 ? parseFloat(compteursN[0].Solde || 0) : 0;
const totalAcquisN = compteursN.length > 0 ? parseFloat(compteursN[0].Total || 0) : 0;
// Calcul anticipation N
const finAnneeN = new Date(currentYear, 11, 31); // 31 déc N
const rttDataTotalN = await calculerAcquisitionRTT(conn, collaborateurId, finAnneeN);
const soldeAnticipeN = Math.max(0, rttDataTotalN.acquisition - totalAcquisN);
return { soldeN, soldeAnticipeN };
}
async function getSoldesRecup(conn, collaborateurId) {
const currentYear = new Date().getFullYear();
const recupType = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = 'Récupération' LIMIT 1`);
if (recupType.length === 0) return 0;
const typeCongeId = recupType[0].Id;
const compteur = await conn.query(
`SELECT Solde FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
[collaborateurId, typeCongeId, currentYear]
);
return compteur.length > 0 ? parseFloat(compteur[0].Solde || 0) : 0;
}
app.get('/api/getAnticipationDisponible', async (req, res) => {
try {
const { userid, dateDebut } = req.query;
if (!userid || !dateDebut) {
return res.json({ success: false, message: 'Paramètres manquants' });
}
const conn = await pool.getConnection();
const [collabInfo] = await conn.query(
`SELECT DateEntree, TypeContrat, role FROM CollaborateurAD WHERE id = ?`,
[userid]
);
const dateEntree = collabInfo.DateEntree;
const isApprenti = collabInfo.role === 'Apprenti';
// Déterminer si c'est une demande N+1
const dateDemandeObj = new Date(dateDebut);
const currentYear = new Date().getFullYear();
const demandeYear = dateDemandeObj.getFullYear();
const demandeMonth = dateDemandeObj.getMonth() + 1;
const isN1 = (demandeYear === currentYear + 1 && demandeMonth >= 6) ||
(demandeYear === currentYear + 2 && demandeMonth <= 5);
// Calculer les soldes avec anticipation
const soldesCP = await getSoldesCP(conn, userid, dateEntree, isN1);
const soldesRTT = isApprenti ? { soldeN: 0, soldeAnticipeN: 0 } :
await getSoldesRTT(conn, userid, collabInfo.TypeContrat, dateEntree);
const soldesRecup = await getSoldesRecup(conn, userid);
conn.release();
res.json({
success: true,
isN1Request: isN1,
CP: {
actuel: soldesCP.soldeN1 + soldesCP.soldeN,
anticipeN: soldesCP.soldeAnticipeN,
anticipeN1: isN1 ? soldesCP.soldeAnticipeN1 : 0,
total: soldesCP.soldeN1 + soldesCP.soldeN + soldesCP.soldeAnticipeN + (isN1 ? soldesCP.soldeAnticipeN1 : 0)
},
RTT: {
actuel: soldesRTT.soldeN,
anticipeN: soldesRTT.soldeAnticipeN,
total: soldesRTT.soldeN + soldesRTT.soldeAnticipeN
},
Recup: {
actuel: soldesRecup,
total: soldesRecup
}
});
} catch (error) {
console.error('Erreur getAnticipationDisponible:', error);
res.status(500).json({ success: false, message: error.message });
}
});
async function deductLeaveBalanceWithN1(conn, collaborateurId, typeCongeId, nombreJours, demandeCongeId, dateDebut) {
const currentYear = new Date().getFullYear();
const previousYear = currentYear - 1;
const nextYear = currentYear + 1;
let joursRestants = nombreJours;
const deductions = [];
const dateDemandeObj = new Date(dateDebut);
const demandeYear = dateDemandeObj.getFullYear();
const demandeMonth = dateDemandeObj.getMonth() + 1;
// Déterminer le type de congé
const [typeRow] = await conn.query('SELECT Nom FROM TypeConge WHERE Id = ?', [typeCongeId]);
const typeNom = typeRow[0].Nom;
const isCP = typeNom === 'Congé payé';
// Vérifier si demande pour N+1
let useN1 = false;
if (isCP) {
useN1 = (demandeYear === nextYear && demandeMonth >= 6) ||
(demandeYear === nextYear + 1 && demandeMonth <= 5);
} else {
useN1 = demandeYear === nextYear;
}
console.log(`\n💰 Déduction ${typeNom}: ${nombreJours}j (useN1: ${useN1})`);
if (useN1) {
// ORDRE N+1 : N+1 anticipé → N anticipé → N actuel → N-1
// 1. N+1 Anticipé (priorité absolue)
const compteurN1Anticipe = await conn.query(
`SELECT Id, SoldeAnticipe FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
[collaborateurId, typeCongeId, nextYear]
);
if (compteurN1Anticipe.length === 0 && joursRestants > 0) {
// Créer le compteur N+1 si inexistant
await conn.query(
`INSERT INTO CompteurConges
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, SoldeAnticipe, IsAnticipe)
VALUES (?, ?, ?, 0, 0, 0, 0, 0)`,
[collaborateurId, typeCongeId, nextYear]
);
}
// Récupérer à nouveau après création
const compteurN1A = await conn.query(
`SELECT Id, SoldeAnticipe FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
[collaborateurId, typeCongeId, nextYear]
);
if (compteurN1A.length > 0) {
const aDeduire = Math.min(joursRestants, joursRestants); // Tous les jours restants
if (aDeduire > 0) {
await conn.query(
`UPDATE CompteurConges
SET SoldeAnticipe = SoldeAnticipe + ?, IsAnticipe = 1
WHERE Id = ?`,
[aDeduire, compteurN1A[0].Id]
);
await conn.query(
`INSERT INTO DeductionDetails
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
VALUES (?, ?, ?, 'N+1 Anticipé', ?)`,
[demandeCongeId, typeCongeId, nextYear, aDeduire]
);
deductions.push({
annee: nextYear,
type: 'N+1 Anticipé',
joursUtilises: aDeduire
});
joursRestants -= aDeduire;
console.log(`✓ N+1 Anticipé: ${aDeduire}j - reste: ${joursRestants}j`);
}
}
// 2. N anticipé
if (joursRestants > 0) {
const [compteurN_Anticipe] = await conn.query(`
SELECT Id, SoldeAnticipe
FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
`, [collaborateurId, typeCongeId, currentYear]);
if (compteurN_Anticipe.length > 0) {
const soldeNA = parseFloat(compteurN_Anticipe[0].SoldeAnticipe || 0);
const aDeduire = Math.min(soldeNA, joursRestants);
if (aDeduire > 0) {
await conn.query(`
UPDATE CompteurConges
SET SoldeAnticipe = GREATEST(0, SoldeAnticipe - ?)
WHERE Id = ?
`, [aDeduire, compteurN_Anticipe[0].Id]);
await conn.query(`
INSERT INTO DeductionDetails
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
VALUES (?, ?, ?, 'N Anticipé', ?)
`, [demandeCongeId, typeCongeId, currentYear, aDeduire]);
deductions.push({
annee: currentYear,
type: 'N Anticipé',
joursUtilises: aDeduire,
soldeAvant: soldeNA
});
joursRestants -= aDeduire;
console.log(` ✓ N Anticipé: ${aDeduire}j (reste: ${joursRestants}j)`);
}
}
}
// 3. N actuel
if (joursRestants > 0) {
const [compteurN] = await conn.query(`
SELECT Id, Solde, SoldeReporte
FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
`, [collaborateurId, typeCongeId, currentYear]);
if (compteurN.length > 0) {
const soldeN = parseFloat(compteurN[0].Solde) - parseFloat(compteurN[0].SoldeReporte || 0);
const aDeduire = Math.min(soldeN, joursRestants);
if (aDeduire > 0) {
await conn.query(`
UPDATE CompteurConges
SET Solde = GREATEST(0, Solde - ?)
WHERE Id = ?
`, [aDeduire, compteurN[0].Id]);
await conn.query(`
INSERT INTO DeductionDetails
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
VALUES (?, ?, ?, 'Année N', ?)
`, [demandeCongeId, typeCongeId, currentYear, aDeduire]);
deductions.push({
annee: currentYear,
type: 'Année N',
joursUtilises: aDeduire,
soldeAvant: soldeN
});
joursRestants -= aDeduire;
console.log(` ✓ Année N: ${aDeduire}j (reste: ${joursRestants}j)`);
}
}
}
// 4. N-1 reporté
if (joursRestants > 0) {
const [compteurN1] = await conn.query(`
SELECT Id, SoldeReporte
FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
`, [collaborateurId, typeCongeId, previousYear]);
if (compteurN1.length > 0) {
const soldeN1 = parseFloat(compteurN1[0].SoldeReporte || 0);
const aDeduire = Math.min(soldeN1, joursRestants);
if (aDeduire > 0) {
await conn.query(`
UPDATE CompteurConges
SET SoldeReporte = GREATEST(0, SoldeReporte - ?),
Solde = GREATEST(0, Solde - ?)
WHERE Id = ?
`, [aDeduire, aDeduire, compteurN1[0].Id]);
await conn.query(`
INSERT INTO DeductionDetails
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
VALUES (?, ?, ?, 'Reporté N-1', ?)
`, [demandeCongeId, typeCongeId, previousYear, aDeduire]);
deductions.push({
annee: previousYear,
type: 'Reporté N-1',
joursUtilises: aDeduire,
soldeAvant: soldeN1
});
joursRestants -= aDeduire;
console.log(` ✓ Reporté N-1: ${aDeduire}j (reste: ${joursRestants}j)`);
}
}
}
} else {
// ORDRE NORMAL : N-1 → N → N anticipé
// 1. Reporté N-1
const [compteurN1] = await conn.query(`
SELECT Id, SoldeReporte, Solde
FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
`, [collaborateurId, typeCongeId, previousYear]);
if (compteurN1.length > 0 && compteurN1[0].SoldeReporte > 0) {
const soldeN1 = parseFloat(compteurN1[0].SoldeReporte);
const aDeduire = Math.min(soldeN1, joursRestants);
if (aDeduire > 0) {
await conn.query(`
UPDATE CompteurConges
SET SoldeReporte = GREATEST(0, SoldeReporte - ?),
Solde = GREATEST(0, Solde - ?)
WHERE Id = ?
`, [aDeduire, aDeduire, compteurN1[0].Id]);
await conn.query(`
INSERT INTO DeductionDetails
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
VALUES (?, ?, ?, 'Reporté N-1', ?)
`, [demandeCongeId, typeCongeId, previousYear, aDeduire]);
deductions.push({
annee: previousYear,
type: 'Reporté N-1',
joursUtilises: aDeduire,
soldeAvant: soldeN1
});
joursRestants -= aDeduire;
console.log(` ✓ Reporté N-1: ${aDeduire}j (reste: ${joursRestants}j)`);
}
}
// 2. Année N
if (joursRestants > 0) {
const [compteurN] = await conn.query(`
SELECT Id, Solde, SoldeReporte
FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
`, [collaborateurId, typeCongeId, currentYear]);
if (compteurN.length > 0) {
const soldeN = parseFloat(compteurN[0].Solde) - parseFloat(compteurN[0].SoldeReporte || 0);
const aDeduire = Math.min(soldeN, joursRestants);
if (aDeduire > 0) {
await conn.query(`
UPDATE CompteurConges
SET Solde = GREATEST(0, Solde - ?)
WHERE Id = ?
`, [aDeduire, compteurN[0].Id]);
await conn.query(`
INSERT INTO DeductionDetails
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
VALUES (?, ?, ?, 'Année N', ?)
`, [demandeCongeId, typeCongeId, currentYear, aDeduire]);
deductions.push({
annee: currentYear,
type: 'Année N',
joursUtilises: aDeduire,
soldeAvant: soldeN
});
joursRestants -= aDeduire;
console.log(` ✓ Année N: ${aDeduire}j (reste: ${joursRestants}j)`);
}
}
}
// 3. N anticipé
if (joursRestants > 0) {
const [compteurN_Anticipe] = await conn.query(`
SELECT Id, SoldeAnticipe
FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
`, [collaborateurId, typeCongeId, currentYear]);
if (compteurN_Anticipe.length > 0) {
const soldeNA = parseFloat(compteurN_Anticipe[0].SoldeAnticipe || 0);
const aDeduire = Math.min(soldeNA, joursRestants);
if (aDeduire > 0) {
await conn.query(`
UPDATE CompteurConges
SET SoldeAnticipe = GREATEST(0, SoldeAnticipe - ?)
WHERE Id = ?
`, [aDeduire, compteurN_Anticipe[0].Id]);
await conn.query(`
INSERT INTO DeductionDetails
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
VALUES (?, ?, ?, 'N Anticipé', ?)
`, [demandeCongeId, typeCongeId, currentYear, aDeduire]);
deductions.push({
annee: currentYear,
type: 'N Anticipé',
joursUtilises: aDeduire,
soldeAvant: soldeNA
});
joursRestants -= aDeduire;
console.log(` ✓ N Anticipé: ${aDeduire}j (reste: ${joursRestants}j)`);
}
}
}
}
return {
success: joursRestants === 0,
joursDeduitsTotal: nombreJours - joursRestants,
joursNonDeduits: joursRestants,
details: deductions,
useN1: useN1
};
}
/**
* Met à jour les soldes anticipés pour un collaborateur
* Appelée après chaque mise à jour de compteur ou soumission de demande
*/
async function updateSoldeAnticipe(conn, collaborateurId) {
const today = new Date();
today.setHours(0, 0, 0, 0);
const currentYear = today.getFullYear();
console.log(`\n🔄 Mise à jour soldes anticipés pour collaborateur ${collaborateurId}`);
const [collab] = await conn.query(
'SELECT DateEntree, TypeContrat, role FROM CollaborateurAD WHERE id = ?',
[collaborateurId]
);
if (collab.length === 0) {
console.log(' ❌ Collaborateur non trouvé');
return;
}
const dateEntree = collab[0].DateEntree;
const typeContrat = collab[0].TypeContrat || '37h';
const isApprenti = collab[0].role === 'Apprenti';
// ===== CP ANTICIPÉ =====
const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']);
if (cpType.length > 0) {
const cpAnticipe = calculerAcquisitionCPAnticipee(today, dateEntree);
// Vérifier si le compteur existe
const [compteurCP] = await conn.query(`
SELECT Id FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
`, [collaborateurId, cpType[0].Id, currentYear]);
if (compteurCP.length > 0) {
await conn.query(`
UPDATE CompteurConges
SET SoldeAnticipe = ?,
DerniereMiseAJour = NOW()
WHERE Id = ?
`, [cpAnticipe, compteurCP[0].Id]);
} else {
// Créer le compteur s'il n'existe pas
const acquisCP = calculerAcquisitionCP(today, dateEntree);
await conn.query(`
INSERT INTO CompteurConges
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, SoldeAnticipe, DerniereMiseAJour)
VALUES (?, ?, ?, ?, ?, 0, ?, NOW())
`, [collaborateurId, cpType[0].Id, currentYear, acquisCP, acquisCP, cpAnticipe]);
}
console.log(` ✓ CP Anticipé: ${cpAnticipe.toFixed(2)}j`);
}
// ===== RTT ANTICIPÉ =====
if (!isApprenti) {
const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']);
if (rttType.length > 0) {
const rttAnticipe = await calculerAcquisitionRTTAnticipee(conn, collaborateurId, today);
// Vérifier si le compteur existe
const [compteurRTT] = await conn.query(`
SELECT Id FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
`, [collaborateurId, rttType[0].Id, currentYear]);
if (compteurRTT.length > 0) {
await conn.query(`
UPDATE CompteurConges
SET SoldeAnticipe = ?,
DerniereMiseAJour = NOW()
WHERE Id = ?
`, [rttAnticipe, compteurRTT[0].Id]);
} else {
// Créer le compteur s'il n'existe pas
const rttData = await calculerAcquisitionRTT(conn, collaborateurId, today);
await conn.query(`
INSERT INTO CompteurConges
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, SoldeAnticipe, DerniereMiseAJour)
VALUES (?, ?, ?, ?, ?, 0, ?, NOW())
`, [collaborateurId, rttType[0].Id, currentYear, rttData.acquisition, rttData.acquisition, rttAnticipe]);
}
console.log(` ✓ RTT Anticipé: ${rttAnticipe.toFixed(2)}j`);
}
}
console.log(` ✅ Soldes anticipés mis à jour\n`);
}
/**
* GET /getSoldesAnticipes
* Retourne les soldes actuels ET anticipés pour un collaborateur
*/
app.get('/api/getSoldesAnticipes', async (req, res) => {
try {
const userIdParam = req.query.user_id;
const dateRefParam = req.query.date_reference;
if (!userIdParam) {
return res.json({ success: false, message: 'ID utilisateur manquant' });
}
const conn = await pool.getConnection();
// Déterminer l'ID
const isUUID = userIdParam.length > 10 && userIdParam.includes('-');
const userQuery = `
SELECT ca.id, ca.prenom, ca.nom, ca.DateEntree, ca.TypeContrat, ca.role
FROM CollaborateurAD ca
WHERE ${isUUID ? 'ca.entraUserId' : 'ca.id'} = ?
AND (ca.Actif = 1 OR ca.Actif IS NULL)
`;
const [userInfo] = await conn.query(userQuery, [userIdParam]);
if (userInfo.length === 0) {
conn.release();
return res.json({ success: false, message: 'Utilisateur non trouvé' });
}
const user = userInfo[0];
const userId = user.id;
const dateEntree = user.DateEntree;
const typeContrat = user.TypeContrat || '37h';
const isApprenti = user.role === 'Apprenti';
const dateReference = dateRefParam ? new Date(dateRefParam) : new Date();
dateReference.setHours(0, 0, 0, 0);
const currentYear = dateReference.getFullYear();
// ===== CP =====
const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']);
let cpData = {
acquis: 0,
solde: 0,
anticipe: 0,
totalDisponible: 0
};
if (cpType.length > 0) {
// Acquisition actuelle
const acquisCP = calculerAcquisitionCP(dateReference, dateEntree);
// Anticipé
const anticipeCP = calculerAcquisitionCPAnticipee(dateReference, dateEntree);
// Solde en base (avec consommations déduites)
const [compteurCP] = await conn.query(`
SELECT Solde, SoldeReporte, SoldeAnticipe
FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
`, [userId, cpType[0].Id, currentYear]);
// Reporté N-1
const [compteurCPN1] = await conn.query(`
SELECT Solde as SoldeReporte
FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
`, [userId, cpType[0].Id, currentYear - 1]);
const soldeN1 = compteurCPN1.length > 0 ? parseFloat(compteurCPN1[0].SoldeReporte || 0) : 0;
const soldeN = compteurCP.length > 0 ? parseFloat(compteurCP[0].Solde || 0) : acquisCP;
cpData = {
acquis: parseFloat(acquisCP.toFixed(2)),
soldeN1: parseFloat(soldeN1.toFixed(2)),
soldeN: parseFloat((soldeN - soldeN1).toFixed(2)),
soldeTotal: parseFloat(soldeN.toFixed(2)),
anticipe: parseFloat(anticipeCP.toFixed(2)),
totalDisponible: parseFloat((soldeN + anticipeCP).toFixed(2))
};
}
// ===== RTT =====
let rttData = {
acquis: 0,
solde: 0,
anticipe: 0,
totalDisponible: 0,
isApprenti: isApprenti
};
if (!isApprenti) {
const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']);
if (rttType.length > 0) {
// Acquisition actuelle
const rttCalc = await calculerAcquisitionRTT(conn, userId, dateReference);
const acquisRTT = rttCalc.acquisition;
// Anticipé
const anticipeRTT = await calculerAcquisitionRTTAnticipee(conn, userId, dateReference);
// Solde en base
const [compteurRTT] = await conn.query(`
SELECT Solde, SoldeAnticipe
FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
`, [userId, rttType[0].Id, currentYear]);
const soldeRTT = compteurRTT.length > 0 ? parseFloat(compteurRTT[0].Solde || 0) : acquisRTT;
rttData = {
acquis: parseFloat(acquisRTT.toFixed(2)),
solde: parseFloat(soldeRTT.toFixed(2)),
anticipe: parseFloat(anticipeRTT.toFixed(2)),
totalDisponible: parseFloat((soldeRTT + anticipeRTT).toFixed(2)),
config: rttCalc.config,
typeContrat: typeContrat
};
}
}
// ===== RÉCUP =====
const [recupType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Récupération']);
let recupData = { solde: 0 };
if (recupType.length > 0) {
const [compteurRecup] = await conn.query(`
SELECT Solde FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
`, [userId, recupType[0].Id, currentYear]);
recupData.solde = compteurRecup.length > 0 ? parseFloat(compteurRecup[0].Solde || 0) : 0;
}
conn.release();
res.json({
success: true,
user: {
id: user.id,
nom: `${user.prenom} ${user.nom}`,
typeContrat: typeContrat,
dateEntree: dateEntree ? formatDateWithoutUTC(dateEntree) : null
},
dateReference: dateReference.toISOString().split('T')[0],
cp: cpData,
rtt: rttData,
recup: recupData,
totalGeneral: {
disponibleActuel: parseFloat((cpData.soldeTotal + rttData.solde + recupData.solde).toFixed(2)),
disponibleAvecAnticipe: parseFloat((cpData.totalDisponible + rttData.totalDisponible + recupData.solde).toFixed(2))
}
});
} catch (error) {
console.error('Erreur getSoldesAnticipes:', error);
res.status(500).json({ success: false, message: 'Erreur serveur', error: error.message });
}
});
/**
* GET /getCongesAnticipes
* Calcule les congés anticipés disponibles pour un collaborateur
*/
app.get('/api/getCongesAnticipes', async (req, res) => {
try {
const userIdParam = req.query.user_id;
if (!userIdParam) {
return res.json({ success: false, message: 'ID utilisateur manquant' });
}
const conn = await pool.getConnection();
// Déterminer l'ID (UUID ou numérique)
const isUUID = userIdParam.length > 10 && userIdParam.includes('-');
const userQuery = `
SELECT
ca.id,
ca.prenom,
ca.nom,
ca.email,
ca.DateEntree,
ca.TypeContrat,
ca.role,
ca.CampusId
FROM CollaborateurAD ca
WHERE ${isUUID ? 'ca.entraUserId' : 'ca.id'} = ?
AND (ca.Actif = 1 OR ca.Actif IS NULL)
`;
const [userInfo] = await conn.query(userQuery, [userIdParam]);
if (userInfo.length === 0) {
conn.release();
return res.json({ success: false, message: 'Utilisateur non trouvé' });
}
const user = userInfo[0];
const userId = user.id;
const dateEntree = user.DateEntree;
const typeContrat = user.TypeContrat || '37h';
const today = new Date();
const currentYear = today.getFullYear();
const finAnnee = new Date(currentYear, 11, 31); // 31 décembre
// ========================================
// CALCUL CP (Congés Payés)
// ========================================
// Acquisition actuelle
const acquisActuelleCP = calculerAcquisitionCP(today, dateEntree);
// Acquisition prévue à la fin de l'exercice (31 mai N+1)
const finExerciceCP = new Date(currentYear + 1, 4, 31); // 31 mai N+1
const acquisTotaleCP = calculerAcquisitionCP(finExerciceCP, dateEntree);
// Récupérer le solde actuel
const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']);
let soldeActuelCP = 0;
let dejaPrisCP = 0;
if (cpType.length > 0) {
const [compteurCP] = await conn.query(`
SELECT Total, Solde, SoldeReporte
FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
`, [userId, cpType[0].Id, currentYear]);
if (compteurCP.length > 0) {
const total = parseFloat(compteurCP[0].Total || 0);
soldeActuelCP = parseFloat(compteurCP[0].Solde || 0);
dejaPrisCP = total - (soldeActuelCP - parseFloat(compteurCP[0].SoldeReporte || 0));
}
}
// Calculer le potentiel anticipé pour CP
const acquisRestanteCP = acquisTotaleCP - acquisActuelleCP;
const anticipePossibleCP = Math.max(0, acquisRestanteCP);
const limiteAnticipeCP = Math.min(anticipePossibleCP, 25 - dejaPrisCP);
// ========================================
// CALCUL RTT
// ========================================
let anticipePossibleRTT = 0;
let limiteAnticipeRTT = 0;
let soldeActuelRTT = 0;
let dejaPrisRTT = 0;
let acquisActuelleRTT = 0;
let acquisTotaleRTT = 0;
if (user.role !== 'Apprenti') {
// Acquisition actuelle
const rttDataActuel = await calculerAcquisitionRTT(conn, userId, today);
acquisActuelleRTT = rttDataActuel.acquisition;
// Acquisition prévue à la fin de l'année
const rttDataTotal = await calculerAcquisitionRTT(conn, userId, finAnnee);
acquisTotaleRTT = rttDataTotal.acquisition;
// Récupérer le solde actuel
const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']);
if (rttType.length > 0) {
const [compteurRTT] = await conn.query(`
SELECT Total, Solde
FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
`, [userId, rttType[0].Id, currentYear]);
if (compteurRTT.length > 0) {
const total = parseFloat(compteurRTT[0].Total || 0);
soldeActuelRTT = parseFloat(compteurRTT[0].Solde || 0);
dejaPrisRTT = total - soldeActuelRTT;
}
}
// Calculer le potentiel anticipé pour RTT
const acquisRestanteRTT = acquisTotaleRTT - acquisActuelleRTT;
anticipePossibleRTT = Math.max(0, acquisRestanteRTT);
const maxRTT = typeContrat === 'forfait_jour' ? 12 : 10;
limiteAnticipeRTT = Math.min(anticipePossibleRTT, maxRTT - dejaPrisRTT);
}
conn.release();
// ========================================
// RÉPONSE
// ========================================
res.json({
success: true,
user: {
id: user.id,
nom: `${user.prenom} ${user.nom}`,
email: user.email,
typeContrat: typeContrat,
dateEntree: dateEntree ? formatDateWithoutUTC(dateEntree) : null
},
dateReference: today.toISOString().split('T')[0],
congesPayes: {
acquisActuelle: parseFloat(acquisActuelleCP.toFixed(2)),
acquisTotalePrevu: parseFloat(acquisTotaleCP.toFixed(2)),
acquisRestante: parseFloat((acquisTotaleCP - acquisActuelleCP).toFixed(2)),
soldeActuel: parseFloat(soldeActuelCP.toFixed(2)),
dejaPris: parseFloat(dejaPrisCP.toFixed(2)),
anticipePossible: parseFloat(anticipePossibleCP.toFixed(2)),
limiteAnticipe: parseFloat(limiteAnticipeCP.toFixed(2)),
totalDisponible: parseFloat((soldeActuelCP + limiteAnticipeCP).toFixed(2)),
message: limiteAnticipeCP > 0
? `Vous pouvez poser jusqu'à ${limiteAnticipeCP.toFixed(1)} jours de CP en anticipé`
: "Vous avez atteint la limite d'anticipation pour les CP"
},
rtt: user.role !== 'Apprenti' ? {
acquisActuelle: parseFloat(acquisActuelleRTT.toFixed(2)),
acquisTotalePrevu: parseFloat(acquisTotaleRTT.toFixed(2)),
acquisRestante: parseFloat((acquisTotaleRTT - acquisActuelleRTT).toFixed(2)),
soldeActuel: parseFloat(soldeActuelRTT.toFixed(2)),
dejaPris: parseFloat(dejaPrisRTT.toFixed(2)),
anticipePossible: parseFloat(anticipePossibleRTT.toFixed(2)),
limiteAnticipe: parseFloat(limiteAnticipeRTT.toFixed(2)),
totalDisponible: parseFloat((soldeActuelRTT + limiteAnticipeRTT).toFixed(2)),
message: limiteAnticipeRTT > 0
? `Vous pouvez poser jusqu'à ${limiteAnticipeRTT.toFixed(1)} jours de RTT en anticipé`
: "Vous avez atteint la limite d'anticipation pour les RTT"
} : null,
regles: {
cpMaxAnnuel: 25,
rttMaxAnnuel: typeContrat === 'forfait_jour' ? 12 : 10,
description: "Les congés anticipés sont basés sur l'acquisition prévue jusqu'à la fin de l'exercice/année"
}
});
} catch (error) {
console.error('Erreur getCongesAnticipes:', error);
res.status(500).json({
success: false,
message: 'Erreur serveur',
error: error.message
});
}
});
/**
* Calcule l'acquisition CP ANTICIPÉE (ce qui reste à acquérir jusqu'à fin d'exercice)
*/
function calculerAcquisitionCPAnticipee(dateReference = new Date(), dateEntree = null) {
const d = new Date(dateReference);
d.setHours(0, 0, 0, 0);
const annee = d.getFullYear();
const mois = d.getMonth() + 1;
// 1⃣ Déterminer la fin de l'exercice CP (31/05)
let finExercice;
if (mois >= 6) {
finExercice = new Date(annee + 1, 4, 31); // 31/05/N+1
} else {
finExercice = new Date(annee, 4, 31); // 31/05/N
}
finExercice.setHours(0, 0, 0, 0);
// 2⃣ Calculer l'acquisition actuelle
const acquisActuelle = calculerAcquisitionCP(d, dateEntree);
// 3⃣ Calculer l'acquisition totale à fin d'exercice
const acquisTotaleFinExercice = calculerAcquisitionCP(finExercice, dateEntree);
// 4⃣ Anticipée = Totale - Actuelle (plafonnée à 25)
const acquisAnticipee = Math.min(25, acquisTotaleFinExercice) - acquisActuelle;
return Math.max(0, Math.round(acquisAnticipee * 100) / 100);
}
/**
* Calcule l'acquisition RTT ANTICIPÉE (ce qui reste à acquérir jusqu'à fin d'année)
*/
async function calculerAcquisitionRTTAnticipee(conn, collaborateurId, dateReference = new Date()) {
const d = new Date(dateReference);
d.setHours(0, 0, 0, 0);
const annee = d.getFullYear();
// 1⃣ Récupérer les infos du collaborateur
const [collabInfo] = await conn.query(
`SELECT TypeContrat, DateEntree, role FROM CollaborateurAD WHERE id = ?`,
[collaborateurId]
);
if (collabInfo.length === 0) {
return 0;
}
const typeContrat = collabInfo[0].TypeContrat || '37h';
const isApprenti = collabInfo[0].role === 'Apprenti';
// 2⃣ Apprentis = pas de RTT
if (isApprenti) {
return 0;
}
// 3⃣ Récupérer la configuration RTT
const config = await getConfigurationRTT(conn, annee, typeContrat);
// 4⃣ Calculer l'acquisition actuelle
const rttActuel = await calculerAcquisitionRTT(conn, collaborateurId, d);
const acquisActuelle = rttActuel.acquisition;
// 5⃣ Calculer l'acquisition totale à fin d'année (31/12)
const finAnnee = new Date(annee, 11, 31);
finAnnee.setHours(0, 0, 0, 0);
const rttFinAnnee = await calculerAcquisitionRTT(conn, collaborateurId, finAnnee);
const acquisTotaleFinAnnee = rttFinAnnee.acquisition;
// 6⃣ Anticipée = Totale - Actuelle (plafonnée au max annuel)
const acquisAnticipee = Math.min(config.joursAnnuels, acquisTotaleFinAnnee) - acquisActuelle;
return Math.max(0, Math.round(acquisAnticipee * 100) / 100);
}
app.get('/api/getStatistiquesCompteurs', async (req, res) => {
try {
const conn = await pool.getConnection();
const currentYear = new Date().getFullYear();
const [totalCollabs] = await conn.query(
'SELECT COUNT(*) as total FROM CollaborateurAD WHERE actif = 1 OR actif IS NULL'
);
const [statsTypeContrat] = await conn.query(`
SELECT
TypeContrat,
COUNT(*) as nombre,
GROUP_CONCAT(CONCAT(prenom, ' ', nom) SEPARATOR ', ') as noms
FROM CollaborateurAD
WHERE actif = 1 OR actif IS NULL
GROUP BY TypeContrat
`);
const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']);
let statsCP = { total_acquis: 0, total_solde: 0, moyenne_utilisation: 0 };
if (cpType.length > 0) {
const [cpStats] = await conn.query(`
SELECT
SUM(Total) as total_acquis,
SUM(Solde) as total_solde,
AVG(CASE WHEN Total > 0 THEN ((Total - Solde) / Total) * 100 ELSE 0 END) as moyenne_utilisation
FROM CompteurConges
WHERE TypeCongeId = ? AND Annee = ?
`, [cpType[0].Id, currentYear]);
if (cpStats.length > 0) {
statsCP = {
total_acquis: parseFloat((cpStats[0].total_acquis || 0).toFixed(2)),
total_solde: parseFloat((cpStats[0].total_solde || 0).toFixed(2)),
moyenne_utilisation: parseFloat((cpStats[0].moyenne_utilisation || 0).toFixed(1))
};
}
}
const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']);
let statsRTT = { total_acquis: 0, total_solde: 0, moyenne_utilisation: 0 };
if (rttType.length > 0) {
const [rttStats] = await conn.query(`
SELECT
SUM(Total) as total_acquis,
SUM(Solde) as total_solde,
AVG(CASE WHEN Total > 0 THEN ((Total - Solde) / Total) * 100 ELSE 0 END) as moyenne_utilisation
FROM CompteurConges
WHERE TypeCongeId = ? AND Annee = ?
`, [rttType[0].Id, currentYear]);
if (rttStats.length > 0) {
statsRTT = {
total_acquis: parseFloat((rttStats[0].total_acquis || 0).toFixed(2)),
total_solde: parseFloat((rttStats[0].total_solde || 0).toFixed(2)),
moyenne_utilisation: parseFloat((rttStats[0].moyenne_utilisation || 0).toFixed(1))
};
}
}
conn.release();
res.json({
success: true,
annee: currentYear,
statistiques: {
collaborateurs: {
total: totalCollabs[0].total,
par_type_contrat: statsTypeContrat
},
conges_payes: statsCP,
rtt: statsRTT
}
});
} catch (error) {
console.error('Erreur getStatistiquesCompteurs:', error);
res.status(500).json({
success: false,
message: 'Erreur serveur',
error: error.message
});
}
});
async function hasCompteRenduAccess(userId) {
try {
const conn = await pool.getConnection();
const [user] = await conn.query(`
SELECT TypeContrat, role
FROM CollaborateurAD
WHERE id = ?
`, [userId]);
conn.release();
if (!user.length) return false;
const userInfo = user[0];
// Accès si :
// 1. TypeContrat = 'forfait_jour'
// 2. role = 'Directeur Campus' ou 'Directrice Campus'
// 3. role = 'RH' ou 'Admin'
return (
userInfo.TypeContrat === 'forfait_jour' ||
userInfo.role === 'Directeur Campus' ||
userInfo.role === 'Directrice Campus' ||
userInfo.role === 'RH' ||
userInfo.role === 'Admin'
);
} catch (error) {
console.error('Erreur vérification accès:', error);
return false;
}
}
// Récupérer les jours du mois
// GET - Récupérer les données du compte-rendu
app.get('/api/getCompteRenduActivites', async (req, res) => {
const { user_id, annee, mois } = req.query;
try {
// Vérifier l'accès
const hasAccess = await hasCompteRenduAccess(user_id);
if (!hasAccess) {
return res.json({
success: false,
message: 'Accès réservé aux collaborateurs en forfait jour et aux directeurs de campus'
});
}
const conn = await pool.getConnection();
const [jours] = await conn.query(`
SELECT
id,
CollaborateurADId,
Annee,
Mois,
DATE_FORMAT(JourDate, '%Y-%m-%d') as JourDate,
JourTravaille,
ReposQuotidienRespect,
ReposHebdomadaireRespect,
CommentaireRepos,
Verrouille,
DateSaisie,
SaisiePar
FROM CompteRenduActivites
WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ?
ORDER BY JourDate
`, [user_id, annee, mois]);
console.log('🔍 Backend - Jours trouvés:', jours.length);
if (jours.length > 0) {
console.log('📅 Premier jour:', jours[0].JourDate, 'Type:', typeof jours[0].JourDate);
}
const [mensuel] = await conn.query(`
SELECT * FROM CompteRenduMensuel
WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ?
`, [user_id, annee, mois]);
conn.release();
res.json({
success: true,
jours: jours,
mensuel: mensuel[0] || null
});
} catch (error) {
console.error('Erreur getCompteRenduActivites:', error);
res.status(500).json({ success: false, message: error.message });
}
});
// POST - Sauvegarder un jour avec AUTO-VERROUILLAGE
// POST - Sauvegarder un jour avec AUTO-VERROUILLAGE
app.post('/api/saveCompteRenduJour', async (req, res) => {
const { user_id, date, jour_travaille, repos_quotidien, repos_hebdo, commentaire, rh_override } = req.body;
try {
const conn = await pool.getConnection();
await conn.beginTransaction();
const dateJour = new Date(date);
const aujourdhui = new Date();
aujourdhui.setHours(0, 0, 0, 0);
dateJour.setHours(0, 0, 0, 0);
// Bloquer saisie du jour actuel (il faut attendre le lendemain)
if (dateJour >= aujourdhui) {
await conn.rollback();
conn.release();
return res.json({ success: false, message: 'Vous ne pouvez pas saisir le jour actuel. Veuillez attendre demain.' });
}
const annee = dateJour.getFullYear();
const mois = dateJour.getMonth() + 1;
// Vérifier si le JOUR est déjà verrouillé (pas le mois entier)
const [jourExistant] = await conn.query(
'SELECT Verrouille FROM CompteRenduActivites WHERE CollaborateurADId = ? AND JourDate = ?',
[user_id, date]
);
if (jourExistant[0]?.Verrouille && !rh_override) {
await conn.rollback();
conn.release();
return res.json({ success: false, message: 'Ce jour est verrouillé - Contactez les RH pour modification' });
}
// Vérifier commentaire obligatoire
if (!repos_quotidien || !repos_hebdo) {
if (!commentaire || commentaire.trim() === '') {
await conn.rollback();
conn.release();
return res.json({ success: false, message: 'Commentaire obligatoire en cas de non-respect des repos' });
}
}
// Insérer ou mettre à jour le jour ET LE VERROUILLER
await conn.query(`
INSERT INTO CompteRenduActivites
(CollaborateurADId, Annee, Mois, JourDate, JourTravaille,
ReposQuotidienRespect, ReposHebdomadaireRespect, CommentaireRepos,
DateSaisie, SaisiePar, Verrouille)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW(), ?, TRUE)
ON DUPLICATE KEY UPDATE
JourTravaille = VALUES(JourTravaille),
ReposQuotidienRespect = VALUES(ReposQuotidienRespect),
ReposHebdomadaireRespect = VALUES(ReposHebdomadaireRespect),
CommentaireRepos = VALUES(CommentaireRepos),
SaisiePar = VALUES(SaisiePar),
Verrouille = TRUE
`, [user_id, annee, mois, date, jour_travaille, repos_quotidien, repos_hebdo, commentaire, user_id]);
// Mettre à jour les statistiques mensuelles (SANS verrouiller le mois)
const [stats] = await conn.query(`
SELECT
COUNT(*) as nbJours,
SUM(CASE WHEN ReposQuotidienRespect = FALSE THEN 1 ELSE 0 END) as nbNonRespectQuotidien,
SUM(CASE WHEN ReposHebdomadaireRespect = FALSE THEN 1 ELSE 0 END) as nbNonRespectHebdo
FROM CompteRenduActivites
WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ?
AND JourTravaille = TRUE
`, [user_id, annee, mois]);
await conn.query(`
INSERT INTO CompteRenduMensuel
(CollaborateurADId, Annee, Mois, NbJoursTravailles,
NbJoursNonRespectsReposQuotidien, NbJoursNonRespectsReposHebdo,
Statut, DateValidation)
VALUES (?, ?, ?, ?, ?, ?, 'En cours', NOW())
ON DUPLICATE KEY UPDATE
NbJoursTravailles = VALUES(NbJoursTravailles),
NbJoursNonRespectsReposQuotidien = VALUES(NbJoursNonRespectsReposQuotidien),
NbJoursNonRespectsReposHebdo = VALUES(NbJoursNonRespectsReposHebdo),
DateValidation = NOW()
`, [user_id, annee, mois, stats[0].nbJours, stats[0].nbNonRespectQuotidien, stats[0].nbNonRespectHebdo]);
await conn.commit();
conn.release();
res.json({
success: true,
message: 'Jour enregistré et verrouillé',
verrouille: true
});
} catch (error) {
console.error('❌ Erreur saveCompteRenduJour:', error);
res.status(500).json({ success: false, message: error.message });
}
});
// POST - Saisie en masse avec AUTO-VERROUILLAGE
app.post('/api/saveCompteRenduMasse', async (req, res) => {
const { user_id, annee, mois, jours, rh_override } = req.body;
try {
const conn = await pool.getConnection();
await conn.beginTransaction();
let count = 0;
let blocked = 0;
for (const jour of jours) {
const dateJour = new Date(jour.date);
const aujourdhui = new Date();
aujourdhui.setHours(0, 0, 0, 0);
dateJour.setHours(0, 0, 0, 0);
// Bloquer le jour actuel
if (dateJour >= aujourdhui) {
blocked++;
continue;
}
// Vérifier si déjà verrouillé
const [jourExistant] = await conn.query(
'SELECT Verrouille FROM CompteRenduActivites WHERE CollaborateurADId = ? AND JourDate = ?',
[user_id, jour.date]
);
if (jourExistant[0]?.Verrouille && !rh_override) {
blocked++;
continue;
}
await conn.query(`
INSERT INTO CompteRenduActivites
(CollaborateurADId, Annee, Mois, JourDate, JourTravaille,
ReposQuotidienRespect, ReposHebdomadaireRespect, CommentaireRepos,
DateSaisie, SaisiePar, Verrouille)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW(), ?, TRUE)
ON DUPLICATE KEY UPDATE
JourTravaille = VALUES(JourTravaille),
ReposQuotidienRespect = VALUES(ReposQuotidienRespect),
ReposHebdomadaireRespect = VALUES(ReposHebdomadaireRespect),
CommentaireRepos = VALUES(CommentaireRepos),
SaisiePar = VALUES(SaisiePar),
Verrouille = TRUE
`, [
user_id, annee, mois, jour.date,
jour.jour_travaille, jour.repos_quotidien, jour.repos_hebdo,
jour.commentaire || null, user_id
]);
count++;
}
// Mettre à jour statistiques mensuelles
const [stats] = await conn.query(`
SELECT
COUNT(*) as nbJours,
SUM(CASE WHEN ReposQuotidienRespect = FALSE THEN 1 ELSE 0 END) as nbNonRespectQuotidien,
SUM(CASE WHEN ReposHebdomadaireRespect = FALSE THEN 1 ELSE 0 END) as nbNonRespectHebdo
FROM CompteRenduActivites
WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ?
AND JourTravaille = TRUE
`, [user_id, annee, mois]);
await conn.query(`
INSERT INTO CompteRenduMensuel
(CollaborateurADId, Annee, Mois, NbJoursTravailles,
NbJoursNonRespectsReposQuotidien, NbJoursNonRespectsReposHebdo,
Statut, DateValidation)
VALUES (?, ?, ?, ?, ?, ?, 'En cours', NOW())
ON DUPLICATE KEY UPDATE
NbJoursTravailles = VALUES(NbJoursTravailles),
NbJoursNonRespectsReposQuotidien = VALUES(NbJoursNonRespectsReposQuotidien),
NbJoursNonRespectsReposHebdo = VALUES(NbJoursNonRespectsReposHebdo),
DateValidation = NOW()
`, [user_id, annee, mois, stats[0].nbJours, stats[0].nbNonRespectQuotidien, stats[0].nbNonRespectHebdo]);
await conn.commit();
conn.release();
res.json({
success: true,
count: count,
blocked: blocked,
message: `${count} jours enregistrés${blocked > 0 ? `, ${blocked} ignorés (jour actuel ou déjà verrouillés)` : ''}`
});
} catch (error) {
console.error('❌ Erreur saisie masse:', error);
res.status(500).json({ success: false, message: error.message });
}
});
app.post('/api/deverrouillerJour', async (req, res) => {
const { user_id, date, rh_user_id } = req.body;
try {
const conn = await pool.getConnection();
const [rhUser] = await conn.query(
'SELECT role FROM CollaborateurAD WHERE id = ?',
[rh_user_id]
);
if (!rhUser.length || (rhUser[0].role !== 'RH' && rhUser[0].role !== 'Admin')) {
conn.release();
return res.json({ success: false, message: 'Action réservée aux RH' });
}
await conn.query(`
UPDATE CompteRenduActivites
SET Verrouille = FALSE
WHERE CollaborateurADId = ? AND JourDate = ?
`, [user_id, date]);
conn.release();
res.json({ success: true });
} catch (error) {
console.error('❌ Erreur déverrouillage jour:', error);
res.status(500).json({ success: false, message: error.message });
}
});
// POST - Verrouiller (RH uniquement)
app.post('/api/verrouillerCompteRendu', async (req, res) => {
const { user_id, annee, mois, rh_user_id } = req.body;
try {
const conn = await pool.getConnection();
// Vérifier que l'utilisateur est RH
const [rhUser] = await conn.query(
'SELECT role FROM CollaborateurAD WHERE id = ?',
[rh_user_id]
);
if (!rhUser.length || (rhUser[0].role !== 'RH' && rhUser[0].role !== 'Admin')) {
conn.release();
return res.json({ success: false, message: 'Action réservée aux RH' });
}
await conn.query(`
UPDATE CompteRenduMensuel
SET Verrouille = TRUE,
DateModification = NOW()
WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ?
`, [user_id, annee, mois]);
conn.release();
res.json({ success: true });
} catch (error) {
console.error('Erreur verrouillage:', error);
res.status(500).json({ success: false, message: error.message });
}
});
// POST - Déverrouiller (RH uniquement)
app.post('/api/deverrouillerCompteRendu', async (req, res) => {
const { user_id, annee, mois, rh_user_id } = req.body;
try {
const conn = await pool.getConnection();
// Vérifier que l'utilisateur est RH
const [rhUser] = await conn.query(
'SELECT role FROM CollaborateurAD WHERE id = ?',
[rh_user_id]
);
if (!rhUser.length || (rhUser[0].role !== 'RH' && rhUser[0].role !== 'Admin')) {
conn.release();
return res.json({ success: false, message: 'Action réservée aux RH' });
}
await conn.query(`
UPDATE CompteRenduMensuel
SET Verrouille = FALSE,
DateDeverrouillage = NOW(),
DeverrouillePar = ?
WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ?
`, [rh_user_id, user_id, annee, mois]);
conn.release();
res.json({ success: true });
} catch (error) {
console.error('Erreur déverrouillage:', error);
res.status(500).json({ success: false, message: error.message });
}
});
// GET - Stats annuelles
app.get('/api/getStatsAnnuelles', async (req, res) => {
const { user_id, annee } = req.query;
try {
const conn = await pool.getConnection();
const [stats] = await conn.query(`
SELECT
SUM(NbJoursTravailles) as totalJoursTravailles,
SUM(NbJoursNonRespectsReposQuotidien) as totalNonRespectQuotidien,
SUM(NbJoursNonRespectsReposHebdo) as totalNonRespectHebdo
FROM CompteRenduMensuel
WHERE CollaborateurADId = ? AND Annee = ?
`, [user_id, annee]);
conn.release();
res.json({
success: true,
stats: stats[0] || {
totalJoursTravailles: 0,
totalNonRespectQuotidien: 0,
totalNonRespectHebdo: 0
}
});
} catch (error) {
console.error('Erreur stats:', error);
res.status(500).json({ success: false, message: error.message });
}
});
// GET - Export PDF (RH uniquement)
app.get('/api/exportCompteRenduPDF', async (req, res) => {
const { user_id, annee, mois } = req.query;
try {
const conn = await pool.getConnection();
// Récupérer les données du collaborateur
const [collab] = await conn.query(
'SELECT prenom, nom, email FROM CollaborateurAD WHERE id = ?',
[user_id]
);
// Récupérer les jours du mois
const [jours] = await conn.query(`
SELECT * FROM CompteRenduActivites
WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ?
ORDER BY JourDate
`, [user_id, annee, mois]);
// Récupérer le mensuel
const [mensuel] = await conn.query(`
SELECT * FROM CompteRenduMensuel
WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ?
`, [user_id, annee, mois]);
conn.release();
// TODO: Générer le PDF avec une bibliothèque comme pdfkit ou puppeteer
// Pour l'instant, retourner les données JSON
res.json({
success: true,
collaborateur: collab[0],
jours: jours,
mensuel: mensuel[0],
message: 'Export PDF à implémenter'
});
} catch (error) {
console.error('Erreur export PDF:', error);
res.status(500).json({ success: false, message: error.message });
}
});
// 📊 ROUTE POUR L'ESPACE RH - Tous les compteurs détaillés
app.get('/api/getAllDetailedCounters', async (req, res) => {
try {
console.log('📊 Récupération de TOUS les compteurs détaillés pour RH');
const conn = await pool.getConnection();
// Récupérer tous les collaborateurs actifs
const [collaborateurs] = await conn.query(`
SELECT DISTINCT ca.id, ca.prenom, ca.nom, ca.email,
ca.role, ca.TypeContrat, ca.DateEntree,
s.Nom as service
FROM CollaborateurAD ca
LEFT JOIN Services s ON ca.ServiceId = s.Id
WHERE ca.Actif = 1 OR ca.Actif IS NULL
ORDER BY ca.nom, ca.prenom
`);
console.log(`👥 ${collaborateurs.length} collaborateurs trouvés`);
const resultats = [];
const currentYear = new Date().getFullYear();
const previousYear = currentYear - 1;
for (const collab of collaborateurs) {
try {
// Récupérer les compteurs détaillés de ce collaborateur
// en utilisant la MÊME logique que getDetailedLeaveCounters
const cpType = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1`, ['Congé payé']);
const rttType = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1`, ['RTT']);
const recupType = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1`, ['Récupération']);
// CP N
if (cpType.length > 0) {
const acquisCP = calculerAcquisitionCP(new Date(), collab.DateEntree);
const [consommeN] = await conn.query(`
SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalConsomme
FROM DeductionDetails dd
JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
WHERE dc.CollaborateurADId = ?
AND dd.TypeCongeId = ?
AND dd.Annee = ?
AND dd.TypeDeduction IN ('Année N', 'Année N', 'Année actuelle N')
AND dd.TypeDeduction NOT IN ('Accum. Récup', 'Accum. Recup', 'N Anticipé')
AND dc.Statut != 'Refusé'
`, [collab.id, cpType[0].Id, currentYear]);
const pris = parseFloat(consommeN.totalConsomme || 0);
const soldeDisponible = Math.max(0, acquisCP - pris);
resultats.push({
collaborateurId: collab.id,
employe: `${collab.prenom} ${collab.nom}`,
email: collab.email,
service: collab.service || 'Non assigné',
typeConge: 'Congé payé',
annee: currentYear,
total: parseFloat(acquisCP.toFixed(2)),
solde: parseFloat(soldeDisponible.toFixed(2)),
consomme: parseFloat(pris.toFixed(2)),
role: collab.role,
typeContrat: collab.TypeContrat
});
}
// CP N-1
if (cpType.length > 0) {
const [cpN1] = await conn.query(`
SELECT Annee, SoldeReporte, Total, Solde
FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
`, [collab.id, cpType[0].Id, previousYear]);
if (cpN1.length > 0 && parseFloat(cpN1[0].Solde || 0) > 0) {
const soldeReporte = parseFloat(cpN1[0].Solde || 0);
const [consommeN1] = await conn.query(`
SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalConsomme
FROM DeductionDetails dd
JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
WHERE dc.CollaborateurADId = ?
AND dd.TypeCongeId = ?
AND dd.Annee = ?
AND dd.TypeDeduction IN ('Année N-1', 'Année N-1', 'Report N-1')
AND dc.Statut != 'Refusé'
`, [collab.id, cpType[0].Id, previousYear]);
const pris = parseFloat(consommeN1.totalConsomme || 0);
const soldeActuel = Math.max(0, soldeReporte - pris);
resultats.push({
collaborateurId: collab.id,
employe: `${collab.prenom} ${collab.nom}`,
email: collab.email,
service: collab.service || 'Non assigné',
typeConge: 'Congé payé',
annee: previousYear,
total: parseFloat(soldeReporte.toFixed(2)),
solde: parseFloat(soldeActuel.toFixed(2)),
consomme: parseFloat(pris.toFixed(2)),
role: collab.role,
typeContrat: collab.TypeContrat
});
}
}
// RTT N
if (rttType.length > 0 && collab.role !== 'Apprenti') {
const rttData = await calculerAcquisitionRTT(conn, collab.id, new Date());
const [consommeRTT] = await conn.query(`
SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalConsomme
FROM DeductionDetails dd
JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
WHERE dc.CollaborateurADId = ?
AND dd.TypeCongeId = ?
AND dd.Annee = ?
AND dd.TypeDeduction NOT IN ('Accum. Récup', 'Accum. Recup', 'Récup Dosée', 'N Anticipé')
AND dc.Statut != 'Refusé'
`, [collab.id, rttType[0].Id, currentYear]);
const pris = parseFloat(consommeRTT.totalConsomme || 0);
const soldeDisponible = Math.max(0, rttData.acquisition - pris);
resultats.push({
collaborateurId: collab.id,
employe: `${collab.prenom} ${collab.nom}`,
email: collab.email,
service: collab.service || 'Non assigné',
typeConge: 'RTT',
annee: currentYear,
total: parseFloat(rttData.acquisition.toFixed(2)),
solde: parseFloat(soldeDisponible.toFixed(2)),
consomme: parseFloat(pris.toFixed(2)),
role: collab.role,
typeContrat: collab.TypeContrat
});
}
// Récupérations N
if (recupType.length > 0) {
const [accumRecup] = await conn.query(`
SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalAccum
FROM DeductionDetails dd
JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
WHERE dc.CollaborateurADId = ?
AND dd.TypeCongeId = ?
AND dd.Annee = ?
AND dd.TypeDeduction IN ('Accum. Récup', 'Accum. Recup')
AND dc.Statut != 'Refusé'
`, [collab.id, recupType[0].Id, currentYear]);
const [consomRecup] = await conn.query(`
SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalConsom
FROM DeductionDetails dd
JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
WHERE dc.CollaborateurADId = ?
AND dd.TypeCongeId = ?
AND dd.Annee = ?
AND dd.TypeDeduction IN ('Récup Dosée', 'Recup Dosee')
AND dc.Statut != 'Refusé'
`, [collab.id, recupType[0].Id, currentYear]);
const acquis = parseFloat(accumRecup.totalAccum || 0);
const pris = parseFloat(consomRecup.totalConsom || 0);
const solde = Math.max(0, acquis - pris);
if (acquis > 0 || pris > 0 || solde > 0) {
resultats.push({
collaborateurId: collab.id,
employe: `${collab.prenom} ${collab.nom}`,
email: collab.email,
service: collab.service || 'Non assigné',
typeConge: 'Récupération',
annee: currentYear,
total: parseFloat(acquis.toFixed(2)),
solde: parseFloat(solde.toFixed(2)),
consomme: parseFloat(pris.toFixed(2)),
role: collab.role,
typeContrat: collab.TypeContrat
});
}
}
} catch (collabError) {
console.error(`❌ Erreur pour ${collab.prenom} ${collab.nom}:`, collabError.message);
}
}
conn.release();
console.log(`${resultats.length} compteurs retournés`);
res.json(resultats);
} catch (error) {
console.error('❌ Erreur getAllDetailedCounters:', error);
res.status(500).json({ error: error.message });
}
});
// ========================================
// DÉMARRAGE DU SERVEUR
// ========================================
app.listen(PORT, '0.0.0.0', () => {
console.log('✅ ✅ ✅ SERVEUR PRINCIPAL DÉMARRÉ ✅ ✅ ✅');
console.log(`📡 Port: ${PORT}`);
console.log(`🗄️ Base: ${dbConfig.database}@${dbConfig.host}`);
console.log(`⏰ Cron jobs: activés`);
console.log(`🌐 CORS origins: ${JSON.stringify(dbConfig)}`);
});