4263 lines
165 KiB
JavaScript
4263 lines
165 KiB
JavaScript
console.log('🚀 1. Démarrage du script...');
|
||
|
||
const express = require('express');
|
||
const cors = require('cors');
|
||
const mysql = require('mysql2/promise');
|
||
const jwt = require('jsonwebtoken');
|
||
const PDFDocument = require('pdfkit');
|
||
|
||
const { ConfidentialClientApplication } = require('@azure/msal-node');
|
||
|
||
console.log('✅ 2. Modules de base chargés');
|
||
|
||
require('dotenv').config();
|
||
console.log('✅ 3. Dotenv chargé');
|
||
|
||
// HANDLERS D'ERREURS
|
||
process.on('uncaughtException', (error) => {
|
||
console.error('\n❌❌❌ ERREUR NON CAPTURÉE ❌❌❌');
|
||
console.error(error);
|
||
console.error(error.stack);
|
||
});
|
||
|
||
process.on('unhandledRejection', (reason, promise) => {
|
||
console.error('\n❌❌❌ PROMESSE REJETÉE ❌❌❌');
|
||
console.error('Raison:', reason);
|
||
console.error('Promise:', promise);
|
||
});
|
||
|
||
process.on('exit', (code) => {
|
||
console.log(`\n⚠️⚠️⚠️ PROCESSUS EN COURS DE TERMINAISON - CODE: ${code} ⚠️⚠️⚠️\n`);
|
||
});
|
||
|
||
console.log('✅ 4. Handlers d\'erreurs installés');
|
||
|
||
// ⭐ IMPORTS WEBHOOKS
|
||
// ⭐ IMPORTS WEBHOOKS
|
||
try {
|
||
console.log('🔄 5. Chargement WebhookManager...');
|
||
const WebhookManager = require('./webhook-utils.js');
|
||
console.log(' Type de WebhookManager:', typeof WebhookManager);
|
||
|
||
console.log('🔄 6. Chargement webhook-config...');
|
||
const { WEBHOOKS, EVENTS } = require('./webhook-config');
|
||
console.log('✅ 7. Webhooks chargés avec succès');
|
||
|
||
const app = express();
|
||
console.log('✅ 8. Express initialisé');
|
||
|
||
const PORT = process.env.PORT || 3001;
|
||
console.log(`✅ 9. Port configuré: ${PORT}`);
|
||
|
||
// ⭐ INITIALISER LE WEBHOOK MANAGER
|
||
console.log('🔄 10. Initialisation WebhookManager...');
|
||
const webhookManager = new WebhookManager(WEBHOOKS.SECRET_KEY);
|
||
console.log('✅ 11. WebhookManager créé');
|
||
|
||
// Middleware
|
||
app.use(cors());
|
||
app.use(express.json());
|
||
console.log('✅ 12. Middlewares installés');
|
||
|
||
// Configuration MySQL
|
||
const dbConfig = {
|
||
host: process.env.DB_SERVER || '192.168.0.4',
|
||
user: process.env.DB_USER || 'wpuser',
|
||
password: process.env.DB_PASSWORD,
|
||
database: process.env.DB_DATABASE || 'DemandeConge',
|
||
waitForConnections: true,
|
||
connectionLimit: 10
|
||
};
|
||
|
||
console.log('🔄 13. Test connexion MySQL...');
|
||
const pool = mysql.createPool(dbConfig);
|
||
|
||
// TEST DE CONNEXION IMMÉDIAT
|
||
pool.getConnection()
|
||
.then(conn => {
|
||
console.log('✅ 14. Connexion MySQL réussie');
|
||
conn.release();
|
||
})
|
||
.catch(err => {
|
||
console.error('❌ 14. ERREUR CONNEXION MYSQL:', err.message);
|
||
console.error(' Host:', dbConfig.host);
|
||
console.error(' User:', dbConfig.user);
|
||
console.error(' Database:', dbConfig.database);
|
||
});
|
||
|
||
console.log('✅ 15. Pool MySQL créé');
|
||
|
||
// Configuration Microsoft O365
|
||
const msalConfig = {
|
||
auth: {
|
||
clientId: process.env.AZURE_CLIENT_ID,
|
||
authority: `https://login.microsoftonline.com/${process.env.AZURE_TENANT_ID}`,
|
||
clientSecret: process.env.AZURE_CLIENT_SECRET
|
||
}
|
||
};
|
||
|
||
const cca = new ConfidentialClientApplication(msalConfig);
|
||
|
||
async function getGraphToken() {
|
||
try {
|
||
console.log('🔑 Demande token Graph API...');
|
||
const params = new URLSearchParams({
|
||
grant_type: 'client_credentials',
|
||
client_id: process.env.AZURE_CLIENT_ID,
|
||
client_secret: process.env.AZURE_CLIENT_SECRET,
|
||
scope: 'https://graph.microsoft.com/.default'
|
||
});
|
||
|
||
const response = await fetch(
|
||
`https://login.microsoftonline.com/${process.env.AZURE_TENANT_ID}/oauth2/v2.0/token`,
|
||
{
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||
body: params.toString()
|
||
}
|
||
);
|
||
|
||
console.log('📊 Réponse OAuth - Status:', response.status);
|
||
|
||
if (!response.ok) {
|
||
const errorText = await response.text();
|
||
console.error('❌ Erreur OAuth:', errorText);
|
||
throw new Error(`OAuth Error: ${response.status}`);
|
||
}
|
||
|
||
const data = await response.json();
|
||
|
||
if (data.access_token) {
|
||
console.log('✅ Token obtenu');
|
||
return data.access_token;
|
||
} else {
|
||
console.error('❌ Pas de token dans la réponse:', data);
|
||
return null;
|
||
}
|
||
} catch (error) {
|
||
console.error('❌ Erreur getGraphToken:', error.message);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
async function sendMailGraph(accessToken, fromEmail, toEmail, subject, bodyHtml) {
|
||
try {
|
||
console.log('📤 Envoi email via Graph API...');
|
||
console.log(' From:', fromEmail);
|
||
console.log(' To:', toEmail);
|
||
console.log(' Subject:', subject);
|
||
|
||
const response = await fetch(
|
||
`https://graph.microsoft.com/v1.0/users/${fromEmail}/sendMail`,
|
||
{
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${accessToken}`,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
message: {
|
||
subject,
|
||
body: { contentType: 'HTML', content: bodyHtml },
|
||
toRecipients: [{ emailAddress: { address: toEmail } }]
|
||
},
|
||
saveToSentItems: false
|
||
})
|
||
}
|
||
);
|
||
|
||
console.log('📊 Réponse Graph API - Status:', response.status);
|
||
|
||
if (!response.ok) {
|
||
const errorText = await response.text();
|
||
console.error('❌ Erreur Graph API:', errorText);
|
||
throw new Error(`Graph API Error: ${response.status} - ${errorText}`);
|
||
}
|
||
|
||
console.log('✅ Email envoyé avec succès');
|
||
return true;
|
||
} catch (error) {
|
||
console.error('❌ Erreur sendMailGraph:', error.message);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// Middleware d'authentification
|
||
const authenticateToken = (req, res, next) => {
|
||
const token = req.headers['authorization']?.split(' ')[1];
|
||
if (!token) return res.status(401).json({ error: 'Token requis' });
|
||
|
||
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
|
||
if (err) return res.status(403).json({ error: 'Token invalide' });
|
||
req.user = user;
|
||
next();
|
||
});
|
||
};
|
||
|
||
// ================================================
|
||
// GESTION DES SERVER-SENT EVENTS (SSE)
|
||
// ================================================
|
||
|
||
// ================================================
|
||
// GESTION DES SERVER-SENT EVENTS (SSE)
|
||
// ================================================
|
||
|
||
const sseClients = new Set();
|
||
|
||
// 🔌 ROUTE SSE POUR LE CALENDRIER
|
||
// ROUTE SSE POUR LE CALENDRIER RH
|
||
app.get('/events', (req, res) => {
|
||
const { token, userid } = req.query;
|
||
|
||
let userId = userid;
|
||
|
||
// ✅ Si token fourni, extraire l'ID utilisateur
|
||
if (token && !userId) {
|
||
try {
|
||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||
userId = decoded.id;
|
||
console.log('🔓 Token JWT décodé, userId:', userId);
|
||
} catch (error) {
|
||
console.error('❌ Token invalide:', error.message);
|
||
return res.status(401).json({ error: 'Token invalide' });
|
||
}
|
||
}
|
||
|
||
if (!userId) {
|
||
console.error('❌ Ni userid ni token fourni');
|
||
return res.status(400).json({ error: 'userid ou token requis' });
|
||
}
|
||
|
||
console.log('🔗 Nouvelle connexion SSE (RH):', userId);
|
||
|
||
// Headers SSE critiques
|
||
res.setHeader('Content-Type', 'text/event-stream');
|
||
res.setHeader('Cache-Control', 'no-cache');
|
||
res.setHeader('Connection', 'keep-alive');
|
||
res.setHeader('X-Accel-Buffering', 'no');
|
||
|
||
// Flush pour établir la connexion
|
||
res.flushHeaders();
|
||
|
||
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) {
|
||
clearInterval(heartbeat);
|
||
}
|
||
}, 30000);
|
||
|
||
// Gérer la déconnexion
|
||
req.on('close', () => {
|
||
console.log('🔌 Déconnexion SSE (RH):', 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 => {
|
||
if (userId && client.id !== userId) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
client.send(event);
|
||
} catch (error) {
|
||
console.error('❌ Erreur envoi event:', error);
|
||
}
|
||
});
|
||
};
|
||
|
||
// ================================================
|
||
// ROUTE WEBHOOK RECEIVER
|
||
// ================================================
|
||
|
||
app.post('/webhook/receive', async (req, res) => {
|
||
try {
|
||
const signature = req.headers['x-webhook-signature'];
|
||
const payload = req.body;
|
||
|
||
console.log('📥 Webhook reçu du serveur RH:', payload.event);
|
||
|
||
if (!webhookManager.verifySignature(payload, signature)) {
|
||
console.error('❌ Signature webhook invalide');
|
||
return res.status(401).json({ error: 'Signature invalide' });
|
||
}
|
||
|
||
const { event, data } = payload;
|
||
|
||
console.log('✅ Signature valide ! Traitement...');
|
||
console.log(' Event:', event);
|
||
console.log(' Data:', data);
|
||
|
||
switch (event) {
|
||
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.type || data.typeConge}`);
|
||
console.log(` Couleur: ${data.couleurHex}`);
|
||
|
||
notifyClients({
|
||
type: 'demande-validated',
|
||
demandeId: data.demandeId,
|
||
statut: data.statut,
|
||
typeConge: data.typeConge || data.type,
|
||
type: data.type || data.typeConge,
|
||
couleurHex: data.couleurHex || '#d946ef',
|
||
date: data.date,
|
||
periode: data.periode,
|
||
collaborateurId: data.collaborateurId,
|
||
timestamp: new Date().toISOString()
|
||
}, data.collaborateurId);
|
||
|
||
console.log(` ✅ SSE envoyé au collaborateur ${data.collaborateurId}`);
|
||
break;
|
||
|
||
// 🆕 AJOUTER CE CAS POUR LES ANNULATIONS
|
||
case EVENTS.DEMANDE_CANCELLED:
|
||
console.log(`\n🔴 === WEBHOOK DEMANDE_CANCELLED REÇU ===`);
|
||
console.log(` Demande: ${data.demandeId}`);
|
||
console.log(` Annulée par: ${data.annulateurRole}`);
|
||
console.log(` Commentaire: ${data.commentaire || 'Aucun'}`);
|
||
|
||
// Notifier les clients SSE
|
||
notifyClients({
|
||
type: 'demande-cancelled',
|
||
demandeId: data.demandeId,
|
||
statut: 'Annulée',
|
||
collaborateurId: data.collaborateurId,
|
||
annulateurRole: data.annulateurRole,
|
||
commentaire: data.commentaire,
|
||
timestamp: new Date().toISOString()
|
||
});
|
||
|
||
// Notifier spécifiquement le collaborateur concerné
|
||
notifyClients({
|
||
type: 'demande-updated',
|
||
demandeId: data.demandeId,
|
||
nouveauStatut: 'Annulée',
|
||
timestamp: new Date().toISOString()
|
||
}, data.collaborateurId);
|
||
|
||
console.log(` ✅ SSE envoyé - Demande ${data.demandeId} marquée comme annulée`);
|
||
break;
|
||
|
||
case EVENTS.COMPTEUR_UPDATED:
|
||
console.log(`\n🔄 === WEBHOOK COMPTEUR_UPDATED REÇU ===`);
|
||
console.log(` Collaborateur: ${data.collaborateurId}`);
|
||
|
||
notifyClients({
|
||
type: 'compteur-updated',
|
||
collaborateurId: data.collaborateurId,
|
||
typeConge: data.typeConge,
|
||
typeUpdate: data.typeUpdate,
|
||
jours: data.jours,
|
||
timestamp: new Date().toISOString()
|
||
}, data.collaborateurId);
|
||
|
||
console.log(` ✅ SSE envoyé`);
|
||
break;
|
||
|
||
case EVENTS.DEMANDE_UPDATED:
|
||
console.log(`✏️ Demande ${data.demandeId} modifiée via RH`);
|
||
|
||
notifyClients({
|
||
type: 'demande-updated-rh',
|
||
demandeId: data.demandeId,
|
||
timestamp: new Date().toISOString()
|
||
}, data.collaborateurId);
|
||
break;
|
||
|
||
case EVENTS.DEMANDE_DELETED:
|
||
console.log(`🗑️ Demande ${data.demandeId} supprimée via RH`);
|
||
|
||
notifyClients({
|
||
type: 'demande-deleted-rh',
|
||
demandeId: data.demandeId,
|
||
timestamp: new Date().toISOString()
|
||
}, data.collaborateurId);
|
||
break;
|
||
|
||
default:
|
||
console.warn(`⚠️ Type d'événement webhook inconnu: ${event}`);
|
||
}
|
||
|
||
res.json({ success: true, message: 'Webhook traité' });
|
||
|
||
} catch (error) {
|
||
console.error('❌ Erreur traitement webhook:', error);
|
||
res.status(500).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
// ================================================
|
||
// ROUTES D'AUTHENTIFICATION
|
||
// ================================================
|
||
|
||
app.get('/users-dev', async (req, res) => {
|
||
try {
|
||
// On récupère juste l'essentiel pour le sélecteur
|
||
const [users] = await pool.query(`
|
||
SELECT id, email, nom, prenom, role, service, Actif
|
||
FROM CollaborateurAD
|
||
WHERE Actif = 1 OR Actif IS NULL
|
||
ORDER BY nom, prenom
|
||
`);
|
||
res.json(users);
|
||
} catch (error) {
|
||
console.error('❌ Erreur users-dev:', error);
|
||
res.status(500).json({ error: error.message });
|
||
}
|
||
});
|
||
app.post('/login-dev', async (req, res) => {
|
||
try {
|
||
console.log('🔐 Login attempt started');
|
||
const { accessToken } = req.body;
|
||
|
||
if (!accessToken) {
|
||
console.error('❌ No access token provided');
|
||
return res.status(400).json({ error: 'Token d\'accès manquant' });
|
||
}
|
||
|
||
console.log('👤 Validating access token...');
|
||
|
||
let userInfo;
|
||
try {
|
||
const graphResponse = await fetch('https://graph.microsoft.com/v1.0/me', {
|
||
headers: {
|
||
'Authorization': `Bearer ${accessToken}`
|
||
}
|
||
});
|
||
|
||
if (!graphResponse.ok) {
|
||
throw new Error('Token invalide');
|
||
}
|
||
|
||
userInfo = await graphResponse.json();
|
||
console.log('✅ Token validated, user:', userInfo.mail || userInfo.userPrincipalName);
|
||
} catch (graphError) {
|
||
console.error('❌ Graph API Error:', graphError);
|
||
return res.status(401).json({
|
||
error: 'Token d\'accès invalide',
|
||
details: graphError.message
|
||
});
|
||
}
|
||
|
||
const userEmail = userInfo.mail || userInfo.userPrincipalName;
|
||
|
||
// ✅ MODIFICATION: Ajouter le filtre Actif
|
||
const [users] = await pool.query(
|
||
`SELECT * FROM CollaborateurAD
|
||
WHERE email = ?
|
||
AND (Actif = 1 OR Actif IS NULL)`,
|
||
[userEmail]
|
||
);
|
||
|
||
if (users.length === 0) {
|
||
console.warn('⚠️ User not found or inactive:', userEmail);
|
||
return res.status(404).json({
|
||
error: 'Utilisateur non trouvé ou compte désactivé',
|
||
details: `Aucun utilisateur actif avec l'email ${userEmail}`
|
||
});
|
||
}
|
||
|
||
const user = users[0];
|
||
console.log('👤 User found:', user.email, 'Role:', user.role);
|
||
|
||
if (!['RH', 'Admin', 'Validateur'].includes(user.role)) {
|
||
console.warn('⚠️ Unauthorized role:', user.role);
|
||
return res.status(403).json({
|
||
error: 'Accès non autorisé',
|
||
details: `Le rôle '${user.role}' n'a pas accès à cette application`
|
||
});
|
||
}
|
||
|
||
const token = jwt.sign(
|
||
{ id: user.id, email: user.email, role: user.role },
|
||
process.env.JWT_SECRET,
|
||
{ expiresIn: '8h' }
|
||
);
|
||
|
||
console.log('✅ Login successful for:', user.email);
|
||
res.json({
|
||
token,
|
||
user: {
|
||
id: user.id,
|
||
nom: user.nom,
|
||
prenom: user.prenom,
|
||
role: user.role
|
||
}
|
||
});
|
||
} catch (error) {
|
||
console.error('❌ Unexpected error in /api/auth/login:', error);
|
||
res.status(500).json({
|
||
error: 'Erreur serveur inattendue',
|
||
details: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
// ================================================
|
||
// ROUTES DES DEMANDES DE CONGÉS
|
||
// ================================================
|
||
|
||
app.get('/demandes', authenticateToken, async (req, res) => {
|
||
try {
|
||
const { statut, dateDebut, dateFin, service } = req.query;
|
||
let query = `
|
||
SELECT dc.*,
|
||
CONCAT(ca.prenom, ' ', ca.nom) as nomEmploye,
|
||
ca.email as emailEmploye,
|
||
ca.Actif as employeActif,
|
||
s.Nom as service,
|
||
GROUP_CONCAT(DISTINCT tc.Nom SEPARATOR ', ') as typesConge,
|
||
CONCAT(v.prenom, ' ', v.nom) as nomValidateur
|
||
FROM DemandeConge dc
|
||
JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id
|
||
LEFT JOIN Services s ON ca.ServiceId = s.Id
|
||
LEFT JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId
|
||
LEFT JOIN TypeConge tc ON dct.TypeCongeId = tc.Id
|
||
LEFT JOIN CollaborateurAD v ON dc.ValidateurADId = v.id
|
||
WHERE 1=1
|
||
`;
|
||
const params = [];
|
||
|
||
if (statut) {
|
||
query += ' AND dc.Statut = ?';
|
||
params.push(statut);
|
||
}
|
||
if (dateDebut) {
|
||
query += ' AND dc.DateDebut >= ?';
|
||
params.push(dateDebut);
|
||
}
|
||
if (dateFin) {
|
||
query += ' AND dc.DateFin <= ?';
|
||
params.push(dateFin);
|
||
}
|
||
if (service) {
|
||
query += ' AND s.Id = ?';
|
||
params.push(service);
|
||
}
|
||
|
||
query += ' GROUP BY dc.Id ORDER BY dc.DateDemande DESC';
|
||
|
||
const [demandes] = await pool.query(query, params);
|
||
res.json(demandes);
|
||
} catch (error) {
|
||
res.status(500).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
app.get('/demandes/:id', authenticateToken, async (req, res) => {
|
||
try {
|
||
const [demandes] = await pool.query(`
|
||
SELECT
|
||
dc.*,
|
||
CONCAT(ca.prenom, ' ', ca.nom) AS nomEmploye,
|
||
ca.email AS emailEmploye,
|
||
ca.Actif AS employeActif,
|
||
ca.DateSortie AS employeDateSortie,
|
||
s.Nom AS service,
|
||
s.Id AS serviceId,
|
||
CONCAT(v.prenom, ' ', v.nom) AS nomValidateur,
|
||
v.email AS emailValidateur
|
||
FROM DemandeConge dc
|
||
JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id
|
||
LEFT JOIN Services s ON ca.ServiceId = s.Id
|
||
LEFT JOIN CollaborateurAD v ON dc.ValidateurADId = v.id
|
||
WHERE dc.Id = ?
|
||
`, [req.params.id]);
|
||
|
||
if (demandes.length === 0) {
|
||
return res.status(404).json({ error: 'Demande non trouvée' });
|
||
}
|
||
|
||
const [types] = await pool.query(`
|
||
SELECT
|
||
dct.TypeCongeId as typeId,
|
||
dct.NombreJours as nombreJours,
|
||
tc.Nom AS nom,
|
||
tc.CouleurHex AS couleur
|
||
FROM DemandeCongeType dct
|
||
JOIN TypeConge tc ON dct.TypeCongeId = tc.Id
|
||
WHERE dct.DemandeCongeId = ?
|
||
`, [req.params.id]);
|
||
|
||
res.json({
|
||
...demandes[0],
|
||
typesConge: types
|
||
});
|
||
} catch (error) {
|
||
console.error('❌ Erreur détails demande:', error);
|
||
console.error(' Code:', error.code);
|
||
console.error(' SQL:', error.sqlMessage);
|
||
res.status(500).json({ error: error.message, details: error.sqlMessage });
|
||
}
|
||
});
|
||
|
||
app.post('/demandes', authenticateToken, async (req, res) => {
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
await conn.beginTransaction();
|
||
|
||
const { collaborateurId, dateDebut, dateFin, typesConge, commentaire } = req.body;
|
||
|
||
// ✅ Vérifier que le collaborateur est actif
|
||
const [collab] = await conn.query(
|
||
'SELECT Actif FROM CollaborateurAD WHERE id = ?',
|
||
[collaborateurId]
|
||
);
|
||
|
||
if (collab.length === 0 || collab[0].Actif === 0) {
|
||
await conn.rollback();
|
||
return res.status(403).json({ error: 'Impossible de créer une demande pour un compte désactivé' });
|
||
}
|
||
|
||
const totalJours = typesConge.reduce((sum, type) => sum + parseFloat(type.nombreJours), 0);
|
||
|
||
const [result] = await conn.query(
|
||
`INSERT INTO DemandeConge (CollaborateurADId, DateDebut, DateFin, Statut, Commentaire, DateDemande, NombreJours)
|
||
VALUES (?, ?, ?, 'En attente', ?, NOW(), ?)`,
|
||
[collaborateurId, dateDebut, dateFin, commentaire || '', totalJours]
|
||
);
|
||
|
||
const demandeId = result.insertId;
|
||
|
||
for (const type of typesConge) {
|
||
await conn.query(
|
||
'INSERT INTO DemandeCongeType (DemandeCongeId, TypeCongeId, NombreJours) VALUES (?, ?, ?)',
|
||
[demandeId, type.typeId, type.nombreJours]
|
||
);
|
||
}
|
||
|
||
await conn.commit();
|
||
|
||
notifyClients({
|
||
type: 'demande-created',
|
||
demandeId: demandeId,
|
||
collaborateurId: collaborateurId,
|
||
timestamp: new Date().toISOString()
|
||
});
|
||
|
||
res.json({ id: demandeId, message: 'Demande créée avec succès' });
|
||
} catch (error) {
|
||
await conn.rollback();
|
||
res.status(500).json({ error: error.message });
|
||
} finally {
|
||
conn.release();
|
||
}
|
||
});
|
||
|
||
app.put('/demandes/:id', authenticateToken, async (req, res) => {
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
await conn.beginTransaction();
|
||
|
||
const { id } = req.params;
|
||
const { dateDebut, dateFin, typesConge, commentaire } = req.body;
|
||
|
||
const [demande] = await conn.query(
|
||
'SELECT Statut, CollaborateurADId FROM DemandeConge WHERE Id = ?',
|
||
[id]
|
||
);
|
||
|
||
if (demande.length === 0) {
|
||
await conn.rollback();
|
||
return res.status(404).json({ error: 'Demande non trouvée' });
|
||
}
|
||
|
||
const ancienStatut = demande[0].Statut;
|
||
const collaborateurId = demande[0].CollaborateurADId;
|
||
|
||
const rolesAutorises = ['Admin', 'RH', 'Validateur'];
|
||
const estAuteur = collaborateurId === req.user.id;
|
||
const estRoleAutorise = rolesAutorises.includes(req.user.role);
|
||
|
||
if (!estAuteur && !estRoleAutorise) {
|
||
await conn.rollback();
|
||
return res.status(403).json({
|
||
error: 'Accès non autorisé',
|
||
details: 'Seuls les Admin, RH, Validateurs ou l\'auteur peuvent modifier cette demande'
|
||
});
|
||
}
|
||
|
||
if (ancienStatut === 'Validée') {
|
||
console.log('⚠️ Modification d\'une demande validée - restauration des compteurs');
|
||
|
||
const [ancienTypes] = await conn.query(
|
||
'SELECT TypeCongeId, NombreJours FROM DemandeCongeType WHERE DemandeCongeId = ?',
|
||
[id]
|
||
);
|
||
|
||
for (const type of ancienTypes) {
|
||
await conn.query(
|
||
`UPDATE CompteurConges
|
||
SET Solde = Solde + ?
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = YEAR(NOW())`,
|
||
[type.NombreJours, collaborateurId, type.TypeCongeId]
|
||
);
|
||
}
|
||
}
|
||
|
||
const totalJours = typesConge.reduce((sum, type) => sum + parseFloat(type.nombreJours), 0);
|
||
|
||
const nouveauStatut = (ancienStatut === 'Validée' || ancienStatut === 'Refusée')
|
||
? 'En attente'
|
||
: ancienStatut;
|
||
|
||
await conn.query(
|
||
`UPDATE DemandeConge
|
||
SET DateDebut = ?,
|
||
DateFin = ?,
|
||
Commentaire = ?,
|
||
NombreJours = ?,
|
||
Statut = ?,
|
||
ValidateurADId = NULL,
|
||
DateValidation = NULL,
|
||
CommentaireValidation = NULL
|
||
WHERE Id = ?`,
|
||
[dateDebut, dateFin, commentaire || '', totalJours, nouveauStatut, id]
|
||
);
|
||
|
||
await conn.query('DELETE FROM DemandeCongeType WHERE DemandeCongeId = ?', [id]);
|
||
|
||
for (const type of typesConge) {
|
||
await conn.query(
|
||
'INSERT INTO DemandeCongeType (DemandeCongeId, TypeCongeId, NombreJours) VALUES (?, ?, ?)',
|
||
[id, type.typeId, type.nombreJours]
|
||
);
|
||
}
|
||
|
||
const actionDetails = ancienStatut === 'Validée'
|
||
? `Modification par ${req.user.role} - demande validée remise en attente`
|
||
: ancienStatut === 'Refusée'
|
||
? `Modification par ${req.user.role} - demande refusée remise en attente`
|
||
: `Modification par ${req.user.role}`;
|
||
|
||
await conn.query(
|
||
`INSERT INTO HistoriqueActions (CollaborateurADId, Action, Details, DemandeCongeId)
|
||
VALUES (?, ?, ?, ?)`,
|
||
[req.user.id, 'Modification demande', actionDetails, id]
|
||
);
|
||
|
||
await conn.commit();
|
||
|
||
notifyClients({
|
||
type: 'demande-updated',
|
||
demandeId: parseInt(id),
|
||
action: 'modification',
|
||
nouveauStatut: nouveauStatut,
|
||
ancienStatut: ancienStatut,
|
||
timestamp: new Date().toISOString()
|
||
}, collaborateurId);
|
||
|
||
notifyClients({
|
||
type: 'demande-list-updated',
|
||
action: 'modification',
|
||
demandeId: parseInt(id),
|
||
timestamp: new Date().toISOString()
|
||
});
|
||
|
||
try {
|
||
await webhookManager.sendWebhook(
|
||
WEBHOOKS.COLLABORATEURS_URL,
|
||
EVENTS.DEMANDE_UPDATED,
|
||
{
|
||
demandeId: parseInt(id),
|
||
collaborateurId: collaborateurId,
|
||
nouveauStatut: nouveauStatut,
|
||
ancienStatut: ancienStatut
|
||
}
|
||
);
|
||
|
||
if (nouveauStatut !== ancienStatut) {
|
||
await webhookManager.sendWebhook(
|
||
WEBHOOKS.COLLABORATEURS_URL,
|
||
EVENTS.COMPTEUR_UPDATED,
|
||
{ collaborateurId: collaborateurId }
|
||
);
|
||
}
|
||
} catch (webhookError) {
|
||
console.error('❌ Erreur envoi webhook (non bloquant):', webhookError.message);
|
||
}
|
||
|
||
const message = nouveauStatut !== ancienStatut
|
||
? `Demande modifiée avec succès et remise en attente (était: ${ancienStatut})`
|
||
: 'Demande modifiée avec succès';
|
||
|
||
res.json({
|
||
message,
|
||
nouveauStatut,
|
||
ancienStatut
|
||
});
|
||
|
||
console.log(`✅ Demande ${id} modifiée par ${req.user.role} - Ancien: ${ancienStatut}, Nouveau: ${nouveauStatut}`);
|
||
|
||
} catch (error) {
|
||
await conn.rollback();
|
||
console.error('Erreur modification demande:', error);
|
||
res.status(500).json({ error: error.message });
|
||
} finally {
|
||
conn.release();
|
||
}
|
||
});
|
||
|
||
app.delete('/demandes/:id', authenticateToken, async (req, res) => {
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
await conn.beginTransaction();
|
||
|
||
const { id } = req.params;
|
||
|
||
const [demande] = await conn.query(
|
||
'SELECT Statut, CollaborateurADId FROM DemandeConge WHERE Id = ?',
|
||
[id]
|
||
);
|
||
|
||
if (demande.length === 0) {
|
||
await conn.rollback();
|
||
return res.status(404).json({ error: 'Demande non trouvée' });
|
||
}
|
||
|
||
const collaborateurId = demande[0].CollaborateurADId;
|
||
|
||
const rolesAutorises = ['Admin', 'RH', 'Validateur'];
|
||
const estAuteur = collaborateurId === req.user.id;
|
||
const estRoleAutorise = rolesAutorises.includes(req.user.role);
|
||
|
||
if (!estAuteur && !estRoleAutorise) {
|
||
await conn.rollback();
|
||
return res.status(403).json({
|
||
error: 'Accès non autorisé',
|
||
details: 'Seuls les Admin, RH, Validateurs ou l\'auteur peuvent supprimer cette demande'
|
||
});
|
||
}
|
||
|
||
// ✅ AJOUT: Supprimer d'abord les entrées d'historique
|
||
await conn.query('DELETE FROM HistoriqueActions WHERE DemandeCongeId = ?', [id]);
|
||
|
||
// Supprimer les types de congé
|
||
await conn.query('DELETE FROM DemandeCongeType WHERE DemandeCongeId = ?', [id]);
|
||
|
||
// Supprimer la demande
|
||
await conn.query('DELETE FROM DemandeConge WHERE Id = ?', [id]);
|
||
|
||
await conn.commit();
|
||
|
||
notifyClients({
|
||
type: 'demande-deleted',
|
||
demandeId: parseInt(id),
|
||
collaborateurId: collaborateurId,
|
||
timestamp: new Date().toISOString()
|
||
});
|
||
|
||
try {
|
||
await webhookManager.sendWebhook(
|
||
WEBHOOKS.COLLABORATEURS_URL,
|
||
EVENTS.DEMANDE_DELETED,
|
||
{
|
||
demandeId: parseInt(id),
|
||
collaborateurId: collaborateurId
|
||
}
|
||
);
|
||
} catch (webhookError) {
|
||
console.error('❌ Erreur envoi webhook (non bloquant):', webhookError.message);
|
||
}
|
||
|
||
console.log(`✅ Demande ${id} supprimée par ${req.user.role}`);
|
||
res.json({ message: 'Demande supprimée avec succès' });
|
||
} catch (error) {
|
||
await conn.rollback();
|
||
console.error('❌ Erreur suppression demande:', error);
|
||
// ✅ AJOUT: Log plus détaillé de l'erreur
|
||
console.error(' Code erreur:', error.code);
|
||
console.error(' SQL Message:', error.sqlMessage);
|
||
res.status(500).json({ error: error.message });
|
||
} finally {
|
||
conn.release();
|
||
}
|
||
});
|
||
|
||
// ⭐ ROUTE VALIDATION AVEC WEBHOOKS
|
||
app.put('/demandes/:id/valider', authenticateToken, async (req, res) => {
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
await conn.beginTransaction();
|
||
|
||
const { id } = req.params;
|
||
const { statut, commentaire } = req.body;
|
||
|
||
console.log('\n🔍 === DÉBUT VALIDATION DEMANDE ===');
|
||
|
||
const [demandeInfo] = await conn.query(
|
||
`SELECT
|
||
dc.Id,
|
||
dc.CollaborateurADId,
|
||
dc.DateDebut,
|
||
dc.DateFin,
|
||
dc.NombreJours,
|
||
ca.prenom,
|
||
ca.nom,
|
||
ca.email as collaborateur_email,
|
||
ca.Actif,
|
||
GROUP_CONCAT(tc.Nom SEPARATOR ', ') as TypeConge
|
||
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
|
||
WHERE dc.Id = ?
|
||
GROUP BY dc.Id
|
||
LIMIT 1`,
|
||
[id]
|
||
);
|
||
|
||
if (demandeInfo.length === 0) {
|
||
await conn.rollback();
|
||
return res.status(404).json({ error: 'Demande non trouvée' });
|
||
}
|
||
|
||
const demande = demandeInfo[0];
|
||
const collaborateurId = demande.CollaborateurADId;
|
||
|
||
await conn.query(
|
||
`UPDATE DemandeConge
|
||
SET Statut = ?, CommentaireValidation = ?, ValidateurADId = ?, DateValidation = NOW()
|
||
WHERE Id = ?`,
|
||
[statut, commentaire || '', req.user.id, id]
|
||
);
|
||
|
||
if (statut === 'Validée') {
|
||
const [types] = await conn.query(
|
||
'SELECT TypeCongeId, NombreJours FROM DemandeCongeType WHERE DemandeCongeId = ?',
|
||
[id]
|
||
);
|
||
|
||
for (const type of types) {
|
||
await conn.query(
|
||
`UPDATE CompteurConges
|
||
SET Solde = Solde - ?
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = YEAR(NOW())`,
|
||
[type.NombreJours, collaborateurId, type.TypeCongeId]
|
||
);
|
||
}
|
||
}
|
||
|
||
await conn.query(
|
||
`INSERT INTO HistoriqueActions (CollaborateurADId, Action, Details, DemandeCongeId)
|
||
VALUES (?, ?, ?, ?)`,
|
||
[req.user.id, statut === 'Validée' ? 'Validation congé' : 'Refus congé', commentaire, id]
|
||
);
|
||
|
||
await conn.commit();
|
||
|
||
// ⭐ ENVOI EMAIL
|
||
const accessToken = await getGraphToken();
|
||
if (accessToken && demande.collaborateur_email) {
|
||
const fromEmail = 'noreply@ensup.eu';
|
||
const collaborateurNom = `${demande.prenom} ${demande.nom}`;
|
||
const validateurNom = req.user.prenom && req.user.nom
|
||
? `${req.user.prenom} ${req.user.nom}`
|
||
: 'l\'équipe RH';
|
||
|
||
const dateDebut = new Date(demande.DateDebut).toLocaleDateString('fr-FR');
|
||
const dateFin = new Date(demande.DateFin).toLocaleDateString('fr-FR');
|
||
const datesPeriode = dateDebut === dateFin ? dateDebut : `du ${dateDebut} au ${dateFin}`;
|
||
|
||
const subject = statut === 'Validée'
|
||
? '✅ Votre demande de congé a été approuvée'
|
||
: '❌ Votre demande de congé a été refusée';
|
||
|
||
const body = statut === 'Validée'
|
||
? `<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||
<div style="background-color: #10b981; color: white; padding: 20px; border-radius: 8px 8px 0 0;">
|
||
<h2 style="margin: 0;">✅ Demande approuvée</h2>
|
||
</div>
|
||
<div style="background-color: #f9fafb; padding: 20px; border: 1px solid #e5e7eb;">
|
||
<p style="font-size: 16px;">Bonjour <strong>${collaborateurNom}</strong>,</p>
|
||
<p>Votre demande de congé a été <strong style="color: #10b981;">approuvée</strong> par ${validateurNom}.</p>
|
||
<div style="background-color: white; border-left: 4px solid #10b981; padding: 15px; margin: 20px 0;">
|
||
<p><strong>Type :</strong> ${demande.TypeConge}</p>
|
||
<p><strong>Période :</strong> ${datesPeriode}</p>
|
||
<p><strong>Durée :</strong> ${demande.NombreJours} jour(s)</p>
|
||
${commentaire ? `<p><strong>Commentaire :</strong> ${commentaire}</p>` : ''}
|
||
</div>
|
||
</div>
|
||
</div>`
|
||
: `<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||
<div style="background-color: #ef4444; color: white; padding: 20px; border-radius: 8px 8px 0 0;">
|
||
<h2 style="margin: 0;">❌ Demande refusée</h2>
|
||
</div>
|
||
<div style="background-color: #f9fafb; padding: 20px; border: 1px solid #e5e7eb;">
|
||
<p style="font-size: 16px;">Bonjour <strong>${collaborateurNom}</strong>,</p>
|
||
<p>Votre demande de congé a été <strong style="color: #ef4444;">refusée</strong> par ${validateurNom}.</p>
|
||
<div style="background-color: white; border-left: 4px solid #ef4444; padding: 15px; margin: 20px 0;">
|
||
<p><strong>Type :</strong> ${demande.TypeConge}</p>
|
||
<p><strong>Période :</strong> ${datesPeriode}</p>
|
||
<p><strong>Durée :</strong> ${demande.NombreJours} jour(s)</p>
|
||
${commentaire ? `<p><strong>Motif du refus :</strong> ${commentaire}</p>` : ''}
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
|
||
await sendMailGraph(accessToken, fromEmail, demande.collaborateur_email, subject, body);
|
||
}
|
||
|
||
notifyClients({
|
||
type: 'demande-validated',
|
||
demandeId: parseInt(id),
|
||
statut: statut,
|
||
collaborateurId: collaborateurId,
|
||
timestamp: new Date().toISOString()
|
||
}, collaborateurId);
|
||
|
||
notifyClients({
|
||
type: 'demande-list-updated',
|
||
action: 'validation',
|
||
demandeId: parseInt(id),
|
||
timestamp: new Date().toISOString()
|
||
});
|
||
|
||
try {
|
||
await webhookManager.sendWebhook(
|
||
WEBHOOKS.COLLABORATEURS_URL,
|
||
EVENTS.DEMANDE_VALIDATED,
|
||
{
|
||
demandeId: parseInt(id),
|
||
statut: statut,
|
||
collaborateurId: collaborateurId,
|
||
validateurId: req.user.id,
|
||
commentaire: commentaire
|
||
}
|
||
);
|
||
|
||
await webhookManager.sendWebhook(
|
||
WEBHOOKS.COLLABORATEURS_URL,
|
||
EVENTS.COMPTEUR_UPDATED,
|
||
{ collaborateurId: collaborateurId }
|
||
);
|
||
} catch (webhookError) {
|
||
console.error('❌ Erreur envoi webhook (non bloquant):', webhookError.message);
|
||
}
|
||
|
||
res.json({ message: 'Demande mise à jour' });
|
||
} catch (error) {
|
||
await conn.rollback();
|
||
console.error('\n❌ ERREUR VALIDATION:', error);
|
||
res.status(500).json({ error: error.message });
|
||
} finally {
|
||
conn.release();
|
||
}
|
||
});
|
||
|
||
// ================================================
|
||
// ROUTES EMPLOYÉS
|
||
// ================================================
|
||
app.get('/employes', authenticateToken, async (req, res) => {
|
||
try {
|
||
const includeInactifs = req.query.include_inactifs === 'true';
|
||
const serviceId = req.query.service_id;
|
||
|
||
let query = `
|
||
SELECT
|
||
ca.id,
|
||
ca.nom,
|
||
ca.prenom,
|
||
CONCAT(ca.prenom, ' ', ca.nom) AS nomComplet,
|
||
ca.email,
|
||
ca.role,
|
||
ca.Actif,
|
||
ca.DateSortie,
|
||
ca.TypeContrat,
|
||
s.Nom AS service,
|
||
s.Id AS serviceId,
|
||
COALESCE(c.Nom, 'Sans campus') AS campus,
|
||
c.Id AS campusId,
|
||
cc1.Solde AS soldeCP,
|
||
cc2.Solde AS soldeRTT
|
||
FROM CollaborateurAD ca
|
||
LEFT JOIN Services s ON ca.ServiceId = s.Id
|
||
LEFT JOIN Campus c ON ca.CampusId = c.Id
|
||
LEFT JOIN CompteurConges cc1 ON ca.id = cc1.CollaborateurADId
|
||
AND cc1.TypeCongeId = 1
|
||
AND cc1.Annee = YEAR(NOW())
|
||
LEFT JOIN CompteurConges cc2 ON ca.id = cc2.CollaborateurADId
|
||
AND cc2.TypeCongeId = 2
|
||
AND cc2.Annee = YEAR(NOW())
|
||
WHERE 1=1
|
||
`;
|
||
|
||
const params = [];
|
||
|
||
if (!includeInactifs) {
|
||
query += ' AND (ca.Actif = 1 OR ca.Actif IS NULL)';
|
||
}
|
||
|
||
// 🆕 Ajouter le filtre service si fourni
|
||
if (serviceId && serviceId !== 'all') {
|
||
query += ' AND ca.ServiceId = ?';
|
||
params.push(serviceId);
|
||
}
|
||
|
||
query += ' ORDER BY c.Nom, s.Nom, ca.nom, ca.prenom';
|
||
|
||
const [employes] = await pool.query(query, params);
|
||
res.json(employes);
|
||
} catch (error) {
|
||
res.status(500).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
// ✅ NOUVELLE ROUTE: Désactiver un collaborateur
|
||
app.post('/employes/desactiver', authenticateToken, async (req, res) => {
|
||
try {
|
||
if (!['Admin', 'RH'].includes(req.user.role)) {
|
||
return res.status(403).json({ error: 'Accès non autorisé' });
|
||
}
|
||
|
||
const { collaborateur_id, date_sortie } = req.body;
|
||
|
||
if (!collaborateur_id) {
|
||
return res.json({ success: false, message: 'ID collaborateur manquant' });
|
||
}
|
||
|
||
const dateSortie = date_sortie || new Date().toISOString().split('T')[0];
|
||
|
||
const [collab] = await pool.query(
|
||
'SELECT prenom, nom, email 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 Actif = FALSE, DateSortie = ?
|
||
WHERE id = ?`,
|
||
[dateSortie, collaborateur_id]
|
||
);
|
||
|
||
res.json({
|
||
success: true,
|
||
message: 'Collaborateur désactivé',
|
||
nom: `${collab[0].prenom} ${collab[0].nom}`,
|
||
date_sortie: dateSortie
|
||
});
|
||
} catch (error) {
|
||
console.error('Erreur desactiverCollaborateur:', error);
|
||
res.status(500).json({ success: false, message: 'Erreur serveur', error: error.message });
|
||
}
|
||
});
|
||
|
||
// ✅ NOUVELLE ROUTE: Réactiver un collaborateur
|
||
app.post('/employes/reactiver', authenticateToken, async (req, res) => {
|
||
try {
|
||
if (!['Admin', 'RH'].includes(req.user.role)) {
|
||
return res.status(403).json({ error: 'Accès non autorisé' });
|
||
}
|
||
|
||
const { collaborateur_id } = req.body;
|
||
|
||
if (!collaborateur_id) {
|
||
return res.json({ success: false, message: 'ID collaborateur manquant' });
|
||
}
|
||
|
||
const [collab] = await pool.query(
|
||
'SELECT prenom, nom, email 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 Actif = TRUE, DateSortie = NULL
|
||
WHERE id = ?`,
|
||
[collaborateur_id]
|
||
);
|
||
|
||
res.json({
|
||
success: true,
|
||
message: 'Collaborateur réactivé',
|
||
nom: `${collab[0].prenom} ${collab[0].nom}`
|
||
});
|
||
} catch (error) {
|
||
console.error('Erreur reactiverCollaborateur:', error);
|
||
res.status(500).json({ success: false, message: 'Erreur serveur', error: error.message });
|
||
}
|
||
});
|
||
|
||
app.get('/types-conge', authenticateToken, async (req, res) => {
|
||
try {
|
||
const [types] = await pool.query(
|
||
'SELECT Id as id, Nom as nom, CouleurHex as couleur FROM TypeConge WHERE Actif = 1 ORDER BY Nom'
|
||
);
|
||
res.json(types);
|
||
} catch (error) {
|
||
res.status(500).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
app.get('/export/paie', authenticateToken, async (req, res) => {
|
||
try {
|
||
const { mois, annee } = req.query;
|
||
|
||
console.log(`📊 Export paie demandé : ${mois}/${annee}`);
|
||
|
||
// Date de début et fin du mois
|
||
const premierJour = `${annee}-${String(mois).padStart(2, '0')}-01`;
|
||
const dernierJour = new Date(annee, mois, 0).getDate();
|
||
const dernierJourMois = `${annee}-${String(mois).padStart(2, '0')}-${dernierJour}`;
|
||
|
||
console.log(`📅 Période : ${premierJour} à ${dernierJourMois}`);
|
||
|
||
// Récupérer toutes les demandes validées qui chevauchent le mois
|
||
const [demandes] = await pool.query(`
|
||
SELECT
|
||
ca.id as collaborateurId,
|
||
CONCAT(ca.prenom, ' ', ca.nom) as employe,
|
||
ca.email,
|
||
s.Nom as service,
|
||
tc.Nom as typeConge,
|
||
dct.NombreJours,
|
||
dc.DateDebut,
|
||
dc.DateFin,
|
||
dc.Statut
|
||
FROM DemandeConge dc
|
||
JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id
|
||
LEFT JOIN Services s ON ca.ServiceId = s.Id
|
||
JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId
|
||
JOIN TypeConge tc ON dct.TypeCongeId = tc.Id
|
||
WHERE dc.Statut = 'Validée'
|
||
AND (
|
||
(dc.DateDebut BETWEEN ? AND ?) OR
|
||
(dc.DateFin BETWEEN ? AND ?) OR
|
||
(dc.DateDebut <= ? AND dc.DateFin >= ?)
|
||
)
|
||
AND (ca.Actif = 1 OR ca.Actif IS NULL)
|
||
ORDER BY ca.nom, ca.prenom, dc.DateDebut
|
||
`, [premierJour, dernierJourMois, premierJour, dernierJourMois, premierJour, dernierJourMois]);
|
||
|
||
console.log(`📋 Nombre de demandes trouvées : ${demandes.length}`);
|
||
|
||
if (demandes.length > 0) {
|
||
console.log('🔍 Exemple de demande:', demandes[0]);
|
||
}
|
||
|
||
// Regrouper par employé
|
||
const employesMap = new Map();
|
||
|
||
demandes.forEach(row => {
|
||
const key = row.collaborateurId;
|
||
|
||
if (!employesMap.has(key)) {
|
||
employesMap.set(key, {
|
||
employe: row.employe,
|
||
email: row.email,
|
||
service: row.service || 'Non assigné',
|
||
rtt: { nb: 0, dates: [] },
|
||
cp: { nb: 0, dates: [] },
|
||
aap: { nb: 0, dates: [] },
|
||
am: { nb: 0, dates: [] },
|
||
autres: []
|
||
});
|
||
}
|
||
|
||
const employe = employesMap.get(key);
|
||
|
||
// Formatter les dates
|
||
const dateDebut = new Date(row.DateDebut);
|
||
const dateFin = new Date(row.DateFin);
|
||
|
||
const formatDate = (date) => {
|
||
return String(date.getDate()).padStart(2, '0') + '/' +
|
||
String(date.getMonth() + 1).padStart(2, '0');
|
||
};
|
||
|
||
const dateDebutStr = formatDate(dateDebut);
|
||
const dateFinStr = formatDate(dateFin);
|
||
const periode = dateDebutStr === dateFinStr ? dateDebutStr : `${dateDebutStr}-${dateFinStr}`;
|
||
|
||
// ✅ CORRECTION : Convertir en nombre
|
||
const nombreJours = parseFloat(row.NombreJours);
|
||
|
||
// Catégoriser selon le type de congé
|
||
const typeConge = row.typeConge.toLowerCase();
|
||
|
||
console.log(` Type détecté: "${row.typeConge}" (${nombreJours}j)`);
|
||
|
||
if (typeConge.includes('rtt')) {
|
||
employe.rtt.nb += nombreJours; // ✅ nombreJours est maintenant un nombre
|
||
employe.rtt.dates.push(periode);
|
||
console.log(` ✅ Classé en RTT (total: ${employe.rtt.nb}j)`);
|
||
} else if (typeConge.includes('cp') || typeConge.includes('congé') || typeConge.includes('conge')) {
|
||
employe.cp.nb += nombreJours; // ✅ nombreJours est maintenant un nombre
|
||
employe.cp.dates.push(periode);
|
||
console.log(` ✅ Classé en CP (total: ${employe.cp.nb}j)`);
|
||
} else if (typeConge.includes('aap') || typeConge.includes('absence autorisée') ||
|
||
typeConge.includes('enfant malade') || typeConge.includes('récup') ||
|
||
typeConge.includes('recuperation')) {
|
||
employe.aap.nb += nombreJours;
|
||
employe.aap.dates.push(periode);
|
||
console.log(` ✅ Classé en AAP (total: ${employe.aap.nb}j)`);
|
||
} else if (typeConge.includes('maladie') || typeConge.includes('arrêt') || typeConge.includes('arret')) {
|
||
employe.am.nb += nombreJours;
|
||
employe.am.dates.push(periode);
|
||
console.log(` ✅ Classé en AM (total: ${employe.am.nb}j)`);
|
||
} else {
|
||
// Autres types
|
||
const existingAutre = employe.autres.find(a => a.type === row.typeConge);
|
||
if (existingAutre) {
|
||
existingAutre.nb += nombreJours;
|
||
existingAutre.dates.push(periode);
|
||
} else {
|
||
employe.autres.push({
|
||
type: row.typeConge,
|
||
nb: nombreJours,
|
||
dates: [periode]
|
||
});
|
||
}
|
||
console.log(` ✅ Classé en Autres (total: ${nombreJours}j)`);
|
||
}
|
||
});
|
||
|
||
// Formatter les dates (joindre avec " ; ")
|
||
const dataFormatted = Array.from(employesMap.values()).map(emp => ({
|
||
employe: emp.employe,
|
||
email: emp.email,
|
||
service: emp.service,
|
||
rtt: { nb: emp.rtt.nb, dates: emp.rtt.dates.join(' ; ') },
|
||
cp: { nb: emp.cp.nb, dates: emp.cp.dates.join(' ; ') },
|
||
aap: { nb: emp.aap.nb, dates: emp.aap.dates.join(' ; ') },
|
||
am: { nb: emp.am.nb, dates: emp.am.dates.join(' ; ') },
|
||
autres: emp.autres.map(a => ({
|
||
type: a.type,
|
||
nb: a.nb,
|
||
dates: a.dates.join(' ; ')
|
||
}))
|
||
}));
|
||
|
||
console.log(`✅ ${dataFormatted.length} collaborateurs dans le rapport`);
|
||
|
||
res.json(dataFormatted);
|
||
} catch (error) {
|
||
console.error('❌ Erreur export paie:', error);
|
||
res.status(500).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
|
||
|
||
// ========================================
|
||
// ROUTE COMPLÈTE : /reinitializeAllCounters
|
||
// ========================================
|
||
app.post('/reinitializeAllCounters', async (req, res) => {
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
await conn.beginTransaction();
|
||
|
||
console.log('🔄 Réinitialisation de tous les compteurs...');
|
||
|
||
const collaborateurs = await conn.query(`
|
||
SELECT id, prenom, nom, DateEntree, TypeContrat, CampusId, SocieteId, role
|
||
FROM CollaborateurAD
|
||
WHERE actif = 1 OR actif IS NULL
|
||
`);
|
||
|
||
console.log(`📋 ${collaborateurs.length} collaborateurs trouvés`);
|
||
|
||
const dateRefParam = req.body.dateReference;
|
||
const today = dateRefParam ? new Date(dateRefParam) : new Date();
|
||
const currentYear = today.getFullYear();
|
||
const previousYear = currentYear - 1;
|
||
const results = [];
|
||
|
||
// Récupérer les types de congés
|
||
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']);
|
||
|
||
for (const collab of collaborateurs) {
|
||
console.log(`\n${'='.repeat(60)}`);
|
||
console.log(`👤 ${collab.prenom} ${collab.nom} (ID: ${collab.id})`);
|
||
console.log(` Type contrat: ${collab.TypeContrat || '37h'}`);
|
||
console.log(` Date entrée: ${collab.DateEntree ? new Date(collab.DateEntree).toISOString().split('T')[0] : 'NULL'}`);
|
||
console.log(` Rôle: ${collab.role || 'Non défini'}`);
|
||
|
||
const dateEntree = collab.DateEntree;
|
||
const typeContrat = collab.TypeContrat || '37h';
|
||
const isApprenti = collab.role === 'Apprenti';
|
||
|
||
// ==========================================
|
||
// CALCUL CP
|
||
// ==========================================
|
||
let acquisCP = calculerAcquisitionCP(today, dateEntree);
|
||
|
||
// ✅ PROTECTION : Si NaN, forcer à 0
|
||
if (isNaN(acquisCP) || !isFinite(acquisCP)) {
|
||
console.warn(`⚠️ Acquisition CP invalide - Forcé à 0`);
|
||
acquisCP = 0;
|
||
}
|
||
|
||
// ==========================================
|
||
// CALCUL RTT
|
||
// ==========================================
|
||
let acquisRTT = 0;
|
||
if (!isApprenti) {
|
||
try {
|
||
const rttData = await calculerAcquisitionRTT(conn, collab.id, today);
|
||
acquisRTT = rttData.acquisition;
|
||
|
||
// ✅ PROTECTION : Si NaN, forcer à 0
|
||
if (isNaN(acquisRTT) || !isFinite(acquisRTT)) {
|
||
console.warn(`⚠️ Acquisition RTT invalide - Forcé à 0`);
|
||
acquisRTT = 0;
|
||
}
|
||
} catch (error) {
|
||
console.error(`❌ Erreur calcul RTT:`, error.message);
|
||
acquisRTT = 0;
|
||
}
|
||
}
|
||
|
||
// ==========================================
|
||
// MISE À JOUR CP N
|
||
// ==========================================
|
||
if (cpType.length > 0) {
|
||
const deductionsCP = await conn.query(`
|
||
SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalConsomme
|
||
FROM DeductionDetails dd
|
||
JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
|
||
WHERE dc.CollaborateurADId = ?
|
||
AND dd.TypeCongeId = ?
|
||
AND dd.Annee = ?
|
||
AND dc.Statut != 'Refusé'
|
||
`, [collab.id, cpType[0].Id, currentYear]);
|
||
|
||
let totalConsomme = parseFloat(deductionsCP[0].totalConsomme || 0);
|
||
|
||
// ✅ PROTECTION
|
||
if (isNaN(totalConsomme) || !isFinite(totalConsomme)) {
|
||
console.warn(`⚠️ Total consommé CP invalide - Forcé à 0`);
|
||
totalConsomme = 0;
|
||
}
|
||
|
||
const compteurExisting = await conn.query(`
|
||
SELECT SoldeReporte FROM CompteurConges
|
||
WHERE CollaborateurADId = ?
|
||
AND TypeCongeId = ?
|
||
AND Annee = ?
|
||
`, [collab.id, cpType[0].Id, currentYear]);
|
||
|
||
let soldeReporte = compteurExisting.length > 0
|
||
? parseFloat(compteurExisting[0].SoldeReporte || 0)
|
||
: 0;
|
||
|
||
// ✅ PROTECTION
|
||
if (isNaN(soldeReporte) || !isFinite(soldeReporte)) {
|
||
console.warn(`⚠️ Solde reporté CP invalide - Forcé à 0`);
|
||
soldeReporte = 0;
|
||
}
|
||
|
||
let nouveauSolde = Math.max(0, acquisCP + soldeReporte - totalConsomme);
|
||
|
||
// ✅ PROTECTION FINALE
|
||
if (isNaN(nouveauSolde) || !isFinite(nouveauSolde)) {
|
||
console.warn(`⚠️ Nouveau solde CP invalide - Forcé à 0`);
|
||
nouveauSolde = 0;
|
||
}
|
||
|
||
console.log(` 📊 CP - Acquis: ${acquisCP.toFixed(2)}j, Consommé: ${totalConsomme.toFixed(2)}j, Reporté: ${soldeReporte.toFixed(2)}j, Solde: ${nouveauSolde.toFixed(2)}j`);
|
||
|
||
if (compteurExisting.length > 0) {
|
||
await conn.query(`
|
||
UPDATE CompteurConges
|
||
SET Total = ?, Solde = ?, DerniereMiseAJour = NOW()
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [acquisCP, nouveauSolde, collab.id, cpType[0].Id, currentYear]);
|
||
} else {
|
||
await conn.query(`
|
||
INSERT INTO CompteurConges
|
||
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour)
|
||
VALUES (?, ?, ?, ?, ?, 0, NOW())
|
||
`, [collab.id, cpType[0].Id, currentYear, acquisCP, nouveauSolde]);
|
||
}
|
||
|
||
// Créer CP N-1 si nécessaire
|
||
const cpN1 = await conn.query(`
|
||
SELECT Id FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collab.id, cpType[0].Id, previousYear]);
|
||
|
||
if (cpN1.length === 0) {
|
||
await conn.query(`
|
||
INSERT INTO CompteurConges
|
||
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour)
|
||
VALUES (?, ?, ?, 0, 0, 0, NOW())
|
||
`, [collab.id, cpType[0].Id, previousYear]);
|
||
}
|
||
}
|
||
|
||
// ==========================================
|
||
// MISE À JOUR RTT N
|
||
// ==========================================
|
||
if (rttType.length > 0 && !isApprenti) {
|
||
const deductionsRTT = await conn.query(`
|
||
SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalConsomme
|
||
FROM DeductionDetails dd
|
||
JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
|
||
WHERE dc.CollaborateurADId = ?
|
||
AND dd.TypeCongeId = ?
|
||
AND dd.Annee = ?
|
||
AND dc.Statut != 'Refusé'
|
||
`, [collab.id, rttType[0].Id, currentYear]);
|
||
|
||
let totalConsomme = parseFloat(deductionsRTT[0].totalConsomme || 0);
|
||
|
||
// ✅ PROTECTION
|
||
if (isNaN(totalConsomme) || !isFinite(totalConsomme)) {
|
||
console.warn(`⚠️ Total consommé RTT invalide - Forcé à 0`);
|
||
totalConsomme = 0;
|
||
}
|
||
|
||
let nouveauSolde = Math.max(0, acquisRTT - totalConsomme);
|
||
|
||
// ✅ PROTECTION FINALE
|
||
if (isNaN(nouveauSolde) || !isFinite(nouveauSolde)) {
|
||
console.warn(`⚠️ Nouveau solde RTT invalide - Forcé à 0`);
|
||
nouveauSolde = 0;
|
||
}
|
||
|
||
console.log(` 📊 RTT - Acquis: ${acquisRTT.toFixed(2)}j, Consommé: ${totalConsomme.toFixed(2)}j, Solde: ${nouveauSolde.toFixed(2)}j`);
|
||
|
||
const compteurExisting = await conn.query(`
|
||
SELECT Id FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collab.id, rttType[0].Id, currentYear]);
|
||
|
||
if (compteurExisting.length > 0) {
|
||
await conn.query(`
|
||
UPDATE CompteurConges
|
||
SET Total = ?, Solde = ?, DerniereMiseAJour = NOW()
|
||
WHERE Id = ?
|
||
`, [acquisRTT, nouveauSolde, compteurExisting[0].Id]);
|
||
} else {
|
||
await conn.query(`
|
||
INSERT INTO CompteurConges
|
||
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour)
|
||
VALUES (?, ?, ?, ?, ?, 0, NOW())
|
||
`, [collab.id, rttType[0].Id, currentYear, acquisRTT, nouveauSolde]);
|
||
}
|
||
|
||
// Créer RTT N-1 si nécessaire
|
||
const rttN1 = await conn.query(`
|
||
SELECT Id FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collab.id, rttType[0].Id, previousYear]);
|
||
|
||
if (rttN1.length === 0) {
|
||
await conn.query(`
|
||
INSERT INTO CompteurConges
|
||
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour)
|
||
VALUES (?, ?, ?, 0, 0, 0, NOW())
|
||
`, [collab.id, rttType[0].Id, previousYear]);
|
||
}
|
||
}
|
||
|
||
// Ajouter au résultat
|
||
results.push({
|
||
collaborateur: `${collab.prenom} ${collab.nom}`,
|
||
typecontrat: typeContrat,
|
||
cpacquis: acquisCP.toFixed(2),
|
||
rttacquis: acquisRTT.toFixed(2)
|
||
});
|
||
}
|
||
|
||
await conn.commit();
|
||
console.log('\n✅ Réinitialisation terminée');
|
||
|
||
res.json({
|
||
success: true,
|
||
message: `Compteurs réinitialisés pour ${collaborateurs.length} collaborateurs`,
|
||
datereference: today.toISOString().split('T')[0],
|
||
totalcollaborateurs: collaborateurs.length,
|
||
results: results
|
||
});
|
||
|
||
} catch (error) {
|
||
await conn.rollback();
|
||
console.error('❌ Erreur réinitialisation:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur lors de la réinitialisation',
|
||
error: error.message
|
||
});
|
||
} finally {
|
||
conn.release();
|
||
}
|
||
});
|
||
// Route pour obtenir les informations d'un collaborateur
|
||
app.get('/employes/:id/info', authenticateToken, async (req, res) => {
|
||
try {
|
||
const [employe] = await pool.query(
|
||
`SELECT
|
||
id,
|
||
nom,
|
||
prenom,
|
||
email,
|
||
role,
|
||
TypeContrat,
|
||
Actif,
|
||
DateSortie,
|
||
ServiceId
|
||
FROM CollaborateurAD
|
||
WHERE id = ?`,
|
||
[req.params.id]
|
||
);
|
||
|
||
if (employe.length === 0) {
|
||
return res.status(404).json({ error: 'Employé non trouvé' });
|
||
}
|
||
|
||
const collab = employe[0];
|
||
|
||
// Déterminer si c'est un apprenti
|
||
const estApprenti = collab.TypeContrat === 'Apprentissage' ||
|
||
collab.role === 'Apprenti' ||
|
||
collab.role?.toLowerCase().includes('apprenti');
|
||
|
||
res.json({
|
||
...collab,
|
||
estApprenti,
|
||
droitRTT: !estApprenti
|
||
});
|
||
} catch (error) {
|
||
console.error('Erreur récupération info employé:', error);
|
||
res.status(500).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
// ✅ Modifier la route GET /api/compteurs pour inclure les infos sur les apprentis
|
||
app.get('/compteurs', authenticateToken, async (req, res) => {
|
||
try {
|
||
const includeInactifs = req.query.include_inactifs === 'true';
|
||
|
||
let query = `
|
||
SELECT
|
||
cc.Id as id,
|
||
cc.CollaborateurADId as collaborateurId,
|
||
cc.TypeCongeId as typeCongeId,
|
||
CONCAT(ca.prenom, ' ', ca.nom) AS employe,
|
||
ca.email,
|
||
ca.Actif,
|
||
ca.role,
|
||
ca.TypeContrat,
|
||
s.Nom AS service,
|
||
tc.Nom AS typeConge,
|
||
cc.Annee AS annee,
|
||
cc.Total AS total,
|
||
cc.Solde AS solde,
|
||
cc.SoldeReporte AS soldeReporte,
|
||
(cc.Total - cc.Solde) AS consomme
|
||
FROM CompteurConges cc
|
||
JOIN CollaborateurAD ca ON cc.CollaborateurADId = ca.id
|
||
LEFT JOIN Services s ON ca.ServiceId = s.Id
|
||
JOIN TypeConge tc ON cc.TypeCongeId = tc.Id
|
||
`;
|
||
|
||
if (!includeInactifs) {
|
||
query += ' WHERE (ca.Actif = 1 OR ca.Actif IS NULL)';
|
||
}
|
||
|
||
query += ' ORDER BY ca.Actif DESC, ca.nom, ca.prenom, tc.Nom';
|
||
|
||
const [compteurs] = await pool.query(query);
|
||
|
||
// Ajouter l'indicateur apprenti à chaque compteur
|
||
const compteursAvecStatut = compteurs.map(c => ({
|
||
...c,
|
||
estApprenti: c.TypeContrat === 'Apprentissage' ||
|
||
c.role === 'Apprenti' ||
|
||
c.role?.toLowerCase().includes('apprenti')
|
||
}));
|
||
|
||
res.json(compteursAvecStatut);
|
||
} catch (error) {
|
||
res.status(500).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
app.put('/compteurs/:id', authenticateToken, async (req, res) => {
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
await conn.beginTransaction();
|
||
|
||
if (!['Admin', 'RH', 'Validateur'].includes(req.user.role)) {
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.status(403).json({ error: 'Accès non autorisé' });
|
||
}
|
||
|
||
const { id } = req.params;
|
||
const { total, solde } = req.body;
|
||
|
||
// ✅ CONVERTIR EN NOMBRES
|
||
const totalNum = parseFloat(total);
|
||
const soldeNum = parseFloat(solde);
|
||
|
||
const [existing] = await conn.query('SELECT * FROM CompteurConges WHERE Id = ?', [id]);
|
||
|
||
if (existing.length === 0) {
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.status(404).json({ error: 'Compteur non trouvé' });
|
||
}
|
||
|
||
const collaborateurId = existing[0].CollaborateurADId;
|
||
const typeCongeId = existing[0].TypeCongeId;
|
||
const annee = existing[0].Annee;
|
||
|
||
// Récupérer le nom du type de congé
|
||
const [typeInfo] = await conn.query(
|
||
'SELECT Nom FROM TypeConge WHERE Id = ?',
|
||
[typeCongeId]
|
||
);
|
||
|
||
const typeConge = typeInfo.length > 0 ? typeInfo[0].Nom : 'Inconnu';
|
||
|
||
// ⭐ MISE À JOUR AVEC TRANSACTION
|
||
await conn.query(
|
||
`UPDATE CompteurConges SET Total = ?, Solde = ?, DerniereMiseAJour = NOW() WHERE Id = ?`,
|
||
[totalNum, soldeNum, id]
|
||
);
|
||
|
||
await conn.commit();
|
||
|
||
// Notifier les clients SSE
|
||
notifyClients({
|
||
type: 'compteur-updated',
|
||
collaborateurId: collaborateurId,
|
||
typeCongeId: typeCongeId,
|
||
typeConge: typeConge,
|
||
annee: annee,
|
||
action: 'modification_rh',
|
||
nouveauTotal: totalNum,
|
||
nouveauSolde: soldeNum,
|
||
timestamp: new Date().toISOString()
|
||
}, collaborateurId);
|
||
|
||
// ⭐ WEBHOOK AMÉLIORÉ AVEC TOUTES LES INFOS
|
||
try {
|
||
await webhookManager.sendWebhook(
|
||
WEBHOOKS.COLLABORATEURS_URL,
|
||
EVENTS.COMPTEUR_UPDATED,
|
||
{
|
||
collaborateurId: collaborateurId,
|
||
typeCongeId: typeCongeId,
|
||
typeConge: typeConge,
|
||
typeUpdate: 'modification_manuelle_rh',
|
||
annee: annee,
|
||
nouveauTotal: totalNum,
|
||
nouveauSolde: soldeNum,
|
||
source: 'rh',
|
||
timestamp: new Date().toISOString()
|
||
}
|
||
);
|
||
console.log(`✅ Webhook envoyé pour ${collaborateurId} (${typeConge} ${annee}: Total=${totalNum}j, Solde=${soldeNum}j)`);
|
||
} catch (webhookError) {
|
||
console.error('❌ Erreur envoi webhook (non bloquant):', webhookError.message);
|
||
}
|
||
|
||
res.json({
|
||
message: 'Compteur modifié avec succès',
|
||
total: totalNum,
|
||
solde: soldeNum
|
||
});
|
||
} catch (error) {
|
||
await conn.rollback();
|
||
console.error('Erreur modification compteur:', error);
|
||
res.status(500).json({ error: error.message });
|
||
} finally {
|
||
conn.release();
|
||
}
|
||
});
|
||
app.get('/compteurs', authenticateToken, async (req, res) => {
|
||
try {
|
||
const { user_id } = req.query;
|
||
const includeInactifs = req.query.include_inactifs === 'true';
|
||
|
||
let query = `
|
||
SELECT
|
||
cc.Id as id,
|
||
cc.CollaborateurADId as collaborateurId,
|
||
cc.TypeCongeId as typeCongeId,
|
||
CONCAT(ca.prenom, ' ', ca.nom) AS employe,
|
||
ca.email,
|
||
ca.Actif,
|
||
ca.role,
|
||
ca.TypeContrat,
|
||
s.Nom AS service,
|
||
tc.Nom AS typeConge,
|
||
cc.Annee AS annee,
|
||
cc.Total AS total,
|
||
cc.Solde AS solde,
|
||
cc.SoldeReporte AS soldeReporte,
|
||
(cc.Total - cc.Solde) AS consomme
|
||
FROM CompteurConges cc
|
||
JOIN CollaborateurAD ca ON cc.CollaborateurADId = ca.id
|
||
LEFT JOIN Services s ON ca.ServiceId = s.Id
|
||
JOIN TypeConge tc ON cc.TypeCongeId = tc.Id
|
||
WHERE 1=1
|
||
`;
|
||
|
||
const params = [];
|
||
|
||
// Filtre par utilisateur si fourni
|
||
if (user_id) {
|
||
query += ' AND cc.CollaborateurADId = ?';
|
||
params.push(user_id);
|
||
}
|
||
|
||
// Filtre actif/inactif
|
||
if (!includeInactifs) {
|
||
query += ' AND (ca.Actif = 1 OR ca.Actif IS NULL)';
|
||
}
|
||
|
||
query += ' ORDER BY ca.Actif DESC, ca.nom, ca.prenom, tc.Nom';
|
||
|
||
const [compteurs] = await pool.query(query, params);
|
||
|
||
// Ajouter l'indicateur apprenti
|
||
const compteursAvecStatut = compteurs.map(c => ({
|
||
...c,
|
||
estApprenti: c.TypeContrat === 'Apprentissage' ||
|
||
c.role === 'Apprenti' ||
|
||
c.role?.toLowerCase().includes('apprenti')
|
||
}));
|
||
|
||
res.json(compteursAvecStatut);
|
||
} catch (error) {
|
||
console.error('❌ Erreur récupération compteurs:', error);
|
||
res.status(500).json({ error: error.message });
|
||
}
|
||
});
|
||
// ================================================
|
||
// ROUTE POUR AJOUTER DES JOURS DE RÉCUPÉRATION
|
||
// ================================================
|
||
|
||
app.post('/compteurs/ajouter-recup', authenticateToken, async (req, res) => {
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
await conn.beginTransaction();
|
||
|
||
if (!['Admin', 'RH'].includes(req.user.role)) {
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.status(403).json({ error: 'Accès non autorisé' });
|
||
}
|
||
|
||
const { collaborateurId, nombreJours, commentaire } = req.body;
|
||
|
||
if (!collaborateurId || !nombreJours || nombreJours <= 0) {
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.json({
|
||
success: false,
|
||
message: 'Données invalides'
|
||
});
|
||
}
|
||
|
||
console.log('📥 Ajout de récupération:', {
|
||
collaborateurId,
|
||
nombreJours,
|
||
commentaire
|
||
});
|
||
|
||
// Récupérer les infos du collaborateur
|
||
const [collaborateur] = await conn.query(
|
||
'SELECT prenom, nom, email, Actif FROM CollaborateurAD WHERE id = ?',
|
||
[collaborateurId]
|
||
);
|
||
|
||
if (collaborateur.length === 0) {
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.status(404).json({
|
||
success: false,
|
||
message: 'Collaborateur non trouvé'
|
||
});
|
||
}
|
||
|
||
if (collaborateur[0].Actif === 0) {
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.status(403).json({
|
||
success: false,
|
||
message: 'Impossible d\'ajouter des jours à un compte désactivé'
|
||
});
|
||
}
|
||
|
||
const collab = collaborateur[0];
|
||
const currentYear = new Date().getFullYear();
|
||
|
||
// Récupérer l'ID du 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.status(500).json({
|
||
success: false,
|
||
message: 'Type de congé "Récupération" non trouvé'
|
||
});
|
||
}
|
||
|
||
const recupTypeId = recupType[0].Id;
|
||
|
||
// Vérifier si le compteur existe déjà
|
||
const [compteurExisting] = await conn.query(
|
||
`SELECT Id, Total, Solde FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, recupTypeId, currentYear]
|
||
);
|
||
|
||
if (compteurExisting.length > 0) {
|
||
// Mettre à jour le compteur existant
|
||
await conn.query(
|
||
`UPDATE CompteurConges
|
||
SET Total = Total + ?,
|
||
Solde = Solde + ?,
|
||
DerniereMiseAJour = NOW()
|
||
WHERE Id = ?`,
|
||
[nombreJours, nombreJours, compteurExisting[0].Id]
|
||
);
|
||
|
||
console.log(`✅ Compteur Récupération mis à jour: +${nombreJours}j (nouveau solde: ${(parseFloat(compteurExisting[0].Solde) + nombreJours).toFixed(2)}j)`);
|
||
} else {
|
||
// Créer un nouveau compteur
|
||
await conn.query(
|
||
`INSERT INTO CompteurConges
|
||
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour)
|
||
VALUES (?, ?, ?, ?, ?, 0, NOW())`,
|
||
[collaborateurId, recupTypeId, currentYear, nombreJours, nombreJours]
|
||
);
|
||
|
||
console.log(`✅ Compteur Récupération créé: ${nombreJours}j`);
|
||
}
|
||
|
||
// Enregistrer dans l'historique
|
||
await conn.query(
|
||
`INSERT INTO HistoriqueActions (CollaborateurADId, Action, Details, DateAction)
|
||
VALUES (?, ?, ?, NOW())`,
|
||
[
|
||
req.user.id,
|
||
'Ajout récupération',
|
||
`Ajout de ${nombreJours}j de récupération pour ${collab.prenom} ${collab.nom}${commentaire ? ` - ${commentaire}` : ''}`
|
||
]
|
||
);
|
||
|
||
await conn.commit();
|
||
|
||
// Notifier les clients SSE
|
||
notifyClients({
|
||
type: 'compteur-updated',
|
||
collaborateurId: collaborateurId,
|
||
typeConge: 'Récupération',
|
||
action: 'ajout',
|
||
nombreJours: nombreJours,
|
||
timestamp: new Date().toISOString()
|
||
}, collaborateurId);
|
||
|
||
// Envoyer webhook au serveur collaborateurs
|
||
try {
|
||
await webhookManager.sendWebhook(
|
||
WEBHOOKS.COLLABORATEURS_URL,
|
||
EVENTS.COMPTEUR_UPDATED,
|
||
{ collaborateurId: collaborateurId }
|
||
);
|
||
} catch (webhookError) {
|
||
console.error('❌ Erreur envoi webhook (non bloquant):', webhookError.message);
|
||
}
|
||
|
||
// Envoyer un email de notification au collaborateur
|
||
const accessToken = await getGraphToken();
|
||
if (accessToken && collab.email) {
|
||
const fromEmail = 'noreply@ensup.eu';
|
||
const subject = '✅ Jours de récupération ajoutés';
|
||
const body = `
|
||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||
<div style="background-color: #8b5cf6; color: white; padding: 20px; border-radius: 8px 8px 0 0;">
|
||
<h2 style="margin: 0;">✅ Jours de récupération ajoutés</h2>
|
||
</div>
|
||
<div style="background-color: #f9fafb; padding: 20px; border: 1px solid #e5e7eb;">
|
||
<p style="font-size: 16px;">Bonjour <strong>${collab.prenom} ${collab.nom}</strong>,</p>
|
||
<p>Des jours de récupération ont été ajoutés à votre compteur.</p>
|
||
<div style="background-color: white; border-left: 4px solid #8b5cf6; padding: 15px; margin: 20px 0;">
|
||
<p><strong>Jours ajoutés :</strong> ${nombreJours} jour${nombreJours > 1 ? 's' : ''}</p>
|
||
${commentaire ? `<p><strong>Motif :</strong> ${commentaire}</p>` : ''}
|
||
<p><strong>Année :</strong> ${currentYear}</p>
|
||
</div>
|
||
<p style="color: #6b7280;">Ces jours sont désormais disponibles dans votre solde de récupération.</p>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
try {
|
||
await sendMailGraph(accessToken, fromEmail, collab.email, subject, body);
|
||
console.log('✅ Email de notification envoyé');
|
||
} catch (emailError) {
|
||
console.error('❌ Erreur envoi email:', emailError);
|
||
}
|
||
}
|
||
|
||
conn.release();
|
||
|
||
res.json({
|
||
success: true,
|
||
message: 'Jours de récupération ajoutés avec succès',
|
||
collaborateur: `${collab.prenom} ${collab.nom}`,
|
||
nombreJours: nombreJours,
|
||
annee: currentYear
|
||
});
|
||
|
||
} catch (error) {
|
||
await conn.rollback();
|
||
if (conn) conn.release();
|
||
console.error('❌ Erreur ajout récupération:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur serveur',
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
// ================================================
|
||
// ROUTE POUR RETIRER DES JOURS DE RÉCUPÉRATION
|
||
// ================================================
|
||
|
||
app.post('/compteurs/retirer-recup', authenticateToken, async (req, res) => {
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
await conn.beginTransaction();
|
||
|
||
if (!['Admin', 'RH'].includes(req.user.role)) {
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.status(403).json({ error: 'Accès non autorisé' });
|
||
}
|
||
|
||
const { collaborateurId, nombreJours, commentaire } = req.body;
|
||
|
||
if (!collaborateurId || !nombreJours || nombreJours <= 0) {
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.json({
|
||
success: false,
|
||
message: 'Données invalides'
|
||
});
|
||
}
|
||
|
||
console.log('📤 Retrait de récupération:', {
|
||
collaborateurId,
|
||
nombreJours,
|
||
commentaire
|
||
});
|
||
|
||
// Récupérer les infos du collaborateur
|
||
const [collaborateur] = await conn.query(
|
||
'SELECT prenom, nom, email FROM CollaborateurAD WHERE id = ?',
|
||
[collaborateurId]
|
||
);
|
||
|
||
if (collaborateur.length === 0) {
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.status(404).json({
|
||
success: false,
|
||
message: 'Collaborateur non trouvé'
|
||
});
|
||
}
|
||
|
||
const collab = collaborateur[0];
|
||
const currentYear = new Date().getFullYear();
|
||
|
||
// Récupérer l'ID du 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.status(500).json({
|
||
success: false,
|
||
message: 'Type de congé "Récupération" non trouvé'
|
||
});
|
||
}
|
||
|
||
const recupTypeId = recupType[0].Id;
|
||
|
||
// Vérifier le solde actuel
|
||
const [compteur] = await conn.query(
|
||
`SELECT Id, Total, Solde FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, recupTypeId, currentYear]
|
||
);
|
||
|
||
if (compteur.length === 0 || compteur[0].Solde < nombreJours) {
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.status(400).json({
|
||
success: false,
|
||
message: 'Solde de récupération insuffisant',
|
||
soldeActuel: compteur.length > 0 ? compteur[0].Solde : 0
|
||
});
|
||
}
|
||
|
||
// Retirer les jours
|
||
await conn.query(
|
||
`UPDATE CompteurConges
|
||
SET Total = GREATEST(0, Total - ?),
|
||
Solde = GREATEST(0, Solde - ?),
|
||
DerniereMiseAJour = NOW()
|
||
WHERE Id = ?`,
|
||
[nombreJours, nombreJours, compteur[0].Id]
|
||
);
|
||
|
||
console.log(`✅ ${nombreJours}j retirés du compteur Récupération`);
|
||
|
||
// Enregistrer dans l'historique
|
||
await conn.query(
|
||
`INSERT INTO HistoriqueActions (CollaborateurADId, Action, Details, DateAction)
|
||
VALUES (?, ?, ?, NOW())`,
|
||
[
|
||
req.user.id,
|
||
'Retrait récupération',
|
||
`Retrait de ${nombreJours}j de récupération pour ${collab.prenom} ${collab.nom}${commentaire ? ` - ${commentaire}` : ''}`
|
||
]
|
||
);
|
||
|
||
await conn.commit();
|
||
|
||
// Notifier les clients SSE
|
||
notifyClients({
|
||
type: 'compteur-updated',
|
||
collaborateurId: collaborateurId,
|
||
typeConge: 'Récupération',
|
||
action: 'retrait',
|
||
nombreJours: nombreJours,
|
||
timestamp: new Date().toISOString()
|
||
}, collaborateurId);
|
||
|
||
// Envoyer webhook
|
||
try {
|
||
await webhookManager.sendWebhook(
|
||
WEBHOOKS.COLLABORATEURS_URL,
|
||
EVENTS.COMPTEUR_UPDATED,
|
||
{ collaborateurId: collaborateurId }
|
||
);
|
||
} catch (webhookError) {
|
||
console.error('❌ Erreur envoi webhook (non bloquant):', webhookError.message);
|
||
}
|
||
|
||
conn.release();
|
||
|
||
res.json({
|
||
success: true,
|
||
message: 'Jours de récupération retirés avec succès',
|
||
collaborateur: `${collab.prenom} ${collab.nom}`,
|
||
nombreJours: nombreJours,
|
||
annee: currentYear
|
||
});
|
||
|
||
} catch (error) {
|
||
await conn.rollback();
|
||
if (conn) conn.release();
|
||
console.error('❌ Erreur retrait récupération:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur serveur',
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
app.get('/equipes', authenticateToken, async (req, res) => {
|
||
try {
|
||
const [equipes] = await pool.query(`
|
||
SELECT
|
||
s.Id,
|
||
s.Nom as nomService,
|
||
s.Nom as service,
|
||
COUNT(DISTINCT CASE WHEN (ca.Actif = 1 OR ca.Actif IS NULL) THEN ca.id END) as nombreMembres,
|
||
COUNT(DISTINCT CASE WHEN dc.Statut = 'En attente' AND (ca.Actif = 1 OR ca.Actif IS NULL) THEN dc.Id END) as demandesEnAttente,
|
||
COALESCE(
|
||
-- D'abord essayer ServiceAffectation
|
||
(SELECT GROUP_CONCAT(DISTINCT c1.Nom ORDER BY c1.Nom SEPARATOR ',')
|
||
FROM ServiceAffectation sa1
|
||
JOIN Campus c1 ON sa1.CampusId = c1.Id
|
||
WHERE sa1.ServiceId = s.Id),
|
||
-- Sinon utiliser les campus des collaborateurs
|
||
(SELECT GROUP_CONCAT(DISTINCT c2.Nom ORDER BY c2.Nom SEPARATOR ',')
|
||
FROM CollaborateurAD ca2
|
||
JOIN Campus c2 ON ca2.CampusId = c2.Id
|
||
WHERE ca2.ServiceId = s.Id AND (ca2.Actif = 1 OR ca2.Actif IS NULL)),
|
||
'Non assigné'
|
||
) as campus
|
||
FROM Services s
|
||
LEFT JOIN CollaborateurAD ca ON s.Id = ca.ServiceId AND (ca.Actif = 1 OR ca.Actif IS NULL)
|
||
LEFT JOIN DemandeConge dc ON ca.id = dc.CollaborateurADId AND dc.Statut = 'En attente'
|
||
GROUP BY s.Id, s.Nom
|
||
HAVING nombreMembres > 0 OR EXISTS (
|
||
SELECT 1 FROM ServiceAffectation sa2 WHERE sa2.ServiceId = s.Id
|
||
)
|
||
ORDER BY s.Nom
|
||
`);
|
||
|
||
res.json(equipes);
|
||
} catch (error) {
|
||
console.error('❌ Erreur /api/equipes:', error);
|
||
res.status(500).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
app.get('/equipes/:id', authenticateToken, async (req, res) => {
|
||
try {
|
||
const [equipes] = await pool.query(`
|
||
SELECT
|
||
s.Id,
|
||
s.Nom,
|
||
s.Nom as nomService,
|
||
COUNT(DISTINCT CASE WHEN (ca.Actif = 1 OR ca.Actif IS NULL) THEN ca.id END) as nombreMembres,
|
||
COUNT(DISTINCT CASE WHEN dc.Statut = 'En attente' AND (ca.Actif = 1 OR ca.Actif IS NULL) THEN dc.Id END) as demandesEnAttente,
|
||
COALESCE(GROUP_CONCAT(DISTINCT c.Nom), 'Non assigné') as campus
|
||
FROM Services s
|
||
LEFT JOIN CollaborateurAD ca ON s.Id = ca.ServiceId
|
||
LEFT JOIN DemandeConge dc ON ca.id = dc.CollaborateurADId
|
||
LEFT JOIN ServiceAffectation sa ON s.Id = sa.ServiceId
|
||
LEFT JOIN Campus c ON sa.CampusId = c.Id
|
||
WHERE s.Id = ?
|
||
GROUP BY s.Id, s.Nom
|
||
`, [req.params.id]);
|
||
|
||
if (equipes.length === 0) {
|
||
return res.status(404).json({ error: 'Service non trouvé' });
|
||
}
|
||
|
||
res.json(equipes[0]);
|
||
} catch (error) {
|
||
res.status(500).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
app.get('/equipes/:id/membres', authenticateToken, async (req, res) => {
|
||
try {
|
||
const [membres] = await pool.query(`
|
||
SELECT
|
||
ca.id,
|
||
CONCAT(ca.prenom, ' ', ca.nom) as nom,
|
||
ca.email,
|
||
ca.role,
|
||
ca.Actif,
|
||
ca.DateSortie,
|
||
COALESCE(cc.Solde, 0) as soldeCP,
|
||
COALESCE(cc2.Solde, 0) as soldeRTT
|
||
FROM CollaborateurAD ca
|
||
LEFT JOIN CompteurConges cc ON ca.id = cc.CollaborateurADId
|
||
AND cc.TypeCongeId = 1
|
||
AND cc.Annee = YEAR(NOW())
|
||
LEFT JOIN CompteurConges cc2 ON ca.id = cc2.CollaborateurADId
|
||
AND cc2.TypeCongeId = 2
|
||
AND cc2.Annee = YEAR(NOW())
|
||
WHERE ca.ServiceId = ?
|
||
AND (ca.Actif = 1 OR ca.Actif IS NULL)
|
||
ORDER BY ca.nom, ca.prenom
|
||
`, [req.params.id]);
|
||
|
||
res.json(membres);
|
||
} catch (error) {
|
||
res.status(500).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
app.get('/stats', authenticateToken, async (req, res) => {
|
||
try {
|
||
const [stats] = await pool.query(`
|
||
SELECT
|
||
COUNT(CASE WHEN Statut = 'En attente' AND (ca.Actif = 1 OR ca.Actif IS NULL) THEN 1 END) as enAttente,
|
||
COUNT(CASE WHEN Statut = 'Validée' AND MONTH(DateValidation) = MONTH(NOW()) AND (ca.Actif = 1 OR ca.Actif IS NULL) THEN 1 END) as valideeCeMois,
|
||
COUNT(DISTINCT ca.ServiceId) as nombreEquipes
|
||
FROM DemandeConge dc
|
||
JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id
|
||
WHERE (ca.Actif = 1 OR ca.Actif IS NULL)
|
||
`);
|
||
|
||
res.json(stats[0]);
|
||
} catch (error) {
|
||
res.status(500).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
app.get('/historique', authenticateToken, async (req, res) => {
|
||
try {
|
||
const { dateDebut, dateFin, action } = req.query;
|
||
|
||
let query = `
|
||
SELECT
|
||
ha.Id,
|
||
CONCAT(ca.prenom, ' ', ca.nom) AS collaborateur,
|
||
ha.Action,
|
||
ha.Details,
|
||
ha.DateAction,
|
||
ha.AdresseIP,
|
||
ha.DemandeCongeId
|
||
FROM HistoriqueActions ha
|
||
JOIN CollaborateurAD ca ON ha.CollaborateurADId = ca.id
|
||
WHERE 1=1
|
||
`;
|
||
|
||
const params = [];
|
||
|
||
if (dateDebut) {
|
||
query += ' AND ha.DateAction >= ?';
|
||
params.push(dateDebut);
|
||
}
|
||
|
||
if (dateFin) {
|
||
query += ' AND ha.DateAction <= ?';
|
||
params.push(dateFin);
|
||
}
|
||
|
||
if (action && action !== 'all') {
|
||
query += ' AND ha.Action = ?';
|
||
params.push(action);
|
||
}
|
||
|
||
query += ' ORDER BY ha.DateAction DESC LIMIT 500';
|
||
|
||
const [historique] = await pool.query(query, params);
|
||
res.json(historique);
|
||
} catch (error) {
|
||
res.status(500).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
// ================================================
|
||
// ROUTES COMPTES-RENDUS D'ACTIVITÉS (RH)
|
||
// ================================================
|
||
|
||
// GET - Stats globales des comptes-rendus
|
||
app.get('/compte-rendu/stats', authenticateToken, async (req, res) => {
|
||
const { annee, mois, service_id } = req.query;
|
||
|
||
console.log('📊 Route /api/compte-rendu/stats appelée');
|
||
console.log(' Année:', annee, 'Mois:', mois, 'Service:', service_id);
|
||
|
||
try {
|
||
let query = `
|
||
SELECT
|
||
COUNT(DISTINCT ca.id) as totalCollaborateurs,
|
||
COALESCE(SUM(CASE WHEN crm.Verrouille = 1 OR EXISTS(
|
||
SELECT 1 FROM CompteRenduActivites cra
|
||
WHERE cra.CollaborateurADId = ca.id
|
||
AND cra.Annee = ? AND cra.Mois = ?
|
||
) THEN 1 ELSE 0 END), 0) as mensuelsValides,
|
||
COALESCE(COUNT(DISTINCT ca.id) - SUM(CASE WHEN crm.Verrouille = 1 OR EXISTS(
|
||
SELECT 1 FROM CompteRenduActivites cra
|
||
WHERE cra.CollaborateurADId = ca.id
|
||
AND cra.Annee = ? AND cra.Mois = ?
|
||
) THEN 1 ELSE 0 END), 0) as mensuelsEnAttente
|
||
FROM CollaborateurAD ca
|
||
LEFT JOIN CompteRenduMensuel crm ON ca.id = crm.CollaborateurADId
|
||
AND crm.Annee = ?
|
||
AND crm.Mois = ?
|
||
WHERE ca.TypeContrat = 'forfait_jour'
|
||
AND (ca.Actif = 1 OR ca.Actif IS NULL)
|
||
`;
|
||
|
||
const params = [annee, mois, annee, mois, annee, mois];
|
||
|
||
// Ajouter le filtre service si fourni
|
||
if (service_id && service_id !== 'all') {
|
||
query += ' AND ca.ServiceId = ?';
|
||
params.push(service_id);
|
||
}
|
||
|
||
const [stats] = await pool.query(query, params);
|
||
|
||
console.log('✅ Stats récupérées:', stats[0]);
|
||
|
||
res.json({
|
||
totalCollaborateurs: stats[0].totalCollaborateurs || 0,
|
||
mensuelsValides: stats[0].mensuelsValides || 0,
|
||
mensuelsEnAttente: stats[0].mensuelsEnAttente || 0
|
||
});
|
||
} catch (error) {
|
||
console.error('❌ Erreur stats:', error);
|
||
console.error(' Message:', error.message);
|
||
console.error(' Code:', error.code);
|
||
|
||
// Retourner des stats par défaut en cas d'erreur
|
||
res.json({
|
||
totalCollaborateurs: 0,
|
||
mensuelsValides: 0,
|
||
mensuelsEnAttente: 0,
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
|
||
// Liste des collaborateurs en forfait jour avec leur statut de saisie
|
||
app.get('/compte-rendu/collaborateurs', authenticateToken, async (req, res) => {
|
||
try {
|
||
if (!['RH', 'Admin'].includes(req.user.role)) {
|
||
return res.status(403).json({ error: 'Accès non autorisé' });
|
||
}
|
||
|
||
const { annee, mois, service_id } = req.query;
|
||
|
||
let query = `
|
||
SELECT
|
||
ca.id,
|
||
ca.nom,
|
||
ca.prenom,
|
||
CONCAT(ca.prenom, ' ', ca.nom) AS nomComplet,
|
||
ca.email,
|
||
s.Nom AS service,
|
||
s.Id AS serviceId,
|
||
crm.Statut as statutMensuel,
|
||
crm.Verrouille,
|
||
crm.NbJoursTravailles,
|
||
crm.NbJoursNonRespectsReposQuotidien,
|
||
crm.NbJoursNonRespectsReposHebdo,
|
||
crm.DateValidation,
|
||
(SELECT COUNT(*) FROM CompteRenduActivites cra
|
||
WHERE cra.CollaborateurADId = ca.id
|
||
AND cra.Annee = ? AND cra.Mois = ?) as nbJoursSaisis
|
||
FROM CollaborateurAD ca
|
||
LEFT JOIN Services s ON ca.ServiceId = s.Id
|
||
LEFT JOIN CompteRenduMensuel crm ON ca.id = crm.CollaborateurADId
|
||
AND crm.Annee = ? AND crm.Mois = ?
|
||
WHERE ca.TypeContrat = 'forfait_jour'
|
||
AND (ca.Actif = 1 OR ca.Actif IS NULL)
|
||
`;
|
||
|
||
const params = [annee, mois, annee, mois];
|
||
|
||
// 🆕 Ajouter le filtre service si fourni
|
||
if (service_id && service_id !== 'all') {
|
||
query += ' AND ca.ServiceId = ?';
|
||
params.push(service_id);
|
||
}
|
||
|
||
query += ' ORDER BY ca.nom, ca.prenom';
|
||
|
||
const [collaborateurs] = await pool.query(query, params);
|
||
|
||
res.json(collaborateurs);
|
||
} catch (error) {
|
||
console.error('Erreur liste collaborateurs compte-rendu:', error);
|
||
res.status(500).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
// Verrouiller un compte-rendu (depuis RH)
|
||
app.post('/compte-rendu/verrouiller', authenticateToken, async (req, res) => {
|
||
try {
|
||
if (!['RH', 'Admin'].includes(req.user.role)) {
|
||
return res.status(403).json({ error: 'Accès non autorisé' });
|
||
}
|
||
|
||
const { collaborateur_id, annee, mois } = req.body;
|
||
|
||
console.log('🔒 Verrouillage manuel par RH');
|
||
console.log(' Collaborateur:', collaborateur_id, 'Année:', annee, 'Mois:', mois);
|
||
|
||
// Calculer les stats
|
||
const [stats] = await pool.query(`
|
||
SELECT
|
||
COUNT(*) as nbJours,
|
||
SUM(CASE WHEN NOT ReposQuotidienRespect THEN 1 ELSE 0 END) as nbNonRespectQuotidien,
|
||
SUM(CASE WHEN NOT ReposHebdomadaireRespect THEN 1 ELSE 0 END) as nbNonRespectHebdo
|
||
FROM CompteRenduActivites
|
||
WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ? AND JourTravaille = TRUE
|
||
`, [collaborateur_id, annee, mois]);
|
||
|
||
// Créer ou mettre à jour le mensuel
|
||
await pool.query(`
|
||
INSERT INTO CompteRenduMensuel
|
||
(CollaborateurADId, Annee, Mois, NbJoursTravailles,
|
||
NbJoursNonRespectsReposQuotidien, NbJoursNonRespectsReposHebdo,
|
||
Statut, DateValidation, Verrouille)
|
||
VALUES (?, ?, ?, ?, ?, ?, 'Validé', NOW(), TRUE)
|
||
ON DUPLICATE KEY UPDATE
|
||
NbJoursTravailles = VALUES(NbJoursTravailles),
|
||
NbJoursNonRespectsReposQuotidien = VALUES(NbJoursNonRespectsReposQuotidien),
|
||
NbJoursNonRespectsReposHebdo = VALUES(NbJoursNonRespectsReposHebdo),
|
||
Statut = 'Validé',
|
||
DateValidation = NOW(),
|
||
Verrouille = TRUE,
|
||
DateModification = NOW()
|
||
`, [collaborateur_id, annee, mois, stats[0].nbJours, stats[0].nbNonRespectQuotidien, stats[0].nbNonRespectHebdo]);
|
||
|
||
// Enregistrer l'action dans l'historique
|
||
await pool.query(`
|
||
INSERT INTO HistoriqueActions (CollaborateurADId, Action, Details, DateAction)
|
||
VALUES (?, ?, ?, NOW())
|
||
`, [
|
||
req.user.id,
|
||
'Verrouillage compte-rendu',
|
||
`Verrouillage du mois ${mois}/${annee} pour le collaborateur ${collaborateur_id}`
|
||
]);
|
||
|
||
console.log(' ✅ Mois verrouillé avec succès');
|
||
|
||
res.json({
|
||
success: true,
|
||
message: 'Compte-rendu verrouillé avec succès'
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('❌ Erreur verrouillage compte-rendu:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
// Déverrouiller un compte-rendu (depuis RH)
|
||
app.post('/compte-rendu/deverrouiller', authenticateToken, async (req, res) => {
|
||
try {
|
||
if (!['RH', 'Admin'].includes(req.user.role)) {
|
||
return res.status(403).json({ error: 'Accès non autorisé' });
|
||
}
|
||
|
||
const { collaborateur_id, annee, mois } = req.body;
|
||
|
||
console.log('🔓 Déverrouillage manuel par RH');
|
||
console.log(' Collaborateur:', collaborateur_id, 'Année:', annee, 'Mois:', mois);
|
||
|
||
// Mettre à jour le statut
|
||
await pool.query(`
|
||
UPDATE CompteRenduMensuel
|
||
SET Verrouille = FALSE,
|
||
Statut = 'En cours',
|
||
DateModification = NOW()
|
||
WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ?
|
||
`, [collaborateur_id, annee, mois]);
|
||
|
||
// Enregistrer l'action dans l'historique
|
||
await pool.query(`
|
||
INSERT INTO HistoriqueActions (CollaborateurADId, Action, Details, DateAction)
|
||
VALUES (?, ?, ?, NOW())
|
||
`, [
|
||
req.user.id,
|
||
'Déverrouillage compte-rendu',
|
||
`Déverrouillage du mois ${mois}/${annee} pour le collaborateur ${collaborateur_id}`
|
||
]);
|
||
|
||
console.log(' ✅ Mois déverrouillé avec succès');
|
||
|
||
res.json({
|
||
success: true,
|
||
message: 'Compte-rendu déverrouillé. Le collaborateur peut maintenant modifier ses saisies.'
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('❌ Erreur déverrouillage compte-rendu:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
|
||
// Export Excel de tous les comptes-rendus
|
||
app.get('/compte-rendu/export-excel', authenticateToken, async (req, res) => {
|
||
try {
|
||
if (!['RH', 'Admin'].includes(req.user.role)) {
|
||
return res.status(403).json({ error: 'Accès non autorisé' });
|
||
}
|
||
|
||
const { annee, mois, service_id } = req.query;
|
||
|
||
let query = `
|
||
SELECT
|
||
CONCAT(ca.prenom, ' ', ca.nom) AS Collaborateur,
|
||
ca.email AS Email,
|
||
s.Nom AS Service,
|
||
crm.NbJoursTravailles AS 'Jours travaillés',
|
||
crm.NbJoursNonRespectsReposQuotidien AS 'Non-respect repos quotidien',
|
||
crm.NbJoursNonRespectsReposHebdo AS 'Non-respect repos hebdo',
|
||
crm.Statut AS Statut,
|
||
crm.Verrouille AS Verrouillé,
|
||
DATE_FORMAT(crm.DateValidation, '%d/%m/%Y %H:%i') AS 'Date validation'
|
||
FROM CollaborateurAD ca
|
||
LEFT JOIN Services s ON ca.ServiceId = s.Id
|
||
LEFT JOIN CompteRenduMensuel crm ON ca.id = crm.CollaborateurADId
|
||
AND crm.Annee = ? AND crm.Mois = ?
|
||
WHERE ca.TypeContrat = 'forfait_jour'
|
||
AND (ca.Actif = 1 OR ca.Actif IS NULL)
|
||
`;
|
||
|
||
const params = [annee, mois];
|
||
|
||
// 🆕 Ajouter le filtre service si fourni
|
||
if (service_id && service_id !== 'all') {
|
||
query += ' AND ca.ServiceId = ?';
|
||
params.push(service_id);
|
||
}
|
||
|
||
query += ' ORDER BY s.Nom, ca.nom, ca.prenom';
|
||
|
||
const [data] = await pool.query(query, params);
|
||
|
||
res.json(data);
|
||
} catch (error) {
|
||
console.error('Erreur export Excel:', error);
|
||
res.status(500).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
app.get('/compte-rendu/services', authenticateToken, async (req, res) => {
|
||
try {
|
||
if (!['RH', 'Admin'].includes(req.user.role)) {
|
||
return res.status(403).json({ error: 'Accès non autorisé' });
|
||
}
|
||
|
||
const [services] = await pool.query(`
|
||
SELECT
|
||
s.Id,
|
||
s.Nom,
|
||
COUNT(DISTINCT ca.id) as nombreForfaitJour
|
||
FROM Services s
|
||
LEFT JOIN CollaborateurAD ca ON s.Id = ca.ServiceId
|
||
AND ca.TypeContrat = 'forfait_jour'
|
||
AND (ca.Actif = 1 OR ca.Actif IS NULL)
|
||
GROUP BY s.Id, s.Nom
|
||
HAVING nombreForfaitJour > 0
|
||
ORDER BY s.Nom
|
||
`);
|
||
|
||
res.json(services);
|
||
} catch (error) {
|
||
console.error('Erreur liste services:', error);
|
||
res.status(500).json({ error: error.message });
|
||
}
|
||
});
|
||
app.get('/compte-rendu-activites', authenticateToken, async (req, res) => {
|
||
try {
|
||
const { user_id, annee, mois } = req.query;
|
||
|
||
console.log('📥 GET /api/compte-rendu-activites');
|
||
console.log(' User ID:', user_id, 'Année:', annee, 'Mois:', mois);
|
||
|
||
if (!user_id || !annee || !mois) {
|
||
return res.status(400).json({
|
||
success: false,
|
||
message: 'Paramètres manquants (user_id, annee, mois)'
|
||
});
|
||
}
|
||
|
||
// Récupérer les jours du compte-rendu
|
||
const [jours] = await pool.query(`
|
||
SELECT
|
||
Id,
|
||
CollaborateurADId,
|
||
JourDate,
|
||
JourTravaille,
|
||
ReposQuotidienRespect,
|
||
ReposHebdomadaireRespect,
|
||
CommentaireRepos,
|
||
DateSaisie,
|
||
DateModification,
|
||
Annee,
|
||
Mois
|
||
FROM CompteRenduActivites
|
||
WHERE CollaborateurADId = ?
|
||
AND Annee = ?
|
||
AND Mois = ?
|
||
ORDER BY JourDate ASC
|
||
`, [user_id, annee, mois]);
|
||
|
||
console.log(` ✅ ${jours.length} jours trouvés`);
|
||
|
||
// 🆕 SI DES JOURS EXISTENT, CRÉER/METTRE À JOUR AUTOMATIQUEMENT LE RÉCAPITULATIF MENSUEL VERROUILLÉ
|
||
let mensuelData = null;
|
||
|
||
if (jours.length > 0) {
|
||
// Calculer les statistiques
|
||
const nbJoursTravailles = jours.filter(j => j.JourTravaille).length;
|
||
const nbNonRespectQuotidien = jours.filter(j => !j.ReposQuotidienRespect && j.JourTravaille).length;
|
||
const nbNonRespectHebdo = jours.filter(j => !j.ReposHebdomadaireRespect && j.JourTravaille).length;
|
||
|
||
// Vérifier si le mensuel existe déjà
|
||
const [existingMensuel] = await pool.query(`
|
||
SELECT * FROM CompteRenduMensuel
|
||
WHERE CollaborateurADId = ?
|
||
AND Annee = ?
|
||
AND Mois = ?
|
||
LIMIT 1
|
||
`, [user_id, annee, mois]);
|
||
|
||
if (existingMensuel.length === 0) {
|
||
// 🆕 CRÉER AUTOMATIQUEMENT UN MENSUEL VERROUILLÉ
|
||
await pool.query(`
|
||
INSERT INTO CompteRenduMensuel
|
||
(CollaborateurADId, Annee, Mois, NbJoursTravailles,
|
||
NbJoursNonRespectsReposQuotidien, NbJoursNonRespectsReposHebdo,
|
||
Statut, DateValidation, Verrouille, DateModification)
|
||
VALUES (?, ?, ?, ?, ?, ?, 'Validé', NOW(), TRUE, NOW())
|
||
`, [user_id, annee, mois, nbJoursTravailles, nbNonRespectQuotidien, nbNonRespectHebdo]);
|
||
|
||
console.log(' ✅ Mensuel créé automatiquement et verrouillé');
|
||
|
||
mensuelData = {
|
||
CollaborateurADId: parseInt(user_id),
|
||
Annee: parseInt(annee),
|
||
Mois: parseInt(mois),
|
||
NbJoursTravailles: nbJoursTravailles,
|
||
NbJoursNonRespectsReposQuotidien: nbNonRespectQuotidien,
|
||
NbJoursNonRespectsReposHebdo: nbNonRespectHebdo,
|
||
Statut: 'Validé',
|
||
Verrouille: 1,
|
||
DateValidation: new Date(),
|
||
DateModification: new Date()
|
||
};
|
||
} else {
|
||
// 🆕 METTRE À JOUR LES STATS ET VERROUILLER AUTOMATIQUEMENT
|
||
await pool.query(`
|
||
UPDATE CompteRenduMensuel
|
||
SET NbJoursTravailles = ?,
|
||
NbJoursNonRespectsReposQuotidien = ?,
|
||
NbJoursNonRespectsReposHebdo = ?,
|
||
Statut = 'Validé',
|
||
Verrouille = TRUE,
|
||
DateValidation = NOW(),
|
||
DateModification = NOW()
|
||
WHERE CollaborateurADId = ?
|
||
AND Annee = ?
|
||
AND Mois = ?
|
||
`, [nbJoursTravailles, nbNonRespectQuotidien, nbNonRespectHebdo, user_id, annee, mois]);
|
||
|
||
console.log(' ✅ Mensuel mis à jour et verrouillé automatiquement');
|
||
|
||
mensuelData = {
|
||
...existingMensuel[0],
|
||
NbJoursTravailles: nbJoursTravailles,
|
||
NbJoursNonRespectsReposQuotidien: nbNonRespectQuotidien,
|
||
NbJoursNonRespectsReposHebdo: nbNonRespectHebdo,
|
||
Statut: 'Validé',
|
||
Verrouille: 1,
|
||
DateValidation: new Date(),
|
||
DateModification: new Date()
|
||
};
|
||
}
|
||
} else {
|
||
// Pas de jours saisis, vérifier quand même s'il existe un mensuel
|
||
const [existingMensuel] = await pool.query(`
|
||
SELECT * FROM CompteRenduMensuel
|
||
WHERE CollaborateurADId = ?
|
||
AND Annee = ?
|
||
AND Mois = ?
|
||
LIMIT 1
|
||
`, [user_id, annee, mois]);
|
||
|
||
mensuelData = existingMensuel.length > 0 ? existingMensuel[0] : null;
|
||
console.log(' ℹ️ Aucun jour saisi pour ce mois');
|
||
}
|
||
|
||
res.json({
|
||
success: true,
|
||
jours: jours,
|
||
mensuel: mensuelData,
|
||
collaborateurId: parseInt(user_id),
|
||
annee: parseInt(annee),
|
||
mois: parseInt(mois),
|
||
autoValidated: jours.length > 0 // Indicateur de validation automatique
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('❌ Erreur GET /api/compte-rendu-activites:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur serveur',
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
|
||
// ⭐ NOUVELLE ROUTE : Saisie de récupération par les RH pour un collaborateur
|
||
app.post('/compteurs/saisir-recup-collaborateur', authenticateToken, async (req, res) => {
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
await conn.beginTransaction();
|
||
|
||
if (!['Admin', 'RH'].includes(req.user.role)) {
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.status(403).json({ error: 'Accès non autorisé' });
|
||
}
|
||
|
||
const {
|
||
collaborateur_id,
|
||
date,
|
||
periode_journee, // 'Matin', 'Après-midi', 'Journée entière'
|
||
commentaire
|
||
} = req.body;
|
||
|
||
console.log('📥 === SAISIE RÉCUP PAR RH ===');
|
||
console.log('Collaborateur:', collaborateur_id);
|
||
console.log('Date:', date);
|
||
console.log('Période:', periode_journee);
|
||
|
||
if (!collaborateur_id || !date || !periode_journee) {
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.json({
|
||
success: false,
|
||
message: 'Données manquantes'
|
||
});
|
||
}
|
||
|
||
const dateObj = new Date(date);
|
||
|
||
// Calculer le nombre de jours selon la période
|
||
let nombre_heures;
|
||
switch (periode_journee) {
|
||
case 'Matin':
|
||
case 'Après-midi':
|
||
nombre_heures = 0.5;
|
||
break;
|
||
case 'Journée entière':
|
||
nombre_heures = 1;
|
||
break;
|
||
default:
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.json({
|
||
success: false,
|
||
message: 'Période invalide'
|
||
});
|
||
}
|
||
|
||
// Vérifier que ce jour/période 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'
|
||
AND dct.PeriodeJournee = ?
|
||
`, [collaborateur_id, date, periode_journee]);
|
||
|
||
if (existing.length > 0) {
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.json({
|
||
success: false,
|
||
message: `Cette date (${periode_journee}) a déjà été déclarée`
|
||
});
|
||
}
|
||
|
||
// Récupérer infos collaborateur
|
||
const [userInfo] = await conn.query(
|
||
'SELECT prenom, nom, email FROM CollaborateurAD WHERE id = ?',
|
||
[collaborateur_id]
|
||
);
|
||
|
||
if (userInfo.length === 0) {
|
||
await conn.rollback();
|
||
conn.release();
|
||
return res.json({
|
||
success: false,
|
||
message: 'Collaborateur 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
|
||
// 🎨 Récupérer le type Récupération avec sa couleur
|
||
const [recupType] = await conn.query(
|
||
'SELECT Id, CouleurHex 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é. Veuillez créer ce type de congé.'
|
||
});
|
||
}
|
||
|
||
const recupTypeId = recupType[0].Id;
|
||
const couleurRecup = recupType[0].CouleurHex || '#d946ef'; // Fuchsia par défaut
|
||
const currentYear = dateObj.getFullYear();
|
||
|
||
console.log(`🎨 Type Récupération trouvé - ID: ${recupTypeId}, Couleur: ${couleurRecup}`);
|
||
|
||
// CRÉER LA DEMANDE (validée automatiquement avec ValidateurADId)
|
||
const commentaireFinal = commentaire || `🎯Jour travaillé (${periode_journee}) - Saisi par RH`;
|
||
|
||
const [result] = await conn.query(`
|
||
INSERT INTO DemandeConge
|
||
(CollaborateurADId, DateDebut, DateFin,
|
||
Statut, DateDemande, DateValidation, ValidateurADId, Commentaire, NombreJours)
|
||
VALUES (?, ?, ?, 'Validée', NOW(), NOW(), ?, ?, ?)
|
||
`, [collaborateur_id, date, date, req.user.id, commentaireFinal, nombre_heures]);
|
||
|
||
const demandeId = result.insertId;
|
||
|
||
// SAUVEGARDER DANS DemandeCongeType
|
||
await conn.query(`
|
||
INSERT INTO DemandeCongeType
|
||
(DemandeCongeId, TypeCongeId, NombreJours, PeriodeJournee)
|
||
VALUES (?, ?, ?, ?)
|
||
`, [demandeId, recupTypeId, nombre_heures, periode_journee]);
|
||
|
||
// ACCUMULER DANS LE COMPTEUR
|
||
const [compteur] = await conn.query(`
|
||
SELECT Id, Total, Solde
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collaborateur_id, recupTypeId, currentYear]);
|
||
|
||
if (compteur.length > 0) {
|
||
await conn.query(`
|
||
UPDATE CompteurConges
|
||
SET Total = Total + ?,
|
||
Solde = Solde + ?,
|
||
DerniereMiseAJour = NOW()
|
||
WHERE Id = ?
|
||
`, [nombre_heures, nombre_heures, compteur[0].Id]);
|
||
|
||
console.log(`✅ Compteur mis à jour: ${parseFloat(compteur[0].Solde) + nombre_heures}j`);
|
||
} else {
|
||
await conn.query(`
|
||
INSERT INTO CompteurConges
|
||
(CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour)
|
||
VALUES (?, ?, ?, ?, ?, 0, NOW())
|
||
`, [collaborateur_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 POUR LE COLLABORATEUR
|
||
await conn.query(`
|
||
INSERT INTO Notifications
|
||
(CollaborateurADId, Type, Titre, Message, DemandeCongeId, DateCreation, Lu)
|
||
VALUES (?, 'Success', '✅ Récupération enregistrée', ?, ?, NOW(), 0)
|
||
`, [
|
||
collaborateur_id,
|
||
`Date ${dateFormatted} (${periode_journee}) enregistrée par RH : +${nombre_heures}j de récupération`,
|
||
demandeId
|
||
]);
|
||
|
||
// ENREGISTRER L'ACTION DANS L'HISTORIQUE
|
||
await conn.query(
|
||
`INSERT INTO HistoriqueActions (CollaborateurADId, Action, Details, DateAction)
|
||
VALUES (?, ?, ?, NOW())`,
|
||
[
|
||
req.user.id,
|
||
'Saisie récupération RH',
|
||
`Ajout de ${nombre_heures}j (${periode_journee}) pour ${userName} - ${dateFormatted}`
|
||
]
|
||
);
|
||
|
||
await conn.commit();
|
||
|
||
// ⭐ ENVOYER WEBHOOK AU SERVEUR COLLABORATEURS
|
||
// ⭐ ENVOYER WEBHOOKS AU SERVEUR COLLABORATEURS
|
||
// ⭐ ENVOYER WEBHOOKS AU SERVEUR COLLABORATEURS
|
||
try {
|
||
// 1. Notifier la création de la demande validée (IMPORTANT pour le calendrier)
|
||
await webhookManager.sendWebhook(
|
||
WEBHOOKS.COLLABORATEURS_URL,
|
||
EVENTS.DEMANDE_VALIDATED,
|
||
{
|
||
demandeId: demandeId,
|
||
statut: 'Validée',
|
||
collaborateurId: collaborateur_id,
|
||
validateurId: req.user.id,
|
||
typeConge: 'Récupération',
|
||
type: 'Récupération',// ✅ Envoyer le nom exact
|
||
couleurHex: couleurRecup, // ⭐ AJOUTER LA COULEUR
|
||
date: date,
|
||
periode: periode_journee,
|
||
nombre_heures: nombre_heures
|
||
}
|
||
);
|
||
|
||
// 2. Notifier la mise à jour du compteur
|
||
await webhookManager.sendWebhook(
|
||
WEBHOOKS.COLLABORATEURS_URL,
|
||
EVENTS.COMPTEUR_UPDATED,
|
||
{
|
||
collaborateurId: collaborateur_id,
|
||
typeUpdate: 'recup_ajoutee',
|
||
date: date,
|
||
periode: periode_journee,
|
||
jours: nombre_heures
|
||
}
|
||
);
|
||
|
||
console.log('✅ Webhooks envoyés au serveur collaborateurs');
|
||
} catch (webhookError) {
|
||
console.error('❌ Erreur envoi webhook (non bloquant):', webhookError.message);
|
||
}
|
||
// Notifier les clients SSE (IMPORTANT pour rafraîchir le calendrier)
|
||
notifyClients({
|
||
type: 'demande-validated',
|
||
demandeId: demandeId,
|
||
statut: 'Validée',
|
||
collaborateurId: collaborateur_id,
|
||
typeConge: 'Récupération',
|
||
timestamp: new Date().toISOString()
|
||
}, collaborateur_id);
|
||
|
||
notifyClients({
|
||
type: 'compteur-updated',
|
||
collaborateurId: collaborateur_id,
|
||
typeConge: 'Récupération',
|
||
action: 'ajout_rh',
|
||
nombreJours: nombre_heures,
|
||
timestamp: new Date().toISOString()
|
||
}, collaborateur_id);
|
||
|
||
// ENVOYER EMAIL AU COLLABORATEUR
|
||
const accessToken = await getGraphToken();
|
||
if (accessToken && user.email) {
|
||
const fromEmail = 'noreply@ensup.eu';
|
||
const subject = '✅ Récupération enregistrée par RH';
|
||
const body = `
|
||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||
<div style="background-color: #8b5cf6; color: white; padding: 20px; border-radius: 8px 8px 0 0;">
|
||
<h2 style="margin: 0;">✅ Récupération ajoutée</h2>
|
||
</div>
|
||
<div style="background-color: #f9fafb; padding: 20px; border: 1px solid #e5e7eb;">
|
||
<p style="font-size: 16px;">Bonjour <strong>${userName}</strong>,</p>
|
||
<p>Le service RH a enregistré une récupération pour vous.</p>
|
||
<div style="background-color: white; border-left: 4px solid #8b5cf6; padding: 15px; margin: 20px 0;">
|
||
<p><strong>Date :</strong> ${dateFormatted}</p>
|
||
<p><strong>Période :</strong> ${periode_journee}</p>
|
||
<p><strong>Jours ajoutés :</strong> ${nombre_heures} jour${nombre_heures > 1 ? 's' : ''}</p>
|
||
${commentaire ? `<p><strong>Commentaire :</strong> ${commentaire}</p>` : ''}
|
||
</div>
|
||
<p style="color: #6b7280;">Ces jours sont désormais disponibles dans votre solde de récupération.</p>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
try {
|
||
await sendMailGraph(accessToken, fromEmail, user.email, subject, body);
|
||
console.log('✅ Email de notification envoyé');
|
||
} catch (emailError) {
|
||
console.error('❌ Erreur envoi email:', emailError);
|
||
}
|
||
}
|
||
|
||
conn.release();
|
||
res.json({
|
||
success: true,
|
||
message: `Récupération enregistrée pour ${userName}`,
|
||
collaborateur: userName,
|
||
date: dateFormatted,
|
||
periode: periode_journee,
|
||
jours_ajoutes: nombre_heures,
|
||
demande_id: demandeId,
|
||
couleur: couleurRecup // 🎨 Retourner la couleur
|
||
});
|
||
|
||
} catch (error) {
|
||
await conn.rollback();
|
||
if (conn) conn.release();
|
||
console.error('❌ Erreur saisie récup RH:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur serveur',
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
// 🆕 BONUS : Route pour exporter le PDF d'un compte-rendu
|
||
app.get('/export-compte-rendu-pdf', authenticateToken, async (req, res) => {
|
||
try {
|
||
if (!['RH', 'Admin'].includes(req.user.role)) {
|
||
return res.status(403).json({ error: 'Accès non autorisé' });
|
||
}
|
||
|
||
const { user_id, annee, mois } = req.query;
|
||
|
||
console.log('📄 Export PDF compte-rendu');
|
||
console.log(' User ID:', user_id, 'Année:', annee, 'Mois:', mois);
|
||
|
||
// Récupérer les infos du collaborateur
|
||
const [collaborateur] = await pool.query(`
|
||
SELECT
|
||
ca.id,
|
||
ca.nom,
|
||
ca.prenom,
|
||
ca.email,
|
||
s.Nom as service
|
||
FROM CollaborateurAD ca
|
||
LEFT JOIN Services s ON ca.ServiceId = s.Id
|
||
WHERE ca.id = ?
|
||
`, [user_id]);
|
||
|
||
if (collaborateur.length === 0) {
|
||
return res.status(404).json({ error: 'Collaborateur non trouvé' });
|
||
}
|
||
|
||
const collab = collaborateur[0];
|
||
|
||
// Récupérer les jours
|
||
const [jours] = await pool.query(`
|
||
SELECT
|
||
JourDate,
|
||
JourTravaille,
|
||
ReposQuotidienRespect,
|
||
ReposHebdomadaireRespect,
|
||
CommentaireRepos
|
||
FROM CompteRenduActivites
|
||
WHERE CollaborateurADId = ?
|
||
AND Annee = ?
|
||
AND Mois = ?
|
||
ORDER BY JourDate ASC
|
||
`, [user_id, annee, mois]);
|
||
|
||
// Récupérer le mensuel
|
||
const [mensuel] = await pool.query(`
|
||
SELECT
|
||
NbJoursTravailles,
|
||
NbJoursNonRespectsReposQuotidien,
|
||
NbJoursNonRespectsReposHebdo,
|
||
Statut,
|
||
Verrouille,
|
||
DateValidation
|
||
FROM CompteRenduMensuel
|
||
WHERE CollaborateurADId = ?
|
||
AND Annee = ?
|
||
AND Mois = ?
|
||
LIMIT 1
|
||
`, [user_id, annee, mois]);
|
||
|
||
const monthNames = ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin',
|
||
'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'];
|
||
|
||
// GÉNÉRER LE PDF
|
||
const doc = new PDFDocument({
|
||
margin: 50,
|
||
size: 'A4'
|
||
});
|
||
|
||
// Headers pour le téléchargement
|
||
res.setHeader('Content-Type', 'application/pdf');
|
||
res.setHeader('Content-Disposition', `attachment; filename=compte-rendu-${collab.nom}-${annee}-${String(mois).padStart(2, '0')}.pdf`);
|
||
|
||
// Pipe le PDF directement dans la réponse
|
||
doc.pipe(res);
|
||
|
||
// ===== EN-TÊTE =====
|
||
doc.fontSize(20)
|
||
.fillColor('#0ea5e9')
|
||
.text('Compte-rendu d\'activités', { align: 'center' })
|
||
.moveDown(0.5);
|
||
|
||
doc.fontSize(14)
|
||
.fillColor('#6b7280')
|
||
.text(`${monthNames[parseInt(mois) - 1]} ${annee}`, { align: 'center' })
|
||
.moveDown(2);
|
||
|
||
// ===== INFORMATIONS COLLABORATEUR =====
|
||
doc.fontSize(12)
|
||
.fillColor('#000000')
|
||
.text('Collaborateur', { underline: true })
|
||
.moveDown(0.3);
|
||
|
||
doc.fontSize(10)
|
||
.fillColor('#374151')
|
||
.text(`Nom: ${collab.prenom} ${collab.nom}`)
|
||
.text(`Email: ${collab.email}`)
|
||
.text(`Service: ${collab.service || 'Non défini'}`)
|
||
.moveDown(1.5);
|
||
|
||
// ===== RÉSUMÉ MENSUEL =====
|
||
if (mensuel.length > 0) {
|
||
const m = mensuel[0];
|
||
|
||
doc.fontSize(12)
|
||
.fillColor('#000000')
|
||
.text('Résumé du mois', { underline: true })
|
||
.moveDown(0.3);
|
||
|
||
// Rectangle de fond pour les stats
|
||
const y = doc.y;
|
||
doc.rect(50, y, 495, 80)
|
||
.fillAndStroke('#f0f9ff', '#0ea5e9');
|
||
|
||
doc.fillColor('#000000')
|
||
.fontSize(10)
|
||
.text(`Jours travaillés: ${m.NbJoursTravailles || 0}`, 60, y + 10)
|
||
.text(`Non-respect repos quotidien: ${m.NbJoursNonRespectsReposQuotidien || 0}`, 60, y + 30)
|
||
.text(`Non-respect repos hebdomadaire: ${m.NbJoursNonRespectsReposHebdo || 0}`, 60, y + 50);
|
||
|
||
const statutText = m.Verrouille ? 'Verrouillé' : 'Ouvert';
|
||
doc.text(`Statut: ${statutText}`, 300, y + 10);
|
||
|
||
if (m.DateValidation) {
|
||
const dateValidation = new Date(m.DateValidation).toLocaleDateString('fr-FR');
|
||
doc.text(`Date validation: ${dateValidation}`, 300, y + 30);
|
||
}
|
||
|
||
doc.y = y + 90;
|
||
doc.moveDown(1);
|
||
}
|
||
|
||
// ===== DÉTAIL DES JOURS =====
|
||
if (jours.length > 0) {
|
||
doc.fontSize(12)
|
||
.fillColor('#000000')
|
||
.text('Détail des jours', { underline: true })
|
||
.moveDown(0.5);
|
||
|
||
// ✅ DÉFINIR LES POSITIONS DES COLONNES
|
||
const cols = {
|
||
date: 60,
|
||
travaille: 150,
|
||
repos: 285, // Centre de la colonne "Respect des repos"
|
||
commentaire: 420
|
||
};
|
||
|
||
// En-têtes du tableau (bien alignés)
|
||
const headerY = doc.y;
|
||
doc.fontSize(9)
|
||
.fillColor('#6b7280');
|
||
|
||
doc.text('Date', cols.date, headerY, { width: 80, continued: false });
|
||
doc.text('Travaillé', cols.travaille - 10, headerY, { width: 90, align: 'center', continued: false });
|
||
doc.text('Respect des repos', cols.repos - 60, headerY, { width: 120, align: 'center', continued: false });
|
||
doc.text('Commentaire', cols.commentaire, headerY, { width: 125, continued: false });
|
||
|
||
doc.moveDown(0.3);
|
||
doc.moveTo(50, doc.y)
|
||
.lineTo(545, doc.y)
|
||
.stroke('#d1d5db');
|
||
doc.moveDown(0.3);
|
||
|
||
// 🎨 Fonction pour dessiner un cercle avec coche verte
|
||
const drawGreenCheck = (x, y) => {
|
||
// Cercle vert
|
||
doc.circle(x, y, 8)
|
||
.fillAndStroke('#22c55e', '#16a34a');
|
||
|
||
// Coche blanche
|
||
doc.strokeColor('#ffffff')
|
||
.lineWidth(2)
|
||
.moveTo(x - 4, y)
|
||
.lineTo(x - 1, y + 3)
|
||
.lineTo(x + 4, y - 4)
|
||
.stroke();
|
||
|
||
// Remettre la couleur de trait par défaut
|
||
doc.strokeColor('#000000').lineWidth(1);
|
||
};
|
||
|
||
// 🎨 Fonction pour dessiner un cercle avec X rouge
|
||
const drawRedX = (x, y) => {
|
||
// Cercle rouge
|
||
doc.circle(x, y, 8)
|
||
.fillAndStroke('#ef4444', '#dc2626');
|
||
|
||
// X blanc
|
||
doc.strokeColor('#ffffff')
|
||
.lineWidth(2)
|
||
.moveTo(x - 4, y - 4)
|
||
.lineTo(x + 4, y + 4)
|
||
.stroke();
|
||
|
||
doc.moveTo(x + 4, y - 4)
|
||
.lineTo(x - 4, y + 4)
|
||
.stroke();
|
||
|
||
// Remettre la couleur de trait par défaut
|
||
doc.strokeColor('#000000').lineWidth(1);
|
||
};
|
||
|
||
// Lignes du tableau
|
||
jours.forEach((jour, index) => {
|
||
if (doc.y > 700) {
|
||
doc.addPage();
|
||
doc.y = 50;
|
||
}
|
||
|
||
const jourDate = new Date(jour.JourDate).toLocaleDateString('fr-FR');
|
||
const travaille = jour.JourTravaille ? '✓' : '✗';
|
||
|
||
// Vérifier si tout est respecté
|
||
const toutRespect = jour.ReposQuotidienRespect && jour.ReposHebdomadaireRespect;
|
||
const commentaire = jour.CommentaireRepos || '-';
|
||
|
||
const rowY = doc.y;
|
||
|
||
// Alterner les couleurs de fond
|
||
if (index % 2 === 0) {
|
||
doc.rect(50, rowY - 2, 495, 20)
|
||
.fill('#f9fafb');
|
||
}
|
||
|
||
// Date
|
||
doc.fontSize(8)
|
||
.fillColor('#000000')
|
||
.text(jourDate, cols.date, rowY + 3, { width: 80, continued: false });
|
||
|
||
// Travaillé (centré)
|
||
doc.text(travaille, cols.travaille - 10, rowY + 3, { width: 90, align: 'center', continued: false });
|
||
|
||
// 🎨 AFFICHER LE CERCLE AVEC COCHE OU X (centré)
|
||
if (toutRespect) {
|
||
// ✅ Cercle vert avec coche blanche
|
||
drawGreenCheck(cols.repos, rowY + 7);
|
||
} else {
|
||
// ❌ Cercle rouge avec X blanc
|
||
drawRedX(cols.repos, rowY + 7);
|
||
|
||
// Ajouter les détails en petit texte rouge
|
||
const details = [];
|
||
if (!jour.ReposQuotidienRespect) details.push('Q');
|
||
if (!jour.ReposHebdomadaireRespect) details.push('H');
|
||
|
||
doc.fontSize(6)
|
||
.fillColor('#ef4444')
|
||
.text(details.join(' '), cols.repos + 12, rowY + 4, { width: 50, continued: false });
|
||
}
|
||
|
||
// Commentaire
|
||
doc.fillColor('#000000')
|
||
.fontSize(7)
|
||
.text(commentaire.substring(0, 30), cols.commentaire, rowY + 3, { width: 125, continued: false });
|
||
|
||
doc.moveDown(0.8);
|
||
});
|
||
} else {
|
||
doc.fontSize(10)
|
||
.fillColor('#6b7280')
|
||
.text('Aucune saisie pour ce mois', { align: 'center' })
|
||
.moveDown(1);
|
||
}
|
||
|
||
// ===== PIED DE PAGE =====
|
||
doc.fontSize(8)
|
||
.fillColor('#9ca3af')
|
||
.text(
|
||
`Document généré le ${new Date().toLocaleDateString('fr-FR')} à ${new Date().toLocaleTimeString('fr-FR')}`,
|
||
50,
|
||
750,
|
||
{ align: 'center' }
|
||
);
|
||
|
||
// Finaliser le PDF
|
||
doc.end();
|
||
|
||
console.log('✅ PDF généré avec succès');
|
||
|
||
} catch (error) {
|
||
console.error('❌ Erreur export PDF:', error);
|
||
|
||
// Si le document n'a pas encore été envoyé
|
||
if (!res.headersSent) {
|
||
res.status(500).json({ error: error.message });
|
||
}
|
||
}
|
||
});
|
||
app.post('/v2/compteurs/update', async (req, res) => {
|
||
const conn = await pool.getConnection();
|
||
|
||
try {
|
||
const { collaborateurId, typeConge, annee, nouveauTotal } = req.body;
|
||
|
||
// Sécurité RH
|
||
if (!['RH', 'Admin'].includes(req.user?.role)) {
|
||
return res.status(403).json({ error: 'Accès refusé' });
|
||
}
|
||
|
||
// Conversion type CP/RTT → Id
|
||
const [typeRow] = await conn.query(
|
||
'SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1',
|
||
[typeConge === 'CP' ? 'Congé payé' : 'RTT']
|
||
);
|
||
|
||
if (typeRow.length === 0) {
|
||
return res.status(400).json({ error: 'Type de congé inconnu' });
|
||
}
|
||
|
||
const typeCongeId = typeRow[0].Id;
|
||
|
||
// Récupération compteur existant
|
||
const [compteur] = await conn.query(`
|
||
SELECT Id, Total, Solde, SoldeReporte
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collaborateurId, typeCongeId, annee]);
|
||
|
||
if (compteur.length === 0) {
|
||
return res.status(404).json({ error: 'Compteur non trouvé' });
|
||
}
|
||
|
||
const compteurId = compteur[0].Id;
|
||
|
||
// Mise à jour propre V2
|
||
await conn.query(`
|
||
UPDATE CompteurConges
|
||
SET Total = ?, Solde = ?, DerniereMiseAJour = NOW()
|
||
WHERE Id = ?
|
||
`, [nouveauTotal, nouveauTotal, compteurId]);
|
||
|
||
// Webhook SSE → pour que le collaborateur voit la mise à jour
|
||
notifyCollabClients({
|
||
type: 'compteur-updated',
|
||
collaborateurId,
|
||
typeConge,
|
||
annee,
|
||
nouveauTotal,
|
||
timestamp: new Date().toISOString()
|
||
}, collaborateurId);
|
||
|
||
res.json({ success: true, message: "Compteur mis à jour (V2)" });
|
||
|
||
} catch (error) {
|
||
console.error('Erreur update compteur V2:', error);
|
||
res.status(500).json({ error: 'Erreur serveur' });
|
||
} finally {
|
||
conn.release();
|
||
}
|
||
});
|
||
|
||
// ============================================================
|
||
// ENDPOINTS API V2 - REQUÊTES CORRIGÉES
|
||
// Ajustées selon la structure réelle de la base DemandeConge
|
||
// ============================================================
|
||
|
||
app.post('/v2/compteurs/update', async (req, res) => {
|
||
const conn = await pool.getConnection();
|
||
|
||
try {
|
||
const { collaborateurId, typeConge, annee, nouveauTotal } = req.body;
|
||
|
||
// Sécurité RH
|
||
if (!['RH', 'Admin'].includes(req.user?.role)) {
|
||
return res.status(403).json({ error: 'Accès refusé' });
|
||
}
|
||
|
||
// Conversion type CP/RTT → Id
|
||
const [typeRow] = await conn.query(
|
||
'SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1',
|
||
[typeConge === 'CP' ? 'Congé payé' : 'RTT']
|
||
);
|
||
|
||
if (typeRow.length === 0) {
|
||
return res.status(400).json({ error: 'Type de congé inconnu' });
|
||
}
|
||
|
||
const typeCongeId = typeRow[0].Id;
|
||
|
||
// Récupération compteur existant
|
||
const [compteur] = await conn.query(`
|
||
SELECT Id, Total, Solde, SoldeReporte
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collaborateurId, typeCongeId, annee]);
|
||
|
||
if (compteur.length === 0) {
|
||
return res.status(404).json({ error: 'Compteur non trouvé' });
|
||
}
|
||
|
||
const compteurId = compteur[0].Id;
|
||
|
||
// Mise à jour propre V2
|
||
await conn.query(`
|
||
UPDATE CompteurConges
|
||
SET Total = ?, Solde = ?, DerniereMiseAJour = NOW()
|
||
WHERE Id = ?
|
||
`, [nouveauTotal, nouveauTotal, compteurId]);
|
||
|
||
// Webhook SSE → pour que le collaborateur voit la mise à jour
|
||
notifyCollabClients({
|
||
type: 'compteur-updated',
|
||
collaborateurId,
|
||
typeConge,
|
||
annee,
|
||
nouveauTotal,
|
||
timestamp: new Date().toISOString()
|
||
}, collaborateurId);
|
||
|
||
res.json({ success: true, message: "Compteur mis à jour (V2)" });
|
||
|
||
} catch (error) {
|
||
console.error('Erreur update compteur V2:', error);
|
||
res.status(500).json({ error: 'Erreur serveur' });
|
||
} finally {
|
||
conn.release();
|
||
}
|
||
});
|
||
|
||
app.get('/v2/compteurs', async (req, res) => {
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
// ============================================================
|
||
// CORRECTIONS APPORTÉES :
|
||
// - c.Nom → c.nom (minuscule dans CollaborateurAD)
|
||
// - c.Prenom → c.prenom (minuscule)
|
||
// - c.Email → c.email (minuscule)
|
||
// - c.Service → c.service (minuscule)
|
||
// - c.Id → c.id (minuscule)
|
||
// - c.Role → c.role (minuscule)
|
||
// - c.EstApprenti → SUPPRIMÉ (n'existe pas dans la table)
|
||
// ============================================================
|
||
const [rows] = await conn.query(`
|
||
SELECT
|
||
cc.Id AS id,
|
||
cc.CollaborateurADId AS collaborateurId,
|
||
cc.TypeCongeId AS typeCongeId,
|
||
CONCAT(c.nom, ' ', c.prenom) AS employe,
|
||
c.email AS email,
|
||
c.service AS service,
|
||
tc.Nom AS typeConge,
|
||
cc.Annee AS annee,
|
||
cc.Total AS total,
|
||
cc.Solde AS solde,
|
||
cc.SoldeReporte AS soldeReporte,
|
||
(cc.Total - cc.Solde) AS consomme,
|
||
c.TypeContrat AS typeContrat,
|
||
c.role AS role
|
||
FROM CompteurConges cc
|
||
INNER JOIN CollaborateurAD c ON cc.CollaborateurADId = c.id
|
||
INNER JOIN TypeConge tc ON cc.TypeCongeId = tc.Id
|
||
ORDER BY c.nom ASC, cc.Annee DESC
|
||
`);
|
||
|
||
return res.json(rows);
|
||
|
||
} catch (error) {
|
||
console.error("❌ Erreur GET compteurs V2 :", error);
|
||
return res.status(500).json({ message: "Erreur serveur V2" });
|
||
} finally {
|
||
conn.release();
|
||
}
|
||
});
|
||
|
||
app.put('/v2/compteurs/update', async (req, res) => {
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
const {
|
||
collaborateurId,
|
||
typeConge,
|
||
annee,
|
||
nouveauTotal,
|
||
nouveauSolde,
|
||
source
|
||
} = req.body;
|
||
|
||
if (!collaborateurId || !typeConge || !annee) {
|
||
return res.status(400).json({ message: "Champs manquants" });
|
||
}
|
||
|
||
// Trouver l'ID du type de congé
|
||
// Note: Utiliser = au lieu de LIKE pour plus de précision
|
||
const [typeRow] = await conn.query(
|
||
"SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1",
|
||
[typeConge === 'CP' ? 'Congé payé' : 'RTT']
|
||
);
|
||
if (typeRow.length === 0) {
|
||
return res.status(404).json({ message: "Type de congé introuvable" });
|
||
}
|
||
const typeCongeId = typeRow[0].Id;
|
||
|
||
// Trouver compteur
|
||
const [comp] = await conn.query(
|
||
`SELECT Id FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
|
||
[collaborateurId, typeCongeId, annee]
|
||
);
|
||
if (comp.length === 0) {
|
||
return res.status(404).json({ message: "Compteur introuvable" });
|
||
}
|
||
|
||
// Maj compteur
|
||
await conn.query(`
|
||
UPDATE CompteurConges
|
||
SET Total = ?, Solde = ?, DerniereMiseAJour = NOW()
|
||
WHERE Id = ?
|
||
`, [nouveauTotal, nouveauSolde, comp[0].Id]);
|
||
|
||
res.json({ success: true, message: "Compteur mis à jour (V2)" });
|
||
|
||
} catch (err) {
|
||
console.error("Erreur update compteur V2:", err);
|
||
res.status(500).json({ message: "Erreur serveur" });
|
||
} finally {
|
||
conn.release();
|
||
}
|
||
});
|
||
|
||
app.post('/v2/compteurs/reinitialiser', async (req, res) => {
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
const { dateReference, collaborateurId } = req.body;
|
||
|
||
if (!dateReference) {
|
||
return res.status(400).json({ message: "dateReference manquante" });
|
||
}
|
||
|
||
// Appel à ta fonction métier V2 (acquisition + arrêté)
|
||
await recalculerTousLesCompteursV2(conn, dateReference, collaborateurId);
|
||
|
||
res.json({ message: "Réinitialisation V2 effectuée" });
|
||
|
||
} catch (err) {
|
||
console.error("Erreur réinitialisation V2:", err);
|
||
res.status(500).json({ message: "Erreur serveur" });
|
||
} finally {
|
||
conn.release();
|
||
}
|
||
});
|
||
|
||
app.post('/v2/compteurs/initialiser-tous-manuel', async (req, res) => {
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
const {
|
||
anneeActuelle,
|
||
cpActuel,
|
||
rttActuel,
|
||
anneePrecedente,
|
||
cpPrecedent,
|
||
rttPrecedent
|
||
} = req.body;
|
||
|
||
if (!anneeActuelle || cpActuel == null)
|
||
return res.status(400).json({ message: "Champs manquants" });
|
||
|
||
const result = await initialiserCompteursManuelV2(
|
||
conn,
|
||
anneeActuelle,
|
||
cpActuel,
|
||
rttActuel,
|
||
anneePrecedente,
|
||
cpPrecedent,
|
||
rttPrecedent
|
||
);
|
||
|
||
res.json(result);
|
||
|
||
} catch (err) {
|
||
console.error("Erreur init manuelle V2:", err);
|
||
res.status(500).json({ message: "Erreur serveur" });
|
||
} finally {
|
||
conn.release();
|
||
}
|
||
});
|
||
|
||
// ============================================
|
||
// CORRECTION : Gérer les dates d'entrée nulles
|
||
// ============================================
|
||
|
||
/**
|
||
* 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-12
|
||
|
||
// 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 && dateEntree !== null) { // ✅ AJOUT : Vérifier que dateEntree n'est pas null
|
||
const entree = new Date(dateEntree);
|
||
entree.setHours(0, 0, 0, 0);
|
||
|
||
// ✅ AJOUT : Vérifier que la date est valide
|
||
if (!isNaN(entree.getTime()) && 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);
|
||
}
|
||
|
||
/**
|
||
* Calcule l'acquisition RTT avec la configuration variable
|
||
*/
|
||
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
|
||
const config = await getConfigurationRTT(conn, annee, typeContrat);
|
||
|
||
// 4. Début d'acquisition : 01/01/N ou date d'entrée
|
||
let dateDebutAcquis = new Date(annee, 0, 1); // 01/01/N
|
||
dateDebutAcquis.setHours(0, 0, 0, 0);
|
||
|
||
if (dateEntree && dateEntree !== null) { // ✅ AJOUT : Vérifier que dateEntree n'est pas null
|
||
const entree = new Date(dateEntree);
|
||
entree.setHours(0, 0, 0, 0);
|
||
|
||
// ✅ AJOUT : Vérifier que la date est valide
|
||
if (!isNaN(entree.getTime())) {
|
||
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
|
||
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
|
||
};
|
||
}
|
||
function dateDifMonths(date1, date2) {
|
||
const d1 = new Date(date1);
|
||
const d2 = new Date(date2);
|
||
|
||
// Vérifier que les dates sont valides
|
||
if (isNaN(d1.getTime()) || isNaN(d2.getTime())) {
|
||
console.error('❌ Date invalide dans dateDifMonths');
|
||
return 0;
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
/**
|
||
* Formule Excel exacte pour calculer l'acquisition
|
||
*/
|
||
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);
|
||
|
||
// ✅ AJOUT : Vérifier que les dates sont valides
|
||
if (isNaN(b1.getTime()) || isNaN(b2.getTime())) {
|
||
console.error('❌ Date invalide dans calculerAcquisitionFormuleExcel');
|
||
return 0;
|
||
}
|
||
|
||
// ✅ AJOUT : Vérifier que coeffMensuel est un nombre
|
||
if (isNaN(coeffMensuel) || coeffMensuel <= 0) {
|
||
console.error('❌ Coefficient mensuel invalide:', coeffMensuel);
|
||
return 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;
|
||
}
|
||
/**
|
||
* Récupère la configuration RTT pour une année et un type de contrat
|
||
*/
|
||
async function getConfigurationRTT(conn, annee, typeContrat = '37h') {
|
||
try {
|
||
// 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)
|
||
};
|
||
}
|
||
|
||
// Sinon, utiliser les règles par défaut
|
||
return getConfigurationRTTDefaut(annee, typeContrat);
|
||
} catch (error) {
|
||
console.error('Erreur getConfigurationRTT:', error);
|
||
return getConfigurationRTTDefaut(annee, typeContrat);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Configuration RTT par défaut selon les règles métier
|
||
*/
|
||
function getConfigurationRTTDefaut(annee, typeContrat = '37h') {
|
||
// 37h : toujours 10 RTT/an
|
||
if (typeContrat === '37h' || typeContrat === 'tempspartiel') {
|
||
return {
|
||
joursAnnuels: 10,
|
||
acquisitionMensuelle: 10 / 12 // 0.8333
|
||
};
|
||
}
|
||
|
||
// Forfait jour : dépend de l'année
|
||
if (typeContrat === 'forfaitjour') {
|
||
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
|
||
};
|
||
}
|
||
|
||
// 📊 ROUTE POUR L'ESPACE RH - Tous les compteurs détaillés
|
||
app.get('/getAllDetailedCounters', async (req, res) => {
|
||
const conn = await pool.getConnection();
|
||
try {
|
||
console.log('📊 Récupération de TOUS les compteurs détaillés (Version Fixée V2 + V1)');
|
||
|
||
// Récupérer tous les collaborateurs actifs
|
||
const [collaborateurs] = await conn.query(`
|
||
SELECT ca.id, ca.prenom, ca.nom, ca.email, ca.role, ca.TypeContrat, ca.DateEntree,
|
||
s.Nom as service
|
||
FROM CollaborateurAD ca
|
||
LEFT JOIN Services s ON ca.ServiceId = s.Id
|
||
WHERE ca.Actif = 1 OR ca.Actif IS NULL
|
||
ORDER BY ca.nom, ca.prenom
|
||
`);
|
||
|
||
const resultats = [];
|
||
const currentYear = new Date().getFullYear();
|
||
const previousYear = currentYear - 1;
|
||
|
||
// Pré-charger les TypeConge Id
|
||
const [[cpType]] = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = 'Congé payé' LIMIT 1`);
|
||
const [[rttType]] = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = 'RTT' LIMIT 1`);
|
||
const [[recupType]] = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = 'Récupération' LIMIT 1`);
|
||
|
||
for (const collab of collaborateurs) {
|
||
|
||
//
|
||
// -----------------------------------------------------
|
||
// 1️⃣ CONGÉS PAYÉS — ANNÉE N (V2 si existant, sinon V1)
|
||
// -----------------------------------------------------
|
||
//
|
||
|
||
let [cpN] = await conn.query(`
|
||
SELECT Total, Solde
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collab.id, cpType.Id, currentYear]);
|
||
|
||
let cpTotalN, cpSoldeN;
|
||
|
||
if (cpN.length > 0) {
|
||
// Utiliser données V2
|
||
cpTotalN = parseFloat(cpN[0].Total);
|
||
cpSoldeN = parseFloat(cpN[0].Solde);
|
||
} else {
|
||
// Fallback ancien calcul
|
||
cpTotalN = calculerAcquisitionCP(new Date(), collab.DateEntree);
|
||
const [[consomme]] = await conn.query(`
|
||
SELECT COALESCE(SUM(JoursUtilises), 0) AS total
|
||
FROM DeductionDetails dd
|
||
JOIN DemandeConge dc ON dc.Id = dd.DemandeCongeId
|
||
WHERE dc.CollaborateurADId = ?
|
||
AND dd.TypeCongeId = ?
|
||
AND dd.Annee = ?
|
||
AND dd.TypeDeduction NOT IN ('Accum. Récup', 'N Anticipé')
|
||
AND dc.Statut != 'Refusé'
|
||
`, [collab.id, cpType.Id, currentYear]);
|
||
|
||
cpSoldeN = Math.max(0, cpTotalN - consomme.total);
|
||
}
|
||
|
||
resultats.push({
|
||
collaborateurId: collab.id,
|
||
employe: `${collab.prenom} ${collab.nom}`,
|
||
email: collab.email,
|
||
service: collab.service || 'Non assigné',
|
||
typeConge: "Congé payé",
|
||
annee: currentYear,
|
||
total: parseFloat(cpTotalN.toFixed(2)),
|
||
solde: parseFloat(cpSoldeN.toFixed(2)),
|
||
consomme: parseFloat((cpTotalN - cpSoldeN).toFixed(2)),
|
||
role: collab.role,
|
||
typeContrat: collab.TypeContrat
|
||
});
|
||
|
||
//
|
||
// -----------------------------------------------------
|
||
// 2️⃣ CP N-1 (toujours basé sur CompteurConges)
|
||
// -----------------------------------------------------
|
||
//
|
||
|
||
let [cpN1] = await conn.query(`
|
||
SELECT Total, Solde
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collab.id, cpType.Id, previousYear]);
|
||
|
||
if (cpN1.length > 0 && cpN1[0].Solde > 0) {
|
||
const total = parseFloat(cpN1[0].Total);
|
||
const solde = parseFloat(cpN1[0].Solde);
|
||
|
||
resultats.push({
|
||
collaborateurId: collab.id,
|
||
employe: `${collab.prenom} ${collab.nom}`,
|
||
email: collab.email,
|
||
service: collab.service || 'Non assigné',
|
||
typeConge: "Congé payé",
|
||
annee: previousYear,
|
||
total,
|
||
solde,
|
||
consomme: total - solde,
|
||
role: collab.role,
|
||
typeContrat: collab.TypeContrat
|
||
});
|
||
}
|
||
|
||
//
|
||
// -----------------------------------------------------
|
||
// 3️⃣ RTT — ANNÉE N (V2 si existant)
|
||
// -----------------------------------------------------
|
||
//
|
||
|
||
if (collab.role !== 'Apprenti') {
|
||
let [rttN] = await conn.query(`
|
||
SELECT Total, Solde
|
||
FROM CompteurConges
|
||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
||
`, [collab.id, rttType.Id, currentYear]);
|
||
|
||
let rttTotalN, rttSoldeN;
|
||
|
||
if (rttN.length > 0) {
|
||
rttTotalN = parseFloat(rttN[0].Total);
|
||
rttSoldeN = parseFloat(rttN[0].Solde);
|
||
} else {
|
||
// Fallback ancien calcul
|
||
const rtt = await calculerAcquisitionRTT(conn, collab.id, new Date());
|
||
|
||
const [[consommeRTT]] = await conn.query(`
|
||
SELECT COALESCE(SUM(JoursUtilises), 0) AS total
|
||
FROM DeductionDetails dd
|
||
JOIN DemandeConge dc ON dc.Id = dd.DemandeCongeId
|
||
WHERE dc.CollaborateurADId = ?
|
||
AND dd.TypeCongeId = ?
|
||
AND dd.Annee = ?
|
||
AND dd.TypeDeduction NOT IN ('Accum. Récup', 'Récup Dosée')
|
||
AND dc.Statut != 'Refusé'
|
||
`, [collab.id, rttType.Id, currentYear]);
|
||
|
||
rttTotalN = rtt.acquisition;
|
||
rttSoldeN = Math.max(0, rttTotalN - consommeRTT.total);
|
||
}
|
||
|
||
resultats.push({
|
||
collaborateurId: collab.id,
|
||
employe: `${collab.prenom} ${collab.nom}`,
|
||
email: collab.email,
|
||
service: collab.service || 'Non assigné',
|
||
typeConge: "RTT",
|
||
annee: currentYear,
|
||
total: parseFloat(rttTotalN.toFixed(2)),
|
||
solde: parseFloat(rttSoldeN.toFixed(2)),
|
||
consomme: parseFloat((rttTotalN - rttSoldeN).toFixed(2)),
|
||
role: collab.role,
|
||
typeContrat: collab.TypeContrat
|
||
});
|
||
}
|
||
|
||
//
|
||
// -----------------------------------------------------
|
||
// 4️⃣ RÉCUP — ANNÉE N
|
||
// -----------------------------------------------------
|
||
//
|
||
|
||
const [[accum]] = await conn.query(`
|
||
SELECT COALESCE(SUM(JoursUtilises), 0) AS total
|
||
FROM DeductionDetails dd
|
||
JOIN DemandeConge dc ON dc.Id = dd.DemandeCongeId
|
||
WHERE dc.CollaborateurADId = ?
|
||
AND dd.TypeCongeId = ?
|
||
AND dd.Annee = ?
|
||
AND dd.TypeDeduction IN ('Accum. Récup', 'Accum. Recup')
|
||
`, [collab.id, recupType.Id, currentYear]);
|
||
|
||
const [[consomm]] = await conn.query(`
|
||
SELECT COALESCE(SUM(JoursUtilises), 0) AS total
|
||
FROM DeductionDetails dd
|
||
JOIN DemandeConge dc ON dc.Id = dd.DemandeCongeId
|
||
WHERE dc.CollaborateurADId = ?
|
||
AND dd.TypeCongeId = ?
|
||
AND dd.Annee = ?
|
||
AND dd.TypeDeduction IN ('Récup Dosée', 'Recup Dosee')
|
||
`, [collab.id, recupType.Id, currentYear]);
|
||
|
||
const recupTotal = parseFloat(accum.total);
|
||
const recupCons = parseFloat(consomm.total);
|
||
const recupSolde = Math.max(0, recupTotal - recupCons);
|
||
|
||
if (recupTotal + recupCons > 0) {
|
||
resultats.push({
|
||
collaborateurId: collab.id,
|
||
employe: `${collab.prenom} ${collab.nom}`,
|
||
email: collab.email,
|
||
service: collab.service || 'Non assigné',
|
||
typeConge: "Récupération",
|
||
annee: currentYear,
|
||
total: recupTotal,
|
||
solde: recupSolde,
|
||
consomme: recupCons,
|
||
role: collab.role,
|
||
typeContrat: collab.TypeContrat
|
||
});
|
||
}
|
||
}
|
||
|
||
conn.release();
|
||
res.json(resultats);
|
||
|
||
} catch (err) {
|
||
conn.release();
|
||
console.error("❌ Erreur GET ALL:", err);
|
||
res.status(500).json({ error: err.message });
|
||
}
|
||
});
|
||
|
||
|
||
// ✅ ROUTE STATS MANQUANTE
|
||
app.get('/api/stats', authenticateToken, async (req, res) => {
|
||
try {
|
||
console.log('📊 GET /api/stats appelé par', req.user.email);
|
||
|
||
// Stats simples pour test
|
||
const stats = {
|
||
totalEmployes: 0,
|
||
demandesEnAttente: 0,
|
||
demandesValidees: 0,
|
||
timestamp: new Date().toISOString()
|
||
};
|
||
|
||
// TODO: Requêtes SQL réelles ici
|
||
res.json(stats);
|
||
} catch (error) {
|
||
console.error('Erreur /api/stats:', error);
|
||
res.status(500).json({ error: 'Erreur stats' });
|
||
}
|
||
});
|
||
|
||
|
||
|
||
// ================================================
|
||
// DÉMARRAGE DU SERVEUR
|
||
// ================================================
|
||
|
||
console.log('✅ 99. Toutes les routes définies, démarrage du serveur...');
|
||
|
||
const server = app.listen(PORT, () => {
|
||
console.log('\n================================================');
|
||
console.log(`✅ SERVEUR RH DÉMARRÉ sur http://localhost:${PORT}`);
|
||
console.log('🔔 Server-Sent Events activés sur /api/events');
|
||
console.log('🔗 WEBHOOKS configurés:');
|
||
console.log(` - Serveur Collaborateurs: ${WEBHOOKS.COLLABORATEURS_URL}`);
|
||
console.log(` - Route webhook receiver: POST /api/webhook/receive`);
|
||
console.log('');
|
||
console.log('📋 Routes disponibles:');
|
||
console.log(' - GET /api/events (SSE)');
|
||
console.log(' - POST /api/webhook/receive');
|
||
console.log(' - POST /api/auth/login ✅ AVEC FILTRE ACTIF');
|
||
console.log(' - GET /api/demandes');
|
||
console.log(' - GET /api/demandes/:id');
|
||
console.log(' - POST /api/demandes');
|
||
console.log(' - PUT /api/demandes/:id');
|
||
console.log(' - DELETE /api/demandes/:id');
|
||
console.log(' - PUT /api/demandes/:id/valider');
|
||
console.log(' - GET /api/employes?include_inactifs=true ✅ MODIFIÉ');
|
||
console.log(' - POST /api/employes/desactiver 🆕 NOUVEAU');
|
||
console.log(' - POST /api/employes/reactiver 🆕 NOUVEAU');
|
||
console.log(' - GET /api/types-conge');
|
||
console.log(' - GET /api/export/paie');
|
||
console.log(' - GET /api/compteurs?include_inactifs=true ✅ MODIFIÉ');
|
||
console.log(' - POST /api/compteurs/reinitialiser');
|
||
console.log(' - POST /api/compteurs/initialiser-manuel');
|
||
console.log(' - POST /api/compteurs/initialiser-tous-manuel ✅ MODIFIÉ');
|
||
console.log(' - PUT /api/compteurs/:id');
|
||
console.log(' - POST /api/compteurs/ajouter-recup 🆕 NOUVEAU');
|
||
console.log(' - POST /api/compteurs/retirer-recup 🆕 NOUVEAU');
|
||
console.log(' - GET /api/equipes ✅ MODIFIÉ');
|
||
console.log(' - GET /api/equipes/:id ✅ MODIFIÉ');
|
||
console.log(' - GET /api/equipes/:id/membres ✅ MODIFIÉ');
|
||
console.log(' - GET /api/stats ✅ MODIFIÉ');
|
||
console.log(' - GET /api/historique');
|
||
console.log('================================================');
|
||
});
|
||
|
||
server.on('error', (error) => {
|
||
console.error('\n❌ ERREUR SERVEUR:', error);
|
||
if (error.code === 'EADDRINUSE') {
|
||
console.error(`⚠️ Le port ${PORT} est déjà utilisé`);
|
||
}
|
||
});
|
||
|
||
setInterval(() => { }, 60000);
|
||
|
||
} catch (error) {
|
||
console.error('\n❌❌❌ ERREUR FATALE AU CHARGEMENT ❌❌❌');
|
||
console.error(error);
|
||
console.error(error.stack);
|
||
process.exit(1);
|
||
} |