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