Files
GTA/project/public/Backend/server.js
2025-12-02 17:50:31 +01:00

6245 lines
256 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();
app.use(cors({
origin: '*',
methods: ['GET', 'POST', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
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',
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}`);
}
};
app.post('/api/webhook/receive', async (req, res) => {
try {
const signature = req.headers['x-webhook-signature'];
const payload = req.body;
console.log('📥 Webhook reçu:', payload.event);
// 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.DEMANDE_VALIDATED:
console.log(`📥 Validation reçue: Demande ${data.demandeId} - Statut: ${data.statut}`);
const conn = await pool.getConnection();
try {
await conn.beginTransaction();
// ⭐ GESTION DES COMPTEURS SELON LE STATUT
if (data.statut === 'Refusée' && data.collaborateurId) {
console.log(`❌ DEMANDE REFUSÉE - Restauration des soldes...`);
// Restaurer les soldes via la fonction existante
const restoration = await restoreLeaveBalance(
conn,
data.demandeId,
data.collaborateurId
);
console.log('✅ Restauration terminée:', restoration);
} else if (data.statut === 'Validée') {
console.log(`✅ DEMANDE VALIDÉE - Les jours ont déjà été déduits à la création`);
}
// ⭐ CRÉER UNE NOTIFICATION EN BASE DE DONNÉES
const notifTitle = data.statut === 'Validée'
? 'Demande approuvée ✅'
: 'Demande refusée ❌';
let notifMessage = `Votre demande a été ${data.statut === 'Validée' ? 'approuvée' : 'refusée'}`;
if (data.commentaire) {
notifMessage += ` (Commentaire: ${data.commentaire})`;
}
const notifType = data.statut === 'Validée' ? 'Success' : 'Error';
// ✅ FIX : Remplacer NOW() par nowFR()
await conn.query(
'INSERT INTO Notifications (CollaborateurADId, Titre, Message, Type, DemandeCongeId, DateCreation, lu) VALUES (?, ?, ?, ?, ?, ?, 0)',
[data.collaborateurId, notifTitle, notifMessage, notifType, data.demandeId, nowFR()]
);
console.log('✅ Notification créée en base de données');
await conn.commit();
} catch (error) {
await conn.rollback();
console.error('❌ Erreur traitement webhook:', error);
throw error;
} finally {
conn.release();
}
// Notifier les clients SSE
notifyCollabClients({
type: 'demande-validated-rh',
demandeId: data.demandeId,
statut: data.statut,
timestamp: new Date().toISOString()
}, data.collaborateurId);
notifyCollabClients({
type: 'demande-list-updated',
action: 'validation-rh',
demandeId: data.demandeId,
timestamp: new Date().toISOString()
});
break;
case EVENTS.COMPTEUR_UPDATED:
console.log(`🔄 Compteur mis à jour pour collaborateur ${data.collaborateurId}`);
notifyCollabClients({
type: 'compteur-updated',
collaborateurId: data.collaborateurId,
timestamp: new Date().toISOString()
}, data.collaborateurId);
break;
case EVENTS.DEMANDE_UPDATED:
console.log(`✏️ Demande ${data.demandeId} modifiée via RH`);
// ⭐ CRÉER UNE NOTIFICATION POUR LA MODIFICATION
const connUpdate = await pool.getConnection();
try {
// ✅ FIX : Remplacer NOW() par nowFR()
await connUpdate.query(
'INSERT INTO Notifications (CollaborateurADId, Titre, Message, Type, DemandeCongeId, DateCreation, lu) VALUES (?, ?, ?, ?, ?, ?, 0)',
[data.collaborateurId, 'Demande modifiée ✏️', 'Votre demande a été modifiée par le service RH', 'Info', data.demandeId, nowFR()]
);
console.log('✅ Notification modification créée');
} catch (error) {
console.error('❌ Erreur création notification:', error);
} finally {
connUpdate.release();
}
notifyCollabClients({
type: 'demande-updated-rh',
demandeId: data.demandeId,
timestamp: new Date().toISOString()
}, data.collaborateurId);
break;
case EVENTS.DEMANDE_DELETED:
console.log(`🗑️ Demande ${data.demandeId} supprimée via RH`);
// ⭐ CRÉER UNE NOTIFICATION POUR LA SUPPRESSION
const connDelete = await pool.getConnection();
try {
// ✅ FIX : Remplacer NOW() par nowFR()
await connDelete.query(
'INSERT INTO Notifications (CollaborateurADId, Titre, Message, Type, DemandeCongeId, DateCreation, lu) VALUES (?, ?, ?, ?, ?, ?, 0)',
[data.collaborateurId, 'Demande supprimée 🗑️', 'Votre demande a été supprimée par le service RH', 'Warning', data.demandeId, nowFR()]
);
console.log('✅ Notification suppression créée');
} catch (error) {
console.error('❌ Erreur création notification:', error);
} finally {
connDelete.release();
}
notifyCollabClients({
type: 'demande-deleted-rh',
demandeId: data.demandeId,
timestamp: new Date().toISOString()
}, data.collaborateurId);
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 });
}
});
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
*/
async function getConfigurationRTT(conn, annee, typeContrat = '37h') {
try {
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)
};
}
// Valeurs par défaut si pas de config trouvée
console.warn(`⚠️ Pas de config RTT pour ${annee}/${typeContrat}, utilisation des valeurs par défaut`);
return typeContrat === 'forfait_jour'
? { joursAnnuels: 12, acquisitionMensuelle: 1.0 }
: { joursAnnuels: 10, acquisitionMensuelle: 0.833333 };
} catch (error) {
console.error('Erreur getConfigurationRTT:', error);
// Retour valeur par défaut en cas d'erreur
return { joursAnnuels: 10, acquisitionMensuelle: 0.833333 };
}
}
/**
* Calcule l'acquisition RTT en tenant compte du type de contrat et de l'année
*/
async function calculerAcquisitionRTT(conn, collaborateurId, dateReference = new Date()) {
try {
const d = new Date(dateReference);
const annee = d.getFullYear();
// Récupérer le type de contrat et la date d'entrée du collaborateur
const [collabInfo] = await conn.query(
`SELECT TypeContrat, DateEntree, CampusId 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;
// Récupérer la configuration RTT pour l'année et le type de contrat
const config = await getConfigurationRTT(conn, annee, typeContrat);
// Calculer les mois travaillés dans l'année
const moisTravailles = getMoisTravaillesRTT(dateReference, dateEntree);
// Calculer l'acquisition cumulée
const acquisition = moisTravailles * config.acquisitionMensuelle;
return {
acquisition: Math.round(acquisition * 100) / 100,
moisTravailles: parseFloat(moisTravailles.toFixed(2)),
config: config,
typeContrat: typeContrat
};
} catch (error) {
console.error('Erreur calculerAcquisitionRTT:', error);
throw error;
}
}
/**
* Calcule l'acquisition CP (inchangé, mais pour cohérence)
*/
function calculerAcquisitionCP(dateReference = new Date(), dateEntree = null) {
const moisTravailles = getMoisTravaillesCP(dateReference, dateEntree);
const acquisition = moisTravailles * (25 / 12);
return Math.round(acquisition * 100) / 100;
}
// ========================================
// 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;
}
}
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 = [];
const [collabInfo] = await conn.query(
'SELECT DateEntree, TypeContrat, CampusId FROM CollaborateurAD WHERE id = ?',
[collaborateurId]
);
const dateEntree = collabInfo.length > 0 && collabInfo[0].DateEntree ? collabInfo[0].DateEntree : null;
const typeContrat = collabInfo.length > 0 && collabInfo[0].TypeContrat ? collabInfo[0].TypeContrat : '37h';
// ===== CP (inchangé) =====
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;
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);
const ancienSolde = parseFloat(existingCP[0].Solde);
const soldeReporte = parseFloat(existingCP[0].SoldeReporte || 0);
const incrementTotal = acquisitionCP - ancienTotal;
const nouveauSolde = ancienSolde + incrementTotal;
await conn.query(
`UPDATE CompteurConges
SET Total = ?, Solde = ?, DerniereMiseAJour = NOW()
WHERE Id = ?`,
[acquisitionCP, Math.max(0, nouveauSolde), existingCP[0].Id]
);
updates.push({
type: 'CP',
exercice: exerciceCP,
acquisitionCumulee: acquisitionCP,
increment: incrementTotal,
nouveauSolde: Math.max(0, nouveauSolde)
});
} else {
await conn.query(
`INSERT INTO CompteurConges
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour)
VALUES (?, ?, ?, ?, ?, 0, NOW())`,
[collaborateurId, cpTypeId, currentYear, acquisitionCP, acquisitionCP]
);
updates.push({
type: 'CP',
exercice: exerciceCP,
acquisitionCumulee: acquisitionCP,
action: 'created',
nouveauSolde: acquisitionCP
});
}
}
// ===== RTT (NOUVEAU avec gestion variable) =====
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;
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);
const ancienSolde = parseFloat(existingRTT[0].Solde);
const incrementTotal = acquisitionRTT - ancienTotal;
const nouveauSolde = ancienSolde + incrementTotal;
await conn.query(
`UPDATE CompteurConges
SET Total = ?, Solde = ?, DerniereMiseAJour = NOW()
WHERE Id = ?`,
[acquisitionRTT, Math.max(0, 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: incrementTotal,
nouveauSolde: Math.max(0, nouveauSolde)
});
} else {
await conn.query(
`INSERT INTO CompteurConges
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour)
VALUES (?, ?, ?, ?, ?, 0, NOW())`,
[collaborateurId, rttTypeId, currentYear, acquisitionRTT, acquisitionRTT]
);
updates.push({
type: 'RTT',
annee: currentYear,
typeContrat: rttData.typeContrat,
config: `${rttData.config.joursAnnuels}j/an`,
moisTravailles: rttData.moisTravailles,
acquisitionCumulee: acquisitionRTT,
action: 'created',
nouveauSolde: acquisitionRTT
});
}
}
return updates;
}
// ========================================
// ROUTES API
// ========================================
app.post('/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('/check-user-groups', async (req, res) => {
try {
const { userPrincipalName } = req.body;
const accessToken = req.headers.authorization?.replace('Bearer ', '');
if (!userPrincipalName || !accessToken) return res.json({ authorized: false, message: 'Email ou token manquant' });
const [users] = await pool.query(`
SELECT ca.id, ca.entraUserId, ca.prenom, ca.nom, ca.email,
s.Nom as service, ca.role, 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.email = ?
LIMIT 1
`, [userPrincipalName]);
if (users.length > 0) {
const user = users[0];
return res.json({
authorized: true,
role: user.role,
groups: [user.role],
localUserId: user.id,
user: {
...user,
societeId: user.SocieteId,
societeNom: user.societe_nom
}
});
}
const userGraph = await axios.get(`https://graph.microsoft.com/v1.0/users/${userPrincipalName}?$select=id,displayName,givenName,surname,mail,department,jobTitle`, { headers: { Authorization: `Bearer ${accessToken}` } });
const userInfo = userGraph.data;
const checkMemberResponse = await axios.post(`https://graph.microsoft.com/v1.0/users/${userInfo.id}/checkMemberGroups`, { groupIds: [AZURE_CONFIG.groupId] }, { headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' } });
const isInGroup = checkMemberResponse.data.value.includes(AZURE_CONFIG.groupId);
if (!isInGroup) return res.json({ authorized: false, message: 'Utilisateur non autorisé' });
// ⭐ Insertion avec SocieteId par défaut (ajuster selon votre logique)
const [result] = await pool.query(
`INSERT INTO CollaborateurAD
(entraUserId, prenom, nom, email, service, role, SocieteId)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[userInfo.id, userInfo.givenName, userInfo.surname, userInfo.mail, userInfo.department, 'Collaborateur', null]
);
res.json({
authorized: true,
role: 'Collaborateur',
groups: ['Collaborateur'],
localUserId: result.insertId,
user: {
id: result.insertId,
entraUserId: userInfo.id,
prenom: userInfo.givenName,
nom: userInfo.surname,
email: userInfo.mail,
service: userInfo.department,
role: 'Collaborateur',
societeId: null,
societeNom: null
}
});
} catch (error) {
res.json({ authorized: false, message: 'Erreur serveur', error: error.message });
}
});
app.get('/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();
// ⭐ NOUVEAU : Récupérer le dernier arrêté
const dernierArrete = await getDernierArrete(conn);
const dateArrete = dernierArrete ? new Date(dernierArrete.DateArrete) : null;
// 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
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 dateRefParam = req.query.dateRef;
const today = dateRefParam ? parseDateYYYYMMDD(dateRefParam) : new Date();
const currentYear = today.getFullYear();
const previousYear = currentYear - 1;
// ⭐ MODIFICATION CRITIQUE : Utiliser la date d'arrêté si elle est plus récente que la référence
const dateCalcul = dateArrete && today <= dateArrete ? dateArrete : today;
const ancienneteMs = today - new Date(dateEntree || today);
const ancienneteMois = Math.floor(ancienneteMs / (1000 * 60 * 60 * 24 * 30.44));
// ⭐ Calculer avec la bonne date
const cpMonthsCurrent = getMoisTravaillesCP(dateCalcul, dateEntree);
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,
typeContrat: typeContrat,
societeId: userInfo.SocieteId,
societeNom: userInfo.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(dateCalcul),
anneeRTT: currentYear,
// ⭐ NOUVEAU : Indiquer si on est en période d'arrêté
arreteInfo: dernierArrete ? {
dateArrete: formatDateWithoutUTC(dateArrete),
libelle: dernierArrete.Libelle,
bloquage: today <= dateArrete
} : null,
cpN1: null,
cpN: null,
rttN: null,
rttN1: null,
totalDisponible: { cp: 0, rtt: 0, total: 0 }
};
const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']);
// ===== 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].SoldeReporte || 0) > 0) {
const soldeReporte = parseFloat(cpN1[0].SoldeReporte || 0);
const soldeActuel = parseFloat(cpN1[0].Solde || 0);
const pris = Math.max(0, soldeReporte - soldeActuel);
counters.cpN1 = {
annee: previousYear,
exercice: `${previousYear - 1}-${previousYear}`,
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;
} else {
counters.cpN1 = {
annee: previousYear,
exercice: `${previousYear - 1}-${previousYear}`,
reporte: 0,
pris: 0,
solde: 0,
pourcentageUtilise: 0
};
}
// ===== CP N (Exercice en cours) =====
const [cpN] = await conn.query(`
SELECT Annee, Total, Solde, SoldeReporte
FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
`, [userId, cpType[0].Id, currentYear]);
// ⭐ CORRECTION : Gérer le retour de calculerAcquisitionDepuisArrete
let acquisCumuleeCP;
if (typeof calculerAcquisitionDepuisArrete === 'function' && dernierArrete) {
const result = await calculerAcquisitionDepuisArrete(conn, userId, 'CP', dateCalcul);
acquisCumuleeCP = typeof result === 'number' ? result : parseFloat(result) || 0;
} else {
acquisCumuleeCP = calculerAcquisitionCP(dateCalcul, dateEntree) || 0;
}
// ⭐ SÉCURITÉ : S'assurer que c'est un nombre
acquisCumuleeCP = parseFloat(acquisCumuleeCP) || 0;
if (cpN.length > 0) {
const total = parseFloat(cpN[0].Total || 0);
const solde = parseFloat(cpN[0].Solde || 0);
const soldeReporte = parseFloat(cpN[0].SoldeReporte || 0);
const soldeN = solde - soldeReporte;
const pris = Math.max(0, total - soldeN);
counters.cpN = {
annee: currentYear,
exercice: getExerciceCP(dateCalcul),
totalAnnuel: 25.00,
moisTravailles: parseFloat(cpMonthsCurrent.toFixed(2)),
acquisitionMensuelle: parseFloat((25 / 12).toFixed(2)),
// ⭐ MODIFICATION : Afficher l'acquisition à la date de calcul
acquis: parseFloat(acquisCumuleeCP.toFixed(2)),
pris: parseFloat(pris.toFixed(2)),
solde: parseFloat(soldeN.toFixed(2)),
tauxAcquisition: parseFloat(((cpMonthsCurrent / 12) * 100).toFixed(1)),
pourcentageUtilise: total > 0 ? parseFloat(((pris / total) * 100).toFixed(1)) : 0,
joursRestantsAAcquerir: parseFloat((25 - acquisCumuleeCP).toFixed(2))
};
counters.totalDisponible.cp += counters.cpN.solde;
} else {
counters.cpN = {
annee: currentYear,
exercice: getExerciceCP(dateCalcul),
totalAnnuel: 25.00,
moisTravailles: parseFloat(cpMonthsCurrent.toFixed(2)),
acquisitionMensuelle: parseFloat((25 / 12).toFixed(2)),
acquis: parseFloat(acquisCumuleeCP.toFixed(2)),
pris: 0,
solde: parseFloat(acquisCumuleeCP.toFixed(2)),
tauxAcquisition: parseFloat(((cpMonthsCurrent / 12) * 100).toFixed(1)),
pourcentageUtilise: 0,
joursRestantsAAcquerir: parseFloat((25 - acquisCumuleeCP).toFixed(2))
};
counters.totalDisponible.cp += counters.cpN.solde;
}
}
// ===== RTT N (même logique) =====
const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']);
const isApprenti = userInfo[0].role === 'Apprenti';
if (rttType.length > 0 && !isApprenti) { // ⭐ Condition modifiée
const rttConfig = await getConfigurationRTT(conn, currentYear, typeContrat)
// ⭐ CORRECTION : Gérer le retour de calculerAcquisitionDepuisArrete pour RTT
let rttData;
if (typeof calculerAcquisitionDepuisArrete === 'function' && dernierArrete) {
const result = await calculerAcquisitionDepuisArrete(conn, userId, 'RTT', dateCalcul);
const acquisRTT = typeof result === 'number' ? result : parseFloat(result) || 0;
rttData = {
acquisition: acquisRTT,
typeContrat: typeContrat,
moisTravailles: getMoisTravaillesRTT(dateCalcul, dateEntree),
config: rttConfig
};
} else {
rttData = await calculerAcquisitionRTT(conn, userId, dateCalcul);
}
// ⭐ SÉCURITÉ : S'assurer que acquisition est un nombre
rttData.acquisition = parseFloat(rttData.acquisition) || 0;
const [rttN] = await conn.query(`
SELECT Annee, Total, Solde, SoldeReporte
FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
`, [userId, rttType[0].Id, currentYear]);
if (rttN.length > 0) {
const total = parseFloat(rttN[0].Total || 0);
const solde = parseFloat(rttN[0].Solde || 0);
const pris = Math.max(0, total - solde);
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(pris.toFixed(2)),
solde: parseFloat(solde.toFixed(2)),
tauxAcquisition: parseFloat(((rttData.moisTravailles / 12) * 100).toFixed(1)),
pourcentageUtilise: total > 0 ? parseFloat(((pris / total) * 100).toFixed(1)) : 0,
joursRestantsAAcquerir: parseFloat((rttConfig.joursAnnuels - rttData.acquisition).toFixed(2))
};
counters.totalDisponible.rtt += counters.rttN.solde;
} else {
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: 0,
solde: parseFloat(rttData.acquisition.toFixed(2)),
tauxAcquisition: parseFloat(((rttData.moisTravailles / 12) * 100).toFixed(1)),
pourcentageUtilise: 0,
joursRestantsAAcquerir: parseFloat((rttConfig.joursAnnuels - rttData.acquisition).toFixed(2))
};
counters.totalDisponible.rtt += counters.rttN.solde;
}
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"
};
}
counters.totalDisponible.total = counters.totalDisponible.cp + counters.totalDisponible.rtt;
// ===== RÉCUP (NOUVEAU) =====
const [recupType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Récupération']);
if (recupType.length > 0) {
const [recupN] = await conn.query(`
SELECT Annee, Total, Solde
FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
`, [userId, recupType[0].Id, currentYear]);
if (recupN.length > 0) {
const total = parseFloat(recupN[0].Total || 0);
const solde = parseFloat(recupN[0].Solde || 0);
counters.recupN = {
annee: currentYear,
acquis: parseFloat(total.toFixed(2)),
pris: parseFloat((total - solde).toFixed(2)),
solde: parseFloat(solde.toFixed(2)),
message: "Jours de récupération accumulés (samedis travaillés)"
};
counters.totalDisponible.recup = counters.recupN.solde;
counters.totalDisponible.total += counters.recupN.solde;
} else {
counters.recupN = {
annee: currentYear,
acquis: 0,
pris: 0,
solde: 0,
message: "Jours de récupération accumulés (samedis travaillés)"
};
counters.totalDisponible.recup = 0;
}
}
conn.release();
res.json({
success: true,
message: 'Compteurs détaillés récupérés avec succès',
data: counters
});
} catch (error) {
console.error('Erreur getDetailedLeaveCounters:', error);
res.status(500).json({
success: false,
message: 'Erreur serveur',
error: error.message
});
}
});
app.post('/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('/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('/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}`);
// Récupérer tous les détails de déduction pour cette demande
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`);
// 🔹 CAS SPÉCIAL : Récupération accumulée (RETIRER les jours)
if (TypeDeduction === 'Accum Récup') {
console.log(`❌ Annulation accumulation ${TypeNom}: -${JoursUtilises}j`);
const [compteur] = await conn.query(
`SELECT Id, Total, Solde FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
[collaborateurId, TypeCongeId, Annee]
);
if (compteur.length > 0) {
await conn.query(
`UPDATE CompteurConges
SET Total = GREATEST(0, Total - ?),
Solde = GREATEST(0, Solde - ?),
DerniereMiseAJour = NOW()
WHERE Id = ?`,
[JoursUtilises, JoursUtilises, compteur[0].Id]
);
restorations.push({
type: TypeNom,
annee: Annee,
typeDeduction: TypeDeduction,
joursRetires: JoursUtilises
});
console.log(`✅ Récup retirée: ${compteur[0].Solde}${Math.max(0, compteur[0].Solde - JoursUtilises)}`);
}
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}`);
}
}
}
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('/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('/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('/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('/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('/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('/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('/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('/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('/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 = `http://localhost:3000/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('/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('/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('/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('/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('/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 });
}
});
app.post('/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) {
// ✅ Nettoyer les fichiers en cas d'erreur
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'
});
}
// ⭐ NOUVEAU : 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));
console.log('Fichiers médicaux:', uploadedFiles.length);
// ⭐ Calculer la somme de la répartition
const sommeRepartition = Repartition.reduce((sum, r) => {
// Ne compter que CP et RTT (pas ABS ni Formation ni Récup)
if (r.TypeConge === 'CP' || r.TypeConge === 'RTT') {
return sum + parseFloat(r.NombreJours || 0);
}
return sum;
}, 0);
console.log('Somme répartition CP+RTT:', sommeRepartition.toFixed(2));
// ⭐ VALIDATION : La somme doit correspondre au total (tolérance 0.01j)
const hasCountableLeave = Repartition.some(r => r.TypeConge === 'CP' || r.TypeConge === 'RTT');
if (hasCountableLeave && Math.abs(sommeRepartition - NombreJours) > 0.01) {
console.error('❌ ERREUR : Répartition incohérente !');
console.error(` Attendu: ${NombreJours}j`);
console.error(` Reçu: ${sommeRepartition}j`);
// ✅ Nettoyer les fichiers
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)`,
details: {
repartition: Repartition,
somme: sommeRepartition,
attendu: NombreJours
}
});
}
// ⭐ Vérifier qu'aucun type n'a 0 jour
for (const rep of Repartition) {
if ((rep.TypeConge === 'CP' || rep.TypeConge === 'RTT') && parseFloat(rep.NombreJours || 0) <= 0) {
console.error(`❌ ERREUR : ${rep.TypeConge} a ${rep.NombreJours} jours !`);
// ✅ Nettoyer les fichiers
uploadedFiles.forEach(file => {
if (fs.existsSync(file.path)) {
fs.unlinkSync(file.path);
}
});
await conn.rollback();
conn.release();
return res.json({
success: false,
message: `Le type ${rep.TypeConge} doit avoir au moins 0.5 jour`
});
}
}
console.log('✅ Validation répartition OK');
// ⭐ Détection si c'est uniquement une formation
const isFormationOnly = Repartition.length === 1 && Repartition[0].TypeConge === 'Formation';
const statutDemande = statut || (isFormationOnly ? 'Validée' : 'En attente');
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}`;
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;
const dateEntree = isAD && collabAD[0].DateEntree ? collabAD[0].DateEntree : null;
let employeeId = null;
if (!isAD) {
const [user] = await conn.query('SELECT ID FROM Users WHERE Email = ? LIMIT 1', [Email]);
if (user.length === 0) {
// ✅ Nettoyer les fichiers
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
// ========================================
if (isAD && collaborateurId && !isFormationOnly) {
console.log('\n🔍 Vérification des soldes (avec anticipation)...');
const [userRole] = await conn.query('SELECT role FROM CollaborateurAD WHERE id = ?', [collaborateurId]);
const isApprenti = userRole.length > 0 && userRole[0].role === 'Apprenti';
for (const rep of Repartition) {
if (rep.TypeConge === 'ABS' || rep.TypeConge === 'Formation' || rep.TypeConge === 'Récup') {
continue;
}
if (rep.TypeConge === 'RTT' && isApprenti) {
uploadedFiles.forEach(file => {
if (fs.existsSync(file.path)) fs.unlinkSync(file.path);
});
await conn.rollback();
conn.release();
return res.json({
success: false,
message: `❌ Les apprentis ne peuvent pas poser de RTT`
});
}
const joursNecessaires = parseFloat(rep.NombreJours);
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) continue;
const typeCongeId = typeRow[0].Id;
// Récupérer le solde actuel
const [compteur] = await conn.query(`
SELECT Total, Solde, SoldeReporte
FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
`, [collaborateurId, typeCongeId, currentYear]);
let soldeDisponible = 0;
if (compteur.length > 0) {
soldeDisponible = parseFloat(compteur[0].Solde || 0);
}
// Si le solde est insuffisant, calculer l'anticipation possible
if (soldeDisponible < joursNecessaires) {
const manque = joursNecessaires - soldeDisponible;
// Calculer l'acquisition future
let acquisitionFuture = 0;
if (rep.TypeConge === 'CP') {
const finExercice = new Date(currentYear + 1, 4, 31);
const acquisTotale = calculerAcquisitionCP(finExercice, dateEntree);
const acquisActuelle = compteur.length > 0 ? parseFloat(compteur[0].Total || 0) : 0;
acquisitionFuture = acquisTotale - acquisActuelle;
} else {
const finAnnee = new Date(currentYear, 11, 31);
const rttDataTotal = await calculerAcquisitionRTT(conn, collaborateurId, finAnnee);
const acquisActuelle = compteur.length > 0 ? parseFloat(compteur[0].Total || 0) : 0;
acquisitionFuture = rttDataTotal.acquisition - acquisActuelle;
}
// Vérifier si l'anticipation est possible
if (manque > acquisitionFuture) {
uploadedFiles.forEach(file => {
if (fs.existsSync(file.path)) fs.unlinkSync(file.path);
});
await conn.rollback();
conn.release();
return res.json({
success: false,
message: `❌ Solde insuffisant pour ${name}`,
details: {
type: name,
demande: joursNecessaires,
soldeActuel: soldeDisponible.toFixed(2),
acquisitionFutureMax: acquisitionFuture.toFixed(2),
manque: (manque - acquisitionFuture).toFixed(2)
}
});
}
console.log(`⚠️ ${name}: Utilisation de ${manque.toFixed(2)}j en anticipé`);
}
}
console.log('✅ Soldes suffisants (avec anticipation si nécessaire)');
}
// ========================================
// ÉTAPE 2 : CRÉER LA DEMANDE EN PREMIER
// ========================================
console.log('\n📝 Création de la demande...');
const typeIds = [];
for (const rep of Repartition) {
const code = rep.TypeConge;
// Ne pas inclure ABS, Formation, Récup dans les typeIds principaux
if (code === 'ABS' || code === 'Formation' || code === 'Récup') {
continue;
}
const name = code === 'CP' ? 'Congé payé' : 'RTT';
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, 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' :
firstType === 'Récup' ? 'Récupération' : '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} (${(file.size / 1024).toFixed(2)} KB)`);
}
}
// ========================================
// É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' // ✅ NOUVELLE COLONNE
]
);
console.log(`${name}: ${rep.NombreJours}j (${rep.PeriodeJournee || 'Journée entière'})`);
}
}
// ========================================
// ÉTAPE 5 : ACCUMULATION DES RÉCUP (maintenant demandeId existe)
// ========================================
if (isAD && collaborateurId && !isFormationOnly) {
const hasRecup = Repartition.some(r => r.TypeConge === 'Récup');
if (hasRecup) {
console.log('\n📥 Accumulation des jours de récupération...');
const [recupType] = await conn.query(
'SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1',
['Récupération']
);
if (recupType.length > 0) {
const recupJours = Repartition.find(r => r.TypeConge === 'Récup')?.NombreJours || 0;
if (recupJours > 0) {
const [compteurExisting] = await conn.query(`
SELECT Id, Total, Solde FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
`, [collaborateurId, recupType[0].Id, currentYear]);
if (compteurExisting.length > 0) {
// ⭐ AJOUTER les jours au compteur existant
await conn.query(`
UPDATE CompteurConges
SET Total = Total + ?,
Solde = Solde + ?,
DerniereMiseAJour = NOW()
WHERE Id = ?
`, [recupJours, recupJours, compteurExisting[0].Id]);
console.log(` ✓ Récupération: +${recupJours}j ajoutés (nouveau solde: ${(parseFloat(compteurExisting[0].Solde) + recupJours).toFixed(2)}j)`);
} else {
// ⭐ CRÉER le compteur avec les jours
await conn.query(`
INSERT INTO CompteurConges
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour)
VALUES (?, ?, ?, ?, ?, 0, NOW())
`, [collaborateurId, recupType[0].Id, currentYear, recupJours, recupJours]);
console.log(` ✓ Récupération: ${recupJours}j créés (nouveau compteur)`);
}
// ⭐ Enregistrer l'ACCUMULATION (maintenant demandeId existe !)
await conn.query(`
INSERT INTO DeductionDetails
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
VALUES (?, ?, ?, 'Accum Récup', ?)
`, [demandeId, recupType[0].Id, currentYear, recupJours]);
console.log(` ✓ Accumulation enregistrée dans DeductionDetails`);
}
}
}
}
// ========================================
// ÉTAPE 6 : Déduction des compteurs CP/RTT
// ========================================
if (isAD && collaborateurId && !isFormationOnly) {
console.log('\n📉 Déduction des compteurs...');
for (const rep of Repartition) {
if (rep.TypeConge === 'ABS' || rep.TypeConge === 'Formation' || rep.TypeConge === 'Récup') {
console.log(`${rep.TypeConge} ignoré (pas de déduction)`);
continue;
}
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 deductLeaveBalanceWithTracking(
conn,
collaborateurId,
typeRow[0].Id,
rep.NombreJours,
demandeId
);
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`);
});
}
}
}
console.log('✅ Déductions terminées');
}
// ========================================
// ÉTAPE 7 : Créer notification pour formation
// ========================================
// ÉTAPE 7 : Créer notification pour formation
if (isFormationOnly && isAD && collaborateurId) {
await conn.query(
`INSERT INTO Notifications (CollaborateurADId, Type, Titre, Message, DemandeCongeId, DateCreation, Lu)
VALUES (?, ?, ?, ?, ?, NOW(), 0)`,
[
collaborateurId,
'Success', // ✅ Valeur correcte de l'enum
'✅ Formation validée automatiquement',
`Votre période de formation ${datesPeriode} a été validée automatiquement.`,
demandeId
]
);
console.log('\n📬 Notification formation créée');
}
// ========================================
// ÉTAPE 8 : 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);
} else {
const [rows] = await conn.query(
`SELECT u.Email FROM HierarchieValidation hv
JOIN Users u ON hv.SuperieurId = u.ID
WHERE hv.EmployeId = ?`,
[employeeId]
);
managers = rows.map(r => r.Email);
}
// ========================================
// COMMIT DE LA TRANSACTION
// ========================================
await conn.commit();
console.log('\n🎉 Transaction validée\n');
// ========================================
// ÉTAPE 9 : Notifier les clients SSE
// ========================================
if (isFormationOnly && isAD && collaborateurId) {
notifyCollabClients({
type: 'demande-validated',
demandeId: parseInt(demandeId),
statut: 'Validée',
timestamp: new Date().toISOString()
}, collaborateurId);
notifyCollabClients({
type: 'demande-list-updated',
action: 'formation-auto-validated',
demandeId: parseInt(demandeId),
timestamp: new Date().toISOString()
});
}
// ========================================
// ENVOI DES EMAILS (code inchangé)
// ========================================
const accessToken = await getGraphToken();
if (accessToken) {
const fromEmail = 'noreply@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;
return `${typeNom}: ${rep.NombreJours}j`;
}).join(' | ');
if (isFormationOnly) {
const subjectCollab = '✅ Votre saisie de période de formation a été enregistrée';
const bodyCollab = `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<div style="background-color: #8b5cf6; color: white; padding: 20px; border-radius: 8px 8px 0 0;">
<h2 style="margin: 0;">✅ Formation enregistrée</h2>
</div>
<div style="background-color: #f9fafb; padding: 20px; border: 1px solid #e5e7eb;">
<p style="font-size: 16px;">Bonjour <strong>${Nom}</strong>,</p>
<p>Votre période de formation a bien été <strong style="color: #8b5cf6;">enregistrée et validée automatiquement</strong>.</p>
<div style="background-color: white; border-left: 4px solid #8b5cf6; padding: 15px; margin: 20px 0;">
<p><strong>Type :</strong> Formation</p>
<p><strong>Période :</strong> ${datesPeriode}</p>
<p><strong>Durée :</strong> ${NombreJours} jour(s)</p>
${Commentaire ? `<p><strong>Description :</strong> ${Commentaire}</p>` : ''}
</div>
</div>
</div>
`;
try {
await sendMailGraph(accessToken, fromEmail, Email, subjectCollab, bodyCollab);
console.log('✅ Email confirmation formation envoyé');
} catch (mailError) {
console.error('❌ Erreur email:', mailError.message);
}
for (const managerEmail of managers) {
const subjectManager = `📚 Information : Formation déclarée - ${Nom}`;
const bodyManager = `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<div style="background-color: #8b5cf6; color: white; padding: 20px;">
<h2 style="margin: 0;">📚 Formation enregistrée</h2>
</div>
<div style="padding: 20px;">
<p><strong>${Nom}</strong> vous informe d'une période de formation.</p>
<p><strong>Période :</strong> ${datesPeriode}</p>
<p><strong>Durée :</strong> ${NombreJours} jour(s)</p>
</div>
</div>
`;
try {
await sendMailGraph(accessToken, fromEmail, managerEmail, subjectManager, bodyManager);
} catch (mailError) {
console.error('❌ Erreur email manager:', mailError.message);
}
}
} else {
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();
// ✅ Nettoyer les fichiers uploadés en cas d'erreur
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('/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('/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('/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 = 'noreply@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('/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;
}
// ========================================
// ROUTE getTeamLeaves COMPLÈTE
// ========================================
app.get('/getTeamLeaves', async (req, res) => {
try {
const { 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
// ========================================
if (role === 'president' || role === 'admin' || role === 'rh') {
console.log("CAS 1: President/Admin/RH - Vue globale");
const [campusList] = await conn.query(`SELECT DISTINCT Nom FROM Campus ORDER BY Nom`);
filters.campus = campusList.map(c => c.Nom);
const [societesList] = await conn.query(`SELECT DISTINCT Nom FROM Societe ORDER BY Nom`);
filters.societes = societesList.map(s => s.Nom);
const [servicesList] = await conn.query(`SELECT DISTINCT Nom FROM Services ORDER BY Nom`);
filters.services = servicesList.map(s => s.Nom);
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.actif = 1 OR ca.actif IS NULL)
ORDER BY ca.prenom, ca.nom
`);
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')
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 = [];
}
// ========================================
// CAS 2: DIRECTEUR/DIRECTRICE DE CAMPUS
// ========================================
else if (role === 'directeur de campus' || role === 'directrice de campus') {
console.log("CAS 2: Directeur de campus");
console.log(` Campus: ${campusNom} (ID: ${campusId})`);
console.log(` Filtres reçus: Société=${selectedSociete}, Service=${selectedService}`);
filters.societes = ['Ensup', 'Ensup Solution et Support'];
let servicesQuery;
let servicesParams = [];
if (selectedSociete === 'Ensup Solution et Support') {
servicesQuery = `
SELECT DISTINCT s.Nom
FROM Services s
JOIN CollaborateurAD ca ON ca.ServiceId = s.Id
WHERE ca.SocieteId = 1
AND (ca.actif = 1 OR ca.actif IS NULL)
ORDER BY s.Nom
`;
servicesParams = [];
} else if (selectedSociete === 'Ensup') {
servicesQuery = `
SELECT DISTINCT s.Nom
FROM Services s
JOIN CollaborateurAD ca ON ca.ServiceId = s.Id
WHERE ca.SocieteId = 2
AND ca.CampusId = ?
AND (ca.actif = 1 OR ca.actif IS NULL)
ORDER BY s.Nom
`;
servicesParams = [campusId];
} else {
servicesQuery = `
SELECT DISTINCT s.Nom
FROM Services s
JOIN CollaborateurAD ca ON ca.ServiceId = s.Id
WHERE (
(ca.SocieteId = 2 AND ca.CampusId = ?)
OR (ca.SocieteId = 1)
)
AND (ca.actif = 1 OR ca.actif IS NULL)
ORDER BY s.Nom
`;
servicesParams = [campusId];
}
const [servicesList] = await conn.query(servicesQuery, servicesParams);
filters.services = servicesList.map(s => s.Nom);
console.log(`📊 Services trouvés:`, filters.services.length, filters.services);
let employeesQuery;
let employeesParams = [];
if (selectedSociete === 'Ensup Solution et Support') {
if (selectedService && selectedService !== 'all') {
employeesQuery = `
SELECT DISTINCT
CONCAT(ca.prenom, ' ', ca.nom) AS fullname,
COALESCE(c.Nom, 'Multi-campus') AS campusnom,
so.Nom AS societenom,
s.Nom AS servicenom
FROM CollaborateurAD ca
JOIN Services s ON ca.ServiceId = s.Id
LEFT JOIN Campus c ON ca.CampusId = c.Id
JOIN Societe so ON ca.SocieteId = so.Id
WHERE ca.SocieteId = 1
AND s.Nom = ?
AND (ca.actif = 1 OR ca.actif IS NULL)
ORDER BY ca.prenom, ca.nom
`;
employeesParams = [selectedService];
} else {
employeesQuery = `
SELECT DISTINCT
CONCAT(ca.prenom, ' ', ca.nom) AS fullname,
COALESCE(c.Nom, 'Multi-campus') AS campusnom,
so.Nom AS societenom,
s.Nom AS servicenom
FROM CollaborateurAD ca
JOIN Services s ON ca.ServiceId = s.Id
LEFT JOIN Campus c ON ca.CampusId = c.Id
JOIN Societe so ON ca.SocieteId = so.Id
WHERE ca.SocieteId = 1
AND (ca.actif = 1 OR ca.actif IS NULL)
ORDER BY ca.prenom, ca.nom
`;
employeesParams = [];
}
} else if (selectedSociete === 'Ensup') {
if (selectedService && selectedService !== 'all') {
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.SocieteId = 2
AND ca.CampusId = ?
AND s.Nom = ?
AND (ca.actif = 1 OR ca.actif IS NULL)
ORDER BY ca.prenom, ca.nom
`;
employeesParams = [campusId, selectedService];
} else {
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.SocieteId = 2
AND ca.CampusId = ?
AND (ca.actif = 1 OR ca.actif IS NULL)
ORDER BY ca.prenom, ca.nom
`;
employeesParams = [campusId];
}
} else {
if (selectedService && selectedService !== 'all') {
employeesQuery = `
SELECT DISTINCT
CONCAT(ca.prenom, ' ', ca.nom) AS fullname,
COALESCE(c.Nom, 'Multi-campus') AS campusnom,
so.Nom AS societenom,
s.Nom AS servicenom
FROM CollaborateurAD ca
JOIN Services s ON ca.ServiceId = s.Id
LEFT JOIN Campus c ON ca.CampusId = c.Id
JOIN Societe so ON ca.SocieteId = so.Id
WHERE (
(ca.SocieteId = 2 AND ca.CampusId = ?)
OR (ca.SocieteId = 1)
)
AND s.Nom = ?
AND (ca.actif = 1 OR ca.actif IS NULL)
ORDER BY ca.prenom, ca.nom
`;
employeesParams = [campusId, selectedService];
} else {
employeesQuery = `
SELECT DISTINCT
CONCAT(ca.prenom, ' ', ca.nom) AS fullname,
COALESCE(c.Nom, 'Multi-campus') AS campusnom,
so.Nom AS societenom,
s.Nom AS servicenom
FROM CollaborateurAD ca
JOIN Services s ON ca.ServiceId = s.Id
LEFT JOIN Campus c ON ca.CampusId = c.Id
JOIN Societe so ON ca.SocieteId = so.Id
WHERE (
(ca.SocieteId = 2 AND ca.CampusId = ?)
OR (ca.SocieteId = 1)
)
AND (ca.actif = 1 OR ca.actif IS NULL)
ORDER BY ca.prenom, ca.nom
`;
employeesParams = [campusId];
}
}
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);
let whereConditions = [`dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente')`];
let whereParams = [];
if (selectedSociete === 'Ensup Solution et Support') {
whereConditions.push(`ca.SocieteId = 1`);
} else if (selectedSociete === 'Ensup') {
whereConditions.push(`ca.SocieteId = 2 AND ca.CampusId = ?`);
whereParams.push(campusId);
} else {
whereConditions.push(`((ca.SocieteId = 2 AND ca.CampusId = ?) OR (ca.SocieteId = 1))`);
whereParams.push(campusId);
}
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,
COALESCE(c.Nom, 'Multi-campus') 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
LEFT 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;
}
// ========================================
// CAS 3: COLLABORATEUR
// ========================================
else if (role === 'collaborateur') {
console.log("CAS 3: Collaborateur");
const serviceNom = userInfo.serviceNom || 'Non défini';
const accesTransversal = getUserAccesTransversal(userEmail);
const isServiceMultiCampus = [
"Administratif & Financier",
"IT"
].includes(serviceNom);
if (accesTransversal) {
console.log(`🌐 Accès transversal détecté:`, accesTransversal);
if (accesTransversal.typeAcces === 'service_multi_campus') {
filters.societes = [];
filters.campus = [];
filters.services = [];
const [employeesList] = await conn.query(`
SELECT DISTINCT
CONCAT(ca.prenom, ' ', ca.nom) AS fullname,
COALESCE(c.Nom, 'Multi-campus') AS campusnom,
so.Nom AS societenom,
s.Nom AS servicenom
FROM CollaborateurAD ca
JOIN Services s ON ca.ServiceId = s.Id
LEFT JOIN Campus c ON ca.CampusId = c.Id
JOIN Societe so ON ca.SocieteId = so.Id
WHERE s.Nom = ?
AND (ca.actif = 1 OR ca.actif IS NULL)
ORDER BY c.Nom, ca.prenom, ca.nom
`, [accesTransversal.serviceNom]);
filters.employees = employeesList.map(emp => ({
name: emp.fullname,
campus: emp.campusnom,
societe: emp.societenom,
service: emp.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,
COALESCE(c.Nom, 'Multi-campus') 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
LEFT JOIN Campus c ON ca.CampusId = c.Id
JOIN Societe so ON ca.SocieteId = so.Id
WHERE s.Nom = ?
AND dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente')
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 = [accesTransversal.serviceNom];
} else if (accesTransversal.typeAcces === 'service_multi_campus_avec_vue_complete') {
filters.societes = [];
filters.campus = [];
filters.services = [];
const [employeesList] = await conn.query(`
SELECT DISTINCT
CONCAT(ca.prenom, ' ', ca.nom) AS fullname,
COALESCE(c.Nom, 'Multi-campus') AS campusnom,
so.Nom AS societenom,
s.Nom AS servicenom
FROM CollaborateurAD ca
JOIN Services s ON ca.ServiceId = s.Id
LEFT JOIN Campus c ON ca.CampusId = c.Id
JOIN Societe so ON ca.SocieteId = so.Id
WHERE s.Nom = ?
AND (ca.actif = 1 OR ca.actif IS NULL)
ORDER BY so.Nom, c.Nom, ca.prenom, ca.nom
`, [accesTransversal.serviceNom]);
filters.employees = employeesList.map(emp => ({
name: emp.fullname,
campus: emp.campusnom,
societe: emp.societenom,
service: emp.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,
COALESCE(c.Nom, 'Multi-campus') 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
LEFT JOIN Campus c ON ca.CampusId = c.Id
JOIN Societe so ON ca.SocieteId = so.Id
WHERE s.Nom = ?
AND dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente')
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 = [accesTransversal.serviceNom];
}
} else if (isServiceMultiCampus) {
filters.societes = [];
filters.campus = [];
filters.services = [];
const [employeesList] = await conn.query(`
SELECT DISTINCT
CONCAT(ca.prenom, ' ', ca.nom) AS fullname,
COALESCE(c.Nom, 'Multi-campus') AS campusnom,
so.Nom AS societenom,
s.Nom AS servicenom
FROM CollaborateurAD ca
JOIN Services s ON ca.ServiceId = s.Id
LEFT JOIN Campus c ON ca.CampusId = c.Id
JOIN Societe so ON ca.SocieteId = so.Id
WHERE s.Nom = ?
AND (ca.actif = 1 OR ca.actif IS NULL)
ORDER BY c.Nom, ca.prenom, ca.nom
`, [serviceNom]);
filters.employees = employeesList.map(emp => ({
name: emp.fullname,
campus: emp.campusnom,
societe: emp.societenom,
service: emp.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,
COALESCE(c.Nom, 'Multi-campus') 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
LEFT JOIN Campus c ON ca.CampusId = c.Id
JOIN Societe so ON ca.SocieteId = so.Id
WHERE s.Nom = ?
AND dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente')
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 = [serviceNom];
} else {
filters.societes = [];
filters.campus = [];
filters.services = [];
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
LEFT JOIN Campus c ON ca.CampusId = c.Id
JOIN Societe so ON ca.SocieteId = so.Id
WHERE ca.ServiceId = ?
AND (ca.CampusId = ? OR ca.CampusId IS NULL)
AND (ca.actif = 1 OR ca.actif IS NULL)
ORDER BY ca.prenom, ca.nom
`, [serviceId, campusId]);
filters.employees = employeesList.map(emp => ({
name: emp.fullname,
campus: emp.campusnom,
societe: emp.societenom,
service: emp.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
LEFT JOIN Campus c ON ca.CampusId = c.Id
JOIN Societe so ON ca.SocieteId = so.Id
WHERE ca.ServiceId = ?
AND (ca.CampusId = ? OR ca.CampusId IS NULL)
AND dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente')
GROUP BY dc.Id, dc.DateDebut, dc.DateFin, ca.prenom, ca.nom, dc.Statut, s.Nom, c.Nom, so.Nom, dc.NombreJours
ORDER BY dc.DateDebut ASC
`;
params = [serviceId, campusId];
}
}
// ========================================
// 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
});
}
});
app.post('/initial-sync', async (req, res) => {
try {
const accessToken = await getGraphToken();
if (!accessToken) return res.json({ success: false, message: 'Impossible obtenir token' });
if (req.body.userPrincipalName || req.body.mail) {
const userEmail = req.body.mail || req.body.userPrincipalName;
const entraUserId = req.body.id;
console.log(`🔄 Synchronisation utilisateur: ${userEmail}`);
// ⭐ Insertion avec SocieteId (ajuster selon votre logique)
await pool.query(`
INSERT INTO CollaborateurAD
(entraUserId, prenom, nom, email, service, description, role, SocieteId)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
prenom=?, nom=?, email=?, service=?, description=?
`, [
entraUserId,
req.body.givenName,
req.body.surname,
userEmail,
req.body.department,
req.body.jobTitle,
'Collaborateur',
null, // ⭐ À ajuster selon votre logique métier
req.body.givenName,
req.body.surname,
userEmail,
req.body.department,
req.body.jobTitle
]);
const [userRows] = await pool.query(`
SELECT
ca.id as localUserId,
ca.entraUserId,
ca.prenom,
ca.nom,
ca.email,
ca.role,
s.Nom as service,
ca.TypeContrat as typeContrat,
ca.DateEntree as dateEntree,
ca.description,
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.email = ?
`, [userEmail]);
if (userRows.length === 0) {
return res.json({
success: false,
message: 'Utilisateur synchronisé mais introuvable en BDD'
});
}
const userData = userRows[0];
console.log(`✅ Utilisateur synchronisé:`, userData);
return res.json({
success: true,
message: 'Utilisateur synchronisé',
localUserId: userData.localUserId,
role: userData.role,
service: userData.service,
typeContrat: userData.typeContrat,
dateEntree: userData.dateEntree,
societeId: userData.SocieteId,
societeNom: userData.societe_nom,
user: userData
});
}
// Full sync
console.log('🔄 Full sync de tous les membres du groupe...');
const groupResponse = await axios.get(
`https://graph.microsoft.com/v1.0/groups/${AZURE_CONFIG.groupId}?$select=id,displayName`,
{ headers: { Authorization: `Bearer ${accessToken}` } }
);
const group = groupResponse.data;
const membersResponse = await axios.get(
`https://graph.microsoft.com/v1.0/groups/${AZURE_CONFIG.groupId}/members?$select=id,givenName,surname,mail,department,jobTitle`,
{ headers: { Authorization: `Bearer ${accessToken}` } }
);
const members = membersResponse.data.value;
let usersInserted = 0;
for (const m of members) {
if (!m.mail) continue;
await pool.query(`
INSERT INTO CollaborateurAD (
entraUserId, prenom, nom, email, service, description, role, SocieteId
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
prenom=?, nom=?, email=?, service=?, description=?
`, [
m.id,
m.givenName || '',
m.surname || '',
m.mail,
m.department || '',
m.jobTitle || null,
'Collaborateur',
null, // ⭐ À ajuster selon votre logique métier
m.givenName,
m.surname,
m.mail,
m.department,
m.jobTitle
]);
usersInserted++;
}
console.log(`✅ Full sync terminée: ${usersInserted} utilisateurs`);
res.json({
success: true,
message: 'Full synchronisation terminée',
groupe_sync: group.displayName,
users_sync: usersInserted
});
} catch (error) {
console.error('❌ Erreur sync:', error);
res.status(500).json({
success: false,
message: 'Erreur sync',
error: error.message
});
}
});
// ========================================
// NOUVELLES ROUTES ADMINISTRATION RTT
// ========================================
app.get('/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('/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('/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('/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('/updateRequest', async (req, res) => {
const conn = await pool.getConnection();
try {
await conn.beginTransaction();
const {
requestId,
leaveType,
startDate,
endDate,
reason,
businessDays,
userId,
userEmail,
userName,
accessToken,
repartition // ⭐ NOUVEAU - Ajout de la répartition
} = req.body;
console.log('\n📝 === MODIFICATION DEMANDE ===');
console.log('Demande ID:', requestId);
console.log('Utilisateur:', userName);
console.log('Nouvelle répartition:', repartition);
// 1. Vérifier que la demande existe et est "En attente"
const [existingRequest] = await conn.query(
'SELECT * FROM DemandeConge WHERE Id = ? AND CollaborateurADId = ?',
[requestId, userId]
);
if (existingRequest.length === 0) {
await conn.rollback();
conn.release();
return res.status(404).json({
success: false,
message: 'Demande introuvable'
});
}
const request = existingRequest[0];
if (request.Statut !== 'En attente') {
await conn.rollback();
conn.release();
return res.status(400).json({
success: false,
message: 'Vous ne pouvez modifier que les demandes en attente'
});
}
// 2. ⭐ RESTAURER LES ANCIENS COMPTEURS
console.log('\n🔄 ÉTAPE 1: Restauration des anciens compteurs...');
try {
const restoration = await restoreLeaveBalance(conn, requestId, userId);
console.log('✅ Compteurs restaurés:', restoration);
} catch (restoreError) {
console.error('❌ Erreur restauration:', restoreError);
await conn.rollback();
conn.release();
return res.status(500).json({
success: false,
message: 'Erreur lors de la restauration des compteurs',
error: restoreError.message
});
}
// 3. ⭐ SUPPRIMER LES ANCIENNES DÉDUCTIONS
console.log('\n🗑 ÉTAPE 2: Suppression des anciennes déductions...');
await conn.query('DELETE FROM DeductionDetails WHERE DemandeCongeId = ?', [requestId]);
await conn.query('DELETE FROM DemandeCongeType WHERE DemandeCongeId = ?', [requestId]);
// 4. METTRE À JOUR LA DEMANDE
console.log('\n📝 ÉTAPE 3: Mise à jour de la demande...');
await conn.query(
`UPDATE DemandeConge
SET DateDebut = ?,
DateFin = ?,
Commentaire = ?,
NombreJours = ?
WHERE Id = ?`,
[startDate, endDate, reason || null, businessDays, requestId]
);
// 5. ⭐ SAUVEGARDER LA NOUVELLE RÉPARTITION
console.log('\n📊 ÉTAPE 4: Sauvegarde de la nouvelle répartition...');
if (repartition && repartition.length > 0) {
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 (?, ?, ?, ?)`,
[
requestId,
typeRow[0].Id,
rep.NombreJours,
rep.PeriodeJournee || 'Journée entière'
]
);
console.log(`${name}: ${rep.NombreJours}j`);
}
}
}
// 6. ⭐ DÉDUIRE LES NOUVEAUX COMPTEURS
console.log('\n📉 ÉTAPE 5: Déduction des nouveaux compteurs...');
const currentYear = new Date().getFullYear();
for (const rep of repartition) {
if (rep.TypeConge === 'ABS' || rep.TypeConge === 'Formation') {
console.log(`${rep.TypeConge} ignoré (pas de déduction)`);
continue;
}
// Récup: ACCUMULATION
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) {
const recupJours = rep.NombreJours;
const [compteurExisting] = await conn.query(`
SELECT Id, Total, Solde FROM CompteurConges
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
`, [userId, recupType[0].Id, currentYear]);
if (compteurExisting.length > 0) {
await conn.query(`
UPDATE CompteurConges
SET Total = Total + ?,
Solde = Solde + ?,
DerniereMiseAJour = NOW()
WHERE Id = ?
`, [recupJours, recupJours, compteurExisting[0].Id]);
} else {
await conn.query(`
INSERT INTO CompteurConges
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour)
VALUES (?, ?, ?, ?, ?, 0, NOW())
`, [userId, recupType[0].Id, currentYear, recupJours, recupJours]);
}
await conn.query(`
INSERT INTO DeductionDetails
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
VALUES (?, ?, ?, 'Accum Récup', ?)
`, [requestId, recupType[0].Id, currentYear, recupJours]);
console.log(` ✓ Récup: +${recupJours}j accumulés`);
}
continue;
}
// CP et RTT: DÉDUCTION
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 deductLeaveBalanceWithTracking(
conn,
userId,
typeRow[0].Id,
rep.NombreJours,
requestId
);
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`);
});
}
}
}
// 7. Récupérer les infos pour l'email
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';
await conn.commit();
console.log('\n✅ Modification terminée avec succès\n');
// 8. Envoyer les emails (après commit)
if (managerEmail && accessToken) {
try {
const newStartDate = new Date(startDate).toLocaleDateString('fr-FR');
const newEndDate = new Date(endDate).toLocaleDateString('fr-FR');
const typesConges = repartition.map(r => `${r.TypeConge}: ${r.NombreJours}j`).join(' | ');
const emailBody = {
message: {
subject: `🔄 Modification de demande de congé - ${userName}`,
body: {
contentType: "HTML",
content: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 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 ${managerName},</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 #667eea;">
<h3 style="color: #667eea; margin-top: 0;">✨ Nouvelles informations</h3>
<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;">${typesConges}</td>
</tr>
<tr>
<td style="padding: 8px 0; color: #666;"><strong>Du :</strong></td>
<td style="padding: 8px 0; color: #333;">${newStartDate}</td>
</tr>
<tr>
<td style="padding: 8px 0; color: #666;"><strong>Au :</strong></td>
<td style="padding: 8px 0; color: #333;">${newEndDate}</td>
</tr>
<tr>
<td style="padding: 8px 0; color: #666;"><strong>Jours :</strong></td>
<td style="padding: 8px 0; color: #333;">${businessDays} jour(s)</td>
</tr>
${reason ? `<tr>
<td style="padding: 8px 0; color: #666; vertical-align: top;"><strong>Motif :</strong></td>
<td style="padding: 8px 0; color: #333; font-style: italic;">${reason}</td>
</tr>` : ''}
</table>
</div>
<p style="font-size: 14px; color: #666; margin-top: 30px;">
Cette demande est toujours en attente de validation.
</p>
</div>
</div>
`
},
toRecipients: [
{
emailAddress: {
address: managerEmail
}
}
]
},
saveToSentItems: "false"
};
await axios.post(
'https://graph.microsoft.com/v1.0/me/sendMail',
emailBody,
{
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
}
}
);
console.log('✅ Email de modification envoyé au manager');
} catch (emailError) {
console.error('❌ Erreur envoi email manager:', emailError);
}
}
conn.release();
res.json({
success: true,
message: 'Demande modifiée avec succès'
});
} catch (error) {
await conn.rollback();
if (conn) conn.release();
console.error('❌ Erreur updateRequest:', error);
res.status(500).json({
success: false,
message: 'Erreur serveur lors de la modification',
error: error.message
});
}
});
/**
* Route pour SUPPRIMER une demande de congé
* POST /deleteRequest
*/
app.post('/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🗑 === SUPPRESSION DEMANDE ===');
console.log('Demande ID:', requestId);
console.log('User ID:', userId);
// 1. Vérifier que la demande existe et appartient à l'utilisateur
const [existingRequest] = await conn.query(
`SELECT d.*, tc.Nom as TypeConge
FROM DemandeConge d
LEFT JOIN TypeConge tc ON d.TypeCongeId = tc.Id
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;
console.log(`📋 Demande trouvée: ID=${requestId}, Statut=${requestStatus}`);
// 2. ⭐ RESTAURER LES COMPTEURS (si nécessaire)
if (['En attente', 'Valide', 'Validé', 'Valid'].includes(requestStatus)) {
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);
// Ne pas bloquer la suppression si la restauration échoue
}
}
// 3. Récupérer le manager pour l'email
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';
// 4. ⭐ SUPPRIMER LES DÉPENDANCES
await conn.query('DELETE FROM HistoriqueActions WHERE DemandeCongeId = ?', [requestId]);
await conn.query('DELETE FROM DeductionDetails WHERE DemandeCongeId = ?', [requestId]);
await conn.query('DELETE FROM DemandeCongeType WHERE DemandeCongeId = ?', [requestId]);
await conn.query('DELETE FROM DocumentsMedicaux WHERE DemandeCongeId = ?', [requestId]);
await conn.query('DELETE FROM Notifications WHERE DemandeCongeId = ?', [requestId]);
// 5. Supprimer la demande
await conn.query('DELETE FROM DemandeConge WHERE Id = ?', [requestId]);
console.log(`✅ Demande ${requestId} supprimée définitivement`);
await conn.commit();
// 6. ⭐ ENVOYER EMAIL AU MANAGER
if (managerEmail && accessToken) {
try {
const emailBody = {
message: {
subject: `🗑️ Suppression de demande - ${userName}`,
body: {
contentType: "HTML",
content: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<div style="background: linear-gradient(135deg, #ef4444 0%, #991b1b 100%); padding: 30px; text-align: center; border-radius: 10px 10px 0 0;">
<h1 style="color: white; margin: 0;">🗑️ Suppression 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 ${managerName},</p>
<p style="font-size: 16px; color: #333;">
<strong>${userName}</strong> a supprimé sa demande de congé.
</p>
<div style="background-color: white; padding: 20px; border-radius: 8px; margin: 20px 0; border-left: 4px solid #ef4444;">
<h3 style="color: #ef4444; margin-top: 0;">📋 Demande supprimée</h3>
<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;">${request.TypeConge || 'N/A'}</td>
</tr>
<tr>
<td style="padding: 8px 0; color: #666;"><strong>Période :</strong></td>
<td style="padding: 8px 0; color: #333;">${new Date(request.DateDebut).toLocaleDateString('fr-FR')} au ${new Date(request.DateFin).toLocaleDateString('fr-FR')}</td>
</tr>
<tr>
<td style="padding: 8px 0; color: #666;"><strong>Jours :</strong></td>
<td style="padding: 8px 0; color: #333;">${request.NombreJours} jour(s)</td>
</tr>
<tr>
<td style="padding: 8px 0; color: #666;"><strong>Statut initial :</strong></td>
<td style="padding: 8px 0; color: #333;">${requestStatus}</td>
</tr>
</table>
</div>
<p style="font-size: 14px; color: #666; margin-top: 30px;">
Les compteurs de congés ont été restaurés si nécessaire.
</p>
</div>
</div>
`
},
toRecipients: [{ emailAddress: { address: managerEmail } }],
saveToSentItems: false
}
};
await axios.post('https://graph.microsoft.com/v1.0/me/sendMail', emailBody, {
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
}
});
console.log('📧 Email de notification envoyé au manager');
} catch (emailError) {
console.error('❌ Erreur email manager (non bloquant):', emailError.message);
}
}
conn.release();
res.json({
success: true,
message: 'Demande supprimée avec succès',
counterRestored: ['En attente', 'Valid', 'Validé', 'Valide'].includes(requestStatus)
});
} catch (error) {
await conn.rollback();
if (conn) conn.release();
console.error('❌ Erreur deleteRequest:', error);
res.status(500).json({
success: false,
message: 'Erreur lors de la suppression',
error: error.message
});
}
});
app.get('/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
});
}
});
/**
* GET /getCongesAnticipes
* Calcule les congés anticipés disponibles pour un collaborateur
*/
app.get('/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
});
}
});
app.get('/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('/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('/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('/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('/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('/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('/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('/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('/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 });
}
});
// ========================================
// DÉMARRAGE DU SERVEUR
// ========================================
app.listen(PORT, () => {
console.log(`✅ Serveur démarré sur http://localhost:${PORT}`);
console.log('📋 Routes disponibles:');
console.log(' POST /login');
console.log(' POST /check-user-groups');
console.log(' GET /getDetailedLeaveCounters');
console.log(' POST /updateCounters');
console.log(' POST /updateAllCounters');
console.log(' POST /reinitializeAllCounters');
console.log(' GET /testProrata');
console.log(' POST /fixAllCounters');
console.log(' POST /submitLeaveRequest');
console.log(' POST /validateRequest');
console.log(' GET /getRequests');
console.log(' GET /getNotifications');
console.log(' POST /markNotificationRead');
console.log(' GET /getPendingRequests');
console.log(' GET /getTeamMembers');
console.log(' GET /getTeamLeaves');
console.log(' GET /getAllTeamRequests');
console.log(' GET /getEmployeRequest');
console.log(' POST /initial-sync');
console.log('');
console.log(' 🆕 ROUTES ADMIN RTT:');
console.log(' GET /getAllCollaborateurs');
console.log(' POST /updateTypeContrat');
console.log(' GET /getConfigurationRTT');
console.log(' POST /updateConfigurationRTT');
console.log(' GET /exportCompteurs');
console.log(' GET /getStatistiquesCompteurs');
console.log('');
console.log('⏰ Tâches CRON actives:');
console.log(' 📅 Mise à jour mensuelle: 1er de chaque mois à 00h01');
console.log(' 📸 Arrêtés mensuels: Dernier jour de chaque mois à 23h55'); // ⭐ NOUVEAU
console.log(' 🎆 Fin d\'année RTT: 31 décembre à 23h59');
console.log(' 📅 Fin d\'exercice CP: 31 mai à 23h59');
});