6245 lines
256 KiB
JavaScript
6245 lines
256 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();
|
||
|
||
app.use(cors({
|
||
origin: '*',
|
||
methods: ['GET', 'POST', 'OPTIONS'],
|
||
allowedHeaders: ['Content-Type', 'Authorization']
|
||
}));
|
||
|
||
app.use(express.json());
|
||
app.use(express.urlencoded({ extended: true }));
|
||
|
||
const dbConfig = {
|
||
host: '192.168.0.4',
|
||
user: 'wpuser',
|
||
password: '-2b/)ru5/Bi8P[7_',
|
||
database: 'DemandeConge',
|
||
charset: 'utf8mb4'
|
||
};
|
||
|
||
function nowFR() {
|
||
const d = new Date();
|
||
d.setHours(d.getHours() + 2);
|
||
return d.toISOString().slice(0, 19).replace('T', ' ');
|
||
}
|
||
|
||
// ========================================
|
||
// HELPER POUR DATES SANS CONVERSION UTC
|
||
// ========================================
|
||
function formatDateWithoutUTC(date) {
|
||
if (!date) return null;
|
||
const d = new Date(date);
|
||
const year = d.getFullYear();
|
||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||
const day = String(d.getDate()).padStart(2, '0');
|
||
return `${year}-${month}-${day}`;
|
||
};
|
||
|
||
function formatDateToFrenchTime(date) {
|
||
if (!date) return null;
|
||
|
||
// Créer un objet Date et le convertir en heure française (Europe/Paris)
|
||
const d = new Date(date);
|
||
|
||
// Formater en ISO avec le fuseau horaire français
|
||
return d.toLocaleString('fr-FR', {
|
||
timeZone: 'Europe/Paris',
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
second: '2-digit',
|
||
hour12: false
|
||
});
|
||
}
|
||
|
||
// Ou plus simple, pour avoir un format ISO compatible avec le frontend
|
||
function formatDateToFrenchISO(date) {
|
||
if (!date) return null;
|
||
|
||
const d = new Date(date);
|
||
|
||
// Convertir en heure française
|
||
const frenchDate = new Date(d.toLocaleString('en-US', { timeZone: 'Europe/Paris' }));
|
||
|
||
return frenchDate.toISOString();
|
||
}
|
||
|
||
// ========================================
|
||
// FONCTIONS POUR GÉRER LES ARRÊTÉS COMPTABLES
|
||
// À ajouter après : const pool = mysql.createPool(dbConfig);
|
||
// ========================================
|
||
|
||
/**
|
||
* Récupère le dernier arrêté validé/clôturé
|
||
*/
|
||
async function getDernierArrete(conn) {
|
||
const [arretes] = await conn.query(`
|
||
SELECT * FROM ArreteComptable
|
||
WHERE Statut IN ('Validé', 'Clôturé')
|
||
ORDER BY DateArrete DESC
|
||
LIMIT 1
|
||
`);
|
||
|
||
return arretes.length > 0 ? arretes[0] : null;
|
||
}
|
||
|
||
/**
|
||
* Vérifie si une date est avant le dernier arrêté
|
||
*/
|
||
async function estAvantArrete(conn, date) {
|
||
const dernierArrete = await getDernierArrete(conn);
|
||
|
||
if (!dernierArrete) {
|
||
return false; // Pas d'arrêté = toutes les dates sont autorisées
|
||
}
|
||
|
||
const dateTest = new Date(date);
|
||
const dateArrete = new Date(dernierArrete.DateArrete);
|
||
|
||
return dateTest <= dateArrete;
|
||
}
|
||
|
||
/**
|
||
* Récupère le solde figé d'un collaborateur pour un type de congé
|
||
*/
|
||
async function getSoldeFige(conn, collaborateurId, typeCongeId, annee) {
|
||
const dernierArrete = await getDernierArrete(conn);
|
||
|
||
if (!dernierArrete) {
|
||
return null; // Pas d'arrêté
|
||
}
|
||
|
||
const [soldes] = await conn.query(`
|
||
SELECT * FROM SoldesFiges
|
||
WHERE ArreteId = ?
|
||
AND CollaborateurADId = ?
|
||
AND TypeCongeId = ?
|
||
AND Annee = ?
|
||
LIMIT 1
|
||
`, [dernierArrete.Id, collaborateurId, typeCongeId, annee]);
|
||
|
||
return soldes.length > 0 ? soldes[0] : null;
|
||
}
|
||
|
||
/**
|
||
* Calcule l'acquisition depuis le dernier arrêté
|
||
*/
|
||
async function calculerAcquisitionDepuisArrete(conn, collaborateurId, typeConge, dateReference = new Date()) {
|
||
const dernierArrete = await getDernierArrete(conn);
|
||
const anneeRef = dateReference.getFullYear();
|
||
|
||
// Déterminer le type de congé
|
||
const [typeRow] = await conn.query(
|
||
'SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1',
|
||
[typeConge === 'CP' ? 'Congé payé' : 'RTT']
|
||
);
|
||
|
||
if (typeRow.length === 0) {
|
||
throw new Error(`Type de congé ${typeConge} non trouvé`);
|
||
}
|
||
|
||
const typeCongeId = typeRow[0].Id;
|
||
|
||
|
||
const [collab] = await conn.query('SELECT role FROM CollaborateurAD WHERE id = ?', [collaborateurId]);
|
||
const isApprenti = collab.length > 0 && collab[0].role === 'Apprenti';
|
||
if (typeConge === 'RTT' && isApprenti) {
|
||
return 0; // ⭐ Les apprentis n'ont pas de RTT
|
||
}
|
||
|
||
// Si pas d'arrêté, calcul normal depuis le début
|
||
if (!dernierArrete) {
|
||
if (typeConge === 'CP') {
|
||
const [collab] = await conn.query('SELECT DateEntree, CampusId FROM CollaborateurAD WHERE id = ?', [collaborateurId]);
|
||
const dateEntree = collab.length > 0 ? collab[0].DateEntree : null;
|
||
return calculerAcquisitionCP(dateReference, dateEntree);
|
||
} else {
|
||
const rttData = await calculerAcquisitionRTT(conn, collaborateurId, dateReference);
|
||
return rttData.acquisition;
|
||
}
|
||
}
|
||
|
||
const dateArrete = new Date(dernierArrete.DateArrete);
|
||
|
||
// Si la date de référence est AVANT l'arrêté, utiliser le solde figé
|
||
if (dateReference <= dateArrete) {
|
||
const soldeFige = await getSoldeFige(conn, collaborateurId, typeCongeId, anneeRef);
|
||
return soldeFige ? soldeFige.TotalAcquis : 0;
|
||
}
|
||
|
||
// Si la date est APRÈS l'arrêté, partir du solde figé + calcul depuis l'arrêté
|
||
const soldeFige = await getSoldeFige(conn, collaborateurId, typeCongeId, anneeRef);
|
||
const acquisFigee = soldeFige ? soldeFige.TotalAcquis : 0;
|
||
|
||
// Calculer l'acquisition DEPUIS l'arrêté
|
||
let acquisDepuisArrete = 0;
|
||
|
||
if (typeConge === 'CP') {
|
||
const moisDepuisArrete = getMoisTravaillesCP(dateReference, dateArrete);
|
||
acquisDepuisArrete = moisDepuisArrete * (25 / 12);
|
||
} else {
|
||
const [collab] = await conn.query('SELECT TypeContrat, CampusId FROM CollaborateurAD WHERE id = ?', [collaborateurId]);
|
||
const typeContrat = collab.length > 0 && collab[0].TypeContrat ? collab[0].TypeContrat : '37h';
|
||
|
||
const config = await getConfigurationRTT(conn, anneeRef, typeContrat);
|
||
const moisDepuisArrete = getMoisTravaillesRTT(dateReference, dateArrete);
|
||
acquisDepuisArrete = moisDepuisArrete * config.acquisitionMensuelle;
|
||
}
|
||
|
||
return acquisFigee + acquisDepuisArrete;
|
||
}
|
||
|
||
const pool = mysql.createPool(dbConfig);
|
||
|
||
const AZURE_CONFIG = {
|
||
tenantId: '9840a2a0-6ae1-4688-b03d-d2ec291be0f9',
|
||
clientId: '4bb4cc24-bac3-427c-b02c-5d14fc67b561',
|
||
clientSecret: 'gvf8Q~545Bafn8yYsgjW~QG_P1lpzaRe6gJNgb2t',
|
||
groupId: 'c1ea877c-6bca-4f47-bfad-f223640813a0'
|
||
};
|
||
|
||
const storage = multer.diskStorage({
|
||
destination: './uploads/',
|
||
filename: (req, file, cb) => {
|
||
cb(null, Date.now() + path.extname(file.originalname));
|
||
}
|
||
});
|
||
const upload = multer({ storage });
|
||
|
||
const medicalStorage = multer.diskStorage({
|
||
destination: './uploads/medical/',
|
||
filename: (req, file, cb) => {
|
||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
|
||
cb(null, 'medical-' + uniqueSuffix + path.extname(file.originalname));
|
||
}
|
||
});
|
||
|
||
const ACCES_TRANSVERSAUX = {
|
||
'sloisil@ensup.eu': {
|
||
typeAcces: 'service_multi_campus',
|
||
serviceNom: 'Pédagogie',
|
||
description: 'Sandrine - Vue complète Pédagogie (tous campus)'
|
||
},
|
||
'mbouteiller@ensup.eu': {
|
||
typeAcces: 'service_multi_campus',
|
||
serviceNom: 'Admissions',
|
||
description: 'Morgane - Vue complète Admissions (tous campus)'
|
||
},
|
||
'vnoel@ensup.eu': {
|
||
typeAcces: 'service_multi_campus',
|
||
serviceNom: 'Relations Entreprises',
|
||
description: 'Viviane - Vue complète Relations Entreprises (tous campus)'
|
||
},
|
||
'vpierrel@ensup.eu': {
|
||
typeAcces: 'service_multi_campus', // ✅ CORRIGÉ - même type que les autres
|
||
serviceNom: 'Administratif & Financier',
|
||
description: 'Virginie - Vue complète Administratif & Financier (tous campus)'
|
||
}
|
||
};
|
||
|
||
function getUserAccesTransversal(userEmail) {
|
||
const acces = ACCES_TRANSVERSAUX[userEmail?.toLowerCase()] || null;
|
||
if (acces) {
|
||
console.log(`🌐 Accès transversal: ${acces.description}`);
|
||
}
|
||
return acces;
|
||
}
|
||
|
||
|
||
|
||
const uploadMedical = multer({
|
||
storage: medicalStorage,
|
||
limits: { fileSize: 5 * 1024 * 1024 },
|
||
fileFilter: (req, file, cb) => {
|
||
const allowedTypes = ['application/pdf', 'image/jpeg', 'image/jpg', 'image/png'];
|
||
if (allowedTypes.includes(file.mimetype)) {
|
||
cb(null, true);
|
||
} else {
|
||
cb(new Error('Type de fichier non autorisé'));
|
||
}
|
||
}
|
||
});
|
||
import fs from 'fs';
|
||
if (!fs.existsSync('./uploads/medical')) {
|
||
fs.mkdirSync('./uploads/medical', { recursive: true });
|
||
}
|
||
|
||
app.get('/api/events/collaborateur', (req, res) => {
|
||
const userId = req.query.user_id;
|
||
|
||
if (!userId) {
|
||
return res.status(401).json({ error: 'user_id requis' });
|
||
}
|
||
|
||
console.log('🔔 Nouvelle connexion SSE collaborateur:', userId);
|
||
|
||
// ⭐ HEADERS CRITIQUES POUR SSE
|
||
res.setHeader('Content-Type', 'text/event-stream');
|
||
res.setHeader('Cache-Control', 'no-cache, no-transform');
|
||
res.setHeader('Connection', 'keep-alive');
|
||
res.setHeader('X-Accel-Buffering', 'no');
|
||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||
|
||
// ⭐ FLUSH IMMÉDIATEMENT POUR ÉTABLIR LA CONNEXION
|
||
res.flushHeaders();
|
||
|
||
const sendEvent = (data) => {
|
||
try {
|
||
if (res.writableEnded) {
|
||
console.log('⚠️ Tentative d\'envoi sur connexion fermée');
|
||
return false;
|
||
}
|
||
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
||
return true;
|
||
} catch (error) {
|
||
console.error('❌ Erreur envoi SSE:', error);
|
||
return false;
|
||
}
|
||
};
|
||
|
||
const client = {
|
||
userId: parseInt(userId),
|
||
send: sendEvent,
|
||
res: res // ⭐ Garder référence pour vérifier l'état
|
||
};
|
||
|
||
sseClientsCollab.add(client);
|
||
|
||
console.log(`📊 Clients SSE collaborateurs connectés: ${sseClientsCollab.size}`);
|
||
|
||
// ⭐ ÉVÉNEMENT DE CONNEXION
|
||
sendEvent({
|
||
type: 'connected',
|
||
message: 'Connexion établie',
|
||
timestamp: new Date().toISOString()
|
||
});
|
||
|
||
// ⭐ HEARTBEAT AVEC VÉRIFICATION
|
||
const heartbeat = setInterval(() => {
|
||
const success = sendEvent({
|
||
type: 'heartbeat',
|
||
timestamp: new Date().toISOString()
|
||
});
|
||
|
||
if (!success) {
|
||
console.log('💔 Heartbeat échoué, nettoyage...');
|
||
clearInterval(heartbeat);
|
||
sseClientsCollab.delete(client);
|
||
}
|
||
}, 30000); // 30 secondes
|
||
|
||
// ⭐ GESTION PROPRE DE LA DÉCONNEXION
|
||
const cleanup = () => {
|
||
console.log('🔌 Déconnexion SSE collaborateur:', userId);
|
||
clearInterval(heartbeat);
|
||
sseClientsCollab.delete(client);
|
||
console.log(`📊 Clients SSE collaborateurs connectés: ${sseClientsCollab.size}`);
|
||
};
|
||
|
||
req.on('close', cleanup);
|
||
req.on('error', (err) => {
|
||
console.error('❌ Erreur SSE connexion:', err.message);
|
||
cleanup();
|
||
});
|
||
|
||
// ⭐ TIMEOUT DE SÉCURITÉ (optionnel, mais recommandé)
|
||
req.socket.setTimeout(0); // Désactiver timeout pour SSE
|
||
});
|
||
|
||
|
||
const notifyCollabClients = (event, targetUserId = null) => {
|
||
console.log(
|
||
`📢 Notification SSE Collab: ${event.type}`,
|
||
targetUserId ? `pour user ${targetUserId}` : 'pour tous'
|
||
);
|
||
|
||
const deadClients = [];
|
||
|
||
sseClientsCollab.forEach(client => {
|
||
// ⭐ FILTRER PAR USER SI NÉCESSAIRE
|
||
if (targetUserId && client.userId !== targetUserId) {
|
||
return;
|
||
}
|
||
|
||
// ⭐ VÉRIFIER SI LA CONNEXION EST TOUJOURS ACTIVE
|
||
if (client.res && client.res.writableEnded) {
|
||
console.log(`💀 Client mort détecté: ${client.userId}`);
|
||
deadClients.push(client);
|
||
return;
|
||
}
|
||
|
||
// ⭐ ENVOYER L'ÉVÉNEMENT
|
||
const success = client.send(event);
|
||
if (!success) {
|
||
deadClients.push(client);
|
||
}
|
||
});
|
||
|
||
// ⭐ NETTOYER LES CLIENTS MORTS
|
||
deadClients.forEach(client => {
|
||
console.log(`🧹 Nettoyage client mort: ${client.userId}`);
|
||
sseClientsCollab.delete(client);
|
||
});
|
||
|
||
if (deadClients.length > 0) {
|
||
console.log(`📊 Clients SSE après nettoyage: ${sseClientsCollab.size}`);
|
||
}
|
||
};
|
||
|
||
|
||
app.post('/api/webhook/receive', async (req, res) => {
|
||
try {
|
||
const signature = req.headers['x-webhook-signature'];
|
||
const payload = req.body;
|
||
|
||
console.log('📥 Webhook reçu:', payload.event);
|
||
|
||
// Vérifier la signature
|
||
if (!webhookManager.verifySignature(payload, signature)) {
|
||
console.error('❌ Signature webhook invalide');
|
||
return res.status(401).json({ error: 'Signature invalide' });
|
||
}
|
||
|
||
const { event, data } = payload;
|
||
|
||
// Traiter selon le type d'événement
|
||
switch (event) {
|
||
case EVENTS.DEMANDE_VALIDATED:
|
||
console.log(`📥 Validation reçue: Demande ${data.demandeId} - Statut: ${data.statut}`);
|
||
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
await conn.beginTransaction();
|
||
|
||
// ⭐ GESTION DES COMPTEURS SELON LE STATUT
|
||
if (data.statut === 'Refusée' && data.collaborateurId) {
|
||
console.log(`❌ DEMANDE REFUSÉE - Restauration des soldes...`);
|
||
|
||
// Restaurer les soldes via la fonction existante
|
||
const restoration = await restoreLeaveBalance(
|
||
conn,
|
||
data.demandeId,
|
||
data.collaborateurId
|
||
);
|
||
|
||
console.log('✅ Restauration terminée:', restoration);
|
||
|
||
} else if (data.statut === 'Validée') {
|
||
console.log(`✅ DEMANDE VALIDÉE - Les jours ont déjà été déduits à la création`);
|
||
}
|
||
|
||
// ⭐ CRÉER UNE NOTIFICATION EN BASE DE DONNÉES
|
||
const notifTitle = data.statut === 'Validée'
|
||
? 'Demande approuvée ✅'
|
||
: 'Demande refusée ❌';
|
||
|
||
let notifMessage = `Votre demande a été ${data.statut === 'Validée' ? 'approuvée' : 'refusée'}`;
|
||
if (data.commentaire) {
|
||
notifMessage += ` (Commentaire: ${data.commentaire})`;
|
||
}
|
||
|
||
const notifType = data.statut === 'Validée' ? 'Success' : 'Error';
|
||
|
||
// ✅ FIX : Remplacer NOW() par nowFR()
|
||
await conn.query(
|
||
'INSERT INTO Notifications (CollaborateurADId, Titre, Message, Type, DemandeCongeId, DateCreation, lu) VALUES (?, ?, ?, ?, ?, ?, 0)',
|
||
[data.collaborateurId, notifTitle, notifMessage, notifType, data.demandeId, nowFR()]
|
||
);
|
||
|
||
console.log('✅ Notification créée en base de données');
|
||
|
||
await conn.commit();
|
||
|
||
} catch (error) {
|
||
await conn.rollback();
|
||
console.error('❌ Erreur traitement webhook:', error);
|
||
throw error;
|
||
} finally {
|
||
conn.release();
|
||
}
|
||
|
||
// Notifier les clients SSE
|
||
notifyCollabClients({
|
||
type: 'demande-validated-rh',
|
||
demandeId: data.demandeId,
|
||
statut: data.statut,
|
||
timestamp: new Date().toISOString()
|
||
}, data.collaborateurId);
|
||
|
||
notifyCollabClients({
|
||
type: 'demande-list-updated',
|
||
action: 'validation-rh',
|
||
demandeId: data.demandeId,
|
||
timestamp: new Date().toISOString()
|
||
});
|
||
break;
|
||
|
||
case EVENTS.COMPTEUR_UPDATED:
|
||
console.log(`🔄 Compteur mis à jour pour collaborateur ${data.collaborateurId}`);
|
||
|
||
notifyCollabClients({
|
||
type: 'compteur-updated',
|
||
collaborateurId: data.collaborateurId,
|
||
timestamp: new Date().toISOString()
|
||
}, data.collaborateurId);
|
||
break;
|
||
|
||
case EVENTS.DEMANDE_UPDATED:
|
||
console.log(`✏️ Demande ${data.demandeId} modifiée via RH`);
|
||
|
||
// ⭐ CRÉER UNE NOTIFICATION POUR LA MODIFICATION
|
||
const connUpdate = await pool.getConnection();
|
||
try {
|
||
// ✅ FIX : Remplacer NOW() par nowFR()
|
||
await connUpdate.query(
|
||
'INSERT INTO Notifications (CollaborateurADId, Titre, Message, Type, DemandeCongeId, DateCreation, lu) VALUES (?, ?, ?, ?, ?, ?, 0)',
|
||
[data.collaborateurId, 'Demande modifiée ✏️', 'Votre demande a été modifiée par le service RH', 'Info', data.demandeId, nowFR()]
|
||
);
|
||
console.log('✅ Notification modification créée');
|
||
} catch (error) {
|
||
console.error('❌ Erreur création notification:', error);
|
||
} finally {
|
||
connUpdate.release();
|
||
}
|
||
|
||
notifyCollabClients({
|
||
type: 'demande-updated-rh',
|
||
demandeId: data.demandeId,
|
||
timestamp: new Date().toISOString()
|
||
}, data.collaborateurId);
|
||
break;
|
||
|
||
case EVENTS.DEMANDE_DELETED:
|
||
console.log(`🗑️ Demande ${data.demandeId} supprimée via RH`);
|
||
|
||
// ⭐ CRÉER UNE NOTIFICATION POUR LA SUPPRESSION
|
||
const connDelete = await pool.getConnection();
|
||
try {
|
||
// ✅ FIX : Remplacer NOW() par nowFR()
|
||
await connDelete.query(
|
||
'INSERT INTO Notifications (CollaborateurADId, Titre, Message, Type, DemandeCongeId, DateCreation, lu) VALUES (?, ?, ?, ?, ?, ?, 0)',
|
||
[data.collaborateurId, 'Demande supprimée 🗑️', 'Votre demande a été supprimée par le service RH', 'Warning', data.demandeId, nowFR()]
|
||
);
|
||
console.log('✅ Notification suppression créée');
|
||
} catch (error) {
|
||
console.error('❌ Erreur création notification:', error);
|
||
} finally {
|
||
connDelete.release();
|
||
}
|
||
|
||
notifyCollabClients({
|
||
type: 'demande-deleted-rh',
|
||
demandeId: data.demandeId,
|
||
timestamp: new Date().toISOString()
|
||
}, data.collaborateurId);
|
||
break;
|
||
|
||
default:
|
||
console.warn(`⚠️ Type d'événement webhook inconnu: ${event}`);
|
||
}
|
||
|
||
res.json({ success: true, message: 'Webhook traité' });
|
||
|
||
} catch (error) {
|
||
console.error('❌ Erreur traitement webhook:', error);
|
||
res.status(500).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
function getDateFinMoisPrecedent(referenceDate = new Date()) {
|
||
const now = new Date(referenceDate);
|
||
now.setHours(0, 0, 0, 0);
|
||
return new Date(now.getFullYear(), now.getMonth(), 0);
|
||
}
|
||
|
||
function parseDateYYYYMMDD(s) {
|
||
if (!s) return null;
|
||
if (/^\d{4}-\d{2}-\d{2}$/.test(s)) {
|
||
const [y, m, d] = s.split('-').map(Number);
|
||
return new Date(y, m - 1, d);
|
||
}
|
||
return new Date(s);
|
||
}
|
||
|
||
const LEAVE_RULES = {
|
||
CP: {
|
||
nom: 'Congé payé',
|
||
joursAnnuels: 25,
|
||
periodeDebut: { mois: 6, jour: 1 },
|
||
periodeFin: { mois: 5, jour: 31 },
|
||
acquisitionMensuelle: 25 / 12,
|
||
reportable: true,
|
||
periodeReport: 'exercice'
|
||
},
|
||
RTT: {
|
||
nom: 'RTT',
|
||
joursAnnuels: 10,
|
||
periodeDebut: { mois: 1, jour: 1 },
|
||
periodeFin: { mois: 12, jour: 31 },
|
||
acquisitionMensuelle: 10 / 12,
|
||
reportable: false,
|
||
periodeReport: null
|
||
}
|
||
};
|
||
|
||
// ========================================
|
||
// NOUVELLES FONCTIONS POUR RTT VARIABLES
|
||
// ========================================
|
||
|
||
/**
|
||
* Récupère la configuration RTT pour une année et un type de contrat donnés
|
||
*/
|
||
async function getConfigurationRTT(conn, annee, typeContrat = '37h') {
|
||
try {
|
||
const [config] = await conn.query(
|
||
`SELECT JoursAnnuels, AcquisitionMensuelle
|
||
FROM ConfigurationRTT
|
||
WHERE Annee = ? AND TypeContrat = ?
|
||
LIMIT 1`,
|
||
[annee, typeContrat]
|
||
);
|
||
|
||
if (config.length > 0) {
|
||
return {
|
||
joursAnnuels: parseFloat(config[0].JoursAnnuels),
|
||
acquisitionMensuelle: parseFloat(config[0].AcquisitionMensuelle)
|
||
};
|
||
}
|
||
|
||
// Valeurs par défaut si pas de config trouvée
|
||
console.warn(`⚠️ Pas de config RTT pour ${annee}/${typeContrat}, utilisation des valeurs par défaut`);
|
||
return typeContrat === 'forfait_jour'
|
||
? { joursAnnuels: 12, acquisitionMensuelle: 1.0 }
|
||
: { joursAnnuels: 10, acquisitionMensuelle: 0.833333 };
|
||
} catch (error) {
|
||
console.error('Erreur getConfigurationRTT:', error);
|
||
// Retour valeur par défaut en cas d'erreur
|
||
return { joursAnnuels: 10, acquisitionMensuelle: 0.833333 };
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Calcule l'acquisition RTT en tenant compte du type de contrat et de l'année
|
||
*/
|
||
async function calculerAcquisitionRTT(conn, collaborateurId, dateReference = new Date()) {
|
||
try {
|
||
const d = new Date(dateReference);
|
||
const annee = d.getFullYear();
|
||
|
||
// Récupérer le type de contrat et la date d'entrée du collaborateur
|
||
const [collabInfo] = await conn.query(
|
||
`SELECT TypeContrat, DateEntree, CampusId FROM CollaborateurAD WHERE id = ?`,
|
||
[collaborateurId]
|
||
);
|
||
|
||
if (collabInfo.length === 0) {
|
||
throw new Error(`Collaborateur ${collaborateurId} non trouvé`);
|
||
}
|
||
|
||
const typeContrat = collabInfo[0].TypeContrat || '37h';
|
||
const dateEntree = collabInfo[0].DateEntree;
|
||
|
||
// Récupérer la configuration RTT pour l'année et le type de contrat
|
||
const config = await getConfigurationRTT(conn, annee, typeContrat);
|
||
|
||
// Calculer les mois travaillés dans l'année
|
||
const moisTravailles = getMoisTravaillesRTT(dateReference, dateEntree);
|
||
|
||
// Calculer l'acquisition cumulée
|
||
const acquisition = moisTravailles * config.acquisitionMensuelle;
|
||
|
||
return {
|
||
acquisition: Math.round(acquisition * 100) / 100,
|
||
moisTravailles: parseFloat(moisTravailles.toFixed(2)),
|
||
config: config,
|
||
typeContrat: typeContrat
|
||
};
|
||
} catch (error) {
|
||
console.error('Erreur calculerAcquisitionRTT:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Calcule l'acquisition CP (inchangé, mais pour cohérence)
|
||
*/
|
||
function calculerAcquisitionCP(dateReference = new Date(), dateEntree = null) {
|
||
const moisTravailles = getMoisTravaillesCP(dateReference, dateEntree);
|
||
const acquisition = moisTravailles * (25 / 12);
|
||
return Math.round(acquisition * 100) / 100;
|
||
}
|
||
|
||
// ========================================
|
||
// TÂCHES CRON
|
||
// ========================================
|
||
|
||
cron.schedule('0 2 * * *', async () => {
|
||
console.log('🔄 [CRON] Mise à jour quotidienne des compteurs...');
|
||
try {
|
||
const conn = await pool.getConnection();
|
||
await conn.beginTransaction();
|
||
|
||
const [collaborateurs] = await conn.query(`
|
||
SELECT id, prenom, nom, CampusId
|
||
FROM CollaborateurAD
|
||
WHERE (actif = 1 OR actif IS NULL)
|
||
`);
|
||
|
||
let successCount = 0;
|
||
const today = new Date();
|
||
|
||
for (const collab of collaborateurs) {
|
||
try {
|
||
await updateMonthlyCounters(conn, collab.id, today);
|
||
successCount++;
|
||
} catch (error) {
|
||
console.error(`❌ Erreur pour ${collab.prenom} ${collab.nom}:`, error.message);
|
||
}
|
||
}
|
||
|
||
await conn.commit();
|
||
console.log(`✅ [CRON] ${successCount}/${collaborateurs.length} compteurs mis à jour`);
|
||
conn.release();
|
||
} catch (error) {
|
||
console.error('❌ [CRON] Erreur mise à jour quotidienne:', error);
|
||
}
|
||
});
|
||
|
||
cron.schedule('59 23 31 12 *', async () => {
|
||
console.log('🎆 [CRON] Traitement fin d\'année RTT...');
|
||
try {
|
||
const conn = await pool.getConnection();
|
||
await conn.beginTransaction();
|
||
const [collaborateurs] = await conn.query('SELECT id, CampusId FROM CollaborateurAD');
|
||
let successCount = 0;
|
||
for (const collab of collaborateurs) {
|
||
try {
|
||
await processEndOfYearRTT(conn, collab.id);
|
||
successCount++;
|
||
} catch (error) {
|
||
console.error(`❌ Erreur RTT pour ${collab.id}:`, error.message);
|
||
}
|
||
}
|
||
await conn.commit();
|
||
console.log(`✅ [CRON] ${successCount}/${collaborateurs.length} RTT réinitialisés`);
|
||
conn.release();
|
||
} catch (error) {
|
||
console.error('❌ [CRON] Erreur traitement fin d\'année:', error);
|
||
}
|
||
});
|
||
|
||
cron.schedule('59 23 31 5 *', async () => {
|
||
console.log('📅 [CRON] Traitement fin d\'exercice CP...');
|
||
try {
|
||
const conn = await pool.getConnection();
|
||
await conn.beginTransaction();
|
||
const [collaborateurs] = await conn.query('SELECT id, CampusId FROM CollaborateurAD');
|
||
let successCount = 0;
|
||
for (const collab of collaborateurs) {
|
||
try {
|
||
await processEndOfExerciceCP(conn, collab.id);
|
||
successCount++;
|
||
} catch (error) {
|
||
console.error(`❌ Erreur CP pour ${collab.id}:`, error.message);
|
||
}
|
||
}
|
||
await conn.commit();
|
||
console.log(`✅ [CRON] ${successCount}/${collaborateurs.length} CP reportés`);
|
||
conn.release();
|
||
} catch (error) {
|
||
console.error('❌ [CRON] Erreur traitement fin d\'exercice:', error);
|
||
}
|
||
});
|
||
|
||
// ========================================
|
||
// CRON : CRÉER ARRÊTÉS MENSUELS AUTOMATIQUEMENT
|
||
// ========================================
|
||
|
||
cron.schedule('55 23 28-31 * *', async () => {
|
||
const today = new Date();
|
||
const lastDay = new Date(today.getFullYear(), today.getMonth() + 1, 0);
|
||
|
||
// ⭐ Vérifier qu'on est bien le dernier jour du mois
|
||
if (today.getDate() === lastDay.getDate()) {
|
||
console.log(`📅 [CRON] Création arrêté fin de mois: ${today.toISOString().split('T')[0]}`);
|
||
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
await conn.beginTransaction();
|
||
|
||
const annee = today.getFullYear();
|
||
const mois = today.getMonth() + 1; // 1-12
|
||
const dateArrete = today.toISOString().split('T')[0];
|
||
|
||
// ⭐ Vérifier si l'arrêté n'existe pas déjà
|
||
const [existing] = await conn.query(
|
||
'SELECT Id FROM ArreteComptable WHERE Annee = ? AND Mois = ?',
|
||
[annee, mois]
|
||
);
|
||
|
||
if (existing.length > 0) {
|
||
console.log(`⚠️ [CRON] Arrêté ${mois}/${annee} existe déjà, skip`);
|
||
await conn.rollback();
|
||
conn.release();
|
||
return;
|
||
}
|
||
|
||
// ⭐ Créer l'arrêté
|
||
const [result] = await conn.query(`
|
||
INSERT INTO ArreteComptable
|
||
(DateArrete, Annee, Mois, Libelle, Description, Statut, DateCreation)
|
||
VALUES (?, ?, ?, ?, ?, 'En cours', NOW())
|
||
`, [
|
||
dateArrete,
|
||
annee,
|
||
mois,
|
||
`Arrêté comptable ${getMonthName(mois)} ${annee}`,
|
||
`Arrêté mensuel automatique - Clôture des soldes au ${dateArrete}`
|
||
]);
|
||
|
||
const arreteId = result.insertId;
|
||
console.log(`✅ [CRON] Arrêté créé: ID ${arreteId}`);
|
||
|
||
// ⭐ Créer le snapshot
|
||
await conn.query('CALL sp_creer_snapshot_arrete(?)', [arreteId]);
|
||
console.log(`📸 [CRON] Snapshot créé pour l'arrêté ${arreteId}`);
|
||
|
||
// ⭐ Compter les soldes figés
|
||
const [count] = await conn.query(
|
||
'SELECT COUNT(*) as total FROM SoldesFiges WHERE ArreteId = ?',
|
||
[arreteId]
|
||
);
|
||
|
||
await conn.commit();
|
||
|
||
console.log(`🎉 [CRON] Arrêté ${mois}/${annee} terminé: ${count[0].total} soldes figés`);
|
||
|
||
} catch (error) {
|
||
await conn.rollback();
|
||
console.error(`❌ [CRON] Erreur création arrêté:`, error.message);
|
||
} finally {
|
||
conn.release();
|
||
}
|
||
}
|
||
});
|
||
|
||
// Mail mensuel le 1er à 9h
|
||
cron.schedule('0 9 1 * *', async () => {
|
||
console.log('📧 Envoi mails compte-rendu mensuel...');
|
||
const conn = await pool.getConnection();
|
||
|
||
const [cadres] = await conn.query(`
|
||
SELECT id, email, prenom, nom
|
||
FROM CollaborateurAD
|
||
WHERE TypeContrat = 'forfait_jour' AND (actif = 1 OR actif IS NULL)
|
||
`);
|
||
|
||
const moisPrecedent = new Date();
|
||
moisPrecedent.setMonth(moisPrecedent.getMonth() - 1);
|
||
|
||
for (const cadre of cadres) {
|
||
// Envoyer mail via Microsoft Graph API
|
||
// Enregistrer dans MailsCompteRendu
|
||
}
|
||
|
||
conn.release();
|
||
});
|
||
|
||
// Relance hebdomadaire le lundi à 9h
|
||
cron.schedule('0 9 * * 1', async () => {
|
||
console.log('🔔 Relance hebdomadaire compte-rendu...');
|
||
const conn = await pool.getConnection();
|
||
|
||
const moisCourant = new Date().getMonth() + 1;
|
||
const anneeCourante = new Date().getFullYear();
|
||
|
||
const [nonValides] = await conn.query(`
|
||
SELECT DISTINCT ca.id, ca.email, ca.prenom, ca.nom
|
||
FROM CollaborateurAD ca
|
||
LEFT JOIN CompteRenduMensuel crm ON ca.id = crm.CollaborateurADId
|
||
AND crm.Annee = ? AND crm.Mois = ?
|
||
WHERE ca.TypeContrat = 'forfait_jour'
|
||
AND (crm.Statut IS NULL OR crm.Statut != 'Validé')
|
||
`, [anneeCourante, moisCourant - 1]);
|
||
|
||
for (const cadre of nonValides) {
|
||
// Envoyer relance
|
||
}
|
||
|
||
conn.release();
|
||
});
|
||
|
||
|
||
// ⭐ Fonction helper pour les noms de mois
|
||
function getMonthName(mois) {
|
||
const mois_names = ['', 'Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin',
|
||
'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'];
|
||
return mois_names[mois] || mois;
|
||
}
|
||
|
||
async function getGraphToken() {
|
||
try {
|
||
const params = new URLSearchParams({
|
||
grant_type: 'client_credentials',
|
||
client_id: AZURE_CONFIG.clientId,
|
||
client_secret: AZURE_CONFIG.clientSecret,
|
||
scope: 'https://graph.microsoft.com/.default'
|
||
});
|
||
const response = await axios.post(
|
||
`https://login.microsoftonline.com/${AZURE_CONFIG.tenantId}/oauth2/v2.0/token`,
|
||
params.toString(),
|
||
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
|
||
);
|
||
return response.data.access_token;
|
||
} catch (error) {
|
||
console.error('Erreur obtention token:', error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
async function sendMailGraph(accessToken, fromEmail, toEmail, subject, bodyHtml) {
|
||
try {
|
||
await axios.post(
|
||
`https://graph.microsoft.com/v1.0/users/${fromEmail}/sendMail`,
|
||
{
|
||
message: {
|
||
subject,
|
||
body: { contentType: 'HTML', content: bodyHtml },
|
||
toRecipients: [{ emailAddress: { address: toEmail } }]
|
||
},
|
||
saveToSentItems: false
|
||
},
|
||
{
|
||
headers: {
|
||
Authorization: `Bearer ${accessToken}`,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
}
|
||
);
|
||
return true;
|
||
} catch (error) {
|
||
console.error('Erreur envoi email:', error);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
function getWorkingDays(startDate, endDate) {
|
||
let workingDays = 0;
|
||
const current = new Date(startDate);
|
||
const end = new Date(endDate);
|
||
while (current <= end) {
|
||
const dayOfWeek = current.getDay();
|
||
if (dayOfWeek !== 0 && dayOfWeek !== 6) {
|
||
workingDays++;
|
||
}
|
||
current.setDate(current.getDate() + 1);
|
||
}
|
||
return workingDays;
|
||
}
|
||
|
||
function formatDate(date) {
|
||
const d = new Date(date);
|
||
const day = String(d.getDate()).padStart(2, '0');
|
||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||
const year = d.getFullYear();
|
||
return `${day}/${month}/${year}`;
|
||
}
|
||
|
||
function getExerciceCP(date = new Date()) {
|
||
const d = new Date(date);
|
||
const annee = d.getFullYear();
|
||
const mois = d.getMonth() + 1;
|
||
if (mois >= 1 && mois <= 5) {
|
||
return `${annee - 1}-${annee}`;
|
||
}
|
||
return `${annee}-${annee + 1}`;
|
||
}
|
||
|
||
function getMoisTravaillesCP(date = new Date(), dateEntree = null) {
|
||
const d = new Date(date);
|
||
d.setHours(0, 0, 0, 0);
|
||
const annee = d.getFullYear();
|
||
const mois = d.getMonth() + 1;
|
||
let debutExercice;
|
||
if (mois >= 6) {
|
||
debutExercice = new Date(annee, 5, 1);
|
||
} else {
|
||
debutExercice = new Date(annee - 1, 5, 1);
|
||
}
|
||
debutExercice.setHours(0, 0, 0, 0);
|
||
if (dateEntree) {
|
||
const entree = new Date(dateEntree);
|
||
entree.setHours(0, 0, 0, 0);
|
||
if (entree > debutExercice) {
|
||
debutExercice = entree;
|
||
}
|
||
}
|
||
const diffMs = d - debutExercice;
|
||
const diffJours = Math.floor(diffMs / (1000 * 60 * 60 * 24)) + 1;
|
||
const moisTravailles = diffJours / 30.44;
|
||
return Math.max(0, Math.min(12, moisTravailles));
|
||
}
|
||
|
||
function getMoisTravaillesRTT(date = new Date(), dateEntree = null) {
|
||
const d = new Date(date);
|
||
d.setHours(0, 0, 0, 0);
|
||
const annee = d.getFullYear();
|
||
let debutAnnee = new Date(annee, 0, 1);
|
||
debutAnnee.setHours(0, 0, 0, 0);
|
||
if (dateEntree) {
|
||
const entree = new Date(dateEntree);
|
||
entree.setHours(0, 0, 0, 0);
|
||
if (entree.getFullYear() === annee && entree > debutAnnee) {
|
||
debutAnnee = entree;
|
||
} else if (entree.getFullYear() > annee) {
|
||
return 0;
|
||
}
|
||
}
|
||
const diffMs = d - debutAnnee;
|
||
const diffJours = Math.floor(diffMs / (1000 * 60 * 60 * 24)) + 1;
|
||
const moisTravailles = diffJours / 30.44;
|
||
return Math.max(0, Math.min(12, moisTravailles));
|
||
}
|
||
|
||
function calculerAcquisitionCumulee(typeConge, dateReference = new Date(), dateEntree = null) {
|
||
const rules = LEAVE_RULES[typeConge];
|
||
if (!rules) return 0;
|
||
let moisTravailles;
|
||
if (typeConge === 'CP') {
|
||
moisTravailles = getMoisTravaillesCP(dateReference, dateEntree);
|
||
} else {
|
||
moisTravailles = getMoisTravaillesRTT(dateReference, dateEntree);
|
||
}
|
||
const acquisition = moisTravailles * rules.acquisitionMensuelle;
|
||
return Math.round(acquisition * 100) / 100;
|
||
}
|
||
|
||
async function processEndOfYearRTT(conn, collaborateurId) {
|
||
const currentYear = new Date().getFullYear();
|
||
const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']);
|
||
if (rttType.length === 0) return null;
|
||
await conn.query(
|
||
`UPDATE CompteurConges SET Solde = 0, Total = 0, SoldeReporte = 0, DerniereMiseAJour = NOW() WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, rttType[0].Id, currentYear]
|
||
);
|
||
return { type: 'RTT', action: 'reset_end_of_year', annee: currentYear };
|
||
}
|
||
|
||
async function processEndOfExerciceCP(conn, collaborateurId) {
|
||
const today = new Date();
|
||
const currentYear = today.getFullYear();
|
||
const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']);
|
||
if (cpType.length === 0) return null;
|
||
const cpTypeId = cpType[0].Id;
|
||
const [currentCounter] = await conn.query(
|
||
`SELECT Id, Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, cpTypeId, currentYear]
|
||
);
|
||
if (currentCounter.length === 0) return null;
|
||
const soldeAReporter = parseFloat(currentCounter[0].Solde);
|
||
const [nextYearCounter] = await conn.query(
|
||
`SELECT Id FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, cpTypeId, currentYear + 1]
|
||
);
|
||
if (nextYearCounter.length > 0) {
|
||
await conn.query(
|
||
`UPDATE CompteurConges SET SoldeReporte = ?, Solde = Solde + ?, DerniereMiseAJour = NOW() WHERE Id = ?`,
|
||
[soldeAReporter, soldeAReporter, nextYearCounter[0].Id]
|
||
);
|
||
} else {
|
||
await conn.query(
|
||
`INSERT INTO CompteurConges (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour) VALUES (?, ?, ?, 0, ?, ?, NOW())`,
|
||
[collaborateurId, cpTypeId, currentYear + 1, soldeAReporter, soldeAReporter]
|
||
);
|
||
}
|
||
return { type: 'CP', action: 'report_exercice', soldeReporte: soldeAReporter };
|
||
}
|
||
|
||
async function deductLeaveBalance(conn, collaborateurId, typeCongeId, nombreJours) {
|
||
const currentYear = new Date().getFullYear();
|
||
const previousYear = currentYear - 1;
|
||
let joursRestants = nombreJours;
|
||
const deductions = [];
|
||
const [compteurN1] = await conn.query(
|
||
`SELECT Id, Solde, SoldeReporte FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, typeCongeId, previousYear]
|
||
);
|
||
if (compteurN1.length > 0 && compteurN1[0].SoldeReporte > 0) {
|
||
const soldeN1 = parseFloat(compteurN1[0].SoldeReporte);
|
||
const aDeduireN1 = Math.min(soldeN1, joursRestants);
|
||
if (aDeduireN1 > 0) {
|
||
await conn.query(
|
||
`UPDATE CompteurConges SET SoldeReporte = GREATEST(0, SoldeReporte - ?), Solde = GREATEST(0, Solde - ?) WHERE Id = ?`,
|
||
[aDeduireN1, aDeduireN1, compteurN1[0].Id]
|
||
);
|
||
deductions.push({ annee: previousYear, type: 'Reporté N-1', joursUtilises: aDeduireN1, soldeAvant: soldeN1 });
|
||
joursRestants -= aDeduireN1;
|
||
}
|
||
}
|
||
if (joursRestants > 0) {
|
||
const [compteurN] = await conn.query(
|
||
`SELECT Id, Solde, SoldeReporte FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, typeCongeId, currentYear]
|
||
);
|
||
if (compteurN.length > 0) {
|
||
const soldeN = parseFloat(compteurN[0].Solde) - parseFloat(compteurN[0].SoldeReporte || 0);
|
||
const aDeduireN = Math.min(soldeN, joursRestants);
|
||
if (aDeduireN > 0) {
|
||
await conn.query(
|
||
`UPDATE CompteurConges SET Solde = GREATEST(0, Solde - ?) WHERE Id = ?`,
|
||
[aDeduireN, compteurN[0].Id]
|
||
);
|
||
deductions.push({ annee: currentYear, type: 'Année actuelle N', joursUtilises: aDeduireN, soldeAvant: soldeN });
|
||
joursRestants -= aDeduireN;
|
||
}
|
||
}
|
||
}
|
||
return { success: joursRestants === 0, joursDeduitsTotal: nombreJours - joursRestants, joursNonDeduits: joursRestants, details: deductions };
|
||
}
|
||
|
||
async function checkLeaveBalance(conn, collaborateurId, repartition) {
|
||
const currentYear = new Date().getFullYear();
|
||
const previousYear = currentYear - 1;
|
||
const verification = [];
|
||
for (const rep of repartition) {
|
||
const typeCode = rep.TypeConge;
|
||
const joursNecessaires = parseFloat(rep.NombreJours);
|
||
if (typeCode === 'ABS' || typeCode === 'Formation') continue;
|
||
const typeName = typeCode === 'CP' ? 'Congé payé' : typeCode === 'RTT' ? 'RTT' : typeCode;
|
||
const [typeRow] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', [typeName]);
|
||
if (typeRow.length === 0) continue;
|
||
const typeCongeId = typeRow[0].Id;
|
||
const [compteurN1] = await conn.query(
|
||
`SELECT SoldeReporte FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, typeCongeId, previousYear]
|
||
);
|
||
const [compteurN] = await conn.query(
|
||
`SELECT Solde, SoldeReporte FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, typeCongeId, currentYear]
|
||
);
|
||
const soldeN1 = compteurN1.length > 0 ? parseFloat(compteurN1[0].SoldeReporte || 0) : 0;
|
||
const soldeN = compteurN.length > 0 ? parseFloat(compteurN[0].Solde || 0) - parseFloat(compteurN[0].SoldeReporte || 0) : 0;
|
||
const soldeTotal = soldeN1 + soldeN;
|
||
verification.push({ type: typeName, joursNecessaires, soldeN1, soldeN, soldeTotal, suffisant: soldeTotal >= joursNecessaires, deficit: Math.max(0, joursNecessaires - soldeTotal) });
|
||
}
|
||
const insuffisants = verification.filter(v => !v.suffisant);
|
||
return { valide: insuffisants.length === 0, details: verification, insuffisants };
|
||
}
|
||
|
||
// ========================================
|
||
// MISE À JOUR DE updateMonthlyCounters
|
||
// ========================================
|
||
|
||
async function updateMonthlyCounters(conn, collaborateurId, dateReference = null) {
|
||
const today = dateReference ? new Date(dateReference) : getDateFinMoisPrecedent();
|
||
const currentYear = today.getFullYear();
|
||
const updates = [];
|
||
|
||
const [collabInfo] = await conn.query(
|
||
'SELECT DateEntree, TypeContrat, CampusId FROM CollaborateurAD WHERE id = ?',
|
||
[collaborateurId]
|
||
);
|
||
const dateEntree = collabInfo.length > 0 && collabInfo[0].DateEntree ? collabInfo[0].DateEntree : null;
|
||
const typeContrat = collabInfo.length > 0 && collabInfo[0].TypeContrat ? collabInfo[0].TypeContrat : '37h';
|
||
|
||
// ===== CP (inchangé) =====
|
||
const exerciceCP = getExerciceCP(today);
|
||
const acquisitionCP = calculerAcquisitionCP(today, dateEntree);
|
||
|
||
const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']);
|
||
if (cpType.length > 0) {
|
||
const cpTypeId = cpType[0].Id;
|
||
const [existingCP] = await conn.query(
|
||
`SELECT Id, Total, Solde, SoldeReporte
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, cpTypeId, currentYear]
|
||
);
|
||
|
||
if (existingCP.length > 0) {
|
||
const ancienTotal = parseFloat(existingCP[0].Total);
|
||
const ancienSolde = parseFloat(existingCP[0].Solde);
|
||
const soldeReporte = parseFloat(existingCP[0].SoldeReporte || 0);
|
||
const incrementTotal = acquisitionCP - ancienTotal;
|
||
const nouveauSolde = ancienSolde + incrementTotal;
|
||
|
||
await conn.query(
|
||
`UPDATE CompteurConges
|
||
SET Total = ?, Solde = ?, DerniereMiseAJour = NOW()
|
||
WHERE Id = ?`,
|
||
[acquisitionCP, Math.max(0, nouveauSolde), existingCP[0].Id]
|
||
);
|
||
|
||
updates.push({
|
||
type: 'CP',
|
||
exercice: exerciceCP,
|
||
acquisitionCumulee: acquisitionCP,
|
||
increment: incrementTotal,
|
||
nouveauSolde: Math.max(0, nouveauSolde)
|
||
});
|
||
} else {
|
||
await conn.query(
|
||
`INSERT INTO CompteurConges
|
||
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour)
|
||
VALUES (?, ?, ?, ?, ?, 0, NOW())`,
|
||
[collaborateurId, cpTypeId, currentYear, acquisitionCP, acquisitionCP]
|
||
);
|
||
|
||
updates.push({
|
||
type: 'CP',
|
||
exercice: exerciceCP,
|
||
acquisitionCumulee: acquisitionCP,
|
||
action: 'created',
|
||
nouveauSolde: acquisitionCP
|
||
});
|
||
}
|
||
}
|
||
|
||
// ===== RTT (NOUVEAU avec gestion variable) =====
|
||
const rttData = await calculerAcquisitionRTT(conn, collaborateurId, today);
|
||
const acquisitionRTT = rttData.acquisition;
|
||
|
||
const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']);
|
||
if (rttType.length > 0) {
|
||
const rttTypeId = rttType[0].Id;
|
||
const [existingRTT] = await conn.query(
|
||
`SELECT Id, Total, Solde
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, rttTypeId, currentYear]
|
||
);
|
||
|
||
if (existingRTT.length > 0) {
|
||
const ancienTotal = parseFloat(existingRTT[0].Total);
|
||
const ancienSolde = parseFloat(existingRTT[0].Solde);
|
||
const incrementTotal = acquisitionRTT - ancienTotal;
|
||
const nouveauSolde = ancienSolde + incrementTotal;
|
||
|
||
await conn.query(
|
||
`UPDATE CompteurConges
|
||
SET Total = ?, Solde = ?, DerniereMiseAJour = NOW()
|
||
WHERE Id = ?`,
|
||
[acquisitionRTT, Math.max(0, nouveauSolde), existingRTT[0].Id]
|
||
);
|
||
|
||
updates.push({
|
||
type: 'RTT',
|
||
annee: currentYear,
|
||
typeContrat: rttData.typeContrat,
|
||
config: `${rttData.config.joursAnnuels}j/an`,
|
||
moisTravailles: rttData.moisTravailles,
|
||
acquisitionCumulee: acquisitionRTT,
|
||
increment: incrementTotal,
|
||
nouveauSolde: Math.max(0, nouveauSolde)
|
||
});
|
||
} else {
|
||
await conn.query(
|
||
`INSERT INTO CompteurConges
|
||
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour)
|
||
VALUES (?, ?, ?, ?, ?, 0, NOW())`,
|
||
[collaborateurId, rttTypeId, currentYear, acquisitionRTT, acquisitionRTT]
|
||
);
|
||
|
||
updates.push({
|
||
type: 'RTT',
|
||
annee: currentYear,
|
||
typeContrat: rttData.typeContrat,
|
||
config: `${rttData.config.joursAnnuels}j/an`,
|
||
moisTravailles: rttData.moisTravailles,
|
||
acquisitionCumulee: acquisitionRTT,
|
||
action: 'created',
|
||
nouveauSolde: acquisitionRTT
|
||
});
|
||
}
|
||
}
|
||
|
||
return updates;
|
||
}
|
||
|
||
// ========================================
|
||
// ROUTES API
|
||
// ========================================
|
||
|
||
app.post('/login', async (req, res) => {
|
||
try {
|
||
const { email, mot_de_passe, entraUserId, userPrincipalName } = req.body;
|
||
const accessToken = req.headers.authorization?.replace('Bearer ', '');
|
||
|
||
if (accessToken && entraUserId) {
|
||
const [users] = await pool.query(`
|
||
SELECT ca.*, s.Nom as service, so.Nom as societe_nom
|
||
FROM CollaborateurAD ca
|
||
LEFT JOIN Services s ON ca.ServiceId = s.Id
|
||
LEFT JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE ca.entraUserId=? OR ca.email=?
|
||
LIMIT 1
|
||
`, [entraUserId, email]);
|
||
|
||
if (users.length === 0) return res.json({ success: false, message: 'Utilisateur non autorisé' });
|
||
const user = users[0];
|
||
|
||
try {
|
||
const graphResponse = await axios.get(`https://graph.microsoft.com/v1.0/users/${userPrincipalName}/memberOf?$select=id`, { headers: { Authorization: `Bearer ${accessToken}` } });
|
||
const userGroups = graphResponse.data.value.map(g => g.id);
|
||
const [allowedGroups] = await pool.query('SELECT Id FROM EntraGroups WHERE IsActive=1');
|
||
const allowed = allowedGroups.map(g => g.Id);
|
||
const authorized = userGroups.some(g => allowed.includes(g));
|
||
|
||
if (authorized) {
|
||
return res.json({
|
||
success: true,
|
||
message: 'Connexion réussie via Azure AD',
|
||
user: {
|
||
id: user.id,
|
||
prenom: user.prenom,
|
||
nom: user.nom,
|
||
email: user.email,
|
||
role: user.role,
|
||
service: user.service,
|
||
societeId: user.SocieteId,
|
||
societeNom: user.societe_nom
|
||
}
|
||
});
|
||
} else {
|
||
return res.json({ success: false, message: 'Utilisateur non autorisé' });
|
||
}
|
||
} catch (error) {
|
||
return res.json({ success: false, message: 'Erreur vérification groupes' });
|
||
}
|
||
}
|
||
|
||
if (email && mot_de_passe) {
|
||
const [users] = await pool.query(`
|
||
SELECT u.ID, u.Prenom, u.Nom, u.Email, u.Role, u.ServiceId, s.Nom AS ServiceNom
|
||
FROM Users u
|
||
LEFT JOIN Services s ON u.ServiceId = s.Id
|
||
WHERE u.Email = ? AND u.MDP = ?
|
||
`, [email, mot_de_passe]);
|
||
|
||
if (users.length === 1) {
|
||
return res.json({
|
||
success: true,
|
||
message: 'Connexion réussie',
|
||
user: {
|
||
id: users[0].ID,
|
||
prenom: users[0].Prenom,
|
||
nom: users[0].Nom,
|
||
email: users[0].Email,
|
||
role: users[0].Role,
|
||
service: users[0].ServiceNom || 'Non défini'
|
||
}
|
||
});
|
||
}
|
||
return res.json({ success: false, message: 'Identifiants incorrects' });
|
||
}
|
||
|
||
res.json({ success: false, message: 'Aucune méthode de connexion fournie' });
|
||
} catch (error) {
|
||
res.status(500).json({ success: false, message: 'Erreur serveur', error: error.message });
|
||
}
|
||
});
|
||
|
||
app.post('/check-user-groups', async (req, res) => {
|
||
try {
|
||
const { userPrincipalName } = req.body;
|
||
const accessToken = req.headers.authorization?.replace('Bearer ', '');
|
||
|
||
if (!userPrincipalName || !accessToken) return res.json({ authorized: false, message: 'Email ou token manquant' });
|
||
|
||
const [users] = await pool.query(`
|
||
SELECT ca.id, ca.entraUserId, ca.prenom, ca.nom, ca.email,
|
||
s.Nom as service, ca.role, ca.CampusId, ca.SocieteId,
|
||
so.Nom as societe_nom
|
||
FROM CollaborateurAD ca
|
||
LEFT JOIN Services s ON ca.ServiceId = s.Id
|
||
LEFT JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE ca.email = ?
|
||
LIMIT 1
|
||
`, [userPrincipalName]);
|
||
|
||
if (users.length > 0) {
|
||
const user = users[0];
|
||
return res.json({
|
||
authorized: true,
|
||
role: user.role,
|
||
groups: [user.role],
|
||
localUserId: user.id,
|
||
user: {
|
||
...user,
|
||
societeId: user.SocieteId,
|
||
societeNom: user.societe_nom
|
||
}
|
||
});
|
||
}
|
||
|
||
const userGraph = await axios.get(`https://graph.microsoft.com/v1.0/users/${userPrincipalName}?$select=id,displayName,givenName,surname,mail,department,jobTitle`, { headers: { Authorization: `Bearer ${accessToken}` } });
|
||
const userInfo = userGraph.data;
|
||
|
||
const checkMemberResponse = await axios.post(`https://graph.microsoft.com/v1.0/users/${userInfo.id}/checkMemberGroups`, { groupIds: [AZURE_CONFIG.groupId] }, { headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' } });
|
||
const isInGroup = checkMemberResponse.data.value.includes(AZURE_CONFIG.groupId);
|
||
|
||
if (!isInGroup) return res.json({ authorized: false, message: 'Utilisateur non autorisé' });
|
||
|
||
// ⭐ Insertion avec SocieteId par défaut (ajuster selon votre logique)
|
||
const [result] = await pool.query(
|
||
`INSERT INTO CollaborateurAD
|
||
(entraUserId, prenom, nom, email, service, role, SocieteId)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||
[userInfo.id, userInfo.givenName, userInfo.surname, userInfo.mail, userInfo.department, 'Collaborateur', null]
|
||
);
|
||
|
||
res.json({
|
||
authorized: true,
|
||
role: 'Collaborateur',
|
||
groups: ['Collaborateur'],
|
||
localUserId: result.insertId,
|
||
user: {
|
||
id: result.insertId,
|
||
entraUserId: userInfo.id,
|
||
prenom: userInfo.givenName,
|
||
nom: userInfo.surname,
|
||
email: userInfo.mail,
|
||
service: userInfo.department,
|
||
role: 'Collaborateur',
|
||
societeId: null,
|
||
societeNom: null
|
||
}
|
||
});
|
||
} catch (error) {
|
||
res.json({ authorized: false, message: 'Erreur serveur', error: error.message });
|
||
}
|
||
});
|
||
|
||
app.get('/getDetailedLeaveCounters', async (req, res) => {
|
||
try {
|
||
const userIdParam = req.query.user_id;
|
||
|
||
if (!userIdParam) {
|
||
return res.json({ success: false, message: 'ID utilisateur manquant' });
|
||
}
|
||
|
||
const conn = await pool.getConnection();
|
||
|
||
// ⭐ NOUVEAU : Récupérer le dernier arrêté
|
||
const dernierArrete = await getDernierArrete(conn);
|
||
const dateArrete = dernierArrete ? new Date(dernierArrete.DateArrete) : null;
|
||
|
||
// Déterminer l'ID (UUID ou numérique)
|
||
const isUUID = userIdParam.length > 10 && userIdParam.includes('-');
|
||
|
||
const userQuery = `
|
||
SELECT ca.id, ca.prenom, ca.nom, ca.email, ca.DateEntree, ca.role,
|
||
ca.TypeContrat, s.Nom as service, ca.CampusId, ca.SocieteId,
|
||
so.Nom as societe_nom
|
||
FROM CollaborateurAD ca
|
||
LEFT JOIN Services s ON ca.ServiceId = s.Id
|
||
LEFT JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE ${isUUID ? 'ca.entraUserId' : 'ca.id'} = ?
|
||
AND (ca.Actif = 1 OR ca.Actif IS NULL)
|
||
`;
|
||
|
||
const [userInfo] = await conn.query(userQuery, [userIdParam]);
|
||
|
||
if (userInfo.length === 0) {
|
||
conn.release();
|
||
return res.json({ success: false, message: 'Utilisateur non trouvé' });
|
||
}
|
||
|
||
const user = userInfo[0];
|
||
const userId = user.id;
|
||
const dateEntree = user.DateEntree;
|
||
const typeContrat = user.TypeContrat || '37h';
|
||
|
||
const dateRefParam = req.query.dateRef;
|
||
const today = dateRefParam ? parseDateYYYYMMDD(dateRefParam) : new Date();
|
||
const currentYear = today.getFullYear();
|
||
const previousYear = currentYear - 1;
|
||
|
||
// ⭐ MODIFICATION CRITIQUE : Utiliser la date d'arrêté si elle est plus récente que la référence
|
||
const dateCalcul = dateArrete && today <= dateArrete ? dateArrete : today;
|
||
|
||
const ancienneteMs = today - new Date(dateEntree || today);
|
||
const ancienneteMois = Math.floor(ancienneteMs / (1000 * 60 * 60 * 24 * 30.44));
|
||
|
||
// ⭐ Calculer avec la bonne date
|
||
const cpMonthsCurrent = getMoisTravaillesCP(dateCalcul, dateEntree);
|
||
|
||
let counters = {
|
||
user: {
|
||
id: user.id,
|
||
nom: `${user.prenom} ${user.nom}`,
|
||
prenom: user.prenom,
|
||
nomFamille: user.nom,
|
||
email: user.email,
|
||
service: user.service || 'Non défini',
|
||
role: user.role,
|
||
typeContrat: typeContrat,
|
||
societeId: userInfo.SocieteId,
|
||
societeNom: userInfo.societe_nom || 'Non défini',
|
||
dateEntree: dateEntree ? formatDateWithoutUTC(dateEntree) : null,
|
||
ancienneteMois: ancienneteMois,
|
||
ancienneteAnnees: Math.floor(ancienneteMois / 12),
|
||
ancienneteMoisRestants: ancienneteMois % 12
|
||
},
|
||
dateReference: today.toISOString().split('T')[0],
|
||
exerciceCP: getExerciceCP(dateCalcul),
|
||
anneeRTT: currentYear,
|
||
|
||
// ⭐ NOUVEAU : Indiquer si on est en période d'arrêté
|
||
arreteInfo: dernierArrete ? {
|
||
dateArrete: formatDateWithoutUTC(dateArrete),
|
||
libelle: dernierArrete.Libelle,
|
||
bloquage: today <= dateArrete
|
||
} : null,
|
||
|
||
cpN1: null,
|
||
cpN: null,
|
||
rttN: null,
|
||
rttN1: null,
|
||
totalDisponible: { cp: 0, rtt: 0, total: 0 }
|
||
};
|
||
|
||
const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']);
|
||
|
||
// ===== CP N-1 (Reporté) =====
|
||
if (cpType.length > 0) {
|
||
const [cpN1] = await conn.query(`
|
||
SELECT Annee, SoldeReporte, Total, Solde
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [userId, cpType[0].Id, previousYear]);
|
||
|
||
if (cpN1.length > 0 && parseFloat(cpN1[0].SoldeReporte || 0) > 0) {
|
||
const soldeReporte = parseFloat(cpN1[0].SoldeReporte || 0);
|
||
const soldeActuel = parseFloat(cpN1[0].Solde || 0);
|
||
const pris = Math.max(0, soldeReporte - soldeActuel);
|
||
|
||
counters.cpN1 = {
|
||
annee: previousYear,
|
||
exercice: `${previousYear - 1}-${previousYear}`,
|
||
reporte: parseFloat(soldeReporte.toFixed(2)),
|
||
pris: parseFloat(pris.toFixed(2)),
|
||
solde: parseFloat(soldeActuel.toFixed(2)),
|
||
pourcentageUtilise: soldeReporte > 0 ? parseFloat(((pris / soldeReporte) * 100).toFixed(1)) : 0
|
||
};
|
||
counters.totalDisponible.cp += counters.cpN1.solde;
|
||
} else {
|
||
counters.cpN1 = {
|
||
annee: previousYear,
|
||
exercice: `${previousYear - 1}-${previousYear}`,
|
||
reporte: 0,
|
||
pris: 0,
|
||
solde: 0,
|
||
pourcentageUtilise: 0
|
||
};
|
||
}
|
||
|
||
// ===== CP N (Exercice en cours) =====
|
||
const [cpN] = await conn.query(`
|
||
SELECT Annee, Total, Solde, SoldeReporte
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [userId, cpType[0].Id, currentYear]);
|
||
|
||
// ⭐ CORRECTION : Gérer le retour de calculerAcquisitionDepuisArrete
|
||
let acquisCumuleeCP;
|
||
if (typeof calculerAcquisitionDepuisArrete === 'function' && dernierArrete) {
|
||
const result = await calculerAcquisitionDepuisArrete(conn, userId, 'CP', dateCalcul);
|
||
acquisCumuleeCP = typeof result === 'number' ? result : parseFloat(result) || 0;
|
||
} else {
|
||
acquisCumuleeCP = calculerAcquisitionCP(dateCalcul, dateEntree) || 0;
|
||
}
|
||
|
||
// ⭐ SÉCURITÉ : S'assurer que c'est un nombre
|
||
acquisCumuleeCP = parseFloat(acquisCumuleeCP) || 0;
|
||
|
||
if (cpN.length > 0) {
|
||
const total = parseFloat(cpN[0].Total || 0);
|
||
const solde = parseFloat(cpN[0].Solde || 0);
|
||
const soldeReporte = parseFloat(cpN[0].SoldeReporte || 0);
|
||
const soldeN = solde - soldeReporte;
|
||
const pris = Math.max(0, total - soldeN);
|
||
|
||
counters.cpN = {
|
||
annee: currentYear,
|
||
exercice: getExerciceCP(dateCalcul),
|
||
totalAnnuel: 25.00,
|
||
moisTravailles: parseFloat(cpMonthsCurrent.toFixed(2)),
|
||
acquisitionMensuelle: parseFloat((25 / 12).toFixed(2)),
|
||
|
||
// ⭐ MODIFICATION : Afficher l'acquisition à la date de calcul
|
||
acquis: parseFloat(acquisCumuleeCP.toFixed(2)),
|
||
pris: parseFloat(pris.toFixed(2)),
|
||
solde: parseFloat(soldeN.toFixed(2)),
|
||
|
||
tauxAcquisition: parseFloat(((cpMonthsCurrent / 12) * 100).toFixed(1)),
|
||
pourcentageUtilise: total > 0 ? parseFloat(((pris / total) * 100).toFixed(1)) : 0,
|
||
joursRestantsAAcquerir: parseFloat((25 - acquisCumuleeCP).toFixed(2))
|
||
};
|
||
counters.totalDisponible.cp += counters.cpN.solde;
|
||
} else {
|
||
counters.cpN = {
|
||
annee: currentYear,
|
||
exercice: getExerciceCP(dateCalcul),
|
||
totalAnnuel: 25.00,
|
||
moisTravailles: parseFloat(cpMonthsCurrent.toFixed(2)),
|
||
acquisitionMensuelle: parseFloat((25 / 12).toFixed(2)),
|
||
acquis: parseFloat(acquisCumuleeCP.toFixed(2)),
|
||
pris: 0,
|
||
solde: parseFloat(acquisCumuleeCP.toFixed(2)),
|
||
tauxAcquisition: parseFloat(((cpMonthsCurrent / 12) * 100).toFixed(1)),
|
||
pourcentageUtilise: 0,
|
||
joursRestantsAAcquerir: parseFloat((25 - acquisCumuleeCP).toFixed(2))
|
||
};
|
||
counters.totalDisponible.cp += counters.cpN.solde;
|
||
}
|
||
}
|
||
|
||
// ===== RTT N (même logique) =====
|
||
const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']);
|
||
const isApprenti = userInfo[0].role === 'Apprenti';
|
||
if (rttType.length > 0 && !isApprenti) { // ⭐ Condition modifiée
|
||
const rttConfig = await getConfigurationRTT(conn, currentYear, typeContrat)
|
||
|
||
// ⭐ CORRECTION : Gérer le retour de calculerAcquisitionDepuisArrete pour RTT
|
||
let rttData;
|
||
if (typeof calculerAcquisitionDepuisArrete === 'function' && dernierArrete) {
|
||
const result = await calculerAcquisitionDepuisArrete(conn, userId, 'RTT', dateCalcul);
|
||
const acquisRTT = typeof result === 'number' ? result : parseFloat(result) || 0;
|
||
rttData = {
|
||
acquisition: acquisRTT,
|
||
typeContrat: typeContrat,
|
||
moisTravailles: getMoisTravaillesRTT(dateCalcul, dateEntree),
|
||
config: rttConfig
|
||
};
|
||
} else {
|
||
rttData = await calculerAcquisitionRTT(conn, userId, dateCalcul);
|
||
}
|
||
|
||
// ⭐ SÉCURITÉ : S'assurer que acquisition est un nombre
|
||
rttData.acquisition = parseFloat(rttData.acquisition) || 0;
|
||
|
||
const [rttN] = await conn.query(`
|
||
SELECT Annee, Total, Solde, SoldeReporte
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [userId, rttType[0].Id, currentYear]);
|
||
|
||
if (rttN.length > 0) {
|
||
const total = parseFloat(rttN[0].Total || 0);
|
||
const solde = parseFloat(rttN[0].Solde || 0);
|
||
const pris = Math.max(0, total - solde);
|
||
|
||
counters.rttN = {
|
||
annee: currentYear,
|
||
typeContrat: typeContrat,
|
||
totalAnnuel: parseFloat(rttConfig.joursAnnuels.toFixed(2)),
|
||
moisTravailles: rttData.moisTravailles,
|
||
acquisitionMensuelle: parseFloat(rttConfig.acquisitionMensuelle.toFixed(6)),
|
||
acquis: parseFloat(rttData.acquisition.toFixed(2)),
|
||
pris: parseFloat(pris.toFixed(2)),
|
||
solde: parseFloat(solde.toFixed(2)),
|
||
tauxAcquisition: parseFloat(((rttData.moisTravailles / 12) * 100).toFixed(1)),
|
||
pourcentageUtilise: total > 0 ? parseFloat(((pris / total) * 100).toFixed(1)) : 0,
|
||
joursRestantsAAcquerir: parseFloat((rttConfig.joursAnnuels - rttData.acquisition).toFixed(2))
|
||
};
|
||
counters.totalDisponible.rtt += counters.rttN.solde;
|
||
} else {
|
||
counters.rttN = {
|
||
annee: currentYear,
|
||
typeContrat: typeContrat,
|
||
totalAnnuel: parseFloat(rttConfig.joursAnnuels.toFixed(2)),
|
||
moisTravailles: rttData.moisTravailles,
|
||
acquisitionMensuelle: parseFloat(rttConfig.acquisitionMensuelle.toFixed(6)),
|
||
acquis: parseFloat(rttData.acquisition.toFixed(2)),
|
||
pris: 0,
|
||
solde: parseFloat(rttData.acquisition.toFixed(2)),
|
||
tauxAcquisition: parseFloat(((rttData.moisTravailles / 12) * 100).toFixed(1)),
|
||
pourcentageUtilise: 0,
|
||
joursRestantsAAcquerir: parseFloat((rttConfig.joursAnnuels - rttData.acquisition).toFixed(2))
|
||
};
|
||
counters.totalDisponible.rtt += counters.rttN.solde;
|
||
}
|
||
|
||
counters.rttN1 = {
|
||
annee: previousYear,
|
||
reporte: 0,
|
||
pris: 0,
|
||
solde: 0,
|
||
pourcentageUtilise: 0,
|
||
message: "Les RTT ne sont pas reportables d'une année sur l'autre"
|
||
};
|
||
}
|
||
|
||
counters.totalDisponible.total = counters.totalDisponible.cp + counters.totalDisponible.rtt;
|
||
// ===== RÉCUP (NOUVEAU) =====
|
||
const [recupType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Récupération']);
|
||
if (recupType.length > 0) {
|
||
const [recupN] = await conn.query(`
|
||
SELECT Annee, Total, Solde
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [userId, recupType[0].Id, currentYear]);
|
||
|
||
if (recupN.length > 0) {
|
||
const total = parseFloat(recupN[0].Total || 0);
|
||
const solde = parseFloat(recupN[0].Solde || 0);
|
||
|
||
counters.recupN = {
|
||
annee: currentYear,
|
||
acquis: parseFloat(total.toFixed(2)),
|
||
pris: parseFloat((total - solde).toFixed(2)),
|
||
solde: parseFloat(solde.toFixed(2)),
|
||
message: "Jours de récupération accumulés (samedis travaillés)"
|
||
};
|
||
counters.totalDisponible.recup = counters.recupN.solde;
|
||
counters.totalDisponible.total += counters.recupN.solde;
|
||
} else {
|
||
counters.recupN = {
|
||
annee: currentYear,
|
||
acquis: 0,
|
||
pris: 0,
|
||
solde: 0,
|
||
message: "Jours de récupération accumulés (samedis travaillés)"
|
||
};
|
||
counters.totalDisponible.recup = 0;
|
||
}
|
||
}
|
||
|
||
conn.release();
|
||
res.json({
|
||
success: true,
|
||
message: 'Compteurs détaillés récupérés avec succès',
|
||
data: counters
|
||
});
|
||
} catch (error) {
|
||
console.error('Erreur getDetailedLeaveCounters:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur serveur',
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
app.post('/reinitializeAllCounters', async (req, res) => {
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
await conn.beginTransaction();
|
||
|
||
console.log('🔄 Réinitialisation de tous les compteurs...');
|
||
|
||
const [collaborateurs] = await conn.query(`
|
||
SELECT id, prenom, nom, DateEntree, TypeContrat, CampusId, SocieteId
|
||
FROM CollaborateurAD
|
||
WHERE (actif = 1 OR actif IS NULL)
|
||
`);
|
||
|
||
console.log(`📊 ${collaborateurs.length} collaborateurs trouvés`);
|
||
|
||
const dateRefParam = req.body.dateReference;
|
||
const today = dateRefParam ? new Date(dateRefParam) : new Date();
|
||
const currentYear = today.getFullYear();
|
||
const previousYear = currentYear - 1;
|
||
|
||
const results = [];
|
||
|
||
for (const collab of collaborateurs) {
|
||
const dateEntree = collab.DateEntree;
|
||
const typeContrat = collab.TypeContrat || '37h';
|
||
|
||
// Calculer les acquisitions
|
||
const acquisCP = calculerAcquisitionCP(today, dateEntree);
|
||
const isApprenti = collab.role === 'Apprenti';
|
||
const rttData = isApprenti ? { acquisition: 0 } : await calculerAcquisitionRTT(conn, collab.id, today);
|
||
const acquisRTT = rttData.acquisition;
|
||
|
||
const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']);
|
||
const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']);
|
||
|
||
// ===== CP N =====
|
||
if (cpType.length > 0) {
|
||
// ⭐ CALCUL FIABLE : Utiliser DeductionDetails au lieu des anciennes valeurs
|
||
const [deductionsCP] = await conn.query(`
|
||
SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalConsomme
|
||
FROM DeductionDetails dd
|
||
JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
|
||
WHERE dc.CollaborateurADId = ?
|
||
AND dd.TypeCongeId = ?
|
||
AND dd.Annee = ?
|
||
AND dc.Statut != 'Refusée'
|
||
`, [collab.id, cpType[0].Id, currentYear]);
|
||
|
||
const totalConsomme = parseFloat(deductionsCP[0].totalConsomme || 0);
|
||
|
||
// Récupérer le reporté (qui ne change pas)
|
||
const [compteurExisting] = await conn.query(`
|
||
SELECT SoldeReporte FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collab.id, cpType[0].Id, currentYear]);
|
||
|
||
const soldeReporte = compteurExisting.length > 0
|
||
? parseFloat(compteurExisting[0].SoldeReporte || 0)
|
||
: 0;
|
||
|
||
// ⭐ CALCUL EXACT DU SOLDE
|
||
const nouveauSolde = Math.max(0, acquisCP + soldeReporte - totalConsomme);
|
||
|
||
// Mise à jour ou insertion
|
||
if (compteurExisting.length > 0) {
|
||
await conn.query(`
|
||
UPDATE CompteurConges
|
||
SET Total = ?, Solde = ?, DerniereMiseAJour = NOW()
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [acquisCP, nouveauSolde, collab.id, cpType[0].Id, currentYear]);
|
||
} else {
|
||
await conn.query(`
|
||
INSERT INTO CompteurConges
|
||
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour)
|
||
VALUES (?, ?, ?, ?, ?, 0, NOW())
|
||
`, [collab.id, cpType[0].Id, currentYear, acquisCP, nouveauSolde]);
|
||
}
|
||
|
||
console.log(`📊 ${collab.prenom} ${collab.nom} - CP: Acquis ${acquisCP.toFixed(2)}j, Consommé ${totalConsomme.toFixed(2)}j, Reporté ${soldeReporte.toFixed(2)}j → Solde ${nouveauSolde.toFixed(2)}j`);
|
||
|
||
// Créer CP N-1 si nécessaire
|
||
const [cpN1] = await conn.query(`
|
||
SELECT Id FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collab.id, cpType[0].Id, previousYear]);
|
||
|
||
if (cpN1.length === 0) {
|
||
await conn.query(`
|
||
INSERT INTO CompteurConges
|
||
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour)
|
||
VALUES (?, ?, ?, 0, 0, 0, NOW())
|
||
`, [collab.id, cpType[0].Id, previousYear]);
|
||
}
|
||
}
|
||
|
||
// ===== RTT N =====
|
||
if (rttType.length > 0 && !isApprenti) {
|
||
// ⭐ MÊME LOGIQUE POUR LES RTT
|
||
const [deductionsRTT] = await conn.query(`
|
||
SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalConsomme
|
||
FROM DeductionDetails dd
|
||
JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
|
||
WHERE dc.CollaborateurADId = ?
|
||
AND dd.TypeCongeId = ?
|
||
AND dd.Annee = ?
|
||
AND dc.Statut != 'Refusée'
|
||
`, [collab.id, rttType[0].Id, currentYear]);
|
||
|
||
const totalConsomme = parseFloat(deductionsRTT[0].totalConsomme || 0);
|
||
const nouveauSolde = Math.max(0, acquisRTT - totalConsomme);
|
||
|
||
const [compteurExisting] = await conn.query(`
|
||
SELECT Id FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collab.id, rttType[0].Id, currentYear]);
|
||
|
||
if (compteurExisting.length > 0) {
|
||
await conn.query(`
|
||
UPDATE CompteurConges
|
||
SET Total = ?, Solde = ?, DerniereMiseAJour = NOW()
|
||
WHERE Id = ?
|
||
`, [acquisRTT, nouveauSolde, compteurExisting[0].Id]);
|
||
} else {
|
||
await conn.query(`
|
||
INSERT INTO CompteurConges
|
||
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour)
|
||
VALUES (?, ?, ?, ?, ?, 0, NOW())
|
||
`, [collab.id, rttType[0].Id, currentYear, acquisRTT, nouveauSolde]);
|
||
}
|
||
|
||
console.log(`📊 ${collab.prenom} ${collab.nom} - RTT: Acquis ${acquisRTT.toFixed(2)}j, Consommé ${totalConsomme.toFixed(2)}j → Solde ${nouveauSolde.toFixed(2)}j`);
|
||
|
||
// Créer RTT N-1 si nécessaire
|
||
const [rttN1] = await conn.query(`
|
||
SELECT Id FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collab.id, rttType[0].Id, previousYear]);
|
||
|
||
if (rttN1.length === 0) {
|
||
await conn.query(`
|
||
INSERT INTO CompteurConges
|
||
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour)
|
||
VALUES (?, ?, ?, 0, 0, 0, NOW())
|
||
`, [collab.id, rttType[0].Id, previousYear]);
|
||
}
|
||
}
|
||
|
||
results.push({
|
||
collaborateur: `${collab.prenom} ${collab.nom}`,
|
||
type_contrat: typeContrat,
|
||
cp_acquis: acquisCP.toFixed(2),
|
||
cp_solde: (acquisCP - (deductionsCP?.[0]?.totalConsomme || 0)).toFixed(2),
|
||
rtt_acquis: acquisRTT.toFixed(2),
|
||
rtt_solde: (acquisRTT - (deductionsRTT?.[0]?.totalConsomme || 0)).toFixed(2)
|
||
});
|
||
}
|
||
|
||
await conn.commit();
|
||
|
||
console.log('✅ Réinitialisation terminée');
|
||
|
||
res.json({
|
||
success: true,
|
||
message: `✅ Compteurs réinitialisés pour ${collaborateurs.length} collaborateurs`,
|
||
date_reference: today.toISOString().split('T')[0],
|
||
total_collaborateurs: collaborateurs.length,
|
||
results: results
|
||
});
|
||
|
||
} catch (error) {
|
||
await conn.rollback();
|
||
console.error('❌ Erreur réinitialisation:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur lors de la réinitialisation',
|
||
error: error.message
|
||
});
|
||
} finally {
|
||
conn.release();
|
||
}
|
||
});
|
||
|
||
app.post('/updateCounters', async (req, res) => {
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
const { collaborateur_id } = req.body;
|
||
if (!collaborateur_id) return res.json({ success: false, message: 'ID collaborateur manquant' });
|
||
await conn.beginTransaction();
|
||
const updates = await updateMonthlyCounters(conn, collaborateur_id, new Date());
|
||
await conn.commit();
|
||
res.json({ success: true, message: 'Compteurs mis à jour', updates });
|
||
} catch (error) {
|
||
await conn.rollback();
|
||
res.status(500).json({ success: false, message: 'Erreur', error: error.message });
|
||
} finally {
|
||
conn.release();
|
||
}
|
||
});
|
||
|
||
app.post('/updateAllCounters', async (req, res) => {
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
await conn.beginTransaction();
|
||
const [collaborateurs] = await conn.query('SELECT id, CampusId FROM CollaborateurAD WHERE actif = 1 OR actif IS NULL');
|
||
const allUpdates = [];
|
||
for (const collab of collaborateurs) {
|
||
const updates = await updateMonthlyCounters(conn, collab.id, new Date());
|
||
allUpdates.push({ collaborateur_id: collab.id, updates });
|
||
}
|
||
await conn.commit();
|
||
res.json({ success: true, message: `Compteurs mis à jour pour ${collaborateurs.length} collaborateurs`, total_collaborateurs: collaborateurs.length, details: allUpdates });
|
||
} catch (error) {
|
||
await conn.rollback();
|
||
res.status(500).json({ success: false, message: 'Erreur', error: error.message });
|
||
} finally {
|
||
conn.release();
|
||
}
|
||
});
|
||
|
||
async function deductLeaveBalanceWithTracking(conn, collaborateurId, typeCongeId, nombreJours, demandeCongeId) {
|
||
const currentYear = new Date().getFullYear();
|
||
const previousYear = currentYear - 1;
|
||
let joursRestants = nombreJours;
|
||
const deductions = [];
|
||
|
||
// Étape 1: Déduire du reporté N-1 d'abord
|
||
const [compteurN1] = await conn.query(
|
||
`SELECT Id, Solde, SoldeReporte FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, typeCongeId, previousYear]
|
||
);
|
||
|
||
if (compteurN1.length > 0 && compteurN1[0].SoldeReporte > 0) {
|
||
const soldeN1 = parseFloat(compteurN1[0].SoldeReporte);
|
||
const aDeduireN1 = Math.min(soldeN1, joursRestants);
|
||
|
||
if (aDeduireN1 > 0) {
|
||
// Déduction dans la base
|
||
await conn.query(
|
||
`UPDATE CompteurConges
|
||
SET SoldeReporte = GREATEST(0, SoldeReporte - ?),
|
||
Solde = GREATEST(0, Solde - ?)
|
||
WHERE Id = ?`,
|
||
[aDeduireN1, aDeduireN1, compteurN1[0].Id]
|
||
);
|
||
|
||
// Sauvegarde du détail de la déduction
|
||
await conn.query(`
|
||
INSERT INTO DeductionDetails
|
||
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
|
||
VALUES (?, ?, ?, 'Accum Récup', ?)
|
||
`, [demandeId, recupType[0].Id, currentYear, recupJours]);
|
||
|
||
deductions.push({
|
||
annee: previousYear,
|
||
type: 'Reporté N-1',
|
||
joursUtilises: aDeduireN1,
|
||
soldeAvant: soldeN1
|
||
});
|
||
|
||
joursRestants -= aDeduireN1;
|
||
}
|
||
}
|
||
|
||
// Étape 2: Déduire de l'année N si besoin
|
||
if (joursRestants > 0) {
|
||
const [compteurN] = await conn.query(
|
||
`SELECT Id, Solde, SoldeReporte FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, typeCongeId, currentYear]
|
||
);
|
||
|
||
if (compteurN.length > 0) {
|
||
const soldeN = parseFloat(compteurN[0].Solde) - parseFloat(compteurN[0].SoldeReporte || 0);
|
||
const aDeduireN = Math.min(soldeN, joursRestants);
|
||
|
||
if (aDeduireN > 0) {
|
||
// Déduction dans la base
|
||
await conn.query(
|
||
`UPDATE CompteurConges
|
||
SET Solde = GREATEST(0, Solde - ?)
|
||
WHERE Id = ?`,
|
||
[aDeduireN, compteurN[0].Id]
|
||
);
|
||
|
||
// Sauvegarde du détail de la déduction
|
||
await conn.query(
|
||
`INSERT INTO DeductionDetails
|
||
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
|
||
VALUES (?, ?, ?, 'Année N', ?)`,
|
||
[demandeCongeId, typeCongeId, currentYear, aDeduireN]
|
||
);
|
||
|
||
deductions.push({
|
||
annee: currentYear,
|
||
type: 'Année actuelle N',
|
||
joursUtilises: aDeduireN,
|
||
soldeAvant: soldeN
|
||
});
|
||
|
||
joursRestants -= aDeduireN;
|
||
}
|
||
}
|
||
}
|
||
|
||
return {
|
||
success: joursRestants === 0,
|
||
joursDeduitsTotal: nombreJours - joursRestants,
|
||
joursNonDeduits: joursRestants,
|
||
details: deductions
|
||
};
|
||
};
|
||
|
||
async function restoreLeaveBalance(conn, demandeCongeId, collaborateurId) {
|
||
try {
|
||
console.log(`\n🔄 === RESTAURATION COMPTEURS ===`);
|
||
console.log(`Demande ID: ${demandeCongeId}`);
|
||
console.log(`Collaborateur ID: ${collaborateurId}`);
|
||
|
||
// Récupérer tous les détails de déduction pour cette demande
|
||
const [deductions] = await conn.query(
|
||
`SELECT dd.TypeCongeId, dd.Annee, dd.TypeDeduction, dd.JoursUtilises, tc.Nom as TypeNom
|
||
FROM DeductionDetails dd
|
||
JOIN TypeConge tc ON dd.TypeCongeId = tc.Id
|
||
WHERE dd.DemandeCongeId = ?
|
||
ORDER BY dd.Id DESC`,
|
||
[demandeCongeId]
|
||
);
|
||
|
||
console.log(`📊 ${deductions.length} déductions trouvées`);
|
||
|
||
if (deductions.length === 0) {
|
||
console.log('⚠️ Aucune déduction trouvée pour cette demande');
|
||
return { success: false, message: 'Aucune déduction à restaurer' };
|
||
}
|
||
|
||
const restorations = [];
|
||
|
||
for (const deduction of deductions) {
|
||
const { TypeCongeId, Annee, TypeDeduction, JoursUtilises, TypeNom } = deduction;
|
||
|
||
console.log(`\n🔍 Traitement: ${TypeNom} - ${TypeDeduction} - ${JoursUtilises}j`);
|
||
|
||
// 🔹 CAS SPÉCIAL : Récupération accumulée (RETIRER les jours)
|
||
if (TypeDeduction === 'Accum Récup') {
|
||
console.log(`❌ Annulation accumulation ${TypeNom}: -${JoursUtilises}j`);
|
||
|
||
const [compteur] = await conn.query(
|
||
`SELECT Id, Total, Solde FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, TypeCongeId, Annee]
|
||
);
|
||
|
||
if (compteur.length > 0) {
|
||
await conn.query(
|
||
`UPDATE CompteurConges
|
||
SET Total = GREATEST(0, Total - ?),
|
||
Solde = GREATEST(0, Solde - ?),
|
||
DerniereMiseAJour = NOW()
|
||
WHERE Id = ?`,
|
||
[JoursUtilises, JoursUtilises, compteur[0].Id]
|
||
);
|
||
|
||
restorations.push({
|
||
type: TypeNom,
|
||
annee: Annee,
|
||
typeDeduction: TypeDeduction,
|
||
joursRetires: JoursUtilises
|
||
});
|
||
console.log(`✅ Récup retirée: ${compteur[0].Solde} → ${Math.max(0, compteur[0].Solde - JoursUtilises)}`);
|
||
}
|
||
continue;
|
||
}
|
||
|
||
// 🔹 Reporté N-1
|
||
if (TypeDeduction === 'Reporté N-1' || TypeDeduction === 'Report N-1') {
|
||
const [compteur] = await conn.query(
|
||
`SELECT Id, SoldeReporte, Solde FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, TypeCongeId, Annee]
|
||
);
|
||
|
||
if (compteur.length > 0) {
|
||
const ancienSolde = parseFloat(compteur[0].Solde || 0);
|
||
const nouveauSolde = ancienSolde + parseFloat(JoursUtilises);
|
||
|
||
await conn.query(
|
||
`UPDATE CompteurConges
|
||
SET SoldeReporte = SoldeReporte + ?,
|
||
Solde = Solde + ?,
|
||
DerniereMiseAJour = NOW()
|
||
WHERE Id = ?`,
|
||
[JoursUtilises, JoursUtilises, compteur[0].Id]
|
||
);
|
||
|
||
restorations.push({
|
||
type: TypeNom,
|
||
annee: Annee,
|
||
typeDeduction: TypeDeduction,
|
||
joursRestores: JoursUtilises
|
||
});
|
||
console.log(`✅ Reporté restauré: ${ancienSolde} → ${nouveauSolde}`);
|
||
}
|
||
}
|
||
|
||
// 🔹 Année N
|
||
else if (TypeDeduction === 'Année N') {
|
||
const [compteur] = await conn.query(
|
||
`SELECT Id, Solde FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, TypeCongeId, Annee]
|
||
);
|
||
|
||
if (compteur.length > 0) {
|
||
const ancienSolde = parseFloat(compteur[0].Solde || 0);
|
||
const nouveauSolde = ancienSolde + parseFloat(JoursUtilises);
|
||
|
||
await conn.query(
|
||
`UPDATE CompteurConges
|
||
SET Solde = Solde + ?,
|
||
DerniereMiseAJour = NOW()
|
||
WHERE Id = ?`,
|
||
[JoursUtilises, compteur[0].Id]
|
||
);
|
||
|
||
restorations.push({
|
||
type: TypeNom,
|
||
annee: Annee,
|
||
typeDeduction: TypeDeduction,
|
||
joursRestores: JoursUtilises
|
||
});
|
||
console.log(`✅ Année N restaurée: ${ancienSolde} → ${nouveauSolde}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
console.log(`\n✅ Restauration terminée: ${restorations.length} opérations\n`);
|
||
|
||
return {
|
||
success: true,
|
||
restorations,
|
||
message: `${restorations.length} restaurations effectuées`
|
||
};
|
||
|
||
} catch (error) {
|
||
console.error('❌ Erreur lors de la restauration des soldes:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
app.get('/testProrata', async (req, res) => {
|
||
try {
|
||
const userId = parseInt(req.query.user_id || 0);
|
||
if (userId <= 0) return res.json({ success: false, message: 'ID utilisateur requis' });
|
||
const conn = await pool.getConnection();
|
||
const [userInfo] = await conn.query(`SELECT id, prenom, nom, DateEntree, TypeContrat, CampusId FROM CollaborateurAD WHERE id = ?`, [userId]);
|
||
if (userInfo.length === 0) { conn.release(); return res.json({ success: false, message: 'Utilisateur non trouvé' }); }
|
||
const user = userInfo[0];
|
||
const dateEntree = user.DateEntree;
|
||
const typeContrat = user.TypeContrat || '37h';
|
||
const today = new Date();
|
||
const moisCP = getMoisTravaillesCP(today, dateEntree);
|
||
const acquisCP = calculerAcquisitionCP(today, dateEntree);
|
||
const rttData = await calculerAcquisitionRTT(conn, userId, today);
|
||
conn.release();
|
||
res.json({
|
||
success: true,
|
||
user: {
|
||
id: user.id,
|
||
nom: `${user.prenom} ${user.nom}`,
|
||
dateEntree: dateEntree ? dateEntree.toISOString().split('T')[0] : null,
|
||
typeContrat: typeContrat
|
||
},
|
||
dateReference: today.toISOString().split('T')[0],
|
||
calculs: {
|
||
CP: {
|
||
moisTravailles: parseFloat(moisCP.toFixed(2)),
|
||
acquisitionMensuelle: 25 / 12,
|
||
acquisitionCumulee: acquisCP,
|
||
formule: `${moisCP.toFixed(2)} mois × ${(25 / 12).toFixed(2)}j/mois = ${acquisCP}j`
|
||
},
|
||
RTT: {
|
||
moisTravailles: rttData.moisTravailles,
|
||
acquisitionMensuelle: rttData.config.acquisitionMensuelle,
|
||
acquisitionCumulee: rttData.acquisition,
|
||
totalAnnuel: rttData.config.joursAnnuels,
|
||
formule: `${rttData.moisTravailles} mois × ${rttData.config.acquisitionMensuelle.toFixed(6)}j/mois = ${rttData.acquisition}j`
|
||
}
|
||
}
|
||
});
|
||
} catch (error) {
|
||
console.error('Erreur testProrata:', error);
|
||
res.status(500).json({ success: false, message: 'Erreur serveur', error: error.message });
|
||
}
|
||
});
|
||
|
||
app.post('/fixAllCounters', async (req, res) => {
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
await conn.beginTransaction();
|
||
const today = new Date();
|
||
const currentYear = today.getFullYear();
|
||
const [collaborateurs] = await conn.query('SELECT id, prenom, nom, DateEntree, CampusId FROM CollaborateurAD WHERE (actif = 1 OR actif IS NULL)');
|
||
console.log(`🔄 Correction de ${collaborateurs.length} compteurs...`);
|
||
const corrections = [];
|
||
for (const collab of collaborateurs) {
|
||
const dateEntree = collab.DateEntree;
|
||
const moisCP = getMoisTravaillesCP(today, dateEntree);
|
||
const acquisCP = calculerAcquisitionCP(today, dateEntree);
|
||
const rttData = await calculerAcquisitionRTT(conn, collab.id, today);
|
||
const acquisRTT = rttData.acquisition;
|
||
const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']);
|
||
const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']);
|
||
if (cpType.length > 0) {
|
||
const [existingCP] = await conn.query(`SELECT Id, Total, Solde, SoldeReporte FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, [collab.id, cpType[0].Id, currentYear]);
|
||
if (existingCP.length > 0) {
|
||
const ancienTotal = parseFloat(existingCP[0].Total);
|
||
const ancienSolde = parseFloat(existingCP[0].Solde);
|
||
const difference = acquisCP - ancienTotal;
|
||
const nouveauSolde = Math.max(0, ancienSolde + difference);
|
||
await conn.query(`UPDATE CompteurConges SET Total = ?, Solde = ?, DerniereMiseAJour = NOW() WHERE Id = ?`, [acquisCP, nouveauSolde, existingCP[0].Id]);
|
||
corrections.push({ collaborateur: `${collab.prenom} ${collab.nom}`, type: 'CP', ancienTotal: ancienTotal.toFixed(2), nouveauTotal: acquisCP.toFixed(2), ancienSolde: ancienSolde.toFixed(2), nouveauSolde: nouveauSolde.toFixed(2) });
|
||
}
|
||
}
|
||
if (rttType.length > 0) {
|
||
const [existingRTT] = await conn.query(`SELECT Id, Total, Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, [collab.id, rttType[0].Id, currentYear]);
|
||
if (existingRTT.length > 0) {
|
||
const ancienTotal = parseFloat(existingRTT[0].Total);
|
||
const ancienSolde = parseFloat(existingRTT[0].Solde);
|
||
const difference = acquisRTT - ancienTotal;
|
||
const nouveauSolde = Math.max(0, ancienSolde + difference);
|
||
await conn.query(`UPDATE CompteurConges SET Total = ?, Solde = ?, DerniereMiseAJour = NOW() WHERE Id = ?`, [acquisRTT, nouveauSolde, existingRTT[0].Id]);
|
||
corrections.push({ collaborateur: `${collab.prenom} ${collab.nom}`, type: 'RTT', ancienTotal: ancienTotal.toFixed(2), nouveauTotal: acquisRTT.toFixed(2), ancienSolde: ancienSolde.toFixed(2), nouveauSolde: nouveauSolde.toFixed(2) });
|
||
}
|
||
}
|
||
}
|
||
await conn.commit();
|
||
res.json({ success: true, message: `✅ ${collaborateurs.length} compteurs corrigés`, corrections: corrections });
|
||
} catch (error) {
|
||
await conn.rollback();
|
||
console.error('❌ Erreur correction compteurs:', error);
|
||
res.status(500).json({ success: false, message: 'Erreur', error: error.message });
|
||
} finally {
|
||
conn.release();
|
||
}
|
||
});
|
||
|
||
app.post('/processEndOfYear', async (req, res) => {
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
const { collaborateur_id } = req.body;
|
||
await conn.beginTransaction();
|
||
let result;
|
||
if (collaborateur_id) {
|
||
result = await processEndOfYearRTT(conn, collaborateur_id);
|
||
} else {
|
||
const [collaborateurs] = await conn.query('SELECT id, CampusId FROM CollaborateurAD');
|
||
const results = [];
|
||
for (const c of collaborateurs) {
|
||
const r = await processEndOfYearRTT(conn, c.id);
|
||
if (r) results.push({ collaborateur_id: c.id, ...r });
|
||
}
|
||
result = results;
|
||
}
|
||
await conn.commit();
|
||
res.json({ success: true, message: 'Traitement de fin d\'année effectué', result });
|
||
} catch (error) {
|
||
await conn.rollback();
|
||
res.status(500).json({ success: false, message: 'Erreur', error: error.message });
|
||
} finally {
|
||
conn.release();
|
||
}
|
||
});
|
||
|
||
app.post('/processEndOfExercice', async (req, res) => {
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
const { collaborateur_id } = req.body;
|
||
await conn.beginTransaction();
|
||
let result;
|
||
if (collaborateur_id) {
|
||
result = await processEndOfExerciceCP(conn, collaborateur_id);
|
||
} else {
|
||
const [collaborateurs] = await conn.query('SELECT id, CampusId FROM CollaborateurAD');
|
||
const results = [];
|
||
for (const c of collaborateurs) {
|
||
const r = await processEndOfExerciceCP(conn, c.id);
|
||
if (r) results.push({ collaborateur_id: c.id, ...r });
|
||
}
|
||
result = results;
|
||
}
|
||
await conn.commit();
|
||
res.json({ success: true, message: 'Traitement de fin d\'exercice CP effectué', result });
|
||
} catch (error) {
|
||
await conn.rollback();
|
||
res.status(500).json({ success: false, message: 'Erreur', error: error.message });
|
||
} finally {
|
||
conn.release();
|
||
}
|
||
});
|
||
|
||
app.get('/getAcquisitionDetails', async (req, res) => {
|
||
try {
|
||
const today = new Date();
|
||
const details = { date_reference: today.toISOString().split('T')[0], CP: { exercice: getExerciceCP(today), mois_travailles: getMoisTravaillesCP(today), acquisition_mensuelle: LEAVE_RULES.CP.acquisitionMensuelle, acquisition_cumulee: calculerAcquisitionCumulee('CP', today), total_annuel: LEAVE_RULES.CP.joursAnnuels, periode: '01/06 - 31/05', reportable: LEAVE_RULES.CP.reportable }, RTT: { annee: today.getFullYear(), mois_travailles: getMoisTravaillesRTT(today), acquisition_mensuelle: LEAVE_RULES.RTT.acquisitionMensuelle, acquisition_cumulee: calculerAcquisitionCumulee('RTT', today), total_annuel: LEAVE_RULES.RTT.joursAnnuels, periode: '01/01 - 31/12', reportable: LEAVE_RULES.RTT.reportable } };
|
||
res.json({ success: true, details });
|
||
} catch (error) {
|
||
res.status(500).json({ success: false, message: 'Erreur', error: error.message });
|
||
}
|
||
});
|
||
|
||
app.get('/getLeaveCounters', async (req, res) => {
|
||
try {
|
||
const userId = parseInt(req.query.user_id || 0);
|
||
const data = {};
|
||
if (userId > 0) {
|
||
const [rows] = await pool.query(`SELECT tc.Nom, cc.Annee, cc.Solde, cc.Total, cc.SoldeReporte FROM CompteurConges cc JOIN TypeConge tc ON cc.TypeCongeId = tc.Id WHERE cc.CollaborateurADId = ?`, [userId]);
|
||
rows.forEach(row => { data[row.Nom] = { Annee: row.Annee, Solde: parseFloat(row.Solde), Total: parseFloat(row.Total), SoldeReporte: parseFloat(row.SoldeReporte) }; });
|
||
}
|
||
res.json({ success: true, message: 'Compteurs récupérés', counters: data });
|
||
} catch (error) {
|
||
res.status(500).json({ success: false, message: 'Erreur', error: error.message });
|
||
}
|
||
});
|
||
|
||
app.get('/getEmploye', async (req, res) => {
|
||
try {
|
||
const id = parseInt(req.query.id || 0);
|
||
if (id <= 0) return res.json({ success: false, message: 'ID invalide' });
|
||
|
||
const conn = await pool.getConnection();
|
||
|
||
// 1️⃣ Récupérer les infos du collaborateur
|
||
const [rows] = await conn.query(`
|
||
SELECT
|
||
ca.id,
|
||
ca.Nom,
|
||
ca.Prenom,
|
||
ca.Email,
|
||
ca.role,
|
||
ca.TypeContrat,
|
||
ca.DateEntree,
|
||
ca.CampusId,
|
||
ca.SocieteId,
|
||
s.Nom as service,
|
||
so.Nom as societe_nom
|
||
FROM CollaborateurAD ca
|
||
LEFT JOIN Services s ON ca.ServiceId = s.Id
|
||
LEFT JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE ca.id = ?
|
||
`, [id]);
|
||
|
||
if (rows.length === 0) {
|
||
conn.release();
|
||
return res.json({ success: false, message: 'Collaborateur non trouvé' });
|
||
}
|
||
|
||
const employee = rows[0];
|
||
|
||
// 2️⃣ Déterminer si c'est un apprenti
|
||
const isApprenti = normalizeRole(employee.role) === 'apprenti';
|
||
|
||
// 3️⃣ Récupérer les compteurs CP
|
||
const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']);
|
||
let cpTotal = 0, cpSolde = 0;
|
||
|
||
if (cpType.length > 0) {
|
||
const currentYear = new Date().getFullYear();
|
||
const previousYear = currentYear - 1;
|
||
|
||
// CP N-1 (reporté)
|
||
const [cpN1] = await conn.query(`
|
||
SELECT SoldeReporte
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [id, cpType[0].Id, previousYear]);
|
||
|
||
// CP N (année courante)
|
||
const [cpN] = await conn.query(`
|
||
SELECT Solde, SoldeReporte
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [id, cpType[0].Id, currentYear]);
|
||
|
||
const cpN1Solde = cpN1.length > 0 ? parseFloat(cpN1[0].SoldeReporte || 0) : 0;
|
||
const cpNSolde = cpN.length > 0 ? parseFloat(cpN[0].Solde || 0) : 0;
|
||
const cpNReporte = cpN.length > 0 ? parseFloat(cpN[0].SoldeReporte || 0) : 0;
|
||
|
||
cpTotal = cpN1Solde + (cpNSolde - cpNReporte);
|
||
cpSolde = cpTotal;
|
||
}
|
||
|
||
// 4️⃣ Récupérer les compteurs RTT (sauf pour apprentis)
|
||
let rttTotal = 0, rttSolde = 0;
|
||
|
||
if (!isApprenti) {
|
||
const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']);
|
||
|
||
if (rttType.length > 0) {
|
||
const currentYear = new Date().getFullYear();
|
||
|
||
const [rttN] = await conn.query(`
|
||
SELECT Solde
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [id, rttType[0].Id, currentYear]);
|
||
|
||
rttTotal = rttN.length > 0 ? parseFloat(rttN[0].Solde || 0) : 0;
|
||
rttSolde = rttTotal;
|
||
}
|
||
}
|
||
|
||
conn.release();
|
||
|
||
// 5️⃣ Retourner les données complètes
|
||
res.json({
|
||
success: true,
|
||
employee: {
|
||
id: employee.id,
|
||
Nom: employee.Nom,
|
||
Prenom: employee.Prenom,
|
||
Email: employee.Email,
|
||
role: employee.role,
|
||
TypeContrat: employee.TypeContrat,
|
||
DateEntree: employee.DateEntree,
|
||
CampusId: employee.CampusId,
|
||
SocieteId: employee.SocieteId,
|
||
service: employee.service,
|
||
societe_nom: employee.societe_nom,
|
||
conges_restants: parseFloat(cpSolde.toFixed(2)),
|
||
rtt_restants: parseFloat(rttSolde.toFixed(2))
|
||
}
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('❌ Erreur getEmploye:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur DB',
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
app.get('/getEmployeRequest', async (req, res) => {
|
||
try {
|
||
const id = parseInt(req.query.id || 0);
|
||
if (id <= 0) return res.json({ success: false, message: 'ID invalide' });
|
||
|
||
const [rows] = await pool.query(`
|
||
SELECT
|
||
dc.Id,
|
||
dc.DateDebut,
|
||
dc.DateFin,
|
||
dc.NombreJours as days,
|
||
dc.Statut as status,
|
||
dc.DateDemande,
|
||
GROUP_CONCAT(DISTINCT tc.Nom ORDER BY tc.Nom SEPARATOR ', ') AS type,
|
||
CONCAT(
|
||
DATE_FORMAT(dc.DateDebut, '%d/%m/%Y'),
|
||
IF(dc.DateDebut = dc.DateFin, '', CONCAT(' - ', DATE_FORMAT(dc.DateFin, '%d/%m/%Y')))
|
||
) as date_display
|
||
FROM DemandeConge dc
|
||
LEFT JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId
|
||
LEFT JOIN TypeConge tc ON dct.TypeCongeId = tc.Id
|
||
WHERE dc.CollaborateurADId = ?
|
||
GROUP BY dc.Id, dc.DateDebut, dc.DateFin, dc.NombreJours, dc.Statut, dc.DateDemande
|
||
ORDER BY dc.DateDemande DESC
|
||
`, [id]);
|
||
|
||
res.json({
|
||
success: true,
|
||
requests: rows
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('❌ Erreur getEmployeRequest:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur DB',
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
app.get('/getRequests', async (req, res) => {
|
||
try {
|
||
const userId = req.query.user_id;
|
||
if (!userId) return res.json({ success: false, message: 'ID utilisateur manquant' });
|
||
|
||
// ✅ REQUÊTE CORRIGÉE avec la table de liaison
|
||
const [rows] = await pool.query(`
|
||
SELECT
|
||
dc.Id,
|
||
dc.DateDebut,
|
||
dc.DateFin,
|
||
dc.Statut,
|
||
dc.DateDemande,
|
||
dc.Commentaire,
|
||
dc.CommentaireValidation,
|
||
dc.Validateur,
|
||
dc.DocumentJoint,
|
||
GROUP_CONCAT(DISTINCT tc.Nom ORDER BY tc.Nom SEPARATOR ', ') AS TypeConges,
|
||
SUM(dct.NombreJours) as NombreJoursTotal
|
||
FROM DemandeConge dc
|
||
LEFT JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId
|
||
LEFT JOIN TypeConge tc ON dct.TypeCongeId = tc.Id
|
||
WHERE (dc.EmployeeId = ? OR dc.CollaborateurADId = ?)
|
||
GROUP BY dc.Id, dc.DateDebut, dc.DateFin, dc.Statut, dc.DateDemande, dc.Commentaire, dc.CommentaireValidation, dc.Validateur, dc.DocumentJoint
|
||
ORDER BY dc.DateDemande DESC
|
||
`, [userId, userId]);
|
||
|
||
const requests = rows.map(row => {
|
||
const workingDays = getWorkingDays(row.DateDebut, row.DateFin);
|
||
const dateDisplay = row.DateDebut === row.DateFin
|
||
? formatDate(row.DateDebut)
|
||
: `${formatDate(row.DateDebut)} - ${formatDate(row.DateFin)}`;
|
||
|
||
let fileUrl = null;
|
||
if (row.TypeConges && row.TypeConges.includes('Congé maladie') && row.DocumentJoint) {
|
||
fileUrl = `http://localhost:3000/uploads/${path.basename(row.DocumentJoint)}`;
|
||
}
|
||
|
||
return {
|
||
id: row.Id,
|
||
type: row.TypeConges || 'Non défini', // ✅ Gérer le cas null
|
||
startDate: row.DateDebut,
|
||
endDate: row.DateFin,
|
||
dateDisplay,
|
||
days: row.NombreJoursTotal || workingDays, // ✅ Utiliser le total de la table de liaison
|
||
status: row.Statut,
|
||
reason: row.Commentaire || 'Aucun commentaire',
|
||
submittedAt: row.DateDemande,
|
||
submittedDisplay: formatDate(row.DateDemande),
|
||
validator: row.Validateur || null,
|
||
validationComment: row.CommentaireValidation || null,
|
||
fileUrl
|
||
};
|
||
});
|
||
|
||
res.json({
|
||
success: true,
|
||
message: 'Demandes récupérées',
|
||
requests,
|
||
total: requests.length
|
||
});
|
||
} catch (error) {
|
||
console.error('❌ Erreur getRequests:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur',
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
app.get('/getAllTeamRequests', async (req, res) => {
|
||
try {
|
||
const managerId = req.query.SuperieurId;
|
||
if (!managerId) return res.json({ success: false, message: 'Paramètre SuperieurId manquant' });
|
||
const [rows] = await pool.query(`SELECT dc.Id, dc.DateDebut, dc.DateFin, dc.Statut, dc.DateDemande, dc.Commentaire, dc.DocumentJoint, dc.CollaborateurADId AS employee_id, CONCAT(ca.Prenom, ' ', ca.Nom) as employee_name, ca.Email as employee_email, tc.Nom as type FROM DemandeConge dc JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id JOIN TypeConge tc ON dc.TypeCongeId = tc.Id JOIN HierarchieValidationAD hv ON hv.CollaborateurId = ca.id WHERE hv.SuperieurId = ? ORDER BY dc.DateDemande DESC`, [managerId]);
|
||
const requests = rows.map(row => ({ id: row.Id, employee_id: row.employee_id, employee_name: row.employee_name, employee_email: row.employee_email, type: row.type, start_date: row.DateDebut, end_date: row.DateFin, date_display: row.DateDebut === row.DateFin ? formatDate(row.DateDebut) : `${formatDate(row.DateDebut)} - ${formatDate(row.DateFin)}`, days: getWorkingDays(row.DateDebut, row.DateFin), status: row.Statut, reason: row.Commentaire || '', file: row.DocumentJoint || null, submitted_at: row.DateDemande, submitted_display: formatDate(row.DateDemande) }));
|
||
res.json({ success: true, requests });
|
||
} catch (error) {
|
||
res.status(500).json({ success: false, message: 'Erreur DB', error: error.message });
|
||
}
|
||
});
|
||
|
||
app.get('/getPendingRequests', async (req, res) => {
|
||
try {
|
||
const managerId = req.query.manager_id;
|
||
if (!managerId) return res.json({ success: false, message: 'ID manager manquant' });
|
||
const [managerRows] = await pool.query('SELECT ServiceId, CampusId FROM CollaborateurAD WHERE id = ?', [managerId]);
|
||
if (managerRows.length === 0) return res.json({ success: false, message: 'Manager non trouvé' });
|
||
const serviceId = managerRows[0].ServiceId;
|
||
const [rows] = await pool.query(`SELECT dc.Id, dc.DateDebut, dc.DateFin, dc.Statut, dc.DateDemande, dc.Commentaire, dc.CollaborateurADId, CONCAT(ca.prenom, ' ', ca.nom) as employee_name, ca.email as employee_email, GROUP_CONCAT(tc.Nom ORDER BY tc.Nom SEPARATOR ', ') as types FROM DemandeConge dc JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id JOIN TypeConge tc ON FIND_IN_SET(tc.Id, dc.TypeCongeId) WHERE ca.ServiceId = ? AND dc.Statut = 'En attente' AND ca.id != ? GROUP BY dc.Id, dc.DateDebut, dc.DateFin, dc.Statut, dc.DateDemande, dc.Commentaire, dc.CollaborateurADId, ca.prenom, ca.nom, ca.email ORDER BY dc.DateDemande ASC`, [serviceId, managerId]);
|
||
const requests = rows.map(row => ({ id: row.Id, employee_id: row.CollaborateurADId, employee_name: row.employee_name, employee_email: row.employee_email, type: row.types, start_date: row.DateDebut, end_date: row.DateFin, date_display: row.DateDebut === row.DateFin ? formatDate(row.DateDebut) : `${formatDate(row.DateDebut)} - ${formatDate(row.DateFin)}`, days: getWorkingDays(row.DateDebut, row.DateFin), status: row.Statut, reason: row.Commentaire || '', submitted_at: row.DateDemande, submitted_display: formatDate(row.DateDemande) }));
|
||
res.json({ success: true, message: 'Demandes récupérées', requests, service_id: serviceId });
|
||
} catch (error) {
|
||
res.status(500).json({ success: false, message: 'Erreur', error: error.message });
|
||
}
|
||
});
|
||
|
||
app.get('/getTeamMembers', async (req, res) => {
|
||
try {
|
||
const managerId = req.query.manager_id;
|
||
if (!managerId) return res.json({ success: false, message: 'ID manager manquant' });
|
||
const [managerRows] = await pool.query('SELECT ServiceId, CampusId FROM CollaborateurAD WHERE id = ?', [managerId]);
|
||
if (managerRows.length === 0) return res.json({ success: false, message: 'Manager non trouvé' });
|
||
const serviceId = managerRows[0].ServiceId;
|
||
const [members] = await pool.query(`SELECT c.id, c.nom, c.prenom, c.email, c.role, s.Nom as service_name, c.CampusId FROM CollaborateurAD c JOIN Services s ON c.ServiceId = s.Id WHERE c.ServiceId = ? AND c.id != ? ORDER BY c.prenom, c.nom`, [serviceId, managerId]);
|
||
res.json({ success: true, message: 'Équipe récupérée', team_members: members, service_id: serviceId });
|
||
} catch (error) {
|
||
res.status(500).json({ success: false, message: 'Erreur', error: error.message });
|
||
}
|
||
});
|
||
|
||
app.get('/getNotifications', async (req, res) => {
|
||
try {
|
||
const userIdParam = req.query.user_id;
|
||
|
||
if (!userIdParam) {
|
||
return res.json({ success: false, message: 'ID utilisateur manquant' });
|
||
}
|
||
|
||
const conn = await pool.getConnection();
|
||
|
||
// ✅ Déterminer si c'est un UUID ou un ID numérique
|
||
const isUUID = userIdParam.length > 10 && userIdParam.includes('-');
|
||
|
||
// ✅ Récupérer l'ID numérique si on a un UUID
|
||
let userId = userIdParam;
|
||
|
||
if (isUUID) {
|
||
const [userRows] = await conn.query(
|
||
'SELECT id, CampusId FROM CollaborateurAD WHERE entraUserId = ? AND (Actif = 1 OR Actif IS NULL)',
|
||
[userIdParam]
|
||
);
|
||
|
||
if (userRows.length === 0) {
|
||
conn.release();
|
||
return res.json({
|
||
success: false,
|
||
message: 'Utilisateur non trouvé ou compte désactivé'
|
||
});
|
||
}
|
||
|
||
userId = userRows[0].id;
|
||
} else {
|
||
userId = parseInt(userIdParam);
|
||
}
|
||
|
||
// ✅ Utiliser l'ID numérique pour la requête
|
||
const [notifications] = await conn.query(`
|
||
SELECT * FROM Notifications
|
||
WHERE CollaborateurADId = ?
|
||
ORDER BY DateCreation DESC
|
||
LIMIT 50
|
||
`, [userId]);
|
||
|
||
conn.release();
|
||
|
||
res.json({
|
||
success: true,
|
||
notifications: notifications || []
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('Erreur getNotifications:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur serveur',
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
app.post('/markNotificationRead', async (req, res) => {
|
||
try {
|
||
const { notificationId } = req.body;
|
||
if (!notificationId || notificationId <= 0) return res.status(400).json({ success: false, message: 'ID notification invalide' });
|
||
await pool.query('UPDATE Notifications SET lu = 1 WHERE Id = ?', [notificationId]);
|
||
res.json({ success: true, message: 'Notification marquée comme lue' });
|
||
} catch (error) {
|
||
res.status(500).json({ success: false, message: 'Erreur', error: error.message });
|
||
}
|
||
});
|
||
|
||
app.post('/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), async (req, res) => {
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
await conn.beginTransaction();
|
||
|
||
const currentYear = new Date().getFullYear();
|
||
|
||
// ✅ Récupérer les fichiers uploadés
|
||
const uploadedFiles = req.files || [];
|
||
console.log('📎 Fichiers médicaux reçus:', uploadedFiles.length);
|
||
|
||
// ✅ Les données arrivent différemment avec FormData
|
||
const { DateDebut, DateFin, NombreJours, Email, Nom, Commentaire, statut } = req.body;
|
||
|
||
// ✅ Parser la répartition (elle arrive en string depuis FormData)
|
||
const Repartition = JSON.parse(req.body.Repartition || '[]');
|
||
|
||
if (!DateDebut || !DateFin || !Repartition || !Email || !Nom) {
|
||
// ✅ Nettoyer les fichiers en cas d'erreur
|
||
uploadedFiles.forEach(file => {
|
||
if (fs.existsSync(file.path)) {
|
||
fs.unlinkSync(file.path);
|
||
}
|
||
});
|
||
return res.json({ success: false, message: 'Données manquantes' });
|
||
}
|
||
|
||
// ✅ Validation : Si arrêt maladie, il faut au moins 1 fichier
|
||
const hasABS = Repartition.some(r => r.TypeConge === 'ABS');
|
||
if (hasABS && uploadedFiles.length === 0) {
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.json({
|
||
success: false,
|
||
message: 'Un justificatif médical est obligatoire pour un arrêt maladie'
|
||
});
|
||
}
|
||
|
||
// ⭐ NOUVEAU : Validation de la répartition
|
||
console.log('\n📥 === SOUMISSION DEMANDE CONGÉ ===');
|
||
console.log('Email:', Email);
|
||
console.log('Période:', DateDebut, '→', DateFin);
|
||
console.log('Nombre de jours total:', NombreJours);
|
||
console.log('Répartition reçue:', JSON.stringify(Repartition, null, 2));
|
||
console.log('Fichiers médicaux:', uploadedFiles.length);
|
||
|
||
// ⭐ Calculer la somme de la répartition
|
||
const sommeRepartition = Repartition.reduce((sum, r) => {
|
||
// Ne compter que CP et RTT (pas ABS ni Formation ni Récup)
|
||
if (r.TypeConge === 'CP' || r.TypeConge === 'RTT') {
|
||
return sum + parseFloat(r.NombreJours || 0);
|
||
}
|
||
return sum;
|
||
}, 0);
|
||
|
||
console.log('Somme répartition CP+RTT:', sommeRepartition.toFixed(2));
|
||
|
||
// ⭐ VALIDATION : La somme doit correspondre au total (tolérance 0.01j)
|
||
const hasCountableLeave = Repartition.some(r => r.TypeConge === 'CP' || r.TypeConge === 'RTT');
|
||
|
||
if (hasCountableLeave && Math.abs(sommeRepartition - NombreJours) > 0.01) {
|
||
console.error('❌ ERREUR : Répartition incohérente !');
|
||
console.error(` Attendu: ${NombreJours}j`);
|
||
console.error(` Reçu: ${sommeRepartition}j`);
|
||
|
||
// ✅ Nettoyer les fichiers
|
||
uploadedFiles.forEach(file => {
|
||
if (fs.existsSync(file.path)) {
|
||
fs.unlinkSync(file.path);
|
||
}
|
||
});
|
||
|
||
await conn.rollback();
|
||
conn.release();
|
||
|
||
return res.json({
|
||
success: false,
|
||
message: `Erreur de répartition : la somme (${sommeRepartition.toFixed(2)}j) ne correspond pas au total (${NombreJours}j)`,
|
||
details: {
|
||
repartition: Repartition,
|
||
somme: sommeRepartition,
|
||
attendu: NombreJours
|
||
}
|
||
});
|
||
}
|
||
|
||
// ⭐ Vérifier qu'aucun type n'a 0 jour
|
||
for (const rep of Repartition) {
|
||
if ((rep.TypeConge === 'CP' || rep.TypeConge === 'RTT') && parseFloat(rep.NombreJours || 0) <= 0) {
|
||
console.error(`❌ ERREUR : ${rep.TypeConge} a ${rep.NombreJours} jours !`);
|
||
|
||
// ✅ Nettoyer les fichiers
|
||
uploadedFiles.forEach(file => {
|
||
if (fs.existsSync(file.path)) {
|
||
fs.unlinkSync(file.path);
|
||
}
|
||
});
|
||
|
||
await conn.rollback();
|
||
conn.release();
|
||
|
||
return res.json({
|
||
success: false,
|
||
message: `Le type ${rep.TypeConge} doit avoir au moins 0.5 jour`
|
||
});
|
||
}
|
||
}
|
||
|
||
console.log('✅ Validation répartition OK');
|
||
|
||
// ⭐ Détection si c'est uniquement une formation
|
||
const isFormationOnly = Repartition.length === 1 && Repartition[0].TypeConge === 'Formation';
|
||
const statutDemande = statut || (isFormationOnly ? 'Validée' : 'En attente');
|
||
|
||
const dateDebut = new Date(DateDebut).toLocaleDateString('fr-FR');
|
||
const dateFin = new Date(DateFin).toLocaleDateString('fr-FR');
|
||
const datesPeriode = dateDebut === dateFin ? dateDebut : `du ${dateDebut} au ${dateFin}`;
|
||
|
||
console.log('🔍 Type de demande:', { isFormationOnly, statut: statutDemande });
|
||
|
||
const [collabAD] = await conn.query('SELECT id, CampusId FROM CollaborateurAD WHERE email = ? LIMIT 1', [Email]);
|
||
const isAD = collabAD.length > 0;
|
||
const collaborateurId = isAD ? collabAD[0].id : null;
|
||
const dateEntree = isAD && collabAD[0].DateEntree ? collabAD[0].DateEntree : null;
|
||
|
||
let employeeId = null;
|
||
if (!isAD) {
|
||
const [user] = await conn.query('SELECT ID FROM Users WHERE Email = ? LIMIT 1', [Email]);
|
||
if (user.length === 0) {
|
||
// ✅ Nettoyer les fichiers
|
||
uploadedFiles.forEach(file => {
|
||
if (fs.existsSync(file.path)) {
|
||
fs.unlinkSync(file.path);
|
||
}
|
||
});
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.json({ success: false, message: 'Utilisateur non trouvé' });
|
||
}
|
||
employeeId = user[0].ID;
|
||
}
|
||
|
||
// ========================================
|
||
// ÉTAPE 1 : Vérification des soldes AVANT tout
|
||
// ========================================
|
||
if (isAD && collaborateurId && !isFormationOnly) {
|
||
console.log('\n🔍 Vérification des soldes (avec anticipation)...');
|
||
|
||
const [userRole] = await conn.query('SELECT role FROM CollaborateurAD WHERE id = ?', [collaborateurId]);
|
||
const isApprenti = userRole.length > 0 && userRole[0].role === 'Apprenti';
|
||
|
||
for (const rep of Repartition) {
|
||
if (rep.TypeConge === 'ABS' || rep.TypeConge === 'Formation' || rep.TypeConge === 'Récup') {
|
||
continue;
|
||
}
|
||
if (rep.TypeConge === 'RTT' && isApprenti) {
|
||
uploadedFiles.forEach(file => {
|
||
if (fs.existsSync(file.path)) fs.unlinkSync(file.path);
|
||
});
|
||
await conn.rollback();
|
||
conn.release();
|
||
|
||
return res.json({
|
||
success: false,
|
||
message: `❌ Les apprentis ne peuvent pas poser de RTT`
|
||
});
|
||
}
|
||
|
||
|
||
const joursNecessaires = parseFloat(rep.NombreJours);
|
||
const name = rep.TypeConge === 'CP' ? 'Congé payé' : 'RTT';
|
||
|
||
const [typeRow] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', [name]);
|
||
|
||
if (typeRow.length === 0) continue;
|
||
|
||
const typeCongeId = typeRow[0].Id;
|
||
|
||
// Récupérer le solde actuel
|
||
const [compteur] = await conn.query(`
|
||
SELECT Total, Solde, SoldeReporte
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collaborateurId, typeCongeId, currentYear]);
|
||
|
||
let soldeDisponible = 0;
|
||
|
||
if (compteur.length > 0) {
|
||
soldeDisponible = parseFloat(compteur[0].Solde || 0);
|
||
}
|
||
|
||
// Si le solde est insuffisant, calculer l'anticipation possible
|
||
if (soldeDisponible < joursNecessaires) {
|
||
const manque = joursNecessaires - soldeDisponible;
|
||
|
||
// Calculer l'acquisition future
|
||
let acquisitionFuture = 0;
|
||
|
||
if (rep.TypeConge === 'CP') {
|
||
const finExercice = new Date(currentYear + 1, 4, 31);
|
||
const acquisTotale = calculerAcquisitionCP(finExercice, dateEntree);
|
||
const acquisActuelle = compteur.length > 0 ? parseFloat(compteur[0].Total || 0) : 0;
|
||
acquisitionFuture = acquisTotale - acquisActuelle;
|
||
} else {
|
||
const finAnnee = new Date(currentYear, 11, 31);
|
||
const rttDataTotal = await calculerAcquisitionRTT(conn, collaborateurId, finAnnee);
|
||
const acquisActuelle = compteur.length > 0 ? parseFloat(compteur[0].Total || 0) : 0;
|
||
acquisitionFuture = rttDataTotal.acquisition - acquisActuelle;
|
||
}
|
||
|
||
// Vérifier si l'anticipation est possible
|
||
if (manque > acquisitionFuture) {
|
||
uploadedFiles.forEach(file => {
|
||
if (fs.existsSync(file.path)) fs.unlinkSync(file.path);
|
||
});
|
||
await conn.rollback();
|
||
conn.release();
|
||
|
||
return res.json({
|
||
success: false,
|
||
message: `❌ Solde insuffisant pour ${name}`,
|
||
details: {
|
||
type: name,
|
||
demande: joursNecessaires,
|
||
soldeActuel: soldeDisponible.toFixed(2),
|
||
acquisitionFutureMax: acquisitionFuture.toFixed(2),
|
||
manque: (manque - acquisitionFuture).toFixed(2)
|
||
}
|
||
});
|
||
}
|
||
|
||
console.log(`⚠️ ${name}: Utilisation de ${manque.toFixed(2)}j en anticipé`);
|
||
}
|
||
}
|
||
|
||
console.log('✅ Soldes suffisants (avec anticipation si nécessaire)');
|
||
}
|
||
// ========================================
|
||
// ÉTAPE 2 : CRÉER LA DEMANDE EN PREMIER
|
||
// ========================================
|
||
console.log('\n📝 Création de la demande...');
|
||
|
||
const typeIds = [];
|
||
for (const rep of Repartition) {
|
||
const code = rep.TypeConge;
|
||
|
||
// Ne pas inclure ABS, Formation, Récup dans les typeIds principaux
|
||
if (code === 'ABS' || code === 'Formation' || code === 'Récup') {
|
||
continue;
|
||
}
|
||
|
||
const name = code === 'CP' ? 'Congé payé' : 'RTT';
|
||
const [typeRow] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', [name]);
|
||
if (typeRow.length > 0) typeIds.push(typeRow[0].Id);
|
||
}
|
||
|
||
// ⭐ Si aucun type CP/RTT, prendre le premier type de la répartition
|
||
if (typeIds.length === 0) {
|
||
const firstType = Repartition[0]?.TypeConge;
|
||
const name = firstType === 'Formation' ? 'Formation' :
|
||
firstType === 'ABS' ? 'Congé maladie' :
|
||
firstType === 'Récup' ? 'Récupération' : 'Congé payé';
|
||
|
||
const [typeRow] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', [name]);
|
||
if (typeRow.length > 0) {
|
||
typeIds.push(typeRow[0].Id);
|
||
} else {
|
||
uploadedFiles.forEach(file => {
|
||
if (fs.existsSync(file.path)) fs.unlinkSync(file.path);
|
||
});
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.json({ success: false, message: 'Aucun type de congé valide' });
|
||
}
|
||
}
|
||
|
||
const typeCongeIdCsv = typeIds.join(',');
|
||
const currentDate = new Date().toISOString().slice(0, 19).replace('T', ' ');
|
||
|
||
// ✅ CRÉER LA DEMANDE
|
||
const [result] = await conn.query(
|
||
`INSERT INTO DemandeConge
|
||
(EmployeeId, CollaborateurADId, DateDebut, DateFin, TypeCongeId, Statut, DateDemande, Commentaire, Validateur, NombreJours)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||
[isAD ? 0 : employeeId, collaborateurId, DateDebut, DateFin, typeCongeIdCsv, statutDemande, currentDate, Commentaire || '', '', NombreJours]
|
||
);
|
||
|
||
const demandeId = result.insertId;
|
||
console.log(`✅ Demande créée avec ID ${demandeId} - Statut: ${statutDemande}`);
|
||
|
||
// ========================================
|
||
// ÉTAPE 3 : Sauvegarder les fichiers médicaux
|
||
// ========================================
|
||
if (uploadedFiles.length > 0) {
|
||
console.log('\n📎 Sauvegarde des fichiers médicaux...');
|
||
for (const file of uploadedFiles) {
|
||
await conn.query(
|
||
`INSERT INTO DocumentsMedicaux
|
||
(DemandeCongeId, NomFichier, CheminFichier, TypeMime, TailleFichier, DateUpload)
|
||
VALUES (?, ?, ?, ?, ?, NOW())`,
|
||
[demandeId, file.originalname, file.path, file.mimetype, file.size]
|
||
);
|
||
console.log(` ✓ ${file.originalname} (${(file.size / 1024).toFixed(2)} KB)`);
|
||
}
|
||
}
|
||
|
||
// ========================================
|
||
// ÉTAPE 4 : Sauvegarder la répartition
|
||
// ========================================
|
||
console.log('\n📊 Sauvegarde de la répartition en base...');
|
||
for (const rep of Repartition) {
|
||
const code = rep.TypeConge;
|
||
const name = code === 'CP' ? 'Congé payé' :
|
||
code === 'RTT' ? 'RTT' :
|
||
code === 'ABS' ? 'Congé maladie' :
|
||
code === 'Formation' ? 'Formation' :
|
||
code === 'Récup' ? 'Récupération' : code;
|
||
|
||
const [typeRow] = await conn.query(
|
||
'SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1',
|
||
[name]
|
||
);
|
||
|
||
if (typeRow.length > 0) {
|
||
await conn.query(
|
||
`INSERT INTO DemandeCongeType
|
||
(DemandeCongeId, TypeCongeId, NombreJours, PeriodeJournee)
|
||
VALUES (?, ?, ?, ?)`,
|
||
[
|
||
demandeId,
|
||
typeRow[0].Id,
|
||
rep.NombreJours,
|
||
rep.PeriodeJournee || 'Journée entière' // ✅ NOUVELLE COLONNE
|
||
]
|
||
);
|
||
console.log(` ✓ ${name}: ${rep.NombreJours}j (${rep.PeriodeJournee || 'Journée entière'})`);
|
||
}
|
||
}
|
||
|
||
// ========================================
|
||
// ÉTAPE 5 : ACCUMULATION DES RÉCUP (maintenant demandeId existe)
|
||
// ========================================
|
||
if (isAD && collaborateurId && !isFormationOnly) {
|
||
const hasRecup = Repartition.some(r => r.TypeConge === 'Récup');
|
||
|
||
if (hasRecup) {
|
||
console.log('\n📥 Accumulation des jours de récupération...');
|
||
|
||
const [recupType] = await conn.query(
|
||
'SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1',
|
||
['Récupération']
|
||
);
|
||
|
||
if (recupType.length > 0) {
|
||
const recupJours = Repartition.find(r => r.TypeConge === 'Récup')?.NombreJours || 0;
|
||
|
||
if (recupJours > 0) {
|
||
const [compteurExisting] = await conn.query(`
|
||
SELECT Id, Total, Solde FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collaborateurId, recupType[0].Id, currentYear]);
|
||
|
||
if (compteurExisting.length > 0) {
|
||
// ⭐ AJOUTER les jours au compteur existant
|
||
await conn.query(`
|
||
UPDATE CompteurConges
|
||
SET Total = Total + ?,
|
||
Solde = Solde + ?,
|
||
DerniereMiseAJour = NOW()
|
||
WHERE Id = ?
|
||
`, [recupJours, recupJours, compteurExisting[0].Id]);
|
||
|
||
console.log(` ✓ Récupération: +${recupJours}j ajoutés (nouveau solde: ${(parseFloat(compteurExisting[0].Solde) + recupJours).toFixed(2)}j)`);
|
||
} else {
|
||
// ⭐ CRÉER le compteur avec les jours
|
||
await conn.query(`
|
||
INSERT INTO CompteurConges
|
||
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour)
|
||
VALUES (?, ?, ?, ?, ?, 0, NOW())
|
||
`, [collaborateurId, recupType[0].Id, currentYear, recupJours, recupJours]);
|
||
|
||
console.log(` ✓ Récupération: ${recupJours}j créés (nouveau compteur)`);
|
||
}
|
||
|
||
// ⭐ Enregistrer l'ACCUMULATION (maintenant demandeId existe !)
|
||
await conn.query(`
|
||
INSERT INTO DeductionDetails
|
||
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
|
||
VALUES (?, ?, ?, 'Accum Récup', ?)
|
||
`, [demandeId, recupType[0].Id, currentYear, recupJours]);
|
||
|
||
console.log(` ✓ Accumulation enregistrée dans DeductionDetails`);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ========================================
|
||
// ÉTAPE 6 : Déduction des compteurs CP/RTT
|
||
// ========================================
|
||
if (isAD && collaborateurId && !isFormationOnly) {
|
||
console.log('\n📉 Déduction des compteurs...');
|
||
|
||
for (const rep of Repartition) {
|
||
if (rep.TypeConge === 'ABS' || rep.TypeConge === 'Formation' || rep.TypeConge === 'Récup') {
|
||
console.log(` ⏩ ${rep.TypeConge} ignoré (pas de déduction)`);
|
||
continue;
|
||
}
|
||
|
||
const name = rep.TypeConge === 'CP' ? 'Congé payé' : 'RTT';
|
||
const [typeRow] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', [name]);
|
||
|
||
if (typeRow.length > 0) {
|
||
const result = await deductLeaveBalanceWithTracking(
|
||
conn,
|
||
collaborateurId,
|
||
typeRow[0].Id,
|
||
rep.NombreJours,
|
||
demandeId
|
||
);
|
||
|
||
console.log(` ✓ ${name}: ${rep.NombreJours}j déduits`);
|
||
if (result.details && result.details.length > 0) {
|
||
result.details.forEach(d => {
|
||
console.log(` - ${d.type} (${d.annee}): ${d.joursUtilises}j`);
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
console.log('✅ Déductions terminées');
|
||
}
|
||
|
||
// ========================================
|
||
// ÉTAPE 7 : Créer notification pour formation
|
||
// ========================================
|
||
// ÉTAPE 7 : Créer notification pour formation
|
||
if (isFormationOnly && isAD && collaborateurId) {
|
||
await conn.query(
|
||
`INSERT INTO Notifications (CollaborateurADId, Type, Titre, Message, DemandeCongeId, DateCreation, Lu)
|
||
VALUES (?, ?, ?, ?, ?, NOW(), 0)`,
|
||
[
|
||
collaborateurId,
|
||
'Success', // ✅ Valeur correcte de l'enum
|
||
'✅ Formation validée automatiquement',
|
||
`Votre période de formation ${datesPeriode} a été validée automatiquement.`,
|
||
demandeId
|
||
]
|
||
);
|
||
console.log('\n📬 Notification formation créée');
|
||
}
|
||
|
||
// ========================================
|
||
// ÉTAPE 8 : Récupérer les managers
|
||
// ========================================
|
||
let managers = [];
|
||
if (isAD) {
|
||
const [rows] = await conn.query(
|
||
`SELECT c.email FROM HierarchieValidationAD hv
|
||
JOIN CollaborateurAD c ON hv.SuperieurId = c.id
|
||
WHERE hv.CollaborateurId = ?`,
|
||
[collaborateurId]
|
||
);
|
||
managers = rows.map(r => r.email);
|
||
} else {
|
||
const [rows] = await conn.query(
|
||
`SELECT u.Email FROM HierarchieValidation hv
|
||
JOIN Users u ON hv.SuperieurId = u.ID
|
||
WHERE hv.EmployeId = ?`,
|
||
[employeeId]
|
||
);
|
||
managers = rows.map(r => r.Email);
|
||
}
|
||
|
||
// ========================================
|
||
// COMMIT DE LA TRANSACTION
|
||
// ========================================
|
||
await conn.commit();
|
||
console.log('\n🎉 Transaction validée\n');
|
||
|
||
// ========================================
|
||
// ÉTAPE 9 : Notifier les clients SSE
|
||
// ========================================
|
||
if (isFormationOnly && isAD && collaborateurId) {
|
||
notifyCollabClients({
|
||
type: 'demande-validated',
|
||
demandeId: parseInt(demandeId),
|
||
statut: 'Validée',
|
||
timestamp: new Date().toISOString()
|
||
}, collaborateurId);
|
||
|
||
notifyCollabClients({
|
||
type: 'demande-list-updated',
|
||
action: 'formation-auto-validated',
|
||
demandeId: parseInt(demandeId),
|
||
timestamp: new Date().toISOString()
|
||
});
|
||
}
|
||
|
||
// ========================================
|
||
// ENVOI DES EMAILS (code inchangé)
|
||
// ========================================
|
||
const accessToken = await getGraphToken();
|
||
if (accessToken) {
|
||
const fromEmail = 'noreply@ensup.eu';
|
||
const typesConges = Repartition.map(rep => {
|
||
const typeNom = rep.TypeConge === 'CP' ? 'Congé payé' :
|
||
rep.TypeConge === 'RTT' ? 'RTT' :
|
||
rep.TypeConge === 'ABS' ? 'Congé maladie' :
|
||
rep.TypeConge === 'Formation' ? 'Formation' :
|
||
rep.TypeConge;
|
||
return `${typeNom}: ${rep.NombreJours}j`;
|
||
}).join(' | ');
|
||
|
||
if (isFormationOnly) {
|
||
const subjectCollab = '✅ Votre saisie de période de formation a été enregistrée';
|
||
const bodyCollab = `
|
||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||
<div style="background-color: #8b5cf6; color: white; padding: 20px; border-radius: 8px 8px 0 0;">
|
||
<h2 style="margin: 0;">✅ Formation enregistrée</h2>
|
||
</div>
|
||
<div style="background-color: #f9fafb; padding: 20px; border: 1px solid #e5e7eb;">
|
||
<p style="font-size: 16px;">Bonjour <strong>${Nom}</strong>,</p>
|
||
<p>Votre période de formation a bien été <strong style="color: #8b5cf6;">enregistrée et validée automatiquement</strong>.</p>
|
||
<div style="background-color: white; border-left: 4px solid #8b5cf6; padding: 15px; margin: 20px 0;">
|
||
<p><strong>Type :</strong> Formation</p>
|
||
<p><strong>Période :</strong> ${datesPeriode}</p>
|
||
<p><strong>Durée :</strong> ${NombreJours} jour(s)</p>
|
||
${Commentaire ? `<p><strong>Description :</strong> ${Commentaire}</p>` : ''}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
try {
|
||
await sendMailGraph(accessToken, fromEmail, Email, subjectCollab, bodyCollab);
|
||
console.log('✅ Email confirmation formation envoyé');
|
||
} catch (mailError) {
|
||
console.error('❌ Erreur email:', mailError.message);
|
||
}
|
||
|
||
for (const managerEmail of managers) {
|
||
const subjectManager = `📚 Information : Formation déclarée - ${Nom}`;
|
||
const bodyManager = `
|
||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||
<div style="background-color: #8b5cf6; color: white; padding: 20px;">
|
||
<h2 style="margin: 0;">📚 Formation enregistrée</h2>
|
||
</div>
|
||
<div style="padding: 20px;">
|
||
<p><strong>${Nom}</strong> vous informe d'une période de formation.</p>
|
||
<p><strong>Période :</strong> ${datesPeriode}</p>
|
||
<p><strong>Durée :</strong> ${NombreJours} jour(s)</p>
|
||
</div>
|
||
</div>
|
||
`;
|
||
try {
|
||
await sendMailGraph(accessToken, fromEmail, managerEmail, subjectManager, bodyManager);
|
||
} catch (mailError) {
|
||
console.error('❌ Erreur email manager:', mailError.message);
|
||
}
|
||
}
|
||
} else {
|
||
const subjectCollab = '✅ Confirmation de réception de votre demande de congé';
|
||
const bodyCollab = `
|
||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||
<div style="background-color: #3b82f6; color: white; padding: 20px;">
|
||
<h2 style="margin: 0;">✅ Demande enregistrée</h2>
|
||
</div>
|
||
<div style="padding: 20px;">
|
||
<p>Bonjour <strong>${Nom}</strong>,</p>
|
||
<p>Votre demande de congé a bien été enregistrée.</p>
|
||
<p><strong>Type :</strong> ${typesConges}</p>
|
||
<p><strong>Période :</strong> ${datesPeriode}</p>
|
||
<p><strong>Durée :</strong> ${NombreJours} jour(s)</p>
|
||
</div>
|
||
</div>
|
||
`;
|
||
try {
|
||
await sendMailGraph(accessToken, fromEmail, Email, subjectCollab, bodyCollab);
|
||
} catch (mailError) {
|
||
console.error('❌ Erreur email:', mailError.message);
|
||
}
|
||
|
||
for (const managerEmail of managers) {
|
||
const subjectManager = `📋 Nouvelle demande de congé - ${Nom}`;
|
||
const bodyManager = `
|
||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||
<div style="background-color: #f59e0b; color: white; padding: 20px;">
|
||
<h2 style="margin: 0;">📋 Nouvelle demande</h2>
|
||
</div>
|
||
<div style="padding: 20px;">
|
||
<p><strong>${Nom}</strong> a soumis une nouvelle demande.</p>
|
||
<p><strong>Type :</strong> ${typesConges}</p>
|
||
<p><strong>Période :</strong> ${datesPeriode}</p>
|
||
</div>
|
||
</div>
|
||
`;
|
||
try {
|
||
await sendMailGraph(accessToken, fromEmail, managerEmail, subjectManager, bodyManager);
|
||
} catch (mailError) {
|
||
console.error('❌ Erreur email manager:', mailError.message);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
res.json({
|
||
success: true,
|
||
message: isFormationOnly ? 'Formation enregistrée et validée automatiquement' : 'Demande soumise',
|
||
request_id: demandeId,
|
||
managers,
|
||
auto_validated: isFormationOnly,
|
||
files_uploaded: uploadedFiles.length
|
||
});
|
||
|
||
} catch (error) {
|
||
await conn.rollback();
|
||
|
||
// ✅ Nettoyer les fichiers uploadés en cas d'erreur
|
||
if (req.files) {
|
||
req.files.forEach(file => {
|
||
if (fs.existsSync(file.path)) {
|
||
fs.unlinkSync(file.path);
|
||
console.log(`🗑️ Fichier supprimé: ${file.originalname}`);
|
||
}
|
||
});
|
||
}
|
||
|
||
console.error('\n❌ ERREUR submitLeaveRequest:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur serveur',
|
||
error: error.message
|
||
});
|
||
} finally {
|
||
conn.release();
|
||
}
|
||
});
|
||
|
||
app.get('/download-medical/:documentId', async (req, res) => {
|
||
try {
|
||
const { documentId } = req.params;
|
||
const conn = await pool.getConnection();
|
||
|
||
const [docs] = await conn.query(
|
||
'SELECT * FROM DocumentsMedicaux WHERE Id = ?',
|
||
[documentId]
|
||
);
|
||
|
||
conn.release();
|
||
|
||
if (docs.length === 0) {
|
||
return res.status(404).json({ success: false, message: 'Document non trouvé' });
|
||
}
|
||
|
||
const doc = docs[0];
|
||
|
||
if (!fs.existsSync(doc.CheminFichier)) {
|
||
return res.status(404).json({ success: false, message: 'Fichier introuvable' });
|
||
}
|
||
|
||
res.download(doc.CheminFichier, doc.NomFichier);
|
||
|
||
} catch (error) {
|
||
console.error('Erreur téléchargement:', error);
|
||
res.status(500).json({ success: false, message: 'Erreur serveur' });
|
||
}
|
||
});
|
||
|
||
// Récupérer les documents d'une demande
|
||
app.get('/medical-documents/:demandeId', async (req, res) => {
|
||
try {
|
||
const { demandeId } = req.params;
|
||
const conn = await pool.getConnection();
|
||
|
||
const [docs] = await conn.query(
|
||
`SELECT Id, NomFichier, TypeMime, TailleFichier, DateUpload
|
||
FROM DocumentsMedicaux
|
||
WHERE DemandeCongeId = ?
|
||
ORDER BY DateUpload DESC`,
|
||
[demandeId]
|
||
);
|
||
|
||
conn.release();
|
||
|
||
res.json({
|
||
success: true,
|
||
documents: docs.map(doc => ({
|
||
id: doc.Id,
|
||
nom: doc.NomFichier,
|
||
type: doc.TypeMime,
|
||
taille: doc.TailleFichier,
|
||
date: doc.DateUpload,
|
||
downloadUrl: `/download-medical/${doc.Id}`
|
||
}))
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('Erreur récupération documents:', error);
|
||
res.status(500).json({ success: false, message: 'Erreur serveur' });
|
||
}
|
||
});
|
||
app.post('/validateRequest', async (req, res) => {
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
await conn.beginTransaction();
|
||
const { request_id, action, validator_id, comment } = req.body;
|
||
|
||
console.log(`\n🔍 Validation demande #${request_id} - Action: ${action}`);
|
||
|
||
if (!request_id || !action || !validator_id) {
|
||
return res.json({ success: false, message: 'Données manquantes' });
|
||
}
|
||
|
||
const [validator] = await conn.query('SELECT Id, prenom, nom, email, CampusId FROM CollaborateurAD WHERE Id = ?', [validator_id]);
|
||
|
||
if (validator.length === 0) {
|
||
throw new Error('Validateur introuvable');
|
||
}
|
||
|
||
// Récupérer les informations de la demande
|
||
const [requests] = await conn.query(
|
||
`SELECT
|
||
dc.Id,
|
||
dc.CollaborateurADId,
|
||
dc.TypeCongeId,
|
||
dc.NombreJours,
|
||
dc.DateDebut,
|
||
dc.DateFin,
|
||
dc.Commentaire,
|
||
dc.Statut,
|
||
ca.prenom,
|
||
ca.nom,
|
||
ca.email as collaborateur_email,
|
||
tc.Nom as TypeConge
|
||
FROM DemandeConge dc
|
||
JOIN TypeConge tc ON dc.TypeCongeId = tc.Id
|
||
LEFT JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.Id
|
||
WHERE dc.Id = ?
|
||
LIMIT 1`,
|
||
[request_id]
|
||
);
|
||
|
||
if (requests.length === 0) {
|
||
throw new Error('Demande non trouvée');
|
||
}
|
||
|
||
const request = requests[0];
|
||
|
||
// ⭐ DÉBOGAGE : Afficher TOUTES les données
|
||
console.log('\n=== DONNÉES RÉCUPÉRÉES ===');
|
||
console.log('request:', JSON.stringify(request, null, 2));
|
||
console.log('validator:', JSON.stringify(validator[0], null, 2));
|
||
console.log('========================\n');
|
||
|
||
if (request.Statut !== 'En attente') {
|
||
throw new Error(`La demande a déjà été traitée (Statut: ${request.Statut})`);
|
||
}
|
||
|
||
const newStatus = action === 'approve' ? 'Validée' : 'Refusée';
|
||
|
||
if (action === 'reject' && request.CollaborateurADId) {
|
||
console.log(`\n🔄 REFUS - Restauration des soldes...`);
|
||
const restoration = await restoreLeaveBalance(conn, request_id, request.CollaborateurADId);
|
||
console.log('Restauration:', restoration);
|
||
}
|
||
|
||
await conn.query(
|
||
`UPDATE DemandeConge SET Statut = ?, ValidateurId = ?, ValidateurADId = ?, DateValidation = NOW(), CommentaireValidation = ? WHERE Id = ?`,
|
||
[newStatus, validator_id, validator_id, comment || '', request_id]
|
||
);
|
||
|
||
const notifTitle = action === 'approve' ? 'Demande approuvée ✅' : 'Demande refusée ❌';
|
||
let notifMessage = `Votre demande a été ${action === 'approve' ? 'approuvée' : 'refusée'}`;
|
||
if (comment) notifMessage += ` (Commentaire: ${comment})`;
|
||
const notifType = action === 'approve' ? 'Success' : 'Error';
|
||
|
||
await conn.query(
|
||
'INSERT INTO Notifications (CollaborateurADId, Titre, Message, Type, DemandeCongeId, DateCreation, lu) VALUES (?, ?, ?, ?, ?, ?, 0)',
|
||
[request.CollaborateurADId, notifTitle, notifMessage, notifType, request_id, nowFR()]
|
||
);
|
||
|
||
await conn.commit();
|
||
|
||
// ⭐ TEST EMAIL
|
||
// ⭐ ENVOI EMAIL AVEC TEMPLATE PROFESSIONNEL
|
||
console.log('\n📧 === TENTATIVE ENVOI EMAIL ===');
|
||
console.log('1. Récupération token...');
|
||
|
||
const accessToken = await getGraphToken();
|
||
console.log('2. Token obtenu ?', accessToken ? 'OUI' : 'NON');
|
||
|
||
if (accessToken && request.collaborateur_email) {
|
||
const fromEmail = 'noreply@ensup.eu';
|
||
const collaborateurNom = `${request.prenom} ${request.nom}`;
|
||
const validateurNom = `${validator[0].prenom} ${validator[0].nom}`;
|
||
|
||
console.log('3. Préparation email professionnel...');
|
||
console.log(' De:', fromEmail);
|
||
console.log(' À:', request.collaborateur_email);
|
||
console.log(' Collaborateur:', collaborateurNom);
|
||
console.log(' Validateur:', validateurNom);
|
||
|
||
const dateDebut = new Date(request.DateDebut).toLocaleDateString('fr-FR');
|
||
const dateFin = new Date(request.DateFin).toLocaleDateString('fr-FR');
|
||
const datesPeriode = dateDebut === dateFin ? dateDebut : `du ${dateDebut} au ${dateFin}`;
|
||
|
||
const subject = action === 'approve'
|
||
? '✅ Votre demande de congé a été approuvée'
|
||
: '❌ Votre demande de congé a été refusée';
|
||
|
||
const body = action === 'approve'
|
||
? `<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||
<div style="background-color: #10b981; color: white; padding: 20px; border-radius: 8px 8px 0 0;">
|
||
<h2 style="margin: 0;">✅ Demande approuvée</h2>
|
||
</div>
|
||
<div style="background-color: #f9fafb; padding: 20px; border: 1px solid #e5e7eb;">
|
||
<p style="font-size: 16px;">Bonjour <strong>${collaborateurNom}</strong>,</p>
|
||
<p>Votre demande de congé a été <strong style="color: #10b981;">approuvée</strong> par ${validateurNom}.</p>
|
||
<div style="background-color: white; border-left: 4px solid #10b981; padding: 15px; margin: 20px 0;">
|
||
<p><strong>Type :</strong> ${request.TypeConge}</p>
|
||
<p><strong>Période :</strong> ${datesPeriode}</p>
|
||
<p><strong>Durée :</strong> ${request.NombreJours} jour(s)</p>
|
||
${comment ? `<p><strong>Commentaire :</strong> ${comment}</p>` : ''}
|
||
</div>
|
||
<p style="color: #6b7280;">Vous pouvez consulter votre demande dans votre espace personnel.</p>
|
||
</div>
|
||
</div>`
|
||
: `<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||
<div style="background-color: #ef4444; color: white; padding: 20px; border-radius: 8px 8px 0 0;">
|
||
<h2 style="margin: 0;">❌ Demande refusée</h2>
|
||
</div>
|
||
<div style="background-color: #f9fafb; padding: 20px; border: 1px solid #e5e7eb;">
|
||
<p style="font-size: 16px;">Bonjour <strong>${collaborateurNom}</strong>,</p>
|
||
<p>Votre demande de congé a été <strong style="color: #ef4444;">refusée</strong> par ${validateurNom}.</p>
|
||
<div style="background-color: white; border-left: 4px solid #ef4444; padding: 15px; margin: 20px 0;">
|
||
<p><strong>Type :</strong> ${request.TypeConge}</p>
|
||
<p><strong>Période :</strong> ${datesPeriode}</p>
|
||
<p><strong>Durée :</strong> ${request.NombreJours} jour(s)</p>
|
||
${comment ? `<p><strong>Motif du refus :</strong> ${comment}</p>` : ''}
|
||
</div>
|
||
<p style="color: #6b7280;">Pour plus d'informations, contactez ${validateurNom}.</p>
|
||
</div>
|
||
</div>`;
|
||
|
||
console.log('4. Sujet:', subject);
|
||
console.log('5. Appel sendMailGraph...');
|
||
|
||
try {
|
||
await sendMailGraph(
|
||
accessToken,
|
||
fromEmail,
|
||
request.collaborateur_email,
|
||
subject,
|
||
body
|
||
);
|
||
console.log('✅✅✅ EMAIL ENVOYÉ AVEC SUCCÈS ! ✅✅✅');
|
||
} catch (mailError) {
|
||
console.error('❌❌❌ ERREUR ENVOI EMAIL ❌❌❌');
|
||
console.error('Message:', mailError.message);
|
||
console.error('Stack:', mailError.stack);
|
||
}
|
||
} else {
|
||
if (!accessToken) console.error('❌ Token manquant');
|
||
if (!request.collaborateur_email) console.error('❌ Email collaborateur manquant');
|
||
}
|
||
console.log('=== FIN ENVOI EMAIL ===\n');
|
||
|
||
// Notifier les clients SSE locaux
|
||
notifyCollabClients({
|
||
type: 'demande-validated',
|
||
demandeId: parseInt(request_id),
|
||
statut: newStatus,
|
||
timestamp: new Date().toISOString()
|
||
}, request.CollaborateurADId);
|
||
|
||
notifyCollabClients({
|
||
type: 'demande-list-updated',
|
||
action: 'validation',
|
||
demandeId: parseInt(request_id),
|
||
timestamp: new Date().toISOString()
|
||
});
|
||
|
||
// Envoyer webhook au serveur RH
|
||
try {
|
||
await webhookManager.sendWebhook(
|
||
WEBHOOKS.RH_URL,
|
||
EVENTS.DEMANDE_VALIDATED,
|
||
{
|
||
demandeId: parseInt(request_id),
|
||
statut: newStatus,
|
||
collaborateurId: request.CollaborateurADId,
|
||
validateurId: validator_id,
|
||
commentaire: comment
|
||
}
|
||
);
|
||
console.log('✅ Webhook envoyé au serveur RH');
|
||
} catch (webhookError) {
|
||
console.error('❌ Erreur envoi webhook (non bloquant):', webhookError.message);
|
||
}
|
||
|
||
res.json({
|
||
success: true,
|
||
message: `Demande ${action === 'approve' ? 'approuvée' : 'refusée'}`,
|
||
new_status: newStatus
|
||
});
|
||
|
||
console.log(`✅ Demande ${request_id} ${newStatus}\n`);
|
||
|
||
} catch (error) {
|
||
await conn.rollback();
|
||
console.error('\n❌ ERREUR lors de la validation:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: error.message
|
||
});
|
||
} finally {
|
||
conn.release();
|
||
}
|
||
});
|
||
|
||
app.get('/testRestoration', async (req, res) => {
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
const { demande_id, collab_id } = req.query;
|
||
|
||
if (!demande_id || !collab_id) {
|
||
return res.json({
|
||
success: false,
|
||
message: 'Paramètres manquants: demande_id et collab_id requis'
|
||
});
|
||
}
|
||
|
||
// 1. Voir les déductions enregistrées
|
||
const [deductions] = await conn.query(
|
||
`SELECT dd.*, tc.Nom as TypeNom
|
||
FROM DeductionDetails dd
|
||
JOIN TypeConge tc ON dd.TypeCongeId = tc.Id
|
||
WHERE dd.DemandeCongeId = ?`,
|
||
[demande_id]
|
||
);
|
||
|
||
// 2. Voir l'état actuel des compteurs
|
||
const [compteurs] = await conn.query(
|
||
`SELECT cc.*, tc.Nom as TypeNom
|
||
FROM CompteurConges cc
|
||
JOIN TypeConge tc ON cc.TypeCongeId = tc.Id
|
||
WHERE cc.CollaborateurADId = ?
|
||
ORDER BY tc.Nom, cc.Annee DESC`,
|
||
[collab_id]
|
||
);
|
||
|
||
// 3. Calculer ce que devrait être la restauration
|
||
const planRestauration = deductions.map(d => ({
|
||
type: d.TypeNom,
|
||
annee: d.Annee,
|
||
typeDeduction: d.TypeDeduction,
|
||
joursARestorer: d.JoursUtilises,
|
||
action: d.TypeDeduction === 'Reporté N-1'
|
||
? 'Ajouter au SoldeReporte ET au Solde'
|
||
: 'Ajouter au Solde uniquement'
|
||
}));
|
||
|
||
conn.release();
|
||
|
||
res.json({
|
||
success: true,
|
||
demande_id: demande_id,
|
||
collaborateur_id: collab_id,
|
||
deductions_enregistrees: deductions,
|
||
compteurs_actuels: compteurs,
|
||
plan_restauration: planRestauration,
|
||
instructions: planRestauration.length === 0
|
||
? "❌ Aucune déduction trouvée - La demande a été créée avant l'installation du tracking"
|
||
: "✅ Déductions trouvées - La restauration devrait fonctionner"
|
||
});
|
||
|
||
} catch (error) {
|
||
conn.release();
|
||
res.status(500).json({
|
||
success: false,
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
|
||
function normalizeRole(role) {
|
||
if (!role) return null;
|
||
|
||
const roleLower = role.toLowerCase();
|
||
|
||
// Normaliser les variantes féminines et masculines
|
||
if (roleLower === 'collaboratrice') return 'collaborateur';
|
||
if (roleLower === 'validatrice') return 'validateur';
|
||
if (roleLower === 'directrice de campus') return 'directeur de campus';
|
||
if (roleLower === 'apprentie') return 'apprenti';
|
||
|
||
return roleLower;
|
||
}
|
||
|
||
|
||
|
||
|
||
// ========================================
|
||
// ROUTE getTeamLeaves COMPLÈTE
|
||
// ========================================
|
||
app.get('/getTeamLeaves', async (req, res) => {
|
||
try {
|
||
const { user_id: userIdParam, role: roleParam, selectedCampus, selectedSociete, selectedService } = req.query;
|
||
|
||
console.log(`🔍 Paramètres reçus: user_id=${userIdParam}, role=${roleParam}, selectedCampus=${selectedCampus}`);
|
||
|
||
if (!userIdParam) {
|
||
return res.json({ success: false, message: 'ID utilisateur manquant' });
|
||
}
|
||
|
||
const conn = await pool.getConnection();
|
||
|
||
const isUUID = userIdParam.length > 10 && userIdParam.includes('-');
|
||
console.log(`📝 Type ID détecté: ${isUUID ? 'UUID' : 'Numérique'}`);
|
||
|
||
const userQuery = `
|
||
SELECT
|
||
ca.id,
|
||
ca.ServiceId,
|
||
ca.CampusId,
|
||
ca.SocieteId,
|
||
ca.email,
|
||
s.Nom AS serviceNom,
|
||
c.Nom AS campusNom,
|
||
so.Nom AS societeNom
|
||
FROM CollaborateurAD ca
|
||
LEFT JOIN Services s ON ca.ServiceId = s.Id
|
||
LEFT JOIN Campus c ON ca.CampusId = c.Id
|
||
LEFT JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE ${isUUID ? 'ca.entraUserId' : 'ca.id'} = ?
|
||
LIMIT 1
|
||
`;
|
||
|
||
const [userRows] = await conn.query(userQuery, [userIdParam]);
|
||
|
||
if (!userRows || userRows.length === 0) {
|
||
conn.release();
|
||
return res.json({ success: false, message: 'Collaborateur non trouvé' });
|
||
}
|
||
|
||
const userInfo = userRows[0];
|
||
const serviceId = userInfo.ServiceId;
|
||
const campusId = userInfo.CampusId;
|
||
const societeId = userInfo.SocieteId;
|
||
const userEmail = userInfo.email;
|
||
const campusNom = userInfo.campusNom;
|
||
|
||
function normalizeRole(role) {
|
||
if (!role) return null;
|
||
const roleLower = role.toLowerCase();
|
||
if (roleLower === 'collaboratrice') return 'collaborateur';
|
||
if (roleLower === 'validatrice') return 'validateur';
|
||
if (roleLower === 'directrice de campus') return 'directeur de campus';
|
||
if (roleLower === 'apprentie') return 'apprenti';
|
||
return roleLower;
|
||
}
|
||
|
||
const roleOriginal = roleParam?.toLowerCase();
|
||
const role = normalizeRole(roleOriginal);
|
||
|
||
console.log(`👤 Utilisateur trouvé:`);
|
||
console.log(` - ID: ${userInfo.id}`);
|
||
console.log(` - Email: ${userEmail}`);
|
||
console.log(` - ServiceId: ${serviceId}`);
|
||
console.log(` - CampusId: ${campusId}`);
|
||
console.log(` - CampusNom: ${campusNom}`);
|
||
console.log(` - SocieteId: ${societeId}`);
|
||
console.log(` - Role normalisé: ${role}`);
|
||
|
||
let query, params;
|
||
const filters = {};
|
||
|
||
// ========================================
|
||
// CAS 1: PRESIDENT, ADMIN, RH
|
||
// ========================================
|
||
if (role === 'president' || role === 'admin' || role === 'rh') {
|
||
console.log("CAS 1: President/Admin/RH - Vue globale");
|
||
|
||
const [campusList] = await conn.query(`SELECT DISTINCT Nom FROM Campus ORDER BY Nom`);
|
||
filters.campus = campusList.map(c => c.Nom);
|
||
|
||
const [societesList] = await conn.query(`SELECT DISTINCT Nom FROM Societe ORDER BY Nom`);
|
||
filters.societes = societesList.map(s => s.Nom);
|
||
|
||
const [servicesList] = await conn.query(`SELECT DISTINCT Nom FROM Services ORDER BY Nom`);
|
||
filters.services = servicesList.map(s => s.Nom);
|
||
|
||
const [employeesList] = await conn.query(`
|
||
SELECT DISTINCT
|
||
CONCAT(ca.prenom, ' ', ca.nom) AS fullname,
|
||
c.Nom AS campusnom,
|
||
so.Nom AS societenom,
|
||
s.Nom AS servicenom
|
||
FROM CollaborateurAD ca
|
||
JOIN Services s ON ca.ServiceId = s.Id
|
||
JOIN Campus c ON ca.CampusId = c.Id
|
||
JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE (ca.actif = 1 OR ca.actif IS NULL)
|
||
ORDER BY ca.prenom, ca.nom
|
||
`);
|
||
|
||
filters.employees = employeesList.map(e => ({
|
||
name: e.fullname,
|
||
campus: e.campusnom,
|
||
societe: e.societenom,
|
||
service: e.servicenom
|
||
}));
|
||
|
||
query = `
|
||
SELECT
|
||
DATE_FORMAT(dc.DateDebut, '%Y-%m-%d') AS startdate,
|
||
DATE_FORMAT(dc.DateFin, '%Y-%m-%d') AS enddate,
|
||
CONCAT(ca.prenom, ' ', ca.nom) AS employeename,
|
||
GROUP_CONCAT(DISTINCT tc.Nom ORDER BY tc.Nom SEPARATOR ', ') AS type,
|
||
CONCAT(
|
||
'[',
|
||
GROUP_CONCAT(
|
||
JSON_OBJECT(
|
||
'type', tc.Nom,
|
||
'jours', dct.NombreJours,
|
||
'periode', COALESCE(dct.PeriodeJournee, 'Journée entière')
|
||
)
|
||
SEPARATOR ','
|
||
),
|
||
']'
|
||
) AS detailsconges,
|
||
MAX(tc.CouleurHex) AS color,
|
||
dc.Statut AS statut,
|
||
s.Nom AS servicenom,
|
||
c.Nom AS campusnom,
|
||
so.Nom AS societenom,
|
||
dc.NombreJours AS nombrejoursouvres
|
||
FROM DemandeConge dc
|
||
JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id
|
||
LEFT JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId
|
||
LEFT JOIN TypeConge tc ON dct.TypeCongeId = tc.Id
|
||
JOIN Services s ON ca.ServiceId = s.Id
|
||
JOIN Campus c ON ca.CampusId = c.Id
|
||
JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente')
|
||
GROUP BY dc.Id, dc.DateDebut, dc.DateFin, ca.prenom, ca.nom, dc.Statut, s.Nom, c.Nom, so.Nom, dc.NombreJours
|
||
ORDER BY c.Nom, dc.DateDebut ASC
|
||
`;
|
||
params = [];
|
||
}
|
||
|
||
// ========================================
|
||
// CAS 2: DIRECTEUR/DIRECTRICE DE CAMPUS
|
||
// ========================================
|
||
else if (role === 'directeur de campus' || role === 'directrice de campus') {
|
||
console.log("CAS 2: Directeur de campus");
|
||
console.log(` Campus: ${campusNom} (ID: ${campusId})`);
|
||
console.log(` Filtres reçus: Société=${selectedSociete}, Service=${selectedService}`);
|
||
|
||
filters.societes = ['Ensup', 'Ensup Solution et Support'];
|
||
|
||
let servicesQuery;
|
||
let servicesParams = [];
|
||
|
||
if (selectedSociete === 'Ensup Solution et Support') {
|
||
servicesQuery = `
|
||
SELECT DISTINCT s.Nom
|
||
FROM Services s
|
||
JOIN CollaborateurAD ca ON ca.ServiceId = s.Id
|
||
WHERE ca.SocieteId = 1
|
||
AND (ca.actif = 1 OR ca.actif IS NULL)
|
||
ORDER BY s.Nom
|
||
`;
|
||
servicesParams = [];
|
||
} else if (selectedSociete === 'Ensup') {
|
||
servicesQuery = `
|
||
SELECT DISTINCT s.Nom
|
||
FROM Services s
|
||
JOIN CollaborateurAD ca ON ca.ServiceId = s.Id
|
||
WHERE ca.SocieteId = 2
|
||
AND ca.CampusId = ?
|
||
AND (ca.actif = 1 OR ca.actif IS NULL)
|
||
ORDER BY s.Nom
|
||
`;
|
||
servicesParams = [campusId];
|
||
} else {
|
||
servicesQuery = `
|
||
SELECT DISTINCT s.Nom
|
||
FROM Services s
|
||
JOIN CollaborateurAD ca ON ca.ServiceId = s.Id
|
||
WHERE (
|
||
(ca.SocieteId = 2 AND ca.CampusId = ?)
|
||
OR (ca.SocieteId = 1)
|
||
)
|
||
AND (ca.actif = 1 OR ca.actif IS NULL)
|
||
ORDER BY s.Nom
|
||
`;
|
||
servicesParams = [campusId];
|
||
}
|
||
|
||
const [servicesList] = await conn.query(servicesQuery, servicesParams);
|
||
filters.services = servicesList.map(s => s.Nom);
|
||
console.log(`📊 Services trouvés:`, filters.services.length, filters.services);
|
||
|
||
let employeesQuery;
|
||
let employeesParams = [];
|
||
|
||
if (selectedSociete === 'Ensup Solution et Support') {
|
||
if (selectedService && selectedService !== 'all') {
|
||
employeesQuery = `
|
||
SELECT DISTINCT
|
||
CONCAT(ca.prenom, ' ', ca.nom) AS fullname,
|
||
COALESCE(c.Nom, 'Multi-campus') AS campusnom,
|
||
so.Nom AS societenom,
|
||
s.Nom AS servicenom
|
||
FROM CollaborateurAD ca
|
||
JOIN Services s ON ca.ServiceId = s.Id
|
||
LEFT JOIN Campus c ON ca.CampusId = c.Id
|
||
JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE ca.SocieteId = 1
|
||
AND s.Nom = ?
|
||
AND (ca.actif = 1 OR ca.actif IS NULL)
|
||
ORDER BY ca.prenom, ca.nom
|
||
`;
|
||
employeesParams = [selectedService];
|
||
} else {
|
||
employeesQuery = `
|
||
SELECT DISTINCT
|
||
CONCAT(ca.prenom, ' ', ca.nom) AS fullname,
|
||
COALESCE(c.Nom, 'Multi-campus') AS campusnom,
|
||
so.Nom AS societenom,
|
||
s.Nom AS servicenom
|
||
FROM CollaborateurAD ca
|
||
JOIN Services s ON ca.ServiceId = s.Id
|
||
LEFT JOIN Campus c ON ca.CampusId = c.Id
|
||
JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE ca.SocieteId = 1
|
||
AND (ca.actif = 1 OR ca.actif IS NULL)
|
||
ORDER BY ca.prenom, ca.nom
|
||
`;
|
||
employeesParams = [];
|
||
}
|
||
} else if (selectedSociete === 'Ensup') {
|
||
if (selectedService && selectedService !== 'all') {
|
||
employeesQuery = `
|
||
SELECT DISTINCT
|
||
CONCAT(ca.prenom, ' ', ca.nom) AS fullname,
|
||
c.Nom AS campusnom,
|
||
so.Nom AS societenom,
|
||
s.Nom AS servicenom
|
||
FROM CollaborateurAD ca
|
||
JOIN Services s ON ca.ServiceId = s.Id
|
||
JOIN Campus c ON ca.CampusId = c.Id
|
||
JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE ca.SocieteId = 2
|
||
AND ca.CampusId = ?
|
||
AND s.Nom = ?
|
||
AND (ca.actif = 1 OR ca.actif IS NULL)
|
||
ORDER BY ca.prenom, ca.nom
|
||
`;
|
||
employeesParams = [campusId, selectedService];
|
||
} else {
|
||
employeesQuery = `
|
||
SELECT DISTINCT
|
||
CONCAT(ca.prenom, ' ', ca.nom) AS fullname,
|
||
c.Nom AS campusnom,
|
||
so.Nom AS societenom,
|
||
s.Nom AS servicenom
|
||
FROM CollaborateurAD ca
|
||
JOIN Services s ON ca.ServiceId = s.Id
|
||
JOIN Campus c ON ca.CampusId = c.Id
|
||
JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE ca.SocieteId = 2
|
||
AND ca.CampusId = ?
|
||
AND (ca.actif = 1 OR ca.actif IS NULL)
|
||
ORDER BY ca.prenom, ca.nom
|
||
`;
|
||
employeesParams = [campusId];
|
||
}
|
||
} else {
|
||
if (selectedService && selectedService !== 'all') {
|
||
employeesQuery = `
|
||
SELECT DISTINCT
|
||
CONCAT(ca.prenom, ' ', ca.nom) AS fullname,
|
||
COALESCE(c.Nom, 'Multi-campus') AS campusnom,
|
||
so.Nom AS societenom,
|
||
s.Nom AS servicenom
|
||
FROM CollaborateurAD ca
|
||
JOIN Services s ON ca.ServiceId = s.Id
|
||
LEFT JOIN Campus c ON ca.CampusId = c.Id
|
||
JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE (
|
||
(ca.SocieteId = 2 AND ca.CampusId = ?)
|
||
OR (ca.SocieteId = 1)
|
||
)
|
||
AND s.Nom = ?
|
||
AND (ca.actif = 1 OR ca.actif IS NULL)
|
||
ORDER BY ca.prenom, ca.nom
|
||
`;
|
||
employeesParams = [campusId, selectedService];
|
||
} else {
|
||
employeesQuery = `
|
||
SELECT DISTINCT
|
||
CONCAT(ca.prenom, ' ', ca.nom) AS fullname,
|
||
COALESCE(c.Nom, 'Multi-campus') AS campusnom,
|
||
so.Nom AS societenom,
|
||
s.Nom AS servicenom
|
||
FROM CollaborateurAD ca
|
||
JOIN Services s ON ca.ServiceId = s.Id
|
||
LEFT JOIN Campus c ON ca.CampusId = c.Id
|
||
JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE (
|
||
(ca.SocieteId = 2 AND ca.CampusId = ?)
|
||
OR (ca.SocieteId = 1)
|
||
)
|
||
AND (ca.actif = 1 OR ca.actif IS NULL)
|
||
ORDER BY ca.prenom, ca.nom
|
||
`;
|
||
employeesParams = [campusId];
|
||
}
|
||
}
|
||
|
||
const [employeesList] = await conn.query(employeesQuery, employeesParams);
|
||
filters.employees = employeesList.map(emp => ({
|
||
name: emp.fullname,
|
||
campus: emp.campusnom,
|
||
societe: emp.societenom,
|
||
service: emp.servicenom
|
||
}));
|
||
|
||
console.log(`👥 Employés trouvés:`, filters.employees.length);
|
||
|
||
let whereConditions = [`dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente')`];
|
||
let whereParams = [];
|
||
|
||
if (selectedSociete === 'Ensup Solution et Support') {
|
||
whereConditions.push(`ca.SocieteId = 1`);
|
||
} else if (selectedSociete === 'Ensup') {
|
||
whereConditions.push(`ca.SocieteId = 2 AND ca.CampusId = ?`);
|
||
whereParams.push(campusId);
|
||
} else {
|
||
whereConditions.push(`((ca.SocieteId = 2 AND ca.CampusId = ?) OR (ca.SocieteId = 1))`);
|
||
whereParams.push(campusId);
|
||
}
|
||
|
||
if (selectedService && selectedService !== 'all') {
|
||
whereConditions.push(`s.Nom = ?`);
|
||
whereParams.push(selectedService);
|
||
}
|
||
|
||
query = `
|
||
SELECT
|
||
DATE_FORMAT(dc.DateDebut, '%Y-%m-%d') AS startdate,
|
||
DATE_FORMAT(dc.DateFin, '%Y-%m-%d') AS enddate,
|
||
CONCAT(ca.prenom, ' ', ca.nom) AS employeename,
|
||
GROUP_CONCAT(DISTINCT tc.Nom ORDER BY tc.Nom SEPARATOR ', ') AS type,
|
||
CONCAT(
|
||
'[',
|
||
GROUP_CONCAT(
|
||
JSON_OBJECT(
|
||
'type', tc.Nom,
|
||
'jours', dct.NombreJours,
|
||
'periode', COALESCE(dct.PeriodeJournee, 'Journée entière')
|
||
)
|
||
SEPARATOR ','
|
||
),
|
||
']'
|
||
) AS detailsconges,
|
||
MAX(tc.CouleurHex) AS color,
|
||
dc.Statut AS statut,
|
||
s.Nom AS servicenom,
|
||
COALESCE(c.Nom, 'Multi-campus') AS campusnom,
|
||
so.Nom AS societenom,
|
||
dc.NombreJours AS nombrejoursouvres
|
||
FROM DemandeConge dc
|
||
JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id
|
||
LEFT JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId
|
||
LEFT JOIN TypeConge tc ON dct.TypeCongeId = tc.Id
|
||
JOIN Services s ON ca.ServiceId = s.Id
|
||
LEFT JOIN Campus c ON ca.CampusId = c.Id
|
||
JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE ${whereConditions.join(' AND ')}
|
||
GROUP BY dc.Id, dc.DateDebut, dc.DateFin, ca.prenom, ca.nom, dc.Statut, s.Nom, c.Nom, so.Nom, dc.NombreJours
|
||
ORDER BY so.Nom, c.Nom, dc.DateDebut ASC
|
||
`;
|
||
params = whereParams;
|
||
}
|
||
|
||
// ========================================
|
||
// CAS 3: COLLABORATEUR
|
||
// ========================================
|
||
else if (role === 'collaborateur') {
|
||
console.log("CAS 3: Collaborateur");
|
||
|
||
const serviceNom = userInfo.serviceNom || 'Non défini';
|
||
const accesTransversal = getUserAccesTransversal(userEmail);
|
||
const isServiceMultiCampus = [
|
||
"Administratif & Financier",
|
||
"IT"
|
||
].includes(serviceNom);
|
||
|
||
if (accesTransversal) {
|
||
console.log(`🌐 Accès transversal détecté:`, accesTransversal);
|
||
|
||
if (accesTransversal.typeAcces === 'service_multi_campus') {
|
||
filters.societes = [];
|
||
filters.campus = [];
|
||
filters.services = [];
|
||
|
||
const [employeesList] = await conn.query(`
|
||
SELECT DISTINCT
|
||
CONCAT(ca.prenom, ' ', ca.nom) AS fullname,
|
||
COALESCE(c.Nom, 'Multi-campus') AS campusnom,
|
||
so.Nom AS societenom,
|
||
s.Nom AS servicenom
|
||
FROM CollaborateurAD ca
|
||
JOIN Services s ON ca.ServiceId = s.Id
|
||
LEFT JOIN Campus c ON ca.CampusId = c.Id
|
||
JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE s.Nom = ?
|
||
AND (ca.actif = 1 OR ca.actif IS NULL)
|
||
ORDER BY c.Nom, ca.prenom, ca.nom
|
||
`, [accesTransversal.serviceNom]);
|
||
|
||
filters.employees = employeesList.map(emp => ({
|
||
name: emp.fullname,
|
||
campus: emp.campusnom,
|
||
societe: emp.societenom,
|
||
service: emp.servicenom
|
||
}));
|
||
|
||
query = `
|
||
SELECT
|
||
DATE_FORMAT(dc.DateDebut, '%Y-%m-%d') AS startdate,
|
||
DATE_FORMAT(dc.DateFin, '%Y-%m-%d') AS enddate,
|
||
CONCAT(ca.prenom, ' ', ca.nom) AS employeename,
|
||
GROUP_CONCAT(DISTINCT tc.Nom ORDER BY tc.Nom SEPARATOR ', ') AS type,
|
||
CONCAT(
|
||
'[',
|
||
GROUP_CONCAT(
|
||
JSON_OBJECT(
|
||
'type', tc.Nom,
|
||
'jours', dct.NombreJours,
|
||
'periode', COALESCE(dct.PeriodeJournee, 'Journée entière')
|
||
)
|
||
SEPARATOR ','
|
||
),
|
||
']'
|
||
) AS detailsconges,
|
||
MAX(tc.CouleurHex) AS color,
|
||
dc.Statut AS statut,
|
||
s.Nom AS servicenom,
|
||
COALESCE(c.Nom, 'Multi-campus') AS campusnom,
|
||
so.Nom AS societenom,
|
||
dc.NombreJours AS nombrejoursouvres
|
||
FROM DemandeConge dc
|
||
JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id
|
||
LEFT JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId
|
||
LEFT JOIN TypeConge tc ON dct.TypeCongeId = tc.Id
|
||
JOIN Services s ON ca.ServiceId = s.Id
|
||
LEFT JOIN Campus c ON ca.CampusId = c.Id
|
||
JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE s.Nom = ?
|
||
AND dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente')
|
||
GROUP BY dc.Id, dc.DateDebut, dc.DateFin, ca.prenom, ca.nom, dc.Statut, s.Nom, c.Nom, so.Nom, dc.NombreJours
|
||
ORDER BY c.Nom, dc.DateDebut ASC
|
||
`;
|
||
params = [accesTransversal.serviceNom];
|
||
|
||
} else if (accesTransversal.typeAcces === 'service_multi_campus_avec_vue_complete') {
|
||
filters.societes = [];
|
||
filters.campus = [];
|
||
filters.services = [];
|
||
|
||
const [employeesList] = await conn.query(`
|
||
SELECT DISTINCT
|
||
CONCAT(ca.prenom, ' ', ca.nom) AS fullname,
|
||
COALESCE(c.Nom, 'Multi-campus') AS campusnom,
|
||
so.Nom AS societenom,
|
||
s.Nom AS servicenom
|
||
FROM CollaborateurAD ca
|
||
JOIN Services s ON ca.ServiceId = s.Id
|
||
LEFT JOIN Campus c ON ca.CampusId = c.Id
|
||
JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE s.Nom = ?
|
||
AND (ca.actif = 1 OR ca.actif IS NULL)
|
||
ORDER BY so.Nom, c.Nom, ca.prenom, ca.nom
|
||
`, [accesTransversal.serviceNom]);
|
||
|
||
filters.employees = employeesList.map(emp => ({
|
||
name: emp.fullname,
|
||
campus: emp.campusnom,
|
||
societe: emp.societenom,
|
||
service: emp.servicenom
|
||
}));
|
||
|
||
query = `
|
||
SELECT
|
||
DATE_FORMAT(dc.DateDebut, '%Y-%m-%d') AS startdate,
|
||
DATE_FORMAT(dc.DateFin, '%Y-%m-%d') AS enddate,
|
||
CONCAT(ca.prenom, ' ', ca.nom) AS employeename,
|
||
GROUP_CONCAT(DISTINCT tc.Nom ORDER BY tc.Nom SEPARATOR ', ') AS type,
|
||
CONCAT(
|
||
'[',
|
||
GROUP_CONCAT(
|
||
JSON_OBJECT(
|
||
'type', tc.Nom,
|
||
'jours', dct.NombreJours,
|
||
'periode', COALESCE(dct.PeriodeJournee, 'Journée entière')
|
||
)
|
||
SEPARATOR ','
|
||
),
|
||
']'
|
||
) AS detailsconges,
|
||
MAX(tc.CouleurHex) AS color,
|
||
dc.Statut AS statut,
|
||
s.Nom AS servicenom,
|
||
COALESCE(c.Nom, 'Multi-campus') AS campusnom,
|
||
so.Nom AS societenom,
|
||
dc.NombreJours AS nombrejoursouvres
|
||
FROM DemandeConge dc
|
||
JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id
|
||
LEFT JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId
|
||
LEFT JOIN TypeConge tc ON dct.TypeCongeId = tc.Id
|
||
JOIN Services s ON ca.ServiceId = s.Id
|
||
LEFT JOIN Campus c ON ca.CampusId = c.Id
|
||
JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE s.Nom = ?
|
||
AND dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente')
|
||
GROUP BY dc.Id, dc.DateDebut, dc.DateFin, ca.prenom, ca.nom, dc.Statut, s.Nom, c.Nom, so.Nom, dc.NombreJours
|
||
ORDER BY so.Nom, c.Nom, dc.DateDebut ASC
|
||
`;
|
||
params = [accesTransversal.serviceNom];
|
||
}
|
||
|
||
} else if (isServiceMultiCampus) {
|
||
filters.societes = [];
|
||
filters.campus = [];
|
||
filters.services = [];
|
||
|
||
const [employeesList] = await conn.query(`
|
||
SELECT DISTINCT
|
||
CONCAT(ca.prenom, ' ', ca.nom) AS fullname,
|
||
COALESCE(c.Nom, 'Multi-campus') AS campusnom,
|
||
so.Nom AS societenom,
|
||
s.Nom AS servicenom
|
||
FROM CollaborateurAD ca
|
||
JOIN Services s ON ca.ServiceId = s.Id
|
||
LEFT JOIN Campus c ON ca.CampusId = c.Id
|
||
JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE s.Nom = ?
|
||
AND (ca.actif = 1 OR ca.actif IS NULL)
|
||
ORDER BY c.Nom, ca.prenom, ca.nom
|
||
`, [serviceNom]);
|
||
|
||
filters.employees = employeesList.map(emp => ({
|
||
name: emp.fullname,
|
||
campus: emp.campusnom,
|
||
societe: emp.societenom,
|
||
service: emp.servicenom
|
||
}));
|
||
|
||
query = `
|
||
SELECT
|
||
DATE_FORMAT(dc.DateDebut, '%Y-%m-%d') AS startdate,
|
||
DATE_FORMAT(dc.DateFin, '%Y-%m-%d') AS enddate,
|
||
CONCAT(ca.prenom, ' ', ca.nom) AS employeename,
|
||
GROUP_CONCAT(DISTINCT tc.Nom ORDER BY tc.Nom SEPARATOR ', ') AS type,
|
||
CONCAT(
|
||
'[',
|
||
GROUP_CONCAT(
|
||
JSON_OBJECT(
|
||
'type', tc.Nom,
|
||
'jours', dct.NombreJours,
|
||
'periode', COALESCE(dct.PeriodeJournee, 'Journée entière')
|
||
)
|
||
SEPARATOR ','
|
||
),
|
||
']'
|
||
) AS detailsconges,
|
||
MAX(tc.CouleurHex) AS color,
|
||
dc.Statut AS statut,
|
||
s.Nom AS servicenom,
|
||
COALESCE(c.Nom, 'Multi-campus') AS campusnom,
|
||
so.Nom AS societenom,
|
||
dc.NombreJours AS nombrejoursouvres
|
||
FROM DemandeConge dc
|
||
JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id
|
||
LEFT JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId
|
||
LEFT JOIN TypeConge tc ON dct.TypeCongeId = tc.Id
|
||
JOIN Services s ON ca.ServiceId = s.Id
|
||
LEFT JOIN Campus c ON ca.CampusId = c.Id
|
||
JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE s.Nom = ?
|
||
AND dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente')
|
||
GROUP BY dc.Id, dc.DateDebut, dc.DateFin, ca.prenom, ca.nom, dc.Statut, s.Nom, c.Nom, so.Nom, dc.NombreJours
|
||
ORDER BY c.Nom, dc.DateDebut ASC
|
||
`;
|
||
params = [serviceNom];
|
||
} else {
|
||
filters.societes = [];
|
||
filters.campus = [];
|
||
filters.services = [];
|
||
|
||
const [employeesList] = await conn.query(`
|
||
SELECT DISTINCT
|
||
CONCAT(ca.prenom, ' ', ca.nom) AS fullname,
|
||
c.Nom AS campusnom,
|
||
so.Nom AS societenom,
|
||
s.Nom AS servicenom
|
||
FROM CollaborateurAD ca
|
||
JOIN Services s ON ca.ServiceId = s.Id
|
||
LEFT JOIN Campus c ON ca.CampusId = c.Id
|
||
JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE ca.ServiceId = ?
|
||
AND (ca.CampusId = ? OR ca.CampusId IS NULL)
|
||
AND (ca.actif = 1 OR ca.actif IS NULL)
|
||
ORDER BY ca.prenom, ca.nom
|
||
`, [serviceId, campusId]);
|
||
|
||
filters.employees = employeesList.map(emp => ({
|
||
name: emp.fullname,
|
||
campus: emp.campusnom,
|
||
societe: emp.societenom,
|
||
service: emp.servicenom
|
||
}));
|
||
|
||
query = `
|
||
SELECT
|
||
DATE_FORMAT(dc.DateDebut, '%Y-%m-%d') AS startdate,
|
||
DATE_FORMAT(dc.DateFin, '%Y-%m-%d') AS enddate,
|
||
CONCAT(ca.prenom, ' ', ca.nom) AS employeename,
|
||
GROUP_CONCAT(DISTINCT tc.Nom ORDER BY tc.Nom SEPARATOR ', ') AS type,
|
||
CONCAT(
|
||
'[',
|
||
GROUP_CONCAT(
|
||
JSON_OBJECT(
|
||
'type', tc.Nom,
|
||
'jours', dct.NombreJours,
|
||
'periode', COALESCE(dct.PeriodeJournee, 'Journée entière')
|
||
)
|
||
SEPARATOR ','
|
||
),
|
||
']'
|
||
) AS detailsconges,
|
||
MAX(tc.CouleurHex) AS color,
|
||
dc.Statut AS statut,
|
||
s.Nom AS servicenom,
|
||
c.Nom AS campusnom,
|
||
so.Nom AS societenom,
|
||
dc.NombreJours AS nombrejoursouvres
|
||
FROM DemandeConge dc
|
||
JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id
|
||
LEFT JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId
|
||
LEFT JOIN TypeConge tc ON dct.TypeCongeId = tc.Id
|
||
JOIN Services s ON ca.ServiceId = s.Id
|
||
LEFT JOIN Campus c ON ca.CampusId = c.Id
|
||
JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE ca.ServiceId = ?
|
||
AND (ca.CampusId = ? OR ca.CampusId IS NULL)
|
||
AND dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente')
|
||
GROUP BY dc.Id, dc.DateDebut, dc.DateFin, ca.prenom, ca.nom, dc.Statut, s.Nom, c.Nom, so.Nom, dc.NombreJours
|
||
ORDER BY dc.DateDebut ASC
|
||
`;
|
||
params = [serviceId, campusId];
|
||
}
|
||
}
|
||
|
||
// ========================================
|
||
// CAS 4: AUTRES RÔLES
|
||
// ========================================
|
||
else {
|
||
console.log("CAS 4: Autres rôles");
|
||
|
||
if (!serviceId) {
|
||
conn.release();
|
||
return res.json({
|
||
success: false,
|
||
message: 'ServiceId manquant'
|
||
});
|
||
}
|
||
|
||
const [checkService] = await conn.query(`SELECT Nom FROM Services WHERE Id = ?`, [serviceId]);
|
||
const serviceNom = checkService.length > 0 ? checkService[0].Nom : "Inconnu";
|
||
const isAdminFinancier = serviceNom === "Administratif & Financier";
|
||
|
||
if (isAdminFinancier) {
|
||
const [employeesList] = await conn.query(`
|
||
SELECT DISTINCT
|
||
CONCAT(ca.prenom, ' ', ca.nom) AS fullname,
|
||
c.Nom AS campusnom,
|
||
so.Nom AS societenom,
|
||
s.Nom AS servicenom
|
||
FROM CollaborateurAD ca
|
||
JOIN Services s ON ca.ServiceId = s.Id
|
||
JOIN Campus c ON ca.CampusId = c.Id
|
||
JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE ca.ServiceId = ?
|
||
AND (ca.actif = 1 OR ca.actif IS NULL)
|
||
ORDER BY ca.prenom, ca.nom
|
||
`, [serviceId]);
|
||
|
||
filters.employees = employeesList.map(e => ({
|
||
name: e.fullname,
|
||
campus: e.campusnom,
|
||
societe: e.societenom,
|
||
service: e.servicenom
|
||
}));
|
||
|
||
query = `
|
||
SELECT
|
||
DATE_FORMAT(dc.DateDebut, '%Y-%m-%d') AS startdate,
|
||
DATE_FORMAT(dc.DateFin, '%Y-%m-%d') AS enddate,
|
||
CONCAT(ca.prenom, ' ', ca.nom) AS employeename,
|
||
GROUP_CONCAT(DISTINCT tc.Nom ORDER BY tc.Nom SEPARATOR ', ') AS type,
|
||
CONCAT(
|
||
'[',
|
||
GROUP_CONCAT(
|
||
JSON_OBJECT(
|
||
'type', tc.Nom,
|
||
'jours', dct.NombreJours,
|
||
'periode', COALESCE(dct.PeriodeJournee, 'Journée entière')
|
||
)
|
||
SEPARATOR ','
|
||
),
|
||
']'
|
||
) AS detailsconges,
|
||
MAX(tc.CouleurHex) AS color,
|
||
dc.Statut AS statut,
|
||
s.Nom AS servicenom,
|
||
c.Nom AS campusnom,
|
||
so.Nom AS societenom,
|
||
dc.NombreJours AS nombrejoursouvres
|
||
FROM DemandeConge dc
|
||
JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id
|
||
LEFT JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId
|
||
LEFT JOIN TypeConge tc ON dct.TypeCongeId = tc.Id
|
||
JOIN Services s ON ca.ServiceId = s.Id
|
||
JOIN Campus c ON ca.CampusId = c.Id
|
||
JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente')
|
||
AND ca.ServiceId = ?
|
||
GROUP BY dc.Id, dc.DateDebut, dc.DateFin, ca.prenom, ca.nom, dc.Statut, s.Nom, c.Nom, so.Nom, dc.NombreJours
|
||
ORDER BY c.Nom, dc.DateDebut ASC
|
||
`;
|
||
params = [serviceId];
|
||
|
||
} else {
|
||
const [employeesList] = await conn.query(`
|
||
SELECT DISTINCT
|
||
CONCAT(ca.prenom, ' ', ca.nom) AS fullname,
|
||
c.Nom AS campusnom,
|
||
so.Nom AS societenom,
|
||
s.Nom AS servicenom
|
||
FROM CollaborateurAD ca
|
||
JOIN Services s ON ca.ServiceId = s.Id
|
||
JOIN Campus c ON ca.CampusId = c.Id
|
||
JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE ca.ServiceId = ?
|
||
AND ca.CampusId = ?
|
||
AND (ca.actif = 1 OR ca.actif IS NULL)
|
||
ORDER BY ca.prenom, ca.nom
|
||
`, [serviceId, campusId]);
|
||
|
||
filters.employees = employeesList.map(e => ({
|
||
name: e.fullname,
|
||
campus: e.campusnom,
|
||
societe: e.societenom,
|
||
service: e.servicenom
|
||
}));
|
||
|
||
query = `
|
||
SELECT
|
||
DATE_FORMAT(dc.DateDebut, '%Y-%m-%d') AS startdate,
|
||
DATE_FORMAT(dc.DateFin, '%Y-%m-%d') AS enddate,
|
||
CONCAT(ca.prenom, ' ', ca.nom) AS employeename,
|
||
GROUP_CONCAT(DISTINCT tc.Nom ORDER BY tc.Nom SEPARATOR ', ') AS type,
|
||
CONCAT(
|
||
'[',
|
||
GROUP_CONCAT(
|
||
JSON_OBJECT(
|
||
'type', tc.Nom,
|
||
'jours', dct.NombreJours,
|
||
'periode', COALESCE(dct.PeriodeJournee, 'Journée entière')
|
||
)
|
||
SEPARATOR ','
|
||
),
|
||
']'
|
||
) AS detailsconges,
|
||
MAX(tc.CouleurHex) AS color,
|
||
dc.Statut AS statut,
|
||
s.Nom AS servicenom,
|
||
c.Nom AS campusnom,
|
||
so.Nom AS societenom,
|
||
dc.NombreJours AS nombrejoursouvres
|
||
FROM DemandeConge dc
|
||
JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id
|
||
LEFT JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId
|
||
LEFT JOIN TypeConge tc ON dct.TypeCongeId = tc.Id
|
||
JOIN Services s ON ca.ServiceId = s.Id
|
||
JOIN Campus c ON ca.CampusId = c.Id
|
||
JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente')
|
||
AND ca.ServiceId = ?
|
||
AND ca.CampusId = ?
|
||
GROUP BY dc.Id, dc.DateDebut, dc.DateFin, ca.prenom, ca.nom, dc.Statut, s.Nom, c.Nom, so.Nom, dc.NombreJours
|
||
ORDER BY c.Nom, dc.DateDebut ASC
|
||
`;
|
||
params = [serviceId, campusId];
|
||
}
|
||
}
|
||
|
||
const [leavesRows] = await conn.query(query, params);
|
||
|
||
const formattedLeaves = leavesRows.map(leave => ({
|
||
...leave
|
||
}));
|
||
|
||
console.log(`✅ ${formattedLeaves.length} congés trouvés`);
|
||
|
||
if (formattedLeaves.length === 0 && (role === 'collaborateur' || role === 'collaboratrice')) {
|
||
console.log('🔍 DEBUG: Aucun congé trouvé, vérification...');
|
||
|
||
const [debugLeaves] = await conn.query(`
|
||
SELECT COUNT(*) as total
|
||
FROM DemandeConge dc
|
||
JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id
|
||
JOIN Services s ON ca.ServiceId = s.Id
|
||
WHERE s.Nom = ?
|
||
AND dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente')
|
||
`, [userInfo.serviceNom]);
|
||
|
||
console.log(`🔍 Total congés dans le service "${userInfo.serviceNom}":`, debugLeaves[0].total);
|
||
|
||
const [debugCollabs] = await conn.query(`
|
||
SELECT ca.id, ca.prenom, ca.nom, ca.email, ca.ServiceId, s.Nom as ServiceNom
|
||
FROM CollaborateurAD ca
|
||
JOIN Services s ON ca.ServiceId = s.Id
|
||
WHERE s.Nom = ?
|
||
`, [userInfo.serviceNom]);
|
||
|
||
console.log(`🔍 Collaborateurs dans "${userInfo.serviceNom}":`, debugCollabs);
|
||
}
|
||
|
||
console.log(`✅ Filtres:`, {
|
||
campus: filters.campus?.length || 0,
|
||
societes: filters.societes?.length || 0,
|
||
services: filters.services?.length || 0,
|
||
employees: filters.employees?.length || 0
|
||
});
|
||
|
||
if (formattedLeaves.length > 0) {
|
||
console.log('📝 Exemple de congé formaté:', formattedLeaves[0]);
|
||
}
|
||
|
||
conn.release();
|
||
|
||
res.json({
|
||
success: true,
|
||
role: role,
|
||
leaves: formattedLeaves,
|
||
filters: filters
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error("❌ Erreur getTeamLeaves:", error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: "Erreur serveur",
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
|
||
|
||
app.post('/initial-sync', async (req, res) => {
|
||
try {
|
||
const accessToken = await getGraphToken();
|
||
if (!accessToken) return res.json({ success: false, message: 'Impossible obtenir token' });
|
||
|
||
if (req.body.userPrincipalName || req.body.mail) {
|
||
const userEmail = req.body.mail || req.body.userPrincipalName;
|
||
const entraUserId = req.body.id;
|
||
|
||
console.log(`🔄 Synchronisation utilisateur: ${userEmail}`);
|
||
|
||
// ⭐ Insertion avec SocieteId (ajuster selon votre logique)
|
||
await pool.query(`
|
||
INSERT INTO CollaborateurAD
|
||
(entraUserId, prenom, nom, email, service, description, role, SocieteId)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||
ON DUPLICATE KEY UPDATE
|
||
prenom=?, nom=?, email=?, service=?, description=?
|
||
`, [
|
||
entraUserId,
|
||
req.body.givenName,
|
||
req.body.surname,
|
||
userEmail,
|
||
req.body.department,
|
||
req.body.jobTitle,
|
||
'Collaborateur',
|
||
null, // ⭐ À ajuster selon votre logique métier
|
||
req.body.givenName,
|
||
req.body.surname,
|
||
userEmail,
|
||
req.body.department,
|
||
req.body.jobTitle
|
||
]);
|
||
|
||
const [userRows] = await pool.query(`
|
||
SELECT
|
||
ca.id as localUserId,
|
||
ca.entraUserId,
|
||
ca.prenom,
|
||
ca.nom,
|
||
ca.email,
|
||
ca.role,
|
||
s.Nom as service,
|
||
ca.TypeContrat as typeContrat,
|
||
ca.DateEntree as dateEntree,
|
||
ca.description,
|
||
ca.CampusId,
|
||
ca.SocieteId,
|
||
so.Nom as societe_nom
|
||
FROM CollaborateurAD ca
|
||
LEFT JOIN Services s ON ca.ServiceId = s.Id
|
||
LEFT JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE ca.email = ?
|
||
`, [userEmail]);
|
||
|
||
if (userRows.length === 0) {
|
||
return res.json({
|
||
success: false,
|
||
message: 'Utilisateur synchronisé mais introuvable en BDD'
|
||
});
|
||
}
|
||
|
||
const userData = userRows[0];
|
||
|
||
console.log(`✅ Utilisateur synchronisé:`, userData);
|
||
|
||
return res.json({
|
||
success: true,
|
||
message: 'Utilisateur synchronisé',
|
||
localUserId: userData.localUserId,
|
||
role: userData.role,
|
||
service: userData.service,
|
||
typeContrat: userData.typeContrat,
|
||
dateEntree: userData.dateEntree,
|
||
societeId: userData.SocieteId,
|
||
societeNom: userData.societe_nom,
|
||
user: userData
|
||
});
|
||
}
|
||
|
||
// Full sync
|
||
console.log('🔄 Full sync de tous les membres du groupe...');
|
||
|
||
const groupResponse = await axios.get(
|
||
`https://graph.microsoft.com/v1.0/groups/${AZURE_CONFIG.groupId}?$select=id,displayName`,
|
||
{ headers: { Authorization: `Bearer ${accessToken}` } }
|
||
);
|
||
const group = groupResponse.data;
|
||
|
||
const membersResponse = await axios.get(
|
||
`https://graph.microsoft.com/v1.0/groups/${AZURE_CONFIG.groupId}/members?$select=id,givenName,surname,mail,department,jobTitle`,
|
||
{ headers: { Authorization: `Bearer ${accessToken}` } }
|
||
);
|
||
const members = membersResponse.data.value;
|
||
|
||
let usersInserted = 0;
|
||
for (const m of members) {
|
||
if (!m.mail) continue;
|
||
|
||
await pool.query(`
|
||
INSERT INTO CollaborateurAD (
|
||
entraUserId, prenom, nom, email, service, description, role, SocieteId
|
||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||
ON DUPLICATE KEY UPDATE
|
||
prenom=?, nom=?, email=?, service=?, description=?
|
||
`, [
|
||
m.id,
|
||
m.givenName || '',
|
||
m.surname || '',
|
||
m.mail,
|
||
m.department || '',
|
||
m.jobTitle || null,
|
||
'Collaborateur',
|
||
null, // ⭐ À ajuster selon votre logique métier
|
||
m.givenName,
|
||
m.surname,
|
||
m.mail,
|
||
m.department,
|
||
m.jobTitle
|
||
]);
|
||
|
||
usersInserted++;
|
||
}
|
||
|
||
console.log(`✅ Full sync terminée: ${usersInserted} utilisateurs`);
|
||
|
||
res.json({
|
||
success: true,
|
||
message: 'Full synchronisation terminée',
|
||
groupe_sync: group.displayName,
|
||
users_sync: usersInserted
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('❌ Erreur sync:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur sync',
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
// ========================================
|
||
// NOUVELLES ROUTES ADMINISTRATION RTT
|
||
// ========================================
|
||
|
||
app.get('/getAllCollaborateurs', async (req, res) => {
|
||
try {
|
||
const [collaborateurs] = await pool.query(`
|
||
SELECT
|
||
ca.id,
|
||
ca.prenom,
|
||
ca.nom,
|
||
ca.email,
|
||
ca.role,
|
||
ca.TypeContrat,
|
||
ca.DateEntree,
|
||
s.Nom as service,
|
||
ca.CampusId,
|
||
ca.SocieteId,
|
||
so.Nom as societe_nom
|
||
FROM CollaborateurAD ca
|
||
LEFT JOIN Services s ON ca.ServiceId = s.Id
|
||
LEFT JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE (ca.actif = 1 OR ca.actif IS NULL)
|
||
ORDER BY ca.nom, ca.prenom
|
||
`);
|
||
|
||
res.json({
|
||
success: true,
|
||
collaborateurs: collaborateurs,
|
||
total: collaborateurs.length
|
||
});
|
||
} catch (error) {
|
||
console.error('Erreur getAllCollaborateurs:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur serveur',
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
app.post('/updateTypeContrat', async (req, res) => {
|
||
try {
|
||
const { collaborateur_id, type_contrat } = req.body;
|
||
|
||
if (!collaborateur_id || !type_contrat) {
|
||
return res.json({
|
||
success: false,
|
||
message: 'Données manquantes'
|
||
});
|
||
}
|
||
|
||
const typesValides = ['37h', 'forfait_jour', 'temps_partiel'];
|
||
if (!typesValides.includes(type_contrat)) {
|
||
return res.json({
|
||
success: false,
|
||
message: 'Type de contrat invalide'
|
||
});
|
||
}
|
||
|
||
const [collab] = await pool.query(
|
||
'SELECT prenom, nom, CampusId FROM CollaborateurAD WHERE id = ?',
|
||
[collaborateur_id]
|
||
);
|
||
|
||
if (collab.length === 0) {
|
||
return res.json({
|
||
success: false,
|
||
message: 'Collaborateur non trouvé'
|
||
});
|
||
}
|
||
|
||
await pool.query(
|
||
'UPDATE CollaborateurAD SET TypeContrat = ? WHERE id = ?',
|
||
[type_contrat, collaborateur_id]
|
||
);
|
||
|
||
res.json({
|
||
success: true,
|
||
message: 'Type de contrat mis à jour',
|
||
nom: `${collab[0].prenom} ${collab[0].nom}`,
|
||
nouveau_type: type_contrat
|
||
});
|
||
} catch (error) {
|
||
console.error('Erreur updateTypeContrat:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur serveur',
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
app.get('/getConfigurationRTT', async (req, res) => {
|
||
try {
|
||
const annee = parseInt(req.query.annee || new Date().getFullYear());
|
||
const [configs] = await pool.query(
|
||
`SELECT Annee, TypeContrat, JoursAnnuels, AcquisitionMensuelle, Description
|
||
FROM ConfigurationRTT
|
||
WHERE Annee = ?
|
||
ORDER BY TypeContrat`,
|
||
[annee]
|
||
);
|
||
res.json({ success: true, configs });
|
||
} catch (error) {
|
||
res.status(500).json({ success: false, message: 'Erreur', error: error.message });
|
||
}
|
||
});
|
||
|
||
app.post('/updateConfigurationRTT', async (req, res) => {
|
||
try {
|
||
const { annee, typeContrat, joursAnnuels } = req.body;
|
||
if (!annee || !typeContrat || !joursAnnuels) {
|
||
return res.json({ success: false, message: 'Données manquantes' });
|
||
}
|
||
|
||
const acquisitionMensuelle = joursAnnuels / 12;
|
||
|
||
await pool.query(
|
||
`INSERT INTO ConfigurationRTT (Annee, TypeContrat, JoursAnnuels, AcquisitionMensuelle)
|
||
VALUES (?, ?, ?, ?)
|
||
ON DUPLICATE KEY UPDATE
|
||
JoursAnnuels = ?, AcquisitionMensuelle = ?`,
|
||
[annee, typeContrat, joursAnnuels, acquisitionMensuelle, joursAnnuels, acquisitionMensuelle]
|
||
);
|
||
|
||
res.json({ success: true, message: 'Configuration mise à jour' });
|
||
} catch (error) {
|
||
res.status(500).json({ success: false, message: 'Erreur', error: error.message });
|
||
}
|
||
});
|
||
|
||
app.post('/updateRequest', async (req, res) => {
|
||
const conn = await pool.getConnection();
|
||
|
||
try {
|
||
await conn.beginTransaction();
|
||
|
||
const {
|
||
requestId,
|
||
leaveType,
|
||
startDate,
|
||
endDate,
|
||
reason,
|
||
businessDays,
|
||
userId,
|
||
userEmail,
|
||
userName,
|
||
accessToken,
|
||
repartition // ⭐ NOUVEAU - Ajout de la répartition
|
||
} = req.body;
|
||
|
||
console.log('\n📝 === MODIFICATION DEMANDE ===');
|
||
console.log('Demande ID:', requestId);
|
||
console.log('Utilisateur:', userName);
|
||
console.log('Nouvelle répartition:', repartition);
|
||
|
||
// 1. Vérifier que la demande existe et est "En attente"
|
||
const [existingRequest] = await conn.query(
|
||
'SELECT * FROM DemandeConge WHERE Id = ? AND CollaborateurADId = ?',
|
||
[requestId, userId]
|
||
);
|
||
|
||
if (existingRequest.length === 0) {
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.status(404).json({
|
||
success: false,
|
||
message: 'Demande introuvable'
|
||
});
|
||
}
|
||
|
||
const request = existingRequest[0];
|
||
|
||
if (request.Statut !== 'En attente') {
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.status(400).json({
|
||
success: false,
|
||
message: 'Vous ne pouvez modifier que les demandes en attente'
|
||
});
|
||
}
|
||
|
||
// 2. ⭐ RESTAURER LES ANCIENS COMPTEURS
|
||
console.log('\n🔄 ÉTAPE 1: Restauration des anciens compteurs...');
|
||
try {
|
||
const restoration = await restoreLeaveBalance(conn, requestId, userId);
|
||
console.log('✅ Compteurs restaurés:', restoration);
|
||
} catch (restoreError) {
|
||
console.error('❌ Erreur restauration:', restoreError);
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur lors de la restauration des compteurs',
|
||
error: restoreError.message
|
||
});
|
||
}
|
||
|
||
// 3. ⭐ SUPPRIMER LES ANCIENNES DÉDUCTIONS
|
||
console.log('\n🗑️ ÉTAPE 2: Suppression des anciennes déductions...');
|
||
await conn.query('DELETE FROM DeductionDetails WHERE DemandeCongeId = ?', [requestId]);
|
||
await conn.query('DELETE FROM DemandeCongeType WHERE DemandeCongeId = ?', [requestId]);
|
||
|
||
// 4. METTRE À JOUR LA DEMANDE
|
||
console.log('\n📝 ÉTAPE 3: Mise à jour de la demande...');
|
||
await conn.query(
|
||
`UPDATE DemandeConge
|
||
SET DateDebut = ?,
|
||
DateFin = ?,
|
||
Commentaire = ?,
|
||
NombreJours = ?
|
||
WHERE Id = ?`,
|
||
[startDate, endDate, reason || null, businessDays, requestId]
|
||
);
|
||
|
||
// 5. ⭐ SAUVEGARDER LA NOUVELLE RÉPARTITION
|
||
console.log('\n📊 ÉTAPE 4: Sauvegarde de la nouvelle répartition...');
|
||
if (repartition && repartition.length > 0) {
|
||
for (const rep of repartition) {
|
||
const code = rep.TypeConge;
|
||
const name = code === 'CP' ? 'Congé payé' :
|
||
code === 'RTT' ? 'RTT' :
|
||
code === 'ABS' ? 'Congé maladie' :
|
||
code === 'Formation' ? 'Formation' :
|
||
code === 'Récup' ? 'Récupération' : code;
|
||
|
||
const [typeRow] = await conn.query(
|
||
'SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1',
|
||
[name]
|
||
);
|
||
|
||
if (typeRow.length > 0) {
|
||
await conn.query(
|
||
`INSERT INTO DemandeCongeType
|
||
(DemandeCongeId, TypeCongeId, NombreJours, PeriodeJournee)
|
||
VALUES (?, ?, ?, ?)`,
|
||
[
|
||
requestId,
|
||
typeRow[0].Id,
|
||
rep.NombreJours,
|
||
rep.PeriodeJournee || 'Journée entière'
|
||
]
|
||
);
|
||
console.log(` ✓ ${name}: ${rep.NombreJours}j`);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 6. ⭐ DÉDUIRE LES NOUVEAUX COMPTEURS
|
||
console.log('\n📉 ÉTAPE 5: Déduction des nouveaux compteurs...');
|
||
const currentYear = new Date().getFullYear();
|
||
|
||
for (const rep of repartition) {
|
||
if (rep.TypeConge === 'ABS' || rep.TypeConge === 'Formation') {
|
||
console.log(` ⏩ ${rep.TypeConge} ignoré (pas de déduction)`);
|
||
continue;
|
||
}
|
||
|
||
// Récup: ACCUMULATION
|
||
if (rep.TypeConge === 'Récup') {
|
||
const [recupType] = await conn.query(
|
||
'SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1',
|
||
['Récupération']
|
||
);
|
||
|
||
if (recupType.length > 0) {
|
||
const recupJours = rep.NombreJours;
|
||
const [compteurExisting] = await conn.query(`
|
||
SELECT Id, Total, Solde FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [userId, recupType[0].Id, currentYear]);
|
||
|
||
if (compteurExisting.length > 0) {
|
||
await conn.query(`
|
||
UPDATE CompteurConges
|
||
SET Total = Total + ?,
|
||
Solde = Solde + ?,
|
||
DerniereMiseAJour = NOW()
|
||
WHERE Id = ?
|
||
`, [recupJours, recupJours, compteurExisting[0].Id]);
|
||
} else {
|
||
await conn.query(`
|
||
INSERT INTO CompteurConges
|
||
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour)
|
||
VALUES (?, ?, ?, ?, ?, 0, NOW())
|
||
`, [userId, recupType[0].Id, currentYear, recupJours, recupJours]);
|
||
}
|
||
|
||
await conn.query(`
|
||
INSERT INTO DeductionDetails
|
||
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
|
||
VALUES (?, ?, ?, 'Accum Récup', ?)
|
||
`, [requestId, recupType[0].Id, currentYear, recupJours]);
|
||
|
||
console.log(` ✓ Récup: +${recupJours}j accumulés`);
|
||
}
|
||
continue;
|
||
}
|
||
|
||
// CP et RTT: DÉDUCTION
|
||
const name = rep.TypeConge === 'CP' ? 'Congé payé' : 'RTT';
|
||
const [typeRow] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', [name]);
|
||
|
||
if (typeRow.length > 0) {
|
||
const result = await deductLeaveBalanceWithTracking(
|
||
conn,
|
||
userId,
|
||
typeRow[0].Id,
|
||
rep.NombreJours,
|
||
requestId
|
||
);
|
||
|
||
console.log(` ✓ ${name}: ${rep.NombreJours}j déduits`);
|
||
if (result.details && result.details.length > 0) {
|
||
result.details.forEach(d => {
|
||
console.log(` - ${d.type} (${d.annee}): ${d.joursUtilises}j`);
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
// 7. Récupérer les infos pour l'email
|
||
const [hierarchie] = await conn.query(
|
||
`SELECT h.SuperieurId, m.email as managerEmail, m.prenom as managerPrenom, m.nom as managerNom
|
||
FROM HierarchieValidationAD h
|
||
LEFT JOIN CollaborateurAD m ON h.SuperieurId = m.id
|
||
WHERE h.CollaborateurId = ?`,
|
||
[userId]
|
||
);
|
||
|
||
const managerEmail = hierarchie[0]?.managerEmail;
|
||
const managerName = hierarchie[0] ? `${hierarchie[0].managerPrenom} ${hierarchie[0].managerNom}` : 'Manager';
|
||
|
||
await conn.commit();
|
||
console.log('\n✅ Modification terminée avec succès\n');
|
||
|
||
// 8. Envoyer les emails (après commit)
|
||
if (managerEmail && accessToken) {
|
||
try {
|
||
const newStartDate = new Date(startDate).toLocaleDateString('fr-FR');
|
||
const newEndDate = new Date(endDate).toLocaleDateString('fr-FR');
|
||
const typesConges = repartition.map(r => `${r.TypeConge}: ${r.NombreJours}j`).join(' | ');
|
||
|
||
const emailBody = {
|
||
message: {
|
||
subject: `🔄 Modification de demande de congé - ${userName}`,
|
||
body: {
|
||
contentType: "HTML",
|
||
content: `
|
||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 30px; text-align: center; border-radius: 10px 10px 0 0;">
|
||
<h1 style="color: white; margin: 0;">🔄 Modification de demande</h1>
|
||
</div>
|
||
|
||
<div style="background-color: #f8f9fa; padding: 30px; border-radius: 0 0 10px 10px;">
|
||
<p style="font-size: 16px; color: #333;">Bonjour ${managerName},</p>
|
||
|
||
<p style="font-size: 16px; color: #333;">
|
||
<strong>${userName}</strong> a modifié sa demande de congé.
|
||
</p>
|
||
|
||
<div style="background-color: white; padding: 20px; border-radius: 8px; margin: 20px 0; border-left: 4px solid #667eea;">
|
||
<h3 style="color: #667eea; margin-top: 0;">✨ Nouvelles informations</h3>
|
||
<table style="width: 100%; border-collapse: collapse;">
|
||
<tr>
|
||
<td style="padding: 8px 0; color: #666;"><strong>Type :</strong></td>
|
||
<td style="padding: 8px 0; color: #333;">${typesConges}</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding: 8px 0; color: #666;"><strong>Du :</strong></td>
|
||
<td style="padding: 8px 0; color: #333;">${newStartDate}</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding: 8px 0; color: #666;"><strong>Au :</strong></td>
|
||
<td style="padding: 8px 0; color: #333;">${newEndDate}</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding: 8px 0; color: #666;"><strong>Jours :</strong></td>
|
||
<td style="padding: 8px 0; color: #333;">${businessDays} jour(s)</td>
|
||
</tr>
|
||
${reason ? `<tr>
|
||
<td style="padding: 8px 0; color: #666; vertical-align: top;"><strong>Motif :</strong></td>
|
||
<td style="padding: 8px 0; color: #333; font-style: italic;">${reason}</td>
|
||
</tr>` : ''}
|
||
</table>
|
||
</div>
|
||
|
||
<p style="font-size: 14px; color: #666; margin-top: 30px;">
|
||
Cette demande est toujours en attente de validation.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
`
|
||
},
|
||
toRecipients: [
|
||
{
|
||
emailAddress: {
|
||
address: managerEmail
|
||
}
|
||
}
|
||
]
|
||
},
|
||
saveToSentItems: "false"
|
||
};
|
||
|
||
await axios.post(
|
||
'https://graph.microsoft.com/v1.0/me/sendMail',
|
||
emailBody,
|
||
{
|
||
headers: {
|
||
'Authorization': `Bearer ${accessToken}`,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
}
|
||
);
|
||
|
||
console.log('✅ Email de modification envoyé au manager');
|
||
|
||
} catch (emailError) {
|
||
console.error('❌ Erreur envoi email manager:', emailError);
|
||
}
|
||
}
|
||
|
||
conn.release();
|
||
|
||
res.json({
|
||
success: true,
|
||
message: 'Demande modifiée avec succès'
|
||
});
|
||
|
||
} catch (error) {
|
||
await conn.rollback();
|
||
if (conn) conn.release();
|
||
console.error('❌ Erreur updateRequest:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur serveur lors de la modification',
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
/**
|
||
* Route pour SUPPRIMER une demande de congé
|
||
* POST /deleteRequest
|
||
*/
|
||
app.post('/deleteRequest', async (req, res) => {
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
await conn.beginTransaction();
|
||
|
||
const { requestId, userId, userEmail, userName, accessToken } = req.body;
|
||
|
||
if (!requestId || !userId) {
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.status(400).json({
|
||
success: false,
|
||
message: 'Paramètres manquants (requestId ou userId)'
|
||
});
|
||
}
|
||
|
||
console.log('\n🗑️ === SUPPRESSION DEMANDE ===');
|
||
console.log('Demande ID:', requestId);
|
||
console.log('User ID:', userId);
|
||
|
||
// 1. Vérifier que la demande existe et appartient à l'utilisateur
|
||
const [existingRequest] = await conn.query(
|
||
`SELECT d.*, tc.Nom as TypeConge
|
||
FROM DemandeConge d
|
||
LEFT JOIN TypeConge tc ON d.TypeCongeId = tc.Id
|
||
WHERE d.Id = ? AND d.CollaborateurADId = ?`,
|
||
[requestId, userId]
|
||
);
|
||
|
||
if (existingRequest.length === 0) {
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.status(404).json({
|
||
success: false,
|
||
message: 'Demande introuvable ou non autorisée'
|
||
});
|
||
}
|
||
|
||
const request = existingRequest[0];
|
||
const requestStatus = request.Statut;
|
||
|
||
console.log(`📋 Demande trouvée: ID=${requestId}, Statut=${requestStatus}`);
|
||
|
||
// 2. ⭐ RESTAURER LES COMPTEURS (si nécessaire)
|
||
if (['En attente', 'Valide', 'Validé', 'Valid'].includes(requestStatus)) {
|
||
console.log(`\n✅ Restauration des compteurs (Statut: ${requestStatus})`);
|
||
try {
|
||
const restoration = await restoreLeaveBalance(conn, requestId, userId);
|
||
if (restoration.success) {
|
||
console.log('✅ Compteurs restaurés:', restoration.restorations.length, 'opérations');
|
||
}
|
||
} catch (restoreError) {
|
||
console.error('❌ Erreur restauration:', restoreError.message);
|
||
// Ne pas bloquer la suppression si la restauration échoue
|
||
}
|
||
}
|
||
|
||
// 3. Récupérer le manager pour l'email
|
||
const [hierarchie] = await conn.query(
|
||
`SELECT h.SuperieurId, m.email as managerEmail,
|
||
m.prenom as managerPrenom, m.nom as managerNom
|
||
FROM HierarchieValidationAD h
|
||
LEFT JOIN CollaborateurAD m ON h.SuperieurId = m.id
|
||
WHERE h.CollaborateurId = ?`,
|
||
[userId]
|
||
);
|
||
|
||
const managerEmail = hierarchie[0]?.managerEmail;
|
||
const managerName = hierarchie[0]
|
||
? `${hierarchie[0].managerPrenom} ${hierarchie[0].managerNom}`
|
||
: 'Manager';
|
||
|
||
// 4. ⭐ SUPPRIMER LES DÉPENDANCES
|
||
await conn.query('DELETE FROM HistoriqueActions WHERE DemandeCongeId = ?', [requestId]);
|
||
await conn.query('DELETE FROM DeductionDetails WHERE DemandeCongeId = ?', [requestId]);
|
||
await conn.query('DELETE FROM DemandeCongeType WHERE DemandeCongeId = ?', [requestId]);
|
||
await conn.query('DELETE FROM DocumentsMedicaux WHERE DemandeCongeId = ?', [requestId]);
|
||
await conn.query('DELETE FROM Notifications WHERE DemandeCongeId = ?', [requestId]);
|
||
|
||
// 5. Supprimer la demande
|
||
await conn.query('DELETE FROM DemandeConge WHERE Id = ?', [requestId]);
|
||
console.log(`✅ Demande ${requestId} supprimée définitivement`);
|
||
|
||
await conn.commit();
|
||
|
||
// 6. ⭐ ENVOYER EMAIL AU MANAGER
|
||
if (managerEmail && accessToken) {
|
||
try {
|
||
const emailBody = {
|
||
message: {
|
||
subject: `🗑️ Suppression de demande - ${userName}`,
|
||
body: {
|
||
contentType: "HTML",
|
||
content: `
|
||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||
<div style="background: linear-gradient(135deg, #ef4444 0%, #991b1b 100%); padding: 30px; text-align: center; border-radius: 10px 10px 0 0;">
|
||
<h1 style="color: white; margin: 0;">🗑️ Suppression de demande</h1>
|
||
</div>
|
||
|
||
<div style="background-color: #f8f9fa; padding: 30px; border-radius: 0 0 10px 10px;">
|
||
<p style="font-size: 16px; color: #333;">Bonjour ${managerName},</p>
|
||
|
||
<p style="font-size: 16px; color: #333;">
|
||
<strong>${userName}</strong> a supprimé sa demande de congé.
|
||
</p>
|
||
|
||
<div style="background-color: white; padding: 20px; border-radius: 8px; margin: 20px 0; border-left: 4px solid #ef4444;">
|
||
<h3 style="color: #ef4444; margin-top: 0;">📋 Demande supprimée</h3>
|
||
<table style="width: 100%; border-collapse: collapse;">
|
||
<tr>
|
||
<td style="padding: 8px 0; color: #666;"><strong>Type :</strong></td>
|
||
<td style="padding: 8px 0; color: #333;">${request.TypeConge || 'N/A'}</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding: 8px 0; color: #666;"><strong>Période :</strong></td>
|
||
<td style="padding: 8px 0; color: #333;">${new Date(request.DateDebut).toLocaleDateString('fr-FR')} au ${new Date(request.DateFin).toLocaleDateString('fr-FR')}</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding: 8px 0; color: #666;"><strong>Jours :</strong></td>
|
||
<td style="padding: 8px 0; color: #333;">${request.NombreJours} jour(s)</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding: 8px 0; color: #666;"><strong>Statut initial :</strong></td>
|
||
<td style="padding: 8px 0; color: #333;">${requestStatus}</td>
|
||
</tr>
|
||
</table>
|
||
</div>
|
||
|
||
<p style="font-size: 14px; color: #666; margin-top: 30px;">
|
||
Les compteurs de congés ont été restaurés si nécessaire.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
`
|
||
},
|
||
toRecipients: [{ emailAddress: { address: managerEmail } }],
|
||
saveToSentItems: false
|
||
}
|
||
};
|
||
|
||
await axios.post('https://graph.microsoft.com/v1.0/me/sendMail', emailBody, {
|
||
headers: {
|
||
'Authorization': `Bearer ${accessToken}`,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
console.log('📧 Email de notification envoyé au manager');
|
||
} catch (emailError) {
|
||
console.error('❌ Erreur email manager (non bloquant):', emailError.message);
|
||
}
|
||
}
|
||
|
||
conn.release();
|
||
|
||
res.json({
|
||
success: true,
|
||
message: 'Demande supprimée avec succès',
|
||
counterRestored: ['En attente', 'Valid', 'Validé', 'Valide'].includes(requestStatus)
|
||
});
|
||
|
||
} catch (error) {
|
||
await conn.rollback();
|
||
if (conn) conn.release();
|
||
console.error('❌ Erreur deleteRequest:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur lors de la suppression',
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
|
||
app.get('/exportCompteurs', async (req, res) => {
|
||
try {
|
||
const dateRef = req.query.dateRef || new Date().toISOString().split('T')[0];
|
||
const conn = await pool.getConnection();
|
||
|
||
const [collaborateurs] = await pool.query(`
|
||
SELECT
|
||
ca.id,
|
||
ca.prenom,
|
||
ca.nom,
|
||
ca.email,
|
||
ca.role,
|
||
ca.TypeContrat,
|
||
ca.DateEntree,
|
||
s.Nom as service,
|
||
ca.CampusId,
|
||
ca.SocieteId,
|
||
so.Nom as societe_nom
|
||
FROM CollaborateurAD ca
|
||
LEFT JOIN Services s ON ca.ServiceId = s.Id
|
||
LEFT JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE (ca.actif = 1 OR ca.actif IS NULL)
|
||
ORDER BY ca.nom, ca.prenom
|
||
`);
|
||
|
||
const rapport = [];
|
||
|
||
for (const collab of collaborateurs) {
|
||
const dateEntree = collab.DateEntree;
|
||
const dateReference = new Date(dateRef);
|
||
|
||
const acquisCP = calculerAcquisitionCP(dateReference, dateEntree);
|
||
|
||
let acquisRTT = 0;
|
||
if (collab.role !== 'Apprenti') {
|
||
const rttData = await calculerAcquisitionRTT(conn, collab.id, dateReference);
|
||
acquisRTT = rttData.acquisition;
|
||
}
|
||
|
||
const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']);
|
||
const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']);
|
||
|
||
let soldeCP = 0;
|
||
let soldeRTT = 0;
|
||
|
||
if (cpType.length > 0) {
|
||
const [compteurCP] = await conn.query(
|
||
'SELECT Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?',
|
||
[collab.id, cpType[0].Id, dateReference.getFullYear()]
|
||
);
|
||
soldeCP = compteurCP.length > 0 ? parseFloat(compteurCP[0].Solde) : 0;
|
||
}
|
||
|
||
if (rttType.length > 0 && collab.role !== 'Apprenti') {
|
||
const [compteurRTT] = await conn.query(
|
||
'SELECT Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?',
|
||
[collab.id, rttType[0].Id, dateReference.getFullYear()]
|
||
);
|
||
soldeRTT = compteurRTT.length > 0 ? parseFloat(compteurRTT[0].Solde) : 0;
|
||
}
|
||
|
||
rapport.push({
|
||
id: collab.id,
|
||
prenom: collab.prenom,
|
||
nom: collab.nom,
|
||
email: collab.email,
|
||
role: collab.role,
|
||
service: collab.service,
|
||
societe_id: collab.SocieteId,
|
||
societe_nom: collab.societe_nom,
|
||
type_contrat: collab.TypeContrat,
|
||
date_entree: dateEntree ? formatDateWithoutUTC(dateEntree) : null,
|
||
cp_acquis: parseFloat(acquisCP.toFixed(2)),
|
||
cp_solde: parseFloat(soldeCP.toFixed(2)),
|
||
rtt_acquis: parseFloat(acquisRTT.toFixed(2)),
|
||
rtt_solde: parseFloat(soldeRTT.toFixed(2)),
|
||
date_reference: dateRef
|
||
});
|
||
}
|
||
|
||
conn.release();
|
||
|
||
res.json({
|
||
success: true,
|
||
date_reference: dateRef,
|
||
total_collaborateurs: rapport.length,
|
||
rapport: rapport
|
||
});
|
||
} catch (error) {
|
||
console.error('Erreur exportCompteurs:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur serveur',
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
/**
|
||
* GET /getCongesAnticipes
|
||
* Calcule les congés anticipés disponibles pour un collaborateur
|
||
*/
|
||
app.get('/getCongesAnticipes', async (req, res) => {
|
||
try {
|
||
const userIdParam = req.query.user_id;
|
||
|
||
if (!userIdParam) {
|
||
return res.json({ success: false, message: 'ID utilisateur manquant' });
|
||
}
|
||
|
||
const conn = await pool.getConnection();
|
||
|
||
// Déterminer l'ID (UUID ou numérique)
|
||
const isUUID = userIdParam.length > 10 && userIdParam.includes('-');
|
||
|
||
const userQuery = `
|
||
SELECT
|
||
ca.id,
|
||
ca.prenom,
|
||
ca.nom,
|
||
ca.email,
|
||
ca.DateEntree,
|
||
ca.TypeContrat,
|
||
ca.role,
|
||
ca.CampusId
|
||
FROM CollaborateurAD ca
|
||
WHERE ${isUUID ? 'ca.entraUserId' : 'ca.id'} = ?
|
||
AND (ca.Actif = 1 OR ca.Actif IS NULL)
|
||
`;
|
||
|
||
const [userInfo] = await conn.query(userQuery, [userIdParam]);
|
||
|
||
if (userInfo.length === 0) {
|
||
conn.release();
|
||
return res.json({ success: false, message: 'Utilisateur non trouvé' });
|
||
}
|
||
|
||
const user = userInfo[0];
|
||
const userId = user.id;
|
||
const dateEntree = user.DateEntree;
|
||
const typeContrat = user.TypeContrat || '37h';
|
||
const today = new Date();
|
||
const currentYear = today.getFullYear();
|
||
const finAnnee = new Date(currentYear, 11, 31); // 31 décembre
|
||
|
||
// ========================================
|
||
// CALCUL CP (Congés Payés)
|
||
// ========================================
|
||
|
||
// Acquisition actuelle
|
||
const acquisActuelleCP = calculerAcquisitionCP(today, dateEntree);
|
||
|
||
// Acquisition prévue à la fin de l'exercice (31 mai N+1)
|
||
const finExerciceCP = new Date(currentYear + 1, 4, 31); // 31 mai N+1
|
||
const acquisTotaleCP = calculerAcquisitionCP(finExerciceCP, dateEntree);
|
||
|
||
// Récupérer le solde actuel
|
||
const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']);
|
||
|
||
let soldeActuelCP = 0;
|
||
let dejaPrisCP = 0;
|
||
|
||
if (cpType.length > 0) {
|
||
const [compteurCP] = await conn.query(`
|
||
SELECT Total, Solde, SoldeReporte
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [userId, cpType[0].Id, currentYear]);
|
||
|
||
if (compteurCP.length > 0) {
|
||
const total = parseFloat(compteurCP[0].Total || 0);
|
||
soldeActuelCP = parseFloat(compteurCP[0].Solde || 0);
|
||
dejaPrisCP = total - (soldeActuelCP - parseFloat(compteurCP[0].SoldeReporte || 0));
|
||
}
|
||
}
|
||
|
||
// Calculer le potentiel anticipé pour CP
|
||
const acquisRestanteCP = acquisTotaleCP - acquisActuelleCP;
|
||
const anticipePossibleCP = Math.max(0, acquisRestanteCP);
|
||
const limiteAnticipeCP = Math.min(anticipePossibleCP, 25 - dejaPrisCP);
|
||
|
||
// ========================================
|
||
// CALCUL RTT
|
||
// ========================================
|
||
|
||
let anticipePossibleRTT = 0;
|
||
let limiteAnticipeRTT = 0;
|
||
let soldeActuelRTT = 0;
|
||
let dejaPrisRTT = 0;
|
||
let acquisActuelleRTT = 0;
|
||
let acquisTotaleRTT = 0;
|
||
|
||
if (user.role !== 'Apprenti') {
|
||
// Acquisition actuelle
|
||
const rttDataActuel = await calculerAcquisitionRTT(conn, userId, today);
|
||
acquisActuelleRTT = rttDataActuel.acquisition;
|
||
|
||
// Acquisition prévue à la fin de l'année
|
||
const rttDataTotal = await calculerAcquisitionRTT(conn, userId, finAnnee);
|
||
acquisTotaleRTT = rttDataTotal.acquisition;
|
||
|
||
// Récupérer le solde actuel
|
||
const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']);
|
||
|
||
if (rttType.length > 0) {
|
||
const [compteurRTT] = await conn.query(`
|
||
SELECT Total, Solde
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [userId, rttType[0].Id, currentYear]);
|
||
|
||
if (compteurRTT.length > 0) {
|
||
const total = parseFloat(compteurRTT[0].Total || 0);
|
||
soldeActuelRTT = parseFloat(compteurRTT[0].Solde || 0);
|
||
dejaPrisRTT = total - soldeActuelRTT;
|
||
}
|
||
}
|
||
|
||
// Calculer le potentiel anticipé pour RTT
|
||
const acquisRestanteRTT = acquisTotaleRTT - acquisActuelleRTT;
|
||
anticipePossibleRTT = Math.max(0, acquisRestanteRTT);
|
||
|
||
const maxRTT = typeContrat === 'forfait_jour' ? 12 : 10;
|
||
limiteAnticipeRTT = Math.min(anticipePossibleRTT, maxRTT - dejaPrisRTT);
|
||
}
|
||
|
||
conn.release();
|
||
|
||
// ========================================
|
||
// RÉPONSE
|
||
// ========================================
|
||
|
||
res.json({
|
||
success: true,
|
||
user: {
|
||
id: user.id,
|
||
nom: `${user.prenom} ${user.nom}`,
|
||
email: user.email,
|
||
typeContrat: typeContrat,
|
||
dateEntree: dateEntree ? formatDateWithoutUTC(dateEntree) : null
|
||
},
|
||
dateReference: today.toISOString().split('T')[0],
|
||
congesPayes: {
|
||
acquisActuelle: parseFloat(acquisActuelleCP.toFixed(2)),
|
||
acquisTotalePrevu: parseFloat(acquisTotaleCP.toFixed(2)),
|
||
acquisRestante: parseFloat((acquisTotaleCP - acquisActuelleCP).toFixed(2)),
|
||
soldeActuel: parseFloat(soldeActuelCP.toFixed(2)),
|
||
dejaPris: parseFloat(dejaPrisCP.toFixed(2)),
|
||
anticipePossible: parseFloat(anticipePossibleCP.toFixed(2)),
|
||
limiteAnticipe: parseFloat(limiteAnticipeCP.toFixed(2)),
|
||
totalDisponible: parseFloat((soldeActuelCP + limiteAnticipeCP).toFixed(2)),
|
||
message: limiteAnticipeCP > 0
|
||
? `Vous pouvez poser jusqu'à ${limiteAnticipeCP.toFixed(1)} jours de CP en anticipé`
|
||
: "Vous avez atteint la limite d'anticipation pour les CP"
|
||
},
|
||
rtt: user.role !== 'Apprenti' ? {
|
||
acquisActuelle: parseFloat(acquisActuelleRTT.toFixed(2)),
|
||
acquisTotalePrevu: parseFloat(acquisTotaleRTT.toFixed(2)),
|
||
acquisRestante: parseFloat((acquisTotaleRTT - acquisActuelleRTT).toFixed(2)),
|
||
soldeActuel: parseFloat(soldeActuelRTT.toFixed(2)),
|
||
dejaPris: parseFloat(dejaPrisRTT.toFixed(2)),
|
||
anticipePossible: parseFloat(anticipePossibleRTT.toFixed(2)),
|
||
limiteAnticipe: parseFloat(limiteAnticipeRTT.toFixed(2)),
|
||
totalDisponible: parseFloat((soldeActuelRTT + limiteAnticipeRTT).toFixed(2)),
|
||
message: limiteAnticipeRTT > 0
|
||
? `Vous pouvez poser jusqu'à ${limiteAnticipeRTT.toFixed(1)} jours de RTT en anticipé`
|
||
: "Vous avez atteint la limite d'anticipation pour les RTT"
|
||
} : null,
|
||
regles: {
|
||
cpMaxAnnuel: 25,
|
||
rttMaxAnnuel: typeContrat === 'forfait_jour' ? 12 : 10,
|
||
description: "Les congés anticipés sont basés sur l'acquisition prévue jusqu'à la fin de l'exercice/année"
|
||
}
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('Erreur getCongesAnticipes:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur serveur',
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
app.get('/getStatistiquesCompteurs', async (req, res) => {
|
||
try {
|
||
const conn = await pool.getConnection();
|
||
const currentYear = new Date().getFullYear();
|
||
|
||
const [totalCollabs] = await conn.query(
|
||
'SELECT COUNT(*) as total FROM CollaborateurAD WHERE actif = 1 OR actif IS NULL'
|
||
);
|
||
|
||
const [statsTypeContrat] = await conn.query(`
|
||
SELECT
|
||
TypeContrat,
|
||
COUNT(*) as nombre,
|
||
GROUP_CONCAT(CONCAT(prenom, ' ', nom) SEPARATOR ', ') as noms
|
||
FROM CollaborateurAD
|
||
WHERE actif = 1 OR actif IS NULL
|
||
GROUP BY TypeContrat
|
||
`);
|
||
|
||
const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']);
|
||
let statsCP = { total_acquis: 0, total_solde: 0, moyenne_utilisation: 0 };
|
||
|
||
if (cpType.length > 0) {
|
||
const [cpStats] = await conn.query(`
|
||
SELECT
|
||
SUM(Total) as total_acquis,
|
||
SUM(Solde) as total_solde,
|
||
AVG(CASE WHEN Total > 0 THEN ((Total - Solde) / Total) * 100 ELSE 0 END) as moyenne_utilisation
|
||
FROM CompteurConges
|
||
WHERE TypeCongeId = ? AND Annee = ?
|
||
`, [cpType[0].Id, currentYear]);
|
||
|
||
if (cpStats.length > 0) {
|
||
statsCP = {
|
||
total_acquis: parseFloat((cpStats[0].total_acquis || 0).toFixed(2)),
|
||
total_solde: parseFloat((cpStats[0].total_solde || 0).toFixed(2)),
|
||
moyenne_utilisation: parseFloat((cpStats[0].moyenne_utilisation || 0).toFixed(1))
|
||
};
|
||
}
|
||
}
|
||
|
||
const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']);
|
||
let statsRTT = { total_acquis: 0, total_solde: 0, moyenne_utilisation: 0 };
|
||
|
||
if (rttType.length > 0) {
|
||
const [rttStats] = await conn.query(`
|
||
SELECT
|
||
SUM(Total) as total_acquis,
|
||
SUM(Solde) as total_solde,
|
||
AVG(CASE WHEN Total > 0 THEN ((Total - Solde) / Total) * 100 ELSE 0 END) as moyenne_utilisation
|
||
FROM CompteurConges
|
||
WHERE TypeCongeId = ? AND Annee = ?
|
||
`, [rttType[0].Id, currentYear]);
|
||
|
||
if (rttStats.length > 0) {
|
||
statsRTT = {
|
||
total_acquis: parseFloat((rttStats[0].total_acquis || 0).toFixed(2)),
|
||
total_solde: parseFloat((rttStats[0].total_solde || 0).toFixed(2)),
|
||
moyenne_utilisation: parseFloat((rttStats[0].moyenne_utilisation || 0).toFixed(1))
|
||
};
|
||
}
|
||
}
|
||
|
||
conn.release();
|
||
|
||
res.json({
|
||
success: true,
|
||
annee: currentYear,
|
||
statistiques: {
|
||
collaborateurs: {
|
||
total: totalCollabs[0].total,
|
||
par_type_contrat: statsTypeContrat
|
||
},
|
||
conges_payes: statsCP,
|
||
rtt: statsRTT
|
||
}
|
||
});
|
||
} catch (error) {
|
||
console.error('Erreur getStatistiquesCompteurs:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur serveur',
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
async function hasCompteRenduAccess(userId) {
|
||
try {
|
||
const conn = await pool.getConnection();
|
||
|
||
const [user] = await conn.query(`
|
||
SELECT TypeContrat, role
|
||
FROM CollaborateurAD
|
||
WHERE id = ?
|
||
`, [userId]);
|
||
|
||
conn.release();
|
||
|
||
if (!user.length) return false;
|
||
|
||
const userInfo = user[0];
|
||
|
||
// Accès si :
|
||
// 1. TypeContrat = 'forfait_jour'
|
||
// 2. role = 'Directeur Campus' ou 'Directrice Campus'
|
||
// 3. role = 'RH' ou 'Admin'
|
||
|
||
return (
|
||
userInfo.TypeContrat === 'forfait_jour' ||
|
||
userInfo.role === 'Directeur Campus' ||
|
||
userInfo.role === 'Directrice Campus' ||
|
||
userInfo.role === 'RH' ||
|
||
userInfo.role === 'Admin'
|
||
);
|
||
|
||
} catch (error) {
|
||
console.error('Erreur vérification accès:', error);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
|
||
// Récupérer les jours du mois
|
||
// GET - Récupérer les données du compte-rendu
|
||
app.get('/getCompteRenduActivites', async (req, res) => {
|
||
const { user_id, annee, mois } = req.query;
|
||
|
||
try {
|
||
// Vérifier l'accès
|
||
const hasAccess = await hasCompteRenduAccess(user_id);
|
||
|
||
if (!hasAccess) {
|
||
return res.json({
|
||
success: false,
|
||
message: 'Accès réservé aux collaborateurs en forfait jour et aux directeurs de campus'
|
||
});
|
||
}
|
||
|
||
const conn = await pool.getConnection();
|
||
|
||
const [jours] = await conn.query(`
|
||
SELECT
|
||
id,
|
||
CollaborateurADId,
|
||
Annee,
|
||
Mois,
|
||
DATE_FORMAT(JourDate, '%Y-%m-%d') as JourDate,
|
||
JourTravaille,
|
||
ReposQuotidienRespect,
|
||
ReposHebdomadaireRespect,
|
||
CommentaireRepos,
|
||
Verrouille,
|
||
DateSaisie,
|
||
SaisiePar
|
||
FROM CompteRenduActivites
|
||
WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ?
|
||
ORDER BY JourDate
|
||
`, [user_id, annee, mois]);
|
||
|
||
console.log('🔍 Backend - Jours trouvés:', jours.length);
|
||
if (jours.length > 0) {
|
||
console.log('📅 Premier jour:', jours[0].JourDate, 'Type:', typeof jours[0].JourDate);
|
||
}
|
||
|
||
const [mensuel] = await conn.query(`
|
||
SELECT * FROM CompteRenduMensuel
|
||
WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ?
|
||
`, [user_id, annee, mois]);
|
||
|
||
conn.release();
|
||
|
||
res.json({
|
||
success: true,
|
||
jours: jours,
|
||
mensuel: mensuel[0] || null
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('Erreur getCompteRenduActivites:', error);
|
||
res.status(500).json({ success: false, message: error.message });
|
||
}
|
||
});
|
||
|
||
// POST - Sauvegarder un jour avec AUTO-VERROUILLAGE
|
||
// POST - Sauvegarder un jour avec AUTO-VERROUILLAGE
|
||
app.post('/saveCompteRenduJour', async (req, res) => {
|
||
const { user_id, date, jour_travaille, repos_quotidien, repos_hebdo, commentaire, rh_override } = req.body;
|
||
|
||
try {
|
||
const conn = await pool.getConnection();
|
||
await conn.beginTransaction();
|
||
|
||
const dateJour = new Date(date);
|
||
const aujourdhui = new Date();
|
||
aujourdhui.setHours(0, 0, 0, 0);
|
||
dateJour.setHours(0, 0, 0, 0);
|
||
|
||
// Bloquer saisie du jour actuel (il faut attendre le lendemain)
|
||
if (dateJour >= aujourdhui) {
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.json({ success: false, message: 'Vous ne pouvez pas saisir le jour actuel. Veuillez attendre demain.' });
|
||
}
|
||
|
||
const annee = dateJour.getFullYear();
|
||
const mois = dateJour.getMonth() + 1;
|
||
|
||
// Vérifier si le JOUR est déjà verrouillé (pas le mois entier)
|
||
const [jourExistant] = await conn.query(
|
||
'SELECT Verrouille FROM CompteRenduActivites WHERE CollaborateurADId = ? AND JourDate = ?',
|
||
[user_id, date]
|
||
);
|
||
|
||
if (jourExistant[0]?.Verrouille && !rh_override) {
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.json({ success: false, message: 'Ce jour est verrouillé - Contactez les RH pour modification' });
|
||
}
|
||
|
||
// Vérifier commentaire obligatoire
|
||
if (!repos_quotidien || !repos_hebdo) {
|
||
if (!commentaire || commentaire.trim() === '') {
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.json({ success: false, message: 'Commentaire obligatoire en cas de non-respect des repos' });
|
||
}
|
||
}
|
||
|
||
// Insérer ou mettre à jour le jour ET LE VERROUILLER
|
||
await conn.query(`
|
||
INSERT INTO CompteRenduActivites
|
||
(CollaborateurADId, Annee, Mois, JourDate, JourTravaille,
|
||
ReposQuotidienRespect, ReposHebdomadaireRespect, CommentaireRepos,
|
||
DateSaisie, SaisiePar, Verrouille)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW(), ?, TRUE)
|
||
ON DUPLICATE KEY UPDATE
|
||
JourTravaille = VALUES(JourTravaille),
|
||
ReposQuotidienRespect = VALUES(ReposQuotidienRespect),
|
||
ReposHebdomadaireRespect = VALUES(ReposHebdomadaireRespect),
|
||
CommentaireRepos = VALUES(CommentaireRepos),
|
||
SaisiePar = VALUES(SaisiePar),
|
||
Verrouille = TRUE
|
||
`, [user_id, annee, mois, date, jour_travaille, repos_quotidien, repos_hebdo, commentaire, user_id]);
|
||
|
||
// Mettre à jour les statistiques mensuelles (SANS verrouiller le mois)
|
||
const [stats] = await conn.query(`
|
||
SELECT
|
||
COUNT(*) as nbJours,
|
||
SUM(CASE WHEN ReposQuotidienRespect = FALSE THEN 1 ELSE 0 END) as nbNonRespectQuotidien,
|
||
SUM(CASE WHEN ReposHebdomadaireRespect = FALSE THEN 1 ELSE 0 END) as nbNonRespectHebdo
|
||
FROM CompteRenduActivites
|
||
WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ?
|
||
AND JourTravaille = TRUE
|
||
`, [user_id, annee, mois]);
|
||
|
||
await conn.query(`
|
||
INSERT INTO CompteRenduMensuel
|
||
(CollaborateurADId, Annee, Mois, NbJoursTravailles,
|
||
NbJoursNonRespectsReposQuotidien, NbJoursNonRespectsReposHebdo,
|
||
Statut, DateValidation)
|
||
VALUES (?, ?, ?, ?, ?, ?, 'En cours', NOW())
|
||
ON DUPLICATE KEY UPDATE
|
||
NbJoursTravailles = VALUES(NbJoursTravailles),
|
||
NbJoursNonRespectsReposQuotidien = VALUES(NbJoursNonRespectsReposQuotidien),
|
||
NbJoursNonRespectsReposHebdo = VALUES(NbJoursNonRespectsReposHebdo),
|
||
DateValidation = NOW()
|
||
`, [user_id, annee, mois, stats[0].nbJours, stats[0].nbNonRespectQuotidien, stats[0].nbNonRespectHebdo]);
|
||
|
||
await conn.commit();
|
||
conn.release();
|
||
|
||
res.json({
|
||
success: true,
|
||
message: 'Jour enregistré et verrouillé',
|
||
verrouille: true
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('❌ Erreur saveCompteRenduJour:', error);
|
||
res.status(500).json({ success: false, message: error.message });
|
||
}
|
||
});
|
||
|
||
// POST - Saisie en masse avec AUTO-VERROUILLAGE
|
||
app.post('/saveCompteRenduMasse', async (req, res) => {
|
||
const { user_id, annee, mois, jours, rh_override } = req.body;
|
||
|
||
try {
|
||
const conn = await pool.getConnection();
|
||
await conn.beginTransaction();
|
||
|
||
let count = 0;
|
||
let blocked = 0;
|
||
|
||
for (const jour of jours) {
|
||
const dateJour = new Date(jour.date);
|
||
const aujourdhui = new Date();
|
||
aujourdhui.setHours(0, 0, 0, 0);
|
||
dateJour.setHours(0, 0, 0, 0);
|
||
|
||
// Bloquer le jour actuel
|
||
if (dateJour >= aujourdhui) {
|
||
blocked++;
|
||
continue;
|
||
}
|
||
|
||
// Vérifier si déjà verrouillé
|
||
const [jourExistant] = await conn.query(
|
||
'SELECT Verrouille FROM CompteRenduActivites WHERE CollaborateurADId = ? AND JourDate = ?',
|
||
[user_id, jour.date]
|
||
);
|
||
|
||
if (jourExistant[0]?.Verrouille && !rh_override) {
|
||
blocked++;
|
||
continue;
|
||
}
|
||
|
||
await conn.query(`
|
||
INSERT INTO CompteRenduActivites
|
||
(CollaborateurADId, Annee, Mois, JourDate, JourTravaille,
|
||
ReposQuotidienRespect, ReposHebdomadaireRespect, CommentaireRepos,
|
||
DateSaisie, SaisiePar, Verrouille)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW(), ?, TRUE)
|
||
ON DUPLICATE KEY UPDATE
|
||
JourTravaille = VALUES(JourTravaille),
|
||
ReposQuotidienRespect = VALUES(ReposQuotidienRespect),
|
||
ReposHebdomadaireRespect = VALUES(ReposHebdomadaireRespect),
|
||
CommentaireRepos = VALUES(CommentaireRepos),
|
||
SaisiePar = VALUES(SaisiePar),
|
||
Verrouille = TRUE
|
||
`, [
|
||
user_id, annee, mois, jour.date,
|
||
jour.jour_travaille, jour.repos_quotidien, jour.repos_hebdo,
|
||
jour.commentaire || null, user_id
|
||
]);
|
||
|
||
count++;
|
||
}
|
||
|
||
// Mettre à jour statistiques mensuelles
|
||
const [stats] = await conn.query(`
|
||
SELECT
|
||
COUNT(*) as nbJours,
|
||
SUM(CASE WHEN ReposQuotidienRespect = FALSE THEN 1 ELSE 0 END) as nbNonRespectQuotidien,
|
||
SUM(CASE WHEN ReposHebdomadaireRespect = FALSE THEN 1 ELSE 0 END) as nbNonRespectHebdo
|
||
FROM CompteRenduActivites
|
||
WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ?
|
||
AND JourTravaille = TRUE
|
||
`, [user_id, annee, mois]);
|
||
|
||
await conn.query(`
|
||
INSERT INTO CompteRenduMensuel
|
||
(CollaborateurADId, Annee, Mois, NbJoursTravailles,
|
||
NbJoursNonRespectsReposQuotidien, NbJoursNonRespectsReposHebdo,
|
||
Statut, DateValidation)
|
||
VALUES (?, ?, ?, ?, ?, ?, 'En cours', NOW())
|
||
ON DUPLICATE KEY UPDATE
|
||
NbJoursTravailles = VALUES(NbJoursTravailles),
|
||
NbJoursNonRespectsReposQuotidien = VALUES(NbJoursNonRespectsReposQuotidien),
|
||
NbJoursNonRespectsReposHebdo = VALUES(NbJoursNonRespectsReposHebdo),
|
||
DateValidation = NOW()
|
||
`, [user_id, annee, mois, stats[0].nbJours, stats[0].nbNonRespectQuotidien, stats[0].nbNonRespectHebdo]);
|
||
|
||
await conn.commit();
|
||
conn.release();
|
||
|
||
res.json({
|
||
success: true,
|
||
count: count,
|
||
blocked: blocked,
|
||
message: `${count} jours enregistrés${blocked > 0 ? `, ${blocked} ignorés (jour actuel ou déjà verrouillés)` : ''}`
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('❌ Erreur saisie masse:', error);
|
||
res.status(500).json({ success: false, message: error.message });
|
||
}
|
||
});
|
||
|
||
app.post('/deverrouillerJour', async (req, res) => {
|
||
const { user_id, date, rh_user_id } = req.body;
|
||
|
||
try {
|
||
const conn = await pool.getConnection();
|
||
|
||
const [rhUser] = await conn.query(
|
||
'SELECT role FROM CollaborateurAD WHERE id = ?',
|
||
[rh_user_id]
|
||
);
|
||
|
||
if (!rhUser.length || (rhUser[0].role !== 'RH' && rhUser[0].role !== 'Admin')) {
|
||
conn.release();
|
||
return res.json({ success: false, message: 'Action réservée aux RH' });
|
||
}
|
||
|
||
await conn.query(`
|
||
UPDATE CompteRenduActivites
|
||
SET Verrouille = FALSE
|
||
WHERE CollaborateurADId = ? AND JourDate = ?
|
||
`, [user_id, date]);
|
||
|
||
conn.release();
|
||
|
||
res.json({ success: true });
|
||
|
||
} catch (error) {
|
||
console.error('❌ Erreur déverrouillage jour:', error);
|
||
res.status(500).json({ success: false, message: error.message });
|
||
}
|
||
});
|
||
|
||
|
||
// POST - Verrouiller (RH uniquement)
|
||
app.post('/verrouillerCompteRendu', async (req, res) => {
|
||
const { user_id, annee, mois, rh_user_id } = req.body;
|
||
|
||
try {
|
||
const conn = await pool.getConnection();
|
||
|
||
// Vérifier que l'utilisateur est RH
|
||
const [rhUser] = await conn.query(
|
||
'SELECT role FROM CollaborateurAD WHERE id = ?',
|
||
[rh_user_id]
|
||
);
|
||
|
||
if (!rhUser.length || (rhUser[0].role !== 'RH' && rhUser[0].role !== 'Admin')) {
|
||
conn.release();
|
||
return res.json({ success: false, message: 'Action réservée aux RH' });
|
||
}
|
||
|
||
await conn.query(`
|
||
UPDATE CompteRenduMensuel
|
||
SET Verrouille = TRUE,
|
||
DateModification = NOW()
|
||
WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ?
|
||
`, [user_id, annee, mois]);
|
||
|
||
conn.release();
|
||
|
||
res.json({ success: true });
|
||
|
||
} catch (error) {
|
||
console.error('Erreur verrouillage:', error);
|
||
res.status(500).json({ success: false, message: error.message });
|
||
}
|
||
});
|
||
|
||
// POST - Déverrouiller (RH uniquement)
|
||
app.post('/deverrouillerCompteRendu', async (req, res) => {
|
||
const { user_id, annee, mois, rh_user_id } = req.body;
|
||
|
||
try {
|
||
const conn = await pool.getConnection();
|
||
|
||
// Vérifier que l'utilisateur est RH
|
||
const [rhUser] = await conn.query(
|
||
'SELECT role FROM CollaborateurAD WHERE id = ?',
|
||
[rh_user_id]
|
||
);
|
||
|
||
if (!rhUser.length || (rhUser[0].role !== 'RH' && rhUser[0].role !== 'Admin')) {
|
||
conn.release();
|
||
return res.json({ success: false, message: 'Action réservée aux RH' });
|
||
}
|
||
|
||
await conn.query(`
|
||
UPDATE CompteRenduMensuel
|
||
SET Verrouille = FALSE,
|
||
DateDeverrouillage = NOW(),
|
||
DeverrouillePar = ?
|
||
WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ?
|
||
`, [rh_user_id, user_id, annee, mois]);
|
||
|
||
conn.release();
|
||
|
||
res.json({ success: true });
|
||
|
||
} catch (error) {
|
||
console.error('Erreur déverrouillage:', error);
|
||
res.status(500).json({ success: false, message: error.message });
|
||
}
|
||
});
|
||
|
||
// GET - Stats annuelles
|
||
app.get('/getStatsAnnuelles', async (req, res) => {
|
||
const { user_id, annee } = req.query;
|
||
|
||
try {
|
||
const conn = await pool.getConnection();
|
||
|
||
const [stats] = await conn.query(`
|
||
SELECT
|
||
SUM(NbJoursTravailles) as totalJoursTravailles,
|
||
SUM(NbJoursNonRespectsReposQuotidien) as totalNonRespectQuotidien,
|
||
SUM(NbJoursNonRespectsReposHebdo) as totalNonRespectHebdo
|
||
FROM CompteRenduMensuel
|
||
WHERE CollaborateurADId = ? AND Annee = ?
|
||
`, [user_id, annee]);
|
||
|
||
conn.release();
|
||
|
||
res.json({
|
||
success: true,
|
||
stats: stats[0] || {
|
||
totalJoursTravailles: 0,
|
||
totalNonRespectQuotidien: 0,
|
||
totalNonRespectHebdo: 0
|
||
}
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('Erreur stats:', error);
|
||
res.status(500).json({ success: false, message: error.message });
|
||
}
|
||
});
|
||
|
||
// GET - Export PDF (RH uniquement)
|
||
app.get('/exportCompteRenduPDF', async (req, res) => {
|
||
const { user_id, annee, mois } = req.query;
|
||
|
||
try {
|
||
const conn = await pool.getConnection();
|
||
|
||
// Récupérer les données du collaborateur
|
||
const [collab] = await conn.query(
|
||
'SELECT prenom, nom, email FROM CollaborateurAD WHERE id = ?',
|
||
[user_id]
|
||
);
|
||
|
||
// Récupérer les jours du mois
|
||
const [jours] = await conn.query(`
|
||
SELECT * FROM CompteRenduActivites
|
||
WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ?
|
||
ORDER BY JourDate
|
||
`, [user_id, annee, mois]);
|
||
|
||
// Récupérer le mensuel
|
||
const [mensuel] = await conn.query(`
|
||
SELECT * FROM CompteRenduMensuel
|
||
WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ?
|
||
`, [user_id, annee, mois]);
|
||
|
||
conn.release();
|
||
|
||
// TODO: Générer le PDF avec une bibliothèque comme pdfkit ou puppeteer
|
||
// Pour l'instant, retourner les données JSON
|
||
res.json({
|
||
success: true,
|
||
collaborateur: collab[0],
|
||
jours: jours,
|
||
mensuel: mensuel[0],
|
||
message: 'Export PDF à implémenter'
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('Erreur export PDF:', error);
|
||
res.status(500).json({ success: false, message: error.message });
|
||
}
|
||
});
|
||
|
||
|
||
|
||
// ========================================
|
||
// DÉMARRAGE DU SERVEUR
|
||
// ========================================
|
||
|
||
app.listen(PORT, () => {
|
||
console.log(`✅ Serveur démarré sur http://localhost:${PORT}`);
|
||
console.log('📋 Routes disponibles:');
|
||
console.log(' POST /login');
|
||
console.log(' POST /check-user-groups');
|
||
console.log(' GET /getDetailedLeaveCounters');
|
||
console.log(' POST /updateCounters');
|
||
console.log(' POST /updateAllCounters');
|
||
console.log(' POST /reinitializeAllCounters');
|
||
console.log(' GET /testProrata');
|
||
console.log(' POST /fixAllCounters');
|
||
console.log(' POST /submitLeaveRequest');
|
||
console.log(' POST /validateRequest');
|
||
console.log(' GET /getRequests');
|
||
console.log(' GET /getNotifications');
|
||
console.log(' POST /markNotificationRead');
|
||
console.log(' GET /getPendingRequests');
|
||
console.log(' GET /getTeamMembers');
|
||
console.log(' GET /getTeamLeaves');
|
||
console.log(' GET /getAllTeamRequests');
|
||
console.log(' GET /getEmployeRequest');
|
||
console.log(' POST /initial-sync');
|
||
console.log('');
|
||
console.log(' 🆕 ROUTES ADMIN RTT:');
|
||
console.log(' GET /getAllCollaborateurs');
|
||
console.log(' POST /updateTypeContrat');
|
||
console.log(' GET /getConfigurationRTT');
|
||
console.log(' POST /updateConfigurationRTT');
|
||
console.log(' GET /exportCompteurs');
|
||
console.log(' GET /getStatistiquesCompteurs');
|
||
console.log('');
|
||
console.log('⏰ Tâches CRON actives:');
|
||
console.log(' 📅 Mise à jour mensuelle: 1er de chaque mois à 00h01');
|
||
console.log(' 📸 Arrêtés mensuels: Dernier jour de chaque mois à 23h55'); // ⭐ NOUVEAU
|
||
console.log(' 🎆 Fin d\'année RTT: 31 décembre à 23h59');
|
||
console.log(' 📅 Fin d\'exercice CP: 31 mai à 23h59');
|
||
}); |