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: { server: {
host: '0.0.0.0', host: '0.0.0.0',
port: 90, port: 80,
strictPort: true, strictPort: true,
allowedHosts: ['mygta-dev.ensup-adm.net', 'localhost'], allowedHosts: ['mygta.ensup-adm.net', 'localhost'],
proxy: { proxy: {
'/api': { '/api': {
target: 'http://backend:3004', target: 'http://backend:3000',
changeOrigin: true, changeOrigin: true,
secure: false, secure: false,
configure: (proxy, options) => { configure: (proxy, options) => {
@@ -39,7 +39,7 @@ export default defineConfig({
console.log('Proxy error:', err); console.log('Proxy error:', err);
}); });
proxy.on('proxyReq', (proxyReq, req, res) => { 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 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 RUN mkdir -p /app/uploads/medical
# Expose the port # Expose the port
EXPOSE 3004 EXPOSE 3000
# Start the server # Start the server
CMD ["node", "server.js"] 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
File diff suppressed because it is too large Load Diff
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 { useParams, useNavigate } from 'react-router-dom';
import Sidebar from '../components/Sidebar'; import Sidebar from '../components/Sidebar';
import { import {
@@ -11,7 +11,11 @@ import {
Briefcase, Briefcase,
Building, Building,
TrendingDown, TrendingDown,
TrendingUp TrendingUp,
ChevronLeft,
ChevronRight,
FileText,
AlertCircle
} from 'lucide-react'; } from 'lucide-react';
const EmployeeDetails = () => { const EmployeeDetails = () => {
@@ -22,10 +26,22 @@ const EmployeeDetails = () => {
const [detailedCounters, setDetailedCounters] = useState(null); const [detailedCounters, setDetailedCounters] = useState(null);
const [isLoading, setIsLoading] = useState(true); 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(() => { useEffect(() => {
fetchEmployeeData(); fetchEmployeeData();
}, [id]); }, [id]);
useEffect(() => {
if (employee && detailedCounters?.user?.typeContrat === 'forfait_jour') {
fetchCraData();
}
}, [id, craMonth, craYear, employee, detailedCounters]);
const fetchEmployeeData = async () => { const fetchEmployeeData = async () => {
try { try {
setIsLoading(true); 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) => { const getStatusConfig = (status) => {
switch (status) { switch (status) {
case 'Validée': 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 ( if (isLoading) return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center"> <div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center"> <div className="text-center">
@@ -175,14 +350,12 @@ const EmployeeDetails = () => {
{/* Profil employé */} {/* Profil employé */}
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 mb-6"> <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"> <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"> <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"> <span className="text-2xl font-bold text-white">
{employee.Prenom?.charAt(0)}{employee.Nom?.charAt(0)} {employee.Prenom?.charAt(0)}{employee.Nom?.charAt(0)}
</span> </span>
</div> </div>
{/* Infos */}
<div className="flex-1"> <div className="flex-1">
<h1 className="text-xl font-bold text-gray-900 mb-1"> <h1 className="text-xl font-bold text-gray-900 mb-1">
{employee.Prenom} {employee.Nom} {employee.Prenom} {employee.Nom}
@@ -210,7 +383,6 @@ const EmployeeDetails = () => {
</div> </div>
</div> </div>
{/* Badge contrat */}
{detailedCounters?.user?.typeContrat && ( {detailedCounters?.user?.typeContrat && (
<div className="px-3 py-1.5 bg-gray-100 rounded-lg text-sm font-medium text-gray-700"> <div className="px-3 py-1.5 bg-gray-100 rounded-lg text-sm font-medium text-gray-700">
{getTypeContratLabel(detailedCounters.user.typeContrat)} {getTypeContratLabel(detailedCounters.user.typeContrat)}
@@ -268,7 +440,143 @@ const EmployeeDetails = () => {
</div> </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="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
<div className="px-6 py-4 border-b border-gray-100"> <div className="px-6 py-4 border-b border-gray-100">
<h2 className="text-lg font-semibold text-gray-900">Historique des demandes</h2> <h2 className="text-lg font-semibold text-gray-900">Historique des demandes</h2>
@@ -283,23 +591,31 @@ const EmployeeDetails = () => {
</div> </div>
) : ( ) : (
requests.map((r) => { requests.map((r) => {
const statusConfig = getStatusConfig(r.status); const statusConfig = getStatusConfig(r.status || r.Statut);
return ( return (
<div key={r.Id} className="px-6 py-4 hover:bg-gray-50 transition-colors"> <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 justify-between">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className={`w-2 h-2 rounded-full ${statusConfig.dot}`}></div> <div className={`w-2 h-2 rounded-full ${statusConfig.dot}`}></div>
<div> <div>
<p className="font-medium text-gray-900">{r.type}</p> <p className="font-medium text-gray-900">
<p className="text-sm text-gray-500">{r.date_display}</p> {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> </div>
<div className="flex items-center gap-3"> <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}`}> <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} {statusConfig.icon}
{r.status} {r.status || r.Statut}
</span> </span>
</div> </div>
</div> </div>
+5
View File
@@ -143,6 +143,11 @@ const Login = () => {
</div> </div>
</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> </div>
</div> </div>
+14 -7
View File
@@ -37,6 +37,13 @@ const Manager = () => {
}, [user]); }, [user]);
const fetchTeamData = async () => { const fetchTeamData = async () => {
// Vérification de user.id
if (!user?.id) {
console.error('❌ user.id est manquant');
setIsLoading(false);
return;
}
try { try {
setIsLoading(true); setIsLoading(true);
await Promise.all([ await Promise.all([
@@ -51,8 +58,7 @@ const Manager = () => {
} }
}; };
// SIMPLIFIÉ - Le backend gère tout le filtrage // CORRIGÉ : manager_id au lieu de managerid
// SIMPLIFIÉ - Le backend gère tout le filtrage
const fetchTeamMembers = async () => { const fetchTeamMembers = async () => {
try { try {
const res = await fetch(`/api/getTeamMembers?manager_id=${user.id}`); 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 () => { const fetchPendingRequests = async () => {
try { try {
// CHANGER validator_id manager_id
const res = await fetch(`/api/getPendingRequests?manager_id=${user.id}`); const res = await fetch(`/api/getPendingRequests?manager_id=${user.id}`);
const data = await res.json(); 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 () => { const fetchAllTeamRequests = async () => {
try { 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(); const data = await res.json();
console.log('📊 getAllTeamRequests:', { console.log('📊 getAllTeamRequests:', {
@@ -242,7 +250,6 @@ const Manager = () => {
); );
} }
return ( return (
<div className="relative min-h-screen bg-gray-50 flex overflow-hidden"> <div className="relative min-h-screen bg-gray-50 flex overflow-hidden">
{/* Toast Notification */} {/* 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 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 className="flex justify-between mb-2">
<div> <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> <p className="text-sm text-gray-600">{r.date_display}</p>
</div> </div>
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getTypeColor(r.type)}`}> <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) { if (dateDebut <= aujourdhui) {
showToast( 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' 'error'
); );
setIsLoading(false); setIsLoading(false);
+2 -2
View File
@@ -9,12 +9,12 @@ export default defineConfig({
server: { server: {
proxy: { proxy: {
'/api': { '/api': {
target: 'http://192.168.0.3:3004', target: 'http://192.168.0.3:3000',
changeOrigin: true, changeOrigin: true,
secure: false secure: false
}, },
'/uploads': { '/uploads': {
target: 'http://192.168.0.3:3004', target: 'http://192.168.0.3:3000',
changeOrigin: true, changeOrigin: true,
secure: false secure: false
} }