10578 lines
417 KiB
JavaScript
10578 lines
417 KiB
JavaScript
// ============================================================================
|
||
// 🚀 GTA - GESTION DES TEMPS ET ABSENCES
|
||
// ============================================================================
|
||
// Serveur Backend Node.js avec SQL Server
|
||
// Port: 3004
|
||
// Base de données: GTA (SQL Server)
|
||
// ============================================================================
|
||
|
||
import express from 'express';
|
||
import sql from 'mssql';
|
||
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 = 3004;
|
||
|
||
const webhookManager = new WebhookManager(WEBHOOKS.SECRET_KEY);
|
||
const sseClientsCollab = new Set();
|
||
process.on('uncaughtException', (err) => {
|
||
console.error('💥 ERREUR CRITIQUE NON CATCHÉE:', err);
|
||
console.error('Stack:', err.stack);
|
||
// On ne crash pas pour pouvoir déboguer
|
||
});
|
||
|
||
process.on('unhandledRejection', (reason, promise) => {
|
||
console.error('💥 PROMESSE REJETÉE NON GÉRÉE:', reason);
|
||
console.error('Promise:', promise);
|
||
});
|
||
|
||
app.use(cors({
|
||
origin: ['http://localhost:3013', 'http://localhost:80', 'https://mygta-dev.ensup-adm.net'],
|
||
credentials: true
|
||
}));
|
||
|
||
|
||
app.use(express.json());
|
||
app.use(express.urlencoded({ extended: true }));
|
||
|
||
const dbConfig = {
|
||
server: '192.168.0.3', // ⭐ 'server' au lieu de 'host'
|
||
user: 'gta_app',
|
||
password: 'GTA2025!Secure',
|
||
database: 'GTA',
|
||
port: 1433, // ⭐ Nombre, pas string
|
||
options: {
|
||
encrypt: true, // ⭐ Pas de SSL en réseau local
|
||
trustServerCertificate: true,
|
||
enableArithAbort: true,
|
||
connectTimeout: 60000,
|
||
requestTimeout: 60000
|
||
},
|
||
pool: {
|
||
max: 10,
|
||
min: 0,
|
||
idleTimeoutMillis: 30000
|
||
}
|
||
};
|
||
|
||
|
||
function nowFR() {
|
||
const d = new Date();
|
||
d.setHours(d.getHours() + 2);
|
||
return d.toISOString().slice(0, 19).replace('T', ' ');
|
||
}
|
||
|
||
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();
|
||
}
|
||
/**
|
||
* Récupère le dernier arrêté validé/clôturé
|
||
*/
|
||
async function getDernierArrete(conn) {
|
||
const [arretes] = await conn.query(`
|
||
SELECT TOP 1 * FROM ArreteComptable
|
||
WHERE Statut IN ('Validé', 'Clôturé')
|
||
ORDER BY DateArrete DESC
|
||
`);
|
||
|
||
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;
|
||
}
|
||
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 TOP 1 * FROM SoldesFiges
|
||
WHERE ArreteId = ?
|
||
AND CollaborateurADId = ?
|
||
AND TypeCongeId = ?
|
||
AND Annee = ?
|
||
`, [dernierArrete.Id, collaborateurId, typeCongeId, annee]);
|
||
|
||
return soldes.length > 0 ? soldes[0] : null;
|
||
}
|
||
|
||
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 TOP 1 Id FROM TypeConge WHERE Nom = ?',
|
||
[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 = new sql.ConnectionPool(dbConfig);
|
||
|
||
|
||
|
||
// ⭐ CONNEXION AU DÉMARRAGE
|
||
pool.connect()
|
||
.then(() => {
|
||
console.log('✅ ==========================================');
|
||
console.log(' CONNECTÉ À SQL SERVER');
|
||
console.log(` Base: ${dbConfig.database}@${dbConfig.host}`);
|
||
console.log('==========================================');
|
||
})
|
||
.catch(err => {
|
||
console.error('❌ ==========================================');
|
||
console.error(' ERREUR CONNEXION SQL SERVER');
|
||
console.error(' Message:', err.message);
|
||
console.error('==========================================');
|
||
});
|
||
|
||
|
||
// ========================================
|
||
// 🔧 MONKEY-PATCH MSSQL POUR SUPPORTER "?"
|
||
// ========================================
|
||
// MONKEY-PATCH MSSQL POUR SUPPORTER ? ET LIMIT
|
||
const originalRequest = sql.Request;
|
||
|
||
sql.Request = function (...args) {
|
||
const request = new originalRequest(...args);
|
||
const originalQuery = request.query.bind(request);
|
||
|
||
request.query = async function (queryText, ...queryArgs) {
|
||
let convertedQuery = queryText; // 🔥 AJOUTÉ ICI
|
||
|
||
try {
|
||
if (!queryArgs || queryArgs.length === 0 || !Array.isArray(queryArgs[0])) {
|
||
return await originalQuery(queryText);
|
||
}
|
||
|
||
const params = queryArgs[0];
|
||
|
||
params.forEach((value, index) => {
|
||
request.input(`param${index}`, value);
|
||
});
|
||
|
||
let paramIndex = 0;
|
||
convertedQuery = convertedQuery.replace(/\?/g, () => `@param${paramIndex++}`);
|
||
|
||
// CONVERSION GETDATE() → GETDATE()
|
||
convertedQuery = convertedQuery.replace(/NOW\(\)/gi, 'GETDATE()');
|
||
|
||
// CONVERSION LEAST() → CASE WHEN
|
||
while (convertedQuery.match(/LEAST\s*\(/i)) {
|
||
convertedQuery = convertedQuery.replace(
|
||
/LEAST\s*\(\s*([^,]+?)\s*,\s*([^)]+?)\s*\)/i,
|
||
'(CASE WHEN $1 < $2 THEN $1 ELSE $2 END)'
|
||
);
|
||
}
|
||
|
||
// LIMIT → TOP conversion
|
||
convertedQuery = convertedQuery.replace(
|
||
/LIMIT\s+(\d+)\s+OFFSET\s+(\d+)/gi,
|
||
'OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY'
|
||
);
|
||
|
||
const simpleLimitMatch = convertedQuery.match(/LIMIT\s+(\d+)(?!\s+OFFSET)/i);
|
||
if (simpleLimitMatch) {
|
||
const limitValue = simpleLimitMatch[1];
|
||
convertedQuery = convertedQuery.replace(/LIMIT\s+\d+(?!\s+OFFSET)/i, '');
|
||
convertedQuery = convertedQuery.replace(
|
||
/SELECT(\s+DISTINCT)?/i,
|
||
`SELECT$1 TOP ${limitValue}`
|
||
);
|
||
}
|
||
|
||
return await originalQuery(convertedQuery);
|
||
} catch (error) {
|
||
console.error('❌ Erreur query SQL:', error.message);
|
||
console.error('Query originale:', queryText.substring(0, 300));
|
||
console.error('Query convertie:', convertedQuery?.substring(0, 300));
|
||
throw error;
|
||
}
|
||
};
|
||
|
||
return request;
|
||
};
|
||
|
||
console.log('✅ Driver mssql patché: support des ?, LIMIT, GETDATE() et LEAST() activé');
|
||
|
||
// ========================================
|
||
// ⭐ WRAPPER POUR COMPATIBILITÉ MYSQL
|
||
// ========================================
|
||
|
||
/**
|
||
* Simule pool.getConnection() de MySQL
|
||
* Retourne un objet avec query(), beginTransaction(), commit(), rollback(), release()
|
||
*/
|
||
// ⭐ WRAPPER POUR COMPATIBILITÉ MYSQL
|
||
// ⭐ WRAPPER POUR COMPATIBILITÉ MYSQL
|
||
pool.getConnection = async function () {
|
||
if (!pool.connected) {
|
||
await pool.connect();
|
||
}
|
||
|
||
let transaction = null;
|
||
|
||
return {
|
||
query: async function (queryText, params = []) {
|
||
// ⭐ FIX: Déclarer parameterizedQuery EN DEHORS du try block
|
||
let parameterizedQuery = queryText;
|
||
|
||
try {
|
||
const request = transaction ? new sql.Request(transaction) : pool.request();
|
||
|
||
// Ajouter les paramètres (@param0, @param1, ...)
|
||
params.forEach((value, index) => {
|
||
request.input(`param${index}`, value);
|
||
});
|
||
|
||
// Remplacer ? par @param0, @param1, etc.
|
||
let paramIndex = 0;
|
||
parameterizedQuery = parameterizedQuery.replace(/\?/g, () => `@param${paramIndex++}`);
|
||
|
||
// ⭐⭐⭐ CONVERSION LIMIT → TOP (VERSION CORRIGÉE) ⭐⭐⭐
|
||
// 1. Gérer LIMIT avec OFFSET
|
||
parameterizedQuery = parameterizedQuery.replace(
|
||
/LIMIT\s+(\d+)\s+OFFSET\s+(\d+)/gi,
|
||
'OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY'
|
||
);
|
||
|
||
// 2. Marquer tous les LIMIT (même sans espace avant)
|
||
parameterizedQuery = parameterizedQuery.replace(
|
||
/\s*LIMIT\s+(\d+)(?!\s+OFFSET)/gi,
|
||
' __LIMIT__$1__'
|
||
);
|
||
|
||
// 3. Injecter TOP après SELECT
|
||
let limitValue = null;
|
||
const limitMatch = parameterizedQuery.match(/__LIMIT__(\d+)__/);
|
||
if (limitMatch) {
|
||
limitValue = limitMatch[1];
|
||
parameterizedQuery = parameterizedQuery.replace(/__LIMIT__\d+__/g, '');
|
||
}
|
||
|
||
if (limitValue) {
|
||
parameterizedQuery = parameterizedQuery.replace(
|
||
/(SELECT\s+(?:DISTINCT\s+)?)/i,
|
||
`$1TOP ${limitValue} `
|
||
);
|
||
}
|
||
|
||
// ⭐ FIX: Convertir TRUE/FALSE en 1/0 pour SQL Server
|
||
parameterizedQuery = parameterizedQuery.replace(/\bTRUE\b/gi, '1');
|
||
parameterizedQuery = parameterizedQuery.replace(/\bFALSE\b/gi, '0');
|
||
|
||
const result = await request.query(parameterizedQuery);
|
||
return [result.recordset || []];
|
||
|
||
} catch (error) {
|
||
console.error('❌ Erreur query SQL:', error.message);
|
||
console.error('Query originale:', queryText);
|
||
console.error('Query convertie:', parameterizedQuery?.substring(0, 500));
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
beginTransaction: async function () {
|
||
transaction = new sql.Transaction(pool);
|
||
await transaction.begin();
|
||
},
|
||
|
||
commit: async function () {
|
||
if (transaction) {
|
||
await transaction.commit();
|
||
transaction = null;
|
||
}
|
||
},
|
||
|
||
rollback: async function () {
|
||
if (transaction) {
|
||
await transaction.rollback();
|
||
transaction = null;
|
||
}
|
||
},
|
||
|
||
release: function () {
|
||
console.log('🔄 Connection released (no-op avec mssql)');
|
||
}
|
||
};
|
||
};
|
||
|
||
// ⭐ pool.query() direct (sans transaction)
|
||
// ⭐ pool.query() direct (sans transaction)
|
||
pool.query = async function (queryText, params = []) {
|
||
if (!pool.connected) {
|
||
await pool.connect();
|
||
}
|
||
|
||
// ⭐ FIX: Déclarer parameterizedQuery EN DEHORS du try/catch implicite
|
||
let parameterizedQuery = queryText;
|
||
|
||
const request = pool.request();
|
||
|
||
params.forEach((value, index) => {
|
||
request.input(`param${index}`, value);
|
||
});
|
||
|
||
let paramIndex = 0;
|
||
parameterizedQuery = parameterizedQuery.replace(/\?/g, () => `@param${paramIndex++}`);
|
||
|
||
// ⭐⭐⭐ CONVERSION LIMIT → TOP (VERSION CORRIGÉE) ⭐⭐⭐
|
||
// 1. Gérer LIMIT avec OFFSET
|
||
parameterizedQuery = parameterizedQuery.replace(
|
||
/LIMIT\s+(\d+)\s+OFFSET\s+(\d+)/gi,
|
||
'OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY'
|
||
);
|
||
|
||
// 2. Marquer tous les LIMIT (même sans espace avant)
|
||
parameterizedQuery = parameterizedQuery.replace(
|
||
/\s*LIMIT\s+(\d+)(?!\s+OFFSET)/gi,
|
||
' __LIMIT__$1__'
|
||
);
|
||
|
||
// 3. Injecter TOP après SELECT
|
||
let limitValue = null;
|
||
const limitMatch = parameterizedQuery.match(/__LIMIT__(\d+)__/);
|
||
if (limitMatch) {
|
||
limitValue = limitMatch[1];
|
||
parameterizedQuery = parameterizedQuery.replace(/__LIMIT__\d+__/g, '');
|
||
}
|
||
|
||
if (limitValue) {
|
||
parameterizedQuery = parameterizedQuery.replace(
|
||
/(SELECT\s+(?:DISTINCT\s+)?)/i,
|
||
`$1TOP ${limitValue} `
|
||
);
|
||
}
|
||
|
||
// ⭐ FIX: Convertir TRUE/FALSE en 1/0 pour SQL Server
|
||
parameterizedQuery = parameterizedQuery.replace(/\bTRUE\b/gi, '1');
|
||
parameterizedQuery = parameterizedQuery.replace(/\bFALSE\b/gi, '0');
|
||
|
||
const result = await request.query(parameterizedQuery);
|
||
return [result.recordset || []];
|
||
};
|
||
|
||
|
||
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.GETDATE() + path.extname(file.originalname));
|
||
}
|
||
});
|
||
const upload = multer({ storage });
|
||
|
||
const medicalStorage = multer.diskStorage({
|
||
destination: './uploads/medical/',
|
||
filename: (req, file, cb) => {
|
||
const uniqueSuffix = Date.GETDATE() + '-' + Math.round(Math.random() * 1E9);
|
||
cb(null, 'medical-' + uniqueSuffix + path.extname(file.originalname));
|
||
}
|
||
});
|
||
|
||
const ACCES_TRANSVERSAUX = {
|
||
'sloisil@ensup.eu': {
|
||
typeAcces: 'service_multi_campus',
|
||
serviceNom: 'Pédagogie',
|
||
description: 'Sandrine - Vue complète Pédagogie (tous campus)'
|
||
},
|
||
'mbouteiller@ensup.eu': {
|
||
typeAcces: 'service_multi_campus',
|
||
serviceNom: 'Admissions',
|
||
description: 'Morgane - Vue complète Admissions (tous campus)'
|
||
},
|
||
'vnoel@ensup.eu': {
|
||
typeAcces: 'service_multi_campus',
|
||
serviceNom: 'Relations Entreprises',
|
||
description: 'Viviane - Vue complète Relations Entreprises (tous campus)'
|
||
},
|
||
'vpierrel@ensup.eu': {
|
||
typeAcces: 'service_multi_campus', // ✅ CORRIGÉ - même type que les autres
|
||
serviceNom: 'Administratif & Financier',
|
||
description: 'Virginie - Vue complète Administratif & Financier (tous campus)'
|
||
}
|
||
};
|
||
|
||
function getUserAccesTransversal(userEmail) {
|
||
const acces = ACCES_TRANSVERSAUX[userEmail?.toLowerCase()] || null;
|
||
if (acces) {
|
||
console.log(`🌐 Accès transversal: ${acces.description}`);
|
||
}
|
||
return acces;
|
||
}
|
||
|
||
|
||
|
||
const uploadMedical = multer({
|
||
storage: medicalStorage,
|
||
limits: { fileSize: 5 * 1024 * 1024 },
|
||
fileFilter: (req, file, cb) => {
|
||
const allowedTypes = ['application/pdf', 'image/jpeg', 'image/jpg', 'image/png'];
|
||
if (allowedTypes.includes(file.mimetype)) {
|
||
cb(null, true);
|
||
} else {
|
||
cb(new Error('Type de fichier non autorisé'));
|
||
}
|
||
}
|
||
});
|
||
import fs from 'fs';
|
||
if (!fs.existsSync('./uploads/medical')) {
|
||
fs.mkdirSync('./uploads/medical', { recursive: true });
|
||
}
|
||
|
||
app.get('/api/events/collaborateur', (req, res) => {
|
||
const userId = req.query.user_id;
|
||
|
||
if (!userId) {
|
||
return res.status(401).json({ error: 'user_id requis' });
|
||
}
|
||
|
||
console.log('🔔 Nouvelle connexion SSE collaborateur:', userId);
|
||
|
||
// ⭐ HEADERS CRITIQUES POUR SSE
|
||
res.setHeader('Content-Type', 'text/event-stream');
|
||
res.setHeader('Cache-Control', 'no-cache, no-transform');
|
||
res.setHeader('Connection', 'keep-alive');
|
||
res.setHeader('X-Accel-Buffering', 'no');
|
||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||
|
||
// ⭐ FLUSH IMMÉDIATEMENT POUR ÉTABLIR LA CONNEXION
|
||
res.flushHeaders();
|
||
|
||
const sendEvent = (data) => {
|
||
try {
|
||
if (res.writableEnded) {
|
||
console.log('⚠️ Tentative d\'envoi sur connexion fermée');
|
||
return false;
|
||
}
|
||
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
||
return true;
|
||
} catch (error) {
|
||
console.error('❌ Erreur envoi SSE:', error);
|
||
return false;
|
||
}
|
||
};
|
||
|
||
const client = {
|
||
userId: parseInt(userId),
|
||
send: sendEvent,
|
||
res: res // ⭐ Garder référence pour vérifier l'état
|
||
};
|
||
|
||
sseClientsCollab.add(client);
|
||
|
||
console.log(`📊 Clients SSE collaborateurs connectés: ${sseClientsCollab.size}`);
|
||
|
||
// ⭐ ÉVÉNEMENT DE CONNEXION
|
||
sendEvent({
|
||
type: 'connected',
|
||
message: 'Connexion établie',
|
||
timestamp: new Date().toISOString()
|
||
});
|
||
|
||
// ⭐ HEARTBEAT AVEC VÉRIFICATION
|
||
const heartbeat = setInterval(() => {
|
||
const success = sendEvent({
|
||
type: 'heartbeat',
|
||
timestamp: new Date().toISOString()
|
||
});
|
||
|
||
if (!success) {
|
||
console.log('💔 Heartbeat échoué, nettoyage...');
|
||
clearInterval(heartbeat);
|
||
sseClientsCollab.delete(client);
|
||
}
|
||
}, 30000); // 30 secondes
|
||
|
||
// ⭐ GESTION PROPRE DE LA DÉCONNEXION
|
||
const cleanup = () => {
|
||
console.log('🔌 Déconnexion SSE collaborateur:', userId);
|
||
clearInterval(heartbeat);
|
||
sseClientsCollab.delete(client);
|
||
console.log(`📊 Clients SSE collaborateurs connectés: ${sseClientsCollab.size}`);
|
||
};
|
||
|
||
req.on('close', cleanup);
|
||
req.on('error', (err) => {
|
||
console.error('❌ Erreur SSE connexion:', err.message);
|
||
cleanup();
|
||
});
|
||
|
||
// ⭐ TIMEOUT DE SÉCURITÉ (optionnel, mais recommandé)
|
||
req.socket.setTimeout(0); // Désactiver timeout pour SSE
|
||
});
|
||
|
||
|
||
const notifyCollabClients = (event, targetUserId = null) => {
|
||
console.log(
|
||
`📢 Notification SSE Collab: ${event.type}`,
|
||
targetUserId ? `pour user ${targetUserId}` : 'pour tous'
|
||
);
|
||
|
||
const deadClients = [];
|
||
|
||
sseClientsCollab.forEach(client => {
|
||
// ⭐ FILTRER PAR USER SI NÉCESSAIRE
|
||
if (targetUserId && client.userId !== targetUserId) {
|
||
return;
|
||
}
|
||
|
||
// ⭐ VÉRIFIER SI LA CONNEXION EST TOUJOURS ACTIVE
|
||
if (client.res && client.res.writableEnded) {
|
||
console.log(`💀 Client mort détecté: ${client.userId}`);
|
||
deadClients.push(client);
|
||
return;
|
||
}
|
||
|
||
// ⭐ ENVOYER L'ÉVÉNEMENT
|
||
const success = client.send(event);
|
||
if (!success) {
|
||
deadClients.push(client);
|
||
}
|
||
});
|
||
|
||
// ⭐ NETTOYER LES CLIENTS MORTS
|
||
deadClients.forEach(client => {
|
||
console.log(`🧹 Nettoyage client mort: ${client.userId}`);
|
||
sseClientsCollab.delete(client);
|
||
});
|
||
|
||
if (deadClients.length > 0) {
|
||
console.log(`📊 Clients SSE après nettoyage: ${sseClientsCollab.size}`);
|
||
}
|
||
};
|
||
|
||
const sseClients = new Set();
|
||
|
||
// 🔌 ROUTE SSE POUR LE CALENDRIER
|
||
app.get('/api/sse', (req, res) => {
|
||
const userId = req.query.user_id;
|
||
|
||
if (!userId) {
|
||
return res.status(400).json({ error: 'user_id requis' });
|
||
}
|
||
|
||
console.log('🔌 Nouvelle connexion SSE:', userId);
|
||
|
||
res.setHeader('Content-Type', 'text/event-stream');
|
||
res.setHeader('Cache-Control', 'no-cache');
|
||
res.setHeader('Connection', 'keep-alive');
|
||
res.setHeader('X-Accel-Buffering', 'no');
|
||
|
||
const sendEvent = (data) => {
|
||
try {
|
||
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
||
} catch (error) {
|
||
console.error('❌ Erreur envoi SSE:', error);
|
||
}
|
||
};
|
||
|
||
const client = { id: userId, send: sendEvent };
|
||
sseClients.add(client);
|
||
|
||
console.log(`📊 Clients SSE connectés: ${sseClients.size}`);
|
||
|
||
// Envoyer un heartbeat initial
|
||
sendEvent({
|
||
type: 'ping',
|
||
message: 'Connexion établie',
|
||
timestamp: new Date().toISOString()
|
||
});
|
||
|
||
// Heartbeat toutes les 30 secondes
|
||
const heartbeat = setInterval(() => {
|
||
try {
|
||
sendEvent({ type: 'ping', timestamp: new Date().toISOString() });
|
||
} catch (error) {
|
||
console.error('❌ Erreur heartbeat:', error);
|
||
clearInterval(heartbeat);
|
||
}
|
||
}, 30000);
|
||
|
||
req.on('close', () => {
|
||
console.log('🔌 Déconnexion SSE:', userId);
|
||
clearInterval(heartbeat);
|
||
sseClients.delete(client);
|
||
console.log(`📊 Clients SSE connectés: ${sseClients.size}`);
|
||
});
|
||
});
|
||
|
||
// 📢 FONCTION POUR NOTIFIER LES CLIENTS
|
||
const notifyClients = (event, userId = null) => {
|
||
console.log(`📢 Notification SSE: ${event.type}${userId ? ` pour ${userId}` : ''}`);
|
||
|
||
sseClients.forEach(client => {
|
||
// Si userId est spécifié, envoyer seulement à ce client
|
||
if (userId && client.id !== userId) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
client.send(event);
|
||
} catch (error) {
|
||
console.error('❌ Erreur envoi event:', error);
|
||
}
|
||
});
|
||
};
|
||
|
||
|
||
app.post('/api/webhook/receive', async (req, res) => {
|
||
try {
|
||
const signature = req.headers['x-webhook-signature'];
|
||
const payload = req.body;
|
||
|
||
console.log('\n📥 === WEBHOOK REÇU (COLLABORATEURS) ===');
|
||
console.log('Event:', payload.event);
|
||
console.log('Data:', JSON.stringify(payload.data, null, 2));
|
||
|
||
// Vérifier la signature
|
||
if (!webhookManager.verifySignature(payload, signature)) {
|
||
console.error('❌ Signature webhook invalide');
|
||
return res.status(401).json({ error: 'Signature invalide' });
|
||
}
|
||
|
||
const { event, data } = payload;
|
||
|
||
// Traiter selon le type d'événement
|
||
switch (event) {
|
||
case EVENTS.COMPTEUR_UPDATED:
|
||
console.log('📊 WEBHOOK COMPTEUR_UPDATED REÇU');
|
||
console.log('Collaborateur:', data.collaborateurId);
|
||
console.log('Type mise à jour:', data.typeUpdate);
|
||
console.log('Type congé:', data.typeConge);
|
||
console.log('Année:', data.annee);
|
||
console.log('Nouveau Total:', data.nouveauTotal + 'j');
|
||
console.log('Nouveau Solde:', data.nouveauSolde + 'j');
|
||
console.log('Source:', data.source);
|
||
|
||
// ✅ PAS D'UPDATE EN BASE (car même DB partagée)
|
||
// ✅ UNIQUEMENT NOTIFICATION SSE pour rafraîchir l'interface
|
||
notifyCollabClients({
|
||
type: 'compteur-updated',
|
||
collaborateurId: data.collaborateurId,
|
||
typeConge: data.typeConge,
|
||
annee: data.annee,
|
||
typeUpdate: data.typeUpdate,
|
||
nouveauTotal: data.nouveauTotal,
|
||
nouveauSolde: data.nouveauSolde,
|
||
source: data.source,
|
||
timestamp: new Date().toISOString()
|
||
}, data.collaborateurId);
|
||
|
||
console.log('✅ Notification SSE envoyée au collaborateur', data.collaborateurId);
|
||
break;
|
||
|
||
case EVENTS.DEMANDE_VALIDATED:
|
||
console.log('\n✅ === WEBHOOK DEMANDE_VALIDATED REÇU ===');
|
||
console.log(` Demande: ${data.demandeId}`);
|
||
console.log(` Statut: ${data.statut}`);
|
||
console.log(` Type: ${data.typeConge}`);
|
||
console.log(` Couleur: ${data.couleurHex}`);
|
||
|
||
// Notifier les clients SSE avec TOUTES les infos
|
||
notifyClients({
|
||
type: 'demande-validated',
|
||
demandeId: data.demandeId,
|
||
statut: data.statut,
|
||
typeConge: data.typeConge,
|
||
couleurHex: data.couleurHex || '#d946ef',
|
||
date: data.date,
|
||
periode: data.periode,
|
||
collaborateurId: data.collaborateurId,
|
||
timestamp: new Date().toISOString()
|
||
}, data.collaborateurId);
|
||
|
||
// Notifier les RH aussi
|
||
notifyClients({
|
||
type: 'demande-list-updated',
|
||
action: 'validation-collab',
|
||
demandeId: data.demandeId,
|
||
statut: data.statut,
|
||
typeConge: data.typeConge,
|
||
couleurHex: data.couleurHex || '#d946ef',
|
||
timestamp: new Date().toISOString()
|
||
});
|
||
|
||
console.log(' 📢 Notifications SSE envoyées');
|
||
break;
|
||
|
||
case EVENTS.DEMANDE_UPDATED:
|
||
console.log('\n✏️ === WEBHOOK DEMANDE_UPDATED REÇU ===');
|
||
console.log(` Demande: ${data.demandeId}`);
|
||
console.log(` Collaborateur: ${data.collaborateurId}`);
|
||
|
||
notifyCollabClients({
|
||
type: 'demande-updated-rh',
|
||
demandeId: data.demandeId,
|
||
timestamp: new Date().toISOString()
|
||
}, data.collaborateurId);
|
||
|
||
console.log(' 📢 Notification modification envoyée');
|
||
break;
|
||
|
||
case EVENTS.DEMANDE_DELETED:
|
||
console.log('\n🗑️ === WEBHOOK DEMANDE_DELETED REÇU ===');
|
||
console.log(` Demande: ${data.demandeId}`);
|
||
console.log(` Collaborateur: ${data.collaborateurId}`);
|
||
|
||
notifyCollabClients({
|
||
type: 'demande-deleted-rh',
|
||
demandeId: data.demandeId,
|
||
timestamp: new Date().toISOString()
|
||
}, data.collaborateurId);
|
||
|
||
console.log(' 📢 Notification suppression envoyée');
|
||
break;
|
||
|
||
default:
|
||
console.warn(`⚠️ Type d'événement webhook inconnu: ${event}`);
|
||
}
|
||
|
||
res.json({ success: true, message: 'Webhook traité' });
|
||
|
||
} catch (error) {
|
||
console.error('❌ Erreur traitement webhook:', error);
|
||
res.status(500).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
|
||
|
||
function getDateFinMoisPrecedent(referenceDate = new Date()) {
|
||
const now = new Date(referenceDate);
|
||
now.setHours(0, 0, 0, 0);
|
||
return new Date(now.getFullYear(), now.getMonth(), 0);
|
||
}
|
||
|
||
function parseDateYYYYMMDD(s) {
|
||
if (!s) return null;
|
||
if (/^\d{4}-\d{2}-\d{2}$/.test(s)) {
|
||
const [y, m, d] = s.split('-').map(Number);
|
||
return new Date(y, m - 1, d);
|
||
}
|
||
return new Date(s);
|
||
}
|
||
|
||
const LEAVE_RULES = {
|
||
CP: {
|
||
nom: 'Congé payé',
|
||
joursAnnuels: 25,
|
||
periodeDebut: { mois: 6, jour: 1 },
|
||
periodeFin: { mois: 5, jour: 31 },
|
||
acquisitionMensuelle: 25 / 12,
|
||
reportable: true,
|
||
periodeReport: 'exercice'
|
||
},
|
||
RTT: {
|
||
nom: 'RTT',
|
||
joursAnnuels: 10,
|
||
periodeDebut: { mois: 1, jour: 1 },
|
||
periodeFin: { mois: 12, jour: 31 },
|
||
acquisitionMensuelle: 10 / 12,
|
||
reportable: false,
|
||
periodeReport: null
|
||
}
|
||
};
|
||
|
||
// ========================================
|
||
// NOUVELLES FONCTIONS POUR RTT VARIABLES
|
||
// ========================================
|
||
|
||
/**
|
||
* Récupère la configuration RTT pour une année et un type de contrat donnés
|
||
* RÈGLES :
|
||
* - 37h : toujours 10 RTT/an (0.8333/mois)
|
||
* - Forfait jour 2025 : 10 RTT/an (0.8333/mois)
|
||
* - Forfait jour 2026+ : 12 RTT/an (1.0/mois)
|
||
*/
|
||
async function getConfigurationRTT(conn, annee, typeContrat = '37h') {
|
||
try {
|
||
// D'abord chercher en base de données
|
||
const [config] = await conn.query(
|
||
`SELECT JoursAnnuels, AcquisitionMensuelle
|
||
FROM ConfigurationRTT
|
||
WHERE Annee = ? AND TypeContrat = ?
|
||
LIMIT 1`,
|
||
[annee, typeContrat]
|
||
);
|
||
|
||
if (config.length > 0) {
|
||
return {
|
||
joursAnnuels: parseFloat(config[0].JoursAnnuels),
|
||
acquisitionMensuelle: parseFloat(config[0].AcquisitionMensuelle)
|
||
};
|
||
}
|
||
|
||
// Si pas en base, utiliser les règles par défaut
|
||
console.warn(`⚠️ Pas de config RTT en base pour ${annee}/${typeContrat}, utilisation des règles par défaut`);
|
||
|
||
return getConfigurationRTTDefaut(annee, typeContrat);
|
||
|
||
} catch (error) {
|
||
console.error('Erreur getConfigurationRTT:', error);
|
||
return getConfigurationRTTDefaut(annee, typeContrat);
|
||
}
|
||
}
|
||
function getConfigurationRTTDefaut(annee, typeContrat) {
|
||
// 37h : toujours 10 RTT/an
|
||
if (typeContrat === '37h' || typeContrat === 'temps_partiel') {
|
||
return {
|
||
joursAnnuels: 10,
|
||
acquisitionMensuelle: 10 / 12 // 0.8333
|
||
};
|
||
}
|
||
|
||
// Forfait jour : dépend de l'année
|
||
if (typeContrat === 'forfait_jour') {
|
||
if (annee <= 2025) {
|
||
// 2025 et avant : 10 RTT/an
|
||
return {
|
||
joursAnnuels: 10,
|
||
acquisitionMensuelle: 10 / 12 // 0.8333
|
||
};
|
||
} else {
|
||
// 2026 et après : 12 RTT/an
|
||
return {
|
||
joursAnnuels: 12,
|
||
acquisitionMensuelle: 12 / 12 // 1.0
|
||
};
|
||
}
|
||
}
|
||
|
||
// Par défaut : 10 RTT/an
|
||
return {
|
||
joursAnnuels: 10,
|
||
acquisitionMensuelle: 10 / 12
|
||
};
|
||
}
|
||
|
||
|
||
/**
|
||
* Calcule l'acquisition RTT avec la formule Excel exacte
|
||
*/
|
||
async function calculerAcquisitionRTT(conn, collaborateurId, dateReference = new Date()) {
|
||
const d = new Date(dateReference);
|
||
d.setHours(0, 0, 0, 0);
|
||
const annee = d.getFullYear();
|
||
|
||
// 1️⃣ Récupérer les infos du collaborateur
|
||
const [collabInfo] = await conn.query(
|
||
`SELECT TypeContrat, DateEntree, role FROM CollaborateurAD WHERE id = ?`,
|
||
[collaborateurId]
|
||
);
|
||
|
||
if (collabInfo.length === 0) {
|
||
throw new Error(`Collaborateur ${collaborateurId} non trouvé`);
|
||
}
|
||
|
||
const typeContrat = collabInfo[0].TypeContrat || '37h';
|
||
const dateEntree = collabInfo[0].DateEntree;
|
||
const isApprenti = collabInfo[0].role === 'Apprenti';
|
||
|
||
// 2️⃣ Apprentis = pas de RTT
|
||
if (isApprenti) {
|
||
return {
|
||
acquisition: 0,
|
||
moisTravailles: 0,
|
||
config: { joursAnnuels: 0, acquisitionMensuelle: 0 },
|
||
typeContrat: typeContrat
|
||
};
|
||
}
|
||
|
||
// 3️⃣ Récupérer la configuration RTT (avec règles 2025/2026)
|
||
const config = await getConfigurationRTT(conn, annee, typeContrat);
|
||
|
||
console.log(`📊 Config RTT ${annee}/${typeContrat}: ${config.joursAnnuels}j/an (${config.acquisitionMensuelle.toFixed(4)}/mois)`);
|
||
|
||
// 4️⃣ Début d'acquisition = 01/01/N ou date d'entrée si postérieure
|
||
let dateDebutAcquis = new Date(annee, 0, 1); // 01/01/N
|
||
dateDebutAcquis.setHours(0, 0, 0, 0);
|
||
|
||
if (dateEntree) {
|
||
const entree = new Date(dateEntree);
|
||
entree.setHours(0, 0, 0, 0);
|
||
|
||
if (entree.getFullYear() === annee && entree > dateDebutAcquis) {
|
||
dateDebutAcquis = entree;
|
||
}
|
||
|
||
if (entree.getFullYear() > annee) {
|
||
return {
|
||
acquisition: 0,
|
||
moisTravailles: 0,
|
||
config: config,
|
||
typeContrat: typeContrat
|
||
};
|
||
}
|
||
}
|
||
|
||
// 5️⃣ Calculer avec la formule Excel
|
||
const acquisition = calculerAcquisitionFormuleExcel(dateDebutAcquis, d, config.acquisitionMensuelle);
|
||
|
||
// 6️⃣ Calculer les mois travaillés (pour info)
|
||
const moisTravailles = config.acquisitionMensuelle > 0
|
||
? acquisition / config.acquisitionMensuelle
|
||
: 0;
|
||
|
||
// 7️⃣ Plafonner au maximum annuel
|
||
const acquisitionFinale = Math.min(acquisition, config.joursAnnuels);
|
||
|
||
return {
|
||
acquisition: Math.round(acquisitionFinale * 100) / 100,
|
||
moisTravailles: Math.round(moisTravailles * 100) / 100,
|
||
config: config,
|
||
typeContrat: typeContrat
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Calcule l'acquisition avec la formule Excel exacte :
|
||
* E1 * ((JOUR(FIN.MOIS(B1;0)) - JOUR(B1) + 1) / JOUR(FIN.MOIS(B1;0))
|
||
* + DATEDIF(B1;B2;"m") - 1
|
||
* + JOUR(B2) / JOUR(FIN.MOIS(B2;0)))
|
||
*/
|
||
function calculerAcquisitionFormuleExcel(dateDebut, dateReference, coeffMensuel) {
|
||
const b1 = new Date(dateDebut);
|
||
const b2 = new Date(dateReference);
|
||
b1.setHours(0, 0, 0, 0);
|
||
b2.setHours(0, 0, 0, 0);
|
||
|
||
// Si date référence avant date début
|
||
if (b2 < b1) {
|
||
return 0;
|
||
}
|
||
|
||
// Si même mois et même année
|
||
if (b1.getFullYear() === b2.getFullYear() && b1.getMonth() === b2.getMonth()) {
|
||
const joursTotal = new Date(b2.getFullYear(), b2.getMonth() + 1, 0).getDate();
|
||
const joursAcquis = b2.getDate() - b1.getDate() + 1;
|
||
return Math.round((joursAcquis / joursTotal) * coeffMensuel * 100) / 100;
|
||
}
|
||
|
||
// 1️⃣ Fraction du PREMIER mois
|
||
const joursFinMoisB1 = new Date(b1.getFullYear(), b1.getMonth() + 1, 0).getDate();
|
||
const jourB1 = b1.getDate();
|
||
const fractionPremierMois = (joursFinMoisB1 - jourB1 + 1) / joursFinMoisB1;
|
||
|
||
// 2️⃣ Mois COMPLETS entre
|
||
const moisComplets = dateDifMonths(b1, b2) - 1;
|
||
|
||
// 3️⃣ Fraction du DERNIER mois
|
||
const joursFinMoisB2 = new Date(b2.getFullYear(), b2.getMonth() + 1, 0).getDate();
|
||
const jourB2 = b2.getDate();
|
||
const fractionDernierMois = jourB2 / joursFinMoisB2;
|
||
|
||
// 4️⃣ Total
|
||
const totalMois = fractionPremierMois + Math.max(0, moisComplets) + fractionDernierMois;
|
||
const acquisition = totalMois * coeffMensuel;
|
||
|
||
return Math.round(acquisition * 100) / 100;
|
||
}
|
||
/**
|
||
* Équivalent de DATEDIF(date1, date2, "m") en JavaScript
|
||
*/
|
||
function dateDifMonths(date1, date2) {
|
||
const d1 = new Date(date1);
|
||
const d2 = new Date(date2);
|
||
|
||
let months = (d2.getFullYear() - d1.getFullYear()) * 12;
|
||
months += d2.getMonth() - d1.getMonth();
|
||
|
||
// Si le jour de d2 < jour de d1, on n'a pas encore complété le mois
|
||
if (d2.getDate() < d1.getDate()) {
|
||
months--;
|
||
}
|
||
|
||
return Math.max(0, months);
|
||
}
|
||
/**
|
||
* Calcule l'acquisition CP avec la formule Excel exacte
|
||
*/
|
||
function calculerAcquisitionCP(dateReference = new Date(), dateEntree = null) {
|
||
const d = new Date(dateReference);
|
||
d.setHours(0, 0, 0, 0);
|
||
|
||
const annee = d.getFullYear();
|
||
const mois = d.getMonth() + 1;
|
||
|
||
// 1️⃣ Déterminer le début de l'exercice CP (01/06)
|
||
let exerciceDebut;
|
||
if (mois >= 6) {
|
||
exerciceDebut = new Date(annee, 5, 1); // 01/06/N
|
||
} else {
|
||
exerciceDebut = new Date(annee - 1, 5, 1); // 01/06/N-1
|
||
}
|
||
exerciceDebut.setHours(0, 0, 0, 0);
|
||
|
||
// 2️⃣ Ajuster si date d'entrée postérieure
|
||
let dateDebutAcquis = new Date(exerciceDebut);
|
||
if (dateEntree) {
|
||
const entree = new Date(dateEntree);
|
||
entree.setHours(0, 0, 0, 0);
|
||
if (entree > exerciceDebut) {
|
||
dateDebutAcquis = entree;
|
||
}
|
||
}
|
||
|
||
// 3️⃣ Calculer avec la formule Excel
|
||
const coeffCP = 25 / 12; // 2.0833
|
||
const acquisition = calculerAcquisitionFormuleExcel(dateDebutAcquis, d, coeffCP);
|
||
|
||
// 4️⃣ Plafonner à 25 jours
|
||
return Math.min(acquisition, 25);
|
||
}
|
||
// ========================================
|
||
// CALCUL CP INTELLIGENT (MODE AUTO)
|
||
// ========================================
|
||
|
||
/**
|
||
* Calcule l'acquisition CP avec détection automatique du mode
|
||
*/
|
||
function calculerAcquisitionCP_Smart(dateReference = new Date(), dateEntree = null) {
|
||
const d = new Date(dateReference);
|
||
d.setHours(0, 0, 0, 0);
|
||
|
||
const annee = d.getFullYear();
|
||
const mois = d.getMonth() + 1;
|
||
|
||
// 1️⃣ Déterminer le début de l'exercice CP (01/06)
|
||
let exerciceDebut;
|
||
if (mois >= 6) {
|
||
exerciceDebut = new Date(annee, 5, 1); // 01/06/N
|
||
} else {
|
||
exerciceDebut = new Date(annee - 1, 5, 1); // 01/06/N-1
|
||
}
|
||
exerciceDebut.setHours(0, 0, 0, 0);
|
||
|
||
// 2️⃣ Obtenir le mode de calcul
|
||
const modeInfo = getModeCalcul('CP', dateEntree);
|
||
const dateDebutAcquis = modeInfo.dateDebut;
|
||
|
||
console.log(` 📅 ${modeInfo.description}`);
|
||
|
||
// 3️⃣ Calculer avec la formule Excel
|
||
const coeffCP = 25 / 12; // 2.0833
|
||
const acquisition = calculerAcquisitionFormuleExcel(dateDebutAcquis, d, coeffCP);
|
||
|
||
// 4️⃣ Plafonner à 25 jours
|
||
return Math.min(acquisition, 25);
|
||
}
|
||
|
||
// ========================================
|
||
// CALCUL RTT INTELLIGENT (MODE AUTO)
|
||
// ========================================
|
||
|
||
/**
|
||
* Calcule l'acquisition RTT avec détection automatique du mode
|
||
*/
|
||
async function calculerAcquisitionRTT_Smart(conn, collaborateurId, dateReference = new Date()) {
|
||
const d = new Date(dateReference);
|
||
d.setHours(0, 0, 0, 0);
|
||
const annee = d.getFullYear();
|
||
|
||
// 1️⃣ Récupérer les infos du collaborateur
|
||
const [collabInfo] = await conn.query(
|
||
`SELECT TypeContrat, DateEntree, role FROM CollaborateurAD WHERE id = ?`,
|
||
[collaborateurId]
|
||
);
|
||
|
||
if (collabInfo.length === 0) {
|
||
throw new Error(`Collaborateur ${collaborateurId} non trouvé`);
|
||
}
|
||
|
||
const typeContrat = collabInfo[0].TypeContrat || '37h';
|
||
const dateEntree = collabInfo[0].DateEntree;
|
||
const isApprenti = collabInfo[0].role === 'Apprenti';
|
||
|
||
// 2️⃣ Apprentis = pas de RTT
|
||
if (isApprenti) {
|
||
return {
|
||
acquisition: 0,
|
||
moisTravailles: 0,
|
||
config: { joursAnnuels: 0, acquisitionMensuelle: 0 },
|
||
typeContrat: typeContrat,
|
||
mode: 'APPRENTI'
|
||
};
|
||
}
|
||
|
||
// 3️⃣ Récupérer la configuration RTT (avec règles 2025/2026)
|
||
const config = await getConfigurationRTT(conn, annee, typeContrat);
|
||
|
||
// 4️⃣ Obtenir le mode de calcul
|
||
const modeInfo = getModeCalcul('RTT', dateEntree);
|
||
const dateDebutAcquis = modeInfo.dateDebut;
|
||
|
||
console.log(` 📅 ${modeInfo.description}`);
|
||
|
||
// 5️⃣ Calculer avec la formule Excel
|
||
const acquisition = calculerAcquisitionFormuleExcel(dateDebutAcquis, d, config.acquisitionMensuelle);
|
||
|
||
// 6️⃣ Calculer les mois travaillés (pour info)
|
||
const moisTravailles = config.acquisitionMensuelle > 0
|
||
? acquisition / config.acquisitionMensuelle
|
||
: 0;
|
||
|
||
// 7️⃣ Plafonner au maximum annuel
|
||
const acquisitionFinale = Math.min(acquisition, config.joursAnnuels);
|
||
|
||
return {
|
||
acquisition: Math.round(acquisitionFinale * 100) / 100,
|
||
moisTravailles: Math.round(moisTravailles * 100) / 100,
|
||
config: config,
|
||
typeContrat: typeContrat,
|
||
mode: modeInfo.mode
|
||
};
|
||
}
|
||
// ========================================
|
||
// FONCTION DE DÉTECTION AUTOMATIQUE DU MODE
|
||
// ========================================
|
||
|
||
/**
|
||
* Détermine automatiquement si on doit utiliser le mode transition
|
||
* en comparant la date actuelle avec le début de l'exercice
|
||
*/
|
||
function isInExerciceActuel(typeConge) {
|
||
const today = new Date();
|
||
today.setHours(0, 0, 0, 0);
|
||
|
||
const currentYear = today.getFullYear();
|
||
const currentMonth = today.getMonth() + 1; // 1-12
|
||
|
||
if (typeConge === 'CP') {
|
||
// Exercice CP : 01/06/N → 31/05/N+1
|
||
|
||
// Déterminer le début de l'exercice ACTUEL
|
||
let debutExercice;
|
||
if (currentMonth >= 6) {
|
||
// On est entre juin et décembre → exercice commence le 01/06 de cette année
|
||
debutExercice = new Date(currentYear, 5, 1); // 01/06/N
|
||
} else {
|
||
// On est entre janvier et mai → exercice a commencé le 01/06 de l'année dernière
|
||
debutExercice = new Date(currentYear - 1, 5, 1); // 01/06/N-1
|
||
}
|
||
|
||
// ⭐ Si aujourd'hui est le PREMIER JOUR du nouvel exercice ou après → MODE NORMAL
|
||
// ⭐ Si on est encore dans l'exercice commencé avant → MODE TRANSITION
|
||
|
||
const finExercice = new Date(debutExercice);
|
||
finExercice.setFullYear(finExercice.getFullYear() + 1);
|
||
finExercice.setMonth(4, 31); // 31/05/N+1
|
||
|
||
// Si on vient de passer le 01/06, c'est le nouvel exercice → mode NORMAL
|
||
const nouveauExercice = new Date(currentYear, 5, 1);
|
||
return today < nouveauExercice; // TRUE = mode transition (avant le 01/06)
|
||
|
||
} else if (typeConge === 'RTT') {
|
||
// Année RTT : 01/01/N → 31/12/N
|
||
|
||
const debutAnnee = new Date(currentYear, 0, 1); // 01/01/N
|
||
|
||
// Si on vient de passer le 01/01, c'est la nouvelle année → mode NORMAL
|
||
return today < debutAnnee; // TRUE = mode transition (avant le 01/01)
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Version améliorée : retourne le mode ET la date de début à utiliser
|
||
*/
|
||
/**
|
||
* Détermine le mode de calcul et la date de début selon le contexte
|
||
* @param {string} typeConge - 'CP' ou 'RTT'
|
||
* @param {Date|null} dateEntree - Date d'entrée du collaborateur
|
||
* @returns {Object} { mode, dateDebut, description }
|
||
*/
|
||
function getModeCalcul(typeConge, dateEntree = null) {
|
||
const today = new Date();
|
||
today.setHours(0, 0, 0, 0);
|
||
|
||
const currentYear = today.getFullYear();
|
||
const currentMonth = today.getMonth() + 1;
|
||
|
||
if (typeConge === 'CP') {
|
||
// Déterminer le début de l'exercice ACTUEL
|
||
let debutExerciceActuel;
|
||
if (currentMonth >= 6) {
|
||
debutExerciceActuel = new Date(currentYear, 5, 1); // 01/06/N
|
||
} else {
|
||
debutExerciceActuel = new Date(currentYear - 1, 5, 1); // 01/06/N-1
|
||
}
|
||
debutExerciceActuel.setHours(0, 0, 0, 0);
|
||
|
||
// ⭐ RÈGLE DE BASCULE :
|
||
// On bascule en mode NORMAL uniquement si :
|
||
// 1. On est dans un NOUVEL exercice (après le prochain 01/06)
|
||
// 2. ET la personne est arrivée APRÈS le début de ce nouvel exercice
|
||
|
||
// Calculer le début du PROCHAIN exercice
|
||
const prochainExercice = new Date(currentYear, 5, 1); // 01/06/N
|
||
if (currentMonth < 6) {
|
||
// Si on est avant juin, le prochain exercice est cette année
|
||
prochainExercice.setFullYear(currentYear);
|
||
} else {
|
||
// Si on est après juin, le prochain exercice est l'année prochaine
|
||
prochainExercice.setFullYear(currentYear + 1);
|
||
}
|
||
|
||
// ⭐ BASCULE : on passe en mode NORMAL seulement si aujourd'hui >= prochain exercice
|
||
if (today >= prochainExercice && dateEntree) {
|
||
const entree = new Date(dateEntree);
|
||
entree.setHours(0, 0, 0, 0);
|
||
|
||
// Si la personne est arrivée APRÈS le début du nouvel exercice
|
||
if (entree >= prochainExercice) {
|
||
return {
|
||
mode: 'NORMAL',
|
||
dateDebut: entree,
|
||
description: `CP avec DateEntree (${entree.toLocaleDateString('fr-FR')})`
|
||
};
|
||
}
|
||
}
|
||
|
||
// ⭐ MODE TRANSITION : calcul depuis début de l'exercice actuel (SANS DateEntree)
|
||
return {
|
||
mode: 'TRANSITION',
|
||
dateDebut: debutExerciceActuel,
|
||
description: `CP mode transition (depuis ${debutExerciceActuel.toLocaleDateString('fr-FR')})`
|
||
};
|
||
|
||
} else if (typeConge === 'RTT') {
|
||
const debutAnneeActuelle = new Date(currentYear, 0, 1); // 01/01/N
|
||
debutAnneeActuelle.setHours(0, 0, 0, 0);
|
||
|
||
// ⭐ RÈGLE DE BASCULE :
|
||
// On bascule en mode NORMAL uniquement si :
|
||
// 1. On est dans une NOUVELLE année (après le prochain 01/01)
|
||
// 2. ET la personne est arrivée APRÈS le début de cette nouvelle année
|
||
|
||
// Calculer le début de la PROCHAINE année
|
||
const prochaineAnnee = new Date(currentYear + 1, 0, 1); // 01/01/N+1
|
||
|
||
// ⭐ BASCULE : on passe en mode NORMAL seulement si aujourd'hui >= prochaine année
|
||
if (today >= prochaineAnnee && dateEntree) {
|
||
const entree = new Date(dateEntree);
|
||
entree.setHours(0, 0, 0, 0);
|
||
|
||
// Si la personne est arrivée APRÈS le début de la nouvelle année
|
||
if (entree >= prochaineAnnee) {
|
||
return {
|
||
mode: 'NORMAL',
|
||
dateDebut: entree,
|
||
description: `RTT avec DateEntree (${entree.toLocaleDateString('fr-FR')})`
|
||
};
|
||
}
|
||
}
|
||
|
||
// ⭐ MODE TRANSITION : calcul depuis début de l'année actuelle (SANS DateEntree)
|
||
return {
|
||
mode: 'TRANSITION',
|
||
dateDebut: debutAnneeActuelle,
|
||
description: `RTT mode transition (depuis ${debutAnneeActuelle.toLocaleDateString('fr-FR')})`
|
||
};
|
||
}
|
||
|
||
return null;
|
||
}
|
||
// ========================================
|
||
// TÂCHES CRON
|
||
// ========================================
|
||
|
||
cron.schedule('1 0 1 1 *', async () => {
|
||
console.log('\n🎉 ===== RÉINITIALISATION RTT - 1ER JANVIER =====');
|
||
|
||
const conn = await pool.getConnection();
|
||
|
||
try {
|
||
await conn.beginTransaction();
|
||
|
||
const today = new Date();
|
||
const nouvelleAnnee = today.getFullYear();
|
||
const ancienneAnnee = nouvelleAnnee - 1;
|
||
|
||
console.log(` 📅 Passage de ${ancienneAnnee} à ${nouvelleAnnee}`);
|
||
|
||
const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ?', ['RTT']);
|
||
|
||
if (rttType.length === 0) {
|
||
throw new Error('Type de congé RTT introuvable');
|
||
}
|
||
|
||
const rttTypeId = rttType[0].Id;
|
||
|
||
const [collaborateurs] = await conn.query(`
|
||
SELECT id, prenom, nom, TypeContrat, role
|
||
FROM CollaborateurAD
|
||
WHERE (Actif = 1 OR Actif IS NULL)
|
||
AND (role IS NULL OR role != 'Apprenti')
|
||
`);
|
||
|
||
console.log(` 👥 ${collaborateurs.length} collaborateurs à traiter`);
|
||
|
||
let compteursReinitialises = 0;
|
||
|
||
for (const collab of collaborateurs) {
|
||
const collaborateurId = collab.id;
|
||
|
||
const [ancienCompteur] = await conn.query(`
|
||
SELECT Total, Solde
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collaborateurId, rttTypeId, ancienneAnnee]);
|
||
|
||
if (ancienCompteur.length > 0) {
|
||
const soldeAncien = parseFloat(ancienCompteur[0].Solde || 0);
|
||
console.log(` 👤 ${collab.prenom} ${collab.nom}: ${soldeAncien.toFixed(2)}j RTT perdus`);
|
||
|
||
await conn.query(`
|
||
UPDATE CompteurConges
|
||
SET DerniereMiseAJour = GETDATE()
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collaborateurId, rttTypeId, ancienneAnnee]);
|
||
}
|
||
|
||
const [nouveauCompteur] = await conn.query(`
|
||
SELECT id FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collaborateurId, rttTypeId, nouvelleAnnee]);
|
||
|
||
if (nouveauCompteur.length === 0) {
|
||
await conn.query(`
|
||
INSERT INTO CompteurConges (
|
||
CollaborateurADId, TypeCongeId, Annee,
|
||
Total, Solde, SoldeReporte, DerniereMiseAJour
|
||
)
|
||
VALUES (?, ?, ?, 0, 0, 0, GETDATE())
|
||
`, [collaborateurId, rttTypeId, nouvelleAnnee]);
|
||
|
||
console.log(` ✅ Compteur RTT ${nouvelleAnnee} créé à 0`);
|
||
} else {
|
||
await conn.query(`
|
||
UPDATE CompteurConges
|
||
SET Total = 0, Solde = 0, SoldeReporte = 0, DerniereMiseAJour = GETDATE()
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collaborateurId, rttTypeId, nouvelleAnnee]);
|
||
|
||
console.log(` ✅ Compteur RTT ${nouvelleAnnee} réinitialisé à 0`);
|
||
}
|
||
|
||
compteursReinitialises++;
|
||
}
|
||
|
||
await conn.commit();
|
||
console.log(`\n✅ Réinitialisation RTT terminée : ${compteursReinitialises} compteurs`);
|
||
|
||
} catch (error) {
|
||
await conn.rollback();
|
||
console.error('❌ Erreur réinitialisation RTT:', error);
|
||
} finally {
|
||
conn.release();
|
||
}
|
||
}, {
|
||
timezone: "Europe/Paris"
|
||
});
|
||
|
||
// ============================================================================
|
||
// 📅 CRON REPORT CP - 31 mai 23h59
|
||
// ============================================================================
|
||
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 ARRÊTÉS MENSUELS
|
||
// ============================================================================
|
||
cron.schedule('55 23 28-31 * *', async () => {
|
||
const today = new Date();
|
||
const lastDay = new Date(today.getFullYear(), today.getMonth() + 1, 0);
|
||
|
||
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;
|
||
const dateArrete = today.toISOString().split('T')[0];
|
||
|
||
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;
|
||
}
|
||
|
||
const [result] = await conn.query(`
|
||
INSERT INTO ArreteComptable
|
||
(DateArrete, Annee, Mois, Libelle, Description, Statut, DateCreation)
|
||
VALUES (?, ?, ?, ?, ?, 'En cours', GETDATE())
|
||
`, [
|
||
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}`);
|
||
|
||
await conn.query('CALL sp_creer_snapshot_arrete(?)', [arreteId]);
|
||
console.log(`📸 [CRON] Snapshot créé pour l'arrêté ${arreteId}`);
|
||
|
||
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();
|
||
}
|
||
}
|
||
});
|
||
|
||
// ============================================================================
|
||
// 📧 CRON MAILS COMPTE-RENDU
|
||
// ============================================================================
|
||
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();
|
||
});
|
||
|
||
// ============================================================================
|
||
// 🔔 CRON RELANCES
|
||
// ============================================================================
|
||
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();
|
||
});
|
||
|
||
// ============================================================================
|
||
// 🚀 RATTRAPAGE IMMÉDIAT - 5 minutes après démarrage
|
||
// ============================================================================
|
||
setTimeout(() => {
|
||
console.log('🚀 ===== EXÉCUTION IMMÉDIATE - RATTRAPAGE DEPUIS LE 1ER DU MOIS =====');
|
||
updateMonthlyCounters(true); // true = mode rattrapage
|
||
}, 5 * 60 * 1000);
|
||
|
||
console.log('⏰ CRON quotidien programmé : 00h00 Europe/Paris');
|
||
console.log('⏰ CRON réinitialisation RTT programmé : 1er janvier à 00h01');
|
||
console.log(`⏰ CRON immédiat programmé dans 5 minutes`);
|
||
|
||
|
||
// ⭐ Fonction helper pour les noms de mois
|
||
function getMonthName(mois) {
|
||
const mois_names = ['', 'Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin',
|
||
'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'];
|
||
return mois_names[mois] || mois;
|
||
}
|
||
|
||
async function getGraphToken() {
|
||
try {
|
||
const params = new URLSearchParams({
|
||
grant_type: 'client_credentials',
|
||
client_id: AZURE_CONFIG.clientId,
|
||
client_secret: AZURE_CONFIG.clientSecret,
|
||
scope: 'https://graph.microsoft.com/.default'
|
||
});
|
||
const response = await axios.post(
|
||
`https://login.microsoftonline.com/${AZURE_CONFIG.tenantId}/oauth2/v2.0/token`,
|
||
params.toString(),
|
||
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
|
||
);
|
||
return response.data.access_token;
|
||
} catch (error) {
|
||
console.error('Erreur obtention token:', error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
async function sendMailGraph(accessToken, fromEmail, toEmail, subject, bodyHtml) {
|
||
try {
|
||
await axios.post(
|
||
`https://graph.microsoft.com/v1.0/users/${fromEmail}/sendMail`,
|
||
{
|
||
message: {
|
||
subject,
|
||
body: { contentType: 'HTML', content: bodyHtml },
|
||
toRecipients: [{ emailAddress: { address: toEmail } }]
|
||
},
|
||
saveToSentItems: false
|
||
},
|
||
{
|
||
headers: {
|
||
Authorization: `Bearer ${accessToken}`,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
}
|
||
);
|
||
return true;
|
||
} catch (error) {
|
||
console.error('Erreur envoi email:', error);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
function getWorkingDays(startDate, endDate) {
|
||
let workingDays = 0;
|
||
const current = new Date(startDate);
|
||
const end = new Date(endDate);
|
||
while (current <= end) {
|
||
const dayOfWeek = current.getDay();
|
||
if (dayOfWeek !== 0 && dayOfWeek !== 6) {
|
||
workingDays++;
|
||
}
|
||
current.setDate(current.getDate() + 1);
|
||
}
|
||
return workingDays;
|
||
}
|
||
|
||
function formatDate(date) {
|
||
const d = new Date(date);
|
||
const day = String(d.getDate()).padStart(2, '0');
|
||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||
const year = d.getFullYear();
|
||
return `${day}/${month}/${year}`;
|
||
}
|
||
|
||
function getExerciceCP(date = new Date()) {
|
||
const d = new Date(date);
|
||
const annee = d.getFullYear();
|
||
const mois = d.getMonth() + 1;
|
||
if (mois >= 1 && mois <= 5) {
|
||
return `${annee - 1}-${annee}`;
|
||
}
|
||
return `${annee}-${annee + 1}`;
|
||
}
|
||
|
||
function getMoisTravaillesCP(date = new Date(), dateEntree = null) {
|
||
const d = new Date(date);
|
||
d.setHours(0, 0, 0, 0);
|
||
|
||
const annee = d.getFullYear();
|
||
const mois = d.getMonth() + 1;
|
||
|
||
let debutExercice;
|
||
if (mois >= 6) {
|
||
debutExercice = new Date(annee, 5, 1);
|
||
} else {
|
||
debutExercice = new Date(annee - 1, 5, 1);
|
||
}
|
||
debutExercice.setHours(0, 0, 0, 0);
|
||
|
||
if (dateEntree) {
|
||
const entree = new Date(dateEntree);
|
||
entree.setHours(0, 0, 0, 0);
|
||
if (entree > debutExercice) {
|
||
debutExercice = entree;
|
||
}
|
||
}
|
||
|
||
// ✅ Calculer jusqu'à aujourd'hui
|
||
const diffMs = d - debutExercice;
|
||
const diffJours = Math.floor(diffMs / (1000 * 60 * 60 * 24)) + 1;
|
||
const moisTravailles = diffJours / 30.44;
|
||
|
||
return Math.max(0, Math.min(12, moisTravailles));
|
||
}
|
||
|
||
|
||
function getMoisTravaillesRTT(date = new Date(), dateEntree = null) {
|
||
const d = new Date(date);
|
||
d.setHours(0, 0, 0, 0);
|
||
const annee = d.getFullYear();
|
||
let debutAnnee = new Date(annee, 0, 1);
|
||
debutAnnee.setHours(0, 0, 0, 0);
|
||
if (dateEntree) {
|
||
const entree = new Date(dateEntree);
|
||
entree.setHours(0, 0, 0, 0);
|
||
if (entree.getFullYear() === annee && entree > debutAnnee) {
|
||
debutAnnee = entree;
|
||
} else if (entree.getFullYear() > annee) {
|
||
return 0;
|
||
}
|
||
}
|
||
const diffMs = d - debutAnnee;
|
||
const diffJours = Math.floor(diffMs / (1000 * 60 * 60 * 24)) + 1;
|
||
const moisTravailles = diffJours / 30.44;
|
||
return Math.max(0, Math.min(12, moisTravailles));
|
||
}
|
||
|
||
function calculerAcquisitionCumulee(typeConge, dateReference = new Date(), dateEntree = null) {
|
||
const rules = LEAVE_RULES[typeConge];
|
||
if (!rules) return 0;
|
||
let moisTravailles;
|
||
if (typeConge === 'CP') {
|
||
moisTravailles = getMoisTravaillesCP(dateReference, dateEntree);
|
||
} else {
|
||
moisTravailles = getMoisTravaillesRTT(dateReference, dateEntree);
|
||
}
|
||
const acquisition = moisTravailles * rules.acquisitionMensuelle;
|
||
return Math.round(acquisition * 100) / 100;
|
||
}
|
||
|
||
async function processEndOfYearRTT(conn, collaborateurId) {
|
||
const currentYear = new Date().getFullYear();
|
||
const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']);
|
||
if (rttType.length === 0) return null;
|
||
await conn.query(
|
||
`UPDATE CompteurConges SET Solde = 0, Total = 0, SoldeReporte = 0, DerniereMiseAJour = GETDATE() 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 = GETDATE() WHERE Id = ?`,
|
||
[soldeAReporter, soldeAReporter, nextYearCounter[0].Id]
|
||
);
|
||
} else {
|
||
await conn.query(
|
||
`INSERT INTO CompteurConges (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour) VALUES (?, ?, ?, 0, ?, ?, GETDATE())`,
|
||
[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 = CASE WHEN (SoldeReporte - ?) < 0 THEN 0 ELSE (SoldeReporte - ?) END, Solde = CASE WHEN (Solde - ?) < 0 THEN 0 ELSE (Solde - ?) END WHERE Id = ?`,
|
||
[aDeduireN1, aDeduireN1, compteurN1[0].Id]
|
||
);
|
||
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 = CASE WHEN (Solde - ?) < 0 THEN 0 ELSE (Solde - ?) END 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 };
|
||
}
|
||
// ============================================================================
|
||
// 📅 FONCTION UTILITAIRE - Obtenir le nombre de jours du mois
|
||
// ============================================================================
|
||
function getJoursDuMois(date) {
|
||
const annee = date.getFullYear();
|
||
const mois = date.getMonth(); // 0-11
|
||
|
||
// Le jour 0 du mois suivant = dernier jour du mois actuel
|
||
const dernierJour = new Date(annee, mois + 1, 0);
|
||
return dernierJour.getDate();
|
||
}
|
||
|
||
|
||
// ============================================================================
|
||
// 🔄 FONCTION DE MISE À JOUR DES COMPTEURS (CORRIGÉE - DÉCRÉMENTE)
|
||
// ============================================================================
|
||
|
||
async function updateMonthlyCounters(rattrapage = false) {
|
||
const conn = await pool.getConnection();
|
||
|
||
try {
|
||
await conn.beginTransaction();
|
||
|
||
const today = new Date();
|
||
const currentYear = today.getFullYear();
|
||
const currentMonth = today.getMonth();
|
||
const jourActuel = today.getDate();
|
||
|
||
// ⭐ Obtenir le nombre de jours du mois actuel
|
||
const joursDuMois = getJoursDuMois(today);
|
||
|
||
console.log(`\n🔄 === MISE À JOUR ${rattrapage ? 'RATTRAPAGE' : 'QUOTIDIENNE'} COMPTEURS - ${today.toLocaleDateString('fr-FR')} ===`);
|
||
console.log(` 📅 Mois actuel : ${joursDuMois} jours`);
|
||
|
||
// ⭐ RATTRAPAGE : Calculer les jours manqués depuis le 1er du mois
|
||
let joursARattraper = 1; // Par défaut : incrément d'1 jour
|
||
|
||
if (rattrapage) {
|
||
joursARattraper = jourActuel; // Du 1er au jour actuel
|
||
console.log(` 📅 Rattrapage depuis le 1er du mois : ${joursARattraper} jours`);
|
||
}
|
||
|
||
// Récupérer tous les collaborateurs actifs
|
||
const [collaborateurs] = await conn.query(`
|
||
SELECT id, prenom, nom, DateEntree, TypeContrat, role
|
||
FROM CollaborateurAD
|
||
WHERE (Actif = 1 OR Actif IS NULL)
|
||
`);
|
||
|
||
const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ?', ['Congé payé']);
|
||
const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ?', ['RTT']);
|
||
|
||
let compteursMisAJour = 0;
|
||
|
||
for (const collab of collaborateurs) {
|
||
const collaborateurId = collab.id;
|
||
const dateEntree = collab.DateEntree;
|
||
const typeContrat = collab.TypeContrat || '37h';
|
||
const role = collab.role;
|
||
|
||
console.log(`\n 👤 ${collab.prenom} ${collab.nom}`);
|
||
|
||
// ====================================
|
||
// 📅 CP - Incrément avec prorata mensuel
|
||
// ====================================
|
||
if (cpType.length > 0) {
|
||
const modeCP = getModeCalcul('CP', dateEntree);
|
||
|
||
// ⭐ Calcul acquisition mensuelle
|
||
const acquisitionMensuelleCP = 25 / 12; // 2.0833j/mois
|
||
|
||
// ⭐ Calcul acquisition quotidienne selon le nombre de jours du mois
|
||
const incrementJournalierCP = acquisitionMensuelleCP / joursDuMois;
|
||
const incrementTotal = incrementJournalierCP * joursARattraper;
|
||
|
||
console.log(` CP: ${incrementJournalierCP.toFixed(6)}j/jour (${acquisitionMensuelleCP.toFixed(4)}j/${joursDuMois}j)`);
|
||
|
||
// Vérifier si compteur existe
|
||
const [compteurCP] = await conn.query(`
|
||
SELECT Total, Solde, SoldeReporte
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collaborateurId, cpType[0].Id, currentYear]);
|
||
|
||
if (compteurCP.length > 0) {
|
||
const totalAvant = parseFloat(compteurCP[0].Total || 0);
|
||
const soldeAvant = parseFloat(compteurCP[0].Solde || 0);
|
||
|
||
// ⭐ INCRÉMENTER (ne pas écraser)
|
||
await conn.query(`
|
||
UPDATE CompteurConges
|
||
SET
|
||
Total = LEAST(Total + ?, 25),
|
||
Solde = LEAST(Solde + ?, 25 + COALESCE(SoldeReporte, 0)),
|
||
DerniereMiseAJour = GETDATE()
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [incrementTotal, incrementTotal, collaborateurId, cpType[0].Id, currentYear]);
|
||
|
||
console.log(` CP: ${totalAvant.toFixed(2)}j → ${(totalAvant + incrementTotal).toFixed(2)}j (+${incrementTotal.toFixed(4)}j) [${modeCP.mode}]`);
|
||
compteursMisAJour++;
|
||
} else {
|
||
// Créer compteur initial avec Smart
|
||
const acquisCP = calculerAcquisitionCP_Smart(today, dateEntree);
|
||
|
||
// Générer l'ID manuellement
|
||
const compteurCPId = await getNextId(conn, 'CompteurConges');
|
||
|
||
await conn.query(`
|
||
INSERT INTO CompteurConges (Id, CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour)
|
||
VALUES (?, ?, ?, ?, ?, ?, 0, GETDATE())
|
||
`, [compteurCPId, collaborateurId, cpType[0].Id, currentYear, acquisCP, acquisCP]);
|
||
|
||
console.log(` ✅ CP créé avec ID ${compteurCPId}: ${acquisCP.toFixed(2)}j [${modeCP.mode}]`);
|
||
compteursMisAJour++;
|
||
}
|
||
}
|
||
|
||
// ====================================
|
||
// 📅 RTT - Incrément avec prorata mensuel
|
||
// ====================================
|
||
if (rttType.length > 0 && role !== 'Apprenti') {
|
||
const modeRTT = getModeCalcul('RTT', dateEntree);
|
||
const rttConfig = await getConfigurationRTT(conn, currentYear, typeContrat);
|
||
|
||
// ⭐ Calcul acquisition mensuelle
|
||
const acquisitionMensuelleRTT = rttConfig.joursAnnuels / 12;
|
||
|
||
// ⭐ Calcul acquisition quotidienne selon le nombre de jours du mois
|
||
const incrementJournalierRTT = acquisitionMensuelleRTT / joursDuMois;
|
||
const incrementTotal = incrementJournalierRTT * joursARattraper;
|
||
|
||
console.log(` RTT: ${incrementJournalierRTT.toFixed(6)}j/jour (${acquisitionMensuelleRTT.toFixed(4)}j/${joursDuMois}j)`);
|
||
|
||
// Vérifier si compteur existe
|
||
const [compteurRTT] = await conn.query(`
|
||
SELECT Total, Solde
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collaborateurId, rttType[0].Id, currentYear]);
|
||
|
||
if (compteurRTT.length > 0) {
|
||
const totalAvant = parseFloat(compteurRTT[0].Total || 0);
|
||
const soldeAvant = parseFloat(compteurRTT[0].Solde || 0);
|
||
|
||
// ⭐ INCRÉMENTER (ne pas écraser)
|
||
await conn.query(`
|
||
UPDATE CompteurConges
|
||
SET
|
||
Total = LEAST(Total + ?, ?),
|
||
Solde = LEAST(Solde + ?, ?),
|
||
DerniereMiseAJour = GETDATE()
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [incrementTotal, rttConfig.joursAnnuels, incrementTotal, rttConfig.joursAnnuels, collaborateurId, rttType[0].Id, currentYear]);
|
||
|
||
console.log(` RTT: ${totalAvant.toFixed(2)}j → ${(totalAvant + incrementTotal).toFixed(2)}j (+${incrementTotal.toFixed(4)}j) [${modeRTT.mode}]`);
|
||
compteursMisAJour++;
|
||
} else {
|
||
// Créer compteur initial avec Smart
|
||
const rttData = await calculerAcquisitionRTT_Smart(conn, collaborateurId, today);
|
||
|
||
// ✅ CODE CORRIGÉ - Génération manuelle de l'ID
|
||
console.log(` 🆕 RTT créé pour ${collaborateurId}`);
|
||
|
||
// Générer l'ID manuellement
|
||
const compteurRTTId = await getNextId(conn, 'CompteurConges');
|
||
|
||
await conn.query(`
|
||
INSERT INTO CompteurConges (Id, CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour)
|
||
VALUES (?, ?, ?, ?, ?, 0, GETDATE())
|
||
`, [compteurRTTId, collaborateurId, rttType[0].Id, currentYear, rttData.acquisition, rttData.acquisition]);
|
||
|
||
console.log(` ✅ RTT créé avec ID ${compteurRTTId}: ${rttData.acquisition.toFixed(2)}j [${modeRTT.mode}]`);
|
||
compteursMisAJour++;
|
||
}
|
||
}
|
||
}
|
||
|
||
await conn.commit();
|
||
console.log(`\n✅ Mise à jour terminée : ${compteursMisAJour} compteurs pour ${collaborateurs.length} collaborateurs`);
|
||
|
||
} catch (error) {
|
||
await conn.rollback();
|
||
console.error('❌ Erreur mise à jour compteurs:', error);
|
||
throw error;
|
||
} finally {
|
||
conn.release();
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// ⏰ PLANIFICATION DES CRONS
|
||
// ============================================================================
|
||
|
||
// ❌ DÉSACTIVER le rattrapage immédiat (commenté)
|
||
|
||
|
||
|
||
// ✅ EXÉCUTION QUOTIDIENNE à 00h01 (à partir de demain - DÉCRÉMENTE)
|
||
cron.schedule('1 0 * * *', () => {
|
||
console.log('🕐 ===== EXÉCUTION QUOTIDIENNE AUTOMATIQUE - 00h01 =====');
|
||
updateMonthlyCounters(false); // false = incrément normal d'1 jour
|
||
}, {
|
||
timezone: "Europe/Paris"
|
||
});
|
||
|
||
console.log('⏰ CRON quotidien programmé : 00h01 Europe/Paris (à partir de demain)');
|
||
console.log('⏰ CRON réinitialisation RTT programmé : 1er janvier à 00h01');
|
||
console.log('⚠️ Rattrapage immédiat DÉSACTIVÉ');
|
||
|
||
// ============================================================================
|
||
// 🎉 CRON - RÉINITIALISATION RTT AU 1ER JANVIER
|
||
// ============================================================================
|
||
cron.schedule('1 0 1 1 *', async () => {
|
||
console.log('\n🎉 ===== RÉINITIALISATION RTT - 1ER JANVIER =====');
|
||
|
||
const conn = await pool.getConnection();
|
||
|
||
try {
|
||
await conn.beginTransaction();
|
||
|
||
const today = new Date();
|
||
const nouvelleAnnee = today.getFullYear();
|
||
const ancienneAnnee = nouvelleAnnee - 1;
|
||
|
||
console.log(` 📅 Passage de ${ancienneAnnee} à ${nouvelleAnnee}`);
|
||
|
||
// Récupérer le TypeCongeId pour RTT
|
||
const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ?', ['RTT']);
|
||
|
||
if (rttType.length === 0) {
|
||
throw new Error('Type de congé RTT introuvable');
|
||
}
|
||
|
||
const rttTypeId = rttType[0].Id;
|
||
|
||
// Récupérer tous les collaborateurs actifs (sauf apprentis)
|
||
const [collaborateurs] = await conn.query(`
|
||
SELECT id, prenom, nom, TypeContrat, role
|
||
FROM CollaborateurAD
|
||
WHERE (Actif = 1 OR Actif IS NULL)
|
||
AND (role IS NULL OR role != 'Apprenti')
|
||
`);
|
||
|
||
console.log(` 👥 ${collaborateurs.length} collaborateurs à traiter`);
|
||
|
||
let compteursReinitialises = 0;
|
||
|
||
for (const collab of collaborateurs) {
|
||
const collaborateurId = collab.id;
|
||
|
||
// 1️⃣ Archiver/Marquer l'ancien compteur RTT N-1
|
||
const [ancienCompteur] = await conn.query(`
|
||
SELECT Total, Solde
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collaborateurId, rttTypeId, ancienneAnnee]);
|
||
|
||
if (ancienCompteur.length > 0) {
|
||
const soldeAncien = parseFloat(ancienCompteur[0].Solde || 0);
|
||
|
||
console.log(` 👤 ${collab.prenom} ${collab.nom}: ${soldeAncien.toFixed(2)}j RTT perdus`);
|
||
|
||
// Marquer l'ancien compteur comme "clos"
|
||
await conn.query(`
|
||
UPDATE CompteurConges
|
||
SET DerniereMiseAJour = GETDATE()
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collaborateurId, rttTypeId, ancienneAnnee]);
|
||
}
|
||
|
||
// 2️⃣ Créer ou réinitialiser le compteur RTT pour la nouvelle année
|
||
const [nouveauCompteur] = await conn.query(`
|
||
SELECT id FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collaborateurId, rttTypeId, nouvelleAnnee]);
|
||
|
||
if (nouveauCompteur.length === 0) {
|
||
// Créer le compteur à 0
|
||
await conn.query(`
|
||
INSERT INTO CompteurConges (
|
||
CollaborateurADId,
|
||
TypeCongeId,
|
||
Annee,
|
||
Total,
|
||
Solde,
|
||
SoldeReporte,
|
||
DerniereMiseAJour
|
||
)
|
||
VALUES (?, ?, ?, 0, 0, 0, GETDATE())
|
||
`, [collaborateurId, rttTypeId, nouvelleAnnee]);
|
||
|
||
console.log(` ✅ Compteur RTT ${nouvelleAnnee} créé à 0`);
|
||
} else {
|
||
// Le compteur existe déjà, le remettre à 0
|
||
await conn.query(`
|
||
UPDATE CompteurConges
|
||
SET Total = 0, Solde = 0, SoldeReporte = 0, DerniereMiseAJour = GETDATE()
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collaborateurId, rttTypeId, nouvelleAnnee]);
|
||
|
||
console.log(` ✅ Compteur RTT ${nouvelleAnnee} réinitialisé à 0`);
|
||
}
|
||
|
||
compteursReinitialises++;
|
||
}
|
||
|
||
await conn.commit();
|
||
console.log(`\n✅ Réinitialisation RTT terminée : ${compteursReinitialises} compteurs`);
|
||
|
||
} catch (error) {
|
||
await conn.rollback();
|
||
console.error('❌ Erreur réinitialisation RTT:', error);
|
||
} finally {
|
||
conn.release();
|
||
}
|
||
}, {
|
||
timezone: "Europe/Paris"
|
||
});
|
||
|
||
console.log('⏰ CRON réinitialisation RTT programmé : 1er janvier à 00h01');
|
||
|
||
|
||
// ========================================
|
||
// MISE À JOUR DE updateMonthlyCounters
|
||
// ========================================
|
||
|
||
async function updateMonthlyCounters_Smart(conn, collaborateurId, dateReference = null) {
|
||
const today = dateReference ? new Date(dateReference) : new Date();
|
||
const currentYear = today.getFullYear();
|
||
const updates = [];
|
||
|
||
// Récupérer les infos du collaborateur
|
||
const [collabInfo] = await conn.query(`
|
||
SELECT DateEntree, TypeContrat, CampusId, role
|
||
FROM CollaborateurAD WHERE id = ?
|
||
`, [collaborateurId]);
|
||
|
||
if (collabInfo.length === 0) {
|
||
throw new Error(`Collaborateur ${collaborateurId} non trouvé`);
|
||
}
|
||
|
||
const dateEntree = collabInfo[0].DateEntree || null;
|
||
const typeContrat = collabInfo[0].TypeContrat || '37h';
|
||
const isApprenti = collabInfo[0].role === 'Apprenti';
|
||
|
||
console.log(`\n📊 === Mise à jour pour collaborateur ${collaborateurId} ===`);
|
||
console.log(` Date référence: ${today.toLocaleDateString('fr-FR')}`);
|
||
|
||
// ======================================
|
||
// CP (Congés Payés)
|
||
// ======================================
|
||
const acquisitionCP = calculerAcquisitionCP_Smart(today, dateEntree);
|
||
|
||
console.log(` CP - Acquisition: ${acquisitionCP.toFixed(2)}j`);
|
||
|
||
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 || 0);
|
||
const ancienSolde = parseFloat(existingCP[0].Solde || 0);
|
||
const soldeReporte = parseFloat(existingCP[0].SoldeReporte || 0);
|
||
|
||
const incrementAcquis = acquisitionCP - ancienTotal;
|
||
|
||
if (incrementAcquis > 0) {
|
||
console.log(` CP - Nouveaux jours: +${incrementAcquis.toFixed(2)}j`);
|
||
|
||
// Gérer le remboursement d'anticipé (logique existante)
|
||
const [anticipeUtilise] = await conn.query(`
|
||
SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalAnticipe
|
||
FROM DeductionDetails dd
|
||
JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
|
||
WHERE dc.CollaborateurADId = ?
|
||
AND dd.TypeCongeId = ?
|
||
AND dd.Annee = ?
|
||
AND dd.TypeDeduction = 'N Anticip'
|
||
AND dc.Statut != 'Refusée'
|
||
AND dd.JoursUtilises > 0
|
||
`, [collaborateurId, cpTypeId, currentYear]);
|
||
|
||
const anticipePris = parseFloat(anticipeUtilise[0]?.totalAnticipe || 0);
|
||
|
||
if (anticipePris > 0) {
|
||
const aRembourser = Math.min(incrementAcquis, anticipePris);
|
||
console.log(` 💳 CP - Remboursement anticipé: ${aRembourser.toFixed(2)}j`);
|
||
|
||
// [Logique de remboursement complète - identique à avant]
|
||
const [deductionsAnticipees] = await conn.query(`
|
||
SELECT dd.Id, dd.DemandeCongeId, dd.JoursUtilises
|
||
FROM DeductionDetails dd
|
||
JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
|
||
WHERE dc.CollaborateurADId = ?
|
||
AND dd.TypeCongeId = ?
|
||
AND dd.Annee = ?
|
||
AND dd.TypeDeduction = 'N Anticip'
|
||
AND dc.Statut != 'Refusée'
|
||
AND dd.JoursUtilises > 0
|
||
ORDER BY dd.Id ASC
|
||
`, [collaborateurId, cpTypeId, currentYear]);
|
||
|
||
let resteARembourser = aRembourser;
|
||
|
||
for (const deduction of deductionsAnticipees) {
|
||
if (resteARembourser <= 0) break;
|
||
|
||
const joursAnticipes = parseFloat(deduction.JoursUtilises);
|
||
const aDeduiteDeCetteDeduction = Math.min(resteARembourser, joursAnticipes);
|
||
|
||
await conn.query(`
|
||
UPDATE DeductionDetails
|
||
SET JoursUtilises = CASE WHEN (JoursUtilises - ?) < 0 THEN 0 ELSE (JoursUtilises - ?) END
|
||
WHERE Id = ?
|
||
`, [aDeduiteDeCetteDeduction, deduction.Id]);
|
||
|
||
const [existingAnneeN] = await conn.query(`
|
||
SELECT Id, JoursUtilises
|
||
FROM DeductionDetails
|
||
WHERE DemandeCongeId = ?
|
||
AND TypeCongeId = ?
|
||
AND Annee = ?
|
||
AND TypeDeduction IN ('Année N', 'Anne N', 'Anne actuelle N')
|
||
`, [deduction.DemandeCongeId, cpTypeId, currentYear]);
|
||
|
||
if (existingAnneeN.length > 0) {
|
||
await conn.query(`
|
||
UPDATE DeductionDetails
|
||
SET JoursUtilises = JoursUtilises + ?
|
||
WHERE Id = ?
|
||
`, [aDeduiteDeCetteDeduction, existingAnneeN[0].Id]);
|
||
} else {
|
||
await conn.query(`
|
||
INSERT INTO DeductionDetails
|
||
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
|
||
VALUES (?, ?, ?, 'Année N', ?)
|
||
`, [deduction.DemandeCongeId, cpTypeId, currentYear, aDeduiteDeCetteDeduction]);
|
||
}
|
||
|
||
resteARembourser -= aDeduiteDeCetteDeduction;
|
||
}
|
||
|
||
// Supprimer les déductions anticipées à zéro
|
||
await conn.query(`
|
||
DELETE FROM DeductionDetails
|
||
WHERE TypeCongeId = ?
|
||
AND Annee = ?
|
||
AND TypeDeduction = 'N Anticip'
|
||
AND JoursUtilises <= 0
|
||
`, [cpTypeId, currentYear]);
|
||
}
|
||
}
|
||
|
||
// Recalculer le solde
|
||
const [consomme] = await conn.query(`
|
||
SELECT COALESCE(SUM(dd.JoursUtilises), 0) as total
|
||
FROM DeductionDetails dd
|
||
JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
|
||
WHERE dc.CollaborateurADId = ?
|
||
AND dd.TypeCongeId = ?
|
||
AND dd.Annee = ?
|
||
AND dd.TypeDeduction NOT IN ('Accum Récup', 'Accum Recup')
|
||
AND dc.Statut != 'Refusée'
|
||
`, [collaborateurId, cpTypeId, currentYear]);
|
||
|
||
const totalConsomme = parseFloat(consomme[0].total || 0);
|
||
const nouveauSolde = Math.max(0, acquisitionCP + soldeReporte - totalConsomme);
|
||
|
||
await conn.query(`
|
||
UPDATE CompteurConges
|
||
SET Total = ?, Solde = ?, DerniereMiseAJour = GETDATE()
|
||
WHERE Id = ?
|
||
`, [acquisitionCP, nouveauSolde, existingCP[0].Id]);
|
||
|
||
updates.push({
|
||
type: 'CP',
|
||
acquisitionCumulee: acquisitionCP,
|
||
increment: incrementAcquis,
|
||
nouveauSolde: nouveauSolde
|
||
});
|
||
} else {
|
||
// Créer le compteur
|
||
await conn.query(`
|
||
INSERT INTO CompteurConges
|
||
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour)
|
||
VALUES (?, ?, ?, ?, ?, 0, GETDATE())
|
||
`, [collaborateurId, cpTypeId, currentYear, acquisitionCP, acquisitionCP]);
|
||
|
||
updates.push({
|
||
type: 'CP',
|
||
action: 'created',
|
||
acquisitionCumulee: acquisitionCP
|
||
});
|
||
}
|
||
}
|
||
|
||
// ======================================
|
||
// RTT (identique avec Smart)
|
||
// ======================================
|
||
if (!isApprenti) {
|
||
const rttData = await calculerAcquisitionRTT(conn, collaborateurId, today);
|
||
const acquisitionRTT = rttData.acquisition;
|
||
|
||
const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']);
|
||
|
||
if (rttType.length > 0) {
|
||
const rttTypeId = rttType[0].Id;
|
||
|
||
// 1️⃣ Récupérer le compteur existant
|
||
const [existingRTT] = await conn.query(`
|
||
SELECT Id, Total, Solde
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collaborateurId, rttTypeId, currentYear]);
|
||
|
||
if (existingRTT.length > 0) {
|
||
const ancienTotal = parseFloat(existingRTT[0].Total || 0);
|
||
const ancienSolde = parseFloat(existingRTT[0].Solde || 0);
|
||
|
||
console.log(` RTT - Ancien acquis: ${ancienTotal.toFixed(2)}j`);
|
||
console.log(` RTT - Nouvel acquis: ${acquisitionRTT.toFixed(2)}j`);
|
||
|
||
// 2️⃣ Calculer l'incrément d'acquisition
|
||
const incrementAcquis = acquisitionRTT - ancienTotal;
|
||
|
||
if (incrementAcquis > 0) {
|
||
console.log(` RTT - Nouveaux jours ce mois: +${incrementAcquis.toFixed(2)}j`);
|
||
|
||
// 3️⃣ Vérifier si le collaborateur a de l'anticipé utilisé
|
||
const [anticipeUtilise] = await conn.query(`
|
||
SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalAnticipe
|
||
FROM DeductionDetails dd
|
||
JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
|
||
WHERE dc.CollaborateurADId = ?
|
||
AND dd.TypeCongeId = ?
|
||
AND dd.Annee = ?
|
||
AND dd.TypeDeduction = 'N Anticip'
|
||
AND dc.Statut != 'Refusée'
|
||
AND dd.JoursUtilises > 0
|
||
`, [collaborateurId, rttTypeId, currentYear]);
|
||
|
||
const anticipePris = parseFloat(anticipeUtilise[0]?.totalAnticipe || 0);
|
||
|
||
if (anticipePris > 0) {
|
||
// 4️⃣ Calculer le montant à rembourser
|
||
const aRembourser = Math.min(incrementAcquis, anticipePris);
|
||
|
||
console.log(` 💳 RTT - Anticipé à rembourser: ${aRembourser.toFixed(2)}j (sur ${anticipePris.toFixed(2)}j)`);
|
||
|
||
// 5️⃣ Rembourser l'anticipé
|
||
const [deductionsAnticipees] = await conn.query(`
|
||
SELECT dd.Id, dd.DemandeCongeId, dd.JoursUtilises
|
||
FROM DeductionDetails dd
|
||
JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
|
||
WHERE dc.CollaborateurADId = ?
|
||
AND dd.TypeCongeId = ?
|
||
AND dd.Annee = ?
|
||
AND dd.TypeDeduction = 'N Anticip'
|
||
AND dc.Statut != 'Refusée'
|
||
AND dd.JoursUtilises > 0
|
||
ORDER BY dd.Id ASC
|
||
`, [collaborateurId, rttTypeId, currentYear]);
|
||
|
||
let resteARembourser = aRembourser;
|
||
|
||
for (const deduction of deductionsAnticipees) {
|
||
if (resteARembourser <= 0) break;
|
||
|
||
const joursAnticipes = parseFloat(deduction.JoursUtilises);
|
||
const aDeduiteDeCetteDeduction = Math.min(resteARembourser, joursAnticipes);
|
||
|
||
// Réduire l'anticipé
|
||
await conn.query(`
|
||
UPDATE DeductionDetails
|
||
SET JoursUtilises = CASE WHEN (JoursUtilises - ?) < 0 THEN 0 ELSE (JoursUtilises - ?) END
|
||
WHERE Id = ?
|
||
`, [aDeduiteDeCetteDeduction, deduction.Id]);
|
||
|
||
// Vérifier si une déduction "Année N" existe déjà
|
||
const [existingAnneeN] = await conn.query(`
|
||
SELECT Id, JoursUtilises
|
||
FROM DeductionDetails
|
||
WHERE DemandeCongeId = ?
|
||
AND TypeCongeId = ?
|
||
AND Annee = ?
|
||
AND TypeDeduction IN ('Année N', 'Anne N')
|
||
`, [deduction.DemandeCongeId, rttTypeId, currentYear]);
|
||
|
||
if (existingAnneeN.length > 0) {
|
||
await conn.query(`
|
||
UPDATE DeductionDetails
|
||
SET JoursUtilises = JoursUtilises + ?
|
||
WHERE Id = ?
|
||
`, [aDeduiteDeCetteDeduction, existingAnneeN[0].Id]);
|
||
} else {
|
||
await conn.query(`
|
||
INSERT INTO DeductionDetails
|
||
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
|
||
VALUES (?, ?, ?, 'Année N', ?)
|
||
`, [deduction.DemandeCongeId, rttTypeId, currentYear, aDeduiteDeCetteDeduction]);
|
||
}
|
||
|
||
resteARembourser -= aDeduiteDeCetteDeduction;
|
||
|
||
console.log(` ✅ RTT - Remboursé ${aDeduiteDeCetteDeduction.toFixed(2)}j (Demande ${deduction.DemandeCongeId})`);
|
||
}
|
||
|
||
// Supprimer les déductions anticipées à zéro
|
||
await conn.query(`
|
||
DELETE FROM DeductionDetails
|
||
WHERE TypeCongeId = ?
|
||
AND Annee = ?
|
||
AND TypeDeduction = 'N Anticip'
|
||
AND JoursUtilises <= 0
|
||
`, [rttTypeId, currentYear]);
|
||
}
|
||
}
|
||
|
||
// 6️⃣ Recalculer le solde total
|
||
const [consomme] = await conn.query(`
|
||
SELECT COALESCE(SUM(dd.JoursUtilises), 0) as total
|
||
FROM DeductionDetails dd
|
||
JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
|
||
WHERE dc.CollaborateurADId = ?
|
||
AND dd.TypeCongeId = ?
|
||
AND dd.Annee = ?
|
||
AND dd.TypeDeduction NOT IN ('Accum Récup', 'Accum Recup', 'Récup Dosée')
|
||
AND dc.Statut != 'Refusée'
|
||
`, [collaborateurId, rttTypeId, currentYear]);
|
||
|
||
const totalConsomme = parseFloat(consomme[0].total || 0);
|
||
const nouveauSolde = Math.max(0, acquisitionRTT - totalConsomme);
|
||
|
||
console.log(` RTT - Consommé total: ${totalConsomme.toFixed(2)}j`);
|
||
console.log(` RTT - Nouveau solde: ${nouveauSolde.toFixed(2)}j`);
|
||
|
||
// 7️⃣ Mettre à jour le compteur
|
||
await conn.query(`
|
||
UPDATE CompteurConges
|
||
SET Total = ?, Solde = ?, DerniereMiseAJour = GETDATE()
|
||
WHERE Id = ?
|
||
`, [acquisitionRTT, nouveauSolde, existingRTT[0].Id]);
|
||
|
||
updates.push({
|
||
type: 'RTT',
|
||
annee: currentYear,
|
||
typeContrat: rttData.typeContrat,
|
||
config: `${rttData.config.joursAnnuels}j/an`,
|
||
moisTravailles: rttData.moisTravailles,
|
||
acquisitionCumulee: acquisitionRTT,
|
||
increment: incrementAcquis,
|
||
nouveauSolde: nouveauSolde
|
||
});
|
||
} else {
|
||
// Créer le compteur s'il n'existe pas
|
||
await conn.query(`
|
||
INSERT INTO CompteurConges
|
||
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour)
|
||
VALUES (?, ?, ?, ?, ?, 0, GETDATE())
|
||
`, [collaborateurId, rttTypeId, currentYear, acquisitionRTT, acquisitionRTT]);
|
||
|
||
console.log(` RTT - Compteur créé: ${acquisitionRTT.toFixed(2)}j`);
|
||
|
||
updates.push({
|
||
type: 'RTT',
|
||
annee: currentYear,
|
||
typeContrat: rttData.typeContrat,
|
||
config: `${rttData.config.joursAnnuels}j/an`,
|
||
moisTravailles: rttData.moisTravailles,
|
||
acquisitionCumulee: acquisitionRTT,
|
||
action: 'created',
|
||
nouveauSolde: acquisitionRTT
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
console.log(`✅ Mise à jour terminée pour collaborateur ${collaborateurId}\n`);
|
||
|
||
return updates;
|
||
}
|
||
|
||
// ========================================
|
||
// ROUTES API
|
||
// ========================================
|
||
|
||
app.post('/api/login', async (req, res) => {
|
||
try {
|
||
const { email, mot_de_passe, entraUserId, userPrincipalName } = req.body;
|
||
const accessToken = req.headers.authorization?.replace('Bearer ', '');
|
||
|
||
if (accessToken && entraUserId) {
|
||
const [users] = await pool.query(`
|
||
SELECT ca.*, s.Nom as service, so.Nom as societe_nom
|
||
FROM CollaborateurAD ca
|
||
LEFT JOIN Services s ON ca.ServiceId = s.Id
|
||
LEFT JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE ca.entraUserId=? OR ca.email=?
|
||
LIMIT 1
|
||
`, [entraUserId, email]);
|
||
|
||
if (users.length === 0) return res.json({ success: false, message: 'Utilisateur non autorisé' });
|
||
const user = users[0];
|
||
|
||
try {
|
||
const graphResponse = await axios.get(`https://graph.microsoft.com/v1.0/users/${userPrincipalName}/memberOf?$select=id`, { headers: { Authorization: `Bearer ${accessToken}` } });
|
||
const userGroups = graphResponse.data.value.map(g => g.id);
|
||
const [allowedGroups] = await pool.query('SELECT Id FROM EntraGroups WHERE IsActive=1');
|
||
const allowed = allowedGroups.map(g => g.Id);
|
||
const authorized = userGroups.some(g => allowed.includes(g));
|
||
|
||
if (authorized) {
|
||
return res.json({
|
||
success: true,
|
||
message: 'Connexion réussie via Azure AD',
|
||
user: {
|
||
id: user.id,
|
||
prenom: user.prenom || 'Prénom',
|
||
nom: user.nom || 'Nom',
|
||
email: user.email,
|
||
role: user.role || 'Collaborateur',
|
||
service: user.service || 'Non défini',
|
||
societeId: user.SocieteId,
|
||
societeNom: user.societe_nom || 'Non défini',
|
||
typeContrat: user.TypeContrat || '37h',
|
||
description: user.description || null,
|
||
dateEntree: user.DateEntree || null,
|
||
campusId: user.CampusId || null
|
||
}
|
||
});
|
||
} else {
|
||
return res.json({ success: false, message: 'Utilisateur non autorisé' });
|
||
}
|
||
} catch (error) {
|
||
return res.json({ success: false, message: 'Erreur vérification groupes' });
|
||
}
|
||
}
|
||
|
||
if (email && mot_de_passe) {
|
||
const [users] = await pool.query(`
|
||
SELECT u.ID, u.Prenom, u.Nom, u.Email, u.Role, u.ServiceId, s.Nom AS ServiceNom
|
||
FROM Users u
|
||
LEFT JOIN Services s ON u.ServiceId = s.Id
|
||
WHERE u.Email = ? AND u.MDP = ?
|
||
`, [email, mot_de_passe]);
|
||
|
||
if (users.length === 1) {
|
||
return res.json({
|
||
success: true,
|
||
message: 'Connexion réussie',
|
||
user: {
|
||
id: users[0].ID,
|
||
prenom: users[0].Prenom,
|
||
nom: users[0].Nom,
|
||
email: users[0].Email,
|
||
role: users[0].Role,
|
||
service: users[0].ServiceNom || 'Non défini'
|
||
}
|
||
});
|
||
}
|
||
return res.json({ success: false, message: 'Identifiants incorrects' });
|
||
}
|
||
|
||
res.json({ success: false, message: 'Aucune méthode de connexion fournie' });
|
||
} catch (error) {
|
||
res.status(500).json({ success: false, message: 'Erreur serveur', error: error.message });
|
||
}
|
||
});
|
||
|
||
app.post('/api/check-user-groups', async (req, res) => {
|
||
try {
|
||
const { userPrincipalName } = req.body;
|
||
const accessToken = req.headers.authorization?.replace('Bearer ', '');
|
||
|
||
if (!userPrincipalName || !accessToken) return res.json({ authorized: false, message: 'Email ou token manquant' });
|
||
|
||
// 1. Vérification locale
|
||
const [users] = await pool.query(`
|
||
SELECT ca.id, ca.entraUserId, ca.prenom, ca.nom, ca.email,
|
||
s.Nom as service, ca.role, ca.CampusId, ca.SocieteId,
|
||
so.Nom as societe_nom
|
||
FROM CollaborateurAD ca
|
||
LEFT JOIN Services s ON ca.ServiceId = s.Id
|
||
LEFT JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE ca.email = ?
|
||
LIMIT 1
|
||
`, [userPrincipalName]);
|
||
|
||
if (users.length > 0) {
|
||
const user = users[0];
|
||
// Si l'utilisateur est inactif, on le bloque
|
||
if (user.Actif === 0) return res.json({ authorized: false, message: 'Compte désactivé' });
|
||
|
||
return res.json({
|
||
authorized: true,
|
||
role: user.role,
|
||
groups: [user.role],
|
||
localUserId: user.id,
|
||
user: {
|
||
...user,
|
||
societeId: user.SocieteId,
|
||
societeNom: user.societe_nom
|
||
}
|
||
});
|
||
}
|
||
|
||
// 2. Si pas trouvé, interrogation Microsoft Graph
|
||
const userGraph = await axios.get(`https://graph.microsoft.com/v1.0/users/${userPrincipalName}?$select=id,displayName,givenName,surname,mail,department,jobTitle`, { headers: { Authorization: `Bearer ${accessToken}` } });
|
||
const userInfo = userGraph.data;
|
||
|
||
const checkMemberResponse = await axios.post(`https://graph.microsoft.com/v1.0/users/${userInfo.id}/checkMemberGroups`, { groupIds: [AZURE_CONFIG.groupId] }, { headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' } });
|
||
const isInGroup = checkMemberResponse.data.value.includes(AZURE_CONFIG.groupId);
|
||
|
||
if (!isInGroup) return res.json({ authorized: false, message: 'Utilisateur non autorisé (Hors groupe)' });
|
||
|
||
// 3. ⭐ INSERTION AVEC VALEURS PAR DÉFAUT CRITIQUES
|
||
// On met SocieteId=1 et TypeContrat='37h' par défaut pour éviter les bugs de calcul
|
||
const [result] = await pool.query(
|
||
`INSERT INTO CollaborateurAD
|
||
(entraUserId, prenom, nom, email, service, role, SocieteId, Actif, DateEntree, TypeContrat)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||
[
|
||
userInfo.id,
|
||
userInfo.givenName || 'Prénom',
|
||
userInfo.surname || 'Nom',
|
||
userInfo.mail || userPrincipalName,
|
||
userInfo.department,
|
||
'Collaborateur',
|
||
1, // SocieteId par défaut (ex: 1 = ENSUP)
|
||
1, // Actif = 1 (Important !)
|
||
new Date(), // DateEntree = Aujourd'hui
|
||
'37h' // TypeContrat par défaut
|
||
]
|
||
);
|
||
|
||
res.json({
|
||
authorized: true,
|
||
role: 'Collaborateur',
|
||
groups: ['Collaborateur'],
|
||
localUserId: result.insertId,
|
||
user: {
|
||
id: result.insertId,
|
||
entraUserId: userInfo.id,
|
||
prenom: userInfo.givenName,
|
||
nom: userInfo.surname,
|
||
email: userInfo.mail,
|
||
service: userInfo.department,
|
||
role: 'Collaborateur',
|
||
societeId: 1,
|
||
societeNom: 'Défaut'
|
||
}
|
||
});
|
||
} catch (error) {
|
||
console.error("Erreur check-user-groups:", error);
|
||
res.json({ authorized: false, message: 'Erreur serveur', error: error.message });
|
||
}
|
||
});
|
||
|
||
// ========================================
|
||
// ✅ CODE CORRIGÉ POUR getDetailedLeaveCounters
|
||
// À remplacer dans server.js à partir de la ligne ~1600
|
||
// ========================================
|
||
|
||
|
||
app.get('/api/getDetailedLeaveCounters', async (req, res) => {
|
||
try {
|
||
const userIdParam = req.query.user_id;
|
||
|
||
if (!userIdParam) {
|
||
return res.json({ success: false, message: 'ID utilisateur manquant' });
|
||
}
|
||
|
||
const conn = await pool.getConnection();
|
||
|
||
const isUUID = userIdParam.length > 10 && userIdParam.includes('-');
|
||
|
||
const userQuery = `
|
||
SELECT
|
||
ca.id, ca.prenom, ca.nom, ca.email, ca.role, ca.TypeContrat, ca.DateEntree,
|
||
ca.CampusId, ca.SocieteId, s.Nom as service, so.Nom as societeNom, ca.description
|
||
FROM CollaborateurAD ca
|
||
LEFT JOIN Services s ON ca.ServiceId = s.Id
|
||
LEFT JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE ${isUUID ? 'ca.entraUserId' : 'ca.id'} = ?
|
||
AND (ca.Actif = 1 OR ca.Actif IS NULL)
|
||
LIMIT 1
|
||
`;
|
||
|
||
const [userInfo] = await conn.query(userQuery, [userIdParam]);
|
||
|
||
if (userInfo.length === 0) {
|
||
conn.release();
|
||
return res.json({ success: false, message: 'Utilisateur non trouvé' });
|
||
}
|
||
|
||
const user = userInfo[0];
|
||
const userId = user.id;
|
||
const dateEntree = user.DateEntree;
|
||
const typeContrat = user.TypeContrat || '37h';
|
||
|
||
const today = new Date();
|
||
const currentYear = today.getFullYear();
|
||
const previousYear = currentYear - 1;
|
||
|
||
console.log('\n📊 === CALCUL COMPTEURS ===');
|
||
console.log('User:', user.prenom, user.nom, '(ID:', userId, ')');
|
||
console.log('Date référence:', today.toLocaleDateString('fr-FR'));
|
||
|
||
// ═══════════════════════════════════════════════════════════
|
||
// 1️⃣ CALCUL AVEC LES FORMULES INTELLIGENTES
|
||
// ═══════════════════════════════════════════════════════════
|
||
|
||
// CP N
|
||
const acquisCP = calculerAcquisitionCP_Smart(today, dateEntree);
|
||
console.log('🧮 CP calculé:', acquisCP.toFixed(2) + 'j');
|
||
|
||
// RTT N
|
||
let acquisRTT = 0;
|
||
let rttTypeId = null;
|
||
const [rttType] = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1`, ['RTT']);
|
||
|
||
if (rttType.length > 0 && user.role !== 'Apprenti') {
|
||
rttTypeId = rttType[0].Id;
|
||
const rttData = await calculerAcquisitionRTT_Smart(conn, userId, today);
|
||
acquisRTT = rttData.acquisition;
|
||
console.log('🧮 RTT calculé:', acquisRTT.toFixed(2) + 'j');
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════
|
||
// 2️⃣ RÉCUPÉRER INFOS POUR CALCUL DES SOLDES
|
||
// ═══════════════════════════════════════════════════════════
|
||
|
||
const [cpType] = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1`, ['Congé payé']);
|
||
|
||
if (cpType.length === 0) {
|
||
conn.release();
|
||
return res.json({ success: false, message: 'Type de congé CP non trouvé' });
|
||
}
|
||
|
||
// Solde reporté CP
|
||
let soldeReporte = 0;
|
||
const [compteurCP] = await conn.query(`
|
||
SELECT SoldeReporte FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[userId, cpType[0].Id, currentYear]
|
||
);
|
||
if (compteurCP.length > 0) {
|
||
soldeReporte = parseFloat(compteurCP[0].SoldeReporte) || 0;
|
||
}
|
||
|
||
// Consommé CP
|
||
const [consommeCP] = await conn.query(`
|
||
SELECT COALESCE(SUM(dd.JoursUtilises), 0) as total
|
||
FROM DeductionDetails dd
|
||
JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
|
||
WHERE dc.CollaborateurADId = ?
|
||
AND dd.TypeCongeId = ?
|
||
AND dd.Annee = ?
|
||
AND dd.TypeDeduction NOT IN ('Accum Récup', 'Accum Recup')
|
||
AND dc.Statut != 'Refusé'`,
|
||
[userId, cpType[0].Id, currentYear]
|
||
);
|
||
const totalConsommeCP = parseFloat(consommeCP[0].total) || 0;
|
||
const nouveauSoldeCP = Math.max(0, acquisCP + soldeReporte - totalConsommeCP);
|
||
|
||
console.log('💰 CP - Acquis:', acquisCP.toFixed(2), '+ Reporté:', soldeReporte.toFixed(2), '- Consommé:', totalConsommeCP.toFixed(2), '= Solde:', nouveauSoldeCP.toFixed(2));
|
||
|
||
// Consommé RTT
|
||
let totalConsommeRTT = 0;
|
||
let nouveauSoldeRTT = 0;
|
||
if (rttTypeId) {
|
||
const [consommeRTT] = await conn.query(`
|
||
SELECT COALESCE(SUM(dd.JoursUtilises), 0) as total
|
||
FROM DeductionDetails dd
|
||
JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
|
||
WHERE dc.CollaborateurADId = ?
|
||
AND dd.TypeCongeId = ?
|
||
AND dd.Annee = ?
|
||
AND dd.TypeDeduction NOT IN ('Accum Récup', 'Accum Recup', 'Récup Dose')
|
||
AND dc.Statut != 'Refusé'`,
|
||
[userId, rttTypeId, currentYear]
|
||
);
|
||
totalConsommeRTT = parseFloat(consommeRTT[0].total) || 0;
|
||
nouveauSoldeRTT = Math.max(0, acquisRTT - totalConsommeRTT);
|
||
|
||
console.log('💰 RTT - Acquis:', acquisRTT.toFixed(2), '- Consommé:', totalConsommeRTT.toFixed(2), '= Solde:', nouveauSoldeRTT.toFixed(2));
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════
|
||
// 3️⃣ ENVOYER À GTA-RH POUR SYNCHRONISATION
|
||
// ═══════════════════════════════════════════════════════════
|
||
|
||
try {
|
||
const rhUrl = process.env.RH_SERVER_URL || 'http://192.168.0.4:3001';
|
||
|
||
const syncPayload = {
|
||
collaborateurId: userId,
|
||
annee: currentYear,
|
||
compteurs: [
|
||
{
|
||
typeConge: 'Congé payé',
|
||
typeCongeId: cpType[0].Id,
|
||
total: parseFloat(acquisCP.toFixed(2)),
|
||
solde: parseFloat(nouveauSoldeCP.toFixed(2)),
|
||
source: 'calcul_gta'
|
||
}
|
||
]
|
||
};
|
||
|
||
if (rttTypeId) {
|
||
syncPayload.compteurs.push({
|
||
typeConge: 'RTT',
|
||
typeCongeId: rttTypeId,
|
||
total: parseFloat(acquisRTT.toFixed(2)),
|
||
solde: parseFloat(nouveauSoldeRTT.toFixed(2)),
|
||
source: 'calcul_gta'
|
||
});
|
||
}
|
||
|
||
console.log('📤 Envoi synchronisation vers GTA-RH...');
|
||
|
||
const syncResponse = await fetch(`${rhUrl}/api/syncCompteursFromGTA`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(syncPayload)
|
||
});
|
||
|
||
if (syncResponse.ok) {
|
||
const syncResult = await syncResponse.json();
|
||
console.log('✅ Synchronisation GTA-RH réussie:', syncResult.message);
|
||
} else {
|
||
console.warn('⚠️ Synchronisation GTA-RH échouée:', syncResponse.status);
|
||
}
|
||
} catch (syncError) {
|
||
console.error('❌ Erreur synchronisation GTA-RH:', syncError.message);
|
||
// On continue même si la sync échoue
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════
|
||
// 4️⃣ RELIRE LA BASE POUR AFFICHER LES VRAIES VALEURS
|
||
// ═══════════════════════════════════════════════════════════
|
||
|
||
console.log('\n📖 Lecture base après synchronisation...');
|
||
|
||
// Ancienneté
|
||
const ancienneteMs = today - new Date(dateEntree || today);
|
||
const ancienneteMois = Math.floor(ancienneteMs / (1000 * 60 * 60 * 24 * 30.44));
|
||
|
||
let counters = {
|
||
user: {
|
||
id: user.id,
|
||
nom: `${user.prenom} ${user.nom}`,
|
||
prenom: user.prenom,
|
||
nomFamille: user.nom,
|
||
email: user.email,
|
||
service: user.service || 'Non défini',
|
||
role: user.role,
|
||
description: user.description,
|
||
typeContrat: typeContrat,
|
||
societeId: user.SocieteId,
|
||
societeNom: user.societeNom || 'Non défini',
|
||
dateEntree: dateEntree ? formatDateWithoutUTC(dateEntree) : null,
|
||
ancienneteMois: ancienneteMois,
|
||
ancienneteAnnees: Math.floor(ancienneteMois / 12),
|
||
ancienneteMoisRestants: ancienneteMois % 12
|
||
},
|
||
dateReference: today.toISOString().split('T')[0],
|
||
exerciceCP: getExerciceCP(today),
|
||
anneeRTT: currentYear,
|
||
cpN1: null,
|
||
cpN: null,
|
||
rttN: null,
|
||
recupN: null,
|
||
totalDisponible: { cp: 0, rtt: 0, recup: 0, total: 0 }
|
||
};
|
||
|
||
// ✅ CP N-1 (Report) - LECTURE BASE
|
||
const [cpN1Data] = await conn.query(`
|
||
SELECT Annee, Total, Solde, SoldeReporte
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[userId, cpType[0].Id, previousYear]
|
||
);
|
||
|
||
if (cpN1Data.length > 0) {
|
||
const totalAcquis = parseFloat(cpN1Data[0].Total) || 0;
|
||
const soldeReporte = parseFloat(cpN1Data[0].Solde) || 0;
|
||
const pris = Math.max(0, totalAcquis - soldeReporte);
|
||
|
||
counters.cpN1 = {
|
||
annee: previousYear,
|
||
exercice: `${previousYear}-${previousYear + 1}`,
|
||
reporte: parseFloat(totalAcquis.toFixed(2)),
|
||
pris: parseFloat(pris.toFixed(2)),
|
||
solde: parseFloat(soldeReporte.toFixed(2)),
|
||
pourcentageUtilise: totalAcquis > 0 ? parseFloat((pris / totalAcquis * 100).toFixed(1)) : 0
|
||
};
|
||
|
||
counters.totalDisponible.cp += counters.cpN1.solde;
|
||
console.log('✅ CP N-1 BASE: Acquis=' + totalAcquis + 'j, Solde=' + soldeReporte + 'j');
|
||
}
|
||
|
||
// ✅ CP N - LECTURE BASE
|
||
const [cpNData] = await conn.query(`
|
||
SELECT Total, Solde, SoldeReporte
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[userId, cpType[0].Id, currentYear]
|
||
);
|
||
|
||
if (cpNData.length > 0) {
|
||
const totalAcquis = parseFloat(cpNData[0].Total) || 0;
|
||
const soldeBDD = parseFloat(cpNData[0].Solde) || 0;
|
||
const soldeReporteBDD = parseFloat(cpNData[0].SoldeReporte) || 0;
|
||
const soldeReel = Math.max(0, soldeBDD - soldeReporteBDD);
|
||
const pris = Math.max(0, totalAcquis - soldeReel);
|
||
|
||
counters.cpN = {
|
||
annee: currentYear,
|
||
exercice: getExerciceCP(today),
|
||
totalAnnuel: 25.00,
|
||
acquis: parseFloat(totalAcquis.toFixed(2)),
|
||
pris: parseFloat(pris.toFixed(2)),
|
||
solde: parseFloat(soldeReel.toFixed(2)),
|
||
pourcentageUtilise: totalAcquis > 0 ? parseFloat((pris / totalAcquis * 100).toFixed(1)) : 0
|
||
};
|
||
|
||
counters.totalDisponible.cp += counters.cpN.solde;
|
||
console.log('✅ CP N BASE: Acquis=' + totalAcquis + 'j, Solde=' + soldeReel + 'j');
|
||
}
|
||
|
||
// ✅ RTT N - LECTURE BASE
|
||
if (rttType.length > 0 && user.role !== 'Apprenti') {
|
||
const [rttNData] = await conn.query(`
|
||
SELECT Total, Solde
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[userId, rttTypeId, currentYear]
|
||
);
|
||
|
||
if (rttNData.length > 0) {
|
||
const totalAcquis = parseFloat(rttNData[0].Total) || 0;
|
||
const soldeBDD = parseFloat(rttNData[0].Solde) || 0;
|
||
const pris = Math.max(0, totalAcquis - soldeBDD);
|
||
|
||
const rttConfig = await getConfigurationRTT(conn, currentYear, typeContrat);
|
||
|
||
counters.rttN = {
|
||
annee: currentYear,
|
||
typeContrat: typeContrat,
|
||
totalAnnuel: parseFloat(rttConfig.joursAnnuels.toFixed(2)),
|
||
acquis: parseFloat(totalAcquis.toFixed(2)),
|
||
pris: parseFloat(pris.toFixed(2)),
|
||
solde: parseFloat(soldeBDD.toFixed(2)),
|
||
pourcentageUtilise: totalAcquis > 0 ? parseFloat((pris / totalAcquis * 100).toFixed(1)) : 0
|
||
};
|
||
|
||
counters.totalDisponible.rtt += counters.rttN.solde;
|
||
console.log('✅ RTT BASE: Acquis=' + totalAcquis + 'j, Solde=' + soldeBDD + 'j');
|
||
}
|
||
}
|
||
|
||
// ✅ Récup - LECTURE BASE
|
||
const [recupType] = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1`, ['Récupération']);
|
||
if (recupType.length > 0) {
|
||
const [recupData] = await conn.query(`
|
||
SELECT
|
||
COALESCE(SUM(CASE WHEN dd.TypeDeduction IN ('Accum Récup', 'Accum Recup') THEN dd.JoursUtilises ELSE 0 END), 0) as acquis,
|
||
COALESCE(SUM(CASE WHEN dd.TypeDeduction = 'Récup Dose' THEN dd.JoursUtilises ELSE 0 END), 0) as pris
|
||
FROM DeductionDetails dd
|
||
JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
|
||
WHERE dc.CollaborateurADId = ? AND dd.TypeCongeId = ? AND dd.Annee = ? AND dc.Statut != 'Refusé'`,
|
||
[userId, recupType[0].Id, currentYear]
|
||
);
|
||
|
||
if (recupData.length > 0) {
|
||
const acquis = parseFloat(recupData[0].acquis) || 0;
|
||
const pris = parseFloat(recupData[0].pris) || 0;
|
||
const solde = Math.max(0, acquis - pris);
|
||
|
||
counters.recupN = {
|
||
annee: currentYear,
|
||
acquis: parseFloat(acquis.toFixed(2)),
|
||
pris: parseFloat(pris.toFixed(2)),
|
||
solde: parseFloat(solde.toFixed(2)),
|
||
message: "Jours de récupération accumulés suite à du temps supplémentaire"
|
||
};
|
||
|
||
counters.totalDisponible.recup = counters.recupN.solde;
|
||
console.log('✅ RÉCUP BASE: Acquis=' + acquis + 'j, Solde=' + solde + 'j');
|
||
}
|
||
}
|
||
|
||
// Total disponible
|
||
counters.totalDisponible.total =
|
||
counters.totalDisponible.cp +
|
||
counters.totalDisponible.rtt +
|
||
counters.totalDisponible.recup;
|
||
|
||
conn.release();
|
||
|
||
console.log('\n✅ Réponse envoyée au frontend');
|
||
console.log(' CP disponible:', counters.totalDisponible.cp + 'j');
|
||
console.log(' RTT disponible:', counters.totalDisponible.rtt + 'j');
|
||
console.log(' RÉCUP disponible:', counters.totalDisponible.recup + 'j');
|
||
console.log(' TOTAL disponible:', counters.totalDisponible.total + 'j');
|
||
|
||
res.json({ success: true, data: counters });
|
||
|
||
} catch (error) {
|
||
console.error('❌ Erreur getDetailedLeaveCounters:', error);
|
||
res.status(500).json({ success: false, message: error.message });
|
||
}
|
||
});
|
||
app.post('/api/updateCounters', async (req, res) => {
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
const { collaborateur_id } = req.body;
|
||
if (!collaborateur_id) return res.json({ success: false, message: 'ID collaborateur manquant' });
|
||
await conn.beginTransaction();
|
||
const updates = await updateMonthlyCounters(conn, collaborateur_id, new Date());
|
||
await conn.commit();
|
||
res.json({ success: true, message: 'Compteurs mis à jour', updates });
|
||
} catch (error) {
|
||
await conn.rollback();
|
||
res.status(500).json({ success: false, message: 'Erreur', error: error.message });
|
||
} finally {
|
||
conn.release();
|
||
}
|
||
});
|
||
|
||
app.post('/api/updateAllCounters', async (req, res) => {
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
await conn.beginTransaction();
|
||
const [collaborateurs] = await conn.query('SELECT id, CampusId FROM CollaborateurAD WHERE actif = 1 OR actif IS NULL');
|
||
const allUpdates = [];
|
||
for (const collab of collaborateurs) {
|
||
const updates = await updateMonthlyCounters(conn, collab.id, new Date());
|
||
allUpdates.push({ collaborateur_id: collab.id, updates });
|
||
}
|
||
await conn.commit();
|
||
res.json({ success: true, message: `Compteurs mis à jour pour ${collaborateurs.length} collaborateurs`, total_collaborateurs: collaborateurs.length, details: allUpdates });
|
||
} catch (error) {
|
||
await conn.rollback();
|
||
res.status(500).json({ success: false, message: 'Erreur', error: error.message });
|
||
} finally {
|
||
conn.release();
|
||
}
|
||
});
|
||
|
||
async function deductLeaveBalanceWithTracking(conn, collaborateurId, typeCongeId, nombreJours, demandeCongeId) {
|
||
const currentYear = new Date().getFullYear();
|
||
const previousYear = currentYear - 1;
|
||
let joursRestants = nombreJours;
|
||
const deductions = [];
|
||
|
||
// Étape 1: Déduire du reporté N-1 d'abord
|
||
const [compteurN1] = await conn.query(
|
||
`SELECT Id, Solde, SoldeReporte FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, typeCongeId, previousYear]
|
||
);
|
||
|
||
if (compteurN1.length > 0 && compteurN1[0].SoldeReporte > 0) {
|
||
const soldeN1 = parseFloat(compteurN1[0].SoldeReporte);
|
||
const aDeduireN1 = Math.min(soldeN1, joursRestants);
|
||
|
||
if (aDeduireN1 > 0) {
|
||
// Déduction dans la base
|
||
await conn.query(
|
||
`UPDATE CompteurConges
|
||
SET SoldeReporte = CASE WHEN (SoldeReporte - ?) < 0 THEN 0 ELSE (SoldeReporte - ?) END,
|
||
Solde = CASE WHEN (Solde - ?) < 0 THEN 0 ELSE (Solde - ?) END
|
||
WHERE Id = ?`,
|
||
[aDeduireN1, aDeduireN1, compteurN1[0].Id]
|
||
);
|
||
|
||
// 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 = CASE WHEN (Solde - ?) < 0 THEN 0 ELSE (Solde - ?) END
|
||
WHERE Id = ?`,
|
||
[aDeduireN, compteurN[0].Id]
|
||
);
|
||
|
||
// Sauvegarde du détail de la déduction
|
||
await conn.query(
|
||
`INSERT INTO DeductionDetails
|
||
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
|
||
VALUES (?, ?, ?, 'Année N', ?)`,
|
||
[demandeCongeId, typeCongeId, currentYear, aDeduireN]
|
||
);
|
||
|
||
deductions.push({
|
||
annee: currentYear,
|
||
type: 'Année actuelle N',
|
||
joursUtilises: aDeduireN,
|
||
soldeAvant: soldeN
|
||
});
|
||
|
||
joursRestants -= aDeduireN;
|
||
}
|
||
}
|
||
}
|
||
|
||
return {
|
||
success: joursRestants === 0,
|
||
joursDeduitsTotal: nombreJours - joursRestants,
|
||
joursNonDeduits: joursRestants,
|
||
details: deductions
|
||
};
|
||
};
|
||
|
||
async function restoreLeaveBalance(conn, demandeCongeId, collaborateurId) {
|
||
try {
|
||
console.log(`\n🔄 === RESTAURATION COMPTEURS ===`);
|
||
console.log(`Demande ID: ${demandeCongeId}`);
|
||
console.log(`Collaborateur ID: ${collaborateurId}`);
|
||
|
||
const [deductions] = await conn.query(
|
||
`SELECT dd.TypeCongeId, dd.Annee, dd.TypeDeduction, dd.JoursUtilises, tc.Nom as TypeNom
|
||
FROM DeductionDetails dd
|
||
JOIN TypeConge tc ON dd.TypeCongeId = tc.Id
|
||
WHERE dd.DemandeCongeId = ?
|
||
ORDER BY dd.Id DESC`,
|
||
[demandeCongeId]
|
||
);
|
||
|
||
console.log(`📊 ${deductions.length} déductions trouvées`);
|
||
|
||
if (deductions.length === 0) {
|
||
console.log('⚠️ Aucune déduction trouvée pour cette demande');
|
||
return { success: false, message: 'Aucune déduction à restaurer' };
|
||
}
|
||
|
||
const restorations = [];
|
||
|
||
for (const deduction of deductions) {
|
||
const { TypeCongeId, Annee, TypeDeduction, JoursUtilises, TypeNom } = deduction;
|
||
|
||
console.log(`\n🔍 Traitement: ${TypeNom} - ${TypeDeduction} - ${JoursUtilises}j (Année: ${Annee})`);
|
||
|
||
// ⭐ NOUVEAU : Gestion des Récup posées
|
||
if (TypeDeduction === 'Récup Posée') {
|
||
console.log(`🔄 Restauration Récup posée: +${JoursUtilises}j`);
|
||
|
||
const [compteur] = await conn.query(
|
||
`SELECT Id, Solde FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, TypeCongeId, Annee]
|
||
);
|
||
|
||
if (compteur.length > 0) {
|
||
const ancienSolde = parseFloat(compteur[0].Solde || 0);
|
||
const nouveauSolde = ancienSolde + parseFloat(JoursUtilises);
|
||
|
||
await conn.query(
|
||
`UPDATE CompteurConges
|
||
SET Solde = ?,
|
||
DerniereMiseAJour = GETDATE()
|
||
WHERE Id = ?`,
|
||
[nouveauSolde, compteur[0].Id]
|
||
);
|
||
|
||
restorations.push({
|
||
type: TypeNom,
|
||
annee: Annee,
|
||
typeDeduction: TypeDeduction,
|
||
joursRestores: JoursUtilises
|
||
});
|
||
console.log(`✅ Récup restaurée: ${ancienSolde} → ${nouveauSolde}`);
|
||
}
|
||
continue;
|
||
}
|
||
|
||
// 🔹 N+1 Anticipé - ⭐ RESTAURATION CORRECTE
|
||
if (TypeDeduction === 'N+1 Anticipé') {
|
||
console.log(`🔄 Restauration N+1 Anticipé: +${JoursUtilises}j`);
|
||
|
||
const [compteur] = await conn.query(
|
||
`SELECT Id, SoldeAnticipe FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, TypeCongeId, Annee]
|
||
);
|
||
|
||
if (compteur.length > 0) {
|
||
const ancienSolde = parseFloat(compteur[0].SoldeAnticipe || 0);
|
||
const nouveauSolde = ancienSolde + parseFloat(JoursUtilises);
|
||
|
||
await conn.query(
|
||
`UPDATE CompteurConges
|
||
SET SoldeAnticipe = ?,
|
||
DerniereMiseAJour = GETDATE()
|
||
WHERE Id = ?`,
|
||
[nouveauSolde, compteur[0].Id]
|
||
);
|
||
|
||
restorations.push({
|
||
type: TypeNom,
|
||
annee: Annee,
|
||
typeDeduction: TypeDeduction,
|
||
joursRestores: JoursUtilises
|
||
});
|
||
console.log(`✅ N+1 Anticipé restauré: ${ancienSolde} → ${nouveauSolde}`);
|
||
} else {
|
||
// ⭐ Créer le compteur N+1 s'il n'existe pas
|
||
await conn.query(
|
||
`INSERT INTO CompteurConges
|
||
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, SoldeAnticipe, DerniereMiseAJour)
|
||
VALUES (?, ?, ?, 0, 0, 0, ?, GETDATE())`,
|
||
[collaborateurId, TypeCongeId, Annee, JoursUtilises]
|
||
);
|
||
|
||
restorations.push({
|
||
type: TypeNom,
|
||
annee: Annee,
|
||
typeDeduction: TypeDeduction,
|
||
joursRestores: JoursUtilises
|
||
});
|
||
console.log(`✅ Compteur N+1 créé avec ${JoursUtilises}j anticipés`);
|
||
}
|
||
continue;
|
||
}
|
||
|
||
// 🔹 N Anticipé - ⭐ RESTAURATION CORRECTE
|
||
if (TypeDeduction === 'N Anticipé') {
|
||
console.log(`🔄 Restauration N Anticipé: +${JoursUtilises}j`);
|
||
|
||
const [compteur] = await conn.query(
|
||
`SELECT Id, SoldeAnticipe FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, TypeCongeId, Annee]
|
||
);
|
||
|
||
if (compteur.length > 0) {
|
||
const ancienSolde = parseFloat(compteur[0].SoldeAnticipe || 0);
|
||
const nouveauSolde = ancienSolde + parseFloat(JoursUtilises);
|
||
|
||
await conn.query(
|
||
`UPDATE CompteurConges
|
||
SET SoldeAnticipe = ?,
|
||
DerniereMiseAJour = GETDATE()
|
||
WHERE Id = ?`,
|
||
[nouveauSolde, compteur[0].Id]
|
||
);
|
||
|
||
restorations.push({
|
||
type: TypeNom,
|
||
annee: Annee,
|
||
typeDeduction: TypeDeduction,
|
||
joursRestores: JoursUtilises
|
||
});
|
||
console.log(`✅ N Anticipé restauré: ${ancienSolde} → ${nouveauSolde}`);
|
||
} else {
|
||
// ⭐ Créer le compteur s'il n'existe pas (cas rare)
|
||
await conn.query(
|
||
`INSERT INTO CompteurConges
|
||
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, SoldeAnticipe, DerniereMiseAJour)
|
||
VALUES (?, ?, ?, 0, 0, 0, ?, GETDATE())`,
|
||
[collaborateurId, TypeCongeId, Annee, JoursUtilises]
|
||
);
|
||
|
||
restorations.push({
|
||
type: TypeNom,
|
||
annee: Annee,
|
||
typeDeduction: TypeDeduction,
|
||
joursRestores: JoursUtilises
|
||
});
|
||
console.log(`✅ Compteur N créé avec ${JoursUtilises}j anticipés`);
|
||
}
|
||
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 = GETDATE()
|
||
WHERE Id = ?`,
|
||
[JoursUtilises, JoursUtilises, compteur[0].Id]
|
||
);
|
||
|
||
restorations.push({
|
||
type: TypeNom,
|
||
annee: Annee,
|
||
typeDeduction: TypeDeduction,
|
||
joursRestores: JoursUtilises
|
||
});
|
||
console.log(`✅ Reporté restauré: ${ancienSolde} → ${nouveauSolde}`);
|
||
}
|
||
}
|
||
|
||
// 🔹 Année N
|
||
else if (TypeDeduction === 'Année N') {
|
||
const [compteur] = await conn.query(
|
||
`SELECT Id, Solde FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, TypeCongeId, Annee]
|
||
);
|
||
|
||
if (compteur.length > 0) {
|
||
const ancienSolde = parseFloat(compteur[0].Solde || 0);
|
||
const nouveauSolde = ancienSolde + parseFloat(JoursUtilises);
|
||
|
||
await conn.query(
|
||
`UPDATE CompteurConges
|
||
SET Solde = Solde + ?,
|
||
DerniereMiseAJour = GETDATE()
|
||
WHERE Id = ?`,
|
||
[JoursUtilises, compteur[0].Id]
|
||
);
|
||
|
||
restorations.push({
|
||
type: TypeNom,
|
||
annee: Annee,
|
||
typeDeduction: TypeDeduction,
|
||
joursRestores: JoursUtilises
|
||
});
|
||
console.log(`✅ Année N restaurée: ${ancienSolde} → ${nouveauSolde}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ⭐ IMPORTANT : Recalculer les soldes anticipés après restauration
|
||
console.log(`\n🔄 Recalcul des soldes anticipés...`);
|
||
await updateSoldeAnticipe(conn, collaborateurId);
|
||
|
||
console.log(`\n✅ Restauration terminée: ${restorations.length} opérations\n`);
|
||
|
||
return {
|
||
success: true,
|
||
restorations,
|
||
message: `${restorations.length} restaurations effectuées`
|
||
};
|
||
|
||
} catch (error) {
|
||
console.error('❌ Erreur lors de la restauration des soldes:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
app.get('/api/testProrata', async (req, res) => {
|
||
try {
|
||
const userId = parseInt(req.query.user_id || 0);
|
||
if (userId <= 0) return res.json({ success: false, message: 'ID utilisateur requis' });
|
||
const conn = await pool.getConnection();
|
||
const [userInfo] = await conn.query(`SELECT id, prenom, nom, DateEntree, TypeContrat, CampusId FROM CollaborateurAD WHERE id = ?`, [userId]);
|
||
if (userInfo.length === 0) { conn.release(); return res.json({ success: false, message: 'Utilisateur non trouvé' }); }
|
||
const user = userInfo[0];
|
||
const dateEntree = user.DateEntree;
|
||
const typeContrat = user.TypeContrat || '37h';
|
||
const today = new Date();
|
||
const moisCP = getMoisTravaillesCP(today, dateEntree);
|
||
const acquisCP = calculerAcquisitionCP_Smart(today, dateEntree);
|
||
const rttData = await calculerAcquisitionRTT_Smart(conn, userId, today);
|
||
conn.release();
|
||
res.json({
|
||
success: true,
|
||
user: {
|
||
id: user.id,
|
||
nom: `${user.prenom} ${user.nom}`,
|
||
dateEntree: dateEntree ? dateEntree.toISOString().split('T')[0] : null,
|
||
typeContrat: typeContrat
|
||
},
|
||
dateReference: today.toISOString().split('T')[0],
|
||
calculs: {
|
||
CP: {
|
||
moisTravailles: parseFloat(moisCP.toFixed(2)),
|
||
acquisitionMensuelle: 25 / 12,
|
||
acquisitionCumulee: acquisCP,
|
||
formule: `${moisCP.toFixed(2)} mois × ${(25 / 12).toFixed(2)}j/mois = ${acquisCP}j`
|
||
},
|
||
RTT: {
|
||
moisTravailles: rttData.moisTravailles,
|
||
acquisitionMensuelle: rttData.config.acquisitionMensuelle,
|
||
acquisitionCumulee: rttData.acquisition,
|
||
totalAnnuel: rttData.config.joursAnnuels,
|
||
formule: `${rttData.moisTravailles} mois × ${rttData.config.acquisitionMensuelle.toFixed(6)}j/mois = ${rttData.acquisition}j`
|
||
}
|
||
}
|
||
});
|
||
} catch (error) {
|
||
console.error('Erreur testProrata:', error);
|
||
res.status(500).json({ success: false, message: 'Erreur serveur', error: error.message });
|
||
}
|
||
});
|
||
|
||
app.post('/api/fixAllCounters', async (req, res) => {
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
await conn.beginTransaction();
|
||
const today = new Date();
|
||
const currentYear = today.getFullYear();
|
||
const [collaborateurs] = await conn.query('SELECT id, prenom, nom, DateEntree, CampusId FROM CollaborateurAD WHERE (actif = 1 OR actif IS NULL)');
|
||
console.log(`🔄 Correction de ${collaborateurs.length} compteurs...`);
|
||
const corrections = [];
|
||
for (const collab of collaborateurs) {
|
||
const dateEntree = collab.DateEntree;
|
||
const moisCP = getMoisTravaillesCP(today, dateEntree);
|
||
const acquisCP = calculerAcquisitionCP_Smart(today, dateEntree);
|
||
const rttData = await calculerAcquisitionRTT_Smart(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 = GETDATE() 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 = GETDATE() WHERE Id = ?`, [acquisRTT, nouveauSolde, existingRTT[0].Id]);
|
||
corrections.push({ collaborateur: `${collab.prenom} ${collab.nom}`, type: 'RTT', ancienTotal: ancienTotal.toFixed(2), nouveauTotal: acquisRTT.toFixed(2), ancienSolde: ancienSolde.toFixed(2), nouveauSolde: nouveauSolde.toFixed(2) });
|
||
}
|
||
}
|
||
}
|
||
await conn.commit();
|
||
res.json({ success: true, message: `✅ ${collaborateurs.length} compteurs corrigés`, corrections: corrections });
|
||
} catch (error) {
|
||
await conn.rollback();
|
||
console.error('❌ Erreur correction compteurs:', error);
|
||
res.status(500).json({ success: false, message: 'Erreur', error: error.message });
|
||
} finally {
|
||
conn.release();
|
||
}
|
||
});
|
||
|
||
app.post('/api/processEndOfYear', async (req, res) => {
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
const { collaborateur_id } = req.body;
|
||
await conn.beginTransaction();
|
||
let result;
|
||
if (collaborateur_id) {
|
||
result = await processEndOfYearRTT(conn, collaborateur_id);
|
||
} else {
|
||
const [collaborateurs] = await conn.query('SELECT id, CampusId FROM CollaborateurAD');
|
||
const results = [];
|
||
for (const c of collaborateurs) {
|
||
const r = await processEndOfYearRTT(conn, c.id);
|
||
if (r) results.push({ collaborateur_id: c.id, ...r });
|
||
}
|
||
result = results;
|
||
}
|
||
await conn.commit();
|
||
res.json({ success: true, message: 'Traitement de fin d\'année effectué', result });
|
||
} catch (error) {
|
||
await conn.rollback();
|
||
res.status(500).json({ success: false, message: 'Erreur', error: error.message });
|
||
} finally {
|
||
conn.release();
|
||
}
|
||
});
|
||
|
||
app.post('/api/processEndOfExercice', async (req, res) => {
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
const { collaborateur_id } = req.body;
|
||
await conn.beginTransaction();
|
||
let result;
|
||
if (collaborateur_id) {
|
||
result = await processEndOfExerciceCP(conn, collaborateur_id);
|
||
} else {
|
||
const [collaborateurs] = await conn.query('SELECT id, CampusId FROM CollaborateurAD');
|
||
const results = [];
|
||
for (const c of collaborateurs) {
|
||
const r = await processEndOfExerciceCP(conn, c.id);
|
||
if (r) results.push({ collaborateur_id: c.id, ...r });
|
||
}
|
||
result = results;
|
||
}
|
||
await conn.commit();
|
||
res.json({ success: true, message: 'Traitement de fin d\'exercice CP effectué', result });
|
||
} catch (error) {
|
||
await conn.rollback();
|
||
res.status(500).json({ success: false, message: 'Erreur', error: error.message });
|
||
} finally {
|
||
conn.release();
|
||
}
|
||
});
|
||
|
||
app.get('/api/getAcquisitionDetails', async (req, res) => {
|
||
try {
|
||
const today = new Date();
|
||
const details = { date_reference: today.toISOString().split('T')[0], CP: { exercice: getExerciceCP(today), mois_travailles: getMoisTravaillesCP(today), acquisition_mensuelle: LEAVE_RULES.CP.acquisitionMensuelle, acquisition_cumulee: calculerAcquisitionCumulee('CP', today), total_annuel: LEAVE_RULES.CP.joursAnnuels, periode: '01/06 - 31/05', reportable: LEAVE_RULES.CP.reportable }, RTT: { annee: today.getFullYear(), mois_travailles: getMoisTravaillesRTT(today), acquisition_mensuelle: LEAVE_RULES.RTT.acquisitionMensuelle, acquisition_cumulee: calculerAcquisitionCumulee('RTT', today), total_annuel: LEAVE_RULES.RTT.joursAnnuels, periode: '01/01 - 31/12', reportable: LEAVE_RULES.RTT.reportable } };
|
||
res.json({ success: true, details });
|
||
} catch (error) {
|
||
res.status(500).json({ success: false, message: 'Erreur', error: error.message });
|
||
}
|
||
});
|
||
|
||
app.get('/api/getLeaveCounters', async (req, res) => {
|
||
try {
|
||
const userId = parseInt(req.query.user_id || 0);
|
||
const data = {};
|
||
if (userId > 0) {
|
||
const [rows] = await pool.query(`SELECT tc.Nom, cc.Annee, cc.Solde, cc.Total, cc.SoldeReporte FROM CompteurConges cc JOIN TypeConge tc ON cc.TypeCongeId = tc.Id WHERE cc.CollaborateurADId = ?`, [userId]);
|
||
rows.forEach(row => { data[row.Nom] = { Annee: row.Annee, Solde: parseFloat(row.Solde), Total: parseFloat(row.Total), SoldeReporte: parseFloat(row.SoldeReporte) }; });
|
||
}
|
||
res.json({ success: true, message: 'Compteurs récupérés', counters: data });
|
||
} catch (error) {
|
||
res.status(500).json({ success: false, message: 'Erreur', error: error.message });
|
||
}
|
||
});
|
||
|
||
app.get('/api/getEmploye', async (req, res) => {
|
||
try {
|
||
const id = parseInt(req.query.id || 0);
|
||
if (id <= 0) return res.json({ success: false, message: 'ID invalide' });
|
||
|
||
const conn = await pool.getConnection();
|
||
|
||
// 1️⃣ Récupérer les infos du collaborateur
|
||
const [rows] = await conn.query(`
|
||
SELECT
|
||
ca.id,
|
||
ca.Nom,
|
||
ca.Prenom,
|
||
ca.Email,
|
||
ca.role,
|
||
ca.TypeContrat,
|
||
ca.DateEntree,
|
||
ca.CampusId,
|
||
ca.SocieteId,
|
||
s.Nom as service,
|
||
so.Nom as societe_nom
|
||
FROM CollaborateurAD ca
|
||
LEFT JOIN Services s ON ca.ServiceId = s.Id
|
||
LEFT JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE ca.id = ?
|
||
`, [id]);
|
||
|
||
if (rows.length === 0) {
|
||
conn.release();
|
||
return res.json({ success: false, message: 'Collaborateur non trouvé' });
|
||
}
|
||
|
||
const employee = rows[0];
|
||
|
||
// 2️⃣ Déterminer si c'est un apprenti
|
||
const isApprenti = normalizeRole(employee.role) === 'apprenti';
|
||
|
||
// 3️⃣ Récupérer les compteurs CP
|
||
const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']);
|
||
let cpTotal = 0, cpSolde = 0;
|
||
|
||
if (cpType.length > 0) {
|
||
const currentYear = new Date().getFullYear();
|
||
const previousYear = currentYear - 1;
|
||
|
||
// CP N-1 (reporté)
|
||
const [cpN1] = await conn.query(`
|
||
SELECT SoldeReporte
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [id, cpType[0].Id, previousYear]);
|
||
|
||
// CP N (année courante)
|
||
const [cpN] = await conn.query(`
|
||
SELECT Solde, SoldeReporte
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [id, cpType[0].Id, currentYear]);
|
||
|
||
const cpN1Solde = cpN1.length > 0 ? parseFloat(cpN1[0].SoldeReporte || 0) : 0;
|
||
const cpNSolde = cpN.length > 0 ? parseFloat(cpN[0].Solde || 0) : 0;
|
||
const cpNReporte = cpN.length > 0 ? parseFloat(cpN[0].SoldeReporte || 0) : 0;
|
||
|
||
cpTotal = cpN1Solde + (cpNSolde - cpNReporte);
|
||
cpSolde = cpTotal;
|
||
}
|
||
|
||
// 4️⃣ Récupérer les compteurs RTT (sauf pour apprentis)
|
||
let rttTotal = 0, rttSolde = 0;
|
||
|
||
if (!isApprenti) {
|
||
const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']);
|
||
|
||
if (rttType.length > 0) {
|
||
const currentYear = new Date().getFullYear();
|
||
|
||
const [rttN] = await conn.query(`
|
||
SELECT Solde
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [id, rttType[0].Id, currentYear]);
|
||
|
||
rttTotal = rttN.length > 0 ? parseFloat(rttN[0].Solde || 0) : 0;
|
||
rttSolde = rttTotal;
|
||
}
|
||
}
|
||
|
||
conn.release();
|
||
|
||
// 5️⃣ Retourner les données complètes
|
||
res.json({
|
||
success: true,
|
||
employee: {
|
||
id: employee.id,
|
||
Nom: employee.Nom || 'Non défini',
|
||
Prenom: employee.Prenom || 'Non défini',
|
||
Email: employee.Email || 'Non défini',
|
||
role: employee.role || 'Collaborateur',
|
||
TypeContrat: employee.TypeContrat || '37h',
|
||
DateEntree: employee.DateEntree,
|
||
CampusId: employee.CampusId,
|
||
SocieteId: employee.SocieteId,
|
||
service: employee.service || 'Non défini',
|
||
societe_nom: employee.societe_nom || 'Non défini',
|
||
conges_restants: parseFloat(cpSolde.toFixed(2)),
|
||
rtt_restants: parseFloat(rttSolde.toFixed(2))
|
||
}
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('❌ Erreur getEmploye:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur DB',
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
app.get('/api/getEmployeRequest', async (req, res) => {
|
||
try {
|
||
const id = parseInt(req.query.id || 0);
|
||
if (id <= 0) return res.json({ success: false, message: 'ID invalide' });
|
||
|
||
const [rows] = await pool.query(`
|
||
SELECT
|
||
dc.Id,
|
||
dc.DateDebut,
|
||
dc.DateFin,
|
||
dc.NombreJours as days,
|
||
dc.Statut as status,
|
||
dc.DateDemande,
|
||
GROUP_CONCAT(DISTINCT tc.Nom ORDER BY tc.Nom SEPARATOR ', ') AS type,
|
||
CONCAT(
|
||
DATE_FORMAT(dc.DateDebut, '%d/%m/%Y'),
|
||
IF(dc.DateDebut = dc.DateFin, '', CONCAT(' - ', DATE_FORMAT(dc.DateFin, '%d/%m/%Y')))
|
||
) as date_display
|
||
FROM DemandeConge dc
|
||
LEFT JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId
|
||
LEFT JOIN TypeConge tc ON dct.TypeCongeId = tc.Id
|
||
WHERE dc.CollaborateurADId = ?
|
||
GROUP BY dc.Id, dc.DateDebut, dc.DateFin, dc.NombreJours, dc.Statut, dc.DateDemande
|
||
ORDER BY dc.DateDemande DESC
|
||
`, [id]);
|
||
|
||
res.json({
|
||
success: true,
|
||
requests: rows
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('❌ Erreur getEmployeRequest:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur DB',
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
app.get('/api/getRequests', async (req, res) => {
|
||
try {
|
||
const userId = req.query.user_id;
|
||
if (!userId) return res.json({ success: false, message: 'ID utilisateur manquant' });
|
||
|
||
// 🔍 Déterminer si c'est un UUID ou un ID numérique
|
||
const isUUID = userId.length > 10 && userId.includes('-');
|
||
console.log(`📝 Type userId détecté: ${isUUID ? 'UUID (entraUserId)' : 'ID numérique'}`);
|
||
|
||
let mainRequest = pool.request();
|
||
let whereClause;
|
||
|
||
if (isUUID) {
|
||
// Si UUID, chercher d'abord le CollaborateurADId
|
||
const lookupRequest = pool.request();
|
||
lookupRequest.input('entraUserId', userId);
|
||
|
||
const userLookup = await lookupRequest.query(`
|
||
SELECT id
|
||
FROM CollaborateurAD
|
||
WHERE entraUserId = @entraUserId
|
||
`);
|
||
|
||
if (userLookup.recordset.length === 0) {
|
||
return res.json({
|
||
success: false,
|
||
message: 'Utilisateur non trouvé',
|
||
requests: [],
|
||
total: 0
|
||
});
|
||
}
|
||
|
||
const collaborateurId = userLookup.recordset[0].id;
|
||
console.log(`✅ CollaborateurADId trouvé: ${collaborateurId}`);
|
||
|
||
// Créer une nouvelle request pour la requête principale
|
||
mainRequest.input('collaborateurId', collaborateurId);
|
||
whereClause = 'dc.CollaborateurADId = @collaborateurId';
|
||
|
||
} else {
|
||
// Si ID numérique, utiliser directement
|
||
mainRequest.input('userId', parseInt(userId));
|
||
whereClause = '(dc.EmployeeId = @userId OR dc.CollaborateurADId = @userId)';
|
||
}
|
||
|
||
// ✅ REQUÊTE CORRIGÉE pour MSSQL avec la table de liaison
|
||
const result = await mainRequest.query(`
|
||
SELECT
|
||
dc.Id,
|
||
dc.DateDebut,
|
||
dc.DateFin,
|
||
dc.Statut,
|
||
dc.DateDemande,
|
||
dc.Commentaire,
|
||
dc.CommentaireValidation,
|
||
dc.Validateur,
|
||
dc.DocumentJoint,
|
||
(
|
||
SELECT STRING_AGG(Nom, ', ') WITHIN GROUP (ORDER BY Nom)
|
||
FROM (
|
||
SELECT DISTINCT tc2.Nom
|
||
FROM DemandeCongeType dct2
|
||
JOIN TypeConge tc2 ON dct2.TypeCongeId = tc2.Id
|
||
WHERE dct2.DemandeCongeId = dc.Id
|
||
) AS DistinctTypes
|
||
) AS TypeConges,
|
||
(
|
||
SELECT SUM(dct2.NombreJours)
|
||
FROM DemandeCongeType dct2
|
||
WHERE dct2.DemandeCongeId = dc.Id
|
||
) AS NombreJoursTotal
|
||
FROM DemandeConge dc
|
||
WHERE ${whereClause}
|
||
ORDER BY dc.DateDemande DESC
|
||
`);
|
||
|
||
const rows = result.recordset;
|
||
console.log(`📋 ${rows.length} demandes trouvées pour userId: ${userId}`);
|
||
|
||
const requests = rows.map(row => {
|
||
const workingDays = getWorkingDays(row.DateDebut, row.DateFin);
|
||
const dateDisplay = row.DateDebut === row.DateFin
|
||
? formatDate(row.DateDebut)
|
||
: `${formatDate(row.DateDebut)} - ${formatDate(row.DateFin)}`;
|
||
|
||
let fileUrl = null;
|
||
if (row.TypeConges && row.TypeConges.includes('Congé maladie') && row.DocumentJoint) {
|
||
fileUrl = `/uploads/${path.basename(row.DocumentJoint)}`;
|
||
}
|
||
|
||
return {
|
||
id: row.Id,
|
||
type: row.TypeConges || 'Non défini', // ✅ Gérer le cas null
|
||
startDate: row.DateDebut,
|
||
endDate: row.DateFin,
|
||
dateDisplay,
|
||
days: row.NombreJoursTotal || workingDays, // ✅ Utiliser le total de la table de liaison
|
||
status: row.Statut,
|
||
reason: row.Commentaire || 'Aucun commentaire',
|
||
submittedAt: row.DateDemande,
|
||
submittedDisplay: formatDate(row.DateDemande),
|
||
validator: row.Validateur || null,
|
||
validationComment: row.CommentaireValidation || null,
|
||
fileUrl
|
||
};
|
||
});
|
||
|
||
res.json({
|
||
success: true,
|
||
message: 'Demandes récupérées',
|
||
requests,
|
||
total: requests.length
|
||
});
|
||
} catch (error) {
|
||
console.error('❌ Erreur getRequests:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur',
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
app.get('/api/getAllTeamRequests', async (req, res) => {
|
||
try {
|
||
const managerId = req.query.SuperieurId;
|
||
if (!managerId) return res.json({ success: false, message: 'Paramètre SuperieurId manquant' });
|
||
const [rows] = await pool.query(`SELECT dc.Id, dc.DateDebut, dc.DateFin, dc.Statut, dc.DateDemande, dc.Commentaire, dc.DocumentJoint, dc.CollaborateurADId AS employee_id, CONCAT(ca.Prenom, ' ', ca.Nom) as employee_name, ca.Email as employee_email, tc.Nom as type FROM DemandeConge dc JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id JOIN TypeConge tc ON dc.TypeCongeId = tc.Id JOIN HierarchieValidationAD hv ON hv.CollaborateurId = ca.id WHERE hv.SuperieurId = ? ORDER BY dc.DateDemande DESC`, [managerId]);
|
||
const requests = rows.map(row => ({ id: row.Id, employee_id: row.employee_id, employee_name: row.employee_name, employee_email: row.employee_email, type: row.type, start_date: row.DateDebut, end_date: row.DateFin, date_display: row.DateDebut === row.DateFin ? formatDate(row.DateDebut) : `${formatDate(row.DateDebut)} - ${formatDate(row.DateFin)}`, days: getWorkingDays(row.DateDebut, row.DateFin), status: row.Statut, reason: row.Commentaire || '', file: row.DocumentJoint || null, submitted_at: row.DateDemande, submitted_display: formatDate(row.DateDemande) }));
|
||
res.json({ success: true, requests });
|
||
} catch (error) {
|
||
res.status(500).json({ success: false, message: 'Erreur DB', error: error.message });
|
||
}
|
||
});
|
||
|
||
app.get('/api/getPendingRequests', async (req, res) => {
|
||
try {
|
||
const validatorId = req.query.validator_id;
|
||
|
||
if (!validatorId) {
|
||
return res.json({ success: false, message: 'ID validateur manquant' });
|
||
}
|
||
|
||
const conn = await pool.getConnection();
|
||
|
||
// Récupérer les infos du validateur
|
||
const [managerRows] = await conn.query(
|
||
'SELECT TOP 1 ServiceId, CampusId, role FROM CollaborateurAD WHERE id = ?',
|
||
[validatorId]
|
||
);
|
||
|
||
if (managerRows.length === 0) {
|
||
conn.release();
|
||
return res.json({ success: false, message: 'Validateur non trouvé' });
|
||
}
|
||
|
||
const serviceId = managerRows[0].ServiceId;
|
||
const campusId = managerRows[0].CampusId;
|
||
const role = normalizeRole(managerRows[0].role);
|
||
|
||
let requests;
|
||
|
||
if (role === 'admin' || role === 'president' || role === 'rh') {
|
||
// Admin/President/RH : toutes les demandes en attente
|
||
[requests] = await conn.query(`
|
||
SELECT
|
||
dc.Id,
|
||
dc.CollaborateurADId,
|
||
dc.DateDebut,
|
||
dc.DateFin,
|
||
dc.NombreJours,
|
||
dc.Statut,
|
||
dc.Commentaire,
|
||
tc.Nom as TypeConge,
|
||
ca.prenom,
|
||
ca.nom,
|
||
s.Nom as service_name,
|
||
camp.Nom as campus_name
|
||
FROM DemandeConge dc
|
||
JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.Id
|
||
JOIN TypeConge tc ON dc.TypeCongeId = tc.Id
|
||
LEFT JOIN Services s ON ca.ServiceId = s.Id
|
||
LEFT JOIN Campus camp ON ca.CampusId = camp.Id
|
||
WHERE dc.Statut = 'En attente'
|
||
ORDER BY dc.DateCreation DESC
|
||
`);
|
||
} else if (role === 'validateur' || role === 'directeur de campus') {
|
||
// Validateur/Directeur : leurs collaborateurs directs via hiérarchie
|
||
[requests] = await conn.query(`
|
||
SELECT DISTINCT
|
||
dc.Id,
|
||
dc.CollaborateurADId,
|
||
dc.DateDebut,
|
||
dc.DateFin,
|
||
dc.NombreJours,
|
||
dc.Statut,
|
||
dc.Commentaire,
|
||
tc.Nom as TypeConge,
|
||
ca.prenom,
|
||
ca.nom,
|
||
s.Nom as service_name,
|
||
camp.Nom as campus_name
|
||
FROM DemandeConge dc
|
||
JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.Id
|
||
JOIN HierarchieValidationAD hv ON ca.id = hv.CollaborateurId
|
||
JOIN TypeConge tc ON dc.TypeCongeId = tc.Id
|
||
LEFT JOIN Services s ON ca.ServiceId = s.Id
|
||
LEFT JOIN Campus camp ON ca.CampusId = camp.Id
|
||
WHERE dc.Statut = 'En attente'
|
||
AND hv.SuperieurId = ?
|
||
ORDER BY dc.DateCreation DESC
|
||
`, [validatorId]);
|
||
} else {
|
||
conn.release();
|
||
return res.json({ success: false, message: 'Rôle non autorisé pour validation' });
|
||
}
|
||
|
||
conn.release();
|
||
|
||
res.json({
|
||
success: true,
|
||
message: 'Demandes récupérées',
|
||
requests,
|
||
service_id: serviceId,
|
||
campus_id: campusId
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('❌ Erreur getPendingRequests:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur',
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
|
||
// Route getTeamMembers - Membres de l'équipe
|
||
app.get('/api/getTeamMembers', async (req, res) => {
|
||
try {
|
||
const managerId = req.query.manager_id;
|
||
if (!managerId) return res.json({ success: false, message: 'ID manager manquant' });
|
||
|
||
const managerRequest = pool.request();
|
||
managerRequest.input('managerId', managerId);
|
||
|
||
const managerResult = await managerRequest.query(`
|
||
SELECT TOP 1 ServiceId, CampusId, role, email
|
||
FROM CollaborateurAD
|
||
WHERE id = @managerId
|
||
`);
|
||
|
||
if (managerResult.recordset.length === 0) {
|
||
return res.json({ success: false, message: 'Manager non trouvé' });
|
||
}
|
||
|
||
const managerInfo = managerResult.recordset[0];
|
||
const serviceId = managerInfo.ServiceId;
|
||
const campusId = managerInfo.CampusId;
|
||
const role = normalizeRole(managerInfo.role);
|
||
|
||
console.log(`🔍 getTeamMembers - Manager: ${managerId}, Role: ${role}, Campus: ${campusId}`);
|
||
|
||
let members;
|
||
|
||
if (role === 'admin' || role === 'president' || role === 'rh') {
|
||
// CAS 1: Admin/President/RH - Vue globale
|
||
console.log("CAS 1: Admin/President/RH - Vue globale");
|
||
|
||
const membersRequest = pool.request();
|
||
membersRequest.input('managerId', managerId);
|
||
|
||
const membersResult = await membersRequest.query(`
|
||
SELECT
|
||
c.id,
|
||
c.nom,
|
||
c.prenom,
|
||
c.email,
|
||
c.role,
|
||
s.Nom as service_name,
|
||
c.CampusId,
|
||
camp.Nom as campus_name
|
||
FROM CollaborateurAD c
|
||
JOIN Services s ON c.ServiceId = s.Id
|
||
LEFT JOIN Campus camp ON c.CampusId = camp.Id
|
||
WHERE c.id != @managerId
|
||
AND (c.actif = 1 OR c.actif IS NULL)
|
||
ORDER BY c.prenom, c.nom
|
||
`);
|
||
|
||
members = membersResult.recordset;
|
||
console.log(` ✅ ${members.length} collaborateur(s) au total`);
|
||
|
||
} else if (role === 'validateur' || role === 'directeur de campus') {
|
||
// CAS 2: Validateur/Directeur - Collaborateurs directs via hiérarchie
|
||
console.log("CAS 2: Validateur/Directeur - Collaborateurs directs via hiérarchie");
|
||
|
||
const membersRequest = pool.request();
|
||
membersRequest.input('managerId', managerId);
|
||
|
||
const membersResult = await membersRequest.query(`
|
||
SELECT DISTINCT
|
||
c.id,
|
||
c.nom,
|
||
c.prenom,
|
||
c.email,
|
||
c.role,
|
||
s.Nom as service_name,
|
||
c.CampusId,
|
||
camp.Nom as campus_name
|
||
FROM CollaborateurAD c
|
||
JOIN HierarchieValidationAD hv ON c.id = hv.CollaborateurId
|
||
JOIN Services s ON c.ServiceId = s.Id
|
||
LEFT JOIN Campus camp ON c.CampusId = camp.Id
|
||
WHERE hv.SuperieurId = @managerId
|
||
AND (c.actif = 1 OR c.actif IS NULL)
|
||
ORDER BY c.prenom, c.nom
|
||
`);
|
||
|
||
members = membersResult.recordset;
|
||
console.log(` ✅ ${members.length} collaborateur(s) sous ${managerId}`);
|
||
|
||
} else if (role === 'collaborateur' || role === 'apprenti') {
|
||
// CAS 3: Collaborateur/Apprenti - Collègues du même service ET campus
|
||
console.log("CAS 3: Collaborateur/Apprenti - Collègues du même service et campus");
|
||
|
||
const membersRequest = pool.request();
|
||
membersRequest.input('managerId', managerId);
|
||
membersRequest.input('serviceId', serviceId);
|
||
membersRequest.input('campusId', campusId);
|
||
|
||
const membersResult = await membersRequest.query(`
|
||
SELECT
|
||
c.id,
|
||
c.nom,
|
||
c.prenom,
|
||
c.email,
|
||
c.role,
|
||
s.Nom as service_name,
|
||
c.CampusId,
|
||
camp.Nom as campus_name
|
||
FROM CollaborateurAD c
|
||
JOIN Services s ON c.ServiceId = s.Id
|
||
LEFT JOIN Campus camp ON c.CampusId = camp.Id
|
||
WHERE c.ServiceId = @serviceId
|
||
AND c.CampusId = @campusId
|
||
AND c.id != @managerId
|
||
AND (c.actif = 1 OR c.actif IS NULL)
|
||
ORDER BY c.prenom, c.nom
|
||
`);
|
||
|
||
members = membersResult.recordset;
|
||
console.log(` ✅ ${members.length} collègue(s) trouvé(s)`);
|
||
|
||
} else {
|
||
return res.json({ success: false, message: 'Rôle non autorisé' });
|
||
}
|
||
|
||
res.json({
|
||
success: true,
|
||
team_members: members || [],
|
||
service_id: serviceId,
|
||
campus_id: campusId
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('❌ Erreur getTeamMembers:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur',
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
|
||
app.get('/api/getNotifications', async (req, res) => {
|
||
try {
|
||
const userIdParam = req.query.user_id;
|
||
|
||
if (!userIdParam) {
|
||
return res.json({ success: false, message: 'ID utilisateur manquant' });
|
||
}
|
||
|
||
const conn = await pool.getConnection();
|
||
|
||
// ✅ Déterminer si c'est un UUID ou un ID numérique
|
||
const isUUID = userIdParam.length > 10 && userIdParam.includes('-');
|
||
|
||
// ✅ Récupérer l'ID numérique si on a un UUID
|
||
let userId = userIdParam;
|
||
|
||
if (isUUID) {
|
||
const [userRows] = await conn.query(
|
||
'SELECT id, CampusId FROM CollaborateurAD WHERE entraUserId = ? AND (Actif = 1 OR Actif IS NULL)',
|
||
[userIdParam]
|
||
);
|
||
|
||
if (userRows.length === 0) {
|
||
conn.release();
|
||
return res.json({
|
||
success: false,
|
||
message: 'Utilisateur non trouvé ou compte désactivé'
|
||
});
|
||
}
|
||
|
||
userId = userRows[0].id;
|
||
} else {
|
||
userId = parseInt(userIdParam);
|
||
}
|
||
|
||
// ✅ Utiliser l'ID numérique pour la requête
|
||
const [notifications] = await conn.query(`
|
||
SELECT * FROM Notifications
|
||
WHERE CollaborateurADId = ?
|
||
ORDER BY DateCreation DESC
|
||
LIMIT 50
|
||
`, [userId]);
|
||
|
||
conn.release();
|
||
|
||
res.json({
|
||
success: true,
|
||
notifications: notifications || []
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('Erreur getNotifications:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur serveur',
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
app.post('/api/markNotificationRead', async (req, res) => {
|
||
try {
|
||
const { notificationId } = req.body;
|
||
if (!notificationId || notificationId <= 0) return res.status(400).json({ success: false, message: 'ID notification invalide' });
|
||
await pool.query('UPDATE Notifications SET lu = 1 WHERE Id = ?', [notificationId]);
|
||
res.json({ success: true, message: 'Notification marquée comme lue' });
|
||
} catch (error) {
|
||
res.status(500).json({ success: false, message: 'Erreur', error: error.message });
|
||
}
|
||
});
|
||
// À ajouter avant app.listen()
|
||
|
||
/**
|
||
* POST /saisirRecupJour
|
||
* Saisir une journée de récupération (samedi travaillé)
|
||
*/
|
||
app.post('/api/saisirRecupJour', async (req, res) => {
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
await conn.beginTransaction();
|
||
|
||
const {
|
||
user_id,
|
||
date, // Date du samedi travaillé
|
||
nombre_heures = 1, // Par défaut 1 jour = 1 samedi
|
||
commentaire
|
||
} = req.body;
|
||
|
||
console.log('\n📝 === SAISIE RÉCUP ===');
|
||
console.log('User ID:', user_id);
|
||
console.log('Date:', date);
|
||
console.log('Heures:', nombre_heures);
|
||
|
||
if (!user_id || !date) {
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.json({
|
||
success: false,
|
||
message: 'Données manquantes'
|
||
});
|
||
}
|
||
|
||
// Vérifier que c'est bien un samedi
|
||
const dateObj = new Date(date);
|
||
const dayOfWeek = dateObj.getDay();
|
||
|
||
if (dayOfWeek !== 6) {
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.json({
|
||
success: false,
|
||
message: 'La récupération ne peut être saisie que pour un samedi'
|
||
});
|
||
}
|
||
|
||
// Vérifier que ce samedi n'a pas déjà été saisi
|
||
const [existing] = await conn.query(`
|
||
SELECT dc.Id
|
||
FROM DemandeConge dc
|
||
JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId
|
||
JOIN TypeConge tc ON dct.TypeCongeId = tc.Id
|
||
WHERE dc.CollaborateurADId = ?
|
||
AND dc.DateDebut = ?
|
||
AND tc.Nom = 'Récupération'
|
||
`, [user_id, date]);
|
||
|
||
if (existing.length > 0) {
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.json({
|
||
success: false,
|
||
message: 'Ce samedi a déjà été déclaré'
|
||
});
|
||
}
|
||
|
||
// Récupérer infos utilisateur
|
||
const [userInfo] = await conn.query(
|
||
'SELECT prenom, nom, email, CampusId FROM CollaborateurAD WHERE id = ?',
|
||
[user_id]
|
||
);
|
||
|
||
if (userInfo.length === 0) {
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.json({
|
||
success: false,
|
||
message: 'Utilisateur non trouvé'
|
||
});
|
||
}
|
||
|
||
const user = userInfo[0];
|
||
const userName = `${user.prenom} ${user.nom}`;
|
||
const dateFormatted = dateObj.toLocaleDateString('fr-FR');
|
||
|
||
// Récupérer le type Récupération
|
||
const [recupType] = await conn.query(
|
||
'SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1',
|
||
['Récupération']
|
||
);
|
||
|
||
if (recupType.length === 0) {
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.json({
|
||
success: false,
|
||
message: 'Type Récupération non trouvé'
|
||
});
|
||
}
|
||
|
||
const recupTypeId = recupType[0].Id;
|
||
const currentYear = dateObj.getFullYear();
|
||
|
||
// CRÉER LA DEMANDE (validée automatiquement)
|
||
const [result] = await conn.query(`
|
||
INSERT INTO DemandeConge
|
||
(CollaborateurADId, DateDebut, DateFin, TypeCongeId,
|
||
Statut, DateDemande, Commentaire, NombreJours)
|
||
VALUES (?, ?, ?, ?, 'Validée', GETDATE(), ?, ?)
|
||
`, [user_id, date, date, recupTypeId, commentaire || `Samedi travaillé - ${dateFormatted}`, nombre_heures]);
|
||
|
||
const demandeId = result.insertId;
|
||
|
||
// SAUVEGARDER DANS DemandeCongeType
|
||
await conn.query(`
|
||
INSERT INTO DemandeCongeType
|
||
(DemandeCongeId, TypeCongeId, NombreJours, PeriodeJournee)
|
||
VALUES (?, ?, ?, 'Journée entière')
|
||
`, [demandeId, recupTypeId, nombre_heures]);
|
||
|
||
// ACCUMULER DANS LE COMPTEUR
|
||
const [compteur] = await conn.query(`
|
||
SELECT Id, Total, Solde
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [user_id, recupTypeId, currentYear]);
|
||
|
||
if (compteur.length > 0) {
|
||
await conn.query(`
|
||
UPDATE CompteurConges
|
||
SET Total = Total + ?,
|
||
Solde = Solde + ?,
|
||
DerniereMiseAJour = GETDATE()
|
||
WHERE Id = ?
|
||
`, [nombre_heures, nombre_heures, compteur[0].Id]);
|
||
|
||
console.log(`✅ Compteur mis à jour: ${parseFloat(compteur[0].Solde) + nombre_heures}j`);
|
||
} else {
|
||
await conn.query(`
|
||
INSERT INTO CompteurConges
|
||
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour)
|
||
VALUES (?, ?, ?, ?, ?, 0, GETDATE())
|
||
`, [user_id, recupTypeId, currentYear, nombre_heures, nombre_heures]);
|
||
|
||
console.log(`✅ Compteur créé: ${nombre_heures}j`);
|
||
}
|
||
|
||
// ENREGISTRER L'ACCUMULATION
|
||
await conn.query(`
|
||
INSERT INTO DeductionDetails
|
||
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
|
||
VALUES (?, ?, ?, 'Accum Récup', ?)
|
||
`, [demandeId, recupTypeId, currentYear, nombre_heures]);
|
||
|
||
// CRÉER NOTIFICATION
|
||
await conn.query(`
|
||
INSERT INTO Notifications
|
||
(CollaborateurADId, Type, Titre, Message, DemandeCongeId, DateCreation, Lu)
|
||
VALUES (?, 'Success', '✅ Récupération enregistrée', ?, ?, GETDATE(), 0)
|
||
`, [
|
||
user_id,
|
||
`Samedi ${dateFormatted} enregistré : +${nombre_heures}j de récupération`,
|
||
demandeId
|
||
]);
|
||
|
||
await conn.commit();
|
||
conn.release();
|
||
|
||
res.json({
|
||
success: true,
|
||
message: `Samedi ${dateFormatted} enregistré`,
|
||
jours_ajoutes: nombre_heures,
|
||
demande_id: demandeId
|
||
});
|
||
|
||
} catch (error) {
|
||
await conn.rollback();
|
||
if (conn) conn.release();
|
||
console.error('❌ Erreur saisie récup:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur serveur',
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
/**
|
||
* GET /getMesSamedis
|
||
* Récupérer les samedis déjà déclarés
|
||
*/
|
||
app.get('/api/getMesSamedis', async (req, res) => {
|
||
try {
|
||
const { user_id, annee } = req.query;
|
||
|
||
const conn = await pool.getConnection();
|
||
|
||
const [samedis] = await conn.query(`
|
||
SELECT
|
||
dc.Id,
|
||
DATE_FORMAT(dc.DateDebut, '%Y-%m-%d') as date,
|
||
dc.NombreJours as jours,
|
||
dc.Commentaire as commentaire,
|
||
DATE_FORMAT(dc.DateDemande, '%d/%m/%Y à %H:%i') as date_saisie
|
||
FROM DemandeConge dc
|
||
JOIN TypeConge tc ON dc.TypeCongeId = tc.Id
|
||
WHERE dc.CollaborateurADId = ?
|
||
AND tc.Nom = 'Récupération'
|
||
AND YEAR(dc.DateDebut) = ?
|
||
ORDER BY dc.DateDebut DESC
|
||
`, [user_id, annee]);
|
||
|
||
conn.release();
|
||
|
||
res.json({
|
||
success: true,
|
||
samedis: samedis
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('Erreur getMesSamedis:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur serveur',
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
async function checkLeaveBalanceWithAnticipation(conn, collaborateurId, repartition, dateDebut) {
|
||
const dateDebutObj = new Date(dateDebut);
|
||
const currentYear = dateDebutObj.getFullYear();
|
||
const previousYear = currentYear - 1;
|
||
|
||
console.log('\n🔍 === CHECK SOLDES AVEC ANTICIPATION ===');
|
||
console.log(`📅 Date demande: ${dateDebut}`);
|
||
console.log(`📅 Année demande: ${currentYear}`);
|
||
|
||
const verification = [];
|
||
|
||
for (const rep of repartition) {
|
||
const typeCode = rep.TypeConge;
|
||
const joursNecessaires = parseFloat(rep.NombreJours || 0);
|
||
|
||
if (typeCode === 'ABS' || typeCode === 'Formation') {
|
||
continue;
|
||
}
|
||
|
||
const typeName = typeCode === 'CP' ? 'Congé payé' : typeCode === 'RTT' ? 'RTT' : typeCode;
|
||
const [typeRow] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', [typeName]);
|
||
|
||
if (typeRow.length === 0) {
|
||
continue;
|
||
}
|
||
|
||
const typeCongeId = typeRow[0].Id;
|
||
|
||
// ====================================
|
||
// 1️⃣ Récupérer les infos du collaborateur
|
||
// ====================================
|
||
const [collabInfo] = await conn.query(`
|
||
SELECT DateEntree, TypeContrat, role
|
||
FROM CollaborateurAD
|
||
WHERE id = ?
|
||
`, [collaborateurId]);
|
||
|
||
const dateEntree = collabInfo[0]?.DateEntree || null;
|
||
const typeContrat = collabInfo[0]?.TypeContrat || '37h';
|
||
const isApprenti = collabInfo[0]?.role === 'Apprenti';
|
||
|
||
// ====================================
|
||
// 2️⃣ Calculer l'acquisition à la date de la demande
|
||
// ====================================
|
||
let acquisALaDate = 0;
|
||
let budgetAnnuel = 0;
|
||
|
||
if (typeCode === 'CP') {
|
||
acquisALaDate = calculerAcquisitionCP_Smart(dateDebutObj, dateEntree);
|
||
budgetAnnuel = 25;
|
||
console.log(`💰 Acquisition CP à la date ${dateDebut}: ${acquisALaDate.toFixed(2)}j`);
|
||
} else if (typeCode === 'RTT' && !isApprenti) {
|
||
const rttData = await calculerAcquisitionRTT_Smart(conn, collaborateurId, dateDebutObj);
|
||
acquisALaDate = rttData.acquisition;
|
||
budgetAnnuel = rttData.config.joursAnnuels;
|
||
console.log(`💰 Acquisition RTT à la date ${dateDebut}: ${acquisALaDate.toFixed(2)}j`);
|
||
}
|
||
|
||
// ====================================
|
||
// 3️⃣ Récupérer le report N-1 (CP uniquement)
|
||
// ====================================
|
||
let reporteN1 = 0;
|
||
|
||
if (typeCode === 'CP') {
|
||
const [compteurN1] = await conn.query(`
|
||
SELECT Solde, SoldeReporte
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collaborateurId, typeCongeId, previousYear]);
|
||
|
||
if (compteurN1.length > 0) {
|
||
reporteN1 = parseFloat(compteurN1[0].Solde || 0);
|
||
}
|
||
}
|
||
|
||
// ====================================
|
||
// 4️⃣ Calculer ce qui a déjà été posé (SANS l'anticipé)
|
||
// ====================================
|
||
const [dejaPose] = await conn.query(`
|
||
SELECT COALESCE(SUM(dd.JoursUtilises), 0) as total
|
||
FROM DeductionDetails dd
|
||
JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
|
||
WHERE dc.CollaborateurADId = ?
|
||
AND dd.TypeCongeId = ?
|
||
AND dd.Annee = ?
|
||
AND dd.TypeDeduction NOT IN ('N Anticip', 'N+1 Anticip', 'Accum Récup', 'Accum Recup')
|
||
AND dc.Statut != 'Refusée'
|
||
`, [collaborateurId, typeCongeId, currentYear]);
|
||
|
||
const dejaPoseNormal = parseFloat(dejaPose[0]?.total || 0);
|
||
|
||
// ====================================
|
||
// 5️⃣ Calculer l'anticipé déjà utilisé
|
||
// ====================================
|
||
const [anticipeUtilise] = await conn.query(`
|
||
SELECT COALESCE(SUM(dd.JoursUtilises), 0) as total
|
||
FROM DeductionDetails dd
|
||
JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
|
||
WHERE dc.CollaborateurADId = ?
|
||
AND dd.TypeCongeId = ?
|
||
AND dd.Annee = ?
|
||
AND dd.TypeDeduction = 'N Anticip'
|
||
AND dc.Statut != 'Refusée'
|
||
`, [collaborateurId, typeCongeId, currentYear]);
|
||
|
||
const dejaPoseAnticipe = parseFloat(anticipeUtilise[0]?.total || 0);
|
||
|
||
// ====================================
|
||
// 6️⃣ Calculer l'anticipé disponible
|
||
// ====================================
|
||
const anticipableMax = Math.max(0, budgetAnnuel - acquisALaDate);
|
||
const anticipeDisponible = Math.max(0, anticipableMax - dejaPoseAnticipe);
|
||
|
||
console.log(`💳 Anticipé max possible: ${anticipableMax.toFixed(2)}j`);
|
||
console.log(`💳 Anticipé déjà utilisé: ${dejaPoseAnticipe.toFixed(2)}j`);
|
||
console.log(`💳 Anticipé disponible: ${anticipeDisponible.toFixed(2)}j`);
|
||
|
||
// ====================================
|
||
// 7️⃣ Calculer le solde TOTAL disponible
|
||
// ====================================
|
||
const soldeActuel = Math.max(0, reporteN1 + acquisALaDate - dejaPoseNormal);
|
||
const soldeTotal = soldeActuel + anticipeDisponible;
|
||
|
||
console.log(`📊 Soldes détaillés ${typeCode}:`);
|
||
console.log(` - Report N-1: ${reporteN1.toFixed(2)}j`);
|
||
console.log(` - Acquis à date: ${acquisALaDate.toFixed(2)}j`);
|
||
console.log(` - Déjà posé (normal): ${dejaPoseNormal.toFixed(2)}j`);
|
||
console.log(` - Solde actuel: ${soldeActuel.toFixed(2)}j`);
|
||
console.log(` - Anticipé disponible: ${anticipeDisponible.toFixed(2)}j`);
|
||
console.log(` ✅ TOTAL DISPONIBLE: ${soldeTotal.toFixed(2)}j`);
|
||
|
||
// ====================================
|
||
// 8️⃣ Vérifier la suffisance
|
||
// ====================================
|
||
const suffisant = soldeTotal >= joursNecessaires;
|
||
const deficit = Math.max(0, joursNecessaires - soldeTotal);
|
||
|
||
verification.push({
|
||
type: typeName,
|
||
joursNecessaires,
|
||
reporteN1,
|
||
acquisALaDate,
|
||
dejaPoseNormal,
|
||
dejaPoseAnticipe,
|
||
soldeActuel,
|
||
anticipeDisponible,
|
||
soldeTotal,
|
||
suffisant,
|
||
deficit
|
||
});
|
||
|
||
console.log(`🔍 Vérification ${typeCode}: ${joursNecessaires}j demandés vs ${soldeTotal.toFixed(2)}j disponibles → ${suffisant ? '✅ OK' : '❌ INSUFFISANT'}`);
|
||
}
|
||
|
||
const insuffisants = verification.filter(v => !v.suffisant);
|
||
|
||
return {
|
||
valide: insuffisants.length === 0,
|
||
details: verification,
|
||
insuffisants
|
||
};
|
||
}
|
||
/**
|
||
* Déduit les jours d'un compteur avec gestion de l'anticipation
|
||
* Ordre de déduction : N-1 → N → N Anticip
|
||
*/
|
||
|
||
// ========================================
|
||
// 💰 FONCTION DE DÉDUCTION AVEC ANTICIPATION (SANS ID MANUEL)
|
||
// ========================================
|
||
|
||
async function deductLeaveBalanceWithAnticipation(conn, collaborateurId, typeCongeId, nombreJours, demandeCongeId, dateDebut) {
|
||
const dateDebutObj = new Date(dateDebut);
|
||
const currentYear = dateDebutObj.getFullYear();
|
||
const previousYear = currentYear - 1;
|
||
let joursRestants = nombreJours;
|
||
const deductions = [];
|
||
|
||
console.log(`💳 === DÉDUCTION AVEC ANTICIPATION ===`);
|
||
console.log(` Collaborateur: ${collaborateurId}`);
|
||
console.log(` Type congé: ${typeCongeId}`);
|
||
console.log(` Jours à déduire: ${nombreJours}j`);
|
||
console.log(` Date début: ${dateDebut}`);
|
||
|
||
// ===== ÉTAPE 1 : Déduire du REPORT N-1 =====
|
||
try {
|
||
const [compteurN1] = await conn.query(`
|
||
SELECT Id, Solde, SoldeReporte
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collaborateurId, typeCongeId, previousYear]);
|
||
|
||
if (compteurN1.length > 0 && compteurN1[0].SoldeReporte > 0) {
|
||
const soldeN1 = parseFloat(compteurN1[0].SoldeReporte || 0);
|
||
const aDeduireN1 = Math.min(soldeN1, joursRestants);
|
||
|
||
if (aDeduireN1 > 0) {
|
||
await conn.query(`
|
||
UPDATE CompteurConges
|
||
SET SoldeReporte = SoldeReporte - ?,
|
||
Solde = Solde - ?,
|
||
DerniereMiseAJour = GETDATE()
|
||
WHERE Id = ?
|
||
`, [aDeduireN1, aDeduireN1, compteurN1[0].Id]);
|
||
|
||
// ⭐ SANS SPÉCIFIER L'ID
|
||
await conn.query(`
|
||
INSERT INTO DeductionDetails
|
||
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
|
||
VALUES (?, ?, ?, ?, ?)
|
||
`, [demandeCongeId, typeCongeId, previousYear, 'Report N-1', aDeduireN1]);
|
||
|
||
deductions.push({
|
||
annee: previousYear,
|
||
type: 'Report N-1',
|
||
joursUtilises: aDeduireN1
|
||
});
|
||
|
||
joursRestants -= aDeduireN1;
|
||
console.log(` ✅ Report N-1: ${aDeduireN1.toFixed(2)}j déduits - reste ${joursRestants}j`);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('❌ Erreur déduction N-1:', error.message);
|
||
throw error;
|
||
}
|
||
|
||
// ===== ÉTAPE 2 : Déduire du SOLDE N =====
|
||
if (joursRestants > 0) {
|
||
try {
|
||
const [compteurN] = await conn.query(`
|
||
SELECT Id, Solde, SoldeReporte, Total
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collaborateurId, typeCongeId, currentYear]);
|
||
|
||
if (compteurN.length > 0) {
|
||
const 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 = Solde - ?,
|
||
DerniereMiseAJour = GETDATE()
|
||
WHERE Id = ?
|
||
`, [aDeduireN, compteurN[0].Id]);
|
||
|
||
// ⭐ SANS SPÉCIFIER L'ID
|
||
await conn.query(`
|
||
INSERT INTO DeductionDetails
|
||
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
|
||
VALUES (?, ?, ?, ?, ?)
|
||
`, [demandeCongeId, typeCongeId, currentYear, 'Année N', aDeduireN]);
|
||
|
||
deductions.push({
|
||
annee: currentYear,
|
||
type: 'Année N',
|
||
joursUtilises: aDeduireN
|
||
});
|
||
|
||
joursRestants -= aDeduireN;
|
||
console.log(` ✅ Solde N: ${aDeduireN.toFixed(2)}j déduits - reste ${joursRestants}j`);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('❌ Erreur déduction N:', error.message);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// ===== ÉTAPE 3 : ANTICIPÉ =====
|
||
if (joursRestants > 0) {
|
||
console.log(` 💳 Il reste ${joursRestants.toFixed(2)}j à déduire → Anticipé`);
|
||
|
||
try {
|
||
const [collabInfo] = await conn.query(`
|
||
SELECT DateEntree, TypeContrat, role
|
||
FROM CollaborateurAD
|
||
WHERE id = ?
|
||
`, [collaborateurId]);
|
||
|
||
const dateEntree = collabInfo[0]?.DateEntree || null;
|
||
|
||
const [typeInfo] = await conn.query(`
|
||
SELECT Nom FROM TypeConge WHERE Id = ?
|
||
`, [typeCongeId]);
|
||
|
||
const typeNom = typeInfo[0]?.Nom;
|
||
let budgetAnnuel = 0;
|
||
let acquisALaDate = 0;
|
||
|
||
if (typeNom === 'Congé payé') {
|
||
acquisALaDate = calculerAcquisitionCP_Smart(dateDebutObj, dateEntree);
|
||
budgetAnnuel = 25;
|
||
} else if (typeNom === 'RTT') {
|
||
const rttData = await calculerAcquisitionRTT_Smart(conn, collaborateurId, dateDebutObj);
|
||
acquisALaDate = rttData.acquisition;
|
||
budgetAnnuel = rttData.config.joursAnnuels;
|
||
}
|
||
|
||
const anticipableMax = Math.max(0, budgetAnnuel - acquisALaDate);
|
||
|
||
const [anticipeUtilise] = await conn.query(`
|
||
SELECT COALESCE(SUM(dd.JoursUtilises), 0) as total
|
||
FROM DeductionDetails dd
|
||
JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
|
||
WHERE dc.CollaborateurADId = ?
|
||
AND dd.TypeCongeId = ?
|
||
AND dd.Annee = ?
|
||
AND dd.TypeDeduction = 'N Anticip'
|
||
AND dc.Statut != 'Refusée'
|
||
`, [collaborateurId, typeCongeId, currentYear]);
|
||
|
||
const dejaPrisAnticipe = parseFloat(anticipeUtilise[0]?.total || 0);
|
||
const anticipeDisponible = Math.max(0, anticipableMax - dejaPrisAnticipe);
|
||
|
||
console.log(` 💳 Anticipable max: ${anticipableMax.toFixed(2)}j`);
|
||
console.log(` 💳 Déjà pris: ${dejaPrisAnticipe.toFixed(2)}j`);
|
||
console.log(` 💳 Disponible: ${anticipeDisponible.toFixed(2)}j`);
|
||
|
||
if (anticipeDisponible >= joursRestants) {
|
||
// ⭐ SANS SPÉCIFIER L'ID
|
||
await conn.query(`
|
||
INSERT INTO DeductionDetails
|
||
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
|
||
VALUES (?, ?, ?, ?, ?)
|
||
`, [demandeCongeId, typeCongeId, currentYear, 'N Anticip', joursRestants]);
|
||
|
||
deductions.push({
|
||
annee: currentYear,
|
||
type: 'N Anticip',
|
||
joursUtilises: joursRestants
|
||
});
|
||
|
||
console.log(` ✅ Anticipé: ${joursRestants.toFixed(2)}j`);
|
||
joursRestants = 0;
|
||
} else {
|
||
return {
|
||
success: false,
|
||
joursDeduitsTotal: nombreJours - joursRestants,
|
||
joursNonDeduits: joursRestants,
|
||
details: deductions,
|
||
error: `Solde insuffisant (manque ${joursRestants.toFixed(2)}j)`
|
||
};
|
||
}
|
||
} catch (error) {
|
||
console.error('❌ Erreur anticipé:', error.message);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
console.log(` ✅ Déduction OK - Total: ${(nombreJours - joursRestants).toFixed(2)}j`);
|
||
|
||
return {
|
||
success: joursRestants === 0,
|
||
joursDeduitsTotal: nombreJours - joursRestants,
|
||
joursNonDeduits: joursRestants,
|
||
details: deductions
|
||
};
|
||
}
|
||
// ========================================
|
||
// 🔧 FONCTION HELPER - GÉNÉRATION D'ID
|
||
// ========================================
|
||
// ✅ VERSION CORRIGÉE
|
||
async function getNextId(connection, tableName) {
|
||
try {
|
||
const [result] = await connection.query(
|
||
`SELECT ISNULL(MAX(Id), 0) + 1 AS NextId FROM ${tableName}`
|
||
);
|
||
return result[0].NextId;
|
||
} catch (error) {
|
||
console.error(`❌ Erreur génération ID pour ${tableName}:`, error.message);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
app.post('/api/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), async (req, res) => {
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
await conn.beginTransaction();
|
||
|
||
const currentYear = new Date().getFullYear();
|
||
|
||
// ✅ Récupérer les fichiers uploadés
|
||
const uploadedFiles = req.files || [];
|
||
console.log('📎 Fichiers médicaux reçus:', uploadedFiles.length);
|
||
|
||
// ✅ Les données arrivent différemment avec FormData
|
||
const DateDebut = req.body.DateDebut;
|
||
const DateFin = req.body.DateFin;
|
||
const NombreJours = parseFloat(req.body.NombreJours);
|
||
const Email = req.body.Email;
|
||
const Nom = req.body.Nom;
|
||
const Commentaire = req.body.Commentaire || '';
|
||
const statut = req.body.statut || null;
|
||
|
||
// ✅ Parser la répartition (elle arrive en string depuis FormData)
|
||
let Repartition;
|
||
try {
|
||
Repartition = JSON.parse(req.body.Repartition || '[]');
|
||
} catch (parseError) {
|
||
console.error('❌ Erreur parsing Repartition:', parseError);
|
||
if (req.files) {
|
||
req.files.forEach(file => {
|
||
if (fs.existsSync(file.path)) {
|
||
fs.unlinkSync(file.path);
|
||
}
|
||
});
|
||
}
|
||
return res.status(400).json({
|
||
success: false,
|
||
message: 'Erreur de format de la répartition'
|
||
});
|
||
}
|
||
|
||
if (!DateDebut || !DateFin || !Repartition || !Email || !Nom) {
|
||
uploadedFiles.forEach(file => {
|
||
if (fs.existsSync(file.path)) {
|
||
fs.unlinkSync(file.path);
|
||
}
|
||
});
|
||
return res.json({ success: false, message: 'Données manquantes' });
|
||
}
|
||
|
||
// ✅ VALIDATION : Si arrêt maladie, il faut au moins 1 fichier
|
||
const hasABS = Repartition.some(r => r.TypeConge === 'ABS');
|
||
if (hasABS && uploadedFiles.length === 0) {
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.json({
|
||
success: false,
|
||
message: 'Un justificatif médical est obligatoire pour un arrêt maladie'
|
||
});
|
||
}
|
||
|
||
// ⭐ VALIDATION DE LA RÉPARTITION
|
||
console.log('\n📥 === SOUMISSION DEMANDE CONGÉ ===');
|
||
console.log('Email:', Email);
|
||
console.log('Période:', DateDebut, '→', DateFin);
|
||
console.log('Nombre de jours total:', NombreJours);
|
||
console.log('Répartition reçue:', JSON.stringify(Repartition, null, 2));
|
||
|
||
// ⭐ Ne compter que CP, RTT ET RÉCUP dans la répartition
|
||
const sommeRepartition = Repartition.reduce((sum, r) => {
|
||
if (r.TypeConge === 'CP' || r.TypeConge === 'RTT' || r.TypeConge === 'Récup') {
|
||
return sum + parseFloat(r.NombreJours || 0);
|
||
}
|
||
return sum;
|
||
}, 0);
|
||
|
||
console.log('Somme répartition CP+RTT+Récup:', sommeRepartition.toFixed(2));
|
||
|
||
// ⭐ VALIDATION : La somme doit correspondre au total
|
||
const hasCountableLeave = Repartition.some(r =>
|
||
r.TypeConge === 'CP' || r.TypeConge === 'RTT' || r.TypeConge === 'Récup'
|
||
);
|
||
|
||
if (hasCountableLeave && Math.abs(sommeRepartition - NombreJours) > 0.01) {
|
||
console.error('❌ ERREUR : Répartition incohérente !');
|
||
|
||
uploadedFiles.forEach(file => {
|
||
if (fs.existsSync(file.path)) {
|
||
fs.unlinkSync(file.path);
|
||
}
|
||
});
|
||
|
||
await conn.rollback();
|
||
conn.release();
|
||
|
||
return res.json({
|
||
success: false,
|
||
message: `Erreur de répartition : la somme (${sommeRepartition.toFixed(2)}j) ne correspond pas au total (${NombreJours}j)`
|
||
});
|
||
}
|
||
|
||
console.log('✅ Validation répartition OK');
|
||
|
||
// ⭐ Récup n'est PAS une demande auto-validée
|
||
const isFormationOnly = Repartition.length === 1 && Repartition[0].TypeConge === 'Formation';
|
||
const statutDemande = statut || (isFormationOnly ? 'Validée' : 'En attente');
|
||
|
||
console.log('🔍 Type de demande:', { isFormationOnly, statut: statutDemande });
|
||
|
||
const [collabAD] = await conn.query('SELECT id, CampusId FROM CollaborateurAD WHERE email = ? LIMIT 1', [Email]);
|
||
const isAD = collabAD.length > 0;
|
||
const collaborateurId = isAD ? collabAD[0].id : null;
|
||
|
||
let employeeId = null;
|
||
if (!isAD) {
|
||
const [user] = await conn.query('SELECT ID FROM Users WHERE Email = ? LIMIT 1', [Email]);
|
||
if (user.length === 0) {
|
||
uploadedFiles.forEach(file => {
|
||
if (fs.existsSync(file.path)) {
|
||
fs.unlinkSync(file.path);
|
||
}
|
||
});
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.json({ success: false, message: 'Utilisateur non trouvé' });
|
||
}
|
||
employeeId = user[0].ID;
|
||
}
|
||
|
||
// ========================================
|
||
// ÉTAPE 1 : Vérification des soldes AVANT tout (MODE MIXTE AVEC ANTICIPATION N+1)
|
||
// ========================================
|
||
if (isAD && collaborateurId && !isFormationOnly) {
|
||
console.log('\n🔍 Vérification des soldes en mode mixte avec anticipation...');
|
||
console.log('Date début:', DateDebut);
|
||
|
||
const [userRole] = await conn.query('SELECT role FROM CollaborateurAD WHERE id = ?', [collaborateurId]);
|
||
const isApprenti = userRole.length > 0 && userRole[0].role === 'Apprenti';
|
||
|
||
const checkResult = await checkLeaveBalanceWithAnticipation(
|
||
conn,
|
||
collaborateurId,
|
||
Repartition,
|
||
DateDebut
|
||
);
|
||
|
||
if (!checkResult.valide) {
|
||
uploadedFiles.forEach(file => {
|
||
if (fs.existsSync(file.path)) fs.unlinkSync(file.path);
|
||
});
|
||
await conn.rollback();
|
||
conn.release();
|
||
|
||
const messagesErreur = checkResult.insuffisants.map(ins => {
|
||
return `${ins.type}: ${ins.joursNecessaires}j demandés mais seulement ${ins.soldeTotal.toFixed(2)}j disponibles (déficit: ${ins.deficit.toFixed(2)}j)`;
|
||
}).join('\n');
|
||
|
||
return res.json({
|
||
success: false,
|
||
message: `❌ Solde(s) insuffisant(s):\n${messagesErreur}`,
|
||
details: checkResult.details,
|
||
insuffisants: checkResult.insuffisants
|
||
});
|
||
}
|
||
|
||
console.log('✅ Tous les soldes sont suffisants (incluant anticipation si nécessaire)\n');
|
||
}
|
||
|
||
// ========================================
|
||
// ÉTAPE 2 : CRÉER LA DEMANDE (AVEC GÉNÉRATION MANUELLE D'ID)
|
||
// ========================================
|
||
console.log('\n📝 Création de la demande...');
|
||
|
||
const typeIds = [];
|
||
for (const rep of Repartition) {
|
||
const code = rep.TypeConge;
|
||
|
||
if (code === 'ABS' || code === 'Formation') {
|
||
continue;
|
||
}
|
||
|
||
const name = code === 'CP' ? 'Congé payé' :
|
||
code === 'RTT' ? 'RTT' :
|
||
code === 'Récup' ? 'Récupération' : code;
|
||
|
||
const [typeRow] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', [name]);
|
||
if (typeRow.length > 0) typeIds.push(typeRow[0].Id);
|
||
}
|
||
|
||
if (typeIds.length === 0) {
|
||
const firstType = Repartition[0]?.TypeConge;
|
||
const name = firstType === 'Formation' ? 'Formation' :
|
||
firstType === 'ABS' ? 'Congé maladie' : 'Congé payé';
|
||
|
||
const [typeRow] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', [name]);
|
||
if (typeRow.length > 0) {
|
||
typeIds.push(typeRow[0].Id);
|
||
} else {
|
||
uploadedFiles.forEach(file => {
|
||
if (fs.existsSync(file.path)) fs.unlinkSync(file.path);
|
||
});
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.json({ success: false, message: 'Aucun type de congé valide' });
|
||
}
|
||
}
|
||
|
||
const typeCongeIdCsv = typeIds.join(',');
|
||
|
||
// 🔥 GÉNÉRATION MANUELLE DE L'ID (CONTOURNEMENT IDENTITY)
|
||
const [maxIdResult] = await conn.query('SELECT ISNULL(MAX(Id), 0) + 1 AS NextId FROM DemandeConge');
|
||
const demandeId = maxIdResult[0].NextId;
|
||
|
||
console.log(`🆔 ID généré manuellement: ${demandeId}`);
|
||
|
||
// 🔥 INSERT AVEC ID EXPLICITE
|
||
await conn.query(`
|
||
INSERT INTO DemandeConge
|
||
(Id, CollaborateurADId, DateDebut, DateFin, TypeCongeId, Statut, DateDemande, Commentaire, Validateur, NombreJours)
|
||
VALUES (?, ?, ?, ?, ?, ?, GETDATE(), ?, ?, ?)
|
||
`, [
|
||
demandeId,
|
||
collaborateurId,
|
||
DateDebut,
|
||
DateFin,
|
||
typeCongeIdCsv,
|
||
statutDemande,
|
||
Commentaire || '',
|
||
'',
|
||
NombreJours
|
||
]);
|
||
|
||
console.log(`✅ Demande créée avec ID ${demandeId} - Statut: ${statutDemande}`);
|
||
|
||
// ========================================
|
||
// ÉTAPE 3 : Sauvegarder les fichiers médicaux
|
||
// ========================================
|
||
if (uploadedFiles.length > 0) {
|
||
console.log('\n📎 Sauvegarde des fichiers médicaux...');
|
||
for (const file of uploadedFiles) {
|
||
await conn.query(`
|
||
INSERT INTO DocumentsMedicaux
|
||
(DemandeCongeId, NomFichier, CheminFichier, TypeMime, TailleFichier, DateUpload)
|
||
VALUES (?, ?, ?, ?, ?, GETDATE())
|
||
`, [demandeId, file.originalname, file.path, file.mimetype, file.size]);
|
||
console.log(` ✓ ${file.originalname}`);
|
||
}
|
||
}
|
||
|
||
// ========================================
|
||
// ÉTAPE 4 : Sauvegarder la répartition
|
||
// ========================================
|
||
// ========================================
|
||
// ÉTAPE 4 : Sauvegarder la répartition
|
||
// ========================================
|
||
// 5️⃣ Sauvegarder la répartition
|
||
console.log('\n📊 Sauvegarde de la répartition en base...');
|
||
for (const rep of Repartition) {
|
||
const code = rep.TypeConge;
|
||
const name = code === 'CP' ? 'Congé payé' :
|
||
code === 'RTT' ? 'RTT' :
|
||
code === 'ABS' ? 'Congé maladie' :
|
||
code === 'Formation' ? 'Formation' :
|
||
code === 'Récup' ? 'Récupération' : code;
|
||
|
||
const [typeRow] = await conn.query(
|
||
'SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1',
|
||
[name]
|
||
);
|
||
|
||
if (typeRow.length > 0) {
|
||
// ⭐ GÉNÉRER L'ID MANUELLEMENT
|
||
const demandeCongeTypeId = await getNextId(conn, 'DemandeCongeType');
|
||
|
||
await conn.query(`
|
||
INSERT INTO DemandeCongeType
|
||
(Id, DemandeCongeId, TypeCongeId, NombreJours, PeriodeJournee)
|
||
VALUES (?, ?, ?, ?, ?)
|
||
`, [
|
||
demandeCongeTypeId,
|
||
demandeId,
|
||
typeRow[0].Id,
|
||
rep.NombreJours,
|
||
rep.PeriodeJournee || 'Journée entière'
|
||
]);
|
||
console.log(` ✓ ${name}: ${rep.NombreJours}j (${rep.PeriodeJournee || 'Journée entière'})`);
|
||
}
|
||
}
|
||
// ========================================
|
||
// ÉTAPE 5 : Déduction des compteurs CP/RTT/RÉCUP (AVEC ANTICIPATION N+1)
|
||
// ========================================
|
||
if (isAD && collaborateurId && !isFormationOnly) {
|
||
console.log('\n📉 Déduction des compteurs (avec anticipation N+1)...');
|
||
|
||
for (const rep of Repartition) {
|
||
if (rep.TypeConge === 'ABS' || rep.TypeConge === 'Formation') {
|
||
console.log(` ⏩ ${rep.TypeConge} ignoré (pas de déduction)`);
|
||
continue;
|
||
}
|
||
|
||
// ⭐ TRAITEMENT SPÉCIAL POUR RÉCUP
|
||
if (rep.TypeConge === 'Récup') {
|
||
const [recupType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Récupération']);
|
||
|
||
if (recupType.length > 0) {
|
||
await conn.query(`
|
||
UPDATE CompteurConges
|
||
SET Solde = CASE WHEN Solde - ? < 0 THEN 0 ELSE Solde - ? END,
|
||
DerniereMiseAJour = GETDATE()
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [rep.NombreJours, rep.NombreJours, collaborateurId, recupType[0].Id, currentYear]);
|
||
|
||
await conn.query(`
|
||
INSERT INTO DeductionDetails
|
||
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
|
||
VALUES (?, ?, ?, 'Récup Posée', ?)
|
||
`, [demandeId, recupType[0].Id, currentYear, rep.NombreJours]);
|
||
|
||
console.log(` ✓ Récup: ${rep.NombreJours}j déduits`);
|
||
}
|
||
continue;
|
||
}
|
||
|
||
// ⭐ CP et RTT : AVEC ANTICIPATION N+1
|
||
const name = rep.TypeConge === 'CP' ? 'Congé payé' : 'RTT';
|
||
const [typeRow] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', [name]);
|
||
|
||
if (typeRow.length > 0) {
|
||
const deductResult = await deductLeaveBalanceWithAnticipation(
|
||
conn,
|
||
collaborateurId,
|
||
typeRow[0].Id,
|
||
rep.NombreJours,
|
||
demandeId,
|
||
DateDebut
|
||
);
|
||
|
||
console.log(` ✓ ${name}: ${rep.NombreJours}j déduits`);
|
||
if (deductResult.details && deductResult.details.length > 0) {
|
||
deductResult.details.forEach(d => {
|
||
console.log(` - ${d.type} (${d.annee}): ${d.joursUtilises}j`);
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
await updateSoldeAnticipe(conn, collaborateurId);
|
||
console.log('✅ Déductions terminées\n');
|
||
}
|
||
|
||
// ========================================
|
||
// ÉTAPE 6 : Notifications (Formation uniquement)
|
||
// ========================================
|
||
const dateDebut = new Date(DateDebut).toLocaleDateString('fr-FR');
|
||
const dateFin = new Date(DateFin).toLocaleDateString('fr-FR');
|
||
const datesPeriode = dateDebut === dateFin ? dateDebut : `du ${dateDebut} au ${dateFin}`;
|
||
|
||
if (isFormationOnly && isAD && collaborateurId) {
|
||
await conn.query(`
|
||
INSERT INTO Notifications (CollaborateurADId, Type, Titre, Message, DemandeCongeId, DateCreation, Lu)
|
||
VALUES (?, ?, ?, ?, ?, GETDATE(), 0)
|
||
`, [
|
||
collaborateurId,
|
||
'Success',
|
||
'✅ Formation validée automatiquement',
|
||
`Votre période de formation ${datesPeriode} a été validée automatiquement.`,
|
||
demandeId
|
||
]);
|
||
console.log('\n📬 Notification formation créée');
|
||
}
|
||
|
||
// ========================================
|
||
// ÉTAPE 7 : Récupérer les managers
|
||
// ========================================
|
||
let managers = [];
|
||
if (isAD) {
|
||
const [rows] = await conn.query(`
|
||
SELECT c.email FROM HierarchieValidationAD hv
|
||
JOIN CollaborateurAD c ON hv.SuperieurId = c.id
|
||
WHERE hv.CollaborateurId = ?
|
||
`, [collaborateurId]);
|
||
managers = rows.map(r => r.email);
|
||
}
|
||
|
||
await conn.commit();
|
||
console.log('\n🎉 Transaction validée\n');
|
||
|
||
// ========================================
|
||
// ÉTAPE 8 : Notifier les clients SSE
|
||
// ========================================
|
||
if (isFormationOnly && isAD && collaborateurId) {
|
||
notifyCollabClients({
|
||
type: 'demande-validated',
|
||
demandeId: parseInt(demandeId),
|
||
statut: 'Validée',
|
||
timestamp: new Date().toISOString()
|
||
}, collaborateurId);
|
||
}
|
||
|
||
// ========================================
|
||
// ENVOI DES EMAILS
|
||
// ========================================
|
||
const accessToken = await getGraphToken();
|
||
if (accessToken) {
|
||
const fromEmail = 'gtanoreply@ensup.eu';
|
||
const typesConges = Repartition.map(rep => {
|
||
const typeNom = rep.TypeConge === 'CP' ? 'Congé payé' :
|
||
rep.TypeConge === 'RTT' ? 'RTT' :
|
||
rep.TypeConge === 'ABS' ? 'Congé maladie' :
|
||
rep.TypeConge === 'Formation' ? 'Formation' :
|
||
rep.TypeConge === 'Récup' ? 'Récupération' : rep.TypeConge;
|
||
return `${typeNom}: ${rep.NombreJours}j`;
|
||
}).join(' | ');
|
||
|
||
if (isFormationOnly) {
|
||
const subjectCollab = '✅ Formation enregistrée et validée';
|
||
const bodyCollab = `
|
||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||
<div style="background-color: #10b981; color: white; padding: 20px;">
|
||
<h2 style="margin: 0;">✅ Formation validée</h2>
|
||
</div>
|
||
<div style="padding: 20px;">
|
||
<p>Bonjour <strong>${Nom}</strong>,</p>
|
||
<p>Votre période de formation a été automatiquement validée.</p>
|
||
<p><strong>Période :</strong> ${datesPeriode}</p>
|
||
<p><strong>Durée :</strong> ${NombreJours} jour(s)</p>
|
||
</div>
|
||
</div>
|
||
`;
|
||
try {
|
||
await sendMailGraph(accessToken, fromEmail, Email, subjectCollab, bodyCollab);
|
||
} catch (mailError) {
|
||
console.error('❌ Erreur email:', mailError.message);
|
||
}
|
||
} else {
|
||
const subjectCollab = '✅ Confirmation de réception de votre demande de congé';
|
||
const bodyCollab = `
|
||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||
<div style="background-color: #3b82f6; color: white; padding: 20px;">
|
||
<h2 style="margin: 0;">✅ Demande enregistrée</h2>
|
||
</div>
|
||
<div style="padding: 20px;">
|
||
<p>Bonjour <strong>${Nom}</strong>,</p>
|
||
<p>Votre demande de congé a bien été enregistrée.</p>
|
||
<p><strong>Type :</strong> ${typesConges}</p>
|
||
<p><strong>Période :</strong> ${datesPeriode}</p>
|
||
<p><strong>Durée :</strong> ${NombreJours} jour(s)</p>
|
||
</div>
|
||
</div>
|
||
`;
|
||
try {
|
||
await sendMailGraph(accessToken, fromEmail, Email, subjectCollab, bodyCollab);
|
||
} catch (mailError) {
|
||
console.error('❌ Erreur email:', mailError.message);
|
||
}
|
||
|
||
for (const managerEmail of managers) {
|
||
const subjectManager = `📋 Nouvelle demande de congé - ${Nom}`;
|
||
const bodyManager = `
|
||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||
<div style="background-color: #f59e0b; color: white; padding: 20px;">
|
||
<h2 style="margin: 0;">📋 Nouvelle demande</h2>
|
||
</div>
|
||
<div style="padding: 20px;">
|
||
<p><strong>${Nom}</strong> a soumis une nouvelle demande.</p>
|
||
<p><strong>Type :</strong> ${typesConges}</p>
|
||
<p><strong>Période :</strong> ${datesPeriode}</p>
|
||
</div>
|
||
</div>
|
||
`;
|
||
try {
|
||
await sendMailGraph(accessToken, fromEmail, managerEmail, subjectManager, bodyManager);
|
||
} catch (mailError) {
|
||
console.error('❌ Erreur email manager:', mailError.message);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
res.json({
|
||
success: true,
|
||
message: isFormationOnly ? 'Formation enregistrée et validée automatiquement' : 'Demande soumise',
|
||
request_id: demandeId,
|
||
managers,
|
||
auto_validated: isFormationOnly,
|
||
files_uploaded: uploadedFiles.length
|
||
});
|
||
|
||
} catch (error) {
|
||
await conn.rollback();
|
||
|
||
if (req.files) {
|
||
req.files.forEach(file => {
|
||
if (fs.existsSync(file.path)) {
|
||
fs.unlinkSync(file.path);
|
||
console.log(`🗑️ Fichier supprimé: ${file.originalname}`);
|
||
}
|
||
});
|
||
}
|
||
|
||
console.error('\n❌ ERREUR submitLeaveRequest:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur serveur',
|
||
error: error.message
|
||
});
|
||
} finally {
|
||
conn.release();
|
||
}
|
||
});
|
||
|
||
app.get('/api/download-medical/:documentId', async (req, res) => {
|
||
try {
|
||
const { documentId } = req.params;
|
||
const conn = await pool.getConnection();
|
||
|
||
const [docs] = await conn.query(
|
||
'SELECT TOP 1 * FROM DocumentsMedicaux WHERE Id = ?',
|
||
[documentId]
|
||
);
|
||
|
||
conn.release();
|
||
|
||
if (docs.length === 0) {
|
||
return res.status(404).json({ success: false, message: 'Document non trouvé' });
|
||
}
|
||
|
||
const doc = docs[0];
|
||
|
||
if (!fs.existsSync(doc.CheminFichier)) {
|
||
return res.status(404).json({ success: false, message: 'Fichier introuvable' });
|
||
}
|
||
|
||
res.download(doc.CheminFichier, doc.NomFichier);
|
||
|
||
} catch (error) {
|
||
console.error('Erreur téléchargement:', error);
|
||
res.status(500).json({ success: false, message: 'Erreur serveur' });
|
||
}
|
||
});
|
||
|
||
|
||
// Récupérer les documents d'une demande
|
||
app.get('/api/medical-documents/:demandeId', async (req, res) => {
|
||
try {
|
||
const { demandeId } = req.params;
|
||
const conn = await pool.getConnection();
|
||
|
||
const [docs] = await conn.query(
|
||
`SELECT Id, NomFichier, TypeMime, TailleFichier, DateUpload
|
||
FROM DocumentsMedicaux
|
||
WHERE DemandeCongeId = ?
|
||
ORDER BY DateUpload DESC`,
|
||
[demandeId]
|
||
);
|
||
|
||
conn.release();
|
||
|
||
res.json({
|
||
success: true,
|
||
documents: docs.map(doc => ({
|
||
id: doc.Id,
|
||
nom: doc.NomFichier,
|
||
type: doc.TypeMime,
|
||
taille: doc.TailleFichier,
|
||
date: doc.DateUpload,
|
||
downloadUrl: `/download-medical/${doc.Id}`
|
||
}))
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('Erreur récupération documents:', error);
|
||
res.status(500).json({ success: false, message: 'Erreur serveur' });
|
||
}
|
||
});
|
||
|
||
|
||
app.post('/api/validateRequest', async (req, res) => {
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
await conn.beginTransaction();
|
||
const { request_id, action, validator_id, comment } = req.body;
|
||
|
||
console.log(`\n🔍 Validation demande #${request_id} - Action: ${action}`);
|
||
|
||
if (!request_id || !action || !validator_id) {
|
||
return res.json({ success: false, message: 'Données manquantes' });
|
||
}
|
||
|
||
// Récupérer le validateur
|
||
const [validator] = await conn.query(
|
||
'SELECT TOP 1 Id, prenom, nom, email, CampusId FROM CollaborateurAD WHERE Id = ?',
|
||
[validator_id]
|
||
);
|
||
|
||
if (validator.length === 0) {
|
||
throw new Error('Validateur introuvable');
|
||
}
|
||
|
||
// Récupérer la demande
|
||
const [requests] = await conn.query(
|
||
`SELECT TOP 1
|
||
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 = ?`,
|
||
[request_id]
|
||
);
|
||
|
||
if (requests.length === 0) {
|
||
throw new Error('Demande non trouvée');
|
||
}
|
||
|
||
const request = requests[0];
|
||
|
||
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';
|
||
|
||
// Si refus, restaurer les soldes
|
||
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);
|
||
}
|
||
|
||
// Mettre à jour le statut
|
||
await conn.query(
|
||
`UPDATE DemandeConge
|
||
SET Statut = ?,
|
||
ValidateurId = ?,
|
||
ValidateurADId = ?,
|
||
DateValidation = GETDATE(),
|
||
CommentaireValidation = ?
|
||
WHERE Id = ?`,
|
||
[newStatus, validator_id, validator_id, comment || '', request_id]
|
||
);
|
||
|
||
// Créer une notification
|
||
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 (?, ?, ?, ?, ?, GETDATE(), 0)`,
|
||
[request.CollaborateurADId, notifTitle, notifMessage, notifType, request_id]
|
||
);
|
||
|
||
await conn.commit();
|
||
|
||
// Envoyer email via Microsoft Graph
|
||
console.log('\n📧 === TENTATIVE ENVOI EMAIL ===');
|
||
console.log('1. Récupération token...');
|
||
|
||
const accessToken = await getGraphToken();
|
||
console.log('2. Token obtenu ?', accessToken ? 'OUI' : 'NON');
|
||
|
||
if (accessToken && request.collaborateur_email) {
|
||
const fromEmail = 'gtanoreply@ensup.eu';
|
||
const collaborateurNom = `${request.prenom} ${request.nom}`;
|
||
const validateurNom = `${validator[0].prenom} ${validator[0].nom}`;
|
||
|
||
console.log('3. Préparation email professionnel...');
|
||
console.log(' De:', fromEmail);
|
||
console.log(' À:', request.collaborateur_email);
|
||
console.log(' Collaborateur:', collaborateurNom);
|
||
console.log(' Validateur:', validateurNom);
|
||
|
||
const dateDebut = new Date(request.DateDebut).toLocaleDateString('fr-FR');
|
||
const dateFin = new Date(request.DateFin).toLocaleDateString('fr-FR');
|
||
const datesPeriode = dateDebut === dateFin ? dateDebut : `du ${dateDebut} au ${dateFin}`;
|
||
|
||
const subject = action === 'approve'
|
||
? '✅ Votre demande de congé a été approuvée'
|
||
: '❌ Votre demande de congé a été refusée';
|
||
|
||
const emailHtml = `
|
||
<html>
|
||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||
<div style="max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #ddd; border-radius: 8px;">
|
||
<h2 style="color: ${action === 'approve' ? '#10b981' : '#ef4444'};">${subject}</h2>
|
||
|
||
<p>Bonjour ${collaborateurNom},</p>
|
||
|
||
<p>Votre demande de ${request.TypeConge} pour ${request.NombreJours} jour(s) ${datesPeriode} a été <strong>${action === 'approve' ? 'approuvée' : 'refusée'}</strong> par ${validateurNom}.</p>
|
||
|
||
${comment ? `<p><strong>Commentaire:</strong> ${comment}</p>` : ''}
|
||
|
||
<p>Vous pouvez consulter les détails dans l'application GTA.</p>
|
||
|
||
<hr style="border: none; border-top: 1px solid #ddd; margin: 20px 0;">
|
||
|
||
<p style="font-size: 12px; color: #666;">
|
||
Ceci est un email automatique, merci de ne pas y répondre.
|
||
</p>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
`;
|
||
|
||
const emailSent = await sendMailGraph(accessToken, fromEmail, request.collaborateur_email, subject, emailHtml);
|
||
console.log('4. Email envoyé ?', emailSent ? 'OUI ✅' : 'NON ❌');
|
||
}
|
||
|
||
res.json({
|
||
success: true,
|
||
message: `Demande ${action === 'approve' ? 'approuvée' : 'refusée'} avec succès`
|
||
});
|
||
|
||
} catch (error) {
|
||
await conn.rollback();
|
||
console.error('❌ Erreur validateRequest:', error);
|
||
res.status(500).json({ success: false, message: error.message });
|
||
} finally {
|
||
conn.release();
|
||
}
|
||
});
|
||
|
||
|
||
app.get('/api/testRestoration', async (req, res) => {
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
const { demande_id, collab_id } = req.query;
|
||
|
||
if (!demande_id || !collab_id) {
|
||
return res.json({
|
||
success: false,
|
||
message: 'Paramètres manquants: demande_id et collab_id requis'
|
||
});
|
||
}
|
||
|
||
// 1. Voir les déductions enregistrées
|
||
const [deductions] = await conn.query(
|
||
`SELECT dd.*, tc.Nom as TypeNom
|
||
FROM DeductionDetails dd
|
||
JOIN TypeConge tc ON dd.TypeCongeId = tc.Id
|
||
WHERE dd.DemandeCongeId = ?`,
|
||
[demande_id]
|
||
);
|
||
|
||
// 2. Voir l'état actuel des compteurs
|
||
const [compteurs] = await conn.query(
|
||
`SELECT cc.*, tc.Nom as TypeNom
|
||
FROM CompteurConges cc
|
||
JOIN TypeConge tc ON cc.TypeCongeId = tc.Id
|
||
WHERE cc.CollaborateurADId = ?
|
||
ORDER BY tc.Nom, cc.Annee DESC`,
|
||
[collab_id]
|
||
);
|
||
|
||
// 3. Calculer ce que devrait être la restauration
|
||
const planRestauration = deductions.map(d => ({
|
||
type: d.TypeNom,
|
||
annee: d.Annee,
|
||
typeDeduction: d.TypeDeduction,
|
||
joursARestorer: d.JoursUtilises,
|
||
action: d.TypeDeduction === 'Reporté N-1'
|
||
? 'Ajouter au SoldeReporte ET au Solde'
|
||
: 'Ajouter au Solde uniquement'
|
||
}));
|
||
|
||
conn.release();
|
||
|
||
res.json({
|
||
success: true,
|
||
demande_id: demande_id,
|
||
collaborateur_id: collab_id,
|
||
deductions_enregistrees: deductions,
|
||
compteurs_actuels: compteurs,
|
||
plan_restauration: planRestauration,
|
||
instructions: planRestauration.length === 0
|
||
? "❌ Aucune déduction trouvée - La demande a été créée avant l'installation du tracking"
|
||
: "✅ Déductions trouvées - La restauration devrait fonctionner"
|
||
});
|
||
|
||
} catch (error) {
|
||
conn.release();
|
||
res.status(500).json({
|
||
success: false,
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
|
||
function normalizeRole(role) {
|
||
if (!role) return null;
|
||
|
||
const roleLower = role.toLowerCase();
|
||
|
||
// Normaliser les variantes féminines et masculines
|
||
if (roleLower === 'collaboratrice') return 'collaborateur';
|
||
if (roleLower === 'validatrice') return 'validateur';
|
||
if (roleLower === 'directrice de campus') return 'directeur de campus';
|
||
if (roleLower === 'apprentie') return 'apprenti';
|
||
|
||
return roleLower;
|
||
}
|
||
|
||
app.get('/api/getSocietesByCampus', async (req, res) => {
|
||
try {
|
||
const { campusId } = req.query;
|
||
|
||
const conn = await pool.getConnection();
|
||
|
||
const [societes] = await conn.query(`
|
||
SELECT DISTINCT s.Id, s.Nom
|
||
FROM SocieteCampus sc
|
||
JOIN Societe s ON sc.SocieteId = s.Id
|
||
WHERE sc.CampusId = ?
|
||
ORDER BY
|
||
CASE WHEN s.Nom LIKE '%SOLUTION%' THEN 1 ELSE 2 END,
|
||
s.Nom
|
||
`, [campusId]);
|
||
|
||
conn.release();
|
||
|
||
res.json({
|
||
success: true,
|
||
societes: societes
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('Erreur getSocietesByCampus:', error);
|
||
res.status(500).json({ success: false, message: error.message });
|
||
}
|
||
});
|
||
|
||
// ⭐ NOUVELLE ROUTE HELPER : Récupérer les campus d'une société
|
||
app.get('/api/getCampusBySociete', async (req, res) => {
|
||
try {
|
||
const { societeId } = req.query;
|
||
|
||
const conn = await pool.getConnection();
|
||
|
||
const [campus] = await conn.query(`
|
||
SELECT DISTINCT c.Id, c.Nom, sc.Principal
|
||
FROM SocieteCampus sc
|
||
JOIN Campus c ON sc.CampusId = c.Id
|
||
WHERE sc.SocieteId = ?
|
||
ORDER BY
|
||
sc.Principal DESC, -- Principal en premier
|
||
c.Nom
|
||
`, [societeId]);
|
||
|
||
conn.release();
|
||
|
||
res.json({
|
||
success: true,
|
||
campus: campus,
|
||
isMultiCampus: campus.length > 1
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('Erreur getCampusBySociete:', error);
|
||
res.status(500).json({ success: false, message: error.message });
|
||
}
|
||
});
|
||
|
||
|
||
|
||
|
||
|
||
// ========================================
|
||
// ROUTE getTeamLeaves COMPLÈTE
|
||
// ========================================
|
||
app.get('/api/getTeamLeaves', async (req, res) => {
|
||
try {
|
||
let { user_id: userIdParam, role: roleParam, selectedCampus, selectedSociete, selectedService } = req.query;
|
||
|
||
console.log(`🔍 Paramètres reçus: user_id=${userIdParam}, role=${roleParam}, selectedCampus=${selectedCampus}`);
|
||
|
||
if (!userIdParam) {
|
||
return res.json({ success: false, message: 'ID utilisateur manquant' });
|
||
}
|
||
|
||
const isUUID = userIdParam.length > 10 && userIdParam.includes('-');
|
||
console.log(`📝 Type ID détecté: ${isUUID ? 'UUID' : 'Numérique'}`);
|
||
|
||
const userRequest = pool.request();
|
||
userRequest.input('userIdParam', userIdParam);
|
||
|
||
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'} = @userIdParam
|
||
`;
|
||
|
||
const userResult = await userRequest.query(userQuery);
|
||
|
||
if (!userResult.recordset || userResult.recordset.length === 0) {
|
||
return res.json({ success: false, message: 'Collaborateur non trouvé' });
|
||
}
|
||
|
||
const userInfo = userResult.recordset[0];
|
||
const serviceId = userInfo.ServiceId;
|
||
const campusId = userInfo.CampusId;
|
||
const societeId = userInfo.SocieteId;
|
||
const userEmail = userInfo.email;
|
||
const campusNom = userInfo.campusNom;
|
||
const serviceNom = userInfo.serviceNom;
|
||
const societeNom = userInfo.societeNom;
|
||
|
||
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(` - ServiceNom: ${serviceNom}`);
|
||
console.log(` - SocieteId: ${societeId}`);
|
||
console.log(` - Role normalisé: ${role}`);
|
||
|
||
const filters = {};
|
||
|
||
// ========================================
|
||
// CAS 1: PRESIDENT, ADMIN, RH, DIRECTEUR DE CAMPUS
|
||
// ⚠️ SANS VALIDATEUR - C'est la correction principale !
|
||
// ========================================
|
||
if (role === 'president' || role === 'admin' || role === 'rh' || role === 'directeur de campus') {
|
||
console.log("CAS 1: President/Admin/RH/Directeur de Campus - Vue globale");
|
||
console.log(` Filtres reçus: Société=${selectedSociete}, Campus=${selectedCampus}, Service=${selectedService}`);
|
||
|
||
// 1️⃣ SOCIÉTÉS
|
||
const societesRequest = pool.request();
|
||
const societesResult = await societesRequest.query(`
|
||
SELECT DISTINCT Nom
|
||
FROM Societe
|
||
ORDER BY Nom
|
||
`);
|
||
filters.societes = societesResult.recordset.map(s => s.Nom);
|
||
console.log('📊 Sociétés disponibles:', filters.societes);
|
||
|
||
// 2️⃣ CAMPUS
|
||
let campusRequest = pool.request();
|
||
let campusQuery;
|
||
|
||
if (selectedSociete && selectedSociete !== 'all') {
|
||
campusQuery = `
|
||
SELECT DISTINCT c.Nom
|
||
FROM Campus c
|
||
JOIN CollaborateurAD ca ON ca.CampusId = c.Id
|
||
JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE so.Nom = @selectedSociete
|
||
AND (ca.actif = 1 OR ca.actif IS NULL)
|
||
ORDER BY c.Nom
|
||
`;
|
||
campusRequest.input('selectedSociete', selectedSociete);
|
||
} else {
|
||
campusQuery = `
|
||
SELECT DISTINCT Nom
|
||
FROM Campus
|
||
ORDER BY Nom
|
||
`;
|
||
}
|
||
|
||
const campusResult = await campusRequest.query(campusQuery);
|
||
filters.campus = campusResult.recordset.map(c => c.Nom);
|
||
console.log('📊 Campus disponibles:', filters.campus);
|
||
|
||
if (role === 'directeur de campus') {
|
||
filters.defaultCampus = campusNom;
|
||
console.log('🏢 Campus par défaut pour directeur:', campusNom);
|
||
}
|
||
|
||
// 3️⃣ SERVICES
|
||
let servicesQuery = `
|
||
SELECT DISTINCT s.Nom
|
||
FROM Services s
|
||
JOIN CollaborateurAD ca ON ca.ServiceId = s.Id
|
||
`;
|
||
|
||
let servicesConditions = ['(ca.actif = 1 OR ca.actif IS NULL)'];
|
||
let servicesRequest = pool.request();
|
||
|
||
if (selectedSociete && selectedSociete !== 'all') {
|
||
servicesQuery += '\nJOIN Societe so ON ca.SocieteId = so.Id';
|
||
servicesConditions.push('so.Nom = @selectedSociete');
|
||
servicesRequest.input('selectedSociete', selectedSociete);
|
||
}
|
||
|
||
if (selectedCampus && selectedCampus !== 'all') {
|
||
servicesQuery += '\nJOIN Campus c ON ca.CampusId = c.Id';
|
||
servicesConditions.push('c.Nom = @selectedCampus');
|
||
servicesRequest.input('selectedCampus', selectedCampus);
|
||
}
|
||
|
||
servicesQuery += `\nWHERE ${servicesConditions.join(' AND ')}\nORDER BY s.Nom`;
|
||
|
||
const servicesResult = await servicesRequest.query(servicesQuery);
|
||
filters.services = servicesResult.recordset.map(s => s.Nom);
|
||
|
||
// ⭐ LISTE DES EMPLOYÉS
|
||
let employeesQuery = `
|
||
SELECT
|
||
CONCAT(ca.prenom, ' ', ca.nom) AS fullname,
|
||
c.Nom AS campusnom,
|
||
so.Nom AS societenom,
|
||
s.Nom AS servicenom
|
||
FROM CollaborateurAD ca
|
||
JOIN Services s ON ca.ServiceId = s.Id
|
||
JOIN Campus c ON ca.CampusId = c.Id
|
||
JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE (ca.actif = 1 OR ca.actif IS NULL)
|
||
`;
|
||
|
||
let employeesConditions = [];
|
||
let employeesRequest = pool.request();
|
||
|
||
if (selectedSociete && selectedSociete !== 'all') {
|
||
employeesConditions.push('so.Nom = @selectedSociete');
|
||
employeesRequest.input('selectedSociete', selectedSociete);
|
||
}
|
||
|
||
if (selectedCampus && selectedCampus !== 'all') {
|
||
employeesConditions.push('c.Nom = @selectedCampus');
|
||
employeesRequest.input('selectedCampus', selectedCampus);
|
||
} else if (role === 'directeur de campus' && campusNom) {
|
||
employeesConditions.push('c.Nom = @campusNom');
|
||
employeesRequest.input('campusNom', campusNom);
|
||
}
|
||
|
||
if (selectedService && selectedService !== 'all') {
|
||
employeesConditions.push('s.Nom = @selectedService');
|
||
employeesRequest.input('selectedService', selectedService);
|
||
}
|
||
|
||
if (employeesConditions.length > 0) {
|
||
employeesQuery += ` AND ${employeesConditions.join(' AND ')}`;
|
||
}
|
||
|
||
employeesQuery += ` ORDER BY so.Nom, c.Nom, ca.prenom, ca.nom`;
|
||
|
||
const employeesResult = await employeesRequest.query(employeesQuery);
|
||
|
||
filters.employees = employeesResult.recordset.map(e => ({
|
||
name: e.fullname,
|
||
campus: e.campusnom,
|
||
societe: e.societenom,
|
||
service: e.servicenom
|
||
}));
|
||
|
||
console.log(`👥 Employés trouvés:`, filters.employees.length);
|
||
|
||
// QUERY DES CONGÉS
|
||
let whereConditions = [`dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente')`];
|
||
let queryRequest = pool.request();
|
||
|
||
if (selectedSociete && selectedSociete !== 'all') {
|
||
whereConditions.push('so.Nom = @selectedSociete');
|
||
queryRequest.input('selectedSociete', selectedSociete);
|
||
}
|
||
|
||
if (selectedCampus && selectedCampus !== 'all') {
|
||
whereConditions.push('c.Nom = @selectedCampus');
|
||
queryRequest.input('selectedCampus', selectedCampus);
|
||
} else if (role === 'directeur de campus' && campusNom) {
|
||
whereConditions.push('c.Nom = @campusNom');
|
||
queryRequest.input('campusNom', campusNom);
|
||
}
|
||
|
||
if (selectedService && selectedService !== 'all') {
|
||
whereConditions.push('s.Nom = @selectedService');
|
||
queryRequest.input('selectedService', selectedService);
|
||
}
|
||
|
||
const query = `
|
||
SELECT
|
||
CONVERT(VARCHAR(10), dc.DateDebut, 23) AS startdate,
|
||
CONVERT(VARCHAR(10), dc.DateFin, 23) AS enddate,
|
||
CONCAT(ca.prenom, ' ', ca.nom) AS employeename,
|
||
(
|
||
SELECT STRING_AGG(Nom, ', ') WITHIN GROUP (ORDER BY Nom)
|
||
FROM (SELECT DISTINCT tc2.Nom
|
||
FROM DemandeCongeType dct2
|
||
JOIN TypeConge tc2 ON dct2.TypeCongeId = tc2.Id
|
||
WHERE dct2.DemandeCongeId = dc.Id) AS DistinctTypes
|
||
) AS type,
|
||
CONCAT(
|
||
'[',
|
||
STRING_AGG(
|
||
CONCAT(
|
||
'{"type":"', tc.Nom,
|
||
'","jours":', dct.NombreJours,
|
||
',"periode":"', COALESCE(dct.PeriodeJournee, 'Journée entière'), '"}'
|
||
),
|
||
','
|
||
),
|
||
']'
|
||
) AS detailsconges,
|
||
MAX(tc.CouleurHex) AS color,
|
||
dc.Statut AS statut,
|
||
s.Nom AS servicenom,
|
||
c.Nom AS campusnom,
|
||
so.Nom AS societenom,
|
||
dc.NombreJours AS nombrejoursouvres
|
||
FROM DemandeConge dc
|
||
JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id
|
||
LEFT JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId
|
||
LEFT JOIN TypeConge tc ON dct.TypeCongeId = tc.Id
|
||
JOIN Services s ON ca.ServiceId = s.Id
|
||
JOIN Campus c ON ca.CampusId = c.Id
|
||
JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE ${whereConditions.join(' AND ')}
|
||
GROUP BY dc.Id, dc.DateDebut, dc.DateFin, ca.prenom, ca.nom, dc.Statut, s.Nom, c.Nom, so.Nom, dc.NombreJours
|
||
ORDER BY c.Nom, dc.DateDebut ASC
|
||
`;
|
||
|
||
console.log(`🔍 Query finale WHERE:`, whereConditions.join(' AND '));
|
||
|
||
const leavesResult = await queryRequest.query(query);
|
||
const formattedLeaves = leavesResult.recordset.map(leave => ({
|
||
...leave
|
||
}));
|
||
|
||
console.log(`✅ ${formattedLeaves.length} congés trouvés`);
|
||
|
||
return res.json({
|
||
success: true,
|
||
role: role,
|
||
leaves: formattedLeaves,
|
||
filters: filters
|
||
});
|
||
}
|
||
|
||
// ========================================
|
||
// CAS 2: VALIDATEUR - Vue équipe via HierarchieValidationAD
|
||
// ⚠️ C'est ici que le validateur doit tomber !
|
||
// ========================================
|
||
else if (role === 'validateur') {
|
||
console.log("CAS 2: Validateur - Vue de l'équipe via HierarchieValidationAD");
|
||
|
||
console.log(`📍 Validateur: Service=${serviceNom}, Campus=${campusNom}, Société=${societeNom}`);
|
||
|
||
const isFirstLoad = !selectedCampus && !selectedService && !selectedSociete;
|
||
|
||
if (isFirstLoad) {
|
||
console.log('🎯 Premier chargement validateur : initialisation avec valeurs par défaut');
|
||
selectedCampus = campusNom;
|
||
selectedService = serviceNom;
|
||
selectedSociete = societeNom;
|
||
}
|
||
|
||
console.log(`📍 Filtres appliqués: Société=${selectedSociete}, Campus=${selectedCampus}, Service=${selectedService}`);
|
||
|
||
// Sociétés disponibles
|
||
const societesRequest = pool.request();
|
||
const societesResult = await societesRequest.query(`
|
||
SELECT DISTINCT so.Nom
|
||
FROM Societe so
|
||
JOIN CollaborateurAD ca ON ca.SocieteId = so.Id
|
||
WHERE (ca.actif = 1 OR ca.actif IS NULL)
|
||
ORDER BY so.Nom
|
||
`);
|
||
filters.societes = societesResult.recordset.map(s => s.Nom);
|
||
|
||
// Campus disponibles
|
||
let campusQuery = `
|
||
SELECT DISTINCT c.Nom
|
||
FROM Campus c
|
||
JOIN CollaborateurAD ca ON ca.CampusId = c.Id
|
||
WHERE (ca.actif = 1 OR ca.actif IS NULL)
|
||
`;
|
||
let campusRequest = pool.request();
|
||
|
||
if (selectedSociete && selectedSociete !== 'all') {
|
||
campusQuery += ` AND ca.SocieteId = (SELECT Id FROM Societe WHERE Nom = @selectedSociete)`;
|
||
campusRequest.input('selectedSociete', selectedSociete);
|
||
}
|
||
|
||
campusQuery += ` ORDER BY c.Nom`;
|
||
const campusResult = await campusRequest.query(campusQuery);
|
||
filters.campus = campusResult.recordset.map(c => c.Nom);
|
||
|
||
// Services disponibles
|
||
let servicesQuery = `
|
||
SELECT DISTINCT s.Nom
|
||
FROM Services s
|
||
JOIN CollaborateurAD ca ON ca.ServiceId = s.Id
|
||
WHERE (ca.actif = 1 OR ca.actif IS NULL)
|
||
`;
|
||
let servicesRequest = pool.request();
|
||
|
||
if (selectedSociete && selectedSociete !== 'all') {
|
||
servicesQuery += ` AND ca.SocieteId = (SELECT Id FROM Societe WHERE Nom = @selectedSociete)`;
|
||
servicesRequest.input('selectedSociete', selectedSociete);
|
||
}
|
||
|
||
if (selectedCampus && selectedCampus !== 'all') {
|
||
servicesQuery += ` AND ca.CampusId = (SELECT Id FROM Campus WHERE Nom = @selectedCampus)`;
|
||
servicesRequest.input('selectedCampus', selectedCampus);
|
||
}
|
||
|
||
servicesQuery += ` ORDER BY s.Nom`;
|
||
const servicesResult = await servicesRequest.query(servicesQuery);
|
||
filters.services = servicesResult.recordset.map(s => s.Nom);
|
||
|
||
filters.defaultCampus = campusNom;
|
||
filters.defaultService = serviceNom;
|
||
filters.defaultSociete = societeNom;
|
||
|
||
// ⭐ LISTE DES EMPLOYÉS - UNIQUEMENT CEUX DE L'ÉQUIPE DU VALIDATEUR
|
||
let employeesQuery = `
|
||
SELECT
|
||
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
|
||
JOIN HierarchieValidationAD h ON ca.id = h.CollaborateurId
|
||
WHERE h.SuperieurId = @userId
|
||
AND (ca.actif = 1 OR ca.actif IS NULL)
|
||
`;
|
||
|
||
let employeesRequest = pool.request();
|
||
employeesRequest.input('userId', userInfo.id);
|
||
let employeesConditions = [];
|
||
|
||
if (selectedSociete && selectedSociete !== 'all') {
|
||
employeesConditions.push('so.Nom = @selectedSociete');
|
||
employeesRequest.input('selectedSociete', selectedSociete);
|
||
}
|
||
|
||
if (selectedCampus && selectedCampus !== 'all') {
|
||
employeesConditions.push('c.Nom = @selectedCampus');
|
||
employeesRequest.input('selectedCampus', selectedCampus);
|
||
}
|
||
|
||
if (selectedService && selectedService !== 'all') {
|
||
employeesConditions.push('s.Nom = @selectedService');
|
||
employeesRequest.input('selectedService', selectedService);
|
||
}
|
||
|
||
if (employeesConditions.length > 0) {
|
||
employeesQuery += ` AND ${employeesConditions.join(' AND ')}`;
|
||
}
|
||
|
||
employeesQuery += ` ORDER BY s.Nom, ca.prenom, ca.nom`;
|
||
|
||
const employeesResult = await employeesRequest.query(employeesQuery);
|
||
|
||
filters.employees = employeesResult.recordset.map(emp => ({
|
||
name: emp.fullname,
|
||
campus: emp.campusnom,
|
||
societe: emp.societenom,
|
||
service: emp.servicenom
|
||
}));
|
||
|
||
console.log(`👥 Équipe du validateur: ${filters.employees.length} personnes`);
|
||
|
||
// ⭐ QUERY DES CONGÉS - UNIQUEMENT LES COLLABORATEURS DU VALIDATEUR
|
||
let queryConditions = `WHERE dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente')
|
||
AND ca.id IN (
|
||
SELECT CollaborateurId FROM HierarchieValidationAD WHERE SuperieurId = @userId
|
||
)`;
|
||
|
||
let queryRequest = pool.request();
|
||
queryRequest.input('userId', userInfo.id);
|
||
let congesConditions = [];
|
||
|
||
if (selectedSociete && selectedSociete !== 'all') {
|
||
congesConditions.push('so.Nom = @selectedSociete');
|
||
queryRequest.input('selectedSociete', selectedSociete);
|
||
}
|
||
|
||
if (selectedCampus && selectedCampus !== 'all') {
|
||
congesConditions.push('c.Nom = @selectedCampus');
|
||
queryRequest.input('selectedCampus', selectedCampus);
|
||
}
|
||
|
||
if (selectedService && selectedService !== 'all') {
|
||
congesConditions.push('s.Nom = @selectedService');
|
||
queryRequest.input('selectedService', selectedService);
|
||
}
|
||
|
||
if (congesConditions.length > 0) {
|
||
queryConditions += ` AND ${congesConditions.join(' AND ')}`;
|
||
}
|
||
|
||
const query = `
|
||
SELECT
|
||
CONVERT(VARCHAR(10), dc.DateDebut, 23) AS startdate,
|
||
CONVERT(VARCHAR(10), dc.DateFin, 23) AS enddate,
|
||
CONCAT(ca.prenom, ' ', ca.nom) AS employeename,
|
||
(
|
||
SELECT STRING_AGG(Nom, ', ') WITHIN GROUP (ORDER BY Nom)
|
||
FROM (SELECT DISTINCT tc2.Nom
|
||
FROM DemandeCongeType dct2
|
||
JOIN TypeConge tc2 ON dct2.TypeCongeId = tc2.Id
|
||
WHERE dct2.DemandeCongeId = dc.Id) AS DistinctTypes
|
||
) AS type,
|
||
CONCAT(
|
||
'[',
|
||
STRING_AGG(
|
||
CONCAT(
|
||
'{"type":"', tc.Nom,
|
||
'","jours":', dct.NombreJours,
|
||
',"periode":"', COALESCE(dct.PeriodeJournee, 'Journée entière'), '"}'
|
||
),
|
||
','
|
||
),
|
||
']'
|
||
) AS detailsconges,
|
||
MAX(tc.CouleurHex) AS color,
|
||
dc.Statut AS statut,
|
||
s.Nom AS servicenom,
|
||
c.Nom AS campusnom,
|
||
so.Nom AS societenom,
|
||
dc.NombreJours AS nombrejoursouvres
|
||
FROM DemandeConge dc
|
||
JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id
|
||
LEFT JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId
|
||
LEFT JOIN TypeConge tc ON dct.TypeCongeId = tc.Id
|
||
JOIN Services s ON ca.ServiceId = s.Id
|
||
JOIN Campus c ON ca.CampusId = c.Id
|
||
JOIN Societe so ON ca.SocieteId = so.Id
|
||
${queryConditions}
|
||
GROUP BY dc.Id, dc.DateDebut, dc.DateFin, ca.prenom, ca.nom, dc.Statut, s.Nom, c.Nom, so.Nom, dc.NombreJours
|
||
ORDER BY s.Nom, dc.DateDebut ASC
|
||
`;
|
||
|
||
console.log(`🔍 Query WHERE final validateur:`, queryConditions);
|
||
|
||
const leavesResult = await queryRequest.query(query);
|
||
const formattedLeaves = leavesResult.recordset.map(leave => ({
|
||
...leave
|
||
}));
|
||
|
||
console.log(`✅ ${formattedLeaves.length} congés trouvés pour l'équipe du validateur`);
|
||
|
||
return res.json({
|
||
success: true,
|
||
role: role,
|
||
leaves: formattedLeaves,
|
||
filters: filters
|
||
});
|
||
}
|
||
|
||
// ========================================
|
||
// CAS 3: COLLABORATEUR / APPRENTI
|
||
// ========================================
|
||
else if (role === 'collaborateur' || role === 'apprenti') {
|
||
console.log("CAS 3: Collaborateur/Apprenti avec filtres avancés");
|
||
|
||
console.log(`📍 Filtres reçus du frontend: Société=${selectedSociete}, Campus=${selectedCampus}, Service=${selectedService}`);
|
||
|
||
const isFirstLoad = !selectedCampus && !selectedService && !selectedSociete;
|
||
|
||
if (isFirstLoad) {
|
||
console.log('🎯 Premier chargement : initialisation avec service par défaut');
|
||
selectedCampus = campusNom;
|
||
selectedService = serviceNom;
|
||
selectedSociete = societeNom;
|
||
}
|
||
|
||
console.log(`📍 Filtres appliqués finaux: Société=${selectedSociete}, Campus=${selectedCampus}, Service=${selectedService}`);
|
||
|
||
// Sociétés disponibles
|
||
const societesRequest = pool.request();
|
||
const societesResult = await societesRequest.query(`
|
||
SELECT DISTINCT so.Nom
|
||
FROM Societe so
|
||
JOIN CollaborateurAD ca ON ca.SocieteId = so.Id
|
||
WHERE (ca.actif = 1 OR ca.actif IS NULL)
|
||
ORDER BY so.Nom
|
||
`);
|
||
filters.societes = societesResult.recordset.map(s => s.Nom);
|
||
|
||
// Campus disponibles
|
||
let campusQuery = `
|
||
SELECT DISTINCT c.Nom
|
||
FROM Campus c
|
||
JOIN CollaborateurAD ca ON ca.CampusId = c.Id
|
||
WHERE (ca.actif = 1 OR ca.actif IS NULL)
|
||
`;
|
||
let campusRequest = pool.request();
|
||
|
||
if (selectedSociete && selectedSociete !== 'all') {
|
||
campusQuery += ` AND ca.SocieteId = (SELECT Id FROM Societe WHERE Nom = @selectedSociete)`;
|
||
campusRequest.input('selectedSociete', selectedSociete);
|
||
}
|
||
|
||
campusQuery += ` ORDER BY c.Nom`;
|
||
const campusResult = await campusRequest.query(campusQuery);
|
||
filters.campus = campusResult.recordset.map(c => c.Nom);
|
||
|
||
// Services disponibles
|
||
let servicesQuery = `
|
||
SELECT DISTINCT s.Nom
|
||
FROM Services s
|
||
JOIN CollaborateurAD ca ON ca.ServiceId = s.Id
|
||
WHERE (ca.actif = 1 OR ca.actif IS NULL)
|
||
`;
|
||
let servicesRequest = pool.request();
|
||
|
||
if (selectedSociete && selectedSociete !== 'all') {
|
||
servicesQuery += ` AND ca.SocieteId = (SELECT Id FROM Societe WHERE Nom = @selectedSociete)`;
|
||
servicesRequest.input('selectedSociete', selectedSociete);
|
||
}
|
||
|
||
if (selectedCampus && selectedCampus !== 'all') {
|
||
servicesQuery += ` AND ca.CampusId = (SELECT Id FROM Campus WHERE Nom = @selectedCampus)`;
|
||
servicesRequest.input('selectedCampus', selectedCampus);
|
||
}
|
||
|
||
servicesQuery += ` ORDER BY s.Nom`;
|
||
const servicesResult = await servicesRequest.query(servicesQuery);
|
||
filters.services = servicesResult.recordset.map(s => s.Nom);
|
||
|
||
filters.defaultCampus = campusNom;
|
||
filters.defaultService = serviceNom;
|
||
filters.defaultSociete = societeNom;
|
||
|
||
// ⭐ LISTE DES EMPLOYÉS
|
||
let employeesQuery = `
|
||
SELECT
|
||
CONCAT(ca.prenom, ' ', ca.nom) AS fullname,
|
||
c.Nom AS campusnom,
|
||
so.Nom AS societenom,
|
||
s.Nom AS servicenom
|
||
FROM CollaborateurAD ca
|
||
JOIN Services s ON ca.ServiceId = s.Id
|
||
JOIN Campus c ON ca.CampusId = c.Id
|
||
JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE (ca.actif = 1 OR ca.actif IS NULL)
|
||
`;
|
||
|
||
let employeesRequest = pool.request();
|
||
let employeesConditions = [];
|
||
|
||
if (selectedSociete && selectedSociete !== 'all') {
|
||
employeesConditions.push('so.Nom = @selectedSociete');
|
||
employeesRequest.input('selectedSociete', selectedSociete);
|
||
}
|
||
|
||
if (selectedCampus && selectedCampus !== 'all') {
|
||
employeesConditions.push('c.Nom = @selectedCampus');
|
||
employeesRequest.input('selectedCampus', selectedCampus);
|
||
}
|
||
|
||
if (selectedService && selectedService !== 'all') {
|
||
employeesConditions.push('s.Nom = @selectedService');
|
||
employeesRequest.input('selectedService', selectedService);
|
||
}
|
||
|
||
if (employeesConditions.length > 0) {
|
||
employeesQuery += ` AND ${employeesConditions.join(' AND ')}`;
|
||
}
|
||
|
||
employeesQuery += ` ORDER BY s.Nom, ca.prenom, ca.nom`;
|
||
|
||
const employeesResult = await employeesRequest.query(employeesQuery);
|
||
|
||
filters.employees = employeesResult.recordset.map(emp => ({
|
||
name: emp.fullname,
|
||
campus: emp.campusnom,
|
||
societe: emp.societenom,
|
||
service: emp.servicenom
|
||
}));
|
||
|
||
console.log(`👥 Employés trouvés: ${filters.employees.length}`);
|
||
|
||
// QUERY DES CONGÉS
|
||
let queryConditions = `WHERE dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente')`;
|
||
let queryRequest = pool.request();
|
||
let congesConditions = [];
|
||
|
||
if (selectedSociete && selectedSociete !== 'all') {
|
||
congesConditions.push('so.Nom = @selectedSociete');
|
||
queryRequest.input('selectedSociete', selectedSociete);
|
||
}
|
||
|
||
if (selectedCampus && selectedCampus !== 'all') {
|
||
congesConditions.push('c.Nom = @selectedCampus');
|
||
queryRequest.input('selectedCampus', selectedCampus);
|
||
}
|
||
|
||
if (selectedService && selectedService !== 'all') {
|
||
congesConditions.push('s.Nom = @selectedService');
|
||
queryRequest.input('selectedService', selectedService);
|
||
}
|
||
|
||
if (congesConditions.length > 0) {
|
||
queryConditions += ` AND ${congesConditions.join(' AND ')}`;
|
||
}
|
||
|
||
const query = `
|
||
SELECT
|
||
CONVERT(VARCHAR(10), dc.DateDebut, 23) AS startdate,
|
||
CONVERT(VARCHAR(10), dc.DateFin, 23) AS enddate,
|
||
CONCAT(ca.prenom, ' ', ca.nom) AS employeename,
|
||
(
|
||
SELECT STRING_AGG(Nom, ', ') WITHIN GROUP (ORDER BY Nom)
|
||
FROM (SELECT DISTINCT tc2.Nom
|
||
FROM DemandeCongeType dct2
|
||
JOIN TypeConge tc2 ON dct2.TypeCongeId = tc2.Id
|
||
WHERE dct2.DemandeCongeId = dc.Id) AS DistinctTypes
|
||
) AS type,
|
||
CONCAT(
|
||
'[',
|
||
STRING_AGG(
|
||
CONCAT(
|
||
'{"type":"', tc.Nom,
|
||
'","jours":', dct.NombreJours,
|
||
',"periode":"', COALESCE(dct.PeriodeJournee, 'Journée entière'), '"}'
|
||
),
|
||
','
|
||
),
|
||
']'
|
||
) AS detailsconges,
|
||
MAX(tc.CouleurHex) AS color,
|
||
dc.Statut AS statut,
|
||
s.Nom AS servicenom,
|
||
c.Nom AS campusnom,
|
||
so.Nom AS societenom,
|
||
dc.NombreJours AS nombrejoursouvres
|
||
FROM DemandeConge dc
|
||
JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id
|
||
LEFT JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId
|
||
LEFT JOIN TypeConge tc ON dct.TypeCongeId = tc.Id
|
||
JOIN Services s ON ca.ServiceId = s.Id
|
||
JOIN Campus c ON ca.CampusId = c.Id
|
||
JOIN Societe so ON ca.SocieteId = so.Id
|
||
${queryConditions}
|
||
GROUP BY dc.Id, dc.DateDebut, dc.DateFin, ca.prenom, ca.nom, dc.Statut, s.Nom, c.Nom, so.Nom, dc.NombreJours
|
||
ORDER BY s.Nom, dc.DateDebut ASC
|
||
`;
|
||
|
||
console.log(`🔍 Query WHERE final:`, queryConditions);
|
||
|
||
const leavesResult = await queryRequest.query(query);
|
||
const formattedLeaves = leavesResult.recordset.map(leave => ({
|
||
...leave
|
||
}));
|
||
|
||
console.log(`✅ ${formattedLeaves.length} congés trouvés`);
|
||
|
||
return res.json({
|
||
success: true,
|
||
role: role,
|
||
leaves: formattedLeaves,
|
||
filters: filters
|
||
});
|
||
}
|
||
|
||
// ========================================
|
||
// CAS 4: AUTRES RÔLES (Fallback)
|
||
// ========================================
|
||
else {
|
||
console.log("CAS 4: Autres rôles - Fallback service/campus");
|
||
|
||
if (!serviceId) {
|
||
return res.json({
|
||
success: false,
|
||
message: 'ServiceId manquant'
|
||
});
|
||
}
|
||
|
||
const checkServiceRequest = pool.request();
|
||
checkServiceRequest.input('serviceId', serviceId);
|
||
const checkServiceResult = await checkServiceRequest.query(`SELECT Nom FROM Services WHERE Id = @serviceId`);
|
||
const serviceNomCheck = checkServiceResult.recordset.length > 0 ? checkServiceResult.recordset[0].Nom : "Inconnu";
|
||
const isAdminFinancier = serviceNomCheck === "Administratif & Financier";
|
||
|
||
if (isAdminFinancier) {
|
||
// Service multi-campus
|
||
const employeesRequest = pool.request();
|
||
employeesRequest.input('serviceId', serviceId);
|
||
const employeesResult = await employeesRequest.query(`
|
||
SELECT
|
||
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 = @serviceId
|
||
AND (ca.actif = 1 OR ca.actif IS NULL)
|
||
ORDER BY ca.prenom, ca.nom
|
||
`);
|
||
|
||
filters.employees = employeesResult.recordset.map(e => ({
|
||
name: e.fullname,
|
||
campus: e.campusnom,
|
||
societe: e.societenom,
|
||
service: e.servicenom
|
||
}));
|
||
|
||
const queryRequest = pool.request();
|
||
queryRequest.input('serviceId', serviceId);
|
||
|
||
const query = `
|
||
SELECT
|
||
CONVERT(VARCHAR(10), dc.DateDebut, 23) AS startdate,
|
||
CONVERT(VARCHAR(10), dc.DateFin, 23) AS enddate,
|
||
CONCAT(ca.prenom, ' ', ca.nom) AS employeename,
|
||
(
|
||
SELECT STRING_AGG(Nom, ', ') WITHIN GROUP (ORDER BY Nom)
|
||
FROM (SELECT DISTINCT tc2.Nom
|
||
FROM DemandeCongeType dct2
|
||
JOIN TypeConge tc2 ON dct2.TypeCongeId = tc2.Id
|
||
WHERE dct2.DemandeCongeId = dc.Id) AS DistinctTypes
|
||
) AS type,
|
||
CONCAT(
|
||
'[',
|
||
STRING_AGG(
|
||
CONCAT(
|
||
'{"type":"', tc.Nom,
|
||
'","jours":', dct.NombreJours,
|
||
',"periode":"', COALESCE(dct.PeriodeJournee, 'Journée entière'), '"}'
|
||
),
|
||
','
|
||
),
|
||
']'
|
||
) AS detailsconges,
|
||
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 = @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
|
||
`;
|
||
|
||
const leavesResult = await queryRequest.query(query);
|
||
const formattedLeaves = leavesResult.recordset.map(leave => ({
|
||
...leave
|
||
}));
|
||
|
||
console.log(`✅ ${formattedLeaves.length} congés trouvés`);
|
||
|
||
return res.json({
|
||
success: true,
|
||
role: role,
|
||
leaves: formattedLeaves,
|
||
filters: filters
|
||
});
|
||
|
||
} else {
|
||
// Service + Campus
|
||
const employeesRequest = pool.request();
|
||
employeesRequest.input('serviceId', serviceId);
|
||
employeesRequest.input('campusId', campusId);
|
||
const employeesResult = await employeesRequest.query(`
|
||
SELECT
|
||
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 = @serviceId
|
||
AND ca.CampusId = @campusId
|
||
AND (ca.actif = 1 OR ca.actif IS NULL)
|
||
ORDER BY ca.prenom, ca.nom
|
||
`);
|
||
|
||
filters.employees = employeesResult.recordset.map(e => ({
|
||
name: e.fullname,
|
||
campus: e.campusnom,
|
||
societe: e.societenom,
|
||
service: e.servicenom
|
||
}));
|
||
|
||
const queryRequest = pool.request();
|
||
queryRequest.input('serviceId', serviceId);
|
||
queryRequest.input('campusId', campusId);
|
||
|
||
const query = `
|
||
SELECT
|
||
CONVERT(VARCHAR(10), dc.DateDebut, 23) AS startdate,
|
||
CONVERT(VARCHAR(10), dc.DateFin, 23) AS enddate,
|
||
CONCAT(ca.prenom, ' ', ca.nom) AS employeename,
|
||
(
|
||
SELECT STRING_AGG(Nom, ', ') WITHIN GROUP (ORDER BY Nom)
|
||
FROM (SELECT DISTINCT tc2.Nom
|
||
FROM DemandeCongeType dct2
|
||
JOIN TypeConge tc2 ON dct2.TypeCongeId = tc2.Id
|
||
WHERE dct2.DemandeCongeId = dc.Id) AS DistinctTypes
|
||
) AS type,
|
||
CONCAT(
|
||
'[',
|
||
STRING_AGG(
|
||
CONCAT(
|
||
'{"type":"', tc.Nom,
|
||
'","jours":', dct.NombreJours,
|
||
',"periode":"', COALESCE(dct.PeriodeJournee, 'Journée entière'), '"}'
|
||
),
|
||
','
|
||
),
|
||
']'
|
||
) AS detailsconges,
|
||
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 = @serviceId
|
||
AND ca.CampusId = @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
|
||
`;
|
||
|
||
const leavesResult = await queryRequest.query(query);
|
||
const formattedLeaves = leavesResult.recordset.map(leave => ({
|
||
...leave
|
||
}));
|
||
|
||
console.log(`✅ ${formattedLeaves.length} congés trouvés`);
|
||
|
||
return 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
|
||
});
|
||
}
|
||
});
|
||
|
||
// ========================================
|
||
// SYNCHRONISATION ENTRA ID CORRIGÉE
|
||
// À remplacer dans server.js à partir de la ligne ~3700
|
||
// ========================================
|
||
|
||
app.post('/api/initial-sync', async (req, res) => {
|
||
let errorCount = 0;
|
||
const syncResults = {
|
||
processed: 0,
|
||
inserted: 0,
|
||
updated: 0,
|
||
deactivated: 0,
|
||
errors: []
|
||
};
|
||
|
||
try {
|
||
console.log('\n🔄 === DÉBUT SYNCHRONISATION ENTRA ID ===');
|
||
|
||
// 1️⃣ Obtenir le token Admin
|
||
const accessToken = await getGraphToken();
|
||
if (!accessToken) {
|
||
return res.json({
|
||
success: false,
|
||
message: '❌ Impossible d\'obtenir le token Microsoft Graph'
|
||
});
|
||
}
|
||
console.log('✅ Token Microsoft Graph obtenu');
|
||
|
||
// =============================================================================
|
||
// SCÉNARIO 1 : Synchronisation unitaire (Un seul utilisateur spécifique)
|
||
// =============================================================================
|
||
if (req.body.userPrincipalName || req.body.mail) {
|
||
const userEmail = (req.body.mail || req.body.userPrincipalName).toLowerCase().trim();
|
||
const entraUserId = req.body.id;
|
||
|
||
console.log(`\n🔄 Synchronisation utilisateur unique : ${userEmail}`);
|
||
|
||
// ⭐ VALIDATION : Email requis
|
||
if (!userEmail || userEmail === '') {
|
||
return res.json({
|
||
success: false,
|
||
message: '❌ Email utilisateur manquant ou invalide'
|
||
});
|
||
}
|
||
|
||
// ⭐ VALIDATION : Format email
|
||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||
if (!emailRegex.test(userEmail)) {
|
||
return res.json({
|
||
success: false,
|
||
message: `❌ Format d'email invalide : ${userEmail}`
|
||
});
|
||
}
|
||
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
await conn.beginTransaction();
|
||
|
||
// Vérifier si l'utilisateur existe déjà
|
||
const [existing] = await conn.query(
|
||
'SELECT id, email, Actif FROM CollaborateurAD WHERE LOWER(email) = ?',
|
||
[userEmail]
|
||
);
|
||
|
||
if (existing.length > 0) {
|
||
// MISE À JOUR
|
||
await conn.query(`
|
||
UPDATE CollaborateurAD
|
||
SET
|
||
entraUserId = ?,
|
||
prenom = ?,
|
||
nom = ?,
|
||
service = ?,
|
||
description = ?,
|
||
Actif = 1
|
||
WHERE LOWER(email) = ?
|
||
`, [
|
||
entraUserId || existing[0].entraUserId,
|
||
req.body.givenName || 'Prénom',
|
||
req.body.surname || 'Nom',
|
||
req.body.department || '',
|
||
req.body.jobTitle || null,
|
||
userEmail
|
||
]);
|
||
|
||
console.log(` ✅ Utilisateur mis à jour : ${userEmail}`);
|
||
syncResults.updated++;
|
||
|
||
} else {
|
||
// INSERTION
|
||
await conn.query(`
|
||
INSERT INTO CollaborateurAD
|
||
(entraUserId, prenom, nom, email, service, description, role, SocieteId, Actif, DateEntree, TypeContrat)
|
||
VALUES (?, ?, ?, ?, ?, ?, 'Collaborateur', 1, 1, GETDATE(), '37h')
|
||
`, [
|
||
entraUserId,
|
||
req.body.givenName || 'Prénom',
|
||
req.body.surname || 'Nom',
|
||
userEmail,
|
||
req.body.department || '',
|
||
req.body.jobTitle || null
|
||
]);
|
||
|
||
console.log(` ✅ Nouvel utilisateur créé : ${userEmail}`);
|
||
syncResults.inserted++;
|
||
}
|
||
|
||
// Récupération des données fraîches
|
||
const [userRows] = await conn.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 LOWER(ca.email) = ?
|
||
`, [userEmail]);
|
||
|
||
await conn.commit();
|
||
|
||
if (userRows.length === 0) {
|
||
await conn.rollback();
|
||
throw new Error('Utilisateur synchronisé mais introuvable en base');
|
||
}
|
||
|
||
const userData = userRows[0];
|
||
console.log(`✅ Synchronisation réussie : ${userData.email}`);
|
||
|
||
return res.json({
|
||
success: true,
|
||
message: 'Utilisateur synchronisé avec succès',
|
||
localUserId: userData.localUserId,
|
||
role: userData.role,
|
||
service: userData.service,
|
||
typeContrat: userData.typeContrat,
|
||
dateEntree: userData.dateEntree,
|
||
societeId: userData.SocieteId,
|
||
user: userData
|
||
});
|
||
|
||
} catch (syncError) {
|
||
await conn.rollback();
|
||
console.error('❌ Erreur sync unitaire:', syncError);
|
||
return res.json({
|
||
success: false,
|
||
message: `❌ Erreur synchronisation: ${syncError.message}`
|
||
});
|
||
} finally {
|
||
conn.release();
|
||
}
|
||
}
|
||
|
||
// =============================================================================
|
||
// SCÉNARIO 2 : Full Sync (Tous les membres du groupe Azure)
|
||
// =============================================================================
|
||
console.log('\n🔄 === FULL SYNC - Tous les membres du groupe ===');
|
||
|
||
// A. Récupérer le nom 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 groupName = groupResponse.data.displayName;
|
||
console.log(`📋 Groupe : ${groupName}`);
|
||
|
||
// B. Récupérer TOUS les membres avec pagination
|
||
let allAzureMembers = [];
|
||
let nextLink = `https://graph.microsoft.com/v1.0/groups/${AZURE_CONFIG.groupId}/members?$select=id,givenName,surname,mail,department,jobTitle,accountEnabled&$top=999`;
|
||
|
||
console.log('📥 Récupération des membres...');
|
||
while (nextLink) {
|
||
const membersResponse = await axios.get(nextLink, {
|
||
headers: { Authorization: `Bearer ${accessToken}` }
|
||
});
|
||
allAzureMembers = allAzureMembers.concat(membersResponse.data.value);
|
||
nextLink = membersResponse.data['@odata.nextLink'];
|
||
|
||
if (nextLink) {
|
||
console.log(` 📄 ${allAzureMembers.length} membres récupérés... (suite)`);
|
||
}
|
||
}
|
||
|
||
console.log(`✅ ${allAzureMembers.length} membres trouvés dans Entra ID`);
|
||
|
||
// C. Filtrer et valider les emails
|
||
const validMembers = allAzureMembers.filter(m => {
|
||
// Ignorer si pas d'email
|
||
if (!m.mail || m.mail.trim() === '') {
|
||
console.log(` ⚠️ Ignoré (pas d'email) : ${m.givenName} ${m.surname} (${m.id})`);
|
||
return false;
|
||
}
|
||
|
||
// Ignorer si compte désactivé
|
||
if (m.accountEnabled === false) {
|
||
console.log(` ⚠️ Ignoré (compte désactivé) : ${m.mail}`);
|
||
return false;
|
||
}
|
||
|
||
// Valider format email
|
||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||
if (!emailRegex.test(m.mail)) {
|
||
console.log(` ⚠️ Ignoré (format email invalide) : ${m.mail}`);
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
});
|
||
|
||
console.log(`✅ ${validMembers.length} membres valides à synchroniser`);
|
||
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
await conn.beginTransaction();
|
||
|
||
// D. Construire la liste des emails valides
|
||
const azureEmails = new Set();
|
||
validMembers.forEach(m => {
|
||
azureEmails.add(m.mail.toLowerCase().trim());
|
||
});
|
||
|
||
console.log('\n📝 Traitement des utilisateurs...');
|
||
|
||
// E. Traitement de chaque membre valide
|
||
for (const m of validMembers) {
|
||
try {
|
||
const emailClean = m.mail.toLowerCase().trim();
|
||
syncResults.processed++;
|
||
|
||
// Vérifier si l'utilisateur existe déjà
|
||
const [existing] = await conn.query(
|
||
'SELECT id, email, entraUserId, Actif FROM CollaborateurAD WHERE LOWER(email) = ?',
|
||
[emailClean]
|
||
);
|
||
|
||
if (existing.length > 0) {
|
||
// MISE À JOUR si changements
|
||
await conn.query(`
|
||
UPDATE CollaborateurAD
|
||
SET
|
||
entraUserId = ?,
|
||
prenom = ?,
|
||
nom = ?,
|
||
service = ?,
|
||
description = ?,
|
||
Actif = 1
|
||
WHERE LOWER(email) = ?
|
||
`, [
|
||
m.id,
|
||
m.givenName || existing[0].prenom || 'Prénom',
|
||
m.surname || existing[0].nom || 'Nom',
|
||
m.department || '',
|
||
m.jobTitle || null,
|
||
emailClean
|
||
]);
|
||
|
||
syncResults.updated++;
|
||
console.log(` ✓ Mis à jour : ${emailClean}`);
|
||
|
||
} else {
|
||
// INSERTION nouveau
|
||
await conn.query(`
|
||
INSERT INTO CollaborateurAD
|
||
(entraUserId, prenom, nom, email, service, description, role, SocieteId, Actif, DateEntree, TypeContrat)
|
||
VALUES (?, ?, ?, ?, ?, ?, 'Collaborateur', 1, 1, GETDATE(), '37h')
|
||
`, [
|
||
m.id,
|
||
m.givenName || 'Prénom',
|
||
m.surname || 'Nom',
|
||
emailClean,
|
||
m.department || '',
|
||
m.jobTitle || null
|
||
]);
|
||
|
||
syncResults.inserted++;
|
||
console.log(` ✓ Créé : ${emailClean}`);
|
||
}
|
||
|
||
} catch (userError) {
|
||
errorCount++;
|
||
syncResults.errors.push({
|
||
email: m.mail,
|
||
error: userError.message
|
||
});
|
||
console.error(` ❌ Erreur ${m.mail}:`, userError.message);
|
||
// Continuer avec les autres
|
||
}
|
||
}
|
||
|
||
// F. ⭐ DÉSACTIVATION SÉCURISÉE
|
||
console.log('\n🔍 Désactivation des comptes obsolètes...');
|
||
|
||
if (azureEmails.size > 0) {
|
||
const activeEmailsArray = Array.from(azureEmails);
|
||
const placeholders = activeEmailsArray.map(() => '?').join(',');
|
||
|
||
// ⭐ REQUÊTE PROTÉGÉE : Ne désactive que les comptes qui :
|
||
// 1. Ont un email valide
|
||
// 2. Ne sont pas dans Entra ID
|
||
// 3. Ne sont pas RH/Admin/President
|
||
// 4. Sont actuellement actifs
|
||
const [resultDeactivate] = await conn.query(`
|
||
UPDATE CollaborateurAD
|
||
SET Actif = 0
|
||
WHERE
|
||
Email IS NOT NULL
|
||
AND Email != ''
|
||
AND Email NOT LIKE '%@noemail.local'
|
||
AND LOWER(Email) NOT IN (${placeholders})
|
||
AND Actif = 1
|
||
AND role NOT IN ('RH', 'Admin', 'President')
|
||
`, activeEmailsArray);
|
||
|
||
syncResults.deactivated = resultDeactivate.affectedRows;
|
||
console.log(` ✓ ${syncResults.deactivated} compte(s) désactivé(s)`);
|
||
}
|
||
|
||
await conn.commit();
|
||
|
||
// G. Logging final
|
||
console.log('\n📊 === RÉSUMÉ SYNCHRONISATION ===');
|
||
console.log(` Groupe Azure: ${groupName}`);
|
||
console.log(` Total membres Entra: ${allAzureMembers.length}`);
|
||
console.log(` Membres valides: ${validMembers.length}`);
|
||
console.log(` Traités: ${syncResults.processed}`);
|
||
console.log(` Créés: ${syncResults.inserted}`);
|
||
console.log(` Mis à jour: ${syncResults.updated}`);
|
||
console.log(` Désactivés: ${syncResults.deactivated}`);
|
||
console.log(` Erreurs: ${errorCount}`);
|
||
|
||
res.json({
|
||
success: true,
|
||
message: 'Synchronisation globale terminée',
|
||
groupe_sync: groupName,
|
||
stats: {
|
||
total_azure: allAzureMembers.length,
|
||
membres_valides: validMembers.length,
|
||
processed: syncResults.processed,
|
||
inserted: syncResults.inserted,
|
||
updated: syncResults.updated,
|
||
deactivated: syncResults.deactivated,
|
||
errors: errorCount,
|
||
error_details: syncResults.errors.length > 0 ? syncResults.errors : undefined
|
||
}
|
||
});
|
||
|
||
} catch (error) {
|
||
await conn.rollback();
|
||
throw error;
|
||
} finally {
|
||
conn.release();
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('\n❌ === ERREUR CRITIQUE SYNCHRONISATION ===');
|
||
console.error('Message:', error.message);
|
||
console.error('Stack:', error.stack);
|
||
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur lors de la synchronisation',
|
||
error: error.message,
|
||
stats: syncResults
|
||
});
|
||
}
|
||
});
|
||
''
|
||
|
||
// ========================================
|
||
// NOUVELLES ROUTES ADMINISTRATION RTT
|
||
// ========================================
|
||
|
||
app.get('/api/getAllCollaborateurs', async (req, res) => {
|
||
try {
|
||
const [collaborateurs] = await pool.query(`
|
||
SELECT
|
||
ca.id,
|
||
ca.prenom,
|
||
ca.nom,
|
||
ca.email,
|
||
ca.role,
|
||
ca.TypeContrat,
|
||
ca.DateEntree,
|
||
s.Nom as service,
|
||
ca.CampusId,
|
||
ca.SocieteId,
|
||
so.Nom as societe_nom
|
||
FROM CollaborateurAD ca
|
||
LEFT JOIN Services s ON ca.ServiceId = s.Id
|
||
LEFT JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE (ca.actif = 1 OR ca.actif IS NULL)
|
||
ORDER BY ca.nom, ca.prenom
|
||
`);
|
||
|
||
res.json({
|
||
success: true,
|
||
collaborateurs: collaborateurs,
|
||
total: collaborateurs.length
|
||
});
|
||
} catch (error) {
|
||
console.error('Erreur getAllCollaborateurs:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur serveur',
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
app.post('/api/updateTypeContrat', async (req, res) => {
|
||
try {
|
||
const { collaborateur_id, type_contrat } = req.body;
|
||
|
||
if (!collaborateur_id || !type_contrat) {
|
||
return res.json({
|
||
success: false,
|
||
message: 'Données manquantes'
|
||
});
|
||
}
|
||
|
||
const typesValides = ['37h', 'forfait_jour', 'temps_partiel'];
|
||
if (!typesValides.includes(type_contrat)) {
|
||
return res.json({
|
||
success: false,
|
||
message: 'Type de contrat invalide'
|
||
});
|
||
}
|
||
|
||
const [collab] = await pool.query(
|
||
'SELECT prenom, nom, CampusId FROM CollaborateurAD WHERE id = ?',
|
||
[collaborateur_id]
|
||
);
|
||
|
||
if (collab.length === 0) {
|
||
return res.json({
|
||
success: false,
|
||
message: 'Collaborateur non trouvé'
|
||
});
|
||
}
|
||
|
||
await pool.query(
|
||
'UPDATE CollaborateurAD SET TypeContrat = ? WHERE id = ?',
|
||
[type_contrat, collaborateur_id]
|
||
);
|
||
|
||
res.json({
|
||
success: true,
|
||
message: 'Type de contrat mis à jour',
|
||
nom: `${collab[0].prenom} ${collab[0].nom}`,
|
||
nouveau_type: type_contrat
|
||
});
|
||
} catch (error) {
|
||
console.error('Erreur updateTypeContrat:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur serveur',
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
app.get('/api/getConfigurationRTT', async (req, res) => {
|
||
try {
|
||
const annee = parseInt(req.query.annee || new Date().getFullYear());
|
||
const [configs] = await pool.query(
|
||
`SELECT Annee, TypeContrat, JoursAnnuels, AcquisitionMensuelle, Description
|
||
FROM ConfigurationRTT
|
||
WHERE Annee = ?
|
||
ORDER BY TypeContrat`,
|
||
[annee]
|
||
);
|
||
res.json({ success: true, configs });
|
||
} catch (error) {
|
||
res.status(500).json({ success: false, message: 'Erreur', error: error.message });
|
||
}
|
||
});
|
||
|
||
app.post('/api/updateConfigurationRTT', async (req, res) => {
|
||
try {
|
||
const { annee, typeContrat, joursAnnuels } = req.body;
|
||
if (!annee || !typeContrat || !joursAnnuels) {
|
||
return res.json({ success: false, message: 'Données manquantes' });
|
||
}
|
||
|
||
const acquisitionMensuelle = joursAnnuels / 12;
|
||
|
||
await pool.query(
|
||
`INSERT INTO ConfigurationRTT (Annee, TypeContrat, JoursAnnuels, AcquisitionMensuelle)
|
||
VALUES (?, ?, ?, ?)
|
||
ON DUPLICATE KEY UPDATE
|
||
JoursAnnuels = ?, AcquisitionMensuelle = ?`,
|
||
[annee, typeContrat, joursAnnuels, acquisitionMensuelle, joursAnnuels, acquisitionMensuelle]
|
||
);
|
||
|
||
res.json({ success: true, message: 'Configuration mise à jour' });
|
||
} catch (error) {
|
||
res.status(500).json({ success: false, message: 'Erreur', error: error.message });
|
||
}
|
||
});
|
||
|
||
app.post('/api/updateRequest', upload.array('medicalDocuments', 5), async (req, res) => {
|
||
let connection;
|
||
try {
|
||
console.log('\n📥 === MODIFICATION DEMANDE ===');
|
||
console.log('Body reçu:', req.body);
|
||
console.log('Fichiers reçus:', req.files?.length || 0);
|
||
|
||
const {
|
||
requestId,
|
||
leaveType,
|
||
startDate,
|
||
endDate,
|
||
reason,
|
||
businessDays,
|
||
userId,
|
||
userEmail,
|
||
userName,
|
||
accessToken
|
||
} = req.body;
|
||
|
||
// ⭐ PARSER LA RÉPARTITION (CRITIQUE)
|
||
let Repartition;
|
||
try {
|
||
Repartition = JSON.parse(req.body.Repartition || '[]');
|
||
console.log('📊 Répartition parsée:', JSON.stringify(Repartition, null, 2));
|
||
} catch (parseError) {
|
||
console.error('❌ Erreur parsing Repartition:', parseError);
|
||
if (req.files) {
|
||
req.files.forEach(file => {
|
||
if (fs.existsSync(file.path)) {
|
||
fs.unlinkSync(file.path);
|
||
}
|
||
});
|
||
}
|
||
return res.status(400).json({
|
||
success: false,
|
||
message: 'Erreur de format de la répartition'
|
||
});
|
||
}
|
||
|
||
// Validation
|
||
if (!requestId || !leaveType || !startDate || !endDate || !businessDays || !userId) {
|
||
if (req.files) {
|
||
req.files.forEach(file => {
|
||
if (fs.existsSync(file.path)) {
|
||
fs.unlinkSync(file.path);
|
||
}
|
||
});
|
||
}
|
||
return res.status(400).json({
|
||
success: false,
|
||
message: '❌ Données manquantes'
|
||
});
|
||
}
|
||
|
||
connection = await pool.getConnection();
|
||
await connection.beginTransaction();
|
||
|
||
const uploadedFiles = req.files || [];
|
||
|
||
console.log(`Demande ID: ${requestId}, User ID: ${userId}`);
|
||
|
||
// 1️⃣ RÉCUPÉRER LA DEMANDE ORIGINALE
|
||
const [originalRequest] = await connection.query(
|
||
'SELECT * FROM DemandeConge WHERE Id = ? AND CollaborateurADId = ?',
|
||
[requestId, userId]
|
||
);
|
||
|
||
if (originalRequest.length === 0) {
|
||
await connection.rollback();
|
||
if (req.files) {
|
||
req.files.forEach(file => {
|
||
if (fs.existsSync(file.path)) {
|
||
fs.unlinkSync(file.path);
|
||
}
|
||
});
|
||
}
|
||
return res.status(404).json({
|
||
success: false,
|
||
message: '❌ Demande introuvable ou non autorisée'
|
||
});
|
||
}
|
||
|
||
const original = originalRequest[0];
|
||
const oldStatus = original.Statut;
|
||
|
||
console.log(`📋 Demande originale: Statut=${oldStatus}`);
|
||
|
||
// ⭐ VALIDATION : Si arrêt maladie, il faut au moins 1 fichier
|
||
const hasABS = Repartition.some(r => r.TypeConge === 'ABS');
|
||
if (hasABS && uploadedFiles.length === 0) {
|
||
await connection.rollback();
|
||
return res.json({
|
||
success: false,
|
||
message: 'Un justificatif médical est obligatoire pour un arrêt maladie'
|
||
});
|
||
}
|
||
|
||
// ⭐ VALIDATION DE LA RÉPARTITION
|
||
console.log('📊 Validation répartition...');
|
||
console.log('Nombre de jours total:', businessDays);
|
||
console.log('Répartition reçue:', JSON.stringify(Repartition, null, 2));
|
||
|
||
// Ne compter que CP, RTT ET RÉCUP dans la répartition
|
||
const sommeRepartition = Repartition.reduce((sum, r) => {
|
||
if (r.TypeConge === 'CP' || r.TypeConge === 'RTT' || r.TypeConge === 'Récup') {
|
||
return sum + parseFloat(r.NombreJours || 0);
|
||
}
|
||
return sum;
|
||
}, 0);
|
||
|
||
console.log('Somme répartition CP+RTT+Récup:', sommeRepartition.toFixed(2));
|
||
|
||
// Validation : La somme doit correspondre au total
|
||
const hasCountableLeave = Repartition.some(r =>
|
||
r.TypeConge === 'CP' || r.TypeConge === 'RTT' || r.TypeConge === 'Récup'
|
||
);
|
||
|
||
if (hasCountableLeave && Math.abs(sommeRepartition - businessDays) > 0.01) {
|
||
console.error('❌ ERREUR : Répartition incohérente !');
|
||
|
||
if (req.files) {
|
||
req.files.forEach(file => {
|
||
if (fs.existsSync(file.path)) {
|
||
fs.unlinkSync(file.path);
|
||
}
|
||
});
|
||
}
|
||
|
||
await connection.rollback();
|
||
|
||
return res.json({
|
||
success: false,
|
||
message: `Erreur de répartition : la somme (${sommeRepartition.toFixed(2)}j) ne correspond pas au total (${businessDays}j)`
|
||
});
|
||
}
|
||
|
||
console.log('✅ Validation répartition OK');
|
||
|
||
// 2️⃣ REMBOURSER L'ANCIENNE DEMANDE (via DeductionDetails)
|
||
let restorationStats = { count: 0, details: [] };
|
||
|
||
if (oldStatus !== 'Refusée' && oldStatus !== 'Annulée' && original.TypeCongeId !== 3) {
|
||
console.log(`🔄 Remboursement de l'ancienne demande...`);
|
||
|
||
const [oldDeductions] = await connection.query(
|
||
'SELECT * FROM DeductionDetails WHERE DemandeCongeId = ?',
|
||
[requestId]
|
||
);
|
||
|
||
if (oldDeductions.length > 0) {
|
||
for (const d of oldDeductions) {
|
||
const [compteur] = await connection.query(
|
||
'SELECT Id, Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?',
|
||
[userId, d.TypeCongeId, d.Annee]
|
||
);
|
||
|
||
if (compteur.length > 0) {
|
||
const newSolde = parseFloat(compteur[0].Solde) + parseFloat(d.JoursUtilises);
|
||
await connection.query(
|
||
'UPDATE CompteurConges SET Solde = ?, DerniereMiseAJour = GETDATE() WHERE Id = ?',
|
||
[newSolde, compteur[0].Id]
|
||
);
|
||
restorationStats.count++;
|
||
restorationStats.details.push(`${d.JoursUtilises}j rendus (Type ${d.TypeCongeId}, Année ${d.Annee})`);
|
||
console.log(` ✅ Remboursé ${d.JoursUtilises}j au compteur TypeId=${d.TypeCongeId}, Annee=${d.Annee}`);
|
||
}
|
||
}
|
||
|
||
// Supprimer les anciennes déductions
|
||
await connection.query('DELETE FROM DeductionDetails WHERE DemandeCongeId = ?', [requestId]);
|
||
console.log(` 🧹 ${oldDeductions.length} déduction(s) supprimée(s)`);
|
||
}
|
||
}
|
||
|
||
// 3️⃣ METTRE À JOUR LA DEMANDE (⭐ SQL SERVER : GETDATE + FORMAT)
|
||
console.log('📝 Mise à jour de la demande...');
|
||
|
||
// Si elle était validée, on la repasse en "En attente"
|
||
const newStatus = (oldStatus === 'Validée' || oldStatus === 'Validé') ? 'En attente' : oldStatus;
|
||
|
||
await connection.query(
|
||
`UPDATE DemandeConge
|
||
SET TypeCongeId = ?,
|
||
DateDebut = ?,
|
||
DateFin = ?,
|
||
Commentaire = ?,
|
||
NombreJours = ?,
|
||
Statut = ?,
|
||
DateValidation = GETDATE(),
|
||
CommentaireValidation = COALESCE(CommentaireValidation, '') +
|
||
CHAR(10) + '[Modifiée le ' +
|
||
FORMAT(GETDATE(), 'dd/MM/yyyy à HH:mm', 'fr-FR') +
|
||
']'
|
||
WHERE Id = ?`,
|
||
[leaveType, startDate, endDate, reason || '', businessDays, newStatus, requestId]
|
||
);
|
||
|
||
console.log(`✅ Demande ${requestId} modifiée - Statut: ${newStatus}`);
|
||
|
||
// 4️⃣ SUPPRIMER L'ANCIENNE RÉPARTITION DANS DemandeCongeType
|
||
await connection.query('DELETE FROM DemandeCongeType WHERE DemandeCongeId = ?', [requestId]);
|
||
|
||
// 5️⃣ Sauvegarder la nouvelle répartition AVEC GÉNÉRATION MANUELLE D'ID
|
||
console.log('\n📊 Sauvegarde de la nouvelle répartition en base...');
|
||
for (const rep of Repartition) {
|
||
const code = rep.TypeConge;
|
||
const name = code === 'CP' ? 'Congé payé' :
|
||
code === 'RTT' ? 'RTT' :
|
||
code === 'ABS' ? 'Congé maladie' :
|
||
code === 'Formation' ? 'Formation' :
|
||
code === 'Récup' ? 'Récupération' : code;
|
||
|
||
const [typeRow] = await connection.query(
|
||
'SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1',
|
||
[name]
|
||
);
|
||
|
||
if (typeRow.length > 0) {
|
||
// ⭐ GÉNÉRER L'ID MANUELLEMENT
|
||
const demandeCongeTypeId = await getNextId(connection, 'DemandeCongeType');
|
||
|
||
await connection.query(`
|
||
INSERT INTO DemandeCongeType
|
||
(Id, DemandeCongeId, TypeCongeId, NombreJours, PeriodeJournee)
|
||
VALUES (?, ?, ?, ?, ?)
|
||
`, [
|
||
demandeCongeTypeId,
|
||
requestId,
|
||
typeRow[0].Id,
|
||
rep.NombreJours,
|
||
rep.PeriodeJournee || 'Journée entière'
|
||
]);
|
||
console.log(` ✓ ${name}: ${rep.NombreJours}j (${rep.PeriodeJournee || 'Journée entière'})`);
|
||
}
|
||
}
|
||
|
||
// 6️⃣ CALCULER ET APPLIQUER LA NOUVELLE DÉDUCTION
|
||
let newRepartition = [];
|
||
const currentYear = new Date().getFullYear();
|
||
const previousYear = currentYear - 1;
|
||
|
||
const isFormationOnly = Repartition.length === 1 && Repartition[0].TypeConge === 'Formation';
|
||
|
||
if (!isFormationOnly) {
|
||
console.log('📉 Déduction des compteurs...');
|
||
|
||
for (const rep of Repartition) {
|
||
if (rep.TypeConge === 'ABS' || rep.TypeConge === 'Formation') {
|
||
console.log(` ⏩ ${rep.TypeConge} ignoré (pas de déduction)`);
|
||
continue;
|
||
}
|
||
|
||
// ⭐ TRAITEMENT SPÉCIAL POUR RÉCUP
|
||
if (rep.TypeConge === 'Récup') {
|
||
const [recupType] = await connection.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Récupération']);
|
||
|
||
if (recupType.length > 0) {
|
||
await connection.query(`
|
||
UPDATE CompteurConges
|
||
SET Solde = CASE WHEN Solde - ? < 0 THEN 0 ELSE Solde - ? END,
|
||
DerniereMiseAJour = GETDATE()
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [rep.NombreJours, rep.NombreJours, userId, recupType[0].Id, currentYear]);
|
||
|
||
await connection.query(`
|
||
INSERT INTO DeductionDetails
|
||
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
|
||
VALUES (?, ?, ?, 'Récup Posée', ?)
|
||
`, [requestId, recupType[0].Id, currentYear, rep.NombreJours]);
|
||
|
||
newRepartition.push({
|
||
typeCongeId: recupType[0].Id,
|
||
annee: currentYear,
|
||
jours: rep.NombreJours,
|
||
typeDeduction: 'Récup Posée'
|
||
});
|
||
|
||
console.log(` ✓ Récup: ${rep.NombreJours}j déduits`);
|
||
}
|
||
continue;
|
||
}
|
||
|
||
// ⭐ CP et RTT : déduction normale
|
||
const name = rep.TypeConge === 'CP' ? 'Congé payé' : 'RTT';
|
||
const [typeRow] = await connection.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', [name]);
|
||
|
||
if (typeRow.length > 0) {
|
||
let joursRestants = parseFloat(rep.NombreJours);
|
||
|
||
// A. Essayer N-1 (CP uniquement)
|
||
if (rep.TypeConge === 'CP') {
|
||
const [compteurN1] = await connection.query(
|
||
'SELECT Id, Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?',
|
||
[userId, typeRow[0].Id, previousYear]
|
||
);
|
||
|
||
if (compteurN1.length > 0 && compteurN1[0].Solde > 0) {
|
||
const disponibleN1 = parseFloat(compteurN1[0].Solde);
|
||
const aPrendreN1 = Math.min(disponibleN1, joursRestants);
|
||
|
||
await connection.query(
|
||
'UPDATE CompteurConges SET Solde = Solde - ?, DerniereMiseAJour = GETDATE() WHERE Id = ?',
|
||
[aPrendreN1, compteurN1[0].Id]
|
||
);
|
||
|
||
await connection.query(`
|
||
INSERT INTO DeductionDetails
|
||
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
|
||
VALUES (?, ?, ?, 'Année N-1', ?)
|
||
`, [requestId, typeRow[0].Id, previousYear, aPrendreN1]);
|
||
|
||
newRepartition.push({
|
||
typeCongeId: typeRow[0].Id,
|
||
annee: previousYear,
|
||
jours: aPrendreN1,
|
||
typeDeduction: 'Année N-1'
|
||
});
|
||
|
||
joursRestants -= aPrendreN1;
|
||
console.log(` ✓ ${name} N-1: ${aPrendreN1}j déduits (reste: ${joursRestants}j)`);
|
||
}
|
||
}
|
||
|
||
// B. Essayer N
|
||
if (joursRestants > 0) {
|
||
const [compteurN] = await connection.query(
|
||
'SELECT Id, Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?',
|
||
[userId, typeRow[0].Id, currentYear]
|
||
);
|
||
|
||
if (compteurN.length > 0) {
|
||
const disponibleN = parseFloat(compteurN[0].Solde);
|
||
const aPrendreN = Math.min(disponibleN, joursRestants);
|
||
|
||
await connection.query(
|
||
'UPDATE CompteurConges SET Solde = Solde - ?, DerniereMiseAJour = GETDATE() WHERE Id = ?',
|
||
[aPrendreN, compteurN[0].Id]
|
||
);
|
||
|
||
await connection.query(`
|
||
INSERT INTO DeductionDetails
|
||
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
|
||
VALUES (?, ?, ?, 'Année N', ?)
|
||
`, [requestId, typeRow[0].Id, currentYear, aPrendreN]);
|
||
|
||
newRepartition.push({
|
||
typeCongeId: typeRow[0].Id,
|
||
annee: currentYear,
|
||
jours: aPrendreN,
|
||
typeDeduction: 'Année N'
|
||
});
|
||
|
||
joursRestants -= aPrendreN;
|
||
console.log(` ✓ ${name} N: ${aPrendreN}j déduits (reste: ${joursRestants}j)`);
|
||
}
|
||
}
|
||
|
||
// C. Si il reste des jours → Anticipé
|
||
if (joursRestants > 0) {
|
||
const [compteurN] = await connection.query(
|
||
'SELECT Id FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?',
|
||
[userId, typeRow[0].Id, currentYear]
|
||
);
|
||
|
||
if (compteurN.length > 0) {
|
||
await connection.query(
|
||
'UPDATE CompteurConges SET Solde = Solde - ?, DerniereMiseAJour = GETDATE() WHERE Id = ?',
|
||
[joursRestants, compteurN[0].Id]
|
||
);
|
||
|
||
await connection.query(`
|
||
INSERT INTO DeductionDetails
|
||
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
|
||
VALUES (?, ?, ?, 'N Anticip', ?)
|
||
`, [requestId, typeRow[0].Id, currentYear, joursRestants]);
|
||
|
||
newRepartition.push({
|
||
typeCongeId: typeRow[0].Id,
|
||
annee: currentYear,
|
||
jours: joursRestants,
|
||
typeDeduction: 'N Anticip'
|
||
});
|
||
|
||
console.log(` ⚠️ ${name} Anticipé: ${joursRestants}j`);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
await connection.commit();
|
||
console.log(`✅ Demande ${requestId} modifiée avec succès`);
|
||
|
||
// 7️⃣ ENVOI DES EMAILS (Asynchrone)
|
||
const graphToken = await getGraphToken();
|
||
|
||
if (graphToken) {
|
||
const [managerInfo] = await connection.query(
|
||
`SELECT m.Email, m.Prenom, m.Nom
|
||
FROM CollaborateurAD c
|
||
JOIN HierarchieValidationAD h ON c.id = h.CollaborateurId
|
||
JOIN CollaborateurAD m ON h.SuperieurId = m.id
|
||
WHERE c.id = ?`,
|
||
[userId]
|
||
);
|
||
|
||
if (managerInfo.length > 0) {
|
||
const manager = managerInfo[0];
|
||
|
||
// Email au manager
|
||
sendMailGraph(
|
||
graphToken,
|
||
'gtanoreply@ensup.eu',
|
||
manager.Email,
|
||
'🔄 Modification de demande de congé',
|
||
`
|
||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||
<div style="background: linear-gradient(135deg, #3b82f6 0%, #1e40af 100%); padding: 30px; text-align: center; border-radius: 10px 10px 0 0;">
|
||
<h1 style="color: white; margin: 0;">🔄 Modification de demande</h1>
|
||
</div>
|
||
<div style="background-color: #f8f9fa; padding: 30px; border-radius: 0 0 10px 10px;">
|
||
<p style="font-size: 16px; color: #333;">Bonjour <strong>${manager.Prenom}</strong>,</p>
|
||
<p style="font-size: 16px; color: #333;"><strong>${userName}</strong> a modifié sa demande de congé :</p>
|
||
<div style="background-color: white; padding: 20px; border-radius: 8px; margin: 20px 0; border-left: 4px solid #3b82f6;">
|
||
<table style="width: 100%; border-collapse: collapse;">
|
||
<tr>
|
||
<td style="padding: 8px 0; color: #666;"><strong>Type :</strong></td>
|
||
<td style="padding: 8px 0; color: #333;">${getLeaveTypeName(leaveType)}</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding: 8px 0; color: #666;"><strong>Dates :</strong></td>
|
||
<td style="padding: 8px 0; color: #333;">du ${formatDateFR(startDate)} au ${formatDateFR(endDate)}</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding: 8px 0; color: #666;"><strong>Durée :</strong></td>
|
||
<td style="padding: 8px 0; color: #333;">${businessDays} jour(s)</td>
|
||
</tr>
|
||
</table>
|
||
</div>
|
||
${restorationStats.count > 0 ? `
|
||
<div style="background-color: #dbeafe; padding: 15px; border-radius: 8px; margin-top: 20px;">
|
||
<p style="margin: 0; color: #1e40af;">
|
||
✅ Les compteurs ont été automatiquement recalculés (${restorationStats.count} opération(s)).
|
||
</p>
|
||
</div>
|
||
` : ''}
|
||
<p style="font-size: 16px; color: #333;">Merci de valider ou refuser cette demande dans l'application.</p>
|
||
</div>
|
||
</div>
|
||
`
|
||
).catch(err => console.error('❌ Erreur email manager:', err));
|
||
|
||
// Email de confirmation au collaborateur
|
||
sendMailGraph(
|
||
graphToken,
|
||
'gtanoreply@ensup.eu',
|
||
userEmail,
|
||
'✅ Confirmation de modification',
|
||
`
|
||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||
<div style="background: linear-gradient(135deg, #10b981 0%, #059669 100%); padding: 30px; text-align: center; border-radius: 10px 10px 0 0;">
|
||
<h1 style="color: white; margin: 0;">✅ Demande modifiée</h1>
|
||
</div>
|
||
<div style="background-color: #f8f9fa; padding: 30px; border-radius: 0 0 10px 10px;">
|
||
<p style="font-size: 16px; color: #333;">Bonjour <strong>${userName.split(' ')[0]}</strong>,</p>
|
||
<p style="font-size: 16px; color: #333;">Votre demande de congé a bien été modifiée :</p>
|
||
<div style="background-color: white; padding: 20px; border-radius: 8px; margin: 20px 0; border-left: 4px solid #10b981;">
|
||
<table style="width: 100%; border-collapse: collapse;">
|
||
<tr>
|
||
<td style="padding: 8px 0; color: #666;"><strong>Type :</strong></td>
|
||
<td style="padding: 8px 0; color: #333;">${getLeaveTypeName(leaveType)}</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding: 8px 0; color: #666;"><strong>Dates :</strong></td>
|
||
<td style="padding: 8px 0; color: #333;">du ${formatDateFR(startDate)} au ${formatDateFR(endDate)}</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding: 8px 0; color: #666;"><strong>Durée :</strong></td>
|
||
<td style="padding: 8px 0; color: #333;">${businessDays} jour(s)</td>
|
||
</tr>
|
||
</table>
|
||
</div>
|
||
<p style="font-size: 16px; color: #333;">Elle est maintenant <strong style="color: #f59e0b;">en attente de validation</strong>.</p>
|
||
</div>
|
||
</div>
|
||
`
|
||
).catch(err => console.error('❌ Erreur email collaborateur:', err));
|
||
}
|
||
}
|
||
|
||
res.json({
|
||
success: true,
|
||
message: '✅ Demande modifiée avec succès',
|
||
newStatus: newStatus,
|
||
restoration: restorationStats,
|
||
repartition: newRepartition
|
||
});
|
||
|
||
} catch (error) {
|
||
if (connection) {
|
||
await connection.rollback();
|
||
}
|
||
console.error('❌ Erreur updateRequest:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: error.message || 'Erreur lors de la modification'
|
||
});
|
||
} finally {
|
||
if (connection) {
|
||
connection.release();
|
||
}
|
||
}
|
||
});
|
||
|
||
|
||
|
||
// ⭐ Fonction helper pour calculer la répartition CP
|
||
function calculateCPRepartition(joursNecessaires, soldeN1, soldeN) {
|
||
const repartition = [];
|
||
let reste = joursNecessaires;
|
||
|
||
// D'abord utiliser N-1
|
||
if (reste > 0 && soldeN1 > 0) {
|
||
const joursN1 = Math.min(reste, soldeN1);
|
||
repartition.push({
|
||
type: 'CP',
|
||
annee: 'N-1',
|
||
jours: joursN1
|
||
});
|
||
reste -= joursN1;
|
||
}
|
||
|
||
// Puis utiliser N
|
||
if (reste > 0 && soldeN > 0) {
|
||
const joursN = Math.min(reste, soldeN);
|
||
repartition.push({
|
||
type: 'CP',
|
||
annee: 'N',
|
||
jours: joursN
|
||
});
|
||
reste -= joursN;
|
||
}
|
||
|
||
return repartition;
|
||
}
|
||
|
||
// ⭐ Fonction helper pour obtenir le champ de compteur
|
||
function getCounterField(type, annee) {
|
||
if (type === 'CP' && annee === 'N-1') return 'SoldeCP_N1';
|
||
if (type === 'CP' && annee === 'N') return 'SoldeCP_N';
|
||
if (type === 'RTT' && annee === 'N') return 'SoldeRTT_N';
|
||
return null;
|
||
}
|
||
|
||
// ⭐ Fonction helper pour le nom du type de congé
|
||
function getLeaveTypeName(typeId) {
|
||
const types = {
|
||
1: 'Congé payé',
|
||
2: 'RTT',
|
||
3: 'Arrêt maladie',
|
||
4: 'Formation',
|
||
5: 'Récupération'
|
||
};
|
||
return types[typeId] || 'Inconnu';
|
||
}
|
||
|
||
// ⭐ Fonction helper pour formater les dates
|
||
function formatDateFR(dateStr) {
|
||
const date = new Date(dateStr);
|
||
return date.toLocaleDateString('fr-FR', {
|
||
day: '2-digit',
|
||
month: '2-digit',
|
||
year: 'numeric'
|
||
});
|
||
}
|
||
/**
|
||
* Route pour SUPPRIMER une demande de congé
|
||
* POST /deleteRequest
|
||
*/
|
||
app.post('/api/deleteRequest', async (req, res) => {
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
await conn.beginTransaction();
|
||
|
||
const { requestId, userId, userEmail, userName } = req.body;
|
||
|
||
if (!requestId || !userId) {
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.status(400).json({
|
||
success: false,
|
||
message: 'Paramètres manquants (requestId ou userId)'
|
||
});
|
||
}
|
||
|
||
console.log('\n🗑️ === ANNULATION DEMANDE ===');
|
||
console.log(`Demande ID: ${requestId}, User ID: ${userId}`);
|
||
|
||
// 1️⃣ Vérifier que la demande existe
|
||
const [existingRequest] = await conn.query(
|
||
`SELECT
|
||
d.Id,
|
||
d.DateDebut,
|
||
d.DateFin,
|
||
d.Statut,
|
||
d.NombreJours,
|
||
d.CollaborateurADId
|
||
FROM DemandeConge d
|
||
WHERE d.Id = ? AND d.CollaborateurADId = ?`,
|
||
[requestId, userId]
|
||
);
|
||
|
||
if (existingRequest.length === 0) {
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.status(404).json({
|
||
success: false,
|
||
message: 'Demande introuvable ou non autorisée'
|
||
});
|
||
}
|
||
|
||
const request = existingRequest[0];
|
||
const requestStatus = request.Statut;
|
||
|
||
// ⭐ CORRECTION 1 : Déclarer `aujourdhui` et `dateDebut` AVANT de les utiliser
|
||
const aujourdhui = new Date();
|
||
aujourdhui.setHours(0, 0, 0, 0);
|
||
|
||
const dateDebut = request.DateDebut ? new Date(request.DateDebut) : null;
|
||
if (dateDebut) {
|
||
dateDebut.setHours(0, 0, 0, 0);
|
||
}
|
||
|
||
if (!dateDebut || isNaN(dateDebut.getTime())) {
|
||
console.warn('⚠️ Date invalide, on autorise l\'annulation');
|
||
// Ne pas bloquer l'annulation si la date est invalide
|
||
} else {
|
||
dateDebut.setHours(0, 0, 0, 0);
|
||
console.log(`📋 Demande: Statut=${requestStatus}, Date début=${dateDebut.toLocaleDateString('fr-FR')}`);
|
||
|
||
// ❌ BLOQUER SI DATE DÉJÀ PASSÉE
|
||
if (dateDebut <= aujourdhui && requestStatus === 'Validée') {
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.status(400).json({
|
||
success: false,
|
||
message: '❌ Impossible d\'annuler : la date de début est déjà passée ou c\'est aujourd\'hui',
|
||
dateDebut: dateDebut.toISOString().split('T')[0]
|
||
});
|
||
}
|
||
}
|
||
|
||
// 2️⃣ RÉCUPÉRER LA RÉPARTITION (pour l'email)
|
||
const [repartition] = await conn.query(`
|
||
SELECT dct.*, tc.Nom as TypeNom
|
||
FROM DemandeCongeType dct
|
||
JOIN TypeConge tc ON dct.TypeCongeId = tc.Id
|
||
WHERE dct.DemandeCongeId = ?
|
||
ORDER BY tc.Nom
|
||
`, [requestId]);
|
||
|
||
// 3️⃣ RESTAURER LES COMPTEURS via DeductionDetails
|
||
let restorationStats = { count: 0, details: [] };
|
||
|
||
if (requestStatus !== 'Refusée' && requestStatus !== 'Annulée') {
|
||
console.log(`🔄 Restauration des compteurs...`);
|
||
|
||
const [deductions] = await conn.query(
|
||
'SELECT * FROM DeductionDetails WHERE DemandeCongeId = ?',
|
||
[requestId]
|
||
);
|
||
|
||
if (deductions.length > 0) {
|
||
for (const d of deductions) {
|
||
const [compteur] = await conn.query(
|
||
'SELECT Id, Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?',
|
||
[userId, d.TypeCongeId, d.Annee]
|
||
);
|
||
|
||
if (compteur.length > 0) {
|
||
const c = compteur[0];
|
||
const newSolde = parseFloat(c.Solde) + parseFloat(d.JoursUtilises);
|
||
|
||
await conn.query(
|
||
'UPDATE CompteurConges SET Solde = ?, DerniereMiseAJour = GETDATE() WHERE Id = ?',
|
||
[newSolde, c.Id]
|
||
);
|
||
|
||
restorationStats.count++;
|
||
restorationStats.details.push({
|
||
typeCongeId: d.TypeCongeId,
|
||
annee: d.Annee,
|
||
joursRendus: d.JoursUtilises
|
||
});
|
||
|
||
console.log(` ✅ Remboursé ${d.JoursUtilises}j au compteur TypeId=${d.TypeCongeId} Année=${d.Annee}`);
|
||
} else {
|
||
console.warn(`⚠️ Compteur introuvable (Type ${d.TypeCongeId}, Année ${d.Annee})`);
|
||
}
|
||
}
|
||
|
||
// Supprimer les déductions
|
||
await conn.query('DELETE FROM DeductionDetails WHERE DemandeCongeId = ?', [requestId]);
|
||
console.log(` 🧹 ${deductions.length} ligne(s) DeductionDetails supprimée(s)`);
|
||
} else {
|
||
console.log(' ℹ️ Aucune déduction à rembourser');
|
||
}
|
||
}
|
||
|
||
// 4️⃣ METTRE À JOUR LE STATUT
|
||
await conn.query(
|
||
`UPDATE DemandeConge
|
||
SET Statut = 'Annulée',
|
||
DateValidation = GETDATE(),
|
||
CommentaireValidation = COALESCE(CommentaireValidation, '') +
|
||
CHAR(10) + '[Annulée par le collaborateur le ' +
|
||
FORMAT(GETDATE(), 'dd/MM/yyyy à HH:mm', 'fr-FR') +
|
||
']'
|
||
WHERE Id = ?`,
|
||
[requestId]
|
||
);
|
||
|
||
console.log(`✅ Demande ${requestId} marquée comme Annulée`);
|
||
|
||
await conn.commit();
|
||
conn.release();
|
||
|
||
// 5️⃣ ENVOI DES EMAILS
|
||
let emailsSent = { collaborateur: false, manager: false };
|
||
const graphToken = await getGraphToken();
|
||
|
||
if (graphToken) {
|
||
const [collabInfo] = await conn.query(
|
||
'SELECT email, prenom, nom FROM CollaborateurAD WHERE id = ?',
|
||
[userId]
|
||
);
|
||
|
||
const collabEmail = collabInfo.length > 0 ? collabInfo[0].email : userEmail;
|
||
const collabName = collabInfo.length > 0
|
||
? `${collabInfo[0].prenom} ${collabInfo[0].nom}`
|
||
: userName;
|
||
|
||
// ⭐ CORRECTION : Formater correctement les dates pour l'email
|
||
const dateDebutFormatted = request.DateDebut
|
||
? new Date(request.DateDebut).toLocaleDateString('fr-FR')
|
||
: 'Date inconnue';
|
||
|
||
const dateFinFormatted = request.DateFin
|
||
? new Date(request.DateFin).toLocaleDateString('fr-FR')
|
||
: 'Date inconnue';
|
||
|
||
const datesPeriode = dateDebutFormatted === dateFinFormatted
|
||
? dateDebutFormatted
|
||
: `du ${dateDebutFormatted} au ${dateFinFormatted}`;
|
||
|
||
const repartitionText = repartition.map(r =>
|
||
`<tr>
|
||
<td style="padding: 8px 0; color: #666;"><strong>${r.TypeNom} :</strong></td>
|
||
<td style="padding: 8px 0; color: #333;">${r.NombreJours}j ${r.PeriodeJournee !== 'Journée entière' ? `(${r.PeriodeJournee})` : ''}</td>
|
||
</tr>`
|
||
).join('');
|
||
|
||
// 📧 EMAIL AU COLLABORATEUR
|
||
if (collabEmail) {
|
||
try {
|
||
const subjectCollab = '✅ Confirmation d\'annulation de votre demande';
|
||
const bodyCollab = `
|
||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||
<div style="background: linear-gradient(135deg, #10b981 0%, #059669 100%); padding: 30px; text-align: center; border-radius: 10px 10px 0 0;">
|
||
<h1 style="color: white; margin: 0;">✅ Demande annulée</h1>
|
||
</div>
|
||
|
||
<div style="background-color: #f8f9fa; padding: 30px; border-radius: 0 0 10px 10px;">
|
||
<p style="font-size: 16px; color: #333;">Bonjour <strong>${collabName}</strong>,</p>
|
||
|
||
<p style="font-size: 16px; color: #333;">
|
||
Votre demande de congé a bien été <strong style="color: #10b981;">annulée</strong>.
|
||
</p>
|
||
|
||
<div style="background-color: white; padding: 20px; border-radius: 8px; margin: 20px 0; border-left: 4px solid #10b981;">
|
||
<h3 style="color: #10b981; margin-top: 0;">📋 Demande annulée</h3>
|
||
<table style="width: 100%; border-collapse: collapse;">
|
||
<tr>
|
||
<td style="padding: 8px 0; color: #666;"><strong>Période :</strong></td>
|
||
<td style="padding: 8px 0; color: #333;">${datesPeriode}</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding: 8px 0; color: #666;"><strong>Durée totale :</strong></td>
|
||
<td style="padding: 8px 0; color: #333;">${request.NombreJours} jour(s)</td>
|
||
</tr>
|
||
<tr>
|
||
<td colspan="2" style="padding: 12px 0 4px 0; color: #666; border-top: 1px solid #e5e7eb;">
|
||
<strong>Répartition :</strong>
|
||
</td>
|
||
</tr>
|
||
${repartitionText}
|
||
</table>
|
||
</div>
|
||
|
||
${restorationStats.count > 0 ? `
|
||
<div style="background-color: #dbeafe; padding: 15px; border-radius: 8px; margin-top: 20px;">
|
||
<p style="margin: 0; color: #1e40af;">
|
||
✅ <strong>Vos compteurs ont été restaurés</strong><br>
|
||
${restorationStats.count} opération(s) de remboursement effectuée(s).
|
||
</p>
|
||
</div>
|
||
` : ''}
|
||
|
||
<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #e5e7eb;">
|
||
<p style="font-size: 14px; color: #666; margin: 0;">
|
||
📧 Cet email est envoyé automatiquement, merci de ne pas y répondre.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
await sendMailGraph(graphToken, 'gtanoreply@ensup.eu', collabEmail, subjectCollab, bodyCollab);
|
||
emailsSent.collaborateur = true;
|
||
console.log('✅ Email envoyé au collaborateur');
|
||
} catch (emailError) {
|
||
console.error('❌ Erreur email collaborateur:', emailError.message);
|
||
}
|
||
}
|
||
|
||
// 📧 EMAIL AU MANAGER
|
||
const [hierarchie] = await conn.query(
|
||
`SELECT h.SuperieurId, m.email as managerEmail,
|
||
m.prenom as managerPrenom, m.nom as managerNom
|
||
FROM HierarchieValidationAD h
|
||
LEFT JOIN CollaborateurAD m ON h.SuperieurId = m.id
|
||
WHERE h.CollaborateurId = ?`,
|
||
[userId]
|
||
);
|
||
|
||
const managerEmail = hierarchie[0]?.managerEmail;
|
||
const managerName = hierarchie[0]
|
||
? `${hierarchie[0].managerPrenom} ${hierarchie[0].managerNom}`
|
||
: 'Manager';
|
||
|
||
if (managerEmail && requestStatus !== 'Refusée' && requestStatus !== 'Annulée') {
|
||
try {
|
||
const isValidated = requestStatus === 'Validée' || requestStatus === 'Validé';
|
||
|
||
const subjectManager = isValidated
|
||
? `🗑️ Annulation de congé validé - ${collabName}`
|
||
: `🗑️ Annulation de demande - ${collabName}`;
|
||
|
||
const bodyManager = `
|
||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||
<div style="background: linear-gradient(135deg, ${isValidated ? '#ef4444' : '#f59e0b'} 0%, ${isValidated ? '#991b1b' : '#d97706'} 100%); padding: 30px; text-align: center; border-radius: 10px 10px 0 0;">
|
||
<h1 style="color: white; margin: 0;">🗑️ Annulation de ${isValidated ? 'congé' : 'demande'}</h1>
|
||
</div>
|
||
|
||
<div style="background-color: #f8f9fa; padding: 30px; border-radius: 0 0 10px 10px;">
|
||
<p style="font-size: 16px; color: #333;">Bonjour ${managerName},</p>
|
||
|
||
<p style="font-size: 16px; color: #333;">
|
||
<strong>${collabName}</strong> a annulé ${isValidated ? 'son congé <strong>validé</strong>' : 'sa demande de congé'}.
|
||
</p>
|
||
|
||
<div style="background-color: white; padding: 20px; border-radius: 8px; margin: 20px 0; border-left: 4px solid ${isValidated ? '#ef4444' : '#f59e0b'};">
|
||
<h3 style="color: ${isValidated ? '#ef4444' : '#f59e0b'}; margin-top: 0;">📋 Demande annulée</h3>
|
||
<table style="width: 100%; border-collapse: collapse;">
|
||
<tr>
|
||
<td style="padding: 8px 0; color: #666;"><strong>Statut initial :</strong></td>
|
||
<td style="padding: 8px 0; color: #333;">${requestStatus}</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding: 8px 0; color: #666;"><strong>Période :</strong></td>
|
||
<td style="padding: 8px 0; color: #333;">${datesPeriode}</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding: 8px 0; color: #666;"><strong>Durée totale :</strong></td>
|
||
<td style="padding: 8px 0; color: #333;">${request.NombreJours} jour(s)</td>
|
||
</tr>
|
||
<tr>
|
||
<td colspan="2" style="padding: 12px 0 4px 0; color: #666; border-top: 1px solid #e5e7eb;">
|
||
<strong>Répartition :</strong>
|
||
</td>
|
||
</tr>
|
||
${repartitionText}
|
||
</table>
|
||
</div>
|
||
|
||
${restorationStats.count > 0 ? `
|
||
<div style="background-color: #dbeafe; padding: 15px; border-radius: 8px; margin-top: 20px;">
|
||
<p style="margin: 0; color: #1e40af;">
|
||
✅ Les compteurs ont été automatiquement restaurés (${restorationStats.count} opération(s)).
|
||
</p>
|
||
</div>
|
||
` : ''}
|
||
|
||
<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #e5e7eb;">
|
||
<p style="font-size: 14px; color: #666; margin: 0;">
|
||
📧 Cet email est envoyé automatiquement, merci de ne pas y répondre.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
await sendMailGraph(graphToken, 'gtanoreply@ensup.eu', managerEmail, subjectManager, bodyManager);
|
||
emailsSent.manager = true;
|
||
console.log('✅ Email envoyé au manager');
|
||
} catch (emailError) {
|
||
console.error('❌ Erreur email manager:', emailError.message);
|
||
}
|
||
}
|
||
}
|
||
|
||
res.json({
|
||
success: true,
|
||
message: 'Demande annulée avec succès',
|
||
restoration: restorationStats,
|
||
emailsSent: emailsSent
|
||
});
|
||
|
||
} catch (error) {
|
||
await conn.rollback();
|
||
console.error('❌ Erreur deleteRequest:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur lors de l\'annulation',
|
||
error: error.message
|
||
});
|
||
} finally {
|
||
if (conn) conn.release();
|
||
}
|
||
});
|
||
|
||
app.get('/api/exportCompteurs', async (req, res) => {
|
||
try {
|
||
const dateRef = req.query.dateRef || new Date().toISOString().split('T')[0];
|
||
const conn = await pool.getConnection();
|
||
|
||
const [collaborateurs] = await pool.query(`
|
||
SELECT
|
||
ca.id,
|
||
ca.prenom,
|
||
ca.nom,
|
||
ca.email,
|
||
ca.role,
|
||
ca.TypeContrat,
|
||
ca.DateEntree,
|
||
s.Nom as service,
|
||
ca.CampusId,
|
||
ca.SocieteId,
|
||
so.Nom as societe_nom
|
||
FROM CollaborateurAD ca
|
||
LEFT JOIN Services s ON ca.ServiceId = s.Id
|
||
LEFT JOIN Societe so ON ca.SocieteId = so.Id
|
||
WHERE (ca.actif = 1 OR ca.actif IS NULL)
|
||
ORDER BY ca.nom, ca.prenom
|
||
`);
|
||
|
||
const rapport = [];
|
||
|
||
for (const collab of collaborateurs) {
|
||
const dateEntree = collab.DateEntree;
|
||
const dateReference = new Date(dateRef);
|
||
|
||
const acquisCP = calculerAcquisitionCP_Smart(dateReference, dateEntree);
|
||
|
||
let acquisRTT = 0;
|
||
if (collab.role !== 'Apprenti') {
|
||
const rttData = await calculerAcquisitionRTT_Smart(conn, collab.id, dateReference);
|
||
acquisRTT = rttData.acquisition;
|
||
}
|
||
|
||
const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']);
|
||
const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']);
|
||
|
||
let soldeCP = 0;
|
||
let soldeRTT = 0;
|
||
|
||
if (cpType.length > 0) {
|
||
const [compteurCP] = await conn.query(
|
||
'SELECT Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?',
|
||
[collab.id, cpType[0].Id, dateReference.getFullYear()]
|
||
);
|
||
soldeCP = compteurCP.length > 0 ? parseFloat(compteurCP[0].Solde) : 0;
|
||
}
|
||
|
||
if (rttType.length > 0 && collab.role !== 'Apprenti') {
|
||
const [compteurRTT] = await conn.query(
|
||
'SELECT Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?',
|
||
[collab.id, rttType[0].Id, dateReference.getFullYear()]
|
||
);
|
||
soldeRTT = compteurRTT.length > 0 ? parseFloat(compteurRTT[0].Solde) : 0;
|
||
}
|
||
|
||
rapport.push({
|
||
id: collab.id,
|
||
prenom: collab.prenom,
|
||
nom: collab.nom,
|
||
email: collab.email,
|
||
role: collab.role,
|
||
service: collab.service,
|
||
societe_id: collab.SocieteId,
|
||
societe_nom: collab.societe_nom,
|
||
type_contrat: collab.TypeContrat,
|
||
date_entree: dateEntree ? formatDateWithoutUTC(dateEntree) : null,
|
||
cp_acquis: parseFloat(acquisCP.toFixed(2)),
|
||
cp_solde: parseFloat(soldeCP.toFixed(2)),
|
||
rtt_acquis: parseFloat(acquisRTT.toFixed(2)),
|
||
rtt_solde: parseFloat(soldeRTT.toFixed(2)),
|
||
date_reference: dateRef
|
||
});
|
||
}
|
||
|
||
conn.release();
|
||
|
||
res.json({
|
||
success: true,
|
||
date_reference: dateRef,
|
||
total_collaborateurs: rapport.length,
|
||
rapport: rapport
|
||
});
|
||
} catch (error) {
|
||
console.error('Erreur exportCompteurs:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur serveur',
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
|
||
function isInPeriodeAnticipation(dateDebut, typeConge) {
|
||
const date = new Date(dateDebut);
|
||
const year = date.getFullYear();
|
||
const month = date.getMonth() + 1; // 1-12
|
||
|
||
if (typeConge === 'CP') {
|
||
// CP : 01/06 année N → 31/05 année N+1
|
||
// Période anticipation : du 01/06 de l'année suivante
|
||
return month >= 6; // Si >= juin, c'est pour l'exercice N+1
|
||
} else if (typeConge === 'RTT') {
|
||
// RTT : 01/01 année N → 31/12 année N
|
||
// Pas d'anticipation possible car année civile
|
||
return month >= 1 && month <= 12;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
function getAnneeCompteur(dateDebut, typeConge) {
|
||
const date = new Date(dateDebut);
|
||
const year = date.getFullYear();
|
||
const month = date.getMonth() + 1;
|
||
|
||
if (typeConge === 'CP') {
|
||
// Si date entre 01/06 et 31/12 → année N
|
||
// Si date entre 01/01 et 31/05 → année N-1 (exercice précédent)
|
||
return month >= 6 ? year : year - 1;
|
||
} else {
|
||
// RTT : toujours année civile
|
||
return year;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Vérifie la disponibilité des soldes pour une demande
|
||
* Retourne : { available: boolean, details: {}, useN1: boolean }
|
||
*/
|
||
|
||
|
||
async function checkSoldesDisponiblesMixte(conn, collaborateurId, repartition, dateDebut, isApprenti) {
|
||
const today = new Date();
|
||
today.setHours(0, 0, 0, 0);
|
||
|
||
const currentYear = today.getFullYear();
|
||
const dateDemandeObj = new Date(dateDebut);
|
||
dateDemandeObj.setHours(0, 0, 0, 0);
|
||
|
||
const demandeYear = dateDemandeObj.getFullYear();
|
||
const demandeMonth = dateDemandeObj.getMonth() + 1;
|
||
|
||
console.log('\n🔍 === CHECK SOLDES MIXTE (AVEC ANTICIPATION) ===');
|
||
console.log('📅 Date AUJOURD\'HUI:', today.toISOString().split('T')[0]);
|
||
console.log('📅 Date DEMANDE:', dateDebut);
|
||
console.log('📅 Année demande:', demandeYear, '/ Mois:', demandeMonth);
|
||
console.log('📅 Année actuelle:', currentYear);
|
||
|
||
let totalDisponible = 0;
|
||
let totalNecessaire = 0;
|
||
const details = {};
|
||
|
||
for (const rep of repartition) {
|
||
const typeCode = rep.TypeConge;
|
||
const joursNecessaires = parseFloat(rep.NombreJours || 0);
|
||
|
||
// Ignorer ABS et Formation
|
||
if (typeCode === 'ABS' || typeCode === 'Formation') {
|
||
continue;
|
||
}
|
||
|
||
totalNecessaire += joursNecessaires;
|
||
|
||
if (typeCode === 'CP') {
|
||
// ⭐ RÉCUPÉRER LES INFOS COLLABORATEUR
|
||
const [collabInfo] = await conn.query(
|
||
`SELECT DateEntree FROM CollaborateurAD WHERE id = ?`,
|
||
[collaborateurId]
|
||
);
|
||
const dateEntree = collabInfo[0]?.DateEntree;
|
||
|
||
// ⭐ CALCULER L'ACQUISITION JUSQU'À LA DATE DEMANDÉE
|
||
const acquisALaDate = calculerAcquisitionCP_Smart(dateDemandeObj, dateEntree);
|
||
|
||
console.log('💰 Acquisition CP à la date', dateDebut, ':', acquisALaDate.toFixed(2), 'j');
|
||
|
||
// ⭐ RÉCUPÉRER LE REPORTÉ N-1
|
||
const previousYear = currentYear - 1;
|
||
const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']);
|
||
const cpTypeId = cpType[0].Id;
|
||
|
||
const [compteurN1] = await conn.query(`
|
||
SELECT SoldeReporte
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collaborateurId, cpTypeId, previousYear]);
|
||
|
||
const reporteN1 = compteurN1.length > 0 ? parseFloat(compteurN1[0].SoldeReporte || 0) : 0;
|
||
|
||
// ⭐ RÉCUPÉRER CE QUI A DÉJÀ ÉTÉ POSÉ (toutes demandes validées ou en attente)
|
||
const [totalPose] = await conn.query(`
|
||
SELECT COALESCE(SUM(dct.NombreJours), 0) as totalPose
|
||
FROM DemandeConge dc
|
||
JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId
|
||
JOIN TypeConge tc ON dct.TypeCongeId = tc.Id
|
||
WHERE dc.CollaborateurADId = ?
|
||
AND tc.Nom = 'Congé payé'
|
||
AND dc.Statut IN ('Validée', 'En attente')
|
||
AND dc.DateDebut <= ?
|
||
`, [collaborateurId, dateDebut]);
|
||
|
||
const dejaPose = parseFloat(totalPose[0].totalPose || 0);
|
||
|
||
// ⭐ SOLDE RÉEL = Reporté N-1 + Acquisition - Déjà posé
|
||
const soldeReel = reporteN1 + acquisALaDate - dejaPose;
|
||
|
||
console.log('💰 Soldes CP détaillés:', {
|
||
reporteN1: reporteN1.toFixed(2),
|
||
acquisALaDate: acquisALaDate.toFixed(2),
|
||
dejaPose: dejaPose.toFixed(2),
|
||
soldeReel: soldeReel.toFixed(2)
|
||
});
|
||
|
||
details.CP = {
|
||
reporteN1: reporteN1,
|
||
acquisALaDate: acquisALaDate,
|
||
dejaPose: dejaPose,
|
||
soldeReel: soldeReel,
|
||
necessaire: joursNecessaires
|
||
};
|
||
|
||
totalDisponible += Math.max(0, soldeReel);
|
||
|
||
if (soldeReel < joursNecessaires) {
|
||
return {
|
||
available: false,
|
||
message: `Solde CP insuffisant (${Math.max(0, soldeReel).toFixed(2)}j disponibles avec anticipation, ${joursNecessaires}j demandés)`,
|
||
details,
|
||
manque: joursNecessaires - soldeReel
|
||
};
|
||
}
|
||
|
||
} else if (typeCode === 'RTT') {
|
||
if (isApprenti) {
|
||
return {
|
||
available: false,
|
||
message: 'Les apprentis ne peuvent pas poser de RTT',
|
||
details
|
||
};
|
||
}
|
||
|
||
// ⭐ CALCUL RTT (utiliser la fonction existante)
|
||
const rttData = await calculerAcquisitionRTT_Smart(conn, collaborateurId, dateDemandeObj);
|
||
const acquisALaDate = rttData.acquisition;
|
||
|
||
console.log('💰 Acquisition RTT à la date', dateDebut, ':', acquisALaDate.toFixed(2), 'j');
|
||
|
||
// ⭐ RÉCUPÉRER CE QUI A DÉJÀ ÉTÉ POSÉ
|
||
const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']);
|
||
const rttTypeId = rttType[0].Id;
|
||
|
||
const [totalPose] = await conn.query(`
|
||
SELECT COALESCE(SUM(dct.NombreJours), 0) as totalPose
|
||
FROM DemandeConge dc
|
||
JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId
|
||
JOIN TypeConge tc ON dct.TypeCongeId = tc.Id
|
||
WHERE dc.CollaborateurADId = ?
|
||
AND tc.Nom = 'RTT'
|
||
AND dc.Statut IN ('Validée', 'En attente')
|
||
AND dc.DateDebut <= ?
|
||
`, [collaborateurId, dateDebut]);
|
||
|
||
const dejaPose = parseFloat(totalPose[0].totalPose || 0);
|
||
|
||
// ⭐ SOLDE RÉEL = Acquisition - Déjà posé
|
||
const soldeReel = acquisALaDate - dejaPose;
|
||
|
||
console.log('💰 Soldes RTT détaillés:', {
|
||
acquisALaDate: acquisALaDate.toFixed(2),
|
||
dejaPose: dejaPose.toFixed(2),
|
||
soldeReel: soldeReel.toFixed(2)
|
||
});
|
||
|
||
details.RTT = {
|
||
acquisALaDate: acquisALaDate,
|
||
dejaPose: dejaPose,
|
||
soldeReel: soldeReel,
|
||
necessaire: joursNecessaires
|
||
};
|
||
|
||
totalDisponible += Math.max(0, soldeReel);
|
||
|
||
if (soldeReel < joursNecessaires) {
|
||
return {
|
||
available: false,
|
||
message: `Solde RTT insuffisant (${Math.max(0, soldeReel).toFixed(2)}j disponibles avec anticipation, ${joursNecessaires}j demandés)`,
|
||
details,
|
||
manque: joursNecessaires - soldeReel
|
||
};
|
||
}
|
||
|
||
} else if (typeCode === 'Récup') {
|
||
const [recupType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Récupération']);
|
||
if (recupType.length === 0) continue;
|
||
|
||
const recupTypeId = recupType[0].Id;
|
||
|
||
const [compteur] = await conn.query(
|
||
`SELECT Solde FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, recupTypeId, currentYear]
|
||
);
|
||
|
||
const soldeRecup = compteur.length > 0 ? parseFloat(compteur[0].Solde || 0) : 0;
|
||
|
||
console.log('💰 Solde Récup:', soldeRecup.toFixed(2), 'j');
|
||
|
||
details.Recup = {
|
||
soldeN: soldeRecup,
|
||
total: soldeRecup,
|
||
necessaire: joursNecessaires
|
||
};
|
||
|
||
totalDisponible += Math.min(joursNecessaires, soldeRecup);
|
||
|
||
if (soldeRecup < joursNecessaires) {
|
||
return {
|
||
available: false,
|
||
message: `Solde Récupération insuffisant (${soldeRecup.toFixed(2)}j disponibles, ${joursNecessaires}j demandés)`,
|
||
details,
|
||
manque: joursNecessaires - soldeRecup
|
||
};
|
||
}
|
||
}
|
||
}
|
||
|
||
console.log('\n✅ Check final:', {
|
||
totalDisponible: totalDisponible.toFixed(2),
|
||
totalNecessaire: totalNecessaire.toFixed(2),
|
||
available: totalDisponible >= totalNecessaire
|
||
});
|
||
|
||
return {
|
||
available: totalDisponible >= totalNecessaire,
|
||
details,
|
||
totalDisponible,
|
||
totalNecessaire
|
||
};
|
||
}
|
||
// ========================================
|
||
// FONCTIONS HELPER
|
||
// ========================================
|
||
|
||
async function getSoldesCP(conn, collaborateurId, dateEntree, includeN1Anticipe = false) {
|
||
const currentYear = new Date().getFullYear();
|
||
const previousYear = currentYear - 1;
|
||
|
||
console.log(`\n📊 getSoldesCP - includeN1Anticipe: ${includeN1Anticipe}`);
|
||
|
||
const [cpType] = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = 'Congé payé' LIMIT 1`);
|
||
const typeCongeId = cpType[0].Id;
|
||
|
||
// N-1 (reporté)
|
||
const [compteursN1] = await conn.query(
|
||
`SELECT Solde, SoldeReporte FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, typeCongeId, previousYear]
|
||
);
|
||
|
||
const soldeN1 = compteursN1.length > 0 ? parseFloat(compteursN1[0].SoldeReporte || 0) : 0;
|
||
|
||
// N (actuel)
|
||
const [compteursN] = await conn.query(
|
||
`SELECT Solde, SoldeReporte, Total FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, typeCongeId, currentYear]
|
||
);
|
||
|
||
const soldeN = compteursN.length > 0
|
||
? parseFloat(compteursN[0].Solde || 0) - parseFloat(compteursN[0].SoldeReporte || 0)
|
||
: 0;
|
||
const totalAcquisN = compteursN.length > 0 ? parseFloat(compteursN[0].Total || 0) : 0;
|
||
|
||
// Anticipation N
|
||
const finExerciceN = new Date(currentYear + 1, 4, 31); // 31 mai N+1
|
||
const acquisTotaleN = calculerAcquisitionCP_Smart(finExerciceN, dateEntree);
|
||
const soldeAnticipeN = Math.max(0, acquisTotaleN - totalAcquisN);
|
||
|
||
console.log(' N-1:', soldeN1);
|
||
console.log(' N:', soldeN);
|
||
console.log(' Anticipé N:', soldeAnticipeN);
|
||
|
||
// ⭐ Anticipation N+1 (si demandé)
|
||
let soldeAnticipeN1 = 0;
|
||
if (includeN1Anticipe) {
|
||
const debutExerciceN1 = new Date(currentYear + 1, 5, 1); // 01 juin N+1
|
||
const finExerciceN1 = new Date(currentYear + 2, 4, 31); // 31 mai N+2
|
||
|
||
let dateCalculN1 = debutExerciceN1;
|
||
if (dateEntree && new Date(dateEntree) > debutExerciceN1) {
|
||
dateCalculN1 = new Date(dateEntree);
|
||
}
|
||
|
||
const acquisTotaleN1 = calculerAcquisitionCP_Smart(finExerciceN1, dateCalculN1);
|
||
soldeAnticipeN1 = acquisTotaleN1;
|
||
|
||
console.log(' Anticipé N+1:', soldeAnticipeN1);
|
||
}
|
||
|
||
return { soldeN1, soldeN, soldeAnticipeN, soldeAnticipeN1 };
|
||
}
|
||
|
||
async function getSoldesRTT(conn, collaborateurId, typeContrat, dateEntree) {
|
||
const currentYear = new Date().getFullYear();
|
||
|
||
const rttType = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = 'RTT' LIMIT 1`);
|
||
const typeCongeId = rttType[0].Id;
|
||
|
||
const compteursN = await conn.query(
|
||
`SELECT Solde, Total FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, typeCongeId, currentYear]
|
||
);
|
||
|
||
const soldeN = compteursN.length > 0 ? parseFloat(compteursN[0].Solde || 0) : 0;
|
||
const totalAcquisN = compteursN.length > 0 ? parseFloat(compteursN[0].Total || 0) : 0;
|
||
|
||
// Calcul anticipation N
|
||
const finAnneeN = new Date(currentYear, 11, 31); // 31 déc N
|
||
const rttDataTotalN = await calculerAcquisitionRTT_Smart(conn, collaborateurId, finAnneeN);
|
||
const soldeAnticipeN = Math.max(0, rttDataTotalN.acquisition - totalAcquisN);
|
||
|
||
return { soldeN, soldeAnticipeN };
|
||
}
|
||
|
||
async function getSoldesRecup(conn, collaborateurId) {
|
||
const currentYear = new Date().getFullYear();
|
||
|
||
const recupType = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = 'Récupération' LIMIT 1`);
|
||
if (recupType.length === 0) return 0;
|
||
|
||
const typeCongeId = recupType[0].Id;
|
||
|
||
const compteur = await conn.query(
|
||
`SELECT Solde FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, typeCongeId, currentYear]
|
||
);
|
||
|
||
return compteur.length > 0 ? parseFloat(compteur[0].Solde || 0) : 0;
|
||
}
|
||
|
||
app.get('/api/getAnticipationDisponible', async (req, res) => {
|
||
try {
|
||
const { userid, dateDebut } = req.query;
|
||
|
||
if (!userid || !dateDebut) {
|
||
return res.json({ success: false, message: 'Paramètres manquants' });
|
||
}
|
||
|
||
const conn = await pool.getConnection();
|
||
|
||
const [collabInfo] = await conn.query(
|
||
`SELECT DateEntree, TypeContrat, role FROM CollaborateurAD WHERE id = ?`,
|
||
[userid]
|
||
);
|
||
|
||
const dateEntree = collabInfo.DateEntree;
|
||
const isApprenti = collabInfo.role === 'Apprenti';
|
||
|
||
// Déterminer si c'est une demande N+1
|
||
const dateDemandeObj = new Date(dateDebut);
|
||
const currentYear = new Date().getFullYear();
|
||
const demandeYear = dateDemandeObj.getFullYear();
|
||
const demandeMonth = dateDemandeObj.getMonth() + 1;
|
||
|
||
const isN1 = (demandeYear === currentYear + 1 && demandeMonth >= 6) ||
|
||
(demandeYear === currentYear + 2 && demandeMonth <= 5);
|
||
|
||
// Calculer les soldes avec anticipation
|
||
const soldesCP = await getSoldesCP(conn, userid, dateEntree, isN1);
|
||
const soldesRTT = isApprenti ? { soldeN: 0, soldeAnticipeN: 0 } :
|
||
await getSoldesRTT(conn, userid, collabInfo.TypeContrat, dateEntree);
|
||
const soldesRecup = await getSoldesRecup(conn, userid);
|
||
|
||
conn.release();
|
||
|
||
res.json({
|
||
success: true,
|
||
isN1Request: isN1,
|
||
CP: {
|
||
actuel: soldesCP.soldeN1 + soldesCP.soldeN,
|
||
anticipeN: soldesCP.soldeAnticipeN,
|
||
anticipeN1: isN1 ? soldesCP.soldeAnticipeN1 : 0,
|
||
total: soldesCP.soldeN1 + soldesCP.soldeN + soldesCP.soldeAnticipeN + (isN1 ? soldesCP.soldeAnticipeN1 : 0)
|
||
},
|
||
RTT: {
|
||
actuel: soldesRTT.soldeN,
|
||
anticipeN: soldesRTT.soldeAnticipeN,
|
||
total: soldesRTT.soldeN + soldesRTT.soldeAnticipeN
|
||
},
|
||
Recup: {
|
||
actuel: soldesRecup,
|
||
total: soldesRecup
|
||
}
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('Erreur getAnticipationDisponible:', error);
|
||
res.status(500).json({ success: false, message: error.message });
|
||
}
|
||
});
|
||
|
||
|
||
|
||
async function deductLeaveBalanceWithN1(conn, collaborateurId, typeCongeId, nombreJours, demandeCongeId, dateDebut) {
|
||
const currentYear = new Date().getFullYear();
|
||
const previousYear = currentYear - 1;
|
||
const nextYear = currentYear + 1;
|
||
|
||
let joursRestants = nombreJours;
|
||
const deductions = [];
|
||
|
||
const dateDemandeObj = new Date(dateDebut);
|
||
const demandeYear = dateDemandeObj.getFullYear();
|
||
const demandeMonth = dateDemandeObj.getMonth() + 1;
|
||
|
||
// Déterminer le type de congé
|
||
const [typeRow] = await conn.query('SELECT Nom FROM TypeConge WHERE Id = ?', [typeCongeId]);
|
||
const typeNom = typeRow[0].Nom;
|
||
const isCP = typeNom === 'Congé payé';
|
||
|
||
// Vérifier si demande pour N+1
|
||
let useN1 = false;
|
||
if (isCP) {
|
||
useN1 = (demandeYear === nextYear && demandeMonth >= 6) ||
|
||
(demandeYear === nextYear + 1 && demandeMonth <= 5);
|
||
} else {
|
||
useN1 = demandeYear === nextYear;
|
||
}
|
||
|
||
console.log(`\n💰 Déduction ${typeNom}: ${nombreJours}j (useN1: ${useN1})`);
|
||
|
||
if (useN1) {
|
||
// ORDRE N+1 : N+1 anticipé → N anticipé → N actuel → N-1
|
||
|
||
// 1. N+1 Anticipé (priorité absolue)
|
||
const compteurN1Anticipe = await conn.query(
|
||
`SELECT Id, SoldeAnticipe FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, typeCongeId, nextYear]
|
||
);
|
||
|
||
if (compteurN1Anticipe.length === 0 && joursRestants > 0) {
|
||
// Créer le compteur N+1 si inexistant
|
||
await conn.query(
|
||
`INSERT INTO CompteurConges
|
||
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, SoldeAnticipe, IsAnticipe)
|
||
VALUES (?, ?, ?, 0, 0, 0, 0, 0)`,
|
||
[collaborateurId, typeCongeId, nextYear]
|
||
);
|
||
}
|
||
|
||
// Récupérer à nouveau après création
|
||
const compteurN1A = await conn.query(
|
||
`SELECT Id, SoldeAnticipe FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, typeCongeId, nextYear]
|
||
);
|
||
|
||
if (compteurN1A.length > 0) {
|
||
const aDeduire = Math.min(joursRestants, joursRestants); // Tous les jours restants
|
||
|
||
if (aDeduire > 0) {
|
||
await conn.query(
|
||
`UPDATE CompteurConges
|
||
SET SoldeAnticipe = SoldeAnticipe + ?, IsAnticipe = 1
|
||
WHERE Id = ?`,
|
||
[aDeduire, compteurN1A[0].Id]
|
||
);
|
||
|
||
await conn.query(
|
||
`INSERT INTO DeductionDetails
|
||
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
|
||
VALUES (?, ?, ?, 'N+1 Anticipé', ?)`,
|
||
[demandeCongeId, typeCongeId, nextYear, aDeduire]
|
||
);
|
||
|
||
deductions.push({
|
||
annee: nextYear,
|
||
type: 'N+1 Anticipé',
|
||
joursUtilises: aDeduire
|
||
});
|
||
|
||
joursRestants -= aDeduire;
|
||
console.log(`✓ N+1 Anticipé: ${aDeduire}j - reste: ${joursRestants}j`);
|
||
}
|
||
}
|
||
// 2. N anticipé
|
||
if (joursRestants > 0) {
|
||
const [compteurN_Anticipe] = await conn.query(`
|
||
SELECT Id, SoldeAnticipe
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collaborateurId, typeCongeId, currentYear]);
|
||
|
||
if (compteurN_Anticipe.length > 0) {
|
||
const soldeNA = parseFloat(compteurN_Anticipe[0].SoldeAnticipe || 0);
|
||
const aDeduire = Math.min(soldeNA, joursRestants);
|
||
|
||
if (aDeduire > 0) {
|
||
await conn.query(`
|
||
UPDATE CompteurConges
|
||
SET SoldeAnticipe = CASE WHEN (SoldeAnticipe - ?) < 0 THEN 0 ELSE (SoldeAnticipe - ?) END
|
||
WHERE Id = ?
|
||
`, [aDeduire, compteurN_Anticipe[0].Id]);
|
||
|
||
await conn.query(`
|
||
INSERT INTO DeductionDetails
|
||
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
|
||
VALUES (?, ?, ?, 'N Anticipé', ?)
|
||
`, [demandeCongeId, typeCongeId, currentYear, aDeduire]);
|
||
|
||
deductions.push({
|
||
annee: currentYear,
|
||
type: 'N Anticipé',
|
||
joursUtilises: aDeduire,
|
||
soldeAvant: soldeNA
|
||
});
|
||
|
||
joursRestants -= aDeduire;
|
||
console.log(` ✓ N Anticipé: ${aDeduire}j (reste: ${joursRestants}j)`);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 3. N actuel
|
||
if (joursRestants > 0) {
|
||
const [compteurN] = await conn.query(`
|
||
SELECT Id, Solde, SoldeReporte
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collaborateurId, typeCongeId, currentYear]);
|
||
|
||
if (compteurN.length > 0) {
|
||
const soldeN = parseFloat(compteurN[0].Solde) - parseFloat(compteurN[0].SoldeReporte || 0);
|
||
const aDeduire = Math.min(soldeN, joursRestants);
|
||
|
||
if (aDeduire > 0) {
|
||
await conn.query(`
|
||
UPDATE CompteurConges
|
||
SET Solde = CASE WHEN (Solde - ?) < 0 THEN 0 ELSE (Solde - ?) END
|
||
WHERE Id = ?
|
||
`, [aDeduire, compteurN[0].Id]);
|
||
|
||
await conn.query(`
|
||
INSERT INTO DeductionDetails
|
||
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
|
||
VALUES (?, ?, ?, 'Année N', ?)
|
||
`, [demandeCongeId, typeCongeId, currentYear, aDeduire]);
|
||
|
||
deductions.push({
|
||
annee: currentYear,
|
||
type: 'Année N',
|
||
joursUtilises: aDeduire,
|
||
soldeAvant: soldeN
|
||
});
|
||
|
||
joursRestants -= aDeduire;
|
||
console.log(` ✓ Année N: ${aDeduire}j (reste: ${joursRestants}j)`);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 4. N-1 reporté
|
||
if (joursRestants > 0) {
|
||
const [compteurN1] = await conn.query(`
|
||
SELECT Id, SoldeReporte
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collaborateurId, typeCongeId, previousYear]);
|
||
|
||
if (compteurN1.length > 0) {
|
||
const soldeN1 = parseFloat(compteurN1[0].SoldeReporte || 0);
|
||
const aDeduire = Math.min(soldeN1, joursRestants);
|
||
|
||
if (aDeduire > 0) {
|
||
await conn.query(`
|
||
UPDATE CompteurConges
|
||
SET SoldeReporte = CASE WHEN (SoldeReporte - ?) < 0 THEN 0 ELSE (SoldeReporte - ?) END,
|
||
Solde = CASE WHEN (Solde - ?) < 0 THEN 0 ELSE (Solde - ?) END
|
||
WHERE Id = ?
|
||
`, [aDeduire, aDeduire, compteurN1[0].Id]);
|
||
|
||
await conn.query(`
|
||
INSERT INTO DeductionDetails
|
||
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
|
||
VALUES (?, ?, ?, 'Reporté N-1', ?)
|
||
`, [demandeCongeId, typeCongeId, previousYear, aDeduire]);
|
||
|
||
deductions.push({
|
||
annee: previousYear,
|
||
type: 'Reporté N-1',
|
||
joursUtilises: aDeduire,
|
||
soldeAvant: soldeN1
|
||
});
|
||
|
||
joursRestants -= aDeduire;
|
||
console.log(` ✓ Reporté N-1: ${aDeduire}j (reste: ${joursRestants}j)`);
|
||
}
|
||
}
|
||
}
|
||
|
||
} else {
|
||
// ORDRE NORMAL : N-1 → N → N anticipé
|
||
|
||
// 1. Reporté N-1
|
||
const [compteurN1] = await conn.query(`
|
||
SELECT Id, SoldeReporte, Solde
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collaborateurId, typeCongeId, previousYear]);
|
||
|
||
if (compteurN1.length > 0 && compteurN1[0].SoldeReporte > 0) {
|
||
const soldeN1 = parseFloat(compteurN1[0].SoldeReporte);
|
||
const aDeduire = Math.min(soldeN1, joursRestants);
|
||
|
||
if (aDeduire > 0) {
|
||
await conn.query(`
|
||
UPDATE CompteurConges
|
||
SET SoldeReporte = CASE WHEN (SoldeReporte - ?) < 0 THEN 0 ELSE (SoldeReporte - ?) END,
|
||
Solde = CASE WHEN (Solde - ?) < 0 THEN 0 ELSE (Solde - ?) END
|
||
WHERE Id = ?
|
||
`, [aDeduire, aDeduire, compteurN1[0].Id]);
|
||
|
||
await conn.query(`
|
||
INSERT INTO DeductionDetails
|
||
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
|
||
VALUES (?, ?, ?, 'Reporté N-1', ?)
|
||
`, [demandeCongeId, typeCongeId, previousYear, aDeduire]);
|
||
|
||
deductions.push({
|
||
annee: previousYear,
|
||
type: 'Reporté N-1',
|
||
joursUtilises: aDeduire,
|
||
soldeAvant: soldeN1
|
||
});
|
||
|
||
joursRestants -= aDeduire;
|
||
console.log(` ✓ Reporté N-1: ${aDeduire}j (reste: ${joursRestants}j)`);
|
||
}
|
||
}
|
||
|
||
// 2. Année N
|
||
if (joursRestants > 0) {
|
||
const [compteurN] = await conn.query(`
|
||
SELECT Id, Solde, SoldeReporte
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collaborateurId, typeCongeId, currentYear]);
|
||
|
||
if (compteurN.length > 0) {
|
||
const soldeN = parseFloat(compteurN[0].Solde) - parseFloat(compteurN[0].SoldeReporte || 0);
|
||
const aDeduire = Math.min(soldeN, joursRestants);
|
||
|
||
if (aDeduire > 0) {
|
||
await conn.query(`
|
||
UPDATE CompteurConges
|
||
SET Solde = CASE WHEN (Solde - ?) < 0 THEN 0 ELSE (Solde - ?) END
|
||
WHERE Id = ?
|
||
`, [aDeduire, compteurN[0].Id]);
|
||
|
||
await conn.query(`
|
||
INSERT INTO DeductionDetails
|
||
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
|
||
VALUES (?, ?, ?, 'Année N', ?)
|
||
`, [demandeCongeId, typeCongeId, currentYear, aDeduire]);
|
||
|
||
deductions.push({
|
||
annee: currentYear,
|
||
type: 'Année N',
|
||
joursUtilises: aDeduire,
|
||
soldeAvant: soldeN
|
||
});
|
||
|
||
joursRestants -= aDeduire;
|
||
console.log(` ✓ Année N: ${aDeduire}j (reste: ${joursRestants}j)`);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 3. N anticipé
|
||
if (joursRestants > 0) {
|
||
const [compteurN_Anticipe] = await conn.query(`
|
||
SELECT Id, SoldeAnticipe
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collaborateurId, typeCongeId, currentYear]);
|
||
|
||
if (compteurN_Anticipe.length > 0) {
|
||
const soldeNA = parseFloat(compteurN_Anticipe[0].SoldeAnticipe || 0);
|
||
const aDeduire = Math.min(soldeNA, joursRestants);
|
||
|
||
if (aDeduire > 0) {
|
||
await conn.query(`
|
||
UPDATE CompteurConges
|
||
SET SoldeAnticipe = CASE WHEN (SoldeAnticipe - ?) < 0 THEN 0 ELSE (SoldeAnticipe - ?) END
|
||
WHERE Id = ?
|
||
`, [aDeduire, compteurN_Anticipe[0].Id]);
|
||
|
||
await conn.query(`
|
||
INSERT INTO DeductionDetails
|
||
(DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
|
||
VALUES (?, ?, ?, 'N Anticipé', ?)
|
||
`, [demandeCongeId, typeCongeId, currentYear, aDeduire]);
|
||
|
||
deductions.push({
|
||
annee: currentYear,
|
||
type: 'N Anticipé',
|
||
joursUtilises: aDeduire,
|
||
soldeAvant: soldeNA
|
||
});
|
||
|
||
joursRestants -= aDeduire;
|
||
console.log(` ✓ N Anticipé: ${aDeduire}j (reste: ${joursRestants}j)`);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return {
|
||
success: joursRestants === 0,
|
||
joursDeduitsTotal: nombreJours - joursRestants,
|
||
joursNonDeduits: joursRestants,
|
||
details: deductions,
|
||
useN1: useN1
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Met à jour les soldes anticipés pour un collaborateur
|
||
* Appelée après chaque mise à jour de compteur ou soumission de demande
|
||
*/
|
||
/**
|
||
* Met à jour les soldes anticipés pour un collaborateur
|
||
* Appelée après chaque mise à jour de compteur ou soumission de demande
|
||
*/
|
||
async function updateSoldeAnticipe(conn, collaborateurId) {
|
||
const today = new Date();
|
||
today.setHours(0, 0, 0, 0);
|
||
const currentYear = today.getFullYear();
|
||
|
||
console.log(`🔄 Mise à jour soldes anticipés pour collaborateur ${collaborateurId}`);
|
||
|
||
const collab = await conn.query(`
|
||
SELECT DateEntree, TypeContrat, role
|
||
FROM CollaborateurAD
|
||
WHERE id = ?
|
||
`, [collaborateurId]);
|
||
|
||
if (collab.length === 0) {
|
||
console.log(`❌ Collaborateur non trouvé`);
|
||
return;
|
||
}
|
||
|
||
const dateEntree = collab[0].DateEntree;
|
||
const typeContrat = collab[0].TypeContrat || '37h';
|
||
const isApprenti = collab[0].role === 'Apprenti';
|
||
|
||
// ========================================
|
||
// CP ANTICIPÉ
|
||
// ========================================
|
||
const cpType = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1`, ['Congé payé']);
|
||
if (cpType.length > 0) {
|
||
const cpAnticipe = calculerAcquisitionCPAnticipee(today, dateEntree);
|
||
|
||
// Vérifier si le compteur existe
|
||
const compteurCP = await conn.query(`
|
||
SELECT Id FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collaborateurId, cpType[0].Id, currentYear]);
|
||
|
||
if (compteurCP.length > 0) {
|
||
await conn.query(`
|
||
UPDATE CompteurConges
|
||
SET SoldeAnticipe = ?,
|
||
DerniereMiseAJour = GETDATE()
|
||
WHERE Id = ?
|
||
`, [cpAnticipe, compteurCP[0].Id]);
|
||
} else {
|
||
// Créer le compteur s'il n'existe pas
|
||
const acquisCP = calculerAcquisitionCP_Smart(today, dateEntree);
|
||
await conn.query(`
|
||
INSERT INTO CompteurConges
|
||
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, SoldeAnticipe, DerniereMiseAJour)
|
||
VALUES (?, ?, ?, ?, ?, 0, ?, GETDATE())
|
||
`, [collaborateurId, cpType[0].Id, currentYear, acquisCP, acquisCP, cpAnticipe]);
|
||
}
|
||
console.log(` ✓ CP Anticipé: ${cpAnticipe.toFixed(2)}j`);
|
||
}
|
||
|
||
// ========================================
|
||
// RTT ANTICIPÉ
|
||
// ========================================
|
||
if (!isApprenti) {
|
||
const rttType = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1`, ['RTT']);
|
||
if (rttType.length > 0) {
|
||
const rttAnticipe = await calculerAcquisitionRTTAnticipee(conn, collaborateurId, today);
|
||
|
||
// Vérifier si le compteur existe
|
||
const compteurRTT = await conn.query(`
|
||
SELECT Id FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collaborateurId, rttType[0].Id, currentYear]);
|
||
|
||
if (compteurRTT.length > 0) {
|
||
await conn.query(`
|
||
UPDATE CompteurConges
|
||
SET SoldeAnticipe = ?,
|
||
DerniereMiseAJour = GETDATE()
|
||
WHERE Id = ?
|
||
`, [rttAnticipe, compteurRTT[0].Id]);
|
||
} else {
|
||
// Créer le compteur s'il n'existe pas
|
||
const rttData = await calculerAcquisitionRTT_Smart(conn, collaborateurId, today);
|
||
await conn.query(`
|
||
INSERT INTO CompteurConges
|
||
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, SoldeAnticipe, DerniereMiseAJour)
|
||
VALUES (?, ?, ?, ?, ?, 0, ?, GETDATE())
|
||
`, [collaborateurId, rttType[0].Id, currentYear, rttData.acquisition, rttData.acquisition, rttAnticipe]);
|
||
}
|
||
console.log(` ✓ RTT Anticipé: ${rttAnticipe.toFixed(2)}j`);
|
||
}
|
||
}
|
||
|
||
console.log(` ✅ Soldes anticipés mis à jour`);
|
||
}
|
||
|
||
|
||
/**
|
||
* GET /getSoldesAnticipes
|
||
* Retourne les soldes actuels ET anticipés pour un collaborateur
|
||
*/
|
||
app.get('/api/getSoldesAnticipes', async (req, res) => {
|
||
try {
|
||
const userIdParam = req.query.user_id;
|
||
const dateRefParam = req.query.date_reference;
|
||
|
||
if (!userIdParam) {
|
||
return res.json({ success: false, message: 'ID utilisateur manquant' });
|
||
}
|
||
|
||
const conn = await pool.getConnection();
|
||
|
||
// Déterminer l'ID
|
||
const isUUID = userIdParam.length > 10 && userIdParam.includes('-');
|
||
const userQuery = `
|
||
SELECT ca.id, ca.prenom, ca.nom, ca.DateEntree, ca.TypeContrat, ca.role
|
||
FROM CollaborateurAD ca
|
||
WHERE ${isUUID ? 'ca.entraUserId' : 'ca.id'} = ?
|
||
AND (ca.Actif = 1 OR ca.Actif IS NULL)
|
||
`;
|
||
|
||
const [userInfo] = await conn.query(userQuery, [userIdParam]);
|
||
|
||
if (userInfo.length === 0) {
|
||
conn.release();
|
||
return res.json({ success: false, message: 'Utilisateur non trouvé' });
|
||
}
|
||
|
||
const user = userInfo[0];
|
||
const userId = user.id;
|
||
const dateEntree = user.DateEntree;
|
||
const typeContrat = user.TypeContrat || '37h';
|
||
const isApprenti = user.role === 'Apprenti';
|
||
|
||
const dateReference = dateRefParam ? new Date(dateRefParam) : new Date();
|
||
dateReference.setHours(0, 0, 0, 0);
|
||
const currentYear = dateReference.getFullYear();
|
||
|
||
// ===== CP =====
|
||
const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']);
|
||
|
||
let cpData = {
|
||
acquis: 0,
|
||
solde: 0,
|
||
anticipe: 0,
|
||
totalDisponible: 0
|
||
};
|
||
|
||
if (cpType.length > 0) {
|
||
// Acquisition actuelle
|
||
const acquisCP = calculerAcquisitionCP_Smart(dateReference, dateEntree);
|
||
|
||
// Anticipé
|
||
const anticipeCP = calculerAcquisitionCPAnticipee(dateReference, dateEntree);
|
||
|
||
// Solde en base (avec consommations déduites)
|
||
const [compteurCP] = await conn.query(`
|
||
SELECT Solde, SoldeReporte, SoldeAnticipe
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [userId, cpType[0].Id, currentYear]);
|
||
|
||
// Reporté N-1
|
||
const [compteurCPN1] = await conn.query(`
|
||
SELECT Solde as SoldeReporte
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [userId, cpType[0].Id, currentYear - 1]);
|
||
|
||
const soldeN1 = compteurCPN1.length > 0 ? parseFloat(compteurCPN1[0].SoldeReporte || 0) : 0;
|
||
const soldeN = compteurCP.length > 0 ? parseFloat(compteurCP[0].Solde || 0) : acquisCP;
|
||
|
||
cpData = {
|
||
acquis: parseFloat(acquisCP.toFixed(2)),
|
||
soldeN1: parseFloat(soldeN1.toFixed(2)),
|
||
soldeN: parseFloat((soldeN - soldeN1).toFixed(2)),
|
||
soldeTotal: parseFloat(soldeN.toFixed(2)),
|
||
anticipe: parseFloat(anticipeCP.toFixed(2)),
|
||
totalDisponible: parseFloat((soldeN + anticipeCP).toFixed(2))
|
||
};
|
||
}
|
||
|
||
// ===== RTT =====
|
||
let rttData = {
|
||
acquis: 0,
|
||
solde: 0,
|
||
anticipe: 0,
|
||
totalDisponible: 0,
|
||
isApprenti: isApprenti
|
||
};
|
||
|
||
if (!isApprenti) {
|
||
const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']);
|
||
|
||
if (rttType.length > 0) {
|
||
// Acquisition actuelle
|
||
const rttCalc = await calculerAcquisitionRTT_Smart(conn, userId, dateReference);
|
||
const acquisRTT = rttCalc.acquisition;
|
||
|
||
// Anticipé
|
||
const anticipeRTT = await calculerAcquisitionRTTAnticipee(conn, userId, dateReference);
|
||
|
||
// Solde en base
|
||
const [compteurRTT] = await conn.query(`
|
||
SELECT Solde, SoldeAnticipe
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [userId, rttType[0].Id, currentYear]);
|
||
|
||
const soldeRTT = compteurRTT.length > 0 ? parseFloat(compteurRTT[0].Solde || 0) : acquisRTT;
|
||
|
||
rttData = {
|
||
acquis: parseFloat(acquisRTT.toFixed(2)),
|
||
solde: parseFloat(soldeRTT.toFixed(2)),
|
||
anticipe: parseFloat(anticipeRTT.toFixed(2)),
|
||
totalDisponible: parseFloat((soldeRTT + anticipeRTT).toFixed(2)),
|
||
config: rttCalc.config,
|
||
typeContrat: typeContrat
|
||
};
|
||
}
|
||
}
|
||
|
||
// ===== RÉCUP =====
|
||
const [recupType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Récupération']);
|
||
let recupData = { solde: 0 };
|
||
|
||
if (recupType.length > 0) {
|
||
const [compteurRecup] = await conn.query(`
|
||
SELECT Solde FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [userId, recupType[0].Id, currentYear]);
|
||
|
||
recupData.solde = compteurRecup.length > 0 ? parseFloat(compteurRecup[0].Solde || 0) : 0;
|
||
}
|
||
|
||
conn.release();
|
||
|
||
res.json({
|
||
success: true,
|
||
user: {
|
||
id: user.id,
|
||
nom: `${user.prenom} ${user.nom}`,
|
||
typeContrat: typeContrat,
|
||
dateEntree: dateEntree ? formatDateWithoutUTC(dateEntree) : null
|
||
},
|
||
dateReference: dateReference.toISOString().split('T')[0],
|
||
cp: cpData,
|
||
rtt: rttData,
|
||
recup: recupData,
|
||
totalGeneral: {
|
||
disponibleActuel: parseFloat((cpData.soldeTotal + rttData.solde + recupData.solde).toFixed(2)),
|
||
disponibleAvecAnticipe: parseFloat((cpData.totalDisponible + rttData.totalDisponible + recupData.solde).toFixed(2))
|
||
}
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('Erreur getSoldesAnticipes:', error);
|
||
res.status(500).json({ success: false, message: 'Erreur serveur', error: error.message });
|
||
}
|
||
});
|
||
/**
|
||
* GET /getCongesAnticipes
|
||
* Calcule les congés anticipés disponibles pour un collaborateur
|
||
*/
|
||
app.get('/api/getCongesAnticipes', async (req, res) => {
|
||
try {
|
||
const userIdParam = req.query.user_id;
|
||
|
||
if (!userIdParam) {
|
||
return res.json({ success: false, message: 'ID utilisateur manquant' });
|
||
}
|
||
|
||
const conn = await pool.getConnection();
|
||
|
||
// Déterminer l'ID (UUID ou numérique)
|
||
const isUUID = userIdParam.length > 10 && userIdParam.includes('-');
|
||
|
||
const userQuery = `
|
||
SELECT
|
||
ca.id,
|
||
ca.prenom,
|
||
ca.nom,
|
||
ca.email,
|
||
ca.DateEntree,
|
||
ca.TypeContrat,
|
||
ca.role,
|
||
ca.CampusId
|
||
FROM CollaborateurAD ca
|
||
WHERE ${isUUID ? 'ca.entraUserId' : 'ca.id'} = ?
|
||
AND (ca.Actif = 1 OR ca.Actif IS NULL)
|
||
`;
|
||
|
||
const [userInfo] = await conn.query(userQuery, [userIdParam]);
|
||
|
||
if (userInfo.length === 0) {
|
||
conn.release();
|
||
return res.json({ success: false, message: 'Utilisateur non trouvé' });
|
||
}
|
||
|
||
const user = userInfo[0];
|
||
const userId = user.id;
|
||
const dateEntree = user.DateEntree;
|
||
const typeContrat = user.TypeContrat || '37h';
|
||
const today = new Date();
|
||
const currentYear = today.getFullYear();
|
||
const finAnnee = new Date(currentYear, 11, 31); // 31 décembre
|
||
|
||
// ========================================
|
||
// CALCUL CP (Congés Payés)
|
||
// ========================================
|
||
|
||
// Acquisition actuelle
|
||
const acquisActuelleCP = calculerAcquisitionCP_Smart(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_Smart(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_Smart(conn, userId, today);
|
||
acquisActuelleRTT = rttDataActuel.acquisition;
|
||
|
||
// Acquisition prévue à la fin de l'année
|
||
const rttDataTotal = await calculerAcquisitionRTT_Smart(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();
|
||
|
||
res.json({
|
||
success: true,
|
||
user: {
|
||
id: user.id,
|
||
nom: `${user.prenom} ${user.nom}`,
|
||
email: user.email,
|
||
typeContrat: typeContrat,
|
||
dateEntree: dateEntree ? formatDateWithoutUTC(dateEntree) : null
|
||
},
|
||
dateReference: today.toISOString().split('T')[0],
|
||
congesPayes: {
|
||
acquisActuelle: parseFloat(acquisActuelleCP.toFixed(2)),
|
||
acquisTotalePrevu: parseFloat(acquisTotaleCP.toFixed(2)),
|
||
acquisRestante: parseFloat((acquisTotaleCP - acquisActuelleCP).toFixed(2)),
|
||
soldeActuel: parseFloat(soldeActuelCP.toFixed(2)),
|
||
dejaPris: parseFloat(dejaPrisCP.toFixed(2)),
|
||
anticipePossible: parseFloat(anticipePossibleCP.toFixed(2)),
|
||
limiteAnticipe: parseFloat(limiteAnticipeCP.toFixed(2)),
|
||
totalDisponible: parseFloat((soldeActuelCP + limiteAnticipeCP).toFixed(2)),
|
||
message: limiteAnticipeCP > 0
|
||
? `Vous pouvez poser jusqu'à ${limiteAnticipeCP.toFixed(1)} jours de CP en anticipé`
|
||
: "Vous avez atteint la limite d'anticipation pour les CP"
|
||
},
|
||
rtt: user.role !== 'Apprenti' ? {
|
||
acquisActuelle: parseFloat(acquisActuelleRTT.toFixed(2)),
|
||
acquisTotalePrevu: parseFloat(acquisTotaleRTT.toFixed(2)),
|
||
acquisRestante: parseFloat((acquisTotaleRTT - acquisActuelleRTT).toFixed(2)),
|
||
soldeActuel: parseFloat(soldeActuelRTT.toFixed(2)),
|
||
dejaPris: parseFloat(dejaPrisRTT.toFixed(2)),
|
||
anticipePossible: parseFloat(anticipePossibleRTT.toFixed(2)),
|
||
limiteAnticipe: parseFloat(limiteAnticipeRTT.toFixed(2)),
|
||
totalDisponible: parseFloat((soldeActuelRTT + limiteAnticipeRTT).toFixed(2)),
|
||
message: limiteAnticipeRTT > 0
|
||
? `Vous pouvez poser jusqu'à ${limiteAnticipeRTT.toFixed(1)} jours de RTT en anticipé`
|
||
: "Vous avez atteint la limite d'anticipation pour les RTT"
|
||
} : null,
|
||
regles: {
|
||
cpMaxAnnuel: 25,
|
||
rttMaxAnnuel: typeContrat === 'forfait_jour' ? 12 : 10,
|
||
description: "Les congés anticipés sont basés sur l'acquisition prévue jusqu'à la fin de l'exercice/année"
|
||
}
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('Erreur getCongesAnticipes:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur serveur',
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
/**
|
||
* Calcule l'acquisition CP ANTICIPÉE (ce qui reste à acquérir jusqu'à fin d'exercice)
|
||
*/
|
||
function calculerAcquisitionCPAnticipee(dateReference = new Date(), dateEntree = null) {
|
||
const d = new Date(dateReference);
|
||
d.setHours(0, 0, 0, 0);
|
||
|
||
const annee = d.getFullYear();
|
||
const mois = d.getMonth() + 1;
|
||
|
||
// 1️⃣ Déterminer la fin de l'exercice CP (31/05)
|
||
let finExercice;
|
||
if (mois >= 6) {
|
||
finExercice = new Date(annee + 1, 4, 31); // 31/05/N+1
|
||
} else {
|
||
finExercice = new Date(annee, 4, 31); // 31/05/N
|
||
}
|
||
finExercice.setHours(0, 0, 0, 0);
|
||
|
||
// 2️⃣ Calculer l'acquisition actuelle
|
||
const acquisActuelle = calculerAcquisitionCP_Smart(d, dateEntree);
|
||
|
||
// 3️⃣ Calculer l'acquisition totale à fin d'exercice
|
||
const acquisTotaleFinExercice = calculerAcquisitionCP_Smart(finExercice, dateEntree);
|
||
|
||
// 4️⃣ Anticipée = Totale - Actuelle (plafonnée à 25)
|
||
const acquisAnticipee = Math.min(25, acquisTotaleFinExercice) - acquisActuelle;
|
||
|
||
return Math.max(0, Math.round(acquisAnticipee * 100) / 100);
|
||
}
|
||
|
||
/**
|
||
* Calcule l'acquisition RTT ANTICIPÉE (ce qui reste à acquérir jusqu'à fin d'année)
|
||
*/
|
||
async function calculerAcquisitionRTTAnticipee(conn, collaborateurId, dateReference = new Date()) {
|
||
const d = new Date(dateReference);
|
||
d.setHours(0, 0, 0, 0);
|
||
const annee = d.getFullYear();
|
||
|
||
// 1️⃣ Récupérer les infos du collaborateur
|
||
const [collabInfo] = await conn.query(
|
||
`SELECT TypeContrat, DateEntree, role FROM CollaborateurAD WHERE id = ?`,
|
||
[collaborateurId]
|
||
);
|
||
|
||
if (collabInfo.length === 0) {
|
||
return 0;
|
||
}
|
||
|
||
const typeContrat = collabInfo[0].TypeContrat || '37h';
|
||
const isApprenti = collabInfo[0].role === 'Apprenti';
|
||
|
||
// 2️⃣ Apprentis = pas de RTT
|
||
if (isApprenti) {
|
||
return 0;
|
||
}
|
||
|
||
// 3️⃣ Récupérer la configuration RTT
|
||
const config = await getConfigurationRTT(conn, annee, typeContrat);
|
||
|
||
// 4️⃣ Calculer l'acquisition actuelle
|
||
const rttActuel = await calculerAcquisitionRTT_Smart(conn, collaborateurId, d);
|
||
const acquisActuelle = rttActuel.acquisition;
|
||
|
||
// 5️⃣ Calculer l'acquisition totale à fin d'année (31/12)
|
||
const finAnnee = new Date(annee, 11, 31);
|
||
finAnnee.setHours(0, 0, 0, 0);
|
||
|
||
const rttFinAnnee = await calculerAcquisitionRTT_Smart(conn, collaborateurId, finAnnee);
|
||
const acquisTotaleFinAnnee = rttFinAnnee.acquisition;
|
||
|
||
// 6️⃣ Anticipée = Totale - Actuelle (plafonnée au max annuel)
|
||
const acquisAnticipee = Math.min(config.joursAnnuels, acquisTotaleFinAnnee) - acquisActuelle;
|
||
|
||
return Math.max(0, Math.round(acquisAnticipee * 100) / 100);
|
||
}
|
||
|
||
app.get('/api/getStatistiquesCompteurs', async (req, res) => {
|
||
try {
|
||
const conn = await pool.getConnection();
|
||
const currentYear = new Date().getFullYear();
|
||
|
||
const [totalCollabs] = await conn.query(
|
||
'SELECT COUNT(*) as total FROM CollaborateurAD WHERE actif = 1 OR actif IS NULL'
|
||
);
|
||
|
||
const [statsTypeContrat] = await conn.query(`
|
||
SELECT
|
||
TypeContrat,
|
||
COUNT(*) as nombre,
|
||
GROUP_CONCAT(CONCAT(prenom, ' ', nom) SEPARATOR ', ') as noms
|
||
FROM CollaborateurAD
|
||
WHERE actif = 1 OR actif IS NULL
|
||
GROUP BY TypeContrat
|
||
`);
|
||
|
||
const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']);
|
||
let statsCP = { total_acquis: 0, total_solde: 0, moyenne_utilisation: 0 };
|
||
|
||
if (cpType.length > 0) {
|
||
const [cpStats] = await conn.query(`
|
||
SELECT
|
||
SUM(Total) as total_acquis,
|
||
SUM(Solde) as total_solde,
|
||
AVG(CASE WHEN Total > 0 THEN ((Total - Solde) / Total) * 100 ELSE 0 END) as moyenne_utilisation
|
||
FROM CompteurConges
|
||
WHERE TypeCongeId = ? AND Annee = ?
|
||
`, [cpType[0].Id, currentYear]);
|
||
|
||
if (cpStats.length > 0) {
|
||
statsCP = {
|
||
total_acquis: parseFloat((cpStats[0].total_acquis || 0).toFixed(2)),
|
||
total_solde: parseFloat((cpStats[0].total_solde || 0).toFixed(2)),
|
||
moyenne_utilisation: parseFloat((cpStats[0].moyenne_utilisation || 0).toFixed(1))
|
||
};
|
||
}
|
||
}
|
||
|
||
const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']);
|
||
let statsRTT = { total_acquis: 0, total_solde: 0, moyenne_utilisation: 0 };
|
||
|
||
if (rttType.length > 0) {
|
||
const [rttStats] = await conn.query(`
|
||
SELECT
|
||
SUM(Total) as total_acquis,
|
||
SUM(Solde) as total_solde,
|
||
AVG(CASE WHEN Total > 0 THEN ((Total - Solde) / Total) * 100 ELSE 0 END) as moyenne_utilisation
|
||
FROM CompteurConges
|
||
WHERE TypeCongeId = ? AND Annee = ?
|
||
`, [rttType[0].Id, currentYear]);
|
||
|
||
if (rttStats.length > 0) {
|
||
statsRTT = {
|
||
total_acquis: parseFloat((rttStats[0].total_acquis || 0).toFixed(2)),
|
||
total_solde: parseFloat((rttStats[0].total_solde || 0).toFixed(2)),
|
||
moyenne_utilisation: parseFloat((rttStats[0].moyenne_utilisation || 0).toFixed(1))
|
||
};
|
||
}
|
||
}
|
||
|
||
conn.release();
|
||
|
||
res.json({
|
||
success: true,
|
||
annee: currentYear,
|
||
statistiques: {
|
||
collaborateurs: {
|
||
total: totalCollabs[0].total,
|
||
par_type_contrat: statsTypeContrat
|
||
},
|
||
conges_payes: statsCP,
|
||
rtt: statsRTT
|
||
}
|
||
});
|
||
} catch (error) {
|
||
console.error('Erreur getStatistiquesCompteurs:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur serveur',
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
async function hasCompteRenduAccess(userId) {
|
||
try {
|
||
const conn = await pool.getConnection();
|
||
|
||
const [user] = await conn.query(`
|
||
SELECT TypeContrat, role
|
||
FROM CollaborateurAD
|
||
WHERE id = ?
|
||
`, [userId]);
|
||
|
||
conn.release();
|
||
|
||
if (!user.length) return false;
|
||
|
||
const userInfo = user[0];
|
||
|
||
// Accès si :
|
||
// 1. TypeContrat = 'forfait_jour'
|
||
// 2. role = 'Directeur Campus' ou 'Directrice Campus'
|
||
// 3. role = 'RH' ou 'Admin'
|
||
|
||
return (
|
||
userInfo.TypeContrat === 'forfait_jour' ||
|
||
userInfo.role === 'Directeur Campus' ||
|
||
userInfo.role === 'Directrice Campus' ||
|
||
userInfo.role === 'RH' ||
|
||
userInfo.role === 'Admin'
|
||
);
|
||
|
||
} catch (error) {
|
||
console.error('Erreur vérification accès:', error);
|
||
return false;
|
||
}
|
||
}
|
||
// Récupérer les jours du mois
|
||
// GET - Récupérer les données du compte-rendu
|
||
app.get('/api/getCompteRenduActivites', async (req, res) => {
|
||
const { user_id, annee, mois } = req.query;
|
||
|
||
try {
|
||
// Vérifier l'accès
|
||
const hasAccess = await hasCompteRenduAccess(user_id);
|
||
|
||
if (!hasAccess) {
|
||
return res.json({
|
||
success: false,
|
||
message: 'Accès réservé aux collaborateurs en forfait jour et aux directeurs de campus'
|
||
});
|
||
}
|
||
|
||
const conn = await pool.getConnection();
|
||
|
||
const [jours] = await conn.query(`
|
||
SELECT
|
||
id,
|
||
CollaborateurADId,
|
||
Annee,
|
||
Mois,
|
||
DATE_FORMAT(JourDate, '%Y-%m-%d') as JourDate,
|
||
JourTravaille,
|
||
ReposQuotidienRespect,
|
||
ReposHebdomadaireRespect,
|
||
CommentaireRepos,
|
||
Verrouille,
|
||
DateSaisie,
|
||
SaisiePar
|
||
FROM CompteRenduActivites
|
||
WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ?
|
||
ORDER BY JourDate
|
||
`, [user_id, annee, mois]);
|
||
|
||
console.log('🔍 Backend - Jours trouvés:', jours.length);
|
||
if (jours.length > 0) {
|
||
console.log('📅 Premier jour:', jours[0].JourDate, 'Type:', typeof jours[0].JourDate);
|
||
}
|
||
|
||
const [mensuel] = await conn.query(`
|
||
SELECT * FROM CompteRenduMensuel
|
||
WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ?
|
||
`, [user_id, annee, mois]);
|
||
|
||
conn.release();
|
||
|
||
res.json({
|
||
success: true,
|
||
jours: jours,
|
||
mensuel: mensuel[0] || null
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('Erreur getCompteRenduActivites:', error);
|
||
res.status(500).json({ success: false, message: error.message });
|
||
}
|
||
});
|
||
// POST - Sauvegarder un jour avec AUTO-VERROUILLAGE
|
||
app.post('/api/saveCompteRenduJour', async (req, res) => {
|
||
const { user_id, date, jour_travaille, repos_quotidien, repos_hebdo, commentaire, rh_override } = req.body;
|
||
|
||
try {
|
||
const conn = await pool.getConnection();
|
||
await conn.beginTransaction();
|
||
|
||
const dateJour = new Date(date);
|
||
const aujourdhui = new Date();
|
||
aujourdhui.setHours(0, 0, 0, 0);
|
||
dateJour.setHours(0, 0, 0, 0);
|
||
|
||
// Bloquer saisie du jour actuel (il faut attendre le lendemain)
|
||
if (dateJour >= aujourdhui) {
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.json({ success: false, message: 'Vous ne pouvez pas saisir le jour actuel. Veuillez attendre demain.' });
|
||
}
|
||
|
||
const annee = dateJour.getFullYear();
|
||
const mois = dateJour.getMonth() + 1;
|
||
|
||
// Vérifier si le JOUR est déjà verrouillé (pas le mois entier)
|
||
const [jourExistant] = await conn.query(
|
||
'SELECT Id, Verrouille FROM CompteRenduActivites WHERE CollaborateurADId = ? AND JourDate = ?',
|
||
[user_id, date]
|
||
);
|
||
|
||
if (jourExistant.length > 0 && 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' });
|
||
}
|
||
}
|
||
|
||
// ⭐ FIX: Utiliser IF EXISTS pattern au lieu de ON DUPLICATE KEY UPDATE
|
||
if (jourExistant.length > 0) {
|
||
// UPDATE
|
||
await conn.query(`
|
||
UPDATE CompteRenduActivites
|
||
SET JourTravaille = ?,
|
||
ReposQuotidienRespect = ?,
|
||
ReposHebdomadaireRespect = ?,
|
||
CommentaireRepos = ?,
|
||
SaisiePar = ?,
|
||
Verrouille = 1
|
||
WHERE CollaborateurADId = ? AND JourDate = ?
|
||
`, [jour_travaille, repos_quotidien, repos_hebdo, commentaire, user_id, user_id, date]);
|
||
} else {
|
||
// INSERT
|
||
await conn.query(`
|
||
INSERT INTO CompteRenduActivites
|
||
(CollaborateurADId, Annee, Mois, JourDate, JourTravaille,
|
||
ReposQuotidienRespect, ReposHebdomadaireRespect, CommentaireRepos,
|
||
DateSaisie, SaisiePar, Verrouille)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, GETDATE(), ?, 1)
|
||
`, [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 = 0 THEN 1 ELSE 0 END) as nbNonRespectQuotidien,
|
||
SUM(CASE WHEN ReposHebdomadaireRespect = 0 THEN 1 ELSE 0 END) as nbNonRespectHebdo
|
||
FROM CompteRenduActivites
|
||
WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ?
|
||
AND JourTravaille = 1
|
||
`, [user_id, annee, mois]);
|
||
|
||
// ⭐ FIX: Vérifier si le mensuel existe
|
||
const [mensuelExistant] = await conn.query(`
|
||
SELECT Id FROM CompteRenduMensuel
|
||
WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ?
|
||
`, [user_id, annee, mois]);
|
||
|
||
if (mensuelExistant.length > 0) {
|
||
await conn.query(`
|
||
UPDATE CompteRenduMensuel
|
||
SET NbJoursTravailles = ?,
|
||
NbJoursNonRespectsReposQuotidien = ?,
|
||
NbJoursNonRespectsReposHebdo = ?,
|
||
DateValidation = GETDATE()
|
||
WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ?
|
||
`, [stats[0].nbJours, stats[0].nbNonRespectQuotidien, stats[0].nbNonRespectHebdo, user_id, annee, mois]);
|
||
} else {
|
||
await conn.query(`
|
||
INSERT INTO CompteRenduMensuel
|
||
(CollaborateurADId, Annee, Mois, NbJoursTravailles,
|
||
NbJoursNonRespectsReposQuotidien, NbJoursNonRespectsReposHebdo,
|
||
Statut, DateValidation)
|
||
VALUES (?, ?, ?, ?, ?, ?, 'En cours', GETDATE())
|
||
`, [user_id, annee, mois, stats[0].nbJours, stats[0].nbNonRespectQuotidien, stats[0].nbNonRespectHebdo]);
|
||
}
|
||
|
||
await conn.commit();
|
||
conn.release();
|
||
|
||
res.json({
|
||
success: true,
|
||
message: 'Jour enregistré et verrouillé',
|
||
verrouille: true
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('❌ Erreur saveCompteRenduJour:', error);
|
||
res.status(500).json({ success: false, message: error.message });
|
||
}
|
||
});
|
||
|
||
// POST - Saisie en masse avec AUTO-VERROUILLAGE
|
||
app.post('/api/saveCompteRenduMasse', async (req, res) => {
|
||
const { user_id, annee, mois, jours, rh_override } = req.body;
|
||
|
||
try {
|
||
const conn = await pool.getConnection();
|
||
await conn.beginTransaction();
|
||
|
||
let count = 0;
|
||
let blocked = 0;
|
||
|
||
for (const jour of jours) {
|
||
const dateJour = new Date(jour.date);
|
||
const aujourdhui = new Date();
|
||
aujourdhui.setHours(0, 0, 0, 0);
|
||
dateJour.setHours(0, 0, 0, 0);
|
||
|
||
// Bloquer le jour actuel
|
||
if (dateJour >= aujourdhui) {
|
||
blocked++;
|
||
continue;
|
||
}
|
||
|
||
// Vérifier si déjà verrouillé
|
||
const [jourExistant] = await conn.query(
|
||
'SELECT Id, Verrouille FROM CompteRenduActivites WHERE CollaborateurADId = ? AND JourDate = ?',
|
||
[user_id, jour.date]
|
||
);
|
||
|
||
if (jourExistant.length > 0 && jourExistant[0].Verrouille && !rh_override) {
|
||
blocked++;
|
||
continue;
|
||
}
|
||
|
||
// ⭐ FIX: Utiliser IF EXISTS pattern au lieu de ON DUPLICATE KEY UPDATE
|
||
if (jourExistant.length > 0) {
|
||
// UPDATE
|
||
await conn.query(`
|
||
UPDATE CompteRenduActivites
|
||
SET JourTravaille = ?,
|
||
ReposQuotidienRespect = ?,
|
||
ReposHebdomadaireRespect = ?,
|
||
CommentaireRepos = ?,
|
||
SaisiePar = ?,
|
||
Verrouille = 1
|
||
WHERE CollaborateurADId = ? AND JourDate = ?
|
||
`, [jour.jour_travaille, jour.repos_quotidien, jour.repos_hebdo,
|
||
jour.commentaire || null, user_id, user_id, jour.date]);
|
||
} else {
|
||
// INSERT
|
||
await conn.query(`
|
||
INSERT INTO CompteRenduActivites
|
||
(CollaborateurADId, Annee, Mois, JourDate, JourTravaille,
|
||
ReposQuotidienRespect, ReposHebdomadaireRespect, CommentaireRepos,
|
||
DateSaisie, SaisiePar, Verrouille)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, GETDATE(), ?, 1)
|
||
`, [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 = 0 THEN 1 ELSE 0 END) as nbNonRespectQuotidien,
|
||
SUM(CASE WHEN ReposHebdomadaireRespect = 0 THEN 1 ELSE 0 END) as nbNonRespectHebdo
|
||
FROM CompteRenduActivites
|
||
WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ?
|
||
AND JourTravaille = 1
|
||
`, [user_id, annee, mois]);
|
||
|
||
// ⭐ FIX: Vérifier si le mensuel existe
|
||
const [mensuelExistant] = await conn.query(`
|
||
SELECT Id FROM CompteRenduMensuel
|
||
WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ?
|
||
`, [user_id, annee, mois]);
|
||
|
||
if (mensuelExistant.length > 0) {
|
||
await conn.query(`
|
||
UPDATE CompteRenduMensuel
|
||
SET NbJoursTravailles = ?,
|
||
NbJoursNonRespectsReposQuotidien = ?,
|
||
NbJoursNonRespectsReposHebdo = ?,
|
||
DateValidation = GETDATE()
|
||
WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ?
|
||
`, [stats[0].nbJours, stats[0].nbNonRespectQuotidien, stats[0].nbNonRespectHebdo, user_id, annee, mois]);
|
||
} else {
|
||
await conn.query(`
|
||
INSERT INTO CompteRenduMensuel
|
||
(CollaborateurADId, Annee, Mois, NbJoursTravailles,
|
||
NbJoursNonRespectsReposQuotidien, NbJoursNonRespectsReposHebdo,
|
||
Statut, DateValidation)
|
||
VALUES (?, ?, ?, ?, ?, ?, 'En cours', GETDATE())
|
||
`, [user_id, annee, mois, stats[0].nbJours, stats[0].nbNonRespectQuotidien, stats[0].nbNonRespectHebdo]);
|
||
}
|
||
|
||
await conn.commit();
|
||
conn.release();
|
||
|
||
res.json({
|
||
success: true,
|
||
count: count,
|
||
blocked: blocked,
|
||
message: `${count} jours enregistrés${blocked > 0 ? `, ${blocked} ignorés (jour actuel ou déjà verrouillés)` : ''}`
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('❌ Erreur saisie masse:', error);
|
||
res.status(500).json({ success: false, message: error.message });
|
||
}
|
||
});
|
||
|
||
app.post('/api/deverrouillerJour', async (req, res) => {
|
||
const { user_id, date, rh_user_id } = req.body;
|
||
|
||
try {
|
||
const conn = await pool.getConnection();
|
||
|
||
const [rhUser] = await conn.query(
|
||
'SELECT role FROM CollaborateurAD WHERE id = ?',
|
||
[rh_user_id]
|
||
);
|
||
|
||
if (!rhUser.length || (rhUser[0].role !== 'RH' && rhUser[0].role !== 'Admin')) {
|
||
conn.release();
|
||
return res.json({ success: false, message: 'Action réservée aux RH' });
|
||
}
|
||
|
||
await conn.query(`
|
||
UPDATE CompteRenduActivites
|
||
SET Verrouille = FALSE
|
||
WHERE CollaborateurADId = ? AND JourDate = ?
|
||
`, [user_id, date]);
|
||
|
||
conn.release();
|
||
|
||
res.json({ success: true });
|
||
|
||
} catch (error) {
|
||
console.error('❌ Erreur déverrouillage jour:', error);
|
||
res.status(500).json({ success: false, message: error.message });
|
||
}
|
||
});
|
||
|
||
|
||
// POST - Verrouiller (RH uniquement)
|
||
app.post('/api/verrouillerCompteRendu', async (req, res) => {
|
||
const { user_id, annee, mois, rh_user_id } = req.body;
|
||
|
||
try {
|
||
const conn = await pool.getConnection();
|
||
|
||
// Vérifier que l'utilisateur est RH
|
||
const [rhUser] = await conn.query(
|
||
'SELECT role FROM CollaborateurAD WHERE id = ?',
|
||
[rh_user_id]
|
||
);
|
||
|
||
if (!rhUser.length || (rhUser[0].role !== 'RH' && rhUser[0].role !== 'Admin')) {
|
||
conn.release();
|
||
return res.json({ success: false, message: 'Action réservée aux RH' });
|
||
}
|
||
|
||
await conn.query(`
|
||
UPDATE CompteRenduMensuel
|
||
SET Verrouille = TRUE,
|
||
DateModification = GETDATE()
|
||
WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ?
|
||
`, [user_id, annee, mois]);
|
||
|
||
conn.release();
|
||
|
||
res.json({ success: true });
|
||
|
||
} catch (error) {
|
||
console.error('Erreur verrouillage:', error);
|
||
res.status(500).json({ success: false, message: error.message });
|
||
}
|
||
});
|
||
|
||
// POST - Déverrouiller (RH uniquement)
|
||
app.post('/api/deverrouillerCompteRendu', async (req, res) => {
|
||
const { user_id, annee, mois, rh_user_id } = req.body;
|
||
|
||
try {
|
||
const conn = await pool.getConnection();
|
||
|
||
// Vérifier que l'utilisateur est RH
|
||
const [rhUser] = await conn.query(
|
||
'SELECT role FROM CollaborateurAD WHERE id = ?',
|
||
[rh_user_id]
|
||
);
|
||
|
||
if (!rhUser.length || (rhUser[0].role !== 'RH' && rhUser[0].role !== 'Admin')) {
|
||
conn.release();
|
||
return res.json({ success: false, message: 'Action réservée aux RH' });
|
||
}
|
||
|
||
await conn.query(`
|
||
UPDATE CompteRenduMensuel
|
||
SET Verrouille = FALSE,
|
||
DateDeverrouillage = GETDATE(),
|
||
DeverrouillePar = ?
|
||
WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ?
|
||
`, [rh_user_id, user_id, annee, mois]);
|
||
|
||
conn.release();
|
||
|
||
res.json({ success: true });
|
||
|
||
} catch (error) {
|
||
console.error('Erreur déverrouillage:', error);
|
||
res.status(500).json({ success: false, message: error.message });
|
||
}
|
||
});
|
||
|
||
// GET - Stats annuelles
|
||
app.get('/api/getStatsAnnuelles', async (req, res) => {
|
||
const { user_id, annee } = req.query;
|
||
|
||
try {
|
||
const conn = await pool.getConnection();
|
||
|
||
const [stats] = await conn.query(`
|
||
SELECT
|
||
SUM(NbJoursTravailles) as totalJoursTravailles,
|
||
SUM(NbJoursNonRespectsReposQuotidien) as totalNonRespectQuotidien,
|
||
SUM(NbJoursNonRespectsReposHebdo) as totalNonRespectHebdo
|
||
FROM CompteRenduMensuel
|
||
WHERE CollaborateurADId = ? AND Annee = ?
|
||
`, [user_id, annee]);
|
||
|
||
conn.release();
|
||
|
||
res.json({
|
||
success: true,
|
||
stats: stats[0] || {
|
||
totalJoursTravailles: 0,
|
||
totalNonRespectQuotidien: 0,
|
||
totalNonRespectHebdo: 0
|
||
}
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('Erreur stats:', error);
|
||
res.status(500).json({ success: false, message: error.message });
|
||
}
|
||
});
|
||
|
||
// GET - Export PDF (RH uniquement)
|
||
app.get('/api/exportCompteRenduPDF', async (req, res) => {
|
||
const { user_id, annee, mois } = req.query;
|
||
|
||
try {
|
||
const conn = await pool.getConnection();
|
||
|
||
// Récupérer les données du collaborateur
|
||
const [collab] = await conn.query(
|
||
'SELECT prenom, nom, email FROM CollaborateurAD WHERE id = ?',
|
||
[user_id]
|
||
);
|
||
|
||
// Récupérer les jours du mois
|
||
const [jours] = await conn.query(`
|
||
SELECT * FROM CompteRenduActivites
|
||
WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ?
|
||
ORDER BY JourDate
|
||
`, [user_id, annee, mois]);
|
||
|
||
// Récupérer le mensuel
|
||
const [mensuel] = await conn.query(`
|
||
SELECT * FROM CompteRenduMensuel
|
||
WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ?
|
||
`, [user_id, annee, mois]);
|
||
|
||
conn.release();
|
||
|
||
// TODO: Générer le PDF avec une bibliothèque comme pdfkit ou puppeteer
|
||
// Pour l'instant, retourner les données JSON
|
||
res.json({
|
||
success: true,
|
||
collaborateur: collab[0],
|
||
jours: jours,
|
||
mensuel: mensuel[0],
|
||
message: 'Export PDF à implémenter'
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('Erreur export PDF:', error);
|
||
res.status(500).json({ success: false, message: error.message });
|
||
}
|
||
});
|
||
app.post('/api/addUserFromEntra', async (req, res) => {
|
||
try {
|
||
const { email } = req.body;
|
||
|
||
if (!email) {
|
||
return res.json({ success: false, message: 'Email requis' });
|
||
}
|
||
|
||
console.log(`\n🔍 Recherche utilisateur Entra ID: ${email}`);
|
||
|
||
// 1️⃣ Obtenir le token
|
||
const accessToken = await getGraphToken();
|
||
if (!accessToken) {
|
||
return res.json({ success: false, message: 'Impossible d\'obtenir le token Microsoft' });
|
||
}
|
||
|
||
// 2️⃣ Rechercher l'utilisateur dans Entra ID
|
||
const searchUrl = `https://graph.microsoft.com/v1.0/users/${encodeURIComponent(email)}?$select=id,givenName,surname,mail,userPrincipalName,department,jobTitle,accountEnabled`;
|
||
|
||
let userData;
|
||
try {
|
||
const response = await axios.get(searchUrl, {
|
||
headers: { Authorization: `Bearer ${accessToken}` }
|
||
});
|
||
userData = response.data;
|
||
} catch (error) {
|
||
return res.json({
|
||
success: false,
|
||
message: `Utilisateur ${email} non trouvé dans Entra ID`
|
||
});
|
||
}
|
||
|
||
// 3️⃣ Vérifier si compte actif
|
||
if (userData.accountEnabled === false) {
|
||
return res.json({
|
||
success: false,
|
||
message: 'Ce compte est désactivé dans Entra ID'
|
||
});
|
||
}
|
||
|
||
// 4️⃣ Vérifier s'il existe déjà en base
|
||
const conn = await pool.getConnection();
|
||
|
||
const [existing] = await conn.query(
|
||
'SELECT id, email FROM CollaborateurAD WHERE LOWER(email) = ?',
|
||
[email.toLowerCase()]
|
||
);
|
||
|
||
if (existing.length > 0) {
|
||
conn.release();
|
||
return res.json({
|
||
success: false,
|
||
message: 'Cet utilisateur existe déjà en base',
|
||
userId: existing[0].id
|
||
});
|
||
}
|
||
|
||
// 5️⃣ Insérer l'utilisateur (SANS DateEntree pour éviter GETDATE())
|
||
await conn.query(`
|
||
INSERT INTO CollaborateurAD
|
||
(entraUserId, prenom, nom, email, service, description, role, SocieteId, Actif, TypeContrat)
|
||
VALUES (?, ?, ?, ?, ?, ?, 'Collaborateur', 1, 1, '37h')
|
||
`, [
|
||
userData.id,
|
||
userData.givenName || 'Prénom',
|
||
userData.surname || 'Nom',
|
||
email.toLowerCase(),
|
||
userData.department || '',
|
||
userData.jobTitle || ''
|
||
]);
|
||
|
||
// 6️⃣ Récupérer l'utilisateur créé
|
||
const [newUser] = await conn.query(
|
||
'SELECT id, prenom, nom, email FROM CollaborateurAD WHERE LOWER(email) = ?',
|
||
[email.toLowerCase()]
|
||
);
|
||
|
||
conn.release();
|
||
|
||
console.log(`✅ Utilisateur créé: ${newUser[0].prenom} ${newUser[0].nom} (ID: ${newUser[0].id})`);
|
||
|
||
res.json({
|
||
success: true,
|
||
message: 'Utilisateur ajouté avec succès',
|
||
user: newUser[0]
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('❌ Erreur addUserFromEntra:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
async function syncEntraIdUsers() {
|
||
const syncResults = {
|
||
processed: 0,
|
||
inserted: 0,
|
||
updated: 0,
|
||
deactivated: 0,
|
||
errors: []
|
||
};
|
||
|
||
try {
|
||
console.log('\n🔄 === DÉBUT SYNCHRONISATION ENTRA ID ===');
|
||
|
||
// 1️⃣ Obtenir le token
|
||
const accessToken = await getGraphToken();
|
||
if (!accessToken) {
|
||
console.error('❌ Impossible d\'obtenir le token');
|
||
return syncResults;
|
||
}
|
||
console.log('✅ Token obtenu');
|
||
|
||
// 2️⃣ Récupérer le groupe
|
||
const groupResponse = await axios.get(
|
||
`https://graph.microsoft.com/v1.0/groups/${AZURE_CONFIG.groupId}?$select=id,displayName`,
|
||
{ headers: { Authorization: `Bearer ${accessToken}` } }
|
||
);
|
||
const groupName = groupResponse.data.displayName;
|
||
console.log(`📋 Groupe : ${groupName}`);
|
||
|
||
// 3️⃣ Récupérer tous les membres avec pagination
|
||
let allAzureMembers = [];
|
||
let nextLink = `https://graph.microsoft.com/v1.0/groups/${AZURE_CONFIG.groupId}/members?$select=id,givenName,surname,mail,department,jobTitle,officeLocation,accountEnabled&$top=999`;
|
||
|
||
console.log('📥 Récupération des membres...');
|
||
while (nextLink) {
|
||
const membersResponse = await axios.get(nextLink, {
|
||
headers: { Authorization: `Bearer ${accessToken}` }
|
||
});
|
||
allAzureMembers = allAzureMembers.concat(membersResponse.data.value);
|
||
nextLink = membersResponse.data['@odata.nextLink'];
|
||
|
||
if (nextLink) {
|
||
console.log(` 📄 ${allAzureMembers.length} membres récupérés...`);
|
||
}
|
||
}
|
||
|
||
console.log(`✅ ${allAzureMembers.length} membres trouvés`);
|
||
|
||
// 4️⃣ Filtrer les membres valides
|
||
const validMembers = allAzureMembers.filter(m => {
|
||
if (!m.mail || m.mail.trim() === '') return false;
|
||
if (m.accountEnabled === false) return false;
|
||
|
||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||
return emailRegex.test(m.mail);
|
||
});
|
||
|
||
console.log(`✅ ${validMembers.length} membres valides`);
|
||
|
||
// 5️⃣ Traitement avec transaction
|
||
const transaction = new sql.Transaction(pool);
|
||
await transaction.begin();
|
||
|
||
try {
|
||
const azureEmails = new Set();
|
||
validMembers.forEach(m => {
|
||
azureEmails.add(m.mail.toLowerCase().trim());
|
||
});
|
||
|
||
console.log('\n📝 Traitement des utilisateurs...');
|
||
|
||
// 6️⃣ Pour chaque membre
|
||
for (const m of validMembers) {
|
||
try {
|
||
const emailClean = m.mail.toLowerCase().trim();
|
||
syncResults.processed++;
|
||
|
||
// ✅ CORRIGÉ : Utiliser entraUserId et Actif
|
||
const request = new sql.Request(transaction);
|
||
request.input('email', sql.NVarChar, emailClean);
|
||
|
||
const result = await request.query(`
|
||
SELECT id, email, entraUserId, Actif
|
||
FROM CollaborateurAD
|
||
WHERE LOWER(email) = LOWER(@email)
|
||
`);
|
||
|
||
if (result.recordset.length > 0) {
|
||
// ✅ MISE À JOUR avec bonnes colonnes
|
||
const updateRequest = new sql.Request(transaction);
|
||
updateRequest.input('entraUserId', sql.NVarChar, m.id);
|
||
updateRequest.input('prenom', sql.NVarChar, m.givenName || '');
|
||
updateRequest.input('nom', sql.NVarChar, m.surname || '');
|
||
updateRequest.input('service', sql.NVarChar, m.department || '');
|
||
updateRequest.input('description', sql.NVarChar, m.jobTitle || '');
|
||
updateRequest.input('email', sql.NVarChar, emailClean);
|
||
|
||
await updateRequest.query(`
|
||
UPDATE CollaborateurAD
|
||
SET
|
||
entraUserId = @entraUserId,
|
||
prenom = @prenom,
|
||
nom = @nom,
|
||
service = @service,
|
||
description = @description,
|
||
Actif = 1
|
||
WHERE LOWER(email) = LOWER(@email)
|
||
`);
|
||
|
||
syncResults.updated++;
|
||
console.log(` ✓ Mis à jour : ${emailClean}`);
|
||
|
||
} else {
|
||
// ✅ INSERTION avec bonnes colonnes
|
||
const insertRequest = new sql.Request(transaction);
|
||
insertRequest.input('entraUserId', sql.NVarChar, m.id);
|
||
insertRequest.input('prenom', sql.NVarChar, m.givenName || '');
|
||
insertRequest.input('nom', sql.NVarChar, m.surname || '');
|
||
insertRequest.input('email', sql.NVarChar, emailClean);
|
||
insertRequest.input('service', sql.NVarChar, m.department || '');
|
||
insertRequest.input('description', sql.NVarChar, m.jobTitle || '');
|
||
|
||
await insertRequest.query(`
|
||
INSERT INTO CollaborateurAD
|
||
(entraUserId, prenom, nom, email, service, description, role, SocieteId, Actif, TypeContrat)
|
||
VALUES (@entraUserId, @prenom, @nom, @email, @service, @description, 'Collaborateur', 1, 1, '37h')
|
||
`);
|
||
|
||
syncResults.inserted++;
|
||
console.log(` ✓ Créé : ${emailClean}`);
|
||
}
|
||
|
||
} catch (userError) {
|
||
syncResults.errors.push({
|
||
email: m.mail,
|
||
error: userError.message
|
||
});
|
||
console.error(` ❌ Erreur ${m.mail}:`, userError.message);
|
||
}
|
||
}
|
||
|
||
// 7️⃣ ✅ DÉSACTIVATION avec bonne colonne (Actif, pas dateMiseAJour)
|
||
console.log('\n🔍 Désactivation des comptes obsolètes...');
|
||
|
||
if (azureEmails.size > 0) {
|
||
const activeEmailsList = Array.from(azureEmails).map(e => `'${e}'`).join(',');
|
||
|
||
const deactivateRequest = new sql.Request(transaction);
|
||
const deactivateResult = await deactivateRequest.query(`
|
||
UPDATE CollaborateurAD
|
||
SET Actif = 0
|
||
WHERE
|
||
email IS NOT NULL
|
||
AND email != ''
|
||
AND LOWER(email) NOT IN (${activeEmailsList})
|
||
AND Actif = 1
|
||
`);
|
||
|
||
syncResults.deactivated = deactivateResult.rowsAffected[0];
|
||
console.log(` ✓ ${syncResults.deactivated} compte(s) désactivé(s)`);
|
||
}
|
||
|
||
await transaction.commit();
|
||
|
||
console.log('\n📊 === RÉSUMÉ ===');
|
||
console.log(` Groupe: ${groupName}`);
|
||
console.log(` Total Entra: ${allAzureMembers.length}`);
|
||
console.log(` Valides: ${validMembers.length}`);
|
||
console.log(` Traités: ${syncResults.processed}`);
|
||
console.log(` Créés: ${syncResults.inserted}`);
|
||
console.log(` Mis à jour: ${syncResults.updated}`);
|
||
console.log(` Désactivés: ${syncResults.deactivated}`);
|
||
console.log(` Erreurs: ${syncResults.errors.length}`);
|
||
|
||
} catch (error) {
|
||
await transaction.rollback();
|
||
throw error;
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('\n❌ ERREUR SYNCHRONISATION:', error.message);
|
||
}
|
||
|
||
return syncResults;
|
||
}
|
||
app.post('/api/sync-all', async (req, res) => {
|
||
try {
|
||
console.log('🚀 Sync complète manuelle...');
|
||
const results = await syncEntraIdUsers();
|
||
|
||
res.json({
|
||
success: true,
|
||
message: 'Sync terminée',
|
||
stats: results
|
||
});
|
||
} catch (error) {
|
||
res.status(500).json({
|
||
success: false,
|
||
message: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
// Route diagnostic
|
||
app.get('/api/diagnostic-sync', async (req, res) => {
|
||
try {
|
||
const totalDB = await pool.query(
|
||
'SELECT COUNT(*) as total, SUM(CASE WHEN actif = 1 THEN 1 ELSE 0 END) as actifs FROM CollaborateurAD',
|
||
[]
|
||
);
|
||
|
||
const sansEmail = await pool.query(
|
||
'SELECT COUNT(*) as total FROM CollaborateurAD WHERE email IS NULL OR email = \'\'',
|
||
[]
|
||
);
|
||
|
||
const derniers = await pool.query(
|
||
'SELECT TOP 10 id, prenom, nom, email, CollaborateurADId, actif FROM CollaborateurAD ORDER BY id DESC',
|
||
[]
|
||
);
|
||
|
||
// Test Entra
|
||
let entraStatus = { connected: false };
|
||
try {
|
||
const token = await getGraphToken();
|
||
if (token) {
|
||
const groupResponse = await axios.get(
|
||
`https://graph.microsoft.com/v1.0/groups/${AZURE_CONFIG.groupId}?$select=id,displayName`,
|
||
{ headers: { Authorization: `Bearer ${token}` } }
|
||
);
|
||
entraStatus = {
|
||
connected: true,
|
||
groupName: groupResponse.data.displayName
|
||
};
|
||
}
|
||
} catch (err) {
|
||
entraStatus.error = err.message;
|
||
}
|
||
|
||
res.json({
|
||
success: true,
|
||
database: {
|
||
total: totalDB[0]?.total || 0,
|
||
actifs: totalDB[0]?.actifs || 0,
|
||
sansEmail: sansEmail[0]?.total || 0
|
||
},
|
||
entraId: entraStatus,
|
||
derniers_utilisateurs: derniers
|
||
});
|
||
|
||
} catch (error) {
|
||
res.status(500).json({
|
||
success: false,
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
// GET - Compteur de demandes en attente pour le badge
|
||
app.get('/api/getPendingRequestsCount', async (req, res) => {
|
||
try {
|
||
const userId = req.query.user_id;
|
||
const userRole = req.query.role;
|
||
const userEmail = req.query.email;
|
||
|
||
if (!userId) {
|
||
return res.json({ success: false, message: 'ID utilisateur manquant' });
|
||
}
|
||
|
||
const conn = await pool.getConnection();
|
||
|
||
let count = 0;
|
||
|
||
// Normaliser le rôle
|
||
const role = normalizeRole(userRole);
|
||
|
||
console.log(`🔍 Comptage demandes pour: ${userEmail}, Role: ${role}`);
|
||
|
||
if (role === 'rh' || role === 'admin' || role === 'president' || role === 'directeur de campus') {
|
||
// Pour RH/Admin/President/Directeur : toutes les demandes en attente de leur périmètre
|
||
|
||
const [userInfo] = await conn.query(
|
||
'SELECT ServiceId, CampusId, SocieteId FROM CollaborateurAD WHERE id = ?',
|
||
[userId]
|
||
);
|
||
|
||
if (userInfo.length === 0) {
|
||
conn.release();
|
||
return res.json({ success: false, message: 'Utilisateur non trouvé' });
|
||
}
|
||
|
||
const serviceId = userInfo[0].ServiceId;
|
||
const campusId = userInfo[0].CampusId;
|
||
|
||
// ⭐ Vérifier accès transversal
|
||
const accesTransversal = getUserAccesTransversal(userEmail);
|
||
|
||
if (accesTransversal) {
|
||
// Accès service multi-campus
|
||
const [result] = await conn.query(`
|
||
SELECT COUNT(DISTINCT dc.Id) as count
|
||
FROM DemandeConge dc
|
||
JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id
|
||
JOIN Services s ON ca.ServiceId = s.Id
|
||
WHERE dc.Statut = 'En attente'
|
||
AND s.Nom = ?
|
||
`, [accesTransversal.serviceNom]);
|
||
|
||
count = result[0].count;
|
||
|
||
} else if (role === 'directeur de campus') {
|
||
// Directeur de campus : toutes les demandes de son campus
|
||
const [result] = await conn.query(`
|
||
SELECT COUNT(DISTINCT dc.Id) as count
|
||
FROM DemandeConge dc
|
||
JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id
|
||
WHERE dc.Statut = 'En attente'
|
||
AND ca.CampusId = ?
|
||
`, [campusId]);
|
||
|
||
count = result[0].count;
|
||
|
||
} else {
|
||
// RH : son service sur son campus uniquement
|
||
const [result] = await conn.query(`
|
||
SELECT COUNT(DISTINCT dc.Id) as count
|
||
FROM DemandeConge dc
|
||
JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id
|
||
WHERE dc.Statut = 'En attente'
|
||
AND ca.ServiceId = ?
|
||
AND ca.CampusId = ?
|
||
`, [serviceId, campusId]);
|
||
|
||
count = result[0].count;
|
||
}
|
||
|
||
} else if (role === 'validateur' || role === 'validatrice') {
|
||
// Pour validateurs : demandes de leur service
|
||
const [userInfo] = await conn.query(
|
||
'SELECT ServiceId, CampusId FROM CollaborateurAD WHERE id = ?',
|
||
[userId]
|
||
);
|
||
|
||
if (userInfo.length === 0) {
|
||
conn.release();
|
||
return res.json({ success: false, message: 'Utilisateur non trouvé' });
|
||
}
|
||
|
||
const serviceId = userInfo[0].ServiceId;
|
||
const campusId = userInfo[0].CampusId;
|
||
|
||
const [result] = await conn.query(`
|
||
SELECT COUNT(DISTINCT dc.Id) as count
|
||
FROM DemandeConge dc
|
||
JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id
|
||
WHERE dc.Statut = 'En attente'
|
||
AND ca.ServiceId = ?
|
||
AND ca.CampusId = ?
|
||
AND ca.id != ?
|
||
`, [serviceId, campusId, userId]);
|
||
|
||
count = result[0].count;
|
||
}
|
||
|
||
conn.release();
|
||
|
||
console.log(`✅ Nombre de demandes en attente: ${count}`);
|
||
|
||
res.json({
|
||
success: true,
|
||
count: count,
|
||
role: role
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('❌ Erreur getPendingRequestsCount:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur serveur',
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
app.listen(PORT, "0.0.0.0", async () => {
|
||
console.log("✅ ==========================================");
|
||
console.log(" SERVEUR PRINCIPAL DÉMARRÉ");
|
||
console.log(" Port:", PORT);
|
||
console.log(` Base: ${dbConfig.database}@${dbConfig.server}`);
|
||
console.log("==========================================");
|
||
// ⚡ Synchronisation Entra ID au démarrage (après 5 secondes)
|
||
setTimeout(async () => {
|
||
console.log("🚀 Lancement synchronisation Entra ID...");
|
||
await syncEntraIdUsers();
|
||
}, 5000);
|
||
}); |