Compare commits

..

1 Commits

Author SHA1 Message Date
oimer 762f9cfd4a GTARH_Modif_anno27 2026-05-28 11:11:52 +02:00
7 changed files with 336 additions and 989 deletions
+273 -387
View File
@@ -1,4 +1,4 @@

const express = require('express'); const express = require('express');
const cors = require('cors'); const cors = require('cors');
@@ -943,23 +943,7 @@ try {
await transaction.rollback(); await transaction.rollback();
return res.status(403).json({ error: 'Impossible de créer une demande pour un compte désactivé' }); return res.status(403).json({ error: 'Impossible de créer une demande pour un compte désactivé' });
} }
// ── Vérification anti-doublon avant création ──────────────────
const doublonCheck = await transaction.request()
.input('collabId', sql.Int, collaborateurId)
.input('dateDebut', sql.Date, dateDebut)
.input('dateFin', sql.Date, dateFin)
.query(`
SELECT Id FROM DemandeConge
WHERE CollaborateurADId = @collabId
AND Statut IN ('Validée', 'Validé', 'En attente')
AND DateDebut <= @dateFin AND DateFin >= @dateDebut
`);
if (doublonCheck.recordset.length > 0) {
await transaction.rollback();
return res.status(409).json({
error: `Une demande existe déjà pour ${collab[0].prenom} ${collab[0].nom} sur cette période.`
});
}
const totalJours = typesConge.reduce((sum, type) => sum + parseFloat(type.nombreJours), 0); const totalJours = typesConge.reduce((sum, type) => sum + parseFloat(type.nombreJours), 0);
const statut = saisieManuelle ? 'Validée' : 'En attente'; const statut = saisieManuelle ? 'Validée' : 'En attente';
@@ -2473,82 +2457,6 @@ WHERE CollaborateurADId = @userId
} }
}); });
// ================================================
// ROUTE GET /demandes/check-doublon
// Vérifie si un ou plusieurs collaborateurs ont déjà une demande
// (Validée ou En attente) qui chevauche la période demandée.
// Usage: GET /demandes/check-doublon?collaborateurIds=12,45&dateDebut=2026-08-01&dateFin=2026-08-05
// ================================================
app.get('/demandes/check-doublon', authenticateToken, async (req, res) => {
try {
const { collaborateurIds, dateDebut, dateFin } = req.query;
if (!collaborateurIds || !dateDebut || !dateFin) {
return res.json({ success: false, message: 'Paramètres requis: collaborateurIds, dateDebut, dateFin' });
}
const ids = collaborateurIds
.split(',')
.map(id => parseInt(id.trim(), 10))
.filter(id => !isNaN(id));
if (ids.length === 0) {
return res.json({ success: false, message: 'Aucun ID collaborateur valide' });
}
const doublonsParCollab = {};
for (const collabId of ids) {
const result = await pool.request()
.input('collabId', sql.Int, collabId)
.input('dateDebut', sql.Date, dateDebut)
.input('dateFin', sql.Date, dateFin)
.query(`
SELECT
dc.Id,
CONVERT(VARCHAR(10), dc.DateDebut, 23) AS dateDebut,
CONVERT(VARCHAR(10), dc.DateFin, 23) AS dateFin,
dc.Statut,
dc.NombreJours,
STUFF((
SELECT DISTINCT ', ' + tc2.Nom
FROM DemandeCongeType dct2
JOIN TypeConge tc2 ON dct2.TypeCongeId = tc2.Id
WHERE dct2.DemandeCongeId = dc.Id
FOR XML PATH('')
), 1, 2, '') AS types
FROM DemandeConge dc
WHERE dc.CollaborateurADId = @collabId
AND dc.Statut IN ('Validée', 'Validé', 'En attente')
AND dc.DateDebut <= @dateFin
AND dc.DateFin >= @dateDebut
ORDER BY dc.DateDebut
`);
if (result.recordset.length > 0) {
doublonsParCollab[collabId] = result.recordset.map(d => ({
id: d.Id,
dateDebut: d.dateDebut,
dateFin: d.dateFin,
statut: d.Statut,
nombreJours: d.NombreJours,
types: d.types
}));
}
}
res.json({
success: true,
hasDoublon: Object.keys(doublonsParCollab).length > 0,
doublons: doublonsParCollab
});
} catch (error) {
console.error('❌ Erreur check-doublon:', error);
res.status(500).json({ success: false, message: error.message });
}
});
// ================================================ // ================================================
// ROUTE PUT /demandes/:id - AVEC VALIDATION AUTOMATIQUE // ROUTE PUT /demandes/:id - AVEC VALIDATION AUTOMATIQUE
@@ -5597,294 +5505,297 @@ app.get('/demandes/check-doublon', authenticateToken, async (req, res) => {
}); });
// ✅ CORRECTION COMPLÈTE - Route /getAllDetailedCounters // ✅ CORRECTION COMPLÈTE - Route /getAllDetailedCounters
app.get('/getAllDetailedCounters', async (req, res) => { app.get('/getAllDetailedCounters', async (req, res) => {
try { try {
console.log('📊 Récupération de TOUS les compteurs détaillés'); console.log('📊 Récupération de TOUS les compteurs détaillés');
const today = new Date(); const today = new Date();
const currentYearSys = today.getFullYear(); // 2026 const currentYearSys = today.getFullYear(); // 2026
// ── Détection bascule CP ────────────────────────────────────────── // ── Détection bascule CP ──────────────────────────────────────────
const cpTypeForCheck = await pool.request() const cpTypeForCheck = await pool.request()
.query("SELECT TOP 1 Id FROM TypeConge WHERE Nom = 'Congé payé'"); .query("SELECT TOP 1 Id FROM TypeConge WHERE Nom = 'Congé payé'");
let currentYear = currentYearSys; let currentYear = currentYearSys; // CP N (2026 ou 2027 après bascule)
let previousYear = currentYearSys - 1; let previousYear = currentYearSys - 1; // CP N-1
if (cpTypeForCheck.recordset.length > 0) { if (cpTypeForCheck.recordset.length > 0) {
const cpTypeId = cpTypeForCheck.recordset[0].Id; const cpTypeId = cpTypeForCheck.recordset[0].Id;
const checkBascule = await pool.request() const checkBascule = await pool.request()
.input('cpTypeId', sql.Int, cpTypeId) .input('cpTypeId', sql.Int, cpTypeId)
.input('anneeN1', sql.Int, currentYearSys + 1) .input('anneeN1', sql.Int, currentYearSys + 1)
.query(` .query(`
SELECT TOP 1 Id FROM CompteurConges SELECT TOP 1 Id FROM CompteurConges
WHERE TypeCongeId = @cpTypeId WHERE TypeCongeId = @cpTypeId
AND Annee = @anneeN1 AND Annee = @anneeN1
AND SoldeReporte > 0
`);
if (checkBascule.recordset.length > 0) {
currentYear = currentYearSys + 1; // 2027
previousYear = currentYearSys; // 2026
console.log('✅ Bascule CP détectée — currentYear=2027, previousYear=2026');
}
}
// RTT et Récup : toujours l'année civile système (jamais basculent)
const anneeRTT = currentYearSys; // 2026
const anneeRecup = currentYearSys; // 2026
console.log(`📅 CP N=${currentYear}, CP N-1=${previousYear}, RTT=${anneeRTT}, Récup=${anneeRecup}`);
const typesCongeResult = await pool.request().query(`
SELECT Id, Nom FROM TypeConge WHERE Nom IN ('Congé payé', 'RTT', 'Récupération')
`); `);
if (checkBascule.recordset.length > 0) { const typeCongeMap = {};
currentYear = currentYearSys + 1; // 2027 typesCongeResult.recordset.forEach(tc => {
previousYear = currentYearSys; // 2026 typeCongeMap[tc.Nom] = tc.Id;
console.log('✅ Bascule CP détectée — currentYear=2027, previousYear=2026'); });
}
}
const anneeRTT = currentYearSys; const collaborateursResult = await pool.request().query(`
const anneeRecup = currentYearSys; SELECT
ca.id,
ca.prenom,
ca.nom,
ca.email,
ca.role,
ca.TypeContrat,
ca.DateEntree,
COALESCE(s.Nom, 'Non assigné') as service,
s.Id as serviceId,
ca.CampusId as campusId,
COALESCE(camp.Nom, 'Sans campus') as campusNom,
ca.SocieteId as societeId,
COALESCE(soc.Nom, 'Sans société') as societe
FROM CollaborateurAD ca
LEFT JOIN Services s ON ca.ServiceId = s.Id
LEFT JOIN Campus camp ON ca.CampusId = camp.Id
LEFT JOIN Societe soc ON ca.SocieteId = soc.Id
WHERE (ca.Actif = 1 OR ca.Actif IS NULL)
AND (ca.description IS NULL OR ca.description NOT LIKE '%stagiaire%')
ORDER BY soc.Nom, camp.Nom, s.Nom, ca.nom, ca.prenom
`);
console.log(`📅 CP N=${currentYear}, CP N-1=${previousYear}, RTT=${anneeRTT}, Récup=${anneeRecup}`); const collaborateurs = collaborateursResult.recordset;
const resultats = [];
const typesCongeResult = await pool.request().query(` console.log(`📋 ${collaborateurs.length} collaborateurs trouvés`);
SELECT Id, Nom FROM TypeConge WHERE Nom IN ('Congé payé', 'RTT', 'Récupération')
`);
const typeCongeMap = {}; for (const collab of collaborateurs) {
typesCongeResult.recordset.forEach(tc => { const campusNom = collab.campusNom;
typeCongeMap[tc.Nom] = tc.Id;
});
const collaborateursResult = await pool.request().query(` // ==================== CP N ====================
SELECT if (typeCongeMap['Congé payé']) {
ca.id, const cpNResult = await pool.request()
ca.prenom, .input('collabId', sql.Int, collab.id)
ca.nom, .input('typeId', sql.Int, typeCongeMap['Congé payé'])
ca.email, .input('annee', sql.Int, currentYear)
ca.role, .query(`
ca.TypeContrat, SELECT Total, Solde
ca.DateEntree, FROM CompteurConges
COALESCE(s.Nom, 'Non assigné') as service, WHERE CollaborateurADId = @collabId
s.Id as serviceId, AND TypeCongeId = @typeId
ca.CampusId as campusId, AND Annee = @annee
COALESCE(camp.Nom, 'Sans campus') as campusNom, `);
ca.SocieteId as societeId,
COALESCE(soc.Nom, 'Sans société') as societe
FROM CollaborateurAD ca
LEFT JOIN Services s ON ca.ServiceId = s.Id
LEFT JOIN Campus camp ON ca.CampusId = camp.Id
LEFT JOIN Societe soc ON ca.SocieteId = soc.Id
WHERE (ca.Actif = 1 OR ca.Actif IS NULL)
AND (ca.description IS NULL OR ca.description NOT LIKE '%stagiaire%')
ORDER BY soc.Nom, camp.Nom, s.Nom, ca.nom, ca.prenom
`);
const collaborateurs = collaborateursResult.recordset; const cpN = cpNResult.recordset;
const resultats = [];
console.log(`📋 ${collaborateurs.length} collaborateurs trouvés`); let cpTotalN, cpSoldeN;
if (cpN.length > 0) {
for (const collab of collaborateurs) { cpTotalN = parseFloat(cpN[0].Total);
const campusNom = collab.campusNom; cpSoldeN = parseFloat(cpN[0].Solde);
} else {
// ==================== CP N ==================== // Après bascule, si pas de ligne en base → 0
if (typeCongeMap['Congé payé']) { // Avant bascule, on calcule l'acquisition
const cpNResult = await pool.request() if (currentYear > currentYearSys) {
.input('collabId', sql.Int, collab.id) cpTotalN = 0;
.input('typeId', sql.Int, typeCongeMap['Congé payé']) cpSoldeN = 0;
.input('annee', sql.Int, currentYear) } else {
.query(` cpTotalN = calculerAcquisitionCP(new Date(), collab.DateEntree);
SELECT Total, Solde, SoldeReporte const consommeResult = await pool.request()
FROM CompteurConges .input('collabId', sql.Int, collab.id)
WHERE CollaborateurADId = @collabId .input('typeId', sql.Int, typeCongeMap['Congé payé'])
AND TypeCongeId = @typeId .input('annee', sql.Int, currentYear)
AND Annee = @annee .query(`
`);
const cpN = cpNResult.recordset;
let cpTotalN, cpSoldeN;
if (cpN.length > 0) {
cpTotalN = parseFloat(cpN[0].Total);
const soldeReporte = parseFloat(cpN[0].SoldeReporte || 0);
// Solde réel = Solde - SoldeReporte
cpSoldeN = Math.max(0, parseFloat(cpN[0].Solde) - soldeReporte);
} else {
if (currentYear > currentYearSys) {
// Bascule faite mais pas de compteur 2027 pour ce collab → 0
cpTotalN = 0;
cpSoldeN = 0;
} else {
cpTotalN = calculerAcquisitionCP(new Date(), collab.DateEntree);
const consommeResult = await pool.request()
.input('collabId', sql.Int, collab.id)
.input('typeId', sql.Int, typeCongeMap['Congé payé'])
.input('annee', sql.Int, currentYear)
.query(`
SELECT COALESCE(SUM(JoursUtilises), 0) AS total
FROM DeductionDetails dd
JOIN DemandeConge dc ON dc.Id = dd.DemandeCongeId
WHERE dc.CollaborateurADId = @collabId
AND dd.TypeCongeId = @typeId
AND dd.Annee = @annee
AND dd.TypeDeduction NOT IN ('Accum. Récup', 'N Anticipé')
AND dc.Statut != 'Refusé'
`);
cpSoldeN = Math.max(0, cpTotalN - consommeResult.recordset[0].total);
}
}
resultats.push({
collaborateurId: collab.id,
employe: `${collab.prenom} ${collab.nom}`,
email: collab.email,
service: collab.service,
campus: campusNom,
societe: collab.societe,
typeCongeId: typeCongeMap['Congé payé'],
typeConge: 'Congé payé',
annee: currentYear,
total: parseFloat(cpTotalN.toFixed(2)),
solde: parseFloat(cpSoldeN.toFixed(2)),
consomme: Math.max(0, parseFloat((cpTotalN - cpSoldeN).toFixed(2))),
role: collab.role,
typeContrat: collab.TypeContrat,
estApprenti: collab.role === 'Apprenti' || collab.TypeContrat === 'Apprentissage'
});
// ==================== CP N-1 ====================
const cpN1Result = await pool.request()
.input('collabId', sql.Int, collab.id)
.input('typeId', sql.Int, typeCongeMap['Congé payé'])
.input('annee', sql.Int, previousYear)
.query(`
SELECT Total, Solde, SoldeReporte
FROM CompteurConges
WHERE CollaborateurADId = @collabId
AND TypeCongeId = @typeId
AND Annee = @annee
`);
const cpN1 = cpN1Result.recordset;
if (cpN1.length > 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,
campus: campusNom,
societe: collab.societe,
typeCongeId: typeCongeMap['Congé payé'],
typeConge: 'Congé payé',
annee: previousYear,
total,
solde,
consomme: Math.max(0, parseFloat((total - solde).toFixed(2))),
role: collab.role,
typeContrat: collab.TypeContrat,
estApprenti: collab.role === 'Apprenti' || collab.TypeContrat === 'Apprentissage'
});
}
}
// ==================== RTT — toujours anneeRTT (2026) ====================
if (typeCongeMap['RTT'] && collab.role !== 'Apprenti' && collab.TypeContrat !== 'Apprentissage') {
const rttNResult = await pool.request()
.input('collabId', sql.Int, collab.id)
.input('typeId', sql.Int, typeCongeMap['RTT'])
.input('annee', sql.Int, anneeRTT)
.query(`
SELECT Total, Solde
FROM CompteurConges
WHERE CollaborateurADId = @collabId
AND TypeCongeId = @typeId
AND Annee = @annee
`);
const rttN = rttNResult.recordset;
let rttTotalN, rttSoldeN;
if (rttN.length > 0) {
rttTotalN = parseFloat(rttN[0].Total);
rttSoldeN = parseFloat(rttN[0].Solde);
} else {
const rtt = await calculerAcquisitionRTT(collab.id, new Date());
const consommeRTTResult = await pool.request()
.input('collabId', sql.Int, collab.id)
.input('typeId', sql.Int, typeCongeMap['RTT'])
.input('annee', sql.Int, anneeRTT)
.query(`
SELECT COALESCE(SUM(JoursUtilises), 0) AS total SELECT COALESCE(SUM(JoursUtilises), 0) AS total
FROM DeductionDetails dd FROM DeductionDetails dd
JOIN DemandeConge dc ON dc.Id = dd.DemandeCongeId JOIN DemandeConge dc ON dc.Id = dd.DemandeCongeId
WHERE dc.CollaborateurADId = @collabId WHERE dc.CollaborateurADId = @collabId
AND dd.TypeCongeId = @typeId AND dd.TypeCongeId = @typeId
AND dd.Annee = @annee AND dd.Annee = @annee
AND dd.TypeDeduction NOT IN ('Accum. Récup', 'Récup Dosée') AND dd.TypeDeduction NOT IN ('Accum. Récup', 'N Anticipé')
AND dc.Statut != 'Refusé' AND dc.Statut != 'Refusé'
`); `);
rttTotalN = rtt.acquisition; cpSoldeN = Math.max(0, cpTotalN - consommeResult.recordset[0].total);
rttSoldeN = Math.max(0, rttTotalN - consommeRTTResult.recordset[0].total); }
} }
resultats.push({ // ⭐ PUSH CP N (était manquant — cause du bug d'affichage)
collaborateurId: collab.id, resultats.push({
employe: `${collab.prenom} ${collab.nom}`, collaborateurId: collab.id,
email: collab.email, employe: `${collab.prenom} ${collab.nom}`,
service: collab.service, email: collab.email,
campus: campusNom, service: collab.service,
societe: collab.societe, campus: campusNom,
typeCongeId: typeCongeMap['RTT'], societe: collab.societe,
typeConge: 'RTT', typeCongeId: typeCongeMap['Congé payé'],
annee: anneeRTT, typeConge: 'Congé payé',
total: parseFloat(rttTotalN.toFixed(2)), annee: currentYear,
solde: parseFloat(rttSoldeN.toFixed(2)), total: parseFloat(cpTotalN.toFixed(2)),
consomme: Math.max(0, parseFloat((rttTotalN - rttSoldeN).toFixed(2))), solde: parseFloat(cpSoldeN.toFixed(2)),
role: collab.role, consomme: parseFloat((cpTotalN - cpSoldeN).toFixed(2)),
typeContrat: collab.TypeContrat, role: collab.role,
estApprenti: collab.role === 'Apprenti' || collab.TypeContrat === 'Apprentissage' typeContrat: collab.TypeContrat,
}); estApprenti: collab.role === 'Apprenti' || collab.TypeContrat === 'Apprentissage'
} });
// ==================== Récupération — toujours anneeRecup (2026) ==================== // ==================== CP N-1 ====================
if (typeCongeMap['Récupération']) { const cpN1Result = await pool.request()
const recupNResult = await pool.request() .input('collabId', sql.Int, collab.id)
.input('collabId', sql.Int, collab.id) .input('typeId', sql.Int, typeCongeMap['Congé payé'])
.input('typeId', sql.Int, typeCongeMap['Récupération']) .input('annee', sql.Int, previousYear)
.input('annee', sql.Int, anneeRecup) .query(`
.query(` SELECT Total, Solde
SELECT Total, Solde FROM CompteurConges
FROM CompteurConges WHERE CollaborateurADId = @collabId
WHERE CollaborateurADId = @collabId AND TypeCongeId = @typeId
AND TypeCongeId = @typeId AND Annee = @annee
AND Annee = @annee `);
const cpN1 = cpN1Result.recordset;
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,
campus: campusNom,
societe: collab.societe,
typeCongeId: typeCongeMap['Congé payé'],
typeConge: 'Congé payé',
annee: previousYear,
total,
solde,
consomme: parseFloat((total - solde).toFixed(2)),
role: collab.role,
typeContrat: collab.TypeContrat,
estApprenti: collab.role === 'Apprenti' || collab.TypeContrat === 'Apprentissage'
});
}
}
// ==================== RTT — toujours anneeRTT (2026) ====================
if (typeCongeMap['RTT'] && collab.role !== 'Apprenti' && collab.TypeContrat !== 'Apprentissage') {
const rttNResult = await pool.request()
.input('collabId', sql.Int, collab.id)
.input('typeId', sql.Int, typeCongeMap['RTT'])
.input('annee', sql.Int, anneeRTT)
.query(`
SELECT Total, Solde
FROM CompteurConges
WHERE CollaborateurADId = @collabId
AND TypeCongeId = @typeId
AND Annee = @annee
`);
const rttN = rttNResult.recordset;
let rttTotalN, rttSoldeN;
if (rttN.length > 0) {
rttTotalN = parseFloat(rttN[0].Total);
rttSoldeN = parseFloat(rttN[0].Solde);
} else {
const rtt = await calculerAcquisitionRTT(collab.id, new Date());
const consommeRTTResult = await pool.request()
.input('collabId', sql.Int, collab.id)
.input('typeId', sql.Int, typeCongeMap['RTT'])
.input('annee', sql.Int, anneeRTT)
.query(`
SELECT COALESCE(SUM(JoursUtilises), 0) AS total
FROM DeductionDetails dd
JOIN DemandeConge dc ON dc.Id = dd.DemandeCongeId
WHERE dc.CollaborateurADId = @collabId
AND dd.TypeCongeId = @typeId
AND dd.Annee = @annee
AND dd.TypeDeduction NOT IN ('Accum. Récup', 'Récup Dosée')
AND dc.Statut != 'Refusé'
`); `);
rttTotalN = rtt.acquisition;
rttSoldeN = Math.max(0, rttTotalN - consommeRTTResult.recordset[0].total);
}
const recupN = recupNResult.recordset; resultats.push({
collaborateurId: collab.id,
employe: `${collab.prenom} ${collab.nom}`,
email: collab.email,
service: collab.service,
campus: campusNom,
societe: collab.societe,
typeCongeId: typeCongeMap['RTT'],
typeConge: 'RTT',
annee: anneeRTT,
total: parseFloat(rttTotalN.toFixed(2)),
solde: parseFloat(rttSoldeN.toFixed(2)),
consomme: parseFloat((rttTotalN - rttSoldeN).toFixed(2)),
role: collab.role,
typeContrat: collab.TypeContrat,
estApprenti: collab.role === 'Apprenti' || collab.TypeContrat === 'Apprentissage'
});
}
if (recupN.length > 0) { // ==================== Récupération — toujours anneeRecup (2026) ====================
const recupTotal = parseFloat(recupN[0].Total); if (typeCongeMap['Récupération']) {
const recupSolde = parseFloat(recupN[0].Solde); const recupNResult = await pool.request()
.input('collabId', sql.Int, collab.id)
.input('typeId', sql.Int, typeCongeMap['Récupération'])
.input('annee', sql.Int, anneeRecup)
.query(`
SELECT Total, Solde
FROM CompteurConges
WHERE CollaborateurADId = @collabId
AND TypeCongeId = @typeId
AND Annee = @annee
`);
resultats.push({ const recupN = recupNResult.recordset;
collaborateurId: collab.id,
employe: `${collab.prenom} ${collab.nom}`, if (recupN.length > 0) {
email: collab.email, const recupTotal = parseFloat(recupN[0].Total);
service: collab.service, const recupSolde = parseFloat(recupN[0].Solde);
campus: campusNom,
societe: collab.societe, resultats.push({
typeCongeId: typeCongeMap['Récupération'], collaborateurId: collab.id,
typeConge: 'Récupération', employe: `${collab.prenom} ${collab.nom}`,
annee: anneeRecup, email: collab.email,
total: recupTotal, service: collab.service,
solde: recupSolde, campus: campusNom,
consomme: Math.max(0, parseFloat((recupTotal - recupSolde).toFixed(2))), societe: collab.societe,
role: collab.role, typeCongeId: typeCongeMap['Récupération'],
typeContrat: collab.TypeContrat, typeConge: 'Récupération',
estApprenti: collab.role === 'Apprenti' || collab.TypeContrat === 'Apprentissage' annee: anneeRecup,
}); total: recupTotal,
} solde: recupSolde,
} consomme: parseFloat((recupTotal - recupSolde).toFixed(2)),
} role: collab.role,
typeContrat: collab.TypeContrat,
estApprenti: collab.role === 'Apprenti' || collab.TypeContrat === 'Apprentissage'
});
}
}
}
console.log(`${resultats.length} compteurs détaillés générés`);
res.json(resultats);
} catch (err) {
console.error('❌ Erreur GET ALL:', err);
res.status(500).json({ error: err.message });
}
});
console.log(`${resultats.length} compteurs détaillés générés`);
res.json(resultats);
} catch (err) {
console.error('❌ Erreur GET ALL:', err);
res.status(500).json({ error: err.message });
}
});
// ================================================ // ================================================
@@ -5910,11 +5821,11 @@ app.get('/getAllDetailedCounters', async (req, res) => {
ca.nom, ca.nom,
ca.prenom, ca.prenom,
ca.email, ca.email,
ca.description as poste, ca.fonction as poste,
ca.role,
ca.TypeContrat as typeContrat, ca.TypeContrat as typeContrat,
ca.DateEntree as dateEntree, ca.DateEntree as dateEntree,
ca.actif as Actif, ca.actif as Actif,
ca.description,
-- Société -- Société
COALESCE(soc.Nom, 'Sans société') as societe, COALESCE(soc.Nom, 'Sans société') as societe,
@@ -6043,31 +5954,6 @@ app.get('/getAllDetailedCounters', async (req, res) => {
// PUT /api/collaborateurs/:id - Modifier un collaborateur // PUT /api/collaborateurs/:id - Modifier un collaborateur
// GET /api/collaborateurs/validateurs - Liste des collaborateurs actifs pouvant être validateurs
app.get('/collaborateurs/validateurs', authenticateToken, async (req, res) => {
try {
const result = await pool.request().query(`
SELECT id, CONCAT(prenom, ' ', nom) as nom, email, role
FROM CollaborateurAD
WHERE (Actif = 1 OR Actif IS NULL)
AND (
role LIKE '%Admin%'
OR role LIKE '%RH%'
OR role LIKE '%Validat%'
OR role LIKE '%Directeur%'
OR role LIKE '%Directrice%'
OR role LIKE '%President%'
OR role LIKE '%Président%'
)
ORDER BY nom, prenom
`);
res.json(result.recordset);
} catch (error) {
console.error('❌ Erreur /collaborateurs/validateurs:', error);
res.status(500).json({ error: error.message });
}
});
app.put('/collaborateurs/:id', authenticateToken, async (req, res) => { app.put('/collaborateurs/:id', authenticateToken, async (req, res) => {
const transaction = new sql.Transaction(pool); const transaction = new sql.Transaction(pool);
try { try {
@@ -6087,11 +5973,11 @@ app.get('/getAllDetailedCounters', async (req, res) => {
campusId, campusId,
serviceId, serviceId,
poste, poste,
role,
typeContrat, typeContrat,
dateEntree, dateEntree,
nPlus1Id, nPlus1Id,
nPlus2Id nPlus2Id,
description
} = req.body; } = req.body;
console.log('Modification collaborateur', id, req.body); console.log('Modification collaborateur', id, req.body);
@@ -6115,10 +6001,10 @@ app.get('/getAllDetailedCounters', async (req, res) => {
.input('societeId', sql.Int, societeId) .input('societeId', sql.Int, societeId)
.input('campusId', sql.Int, campusId) .input('campusId', sql.Int, campusId)
.input('serviceId', sql.Int, serviceId) .input('serviceId', sql.Int, serviceId)
.input('fonction', sql.NVarChar(255), poste)
.input('typeContrat', sql.NVarChar(50), typeContrat) .input('typeContrat', sql.NVarChar(50), typeContrat)
.input('dateEntree', sql.Date, dateEntree || null) .input('dateEntree', sql.Date, dateEntree || null)
.input('description', sql.NVarChar(500), poste || null) .input('description', sql.NVarChar(500), description || null)
.input('role', sql.NVarChar(100), role || null)
.query(` .query(`
UPDATE CollaborateurAD UPDATE CollaborateurAD
SET nom = @nom, SET nom = @nom,
@@ -6127,10 +6013,10 @@ app.get('/getAllDetailedCounters', async (req, res) => {
SocieteId = @societeId, SocieteId = @societeId,
CampusId = @campusId, CampusId = @campusId,
ServiceId = @serviceId, ServiceId = @serviceId,
fonction = @fonction,
TypeContrat = @typeContrat, TypeContrat = @typeContrat,
DateEntree = @dateEntree, DateEntree = @dateEntree,
description = @description, description = @description
role = @role
WHERE id = @id WHERE id = @id
`); `);
-79
View File
@@ -1,79 +0,0 @@
import { useState } from "react";
import { Check, ChevronsUpDown } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { cn } from "@/lib/utils";
interface Validateur {
id: number;
nom: string;
email: string;
role: string;
}
interface ValidateurComboboxProps {
validateurs: Validateur[];
value: string;
onChange: (value: string) => void;
placeholder?: string;
}
export function ValidateurCombobox({ validateurs, value, onChange, placeholder = "Sélectionner..." }: ValidateurComboboxProps) {
const [open, setOpen] = useState(false);
const selected = validateurs.find((v) => String(v.id) === value);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="w-full justify-between font-normal"
>
{selected ? `${selected.nom} (${selected.role})` : placeholder}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[--radix-popover-trigger-width] p-0">
<Command>
<CommandInput placeholder="Rechercher un validateur..." />
<CommandList>
<CommandEmpty>Aucun validateur trouvé.</CommandEmpty>
<CommandGroup>
{validateurs.map((v) => (
<CommandItem
key={v.id}
value={`${v.nom} ${v.email}`}
onSelect={() => {
onChange(String(v.id) === value ? "" : String(v.id));
setOpen(false);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
value === String(v.id) ? "opacity-100" : "opacity-0"
)}
/>
<div className="flex flex-col">
<span>{v.nom} <span className="text-muted-foreground">({v.role})</span></span>
<span className="text-xs text-muted-foreground">{v.email}</span>
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}
+3
View File
@@ -54,6 +54,9 @@ const AuthMicrosoft = () => {
// 🔑 Envoie accessToken au backend // 🔑 Envoie accessToken au backend
const authenticateWithBackend = async (accessToken: string) => { const authenticateWithBackend = async (accessToken: string) => {
try { try {
console.log('🔑 Token complet:', accessToken);
console.log('🔑 Token (50 premiers):', accessToken.substring(0, 50));
console.log('🔑 Longueur:', accessToken.length);
const response = await fetch('/api/login-dev', { const response = await fetch('/api/login-dev', {
method: 'POST', method: 'POST',
headers: { headers: {
+1 -26
View File
@@ -3,7 +3,7 @@ import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { import {
Calendar, Users, Clock, CheckCircle, XCircle, AlertCircle, AlertTriangle, Calendar, Users, Clock, CheckCircle, XCircle, AlertCircle,
Plus, LogOut, FileSpreadsheet, RefreshCw, ClipboardList, Plus, LogOut, FileSpreadsheet, RefreshCw, ClipboardList,
FileText, ChevronLeft, ChevronRight FileText, ChevronLeft, ChevronRight
} from "lucide-react"; } from "lucide-react";
@@ -55,8 +55,6 @@ interface Collaborateur {
campusId: number | null; campusId: number | null;
societe: string; societe: string;
societeId: number | null; societeId: number | null;
dateEntree: string | null;
nPlus1Id: number | null;
} }
// ─── Helpers ────────────────────────────────────────────────────────────────── // ─── Helpers ──────────────────────────────────────────────────────────────────
@@ -186,8 +184,6 @@ const Dashboard = () => {
campusId: c.campusId ?? null, campusId: c.campusId ?? null,
societe: c.societe || "", societe: c.societe || "",
societeId: c.societeId ?? null, societeId: c.societeId ?? null,
dateEntree: c.dateEntree ?? null,
nPlus1Id: c.nPlus1Id ?? null,
})) }))
); );
} }
@@ -389,12 +385,6 @@ const Dashboard = () => {
); );
const usedTypes = [...new Set(leavesThisMonth.map(l => getTypeKey(l.typesConge)))]; const usedTypes = [...new Set(leavesThisMonth.map(l => getTypeKey(l.typesConge)))];
// ── Profils à compléter (nouveaux arrivants synchronisés depuis Entra) ─────
const profilsIncompletsCount = allCollabs.filter(
c => !c.societeId || !c.campusId || !c.serviceId || !c.dateEntree || !c.nPlus1Id
).length;
const peutGererCollaborateurs = user?.role === "Admin" || user?.role === "RH";
// ── Cartes stats & actions ──────────────────────────────────────────────── // ── Cartes stats & actions ────────────────────────────────────────────────
const statsCards = [ const statsCards = [
{ title: "Demandes en attente", value: stats.enAttente, icon: Clock, color: "text-warning", bg: "bg-warning/10", action: () => navigate("/validation") }, { title: "Demandes en attente", value: stats.enAttente, icon: Clock, color: "text-warning", bg: "bg-warning/10", action: () => navigate("/validation") },
@@ -449,21 +439,6 @@ const Dashboard = () => {
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-8"> <main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-8">
{/* Alerte profils à compléter */}
{peutGererCollaborateurs && profilsIncompletsCount > 0 && (
<Card
className="shadow-card border-0 border-l-4 border-l-amber-400 bg-amber-50 cursor-pointer hover:shadow-elegant transition-smooth"
onClick={() => navigate("/teams")}
>
<CardContent className="p-4 flex items-center gap-3">
<AlertTriangle className="w-5 h-5 text-amber-600 shrink-0" />
<p className="text-sm text-amber-900">
<strong>{profilsIncompletsCount} profil{profilsIncompletsCount > 1 ? "s" : ""}</strong> à compléter (société, campus, service, date d'entrée ou validateur manquant) — cliquez pour ouvrir la gestion des collaborateurs.
</p>
</CardContent>
</Card>
)}
{/* Stats */} {/* Stats */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{statsCards.map((stat, i) => ( {statsCards.map((stat, i) => (
+37 -31
View File
@@ -64,6 +64,7 @@ interface SoldesALaDate {
}>; }>;
} }
// ⭐ Interface prévisionnel (identique à SaisieManuelle)
interface Previsionnel { interface Previsionnel {
mode: string; mode: string;
isAnticipe: boolean; isAnticipe: boolean;
@@ -370,7 +371,7 @@ const HistoriqueSnapshotDialog = ({ compteur, onClose }: HistoriqueDialogProps)
}; };
// ===================================================== // =====================================================
// PANNEAU PRÉVISIONNEL // PANNEAU PRÉVISIONNEL (réutilisé depuis SaisieManuelle)
// ===================================================== // =====================================================
interface PrevisonnnelPanelProps { interface PrevisonnnelPanelProps {
@@ -399,6 +400,10 @@ const PrevisonnnelPanel = ({ collaborateurId, dateDebut, loadingPrevisionnel, pr
if (!previsionnel) return null; if (!previsionnel) return null;
const estOk = previsionnel.suffisantCP && previsionnel.suffisantRTT; const estOk = previsionnel.suffisantCP && previsionnel.suffisantRTT;
const cpInsuffisant = !previsionnel.suffisantCP && previsionnel.joursNecessairesCP > 0;
const rttInsuffisant = !previsionnel.suffisantRTT && previsionnel.joursNecessairesRTT > 0;
// En mode consultation (pas de saisie), on affiche toujours CP et RTT si dispo
const showCP = previsionnel.totalCPDisponible > 0 || previsionnel.cpNSolde > 0; const showCP = previsionnel.totalCPDisponible > 0 || previsionnel.cpNSolde > 0;
const showRTT = previsionnel.rttDispo > 0 || previsionnel.rttNAcquisAujourdHui > 0; const showRTT = previsionnel.rttDispo > 0 || previsionnel.rttNAcquisAujourdHui > 0;
@@ -424,6 +429,8 @@ const PrevisonnnelPanel = ({ collaborateurId, dateDebut, loadingPrevisionnel, pr
</div> </div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{/* CP */}
{/* CP */}
{showCP && ( {showCP && (
<div className="rounded-lg p-3 border bg-white border-gray-200"> <div className="rounded-lg p-3 border bg-white border-gray-200">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
@@ -434,6 +441,7 @@ const PrevisonnnelPanel = ({ collaborateurId, dateDebut, loadingPrevisionnel, pr
</div> </div>
<div className="space-y-1 text-xs text-gray-600"> <div className="space-y-1 text-xs text-gray-600">
{previsionnel.apresBasculement ? ( {previsionnel.apresBasculement ? (
// ✅ APRÈS LE 1er JUIN : ancien CP N (devenu N-1) + nouveau CP N depuis juin
<> <>
{previsionnel.cpN1Reporte > 0 && ( {previsionnel.cpN1Reporte > 0 && (
<div className="flex justify-between"> <div className="flex justify-between">
@@ -459,6 +467,7 @@ const PrevisonnnelPanel = ({ collaborateurId, dateDebut, loadingPrevisionnel, pr
)} )}
</> </>
) : ( ) : (
// ✅ AVANT LE 1er JUIN : CP N-1 + CP N en cours + anticipé
<> <>
{previsionnel.cpN1Reporte > 0 && ( {previsionnel.cpN1Reporte > 0 && (
<div className="flex justify-between"> <div className="flex justify-between">
@@ -494,6 +503,7 @@ const PrevisonnnelPanel = ({ collaborateurId, dateDebut, loadingPrevisionnel, pr
</div> </div>
)} )}
{/* RTT */}
{showRTT && ( {showRTT && (
<div className="rounded-lg p-3 border bg-white border-gray-200"> <div className="rounded-lg p-3 border bg-white border-gray-200">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
@@ -555,23 +565,22 @@ const GestionCompteurs = () => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [showEditDialog, setShowEditDialog] = useState(false); const [showEditDialog, setShowEditDialog] = useState(false);
// Dialog consultation soldes à une date
const [showSoldesDateDialog, setShowSoldesDateDialog] = useState(false); const [showSoldesDateDialog, setShowSoldesDateDialog] = useState(false);
const [selectedCollaborateurId, setSelectedCollaborateurId] = useState<number | null>(null); const [selectedCollaborateurId, setSelectedCollaborateurId] = useState<number | null>(null);
const [selectedEmployeName, setSelectedEmployeName] = useState<string>(""); const [selectedEmployeName, setSelectedEmployeName] = useState<string>("");
const [dateConsultation, setDateConsultation] = useState(new Date().toISOString().split('T')[0]); const [dateConsultation, setDateConsultation] = useState(new Date().toISOString().split('T')[0]);
// ⭐ État prévisionnel
const [previsionnel, setPrevisionnel] = useState<Previsionnel | null>(null); const [previsionnel, setPrevisionnel] = useState<Previsionnel | null>(null);
const [loadingPrevisionnel, setLoadingPrevisionnel] = useState(false); const [loadingPrevisionnel, setLoadingPrevisionnel] = useState(false);
// ✅ CORRECTION : currentYear + 1 = 2027 = année en cours après bascule
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();
const anneeEnCours = currentYear + 1; // 2027
const anneeN1 = currentYear; // 2026
const [editForm, setEditForm] = useState({ total: 0, solde: 0 }); const [editForm, setEditForm] = useState({ total: 0, solde: 0 });
useEffect(() => { chargerCompteurs(); }, []); useEffect(() => { chargerCompteurs(); }, []);
// ⭐ Recharger le prévisionnel quand la date ou le collaborateur change
useEffect(() => { useEffect(() => {
if (showSoldesDateDialog && selectedCollaborateurId && dateConsultation) { if (showSoldesDateDialog && selectedCollaborateurId && dateConsultation) {
chargerPrevisionnel(); chargerPrevisionnel();
@@ -585,6 +594,7 @@ const GestionCompteurs = () => {
return token ? { Authorization: `Bearer ${token}` } : {}; return token ? { Authorization: `Bearer ${token}` } : {};
}; };
// ⭐ Chargement du prévisionnel
const chargerPrevisionnel = useCallback(async () => { const chargerPrevisionnel = useCallback(async () => {
if (!selectedCollaborateurId || !dateConsultation) return; if (!selectedCollaborateurId || !dateConsultation) return;
@@ -782,8 +792,7 @@ const GestionCompteurs = () => {
<SelectItem value="all">Toutes les années</SelectItem> <SelectItem value="all">Toutes les années</SelectItem>
{annees.map(a => ( {annees.map(a => (
<SelectItem key={a} value={a.toString()}> <SelectItem key={a} value={a.toString()}>
{/* ✅ CORRECTION : En cours = 2027 */} {a} {a === currentYear && '(en cours)'}
{a} {a === anneeEnCours && '(en cours)'}
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
@@ -859,28 +868,19 @@ const GestionCompteurs = () => {
{compteur.typeConge} {compteur.typeConge}
</Badge> </Badge>
</TableCell> </TableCell>
<TableCell className="text-center"> <TableCell className="text-center">
<div className="flex items-center justify-center gap-2"> <div className="flex items-center justify-center gap-2">
<span className="font-medium">{compteur.annee}</span> <span className="font-medium">{compteur.annee}</span>
{/* CP 2027 = En cours */} {compteur.annee === currentYear && (
{compteur.annee === anneeEnCours && compteur.typeConge === 'Congé payé' && ( <Badge variant="outline" className="bg-blue-100 text-blue-700 border-blue-300 text-xs">En cours</Badge>
<Badge variant="outline" className="bg-blue-100 text-blue-700 border-blue-300 text-xs">En cours</Badge> )}
)} {compteur.annee === currentYear - 1 && (
{/* CP 2026 = N-1 */} <Badge variant="outline" className="bg-gray-100 text-gray-600 border-gray-300 text-xs">N-1</Badge>
{compteur.annee === anneeN1 && compteur.typeConge === 'Congé payé' && ( )}
<Badge variant="outline" className="bg-gray-100 text-gray-600 border-gray-300 text-xs">N-1</Badge> </div>
)}
{/* RTT et Récupération 2026 = En cours */}
{compteur.annee === currentYear && compteur.typeConge !== 'Congé payé' && (
<Badge variant="outline" className="bg-blue-100 text-blue-700 border-blue-300 text-xs">En cours</Badge>
)}
</div>
</TableCell>
<TableCell className="text-right font-medium">{compteur.total}</TableCell>
{/* ✅ CORRECTION : consommé jamais négatif */}
<TableCell className="text-right text-muted-foreground">
{compteur.consomme < 0 ? 0 : compteur.consomme}
</TableCell> </TableCell>
<TableCell className="text-right font-medium">{compteur.total}</TableCell>
<TableCell className="text-right text-muted-foreground">{compteur.consomme}</TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
<span className={`font-bold text-lg ${compteur.solde <= 0 ? 'text-destructive' : compteur.total > 0 && compteur.solde / compteur.total <= 0.2 ? 'text-orange-600' : 'text-green-600'}`}> <span className={`font-bold text-lg ${compteur.solde <= 0 ? 'text-destructive' : compteur.total > 0 && compteur.solde / compteur.total <= 0.2 ? 'text-orange-600' : 'text-green-600'}`}>
{compteur.solde} {compteur.solde}
@@ -893,6 +893,7 @@ const GestionCompteurs = () => {
title="Modifier le compteur"> title="Modifier le compteur">
<Edit className="w-4 h-4" /> <Edit className="w-4 h-4" />
</Button> </Button>
<Button variant="outline" size="sm" <Button variant="outline" size="sm"
onClick={() => ouvrirConsultationSoldes(compteur)} onClick={() => ouvrirConsultationSoldes(compteur)}
title="Voir les soldes à une date spécifique" title="Voir les soldes à une date spécifique"
@@ -911,7 +912,9 @@ const GestionCompteurs = () => {
</Card> </Card>
</main> </main>
{/* DIALOG CONSULTATION SOLDES */}
{/* ⭐ DIALOG CONSULTATION SOLDES À UNE DATE + PRÉVISIONNEL */}
<Dialog open={showSoldesDateDialog} onOpenChange={fermerConsultationSoldes}> <Dialog open={showSoldesDateDialog} onOpenChange={fermerConsultationSoldes}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto"> <DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader> <DialogHeader>
@@ -925,6 +928,7 @@ const GestionCompteurs = () => {
</DialogHeader> </DialogHeader>
<div className="space-y-4 py-2"> <div className="space-y-4 py-2">
{/* Sélecteur de date */}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="dateConsultation">Date de consultation</Label> <Label htmlFor="dateConsultation">Date de consultation</Label>
<Input <Input
@@ -938,6 +942,7 @@ const GestionCompteurs = () => {
</p> </p>
</div> </div>
{/* ⭐ Panneau prévisionnel */}
{selectedCollaborateurId && dateConsultation && ( {selectedCollaborateurId && dateConsultation && (
<PrevisonnnelPanel <PrevisonnnelPanel
collaborateurId={selectedCollaborateurId} collaborateurId={selectedCollaborateurId}
@@ -947,6 +952,7 @@ const GestionCompteurs = () => {
/> />
)} )}
{/* Snapshot historique (dates passées uniquement) */}
{selectedCollaborateurId && dateConsultation && {selectedCollaborateurId && dateConsultation &&
dateConsultation <= new Date().toISOString().split('T')[0] && ( dateConsultation <= new Date().toISOString().split('T')[0] && (
<SnapshotDateDisplay <SnapshotDateDisplay
@@ -967,7 +973,7 @@ const GestionCompteurs = () => {
<Dialog open={showEditDialog} onOpenChange={setShowEditDialog}> <Dialog open={showEditDialog} onOpenChange={setShowEditDialog}>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>Modifier le compteur </DialogTitle> <DialogTitle>Modifier le compteur (V2)</DialogTitle>
<DialogDescription> <DialogDescription>
{selectedCompteur && `${selectedCompteur.employe} - ${selectedCompteur.typeConge} ${selectedCompteur.annee}`} {selectedCompteur && `${selectedCompteur.employe} - ${selectedCompteur.typeConge} ${selectedCompteur.annee}`}
</DialogDescription> </DialogDescription>
@@ -1014,7 +1020,7 @@ const GestionCompteurs = () => {
}; };
// ===================================================== // =====================================================
// SNAPSHOT À UNE DATE // SNAPSHOT À UNE DATE (affiché en complément du prévisionnel pour les dates passées)
// ===================================================== // =====================================================
interface SnapshotDateDisplayProps { interface SnapshotDateDisplayProps {
+17 -137
View File
@@ -33,14 +33,6 @@ interface Previsionnel {
suffisant: boolean; suffisantCP: boolean; suffisantRTT: boolean; suffisant: boolean; suffisantCP: boolean; suffisantRTT: boolean;
deficitCP: number; deficitRTT: number; message: string; deficitCP: number; deficitRTT: number; message: string;
} }
interface DoublonDemande {
id: number;
dateDebut: string;
dateFin: string;
statut: string;
nombreJours: number;
types: string;
}
// ─── Helpers ────────────────────────────────────────────────────────────────── // ─── Helpers ──────────────────────────────────────────────────────────────────
const calcWorkingDays = ( const calcWorkingDays = (
@@ -425,8 +417,6 @@ const ImpactPreview = ({
// ═══════════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════════
const SaisieManuelle = () => { const SaisieManuelle = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const [doublonsInfo, setDoublonsInfo] = useState<Record<string, DoublonDemande[]>>({});
const [isCheckingDoublon, setIsCheckingDoublon] = useState(false);
const [employes, setEmployes] = useState<Employe[]>([]); const [employes, setEmployes] = useState<Employe[]>([]);
const [employesFiltres, setEmployesFiltres] = useState<Employe[]>([]); const [employesFiltres, setEmployesFiltres] = useState<Employe[]>([]);
@@ -466,40 +456,6 @@ const SaisieManuelle = () => {
]).then(([a, b]) => setPublicHolidays({ ...a, ...b })).catch(() => { }); ]).then(([a, b]) => setPublicHolidays({ ...a, ...b })).catch(() => { });
}, []); }, []);
// ── Vérification anti-doublon ──────────────────────────────────────────
useEffect(() => {
let cancelled = false;
const checkDoublon = async () => {
if (!employeIdsSelectionnes.length || !dateDebut || !dateFin) {
setDoublonsInfo({});
return;
}
setIsCheckingDoublon(true);
try {
const token = localStorage.getItem("token");
const p = new URLSearchParams({
collaborateurIds: employeIdsSelectionnes.join(","),
dateDebut,
dateFin,
});
const res = await fetch(`/api/demandes/check-doublon?${p}`, {
headers: { Authorization: `Bearer ${token}` },
});
const data = await res.json();
if (cancelled) return;
setDoublonsInfo(data.success && data.hasDoublon ? data.doublons : {});
} catch {
if (!cancelled) setDoublonsInfo({});
} finally {
if (!cancelled) setIsCheckingDoublon(false);
}
};
const debounceTimer = setTimeout(checkDoublon, 250);
return () => { cancelled = true; clearTimeout(debounceTimer); };
}, [employeIdsSelectionnes, dateDebut, dateFin]);
useEffect(() => { useEffect(() => {
const token = localStorage.getItem("token"); const token = localStorage.getItem("token");
const h = { Authorization: `Bearer ${token}` }; const h = { Authorization: `Bearer ${token}` };
@@ -612,30 +568,17 @@ const SaisieManuelle = () => {
return t?.nom.toLowerCase().includes("récup") && l.nombreJours > 0; return t?.nom.toLowerCase().includes("récup") && l.nombreJours > 0;
}); });
// ── Helper : nom d'un employé à partir de son ID
const getEmployeNom = (id: string) => {
const e = employes.find(emp => emp.id.toString() === id);
return e ? `${e.prenom} ${e.nom}` : `Collaborateur #${id}`;
};
const nbDoublons = Object.keys(doublonsInfo).length;
const handleSubmit = async () => { const handleSubmit = async () => {
setError(""); setError("");
if (!employeIdsSelectionnes.length || !dateDebut || !dateFin) { if (!employeIdsSelectionnes.length || !dateDebut || !dateFin) {
setError("Veuillez sélectionner un collaborateur et remplir les dates."); return; setError("Veuillez sélectionner un collaborateur et remplir les dates."); return;
} }
if (nbDoublons > 0) {
setError("Des doublons ont été détectés. Modifiez les dates ou retirez les collaborateurs concernés avant de continuer.");
return;
}
const lignesValides = lignesConge.filter(l => l.typeId > 0 && l.nombreJours > 0); const lignesValides = lignesConge.filter(l => l.typeId > 0 && l.nombreJours > 0);
if (!lignesValides.length) { if (!lignesValides.length) {
setError("Ajoutez au moins un type de congé avec un nombre de jours."); return; setError("Ajoutez au moins un type de congé avec un nombre de jours."); return;
} }
setLoading(true); setLoading(true);
let dernierMessageErreur = ""; // ⭐ Capture le dernier message d'erreur précis
try { try {
const token = localStorage.getItem("token"); const token = localStorage.getItem("token");
let ok = 0, ko = 0; let ok = 0, ko = 0;
@@ -663,35 +606,20 @@ const SaisieManuelle = () => {
body: JSON.stringify({ jours: jJPOSF }), body: JSON.stringify({ jours: jJPOSF }),
}); });
} }
} else { } else ko++;
ko++; } catch { ko++; }
try {
const errData = await res.json();
console.error("❌ Erreur POST /demandes:", res.status, errData);
dernierMessageErreur = errData?.error || `Erreur ${res.status}`;
} catch {
dernierMessageErreur = `Erreur serveur (${res.status})`;
}
}
} catch (fetchErr) {
ko++;
console.error("❌ Exception fetch:", fetchErr);
dernierMessageErreur = "Impossible de contacter le serveur";
}
} }
if (ok > 0) { if (ok > 0) {
toast.success(`✅ ${ok} demande(s) enregistrée(s) et validée(s)`, { toast.success(`✅ ${ok} demande(s) enregistrée(s) et validée(s)`, {
description: ko > 0 ? `${ko} erreur(s)` : undefined, description: ko > 0 ? `${ko} erreur(s)` : undefined,
}); });
navigate("/dashboard"); navigate("/dashboard");
} else { } else throw new Error("Aucune demande créée");
// ⭐ Utilise le message précis capturé, sinon fallback générique
setError(dernierMessageErreur || "Aucune demande n'a pu être créée.");
}
} catch (e: any) { } catch (e: any) {
setError(e.message || "Une erreur est survenue"); setError(e.message || "Erreur lors de l'enregistrement");
} finally { setLoading(false); } } finally { setLoading(false); }
}; };
// ═════════════════════════════════════════════════════════════════════════ // ═════════════════════════════════════════════════════════════════════════
// RENDU — Layout 2 colonnes SAISIE | SOLDES (côte à côte) // RENDU — Layout 2 colonnes SAISIE | SOLDES (côte à côte)
// ═════════════════════════════════════════════════════════════════════════ // ═════════════════════════════════════════════════════════════════════════
@@ -836,21 +764,14 @@ const SaisieManuelle = () => {
{employesSelectionnes.length > 0 && ( {employesSelectionnes.length > 0 && (
<div className="flex flex-wrap gap-1.5 p-3 bg-gray-50 border border-gray-200 rounded-lg"> <div className="flex flex-wrap gap-1.5 p-3 bg-gray-50 border border-gray-200 rounded-lg">
{employesSelectionnes.map(e => { {employesSelectionnes.map(e => (
const aUnDoublon = !!doublonsInfo[e.id.toString()]; <Badge key={e.id} variant="secondary" className="px-2.5 py-1 text-sm">
return ( {e.nom} {e.prenom}
<Badge <button type="button" onClick={() => toggleEmploye(e.id.toString())} className="ml-1.5 hover:text-red-500">
key={e.id} <X className="w-3 h-3" />
variant={aUnDoublon ? "destructive" : "secondary"} </button>
className="px-2.5 py-1 text-sm" </Badge>
> ))}
{aUnDoublon && "⚠️ "}{e.nom} {e.prenom}
<button type="button" onClick={() => toggleEmploye(e.id.toString())} className="ml-1.5 hover:text-red-500">
<X className="w-3 h-3" />
</button>
</Badge>
);
})}
</div> </div>
)} )}
@@ -945,44 +866,6 @@ const SaisieManuelle = () => {
)} )}
</div> </div>
)} )}
{/* ── Bandeau anti-doublon ────────────────────────────── */}
{isCheckingDoublon && (
<div className="flex items-center gap-2 bg-gray-50 border border-gray-200 rounded-lg px-4 py-2 text-xs text-gray-500">
<RefreshCw className="w-3.5 h-3.5 animate-spin" />
Vérification des doublons…
</div>
)}
{!isCheckingDoublon && nbDoublons > 0 && (
<div className="bg-red-50 border-2 border-red-300 rounded-lg p-3">
<div className="flex items-start gap-2">
<AlertCircle className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" />
<div className="flex-1">
<p className="text-sm font-semibold text-red-800">
⚠️ Doublon détecté pour {nbDoublons} collaborateur{nbDoublons > 1 ? "s" : ""}
</p>
<div className="mt-2 space-y-2">
{Object.entries(doublonsInfo).map(([empId, demandes]) => (
<div key={empId} className="bg-white border border-red-200 rounded px-2 py-1.5">
<p className="text-xs font-semibold text-gray-800">{getEmployeNom(empId)}</p>
{demandes.map(d => (
<div key={d.id} className="text-xs text-gray-600 mt-0.5">
{d.types || "Congé"} — {fmtDate(d.dateDebut, { day: "2-digit", month: "short" })}
{d.dateDebut !== d.dateFin && ` au ${fmtDate(d.dateFin, { day: "2-digit", month: "short" })}`}
{" "}({d.nombreJours}j) — <span className={d.statut === "En attente" ? "text-orange-600" : "text-green-600"}>{d.statut}</span>
</div>
))}
</div>
))}
</div>
<p className="text-xs text-red-700 mt-2">
Modifiez les dates ou retirez ces collaborateurs de la sélection pour continuer.
</p>
</div>
</div>
</div>
)}
</div> </div>
</div> </div>
@@ -1086,16 +969,13 @@ const SaisieManuelle = () => {
Annuler Annuler
</button> </button>
<button <button
type="button" onClick={handleSubmit} type="button" onClick={handleSubmit} disabled={loading}
disabled={loading || isCheckingDoublon || nbDoublons > 0}
className="flex-1 px-4 py-2.5 rounded-lg font-medium text-sm flex items-center justify-center gap-2 transition-all bg-blue-600 hover:bg-blue-700 text-white shadow-sm disabled:opacity-50" className="flex-1 px-4 py-2.5 rounded-lg font-medium text-sm flex items-center justify-center gap-2 transition-all bg-blue-600 hover:bg-blue-700 text-white shadow-sm disabled:opacity-50"
> >
{loading ? "Enregistrement" {loading ? "Enregistrement"
: nbDoublons > 0 : previsionnel && !previsionnel.suffisant
? <><AlertCircle className="w-4 h-4" /> Doublon détecté</> ? <><AlertCircle className="w-4 h-4" /> Enregistrer malgré le déficit</>
: previsionnel && !previsionnel.suffisant : <><CheckCircle className="w-4 h-4" /> Enregistrer et valider</>}
? <><AlertCircle className="w-4 h-4" /> Enregistrer malgré le déficit</>
: <><CheckCircle className="w-4 h-4" /> Enregistrer et valider</>}
</button> </button>
</div> </div>
</div> </div>
+3 -327
View File
@@ -4,20 +4,11 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Calendar, Search, ArrowLeft, Users, ChevronDown, ChevronUp, UserCog, AlertTriangle } from "lucide-react"; import { Calendar, Search, ArrowLeft, Users, ChevronDown, ChevronUp } from "lucide-react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { toast } from "sonner"; import { toast } from "sonner";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { ValidateurCombobox } from "@/components/ValidateurCombobox";
interface Collaborateur { interface Collaborateur {
id: number; id: number;
@@ -31,9 +22,9 @@ interface Collaborateur {
service: string; service: string;
serviceId: number | null; serviceId: number | null;
poste: string; poste: string;
role: string;
typeContrat: string; typeContrat: string;
dateEntree: string; dateEntree: string;
description: string;
soldeCPN: number; soldeCPN: number;
soldeCPNMoins1: number; soldeCPNMoins1: number;
soldeRTT: number; soldeRTT: number;
@@ -62,41 +53,6 @@ interface Service {
nom: string; nom: string;
} }
interface Validateur {
id: number;
nom: string;
email: string;
role: string;
}
const ROLES_DISPONIBLES = [
"Collaborateur", "Collaboratrice",
"Validateur", "Validatrice",
"RH", "Admin",
"Directeur de campus", "Directrice de campus",
"President",
"Apprenti", "Stagiaire",
];
const EDITION_INITIALE = {
id: 0,
nom: "",
prenom: "",
email: "",
societeId: "",
campusId: "",
serviceId: "",
poste: "",
role: "",
typeContrat: "",
dateEntree: "",
nPlus1Id: "",
nPlus2Id: "",
};
const estProfilIncomplet = (c: Collaborateur) =>
!c.societeId || !c.campusId || !c.serviceId || !c.dateEntree || !c.nPlus1Id;
const Teams = () => { const Teams = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
@@ -113,95 +69,10 @@ const Teams = () => {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [expandedRows, setExpandedRows] = useState<Set<number>>(new Set()); const [expandedRows, setExpandedRows] = useState<Set<number>>(new Set());
const [validateurs, setValidateurs] = useState<Validateur[]>([]);
const [editionOuverte, setEditionOuverte] = useState(false);
const [edition, setEdition] = useState(EDITION_INITIALE);
const [enregistrement, setEnregistrement] = useState(false);
useEffect(() => { useEffect(() => {
chargerDonnees(); chargerDonnees();
}, []); }, []);
const chargerValidateurs = async () => {
try {
const token = localStorage.getItem('token');
const response = await fetch('/api/collaborateurs/validateurs', {
headers: { 'Authorization': `Bearer ${token}` }
});
if (response.ok) {
setValidateurs(await response.json());
}
} catch (error) {
console.error('Erreur chargement validateurs:', error);
}
};
const ouvrirEdition = (collab: Collaborateur) => {
setEdition({
id: collab.id,
nom: collab.nom || "",
prenom: collab.prenom || "",
email: collab.email || "",
societeId: collab.societeId ? String(collab.societeId) : "",
campusId: collab.campusId ? String(collab.campusId) : "",
serviceId: collab.serviceId ? String(collab.serviceId) : "",
poste: collab.poste || "",
role: collab.role || "",
typeContrat: collab.typeContrat || "",
dateEntree: collab.dateEntree ? collab.dateEntree.split("T")[0] : "",
nPlus1Id: collab.nPlus1Id ? String(collab.nPlus1Id) : "",
nPlus2Id: collab.nPlus2Id ? String(collab.nPlus2Id) : "",
});
chargerValidateurs();
setEditionOuverte(true);
};
const enregistrerEdition = async () => {
if (!edition.role) {
toast.error("Le rôle est obligatoire");
return;
}
setEnregistrement(true);
try {
const token = localStorage.getItem('token');
const response = await fetch(`/api/collaborateurs/${edition.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
nom: edition.nom,
prenom: edition.prenom,
email: edition.email,
societeId: edition.societeId ? parseInt(edition.societeId) : null,
campusId: edition.campusId ? parseInt(edition.campusId) : null,
serviceId: edition.serviceId ? parseInt(edition.serviceId) : null,
poste: edition.poste || null,
role: edition.role || null,
typeContrat: edition.typeContrat || null,
dateEntree: edition.dateEntree || null,
nPlus1Id: edition.nPlus1Id ? parseInt(edition.nPlus1Id) : null,
nPlus2Id: edition.nPlus2Id ? parseInt(edition.nPlus2Id) : null,
})
});
if (!response.ok) {
const data = await response.json().catch(() => ({}));
throw new Error(data.error || "Erreur lors de la mise à jour");
}
toast.success("Collaborateur mis à jour avec succès");
setEditionOuverte(false);
chargerDonnees();
} catch (error) {
toast.error(error instanceof Error ? error.message : "Erreur lors de la mise à jour");
} finally {
setEnregistrement(false);
}
};
const chargerDonnees = async () => { const chargerDonnees = async () => {
setLoading(true); setLoading(true);
try { try {
@@ -264,23 +135,6 @@ const Teams = () => {
} }
}; };
const getRoleBadge = (role: string) => {
switch (role) {
case 'Admin':
return <Badge variant="destructive">{role}</Badge>;
case 'RH':
return <Badge variant="default">{role}</Badge>;
case 'Validateur':
return <Badge variant="secondary">{role}</Badge>;
case 'Directeur de campus':
return <Badge className="bg-purple-600">{role}</Badge>;
case 'Apprenti':
return <Badge variant="outline" className="bg-blue-100 text-blue-700 border-blue-300">{role}</Badge>;
default:
return <Badge variant="outline">{role || 'Non défini'}</Badge>;
}
};
const getTypeContratBadge = (type: string) => { const getTypeContratBadge = (type: string) => {
switch (type) { switch (type) {
case 'forfait_jour': case 'forfait_jour':
@@ -316,9 +170,6 @@ const Teams = () => {
`${a.nom} ${a.prenom}`.localeCompare(`${b.nom} ${b.prenom}`) `${a.nom} ${a.prenom}`.localeCompare(`${b.nom} ${b.prenom}`)
); );
// Profils incomplets (nouveaux arrivants synchronisés depuis Entra sans société/campus/service/date d'entrée/validateur)
const profilsIncomplets = collaborateurs.filter(estProfilIncomplet);
// Listes uniques pour les filtres // Listes uniques pour les filtres
const societesUniques = [...new Set(collaborateurs.map(c => c.societe).filter(Boolean))].sort(); const societesUniques = [...new Set(collaborateurs.map(c => c.societe).filter(Boolean))].sort();
const campusUniques = [...new Set(collaborateurs.map(c => c.campus).filter(Boolean))].sort(); const campusUniques = [...new Set(collaborateurs.map(c => c.campus).filter(Boolean))].sort();
@@ -354,182 +205,11 @@ const Teams = () => {
</div> </div>
</div> </div>
</div> </div>
{profilsIncomplets.length > 0 && (
<Badge variant="outline" className="bg-amber-100 text-amber-800 border-amber-300 gap-1.5 px-3 py-1.5">
<AlertTriangle className="w-4 h-4" />
{profilsIncomplets.length} profil{profilsIncomplets.length > 1 ? 's' : ''} à compléter
</Badge>
)}
</div> </div>
</div> </div>
</header> </header>
<Dialog open={editionOuverte} onOpenChange={setEditionOuverte}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Modifier {edition.prenom} {edition.nom}</DialogTitle>
<DialogDescription>
Renseignez la date d'entrée, le rôle, le validateur, la société, le campus, le service et le type de contrat.
</DialogDescription>
</DialogHeader>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 py-2">
<div className="space-y-2">
<Label htmlFor="ed-dateEntree">Date d'entrée</Label>
<Input
id="ed-dateEntree"
type="date"
value={edition.dateEntree}
onChange={(e) => setEdition({ ...edition, dateEntree: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label>Rôle</Label>
<Select
value={edition.role}
onValueChange={(v) => setEdition({ ...edition, role: v })}
>
<SelectTrigger>
<SelectValue placeholder="Sélectionner..." />
</SelectTrigger>
<SelectContent>
{ROLES_DISPONIBLES.map((r) => (
<SelectItem key={r} value={r}>{r}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Type de contrat</Label>
<Select
value={edition.typeContrat}
onValueChange={(v) => setEdition({ ...edition, typeContrat: v })}
>
<SelectTrigger>
<SelectValue placeholder="Sélectionner..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="forfait_jour">Forfait jours</SelectItem>
<SelectItem value="37h">37h</SelectItem>
<SelectItem value="Apprentissage">Alternant</SelectItem>
<SelectItem value="tempspartiel">Temps partiel</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Société</Label>
<Select
value={edition.societeId}
onValueChange={(v) => setEdition({ ...edition, societeId: v })}
>
<SelectTrigger>
<SelectValue placeholder="Sélectionner..." />
</SelectTrigger>
<SelectContent>
{societes.map((s) => (
<SelectItem key={s.id} value={String(s.id)}>{s.nom}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Campus</Label>
<Select
value={edition.campusId}
onValueChange={(v) => setEdition({ ...edition, campusId: v })}
>
<SelectTrigger>
<SelectValue placeholder="Sélectionner..." />
</SelectTrigger>
<SelectContent>
{campusList.map((c) => (
<SelectItem key={c.id} value={String(c.id)}>{c.nom}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Service</Label>
<Select
value={edition.serviceId}
onValueChange={(v) => setEdition({ ...edition, serviceId: v })}
>
<SelectTrigger>
<SelectValue placeholder="Sélectionner..." />
</SelectTrigger>
<SelectContent>
{services.map((s) => (
<SelectItem key={s.id} value={String(s.id)}>{s.nom}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2 md:col-span-2">
<Label>Validateur (N+1)</Label>
<ValidateurCombobox
validateurs={validateurs}
value={edition.nPlus1Id}
onChange={(v) => setEdition({ ...edition, nPlus1Id: v })}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setEditionOuverte(false)} disabled={enregistrement}>
Annuler
</Button>
<Button onClick={enregistrerEdition} disabled={enregistrement}>
{enregistrement ? "Enregistrement..." : "Enregistrer"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Profils à compléter (nouveaux arrivants synchronisés depuis Entra) */}
{profilsIncomplets.length > 0 && (
<Card className="shadow-card border-0 mb-8 border-l-4 border-l-amber-400">
<CardHeader>
<div className="flex items-center gap-2">
<AlertTriangle className="w-5 h-5 text-amber-600" />
<CardTitle>Profils à compléter</CardTitle>
</div>
<CardDescription>
Ces collaborateurs ont é créés par la synchronisation Entra mais il manque leur société, campus, service, date d'entrée ou validateur.
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-2">
{profilsIncomplets.map((collab) => (
<div
key={collab.id}
className="flex items-center justify-between p-3 rounded-lg border border-amber-200 bg-amber-50"
>
<div>
<p className="font-medium text-sm">{collab.nom} {collab.prenom}</p>
<p className="text-xs text-muted-foreground">{collab.email}</p>
<div className="flex flex-wrap gap-1 mt-1">
{!collab.societeId && <Badge variant="outline" className="text-xs bg-white">Société manquante</Badge>}
{!collab.campusId && <Badge variant="outline" className="text-xs bg-white">Campus manquant</Badge>}
{!collab.serviceId && <Badge variant="outline" className="text-xs bg-white">Service manquant</Badge>}
{!collab.dateEntree && <Badge variant="outline" className="text-xs bg-white">Date d'entrée manquante</Badge>}
{!collab.nPlus1Id && <Badge variant="outline" className="text-xs bg-white">Validateur manquant</Badge>}
</div>
</div>
<Button size="sm" onClick={() => ouvrirEdition(collab)}>
<UserCog className="w-4 h-4 mr-2" />
Compléter
</Button>
</div>
))}
</div>
</CardContent>
</Card>
)}
{/* Filtres */} {/* Filtres */}
<Card className="shadow-card border-0 mb-8"> <Card className="shadow-card border-0 mb-8">
<CardContent className="p-6"> <CardContent className="p-6">
@@ -628,8 +308,6 @@ const Teams = () => {
<TableHead className="w-8"></TableHead> <TableHead className="w-8"></TableHead>
<TableHead>Nom / Prénom</TableHead> <TableHead>Nom / Prénom</TableHead>
<TableHead>Email</TableHead> <TableHead>Email</TableHead>
<TableHead>Poste</TableHead>
<TableHead>Rôle</TableHead>
<TableHead>Société</TableHead> <TableHead>Société</TableHead>
<TableHead>Campus</TableHead> <TableHead>Campus</TableHead>
<TableHead>Service</TableHead> <TableHead>Service</TableHead>
@@ -661,8 +339,6 @@ const Teams = () => {
<TableCell className="text-muted-foreground"> <TableCell className="text-muted-foreground">
{collab.email} {collab.email}
</TableCell> </TableCell>
<TableCell className="text-muted-foreground">{collab.poste || '-'}</TableCell>
<TableCell>{getRoleBadge(collab.role)}</TableCell>
<TableCell>{collab.societe || '-'}</TableCell> <TableCell>{collab.societe || '-'}</TableCell>
<TableCell>{collab.campus || '-'}</TableCell> <TableCell>{collab.campus || '-'}</TableCell>
<TableCell>{collab.service || '-'}</TableCell> <TableCell>{collab.service || '-'}</TableCell>
@@ -692,7 +368,7 @@ const Teams = () => {
{/* Ligne de détails expansible */} {/* Ligne de détails expansible */}
{expandedRows.has(collab.id) && ( {expandedRows.has(collab.id) && (
<TableRow className="bg-muted/30"> <TableRow className="bg-muted/30">
<TableCell colSpan={13}> <TableCell colSpan={11}>
<div className="p-4 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="p-4 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{/* Infos personnelles */} {/* Infos personnelles */}
<div className="space-y-2"> <div className="space-y-2">