1 Commits

Author SHA1 Message Date
oimer dad1a4af30 prod 2026-05-28 11:32:18 +02:00
14 changed files with 9858 additions and 4410 deletions
+6 -6
View File
@@ -26,12 +26,12 @@ export default defineConfig({
},
server: {
host: '0.0.0.0',
port: 90,
port: 80,
strictPort: true,
allowedHosts: ['mygta-dev.ensup-adm.net', 'localhost'],
allowedHosts: ['mygta.ensup-adm.net', 'localhost'],
proxy: {
'/api': {
target: 'http://backend:3004',
target: 'http://backend:3000',
changeOrigin: true,
secure: false,
configure: (proxy, options) => {
@@ -39,7 +39,7 @@ export default defineConfig({
console.log('Proxy error:', err);
});
proxy.on('proxyReq', (proxyReq, req, res) => {
console.log('Proxying:', req.method, req.url, '-> http://backend:3004');
console.log('Proxying:', req.method, req.url, '-> http://backend:3000');
});
}
}
@@ -48,6 +48,6 @@ export default defineConfig({
});
VITECONFIG
EXPOSE 90
EXPOSE 80
CMD ["npx", "vite", "--host", "0.0.0.0", "--port", "90"]
CMD ["npx", "vite", "--host", "0.0.0.0", "--port", "80"]
@@ -18,7 +18,7 @@ COPY . .
RUN mkdir -p /app/uploads/medical
# Expose the port
EXPOSE 3004
EXPOSE 3000
# Start the server
CMD ["node", "server.js"]
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,185 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Test Bascule CP</title>
<style>
body { font-family: Arial; padding: 30px; max-width: 1000px; margin: 0 auto; background: #f9fafb; }
h1 { color: #1e40af; }
h2 { color: #374151; margin-top: 30px; }
button { padding: 12px 24px; margin: 8px; border: none; border-radius: 8px; cursor: pointer; font-size: 15px; font-weight: bold; }
.btn-verif { background: #3b82f6; color: white; }
.btn-bascule { background: #ef4444; color: white; }
input { padding: 10px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 14px; }
.result-box { background: white; border: 1px solid #e5e7eb; border-radius: 8px; padding: 20px; margin-top: 15px; overflow: auto; max-height: 500px; }
.stat { display: inline-block; background: #dbeafe; color: #1e40af; padding: 8px 16px; border-radius: 20px; margin: 4px; font-weight: bold; }
.stat.rouge { background: #fee2e2; color: #dc2626; }
.stat.vert { background: #dcfce7; color: #16a34a; }
table { width: 100%; border-collapse: collapse; font-size: 13px; }
th { background: #f3f4f6; padding: 8px; text-align: left; border-bottom: 2px solid #e5e7eb; }
td { padding: 7px 8px; border-bottom: 1px solid #f3f4f6; }
tr.negatif { background: #fee2e2; font-weight: bold; }
tr.vide { background: #fef9c3; }
.badge { padding: 3px 10px; border-radius: 12px; font-size: 12px; font-weight: bold; }
.badge.ok { background: #dcfce7; color: #16a34a; }
.badge.negatif { background: #fee2e2; color: #dc2626; }
.badge.vide { background: #fef9c3; color: #92400e; }
.warning { background: #fef3c7; border: 1px solid #f59e0b; border-radius: 8px; padding: 15px; margin: 10px 0; }
</style>
</head>
<body>
<h1>🗓️ Test Bascule CP 31 mai 2026</h1>
<div>
<label>URL du serveur dev :</label><br>
<input type="text" id="serverUrl" value="http://localhost:3000" style="width:280px">
<button onclick="setUrl()" style="background:#6b7280;color:white">Mettre à jour</button>
</div>
<h2>Étape 1 — Vérifier l'état avant bascule</h2>
<button class="btn-verif" onclick="verifier()">🔍 Vérifier les compteurs CP</button>
<h2>Étape 2 — Lancer la bascule</h2>
<div class="warning">
⚠️ À ne faire qu'une seule fois. En prod : le 31 mai soir uniquement.
</div>
<input type="password" id="secret" placeholder="Secret admin" style="width:200px">
<button class="btn-bascule" onclick="basculer()">🔄 Lancer la bascule CP 2026 → 2027</button>
<h2>Étape 3 — Vérifier le résultat après bascule</h2>
<button class="btn-verif" onclick="verifier()">✅ Revérifier les compteurs</button>
<div id="result" class="result-box">
<p style="color:#9ca3af">Lance une vérification pour voir les données...</p>
</div>
<script>
let BASE_URL = 'http://localhost:3004';
function setUrl() {
BASE_URL = document.getElementById('serverUrl').value.replace(/\/$/, '');
}
async function verifier() {
document.getElementById('result').innerHTML = '<p>⏳ Chargement...</p>';
try {
const res = await fetch(`${BASE_URL}/api/admin/verif-bascule-cp`);
const data = await res.json();
if (!data.success) {
document.getElementById('result').innerHTML = `<p style="color:red">Erreur: ${data.message}</p>`;
return;
}
const s = data.stats;
let html = `
<div style="margin-bottom:15px">
<span class="stat">Total lignes: ${s.total_lignes}</span>
<span class="stat ${s.negatifs > 0 ? 'rouge' : 'vert'}">⚠️ Négatifs: ${s.negatifs}</span>
<span class="stat">2025: ${s.annee_2025}</span>
<span class="stat">2026: ${s.annee_2026}</span>
<span class="stat">2027: ${s.annee_2027}</span>
</div>
`;
if (data.attention.length > 0) {
html += `<p style="color:#dc2626;font-weight:bold">🚨 ${data.attention.length} compteur(s) négatif(s) :</p>`;
html += buildTable(data.attention);
html += `<hr style="margin:15px 0">`;
}
html += `<p style="color:#374151;font-weight:bold">Tous les compteurs :</p>`;
html += buildTable(data.data);
document.getElementById('result').innerHTML = html;
} catch (err) {
document.getElementById('result').innerHTML =
`<p style="color:red">❌ Erreur connexion: ${err.message}</p>`;
}
}
function buildTable(rows) {
let t = `<table>
<thead>
<tr>
<th>Collaborateur</th>
<th>Année</th>
<th>Total acquis</th>
<th>Solde</th>
<th>Report</th>
<th>État</th>
</tr>
</thead><tbody>`;
rows.forEach(r => {
const cls = r.etat === 'NEGATIF' ? 'negatif' : r.etat === 'VIDE' ? 'vide' : '';
const badge = `<span class="badge ${r.etat.toLowerCase()}">${r.etat}</span>`;
t += `<tr class="${cls}">
<td>${r.collaborateur}</td>
<td><strong>${r.Annee}</strong></td>
<td>${r.Total}</td>
<td><strong>${r.Solde}</strong></td>
<td>${r.SoldeReporte}</td>
<td>${badge}</td>
</tr>`;
});
t += '</tbody></table>';
return t;
}
async function basculer() {
const secret = document.getElementById('secret').value;
if (!secret) { alert('Entre le secret admin'); return; }
if (!confirm('⚠️ Confirmes-tu le lancement de la bascule CP 2026→2027 ?\n\nCette action modifie tous les compteurs.')) return;
document.getElementById('result').innerHTML = '<p>⏳ Bascule en cours...</p>';
try {
const res = await fetch(`${BASE_URL}/api/admin/basculement-cp-2026`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ secret })
});
const data = await res.json();
if (!data.success) {
document.getElementById('result').innerHTML =
`<p style="color:red">❌ Erreur: ${data.message}</p>`;
return;
}
let html = `
<p style="color:#16a34a;font-size:18px;font-weight:bold">
✅ Bascule terminée — ${data.rapport.length} opérations sur ${data.total_collaborateurs} collaborateurs
</p>
<table>
<thead>
<tr><th>Collaborateur</th><th>Étape</th><th>Détail</th></tr>
</thead><tbody>
`;
data.rapport.forEach(r => {
html += `<tr>
<td>${r.collaborateur}</td>
<td>${r.etape}</td>
<td>${r.detail}</td>
</tr>`;
});
html += '</tbody></table>';
html += `<p style="margin-top:15px;color:#374151">👆 Lance maintenant l'Étape 3 pour vérifier le résultat.</p>`;
document.getElementById('result').innerHTML = html;
} catch (err) {
document.getElementById('result').innerHTML =
`<p style="color:red">❌ Erreur connexion: ${err.message}</p>`;
}
}
</script>
</body>
</html>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+207 -279
View File
@@ -51,7 +51,6 @@ const Calendar = () => {
const [initialFiltersSet, setInitialFiltersSet] = useState(false);
// ⭐ Liste des employés à exclure de l'affichage
const EXCLUDED_EMPLOYEES = ['Kevin Lambert'];
useEffect(() => {
@@ -147,12 +146,6 @@ const Calendar = () => {
try {
let url = `/api/getTeamLeaves?user_id=${userId}&role=${user.role}`;
console.log("📡 AVANT construction URL:", {
selectedSociete,
selectedCampus,
selectedService
});
if (role === 'president' || role === 'rh' || role === 'admin' ||
role === 'directeur de campus' || role === 'directrice de campus' ||
role === 'collaborateur' || role === 'collaboratrice' || role === 'apprenti' ||
@@ -163,24 +156,15 @@ const Calendar = () => {
url += `&selectedService=${encodeURIComponent(selectedService || 'all')}`;
}
console.log("📡 URL finale:", url);
const response = await fetch(url);
const data = await response.json();
console.log("📡 Réponse API:", {
employees: data.filters?.employees?.length,
leaves: data.leaves?.length
});
if (data.success) {
// ⭐ Filtrer les congés des employés exclus
const filteredLeaves = (data.leaves || []).filter(leave =>
!EXCLUDED_EMPLOYEES.includes(leave.employeename)
);
setTeamLeaves(filteredLeaves);
// ⭐ Filtrer les employés exclus des filtres
const filteredFilters = { ...data.filters };
if (filteredFilters.employees) {
filteredFilters.employees = filteredFilters.employees.filter(emp => {
@@ -223,35 +207,40 @@ const Calendar = () => {
}, []);
const handleSSEEvent = useCallback((eventData) => {
console.log('📅 Événement SSE reçu:', eventData.type);
if (eventData.type === 'demande-validated') {
showToast(
`✅ Demande validée: ${eventData.typeConge}`,
'success'
);
console.log(`🎨 Demande ${eventData.demandeId} validée - Type: ${eventData.typeConge}`);
refreshAllData();
}
}, [showToast, refreshAllData]);
useEffect(() => {
if (!userId) {
console.log('❌ userId manquant, SSE non démarré');
setSseConnected(false);
return;
}
console.log('🔌 Initialisation SSE avec userId:', userId);
const eventSource = new EventSource(`/api/sse?user_id=${userId}`);
const eventSource = new EventSource(
`/api/sse?user_id=${userId}`
);
const handleMessage = (event) => {
try {
const eventData = JSON.parse(event.data);
if (eventData.type === 'ping') {
setSseConnected(true);
return;
}
setSseConnected(true);
handleSSEEvent(eventData);
} catch (error) {
console.error('❌ Erreur parsing SSE:', error);
}
};
const handleValidated = (event) => {
try {
const eventData = JSON.parse(event.data);
console.log('📨 SSE demande-validated reçu:', eventData);
setSseConnected(true);
handleSSEEvent(eventData);
} catch (error) {
@@ -260,7 +249,6 @@ const Calendar = () => {
};
const handleHeartbeat = () => {
console.log('💓 SSE Heartbeat');
setSseConnected(true);
};
@@ -270,12 +258,13 @@ const Calendar = () => {
eventSource.close();
};
eventSource.addEventListener('message', handleMessage);
eventSource.addEventListener('demande-validated', handleValidated);
eventSource.addEventListener('ping', handleHeartbeat);
eventSource.onerror = handleError;
return () => {
console.log('🔌 Fermeture SSE');
eventSource.removeEventListener('message', handleMessage);
eventSource.removeEventListener('demande-validated', handleValidated);
eventSource.removeEventListener('ping', handleHeartbeat);
eventSource.close();
@@ -289,7 +278,6 @@ const Calendar = () => {
useEffect(() => {
if (isFirstLoad && filters.defaultCampus && (role === 'directeur de campus' || role === 'directrice de campus')) {
console.log('🏢 Initialisation campus par défaut:', filters.defaultCampus);
setSelectedCampus(filters.defaultCampus);
setIsFirstLoad(false);
}
@@ -298,19 +286,11 @@ const Calendar = () => {
useEffect(() => {
if (!initialFiltersSet &&
filters.defaultCampus &&
(role === 'collaborateur' || role === 'collaboratrice' || role === 'apprenti')) {
console.log('🎯 Initialisation des filtres par défaut pour collaborateur');
console.log('📍 Valeurs reçues du backend:', {
defaultSociete: filters.defaultSociete,
defaultCampus: filters.defaultCampus,
defaultService: filters.defaultService
});
(role === 'collaborateur' || role === 'collaboratrice' || role === 'apprenti' || role === 'rh')) {
setSelectedSociete(filters.defaultSociete || 'all');
setSelectedCampus(filters.defaultCampus || 'all');
setSelectedService(filters.defaultService || 'all');
setInitialFiltersSet(true);
}
}, [filters.defaultCampus, filters.defaultService, filters.defaultSociete, initialFiltersSet, role]);
@@ -320,35 +300,19 @@ const Calendar = () => {
filters.defaultCampus &&
(role === 'validateur' || role === 'validatrice')) {
console.log('🎯 Initialisation des filtres par défaut pour validateur');
console.log('📍 Valeurs reçues du backend:', {
defaultSociete: filters.defaultSociete,
defaultCampus: filters.defaultCampus,
defaultService: filters.defaultService
});
setSelectedSociete(filters.defaultSociete || 'all');
setSelectedCampus(filters.defaultCampus || 'all');
setSelectedService(filters.defaultService || 'all');
setInitialFiltersSet(true);
}
}, [filters.defaultCampus, filters.defaultService, filters.defaultSociete, initialFiltersSet, role]);
useEffect(() => {
// ⭐ Exclure "president" de la liste des rôles qui déclenchent le rechargement
if (role === 'rh' || role === 'admin' ||
role === 'directeur de campus' || role === 'directrice de campus' ||
role === 'collaborateur' || role === 'collaboratrice' || role === 'apprenti' ||
role === 'validateur' || role === 'validatrice') {
console.log("🔄 Rechargement données:", {
societe: selectedSociete,
campus: selectedCampus,
service: selectedService,
role: role
});
loadTeamLeaves();
}
}, [selectedSociete, selectedCampus, selectedService, role]);
@@ -358,13 +322,11 @@ const Calendar = () => {
const month = currentDate.getMonth();
const lastDay = new Date(year, month + 1, 0);
const daysInMonth = lastDay.getDate();
const days = [];
for (let day = 1; day <= daysInMonth; day++) {
const currentDay = new Date(year, month, day);
const dayOfWeek = currentDay.getDay();
if ((dayOfWeek >= 1 && dayOfWeek <= 6) || dayOfWeek === 0) {
days.push(currentDay);
}
@@ -384,7 +346,6 @@ const Calendar = () => {
const lastDay = new Date(year, month + 1, 0);
const daysInMonth = lastDay.getDate();
const startingDayOfWeek = (firstDay.getDay() + 6) % 7;
const days = [];
for (let i = 0; i < startingDayOfWeek; i++) {
@@ -419,7 +380,6 @@ const Calendar = () => {
const seenNames = new Set();
for (const emp of employeeList) {
// ⭐ Exclure les employés de la liste noire
if (!seenNames.has(emp.name) && !EXCLUDED_EMPLOYEES.includes(emp.name)) {
seenNames.add(emp.name);
uniqueEmployees.push(emp);
@@ -431,14 +391,12 @@ const Calendar = () => {
const getDisplayedEmployees = () => {
const allEmployees = getAllEmployees();
let filteredEmployees = allEmployees;
if (selectedEmployees.length > 0) {
filteredEmployees = allEmployees.filter(emp => selectedEmployees.includes(emp.name));
}
// ⭐ Exclure "president" de la condition de tri automatique
const shouldAutoSort = (
['rh', 'admin', 'directeur de campus', 'directrice de campus'].includes(role) &&
selectedCampus === 'all' &&
@@ -452,8 +410,6 @@ const Calendar = () => {
);
if (shouldAutoSort) {
console.log("🔄 Tri automatique par société → campus → service → nom");
return filteredEmployees.sort((a, b) => {
const isAMultiCampus = a.societe === 'Ensup Solution & Support' || a.societe === 'Ensup Solution et Support';
const isBMultiCampus = b.societe === 'Ensup Solution & Support' || b.societe === 'Ensup Solution et Support';
@@ -463,14 +419,8 @@ const Calendar = () => {
if (isAMultiCampus && isBMultiCampus) {
const campusOrder = {
'CGY': 1,
'Cergy': 1,
'SQY': 2,
'Saint-Quentin': 2,
'Marseille': 3,
'Nantes': 4,
'Multi-campus': 5,
'N/A': 6
'CGY': 1, 'Cergy': 1, 'SQY': 2, 'Saint-Quentin': 2,
'Marseille': 3, 'Nantes': 4, 'Multi-campus': 5, 'N/A': 6
};
const orderA = campusOrder[a.campus] || 999;
@@ -485,12 +435,8 @@ const Calendar = () => {
}
const campusOrder = {
'CGY': 1,
'Cergy': 1,
'SQY': 2,
'Saint-Quentin': 2,
'Marseille': 3,
'Nantes': 4
'CGY': 1, 'Cergy': 1, 'SQY': 2, 'Saint-Quentin': 2,
'Marseille': 3, 'Nantes': 4
};
const orderA = campusOrder[a.campus] || 999;
@@ -509,7 +455,6 @@ const Calendar = () => {
}
if (shouldSortByService) {
console.log("🔄 Tri par service pour collaborateur/validateur");
return filteredEmployees.sort((a, b) => {
const serviceCompare = (a.service || '').localeCompare(b.service || '');
if (serviceCompare !== 0) return serviceCompare;
@@ -524,7 +469,6 @@ const Calendar = () => {
const baseFiltered = getBaseFilteredLeaves();
const displayedEmployees = getDisplayedEmployees();
const displayedNames = displayedEmployees.map(emp => emp.name);
return baseFiltered.filter(leave => displayedNames.includes(leave.employeename));
};
@@ -537,22 +481,12 @@ const Calendar = () => {
setSelectedEmployees(prev => {
const filtered = prev.filter(name => availableNames.includes(name));
if (filtered.length !== prev.length) {
console.log("🧹 Nettoyage employés:", {
avant: prev.length,
après: filtered.length
});
return filtered;
}
return prev;
});
}, [allEmployeesData]);
useEffect(() => {
console.log("👥 Employés disponibles (après filtres):", allEmployeesData.length);
console.log("✅ Employés sélectionnés:", selectedEmployees.length);
console.log("📊 Employés affichés dans le tableau:", getDisplayedEmployees().length);
}, [allEmployeesData, selectedEmployees]);
const toggleEmployee = (employeeName) => {
setSelectedEmployees(prev => {
if (prev.includes(employeeName)) {
@@ -580,15 +514,39 @@ const Calendar = () => {
const getLeaveForEmployeeOnDate = (employeeName, date) => {
if (!date) return null;
const displayedLeaves = getDisplayedLeaves();
return displayedLeaves.find(leave => {
const leaves = displayedLeaves.filter(leave => {
if (leave.employeename !== employeeName) return false;
const start = parseLocalDate(leave.startdate);
const end = parseLocalDate(leave.enddate);
return date >= start && date <= end;
});
if (leaves.length === 0) return null;
if (leaves.length === 1) return leaves[0];
// Fusionner : sommer les jours + combiner les détails
const totalJours = leaves.reduce((sum, l) => sum + parseFloat(l.nombrejoursouvres || 0), 0);
// ✅ Conserver les détails de chaque leave séparément pour l'affichage bi-couleur
const allDetails = leaves.flatMap(l => {
try {
const parsed = typeof l.detailsconges === 'string'
? JSON.parse(l.detailsconges)
: l.detailsconges || [];
// Attacher la couleur du leave parent à chaque détail
const colorObj = getLeaveColor(l);
return parsed.map(d => ({ ...d, _color: colorObj.hexColor || '#4ade80' }));
} catch { return []; }
});
return {
...leaves[0],
nombrejoursouvres: totalJours,
type: leaves.map(l => l.type).join(', '),
_isMerged: true,
_mergedDetails: allDetails,
detailsconges: JSON.stringify(allDetails)
};
};
const hasLeave = (date) => {
@@ -601,77 +559,38 @@ const Calendar = () => {
});
};
// 🆕 FONCTION CORRIGÉE : RÉCUP SUR WEEKEND = MAGENTA
const isJPOSFLeave = (leave) => {
if (!leave || !leave.type) return false;
const type = leave.type.toLowerCase();
return type.includes('jpo') ||
type.includes('jposf') ||
type.includes('sf') ||
type.includes('jpocpf') ||
type.includes('journée portes ouvertes') ||
type.includes('journee portes ouvertes') ||
type.includes('salon formation') ||
type.includes('jpos');
};
const getLeaveColor = (leave, date = null) => {
if (!leave) return { tailwindClass: '', hexColor: '' };
const status = leave.statut?.toLowerCase();
const type = leave.type?.toLowerCase();
// Détection si saisie par RH
const saisieParRH = leave.createdbyrole?.toLowerCase() === 'rh' || leave.saisieParRH === true;
// Détection des types
const isRecup = type?.includes('récup') ||
type?.includes('recup') ||
type?.includes('récupération') ||
type?.includes('recuperation');
const isJPOSF = type?.includes('jpo') ||
type?.includes('sf') ||
type?.includes('jpocpf') ||
type?.includes('journée portes ouvertes') ||
type?.includes('journee portes ouvertes') ||
type?.includes('salon formation');
// Détection weekend
const isWeekend = date && (date.getDay() === 6 || date.getDay() === 0);
// 🔴 PRIORITÉ 1 : RÉCUP SUR SAMEDI/DIMANCHE = MAGENTA (#d946ef)
if (isRecup && isWeekend) {
if (isJPOSFLeave(leave)) {
return { tailwindClass: '', hexColor: '#d946ef' };
}
// 🔴 PRIORITÉ 2 : JPO/SF SAISIE PAR RH = MAGENTA (#d946ef) - TOUJOURS
if (saisieParRH && isJPOSF) {
return { tailwindClass: '', hexColor: '#d946ef' };
if (type.includes('télétravail') || type.includes('teletravail') || type.includes('distanciel')) {
return { tailwindClass: 'bg-teal-200', hexColor: '#99e4d8' }; // ← vert-bleu clair comme l'image
}
// 🟠 PRIORITÉ 3 : Statut "en attente" = ORANGE (pour TOUS les types)
if (status === 'en attente' || status === 'pending' || status === 'en attente de validation') {
return { tailwindClass: 'bg-orange-400', hexColor: '#fb923c' };
}
// 🟢 PRIORITÉ 4 : Si validé, on applique les couleurs selon le type
// JPO/SF NORMAL (non RH) = MAGENTA aussi
if (isJPOSF) {
return { tailwindClass: '', hexColor: '#d946ef' };
}
// Récup (peu importe qui l'a saisie, si validée) = VERT
if (isRecup) {
return { tailwindClass: 'bg-green-400', hexColor: '#4ade80' };
}
// Formation = BLEU FONCÉ (#60a5fa)
if (type?.includes('formation')) {
if (type.includes('formation')) {
return { tailwindClass: 'bg-blue-400', hexColor: '#60a5fa' };
}
// CP/Congé validé = VERT (#4ade80)
if (type?.includes('cp') ||
type?.includes('congé') ||
type?.includes('conge') ||
type?.includes('payé') ||
type?.includes('paye')) {
return { tailwindClass: 'bg-green-400', hexColor: '#4ade80' };
}
// Par défaut : VERT (congé validé sans type spécifique)
return { tailwindClass: 'bg-green-400', hexColor: '#4ade80' };
return { tailwindClass: 'bg-green-400', hexColor: '#4ade80' }; // congé reste vert classique
};
const handleMouseMove = (e) => {
if (!isMobile) {
setMousePosition({ x: e.clientX, y: e.clientY });
@@ -721,7 +640,6 @@ const Calendar = () => {
const calculateWorkingDays = (start, end) => {
if (!start || !end) return 0;
let workingDays = 0;
const current = new Date(start);
@@ -756,7 +674,6 @@ const Calendar = () => {
if (date >= selectedDate) {
setSelectedEndDate(date);
setIsSelectingRange(false);
setTimeout(() => {
setContextMenu({
show: true,
@@ -792,8 +709,6 @@ const Calendar = () => {
const startDate = selectedDate;
const endDate = selectedEndDate || selectedDate;
const includesSaturday = checkIfRangeIncludesSaturday(startDate, endDate);
setPreselectedDates({
startDate: startDate,
endDate: endDate,
@@ -839,7 +754,6 @@ const Calendar = () => {
? (detailedCounters.cpN?.solde || 0) + (detailedCounters.cpN1?.solde || 0) + (detailedCounters.rttN?.solde || 0)
: 0;
// ⭐ Exclure "president" de la liste des rôles autorisés pour les filtres
const canViewAllFilters = ['rh', 'admin', 'directeur de campus', 'directrice de campus', 'validateur', 'validatrice', 'collaborateur', 'collaboratrice', 'apprenti'].includes(role);
const canViewCampusFilters = ['rh', 'admin', 'directeur de campus', 'directrice de campus', 'validateur', 'validatrice', 'collaborateur', 'collaboratrice', 'apprenti'].includes(role);
@@ -878,50 +792,100 @@ const Calendar = () => {
let details;
try {
if (typeof leave.detailsconges === 'string') {
details = JSON.parse(leave.detailsconges);
} else {
details = leave.detailsconges || [];
}
details = typeof leave.detailsconges === 'string'
? JSON.parse(leave.detailsconges)
: leave.detailsconges || [];
} catch (e) {
console.error('Erreur parsing detailsconges:', e, leave.detailsconges);
details = [];
}
const isPeriodePartielle = Array.isArray(details) && details.some(d =>
d.periode === 'Matin' || d.periode === 'Après-midi'
);
if (isPeriodePartielle) {
const hasMatin = details?.some(d => d.periode === 'Matin');
const hasApresMidi = details?.some(d => d.periode === 'Après-midi');
const dateStr = date ? formatDateToString(date) : null;
// ── Cas fusion : deux demi-journées = journée complète ───────────
if (leave._isMerged) {
return (
<div className="relative h-6 w-6 mx-auto rounded overflow-hidden cursor-pointer">
{hasMatin && <div
className="absolute top-0 left-0 right-0 h-3"
style={{ backgroundColor: bgColor }}
></div>}
{hasApresMidi && <div
className="absolute bottom-0 left-0 right-0 h-3"
style={{ backgroundColor: bgColor }}
></div>}
{!hasMatin && !hasApresMidi && <div
className="h-full w-full"
style={{ backgroundColor: bgColor }}
></div>}
</div>
<div className="h-6 w-6 mx-auto rounded cursor-pointer"
style={{ backgroundColor: bgColor }} />
);
}
// ── Cas normal (une seule demande) ───────────────────────────────
const detail = details[0] || {};
const periodeDebut = detail.periodeDebut || detail.periode || 'Journée entière';
const periodeFin = detail.periodeFin || detail.periode || 'Journée entière';
const segStartdate = detail.dateDebut || leave.startdate;
const segEnddate = detail.dateFin || leave.enddate;
const isStartDay = dateStr === segStartdate;
const isEndDay = dateStr === segEnddate;
const isSingleDay = segStartdate === segEnddate;
// ── Jour unique ──────────────────────────────────────────────────
if (isSingleDay) {
const isPartial = periodeDebut !== 'Journée entière' || periodeFin !== 'Journée entière';
if (isPartial) {
const afficheMatin = periodeDebut === 'Matin' || periodeFin === 'Matin';
const afficheApresMidi = periodeDebut === 'Après-midi' || periodeFin === 'Après-midi';
return (
<div
className="h-6 w-6 mx-auto rounded cursor-pointer"
style={{ backgroundColor: bgColor }}
></div>
<div className="relative h-6 w-6 mx-auto rounded overflow-hidden cursor-pointer">
{afficheMatin && (
<div className="absolute top-0 left-0 right-0 h-3"
style={{ backgroundColor: bgColor }} />
)}
{afficheApresMidi && (
<div className="absolute bottom-0 left-0 right-0 h-3"
style={{ backgroundColor: bgColor }} />
)}
</div>
);
}
}
// ── Premier jour ─────────────────────────────────────────────────
if (isStartDay && !isSingleDay) {
if (periodeDebut === 'Après-midi') {
return (
<div className="relative h-6 w-6 mx-auto rounded overflow-hidden cursor-pointer">
<div className="absolute bottom-0 left-0 right-0 h-3"
style={{ backgroundColor: bgColor }} />
</div>
);
}
if (periodeDebut === 'Matin') {
return (
<div className="relative h-6 w-6 mx-auto rounded overflow-hidden cursor-pointer">
<div className="absolute top-0 left-0 right-0 h-3"
style={{ backgroundColor: bgColor }} />
</div>
);
}
}
// ── Dernier jour ─────────────────────────────────────────────────
if (isEndDay && !isSingleDay) {
if (periodeFin === 'Matin') {
return (
<div className="relative h-6 w-6 mx-auto rounded overflow-hidden cursor-pointer">
<div className="absolute top-0 left-0 right-0 h-3"
style={{ backgroundColor: bgColor }} />
</div>
);
}
if (periodeFin === 'Après-midi') {
return (
<div className="relative h-6 w-6 mx-auto rounded overflow-hidden cursor-pointer">
<div className="absolute bottom-0 left-0 right-0 h-3"
style={{ backgroundColor: bgColor }} />
</div>
);
}
}
// ── Journée complète ─────────────────────────────────────────────
return (
<div className="h-6 w-6 mx-auto rounded cursor-pointer"
style={{ backgroundColor: bgColor }} />
);
};
const renderMobileLeaveCell = (leave, date) => {
if (!leave) {
if (isHoliday(date)) {
@@ -977,7 +941,7 @@ const Calendar = () => {
{/* Selection mode banner */}
{isSelectingRange && (
<div className="mb-4 p-4 bg-blue-50 border border-blue-200 rounded-lg flex items-center justify-between" data-tour="mode-selection">
<div className="mb-4 p-4 bg-blue-50 border border-blue-200 rounded-lg flex items-center justify-between">
<div className="flex items-center gap-3">
<CalendarIcon className="w-5 h-5 text-blue-600" />
<div>
@@ -1028,7 +992,6 @@ const Calendar = () => {
<button
onClick={() => setShowFilters(!showFilters)}
className="flex items-center gap-2 px-4 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
data-tour="filtres-btn"
>
<Filter className="w-4 h-4" />
<span className="text-sm font-medium">
@@ -1052,7 +1015,6 @@ const Calendar = () => {
onClick={refreshAllData}
disabled={isRefreshing}
className="p-2 text-gray-600 hover:bg-gray-100 rounded border border-gray-300"
data-tour="refresh-btn"
>
<RefreshCw className={`w-5 h-5 mx-auto ${isRefreshing ? 'animate-spin' : ''}`} />
</button>
@@ -1063,14 +1025,13 @@ const Calendar = () => {
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 pt-4 border-t border-gray-200">
{/* Société filter */}
{canViewAllFilters && filters.societes && filters.societes.length > 0 && (
<div data-tour="filtre-societe">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Société {selectedSociete !== 'all' && <span className="text-xs text-gray-500 ml-1">(filtré)</span>}
</label>
<select
value={selectedSociete}
onChange={(e) => {
console.log("📍 Changement société:", e.target.value);
setSelectedSociete(e.target.value);
setSelectedCampus('all');
setSelectedService('all');
@@ -1088,7 +1049,7 @@ const Calendar = () => {
{/* Campus filter */}
{canViewCampusFilters && filters.campus && filters.campus.length > 0 && (
<div data-tour="filtre-campus">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Campus
{selectedSociete !== 'all' && <span className="text-xs text-gray-500 ml-1">(filtré par {selectedSociete})</span>}
@@ -1096,7 +1057,6 @@ const Calendar = () => {
<select
value={selectedCampus}
onChange={(e) => {
console.log("📍 Changement campus:", e.target.value);
setSelectedCampus(e.target.value);
setSelectedService('all');
setSelectedEmployees([]);
@@ -1113,7 +1073,7 @@ const Calendar = () => {
{/* Service filter */}
{filters.services && filters.services.length > 0 && (
<div data-tour="filtre-service">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Service
{(selectedSociete !== 'all' || selectedCampus !== 'all') && <span className="text-xs text-gray-500 ml-1">(filtré)</span>}
@@ -1121,7 +1081,6 @@ const Calendar = () => {
<select
value={selectedService}
onChange={(e) => {
console.log("📍 Changement service:", e.target.value);
setSelectedService(e.target.value);
setSelectedEmployees([]);
}}
@@ -1136,7 +1095,7 @@ const Calendar = () => {
)}
{/* Employee selection */}
<div className="col-span-full" data-tour="selection-collaborateurs">
<div className="col-span-full">
<div className="flex items-center justify-between mb-2">
<label className="block text-sm font-medium text-gray-700">
Collaborateurs à afficher
@@ -1238,10 +1197,12 @@ const Calendar = () => {
{/* CALENDRIER DESKTOP */}
{!isMobile && (
<div className="bg-white rounded-lg border overflow-hidden shadow-sm">
<div className="overflow-x-auto">
{/* Conteneur avec scroll horizontal et vertical optimisé */}
<div className="relative">
<div className="overflow-x-auto overflow-y-auto max-h-[calc(100vh-300px)] scroll-smooth">
<div className="inline-block min-w-full">
{/* Month navigation */}
<div className="flex items-center justify-center py-4 px-4 border-b bg-gray-50" data-tour="navigation-mois">
<div className="sticky top-0 z-30 flex items-center justify-center py-4 px-4 border-b bg-gray-50">
<button onClick={() => navigateMonth('prev')} className="p-2 hover:bg-gray-200 rounded">
<ChevronLeft className="w-5 h-5 text-blue-600" />
</button>
@@ -1256,11 +1217,11 @@ const Calendar = () => {
</div>
{/* Calendar table */}
<table className="w-full border-collapse" data-tour="calendar-grid">
<thead>
<table className="w-full border-collapse">
<thead className="sticky top-[60px] z-20">
<tr className="border-b bg-gray-50">
<th className="sticky left-0 bg-gray-50 z-20 border-r px-4 py-3 text-left w-48">
<span className="text-sm font-semibold">
<th className="sticky left-0 bg-gray-50 z-30 border-r px-4 py-3 text-left min-w-[180px] max-w-[220px]">
<span className="text-sm font-semibold truncate block">
Collaborateurs ({employees.length}{selectedEmployees.length > 0 && `/${allEmployees.length}`})
</span>
</th>
@@ -1275,7 +1236,7 @@ const Calendar = () => {
return (
<th
key={index}
className={`border-r px-2 py-2 text-center min-w-[40px] ${holiday ? 'bg-gray-600 text-white' :
className={`border-r px-1 py-2 text-center min-w-[32px] w-[36px] ${holiday ? 'bg-gray-600 text-white' :
sunday ? 'bg-gray-300' :
saturday ? 'bg-gray-300' :
inRange ? 'bg-blue-100' :
@@ -1324,18 +1285,17 @@ const Calendar = () => {
) : (
employees.map((employee, empIndex) => (
<tr key={empIndex} className="border-b hover:bg-gray-50">
<td className="sticky left-0 bg-white z-10 border-r px-4 py-3">
<div className="font-medium text-sm">{employee.name}</div>
<div className="text-xs text-gray-500 mt-0.5">
<td className="sticky left-0 bg-white z-10 border-r px-2 py-3 min-w-[160px] max-w-[200px]">
<div className="font-medium text-xs lg:text-sm truncate" title={employee.name}>
{employee.name}
</div>
<div className="text-xs text-gray-500 mt-0.5 truncate">
{employee.service}
{canViewCampusFilters && employee.societe && (
<> {employee.societe}</>
<span className="hidden lg:inline"> {employee.societe}</span>
)}
{(role === 'rh' || role === 'admin') && employee.campus && (
<> {employee.campus}</>
)}
{(role === 'directeur de campus' || role === 'directrice de campus') && employee.societe === 'Ensup Solution & Support' && employee.campus !== 'N/A' && (
<> {employee.campus}</>
<span className="hidden xl:inline"> {employee.campus}</span>
)}
</div>
</td>
@@ -1349,29 +1309,6 @@ const Calendar = () => {
const isToday = date.toDateString() === new Date().toDateString();
const isWeekend = saturday || sunday;
// 🆕 Détection récup sur weekend
const isRecupWeekend = leave && isWeekend && (
leave.type?.toLowerCase().includes('récup') ||
leave.type?.toLowerCase().includes('recup') ||
leave.type?.toLowerCase().includes('récupération') ||
leave.type?.toLowerCase().includes('recuperation')
);
let details, hasMatin = false, hasApresMidi = false;
if (leave) {
try {
if (typeof leave.detailsconges === 'string') {
details = JSON.parse(leave.detailsconges);
} else if (Array.isArray(leave.detailsconges)) {
details = leave.detailsconges;
}
hasMatin = details?.some(d => d.periode === 'Matin');
hasApresMidi = details?.some(d => d.periode === 'Après-midi');
} catch (e) {
console.error('Erreur parsing:', e);
}
}
return (
<td
key={dayIndex}
@@ -1379,7 +1316,7 @@ const Calendar = () => {
inRange ? 'bg-blue-100' :
past ? 'bg-gray-50 opacity-60' :
holiday ? 'bg-gray-600' :
isWeekend && !isRecupWeekend ? 'bg-gray-200' : ''
isWeekend ? 'bg-gray-200' : ''
} ${leave || (employee.name === `${user.prenom} ${user.nom}` && !past && !holiday && !isWeekend)
? ''
: 'cursor-not-allowed'
@@ -1397,51 +1334,25 @@ const Calendar = () => {
onMouseEnter={() => !isMobile && leave && setHoveredLeave({ employee, leave, date })}
onMouseLeave={() => setHoveredLeave(null)}
title={
isRecupWeekend ? `${saturday ? 'Samedi' : 'Dimanche'} - Récupération` :
isHoliday(date) ? getHolidayName(date) :
sunday ? 'Dimanche - Non sélectionnable' :
saturday ? 'Samedi - Non sélectionnable' :
''
}
>
{/* JOUR FÉRIÉ */}
{holiday ? (
<div className="h-6 w-6 mx-auto bg-gray-700 rounded" title={getHolidayName(date)}></div>
/* 🆕 RÉCUP SUR WEEKEND → MAGENTA */
) : isRecupWeekend ? (
<div className="relative h-6 w-6 mx-auto rounded overflow-hidden">
{hasMatin && (
<div className="absolute top-0 left-0 right-0 h-3" style={{ backgroundColor: '#d946ef' }}></div>
)}
{hasApresMidi && (
<div className="absolute bottom-0 left-0 right-0 h-3" style={{ backgroundColor: '#d946ef' }}></div>
)}
{!hasMatin && !hasApresMidi && (
<div className="h-full w-full" style={{ backgroundColor: '#d946ef' }}></div>
)}
</div>
/* WEEKEND NORMAL SANS CONGÉ → GRIS */
) : isWeekend && !leave ? (
<div className="h-6 w-6 mx-auto bg-gray-300 rounded"></div>
/* WEEKEND AVEC AUTRE CONGÉ → GRIS AUSSI */
) : isWeekend && leave ? (
) : isJPOSFLeave(leave) ? (
renderLeaveCell(leave, date)
) : isWeekend ? (
<div className="h-6 w-6 mx-auto bg-gray-300 rounded"
title={`${saturday ? 'Samedi' : 'Dimanche'} - Congé non modifiable`}>
title={leave ? `${saturday ? 'Samedi' : 'Dimanche'} - Congé non modifiable` : ''}>
</div>
/* CONGÉ NORMAL EN SEMAINE */
) : leave ? (
renderLeaveCell(leave, date)
/* CELLULE VIDE CLIQUABLE */
) : employee.name === `${user.prenom} ${user.nom}` && !past && !holiday ? (
<div className={`h-6 w-6 mx-auto rounded cursor-pointer transition-all ${inRange ? 'bg-blue-500 scale-110' : 'bg-gray-100 hover:bg-blue-200 hover:scale-105'
}`}></div>
/* CELLULE VIDE NON CLIQUABLE */
) : (
<div className="h-6 w-6 mx-auto bg-gray-100 rounded"></div>
)}
@@ -1455,25 +1366,30 @@ const Calendar = () => {
</table>
</div>
</div>
</div>
{/* Legend */}
<div className="flex items-center gap-3 lg:gap-6 p-4 border-t border-gray-100 flex-wrap text-xs lg:text-sm" data-tour="legende">
<div className="flex items-center gap-3 lg:gap-6 p-4 border-t border-gray-100 flex-wrap text-xs lg:text-sm">
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-cyan-50 border border-cyan-200 rounded"></div>
<span className="text-gray-600">Aujourd'hui</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-green-400 rounded"></div>
<span className="text-gray-600">Validé</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-orange-400 rounded"></div>
<span className="text-gray-600">En attente</span>
<span className="text-gray-600">Absence</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-blue-400 rounded"></div>
<span className="text-gray-600">Formation</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded" style={{ backgroundColor: '#99e4d8' }}></div>
<span className="text-gray-600">Travail en distanciel</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded" style={{ backgroundColor: '#d946ef' }}></div>
<span className="text-gray-600">JPO/SF</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-gray-400 rounded"></div>
<span className="text-gray-600">Samedi/Dimanche</span>
@@ -1482,10 +1398,6 @@ const Calendar = () => {
<div className="w-3 h-3 bg-gray-700 rounded"></div>
<span className="text-gray-600">Jours fériés</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded" style={{ backgroundColor: '#d946ef' }}></div>
<span className="text-gray-600">JPO/SF</span>
</div>
</div>
</div>
)}
@@ -1594,11 +1506,7 @@ const Calendar = () => {
<div className="flex flex-wrap gap-3 text-xs">
<div className="flex items-center gap-1">
<div className="w-2 h-2 bg-green-400 rounded-full"></div>
<span>Validé</span>
</div>
<div className="flex items-center gap-1">
<div className="w-2 h-2 bg-orange-400 rounded-full"></div>
<span>En attente</span>
<span>Absence</span>
</div>
<div className="flex items-center gap-1">
<div className="w-2 h-2 bg-blue-400 rounded-full"></div>
@@ -1608,6 +1516,10 @@ const Calendar = () => {
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: '#d946ef' }}></div>
<span>JPO/SF</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded" style={{ backgroundColor: '#99e4d8' }}></div>
<span className="text-gray-600">Travail en distenciel</span>
</div>
</div>
</div>
</div>
@@ -1666,7 +1578,7 @@ const Calendar = () => {
</div>
)}
{/* Tooltip (désactivé, ne montre plus le type) */}
{/* Tooltip */}
{hoveredLeave && !isMobile && (
<div
className="fixed bg-white rounded-lg shadow-xl border p-4 z-50 min-w-[250px]"
@@ -1743,6 +1655,22 @@ const Calendar = () => {
.animate-slideInRight {
animation: slideInRight 0.3s ease-out;
}
/* Amélioration du scroll */
.overflow-x-auto::-webkit-scrollbar {
height: 8px;
}
.overflow-x-auto::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
.overflow-x-auto::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
.overflow-x-auto::-webkit-scrollbar-thumb:hover {
background: #555;
}
`}</style>
</div>
);
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+327 -11
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import Sidebar from '../components/Sidebar';
import {
@@ -11,7 +11,11 @@ import {
Briefcase,
Building,
TrendingDown,
TrendingUp
TrendingUp,
ChevronLeft,
ChevronRight,
FileText,
AlertCircle
} from 'lucide-react';
const EmployeeDetails = () => {
@@ -22,10 +26,22 @@ const EmployeeDetails = () => {
const [detailedCounters, setDetailedCounters] = useState(null);
const [isLoading, setIsLoading] = useState(true);
// États pour le CRA
const [craData, setCraData] = useState(null);
const [craMonth, setCraMonth] = useState(new Date().getMonth() + 1);
const [craYear, setCraYear] = useState(new Date().getFullYear());
const [isLoadingCra, setIsLoadingCra] = useState(false);
useEffect(() => {
fetchEmployeeData();
}, [id]);
useEffect(() => {
if (employee && detailedCounters?.user?.typeContrat === 'forfait_jour') {
fetchCraData();
}
}, [id, craMonth, craYear, employee, detailedCounters]);
const fetchEmployeeData = async () => {
try {
setIsLoading(true);
@@ -61,6 +77,117 @@ const EmployeeDetails = () => {
}
};
const fetchCraData = async () => {
try {
setIsLoadingCra(true);
const res = await fetch(`/api/getCra?user_id=${id}&month=${craMonth}&year=${craYear}`);
const data = await res.json();
if (data.success) {
setCraData(data.cra);
} else {
setCraData(null);
}
} catch (err) {
console.error("Erreur récupération CRA:", err);
setCraData(null);
} finally {
setIsLoadingCra(false);
}
};
const navigateCraMonth = (direction) => {
if (direction === 'prev') {
if (craMonth === 1) {
setCraMonth(12);
setCraYear(craYear - 1);
} else {
setCraMonth(craMonth - 1);
}
} else {
if (craMonth === 12) {
setCraMonth(1);
setCraYear(craYear + 1);
} else {
setCraMonth(craMonth + 1);
}
}
};
const getMonthName = (month) => {
const months = ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin',
'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'];
return months[month - 1];
};
const getDaysInMonth = (month, year) => {
return new Date(year, month, 0).getDate();
};
const getFirstDayOfMonth = (month, year) => {
const day = new Date(year, month - 1, 1).getDay();
return day === 0 ? 6 : day - 1;
};
const isWeekend = (day, month, year) => {
const date = new Date(year, month - 1, day);
const dayOfWeek = date.getDay();
return dayOfWeek === 0 || dayOfWeek === 6;
};
const getCraDayStatus = (day) => {
if (!craData || !craData.days) return null;
return craData.days.find(d => d.day === day);
};
const getCraDayClass = (dayStatus, isWeekendDay) => {
if (isWeekendDay) return 'bg-gray-100 text-gray-400';
if (!dayStatus) return 'bg-white text-gray-700';
switch (dayStatus.type) {
case 'worked':
return 'bg-emerald-100 text-emerald-700 border border-emerald-300';
case 'leave':
return 'bg-blue-100 text-blue-700 border border-blue-300';
case 'rtt':
return 'bg-violet-100 text-violet-700 border border-violet-300';
case 'sick':
return 'bg-red-100 text-red-700 border border-red-300';
case 'holiday':
return 'bg-amber-100 text-amber-700 border border-amber-300';
case 'not_worked':
return 'bg-orange-100 text-orange-700 border border-orange-300';
case 'missing':
return 'bg-red-200 text-red-800 border-2 border-red-400';
case 'pending':
return 'bg-gray-50 text-gray-400 border border-dashed border-gray-300';
default:
return 'bg-white text-gray-700 border border-gray-200';
}
};
// NOUVELLE FONCTION : Formatage des dates côté frontend
const formatDateRange = (start, end) => {
if (!start) return 'Date non définie';
const formatDate = (dateStr) => {
try {
const date = new Date(dateStr);
if (isNaN(date.getTime())) return 'Date invalide';
return date.toLocaleDateString('fr-FR', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
});
} catch {
return 'Date invalide';
}
};
if (start === end) return formatDate(start);
return `${formatDate(start)} - ${formatDate(end)}`;
};
const getStatusConfig = (status) => {
switch (status) {
case 'Validée':
@@ -135,6 +262,54 @@ const EmployeeDetails = () => {
);
};
const renderCraCalendar = () => {
const daysInMonth = getDaysInMonth(craMonth, craYear);
const firstDay = getFirstDayOfMonth(craMonth, craYear);
const dayNames = ['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim'];
const cells = [];
for (let i = 0; i < firstDay; i++) {
cells.push(<div key={`empty-${i}`} className="h-10"></div>);
}
for (let day = 1; day <= daysInMonth; day++) {
const isWeekendDay = isWeekend(day, craMonth, craYear);
const dayStatus = getCraDayStatus(day);
const dayClass = getCraDayClass(dayStatus, isWeekendDay);
cells.push(
<div
key={day}
className={`h-10 flex items-center justify-center rounded-lg text-sm font-medium ${dayClass} transition-colors relative cursor-default`}
title={dayStatus?.label || ''}
>
{day}
{dayStatus?.type === 'missing' && (
<span className="absolute -top-1 -right-1 w-2.5 h-2.5 bg-red-500 rounded-full animate-pulse"></span>
)}
</div>
);
}
return (
<div className="grid grid-cols-7 gap-1">
{dayNames.map(name => (
<div key={name} className="h-8 flex items-center justify-center text-xs font-semibold text-gray-500">
{name}
</div>
))}
{cells}
</div>
);
};
const getTauxSaisieColor = (taux) => {
if (taux >= 80) return 'text-emerald-600';
if (taux >= 50) return 'text-amber-600';
return 'text-red-600';
};
if (isLoading) return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
@@ -175,14 +350,12 @@ const EmployeeDetails = () => {
{/* Profil employé */}
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 mb-6">
<div className="flex flex-col sm:flex-row sm:items-center gap-4">
{/* Avatar */}
<div className="w-16 h-16 bg-gradient-to-br from-cyan-400 to-blue-500 rounded-2xl flex items-center justify-center flex-shrink-0">
<span className="text-2xl font-bold text-white">
{employee.Prenom?.charAt(0)}{employee.Nom?.charAt(0)}
</span>
</div>
{/* Infos */}
<div className="flex-1">
<h1 className="text-xl font-bold text-gray-900 mb-1">
{employee.Prenom} {employee.Nom}
@@ -210,7 +383,6 @@ const EmployeeDetails = () => {
</div>
</div>
{/* Badge contrat */}
{detailedCounters?.user?.typeContrat && (
<div className="px-3 py-1.5 bg-gray-100 rounded-lg text-sm font-medium text-gray-700">
{getTypeContratLabel(detailedCounters.user.typeContrat)}
@@ -268,7 +440,143 @@ const EmployeeDetails = () => {
</div>
)}
{/* Historique */}
{/* Section CRA - Uniquement pour forfait jour */}
{detailedCounters?.user?.typeContrat === 'forfait_jour' && (
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden mb-6">
<div className="px-6 py-4 border-b border-gray-100">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 bg-cyan-50 rounded-lg">
<FileText className="w-5 h-5 text-cyan-600" />
</div>
<div>
<h2 className="text-lg font-semibold text-gray-900">Compte Rendu d'Activité</h2>
<p className="text-sm text-gray-500">Suivi mensuel des activités</p>
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => navigateCraMonth('prev')}
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
>
<ChevronLeft className="w-5 h-5 text-gray-600" />
</button>
<span className="text-sm font-medium text-gray-900 min-w-[140px] text-center">
{getMonthName(craMonth)} {craYear}
</span>
<button
onClick={() => navigateCraMonth('next')}
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
>
<ChevronRight className="w-5 h-5 text-gray-600" />
</button>
</div>
</div>
</div>
<div className="p-6">
{isLoadingCra ? (
<div className="flex items-center justify-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-cyan-600"></div>
</div>
) : (
<>
{/* Alerte si jours non saisis */}
{craData?.summary?.missing > 0 && (
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-xl flex items-center gap-3">
<AlertCircle className="w-5 h-5 text-red-500 flex-shrink-0" />
<p className="text-sm text-red-700">
<span className="font-semibold">{craData.summary.missing} jour{craData.summary.missing > 1 ? 's' : ''}</span> non saisi{craData.summary.missing > 1 ? 's' : ''} ce mois-ci
</p>
</div>
)}
{/* Calendrier */}
{renderCraCalendar()}
{/* Légende */}
<div className="mt-6 pt-4 border-t border-gray-100">
<p className="text-xs font-medium text-gray-500 mb-3">Légende</p>
<div className="flex flex-wrap gap-3">
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded bg-emerald-100 border border-emerald-300"></div>
<span className="text-xs text-gray-600">Travaillé</span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded bg-blue-100 border border-blue-300"></div>
<span className="text-xs text-gray-600">Congés</span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded bg-violet-100 border border-violet-300"></div>
<span className="text-xs text-gray-600">RTT</span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded bg-red-100 border border-red-300"></div>
<span className="text-xs text-gray-600">Maladie</span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded bg-amber-100 border border-amber-300"></div>
<span className="text-xs text-gray-600">Férié</span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded bg-orange-100 border border-orange-300"></div>
<span className="text-xs text-gray-600">Non travaillé</span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded bg-red-200 border-2 border-red-400 relative">
<span className="absolute -top-0.5 -right-0.5 w-1.5 h-1.5 bg-red-500 rounded-full"></span>
</div>
<span className="text-xs text-gray-600">Non saisi</span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded bg-gray-50 border border-dashed border-gray-300"></div>
<span className="text-xs text-gray-600">À saisir</span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded bg-gray-100"></div>
<span className="text-xs text-gray-600">Weekend</span>
</div>
</div>
</div>
{/* Résumé du mois */}
{craData?.summary && (
<div className="mt-4 pt-4 border-t border-gray-100">
<p className="text-xs font-medium text-gray-500 mb-3">Résumé du mois</p>
<div className="grid grid-cols-2 sm:grid-cols-5 gap-4">
<div className="text-center p-3 bg-gray-50 rounded-xl">
<p className="text-2xl font-bold text-emerald-600">{craData.summary.worked || 0}</p>
<p className="text-xs text-gray-500">Jours travaillés</p>
</div>
<div className="text-center p-3 bg-gray-50 rounded-xl">
<p className="text-2xl font-bold text-blue-600">{craData.summary.leave || 0}</p>
<p className="text-xs text-gray-500">Congés</p>
</div>
<div className="text-center p-3 bg-gray-50 rounded-xl">
<p className="text-2xl font-bold text-violet-600">{craData.summary.rtt || 0}</p>
<p className="text-xs text-gray-500">RTT</p>
</div>
<div className="text-center p-3 bg-gray-50 rounded-xl">
<p className="text-2xl font-bold text-red-600">{craData.summary.missing || 0}</p>
<p className="text-xs text-gray-500">Non saisis</p>
</div>
<div className="text-center p-3 bg-gray-50 rounded-xl">
<p className={`text-2xl font-bold ${getTauxSaisieColor(craData.summary.tauxSaisie || 0)}`}>
{craData.summary.tauxSaisie || 0}%
</p>
<p className="text-xs text-gray-500">Taux de saisie</p>
</div>
</div>
</div>
)}
</>
)}
</div>
</div>
)}
{/* Historique ✅ CORRIGÉ */}
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
<div className="px-6 py-4 border-b border-gray-100">
<h2 className="text-lg font-semibold text-gray-900">Historique des demandes</h2>
@@ -283,23 +591,31 @@ const EmployeeDetails = () => {
</div>
) : (
requests.map((r) => {
const statusConfig = getStatusConfig(r.status);
const statusConfig = getStatusConfig(r.status || r.Statut);
return (
<div key={r.Id} className="px-6 py-4 hover:bg-gray-50 transition-colors">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className={`w-2 h-2 rounded-full ${statusConfig.dot}`}></div>
<div>
<p className="font-medium text-gray-900">{r.type}</p>
<p className="text-sm text-gray-500">{r.date_display}</p>
<p className="font-medium text-gray-900">
{r.type || r.typeConges || 'Congés'}
</p>
{/* ✅ UTILISE formatDateRange avec DateDebut/DateFin */}
<p className="text-sm text-gray-500">
{formatDateRange(r.DateDebut, r.DateFin)}
</p>
</div>
</div>
<div className="flex items-center gap-3">
<span className="text-sm font-semibold text-gray-700">{r.days}j</span>
<span className="text-sm font-semibold text-gray-700">
{r.days || r.NombreJours || 0}j
</span>
<span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium ${statusConfig.bg} ${statusConfig.text}`}>
{statusConfig.icon}
{r.status}
{r.status || r.Statut}
</span>
</div>
</div>
+5
View File
@@ -143,6 +143,11 @@ const Login = () => {
</div>
</div>
)}
{/* Version */}
<p className="text-xs text-center text-gray-400 mt-6 pt-4 border-t border-gray-100">
Version 2.0.0
</p>
</div>
</div>
</div>
+14 -7
View File
@@ -37,6 +37,13 @@ const Manager = () => {
}, [user]);
const fetchTeamData = async () => {
// Vérification de user.id
if (!user?.id) {
console.error('❌ user.id est manquant');
setIsLoading(false);
return;
}
try {
setIsLoading(true);
await Promise.all([
@@ -51,8 +58,7 @@ const Manager = () => {
}
};
// SIMPLIFIÉ - Le backend gère tout le filtrage
// SIMPLIFIÉ - Le backend gère tout le filtrage
// CORRIGÉ : manager_id au lieu de managerid
const fetchTeamMembers = async () => {
try {
const res = await fetch(`/api/getTeamMembers?manager_id=${user.id}`);
@@ -78,9 +84,10 @@ const Manager = () => {
}
};
// SIMPLIFIÉ - Le backend gère tout le filtrage
// CORRIGÉ : validator_id au lieu de validatorid
const fetchPendingRequests = async () => {
try {
// CHANGER validator_id manager_id
const res = await fetch(`/api/getPendingRequests?manager_id=${user.id}`);
const data = await res.json();
@@ -102,10 +109,11 @@ const Manager = () => {
}
};
// SIMPLIFIÉ - Le backend gère tout le filtrage
// CORRIGÉ : manager_id au lieu de SuperieurId (pour cohérence)
const fetchAllTeamRequests = async () => {
try {
const res = await fetch(`/api/getAllTeamRequests?SuperieurId=${user.id}`);
const res = await fetch(`/api/getAllTeamRequests?manager_id=${user.id}`);
const data = await res.json();
console.log('📊 getAllTeamRequests:', {
@@ -242,7 +250,6 @@ const Manager = () => {
);
}
return (
<div className="relative min-h-screen bg-gray-50 flex overflow-hidden">
{/* Toast Notification */}
@@ -422,7 +429,7 @@ const Manager = () => {
<div key={r.id} className="border p-4 rounded-lg bg-gray-50 hover:bg-gray-100 transition">
<div className="flex justify-between mb-2">
<div>
<p className="font-medium text-gray-900">{r.employee_name}</p>
<p className="font-medium text-gray-900">{r.employee_name}</p> {/* ✅ */}
<p className="text-sm text-gray-600">{r.date_display}</p>
</div>
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getTypeColor(r.type)}`}>
+1 -1
View File
@@ -243,7 +243,7 @@ const Requests = () => {
if (dateDebut <= aujourdhui) {
showToast(
`❌ Impossible d'annuler : la date de début (${dateDebut.toLocaleDateString('fr-FR')}) est déjà passée ou c'est aujourd'hui`,
`❌ Impossible d'annuler : la date de début (${dateDebut.toLocaleDateString('fr-FR')}) est déjà passée`,
'error'
);
setIsLoading(false);
+2 -2
View File
@@ -9,12 +9,12 @@ export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://192.168.0.3:3004',
target: 'http://192.168.0.3:3000',
changeOrigin: true,
secure: false
},
'/uploads': {
target: 'http://192.168.0.3:3004',
target: 'http://192.168.0.3:3000',
changeOrigin: true,
secure: false
}