Compare commits
1 Commits
314a98c1ad
..
prod
| Author | SHA1 | Date | |
|---|---|---|---|
| 762f9cfd4a |
+273
-387
@@ -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
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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
@@ -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) => (
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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 été 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">
|
||||||
|
|||||||
Reference in New Issue
Block a user