8628 lines
342 KiB
JavaScript
8628 lines
342 KiB
JavaScript
import express from 'express';
|
||
import mysql from 'mysql2/promise';
|
||
import cors from 'cors';
|
||
import axios from 'axios';
|
||
import multer from 'multer';
|
||
import path from 'path';
|
||
import { fileURLToPath } from 'url';
|
||
import cron from 'node-cron';
|
||
import crypto from 'crypto';
|
||
|
||
import { createRequire } from 'module';
|
||
const require = createRequire(import.meta.url);
|
||
|
||
import WebhookManager from './webhook-utils.js';
|
||
import { WEBHOOKS, EVENTS } from './webhook-config.js';
|
||
const __filename = fileURLToPath(import.meta.url);
|
||
const __dirname = path.dirname(__filename);
|
||
|
||
const app = express();
|
||
const PORT = 3000;
|
||
|
||
const webhookManager = new WebhookManager(WEBHOOKS.SECRET_KEY);
|
||
const sseClientsCollab = new Set();
|
||
process.on('uncaughtException', (err) => {
|
||
console.error('💥 ERREUR CRITIQUE NON CATCHÉE:', err);
|
||
console.error('Stack:', err.stack);
|
||
// On ne crash pas pour pouvoir déboguer
|
||
});
|
||
|
||
process.on('unhandledRejection', (reason, promise) => {
|
||
console.error('💥 PROMESSE REJETÉE NON GÉRÉE:', reason);
|
||
console.error('Promise:', promise);
|
||
});
|
||
|
||
app.use(cors({
|
||
origin: ['http://localhost:3013', 'http://localhost:80', 'https://mygta.ensup-adm.net'],
|
||
credentials: true
|
||
}));
|
||
|
||
|
||
app.use(express.json());
|
||
app.use(express.urlencoded({ extended: true }));
|
||
|
||
|
||
const dbConfig = {
|
||
host: '192.168.0.4',
|
||
user: 'wpuser',
|
||
password: '-2b/)ru5/Bi8P[7_',
|
||
database: 'DemandeConge',
|
||
port:'3306',
|
||
charset: 'utf8mb4'
|
||
};
|
||
|
||
|
||
function nowFR() {
|
||
const d = new Date();
|
||
d.setHours(d.getHours() + 2);
|
||
return d.toISOString().slice(0, 19).replace('T', ' ');
|
||
}
|
||
|
||
// ========================================
|
||
// HELPER POUR DATES SANS CONVERSION UTC
|
||
// ========================================
|
||
function formatDateWithoutUTC(date) {
|
||
if (!date) return null;
|
||
const d = new Date(date);
|
||
const year = d.getFullYear();
|
||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||
const day = String(d.getDate()).padStart(2, '0');
|
||
return `${year}-${month}-${day}`;
|
||
};
|
||
|
||
function formatDateToFrenchTime(date) {
|
||
if (!date) return null;
|
||
|
||
// Créer un objet Date et le convertir en heure française (Europe/Paris)
|
||
const d = new Date(date);
|
||
|
||
// Formater en ISO avec le fuseau horaire français
|
||
return d.toLocaleString('fr-FR', {
|
||
timeZone: 'Europe/Paris',
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
second: '2-digit',
|
||
hour12: false
|
||
});
|
||
}
|
||
|
||
// Ou plus simple, pour avoir un format ISO compatible avec le frontend
|
||
function formatDateToFrenchISO(date) {
|
||
if (!date) return null;
|
||
|
||
const d = new Date(date);
|
||
|
||
// Convertir en heure française
|
||
const frenchDate = new Date(d.toLocaleString('en-US', { timeZone: 'Europe/Paris' }));
|
||
|
||
return frenchDate.toISOString();
|
||
}
|
||
|
||
// ========================================
|
||
// FONCTIONS POUR GÉRER LES ARRÊTÉS COMPTABLES
|
||
// À ajouter après : const pool = mysql.createPool(dbConfig);
|
||
// ========================================
|
||
|
||
/**
|
||
* Récupère le dernier arrêté validé/clôturé
|
||
*/
|
||
async function getDernierArrete(conn) {
|
||
const [arretes] = await conn.query(`
|
||
SELECT * FROM ArreteComptable
|
||
WHERE Statut IN ('Validé', 'Clôturé')
|
||
ORDER BY DateArrete DESC
|
||
LIMIT 1
|
||
`);
|
||
|
||
return arretes.length > 0 ? arretes[0] : null;
|
||
}
|
||
|
||
/**
|
||
* Vérifie si une date est avant le dernier arrêté
|
||
*/
|
||
async function estAvantArrete(conn, date) {
|
||
const dernierArrete = await getDernierArrete(conn);
|
||
|
||
if (!dernierArrete) {
|
||
return false; // Pas d'arrêté = toutes les dates sont autorisées
|
||
}
|
||
|
||
const dateTest = new Date(date);
|
||
const dateArrete = new Date(dernierArrete.DateArrete);
|
||
|
||
return dateTest <= dateArrete;
|
||
}
|
||
|
||
/**
|
||
* Récupère le solde figé d'un collaborateur pour un type de congé
|
||
*/
|
||
async function getSoldeFige(conn, collaborateurId, typeCongeId, annee) {
|
||
const dernierArrete = await getDernierArrete(conn);
|
||
|
||
if (!dernierArrete) {
|
||
return null; // Pas d'arrêté
|
||
}
|
||
|
||
const [soldes] = await conn.query(`
|
||
SELECT * FROM SoldesFiges
|
||
WHERE ArreteId = ?
|
||
AND CollaborateurADId = ?
|
||
AND TypeCongeId = ?
|
||
AND Annee = ?
|
||
LIMIT 1
|
||
`, [dernierArrete.Id, collaborateurId, typeCongeId, annee]);
|
||
|
||
return soldes.length > 0 ? soldes[0] : null;
|
||
}
|
||
|
||
/**
|
||
* Calcule l'acquisition depuis le dernier arrêté
|
||
*/
|
||
async function calculerAcquisitionDepuisArrete(conn, collaborateurId, typeConge, dateReference = new Date()) {
|
||
const dernierArrete = await getDernierArrete(conn);
|
||
const anneeRef = dateReference.getFullYear();
|
||
|
||
// Déterminer le type de congé
|
||
const [typeRow] = await conn.query(
|
||
'SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1',
|
||
[typeConge === 'CP' ? 'Congé payé' : 'RTT']
|
||
);
|
||
|
||
if (typeRow.length === 0) {
|
||
throw new Error(`Type de congé ${typeConge} non trouvé`);
|
||
}
|
||
|
||
const typeCongeId = typeRow[0].Id;
|
||
|
||
|
||
const [collab] = await conn.query('SELECT role FROM CollaborateurAD WHERE id = ?', [collaborateurId]);
|
||
const isApprenti = collab.length > 0 && collab[0].role === 'Apprenti';
|
||
if (typeConge === 'RTT' && isApprenti) {
|
||
return 0; // ⭐ Les apprentis n'ont pas de RTT
|
||
}
|
||
|
||
// Si pas d'arrêté, calcul normal depuis le début
|
||
if (!dernierArrete) {
|
||
if (typeConge === 'CP') {
|
||
const [collab] = await conn.query('SELECT DateEntree, CampusId FROM CollaborateurAD WHERE id = ?', [collaborateurId]);
|
||
const dateEntree = collab.length > 0 ? collab[0].DateEntree : null;
|
||
return calculerAcquisitionCP(dateReference, dateEntree);
|
||
} else {
|
||
const rttData = await calculerAcquisitionRTT(conn, collaborateurId, dateReference);
|
||
return rttData.acquisition;
|
||
}
|
||
}
|
||
|
||
const dateArrete = new Date(dernierArrete.DateArrete);
|
||
|
||
// Si la date de référence est AVANT l'arrêté, utiliser le solde figé
|
||
if (dateReference <= dateArrete) {
|
||
const soldeFige = await getSoldeFige(conn, collaborateurId, typeCongeId, anneeRef);
|
||
return soldeFige ? soldeFige.TotalAcquis : 0;
|
||
}
|
||
|
||
// Si la date est APRÈS l'arrêté, partir du solde figé + calcul depuis l'arrêté
|
||
const soldeFige = await getSoldeFige(conn, collaborateurId, typeCongeId, anneeRef);
|
||
const acquisFigee = soldeFige ? soldeFige.TotalAcquis : 0;
|
||
|
||
// Calculer l'acquisition DEPUIS l'arrêté
|
||
let acquisDepuisArrete = 0;
|
||
|
||
if (typeConge === 'CP') {
|
||
const moisDepuisArrete = getMoisTravaillesCP(dateReference, dateArrete);
|
||
acquisDepuisArrete = moisDepuisArrete * (25 / 12);
|
||
} else {
|
||
const [collab] = await conn.query('SELECT TypeContrat, CampusId FROM CollaborateurAD WHERE id = ?', [collaborateurId]);
|
||
const typeContrat = collab.length > 0 && collab[0].TypeContrat ? collab[0].TypeContrat : '37h';
|
||
|
||
const config = await getConfigurationRTT(conn, anneeRef, typeContrat);
|
||
const moisDepuisArrete = getMoisTravaillesRTT(dateReference, dateArrete);
|
||
acquisDepuisArrete = moisDepuisArrete * config.acquisitionMensuelle;
|
||
}
|
||
|
||
return acquisFigee + acquisDepuisArrete;
|
||
}
|
||
|
||
const pool = mysql.createPool(dbConfig);
|
||
|
||
const AZURE_CONFIG = {
|
||
tenantId: '9840a2a0-6ae1-4688-b03d-d2ec291be0f9',
|
||
clientId: '4bb4cc24-bac3-427c-b02c-5d14fc67b561',
|
||
clientSecret: 'gvf8Q~545Bafn8yYsgjW~QG_P1lpzaRe6gJNgb2t',
|
||
groupId: 'c1ea877c-6bca-4f47-bfad-f223640813a0'
|
||
};
|
||
|
||
const storage = multer.diskStorage({
|
||
destination: './uploads/',
|
||
filename: (req, file, cb) => {
|
||
cb(null, Date.now() + path.extname(file.originalname));
|
||
}
|
||
});
|
||
const upload = multer({ storage });
|
||
|
||
const medicalStorage = multer.diskStorage({
|
||
destination: './uploads/medical/',
|
||
filename: (req, file, cb) => {
|
||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
|
||
cb(null, 'medical-' + uniqueSuffix + path.extname(file.originalname));
|
||
}
|
||
});
|
||
|
||
const ACCES_TRANSVERSAUX = {
|
||
'sloisil@ensup.eu': {
|
||
typeAcces: 'service_multi_campus',
|
||
serviceNom: 'Pédagogie',
|
||
description: 'Sandrine - Vue complète Pédagogie (tous campus)'
|
||
},
|
||
'mbouteiller@ensup.eu': {
|
||
typeAcces: 'service_multi_campus',
|
||
serviceNom: 'Admissions',
|
||
description: 'Morgane - Vue complète Admissions (tous campus)'
|
||
},
|
||
'vnoel@ensup.eu': {
|
||
typeAcces: 'service_multi_campus',
|
||
serviceNom: 'Relations Entreprises',
|
||
description: 'Viviane - Vue complète Relations Entreprises (tous campus)'
|
||
},
|
||
'vpierrel@ensup.eu': {
|
||
typeAcces: 'service_multi_campus', // ✅ CORRIGÉ - même type que les autres
|
||
serviceNom: 'Administratif & Financier',
|
||
description: 'Virginie - Vue complète Administratif & Financier (tous campus)'
|
||
}
|
||
};
|
||
|
||
function getUserAccesTransversal(userEmail) {
|
||
const acces = ACCES_TRANSVERSAUX[userEmail?.toLowerCase()] || null;
|
||
if (acces) {
|
||
console.log(`🌐 Accès transversal: ${acces.description}`);
|
||
}
|
||
return acces;
|
||
}
|
||
|
||
|
||
|
||
const uploadMedical = multer({
|
||
storage: medicalStorage,
|
||
limits: { fileSize: 5 * 1024 * 1024 },
|
||
fileFilter: (req, file, cb) => {
|
||
const allowedTypes = ['application/pdf', 'image/jpeg', 'image/jpg', 'image/png'];
|
||
if (allowedTypes.includes(file.mimetype)) {
|
||
cb(null, true);
|
||
} else {
|
||
cb(new Error('Type de fichier non autorisé'));
|
||
}
|
||
}
|
||
});
|
||
import fs from 'fs';
|
||
if (!fs.existsSync('./uploads/medical')) {
|
||
fs.mkdirSync('./uploads/medical', { recursive: true });
|
||
}
|
||
|
||
app.get('/api/events/collaborateur', (req, res) => {
|
||
const userId = req.query.user_id;
|
||
|
||
if (!userId) {
|
||
return res.status(401).json({ error: 'user_id requis' });
|
||
}
|
||
|
||
console.log('🔔 Nouvelle connexion SSE collaborateur:', userId);
|
||
|
||
// ⭐ HEADERS CRITIQUES POUR SSE
|
||
res.setHeader('Content-Type', 'text/event-stream');
|
||
res.setHeader('Cache-Control', 'no-cache, no-transform');
|
||
res.setHeader('Connection', 'keep-alive');
|
||
res.setHeader('X-Accel-Buffering', 'no');
|
||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||
|
||
// ⭐ FLUSH IMMÉDIATEMENT POUR ÉTABLIR LA CONNEXION
|
||
res.flushHeaders();
|
||
|
||
const sendEvent = (data) => {
|
||
try {
|
||
if (res.writableEnded) {
|
||
console.log('⚠️ Tentative d\'envoi sur connexion fermée');
|
||
return false;
|
||
}
|
||
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
||
return true;
|
||
} catch (error) {
|
||
console.error('❌ Erreur envoi SSE:', error);
|
||
return false;
|
||
}
|
||
};
|
||
|
||
const client = {
|
||
userId: parseInt(userId),
|
||
send: sendEvent,
|
||
res: res // ⭐ Garder référence pour vérifier l'état
|
||
};
|
||
|
||
sseClientsCollab.add(client);
|
||
|
||
console.log(`📊 Clients SSE collaborateurs connectés: ${sseClientsCollab.size}`);
|
||
|
||
// ⭐ ÉVÉNEMENT DE CONNEXION
|
||
sendEvent({
|
||
type: 'connected',
|
||
message: 'Connexion établie',
|
||
timestamp: new Date().toISOString()
|
||
});
|
||
|
||
// ⭐ HEARTBEAT AVEC VÉRIFICATION
|
||
const heartbeat = setInterval(() => {
|
||
const success = sendEvent({
|
||
type: 'heartbeat',
|
||
timestamp: new Date().toISOString()
|
||
});
|
||
|
||
if (!success) {
|
||
console.log('💔 Heartbeat échoué, nettoyage...');
|
||
clearInterval(heartbeat);
|
||
sseClientsCollab.delete(client);
|
||
}
|
||
}, 30000); // 30 secondes
|
||
|
||
// ⭐ GESTION PROPRE DE LA DÉCONNEXION
|
||
const cleanup = () => {
|
||
console.log('🔌 Déconnexion SSE collaborateur:', userId);
|
||
clearInterval(heartbeat);
|
||
sseClientsCollab.delete(client);
|
||
console.log(`📊 Clients SSE collaborateurs connectés: ${sseClientsCollab.size}`);
|
||
};
|
||
|
||
req.on('close', cleanup);
|
||
req.on('error', (err) => {
|
||
console.error('❌ Erreur SSE connexion:', err.message);
|
||
cleanup();
|
||
});
|
||
|
||
// ⭐ TIMEOUT DE SÉCURITÉ (optionnel, mais recommandé)
|
||
req.socket.setTimeout(0); // Désactiver timeout pour SSE
|
||
});
|
||
|
||
|
||
const notifyCollabClients = (event, targetUserId = null) => {
|
||
console.log(
|
||
`📢 Notification SSE Collab: ${event.type}`,
|
||
targetUserId ? `pour user ${targetUserId}` : 'pour tous'
|
||
);
|
||
|
||
const deadClients = [];
|
||
|
||
sseClientsCollab.forEach(client => {
|
||
// ⭐ FILTRER PAR USER SI NÉCESSAIRE
|
||
if (targetUserId && client.userId !== targetUserId) {
|
||
return;
|
||
}
|
||
|
||
// ⭐ VÉRIFIER SI LA CONNEXION EST TOUJOURS ACTIVE
|
||
if (client.res && client.res.writableEnded) {
|
||
console.log(`💀 Client mort détecté: ${client.userId}`);
|
||
deadClients.push(client);
|
||
return;
|
||
}
|
||
|
||
// ⭐ ENVOYER L'ÉVÉNEMENT
|
||
const success = client.send(event);
|
||
if (!success) {
|
||
deadClients.push(client);
|
||
}
|
||
});
|
||
|
||
// ⭐ NETTOYER LES CLIENTS MORTS
|
||
deadClients.forEach(client => {
|
||
console.log(`🧹 Nettoyage client mort: ${client.userId}`);
|
||
sseClientsCollab.delete(client);
|
||
});
|
||
|
||
if (deadClients.length > 0) {
|
||
console.log(`📊 Clients SSE après nettoyage: ${sseClientsCollab.size}`);
|
||
}
|
||
};
|
||
|
||
const sseClients = new Set();
|
||
|
||
// 🔌 ROUTE SSE POUR LE CALENDRIER
|
||
app.get('/api/sse', (req, res) => {
|
||
const userId = req.query.user_id;
|
||
|
||
if (!userId) {
|
||
return res.status(400).json({ error: 'user_id requis' });
|
||
}
|
||
|
||
console.log('🔌 Nouvelle connexion SSE:', userId);
|
||
|
||
res.setHeader('Content-Type', 'text/event-stream');
|
||
res.setHeader('Cache-Control', 'no-cache');
|
||
res.setHeader('Connection', 'keep-alive');
|
||
res.setHeader('X-Accel-Buffering', 'no');
|
||
|
||
const sendEvent = (data) => {
|
||
try {
|
||
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
||
} catch (error) {
|
||
console.error('❌ Erreur envoi SSE:', error);
|
||
}
|
||
};
|
||
|
||
const client = { id: userId, send: sendEvent };
|
||
sseClients.add(client);
|
||
|
||
console.log(`📊 Clients SSE connectés: ${sseClients.size}`);
|
||
|
||
// Envoyer un heartbeat initial
|
||
sendEvent({
|
||
type: 'ping',
|
||
message: 'Connexion établie',
|
||
timestamp: new Date().toISOString()
|
||
});
|
||
|
||
// Heartbeat toutes les 30 secondes
|
||
const heartbeat = setInterval(() => {
|
||
try {
|
||
sendEvent({ type: 'ping', timestamp: new Date().toISOString() });
|
||
} catch (error) {
|
||
console.error('❌ Erreur heartbeat:', error);
|
||
clearInterval(heartbeat);
|
||
}
|
||
}, 30000);
|
||
|
||
req.on('close', () => {
|
||
console.log('🔌 Déconnexion SSE:', userId);
|
||
clearInterval(heartbeat);
|
||
sseClients.delete(client);
|
||
console.log(`📊 Clients SSE connectés: ${sseClients.size}`);
|
||
});
|
||
});
|
||
|
||
// 📢 FONCTION POUR NOTIFIER LES CLIENTS
|
||
const notifyClients = (event, userId = null) => {
|
||
console.log(`📢 Notification SSE: ${event.type}${userId ? ` pour ${userId}` : ''}`);
|
||
|
||
sseClients.forEach(client => {
|
||
// Si userId est spécifié, envoyer seulement à ce client
|
||
if (userId && client.id !== userId) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
client.send(event);
|
||
} catch (error) {
|
||
console.error('❌ Erreur envoi event:', error);
|
||
}
|
||
});
|
||
};
|
||
|
||
|
||
app.post('/api/webhook/receive', async (req, res) => {
|
||
try {
|
||
const signature = req.headers['x-webhook-signature'];
|
||
const payload = req.body;
|
||
|
||
console.log('\n📥 === WEBHOOK REÇU (COLLABORATEURS) ===');
|
||
console.log('Event:', payload.event);
|
||
console.log('Data:', JSON.stringify(payload.data, null, 2));
|
||
|
||
// Vérifier la signature
|
||
if (!webhookManager.verifySignature(payload, signature)) {
|
||
console.error('❌ Signature webhook invalide');
|
||
return res.status(401).json({ error: 'Signature invalide' });
|
||
}
|
||
|
||
const { event, data } = payload;
|
||
|
||
// Traiter selon le type d'événement
|
||
switch (event) {
|
||
case EVENTS.COMPTEUR_UPDATED:
|
||
console.log('📊 WEBHOOK COMPTEUR_UPDATED REÇ');
|
||
console.log('Collaborateur:', data.collaborateurId);
|
||
console.log('Type mise à jour:', data.typeUpdate);
|
||
console.log('Type congé:', data.typeConge);
|
||
console.log('Année:', data.annee);
|
||
console.log('Source:', data.source);
|
||
|
||
// SI MODIFICATION RH OU RECALCUL, METTRE À JOUR LA BASE LOCALE
|
||
if ((data.source === 'rh' || data.source === 'recalcul') &&
|
||
data.nouveauTotal !== undefined &&
|
||
data.nouveauSolde !== undefined) {
|
||
|
||
console.log('🔄 Synchronisation depuis RH (source:', data.source + ')...');
|
||
console.log('Nouveau Total:', data.nouveauTotal + 'j');
|
||
console.log('Nouveau Solde:', data.nouveauSolde + 'j');
|
||
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
// Identifier le type de congé
|
||
const typeName = data.typeConge === 'Congé payé' ? 'Congé payé' :
|
||
data.typeConge === 'RTT' ? 'RTT' : data.typeConge;
|
||
|
||
const [typeRow] = await conn.query(
|
||
'SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1',
|
||
[typeName]
|
||
);
|
||
|
||
if (typeRow.length > 0) {
|
||
const typeCongeId = typeRow[0].Id;
|
||
|
||
// Vérifier si le compteur existe
|
||
const [existing] = await conn.query(
|
||
`SELECT Id FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[data.collaborateurId, typeCongeId, data.annee]
|
||
);
|
||
|
||
if (existing.length > 0) {
|
||
// Mettre à jour
|
||
await conn.query(
|
||
`UPDATE CompteurConges
|
||
SET Total = ?, Solde = ?, DerniereMiseAJour = NOW()
|
||
WHERE Id = ?`,
|
||
[data.nouveauTotal, data.nouveauSolde, existing[0].Id]
|
||
);
|
||
console.log('✅ Compteur local mis à jour');
|
||
} else {
|
||
// Créer
|
||
await conn.query(
|
||
`INSERT INTO CompteurConges
|
||
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour)
|
||
VALUES (?, ?, ?, ?, ?, 0, NOW())`,
|
||
[data.collaborateurId, typeCongeId, data.annee, data.nouveauTotal, data.nouveauSolde]
|
||
);
|
||
console.log('✅ Compteur local créé');
|
||
}
|
||
}
|
||
|
||
conn.release();
|
||
} catch (dbError) {
|
||
console.error('❌ Erreur mise à jour locale:', dbError.message);
|
||
if (conn) conn.release();
|
||
}
|
||
}
|
||
|
||
// NOTIFIER LE CLIENT SSE DU COLLABORATEUR
|
||
notifyCollabClients({
|
||
type: 'compteur-updated',
|
||
collaborateurId: data.collaborateurId,
|
||
typeConge: data.typeConge,
|
||
annee: data.annee,
|
||
typeUpdate: data.typeUpdate,
|
||
nouveauTotal: data.nouveauTotal,
|
||
nouveauSolde: data.nouveauSolde,
|
||
source: data.source, // Garder la source originale
|
||
timestamp: new Date().toISOString()
|
||
}, data.collaborateurId);
|
||
|
||
console.log('✅ Notification SSE envoyée au collaborateur', data.collaborateurId);
|
||
break;
|
||
|
||
|
||
case EVENTS.DEMANDE_VALIDATED:
|
||
console.log('\n✅ === WEBHOOK DEMANDE_VALIDATED REÇU ===');
|
||
console.log(` Demande: ${data.demandeId}`);
|
||
console.log(` Statut: ${data.statut}`);
|
||
console.log(` Type: ${data.typeConge}`);
|
||
console.log(` Couleur: ${data.couleurHex}`);
|
||
|
||
// Notifier les clients SSE avec TOUTES les infos
|
||
notifyClients({
|
||
type: 'demande-validated',
|
||
demandeId: data.demandeId,
|
||
statut: data.statut,
|
||
typeConge: data.typeConge,
|
||
couleurHex: data.couleurHex || '#d946ef',
|
||
date: data.date,
|
||
periode: data.periode,
|
||
collaborateurId: data.collaborateurId,
|
||
timestamp: new Date().toISOString()
|
||
}, data.collaborateurId);
|
||
|
||
// Notifier les RH aussi
|
||
notifyClients({
|
||
type: 'demande-list-updated',
|
||
action: 'validation-collab',
|
||
demandeId: data.demandeId,
|
||
statut: data.statut,
|
||
typeConge: data.typeConge,
|
||
couleurHex: data.couleurHex || '#d946ef',
|
||
timestamp: new Date().toISOString()
|
||
});
|
||
|
||
console.log(' 📢 Notifications SSE envoyées');
|
||
break;
|
||
|
||
case EVENTS.DEMANDE_UPDATED:
|
||
console.log('\n✏️ === WEBHOOK DEMANDE_UPDATED REÇU ===');
|
||
console.log(` Demande: ${data.demandeId}`);
|
||
console.log(` Collaborateur: ${data.collaborateurId}`);
|
||
|
||
notifyCollabClients({
|
||
type: 'demande-updated-rh',
|
||
demandeId: data.demandeId,
|
||
timestamp: new Date().toISOString()
|
||
}, data.collaborateurId);
|
||
|
||
console.log(' 📢 Notification modification envoyée');
|
||
break;
|
||
|
||
case EVENTS.DEMANDE_DELETED:
|
||
console.log('\n🗑️ === WEBHOOK DEMANDE_DELETED REÇU ===');
|
||
console.log(` Demande: ${data.demandeId}`);
|
||
console.log(` Collaborateur: ${data.collaborateurId}`);
|
||
|
||
notifyCollabClients({
|
||
type: 'demande-deleted-rh',
|
||
demandeId: data.demandeId,
|
||
timestamp: new Date().toISOString()
|
||
}, data.collaborateurId);
|
||
|
||
console.log(' 📢 Notification suppression envoyée');
|
||
break;
|
||
|
||
default:
|
||
console.warn(`⚠️ Type d'événement webhook inconnu: ${event}`);
|
||
}
|
||
|
||
res.json({ success: true, message: 'Webhook traité' });
|
||
|
||
} catch (error) {
|
||
console.error('❌ Erreur traitement webhook:', error);
|
||
res.status(500).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
app.post('/api/syncCompteursFromRH', async (req, res) => {
|
||
try {
|
||
const { user_id } = req.body;
|
||
|
||
if (!user_id) {
|
||
return res.json({ success: false, message: 'user_id manquant' });
|
||
}
|
||
|
||
console.log('\n🔄 === SYNCHRONISATION MANUELLE DEPUIS RH ===');
|
||
console.log('User ID:', user_id);
|
||
|
||
// Récupérer les compteurs depuis le serveur RH
|
||
const rhUrl = process.env.RH_SERVER_URL || 'http://localhost:3001';
|
||
|
||
try {
|
||
const response = await fetch(`${rhUrl}/api/compteurs?user_id=${user_id}`, {
|
||
method: 'GET',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`Erreur serveur RH: ${response.status}`);
|
||
}
|
||
|
||
const rhCompteurs = await response.json();
|
||
|
||
console.log('📊 Compteurs RH récupérés:', rhCompteurs.length);
|
||
|
||
// Mettre à jour la base locale
|
||
const conn = await pool.getConnection();
|
||
await conn.beginTransaction();
|
||
|
||
let updated = 0;
|
||
let created = 0;
|
||
|
||
for (const compteur of rhCompteurs) {
|
||
// Identifier le type de congé
|
||
const [typeRow] = await conn.query(
|
||
'SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1',
|
||
[compteur.typeConge]
|
||
);
|
||
|
||
if (typeRow.length === 0) {
|
||
console.warn(`⚠️ Type ${compteur.typeConge} non trouvé`);
|
||
continue;
|
||
}
|
||
|
||
const typeCongeId = typeRow[0].Id;
|
||
|
||
// Vérifier si existe
|
||
const [existing] = await conn.query(`
|
||
SELECT Id FROM CompteurConges
|
||
WHERE CollaborateurADId = ?
|
||
AND TypeCongeId = ?
|
||
AND Annee = ?
|
||
`, [compteur.collaborateurId, typeCongeId, compteur.annee]);
|
||
|
||
if (existing.length > 0) {
|
||
// Mettre à jour
|
||
await conn.query(`
|
||
UPDATE CompteurConges
|
||
SET Total = ?,
|
||
Solde = ?,
|
||
SoldeReporte = ?,
|
||
DerniereMiseAJour = NOW()
|
||
WHERE Id = ?
|
||
`, [
|
||
compteur.total,
|
||
compteur.solde,
|
||
compteur.soldeReporte || 0,
|
||
existing[0].Id
|
||
]);
|
||
updated++;
|
||
} else {
|
||
// Créer
|
||
await conn.query(`
|
||
INSERT INTO CompteurConges
|
||
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour)
|
||
VALUES (?, ?, ?, ?, ?, ?, NOW())
|
||
`, [
|
||
compteur.collaborateurId,
|
||
typeCongeId,
|
||
compteur.annee,
|
||
compteur.total,
|
||
compteur.solde,
|
||
compteur.soldeReporte || 0
|
||
]);
|
||
created++;
|
||
}
|
||
}
|
||
|
||
await conn.commit();
|
||
conn.release();
|
||
|
||
console.log(`✅ Synchronisation terminée: ${updated} mis à jour, ${created} créés`);
|
||
|
||
res.json({
|
||
success: true,
|
||
message: 'Synchronisation réussie',
|
||
stats: {
|
||
total: rhCompteurs.length,
|
||
updated: updated,
|
||
created: created
|
||
}
|
||
});
|
||
|
||
} catch (fetchError) {
|
||
console.error('❌ Erreur communication avec serveur RH:', fetchError.message);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Impossible de contacter le serveur RH',
|
||
error: fetchError.message
|
||
});
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('❌ Erreur synchronisation:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur serveur',
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
function getDateFinMoisPrecedent(referenceDate = new Date()) {
|
||
const now = new Date(referenceDate);
|
||
now.setHours(0, 0, 0, 0);
|
||
return new Date(now.getFullYear(), now.getMonth(), 0);
|
||
}
|
||
|
||
function parseDateYYYYMMDD(s) {
|
||
if (!s) return null;
|
||
if (/^\d{4}-\d{2}-\d{2}$/.test(s)) {
|
||
const [y, m, d] = s.split('-').map(Number);
|
||
return new Date(y, m - 1, d);
|
||
}
|
||
return new Date(s);
|
||
}
|
||
|
||
const LEAVE_RULES = {
|
||
CP: {
|
||
nom: 'Congé payé',
|
||
joursAnnuels: 25,
|
||
periodeDebut: { mois: 6, jour: 1 },
|
||
periodeFin: { mois: 5, jour: 31 },
|
||
acquisitionMensuelle: 25 / 12,
|
||
reportable: true,
|
||
periodeReport: 'exercice'
|
||
},
|
||
RTT: {
|
||
nom: 'RTT',
|
||
joursAnnuels: 10,
|
||
periodeDebut: { mois: 1, jour: 1 },
|
||
periodeFin: { mois: 12, jour: 31 },
|
||
acquisitionMensuelle: 10 / 12,
|
||
reportable: false,
|
||
periodeReport: null
|
||
}
|
||
};
|
||
|
||
// ========================================
|
||
// NOUVELLES FONCTIONS POUR RTT VARIABLES
|
||
// ========================================
|
||
|
||
/**
|
||
* Récupère la configuration RTT pour une année et un type de contrat donnés
|
||
* RÈGLES :
|
||
* - 37h : toujours 10 RTT/an (0.8333/mois)
|
||
* - Forfait jour 2025 : 10 RTT/an (0.8333/mois)
|
||
* - Forfait jour 2026+ : 12 RTT/an (1.0/mois)
|
||
*/
|
||
async function getConfigurationRTT(conn, annee, typeContrat = '37h') {
|
||
try {
|
||
// D'abord chercher en base de données
|
||
const [config] = await conn.query(
|
||
`SELECT JoursAnnuels, AcquisitionMensuelle
|
||
FROM ConfigurationRTT
|
||
WHERE Annee = ? AND TypeContrat = ?
|
||
LIMIT 1`,
|
||
[annee, typeContrat]
|
||
);
|
||
|
||
if (config.length > 0) {
|
||
return {
|
||
joursAnnuels: parseFloat(config[0].JoursAnnuels),
|
||
acquisitionMensuelle: parseFloat(config[0].AcquisitionMensuelle)
|
||
};
|
||
}
|
||
|
||
// Si pas en base, utiliser les règles par défaut
|
||
console.warn(`⚠️ Pas de config RTT en base pour ${annee}/${typeContrat}, utilisation des règles par défaut`);
|
||
|
||
return getConfigurationRTTDefaut(annee, typeContrat);
|
||
|
||
} catch (error) {
|
||
console.error('Erreur getConfigurationRTT:', error);
|
||
return getConfigurationRTTDefaut(annee, typeContrat);
|
||
}
|
||
}
|
||
function getConfigurationRTTDefaut(annee, typeContrat) {
|
||
// 37h : toujours 10 RTT/an
|
||
if (typeContrat === '37h' || typeContrat === 'temps_partiel') {
|
||
return {
|
||
joursAnnuels: 10,
|
||
acquisitionMensuelle: 10 / 12 // 0.8333
|
||
};
|
||
}
|
||
|
||
// Forfait jour : dépend de l'année
|
||
if (typeContrat === 'forfait_jour') {
|
||
if (annee <= 2025) {
|
||
// 2025 et avant : 10 RTT/an
|
||
return {
|
||
joursAnnuels: 10,
|
||
acquisitionMensuelle: 10 / 12 // 0.8333
|
||
};
|
||
} else {
|
||
// 2026 et après : 12 RTT/an
|
||
return {
|
||
joursAnnuels: 12,
|
||
acquisitionMensuelle: 12 / 12 // 1.0
|
||
};
|
||
}
|
||
}
|
||
|
||
// Par défaut : 10 RTT/an
|
||
return {
|
||
joursAnnuels: 10,
|
||
acquisitionMensuelle: 10 / 12
|
||
};
|
||
}
|
||
|
||
|
||
/**
|
||
* Calcule l'acquisition RTT avec la formule Excel exacte
|
||
*/
|
||
async function calculerAcquisitionRTT(conn, collaborateurId, dateReference = new Date()) {
|
||
const d = new Date(dateReference);
|
||
d.setHours(0, 0, 0, 0);
|
||
const annee = d.getFullYear();
|
||
|
||
// 1️⃣ Récupérer les infos du collaborateur
|
||
const [collabInfo] = await conn.query(
|
||
`SELECT TypeContrat, DateEntree, role FROM CollaborateurAD WHERE id = ?`,
|
||
[collaborateurId]
|
||
);
|
||
|
||
if (collabInfo.length === 0) {
|
||
throw new Error(`Collaborateur ${collaborateurId} non trouvé`);
|
||
}
|
||
|
||
const typeContrat = collabInfo[0].TypeContrat || '37h';
|
||
const dateEntree = collabInfo[0].DateEntree;
|
||
const isApprenti = collabInfo[0].role === 'Apprenti';
|
||
|
||
// 2️⃣ Apprentis = pas de RTT
|
||
if (isApprenti) {
|
||
return {
|
||
acquisition: 0,
|
||
moisTravailles: 0,
|
||
config: { joursAnnuels: 0, acquisitionMensuelle: 0 },
|
||
typeContrat: typeContrat
|
||
};
|
||
}
|
||
|
||
// 3️⃣ Récupérer la configuration RTT (avec règles 2025/2026)
|
||
const config = await getConfigurationRTT(conn, annee, typeContrat);
|
||
|
||
console.log(`📊 Config RTT ${annee}/${typeContrat}: ${config.joursAnnuels}j/an (${config.acquisitionMensuelle.toFixed(4)}/mois)`);
|
||
|
||
// 4️⃣ Début d'acquisition = 01/01/N ou date d'entrée si postérieure
|
||
let dateDebutAcquis = new Date(annee, 0, 1); // 01/01/N
|
||
dateDebutAcquis.setHours(0, 0, 0, 0);
|
||
|
||
if (dateEntree) {
|
||
const entree = new Date(dateEntree);
|
||
entree.setHours(0, 0, 0, 0);
|
||
|
||
if (entree.getFullYear() === annee && entree > dateDebutAcquis) {
|
||
dateDebutAcquis = entree;
|
||
}
|
||
|
||
if (entree.getFullYear() > annee) {
|
||
return {
|
||
acquisition: 0,
|
||
moisTravailles: 0,
|
||
config: config,
|
||
typeContrat: typeContrat
|
||
};
|
||
}
|
||
}
|
||
|
||
// 5️⃣ Calculer avec la formule Excel
|
||
const acquisition = calculerAcquisitionFormuleExcel(dateDebutAcquis, d, config.acquisitionMensuelle);
|
||
|
||
// 6️⃣ Calculer les mois travaillés (pour info)
|
||
const moisTravailles = config.acquisitionMensuelle > 0
|
||
? acquisition / config.acquisitionMensuelle
|
||
: 0;
|
||
|
||
// 7️⃣ Plafonner au maximum annuel
|
||
const acquisitionFinale = Math.min(acquisition, config.joursAnnuels);
|
||
|
||
return {
|
||
acquisition: Math.round(acquisitionFinale * 100) / 100,
|
||
moisTravailles: Math.round(moisTravailles * 100) / 100,
|
||
config: config,
|
||
typeContrat: typeContrat
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Calcule l'acquisition avec la formule Excel exacte :
|
||
* E1 * ((JOUR(FIN.MOIS(B1;0)) - JOUR(B1) + 1) / JOUR(FIN.MOIS(B1;0))
|
||
* + DATEDIF(B1;B2;"m") - 1
|
||
* + JOUR(B2) / JOUR(FIN.MOIS(B2;0)))
|
||
*/
|
||
function calculerAcquisitionFormuleExcel(dateDebut, dateReference, coeffMensuel) {
|
||
const b1 = new Date(dateDebut);
|
||
const b2 = new Date(dateReference);
|
||
b1.setHours(0, 0, 0, 0);
|
||
b2.setHours(0, 0, 0, 0);
|
||
|
||
// Si date référence avant date début
|
||
if (b2 < b1) {
|
||
return 0;
|
||
}
|
||
|
||
// Si même mois et même année
|
||
if (b1.getFullYear() === b2.getFullYear() && b1.getMonth() === b2.getMonth()) {
|
||
const joursTotal = new Date(b2.getFullYear(), b2.getMonth() + 1, 0).getDate();
|
||
const joursAcquis = b2.getDate() - b1.getDate() + 1;
|
||
return Math.round((joursAcquis / joursTotal) * coeffMensuel * 100) / 100;
|
||
}
|
||
|
||
// 1️⃣ Fraction du PREMIER mois
|
||
const joursFinMoisB1 = new Date(b1.getFullYear(), b1.getMonth() + 1, 0).getDate();
|
||
const jourB1 = b1.getDate();
|
||
const fractionPremierMois = (joursFinMoisB1 - jourB1 + 1) / joursFinMoisB1;
|
||
|
||
// 2️⃣ Mois COMPLETS entre
|
||
const moisComplets = dateDifMonths(b1, b2) - 1;
|
||
|
||
// 3️⃣ Fraction du DERNIER mois
|
||
const joursFinMoisB2 = new Date(b2.getFullYear(), b2.getMonth() + 1, 0).getDate();
|
||
const jourB2 = b2.getDate();
|
||
const fractionDernierMois = jourB2 / joursFinMoisB2;
|
||
|
||
// 4️⃣ Total
|
||
const totalMois = fractionPremierMois + Math.max(0, moisComplets) + fractionDernierMois;
|
||
const acquisition = totalMois * coeffMensuel;
|
||
|
||
return Math.round(acquisition * 100) / 100;
|
||
}
|
||
/**
|
||
* Équivalent de DATEDIF(date1, date2, "m") en JavaScript
|
||
*/
|
||
function dateDifMonths(date1, date2) {
|
||
const d1 = new Date(date1);
|
||
const d2 = new Date(date2);
|
||
|
||
let months = (d2.getFullYear() - d1.getFullYear()) * 12;
|
||
months += d2.getMonth() - d1.getMonth();
|
||
|
||
// Si le jour de d2 < jour de d1, on n'a pas encore complété le mois
|
||
if (d2.getDate() < d1.getDate()) {
|
||
months--;
|
||
}
|
||
|
||
return Math.max(0, months);
|
||
}
|
||
/**
|
||
* Calcule l'acquisition CP avec la formule Excel exacte
|
||
*/
|
||
function calculerAcquisitionCP(dateReference = new Date(), dateEntree = null) {
|
||
const d = new Date(dateReference);
|
||
d.setHours(0, 0, 0, 0);
|
||
|
||
const annee = d.getFullYear();
|
||
const mois = d.getMonth() + 1;
|
||
|
||
// 1️⃣ Déterminer le début de l'exercice CP (01/06)
|
||
let exerciceDebut;
|
||
if (mois >= 6) {
|
||
exerciceDebut = new Date(annee, 5, 1); // 01/06/N
|
||
} else {
|
||
exerciceDebut = new Date(annee - 1, 5, 1); // 01/06/N-1
|
||
}
|
||
exerciceDebut.setHours(0, 0, 0, 0);
|
||
|
||
// 2️⃣ Ajuster si date d'entrée postérieure
|
||
let dateDebutAcquis = new Date(exerciceDebut);
|
||
if (dateEntree) {
|
||
const entree = new Date(dateEntree);
|
||
entree.setHours(0, 0, 0, 0);
|
||
if (entree > exerciceDebut) {
|
||
dateDebutAcquis = entree;
|
||
}
|
||
}
|
||
|
||
// 3️⃣ Calculer avec la formule Excel
|
||
const coeffCP = 25 / 12; // 2.0833
|
||
const acquisition = calculerAcquisitionFormuleExcel(dateDebutAcquis, d, coeffCP);
|
||
|
||
// 4️⃣ Plafonner à 25 jours
|
||
return Math.min(acquisition, 25);
|
||
}
|
||
|
||
|
||
// ========================================
|
||
// TÂCHES CRON
|
||
// ========================================
|
||
|
||
cron.schedule('0 2 * * *', async () => {
|
||
console.log('🔄 [CRON] Mise à jour quotidienne des compteurs...');
|
||
try {
|
||
const conn = await pool.getConnection();
|
||
await conn.beginTransaction();
|
||
|
||
const [collaborateurs] = await conn.query(`
|
||
SELECT id, prenom, nom, CampusId
|
||
FROM CollaborateurAD
|
||
WHERE (actif = 1 OR actif IS NULL)
|
||
`);
|
||
|
||
let successCount = 0;
|
||
const today = new Date();
|
||
|
||
for (const collab of collaborateurs) {
|
||
try {
|
||
await updateMonthlyCounters(conn, collab.id, today);
|
||
successCount++;
|
||
} catch (error) {
|
||
console.error(`❌ Erreur pour ${collab.prenom} ${collab.nom}:`, error.message);
|
||
}
|
||
}
|
||
|
||
await conn.commit();
|
||
console.log(`✅ [CRON] ${successCount}/${collaborateurs.length} compteurs mis à jour`);
|
||
conn.release();
|
||
} catch (error) {
|
||
console.error('❌ [CRON] Erreur mise à jour quotidienne:', error);
|
||
}
|
||
});
|
||
|
||
cron.schedule('59 23 31 12 *', async () => {
|
||
console.log('🎆 [CRON] Traitement fin d\'année RTT...');
|
||
try {
|
||
const conn = await pool.getConnection();
|
||
await conn.beginTransaction();
|
||
const [collaborateurs] = await conn.query('SELECT id, CampusId FROM CollaborateurAD');
|
||
let successCount = 0;
|
||
for (const collab of collaborateurs) {
|
||
try {
|
||
await processEndOfYearRTT(conn, collab.id);
|
||
successCount++;
|
||
} catch (error) {
|
||
console.error(`❌ Erreur RTT pour ${collab.id}:`, error.message);
|
||
}
|
||
}
|
||
await conn.commit();
|
||
console.log(`✅ [CRON] ${successCount}/${collaborateurs.length} RTT réinitialisés`);
|
||
conn.release();
|
||
} catch (error) {
|
||
console.error('❌ [CRON] Erreur traitement fin d\'année:', error);
|
||
}
|
||
});
|
||
|
||
cron.schedule('59 23 31 5 *', async () => {
|
||
console.log('📅 [CRON] Traitement fin d\'exercice CP...');
|
||
try {
|
||
const conn = await pool.getConnection();
|
||
await conn.beginTransaction();
|
||
const [collaborateurs] = await conn.query('SELECT id, CampusId FROM CollaborateurAD');
|
||
let successCount = 0;
|
||
for (const collab of collaborateurs) {
|
||
try {
|
||
await processEndOfExerciceCP(conn, collab.id);
|
||
successCount++;
|
||
} catch (error) {
|
||
console.error(`❌ Erreur CP pour ${collab.id}:`, error.message);
|
||
}
|
||
}
|
||
await conn.commit();
|
||
console.log(`✅ [CRON] ${successCount}/${collaborateurs.length} CP reportés`);
|
||
conn.release();
|
||
} catch (error) {
|
||
console.error('❌ [CRON] Erreur traitement fin d\'exercice:', error);
|
||
}
|
||
});
|
||
|
||
// ========================================
|
||
// CRON : CRÉER ARRÊTÉS MENSUELS AUTOMATIQUEMENT
|
||
// ========================================
|
||
|
||
cron.schedule('55 23 28-31 * *', async () => {
|
||
const today = new Date();
|
||
const lastDay = new Date(today.getFullYear(), today.getMonth() + 1, 0);
|
||
|
||
// ⭐ Vérifier qu'on est bien le dernier jour du mois
|
||
if (today.getDate() === lastDay.getDate()) {
|
||
console.log(`📅 [CRON] Création arrêté fin de mois: ${today.toISOString().split('T')[0]}`);
|
||
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
await conn.beginTransaction();
|
||
|
||
const annee = today.getFullYear();
|
||
const mois = today.getMonth() + 1; // 1-12
|
||
const dateArrete = today.toISOString().split('T')[0];
|
||
|
||
// ⭐ Vérifier si l'arrêté n'existe pas déjà
|
||
const [existing] = await conn.query(
|
||
'SELECT Id FROM ArreteComptable WHERE Annee = ? AND Mois = ?',
|
||
[annee, mois]
|
||
);
|
||
|
||
if (existing.length > 0) {
|
||
console.log(`⚠️ [CRON] Arrêté ${mois}/${annee} existe déjà, skip`);
|
||
await conn.rollback();
|
||
conn.release();
|
||
return;
|
||
}
|
||
|
||
// ⭐ Créer l'arrêté
|
||
const [result] = await conn.query(`
|
||
INSERT INTO ArreteComptable
|
||
(DateArrete, Annee, Mois, Libelle, Description, Statut, DateCreation)
|
||
VALUES (?, ?, ?, ?, ?, 'En cours', NOW())
|
||
`, [
|
||
dateArrete,
|
||
annee,
|
||
mois,
|
||
`Arrêté comptable ${getMonthName(mois)} ${annee}`,
|
||
`Arrêté mensuel automatique - Clôture des soldes au ${dateArrete}`
|
||
]);
|
||
|
||
const arreteId = result.insertId;
|
||
console.log(`✅ [CRON] Arrêté créé: ID ${arreteId}`);
|
||
|
||
// ⭐ Créer le snapshot
|
||
await conn.query('CALL sp_creer_snapshot_arrete(?)', [arreteId]);
|
||
console.log(`📸 [CRON] Snapshot créé pour l'arrêté ${arreteId}`);
|
||
|
||
// ⭐ Compter les soldes figés
|
||
const [count] = await conn.query(
|
||
'SELECT COUNT(*) as total FROM SoldesFiges WHERE ArreteId = ?',
|
||
[arreteId]
|
||
);
|
||
|
||
await conn.commit();
|
||
|
||
console.log(`🎉 [CRON] Arrêté ${mois}/${annee} terminé: ${count[0].total} soldes figés`);
|
||
|
||
} catch (error) {
|
||
await conn.rollback();
|
||
console.error(`❌ [CRON] Erreur création arrêté:`, error.message);
|
||
} finally {
|
||
conn.release();
|
||
}
|
||
}
|
||
});
|
||
|
||
// Mail mensuel le 1er à 9h
|
||
cron.schedule('0 9 1 * *', async () => {
|
||
console.log('📧 Envoi mails compte-rendu mensuel...');
|
||
const conn = await pool.getConnection();
|
||
|
||
const [cadres] = await conn.query(`
|
||
SELECT id, email, prenom, nom
|
||
FROM CollaborateurAD
|
||
WHERE TypeContrat = 'forfait_jour' AND (actif = 1 OR actif IS NULL)
|
||
`);
|
||
|
||
const moisPrecedent = new Date();
|
||
moisPrecedent.setMonth(moisPrecedent.getMonth() - 1);
|
||
|
||
for (const cadre of cadres) {
|
||
// Envoyer mail via Microsoft Graph API
|
||
// Enregistrer dans MailsCompteRendu
|
||
}
|
||
|
||
conn.release();
|
||
});
|
||
|
||
// Relance hebdomadaire le lundi à 9h
|
||
cron.schedule('0 9 * * 1', async () => {
|
||
console.log('🔔 Relance hebdomadaire compte-rendu...');
|
||
const conn = await pool.getConnection();
|
||
|
||
const moisCourant = new Date().getMonth() + 1;
|
||
const anneeCourante = new Date().getFullYear();
|
||
|
||
const [nonValides] = await conn.query(`
|
||
SELECT DISTINCT ca.id, ca.email, ca.prenom, ca.nom
|
||
FROM CollaborateurAD ca
|
||
LEFT JOIN CompteRenduMensuel crm ON ca.id = crm.CollaborateurADId
|
||
AND crm.Annee = ? AND crm.Mois = ?
|
||
WHERE ca.TypeContrat = 'forfait_jour'
|
||
AND (crm.Statut IS NULL OR crm.Statut != 'Validé')
|
||
`, [anneeCourante, moisCourant - 1]);
|
||
|
||
for (const cadre of nonValides) {
|
||
// Envoyer relance
|
||
}
|
||
|
||
conn.release();
|
||
});
|
||
|
||
|
||
// ⭐ Fonction helper pour les noms de mois
|
||
function getMonthName(mois) {
|
||
const mois_names = ['', 'Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin',
|
||
'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'];
|
||
return mois_names[mois] || mois;
|
||
}
|
||
|
||
async function getGraphToken() {
|
||
try {
|
||
const params = new URLSearchParams({
|
||
grant_type: 'client_credentials',
|
||
client_id: AZURE_CONFIG.clientId,
|
||
client_secret: AZURE_CONFIG.clientSecret,
|
||
scope: 'https://graph.microsoft.com/.default'
|
||
});
|
||
const response = await axios.post(
|
||
`https://login.microsoftonline.com/${AZURE_CONFIG.tenantId}/oauth2/v2.0/token`,
|
||
params.toString(),
|
||
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
|
||
);
|
||
return response.data.access_token;
|
||
} catch (error) {
|
||
console.error('Erreur obtention token:', error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
async function sendMailGraph(accessToken, fromEmail, toEmail, subject, bodyHtml) {
|
||
try {
|
||
await axios.post(
|
||
`https://graph.microsoft.com/v1.0/users/${fromEmail}/sendMail`,
|
||
{
|
||
message: {
|
||
subject,
|
||
body: { contentType: 'HTML', content: bodyHtml },
|
||
toRecipients: [{ emailAddress: { address: toEmail } }]
|
||
},
|
||
saveToSentItems: false
|
||
},
|
||
{
|
||
headers: {
|
||
Authorization: `Bearer ${accessToken}`,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
}
|
||
);
|
||
return true;
|
||
} catch (error) {
|
||
console.error('Erreur envoi email:', error);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
function getWorkingDays(startDate, endDate) {
|
||
let workingDays = 0;
|
||
const current = new Date(startDate);
|
||
const end = new Date(endDate);
|
||
while (current <= end) {
|
||
const dayOfWeek = current.getDay();
|
||
if (dayOfWeek !== 0 && dayOfWeek !== 6) {
|
||
workingDays++;
|
||
}
|
||
current.setDate(current.getDate() + 1);
|
||
}
|
||
return workingDays;
|
||
}
|
||
|
||
function formatDate(date) {
|
||
const d = new Date(date);
|
||
const day = String(d.getDate()).padStart(2, '0');
|
||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||
const year = d.getFullYear();
|
||
return `${day}/${month}/${year}`;
|
||
}
|
||
|
||
function getExerciceCP(date = new Date()) {
|
||
const d = new Date(date);
|
||
const annee = d.getFullYear();
|
||
const mois = d.getMonth() + 1;
|
||
if (mois >= 1 && mois <= 5) {
|
||
return `${annee - 1}-${annee}`;
|
||
}
|
||
return `${annee}-${annee + 1}`;
|
||
}
|
||
|
||
function getMoisTravaillesCP(date = new Date(), dateEntree = null) {
|
||
const d = new Date(date);
|
||
d.setHours(0, 0, 0, 0);
|
||
|
||
const annee = d.getFullYear();
|
||
const mois = d.getMonth() + 1;
|
||
|
||
let debutExercice;
|
||
if (mois >= 6) {
|
||
debutExercice = new Date(annee, 5, 1);
|
||
} else {
|
||
debutExercice = new Date(annee - 1, 5, 1);
|
||
}
|
||
debutExercice.setHours(0, 0, 0, 0);
|
||
|
||
if (dateEntree) {
|
||
const entree = new Date(dateEntree);
|
||
entree.setHours(0, 0, 0, 0);
|
||
if (entree > debutExercice) {
|
||
debutExercice = entree;
|
||
}
|
||
}
|
||
|
||
// ✅ Calculer jusqu'à aujourd'hui
|
||
const diffMs = d - debutExercice;
|
||
const diffJours = Math.floor(diffMs / (1000 * 60 * 60 * 24)) + 1;
|
||
const moisTravailles = diffJours / 30.44;
|
||
|
||
return Math.max(0, Math.min(12, moisTravailles));
|
||
}
|
||
|
||
|
||
function getMoisTravaillesRTT(date = new Date(), dateEntree = null) {
|
||
const d = new Date(date);
|
||
d.setHours(0, 0, 0, 0);
|
||
const annee = d.getFullYear();
|
||
let debutAnnee = new Date(annee, 0, 1);
|
||
debutAnnee.setHours(0, 0, 0, 0);
|
||
if (dateEntree) {
|
||
const entree = new Date(dateEntree);
|
||
entree.setHours(0, 0, 0, 0);
|
||
if (entree.getFullYear() === annee && entree > debutAnnee) {
|
||
debutAnnee = entree;
|
||
} else if (entree.getFullYear() > annee) {
|
||
return 0;
|
||
}
|
||
}
|
||
const diffMs = d - debutAnnee;
|
||
const diffJours = Math.floor(diffMs / (1000 * 60 * 60 * 24)) + 1;
|
||
const moisTravailles = diffJours / 30.44;
|
||
return Math.max(0, Math.min(12, moisTravailles));
|
||
}
|
||
|
||
function calculerAcquisitionCumulee(typeConge, dateReference = new Date(), dateEntree = null) {
|
||
const rules = LEAVE_RULES[typeConge];
|
||
if (!rules) return 0;
|
||
let moisTravailles;
|
||
if (typeConge === 'CP') {
|
||
moisTravailles = getMoisTravaillesCP(dateReference, dateEntree);
|
||
} else {
|
||
moisTravailles = getMoisTravaillesRTT(dateReference, dateEntree);
|
||
}
|
||
const acquisition = moisTravailles * rules.acquisitionMensuelle;
|
||
return Math.round(acquisition * 100) / 100;
|
||
}
|
||
|
||
async function processEndOfYearRTT(conn, collaborateurId) {
|
||
const currentYear = new Date().getFullYear();
|
||
const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']);
|
||
if (rttType.length === 0) return null;
|
||
await conn.query(
|
||
`UPDATE CompteurConges SET Solde = 0, Total = 0, SoldeReporte = 0, DerniereMiseAJour = NOW() WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, rttType[0].Id, currentYear]
|
||
);
|
||
return { type: 'RTT', action: 'reset_end_of_year', annee: currentYear };
|
||
}
|
||
|
||
async function processEndOfExerciceCP(conn, collaborateurId) {
|
||
const today = new Date();
|
||
const currentYear = today.getFullYear();
|
||
const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']);
|
||
if (cpType.length === 0) return null;
|
||
const cpTypeId = cpType[0].Id;
|
||
const [currentCounter] = await conn.query(
|
||
`SELECT Id, Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, cpTypeId, currentYear]
|
||
);
|
||
if (currentCounter.length === 0) return null;
|
||
const soldeAReporter = parseFloat(currentCounter[0].Solde);
|
||
const [nextYearCounter] = await conn.query(
|
||
`SELECT Id FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, cpTypeId, currentYear + 1]
|
||
);
|
||
if (nextYearCounter.length > 0) {
|
||
await conn.query(
|
||
`UPDATE CompteurConges SET SoldeReporte = ?, Solde = Solde + ?, DerniereMiseAJour = NOW() WHERE Id = ?`,
|
||
[soldeAReporter, soldeAReporter, nextYearCounter[0].Id]
|
||
);
|
||
} else {
|
||
await conn.query(
|
||
`INSERT INTO CompteurConges (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour) VALUES (?, ?, ?, 0, ?, ?, NOW())`,
|
||
[collaborateurId, cpTypeId, currentYear + 1, soldeAReporter, soldeAReporter]
|
||
);
|
||
}
|
||
return { type: 'CP', action: 'report_exercice', soldeReporte: soldeAReporter };
|
||
}
|
||
|
||
async function deductLeaveBalance(conn, collaborateurId, typeCongeId, nombreJours) {
|
||
const currentYear = new Date().getFullYear();
|
||
const previousYear = currentYear - 1;
|
||
let joursRestants = nombreJours;
|
||
const deductions = [];
|
||
const [compteurN1] = await conn.query(
|
||
`SELECT Id, Solde, SoldeReporte FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, typeCongeId, previousYear]
|
||
);
|
||
if (compteurN1.length > 0 && compteurN1[0].SoldeReporte > 0) {
|
||
const soldeN1 = parseFloat(compteurN1[0].SoldeReporte);
|
||
const aDeduireN1 = Math.min(soldeN1, joursRestants);
|
||
if (aDeduireN1 > 0) {
|
||
await conn.query(
|
||
`UPDATE CompteurConges SET SoldeReporte = GREATEST(0, SoldeReporte - ?), Solde = GREATEST(0, Solde - ?) WHERE Id = ?`,
|
||
[aDeduireN1, aDeduireN1, compteurN1[0].Id]
|
||
);
|
||
deductions.push({ annee: previousYear, type: 'Reporté N-1', joursUtilises: aDeduireN1, soldeAvant: soldeN1 });
|
||
joursRestants -= aDeduireN1;
|
||
}
|
||
}
|
||
if (joursRestants > 0) {
|
||
const [compteurN] = await conn.query(
|
||
`SELECT Id, Solde, SoldeReporte FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, typeCongeId, currentYear]
|
||
);
|
||
if (compteurN.length > 0) {
|
||
const soldeN = parseFloat(compteurN[0].Solde) - parseFloat(compteurN[0].SoldeReporte || 0);
|
||
const aDeduireN = Math.min(soldeN, joursRestants);
|
||
if (aDeduireN > 0) {
|
||
await conn.query(
|
||
`UPDATE CompteurConges SET Solde = GREATEST(0, Solde - ?) WHERE Id = ?`,
|
||
[aDeduireN, compteurN[0].Id]
|
||
);
|
||
deductions.push({ annee: currentYear, type: 'Année actuelle N', joursUtilises: aDeduireN, soldeAvant: soldeN });
|
||
joursRestants -= aDeduireN;
|
||
}
|
||
}
|
||
}
|
||
return { success: joursRestants === 0, joursDeduitsTotal: nombreJours - joursRestants, joursNonDeduits: joursRestants, details: deductions };
|
||
}
|
||
|
||
async function checkLeaveBalance(conn, collaborateurId, repartition) {
|
||
const currentYear = new Date().getFullYear();
|
||
const previousYear = currentYear - 1;
|
||
const verification = [];
|
||
for (const rep of repartition) {
|
||
const typeCode = rep.TypeConge;
|
||
const joursNecessaires = parseFloat(rep.NombreJours);
|
||
if (typeCode === 'ABS' || typeCode === 'Formation') continue;
|
||
const typeName = typeCode === 'CP' ? 'Congé payé' : typeCode === 'RTT' ? 'RTT' : typeCode;
|
||
const [typeRow] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', [typeName]);
|
||
if (typeRow.length === 0) continue;
|
||
const typeCongeId = typeRow[0].Id;
|
||
const [compteurN1] = await conn.query(
|
||
`SELECT SoldeReporte FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, typeCongeId, previousYear]
|
||
);
|
||
const [compteurN] = await conn.query(
|
||
`SELECT Solde, SoldeReporte FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, typeCongeId, currentYear]
|
||
);
|
||
const soldeN1 = compteurN1.length > 0 ? parseFloat(compteurN1[0].SoldeReporte || 0) : 0;
|
||
const soldeN = compteurN.length > 0 ? parseFloat(compteurN[0].Solde || 0) - parseFloat(compteurN[0].SoldeReporte || 0) : 0;
|
||
const soldeTotal = soldeN1 + soldeN;
|
||
verification.push({ type: typeName, joursNecessaires, soldeN1, soldeN, soldeTotal, suffisant: soldeTotal >= joursNecessaires, deficit: Math.max(0, joursNecessaires - soldeTotal) });
|
||
}
|
||
const insuffisants = verification.filter(v => !v.suffisant);
|
||
return { valide: insuffisants.length === 0, details: verification, insuffisants };
|
||
}
|
||
|
||
|
||
|
||
// ========================================
|
||
// MISE À JOUR DE updateMonthlyCounters
|
||
// ========================================
|
||
|
||
async function updateMonthlyCounters(conn, collaborateurId, dateReference = null) {
|
||
const today = dateReference ? new Date(dateReference) : getDateFinMoisPrecedent();
|
||
const currentYear = today.getFullYear();
|
||
const updates = [];
|
||
|
||
// Récupérer les infos du collaborateur
|
||
const [collabInfo] = await conn.query(`
|
||
SELECT DateEntree, TypeContrat, CampusId, role
|
||
FROM CollaborateurAD WHERE id = ?
|
||
`, [collaborateurId]);
|
||
|
||
if (collabInfo.length === 0) {
|
||
throw new Error(`Collaborateur ${collaborateurId} non trouvé`);
|
||
}
|
||
|
||
const dateEntree = collabInfo[0].DateEntree || null;
|
||
const typeContrat = collabInfo[0].TypeContrat || '37h';
|
||
const isApprenti = collabInfo[0].role === 'Apprenti';
|
||
|
||
console.log(`\n📊 === Mise à jour compteurs pour collaborateur ${collaborateurId} ===`);
|
||
console.log(` Date référence: ${today.toLocaleDateString('fr-FR')}`);
|
||
|
||
// ======================================
|
||
// CP (Congés Payés)
|
||
// ======================================
|
||
const exerciceCP = getExerciceCP(today);
|
||
const acquisitionCP = calculerAcquisitionCP(today, dateEntree);
|
||
|
||
const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']);
|
||
|
||
if (cpType.length > 0) {
|
||
const cpTypeId = cpType[0].Id;
|
||
|
||
// 1️⃣ Récupérer le compteur existant
|
||
const [existingCP] = await conn.query(`
|
||
SELECT Id, Total, Solde, SoldeReporte
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collaborateurId, cpTypeId, currentYear]);
|
||
|
||
if (existingCP.length > 0) {
|
||
const ancienTotal = parseFloat(existingCP[0].Total || 0);
|
||
const ancienSolde = parseFloat(existingCP[0].Solde || 0);
|
||
const soldeReporte = parseFloat(existingCP[0].SoldeReporte || 0);
|
||
|
||
console.log(` CP - Ancien acquis: ${ancienTotal.toFixed(2)}j`);
|
||
console.log(` CP - Nouvel acquis: ${acquisitionCP.toFixed(2)}j`);
|
||
|
||
// 2️⃣ Calculer l'incrément d'acquisition (nouveaux jours acquis ce mois)
|
||
const incrementAcquis = acquisitionCP - ancienTotal;
|
||
|
||
if (incrementAcquis > 0) {
|
||
console.log(` CP - Nouveaux jours ce mois: +${incrementAcquis.toFixed(2)}j`);
|
||
|
||
// 3️⃣ Vérifier si le collaborateur a de l'anticipé utilisé
|
||
const [anticipeUtilise] = await conn.query(`
|
||
SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalAnticipe,
|
||
MIN(dd.Id) as firstDeductionId,
|
||
MIN(dd.DemandeCongeId) as demandeId
|
||
FROM DeductionDetails dd
|
||
JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
|
||
WHERE dc.CollaborateurADId = ?
|
||
AND dd.TypeCongeId = ?
|
||
AND dd.Annee = ?
|
||
AND dd.TypeDeduction = 'N Anticip'
|
||
AND dc.Statut != 'Refusée'
|
||
AND dd.JoursUtilises > 0
|
||
`, [collaborateurId, cpTypeId, currentYear]);
|
||
|
||
const anticipePris = parseFloat(anticipeUtilise[0]?.totalAnticipe || 0);
|
||
|
||
if (anticipePris > 0) {
|
||
// 4️⃣ Calculer le montant à rembourser
|
||
const aRembourser = Math.min(incrementAcquis, anticipePris);
|
||
|
||
console.log(` 💳 CP - Anticipé à rembourser: ${aRembourser.toFixed(2)}j (sur ${anticipePris.toFixed(2)}j)`);
|
||
|
||
// 5️⃣ Rembourser l'anticipé en transférant vers "Année N"
|
||
// On récupère toutes les déductions anticipées pour ce type
|
||
const [deductionsAnticipees] = await conn.query(`
|
||
SELECT dd.Id, dd.DemandeCongeId, dd.JoursUtilises
|
||
FROM DeductionDetails dd
|
||
JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
|
||
WHERE dc.CollaborateurADId = ?
|
||
AND dd.TypeCongeId = ?
|
||
AND dd.Annee = ?
|
||
AND dd.TypeDeduction = 'N Anticip'
|
||
AND dc.Statut != 'Refusée'
|
||
AND dd.JoursUtilises > 0
|
||
ORDER BY dd.Id ASC
|
||
`, [collaborateurId, cpTypeId, currentYear]);
|
||
|
||
let resteARembourser = aRembourser;
|
||
|
||
for (const deduction of deductionsAnticipees) {
|
||
if (resteARembourser <= 0) break;
|
||
|
||
const joursAnticipes = parseFloat(deduction.JoursUtilises);
|
||
const aDeduiteDeCetteDeduction = Math.min(resteARembourser, joursAnticipes);
|
||
|
||
// Réduire l'anticipé
|
||
await conn.query(`
|
||
UPDATE DeductionDetails
|
||
SET JoursUtilises = GREATEST(0, JoursUtilises - ?)
|
||
WHERE Id = ?
|
||
`, [aDeduiteDeCetteDeduction, deduction.Id]);
|
||
|
||
// Vérifier si une déduction "Année N" existe déjà pour cette demande
|
||
const [existingAnneeN] = await conn.query(`
|
||
SELECT Id, JoursUtilises
|
||
FROM DeductionDetails
|
||
WHERE DemandeCongeId = ?
|
||
AND TypeCongeId = ?
|
||
AND Annee = ?
|
||
AND TypeDeduction IN ('Année N', 'Anne N', 'Anne actuelle N')
|
||
`, [deduction.DemandeCongeId, cpTypeId, currentYear]);
|
||
|
||
if (existingAnneeN.length > 0) {
|
||
// Augmenter la déduction "Année N" existante
|
||
await conn.query(`
|
||
UPDATE DeductionDetails
|
||
SET JoursUtilises = JoursUtilises + ?
|
||
WHERE Id = ?
|
||
`, [aDeduiteDeCetteDeduction, existingAnneeN[0].Id]);
|
||
} else {
|
||
// Créer une nouvelle déduction "Année N"
|
||
await conn.query(`
|
||
INSERT INTO DeductionDetails
|
||
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
|
||
VALUES (?, ?, ?, 'Année N', ?)
|
||
`, [deduction.DemandeCongeId, cpTypeId, currentYear, aDeduiteDeCetteDeduction]);
|
||
}
|
||
|
||
resteARembourser -= aDeduiteDeCetteDeduction;
|
||
|
||
console.log(` ✅ CP - Remboursé ${aDeduiteDeCetteDeduction.toFixed(2)}j (Demande ${deduction.DemandeCongeId})`);
|
||
}
|
||
|
||
// Supprimer les déductions anticipées à zéro
|
||
await conn.query(`
|
||
DELETE FROM DeductionDetails
|
||
WHERE TypeCongeId = ?
|
||
AND Annee = ?
|
||
AND TypeDeduction = 'N Anticip'
|
||
AND JoursUtilises <= 0
|
||
`, [cpTypeId, currentYear]);
|
||
}
|
||
}
|
||
|
||
// 6️⃣ Recalculer le solde total (acquis + report - consommé)
|
||
const [consomme] = await conn.query(`
|
||
SELECT COALESCE(SUM(dd.JoursUtilises), 0) as total
|
||
FROM DeductionDetails dd
|
||
JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
|
||
WHERE dc.CollaborateurADId = ?
|
||
AND dd.TypeCongeId = ?
|
||
AND dd.Annee = ?
|
||
AND dd.TypeDeduction NOT IN ('Accum Récup', 'Accum Recup')
|
||
AND dc.Statut != 'Refusée'
|
||
`, [collaborateurId, cpTypeId, currentYear]);
|
||
|
||
const totalConsomme = parseFloat(consomme[0].total || 0);
|
||
const nouveauSolde = Math.max(0, acquisitionCP + soldeReporte - totalConsomme);
|
||
|
||
console.log(` CP - Consommé total: ${totalConsomme.toFixed(2)}j`);
|
||
console.log(` CP - Nouveau solde: ${nouveauSolde.toFixed(2)}j`);
|
||
|
||
// 7️⃣ Mettre à jour le compteur
|
||
await conn.query(`
|
||
UPDATE CompteurConges
|
||
SET Total = ?, Solde = ?, DerniereMiseAJour = NOW()
|
||
WHERE Id = ?
|
||
`, [acquisitionCP, nouveauSolde, existingCP[0].Id]);
|
||
|
||
updates.push({
|
||
type: 'CP',
|
||
exercice: exerciceCP,
|
||
acquisitionCumulee: acquisitionCP,
|
||
increment: incrementAcquis,
|
||
nouveauSolde: nouveauSolde
|
||
});
|
||
} else {
|
||
// Créer le compteur s'il n'existe pas
|
||
await conn.query(`
|
||
INSERT INTO CompteurConges
|
||
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour)
|
||
VALUES (?, ?, ?, ?, ?, 0, NOW())
|
||
`, [collaborateurId, cpTypeId, currentYear, acquisitionCP, acquisitionCP]);
|
||
|
||
console.log(` CP - Compteur créé: ${acquisitionCP.toFixed(2)}j`);
|
||
|
||
updates.push({
|
||
type: 'CP',
|
||
exercice: exerciceCP,
|
||
acquisitionCumulee: acquisitionCP,
|
||
action: 'created',
|
||
nouveauSolde: acquisitionCP
|
||
});
|
||
}
|
||
}
|
||
|
||
// ======================================
|
||
// RTT
|
||
// ======================================
|
||
if (!isApprenti) {
|
||
const rttData = await calculerAcquisitionRTT(conn, collaborateurId, today);
|
||
const acquisitionRTT = rttData.acquisition;
|
||
|
||
const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']);
|
||
|
||
if (rttType.length > 0) {
|
||
const rttTypeId = rttType[0].Id;
|
||
|
||
// 1️⃣ Récupérer le compteur existant
|
||
const [existingRTT] = await conn.query(`
|
||
SELECT Id, Total, Solde
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collaborateurId, rttTypeId, currentYear]);
|
||
|
||
if (existingRTT.length > 0) {
|
||
const ancienTotal = parseFloat(existingRTT[0].Total || 0);
|
||
const ancienSolde = parseFloat(existingRTT[0].Solde || 0);
|
||
|
||
console.log(` RTT - Ancien acquis: ${ancienTotal.toFixed(2)}j`);
|
||
console.log(` RTT - Nouvel acquis: ${acquisitionRTT.toFixed(2)}j`);
|
||
|
||
// 2️⃣ Calculer l'incrément d'acquisition
|
||
const incrementAcquis = acquisitionRTT - ancienTotal;
|
||
|
||
if (incrementAcquis > 0) {
|
||
console.log(` RTT - Nouveaux jours ce mois: +${incrementAcquis.toFixed(2)}j`);
|
||
|
||
// 3️⃣ Vérifier si le collaborateur a de l'anticipé utilisé
|
||
const [anticipeUtilise] = await conn.query(`
|
||
SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalAnticipe
|
||
FROM DeductionDetails dd
|
||
JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
|
||
WHERE dc.CollaborateurADId = ?
|
||
AND dd.TypeCongeId = ?
|
||
AND dd.Annee = ?
|
||
AND dd.TypeDeduction = 'N Anticip'
|
||
AND dc.Statut != 'Refusée'
|
||
AND dd.JoursUtilises > 0
|
||
`, [collaborateurId, rttTypeId, currentYear]);
|
||
|
||
const anticipePris = parseFloat(anticipeUtilise[0]?.totalAnticipe || 0);
|
||
|
||
if (anticipePris > 0) {
|
||
// 4️⃣ Calculer le montant à rembourser
|
||
const aRembourser = Math.min(incrementAcquis, anticipePris);
|
||
|
||
console.log(` 💳 RTT - Anticipé à rembourser: ${aRembourser.toFixed(2)}j (sur ${anticipePris.toFixed(2)}j)`);
|
||
|
||
// 5️⃣ Rembourser l'anticipé
|
||
const [deductionsAnticipees] = await conn.query(`
|
||
SELECT dd.Id, dd.DemandeCongeId, dd.JoursUtilises
|
||
FROM DeductionDetails dd
|
||
JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
|
||
WHERE dc.CollaborateurADId = ?
|
||
AND dd.TypeCongeId = ?
|
||
AND dd.Annee = ?
|
||
AND dd.TypeDeduction = 'N Anticip'
|
||
AND dc.Statut != 'Refusée'
|
||
AND dd.JoursUtilises > 0
|
||
ORDER BY dd.Id ASC
|
||
`, [collaborateurId, rttTypeId, currentYear]);
|
||
|
||
let resteARembourser = aRembourser;
|
||
|
||
for (const deduction of deductionsAnticipees) {
|
||
if (resteARembourser <= 0) break;
|
||
|
||
const joursAnticipes = parseFloat(deduction.JoursUtilises);
|
||
const aDeduiteDeCetteDeduction = Math.min(resteARembourser, joursAnticipes);
|
||
|
||
// Réduire l'anticipé
|
||
await conn.query(`
|
||
UPDATE DeductionDetails
|
||
SET JoursUtilises = GREATEST(0, JoursUtilises - ?)
|
||
WHERE Id = ?
|
||
`, [aDeduiteDeCetteDeduction, deduction.Id]);
|
||
|
||
// Vérifier si une déduction "Année N" existe déjà
|
||
const [existingAnneeN] = await conn.query(`
|
||
SELECT Id, JoursUtilises
|
||
FROM DeductionDetails
|
||
WHERE DemandeCongeId = ?
|
||
AND TypeCongeId = ?
|
||
AND Annee = ?
|
||
AND TypeDeduction IN ('Année N', 'Anne N')
|
||
`, [deduction.DemandeCongeId, rttTypeId, currentYear]);
|
||
|
||
if (existingAnneeN.length > 0) {
|
||
await conn.query(`
|
||
UPDATE DeductionDetails
|
||
SET JoursUtilises = JoursUtilises + ?
|
||
WHERE Id = ?
|
||
`, [aDeduiteDeCetteDeduction, existingAnneeN[0].Id]);
|
||
} else {
|
||
await conn.query(`
|
||
INSERT INTO DeductionDetails
|
||
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
|
||
VALUES (?, ?, ?, 'Année N', ?)
|
||
`, [deduction.DemandeCongeId, rttTypeId, currentYear, aDeduiteDeCetteDeduction]);
|
||
}
|
||
|
||
resteARembourser -= aDeduiteDeCetteDeduction;
|
||
|
||
console.log(` ✅ RTT - Remboursé ${aDeduiteDeCetteDeduction.toFixed(2)}j (Demande ${deduction.DemandeCongeId})`);
|
||
}
|
||
|
||
// Supprimer les déductions anticipées à zéro
|
||
await conn.query(`
|
||
DELETE FROM DeductionDetails
|
||
WHERE TypeCongeId = ?
|
||
AND Annee = ?
|
||
AND TypeDeduction = 'N Anticip'
|
||
AND JoursUtilises <= 0
|
||
`, [rttTypeId, currentYear]);
|
||
}
|
||
}
|
||
|
||
// 6️⃣ Recalculer le solde total
|
||
const [consomme] = await conn.query(`
|
||
SELECT COALESCE(SUM(dd.JoursUtilises), 0) as total
|
||
FROM DeductionDetails dd
|
||
JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
|
||
WHERE dc.CollaborateurADId = ?
|
||
AND dd.TypeCongeId = ?
|
||
AND dd.Annee = ?
|
||
AND dd.TypeDeduction NOT IN ('Accum Récup', 'Accum Recup', 'Récup Dosée')
|
||
AND dc.Statut != 'Refusée'
|
||
`, [collaborateurId, rttTypeId, currentYear]);
|
||
|
||
const totalConsomme = parseFloat(consomme[0].total || 0);
|
||
const nouveauSolde = Math.max(0, acquisitionRTT - totalConsomme);
|
||
|
||
console.log(` RTT - Consommé total: ${totalConsomme.toFixed(2)}j`);
|
||
console.log(` RTT - Nouveau solde: ${nouveauSolde.toFixed(2)}j`);
|
||
|
||
// 7️⃣ Mettre à jour le compteur
|
||
await conn.query(`
|
||
UPDATE CompteurConges
|
||
SET Total = ?, Solde = ?, DerniereMiseAJour = NOW()
|
||
WHERE Id = ?
|
||
`, [acquisitionRTT, nouveauSolde, existingRTT[0].Id]);
|
||
|
||
updates.push({
|
||
type: 'RTT',
|
||
annee: currentYear,
|
||
typeContrat: rttData.typeContrat,
|
||
config: `${rttData.config.joursAnnuels}j/an`,
|
||
moisTravailles: rttData.moisTravailles,
|
||
acquisitionCumulee: acquisitionRTT,
|
||
increment: incrementAcquis,
|
||
nouveauSolde: nouveauSolde
|
||
});
|
||
} else {
|
||
// Créer le compteur s'il n'existe pas
|
||
await conn.query(`
|
||
INSERT INTO CompteurConges
|
||
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour)
|
||
VALUES (?, ?, ?, ?, ?, 0, NOW())
|
||
`, [collaborateurId, rttTypeId, currentYear, acquisitionRTT, acquisitionRTT]);
|
||
|
||
console.log(` RTT - Compteur créé: ${acquisitionRTT.toFixed(2)}j`);
|
||
|
||
updates.push({
|
||
type: 'RTT',
|
||
annee: currentYear,
|
||
typeContrat: rttData.typeContrat,
|
||
config: `${rttData.config.joursAnnuels}j/an`,
|
||
moisTravailles: rttData.moisTravailles,
|
||
acquisitionCumulee: acquisitionRTT,
|
||
action: 'created',
|
||
nouveauSolde: acquisitionRTT
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
console.log(`✅ Mise à jour terminée pour collaborateur ${collaborateurId}\n`);
|
||
|
||
return updates;
|
||
}
|
||
|
||
// ========================================
|
||
// ROUTES API
|
||
// ========================================
|
||
|
||
app.post('/api/login', async (req, res) => {
|
||
try {
|
||
const { email, mot_de_passe, entraUserId, userPrincipalName } = req.body;
|
||
const accessToken = req.headers.authorization?.replace('Bearer ', '');
|
||
|
||
if (accessToken && entraUserId) {
|
||
const [users] = await pool.query(`
|
||
SELECT ca.*, s.Nom as service, so.Nom as societe_nom
|
||
FROM CollaborateurAD ca
|
||
LEFT JOIN Services s ON ca.ServiceId = s.Id
|
||
LEFT JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE ca.entraUserId=? OR ca.email=?
|
||
LIMIT 1
|
||
`, [entraUserId, email]);
|
||
|
||
if (users.length === 0) return res.json({ success: false, message: 'Utilisateur non autorisé' });
|
||
const user = users[0];
|
||
|
||
try {
|
||
const graphResponse = await axios.get(`https://graph.microsoft.com/v1.0/users/${userPrincipalName}/memberOf?$select=id`, { headers: { Authorization: `Bearer ${accessToken}` } });
|
||
const userGroups = graphResponse.data.value.map(g => g.id);
|
||
const [allowedGroups] = await pool.query('SELECT Id FROM EntraGroups WHERE IsActive=1');
|
||
const allowed = allowedGroups.map(g => g.Id);
|
||
const authorized = userGroups.some(g => allowed.includes(g));
|
||
|
||
if (authorized) {
|
||
return res.json({
|
||
success: true,
|
||
message: 'Connexion réussie via Azure AD',
|
||
user: {
|
||
id: user.id,
|
||
prenom: user.prenom,
|
||
nom: user.nom,
|
||
email: user.email,
|
||
role: user.role,
|
||
service: user.service,
|
||
societeId: user.SocieteId,
|
||
societeNom: user.societe_nom
|
||
}
|
||
});
|
||
} else {
|
||
return res.json({ success: false, message: 'Utilisateur non autorisé' });
|
||
}
|
||
} catch (error) {
|
||
return res.json({ success: false, message: 'Erreur vérification groupes' });
|
||
}
|
||
}
|
||
|
||
if (email && mot_de_passe) {
|
||
const [users] = await pool.query(`
|
||
SELECT u.ID, u.Prenom, u.Nom, u.Email, u.Role, u.ServiceId, s.Nom AS ServiceNom
|
||
FROM Users u
|
||
LEFT JOIN Services s ON u.ServiceId = s.Id
|
||
WHERE u.Email = ? AND u.MDP = ?
|
||
`, [email, mot_de_passe]);
|
||
|
||
if (users.length === 1) {
|
||
return res.json({
|
||
success: true,
|
||
message: 'Connexion réussie',
|
||
user: {
|
||
id: users[0].ID,
|
||
prenom: users[0].Prenom,
|
||
nom: users[0].Nom,
|
||
email: users[0].Email,
|
||
role: users[0].Role,
|
||
service: users[0].ServiceNom || 'Non défini'
|
||
}
|
||
});
|
||
}
|
||
return res.json({ success: false, message: 'Identifiants incorrects' });
|
||
}
|
||
|
||
res.json({ success: false, message: 'Aucune méthode de connexion fournie' });
|
||
} catch (error) {
|
||
res.status(500).json({ success: false, message: 'Erreur serveur', error: error.message });
|
||
}
|
||
});
|
||
|
||
app.post('/api/check-user-groups', async (req, res) => {
|
||
try {
|
||
const { userPrincipalName } = req.body;
|
||
const accessToken = req.headers.authorization?.replace('Bearer ', '');
|
||
|
||
if (!userPrincipalName || !accessToken) return res.json({ authorized: false, message: 'Email ou token manquant' });
|
||
|
||
// 1. Vérification locale
|
||
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];
|
||
// Si l'utilisateur est inactif, on le bloque
|
||
if (user.Actif === 0) return res.json({ authorized: false, message: 'Compte désactivé' });
|
||
|
||
return res.json({
|
||
authorized: true,
|
||
role: user.role,
|
||
groups: [user.role],
|
||
localUserId: user.id,
|
||
user: {
|
||
...user,
|
||
societeId: user.SocieteId,
|
||
societeNom: user.societe_nom
|
||
}
|
||
});
|
||
}
|
||
|
||
// 2. Si pas trouvé, interrogation Microsoft Graph
|
||
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é (Hors groupe)' });
|
||
|
||
// 3. ⭐ INSERTION AVEC VALEURS PAR DÉFAUT CRITIQUES
|
||
// On met SocieteId=1 et TypeContrat='37h' par défaut pour éviter les bugs de calcul
|
||
const [result] = await pool.query(
|
||
`INSERT INTO CollaborateurAD
|
||
(entraUserId, prenom, nom, email, service, role, SocieteId, Actif, DateEntree, TypeContrat)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||
[
|
||
userInfo.id,
|
||
userInfo.givenName || 'Prénom',
|
||
userInfo.surname || 'Nom',
|
||
userInfo.mail || userPrincipalName,
|
||
userInfo.department,
|
||
'Collaborateur',
|
||
1, // SocieteId par défaut (ex: 1 = ENSUP)
|
||
1, // Actif = 1 (Important !)
|
||
new Date(), // DateEntree = Aujourd'hui
|
||
'37h' // TypeContrat par défaut
|
||
]
|
||
);
|
||
|
||
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: 1,
|
||
societeNom: 'Défaut'
|
||
}
|
||
});
|
||
} catch (error) {
|
||
console.error("Erreur check-user-groups:", error);
|
||
res.json({ authorized: false, message: 'Erreur serveur', error: error.message });
|
||
}
|
||
});
|
||
|
||
|
||
// ========================================
|
||
// ✅ CODE CORRIGÉ POUR getDetailedLeaveCounters
|
||
// À remplacer dans server.js à partir de la ligne ~1600
|
||
// ========================================
|
||
|
||
|
||
app.get('/api/getDetailedLeaveCounters', async (req, res) => {
|
||
try {
|
||
const userIdParam = req.query.user_id;
|
||
|
||
if (!userIdParam) {
|
||
return res.json({ success: false, message: 'ID utilisateur manquant' });
|
||
}
|
||
|
||
const conn = await pool.getConnection();
|
||
|
||
const isUUID = userIdParam.length > 10 && userIdParam.includes('-');
|
||
|
||
const userQuery = `
|
||
SELECT
|
||
ca.id,
|
||
ca.prenom,
|
||
ca.nom,
|
||
ca.email,
|
||
ca.role,
|
||
ca.TypeContrat,
|
||
ca.DateEntree,
|
||
ca.CampusId,
|
||
ca.SocieteId,
|
||
s.Nom as service,
|
||
so.Nom as societe_nom,
|
||
ca.description
|
||
FROM CollaborateurAD ca
|
||
LEFT JOIN Services s ON ca.ServiceId = s.Id
|
||
LEFT JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE ${isUUID ? 'ca.entraUserId' : 'ca.id'} = ?
|
||
AND (ca.Actif = 1 OR ca.Actif IS NULL)
|
||
`;
|
||
|
||
const [userInfo] = await conn.query(userQuery, [userIdParam]);
|
||
|
||
if (userInfo.length === 0) {
|
||
conn.release();
|
||
return res.json({ success: false, message: 'Utilisateur non trouvé' });
|
||
}
|
||
|
||
const user = userInfo[0];
|
||
const userId = user.id;
|
||
const dateEntree = user.DateEntree;
|
||
const typeContrat = user.TypeContrat || '37h';
|
||
|
||
const today = new Date();
|
||
const currentYear = today.getFullYear();
|
||
const previousYear = currentYear - 1;
|
||
|
||
console.log(`\n📊 === CALCUL COMPTEURS pour ${user.prenom} ${user.nom} ===`);
|
||
console.log(` Date référence: ${today.toLocaleDateString('fr-FR')}`);
|
||
|
||
const ancienneteMs = today - new Date(dateEntree || today);
|
||
const ancienneteMois = Math.floor(ancienneteMs / (1000 * 60 * 60 * 24 * 30.44));
|
||
|
||
let counters = {
|
||
user: {
|
||
id: user.id,
|
||
nom: `${user.prenom} ${user.nom}`,
|
||
prenom: user.prenom,
|
||
nomFamille: user.nom,
|
||
email: user.email,
|
||
service: user.service || 'Non défini',
|
||
role: user.role,
|
||
description: user.description,
|
||
typeContrat: typeContrat,
|
||
societeId: user.SocieteId,
|
||
societeNom: user.societe_nom || 'Non défini',
|
||
dateEntree: dateEntree ? formatDateWithoutUTC(dateEntree) : null,
|
||
ancienneteMois: ancienneteMois,
|
||
ancienneteAnnees: Math.floor(ancienneteMois / 12),
|
||
ancienneteMoisRestants: ancienneteMois % 12
|
||
},
|
||
dateReference: today.toISOString().split('T')[0],
|
||
exerciceCP: getExerciceCP(today),
|
||
anneeRTT: currentYear,
|
||
cpN1: null,
|
||
cpN: null,
|
||
rttN: null,
|
||
rttN1: null,
|
||
recupN: null,
|
||
totalDisponible: { cp: 0, rtt: 0, recup: 0, total: 0 }
|
||
};
|
||
|
||
const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']);
|
||
|
||
// ====================================
|
||
// 1️⃣ CP N-1 (Report) - CALCUL CONSOMMÉ = ACQUIS - SOLDE
|
||
// ====================================
|
||
if (cpType.length > 0) {
|
||
const [cpN1] = await conn.query(`
|
||
SELECT Annee, Total, Solde, SoldeReporte
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [userId, cpType[0].Id, previousYear]);
|
||
|
||
if (cpN1.length > 0) {
|
||
const totalAcquis = parseFloat(cpN1[0].Total || 0);
|
||
const soldeReporte = parseFloat(cpN1[0].Solde || 0);
|
||
|
||
// ⭐ CALCUL : Consommé = Acquis - Solde
|
||
const pris = Math.max(0, totalAcquis - soldeReporte);
|
||
|
||
counters.cpN1 = {
|
||
annee: previousYear,
|
||
exercice: `${previousYear}-${previousYear + 1}`,
|
||
reporte: parseFloat(totalAcquis.toFixed(2)),
|
||
pris: parseFloat(pris.toFixed(2)),
|
||
solde: parseFloat(soldeReporte.toFixed(2)),
|
||
pourcentageUtilise: totalAcquis > 0 ? parseFloat(((pris / totalAcquis) * 100).toFixed(1)) : 0
|
||
};
|
||
counters.totalDisponible.cp += counters.cpN1.solde;
|
||
|
||
console.log(`✅ CP N-1: Acquis=${totalAcquis}j, Solde=${soldeReporte}j → Consommé=${pris}j`);
|
||
} else {
|
||
counters.cpN1 = {
|
||
annee: previousYear,
|
||
exercice: `${previousYear}-${previousYear + 1}`,
|
||
reporte: 0,
|
||
pris: 0,
|
||
solde: 0,
|
||
pourcentageUtilise: 0
|
||
};
|
||
}
|
||
|
||
// ====================================
|
||
// 2️⃣ CP N (Exercice en cours) - CALCUL CONSOMMÉ = ACQUIS - SOLDE
|
||
// ====================================
|
||
const [compteurCPN] = await conn.query(`
|
||
SELECT Solde, SoldeReporte, Total
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [userId, cpType[0].Id, currentYear]);
|
||
|
||
let soldeActuelCP = 0;
|
||
let totalAcquis = 0;
|
||
let cpPris = 0;
|
||
|
||
if (compteurCPN.length > 0) {
|
||
const soldeBDD = parseFloat(compteurCPN[0].Solde || 0);
|
||
const soldeReporte = parseFloat(compteurCPN[0].SoldeReporte || 0);
|
||
totalAcquis = parseFloat(compteurCPN[0].Total || 0);
|
||
soldeActuelCP = Math.max(0, soldeBDD - soldeReporte);
|
||
|
||
// ⭐ CALCUL : Consommé = Acquis - (Solde - Report)
|
||
cpPris = Math.max(0, totalAcquis - soldeActuelCP);
|
||
|
||
console.log(` CP N - Total=${totalAcquis}j, Solde BDD=${soldeBDD}j, Report=${soldeReporte}j → Solde N=${soldeActuelCP}j, Consommé=${cpPris}j`);
|
||
} else {
|
||
const acquisCP = calculerAcquisitionCP(today, dateEntree);
|
||
soldeActuelCP = acquisCP;
|
||
totalAcquis = acquisCP;
|
||
cpPris = 0;
|
||
console.log(` CP N - Pas de compteur BDD → Calcul: ${acquisCP}j`);
|
||
}
|
||
|
||
// Calculer l'anticipé disponible
|
||
const [anticipeUtiliseCP] = await conn.query(`
|
||
SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalConsomme
|
||
FROM DeductionDetails dd
|
||
JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
|
||
WHERE dc.CollaborateurADId = ?
|
||
AND dd.TypeCongeId = ?
|
||
AND dd.Annee = ?
|
||
AND dd.TypeDeduction = 'N Anticip'
|
||
AND dc.Statut != 'Refusée'
|
||
`, [userId, cpType[0].Id, currentYear]);
|
||
|
||
const cpAnticipeUtilise = parseFloat(anticipeUtiliseCP[0]?.totalConsomme || 0);
|
||
const cpAnticipeMax = Math.max(0, 25 - totalAcquis);
|
||
const cpAnticipeDisponible = Math.max(0, cpAnticipeMax - cpAnticipeUtilise);
|
||
|
||
console.log(` CP - Acquis: ${totalAcquis.toFixed(2)}j`);
|
||
console.log(` CP - Consommé: ${cpPris.toFixed(2)}j`);
|
||
console.log(` CP - Solde: ${soldeActuelCP.toFixed(2)}j`);
|
||
|
||
counters.cpN = {
|
||
annee: currentYear,
|
||
exercice: getExerciceCP(today),
|
||
totalAnnuel: 25.00,
|
||
moisTravailles: parseFloat(getMoisTravaillesCP(today, dateEntree).toFixed(2)),
|
||
acquisitionMensuelle: parseFloat((25 / 12).toFixed(2)),
|
||
acquis: parseFloat(totalAcquis.toFixed(2)),
|
||
pris: parseFloat(cpPris.toFixed(2)), // ⭐ CONSOMMÉ CALCULÉ
|
||
solde: parseFloat(soldeActuelCP.toFixed(2)),
|
||
tauxAcquisition: parseFloat((getMoisTravaillesCP(today, dateEntree) / 12 * 100).toFixed(1)),
|
||
pourcentageUtilise: totalAcquis > 0 ? parseFloat((cpPris / totalAcquis * 100).toFixed(1)) : 0,
|
||
joursRestantsAAcquerir: parseFloat((25 - totalAcquis).toFixed(2)),
|
||
anticipe: {
|
||
acquisPrevu: parseFloat(cpAnticipeMax.toFixed(2)),
|
||
pris: parseFloat(cpAnticipeUtilise.toFixed(2)),
|
||
disponible: parseFloat(cpAnticipeDisponible.toFixed(2)),
|
||
depassement: cpAnticipeUtilise > cpAnticipeMax ? parseFloat((cpAnticipeUtilise - cpAnticipeMax).toFixed(2)) : 0
|
||
}
|
||
};
|
||
|
||
counters.totalDisponible.cp += counters.cpN.solde + cpAnticipeDisponible;
|
||
}
|
||
|
||
// ====================================
|
||
// 3️⃣ RTT N - CALCUL CONSOMMÉ = ACQUIS - SOLDE
|
||
// ====================================
|
||
const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']);
|
||
|
||
if (rttType.length > 0 && user.role !== 'Apprenti') {
|
||
const [compteurRTT] = await conn.query(`
|
||
SELECT Solde, Total
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [userId, rttType[0].Id, currentYear]);
|
||
|
||
let soldeActuelRTT = 0;
|
||
let totalAcquis = 0;
|
||
let rttPris = 0;
|
||
|
||
const rttData = await calculerAcquisitionRTT(conn, userId, today);
|
||
const rttConfig = await getConfigurationRTT(conn, currentYear, typeContrat);
|
||
|
||
if (compteurRTT.length > 0) {
|
||
soldeActuelRTT = parseFloat(compteurRTT[0].Solde || 0);
|
||
totalAcquis = parseFloat(compteurRTT[0].Total || 0);
|
||
|
||
// ⭐ CALCUL : Consommé = Acquis - Solde
|
||
rttPris = Math.max(0, totalAcquis - soldeActuelRTT);
|
||
|
||
console.log(` RTT - Acquis: ${totalAcquis}j, Solde: ${soldeActuelRTT}j → Consommé: ${rttPris}j`);
|
||
} else {
|
||
soldeActuelRTT = rttData.acquisition;
|
||
totalAcquis = rttData.acquisition;
|
||
rttPris = 0;
|
||
console.log(` RTT - Pas de compteur BDD → Calcul: ${rttData.acquisition}j`);
|
||
}
|
||
|
||
counters.rttN = {
|
||
annee: currentYear,
|
||
typeContrat: typeContrat,
|
||
totalAnnuel: parseFloat(rttConfig.joursAnnuels.toFixed(2)),
|
||
moisTravailles: rttData.moisTravailles,
|
||
acquisitionMensuelle: parseFloat(rttConfig.acquisitionMensuelle.toFixed(6)),
|
||
acquis: parseFloat(totalAcquis.toFixed(2)),
|
||
pris: parseFloat(rttPris.toFixed(2)), // ⭐ CONSOMMÉ CALCULÉ
|
||
solde: parseFloat(soldeActuelRTT.toFixed(2)),
|
||
tauxAcquisition: parseFloat((rttData.moisTravailles / 12 * 100).toFixed(1)),
|
||
pourcentageUtilise: totalAcquis > 0 ? parseFloat((rttPris / totalAcquis * 100).toFixed(1)) : 0,
|
||
joursRestantsAAcquerir: parseFloat((rttConfig.joursAnnuels - totalAcquis).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"
|
||
};
|
||
|
||
// ====================================
|
||
// 4️⃣ RÉCUP - CALCUL CONSOMMÉ = ACQUIS - SOLDE
|
||
// ====================================
|
||
const [recupType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Récupération']);
|
||
|
||
if (recupType.length > 0) {
|
||
const [compteurRecup] = await conn.query(`
|
||
SELECT Solde FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [userId, recupType[0].Id, currentYear]);
|
||
|
||
const soldeRecup = compteurRecup.length > 0 ? parseFloat(compteurRecup[0].Solde || 0) : 0;
|
||
|
||
// Récupérer accumulations depuis DeductionDetails
|
||
const [accumRecup] = await conn.query(`
|
||
SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalAccum
|
||
FROM DeductionDetails dd
|
||
JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
|
||
WHERE dc.CollaborateurADId = ?
|
||
AND dd.TypeCongeId = ?
|
||
AND dd.Annee = ?
|
||
AND dd.TypeDeduction IN ('Accum Récup', 'Accum Recup')
|
||
AND dc.Statut != 'Refusée'
|
||
`, [userId, recupType[0].Id, currentYear]);
|
||
|
||
const acquis = parseFloat(accumRecup[0]?.totalAccum || 0);
|
||
|
||
// ⭐ CALCUL : Consommé = Acquis - Solde
|
||
const pris = Math.max(0, acquis - soldeRecup);
|
||
|
||
counters.recupN = {
|
||
annee: currentYear,
|
||
acquis: parseFloat(acquis.toFixed(2)),
|
||
pris: parseFloat(pris.toFixed(2)), // ⭐ CONSOMMÉ CALCULÉ
|
||
solde: parseFloat(soldeRecup.toFixed(2)),
|
||
message: "Jours de récupération"
|
||
};
|
||
|
||
counters.totalDisponible.recup = counters.recupN.solde;
|
||
|
||
console.log(`✅ Récup: Acquis=${acquis}j, Solde=${soldeRecup}j → Consommé=${pris}j`);
|
||
}
|
||
|
||
counters.totalDisponible.total = counters.totalDisponible.cp + counters.totalDisponible.rtt + counters.totalDisponible.recup;
|
||
|
||
console.log(`\n✅ TOTAL FINAL: ${counters.totalDisponible.total.toFixed(2)}j disponibles`);
|
||
|
||
conn.release();
|
||
|
||
res.json({
|
||
success: true,
|
||
message: 'Compteurs détaillés récupérés avec succès',
|
||
data: counters,
|
||
availableCP: counters.totalDisponible.cp,
|
||
availableRTT: counters.totalDisponible.rtt,
|
||
availableRecup: counters.totalDisponible.recup
|
||
});
|
||
} catch (error) {
|
||
console.error('Erreur getDetailedLeaveCounters:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur serveur',
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
app.post('/api/updateCounters', async (req, res) => {
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
const { collaborateur_id } = req.body;
|
||
if (!collaborateur_id) return res.json({ success: false, message: 'ID collaborateur manquant' });
|
||
await conn.beginTransaction();
|
||
const updates = await updateMonthlyCounters(conn, collaborateur_id, new Date());
|
||
await conn.commit();
|
||
res.json({ success: true, message: 'Compteurs mis à jour', updates });
|
||
} catch (error) {
|
||
await conn.rollback();
|
||
res.status(500).json({ success: false, message: 'Erreur', error: error.message });
|
||
} finally {
|
||
conn.release();
|
||
}
|
||
});
|
||
|
||
app.post('/api/updateAllCounters', async (req, res) => {
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
await conn.beginTransaction();
|
||
const [collaborateurs] = await conn.query('SELECT id, CampusId FROM CollaborateurAD WHERE actif = 1 OR actif IS NULL');
|
||
const allUpdates = [];
|
||
for (const collab of collaborateurs) {
|
||
const updates = await updateMonthlyCounters(conn, collab.id, new Date());
|
||
allUpdates.push({ collaborateur_id: collab.id, updates });
|
||
}
|
||
await conn.commit();
|
||
res.json({ success: true, message: `Compteurs mis à jour pour ${collaborateurs.length} collaborateurs`, total_collaborateurs: collaborateurs.length, details: allUpdates });
|
||
} catch (error) {
|
||
await conn.rollback();
|
||
res.status(500).json({ success: false, message: 'Erreur', error: error.message });
|
||
} finally {
|
||
conn.release();
|
||
}
|
||
});
|
||
|
||
async function deductLeaveBalanceWithTracking(conn, collaborateurId, typeCongeId, nombreJours, demandeCongeId) {
|
||
const currentYear = new Date().getFullYear();
|
||
const previousYear = currentYear - 1;
|
||
let joursRestants = nombreJours;
|
||
const deductions = [];
|
||
|
||
// Étape 1: Déduire du reporté N-1 d'abord
|
||
const [compteurN1] = await conn.query(
|
||
`SELECT Id, Solde, SoldeReporte FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, typeCongeId, previousYear]
|
||
);
|
||
|
||
if (compteurN1.length > 0 && compteurN1[0].SoldeReporte > 0) {
|
||
const soldeN1 = parseFloat(compteurN1[0].SoldeReporte);
|
||
const aDeduireN1 = Math.min(soldeN1, joursRestants);
|
||
|
||
if (aDeduireN1 > 0) {
|
||
// Déduction dans la base
|
||
await conn.query(
|
||
`UPDATE CompteurConges
|
||
SET SoldeReporte = GREATEST(0, SoldeReporte - ?),
|
||
Solde = GREATEST(0, Solde - ?)
|
||
WHERE Id = ?`,
|
||
[aDeduireN1, aDeduireN1, compteurN1[0].Id]
|
||
);
|
||
|
||
// Sauvegarde du détail de la déduction
|
||
await conn.query(`
|
||
INSERT INTO DeductionDetails
|
||
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
|
||
VALUES (?, ?, ?, 'Accum Récup', ?)
|
||
`, [demandeId, recupType[0].Id, currentYear, recupJours]);
|
||
|
||
deductions.push({
|
||
annee: previousYear,
|
||
type: 'Reporté N-1',
|
||
joursUtilises: aDeduireN1,
|
||
soldeAvant: soldeN1
|
||
});
|
||
|
||
joursRestants -= aDeduireN1;
|
||
}
|
||
}
|
||
|
||
// Étape 2: Déduire de l'année N si besoin
|
||
if (joursRestants > 0) {
|
||
const [compteurN] = await conn.query(
|
||
`SELECT Id, Solde, SoldeReporte FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, typeCongeId, currentYear]
|
||
);
|
||
|
||
if (compteurN.length > 0) {
|
||
const soldeN = parseFloat(compteurN[0].Solde) - parseFloat(compteurN[0].SoldeReporte || 0);
|
||
const aDeduireN = Math.min(soldeN, joursRestants);
|
||
|
||
if (aDeduireN > 0) {
|
||
// Déduction dans la base
|
||
await conn.query(
|
||
`UPDATE CompteurConges
|
||
SET Solde = GREATEST(0, Solde - ?)
|
||
WHERE Id = ?`,
|
||
[aDeduireN, compteurN[0].Id]
|
||
);
|
||
|
||
// Sauvegarde du détail de la déduction
|
||
await conn.query(
|
||
`INSERT INTO DeductionDetails
|
||
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
|
||
VALUES (?, ?, ?, 'Année N', ?)`,
|
||
[demandeCongeId, typeCongeId, currentYear, aDeduireN]
|
||
);
|
||
|
||
deductions.push({
|
||
annee: currentYear,
|
||
type: 'Année actuelle N',
|
||
joursUtilises: aDeduireN,
|
||
soldeAvant: soldeN
|
||
});
|
||
|
||
joursRestants -= aDeduireN;
|
||
}
|
||
}
|
||
}
|
||
|
||
return {
|
||
success: joursRestants === 0,
|
||
joursDeduitsTotal: nombreJours - joursRestants,
|
||
joursNonDeduits: joursRestants,
|
||
details: deductions
|
||
};
|
||
};
|
||
|
||
async function restoreLeaveBalance(conn, demandeCongeId, collaborateurId) {
|
||
try {
|
||
console.log(`\n🔄 === RESTAURATION COMPTEURS ===`);
|
||
console.log(`Demande ID: ${demandeCongeId}`);
|
||
console.log(`Collaborateur ID: ${collaborateurId}`);
|
||
|
||
// 1️⃣ Récupérer TOUTES les déductions (y compris Récup)
|
||
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 = ?
|
||
ORDER BY dd.Id DESC`,
|
||
[demandeCongeId]
|
||
);
|
||
|
||
console.log(`📊 ${deductions.length} déductions trouvées`);
|
||
|
||
if (deductions.length === 0) {
|
||
console.log('⚠️ Aucune déduction trouvée pour cette demande');
|
||
return { success: false, message: 'Aucune déduction à restaurer' };
|
||
}
|
||
|
||
const restorations = [];
|
||
|
||
for (const deduction of deductions) {
|
||
const { TypeCongeId, Annee, TypeDeduction, JoursUtilises, TypeNom } = deduction;
|
||
|
||
console.log(`\n🔍 Traitement: ${TypeNom} - ${TypeDeduction} - ${JoursUtilises}j (Année: ${Annee})`);
|
||
|
||
// ========================================
|
||
// RÉCUP POSÉE - RESTAURATION
|
||
// ========================================
|
||
if (TypeDeduction === 'Récup Posée') {
|
||
console.log(`🔄 Restauration Récup posée: +${JoursUtilises}j`);
|
||
|
||
const [compteur] = await conn.query(
|
||
`SELECT Id, Solde FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, TypeCongeId, Annee]
|
||
);
|
||
|
||
if (compteur.length > 0) {
|
||
const ancienSolde = parseFloat(compteur[0].Solde || 0);
|
||
const nouveauSolde = ancienSolde + parseFloat(JoursUtilises);
|
||
|
||
await conn.query(
|
||
`UPDATE CompteurConges
|
||
SET Solde = ?,
|
||
DerniereMiseAJour = NOW()
|
||
WHERE Id = ?`,
|
||
[nouveauSolde, compteur[0].Id]
|
||
);
|
||
|
||
restorations.push({
|
||
type: TypeNom,
|
||
annee: Annee,
|
||
typeDeduction: TypeDeduction,
|
||
joursRestores: JoursUtilises
|
||
});
|
||
console.log(`✅ Récup restaurée: ${ancienSolde} → ${nouveauSolde}`);
|
||
} else {
|
||
console.warn(`⚠️ Compteur Récup non trouvé pour l'année ${Annee}`);
|
||
}
|
||
continue;
|
||
}
|
||
|
||
// ========================================
|
||
// N+1 ANTICIPÉ - RESTAURATION
|
||
// ========================================
|
||
if (TypeDeduction === 'N+1 Anticipé') {
|
||
console.log(`🔄 Restauration N+1 Anticipé: +${JoursUtilises}j`);
|
||
|
||
const [compteur] = await conn.query(
|
||
`SELECT Id, SoldeAnticipe FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, TypeCongeId, Annee]
|
||
);
|
||
|
||
if (compteur.length > 0) {
|
||
const ancienSolde = parseFloat(compteur[0].SoldeAnticipe || 0);
|
||
const nouveauSolde = ancienSolde + parseFloat(JoursUtilises);
|
||
|
||
await conn.query(
|
||
`UPDATE CompteurConges
|
||
SET SoldeAnticipe = ?,
|
||
DerniereMiseAJour = NOW()
|
||
WHERE Id = ?`,
|
||
[nouveauSolde, compteur[0].Id]
|
||
);
|
||
|
||
restorations.push({
|
||
type: TypeNom,
|
||
annee: Annee,
|
||
typeDeduction: TypeDeduction,
|
||
joursRestores: JoursUtilises
|
||
});
|
||
console.log(`✅ N+1 Anticipé restauré: ${ancienSolde} → ${nouveauSolde}`);
|
||
} else {
|
||
// Créer le compteur N+1 s'il n'existe pas
|
||
await conn.query(
|
||
`INSERT INTO CompteurConges
|
||
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, SoldeAnticipe, DerniereMiseAJour)
|
||
VALUES (?, ?, ?, 0, 0, 0, ?, NOW())`,
|
||
[collaborateurId, TypeCongeId, Annee, JoursUtilises]
|
||
);
|
||
|
||
restorations.push({
|
||
type: TypeNom,
|
||
annee: Annee,
|
||
typeDeduction: TypeDeduction,
|
||
joursRestores: JoursUtilises
|
||
});
|
||
console.log(`✅ Compteur N+1 créé avec ${JoursUtilises}j anticipés`);
|
||
}
|
||
continue;
|
||
}
|
||
|
||
// ========================================
|
||
// N ANTICIPÉ - RESTAURATION
|
||
// ========================================
|
||
if (TypeDeduction === 'N Anticipé') {
|
||
console.log(`🔄 Restauration N Anticipé: +${JoursUtilises}j`);
|
||
|
||
const [compteur] = await conn.query(
|
||
`SELECT Id, SoldeAnticipe FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, TypeCongeId, Annee]
|
||
);
|
||
|
||
if (compteur.length > 0) {
|
||
const ancienSolde = parseFloat(compteur[0].SoldeAnticipe || 0);
|
||
const nouveauSolde = ancienSolde + parseFloat(JoursUtilises);
|
||
|
||
await conn.query(
|
||
`UPDATE CompteurConges
|
||
SET SoldeAnticipe = ?,
|
||
DerniereMiseAJour = NOW()
|
||
WHERE Id = ?`,
|
||
[nouveauSolde, compteur[0].Id]
|
||
);
|
||
|
||
restorations.push({
|
||
type: TypeNom,
|
||
annee: Annee,
|
||
typeDeduction: TypeDeduction,
|
||
joursRestores: JoursUtilises
|
||
});
|
||
console.log(`✅ N Anticipé restauré: ${ancienSolde} → ${nouveauSolde}`);
|
||
} else {
|
||
// Créer le compteur s'il n'existe pas
|
||
await conn.query(
|
||
`INSERT INTO CompteurConges
|
||
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, SoldeAnticipe, DerniereMiseAJour)
|
||
VALUES (?, ?, ?, 0, 0, 0, ?, NOW())`,
|
||
[collaborateurId, TypeCongeId, Annee, JoursUtilises]
|
||
);
|
||
|
||
restorations.push({
|
||
type: TypeNom,
|
||
annee: Annee,
|
||
typeDeduction: TypeDeduction,
|
||
joursRestores: JoursUtilises
|
||
});
|
||
console.log(`✅ Compteur N créé avec ${JoursUtilises}j anticipés`);
|
||
}
|
||
continue;
|
||
}
|
||
|
||
// ========================================
|
||
// REPORTÉ N-1
|
||
// ========================================
|
||
if (TypeDeduction === 'Reporté N-1' || TypeDeduction === 'Report N-1' || TypeDeduction === 'Année 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}`);
|
||
}
|
||
}
|
||
|
||
// ========================================
|
||
// ACCUM RÉCUP (enlever de l'accumulation)
|
||
// ========================================
|
||
else if (TypeDeduction === 'Accum Récup' || TypeDeduction === 'Accum Recup') {
|
||
console.log(`⚠️ Accumulation Récup détectée - À enlever: ${JoursUtilises}j`);
|
||
|
||
const [compteur] = await conn.query(
|
||
`SELECT Id, Solde FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, TypeCongeId, Annee]
|
||
);
|
||
|
||
if (compteur.length > 0) {
|
||
const ancienSolde = parseFloat(compteur[0].Solde || 0);
|
||
const nouveauSolde = Math.max(0, ancienSolde - parseFloat(JoursUtilises));
|
||
|
||
await conn.query(
|
||
`UPDATE CompteurConges
|
||
SET Solde = ?,
|
||
DerniereMiseAJour = NOW()
|
||
WHERE Id = ?`,
|
||
[nouveauSolde, compteur[0].Id]
|
||
);
|
||
|
||
restorations.push({
|
||
type: TypeNom,
|
||
annee: Annee,
|
||
typeDeduction: 'Annulation Accumulation',
|
||
joursRestores: JoursUtilises
|
||
});
|
||
console.log(`✅ Accumulation annulée: ${ancienSolde} → ${nouveauSolde}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ⭐ SUPPRIMER LES DÉDUCTIONS
|
||
await conn.query(
|
||
'DELETE FROM DeductionDetails WHERE DemandeCongeId = ?',
|
||
[demandeCongeId]
|
||
);
|
||
console.log('🗑️ Déductions supprimées de la base');
|
||
|
||
// ⭐ Recalculer les soldes anticipés
|
||
console.log(`\n🔄 Recalcul des soldes anticipés...`);
|
||
await updateSoldeAnticipe(conn, collaborateurId);
|
||
|
||
console.log(`\n✅ Restauration terminée: ${restorations.length} opérations\n`);
|
||
|
||
return {
|
||
success: true,
|
||
restorations,
|
||
message: `${restorations.length} restaurations effectuées`
|
||
};
|
||
|
||
} catch (error) {
|
||
console.error('❌ Erreur lors de la restauration des soldes:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
app.get('/api/testProrata', async (req, res) => {
|
||
try {
|
||
const userId = parseInt(req.query.user_id || 0);
|
||
if (userId <= 0) return res.json({ success: false, message: 'ID utilisateur requis' });
|
||
const conn = await pool.getConnection();
|
||
const [userInfo] = await conn.query(`SELECT id, prenom, nom, DateEntree, TypeContrat, CampusId FROM CollaborateurAD WHERE id = ?`, [userId]);
|
||
if (userInfo.length === 0) { conn.release(); return res.json({ success: false, message: 'Utilisateur non trouvé' }); }
|
||
const user = userInfo[0];
|
||
const dateEntree = user.DateEntree;
|
||
const typeContrat = user.TypeContrat || '37h';
|
||
const today = new Date();
|
||
const moisCP = getMoisTravaillesCP(today, dateEntree);
|
||
const acquisCP = calculerAcquisitionCP(today, dateEntree);
|
||
const rttData = await calculerAcquisitionRTT(conn, userId, today);
|
||
conn.release();
|
||
res.json({
|
||
success: true,
|
||
user: {
|
||
id: user.id,
|
||
nom: `${user.prenom} ${user.nom}`,
|
||
dateEntree: dateEntree ? dateEntree.toISOString().split('T')[0] : null,
|
||
typeContrat: typeContrat
|
||
},
|
||
dateReference: today.toISOString().split('T')[0],
|
||
calculs: {
|
||
CP: {
|
||
moisTravailles: parseFloat(moisCP.toFixed(2)),
|
||
acquisitionMensuelle: 25 / 12,
|
||
acquisitionCumulee: acquisCP,
|
||
formule: `${moisCP.toFixed(2)} mois × ${(25 / 12).toFixed(2)}j/mois = ${acquisCP}j`
|
||
},
|
||
RTT: {
|
||
moisTravailles: rttData.moisTravailles,
|
||
acquisitionMensuelle: rttData.config.acquisitionMensuelle,
|
||
acquisitionCumulee: rttData.acquisition,
|
||
totalAnnuel: rttData.config.joursAnnuels,
|
||
formule: `${rttData.moisTravailles} mois × ${rttData.config.acquisitionMensuelle.toFixed(6)}j/mois = ${rttData.acquisition}j`
|
||
}
|
||
}
|
||
});
|
||
} catch (error) {
|
||
console.error('Erreur testProrata:', error);
|
||
res.status(500).json({ success: false, message: 'Erreur serveur', error: error.message });
|
||
}
|
||
});
|
||
|
||
app.post('/api/fixAllCounters', async (req, res) => {
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
await conn.beginTransaction();
|
||
const today = new Date();
|
||
const currentYear = today.getFullYear();
|
||
const [collaborateurs] = await conn.query('SELECT id, prenom, nom, DateEntree, CampusId FROM CollaborateurAD WHERE (actif = 1 OR actif IS NULL)');
|
||
console.log(`🔄 Correction de ${collaborateurs.length} compteurs...`);
|
||
const corrections = [];
|
||
for (const collab of collaborateurs) {
|
||
const dateEntree = collab.DateEntree;
|
||
const moisCP = getMoisTravaillesCP(today, dateEntree);
|
||
const acquisCP = calculerAcquisitionCP(today, dateEntree);
|
||
const rttData = await calculerAcquisitionRTT(conn, collab.id, today);
|
||
const acquisRTT = rttData.acquisition;
|
||
const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']);
|
||
const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']);
|
||
if (cpType.length > 0) {
|
||
const [existingCP] = await conn.query(`SELECT Id, Total, Solde, SoldeReporte FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, [collab.id, cpType[0].Id, currentYear]);
|
||
if (existingCP.length > 0) {
|
||
const ancienTotal = parseFloat(existingCP[0].Total);
|
||
const ancienSolde = parseFloat(existingCP[0].Solde);
|
||
const difference = acquisCP - ancienTotal;
|
||
const nouveauSolde = Math.max(0, ancienSolde + difference);
|
||
await conn.query(`UPDATE CompteurConges SET Total = ?, Solde = ?, DerniereMiseAJour = NOW() WHERE Id = ?`, [acquisCP, nouveauSolde, existingCP[0].Id]);
|
||
corrections.push({ collaborateur: `${collab.prenom} ${collab.nom}`, type: 'CP', ancienTotal: ancienTotal.toFixed(2), nouveauTotal: acquisCP.toFixed(2), ancienSolde: ancienSolde.toFixed(2), nouveauSolde: nouveauSolde.toFixed(2) });
|
||
}
|
||
}
|
||
if (rttType.length > 0) {
|
||
const [existingRTT] = await conn.query(`SELECT Id, Total, Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, [collab.id, rttType[0].Id, currentYear]);
|
||
if (existingRTT.length > 0) {
|
||
const ancienTotal = parseFloat(existingRTT[0].Total);
|
||
const ancienSolde = parseFloat(existingRTT[0].Solde);
|
||
const difference = acquisRTT - ancienTotal;
|
||
const nouveauSolde = Math.max(0, ancienSolde + difference);
|
||
await conn.query(`UPDATE CompteurConges SET Total = ?, Solde = ?, DerniereMiseAJour = NOW() WHERE Id = ?`, [acquisRTT, nouveauSolde, existingRTT[0].Id]);
|
||
corrections.push({ collaborateur: `${collab.prenom} ${collab.nom}`, type: 'RTT', ancienTotal: ancienTotal.toFixed(2), nouveauTotal: acquisRTT.toFixed(2), ancienSolde: ancienSolde.toFixed(2), nouveauSolde: nouveauSolde.toFixed(2) });
|
||
}
|
||
}
|
||
}
|
||
await conn.commit();
|
||
res.json({ success: true, message: `✅ ${collaborateurs.length} compteurs corrigés`, corrections: corrections });
|
||
} catch (error) {
|
||
await conn.rollback();
|
||
console.error('❌ Erreur correction compteurs:', error);
|
||
res.status(500).json({ success: false, message: 'Erreur', error: error.message });
|
||
} finally {
|
||
conn.release();
|
||
}
|
||
});
|
||
|
||
app.post('/api/processEndOfYear', async (req, res) => {
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
const { collaborateur_id } = req.body;
|
||
await conn.beginTransaction();
|
||
let result;
|
||
if (collaborateur_id) {
|
||
result = await processEndOfYearRTT(conn, collaborateur_id);
|
||
} else {
|
||
const [collaborateurs] = await conn.query('SELECT id, CampusId FROM CollaborateurAD');
|
||
const results = [];
|
||
for (const c of collaborateurs) {
|
||
const r = await processEndOfYearRTT(conn, c.id);
|
||
if (r) results.push({ collaborateur_id: c.id, ...r });
|
||
}
|
||
result = results;
|
||
}
|
||
await conn.commit();
|
||
res.json({ success: true, message: 'Traitement de fin d\'année effectué', result });
|
||
} catch (error) {
|
||
await conn.rollback();
|
||
res.status(500).json({ success: false, message: 'Erreur', error: error.message });
|
||
} finally {
|
||
conn.release();
|
||
}
|
||
});
|
||
|
||
app.post('/api/processEndOfExercice', async (req, res) => {
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
const { collaborateur_id } = req.body;
|
||
await conn.beginTransaction();
|
||
let result;
|
||
if (collaborateur_id) {
|
||
result = await processEndOfExerciceCP(conn, collaborateur_id);
|
||
} else {
|
||
const [collaborateurs] = await conn.query('SELECT id, CampusId FROM CollaborateurAD');
|
||
const results = [];
|
||
for (const c of collaborateurs) {
|
||
const r = await processEndOfExerciceCP(conn, c.id);
|
||
if (r) results.push({ collaborateur_id: c.id, ...r });
|
||
}
|
||
result = results;
|
||
}
|
||
await conn.commit();
|
||
res.json({ success: true, message: 'Traitement de fin d\'exercice CP effectué', result });
|
||
} catch (error) {
|
||
await conn.rollback();
|
||
res.status(500).json({ success: false, message: 'Erreur', error: error.message });
|
||
} finally {
|
||
conn.release();
|
||
}
|
||
});
|
||
|
||
app.get('/api/getAcquisitionDetails', async (req, res) => {
|
||
try {
|
||
const today = new Date();
|
||
const details = { date_reference: today.toISOString().split('T')[0], CP: { exercice: getExerciceCP(today), mois_travailles: getMoisTravaillesCP(today), acquisition_mensuelle: LEAVE_RULES.CP.acquisitionMensuelle, acquisition_cumulee: calculerAcquisitionCumulee('CP', today), total_annuel: LEAVE_RULES.CP.joursAnnuels, periode: '01/06 - 31/05', reportable: LEAVE_RULES.CP.reportable }, RTT: { annee: today.getFullYear(), mois_travailles: getMoisTravaillesRTT(today), acquisition_mensuelle: LEAVE_RULES.RTT.acquisitionMensuelle, acquisition_cumulee: calculerAcquisitionCumulee('RTT', today), total_annuel: LEAVE_RULES.RTT.joursAnnuels, periode: '01/01 - 31/12', reportable: LEAVE_RULES.RTT.reportable } };
|
||
res.json({ success: true, details });
|
||
} catch (error) {
|
||
res.status(500).json({ success: false, message: 'Erreur', error: error.message });
|
||
}
|
||
});
|
||
|
||
app.get('/api/getLeaveCounters', async (req, res) => {
|
||
try {
|
||
const userId = parseInt(req.query.user_id || 0);
|
||
const data = {};
|
||
if (userId > 0) {
|
||
const [rows] = await pool.query(`SELECT tc.Nom, cc.Annee, cc.Solde, cc.Total, cc.SoldeReporte FROM CompteurConges cc JOIN TypeConge tc ON cc.TypeCongeId = tc.Id WHERE cc.CollaborateurADId = ?`, [userId]);
|
||
rows.forEach(row => { data[row.Nom] = { Annee: row.Annee, Solde: parseFloat(row.Solde), Total: parseFloat(row.Total), SoldeReporte: parseFloat(row.SoldeReporte) }; });
|
||
}
|
||
res.json({ success: true, message: 'Compteurs récupérés', counters: data });
|
||
} catch (error) {
|
||
res.status(500).json({ success: false, message: 'Erreur', error: error.message });
|
||
}
|
||
});
|
||
|
||
app.get('/api/getEmploye', async (req, res) => {
|
||
try {
|
||
const id = parseInt(req.query.id || 0);
|
||
if (id <= 0) return res.json({ success: false, message: 'ID invalide' });
|
||
|
||
const conn = await pool.getConnection();
|
||
|
||
// 1️⃣ Récupérer les infos du collaborateur
|
||
const [rows] = await conn.query(`
|
||
SELECT
|
||
ca.id,
|
||
ca.Nom,
|
||
ca.Prenom,
|
||
ca.Email,
|
||
ca.role,
|
||
ca.TypeContrat,
|
||
ca.DateEntree,
|
||
ca.CampusId,
|
||
ca.SocieteId,
|
||
s.Nom as service,
|
||
so.Nom as societe_nom
|
||
FROM CollaborateurAD ca
|
||
LEFT JOIN Services s ON ca.ServiceId = s.Id
|
||
LEFT JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE ca.id = ?
|
||
`, [id]);
|
||
|
||
if (rows.length === 0) {
|
||
conn.release();
|
||
return res.json({ success: false, message: 'Collaborateur non trouvé' });
|
||
}
|
||
|
||
const employee = rows[0];
|
||
|
||
// 2️⃣ Déterminer si c'est un apprenti
|
||
const isApprenti = normalizeRole(employee.role) === 'apprenti';
|
||
|
||
// 3️⃣ Récupérer les compteurs CP
|
||
const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']);
|
||
let cpTotal = 0, cpSolde = 0;
|
||
|
||
if (cpType.length > 0) {
|
||
const currentYear = new Date().getFullYear();
|
||
const previousYear = currentYear - 1;
|
||
|
||
// CP N-1 (reporté)
|
||
const [cpN1] = await conn.query(`
|
||
SELECT SoldeReporte
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [id, cpType[0].Id, previousYear]);
|
||
|
||
// CP N (année courante)
|
||
const [cpN] = await conn.query(`
|
||
SELECT Solde, SoldeReporte
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [id, cpType[0].Id, currentYear]);
|
||
|
||
const cpN1Solde = cpN1.length > 0 ? parseFloat(cpN1[0].SoldeReporte || 0) : 0;
|
||
const cpNSolde = cpN.length > 0 ? parseFloat(cpN[0].Solde || 0) : 0;
|
||
const cpNReporte = cpN.length > 0 ? parseFloat(cpN[0].SoldeReporte || 0) : 0;
|
||
|
||
cpTotal = cpN1Solde + (cpNSolde - cpNReporte);
|
||
cpSolde = cpTotal;
|
||
}
|
||
|
||
// 4️⃣ Récupérer les compteurs RTT (sauf pour apprentis)
|
||
let rttTotal = 0, rttSolde = 0;
|
||
|
||
if (!isApprenti) {
|
||
const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']);
|
||
|
||
if (rttType.length > 0) {
|
||
const currentYear = new Date().getFullYear();
|
||
|
||
const [rttN] = await conn.query(`
|
||
SELECT Solde
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [id, rttType[0].Id, currentYear]);
|
||
|
||
rttTotal = rttN.length > 0 ? parseFloat(rttN[0].Solde || 0) : 0;
|
||
rttSolde = rttTotal;
|
||
}
|
||
}
|
||
|
||
conn.release();
|
||
|
||
// 5️⃣ Retourner les données complètes
|
||
res.json({
|
||
success: true,
|
||
employee: {
|
||
id: employee.id,
|
||
Nom: employee.Nom,
|
||
Prenom: employee.Prenom,
|
||
Email: employee.Email,
|
||
role: employee.role,
|
||
TypeContrat: employee.TypeContrat,
|
||
DateEntree: employee.DateEntree,
|
||
CampusId: employee.CampusId,
|
||
SocieteId: employee.SocieteId,
|
||
service: employee.service,
|
||
societe_nom: employee.societe_nom,
|
||
conges_restants: parseFloat(cpSolde.toFixed(2)),
|
||
rtt_restants: parseFloat(rttSolde.toFixed(2))
|
||
}
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('❌ Erreur getEmploye:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur DB',
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
app.get('/api/getEmployeRequest', async (req, res) => {
|
||
try {
|
||
const id = parseInt(req.query.id || 0);
|
||
if (id <= 0) return res.json({ success: false, message: 'ID invalide' });
|
||
|
||
const [rows] = await pool.query(`
|
||
SELECT
|
||
dc.Id,
|
||
dc.DateDebut,
|
||
dc.DateFin,
|
||
dc.NombreJours as days,
|
||
dc.Statut as status,
|
||
dc.DateDemande,
|
||
GROUP_CONCAT(DISTINCT tc.Nom ORDER BY tc.Nom SEPARATOR ', ') AS type,
|
||
CONCAT(
|
||
DATE_FORMAT(dc.DateDebut, '%d/%m/%Y'),
|
||
IF(dc.DateDebut = dc.DateFin, '', CONCAT(' - ', DATE_FORMAT(dc.DateFin, '%d/%m/%Y')))
|
||
) as date_display
|
||
FROM DemandeConge dc
|
||
LEFT JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId
|
||
LEFT JOIN TypeConge tc ON dct.TypeCongeId = tc.Id
|
||
WHERE dc.CollaborateurADId = ?
|
||
GROUP BY dc.Id, dc.DateDebut, dc.DateFin, dc.NombreJours, dc.Statut, dc.DateDemande
|
||
ORDER BY dc.DateDemande DESC
|
||
`, [id]);
|
||
|
||
res.json({
|
||
success: true,
|
||
requests: rows
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('❌ Erreur getEmployeRequest:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur DB',
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
app.get('/api/getRequests', async (req, res) => {
|
||
try {
|
||
const userId = req.query.user_id;
|
||
if (!userId) return res.json({ success: false, message: 'ID utilisateur manquant' });
|
||
|
||
// ✅ REQUÊTE CORRIGÉE avec la table de liaison
|
||
const [rows] = await pool.query(`
|
||
SELECT
|
||
dc.Id,
|
||
dc.DateDebut,
|
||
dc.DateFin,
|
||
dc.Statut,
|
||
dc.DateDemande,
|
||
dc.Commentaire,
|
||
dc.CommentaireValidation,
|
||
dc.Validateur,
|
||
dc.DocumentJoint,
|
||
GROUP_CONCAT(DISTINCT tc.Nom ORDER BY tc.Nom SEPARATOR ', ') AS TypeConges,
|
||
SUM(dct.NombreJours) as NombreJoursTotal
|
||
FROM DemandeConge dc
|
||
LEFT JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId
|
||
LEFT JOIN TypeConge tc ON dct.TypeCongeId = tc.Id
|
||
WHERE (dc.EmployeeId = ? OR dc.CollaborateurADId = ?)
|
||
GROUP BY dc.Id, dc.DateDebut, dc.DateFin, dc.Statut, dc.DateDemande, dc.Commentaire, dc.CommentaireValidation, dc.Validateur, dc.DocumentJoint
|
||
ORDER BY dc.DateDemande DESC
|
||
`, [userId, userId]);
|
||
|
||
const requests = rows.map(row => {
|
||
const workingDays = getWorkingDays(row.DateDebut, row.DateFin);
|
||
const dateDisplay = row.DateDebut === row.DateFin
|
||
? formatDate(row.DateDebut)
|
||
: `${formatDate(row.DateDebut)} - ${formatDate(row.DateFin)}`;
|
||
|
||
let fileUrl = null;
|
||
if (row.TypeConges && row.TypeConges.includes('Congé maladie') && row.DocumentJoint) {
|
||
fileUrl = `/uploads/${path.basename(row.DocumentJoint)}`;
|
||
}
|
||
|
||
return {
|
||
id: row.Id,
|
||
type: row.TypeConges || 'Non défini', // ✅ Gérer le cas null
|
||
startDate: row.DateDebut,
|
||
endDate: row.DateFin,
|
||
dateDisplay,
|
||
days: row.NombreJoursTotal || workingDays, // ✅ Utiliser le total de la table de liaison
|
||
status: row.Statut,
|
||
reason: row.Commentaire || 'Aucun commentaire',
|
||
submittedAt: row.DateDemande,
|
||
submittedDisplay: formatDate(row.DateDemande),
|
||
validator: row.Validateur || null,
|
||
validationComment: row.CommentaireValidation || null,
|
||
fileUrl
|
||
};
|
||
});
|
||
|
||
res.json({
|
||
success: true,
|
||
message: 'Demandes récupérées',
|
||
requests,
|
||
total: requests.length
|
||
});
|
||
} catch (error) {
|
||
console.error('❌ Erreur getRequests:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur',
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
app.get('/api/getAllTeamRequests', async (req, res) => {
|
||
try {
|
||
const managerId = req.query.SuperieurId;
|
||
if (!managerId) return res.json({ success: false, message: 'Paramètre SuperieurId manquant' });
|
||
const [rows] = await pool.query(`SELECT dc.Id, dc.DateDebut, dc.DateFin, dc.Statut, dc.DateDemande, dc.Commentaire, dc.DocumentJoint, dc.CollaborateurADId AS employee_id, CONCAT(ca.Prenom, ' ', ca.Nom) as employee_name, ca.Email as employee_email, tc.Nom as type FROM DemandeConge dc JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id JOIN TypeConge tc ON dc.TypeCongeId = tc.Id JOIN HierarchieValidationAD hv ON hv.CollaborateurId = ca.id WHERE hv.SuperieurId = ? ORDER BY dc.DateDemande DESC`, [managerId]);
|
||
const requests = rows.map(row => ({ id: row.Id, employee_id: row.employee_id, employee_name: row.employee_name, employee_email: row.employee_email, type: row.type, start_date: row.DateDebut, end_date: row.DateFin, date_display: row.DateDebut === row.DateFin ? formatDate(row.DateDebut) : `${formatDate(row.DateDebut)} - ${formatDate(row.DateFin)}`, days: getWorkingDays(row.DateDebut, row.DateFin), status: row.Statut, reason: row.Commentaire || '', file: row.DocumentJoint || null, submitted_at: row.DateDemande, submitted_display: formatDate(row.DateDemande) }));
|
||
res.json({ success: true, requests });
|
||
} catch (error) {
|
||
res.status(500).json({ success: false, message: 'Erreur DB', error: error.message });
|
||
}
|
||
});
|
||
|
||
app.get('/api/getPendingRequests', async (req, res) => {
|
||
try {
|
||
const managerId = req.query.manager_id;
|
||
if (!managerId) return res.json({ success: false, message: 'ID manager manquant' });
|
||
const [managerRows] = await pool.query('SELECT ServiceId, CampusId FROM CollaborateurAD WHERE id = ?', [managerId]);
|
||
if (managerRows.length === 0) return res.json({ success: false, message: 'Manager non trouvé' });
|
||
const serviceId = managerRows[0].ServiceId;
|
||
const [rows] = await pool.query(`SELECT dc.Id, dc.DateDebut, dc.DateFin, dc.Statut, dc.DateDemande, dc.Commentaire, dc.CollaborateurADId, CONCAT(ca.prenom, ' ', ca.nom) as employee_name, ca.email as employee_email, GROUP_CONCAT(tc.Nom ORDER BY tc.Nom SEPARATOR ', ') as types FROM DemandeConge dc JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id JOIN TypeConge tc ON FIND_IN_SET(tc.Id, dc.TypeCongeId) WHERE ca.ServiceId = ? AND dc.Statut = 'En attente' AND ca.id != ? GROUP BY dc.Id, dc.DateDebut, dc.DateFin, dc.Statut, dc.DateDemande, dc.Commentaire, dc.CollaborateurADId, ca.prenom, ca.nom, ca.email ORDER BY dc.DateDemande ASC`, [serviceId, managerId]);
|
||
const requests = rows.map(row => ({ id: row.Id, employee_id: row.CollaborateurADId, employee_name: row.employee_name, employee_email: row.employee_email, type: row.types, start_date: row.DateDebut, end_date: row.DateFin, date_display: row.DateDebut === row.DateFin ? formatDate(row.DateDebut) : `${formatDate(row.DateDebut)} - ${formatDate(row.DateFin)}`, days: getWorkingDays(row.DateDebut, row.DateFin), status: row.Statut, reason: row.Commentaire || '', submitted_at: row.DateDemande, submitted_display: formatDate(row.DateDemande) }));
|
||
res.json({ success: true, message: 'Demandes récupérées', requests, service_id: serviceId });
|
||
} catch (error) {
|
||
res.status(500).json({ success: false, message: 'Erreur', error: error.message });
|
||
}
|
||
});
|
||
|
||
app.get('/api/getTeamMembers', async (req, res) => {
|
||
try {
|
||
const managerId = req.query.manager_id;
|
||
if (!managerId) return res.json({ success: false, message: 'ID manager manquant' });
|
||
const [managerRows] = await pool.query('SELECT ServiceId, CampusId FROM CollaborateurAD WHERE id = ?', [managerId]);
|
||
if (managerRows.length === 0) return res.json({ success: false, message: 'Manager non trouvé' });
|
||
const serviceId = managerRows[0].ServiceId;
|
||
const [members] = await pool.query(`SELECT c.id, c.nom, c.prenom, c.email, c.role, s.Nom as service_name, c.CampusId FROM CollaborateurAD c JOIN Services s ON c.ServiceId = s.Id WHERE c.ServiceId = ? AND c.id != ? ORDER BY c.prenom, c.nom`, [serviceId, managerId]);
|
||
res.json({ success: true, message: 'Équipe récupérée', team_members: members, service_id: serviceId });
|
||
} catch (error) {
|
||
res.status(500).json({ success: false, message: 'Erreur', error: error.message });
|
||
}
|
||
});
|
||
|
||
app.get('/api/getNotifications', async (req, res) => {
|
||
try {
|
||
const userIdParam = req.query.user_id;
|
||
|
||
if (!userIdParam) {
|
||
return res.json({ success: false, message: 'ID utilisateur manquant' });
|
||
}
|
||
|
||
const conn = await pool.getConnection();
|
||
|
||
// ✅ Déterminer si c'est un UUID ou un ID numérique
|
||
const isUUID = userIdParam.length > 10 && userIdParam.includes('-');
|
||
|
||
// ✅ Récupérer l'ID numérique si on a un UUID
|
||
let userId = userIdParam;
|
||
|
||
if (isUUID) {
|
||
const [userRows] = await conn.query(
|
||
'SELECT id, CampusId FROM CollaborateurAD WHERE entraUserId = ? AND (Actif = 1 OR Actif IS NULL)',
|
||
[userIdParam]
|
||
);
|
||
|
||
if (userRows.length === 0) {
|
||
conn.release();
|
||
return res.json({
|
||
success: false,
|
||
message: 'Utilisateur non trouvé ou compte désactivé'
|
||
});
|
||
}
|
||
|
||
userId = userRows[0].id;
|
||
} else {
|
||
userId = parseInt(userIdParam);
|
||
}
|
||
|
||
// ✅ Utiliser l'ID numérique pour la requête
|
||
const [notifications] = await conn.query(`
|
||
SELECT * FROM Notifications
|
||
WHERE CollaborateurADId = ?
|
||
ORDER BY DateCreation DESC
|
||
LIMIT 50
|
||
`, [userId]);
|
||
|
||
conn.release();
|
||
|
||
res.json({
|
||
success: true,
|
||
notifications: notifications || []
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('Erreur getNotifications:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur serveur',
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
app.post('/api/markNotificationRead', async (req, res) => {
|
||
try {
|
||
const { notificationId } = req.body;
|
||
if (!notificationId || notificationId <= 0) return res.status(400).json({ success: false, message: 'ID notification invalide' });
|
||
await pool.query('UPDATE Notifications SET lu = 1 WHERE Id = ?', [notificationId]);
|
||
res.json({ success: true, message: 'Notification marquée comme lue' });
|
||
} catch (error) {
|
||
res.status(500).json({ success: false, message: 'Erreur', error: error.message });
|
||
}
|
||
});
|
||
// À ajouter avant app.listen()
|
||
|
||
/**
|
||
* POST /saisirRecupJour
|
||
* Saisir une journée de récupération (samedi travaillé)
|
||
*/
|
||
app.post('/api/saisirRecupJour', async (req, res) => {
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
await conn.beginTransaction();
|
||
|
||
const {
|
||
user_id,
|
||
date, // Date du samedi travaillé
|
||
nombre_heures = 1, // Par défaut 1 jour = 1 samedi
|
||
commentaire
|
||
} = req.body;
|
||
|
||
console.log('\n📝 === SAISIE RÉCUP ===');
|
||
console.log('User ID:', user_id);
|
||
console.log('Date:', date);
|
||
console.log('Heures:', nombre_heures);
|
||
|
||
if (!user_id || !date) {
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.json({
|
||
success: false,
|
||
message: 'Données manquantes'
|
||
});
|
||
}
|
||
|
||
// Vérifier que c'est bien un samedi
|
||
const dateObj = new Date(date);
|
||
const dayOfWeek = dateObj.getDay();
|
||
|
||
if (dayOfWeek !== 6) {
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.json({
|
||
success: false,
|
||
message: 'La récupération ne peut être saisie que pour un samedi'
|
||
});
|
||
}
|
||
|
||
// Vérifier que ce samedi n'a pas déjà été saisi
|
||
const [existing] = await conn.query(`
|
||
SELECT dc.Id
|
||
FROM DemandeConge dc
|
||
JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId
|
||
JOIN TypeConge tc ON dct.TypeCongeId = tc.Id
|
||
WHERE dc.CollaborateurADId = ?
|
||
AND dc.DateDebut = ?
|
||
AND tc.Nom = 'Récupération'
|
||
`, [user_id, date]);
|
||
|
||
if (existing.length > 0) {
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.json({
|
||
success: false,
|
||
message: 'Ce samedi a déjà été déclaré'
|
||
});
|
||
}
|
||
|
||
// Récupérer infos utilisateur
|
||
const [userInfo] = await conn.query(
|
||
'SELECT prenom, nom, email, CampusId FROM CollaborateurAD WHERE id = ?',
|
||
[user_id]
|
||
);
|
||
|
||
if (userInfo.length === 0) {
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.json({
|
||
success: false,
|
||
message: 'Utilisateur non trouvé'
|
||
});
|
||
}
|
||
|
||
const user = userInfo[0];
|
||
const userName = `${user.prenom} ${user.nom}`;
|
||
const dateFormatted = dateObj.toLocaleDateString('fr-FR');
|
||
|
||
// Récupérer le type Récupération
|
||
const [recupType] = await conn.query(
|
||
'SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1',
|
||
['Récupération']
|
||
);
|
||
|
||
if (recupType.length === 0) {
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.json({
|
||
success: false,
|
||
message: 'Type Récupération non trouvé'
|
||
});
|
||
}
|
||
|
||
const recupTypeId = recupType[0].Id;
|
||
const currentYear = dateObj.getFullYear();
|
||
|
||
// CRÉER LA DEMANDE (validée automatiquement)
|
||
const [result] = await conn.query(`
|
||
INSERT INTO DemandeConge
|
||
(CollaborateurADId, DateDebut, DateFin, TypeCongeId,
|
||
Statut, DateDemande, Commentaire, NombreJours)
|
||
VALUES (?, ?, ?, ?, 'Validée', NOW(), ?, ?)
|
||
`, [user_id, date, date, recupTypeId, commentaire || `Samedi travaillé - ${dateFormatted}`, nombre_heures]);
|
||
|
||
const demandeId = result.insertId;
|
||
|
||
// SAUVEGARDER DANS DemandeCongeType
|
||
await conn.query(`
|
||
INSERT INTO DemandeCongeType
|
||
(DemandeCongeId, TypeCongeId, NombreJours, PeriodeJournee)
|
||
VALUES (?, ?, ?, 'Journée entière')
|
||
`, [demandeId, recupTypeId, nombre_heures]);
|
||
|
||
// ACCUMULER DANS LE COMPTEUR
|
||
const [compteur] = await conn.query(`
|
||
SELECT Id, Total, Solde
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [user_id, recupTypeId, currentYear]);
|
||
|
||
if (compteur.length > 0) {
|
||
await conn.query(`
|
||
UPDATE CompteurConges
|
||
SET Total = Total + ?,
|
||
Solde = Solde + ?,
|
||
DerniereMiseAJour = NOW()
|
||
WHERE Id = ?
|
||
`, [nombre_heures, nombre_heures, compteur[0].Id]);
|
||
|
||
console.log(`✅ Compteur mis à jour: ${parseFloat(compteur[0].Solde) + nombre_heures}j`);
|
||
} else {
|
||
await conn.query(`
|
||
INSERT INTO CompteurConges
|
||
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour)
|
||
VALUES (?, ?, ?, ?, ?, 0, NOW())
|
||
`, [user_id, recupTypeId, currentYear, nombre_heures, nombre_heures]);
|
||
|
||
console.log(`✅ Compteur créé: ${nombre_heures}j`);
|
||
}
|
||
|
||
// ENREGISTRER L'ACCUMULATION
|
||
await conn.query(`
|
||
INSERT INTO DeductionDetails
|
||
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
|
||
VALUES (?, ?, ?, 'Accum Récup', ?)
|
||
`, [demandeId, recupTypeId, currentYear, nombre_heures]);
|
||
|
||
// CRÉER NOTIFICATION
|
||
await conn.query(`
|
||
INSERT INTO Notifications
|
||
(CollaborateurADId, Type, Titre, Message, DemandeCongeId, DateCreation, Lu)
|
||
VALUES (?, 'Success', '✅ Récupération enregistrée', ?, ?, NOW(), 0)
|
||
`, [
|
||
user_id,
|
||
`Samedi ${dateFormatted} enregistré : +${nombre_heures}j de récupération`,
|
||
demandeId
|
||
]);
|
||
|
||
await conn.commit();
|
||
conn.release();
|
||
|
||
res.json({
|
||
success: true,
|
||
message: `Samedi ${dateFormatted} enregistré`,
|
||
jours_ajoutes: nombre_heures,
|
||
demande_id: demandeId
|
||
});
|
||
|
||
} catch (error) {
|
||
await conn.rollback();
|
||
if (conn) conn.release();
|
||
console.error('❌ Erreur saisie récup:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur serveur',
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
/**
|
||
* GET /getMesSamedis
|
||
* Récupérer les samedis déjà déclarés
|
||
*/
|
||
app.get('/api/getMesSamedis', async (req, res) => {
|
||
try {
|
||
const { user_id, annee } = req.query;
|
||
|
||
const conn = await pool.getConnection();
|
||
|
||
const [samedis] = await conn.query(`
|
||
SELECT
|
||
dc.Id,
|
||
DATE_FORMAT(dc.DateDebut, '%Y-%m-%d') as date,
|
||
dc.NombreJours as jours,
|
||
dc.Commentaire as commentaire,
|
||
DATE_FORMAT(dc.DateDemande, '%d/%m/%Y à %H:%i') as date_saisie
|
||
FROM DemandeConge dc
|
||
JOIN TypeConge tc ON dc.TypeCongeId = tc.Id
|
||
WHERE dc.CollaborateurADId = ?
|
||
AND tc.Nom = 'Récupération'
|
||
AND YEAR(dc.DateDebut) = ?
|
||
ORDER BY dc.DateDebut DESC
|
||
`, [user_id, annee]);
|
||
|
||
conn.release();
|
||
|
||
res.json({
|
||
success: true,
|
||
samedis: samedis
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('Erreur getMesSamedis:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur serveur',
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
async function checkLeaveBalanceWithAnticipation(conn, collaborateurId, repartition, dateDebut) {
|
||
const dateDebutObj = new Date(dateDebut);
|
||
const currentYear = dateDebutObj.getFullYear();
|
||
const previousYear = currentYear - 1;
|
||
|
||
console.log('\n🔍 === CHECK SOLDES AVEC ANTICIPATION ===');
|
||
console.log(`📅 Date demande: ${dateDebut}`);
|
||
console.log(`📅 Année demande: ${currentYear}`);
|
||
|
||
const verification = [];
|
||
|
||
for (const rep of repartition) {
|
||
const typeCode = rep.TypeConge;
|
||
const joursNecessaires = parseFloat(rep.NombreJours || 0);
|
||
|
||
if (typeCode === 'ABS' || typeCode === 'Formation') {
|
||
continue;
|
||
}
|
||
|
||
const typeName = typeCode === 'CP' ? 'Congé payé' : typeCode === 'RTT' ? 'RTT' : typeCode;
|
||
const [typeRow] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', [typeName]);
|
||
|
||
if (typeRow.length === 0) {
|
||
continue;
|
||
}
|
||
|
||
const typeCongeId = typeRow[0].Id;
|
||
|
||
// ====================================
|
||
// 1️⃣ Récupérer les infos du collaborateur
|
||
// ====================================
|
||
const [collabInfo] = await conn.query(`
|
||
SELECT DateEntree, TypeContrat, role
|
||
FROM CollaborateurAD
|
||
WHERE id = ?
|
||
`, [collaborateurId]);
|
||
|
||
const dateEntree = collabInfo[0]?.DateEntree || null;
|
||
const typeContrat = collabInfo[0]?.TypeContrat || '37h';
|
||
const isApprenti = collabInfo[0]?.role === 'Apprenti';
|
||
|
||
// ====================================
|
||
// 2️⃣ Calculer l'acquisition à la date de la demande
|
||
// ====================================
|
||
let acquisALaDate = 0;
|
||
let budgetAnnuel = 0;
|
||
|
||
if (typeCode === 'CP') {
|
||
acquisALaDate = calculerAcquisitionCP(dateDebutObj, dateEntree);
|
||
budgetAnnuel = 25;
|
||
console.log(`💰 Acquisition CP à la date ${dateDebut}: ${acquisALaDate.toFixed(2)}j`);
|
||
} else if (typeCode === 'RTT' && !isApprenti) {
|
||
const rttData = await calculerAcquisitionRTT(conn, collaborateurId, dateDebutObj);
|
||
acquisALaDate = rttData.acquisition;
|
||
budgetAnnuel = rttData.config.joursAnnuels;
|
||
console.log(`💰 Acquisition RTT à la date ${dateDebut}: ${acquisALaDate.toFixed(2)}j`);
|
||
}
|
||
|
||
// ====================================
|
||
// 3️⃣ Récupérer le report N-1 (CP uniquement)
|
||
// ====================================
|
||
let reporteN1 = 0;
|
||
|
||
if (typeCode === 'CP') {
|
||
const [compteurN1] = await conn.query(`
|
||
SELECT Solde, SoldeReporte
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collaborateurId, typeCongeId, previousYear]);
|
||
|
||
if (compteurN1.length > 0) {
|
||
reporteN1 = parseFloat(compteurN1[0].Solde || 0);
|
||
}
|
||
}
|
||
|
||
// ====================================
|
||
// 4️⃣ Calculer ce qui a déjà été posé (SANS l'anticipé)
|
||
// ====================================
|
||
const [dejaPose] = await conn.query(`
|
||
SELECT COALESCE(SUM(dd.JoursUtilises), 0) as total
|
||
FROM DeductionDetails dd
|
||
JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
|
||
WHERE dc.CollaborateurADId = ?
|
||
AND dd.TypeCongeId = ?
|
||
AND dd.Annee = ?
|
||
AND dd.TypeDeduction NOT IN ('N Anticip', 'N+1 Anticip', 'Accum Récup', 'Accum Recup')
|
||
AND dc.Statut != 'Refusée'
|
||
`, [collaborateurId, typeCongeId, currentYear]);
|
||
|
||
const dejaPoseNormal = parseFloat(dejaPose[0]?.total || 0);
|
||
|
||
// ====================================
|
||
// 5️⃣ Calculer l'anticipé déjà utilisé
|
||
// ====================================
|
||
const [anticipeUtilise] = await conn.query(`
|
||
SELECT COALESCE(SUM(dd.JoursUtilises), 0) as total
|
||
FROM DeductionDetails dd
|
||
JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
|
||
WHERE dc.CollaborateurADId = ?
|
||
AND dd.TypeCongeId = ?
|
||
AND dd.Annee = ?
|
||
AND dd.TypeDeduction = 'N Anticip'
|
||
AND dc.Statut != 'Refusée'
|
||
`, [collaborateurId, typeCongeId, currentYear]);
|
||
|
||
const dejaPoseAnticipe = parseFloat(anticipeUtilise[0]?.total || 0);
|
||
|
||
// ====================================
|
||
// 6️⃣ Calculer l'anticipé disponible
|
||
// ====================================
|
||
const anticipableMax = Math.max(0, budgetAnnuel - acquisALaDate);
|
||
const anticipeDisponible = Math.max(0, anticipableMax - dejaPoseAnticipe);
|
||
|
||
console.log(`💳 Anticipé max possible: ${anticipableMax.toFixed(2)}j`);
|
||
console.log(`💳 Anticipé déjà utilisé: ${dejaPoseAnticipe.toFixed(2)}j`);
|
||
console.log(`💳 Anticipé disponible: ${anticipeDisponible.toFixed(2)}j`);
|
||
|
||
// ====================================
|
||
// 7️⃣ Calculer le solde TOTAL disponible
|
||
// ====================================
|
||
const soldeActuel = Math.max(0, reporteN1 + acquisALaDate - dejaPoseNormal);
|
||
const soldeTotal = soldeActuel + anticipeDisponible;
|
||
|
||
console.log(`📊 Soldes détaillés ${typeCode}:`);
|
||
console.log(` - Report N-1: ${reporteN1.toFixed(2)}j`);
|
||
console.log(` - Acquis à date: ${acquisALaDate.toFixed(2)}j`);
|
||
console.log(` - Déjà posé (normal): ${dejaPoseNormal.toFixed(2)}j`);
|
||
console.log(` - Solde actuel: ${soldeActuel.toFixed(2)}j`);
|
||
console.log(` - Anticipé disponible: ${anticipeDisponible.toFixed(2)}j`);
|
||
console.log(` ✅ TOTAL DISPONIBLE: ${soldeTotal.toFixed(2)}j`);
|
||
|
||
// ====================================
|
||
// 8️⃣ Vérifier la suffisance
|
||
// ====================================
|
||
const suffisant = soldeTotal >= joursNecessaires;
|
||
const deficit = Math.max(0, joursNecessaires - soldeTotal);
|
||
|
||
verification.push({
|
||
type: typeName,
|
||
joursNecessaires,
|
||
reporteN1,
|
||
acquisALaDate,
|
||
dejaPoseNormal,
|
||
dejaPoseAnticipe,
|
||
soldeActuel,
|
||
anticipeDisponible,
|
||
soldeTotal,
|
||
suffisant,
|
||
deficit
|
||
});
|
||
|
||
console.log(`🔍 Vérification ${typeCode}: ${joursNecessaires}j demandés vs ${soldeTotal.toFixed(2)}j disponibles → ${suffisant ? '✅ OK' : '❌ INSUFFISANT'}`);
|
||
}
|
||
|
||
const insuffisants = verification.filter(v => !v.suffisant);
|
||
|
||
return {
|
||
valide: insuffisants.length === 0,
|
||
details: verification,
|
||
insuffisants
|
||
};
|
||
}
|
||
/**
|
||
* Déduit les jours d'un compteur avec gestion de l'anticipation
|
||
* Ordre de déduction : N-1 → N → N Anticip
|
||
*/
|
||
async function deductLeaveBalanceWithAnticipation(conn, collaborateurId, typeCongeId, nombreJours, demandeCongeId, dateDebut) {
|
||
const dateDebutObj = new Date(dateDebut);
|
||
const currentYear = dateDebutObj.getFullYear();
|
||
const previousYear = currentYear - 1;
|
||
|
||
let joursRestants = nombreJours;
|
||
const deductions = [];
|
||
|
||
console.log(`\n💳 === DÉDUCTION AVEC ANTICIPATION ===`);
|
||
console.log(` Collaborateur: ${collaborateurId}`);
|
||
console.log(` Type congé: ${typeCongeId}`);
|
||
console.log(` Jours à déduire: ${nombreJours}j`);
|
||
console.log(` Date début: ${dateDebut}`);
|
||
|
||
// ====================================
|
||
// 1️⃣ Déduire du REPORT N-1 (CP uniquement)
|
||
// ====================================
|
||
const [compteurN1] = await conn.query(`
|
||
SELECT Id, Solde, SoldeReporte
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collaborateurId, typeCongeId, previousYear]);
|
||
|
||
if (compteurN1.length > 0) {
|
||
const soldeN1 = parseFloat(compteurN1[0].Solde || 0);
|
||
const aDeduireN1 = Math.min(soldeN1, joursRestants);
|
||
|
||
if (aDeduireN1 > 0) {
|
||
await conn.query(`
|
||
UPDATE CompteurConges
|
||
SET Solde = GREATEST(0, Solde - ?),
|
||
SoldeReporte = GREATEST(0, SoldeReporte - ?),
|
||
DerniereMiseAJour = NOW()
|
||
WHERE Id = ?
|
||
`, [aDeduireN1, aDeduireN1, compteurN1[0].Id]);
|
||
|
||
await conn.query(`
|
||
INSERT INTO DeductionDetails
|
||
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
|
||
VALUES (?, ?, ?, 'Année N-1', ?)
|
||
`, [demandeCongeId, typeCongeId, previousYear, aDeduireN1]);
|
||
|
||
deductions.push({
|
||
annee: previousYear,
|
||
type: 'Report N-1',
|
||
joursUtilises: aDeduireN1,
|
||
soldeAvant: soldeN1
|
||
});
|
||
|
||
joursRestants -= aDeduireN1;
|
||
console.log(` ✓ Déduit ${aDeduireN1.toFixed(2)}j du report N-1`);
|
||
}
|
||
}
|
||
|
||
// ====================================
|
||
// 2️⃣ Déduire du SOLDE N (acquis actuel)
|
||
// ====================================
|
||
if (joursRestants > 0) {
|
||
const [compteurN] = await conn.query(`
|
||
SELECT Id, Solde, SoldeReporte, Total
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collaborateurId, typeCongeId, currentYear]);
|
||
|
||
if (compteurN.length > 0) {
|
||
const soldeTotal = parseFloat(compteurN[0].Solde || 0);
|
||
const soldeReporte = parseFloat(compteurN[0].SoldeReporte || 0);
|
||
const soldeN = Math.max(0, soldeTotal - soldeReporte); // Solde actuel sans le report
|
||
|
||
const aDeduireN = Math.min(soldeN, joursRestants);
|
||
|
||
if (aDeduireN > 0) {
|
||
await conn.query(`
|
||
UPDATE CompteurConges
|
||
SET Solde = GREATEST(0, Solde - ?),
|
||
DerniereMiseAJour = NOW()
|
||
WHERE Id = ?
|
||
`, [aDeduireN, compteurN[0].Id]);
|
||
|
||
await conn.query(`
|
||
INSERT INTO DeductionDetails
|
||
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
|
||
VALUES (?, ?, ?, 'Année N', ?)
|
||
`, [demandeCongeId, typeCongeId, currentYear, aDeduireN]);
|
||
|
||
deductions.push({
|
||
annee: currentYear,
|
||
type: 'Année N',
|
||
joursUtilises: aDeduireN,
|
||
soldeAvant: soldeN
|
||
});
|
||
|
||
joursRestants -= aDeduireN;
|
||
console.log(` ✓ Déduit ${aDeduireN.toFixed(2)}j du solde N actuel`);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ====================================
|
||
// 3️⃣ Déduire de l'ANTICIPÉ N (ce qui reste à acquérir)
|
||
// ====================================
|
||
if (joursRestants > 0) {
|
||
console.log(` 💳 Il reste ${joursRestants.toFixed(2)}j à déduire → Utilisation de l'anticipé`);
|
||
|
||
// Récupérer les infos pour calculer l'anticipé disponible
|
||
const [collabInfo] = await conn.query(`
|
||
SELECT DateEntree, TypeContrat, role
|
||
FROM CollaborateurAD
|
||
WHERE id = ?
|
||
`, [collaborateurId]);
|
||
|
||
const dateEntree = collabInfo[0]?.DateEntree || null;
|
||
const typeContrat = collabInfo[0]?.TypeContrat || '37h';
|
||
const isApprenti = collabInfo[0]?.role === 'Apprenti';
|
||
|
||
// Déterminer le type de congé
|
||
const [typeInfo] = await conn.query('SELECT Nom FROM TypeConge WHERE Id = ?', [typeCongeId]);
|
||
const typeNom = typeInfo[0]?.Nom || '';
|
||
|
||
let acquisALaDate = 0;
|
||
let budgetAnnuel = 0;
|
||
|
||
if (typeNom === 'Congé payé') {
|
||
acquisALaDate = calculerAcquisitionCP(dateDebutObj, dateEntree);
|
||
budgetAnnuel = 25;
|
||
} else if (typeNom === 'RTT' && !isApprenti) {
|
||
const rttData = await calculerAcquisitionRTT(conn, collaborateurId, dateDebutObj);
|
||
acquisALaDate = rttData.acquisition;
|
||
budgetAnnuel = rttData.config.joursAnnuels;
|
||
}
|
||
|
||
// Calculer l'anticipé disponible
|
||
const anticipableMax = Math.max(0, budgetAnnuel - acquisALaDate);
|
||
|
||
// Vérifier combien a déjà été pris en anticipé
|
||
const [anticipeUtilise] = await conn.query(`
|
||
SELECT COALESCE(SUM(dd.JoursUtilises), 0) as total
|
||
FROM DeductionDetails dd
|
||
JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
|
||
WHERE dc.CollaborateurADId = ?
|
||
AND dd.TypeCongeId = ?
|
||
AND dd.Annee = ?
|
||
AND dd.TypeDeduction = 'N Anticip'
|
||
AND dc.Statut != 'Refusée'
|
||
AND dc.Id != ?
|
||
`, [collaborateurId, typeCongeId, currentYear, demandeCongeId]);
|
||
|
||
const dejaPrisAnticipe = parseFloat(anticipeUtilise[0]?.total || 0);
|
||
const anticipeDisponible = Math.max(0, anticipableMax - dejaPrisAnticipe);
|
||
|
||
console.log(` 💳 Anticipé max: ${anticipableMax.toFixed(2)}j`);
|
||
console.log(` 💳 Déjà pris: ${dejaPrisAnticipe.toFixed(2)}j`);
|
||
console.log(` 💳 Disponible: ${anticipeDisponible.toFixed(2)}j`);
|
||
|
||
const aDeduireAnticipe = Math.min(anticipeDisponible, joursRestants);
|
||
|
||
if (aDeduireAnticipe > 0) {
|
||
// Enregistrer la déduction anticipée
|
||
await conn.query(`
|
||
INSERT INTO DeductionDetails
|
||
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
|
||
VALUES (?, ?, ?, 'N Anticip', ?)
|
||
`, [demandeCongeId, typeCongeId, currentYear, aDeduireAnticipe]);
|
||
|
||
// Mettre à jour SoldeAnticipe dans CompteurConges
|
||
await conn.query(`
|
||
UPDATE CompteurConges
|
||
SET SoldeAnticipe = GREATEST(0, ? - ?),
|
||
DerniereMiseAJour = NOW()
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [anticipeDisponible, aDeduireAnticipe, collaborateurId, typeCongeId, currentYear]);
|
||
|
||
deductions.push({
|
||
annee: currentYear,
|
||
type: 'N Anticip',
|
||
joursUtilises: aDeduireAnticipe,
|
||
soldeAvant: anticipeDisponible
|
||
});
|
||
|
||
joursRestants -= aDeduireAnticipe;
|
||
console.log(` ✓ Déduit ${aDeduireAnticipe.toFixed(2)}j de l'anticipé N`);
|
||
} else if (joursRestants > 0) {
|
||
console.error(` ❌ Impossible de déduire ${joursRestants.toFixed(2)}j : anticipé épuisé !`);
|
||
}
|
||
}
|
||
|
||
console.log(` ✅ Déduction terminée - Total déduit: ${(nombreJours - joursRestants).toFixed(2)}j\n`);
|
||
|
||
return {
|
||
success: joursRestants === 0,
|
||
joursDeduitsTotal: nombreJours - joursRestants,
|
||
joursNonDeduits: joursRestants,
|
||
details: deductions
|
||
};
|
||
}
|
||
|
||
|
||
app.post('/api/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), async (req, res) => {
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
await conn.beginTransaction();
|
||
|
||
const currentYear = new Date().getFullYear();
|
||
|
||
// ✅ Récupérer les fichiers uploadés
|
||
const uploadedFiles = req.files || [];
|
||
console.log('📎 Fichiers médicaux reçus:', uploadedFiles.length);
|
||
|
||
// ✅ Les données arrivent différemment avec FormData
|
||
const { DateDebut, DateFin, NombreJours, Email, Nom, Commentaire, statut } = req.body;
|
||
|
||
// ✅ Parser la répartition (elle arrive en string depuis FormData)
|
||
const Repartition = JSON.parse(req.body.Repartition || '[]');
|
||
|
||
if (!DateDebut || !DateFin || !Repartition || !Email || !Nom) {
|
||
uploadedFiles.forEach(file => {
|
||
if (fs.existsSync(file.path)) {
|
||
fs.unlinkSync(file.path);
|
||
}
|
||
});
|
||
return res.json({ success: false, message: 'Données manquantes' });
|
||
}
|
||
|
||
// ✅ Validation : Si arrêt maladie, il faut au moins 1 fichier
|
||
const hasABS = Repartition.some(r => r.TypeConge === 'ABS');
|
||
if (hasABS && uploadedFiles.length === 0) {
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.json({
|
||
success: false,
|
||
message: 'Un justificatif médical est obligatoire pour un arrêt maladie'
|
||
});
|
||
}
|
||
|
||
// ⭐ VALIDATION DE LA RÉPARTITION
|
||
console.log('\n📥 === SOUMISSION DEMANDE CONGÉ ===');
|
||
console.log('Email:', Email);
|
||
console.log('Période:', DateDebut, '→', DateFin);
|
||
console.log('Nombre de jours total:', NombreJours);
|
||
console.log('Répartition reçue:', JSON.stringify(Repartition, null, 2));
|
||
|
||
// ⭐ Ne compter que CP, RTT ET RÉCUP dans la répartition
|
||
const sommeRepartition = Repartition.reduce((sum, r) => {
|
||
if (r.TypeConge === 'CP' || r.TypeConge === 'RTT' || r.TypeConge === 'Récup') {
|
||
return sum + parseFloat(r.NombreJours || 0);
|
||
}
|
||
return sum;
|
||
}, 0);
|
||
|
||
console.log('Somme répartition CP+RTT+Récup:', sommeRepartition.toFixed(2));
|
||
|
||
// ⭐ VALIDATION : La somme doit correspondre au total
|
||
const hasCountableLeave = Repartition.some(r =>
|
||
r.TypeConge === 'CP' || r.TypeConge === 'RTT' || r.TypeConge === 'Récup'
|
||
);
|
||
|
||
if (hasCountableLeave && Math.abs(sommeRepartition - NombreJours) > 0.01) {
|
||
console.error('❌ ERREUR : Répartition incohérente !');
|
||
|
||
uploadedFiles.forEach(file => {
|
||
if (fs.existsSync(file.path)) {
|
||
fs.unlinkSync(file.path);
|
||
}
|
||
});
|
||
|
||
await conn.rollback();
|
||
conn.release();
|
||
|
||
return res.json({
|
||
success: false,
|
||
message: `Erreur de répartition : la somme (${sommeRepartition.toFixed(2)}j) ne correspond pas au total (${NombreJours}j)`
|
||
});
|
||
}
|
||
|
||
console.log('✅ Validation répartition OK');
|
||
|
||
// ⭐ Récup n'est PAS une demande auto-validée
|
||
const isFormationOnly = Repartition.length === 1 && Repartition[0].TypeConge === 'Formation';
|
||
const statutDemande = statut || (isFormationOnly ? 'Validée' : 'En attente');
|
||
|
||
console.log('🔍 Type de demande:', { isFormationOnly, statut: statutDemande });
|
||
|
||
const [collabAD] = await conn.query('SELECT id, CampusId FROM CollaborateurAD WHERE email = ? LIMIT 1', [Email]);
|
||
const isAD = collabAD.length > 0;
|
||
const collaborateurId = isAD ? collabAD[0].id : null;
|
||
|
||
let employeeId = null;
|
||
if (!isAD) {
|
||
const [user] = await conn.query('SELECT ID FROM Users WHERE Email = ? LIMIT 1', [Email]);
|
||
if (user.length === 0) {
|
||
uploadedFiles.forEach(file => {
|
||
if (fs.existsSync(file.path)) {
|
||
fs.unlinkSync(file.path);
|
||
}
|
||
});
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.json({ success: false, message: 'Utilisateur non trouvé' });
|
||
}
|
||
employeeId = user[0].ID;
|
||
}
|
||
|
||
// ========================================
|
||
// ÉTAPE 1 : Vérification des soldes AVANT tout (MODE MIXTE AVEC ANTICIPATION N+1)
|
||
// ========================================
|
||
if (isAD && collaborateurId && !isFormationOnly) {
|
||
console.log('\n🔍 Vérification des soldes en mode mixte avec anticipation...');
|
||
console.log('Date début:', DateDebut);
|
||
|
||
const [userRole] = await conn.query('SELECT role FROM CollaborateurAD WHERE id = ?', [collaborateurId]);
|
||
const isApprenti = userRole.length > 0 && userRole[0].role === 'Apprenti';
|
||
|
||
// ⭐ CORRECTION : Passer la date de début pour détecter N+1
|
||
// ✅ APRÈS (avec anticipation)
|
||
const checkResult = await checkLeaveBalanceWithAnticipation(
|
||
conn,
|
||
collaborateurId,
|
||
Repartition,
|
||
DateDebut
|
||
);
|
||
|
||
// Adapter le format de la réponse
|
||
if (!checkResult.valide) {
|
||
uploadedFiles.forEach(file => {
|
||
if (fs.existsSync(file.path)) fs.unlinkSync(file.path);
|
||
});
|
||
await conn.rollback();
|
||
conn.release();
|
||
|
||
// Construire le message d'erreur
|
||
const messagesErreur = checkResult.insuffisants.map(ins => {
|
||
return `${ins.type}: ${ins.joursNecessaires}j demandés mais seulement ${ins.soldeTotal.toFixed(2)}j disponibles (déficit: ${ins.deficit.toFixed(2)}j)`;
|
||
}).join('\n');
|
||
|
||
return res.json({
|
||
success: false,
|
||
message: `❌ Solde(s) insuffisant(s):\n${messagesErreur}`,
|
||
details: checkResult.details,
|
||
insuffisants: checkResult.insuffisants
|
||
});
|
||
}
|
||
|
||
|
||
console.log('✅ Tous les soldes sont suffisants (incluant anticipation si nécessaire)\n');
|
||
}
|
||
|
||
// ========================================
|
||
// ÉTAPE 2 : CRÉER LA DEMANDE
|
||
// ========================================
|
||
console.log('\n📝 Création de la demande...');
|
||
|
||
const typeIds = [];
|
||
for (const rep of Repartition) {
|
||
const code = rep.TypeConge;
|
||
|
||
// Ne pas inclure ABS et Formation dans les typeIds principaux
|
||
if (code === 'ABS' || code === 'Formation') {
|
||
continue;
|
||
}
|
||
|
||
const name = code === 'CP' ? 'Congé payé' :
|
||
code === 'RTT' ? 'RTT' :
|
||
code === 'Récup' ? 'Récupération' : code;
|
||
|
||
const [typeRow] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', [name]);
|
||
if (typeRow.length > 0) typeIds.push(typeRow[0].Id);
|
||
}
|
||
|
||
// Si aucun type CP/RTT/Récup, prendre le premier type de la répartition
|
||
if (typeIds.length === 0) {
|
||
const firstType = Repartition[0]?.TypeConge;
|
||
const name = firstType === 'Formation' ? 'Formation' :
|
||
firstType === 'ABS' ? 'Congé maladie' : 'Congé payé';
|
||
|
||
const [typeRow] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', [name]);
|
||
if (typeRow.length > 0) {
|
||
typeIds.push(typeRow[0].Id);
|
||
} else {
|
||
uploadedFiles.forEach(file => {
|
||
if (fs.existsSync(file.path)) fs.unlinkSync(file.path);
|
||
});
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.json({ success: false, message: 'Aucun type de congé valide' });
|
||
}
|
||
}
|
||
|
||
const typeCongeIdCsv = typeIds.join(',');
|
||
const currentDate = new Date().toISOString().slice(0, 19).replace('T', ' ');
|
||
|
||
// ✅ CRÉER LA DEMANDE
|
||
const [result] = await conn.query(
|
||
`INSERT INTO DemandeConge
|
||
(EmployeeId, CollaborateurADId, DateDebut, DateFin, TypeCongeId, Statut, DateDemande, Commentaire, Validateur, NombreJours)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||
[isAD ? 0 : employeeId, collaborateurId, DateDebut, DateFin, typeCongeIdCsv, statutDemande, currentDate, Commentaire || '', '', NombreJours]
|
||
);
|
||
|
||
const demandeId = result.insertId;
|
||
console.log(`✅ Demande créée avec ID ${demandeId} - Statut: ${statutDemande}`);
|
||
|
||
// ========================================
|
||
// ÉTAPE 3 : Sauvegarder les fichiers médicaux
|
||
// ========================================
|
||
if (uploadedFiles.length > 0) {
|
||
console.log('\n📎 Sauvegarde des fichiers médicaux...');
|
||
for (const file of uploadedFiles) {
|
||
await conn.query(
|
||
`INSERT INTO DocumentsMedicaux
|
||
(DemandeCongeId, NomFichier, CheminFichier, TypeMime, TailleFichier, DateUpload)
|
||
VALUES (?, ?, ?, ?, ?, NOW())`,
|
||
[demandeId, file.originalname, file.path, file.mimetype, file.size]
|
||
);
|
||
console.log(` ✓ ${file.originalname}`);
|
||
}
|
||
}
|
||
|
||
// ========================================
|
||
// ÉTAPE 4 : Sauvegarder la répartition
|
||
// ========================================
|
||
console.log('\n📊 Sauvegarde de la répartition en base...');
|
||
for (const rep of Repartition) {
|
||
const code = rep.TypeConge;
|
||
const name = code === 'CP' ? 'Congé payé' :
|
||
code === 'RTT' ? 'RTT' :
|
||
code === 'ABS' ? 'Congé maladie' :
|
||
code === 'Formation' ? 'Formation' :
|
||
code === 'Récup' ? 'Récupération' : code;
|
||
|
||
const [typeRow] = await conn.query(
|
||
'SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1',
|
||
[name]
|
||
);
|
||
|
||
if (typeRow.length > 0) {
|
||
await conn.query(
|
||
`INSERT INTO DemandeCongeType
|
||
(DemandeCongeId, TypeCongeId, NombreJours, PeriodeJournee)
|
||
VALUES (?, ?, ?, ?)`,
|
||
[
|
||
demandeId,
|
||
typeRow[0].Id,
|
||
rep.NombreJours,
|
||
rep.PeriodeJournee || 'Journée entière'
|
||
]
|
||
);
|
||
console.log(` ✓ ${name}: ${rep.NombreJours}j (${rep.PeriodeJournee || 'Journée entière'})`);
|
||
}
|
||
}
|
||
|
||
// ========================================
|
||
// ÉTAPE 5 : Déduction des compteurs CP/RTT/RÉCUP (AVEC ANTICIPATION N+1)
|
||
// ========================================
|
||
if (isAD && collaborateurId && !isFormationOnly) {
|
||
console.log('\n📉 Déduction des compteurs (avec anticipation N+1)...');
|
||
|
||
for (const rep of Repartition) {
|
||
if (rep.TypeConge === 'ABS' || rep.TypeConge === 'Formation') {
|
||
console.log(` ⏩ ${rep.TypeConge} ignoré (pas de déduction)`);
|
||
continue;
|
||
}
|
||
|
||
// ⭐ TRAITEMENT SPÉCIAL POUR RÉCUP
|
||
if (rep.TypeConge === 'Récup') {
|
||
const [recupType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Récupération']);
|
||
|
||
if (recupType.length > 0) {
|
||
await conn.query(`
|
||
UPDATE CompteurConges
|
||
SET Solde = GREATEST(0, Solde - ?),
|
||
DerniereMiseAJour = NOW()
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [rep.NombreJours, collaborateurId, recupType[0].Id, currentYear]);
|
||
|
||
await conn.query(`
|
||
INSERT INTO DeductionDetails
|
||
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
|
||
VALUES (?, ?, ?, 'Récup Posée', ?)
|
||
`, [demandeId, recupType[0].Id, currentYear, rep.NombreJours]);
|
||
|
||
console.log(` ✓ Récup: ${rep.NombreJours}j déduits`);
|
||
}
|
||
continue;
|
||
}
|
||
|
||
// ⭐ CP et RTT : AVEC ANTICIPATION N+1
|
||
const name = rep.TypeConge === 'CP' ? 'Congé payé' : 'RTT';
|
||
const [typeRow] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', [name]);
|
||
|
||
if (typeRow.length > 0) {
|
||
const result = await deductLeaveBalanceWithAnticipation(
|
||
conn,
|
||
collaborateurId,
|
||
typeRow[0].Id,
|
||
rep.NombreJours,
|
||
demandeId,
|
||
DateDebut
|
||
);
|
||
|
||
console.log(` ✓ ${name}: ${rep.NombreJours}j déduits`);
|
||
if (result.details && result.details.length > 0) {
|
||
result.details.forEach(d => {
|
||
console.log(` - ${d.type} (${d.annee}): ${d.joursUtilises}j`);
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
await updateSoldeAnticipe(conn, collaborateurId);
|
||
console.log('✅ Déductions terminées\n');
|
||
}
|
||
|
||
// ========================================
|
||
// ÉTAPE 6 : Notifications (Formation uniquement)
|
||
// ========================================
|
||
const dateDebut = new Date(DateDebut).toLocaleDateString('fr-FR');
|
||
const dateFin = new Date(DateFin).toLocaleDateString('fr-FR');
|
||
const datesPeriode = dateDebut === dateFin ? dateDebut : `du ${dateDebut} au ${dateFin}`;
|
||
|
||
if (isFormationOnly && isAD && collaborateurId) {
|
||
await conn.query(
|
||
`INSERT INTO Notifications (CollaborateurADId, Type, Titre, Message, DemandeCongeId, DateCreation, Lu)
|
||
VALUES (?, ?, ?, ?, ?, NOW(), 0)`,
|
||
[
|
||
collaborateurId,
|
||
'Success',
|
||
'✅ Formation validée automatiquement',
|
||
`Votre période de formation ${datesPeriode} a été validée automatiquement.`,
|
||
demandeId
|
||
]
|
||
);
|
||
console.log('\n📬 Notification formation créée');
|
||
}
|
||
|
||
// ========================================
|
||
// ÉTAPE 7 : Récupérer les managers
|
||
// ========================================
|
||
let managers = [];
|
||
if (isAD) {
|
||
const [rows] = await conn.query(
|
||
`SELECT c.email FROM HierarchieValidationAD hv
|
||
JOIN CollaborateurAD c ON hv.SuperieurId = c.id
|
||
WHERE hv.CollaborateurId = ?`,
|
||
[collaborateurId]
|
||
);
|
||
managers = rows.map(r => r.email);
|
||
}
|
||
|
||
await conn.commit();
|
||
console.log('\n🎉 Transaction validée\n');
|
||
|
||
// ========================================
|
||
// ÉTAPE 8 : Notifier les clients SSE
|
||
// ========================================
|
||
if (isFormationOnly && isAD && collaborateurId) {
|
||
notifyCollabClients({
|
||
type: 'demande-validated',
|
||
demandeId: parseInt(demandeId),
|
||
statut: 'Validée',
|
||
timestamp: new Date().toISOString()
|
||
}, collaborateurId);
|
||
}
|
||
|
||
// ========================================
|
||
// ENVOI DES EMAILS
|
||
// ========================================
|
||
const accessToken = await getGraphToken();
|
||
if (accessToken) {
|
||
const fromEmail = 'gtanoreply@ensup.eu';
|
||
const typesConges = Repartition.map(rep => {
|
||
const typeNom = rep.TypeConge === 'CP' ? 'Congé payé' :
|
||
rep.TypeConge === 'RTT' ? 'RTT' :
|
||
rep.TypeConge === 'ABS' ? 'Congé maladie' :
|
||
rep.TypeConge === 'Formation' ? 'Formation' :
|
||
rep.TypeConge === 'Récup' ? 'Récupération' : rep.TypeConge;
|
||
return `${typeNom}: ${rep.NombreJours}j`;
|
||
}).join(' | ');
|
||
|
||
if (isFormationOnly) {
|
||
// Email formation
|
||
const subjectCollab = '✅ Formation enregistrée et validée';
|
||
const bodyCollab = `
|
||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||
<div style="background-color: #10b981; color: white; padding: 20px;">
|
||
<h2 style="margin: 0;">✅ Formation validée</h2>
|
||
</div>
|
||
<div style="padding: 20px;">
|
||
<p>Bonjour <strong>${Nom}</strong>,</p>
|
||
<p>Votre période de formation a été automatiquement validée.</p>
|
||
<p><strong>Période :</strong> ${datesPeriode}</p>
|
||
<p><strong>Durée :</strong> ${NombreJours} jour(s)</p>
|
||
</div>
|
||
</div>
|
||
`;
|
||
try {
|
||
await sendMailGraph(accessToken, fromEmail, Email, subjectCollab, bodyCollab);
|
||
} catch (mailError) {
|
||
console.error('❌ Erreur email:', mailError.message);
|
||
}
|
||
} else {
|
||
// ⭐ EMAIL NORMAL (incluant Récup)
|
||
const subjectCollab = '✅ Confirmation de réception de votre demande de congé';
|
||
const bodyCollab = `
|
||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||
<div style="background-color: #3b82f6; color: white; padding: 20px;">
|
||
<h2 style="margin: 0;">✅ Demande enregistrée</h2>
|
||
</div>
|
||
<div style="padding: 20px;">
|
||
<p>Bonjour <strong>${Nom}</strong>,</p>
|
||
<p>Votre demande de congé a bien été enregistrée.</p>
|
||
<p><strong>Type :</strong> ${typesConges}</p>
|
||
<p><strong>Période :</strong> ${datesPeriode}</p>
|
||
<p><strong>Durée :</strong> ${NombreJours} jour(s)</p>
|
||
</div>
|
||
</div>
|
||
`;
|
||
try {
|
||
await sendMailGraph(accessToken, fromEmail, Email, subjectCollab, bodyCollab);
|
||
} catch (mailError) {
|
||
console.error('❌ Erreur email:', mailError.message);
|
||
}
|
||
|
||
for (const managerEmail of managers) {
|
||
const subjectManager = `📋 Nouvelle demande de congé - ${Nom}`;
|
||
const bodyManager = `
|
||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||
<div style="background-color: #f59e0b; color: white; padding: 20px;">
|
||
<h2 style="margin: 0;">📋 Nouvelle demande</h2>
|
||
</div>
|
||
<div style="padding: 20px;">
|
||
<p><strong>${Nom}</strong> a soumis une nouvelle demande.</p>
|
||
<p><strong>Type :</strong> ${typesConges}</p>
|
||
<p><strong>Période :</strong> ${datesPeriode}</p>
|
||
</div>
|
||
</div>
|
||
`;
|
||
try {
|
||
await sendMailGraph(accessToken, fromEmail, managerEmail, subjectManager, bodyManager);
|
||
} catch (mailError) {
|
||
console.error('❌ Erreur email manager:', mailError.message);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
res.json({
|
||
success: true,
|
||
message: isFormationOnly ? 'Formation enregistrée et validée automatiquement' : 'Demande soumise',
|
||
request_id: demandeId,
|
||
managers,
|
||
auto_validated: isFormationOnly,
|
||
files_uploaded: uploadedFiles.length
|
||
});
|
||
|
||
} catch (error) {
|
||
await conn.rollback();
|
||
|
||
if (req.files) {
|
||
req.files.forEach(file => {
|
||
if (fs.existsSync(file.path)) {
|
||
fs.unlinkSync(file.path);
|
||
console.log(`🗑️ Fichier supprimé: ${file.originalname}`);
|
||
}
|
||
});
|
||
}
|
||
|
||
console.error('\n❌ ERREUR submitLeaveRequest:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur serveur',
|
||
error: error.message
|
||
});
|
||
} finally {
|
||
conn.release();
|
||
}
|
||
});
|
||
|
||
|
||
app.get('/api/download-medical/:documentId', async (req, res) => {
|
||
try {
|
||
const { documentId } = req.params;
|
||
const conn = await pool.getConnection();
|
||
|
||
const [docs] = await conn.query(
|
||
'SELECT * FROM DocumentsMedicaux WHERE Id = ?',
|
||
[documentId]
|
||
);
|
||
|
||
conn.release();
|
||
|
||
if (docs.length === 0) {
|
||
return res.status(404).json({ success: false, message: 'Document non trouvé' });
|
||
}
|
||
|
||
const doc = docs[0];
|
||
|
||
if (!fs.existsSync(doc.CheminFichier)) {
|
||
return res.status(404).json({ success: false, message: 'Fichier introuvable' });
|
||
}
|
||
|
||
res.download(doc.CheminFichier, doc.NomFichier);
|
||
|
||
} catch (error) {
|
||
console.error('Erreur téléchargement:', error);
|
||
res.status(500).json({ success: false, message: 'Erreur serveur' });
|
||
}
|
||
});
|
||
|
||
// Récupérer les documents d'une demande
|
||
app.get('/api/medical-documents/:demandeId', async (req, res) => {
|
||
try {
|
||
const { demandeId } = req.params;
|
||
const conn = await pool.getConnection();
|
||
|
||
const [docs] = await conn.query(
|
||
`SELECT Id, NomFichier, TypeMime, TailleFichier, DateUpload
|
||
FROM DocumentsMedicaux
|
||
WHERE DemandeCongeId = ?
|
||
ORDER BY DateUpload DESC`,
|
||
[demandeId]
|
||
);
|
||
|
||
conn.release();
|
||
|
||
res.json({
|
||
success: true,
|
||
documents: docs.map(doc => ({
|
||
id: doc.Id,
|
||
nom: doc.NomFichier,
|
||
type: doc.TypeMime,
|
||
taille: doc.TailleFichier,
|
||
date: doc.DateUpload,
|
||
downloadUrl: `/download-medical/${doc.Id}`
|
||
}))
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('Erreur récupération documents:', error);
|
||
res.status(500).json({ success: false, message: 'Erreur serveur' });
|
||
}
|
||
});
|
||
app.post('/api/validateRequest', async (req, res) => {
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
await conn.beginTransaction();
|
||
const { request_id, action, validator_id, comment } = req.body;
|
||
|
||
console.log(`\n🔍 Validation demande #${request_id} - Action: ${action}`);
|
||
|
||
if (!request_id || !action || !validator_id) {
|
||
return res.json({ success: false, message: 'Données manquantes' });
|
||
}
|
||
|
||
const [validator] = await conn.query('SELECT Id, prenom, nom, email, CampusId FROM CollaborateurAD WHERE Id = ?', [validator_id]);
|
||
|
||
if (validator.length === 0) {
|
||
throw new Error('Validateur introuvable');
|
||
}
|
||
|
||
// Récupérer les informations de la demande
|
||
const [requests] = await conn.query(
|
||
`SELECT
|
||
dc.Id,
|
||
dc.CollaborateurADId,
|
||
dc.TypeCongeId,
|
||
dc.NombreJours,
|
||
dc.DateDebut,
|
||
dc.DateFin,
|
||
dc.Commentaire,
|
||
dc.Statut,
|
||
ca.prenom,
|
||
ca.nom,
|
||
ca.email as collaborateur_email,
|
||
tc.Nom as TypeConge
|
||
FROM DemandeConge dc
|
||
JOIN TypeConge tc ON dc.TypeCongeId = tc.Id
|
||
LEFT JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.Id
|
||
WHERE dc.Id = ?
|
||
LIMIT 1`,
|
||
[request_id]
|
||
);
|
||
|
||
if (requests.length === 0) {
|
||
throw new Error('Demande non trouvée');
|
||
}
|
||
|
||
const request = requests[0];
|
||
|
||
// ⭐ DÉBOGAGE : Afficher TOUTES les données
|
||
console.log('\n=== DONNÉES RÉCUPÉRÉES ===');
|
||
console.log('request:', JSON.stringify(request, null, 2));
|
||
console.log('validator:', JSON.stringify(validator[0], null, 2));
|
||
console.log('========================\n');
|
||
|
||
if (request.Statut !== 'En attente') {
|
||
throw new Error(`La demande a déjà été traitée (Statut: ${request.Statut})`);
|
||
}
|
||
|
||
const newStatus = action === 'approve' ? 'Validée' : 'Refusée';
|
||
|
||
if (action === 'reject' && request.CollaborateurADId) {
|
||
console.log(`\n🔄 REFUS - Restauration des soldes...`);
|
||
const restoration = await restoreLeaveBalance(conn, request_id, request.CollaborateurADId);
|
||
console.log('Restauration:', restoration);
|
||
}
|
||
|
||
await conn.query(
|
||
`UPDATE DemandeConge SET Statut = ?, ValidateurId = ?, ValidateurADId = ?, DateValidation = NOW(), CommentaireValidation = ? WHERE Id = ?`,
|
||
[newStatus, validator_id, validator_id, comment || '', request_id]
|
||
);
|
||
|
||
const notifTitle = action === 'approve' ? 'Demande approuvée ✅' : 'Demande refusée ❌';
|
||
let notifMessage = `Votre demande a été ${action === 'approve' ? 'approuvée' : 'refusée'}`;
|
||
if (comment) notifMessage += ` (Commentaire: ${comment})`;
|
||
const notifType = action === 'approve' ? 'Success' : 'Error';
|
||
|
||
await conn.query(
|
||
'INSERT INTO Notifications (CollaborateurADId, Titre, Message, Type, DemandeCongeId, DateCreation, lu) VALUES (?, ?, ?, ?, ?, ?, 0)',
|
||
[request.CollaborateurADId, notifTitle, notifMessage, notifType, request_id, nowFR()]
|
||
);
|
||
|
||
await conn.commit();
|
||
|
||
// ⭐ TEST EMAIL
|
||
// ⭐ ENVOI EMAIL AVEC TEMPLATE PROFESSIONNEL
|
||
console.log('\n📧 === TENTATIVE ENVOI EMAIL ===');
|
||
console.log('1. Récupération token...');
|
||
|
||
const accessToken = await getGraphToken();
|
||
console.log('2. Token obtenu ?', accessToken ? 'OUI' : 'NON');
|
||
|
||
if (accessToken && request.collaborateur_email) {
|
||
const fromEmail = 'gtanoreply@ensup.eu';
|
||
const collaborateurNom = `${request.prenom} ${request.nom}`;
|
||
const validateurNom = `${validator[0].prenom} ${validator[0].nom}`;
|
||
|
||
console.log('3. Préparation email professionnel...');
|
||
console.log(' De:', fromEmail);
|
||
console.log(' À:', request.collaborateur_email);
|
||
console.log(' Collaborateur:', collaborateurNom);
|
||
console.log(' Validateur:', validateurNom);
|
||
|
||
const dateDebut = new Date(request.DateDebut).toLocaleDateString('fr-FR');
|
||
const dateFin = new Date(request.DateFin).toLocaleDateString('fr-FR');
|
||
const datesPeriode = dateDebut === dateFin ? dateDebut : `du ${dateDebut} au ${dateFin}`;
|
||
|
||
const subject = action === 'approve'
|
||
? '✅ Votre demande de congé a été approuvée'
|
||
: '❌ Votre demande de congé a été refusée';
|
||
|
||
const body = action === 'approve'
|
||
? `<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||
<div style="background-color: #10b981; color: white; padding: 20px; border-radius: 8px 8px 0 0;">
|
||
<h2 style="margin: 0;">✅ Demande approuvée</h2>
|
||
</div>
|
||
<div style="background-color: #f9fafb; padding: 20px; border: 1px solid #e5e7eb;">
|
||
<p style="font-size: 16px;">Bonjour <strong>${collaborateurNom}</strong>,</p>
|
||
<p>Votre demande de congé a été <strong style="color: #10b981;">approuvée</strong> par ${validateurNom}.</p>
|
||
<div style="background-color: white; border-left: 4px solid #10b981; padding: 15px; margin: 20px 0;">
|
||
<p><strong>Type :</strong> ${request.TypeConge}</p>
|
||
<p><strong>Période :</strong> ${datesPeriode}</p>
|
||
<p><strong>Durée :</strong> ${request.NombreJours} jour(s)</p>
|
||
${comment ? `<p><strong>Commentaire :</strong> ${comment}</p>` : ''}
|
||
</div>
|
||
<p style="color: #6b7280;">Vous pouvez consulter votre demande dans votre espace personnel.</p>
|
||
</div>
|
||
</div>`
|
||
: `<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||
<div style="background-color: #ef4444; color: white; padding: 20px; border-radius: 8px 8px 0 0;">
|
||
<h2 style="margin: 0;">❌ Demande refusée</h2>
|
||
</div>
|
||
<div style="background-color: #f9fafb; padding: 20px; border: 1px solid #e5e7eb;">
|
||
<p style="font-size: 16px;">Bonjour <strong>${collaborateurNom}</strong>,</p>
|
||
<p>Votre demande de congé a été <strong style="color: #ef4444;">refusée</strong> par ${validateurNom}.</p>
|
||
<div style="background-color: white; border-left: 4px solid #ef4444; padding: 15px; margin: 20px 0;">
|
||
<p><strong>Type :</strong> ${request.TypeConge}</p>
|
||
<p><strong>Période :</strong> ${datesPeriode}</p>
|
||
<p><strong>Durée :</strong> ${request.NombreJours} jour(s)</p>
|
||
${comment ? `<p><strong>Motif du refus :</strong> ${comment}</p>` : ''}
|
||
</div>
|
||
<p style="color: #6b7280;">Pour plus d'informations, contactez ${validateurNom}.</p>
|
||
</div>
|
||
</div>`;
|
||
|
||
console.log('4. Sujet:', subject);
|
||
console.log('5. Appel sendMailGraph...');
|
||
|
||
try {
|
||
await sendMailGraph(
|
||
accessToken,
|
||
fromEmail,
|
||
request.collaborateur_email,
|
||
subject,
|
||
body
|
||
);
|
||
console.log('✅✅✅ EMAIL ENVOYÉ AVEC SUCCÈS ! ✅✅✅');
|
||
} catch (mailError) {
|
||
console.error('❌❌❌ ERREUR ENVOI EMAIL ❌❌❌');
|
||
console.error('Message:', mailError.message);
|
||
console.error('Stack:', mailError.stack);
|
||
}
|
||
} else {
|
||
if (!accessToken) console.error('❌ Token manquant');
|
||
if (!request.collaborateur_email) console.error('❌ Email collaborateur manquant');
|
||
}
|
||
console.log('=== FIN ENVOI EMAIL ===\n');
|
||
|
||
// Notifier les clients SSE locaux
|
||
notifyCollabClients({
|
||
type: 'demande-validated',
|
||
demandeId: parseInt(request_id),
|
||
statut: newStatus,
|
||
timestamp: new Date().toISOString()
|
||
}, request.CollaborateurADId);
|
||
|
||
notifyCollabClients({
|
||
type: 'demande-list-updated',
|
||
action: 'validation',
|
||
demandeId: parseInt(request_id),
|
||
timestamp: new Date().toISOString()
|
||
});
|
||
|
||
// Envoyer webhook au serveur RH
|
||
try {
|
||
await webhookManager.sendWebhook(
|
||
WEBHOOKS.RH_URL,
|
||
EVENTS.DEMANDE_VALIDATED,
|
||
{
|
||
demandeId: parseInt(request_id),
|
||
statut: newStatus,
|
||
collaborateurId: request.CollaborateurADId,
|
||
validateurId: validator_id,
|
||
commentaire: comment
|
||
}
|
||
);
|
||
console.log('✅ Webhook envoyé au serveur RH');
|
||
} catch (webhookError) {
|
||
console.error('❌ Erreur envoi webhook (non bloquant):', webhookError.message);
|
||
}
|
||
|
||
res.json({
|
||
success: true,
|
||
message: `Demande ${action === 'approve' ? 'approuvée' : 'refusée'}`,
|
||
new_status: newStatus
|
||
});
|
||
|
||
console.log(`✅ Demande ${request_id} ${newStatus}\n`);
|
||
|
||
} catch (error) {
|
||
await conn.rollback();
|
||
console.error('\n❌ ERREUR lors de la validation:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: error.message
|
||
});
|
||
} finally {
|
||
conn.release();
|
||
}
|
||
});
|
||
|
||
app.get('/api/testRestoration', async (req, res) => {
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
const { demande_id, collab_id } = req.query;
|
||
|
||
if (!demande_id || !collab_id) {
|
||
return res.json({
|
||
success: false,
|
||
message: 'Paramètres manquants: demande_id et collab_id requis'
|
||
});
|
||
}
|
||
|
||
// 1. Voir les déductions enregistrées
|
||
const [deductions] = await conn.query(
|
||
`SELECT dd.*, tc.Nom as TypeNom
|
||
FROM DeductionDetails dd
|
||
JOIN TypeConge tc ON dd.TypeCongeId = tc.Id
|
||
WHERE dd.DemandeCongeId = ?`,
|
||
[demande_id]
|
||
);
|
||
|
||
// 2. Voir l'état actuel des compteurs
|
||
const [compteurs] = await conn.query(
|
||
`SELECT cc.*, tc.Nom as TypeNom
|
||
FROM CompteurConges cc
|
||
JOIN TypeConge tc ON cc.TypeCongeId = tc.Id
|
||
WHERE cc.CollaborateurADId = ?
|
||
ORDER BY tc.Nom, cc.Annee DESC`,
|
||
[collab_id]
|
||
);
|
||
|
||
// 3. Calculer ce que devrait être la restauration
|
||
const planRestauration = deductions.map(d => ({
|
||
type: d.TypeNom,
|
||
annee: d.Annee,
|
||
typeDeduction: d.TypeDeduction,
|
||
joursARestorer: d.JoursUtilises,
|
||
action: d.TypeDeduction === 'Reporté N-1'
|
||
? 'Ajouter au SoldeReporte ET au Solde'
|
||
: 'Ajouter au Solde uniquement'
|
||
}));
|
||
|
||
conn.release();
|
||
|
||
res.json({
|
||
success: true,
|
||
demande_id: demande_id,
|
||
collaborateur_id: collab_id,
|
||
deductions_enregistrees: deductions,
|
||
compteurs_actuels: compteurs,
|
||
plan_restauration: planRestauration,
|
||
instructions: planRestauration.length === 0
|
||
? "❌ Aucune déduction trouvée - La demande a été créée avant l'installation du tracking"
|
||
: "✅ Déductions trouvées - La restauration devrait fonctionner"
|
||
});
|
||
|
||
} catch (error) {
|
||
conn.release();
|
||
res.status(500).json({
|
||
success: false,
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
|
||
function normalizeRole(role) {
|
||
if (!role) return null;
|
||
|
||
const roleLower = role.toLowerCase();
|
||
|
||
// Normaliser les variantes féminines et masculines
|
||
if (roleLower === 'collaboratrice') return 'collaborateur';
|
||
if (roleLower === 'validatrice') return 'validateur';
|
||
if (roleLower === 'directrice de campus') return 'directeur de campus';
|
||
if (roleLower === 'apprentie') return 'apprenti';
|
||
|
||
return roleLower;
|
||
}
|
||
|
||
app.get('/api/getSocietesByCampus', async (req, res) => {
|
||
try {
|
||
const { campusId } = req.query;
|
||
|
||
const conn = await pool.getConnection();
|
||
|
||
const [societes] = await conn.query(`
|
||
SELECT DISTINCT s.Id, s.Nom
|
||
FROM SocieteCampus sc
|
||
JOIN Societe s ON sc.SocieteId = s.Id
|
||
WHERE sc.CampusId = ?
|
||
ORDER BY
|
||
CASE WHEN s.Nom LIKE '%SOLUTION%' THEN 1 ELSE 2 END,
|
||
s.Nom
|
||
`, [campusId]);
|
||
|
||
conn.release();
|
||
|
||
res.json({
|
||
success: true,
|
||
societes: societes
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('Erreur getSocietesByCampus:', error);
|
||
res.status(500).json({ success: false, message: error.message });
|
||
}
|
||
});
|
||
|
||
// ⭐ NOUVELLE ROUTE HELPER : Récupérer les campus d'une société
|
||
app.get('/api/getCampusBySociete', async (req, res) => {
|
||
try {
|
||
const { societeId } = req.query;
|
||
|
||
const conn = await pool.getConnection();
|
||
|
||
const [campus] = await conn.query(`
|
||
SELECT DISTINCT c.Id, c.Nom, sc.Principal
|
||
FROM SocieteCampus sc
|
||
JOIN Campus c ON sc.CampusId = c.Id
|
||
WHERE sc.SocieteId = ?
|
||
ORDER BY
|
||
sc.Principal DESC, -- Principal en premier
|
||
c.Nom
|
||
`, [societeId]);
|
||
|
||
conn.release();
|
||
|
||
res.json({
|
||
success: true,
|
||
campus: campus,
|
||
isMultiCampus: campus.length > 1
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('Erreur getCampusBySociete:', error);
|
||
res.status(500).json({ success: false, message: error.message });
|
||
}
|
||
});
|
||
|
||
|
||
|
||
|
||
|
||
// ========================================
|
||
// ROUTE getTeamLeaves COMPLÈTE
|
||
// ========================================
|
||
app.get('/api/getTeamLeaves', async (req, res) => {
|
||
try {
|
||
let { user_id: userIdParam, role: roleParam, selectedCampus, selectedSociete, selectedService } = req.query;
|
||
|
||
console.log(`🔍 Paramètres reçus: user_id=${userIdParam}, role=${roleParam}, selectedCampus=${selectedCampus}`);
|
||
|
||
if (!userIdParam) {
|
||
return res.json({ success: false, message: 'ID utilisateur manquant' });
|
||
}
|
||
|
||
const conn = await pool.getConnection();
|
||
|
||
const isUUID = userIdParam.length > 10 && userIdParam.includes('-');
|
||
console.log(`📝 Type ID détecté: ${isUUID ? 'UUID' : 'Numérique'}`);
|
||
|
||
const userQuery = `
|
||
SELECT
|
||
ca.id,
|
||
ca.ServiceId,
|
||
ca.CampusId,
|
||
ca.SocieteId,
|
||
ca.email,
|
||
s.Nom AS serviceNom,
|
||
c.Nom AS campusNom,
|
||
so.Nom AS societeNom
|
||
FROM CollaborateurAD ca
|
||
LEFT JOIN Services s ON ca.ServiceId = s.Id
|
||
LEFT JOIN Campus c ON ca.CampusId = c.Id
|
||
LEFT JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE ${isUUID ? 'ca.entraUserId' : 'ca.id'} = ?
|
||
LIMIT 1
|
||
`;
|
||
|
||
const [userRows] = await conn.query(userQuery, [userIdParam]);
|
||
|
||
if (!userRows || userRows.length === 0) {
|
||
conn.release();
|
||
return res.json({ success: false, message: 'Collaborateur non trouvé' });
|
||
}
|
||
|
||
const userInfo = userRows[0];
|
||
const serviceId = userInfo.ServiceId;
|
||
const campusId = userInfo.CampusId;
|
||
const societeId = userInfo.SocieteId;
|
||
const userEmail = userInfo.email;
|
||
const campusNom = userInfo.campusNom;
|
||
|
||
function normalizeRole(role) {
|
||
if (!role) return null;
|
||
const roleLower = role.toLowerCase();
|
||
if (roleLower === 'collaboratrice') return 'collaborateur';
|
||
if (roleLower === 'validatrice') return 'validateur';
|
||
if (roleLower === 'directrice de campus') return 'directeur de campus';
|
||
if (roleLower === 'apprentie') return 'apprenti';
|
||
return roleLower;
|
||
}
|
||
|
||
const roleOriginal = roleParam?.toLowerCase();
|
||
const role = normalizeRole(roleOriginal);
|
||
|
||
console.log(`👤 Utilisateur trouvé:`);
|
||
console.log(` - ID: ${userInfo.id}`);
|
||
console.log(` - Email: ${userEmail}`);
|
||
console.log(` - ServiceId: ${serviceId}`);
|
||
console.log(` - CampusId: ${campusId}`);
|
||
console.log(` - CampusNom: ${campusNom}`);
|
||
console.log(` - SocieteId: ${societeId}`);
|
||
console.log(` - Role normalisé: ${role}`);
|
||
|
||
let query, params;
|
||
const filters = {};
|
||
|
||
|
||
|
||
// ========================================
|
||
// CAS 1: PRESIDENT, ADMIN, RH
|
||
// ========================================
|
||
// ========================================
|
||
// CAS 1: PRESIDENT, ADMIN, RH, DIRECTEUR DE CAMPUS
|
||
// ========================================
|
||
if (role === 'president' || role === 'admin' || role === 'rh' || role === 'directeur de campus') {
|
||
console.log("CAS 1: President/Admin/RH/Directeur de Campus - Vue globale");
|
||
console.log(` Filtres reçus: Société=${selectedSociete}, Campus=${selectedCampus}, Service=${selectedService}`);
|
||
|
||
// ========================================
|
||
// 🔧 LISTE COMPLÈTE DES FILTRES DISPONIBLES
|
||
// ========================================
|
||
|
||
// 1️⃣ SOCIÉTÉS (toutes disponibles)
|
||
const [societesList] = await conn.query(`
|
||
SELECT DISTINCT Nom
|
||
FROM Societe
|
||
ORDER BY Nom
|
||
`);
|
||
filters.societes = societesList.map(s => s.Nom);
|
||
console.log('📊 Sociétés disponibles:', filters.societes);
|
||
|
||
// 2️⃣ CAMPUS (tous les campus, filtrés par société si nécessaire)
|
||
let campusQuery;
|
||
let campusParams = [];
|
||
|
||
if (selectedSociete && selectedSociete !== 'all') {
|
||
campusQuery = `
|
||
SELECT DISTINCT c.Nom
|
||
FROM Campus c
|
||
JOIN CollaborateurAD ca ON ca.CampusId = c.Id
|
||
JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE so.Nom = ?
|
||
AND (ca.actif = 1 OR ca.actif IS NULL)
|
||
ORDER BY c.Nom
|
||
`;
|
||
campusParams = [selectedSociete];
|
||
} else {
|
||
campusQuery = `
|
||
SELECT DISTINCT Nom
|
||
FROM Campus
|
||
ORDER BY Nom
|
||
`;
|
||
}
|
||
|
||
const [campusList] = await conn.query(campusQuery, campusParams);
|
||
filters.campus = campusList.map(c => c.Nom);
|
||
console.log('📊 Campus disponibles:', filters.campus);
|
||
|
||
// ⭐ NOUVEAU : Pour directeur de campus, envoyer son campus par défaut
|
||
if (role === 'directeur de campus') {
|
||
filters.defaultCampus = campusNom; // Le campus du directeur
|
||
console.log('🏢 Campus par défaut pour directeur:', campusNom);
|
||
}
|
||
|
||
// 3️⃣ SERVICES (filtrés selon société + campus)
|
||
let servicesQuery = `
|
||
SELECT DISTINCT s.Nom
|
||
FROM Services s
|
||
JOIN CollaborateurAD ca ON ca.ServiceId = s.Id
|
||
`;
|
||
|
||
let servicesJoins = [];
|
||
let servicesConditions = ['(ca.actif = 1 OR ca.actif IS NULL)'];
|
||
let servicesParams = [];
|
||
|
||
if (selectedSociete && selectedSociete !== 'all') {
|
||
servicesJoins.push('JOIN Societe so ON ca.SocieteId = so.Id');
|
||
servicesConditions.push('so.Nom = ?');
|
||
servicesParams.push(selectedSociete);
|
||
}
|
||
|
||
if (selectedCampus && selectedCampus !== 'all') {
|
||
servicesJoins.push('JOIN Campus c ON ca.CampusId = c.Id');
|
||
servicesConditions.push('c.Nom = ?');
|
||
servicesParams.push(selectedCampus);
|
||
}
|
||
|
||
if (servicesJoins.length > 0) {
|
||
servicesQuery += '\n' + servicesJoins.join('\n');
|
||
}
|
||
|
||
servicesQuery += `\nWHERE ${servicesConditions.join(' AND ')}\nORDER BY s.Nom`;
|
||
|
||
const [servicesList] = await conn.query(servicesQuery, servicesParams);
|
||
filters.services = servicesList.map(s => s.Nom);
|
||
|
||
// ========================================
|
||
// 🔧 LISTE DES EMPLOYÉS (avec filtres appliqués)
|
||
// ========================================
|
||
let employeesQuery = `
|
||
SELECT DISTINCT
|
||
CONCAT(ca.prenom, ' ', ca.nom) AS fullname,
|
||
c.Nom AS campusnom,
|
||
so.Nom AS societenom,
|
||
s.Nom AS servicenom
|
||
FROM CollaborateurAD ca
|
||
JOIN Services s ON ca.ServiceId = s.Id
|
||
JOIN Campus c ON ca.CampusId = c.Id
|
||
JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE (ca.actif = 1 OR ca.actif IS NULL)
|
||
`;
|
||
|
||
let employeesConditions = [];
|
||
let employeesParams = [];
|
||
|
||
if (selectedSociete && selectedSociete !== 'all') {
|
||
employeesConditions.push('so.Nom = ?');
|
||
employeesParams.push(selectedSociete);
|
||
}
|
||
|
||
if (selectedCampus && selectedCampus !== 'all') {
|
||
employeesConditions.push('c.Nom = ?');
|
||
employeesParams.push(selectedCampus);
|
||
} else if (role === 'directeur de campus' && campusNom) {
|
||
// ⭐ NOUVEAU : Si directeur et pas de filtre campus, filtrer par son campus par défaut
|
||
employeesConditions.push('c.Nom = ?');
|
||
employeesParams.push(campusNom);
|
||
}
|
||
|
||
if (selectedService && selectedService !== 'all') {
|
||
employeesConditions.push('s.Nom = ?');
|
||
employeesParams.push(selectedService);
|
||
}
|
||
|
||
if (employeesConditions.length > 0) {
|
||
employeesQuery += ` AND ${employeesConditions.join(' AND ')}`;
|
||
}
|
||
|
||
employeesQuery += ` ORDER BY so.Nom, c.Nom, ca.prenom, ca.nom`;
|
||
|
||
const [employeesList] = await conn.query(employeesQuery, employeesParams);
|
||
|
||
filters.employees = employeesList.map(e => ({
|
||
name: e.fullname,
|
||
campus: e.campusnom,
|
||
societe: e.societenom,
|
||
service: e.servicenom
|
||
}));
|
||
|
||
console.log(`👥 Employés trouvés:`, filters.employees.length);
|
||
|
||
// ========================================
|
||
// 🔧 QUERY DES CONGÉS (avec filtres appliqués)
|
||
// ========================================
|
||
let whereConditions = [`dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente')`];
|
||
let whereParams = [];
|
||
|
||
if (selectedSociete && selectedSociete !== 'all') {
|
||
whereConditions.push('so.Nom = ?');
|
||
whereParams.push(selectedSociete);
|
||
}
|
||
|
||
if (selectedCampus && selectedCampus !== 'all') {
|
||
whereConditions.push('c.Nom = ?');
|
||
whereParams.push(selectedCampus);
|
||
} else if (role === 'directeur de campus' && campusNom) {
|
||
// ⭐ NOUVEAU : Si directeur et pas de filtre campus, filtrer par son campus par défaut
|
||
whereConditions.push('c.Nom = ?');
|
||
whereParams.push(campusNom);
|
||
}
|
||
|
||
if (selectedService && selectedService !== 'all') {
|
||
whereConditions.push('s.Nom = ?');
|
||
whereParams.push(selectedService);
|
||
}
|
||
|
||
query = `
|
||
SELECT
|
||
DATE_FORMAT(dc.DateDebut, '%Y-%m-%d') AS startdate,
|
||
DATE_FORMAT(dc.DateFin, '%Y-%m-%d') AS enddate,
|
||
CONCAT(ca.prenom, ' ', ca.nom) AS employeename,
|
||
GROUP_CONCAT(DISTINCT tc.Nom ORDER BY tc.Nom SEPARATOR ', ') AS type,
|
||
CONCAT(
|
||
'[',
|
||
GROUP_CONCAT(
|
||
JSON_OBJECT(
|
||
'type', tc.Nom,
|
||
'jours', dct.NombreJours,
|
||
'periode', COALESCE(dct.PeriodeJournee, 'Journée entière')
|
||
)
|
||
SEPARATOR ','
|
||
),
|
||
']'
|
||
) AS detailsconges,
|
||
MAX(tc.CouleurHex) AS color,
|
||
dc.Statut AS statut,
|
||
s.Nom AS servicenom,
|
||
c.Nom AS campusnom,
|
||
so.Nom AS societenom,
|
||
dc.NombreJours AS nombrejoursouvres
|
||
FROM DemandeConge dc
|
||
JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id
|
||
LEFT JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId
|
||
LEFT JOIN TypeConge tc ON dct.TypeCongeId = tc.Id
|
||
JOIN Services s ON ca.ServiceId = s.Id
|
||
JOIN Campus c ON ca.CampusId = c.Id
|
||
JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE ${whereConditions.join(' AND ')}
|
||
GROUP BY dc.Id, dc.DateDebut, dc.DateFin, ca.prenom, ca.nom, dc.Statut, s.Nom, c.Nom, so.Nom, dc.NombreJours
|
||
ORDER BY so.Nom, c.Nom, dc.DateDebut ASC
|
||
`;
|
||
params = whereParams;
|
||
|
||
console.log(`🔍 Query finale WHERE:`, whereConditions.join(' AND '));
|
||
console.log(`🔍 Params:`, whereParams);
|
||
}
|
||
|
||
|
||
|
||
// ========================================
|
||
// CAS 3: COLLABORATEUR
|
||
// ========================================
|
||
// Dans la route /getTeamLeaves, modifiez la section CAS 3: COLLABORATEUR
|
||
// ========================================
|
||
// CAS 3: COLLABORATEUR
|
||
// ========================================
|
||
// ========================================
|
||
// CAS 3: COLLABORATEUR
|
||
// ========================================
|
||
else if (role === 'collaborateur' || role === 'validateur' || role === 'apprenti') {
|
||
console.log("CAS 3: Collaborateur/Apprenti avec filtres avancés");
|
||
|
||
const serviceNom = userInfo.serviceNom || 'Non défini';
|
||
const campusNom = userInfo.campusNom || 'Non défini';
|
||
const societeNom = userInfo.societeNom || 'Non défini';
|
||
|
||
console.log(`📍 Filtres reçus du frontend: Société=${selectedSociete}, Campus=${selectedCampus}, Service=${selectedService}`);
|
||
|
||
// ⭐ NOUVEAU : Si AUCUN filtre n'est envoyé (premier chargement), utiliser les valeurs par défaut
|
||
// Sinon, respecter EXACTEMENT ce que le frontend envoie (même "all")
|
||
const isFirstLoad = !selectedCampus && !selectedService && !selectedSociete;
|
||
|
||
if (isFirstLoad) {
|
||
console.log('🎯 Premier chargement : initialisation avec service par défaut');
|
||
selectedCampus = campusNom;
|
||
selectedService = serviceNom;
|
||
selectedSociete = societeNom;
|
||
}
|
||
// Si le frontend envoie "all", on garde "all" (ne pas forcer les valeurs par défaut)
|
||
|
||
console.log(`📍 Filtres appliqués finaux: Société=${selectedSociete}, Campus=${selectedCampus}, Service=${selectedService}`);
|
||
|
||
// ⭐ Construire les listes de filtres disponibles
|
||
// 1️⃣ Sociétés disponibles (TOUTES)
|
||
const [societesList] = await conn.query(`
|
||
SELECT DISTINCT so.Nom
|
||
FROM Societe so
|
||
JOIN CollaborateurAD ca ON ca.SocieteId = so.Id
|
||
WHERE (ca.actif = 1 OR ca.actif IS NULL)
|
||
ORDER BY so.Nom
|
||
`);
|
||
filters.societes = societesList.map(s => s.Nom);
|
||
|
||
// 2️⃣ Campus disponibles (filtrés par société si sélectionné)
|
||
let campusQuery = `
|
||
SELECT DISTINCT c.Nom
|
||
FROM Campus c
|
||
JOIN CollaborateurAD ca ON ca.CampusId = c.Id
|
||
WHERE (ca.actif = 1 OR ca.actif IS NULL)
|
||
`;
|
||
let campusParams = [];
|
||
|
||
if (selectedSociete && selectedSociete !== 'all') {
|
||
campusQuery += ` AND ca.SocieteId = (SELECT Id FROM Societe WHERE Nom = ? LIMIT 1)`;
|
||
campusParams.push(selectedSociete);
|
||
}
|
||
|
||
campusQuery += ` ORDER BY c.Nom`;
|
||
const [campusList] = await conn.query(campusQuery, campusParams);
|
||
filters.campus = campusList.map(c => c.Nom);
|
||
|
||
// 3️⃣ Services disponibles (filtrés par société + campus)
|
||
let servicesQuery = `
|
||
SELECT DISTINCT s.Nom
|
||
FROM Services s
|
||
JOIN CollaborateurAD ca ON ca.ServiceId = s.Id
|
||
WHERE (ca.actif = 1 OR ca.actif IS NULL)
|
||
`;
|
||
let servicesParams = [];
|
||
|
||
if (selectedSociete && selectedSociete !== 'all') {
|
||
servicesQuery += ` AND ca.SocieteId = (SELECT Id FROM Societe WHERE Nom = ? LIMIT 1)`;
|
||
servicesParams.push(selectedSociete);
|
||
}
|
||
|
||
if (selectedCampus && selectedCampus !== 'all') {
|
||
servicesQuery += ` AND ca.CampusId = (SELECT Id FROM Campus WHERE Nom = ? LIMIT 1)`;
|
||
servicesParams.push(selectedCampus);
|
||
}
|
||
|
||
servicesQuery += ` ORDER BY s.Nom`;
|
||
const [servicesList] = await conn.query(servicesQuery, servicesParams);
|
||
filters.services = servicesList.map(s => s.Nom);
|
||
|
||
// ⭐ Envoyer les valeurs par défaut au frontend (pour initialisation)
|
||
filters.defaultCampus = campusNom;
|
||
filters.defaultService = serviceNom;
|
||
filters.defaultSociete = societeNom;
|
||
|
||
// ⭐ LISTE DES EMPLOYÉS (avec filtres conditionnels)
|
||
let employeesQuery = `
|
||
SELECT DISTINCT
|
||
CONCAT(ca.prenom, ' ', ca.nom) AS fullname,
|
||
c.Nom AS campusnom,
|
||
so.Nom AS societenom,
|
||
s.Nom AS servicenom
|
||
FROM CollaborateurAD ca
|
||
JOIN Services s ON ca.ServiceId = s.Id
|
||
JOIN Campus c ON ca.CampusId = c.Id
|
||
JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE (ca.actif = 1 OR ca.actif IS NULL)
|
||
`;
|
||
|
||
let employeesParams = [];
|
||
let employeesConditions = [];
|
||
|
||
// ⭐ N'ajouter les filtres QUE si différents de "all"
|
||
if (selectedSociete && selectedSociete !== 'all') {
|
||
employeesConditions.push('so.Nom = ?');
|
||
employeesParams.push(selectedSociete);
|
||
}
|
||
|
||
if (selectedCampus && selectedCampus !== 'all') {
|
||
employeesConditions.push('c.Nom = ?');
|
||
employeesParams.push(selectedCampus);
|
||
}
|
||
|
||
if (selectedService && selectedService !== 'all') {
|
||
employeesConditions.push('s.Nom = ?');
|
||
employeesParams.push(selectedService);
|
||
}
|
||
|
||
if (employeesConditions.length > 0) {
|
||
employeesQuery += ` AND ${employeesConditions.join(' AND ')}`;
|
||
}
|
||
|
||
employeesQuery += ` ORDER BY s.Nom, ca.prenom, ca.nom`;
|
||
|
||
const [employeesList] = await conn.query(employeesQuery, employeesParams);
|
||
|
||
filters.employees = employeesList.map(emp => ({
|
||
name: emp.fullname,
|
||
campus: emp.campusnom,
|
||
societe: emp.societenom,
|
||
service: emp.servicenom
|
||
}));
|
||
|
||
console.log(`👥 Employés trouvés: ${filters.employees.length}`);
|
||
|
||
// ⭐ QUERY DES CONGÉS (avec mêmes filtres conditionnels)
|
||
let queryConditions = `WHERE dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente')`;
|
||
params = [];
|
||
let congesConditions = [];
|
||
|
||
// ⭐ N'ajouter les filtres QUE si différents de "all"
|
||
if (selectedSociete && selectedSociete !== 'all') {
|
||
congesConditions.push('so.Nom = ?');
|
||
params.push(selectedSociete);
|
||
}
|
||
|
||
if (selectedCampus && selectedCampus !== 'all') {
|
||
congesConditions.push('c.Nom = ?');
|
||
params.push(selectedCampus);
|
||
}
|
||
|
||
if (selectedService && selectedService !== 'all') {
|
||
congesConditions.push('s.Nom = ?');
|
||
params.push(selectedService);
|
||
}
|
||
|
||
if (congesConditions.length > 0) {
|
||
queryConditions += ` AND ${congesConditions.join(' AND ')}`;
|
||
}
|
||
|
||
query = `
|
||
SELECT
|
||
DATE_FORMAT(dc.DateDebut, '%Y-%m-%d') AS startdate,
|
||
DATE_FORMAT(dc.DateFin, '%Y-%m-%d') AS enddate,
|
||
CONCAT(ca.prenom, ' ', ca.nom) AS employeename,
|
||
GROUP_CONCAT(DISTINCT tc.Nom ORDER BY tc.Nom SEPARATOR ', ') AS type,
|
||
CONCAT(
|
||
'[',
|
||
GROUP_CONCAT(
|
||
JSON_OBJECT(
|
||
'type', tc.Nom,
|
||
'jours', dct.NombreJours,
|
||
'periode', COALESCE(dct.PeriodeJournee, 'Journée entière')
|
||
)
|
||
SEPARATOR ','
|
||
),
|
||
']'
|
||
) AS detailsconges,
|
||
MAX(tc.CouleurHex) AS color,
|
||
dc.Statut AS statut,
|
||
s.Nom AS servicenom,
|
||
c.Nom AS campusnom,
|
||
so.Nom AS societenom,
|
||
dc.NombreJours AS nombrejoursouvres
|
||
FROM DemandeConge dc
|
||
JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id
|
||
LEFT JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId
|
||
LEFT JOIN TypeConge tc ON dct.TypeCongeId = tc.Id
|
||
JOIN Services s ON ca.ServiceId = s.Id
|
||
JOIN Campus c ON ca.CampusId = c.Id
|
||
JOIN Societe so ON ca.SocieteId = so.Id
|
||
${queryConditions}
|
||
GROUP BY dc.Id, dc.DateDebut, dc.DateFin, ca.prenom, ca.nom, dc.Statut, s.Nom, c.Nom, so.Nom, dc.NombreJours
|
||
ORDER BY s.Nom, dc.DateDebut ASC
|
||
`;
|
||
|
||
console.log(`🔍 Query WHERE final:`, queryConditions);
|
||
console.log(`🔍 Params:`, params);
|
||
}
|
||
|
||
|
||
|
||
// ========================================
|
||
// CAS 4: AUTRES RÔLES
|
||
// ========================================
|
||
else {
|
||
console.log("CAS 4: Autres rôles");
|
||
|
||
if (!serviceId) {
|
||
conn.release();
|
||
return res.json({
|
||
success: false,
|
||
message: 'ServiceId manquant'
|
||
});
|
||
}
|
||
|
||
const [checkService] = await conn.query(`SELECT Nom FROM Services WHERE Id = ?`, [serviceId]);
|
||
const serviceNom = checkService.length > 0 ? checkService[0].Nom : "Inconnu";
|
||
const isAdminFinancier = serviceNom === "Administratif & Financier";
|
||
|
||
if (isAdminFinancier) {
|
||
const [employeesList] = await conn.query(`
|
||
SELECT DISTINCT
|
||
CONCAT(ca.prenom, ' ', ca.nom) AS fullname,
|
||
c.Nom AS campusnom,
|
||
so.Nom AS societenom,
|
||
s.Nom AS servicenom
|
||
FROM CollaborateurAD ca
|
||
JOIN Services s ON ca.ServiceId = s.Id
|
||
JOIN Campus c ON ca.CampusId = c.Id
|
||
JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE ca.ServiceId = ?
|
||
AND (ca.actif = 1 OR ca.actif IS NULL)
|
||
ORDER BY ca.prenom, ca.nom
|
||
`, [serviceId]);
|
||
|
||
filters.employees = employeesList.map(e => ({
|
||
name: e.fullname,
|
||
campus: e.campusnom,
|
||
societe: e.societenom,
|
||
service: e.servicenom
|
||
}));
|
||
|
||
query = `
|
||
SELECT
|
||
DATE_FORMAT(dc.DateDebut, '%Y-%m-%d') AS startdate,
|
||
DATE_FORMAT(dc.DateFin, '%Y-%m-%d') AS enddate,
|
||
CONCAT(ca.prenom, ' ', ca.nom) AS employeename,
|
||
GROUP_CONCAT(DISTINCT tc.Nom ORDER BY tc.Nom SEPARATOR ', ') AS type,
|
||
CONCAT(
|
||
'[',
|
||
GROUP_CONCAT(
|
||
JSON_OBJECT(
|
||
'type', tc.Nom,
|
||
'jours', dct.NombreJours,
|
||
'periode', COALESCE(dct.PeriodeJournee, 'Journée entière')
|
||
)
|
||
SEPARATOR ','
|
||
),
|
||
']'
|
||
) AS detailsconges,
|
||
MAX(tc.CouleurHex) AS color,
|
||
dc.Statut AS statut,
|
||
s.Nom AS servicenom,
|
||
c.Nom AS campusnom,
|
||
so.Nom AS societenom,
|
||
dc.NombreJours AS nombrejoursouvres
|
||
FROM DemandeConge dc
|
||
JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id
|
||
LEFT JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId
|
||
LEFT JOIN TypeConge tc ON dct.TypeCongeId = tc.Id
|
||
JOIN Services s ON ca.ServiceId = s.Id
|
||
JOIN Campus c ON ca.CampusId = c.Id
|
||
JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente')
|
||
AND ca.ServiceId = ?
|
||
GROUP BY dc.Id, dc.DateDebut, dc.DateFin, ca.prenom, ca.nom, dc.Statut, s.Nom, c.Nom, so.Nom, dc.NombreJours
|
||
ORDER BY c.Nom, dc.DateDebut ASC
|
||
`;
|
||
params = [serviceId];
|
||
|
||
} else {
|
||
const [employeesList] = await conn.query(`
|
||
SELECT DISTINCT
|
||
CONCAT(ca.prenom, ' ', ca.nom) AS fullname,
|
||
c.Nom AS campusnom,
|
||
so.Nom AS societenom,
|
||
s.Nom AS servicenom
|
||
FROM CollaborateurAD ca
|
||
JOIN Services s ON ca.ServiceId = s.Id
|
||
JOIN Campus c ON ca.CampusId = c.Id
|
||
JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE ca.ServiceId = ?
|
||
AND ca.CampusId = ?
|
||
AND (ca.actif = 1 OR ca.actif IS NULL)
|
||
ORDER BY ca.prenom, ca.nom
|
||
`, [serviceId, campusId]);
|
||
|
||
filters.employees = employeesList.map(e => ({
|
||
name: e.fullname,
|
||
campus: e.campusnom,
|
||
societe: e.societenom,
|
||
service: e.servicenom
|
||
}));
|
||
|
||
query = `
|
||
SELECT
|
||
DATE_FORMAT(dc.DateDebut, '%Y-%m-%d') AS startdate,
|
||
DATE_FORMAT(dc.DateFin, '%Y-%m-%d') AS enddate,
|
||
CONCAT(ca.prenom, ' ', ca.nom) AS employeename,
|
||
GROUP_CONCAT(DISTINCT tc.Nom ORDER BY tc.Nom SEPARATOR ', ') AS type,
|
||
CONCAT(
|
||
'[',
|
||
GROUP_CONCAT(
|
||
JSON_OBJECT(
|
||
'type', tc.Nom,
|
||
'jours', dct.NombreJours,
|
||
'periode', COALESCE(dct.PeriodeJournee, 'Journée entière')
|
||
)
|
||
SEPARATOR ','
|
||
),
|
||
']'
|
||
) AS detailsconges,
|
||
MAX(tc.CouleurHex) AS color,
|
||
dc.Statut AS statut,
|
||
s.Nom AS servicenom,
|
||
c.Nom AS campusnom,
|
||
so.Nom AS societenom,
|
||
dc.NombreJours AS nombrejoursouvres
|
||
FROM DemandeConge dc
|
||
JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id
|
||
LEFT JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId
|
||
LEFT JOIN TypeConge tc ON dct.TypeCongeId = tc.Id
|
||
JOIN Services s ON ca.ServiceId = s.Id
|
||
JOIN Campus c ON ca.CampusId = c.Id
|
||
JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente')
|
||
AND ca.ServiceId = ?
|
||
AND ca.CampusId = ?
|
||
GROUP BY dc.Id, dc.DateDebut, dc.DateFin, ca.prenom, ca.nom, dc.Statut, s.Nom, c.Nom, so.Nom, dc.NombreJours
|
||
ORDER BY c.Nom, dc.DateDebut ASC
|
||
`;
|
||
params = [serviceId, campusId];
|
||
}
|
||
}
|
||
|
||
const [leavesRows] = await conn.query(query, params);
|
||
|
||
const formattedLeaves = leavesRows.map(leave => ({
|
||
...leave
|
||
}));
|
||
|
||
console.log(`✅ ${formattedLeaves.length} congés trouvés`);
|
||
|
||
if (formattedLeaves.length === 0 && (role === 'collaborateur' || role === 'collaboratrice')) {
|
||
console.log('🔍 DEBUG: Aucun congé trouvé, vérification...');
|
||
|
||
const [debugLeaves] = await conn.query(`
|
||
SELECT COUNT(*) as total
|
||
FROM DemandeConge dc
|
||
JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id
|
||
JOIN Services s ON ca.ServiceId = s.Id
|
||
WHERE s.Nom = ?
|
||
AND dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente')
|
||
`, [userInfo.serviceNom]);
|
||
|
||
console.log(`🔍 Total congés dans le service "${userInfo.serviceNom}":`, debugLeaves[0].total);
|
||
|
||
const [debugCollabs] = await conn.query(`
|
||
SELECT ca.id, ca.prenom, ca.nom, ca.email, ca.ServiceId, s.Nom as ServiceNom
|
||
FROM CollaborateurAD ca
|
||
JOIN Services s ON ca.ServiceId = s.Id
|
||
WHERE s.Nom = ?
|
||
`, [userInfo.serviceNom]);
|
||
|
||
console.log(`🔍 Collaborateurs dans "${userInfo.serviceNom}":`, debugCollabs);
|
||
}
|
||
|
||
console.log(`✅ Filtres:`, {
|
||
campus: filters.campus?.length || 0,
|
||
societes: filters.societes?.length || 0,
|
||
services: filters.services?.length || 0,
|
||
employees: filters.employees?.length || 0
|
||
});
|
||
|
||
if (formattedLeaves.length > 0) {
|
||
console.log('📝 Exemple de congé formaté:', formattedLeaves[0]);
|
||
}
|
||
|
||
conn.release();
|
||
|
||
res.json({
|
||
success: true,
|
||
role: role,
|
||
leaves: formattedLeaves,
|
||
filters: filters
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error("❌ Erreur getTeamLeaves:", error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: "Erreur serveur",
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
|
||
|
||
// ================================================
|
||
// ROUTE DE SYNCHRONISATION INITIALE (CORRIGÉE)
|
||
// ================================================
|
||
|
||
|
||
// ✅ APRÈS - Version CORRIGÉE
|
||
// ✅ VERSION COMPLÈTE ET CORRIGÉE
|
||
app.post('/api/initial-sync', async (req, res) => {
|
||
try {
|
||
// 1. Obtenir le token Admin
|
||
const accessToken = await getGraphToken();
|
||
if (!accessToken) return res.json({ success: false, message: 'Impossible obtenir token Microsoft Graph' });
|
||
|
||
// =============================================================================
|
||
// SCÉNARIO 1 : Synchronisation unitaire (Un seul utilisateur spécifique)
|
||
// =============================================================================
|
||
if (req.body.userPrincipalName || req.body.mail) {
|
||
const userEmail = req.body.mail || req.body.userPrincipalName;
|
||
const entraUserId = req.body.id;
|
||
|
||
console.log(`🔄 Synchronisation utilisateur unique : ${userEmail}`);
|
||
|
||
// Insertion ou Mise à jour
|
||
await pool.query(`
|
||
INSERT INTO CollaborateurAD
|
||
(entraUserId, prenom, nom, email, service, description, role, SocieteId, Actif, DateEntree, TypeContrat)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, NOW(), '37h')
|
||
ON DUPLICATE KEY UPDATE
|
||
prenom=VALUES(prenom),
|
||
nom=VALUES(nom),
|
||
email=VALUES(email),
|
||
service=VALUES(service),
|
||
description=VALUES(description),
|
||
Actif=1, -- On réactive si l'utilisateur revient
|
||
entraUserId=VALUES(entraUserId)
|
||
`, [
|
||
entraUserId,
|
||
req.body.givenName || 'Prénom',
|
||
req.body.surname || 'Nom',
|
||
userEmail,
|
||
req.body.department || '',
|
||
req.body.jobTitle || null,
|
||
'Collaborateur',
|
||
1 // SocieteId par défaut (ex: ENSUP)
|
||
]);
|
||
|
||
// Récupération des données fraîches pour renvoyer au front
|
||
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: 'Erreur : Utilisateur synchronisé mais introuvable en base.' });
|
||
}
|
||
|
||
const userData = userRows[0];
|
||
console.log(`✅ Utilisateur synchronisé avec succès : ${userData.email}`);
|
||
|
||
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,
|
||
user: userData
|
||
});
|
||
}
|
||
|
||
// =============================================================================
|
||
// SCÉNARIO 2 : Full Sync (Tous les membres du groupe Azure)
|
||
// =============================================================================
|
||
console.log('🔄 Démarrage Full Sync des membres du groupe...');
|
||
|
||
// A. Récupérer le nom du groupe (pour info)
|
||
const groupResponse = await axios.get(
|
||
`https://graph.microsoft.com/v1.0/groups/${AZURE_CONFIG.groupId}?$select=id,displayName`,
|
||
{ headers: { Authorization: `Bearer ${accessToken}` } }
|
||
);
|
||
const groupName = groupResponse.data.displayName;
|
||
|
||
// B. Récupérer TOUS les membres avec pagination (boucle while)
|
||
let allAzureMembers = [];
|
||
let nextLink = `https://graph.microsoft.com/v1.0/groups/${AZURE_CONFIG.groupId}/members?$select=id,givenName,surname,mail,department,jobTitle&$top=999`;
|
||
|
||
while (nextLink) {
|
||
const membersResponse = await axios.get(nextLink, { headers: { Authorization: `Bearer ${accessToken}` } });
|
||
allAzureMembers = allAzureMembers.concat(membersResponse.data.value);
|
||
nextLink = membersResponse.data['@odata.nextLink']; // Lien vers la page suivante (si existe)
|
||
}
|
||
|
||
console.log(`📋 ${allAzureMembers.length} utilisateurs trouvés dans le groupe Azure "${groupName}".`);
|
||
|
||
const azureEmails = new Set(); // Liste blanche des emails actifs
|
||
let usersInserted = 0;
|
||
|
||
// C. Traitement de chaque membre Azure
|
||
for (const m of allAzureMembers) {
|
||
if (!m.mail) continue; // Ignorer ceux sans email
|
||
|
||
azureEmails.add(m.mail.toLowerCase());
|
||
|
||
await pool.query(`
|
||
INSERT INTO CollaborateurAD (
|
||
entraUserId, prenom, nom, email, service, description, role, SocieteId, Actif, DateEntree, TypeContrat
|
||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, NOW(), '37h')
|
||
ON DUPLICATE KEY UPDATE
|
||
prenom=VALUES(prenom),
|
||
nom=VALUES(nom),
|
||
service=VALUES(service),
|
||
entraUserId=VALUES(entraUserId),
|
||
Actif = 1 -- On s'assure qu'il est actif
|
||
`, [
|
||
m.id,
|
||
m.givenName || 'Prénom',
|
||
m.surname || 'Nom',
|
||
m.mail,
|
||
m.department || '',
|
||
m.jobTitle || null,
|
||
'Collaborateur',
|
||
1, // SocieteId par défaut
|
||
]);
|
||
|
||
usersInserted++;
|
||
}
|
||
|
||
// D. Désactivation des fantômes (Ceux en base locale mais ABSENTS d'Azure)
|
||
const activeEmailsArray = Array.from(azureEmails);
|
||
let deactivatedCount = 0;
|
||
|
||
if (activeEmailsArray.length > 0) {
|
||
// Création dynamique des placeholders (?, ?, ?)
|
||
const placeholders = activeEmailsArray.map(() => '?').join(',');
|
||
|
||
// On désactive tous ceux qui NE SONT PAS dans la liste activeEmailsArray
|
||
const [resultDeactivate] = await pool.query(`
|
||
UPDATE CollaborateurAD
|
||
SET Actif = 0
|
||
WHERE Email IS NOT NULL
|
||
AND LOWER(Email) NOT IN (${placeholders})
|
||
AND Actif = 1 -- On ne modifie que ceux qui étaient actifs
|
||
`, activeEmailsArray);
|
||
|
||
deactivatedCount = resultDeactivate.affectedRows;
|
||
}
|
||
|
||
console.log(`✅ Full sync terminée avec succès.`);
|
||
console.log(` - ${usersInserted} utilisateurs synchronisés/actifs`);
|
||
console.log(` - ${deactivatedCount} utilisateurs désactivés (partis)`);
|
||
|
||
res.json({
|
||
success: true,
|
||
message: 'Synchronisation globale terminée',
|
||
groupe_sync: groupName,
|
||
stats: {
|
||
total_azure: allAzureMembers.length,
|
||
processed: usersInserted,
|
||
deactivated: deactivatedCount
|
||
}
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('❌ Erreur critique lors de la synchronisation:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur lors de la synchronisation',
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
|
||
// ========================================
|
||
// NOUVELLES ROUTES ADMINISTRATION RTT
|
||
// ========================================
|
||
|
||
app.get('/api/getAllCollaborateurs', async (req, res) => {
|
||
try {
|
||
const [collaborateurs] = await pool.query(`
|
||
SELECT
|
||
ca.id,
|
||
ca.prenom,
|
||
ca.nom,
|
||
ca.email,
|
||
ca.role,
|
||
ca.TypeContrat,
|
||
ca.DateEntree,
|
||
s.Nom as service,
|
||
ca.CampusId,
|
||
ca.SocieteId,
|
||
so.Nom as societe_nom
|
||
FROM CollaborateurAD ca
|
||
LEFT JOIN Services s ON ca.ServiceId = s.Id
|
||
LEFT JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE (ca.actif = 1 OR ca.actif IS NULL)
|
||
ORDER BY ca.nom, ca.prenom
|
||
`);
|
||
|
||
res.json({
|
||
success: true,
|
||
collaborateurs: collaborateurs,
|
||
total: collaborateurs.length
|
||
});
|
||
} catch (error) {
|
||
console.error('Erreur getAllCollaborateurs:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur serveur',
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
app.post('/api/updateTypeContrat', async (req, res) => {
|
||
try {
|
||
const { collaborateur_id, type_contrat } = req.body;
|
||
|
||
if (!collaborateur_id || !type_contrat) {
|
||
return res.json({
|
||
success: false,
|
||
message: 'Données manquantes'
|
||
});
|
||
}
|
||
|
||
const typesValides = ['37h', 'forfait_jour', 'temps_partiel'];
|
||
if (!typesValides.includes(type_contrat)) {
|
||
return res.json({
|
||
success: false,
|
||
message: 'Type de contrat invalide'
|
||
});
|
||
}
|
||
|
||
const [collab] = await pool.query(
|
||
'SELECT prenom, nom, CampusId FROM CollaborateurAD WHERE id = ?',
|
||
[collaborateur_id]
|
||
);
|
||
|
||
if (collab.length === 0) {
|
||
return res.json({
|
||
success: false,
|
||
message: 'Collaborateur non trouvé'
|
||
});
|
||
}
|
||
|
||
await pool.query(
|
||
'UPDATE CollaborateurAD SET TypeContrat = ? WHERE id = ?',
|
||
[type_contrat, collaborateur_id]
|
||
);
|
||
|
||
res.json({
|
||
success: true,
|
||
message: 'Type de contrat mis à jour',
|
||
nom: `${collab[0].prenom} ${collab[0].nom}`,
|
||
nouveau_type: type_contrat
|
||
});
|
||
} catch (error) {
|
||
console.error('Erreur updateTypeContrat:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur serveur',
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
app.get('/api/getConfigurationRTT', async (req, res) => {
|
||
try {
|
||
const annee = parseInt(req.query.annee || new Date().getFullYear());
|
||
const [configs] = await pool.query(
|
||
`SELECT Annee, TypeContrat, JoursAnnuels, AcquisitionMensuelle, Description
|
||
FROM ConfigurationRTT
|
||
WHERE Annee = ?
|
||
ORDER BY TypeContrat`,
|
||
[annee]
|
||
);
|
||
res.json({ success: true, configs });
|
||
} catch (error) {
|
||
res.status(500).json({ success: false, message: 'Erreur', error: error.message });
|
||
}
|
||
});
|
||
|
||
app.post('/api/updateConfigurationRTT', async (req, res) => {
|
||
try {
|
||
const { annee, typeContrat, joursAnnuels } = req.body;
|
||
if (!annee || !typeContrat || !joursAnnuels) {
|
||
return res.json({ success: false, message: 'Données manquantes' });
|
||
}
|
||
|
||
const acquisitionMensuelle = joursAnnuels / 12;
|
||
|
||
await pool.query(
|
||
`INSERT INTO ConfigurationRTT (Annee, TypeContrat, JoursAnnuels, AcquisitionMensuelle)
|
||
VALUES (?, ?, ?, ?)
|
||
ON DUPLICATE KEY UPDATE
|
||
JoursAnnuels = ?, AcquisitionMensuelle = ?`,
|
||
[annee, typeContrat, joursAnnuels, acquisitionMensuelle, joursAnnuels, acquisitionMensuelle]
|
||
);
|
||
|
||
res.json({ success: true, message: 'Configuration mise à jour' });
|
||
} catch (error) {
|
||
res.status(500).json({ success: false, message: 'Erreur', error: error.message });
|
||
}
|
||
});
|
||
|
||
app.post('/api/updateRequest', upload.array('medicalDocuments', 5), async (req, res) => {
|
||
let connection;
|
||
try {
|
||
console.log('📥 Body reçu:', req.body);
|
||
console.log('📎 Fichiers reçus:', req.files);
|
||
|
||
const {
|
||
requestId,
|
||
leaveType,
|
||
startDate,
|
||
endDate,
|
||
reason,
|
||
businessDays,
|
||
userId,
|
||
userEmail,
|
||
userName,
|
||
accessToken
|
||
} = req.body;
|
||
|
||
// Validation
|
||
if (!requestId || !leaveType || !startDate || !endDate || !businessDays || !userId) {
|
||
return res.status(400).json({
|
||
success: false,
|
||
message: '❌ Données manquantes'
|
||
});
|
||
}
|
||
|
||
connection = await pool.getConnection();
|
||
await connection.beginTransaction();
|
||
|
||
console.log('\n✏️ === MODIFICATION DEMANDE ===');
|
||
console.log(`Demande ID: ${requestId}, User ID: ${userId}`);
|
||
|
||
// 1️⃣ RÉCUPÉRER LA DEMANDE ORIGINALE
|
||
const [originalRequest] = await connection.query(
|
||
'SELECT * FROM DemandeConge WHERE Id = ? AND CollaborateurADId = ?',
|
||
[requestId, userId]
|
||
);
|
||
|
||
if (originalRequest.length === 0) {
|
||
await connection.rollback();
|
||
return res.status(404).json({
|
||
success: false,
|
||
message: '❌ Demande introuvable ou non autorisée'
|
||
});
|
||
}
|
||
|
||
const original = originalRequest[0];
|
||
const oldStatus = original.Statut;
|
||
|
||
console.log(`📋 Demande originale: Statut=${oldStatus}`);
|
||
|
||
// 2️⃣ REMBOURSER L'ANCIENNE DEMANDE (via DeductionDetails)
|
||
let restorationStats = { count: 0, details: [] };
|
||
|
||
if (oldStatus !== 'Refusée' && oldStatus !== 'Annulée' && original.TypeCongeId !== 3) {
|
||
console.log(`🔄 Remboursement de l'ancienne demande...`);
|
||
|
||
const [oldDeductions] = await connection.query(
|
||
'SELECT * FROM DeductionDetails WHERE DemandeCongeId = ?',
|
||
[requestId]
|
||
);
|
||
|
||
if (oldDeductions.length > 0) {
|
||
for (const d of oldDeductions) {
|
||
const [compteur] = await connection.query(
|
||
'SELECT Id, Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?',
|
||
[userId, d.TypeCongeId, d.Annee]
|
||
);
|
||
|
||
if (compteur.length > 0) {
|
||
const newSolde = parseFloat(compteur[0].Solde) + parseFloat(d.JoursUtilises);
|
||
await connection.query(
|
||
'UPDATE CompteurConges SET Solde = ?, DerniereMiseAJour = NOW() WHERE Id = ?',
|
||
[newSolde, compteur[0].Id]
|
||
);
|
||
restorationStats.count++;
|
||
restorationStats.details.push(`${d.JoursUtilises}j rendus (Type ${d.TypeCongeId}, Année ${d.Annee})`);
|
||
console.log(` ✅ Remboursé ${d.JoursUtilises}j au compteur TypeId=${d.TypeCongeId}, Annee=${d.Annee}`);
|
||
}
|
||
}
|
||
|
||
// Supprimer les anciennes déductions
|
||
await connection.query('DELETE FROM DeductionDetails WHERE DemandeCongeId = ?', [requestId]);
|
||
console.log(` 🧹 ${oldDeductions.length} déduction(s) supprimée(s)`);
|
||
}
|
||
}
|
||
|
||
// 3️⃣ METTRE À JOUR LA DEMANDE
|
||
console.log('📝 Mise à jour de la demande...');
|
||
|
||
// Si elle était validée, on la repasse en "En attente"
|
||
const newStatus = (oldStatus === 'Validée' || oldStatus === 'Validé') ? 'En attente' : oldStatus;
|
||
|
||
await connection.query(
|
||
`UPDATE DemandeConge
|
||
SET TypeCongeId = ?,
|
||
DateDebut = ?,
|
||
DateFin = ?,
|
||
Commentaire = ?,
|
||
NombreJours = ?,
|
||
Statut = ?,
|
||
DateValidation = NOW(),
|
||
CommentaireValidation = CONCAT(
|
||
COALESCE(CommentaireValidation, ''),
|
||
'\n[Modifiée le ',
|
||
DATE_FORMAT(NOW(), '%d/%m/%Y à %H:%i'),
|
||
']'
|
||
)
|
||
WHERE Id = ?`,
|
||
[leaveType, startDate, endDate, reason || '', businessDays, newStatus, requestId]
|
||
);
|
||
|
||
// 4️⃣ CALCULER ET APPLIQUER LA NOUVELLE RÉPARTITION
|
||
let newRepartition = [];
|
||
|
||
if (parseInt(leaveType) !== 3) { // Pas pour Arrêt maladie
|
||
console.log('📊 Calcul de la nouvelle répartition...');
|
||
|
||
const currentYear = new Date().getFullYear();
|
||
const previousYear = currentYear - 1;
|
||
let joursRestants = parseFloat(businessDays);
|
||
|
||
// A. CONGÉ PAYÉ : N-1 → N → Anticipé
|
||
if (parseInt(leaveType) === 1) {
|
||
// Essayer N-1
|
||
const [compteurN1] = await connection.query(
|
||
'SELECT Id, Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?',
|
||
[userId, leaveType, previousYear]
|
||
);
|
||
|
||
if (compteurN1.length > 0 && compteurN1[0].Solde > 0 && joursRestants > 0) {
|
||
const disponibleN1 = parseFloat(compteurN1[0].Solde);
|
||
const aPrendreN1 = Math.min(disponibleN1, joursRestants);
|
||
|
||
await connection.query(
|
||
'UPDATE CompteurConges SET Solde = Solde - ?, DerniereMiseAJour = NOW() WHERE Id = ?',
|
||
[aPrendreN1, compteurN1[0].Id]
|
||
);
|
||
|
||
await connection.query(`
|
||
INSERT INTO DeductionDetails (DemandeCongeId, TypeCongeId, Annee, JoursUtilises, TypeDeduction)
|
||
VALUES (?, ?, ?, ?, ?)
|
||
`, [requestId, leaveType, previousYear, aPrendreN1, 'Année N-1']);
|
||
|
||
newRepartition.push({
|
||
typeCongeId: leaveType,
|
||
annee: previousYear,
|
||
jours: aPrendreN1,
|
||
typeDeduction: 'Année N-1'
|
||
});
|
||
|
||
joursRestants -= aPrendreN1;
|
||
console.log(` ✅ Déduit ${aPrendreN1}j de N-1`);
|
||
}
|
||
|
||
// Essayer N
|
||
if (joursRestants > 0) {
|
||
const [compteurN] = await connection.query(
|
||
'SELECT Id, Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?',
|
||
[userId, leaveType, currentYear]
|
||
);
|
||
|
||
if (compteurN.length > 0) {
|
||
const disponibleN = parseFloat(compteurN[0].Solde);
|
||
const aPrendreN = Math.min(disponibleN, joursRestants);
|
||
|
||
await connection.query(
|
||
'UPDATE CompteurConges SET Solde = Solde - ?, DerniereMiseAJour = NOW() WHERE Id = ?',
|
||
[aPrendreN, compteurN[0].Id]
|
||
);
|
||
|
||
await connection.query(`
|
||
INSERT INTO DeductionDetails (DemandeCongeId, TypeCongeId, Annee, JoursUtilises, TypeDeduction)
|
||
VALUES (?, ?, ?, ?, ?)
|
||
`, [requestId, leaveType, currentYear, aPrendreN, 'Année N']);
|
||
|
||
newRepartition.push({
|
||
typeCongeId: leaveType,
|
||
annee: currentYear,
|
||
jours: aPrendreN,
|
||
typeDeduction: 'Année N'
|
||
});
|
||
|
||
joursRestants -= aPrendreN;
|
||
console.log(` ✅ Déduit ${aPrendreN}j de N`);
|
||
}
|
||
}
|
||
|
||
// Anticipé (si encore des jours restants)
|
||
if (joursRestants > 0) {
|
||
const [compteurN] = await connection.query(
|
||
'SELECT Id FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?',
|
||
[userId, leaveType, currentYear]
|
||
);
|
||
|
||
if (compteurN.length > 0) {
|
||
await connection.query(
|
||
'UPDATE CompteurConges SET Solde = Solde - ?, DerniereMiseAJour = NOW() WHERE Id = ?',
|
||
[joursRestants, compteurN[0].Id]
|
||
);
|
||
|
||
await connection.query(`
|
||
INSERT INTO DeductionDetails (DemandeCongeId, TypeCongeId, Annee, JoursUtilises, TypeDeduction, IsAnticipe)
|
||
VALUES (?, ?, ?, ?, ?, 1)
|
||
`, [requestId, leaveType, currentYear, joursRestants, 'N Anticip']);
|
||
|
||
newRepartition.push({
|
||
typeCongeId: leaveType,
|
||
annee: currentYear,
|
||
jours: joursRestants,
|
||
typeDeduction: 'N Anticip'
|
||
});
|
||
|
||
console.log(` ⚠️ Déduit ${joursRestants}j en ANTICIPÉ`);
|
||
}
|
||
}
|
||
}
|
||
|
||
// B. RTT : Uniquement année N
|
||
else if (parseInt(leaveType) === 2) {
|
||
const [compteurRTT] = await connection.query(
|
||
'SELECT Id, Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?',
|
||
[userId, leaveType, currentYear]
|
||
);
|
||
|
||
if (compteurRTT.length > 0) {
|
||
const disponible = parseFloat(compteurRTT[0].Solde);
|
||
|
||
if (disponible < joursRestants) {
|
||
throw new Error(`Solde RTT insuffisant: ${disponible.toFixed(2)}j disponibles, ${joursRestants}j demandés`);
|
||
}
|
||
|
||
await connection.query(
|
||
'UPDATE CompteurConges SET Solde = Solde - ?, DerniereMiseAJour = NOW() WHERE Id = ?',
|
||
[joursRestants, compteurRTT[0].Id]
|
||
);
|
||
|
||
await connection.query(`
|
||
INSERT INTO DeductionDetails (DemandeCongeId, TypeCongeId, Annee, JoursUtilises, TypeDeduction)
|
||
VALUES (?, ?, ?, ?, ?)
|
||
`, [requestId, leaveType, currentYear, joursRestants, 'Année N']);
|
||
|
||
newRepartition.push({
|
||
typeCongeId: leaveType,
|
||
annee: currentYear,
|
||
jours: joursRestants,
|
||
typeDeduction: 'Année N'
|
||
});
|
||
|
||
console.log(` ✅ Déduit ${joursRestants}j RTT de N`);
|
||
}
|
||
}
|
||
}
|
||
|
||
await connection.commit();
|
||
console.log(`✅ Demande ${requestId} modifiée avec succès`);
|
||
|
||
// 5️⃣ ENVOI DES EMAILS (Asynchrone, ne bloque pas la réponse)
|
||
const graphToken = await getGraphToken();
|
||
|
||
if (graphToken) {
|
||
const [managerInfo] = await connection.query(
|
||
`SELECT m.Email, m.Prenom, m.Nom
|
||
FROM CollaborateurAD c
|
||
JOIN HierarchieValidationAD h ON c.id = h.CollaborateurId
|
||
JOIN CollaborateurAD m ON h.SuperieurId = m.id
|
||
WHERE c.id = ?`,
|
||
[userId]
|
||
);
|
||
|
||
if (managerInfo.length > 0) {
|
||
const manager = managerInfo[0];
|
||
|
||
// Email au manager
|
||
sendMailGraph(
|
||
graphToken,
|
||
'gtanoreply@ensup.eu',
|
||
manager.Email,
|
||
'🔄 Modification de demande de congé',
|
||
`
|
||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||
<div style="background: linear-gradient(135deg, #3b82f6 0%, #1e40af 100%); padding: 30px; text-align: center; border-radius: 10px 10px 0 0;">
|
||
<h1 style="color: white; margin: 0;">🔄 Modification de demande</h1>
|
||
</div>
|
||
<div style="background-color: #f8f9fa; padding: 30px; border-radius: 0 0 10px 10px;">
|
||
<p style="font-size: 16px; color: #333;">Bonjour <strong>${manager.Prenom}</strong>,</p>
|
||
<p style="font-size: 16px; color: #333;"><strong>${userName}</strong> a modifié sa demande de congé :</p>
|
||
<div style="background-color: white; padding: 20px; border-radius: 8px; margin: 20px 0; border-left: 4px solid #3b82f6;">
|
||
<table style="width: 100%; border-collapse: collapse;">
|
||
<tr>
|
||
<td style="padding: 8px 0; color: #666;"><strong>Type :</strong></td>
|
||
<td style="padding: 8px 0; color: #333;">${getLeaveTypeName(leaveType)}</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding: 8px 0; color: #666;"><strong>Dates :</strong></td>
|
||
<td style="padding: 8px 0; color: #333;">du ${formatDateFR(startDate)} au ${formatDateFR(endDate)}</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding: 8px 0; color: #666;"><strong>Durée :</strong></td>
|
||
<td style="padding: 8px 0; color: #333;">${businessDays} jour(s)</td>
|
||
</tr>
|
||
</table>
|
||
</div>
|
||
${restorationStats.count > 0 ? `
|
||
<div style="background-color: #dbeafe; padding: 15px; border-radius: 8px; margin-top: 20px;">
|
||
<p style="margin: 0; color: #1e40af;">
|
||
✅ Les compteurs ont été automatiquement recalculés (${restorationStats.count} opération(s)).
|
||
</p>
|
||
</div>
|
||
` : ''}
|
||
<p style="font-size: 16px; color: #333;">Merci de valider ou refuser cette demande dans l'application.</p>
|
||
<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #e5e7eb;">
|
||
<p style="font-size: 14px; color: #666; margin: 0;">📧 Cet email est envoyé automatiquement, merci de ne pas y répondre.</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`
|
||
).catch(err => console.error('❌ Erreur email manager:', err));
|
||
|
||
// Email de confirmation au collaborateur
|
||
sendMailGraph(
|
||
graphToken,
|
||
'gtanoreply@ensup.eu',
|
||
userEmail,
|
||
'✅ Confirmation de modification',
|
||
`
|
||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||
<div style="background: linear-gradient(135deg, #10b981 0%, #059669 100%); padding: 30px; text-align: center; border-radius: 10px 10px 0 0;">
|
||
<h1 style="color: white; margin: 0;">✅ Demande modifiée</h1>
|
||
</div>
|
||
<div style="background-color: #f8f9fa; padding: 30px; border-radius: 0 0 10px 10px;">
|
||
<p style="font-size: 16px; color: #333;">Bonjour <strong>${userName.split(' ')[0]}</strong>,</p>
|
||
<p style="font-size: 16px; color: #333;">Votre demande de congé a bien été modifiée :</p>
|
||
<div style="background-color: white; padding: 20px; border-radius: 8px; margin: 20px 0; border-left: 4px solid #10b981;">
|
||
<table style="width: 100%; border-collapse: collapse;">
|
||
<tr>
|
||
<td style="padding: 8px 0; color: #666;"><strong>Type :</strong></td>
|
||
<td style="padding: 8px 0; color: #333;">${getLeaveTypeName(leaveType)}</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding: 8px 0; color: #666;"><strong>Dates :</strong></td>
|
||
<td style="padding: 8px 0; color: #333;">du ${formatDateFR(startDate)} au ${formatDateFR(endDate)}</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding: 8px 0; color: #666;"><strong>Durée :</strong></td>
|
||
<td style="padding: 8px 0; color: #333;">${businessDays} jour(s)</td>
|
||
</tr>
|
||
</table>
|
||
</div>
|
||
<p style="font-size: 16px; color: #333;">Elle est maintenant <strong style="color: #f59e0b;">en attente de validation</strong>.</p>
|
||
<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #e5e7eb;">
|
||
<p style="font-size: 14px; color: #666; margin: 0;">📧 Cet email est envoyé automatiquement, merci de ne pas y répondre.</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`
|
||
).catch(err => console.error('❌ Erreur email collaborateur:', err));
|
||
}
|
||
}
|
||
|
||
res.json({
|
||
success: true,
|
||
message: '✅ Demande modifiée avec succès',
|
||
newStatus: newStatus,
|
||
restoration: restorationStats,
|
||
repartition: newRepartition
|
||
});
|
||
|
||
} catch (error) {
|
||
if (connection) {
|
||
await connection.rollback();
|
||
}
|
||
console.error('❌ Erreur updateRequest:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: error.message || 'Erreur lors de la modification'
|
||
});
|
||
} finally {
|
||
if (connection) {
|
||
connection.release();
|
||
}
|
||
}
|
||
});
|
||
|
||
|
||
|
||
// ⭐ Fonction helper pour calculer la répartition CP
|
||
function calculateCPRepartition(joursNecessaires, soldeN1, soldeN) {
|
||
const repartition = [];
|
||
let reste = joursNecessaires;
|
||
|
||
// D'abord utiliser N-1
|
||
if (reste > 0 && soldeN1 > 0) {
|
||
const joursN1 = Math.min(reste, soldeN1);
|
||
repartition.push({
|
||
type: 'CP',
|
||
annee: 'N-1',
|
||
jours: joursN1
|
||
});
|
||
reste -= joursN1;
|
||
}
|
||
|
||
// Puis utiliser N
|
||
if (reste > 0 && soldeN > 0) {
|
||
const joursN = Math.min(reste, soldeN);
|
||
repartition.push({
|
||
type: 'CP',
|
||
annee: 'N',
|
||
jours: joursN
|
||
});
|
||
reste -= joursN;
|
||
}
|
||
|
||
return repartition;
|
||
}
|
||
|
||
// ⭐ Fonction helper pour obtenir le champ de compteur
|
||
function getCounterField(type, annee) {
|
||
if (type === 'CP' && annee === 'N-1') return 'SoldeCP_N1';
|
||
if (type === 'CP' && annee === 'N') return 'SoldeCP_N';
|
||
if (type === 'RTT' && annee === 'N') return 'SoldeRTT_N';
|
||
return null;
|
||
}
|
||
|
||
// ⭐ Fonction helper pour le nom du type de congé
|
||
function getLeaveTypeName(typeId) {
|
||
const types = {
|
||
1: 'Congé payé',
|
||
2: 'RTT',
|
||
3: 'Arrêt maladie',
|
||
4: 'Formation',
|
||
5: 'Récupération'
|
||
};
|
||
return types[typeId] || 'Inconnu';
|
||
}
|
||
|
||
// ⭐ Fonction helper pour formater les dates
|
||
function formatDateFR(dateStr) {
|
||
const date = new Date(dateStr);
|
||
return date.toLocaleDateString('fr-FR', {
|
||
day: '2-digit',
|
||
month: '2-digit',
|
||
year: 'numeric'
|
||
});
|
||
}
|
||
/**
|
||
* Route pour SUPPRIMER une demande de congé
|
||
* POST /deleteRequest
|
||
*/
|
||
app.post('/api/deleteRequest', async (req, res) => {
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
await conn.beginTransaction();
|
||
|
||
const { requestId, userId, userEmail, userName } = req.body;
|
||
|
||
if (!requestId || !userId) {
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.status(400).json({
|
||
success: false,
|
||
message: 'Paramètres manquants (requestId ou userId)'
|
||
});
|
||
}
|
||
|
||
console.log('\n🗑️ === ANNULATION DEMANDE ===');
|
||
console.log(`Demande ID: ${requestId}, User ID: ${userId}`);
|
||
|
||
// 1️⃣ Vérifier que la demande existe
|
||
const [existingRequest] = await conn.query(
|
||
`SELECT d.*,
|
||
DATE_FORMAT(d.DateDebut, '%Y-%m-%d') as DateDebut,
|
||
DATE_FORMAT(d.DateFin, '%Y-%m-%d') as DateFin
|
||
FROM DemandeConge d
|
||
WHERE d.Id = ? AND d.CollaborateurADId = ?`,
|
||
[requestId, userId]
|
||
);
|
||
|
||
if (existingRequest.length === 0) {
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.status(404).json({
|
||
success: false,
|
||
message: 'Demande introuvable ou non autorisée'
|
||
});
|
||
}
|
||
|
||
const request = existingRequest[0];
|
||
const requestStatus = request.Statut;
|
||
const dateDebut = new Date(request.DateDebut);
|
||
const aujourdhui = new Date();
|
||
aujourdhui.setHours(0, 0, 0, 0);
|
||
dateDebut.setHours(0, 0, 0, 0);
|
||
|
||
console.log(`📋 Demande: Statut=${requestStatus}, Date début=${dateDebut.toLocaleDateString('fr-FR')}`);
|
||
|
||
// ❌ BLOQUER SI DATE DÉJÀ PASSÉE
|
||
if (dateDebut <= aujourdhui && requestStatus === 'Validée') {
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.status(400).json({
|
||
success: false,
|
||
message: '❌ Impossible d\'annuler : la date de début est déjà passée ou c\'est aujourd\'hui',
|
||
dateDebut: formatDateWithoutUTC(dateDebut)
|
||
});
|
||
}
|
||
|
||
// 2️⃣ RÉCUPÉRER LA RÉPARTITION (pour l'email)
|
||
const [repartition] = await conn.query(`
|
||
SELECT dct.*, tc.Nom as TypeNom
|
||
FROM DemandeCongeType dct
|
||
JOIN TypeConge tc ON dct.TypeCongeId = tc.Id
|
||
WHERE dct.DemandeCongeId = ?
|
||
ORDER BY tc.Nom
|
||
`, [requestId]);
|
||
|
||
// 3️⃣ RESTAURER LES COMPTEURS via DeductionDetails
|
||
let restorationStats = { count: 0, details: [] };
|
||
|
||
if (requestStatus !== 'Refusée' && requestStatus !== 'Annulée') {
|
||
console.log(`🔄 Restauration des compteurs...`);
|
||
|
||
const [deductions] = await conn.query(
|
||
'SELECT * FROM DeductionDetails WHERE DemandeCongeId = ?',
|
||
[requestId]
|
||
);
|
||
|
||
if (deductions.length > 0) {
|
||
for (const d of deductions) {
|
||
const [compteur] = await conn.query(
|
||
'SELECT Id, Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?',
|
||
[userId, d.TypeCongeId, d.Annee]
|
||
);
|
||
|
||
if (compteur.length > 0) {
|
||
const c = compteur[0];
|
||
const newSolde = parseFloat(c.Solde) + parseFloat(d.JoursUtilises);
|
||
|
||
await conn.query(
|
||
'UPDATE CompteurConges SET Solde = ?, DerniereMiseAJour = NOW() WHERE Id = ?',
|
||
[newSolde, c.Id]
|
||
);
|
||
|
||
restorationStats.count++;
|
||
restorationStats.details.push({
|
||
typeCongeId: d.TypeCongeId,
|
||
annee: d.Annee,
|
||
joursRendus: d.JoursUtilises
|
||
});
|
||
|
||
console.log(` ✅ Remboursé ${d.JoursUtilises}j au compteur TypeId=${d.TypeCongeId} Année=${d.Annee}`);
|
||
} else {
|
||
console.warn(`⚠️ Compteur introuvable (Type ${d.TypeCongeId}, Année ${d.Annee})`);
|
||
}
|
||
}
|
||
|
||
// Supprimer les déductions
|
||
await conn.query('DELETE FROM DeductionDetails WHERE DemandeCongeId = ?', [requestId]);
|
||
console.log(` 🧹 ${deductions.length} ligne(s) DeductionDetails supprimée(s)`);
|
||
} else {
|
||
console.log(' ℹ️ Aucune déduction à rembourser');
|
||
}
|
||
}
|
||
|
||
// 4️⃣ METTRE À JOUR LE STATUT
|
||
await conn.query(
|
||
`UPDATE DemandeConge
|
||
SET Statut = 'Annulée',
|
||
DateValidation = NOW(),
|
||
CommentaireValidation = CONCAT(
|
||
COALESCE(CommentaireValidation, ''),
|
||
'\n[Annulée par le collaborateur le ',
|
||
DATE_FORMAT(NOW(), '%d/%m/%Y à %H:%i'),
|
||
']'
|
||
)
|
||
WHERE Id = ?`,
|
||
[requestId]
|
||
);
|
||
|
||
console.log(`✅ Demande ${requestId} marquée comme Annulée`);
|
||
|
||
await conn.commit();
|
||
conn.release();
|
||
|
||
// 5️⃣ ENVOI DES EMAILS
|
||
let emailsSent = { collaborateur: false, manager: false };
|
||
const graphToken = await getGraphToken();
|
||
|
||
if (graphToken) {
|
||
const [collabInfo] = await conn.query(
|
||
'SELECT email, prenom, nom FROM CollaborateurAD WHERE id = ?',
|
||
[userId]
|
||
);
|
||
|
||
const collabEmail = collabInfo.length > 0 ? collabInfo[0].email : userEmail;
|
||
const collabName = collabInfo.length > 0
|
||
? `${collabInfo[0].prenom} ${collabInfo[0].nom}`
|
||
: userName;
|
||
|
||
const dateDebutFormatted = new Date(request.DateDebut).toLocaleDateString('fr-FR');
|
||
const dateFinFormatted = new Date(request.DateFin).toLocaleDateString('fr-FR');
|
||
const datesPeriode = dateDebutFormatted === dateFinFormatted
|
||
? dateDebutFormatted
|
||
: `du ${dateDebutFormatted} au ${dateFinFormatted}`;
|
||
|
||
const repartitionText = repartition.map(r =>
|
||
`<tr>
|
||
<td style="padding: 8px 0; color: #666;"><strong>${r.TypeNom} :</strong></td>
|
||
<td style="padding: 8px 0; color: #333;">${r.NombreJours}j ${r.PeriodeJournee !== 'Journée entière' ? `(${r.PeriodeJournee})` : ''}</td>
|
||
</tr>`
|
||
).join('');
|
||
|
||
// 📧 EMAIL AU COLLABORATEUR
|
||
if (collabEmail) {
|
||
try {
|
||
const subjectCollab = '✅ Confirmation d\'annulation de votre demande';
|
||
const bodyCollab = `
|
||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||
<div style="background: linear-gradient(135deg, #10b981 0%, #059669 100%); padding: 30px; text-align: center; border-radius: 10px 10px 0 0;">
|
||
<h1 style="color: white; margin: 0;">✅ Demande annulée</h1>
|
||
</div>
|
||
|
||
<div style="background-color: #f8f9fa; padding: 30px; border-radius: 0 0 10px 10px;">
|
||
<p style="font-size: 16px; color: #333;">Bonjour <strong>${collabName}</strong>,</p>
|
||
|
||
<p style="font-size: 16px; color: #333;">
|
||
Votre demande de congé a bien été <strong style="color: #10b981;">annulée</strong>.
|
||
</p>
|
||
|
||
<div style="background-color: white; padding: 20px; border-radius: 8px; margin: 20px 0; border-left: 4px solid #10b981;">
|
||
<h3 style="color: #10b981; margin-top: 0;">📋 Demande annulée</h3>
|
||
<table style="width: 100%; border-collapse: collapse;">
|
||
<tr>
|
||
<td style="padding: 8px 0; color: #666;"><strong>Période :</strong></td>
|
||
<td style="padding: 8px 0; color: #333;">${datesPeriode}</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding: 8px 0; color: #666;"><strong>Durée totale :</strong></td>
|
||
<td style="padding: 8px 0; color: #333;">${request.NombreJours} jour(s)</td>
|
||
</tr>
|
||
<tr>
|
||
<td colspan="2" style="padding: 12px 0 4px 0; color: #666; border-top: 1px solid #e5e7eb;">
|
||
<strong>Répartition :</strong>
|
||
</td>
|
||
</tr>
|
||
${repartitionText}
|
||
</table>
|
||
</div>
|
||
|
||
${restorationStats.count > 0 ? `
|
||
<div style="background-color: #dbeafe; padding: 15px; border-radius: 8px; margin-top: 20px;">
|
||
<p style="margin: 0; color: #1e40af;">
|
||
✅ <strong>Vos compteurs ont été restaurés</strong><br>
|
||
${restorationStats.count} opération(s) de remboursement effectuée(s).
|
||
</p>
|
||
</div>
|
||
` : ''}
|
||
|
||
<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #e5e7eb;">
|
||
<p style="font-size: 14px; color: #666; margin: 0;">
|
||
📧 Cet email est envoyé automatiquement, merci de ne pas y répondre.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
await sendMailGraph(graphToken, 'gtanoreply@ensup.eu', collabEmail, subjectCollab, bodyCollab);
|
||
emailsSent.collaborateur = true;
|
||
console.log('✅ Email envoyé au collaborateur');
|
||
} catch (emailError) {
|
||
console.error('❌ Erreur email collaborateur:', emailError.message);
|
||
}
|
||
}
|
||
|
||
// 📧 EMAIL AU MANAGER
|
||
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';
|
||
|
||
if (managerEmail && requestStatus !== 'Refusée' && requestStatus !== 'Annulée') {
|
||
try {
|
||
const isValidated = requestStatus === 'Validée' || requestStatus === 'Validé';
|
||
|
||
const subjectManager = isValidated
|
||
? `🗑️ Annulation de congé validé - ${collabName}`
|
||
: `🗑️ Annulation de demande - ${collabName}`;
|
||
|
||
const bodyManager = `
|
||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||
<div style="background: linear-gradient(135deg, ${isValidated ? '#ef4444' : '#f59e0b'} 0%, ${isValidated ? '#991b1b' : '#d97706'} 100%); padding: 30px; text-align: center; border-radius: 10px 10px 0 0;">
|
||
<h1 style="color: white; margin: 0;">🗑️ Annulation de ${isValidated ? 'congé' : 'demande'}</h1>
|
||
</div>
|
||
|
||
<div style="background-color: #f8f9fa; padding: 30px; border-radius: 0 0 10px 10px;">
|
||
<p style="font-size: 16px; color: #333;">Bonjour ${managerName},</p>
|
||
|
||
<p style="font-size: 16px; color: #333;">
|
||
<strong>${collabName}</strong> a annulé ${isValidated ? 'son congé <strong>validé</strong>' : 'sa demande de congé'}.
|
||
</p>
|
||
|
||
<div style="background-color: white; padding: 20px; border-radius: 8px; margin: 20px 0; border-left: 4px solid ${isValidated ? '#ef4444' : '#f59e0b'};">
|
||
<h3 style="color: ${isValidated ? '#ef4444' : '#f59e0b'}; margin-top: 0;">📋 Demande annulée</h3>
|
||
<table style="width: 100%; border-collapse: collapse;">
|
||
<tr>
|
||
<td style="padding: 8px 0; color: #666;"><strong>Statut initial :</strong></td>
|
||
<td style="padding: 8px 0; color: #333;">${requestStatus}</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding: 8px 0; color: #666;"><strong>Période :</strong></td>
|
||
<td style="padding: 8px 0; color: #333;">${datesPeriode}</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding: 8px 0; color: #666;"><strong>Durée totale :</strong></td>
|
||
<td style="padding: 8px 0; color: #333;">${request.NombreJours} jour(s)</td>
|
||
</tr>
|
||
<tr>
|
||
<td colspan="2" style="padding: 12px 0 4px 0; color: #666; border-top: 1px solid #e5e7eb;">
|
||
<strong>Répartition :</strong>
|
||
</td>
|
||
</tr>
|
||
${repartitionText}
|
||
</table>
|
||
</div>
|
||
|
||
${restorationStats.count > 0 ? `
|
||
<div style="background-color: #dbeafe; padding: 15px; border-radius: 8px; margin-top: 20px;">
|
||
<p style="margin: 0; color: #1e40af;">
|
||
✅ Les compteurs ont été automatiquement restaurés (${restorationStats.count} opération(s)).
|
||
</p>
|
||
</div>
|
||
` : ''}
|
||
|
||
<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #e5e7eb;">
|
||
<p style="font-size: 14px; color: #666; margin: 0;">
|
||
📧 Cet email est envoyé automatiquement, merci de ne pas y répondre.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
await sendMailGraph(graphToken, 'gtanoreply@ensup.eu', managerEmail, subjectManager, bodyManager);
|
||
emailsSent.manager = true;
|
||
console.log('✅ Email envoyé au manager');
|
||
} catch (emailError) {
|
||
console.error('❌ Erreur email manager:', emailError.message);
|
||
}
|
||
}
|
||
}
|
||
|
||
res.json({
|
||
success: true,
|
||
message: 'Demande annulée avec succès',
|
||
restoration: restorationStats,
|
||
emailsSent: emailsSent
|
||
});
|
||
|
||
} catch (error) {
|
||
await conn.rollback();
|
||
console.error('❌ Erreur deleteRequest:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur lors de l\'annulation',
|
||
error: error.message
|
||
});
|
||
} finally {
|
||
if (conn) conn.release();
|
||
}
|
||
});
|
||
|
||
app.get('/api/exportCompteurs', async (req, res) => {
|
||
try {
|
||
const dateRef = req.query.dateRef || new Date().toISOString().split('T')[0];
|
||
const conn = await pool.getConnection();
|
||
|
||
const [collaborateurs] = await pool.query(`
|
||
SELECT
|
||
ca.id,
|
||
ca.prenom,
|
||
ca.nom,
|
||
ca.email,
|
||
ca.role,
|
||
ca.TypeContrat,
|
||
ca.DateEntree,
|
||
s.Nom as service,
|
||
ca.CampusId,
|
||
ca.SocieteId,
|
||
so.Nom as societe_nom
|
||
FROM CollaborateurAD ca
|
||
LEFT JOIN Services s ON ca.ServiceId = s.Id
|
||
LEFT JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE (ca.actif = 1 OR ca.actif IS NULL)
|
||
ORDER BY ca.nom, ca.prenom
|
||
`);
|
||
|
||
const rapport = [];
|
||
|
||
for (const collab of collaborateurs) {
|
||
const dateEntree = collab.DateEntree;
|
||
const dateReference = new Date(dateRef);
|
||
|
||
const acquisCP = calculerAcquisitionCP(dateReference, dateEntree);
|
||
|
||
let acquisRTT = 0;
|
||
if (collab.role !== 'Apprenti') {
|
||
const rttData = await calculerAcquisitionRTT(conn, collab.id, dateReference);
|
||
acquisRTT = rttData.acquisition;
|
||
}
|
||
|
||
const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']);
|
||
const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']);
|
||
|
||
let soldeCP = 0;
|
||
let soldeRTT = 0;
|
||
|
||
if (cpType.length > 0) {
|
||
const [compteurCP] = await conn.query(
|
||
'SELECT Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?',
|
||
[collab.id, cpType[0].Id, dateReference.getFullYear()]
|
||
);
|
||
soldeCP = compteurCP.length > 0 ? parseFloat(compteurCP[0].Solde) : 0;
|
||
}
|
||
|
||
if (rttType.length > 0 && collab.role !== 'Apprenti') {
|
||
const [compteurRTT] = await conn.query(
|
||
'SELECT Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?',
|
||
[collab.id, rttType[0].Id, dateReference.getFullYear()]
|
||
);
|
||
soldeRTT = compteurRTT.length > 0 ? parseFloat(compteurRTT[0].Solde) : 0;
|
||
}
|
||
|
||
rapport.push({
|
||
id: collab.id,
|
||
prenom: collab.prenom,
|
||
nom: collab.nom,
|
||
email: collab.email,
|
||
role: collab.role,
|
||
service: collab.service,
|
||
societe_id: collab.SocieteId,
|
||
societe_nom: collab.societe_nom,
|
||
type_contrat: collab.TypeContrat,
|
||
date_entree: dateEntree ? formatDateWithoutUTC(dateEntree) : null,
|
||
cp_acquis: parseFloat(acquisCP.toFixed(2)),
|
||
cp_solde: parseFloat(soldeCP.toFixed(2)),
|
||
rtt_acquis: parseFloat(acquisRTT.toFixed(2)),
|
||
rtt_solde: parseFloat(soldeRTT.toFixed(2)),
|
||
date_reference: dateRef
|
||
});
|
||
}
|
||
|
||
conn.release();
|
||
|
||
res.json({
|
||
success: true,
|
||
date_reference: dateRef,
|
||
total_collaborateurs: rapport.length,
|
||
rapport: rapport
|
||
});
|
||
} catch (error) {
|
||
console.error('Erreur exportCompteurs:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur serveur',
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
|
||
function isInPeriodeAnticipation(dateDebut, typeConge) {
|
||
const date = new Date(dateDebut);
|
||
const year = date.getFullYear();
|
||
const month = date.getMonth() + 1; // 1-12
|
||
|
||
if (typeConge === 'CP') {
|
||
// CP : 01/06 année N → 31/05 année N+1
|
||
// Période anticipation : du 01/06 de l'année suivante
|
||
return month >= 6; // Si >= juin, c'est pour l'exercice N+1
|
||
} else if (typeConge === 'RTT') {
|
||
// RTT : 01/01 année N → 31/12 année N
|
||
// Pas d'anticipation possible car année civile
|
||
return month >= 1 && month <= 12;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
function getAnneeCompteur(dateDebut, typeConge) {
|
||
const date = new Date(dateDebut);
|
||
const year = date.getFullYear();
|
||
const month = date.getMonth() + 1;
|
||
|
||
if (typeConge === 'CP') {
|
||
// Si date entre 01/06 et 31/12 → année N
|
||
// Si date entre 01/01 et 31/05 → année N-1 (exercice précédent)
|
||
return month >= 6 ? year : year - 1;
|
||
} else {
|
||
// RTT : toujours année civile
|
||
return year;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Vérifie la disponibilité des soldes pour une demande
|
||
* Retourne : { available: boolean, details: {}, useN1: boolean }
|
||
*/
|
||
|
||
|
||
async function checkSoldesDisponiblesMixte(conn, collaborateurId, repartition, dateDebut, isApprenti) {
|
||
const today = new Date();
|
||
today.setHours(0, 0, 0, 0);
|
||
|
||
const currentYear = today.getFullYear();
|
||
const dateDemandeObj = new Date(dateDebut);
|
||
dateDemandeObj.setHours(0, 0, 0, 0);
|
||
|
||
const demandeYear = dateDemandeObj.getFullYear();
|
||
const demandeMonth = dateDemandeObj.getMonth() + 1;
|
||
|
||
console.log('\n🔍 === CHECK SOLDES MIXTE (AVEC ANTICIPATION) ===');
|
||
console.log('📅 Date AUJOURD\'HUI:', today.toISOString().split('T')[0]);
|
||
console.log('📅 Date DEMANDE:', dateDebut);
|
||
console.log('📅 Année demande:', demandeYear, '/ Mois:', demandeMonth);
|
||
console.log('📅 Année actuelle:', currentYear);
|
||
|
||
let totalDisponible = 0;
|
||
let totalNecessaire = 0;
|
||
const details = {};
|
||
|
||
for (const rep of repartition) {
|
||
const typeCode = rep.TypeConge;
|
||
const joursNecessaires = parseFloat(rep.NombreJours || 0);
|
||
|
||
// Ignorer ABS et Formation
|
||
if (typeCode === 'ABS' || typeCode === 'Formation') {
|
||
continue;
|
||
}
|
||
|
||
totalNecessaire += joursNecessaires;
|
||
|
||
if (typeCode === 'CP') {
|
||
// ⭐ RÉCUPÉRER LES INFOS COLLABORATEUR
|
||
const [collabInfo] = await conn.query(
|
||
`SELECT DateEntree FROM CollaborateurAD WHERE id = ?`,
|
||
[collaborateurId]
|
||
);
|
||
const dateEntree = collabInfo[0]?.DateEntree;
|
||
|
||
// ⭐ CALCULER L'ACQUISITION JUSQU'À LA DATE DEMANDÉE
|
||
const acquisALaDate = calculerAcquisitionCP(dateDemandeObj, dateEntree);
|
||
|
||
console.log('💰 Acquisition CP à la date', dateDebut, ':', acquisALaDate.toFixed(2), 'j');
|
||
|
||
// ⭐ RÉCUPÉRER LE REPORTÉ N-1
|
||
const previousYear = currentYear - 1;
|
||
const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']);
|
||
const cpTypeId = cpType[0].Id;
|
||
|
||
const [compteurN1] = await conn.query(`
|
||
SELECT SoldeReporte
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collaborateurId, cpTypeId, previousYear]);
|
||
|
||
const reporteN1 = compteurN1.length > 0 ? parseFloat(compteurN1[0].SoldeReporte || 0) : 0;
|
||
|
||
// ⭐ RÉCUPÉRER CE QUI A DÉJÀ ÉTÉ POSÉ (toutes demandes validées ou en attente)
|
||
const [totalPose] = await conn.query(`
|
||
SELECT COALESCE(SUM(dct.NombreJours), 0) as totalPose
|
||
FROM DemandeConge dc
|
||
JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId
|
||
JOIN TypeConge tc ON dct.TypeCongeId = tc.Id
|
||
WHERE dc.CollaborateurADId = ?
|
||
AND tc.Nom = 'Congé payé'
|
||
AND dc.Statut IN ('Validée', 'En attente')
|
||
AND dc.DateDebut <= ?
|
||
`, [collaborateurId, dateDebut]);
|
||
|
||
const dejaPose = parseFloat(totalPose[0].totalPose || 0);
|
||
|
||
// ⭐ SOLDE RÉEL = Reporté N-1 + Acquisition - Déjà posé
|
||
const soldeReel = reporteN1 + acquisALaDate - dejaPose;
|
||
|
||
console.log('💰 Soldes CP détaillés:', {
|
||
reporteN1: reporteN1.toFixed(2),
|
||
acquisALaDate: acquisALaDate.toFixed(2),
|
||
dejaPose: dejaPose.toFixed(2),
|
||
soldeReel: soldeReel.toFixed(2)
|
||
});
|
||
|
||
details.CP = {
|
||
reporteN1: reporteN1,
|
||
acquisALaDate: acquisALaDate,
|
||
dejaPose: dejaPose,
|
||
soldeReel: soldeReel,
|
||
necessaire: joursNecessaires
|
||
};
|
||
|
||
totalDisponible += Math.max(0, soldeReel);
|
||
|
||
if (soldeReel < joursNecessaires) {
|
||
return {
|
||
available: false,
|
||
message: `Solde CP insuffisant (${Math.max(0, soldeReel).toFixed(2)}j disponibles avec anticipation, ${joursNecessaires}j demandés)`,
|
||
details,
|
||
manque: joursNecessaires - soldeReel
|
||
};
|
||
}
|
||
|
||
} else if (typeCode === 'RTT') {
|
||
if (isApprenti) {
|
||
return {
|
||
available: false,
|
||
message: 'Les apprentis ne peuvent pas poser de RTT',
|
||
details
|
||
};
|
||
}
|
||
|
||
// ⭐ CALCUL RTT (utiliser la fonction existante)
|
||
const rttData = await calculerAcquisitionRTT(conn, collaborateurId, dateDemandeObj);
|
||
const acquisALaDate = rttData.acquisition;
|
||
|
||
console.log('💰 Acquisition RTT à la date', dateDebut, ':', acquisALaDate.toFixed(2), 'j');
|
||
|
||
// ⭐ RÉCUPÉRER CE QUI A DÉJÀ ÉTÉ POSÉ
|
||
const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']);
|
||
const rttTypeId = rttType[0].Id;
|
||
|
||
const [totalPose] = await conn.query(`
|
||
SELECT COALESCE(SUM(dct.NombreJours), 0) as totalPose
|
||
FROM DemandeConge dc
|
||
JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId
|
||
JOIN TypeConge tc ON dct.TypeCongeId = tc.Id
|
||
WHERE dc.CollaborateurADId = ?
|
||
AND tc.Nom = 'RTT'
|
||
AND dc.Statut IN ('Validée', 'En attente')
|
||
AND dc.DateDebut <= ?
|
||
`, [collaborateurId, dateDebut]);
|
||
|
||
const dejaPose = parseFloat(totalPose[0].totalPose || 0);
|
||
|
||
// ⭐ SOLDE RÉEL = Acquisition - Déjà posé
|
||
const soldeReel = acquisALaDate - dejaPose;
|
||
|
||
console.log('💰 Soldes RTT détaillés:', {
|
||
acquisALaDate: acquisALaDate.toFixed(2),
|
||
dejaPose: dejaPose.toFixed(2),
|
||
soldeReel: soldeReel.toFixed(2)
|
||
});
|
||
|
||
details.RTT = {
|
||
acquisALaDate: acquisALaDate,
|
||
dejaPose: dejaPose,
|
||
soldeReel: soldeReel,
|
||
necessaire: joursNecessaires
|
||
};
|
||
|
||
totalDisponible += Math.max(0, soldeReel);
|
||
|
||
if (soldeReel < joursNecessaires) {
|
||
return {
|
||
available: false,
|
||
message: `Solde RTT insuffisant (${Math.max(0, soldeReel).toFixed(2)}j disponibles avec anticipation, ${joursNecessaires}j demandés)`,
|
||
details,
|
||
manque: joursNecessaires - soldeReel
|
||
};
|
||
}
|
||
|
||
} else if (typeCode === 'Récup') {
|
||
const [recupType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Récupération']);
|
||
if (recupType.length === 0) continue;
|
||
|
||
const recupTypeId = recupType[0].Id;
|
||
|
||
const [compteur] = await conn.query(
|
||
`SELECT Solde FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, recupTypeId, currentYear]
|
||
);
|
||
|
||
const soldeRecup = compteur.length > 0 ? parseFloat(compteur[0].Solde || 0) : 0;
|
||
|
||
console.log('💰 Solde Récup:', soldeRecup.toFixed(2), 'j');
|
||
|
||
details.Recup = {
|
||
soldeN: soldeRecup,
|
||
total: soldeRecup,
|
||
necessaire: joursNecessaires
|
||
};
|
||
|
||
totalDisponible += Math.min(joursNecessaires, soldeRecup);
|
||
|
||
if (soldeRecup < joursNecessaires) {
|
||
return {
|
||
available: false,
|
||
message: `Solde Récupération insuffisant (${soldeRecup.toFixed(2)}j disponibles, ${joursNecessaires}j demandés)`,
|
||
details,
|
||
manque: joursNecessaires - soldeRecup
|
||
};
|
||
}
|
||
}
|
||
}
|
||
|
||
console.log('\n✅ Check final:', {
|
||
totalDisponible: totalDisponible.toFixed(2),
|
||
totalNecessaire: totalNecessaire.toFixed(2),
|
||
available: totalDisponible >= totalNecessaire
|
||
});
|
||
|
||
return {
|
||
available: totalDisponible >= totalNecessaire,
|
||
details,
|
||
totalDisponible,
|
||
totalNecessaire
|
||
};
|
||
}
|
||
// ========================================
|
||
// FONCTIONS HELPER
|
||
// ========================================
|
||
|
||
async function getSoldesCP(conn, collaborateurId, dateEntree, includeN1Anticipe = false) {
|
||
const currentYear = new Date().getFullYear();
|
||
const previousYear = currentYear - 1;
|
||
|
||
console.log(`\n📊 getSoldesCP - includeN1Anticipe: ${includeN1Anticipe}`);
|
||
|
||
const [cpType] = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = 'Congé payé' LIMIT 1`);
|
||
const typeCongeId = cpType[0].Id;
|
||
|
||
// N-1 (reporté)
|
||
const [compteursN1] = await conn.query(
|
||
`SELECT Solde, SoldeReporte FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, typeCongeId, previousYear]
|
||
);
|
||
|
||
const soldeN1 = compteursN1.length > 0 ? parseFloat(compteursN1[0].SoldeReporte || 0) : 0;
|
||
|
||
// N (actuel)
|
||
const [compteursN] = await conn.query(
|
||
`SELECT Solde, SoldeReporte, Total FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, typeCongeId, currentYear]
|
||
);
|
||
|
||
const soldeN = compteursN.length > 0
|
||
? parseFloat(compteursN[0].Solde || 0) - parseFloat(compteursN[0].SoldeReporte || 0)
|
||
: 0;
|
||
const totalAcquisN = compteursN.length > 0 ? parseFloat(compteursN[0].Total || 0) : 0;
|
||
|
||
// Anticipation N
|
||
const finExerciceN = new Date(currentYear + 1, 4, 31); // 31 mai N+1
|
||
const acquisTotaleN = calculerAcquisitionCP(finExerciceN, dateEntree);
|
||
const soldeAnticipeN = Math.max(0, acquisTotaleN - totalAcquisN);
|
||
|
||
console.log(' N-1:', soldeN1);
|
||
console.log(' N:', soldeN);
|
||
console.log(' Anticipé N:', soldeAnticipeN);
|
||
|
||
// ⭐ Anticipation N+1 (si demandé)
|
||
let soldeAnticipeN1 = 0;
|
||
if (includeN1Anticipe) {
|
||
const debutExerciceN1 = new Date(currentYear + 1, 5, 1); // 01 juin N+1
|
||
const finExerciceN1 = new Date(currentYear + 2, 4, 31); // 31 mai N+2
|
||
|
||
let dateCalculN1 = debutExerciceN1;
|
||
if (dateEntree && new Date(dateEntree) > debutExerciceN1) {
|
||
dateCalculN1 = new Date(dateEntree);
|
||
}
|
||
|
||
const acquisTotaleN1 = calculerAcquisitionCP(finExerciceN1, dateCalculN1);
|
||
soldeAnticipeN1 = acquisTotaleN1;
|
||
|
||
console.log(' Anticipé N+1:', soldeAnticipeN1);
|
||
}
|
||
|
||
return { soldeN1, soldeN, soldeAnticipeN, soldeAnticipeN1 };
|
||
}
|
||
|
||
async function getSoldesRTT(conn, collaborateurId, typeContrat, dateEntree) {
|
||
const currentYear = new Date().getFullYear();
|
||
|
||
const rttType = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = 'RTT' LIMIT 1`);
|
||
const typeCongeId = rttType[0].Id;
|
||
|
||
const compteursN = await conn.query(
|
||
`SELECT Solde, Total FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, typeCongeId, currentYear]
|
||
);
|
||
|
||
const soldeN = compteursN.length > 0 ? parseFloat(compteursN[0].Solde || 0) : 0;
|
||
const totalAcquisN = compteursN.length > 0 ? parseFloat(compteursN[0].Total || 0) : 0;
|
||
|
||
// Calcul anticipation N
|
||
const finAnneeN = new Date(currentYear, 11, 31); // 31 déc N
|
||
const rttDataTotalN = await calculerAcquisitionRTT(conn, collaborateurId, finAnneeN);
|
||
const soldeAnticipeN = Math.max(0, rttDataTotalN.acquisition - totalAcquisN);
|
||
|
||
return { soldeN, soldeAnticipeN };
|
||
}
|
||
|
||
async function getSoldesRecup(conn, collaborateurId) {
|
||
const currentYear = new Date().getFullYear();
|
||
|
||
const recupType = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = 'Récupération' LIMIT 1`);
|
||
if (recupType.length === 0) return 0;
|
||
|
||
const typeCongeId = recupType[0].Id;
|
||
|
||
const compteur = await conn.query(
|
||
`SELECT Solde FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, typeCongeId, currentYear]
|
||
);
|
||
|
||
return compteur.length > 0 ? parseFloat(compteur[0].Solde || 0) : 0;
|
||
}
|
||
|
||
app.get('/api/getAnticipationDisponible', async (req, res) => {
|
||
try {
|
||
const { userid, dateDebut } = req.query;
|
||
|
||
if (!userid || !dateDebut) {
|
||
return res.json({ success: false, message: 'Paramètres manquants' });
|
||
}
|
||
|
||
const conn = await pool.getConnection();
|
||
|
||
const [collabInfo] = await conn.query(
|
||
`SELECT DateEntree, TypeContrat, role FROM CollaborateurAD WHERE id = ?`,
|
||
[userid]
|
||
);
|
||
|
||
const dateEntree = collabInfo.DateEntree;
|
||
const isApprenti = collabInfo.role === 'Apprenti';
|
||
|
||
// Déterminer si c'est une demande N+1
|
||
const dateDemandeObj = new Date(dateDebut);
|
||
const currentYear = new Date().getFullYear();
|
||
const demandeYear = dateDemandeObj.getFullYear();
|
||
const demandeMonth = dateDemandeObj.getMonth() + 1;
|
||
|
||
const isN1 = (demandeYear === currentYear + 1 && demandeMonth >= 6) ||
|
||
(demandeYear === currentYear + 2 && demandeMonth <= 5);
|
||
|
||
// Calculer les soldes avec anticipation
|
||
const soldesCP = await getSoldesCP(conn, userid, dateEntree, isN1);
|
||
const soldesRTT = isApprenti ? { soldeN: 0, soldeAnticipeN: 0 } :
|
||
await getSoldesRTT(conn, userid, collabInfo.TypeContrat, dateEntree);
|
||
const soldesRecup = await getSoldesRecup(conn, userid);
|
||
|
||
conn.release();
|
||
|
||
res.json({
|
||
success: true,
|
||
isN1Request: isN1,
|
||
CP: {
|
||
actuel: soldesCP.soldeN1 + soldesCP.soldeN,
|
||
anticipeN: soldesCP.soldeAnticipeN,
|
||
anticipeN1: isN1 ? soldesCP.soldeAnticipeN1 : 0,
|
||
total: soldesCP.soldeN1 + soldesCP.soldeN + soldesCP.soldeAnticipeN + (isN1 ? soldesCP.soldeAnticipeN1 : 0)
|
||
},
|
||
RTT: {
|
||
actuel: soldesRTT.soldeN,
|
||
anticipeN: soldesRTT.soldeAnticipeN,
|
||
total: soldesRTT.soldeN + soldesRTT.soldeAnticipeN
|
||
},
|
||
Recup: {
|
||
actuel: soldesRecup,
|
||
total: soldesRecup
|
||
}
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('Erreur getAnticipationDisponible:', error);
|
||
res.status(500).json({ success: false, message: error.message });
|
||
}
|
||
});
|
||
|
||
|
||
|
||
async function deductLeaveBalanceWithN1(conn, collaborateurId, typeCongeId, nombreJours, demandeCongeId, dateDebut) {
|
||
const currentYear = new Date().getFullYear();
|
||
const previousYear = currentYear - 1;
|
||
const nextYear = currentYear + 1;
|
||
|
||
let joursRestants = nombreJours;
|
||
const deductions = [];
|
||
|
||
const dateDemandeObj = new Date(dateDebut);
|
||
const demandeYear = dateDemandeObj.getFullYear();
|
||
const demandeMonth = dateDemandeObj.getMonth() + 1;
|
||
|
||
// Déterminer le type de congé
|
||
const [typeRow] = await conn.query('SELECT Nom FROM TypeConge WHERE Id = ?', [typeCongeId]);
|
||
const typeNom = typeRow[0].Nom;
|
||
const isCP = typeNom === 'Congé payé';
|
||
|
||
// Vérifier si demande pour N+1
|
||
let useN1 = false;
|
||
if (isCP) {
|
||
useN1 = (demandeYear === nextYear && demandeMonth >= 6) ||
|
||
(demandeYear === nextYear + 1 && demandeMonth <= 5);
|
||
} else {
|
||
useN1 = demandeYear === nextYear;
|
||
}
|
||
|
||
console.log(`\n💰 Déduction ${typeNom}: ${nombreJours}j (useN1: ${useN1})`);
|
||
|
||
if (useN1) {
|
||
// ORDRE N+1 : N+1 anticipé → N anticipé → N actuel → N-1
|
||
|
||
// 1. N+1 Anticipé (priorité absolue)
|
||
const compteurN1Anticipe = await conn.query(
|
||
`SELECT Id, SoldeAnticipe FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, typeCongeId, nextYear]
|
||
);
|
||
|
||
if (compteurN1Anticipe.length === 0 && joursRestants > 0) {
|
||
// Créer le compteur N+1 si inexistant
|
||
await conn.query(
|
||
`INSERT INTO CompteurConges
|
||
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, SoldeAnticipe, IsAnticipe)
|
||
VALUES (?, ?, ?, 0, 0, 0, 0, 0)`,
|
||
[collaborateurId, typeCongeId, nextYear]
|
||
);
|
||
}
|
||
|
||
// Récupérer à nouveau après création
|
||
const compteurN1A = await conn.query(
|
||
`SELECT Id, SoldeAnticipe FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, typeCongeId, nextYear]
|
||
);
|
||
|
||
if (compteurN1A.length > 0) {
|
||
const aDeduire = Math.min(joursRestants, joursRestants); // Tous les jours restants
|
||
|
||
if (aDeduire > 0) {
|
||
await conn.query(
|
||
`UPDATE CompteurConges
|
||
SET SoldeAnticipe = SoldeAnticipe + ?, IsAnticipe = 1
|
||
WHERE Id = ?`,
|
||
[aDeduire, compteurN1A[0].Id]
|
||
);
|
||
|
||
await conn.query(
|
||
`INSERT INTO DeductionDetails
|
||
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
|
||
VALUES (?, ?, ?, 'N+1 Anticipé', ?)`,
|
||
[demandeCongeId, typeCongeId, nextYear, aDeduire]
|
||
);
|
||
|
||
deductions.push({
|
||
annee: nextYear,
|
||
type: 'N+1 Anticipé',
|
||
joursUtilises: aDeduire
|
||
});
|
||
|
||
joursRestants -= aDeduire;
|
||
console.log(`✓ N+1 Anticipé: ${aDeduire}j - reste: ${joursRestants}j`);
|
||
}
|
||
}
|
||
// 2. N anticipé
|
||
if (joursRestants > 0) {
|
||
const [compteurN_Anticipe] = await conn.query(`
|
||
SELECT Id, SoldeAnticipe
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collaborateurId, typeCongeId, currentYear]);
|
||
|
||
if (compteurN_Anticipe.length > 0) {
|
||
const soldeNA = parseFloat(compteurN_Anticipe[0].SoldeAnticipe || 0);
|
||
const aDeduire = Math.min(soldeNA, joursRestants);
|
||
|
||
if (aDeduire > 0) {
|
||
await conn.query(`
|
||
UPDATE CompteurConges
|
||
SET SoldeAnticipe = GREATEST(0, SoldeAnticipe - ?)
|
||
WHERE Id = ?
|
||
`, [aDeduire, compteurN_Anticipe[0].Id]);
|
||
|
||
await conn.query(`
|
||
INSERT INTO DeductionDetails
|
||
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
|
||
VALUES (?, ?, ?, 'N Anticipé', ?)
|
||
`, [demandeCongeId, typeCongeId, currentYear, aDeduire]);
|
||
|
||
deductions.push({
|
||
annee: currentYear,
|
||
type: 'N Anticipé',
|
||
joursUtilises: aDeduire,
|
||
soldeAvant: soldeNA
|
||
});
|
||
|
||
joursRestants -= aDeduire;
|
||
console.log(` ✓ N Anticipé: ${aDeduire}j (reste: ${joursRestants}j)`);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 3. N actuel
|
||
if (joursRestants > 0) {
|
||
const [compteurN] = await conn.query(`
|
||
SELECT Id, Solde, SoldeReporte
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collaborateurId, typeCongeId, currentYear]);
|
||
|
||
if (compteurN.length > 0) {
|
||
const soldeN = parseFloat(compteurN[0].Solde) - parseFloat(compteurN[0].SoldeReporte || 0);
|
||
const aDeduire = Math.min(soldeN, joursRestants);
|
||
|
||
if (aDeduire > 0) {
|
||
await conn.query(`
|
||
UPDATE CompteurConges
|
||
SET Solde = GREATEST(0, Solde - ?)
|
||
WHERE Id = ?
|
||
`, [aDeduire, compteurN[0].Id]);
|
||
|
||
await conn.query(`
|
||
INSERT INTO DeductionDetails
|
||
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
|
||
VALUES (?, ?, ?, 'Année N', ?)
|
||
`, [demandeCongeId, typeCongeId, currentYear, aDeduire]);
|
||
|
||
deductions.push({
|
||
annee: currentYear,
|
||
type: 'Année N',
|
||
joursUtilises: aDeduire,
|
||
soldeAvant: soldeN
|
||
});
|
||
|
||
joursRestants -= aDeduire;
|
||
console.log(` ✓ Année N: ${aDeduire}j (reste: ${joursRestants}j)`);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 4. N-1 reporté
|
||
if (joursRestants > 0) {
|
||
const [compteurN1] = await conn.query(`
|
||
SELECT Id, SoldeReporte
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collaborateurId, typeCongeId, previousYear]);
|
||
|
||
if (compteurN1.length > 0) {
|
||
const soldeN1 = parseFloat(compteurN1[0].SoldeReporte || 0);
|
||
const aDeduire = Math.min(soldeN1, joursRestants);
|
||
|
||
if (aDeduire > 0) {
|
||
await conn.query(`
|
||
UPDATE CompteurConges
|
||
SET SoldeReporte = GREATEST(0, SoldeReporte - ?),
|
||
Solde = GREATEST(0, Solde - ?)
|
||
WHERE Id = ?
|
||
`, [aDeduire, aDeduire, compteurN1[0].Id]);
|
||
|
||
await conn.query(`
|
||
INSERT INTO DeductionDetails
|
||
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
|
||
VALUES (?, ?, ?, 'Reporté N-1', ?)
|
||
`, [demandeCongeId, typeCongeId, previousYear, aDeduire]);
|
||
|
||
deductions.push({
|
||
annee: previousYear,
|
||
type: 'Reporté N-1',
|
||
joursUtilises: aDeduire,
|
||
soldeAvant: soldeN1
|
||
});
|
||
|
||
joursRestants -= aDeduire;
|
||
console.log(` ✓ Reporté N-1: ${aDeduire}j (reste: ${joursRestants}j)`);
|
||
}
|
||
}
|
||
}
|
||
|
||
} else {
|
||
// ORDRE NORMAL : N-1 → N → N anticipé
|
||
|
||
// 1. Reporté N-1
|
||
const [compteurN1] = await conn.query(`
|
||
SELECT Id, SoldeReporte, Solde
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collaborateurId, typeCongeId, previousYear]);
|
||
|
||
if (compteurN1.length > 0 && compteurN1[0].SoldeReporte > 0) {
|
||
const soldeN1 = parseFloat(compteurN1[0].SoldeReporte);
|
||
const aDeduire = Math.min(soldeN1, joursRestants);
|
||
|
||
if (aDeduire > 0) {
|
||
await conn.query(`
|
||
UPDATE CompteurConges
|
||
SET SoldeReporte = GREATEST(0, SoldeReporte - ?),
|
||
Solde = GREATEST(0, Solde - ?)
|
||
WHERE Id = ?
|
||
`, [aDeduire, aDeduire, compteurN1[0].Id]);
|
||
|
||
await conn.query(`
|
||
INSERT INTO DeductionDetails
|
||
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
|
||
VALUES (?, ?, ?, 'Reporté N-1', ?)
|
||
`, [demandeCongeId, typeCongeId, previousYear, aDeduire]);
|
||
|
||
deductions.push({
|
||
annee: previousYear,
|
||
type: 'Reporté N-1',
|
||
joursUtilises: aDeduire,
|
||
soldeAvant: soldeN1
|
||
});
|
||
|
||
joursRestants -= aDeduire;
|
||
console.log(` ✓ Reporté N-1: ${aDeduire}j (reste: ${joursRestants}j)`);
|
||
}
|
||
}
|
||
|
||
// 2. Année N
|
||
if (joursRestants > 0) {
|
||
const [compteurN] = await conn.query(`
|
||
SELECT Id, Solde, SoldeReporte
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collaborateurId, typeCongeId, currentYear]);
|
||
|
||
if (compteurN.length > 0) {
|
||
const soldeN = parseFloat(compteurN[0].Solde) - parseFloat(compteurN[0].SoldeReporte || 0);
|
||
const aDeduire = Math.min(soldeN, joursRestants);
|
||
|
||
if (aDeduire > 0) {
|
||
await conn.query(`
|
||
UPDATE CompteurConges
|
||
SET Solde = GREATEST(0, Solde - ?)
|
||
WHERE Id = ?
|
||
`, [aDeduire, compteurN[0].Id]);
|
||
|
||
await conn.query(`
|
||
INSERT INTO DeductionDetails
|
||
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
|
||
VALUES (?, ?, ?, 'Année N', ?)
|
||
`, [demandeCongeId, typeCongeId, currentYear, aDeduire]);
|
||
|
||
deductions.push({
|
||
annee: currentYear,
|
||
type: 'Année N',
|
||
joursUtilises: aDeduire,
|
||
soldeAvant: soldeN
|
||
});
|
||
|
||
joursRestants -= aDeduire;
|
||
console.log(` ✓ Année N: ${aDeduire}j (reste: ${joursRestants}j)`);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 3. N anticipé
|
||
if (joursRestants > 0) {
|
||
const [compteurN_Anticipe] = await conn.query(`
|
||
SELECT Id, SoldeAnticipe
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collaborateurId, typeCongeId, currentYear]);
|
||
|
||
if (compteurN_Anticipe.length > 0) {
|
||
const soldeNA = parseFloat(compteurN_Anticipe[0].SoldeAnticipe || 0);
|
||
const aDeduire = Math.min(soldeNA, joursRestants);
|
||
|
||
if (aDeduire > 0) {
|
||
await conn.query(`
|
||
UPDATE CompteurConges
|
||
SET SoldeAnticipe = GREATEST(0, SoldeAnticipe - ?)
|
||
WHERE Id = ?
|
||
`, [aDeduire, compteurN_Anticipe[0].Id]);
|
||
|
||
await conn.query(`
|
||
INSERT INTO DeductionDetails
|
||
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
|
||
VALUES (?, ?, ?, 'N Anticipé', ?)
|
||
`, [demandeCongeId, typeCongeId, currentYear, aDeduire]);
|
||
|
||
deductions.push({
|
||
annee: currentYear,
|
||
type: 'N Anticipé',
|
||
joursUtilises: aDeduire,
|
||
soldeAvant: soldeNA
|
||
});
|
||
|
||
joursRestants -= aDeduire;
|
||
console.log(` ✓ N Anticipé: ${aDeduire}j (reste: ${joursRestants}j)`);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return {
|
||
success: joursRestants === 0,
|
||
joursDeduitsTotal: nombreJours - joursRestants,
|
||
joursNonDeduits: joursRestants,
|
||
details: deductions,
|
||
useN1: useN1
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Met à jour les soldes anticipés pour un collaborateur
|
||
* Appelée après chaque mise à jour de compteur ou soumission de demande
|
||
*/
|
||
async function updateSoldeAnticipe(conn, collaborateurId) {
|
||
const today = new Date();
|
||
today.setHours(0, 0, 0, 0);
|
||
const currentYear = today.getFullYear();
|
||
|
||
console.log(`\n🔄 Mise à jour soldes anticipés pour collaborateur ${collaborateurId}`);
|
||
|
||
const [collab] = await conn.query(
|
||
'SELECT DateEntree, TypeContrat, role FROM CollaborateurAD WHERE id = ?',
|
||
[collaborateurId]
|
||
);
|
||
|
||
if (collab.length === 0) {
|
||
console.log(' ❌ Collaborateur non trouvé');
|
||
return;
|
||
}
|
||
|
||
const dateEntree = collab[0].DateEntree;
|
||
const typeContrat = collab[0].TypeContrat || '37h';
|
||
const isApprenti = collab[0].role === 'Apprenti';
|
||
|
||
// ===== CP ANTICIPÉ =====
|
||
const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']);
|
||
|
||
if (cpType.length > 0) {
|
||
const cpAnticipe = calculerAcquisitionCPAnticipee(today, dateEntree);
|
||
|
||
// Vérifier si le compteur existe
|
||
const [compteurCP] = await conn.query(`
|
||
SELECT Id FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collaborateurId, cpType[0].Id, currentYear]);
|
||
|
||
if (compteurCP.length > 0) {
|
||
await conn.query(`
|
||
UPDATE CompteurConges
|
||
SET SoldeAnticipe = ?,
|
||
DerniereMiseAJour = NOW()
|
||
WHERE Id = ?
|
||
`, [cpAnticipe, compteurCP[0].Id]);
|
||
} else {
|
||
// Créer le compteur s'il n'existe pas
|
||
const acquisCP = calculerAcquisitionCP(today, dateEntree);
|
||
await conn.query(`
|
||
INSERT INTO CompteurConges
|
||
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, SoldeAnticipe, DerniereMiseAJour)
|
||
VALUES (?, ?, ?, ?, ?, 0, ?, NOW())
|
||
`, [collaborateurId, cpType[0].Id, currentYear, acquisCP, acquisCP, cpAnticipe]);
|
||
}
|
||
|
||
console.log(` ✓ CP Anticipé: ${cpAnticipe.toFixed(2)}j`);
|
||
}
|
||
|
||
// ===== RTT ANTICIPÉ =====
|
||
if (!isApprenti) {
|
||
const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']);
|
||
|
||
if (rttType.length > 0) {
|
||
const rttAnticipe = await calculerAcquisitionRTTAnticipee(conn, collaborateurId, today);
|
||
|
||
// Vérifier si le compteur existe
|
||
const [compteurRTT] = await conn.query(`
|
||
SELECT Id FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collaborateurId, rttType[0].Id, currentYear]);
|
||
|
||
if (compteurRTT.length > 0) {
|
||
await conn.query(`
|
||
UPDATE CompteurConges
|
||
SET SoldeAnticipe = ?,
|
||
DerniereMiseAJour = NOW()
|
||
WHERE Id = ?
|
||
`, [rttAnticipe, compteurRTT[0].Id]);
|
||
} else {
|
||
// Créer le compteur s'il n'existe pas
|
||
const rttData = await calculerAcquisitionRTT(conn, collaborateurId, today);
|
||
await conn.query(`
|
||
INSERT INTO CompteurConges
|
||
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, SoldeAnticipe, DerniereMiseAJour)
|
||
VALUES (?, ?, ?, ?, ?, 0, ?, NOW())
|
||
`, [collaborateurId, rttType[0].Id, currentYear, rttData.acquisition, rttData.acquisition, rttAnticipe]);
|
||
}
|
||
|
||
console.log(` ✓ RTT Anticipé: ${rttAnticipe.toFixed(2)}j`);
|
||
}
|
||
}
|
||
|
||
console.log(` ✅ Soldes anticipés mis à jour\n`);
|
||
}
|
||
|
||
/**
|
||
* GET /getSoldesAnticipes
|
||
* Retourne les soldes actuels ET anticipés pour un collaborateur
|
||
*/
|
||
app.get('/api/getSoldesAnticipes', async (req, res) => {
|
||
try {
|
||
const userIdParam = req.query.user_id;
|
||
const dateRefParam = req.query.date_reference;
|
||
|
||
if (!userIdParam) {
|
||
return res.json({ success: false, message: 'ID utilisateur manquant' });
|
||
}
|
||
|
||
const conn = await pool.getConnection();
|
||
|
||
// Déterminer l'ID
|
||
const isUUID = userIdParam.length > 10 && userIdParam.includes('-');
|
||
const userQuery = `
|
||
SELECT ca.id, ca.prenom, ca.nom, ca.DateEntree, ca.TypeContrat, ca.role
|
||
FROM CollaborateurAD ca
|
||
WHERE ${isUUID ? 'ca.entraUserId' : 'ca.id'} = ?
|
||
AND (ca.Actif = 1 OR ca.Actif IS NULL)
|
||
`;
|
||
|
||
const [userInfo] = await conn.query(userQuery, [userIdParam]);
|
||
|
||
if (userInfo.length === 0) {
|
||
conn.release();
|
||
return res.json({ success: false, message: 'Utilisateur non trouvé' });
|
||
}
|
||
|
||
const user = userInfo[0];
|
||
const userId = user.id;
|
||
const dateEntree = user.DateEntree;
|
||
const typeContrat = user.TypeContrat || '37h';
|
||
const isApprenti = user.role === 'Apprenti';
|
||
|
||
const dateReference = dateRefParam ? new Date(dateRefParam) : new Date();
|
||
dateReference.setHours(0, 0, 0, 0);
|
||
const currentYear = dateReference.getFullYear();
|
||
|
||
// ===== CP =====
|
||
const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']);
|
||
|
||
let cpData = {
|
||
acquis: 0,
|
||
solde: 0,
|
||
anticipe: 0,
|
||
totalDisponible: 0
|
||
};
|
||
|
||
if (cpType.length > 0) {
|
||
// Acquisition actuelle
|
||
const acquisCP = calculerAcquisitionCP(dateReference, dateEntree);
|
||
|
||
// Anticipé
|
||
const anticipeCP = calculerAcquisitionCPAnticipee(dateReference, dateEntree);
|
||
|
||
// Solde en base (avec consommations déduites)
|
||
const [compteurCP] = await conn.query(`
|
||
SELECT Solde, SoldeReporte, SoldeAnticipe
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [userId, cpType[0].Id, currentYear]);
|
||
|
||
// Reporté N-1
|
||
const [compteurCPN1] = await conn.query(`
|
||
SELECT Solde as SoldeReporte
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [userId, cpType[0].Id, currentYear - 1]);
|
||
|
||
const soldeN1 = compteurCPN1.length > 0 ? parseFloat(compteurCPN1[0].SoldeReporte || 0) : 0;
|
||
const soldeN = compteurCP.length > 0 ? parseFloat(compteurCP[0].Solde || 0) : acquisCP;
|
||
|
||
cpData = {
|
||
acquis: parseFloat(acquisCP.toFixed(2)),
|
||
soldeN1: parseFloat(soldeN1.toFixed(2)),
|
||
soldeN: parseFloat((soldeN - soldeN1).toFixed(2)),
|
||
soldeTotal: parseFloat(soldeN.toFixed(2)),
|
||
anticipe: parseFloat(anticipeCP.toFixed(2)),
|
||
totalDisponible: parseFloat((soldeN + anticipeCP).toFixed(2))
|
||
};
|
||
}
|
||
|
||
// ===== RTT =====
|
||
let rttData = {
|
||
acquis: 0,
|
||
solde: 0,
|
||
anticipe: 0,
|
||
totalDisponible: 0,
|
||
isApprenti: isApprenti
|
||
};
|
||
|
||
if (!isApprenti) {
|
||
const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']);
|
||
|
||
if (rttType.length > 0) {
|
||
// Acquisition actuelle
|
||
const rttCalc = await calculerAcquisitionRTT(conn, userId, dateReference);
|
||
const acquisRTT = rttCalc.acquisition;
|
||
|
||
// Anticipé
|
||
const anticipeRTT = await calculerAcquisitionRTTAnticipee(conn, userId, dateReference);
|
||
|
||
// Solde en base
|
||
const [compteurRTT] = await conn.query(`
|
||
SELECT Solde, SoldeAnticipe
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [userId, rttType[0].Id, currentYear]);
|
||
|
||
const soldeRTT = compteurRTT.length > 0 ? parseFloat(compteurRTT[0].Solde || 0) : acquisRTT;
|
||
|
||
rttData = {
|
||
acquis: parseFloat(acquisRTT.toFixed(2)),
|
||
solde: parseFloat(soldeRTT.toFixed(2)),
|
||
anticipe: parseFloat(anticipeRTT.toFixed(2)),
|
||
totalDisponible: parseFloat((soldeRTT + anticipeRTT).toFixed(2)),
|
||
config: rttCalc.config,
|
||
typeContrat: typeContrat
|
||
};
|
||
}
|
||
}
|
||
|
||
// ===== RÉCUP =====
|
||
const [recupType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Récupération']);
|
||
let recupData = { solde: 0 };
|
||
|
||
if (recupType.length > 0) {
|
||
const [compteurRecup] = await conn.query(`
|
||
SELECT Solde FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [userId, recupType[0].Id, currentYear]);
|
||
|
||
recupData.solde = compteurRecup.length > 0 ? parseFloat(compteurRecup[0].Solde || 0) : 0;
|
||
}
|
||
|
||
conn.release();
|
||
|
||
res.json({
|
||
success: true,
|
||
user: {
|
||
id: user.id,
|
||
nom: `${user.prenom} ${user.nom}`,
|
||
typeContrat: typeContrat,
|
||
dateEntree: dateEntree ? formatDateWithoutUTC(dateEntree) : null
|
||
},
|
||
dateReference: dateReference.toISOString().split('T')[0],
|
||
cp: cpData,
|
||
rtt: rttData,
|
||
recup: recupData,
|
||
totalGeneral: {
|
||
disponibleActuel: parseFloat((cpData.soldeTotal + rttData.solde + recupData.solde).toFixed(2)),
|
||
disponibleAvecAnticipe: parseFloat((cpData.totalDisponible + rttData.totalDisponible + recupData.solde).toFixed(2))
|
||
}
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('Erreur getSoldesAnticipes:', error);
|
||
res.status(500).json({ success: false, message: 'Erreur serveur', error: error.message });
|
||
}
|
||
});
|
||
/**
|
||
* GET /getCongesAnticipes
|
||
* Calcule les congés anticipés disponibles pour un collaborateur
|
||
*/
|
||
app.get('/api/getCongesAnticipes', async (req, res) => {
|
||
try {
|
||
const userIdParam = req.query.user_id;
|
||
|
||
if (!userIdParam) {
|
||
return res.json({ success: false, message: 'ID utilisateur manquant' });
|
||
}
|
||
|
||
const conn = await pool.getConnection();
|
||
|
||
// Déterminer l'ID (UUID ou numérique)
|
||
const isUUID = userIdParam.length > 10 && userIdParam.includes('-');
|
||
|
||
const userQuery = `
|
||
SELECT
|
||
ca.id,
|
||
ca.prenom,
|
||
ca.nom,
|
||
ca.email,
|
||
ca.DateEntree,
|
||
ca.TypeContrat,
|
||
ca.role,
|
||
ca.CampusId
|
||
FROM CollaborateurAD ca
|
||
WHERE ${isUUID ? 'ca.entraUserId' : 'ca.id'} = ?
|
||
AND (ca.Actif = 1 OR ca.Actif IS NULL)
|
||
`;
|
||
|
||
const [userInfo] = await conn.query(userQuery, [userIdParam]);
|
||
|
||
if (userInfo.length === 0) {
|
||
conn.release();
|
||
return res.json({ success: false, message: 'Utilisateur non trouvé' });
|
||
}
|
||
|
||
const user = userInfo[0];
|
||
const userId = user.id;
|
||
const dateEntree = user.DateEntree;
|
||
const typeContrat = user.TypeContrat || '37h';
|
||
const today = new Date();
|
||
const currentYear = today.getFullYear();
|
||
const finAnnee = new Date(currentYear, 11, 31); // 31 décembre
|
||
|
||
// ========================================
|
||
// CALCUL CP (Congés Payés)
|
||
// ========================================
|
||
|
||
// Acquisition actuelle
|
||
const acquisActuelleCP = calculerAcquisitionCP(today, dateEntree);
|
||
|
||
// Acquisition prévue à la fin de l'exercice (31 mai N+1)
|
||
const finExerciceCP = new Date(currentYear + 1, 4, 31); // 31 mai N+1
|
||
const acquisTotaleCP = calculerAcquisitionCP(finExerciceCP, dateEntree);
|
||
|
||
// Récupérer le solde actuel
|
||
const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']);
|
||
|
||
let soldeActuelCP = 0;
|
||
let dejaPrisCP = 0;
|
||
|
||
if (cpType.length > 0) {
|
||
const [compteurCP] = await conn.query(`
|
||
SELECT Total, Solde, SoldeReporte
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [userId, cpType[0].Id, currentYear]);
|
||
|
||
if (compteurCP.length > 0) {
|
||
const total = parseFloat(compteurCP[0].Total || 0);
|
||
soldeActuelCP = parseFloat(compteurCP[0].Solde || 0);
|
||
dejaPrisCP = total - (soldeActuelCP - parseFloat(compteurCP[0].SoldeReporte || 0));
|
||
}
|
||
}
|
||
|
||
// Calculer le potentiel anticipé pour CP
|
||
const acquisRestanteCP = acquisTotaleCP - acquisActuelleCP;
|
||
const anticipePossibleCP = Math.max(0, acquisRestanteCP);
|
||
const limiteAnticipeCP = Math.min(anticipePossibleCP, 25 - dejaPrisCP);
|
||
|
||
// ========================================
|
||
// CALCUL RTT
|
||
// ========================================
|
||
|
||
let anticipePossibleRTT = 0;
|
||
let limiteAnticipeRTT = 0;
|
||
let soldeActuelRTT = 0;
|
||
let dejaPrisRTT = 0;
|
||
let acquisActuelleRTT = 0;
|
||
let acquisTotaleRTT = 0;
|
||
|
||
if (user.role !== 'Apprenti') {
|
||
// Acquisition actuelle
|
||
const rttDataActuel = await calculerAcquisitionRTT(conn, userId, today);
|
||
acquisActuelleRTT = rttDataActuel.acquisition;
|
||
|
||
// Acquisition prévue à la fin de l'année
|
||
const rttDataTotal = await calculerAcquisitionRTT(conn, userId, finAnnee);
|
||
acquisTotaleRTT = rttDataTotal.acquisition;
|
||
|
||
// Récupérer le solde actuel
|
||
const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']);
|
||
|
||
if (rttType.length > 0) {
|
||
const [compteurRTT] = await conn.query(`
|
||
SELECT Total, Solde
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [userId, rttType[0].Id, currentYear]);
|
||
|
||
if (compteurRTT.length > 0) {
|
||
const total = parseFloat(compteurRTT[0].Total || 0);
|
||
soldeActuelRTT = parseFloat(compteurRTT[0].Solde || 0);
|
||
dejaPrisRTT = total - soldeActuelRTT;
|
||
}
|
||
}
|
||
|
||
// Calculer le potentiel anticipé pour RTT
|
||
const acquisRestanteRTT = acquisTotaleRTT - acquisActuelleRTT;
|
||
anticipePossibleRTT = Math.max(0, acquisRestanteRTT);
|
||
|
||
const maxRTT = typeContrat === 'forfait_jour' ? 12 : 10;
|
||
limiteAnticipeRTT = Math.min(anticipePossibleRTT, maxRTT - dejaPrisRTT);
|
||
}
|
||
|
||
conn.release();
|
||
|
||
// ========================================
|
||
// RÉPONSE
|
||
// ========================================
|
||
|
||
res.json({
|
||
success: true,
|
||
user: {
|
||
id: user.id,
|
||
nom: `${user.prenom} ${user.nom}`,
|
||
email: user.email,
|
||
typeContrat: typeContrat,
|
||
dateEntree: dateEntree ? formatDateWithoutUTC(dateEntree) : null
|
||
},
|
||
dateReference: today.toISOString().split('T')[0],
|
||
congesPayes: {
|
||
acquisActuelle: parseFloat(acquisActuelleCP.toFixed(2)),
|
||
acquisTotalePrevu: parseFloat(acquisTotaleCP.toFixed(2)),
|
||
acquisRestante: parseFloat((acquisTotaleCP - acquisActuelleCP).toFixed(2)),
|
||
soldeActuel: parseFloat(soldeActuelCP.toFixed(2)),
|
||
dejaPris: parseFloat(dejaPrisCP.toFixed(2)),
|
||
anticipePossible: parseFloat(anticipePossibleCP.toFixed(2)),
|
||
limiteAnticipe: parseFloat(limiteAnticipeCP.toFixed(2)),
|
||
totalDisponible: parseFloat((soldeActuelCP + limiteAnticipeCP).toFixed(2)),
|
||
message: limiteAnticipeCP > 0
|
||
? `Vous pouvez poser jusqu'à ${limiteAnticipeCP.toFixed(1)} jours de CP en anticipé`
|
||
: "Vous avez atteint la limite d'anticipation pour les CP"
|
||
},
|
||
rtt: user.role !== 'Apprenti' ? {
|
||
acquisActuelle: parseFloat(acquisActuelleRTT.toFixed(2)),
|
||
acquisTotalePrevu: parseFloat(acquisTotaleRTT.toFixed(2)),
|
||
acquisRestante: parseFloat((acquisTotaleRTT - acquisActuelleRTT).toFixed(2)),
|
||
soldeActuel: parseFloat(soldeActuelRTT.toFixed(2)),
|
||
dejaPris: parseFloat(dejaPrisRTT.toFixed(2)),
|
||
anticipePossible: parseFloat(anticipePossibleRTT.toFixed(2)),
|
||
limiteAnticipe: parseFloat(limiteAnticipeRTT.toFixed(2)),
|
||
totalDisponible: parseFloat((soldeActuelRTT + limiteAnticipeRTT).toFixed(2)),
|
||
message: limiteAnticipeRTT > 0
|
||
? `Vous pouvez poser jusqu'à ${limiteAnticipeRTT.toFixed(1)} jours de RTT en anticipé`
|
||
: "Vous avez atteint la limite d'anticipation pour les RTT"
|
||
} : null,
|
||
regles: {
|
||
cpMaxAnnuel: 25,
|
||
rttMaxAnnuel: typeContrat === 'forfait_jour' ? 12 : 10,
|
||
description: "Les congés anticipés sont basés sur l'acquisition prévue jusqu'à la fin de l'exercice/année"
|
||
}
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('Erreur getCongesAnticipes:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur serveur',
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
/**
|
||
* Calcule l'acquisition CP ANTICIPÉE (ce qui reste à acquérir jusqu'à fin d'exercice)
|
||
*/
|
||
function calculerAcquisitionCPAnticipee(dateReference = new Date(), dateEntree = null) {
|
||
const d = new Date(dateReference);
|
||
d.setHours(0, 0, 0, 0);
|
||
|
||
const annee = d.getFullYear();
|
||
const mois = d.getMonth() + 1;
|
||
|
||
// 1️⃣ Déterminer la fin de l'exercice CP (31/05)
|
||
let finExercice;
|
||
if (mois >= 6) {
|
||
finExercice = new Date(annee + 1, 4, 31); // 31/05/N+1
|
||
} else {
|
||
finExercice = new Date(annee, 4, 31); // 31/05/N
|
||
}
|
||
finExercice.setHours(0, 0, 0, 0);
|
||
|
||
// 2️⃣ Calculer l'acquisition actuelle
|
||
const acquisActuelle = calculerAcquisitionCP(d, dateEntree);
|
||
|
||
// 3️⃣ Calculer l'acquisition totale à fin d'exercice
|
||
const acquisTotaleFinExercice = calculerAcquisitionCP(finExercice, dateEntree);
|
||
|
||
// 4️⃣ Anticipée = Totale - Actuelle (plafonnée à 25)
|
||
const acquisAnticipee = Math.min(25, acquisTotaleFinExercice) - acquisActuelle;
|
||
|
||
return Math.max(0, Math.round(acquisAnticipee * 100) / 100);
|
||
}
|
||
|
||
/**
|
||
* Calcule l'acquisition RTT ANTICIPÉE (ce qui reste à acquérir jusqu'à fin d'année)
|
||
*/
|
||
async function calculerAcquisitionRTTAnticipee(conn, collaborateurId, dateReference = new Date()) {
|
||
const d = new Date(dateReference);
|
||
d.setHours(0, 0, 0, 0);
|
||
const annee = d.getFullYear();
|
||
|
||
// 1️⃣ Récupérer les infos du collaborateur
|
||
const [collabInfo] = await conn.query(
|
||
`SELECT TypeContrat, DateEntree, role FROM CollaborateurAD WHERE id = ?`,
|
||
[collaborateurId]
|
||
);
|
||
|
||
if (collabInfo.length === 0) {
|
||
return 0;
|
||
}
|
||
|
||
const typeContrat = collabInfo[0].TypeContrat || '37h';
|
||
const isApprenti = collabInfo[0].role === 'Apprenti';
|
||
|
||
// 2️⃣ Apprentis = pas de RTT
|
||
if (isApprenti) {
|
||
return 0;
|
||
}
|
||
|
||
// 3️⃣ Récupérer la configuration RTT
|
||
const config = await getConfigurationRTT(conn, annee, typeContrat);
|
||
|
||
// 4️⃣ Calculer l'acquisition actuelle
|
||
const rttActuel = await calculerAcquisitionRTT(conn, collaborateurId, d);
|
||
const acquisActuelle = rttActuel.acquisition;
|
||
|
||
// 5️⃣ Calculer l'acquisition totale à fin d'année (31/12)
|
||
const finAnnee = new Date(annee, 11, 31);
|
||
finAnnee.setHours(0, 0, 0, 0);
|
||
|
||
const rttFinAnnee = await calculerAcquisitionRTT(conn, collaborateurId, finAnnee);
|
||
const acquisTotaleFinAnnee = rttFinAnnee.acquisition;
|
||
|
||
// 6️⃣ Anticipée = Totale - Actuelle (plafonnée au max annuel)
|
||
const acquisAnticipee = Math.min(config.joursAnnuels, acquisTotaleFinAnnee) - acquisActuelle;
|
||
|
||
return Math.max(0, Math.round(acquisAnticipee * 100) / 100);
|
||
}
|
||
|
||
app.get('/api/getStatistiquesCompteurs', async (req, res) => {
|
||
try {
|
||
const conn = await pool.getConnection();
|
||
const currentYear = new Date().getFullYear();
|
||
|
||
const [totalCollabs] = await conn.query(
|
||
'SELECT COUNT(*) as total FROM CollaborateurAD WHERE actif = 1 OR actif IS NULL'
|
||
);
|
||
|
||
const [statsTypeContrat] = await conn.query(`
|
||
SELECT
|
||
TypeContrat,
|
||
COUNT(*) as nombre,
|
||
GROUP_CONCAT(CONCAT(prenom, ' ', nom) SEPARATOR ', ') as noms
|
||
FROM CollaborateurAD
|
||
WHERE actif = 1 OR actif IS NULL
|
||
GROUP BY TypeContrat
|
||
`);
|
||
|
||
const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']);
|
||
let statsCP = { total_acquis: 0, total_solde: 0, moyenne_utilisation: 0 };
|
||
|
||
if (cpType.length > 0) {
|
||
const [cpStats] = await conn.query(`
|
||
SELECT
|
||
SUM(Total) as total_acquis,
|
||
SUM(Solde) as total_solde,
|
||
AVG(CASE WHEN Total > 0 THEN ((Total - Solde) / Total) * 100 ELSE 0 END) as moyenne_utilisation
|
||
FROM CompteurConges
|
||
WHERE TypeCongeId = ? AND Annee = ?
|
||
`, [cpType[0].Id, currentYear]);
|
||
|
||
if (cpStats.length > 0) {
|
||
statsCP = {
|
||
total_acquis: parseFloat((cpStats[0].total_acquis || 0).toFixed(2)),
|
||
total_solde: parseFloat((cpStats[0].total_solde || 0).toFixed(2)),
|
||
moyenne_utilisation: parseFloat((cpStats[0].moyenne_utilisation || 0).toFixed(1))
|
||
};
|
||
}
|
||
}
|
||
|
||
const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']);
|
||
let statsRTT = { total_acquis: 0, total_solde: 0, moyenne_utilisation: 0 };
|
||
|
||
if (rttType.length > 0) {
|
||
const [rttStats] = await conn.query(`
|
||
SELECT
|
||
SUM(Total) as total_acquis,
|
||
SUM(Solde) as total_solde,
|
||
AVG(CASE WHEN Total > 0 THEN ((Total - Solde) / Total) * 100 ELSE 0 END) as moyenne_utilisation
|
||
FROM CompteurConges
|
||
WHERE TypeCongeId = ? AND Annee = ?
|
||
`, [rttType[0].Id, currentYear]);
|
||
|
||
if (rttStats.length > 0) {
|
||
statsRTT = {
|
||
total_acquis: parseFloat((rttStats[0].total_acquis || 0).toFixed(2)),
|
||
total_solde: parseFloat((rttStats[0].total_solde || 0).toFixed(2)),
|
||
moyenne_utilisation: parseFloat((rttStats[0].moyenne_utilisation || 0).toFixed(1))
|
||
};
|
||
}
|
||
}
|
||
|
||
conn.release();
|
||
|
||
res.json({
|
||
success: true,
|
||
annee: currentYear,
|
||
statistiques: {
|
||
collaborateurs: {
|
||
total: totalCollabs[0].total,
|
||
par_type_contrat: statsTypeContrat
|
||
},
|
||
conges_payes: statsCP,
|
||
rtt: statsRTT
|
||
}
|
||
});
|
||
} catch (error) {
|
||
console.error('Erreur getStatistiquesCompteurs:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur serveur',
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
async function hasCompteRenduAccess(userId) {
|
||
try {
|
||
const conn = await pool.getConnection();
|
||
|
||
const [user] = await conn.query(`
|
||
SELECT TypeContrat, role
|
||
FROM CollaborateurAD
|
||
WHERE id = ?
|
||
`, [userId]);
|
||
|
||
conn.release();
|
||
|
||
if (!user.length) return false;
|
||
|
||
const userInfo = user[0];
|
||
|
||
// Accès si :
|
||
// 1. TypeContrat = 'forfait_jour'
|
||
// 2. role = 'Directeur Campus' ou 'Directrice Campus'
|
||
// 3. role = 'RH' ou 'Admin'
|
||
|
||
return (
|
||
userInfo.TypeContrat === 'forfait_jour' ||
|
||
userInfo.role === 'Directeur Campus' ||
|
||
userInfo.role === 'Directrice Campus' ||
|
||
userInfo.role === 'RH' ||
|
||
userInfo.role === 'Admin'
|
||
);
|
||
|
||
} catch (error) {
|
||
console.error('Erreur vérification accès:', error);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
|
||
// Récupérer les jours du mois
|
||
// GET - Récupérer les données du compte-rendu
|
||
app.get('/api/getCompteRenduActivites', async (req, res) => {
|
||
const { user_id, annee, mois } = req.query;
|
||
|
||
try {
|
||
// Vérifier l'accès
|
||
const hasAccess = await hasCompteRenduAccess(user_id);
|
||
|
||
if (!hasAccess) {
|
||
return res.json({
|
||
success: false,
|
||
message: 'Accès réservé aux collaborateurs en forfait jour et aux directeurs de campus'
|
||
});
|
||
}
|
||
|
||
const conn = await pool.getConnection();
|
||
|
||
const [jours] = await conn.query(`
|
||
SELECT
|
||
id,
|
||
CollaborateurADId,
|
||
Annee,
|
||
Mois,
|
||
DATE_FORMAT(JourDate, '%Y-%m-%d') as JourDate,
|
||
JourTravaille,
|
||
ReposQuotidienRespect,
|
||
ReposHebdomadaireRespect,
|
||
CommentaireRepos,
|
||
Verrouille,
|
||
DateSaisie,
|
||
SaisiePar
|
||
FROM CompteRenduActivites
|
||
WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ?
|
||
ORDER BY JourDate
|
||
`, [user_id, annee, mois]);
|
||
|
||
console.log('🔍 Backend - Jours trouvés:', jours.length);
|
||
if (jours.length > 0) {
|
||
console.log('📅 Premier jour:', jours[0].JourDate, 'Type:', typeof jours[0].JourDate);
|
||
}
|
||
|
||
const [mensuel] = await conn.query(`
|
||
SELECT * FROM CompteRenduMensuel
|
||
WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ?
|
||
`, [user_id, annee, mois]);
|
||
|
||
conn.release();
|
||
|
||
res.json({
|
||
success: true,
|
||
jours: jours,
|
||
mensuel: mensuel[0] || null
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('Erreur getCompteRenduActivites:', error);
|
||
res.status(500).json({ success: false, message: error.message });
|
||
}
|
||
});
|
||
|
||
// POST - Sauvegarder un jour avec AUTO-VERROUILLAGE
|
||
// POST - Sauvegarder un jour avec AUTO-VERROUILLAGE
|
||
app.post('/api/saveCompteRenduJour', async (req, res) => {
|
||
const { user_id, date, jour_travaille, repos_quotidien, repos_hebdo, commentaire, rh_override } = req.body;
|
||
|
||
try {
|
||
const conn = await pool.getConnection();
|
||
await conn.beginTransaction();
|
||
|
||
const dateJour = new Date(date);
|
||
const aujourdhui = new Date();
|
||
aujourdhui.setHours(0, 0, 0, 0);
|
||
dateJour.setHours(0, 0, 0, 0);
|
||
|
||
// Bloquer saisie du jour actuel (il faut attendre le lendemain)
|
||
if (dateJour >= aujourdhui) {
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.json({ success: false, message: 'Vous ne pouvez pas saisir le jour actuel. Veuillez attendre demain.' });
|
||
}
|
||
|
||
const annee = dateJour.getFullYear();
|
||
const mois = dateJour.getMonth() + 1;
|
||
|
||
// Vérifier si le JOUR est déjà verrouillé (pas le mois entier)
|
||
const [jourExistant] = await conn.query(
|
||
'SELECT Verrouille FROM CompteRenduActivites WHERE CollaborateurADId = ? AND JourDate = ?',
|
||
[user_id, date]
|
||
);
|
||
|
||
if (jourExistant[0]?.Verrouille && !rh_override) {
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.json({ success: false, message: 'Ce jour est verrouillé - Contactez les RH pour modification' });
|
||
}
|
||
|
||
// Vérifier commentaire obligatoire
|
||
if (!repos_quotidien || !repos_hebdo) {
|
||
if (!commentaire || commentaire.trim() === '') {
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.json({ success: false, message: 'Commentaire obligatoire en cas de non-respect des repos' });
|
||
}
|
||
}
|
||
|
||
// Insérer ou mettre à jour le jour ET LE VERROUILLER
|
||
await conn.query(`
|
||
INSERT INTO CompteRenduActivites
|
||
(CollaborateurADId, Annee, Mois, JourDate, JourTravaille,
|
||
ReposQuotidienRespect, ReposHebdomadaireRespect, CommentaireRepos,
|
||
DateSaisie, SaisiePar, Verrouille)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW(), ?, TRUE)
|
||
ON DUPLICATE KEY UPDATE
|
||
JourTravaille = VALUES(JourTravaille),
|
||
ReposQuotidienRespect = VALUES(ReposQuotidienRespect),
|
||
ReposHebdomadaireRespect = VALUES(ReposHebdomadaireRespect),
|
||
CommentaireRepos = VALUES(CommentaireRepos),
|
||
SaisiePar = VALUES(SaisiePar),
|
||
Verrouille = TRUE
|
||
`, [user_id, annee, mois, date, jour_travaille, repos_quotidien, repos_hebdo, commentaire, user_id]);
|
||
|
||
// Mettre à jour les statistiques mensuelles (SANS verrouiller le mois)
|
||
const [stats] = await conn.query(`
|
||
SELECT
|
||
COUNT(*) as nbJours,
|
||
SUM(CASE WHEN ReposQuotidienRespect = FALSE THEN 1 ELSE 0 END) as nbNonRespectQuotidien,
|
||
SUM(CASE WHEN ReposHebdomadaireRespect = FALSE THEN 1 ELSE 0 END) as nbNonRespectHebdo
|
||
FROM CompteRenduActivites
|
||
WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ?
|
||
AND JourTravaille = TRUE
|
||
`, [user_id, annee, mois]);
|
||
|
||
await conn.query(`
|
||
INSERT INTO CompteRenduMensuel
|
||
(CollaborateurADId, Annee, Mois, NbJoursTravailles,
|
||
NbJoursNonRespectsReposQuotidien, NbJoursNonRespectsReposHebdo,
|
||
Statut, DateValidation)
|
||
VALUES (?, ?, ?, ?, ?, ?, 'En cours', NOW())
|
||
ON DUPLICATE KEY UPDATE
|
||
NbJoursTravailles = VALUES(NbJoursTravailles),
|
||
NbJoursNonRespectsReposQuotidien = VALUES(NbJoursNonRespectsReposQuotidien),
|
||
NbJoursNonRespectsReposHebdo = VALUES(NbJoursNonRespectsReposHebdo),
|
||
DateValidation = NOW()
|
||
`, [user_id, annee, mois, stats[0].nbJours, stats[0].nbNonRespectQuotidien, stats[0].nbNonRespectHebdo]);
|
||
|
||
await conn.commit();
|
||
conn.release();
|
||
|
||
res.json({
|
||
success: true,
|
||
message: 'Jour enregistré et verrouillé',
|
||
verrouille: true
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('❌ Erreur saveCompteRenduJour:', error);
|
||
res.status(500).json({ success: false, message: error.message });
|
||
}
|
||
});
|
||
|
||
// POST - Saisie en masse avec AUTO-VERROUILLAGE
|
||
app.post('/api/saveCompteRenduMasse', async (req, res) => {
|
||
const { user_id, annee, mois, jours, rh_override } = req.body;
|
||
|
||
try {
|
||
const conn = await pool.getConnection();
|
||
await conn.beginTransaction();
|
||
|
||
let count = 0;
|
||
let blocked = 0;
|
||
|
||
for (const jour of jours) {
|
||
const dateJour = new Date(jour.date);
|
||
const aujourdhui = new Date();
|
||
aujourdhui.setHours(0, 0, 0, 0);
|
||
dateJour.setHours(0, 0, 0, 0);
|
||
|
||
// Bloquer le jour actuel
|
||
if (dateJour >= aujourdhui) {
|
||
blocked++;
|
||
continue;
|
||
}
|
||
|
||
// Vérifier si déjà verrouillé
|
||
const [jourExistant] = await conn.query(
|
||
'SELECT Verrouille FROM CompteRenduActivites WHERE CollaborateurADId = ? AND JourDate = ?',
|
||
[user_id, jour.date]
|
||
);
|
||
|
||
if (jourExistant[0]?.Verrouille && !rh_override) {
|
||
blocked++;
|
||
continue;
|
||
}
|
||
|
||
await conn.query(`
|
||
INSERT INTO CompteRenduActivites
|
||
(CollaborateurADId, Annee, Mois, JourDate, JourTravaille,
|
||
ReposQuotidienRespect, ReposHebdomadaireRespect, CommentaireRepos,
|
||
DateSaisie, SaisiePar, Verrouille)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW(), ?, TRUE)
|
||
ON DUPLICATE KEY UPDATE
|
||
JourTravaille = VALUES(JourTravaille),
|
||
ReposQuotidienRespect = VALUES(ReposQuotidienRespect),
|
||
ReposHebdomadaireRespect = VALUES(ReposHebdomadaireRespect),
|
||
CommentaireRepos = VALUES(CommentaireRepos),
|
||
SaisiePar = VALUES(SaisiePar),
|
||
Verrouille = TRUE
|
||
`, [
|
||
user_id, annee, mois, jour.date,
|
||
jour.jour_travaille, jour.repos_quotidien, jour.repos_hebdo,
|
||
jour.commentaire || null, user_id
|
||
]);
|
||
|
||
count++;
|
||
}
|
||
|
||
// Mettre à jour statistiques mensuelles
|
||
const [stats] = await conn.query(`
|
||
SELECT
|
||
COUNT(*) as nbJours,
|
||
SUM(CASE WHEN ReposQuotidienRespect = FALSE THEN 1 ELSE 0 END) as nbNonRespectQuotidien,
|
||
SUM(CASE WHEN ReposHebdomadaireRespect = FALSE THEN 1 ELSE 0 END) as nbNonRespectHebdo
|
||
FROM CompteRenduActivites
|
||
WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ?
|
||
AND JourTravaille = TRUE
|
||
`, [user_id, annee, mois]);
|
||
|
||
await conn.query(`
|
||
INSERT INTO CompteRenduMensuel
|
||
(CollaborateurADId, Annee, Mois, NbJoursTravailles,
|
||
NbJoursNonRespectsReposQuotidien, NbJoursNonRespectsReposHebdo,
|
||
Statut, DateValidation)
|
||
VALUES (?, ?, ?, ?, ?, ?, 'En cours', NOW())
|
||
ON DUPLICATE KEY UPDATE
|
||
NbJoursTravailles = VALUES(NbJoursTravailles),
|
||
NbJoursNonRespectsReposQuotidien = VALUES(NbJoursNonRespectsReposQuotidien),
|
||
NbJoursNonRespectsReposHebdo = VALUES(NbJoursNonRespectsReposHebdo),
|
||
DateValidation = NOW()
|
||
`, [user_id, annee, mois, stats[0].nbJours, stats[0].nbNonRespectQuotidien, stats[0].nbNonRespectHebdo]);
|
||
|
||
await conn.commit();
|
||
conn.release();
|
||
|
||
res.json({
|
||
success: true,
|
||
count: count,
|
||
blocked: blocked,
|
||
message: `${count} jours enregistrés${blocked > 0 ? `, ${blocked} ignorés (jour actuel ou déjà verrouillés)` : ''}`
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('❌ Erreur saisie masse:', error);
|
||
res.status(500).json({ success: false, message: error.message });
|
||
}
|
||
});
|
||
|
||
app.post('/api/deverrouillerJour', async (req, res) => {
|
||
const { user_id, date, rh_user_id } = req.body;
|
||
|
||
try {
|
||
const conn = await pool.getConnection();
|
||
|
||
const [rhUser] = await conn.query(
|
||
'SELECT role FROM CollaborateurAD WHERE id = ?',
|
||
[rh_user_id]
|
||
);
|
||
|
||
if (!rhUser.length || (rhUser[0].role !== 'RH' && rhUser[0].role !== 'Admin')) {
|
||
conn.release();
|
||
return res.json({ success: false, message: 'Action réservée aux RH' });
|
||
}
|
||
|
||
await conn.query(`
|
||
UPDATE CompteRenduActivites
|
||
SET Verrouille = FALSE
|
||
WHERE CollaborateurADId = ? AND JourDate = ?
|
||
`, [user_id, date]);
|
||
|
||
conn.release();
|
||
|
||
res.json({ success: true });
|
||
|
||
} catch (error) {
|
||
console.error('❌ Erreur déverrouillage jour:', error);
|
||
res.status(500).json({ success: false, message: error.message });
|
||
}
|
||
});
|
||
|
||
|
||
// POST - Verrouiller (RH uniquement)
|
||
app.post('/api/verrouillerCompteRendu', async (req, res) => {
|
||
const { user_id, annee, mois, rh_user_id } = req.body;
|
||
|
||
try {
|
||
const conn = await pool.getConnection();
|
||
|
||
// Vérifier que l'utilisateur est RH
|
||
const [rhUser] = await conn.query(
|
||
'SELECT role FROM CollaborateurAD WHERE id = ?',
|
||
[rh_user_id]
|
||
);
|
||
|
||
if (!rhUser.length || (rhUser[0].role !== 'RH' && rhUser[0].role !== 'Admin')) {
|
||
conn.release();
|
||
return res.json({ success: false, message: 'Action réservée aux RH' });
|
||
}
|
||
|
||
await conn.query(`
|
||
UPDATE CompteRenduMensuel
|
||
SET Verrouille = TRUE,
|
||
DateModification = NOW()
|
||
WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ?
|
||
`, [user_id, annee, mois]);
|
||
|
||
conn.release();
|
||
|
||
res.json({ success: true });
|
||
|
||
} catch (error) {
|
||
console.error('Erreur verrouillage:', error);
|
||
res.status(500).json({ success: false, message: error.message });
|
||
}
|
||
});
|
||
|
||
// POST - Déverrouiller (RH uniquement)
|
||
app.post('/api/deverrouillerCompteRendu', async (req, res) => {
|
||
const { user_id, annee, mois, rh_user_id } = req.body;
|
||
|
||
try {
|
||
const conn = await pool.getConnection();
|
||
|
||
// Vérifier que l'utilisateur est RH
|
||
const [rhUser] = await conn.query(
|
||
'SELECT role FROM CollaborateurAD WHERE id = ?',
|
||
[rh_user_id]
|
||
);
|
||
|
||
if (!rhUser.length || (rhUser[0].role !== 'RH' && rhUser[0].role !== 'Admin')) {
|
||
conn.release();
|
||
return res.json({ success: false, message: 'Action réservée aux RH' });
|
||
}
|
||
|
||
await conn.query(`
|
||
UPDATE CompteRenduMensuel
|
||
SET Verrouille = FALSE,
|
||
DateDeverrouillage = NOW(),
|
||
DeverrouillePar = ?
|
||
WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ?
|
||
`, [rh_user_id, user_id, annee, mois]);
|
||
|
||
conn.release();
|
||
|
||
res.json({ success: true });
|
||
|
||
} catch (error) {
|
||
console.error('Erreur déverrouillage:', error);
|
||
res.status(500).json({ success: false, message: error.message });
|
||
}
|
||
});
|
||
|
||
// GET - Stats annuelles
|
||
app.get('/api/getStatsAnnuelles', async (req, res) => {
|
||
const { user_id, annee } = req.query;
|
||
|
||
try {
|
||
const conn = await pool.getConnection();
|
||
|
||
const [stats] = await conn.query(`
|
||
SELECT
|
||
SUM(NbJoursTravailles) as totalJoursTravailles,
|
||
SUM(NbJoursNonRespectsReposQuotidien) as totalNonRespectQuotidien,
|
||
SUM(NbJoursNonRespectsReposHebdo) as totalNonRespectHebdo
|
||
FROM CompteRenduMensuel
|
||
WHERE CollaborateurADId = ? AND Annee = ?
|
||
`, [user_id, annee]);
|
||
|
||
conn.release();
|
||
|
||
res.json({
|
||
success: true,
|
||
stats: stats[0] || {
|
||
totalJoursTravailles: 0,
|
||
totalNonRespectQuotidien: 0,
|
||
totalNonRespectHebdo: 0
|
||
}
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('Erreur stats:', error);
|
||
res.status(500).json({ success: false, message: error.message });
|
||
}
|
||
});
|
||
|
||
// GET - Export PDF (RH uniquement)
|
||
app.get('/api/exportCompteRenduPDF', async (req, res) => {
|
||
const { user_id, annee, mois } = req.query;
|
||
|
||
try {
|
||
const conn = await pool.getConnection();
|
||
|
||
// Récupérer les données du collaborateur
|
||
const [collab] = await conn.query(
|
||
'SELECT prenom, nom, email FROM CollaborateurAD WHERE id = ?',
|
||
[user_id]
|
||
);
|
||
|
||
// Récupérer les jours du mois
|
||
const [jours] = await conn.query(`
|
||
SELECT * FROM CompteRenduActivites
|
||
WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ?
|
||
ORDER BY JourDate
|
||
`, [user_id, annee, mois]);
|
||
|
||
// Récupérer le mensuel
|
||
const [mensuel] = await conn.query(`
|
||
SELECT * FROM CompteRenduMensuel
|
||
WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ?
|
||
`, [user_id, annee, mois]);
|
||
|
||
conn.release();
|
||
|
||
// TODO: Générer le PDF avec une bibliothèque comme pdfkit ou puppeteer
|
||
// Pour l'instant, retourner les données JSON
|
||
res.json({
|
||
success: true,
|
||
collaborateur: collab[0],
|
||
jours: jours,
|
||
mensuel: mensuel[0],
|
||
message: 'Export PDF à implémenter'
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('Erreur export PDF:', error);
|
||
res.status(500).json({ success: false, message: error.message });
|
||
}
|
||
});
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
// ========================================
|
||
// DÉMARRAGE DU SERVEUR
|
||
// ========================================
|
||
|
||
app.listen(PORT, '0.0.0.0', () => {
|
||
console.log('✅ ✅ ✅ SERVEUR PRINCIPAL DÉMARRÉ ✅ ✅ ✅');
|
||
console.log(`📡 Port: ${PORT}`);
|
||
console.log(`🗄️ Base: ${dbConfig.database}@${dbConfig.host}`);
|
||
console.log(`⏰ Cron jobs: activés`);
|
||
console.log(`🌐 CORS origins: ${JSON.stringify(dbConfig)}`);
|
||
}); |