Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dad1a4af30 |
@@ -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"]
|
||||
|
||||
+5978
-1929
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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)}`}>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user