Files
GTARH/src/pages/CompteRenduRH.tsx
T
oimer 314a98c1ad Ajouter completion des profils nouveaux arrivants et corrections diverses
- Détection des profils incomplets (société/campus/service/date d'entrée/validateur) avec alerte Dashboard et section dédiée dans Équipe
- Formulaire d'édition des collaborateurs (date d'entrée, rôle, validateur N+1, société, campus, service, type de contrat) avec combobox de recherche pour le validateur
- Correction du mapping poste (colonne description au lieu de fonction) et du filtre des validateurs (variantes de rôle: Validateur/Validatrice, Directeur/Directrice, President)
- Fix du proxy Vite vers le backend Docker

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 15:37:03 +02:00

748 lines
44 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect } from 'react';
import {
Calendar, Lock, Unlock, Download,
ChevronLeft, ChevronRight, RefreshCw,
Search, Users, CheckCircle, XCircle, Clock, ArrowLeft,
TrendingUp, Building2, MapPin, Briefcase, FileText, Edit3
} from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
// ─── Composant ligne éditable ───────────────────────────────────────────────
const JourEditRow = ({ jour, collaborateurId, token, onSaved }: any) => {
const [travaille, setTravaille] = useState<boolean>(!!jour.JourTravaille);
const [reposQ, setReposQ] = useState<boolean>(!!jour.ReposQuotidienRespect);
const [reposH, setReposH] = useState<boolean>(!!jour.ReposHebdomadaireRespect);
const [commentaire, setCommentaire] = useState<string>(jour.CommentaireRepos || '');
const [saving, setSaving] = useState(false);
const [saved, setSaved] = useState(false);
const [dirty, setDirty] = useState(false);
const dateObj = new Date(jour.JourDate);
const isWeekend = dateObj.getDay() === 0 || dateObj.getDay() === 6;
const dateStr = dateObj.toLocaleDateString('fr-FR', {
weekday: 'short', day: '2-digit', month: '2-digit'
});
const nonSaisi = !jour.DateSaisie; // jour jamais rempli par le collaborateur
const mark = (fn: () => void) => { fn(); setDirty(true); setSaved(false); };
const handleSave = async () => {
setSaving(true);
try {
const res = await fetch('/api/compte-rendu/jour', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
// ✅ CORRECTION : noms de champs alignés avec le backend
body: JSON.stringify({
collaborateurid: collaborateurId,
jourdate: jour.JourDate,
jourtravaille: travaille,
reposquotidien: reposQ,
reposhebdo: reposH,
commentaire
})
});
const data = await res.json();
if (data.success) {
setSaved(true);
setDirty(false);
setTimeout(() => setSaved(false), 2000);
onSaved();
toast.success('Jour mis à jour');
} else {
toast.error(data.error || 'Erreur serveur');
}
} catch {
toast.error('Erreur serveur');
} finally {
setSaving(false);
}
};
return (
<tr className={`
border-b transition-colors text-sm
${isWeekend ? 'opacity-50' : ''}
${dirty ? 'bg-yellow-50 dark:bg-yellow-950' : ''}
${nonSaisi && !dirty ? 'bg-gray-50 dark:bg-gray-900' : ''}
`}>
{/* Date */}
<td className="py-2 px-3 font-medium whitespace-nowrap text-foreground">
{dateStr}
{isWeekend && <span className="ml-1 text-xs text-muted-foreground">(WE)</span>}
{/* ✅ Badge "Non saisi" pour les jours vides */}
{nonSaisi && (
<span className="ml-2 text-xs bg-orange-100 text-orange-600 px-1.5 py-0.5 rounded">
Non saisi
</span>
)}
</td>
{/* Jour travaillé */}
<td className="py-2 px-3 text-center">
<input
type="checkbox"
checked={travaille}
onChange={e => mark(() => {
const v = e.target.checked;
setTravaille(v);
if (!v) { setReposQ(false); setReposH(false); }
})}
className="w-4 h-4 accent-blue-600 cursor-pointer"
/>
</td>
{/* Repos quotidien */}
<td className="py-2 px-3 text-center">
<input
type="checkbox"
checked={reposQ}
onChange={e => mark(() => setReposQ(e.target.checked))}
disabled={!travaille}
className="w-4 h-4 accent-green-600 cursor-pointer disabled:opacity-30 disabled:cursor-not-allowed"
/>
</td>
{/* Repos hebdo */}
<td className="py-2 px-3 text-center">
<input
type="checkbox"
checked={reposH}
onChange={e => mark(() => setReposH(e.target.checked))}
disabled={!travaille}
className="w-4 h-4 accent-green-600 cursor-pointer disabled:opacity-30 disabled:cursor-not-allowed"
/>
</td>
{/* Commentaire */}
<td className="py-2 px-3">
<input
type="text"
value={commentaire}
onChange={e => mark(() => setCommentaire(e.target.value))}
placeholder="Commentaire..."
className="w-full text-xs border border-border rounded px-2 py-1 bg-transparent focus:outline-none focus:ring-1 focus:ring-blue-400"
/>
</td>
{/* Bouton sauvegarde */}
<td className="py-2 px-3 text-center">
{saved ? (
<CheckCircle className="w-5 h-5 text-green-500 mx-auto" />
) : (
<button
onClick={handleSave}
disabled={saving || !dirty}
className="px-3 py-1 rounded text-xs font-medium bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
>
{saving ? '…' : 'Sauver'}
</button>
)}
</td>
</tr>
);
};
// ─── Génère tous les jours d'un mois (y compris non saisis) ─────────────────
const genererJoursDuMois = (annee: number, mois: number, joursExistants: any[]) => {
const jours: any[] = [];
const nbJours = new Date(annee, mois, 0).getDate();
for (let d = 1; d <= nbJours; d++) {
const dateStr = `${annee}-${String(mois).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
const existing = joursExistants.find(j => j.JourDate?.substring(0, 10) === dateStr);
if (existing) {
jours.push(existing);
} else {
// Jour non saisi → ligne vide modifiable par les RH
jours.push({
JourDate: dateStr,
JourTravaille: false,
ReposQuotidienRespect: false,
ReposHebdomadaireRespect: false,
CommentaireRepos: '',
DateSaisie: null, // null = non saisi
DateModification: null
});
}
}
return jours;
};
// ─── Composant principal ────────────────────────────────────────────────────
const CompteRenduRH = () => {
const navigate = useNavigate();
const [user, setUser] = useState<any>(null);
const [token, setToken] = useState<string>('');
const [currentDate, setCurrentDate] = useState(new Date());
const [isLoading, setIsLoading] = useState(true);
const [collaborateurs, setCollaborateurs] = useState<any[]>([]);
const [allCollaborateurs, setAllCollaborateurs] = useState<any[]>([]);
const [selectedCollab, setSelectedCollab] = useState<any>(null);
const [compteRenduData, setCompteRenduData] = useState<any[]>([]);
const [mensuelData, setMensuelData] = useState<any>(null);
const [statsGlobales, setStatsGlobales] = useState<any>(null);
const [searchTerm, setSearchTerm] = useState('');
const [showDetailModal, setShowDetailModal] = useState(false);
const [selectedJour, setSelectedJour] = useState<any>(null);
const [cumulAnnuel, setCumulAnnuel] = useState<any>(null);
// Filtres
const [filterSociete, setFilterSociete] = useState<string>('all');
const [filterCampus, setFilterCampus] = useState<string>('all');
const [filterService, setFilterService] = useState<string>('all');
const annee = currentDate.getFullYear();
const mois = currentDate.getMonth() + 1;
// ── Auth
useEffect(() => {
const userData = localStorage.getItem('user');
const userToken = localStorage.getItem('token');
if (!userData || !userToken) { toast.error('Session expirée'); navigate('/'); return; }
const parsedUser = JSON.parse(userData);
if (!['RH', 'Admin'].includes(parsedUser?.role)) {
toast.error('Accès réservé aux RH et Administrateurs');
navigate('/dashboard');
return;
}
setUser(parsedUser);
setToken(userToken);
}, [navigate]);
// ── Chargement collaborateurs
useEffect(() => {
if (!token) return;
const load = async () => {
setIsLoading(true);
try {
const res = await fetch('/api/collaborateurs/details', {
headers: { 'Authorization': `Bearer ${token}` }
});
if (!res.ok) throw new Error(`Erreur ${res.status}`);
const data = await res.json();
const forfaitJour = data.filter((emp: any) => {
const contrat = (emp.typeContrat || emp.TypeContrat || '').toLowerCase();
const isActif = emp.Actif === 1 || emp.Actif === true || emp.Actif === null;
return ['forfait_jour', 'forfaitjour', 'forfait jour', 'forfait-jour'].includes(contrat) && isActif;
}).map((e: any) => ({ ...e, nomComplet: `${e.prenom} ${e.nom}`.trim() }))
.sort((a: any, b: any) => a.nomComplet.localeCompare(b.nomComplet));
setAllCollaborateurs(forfaitJour);
setCollaborateurs(forfaitJour);
} catch (err: any) {
toast.error('Erreur chargement collaborateurs: ' + err.message);
} finally {
setIsLoading(false);
}
};
load();
}, [token]);
// ── Listes uniques pour filtres
const societesUniques = [...new Set(allCollaborateurs.map(e => e.societe).filter(Boolean))].sort();
const campusUniques = [...new Set(allCollaborateurs.map(e => e.campus).filter(Boolean))].sort();
const servicesUniques = [...new Set(allCollaborateurs.map(e => e.service).filter(Boolean))].sort();
// ── Filtrage
useEffect(() => {
let f = [...allCollaborateurs];
if (searchTerm) f = f.filter(e =>
e.nomComplet?.toLowerCase().includes(searchTerm.toLowerCase()) ||
e.email?.toLowerCase().includes(searchTerm.toLowerCase()) ||
e.service?.toLowerCase().includes(searchTerm.toLowerCase())
);
if (filterSociete !== 'all') f = f.filter(e => e.societe === filterSociete);
if (filterCampus !== 'all') f = f.filter(e => e.campus === filterCampus);
if (filterService !== 'all') f = f.filter(e => e.service === filterService);
setCollaborateurs(f);
if (selectedCollab && !f.find(c => c.id === selectedCollab.id)) setSelectedCollab(null);
}, [allCollaborateurs, searchTerm, filterSociete, filterCampus, filterService]);
// ── Stats globales
useEffect(() => {
if (!token || collaborateurs.length === 0) return;
fetch(`/api/compte-rendu/stats?annee=${annee}&mois=${mois}`, {
headers: { 'Authorization': `Bearer ${token}` }
}).then(r => r.ok ? r.json() : null)
.then(data => setStatsGlobales(data || {
totalCollaborateurs: collaborateurs.length,
mensuelsValides: 0,
mensuelsEnAttente: collaborateurs.length
}));
}, [token, collaborateurs.length, annee, mois]);
// ── Chargement compte-rendu
const loadCompteRendu = async () => {
if (!selectedCollab || !token) return;
setIsLoading(true);
try {
// ✅ CORRECTION : paramètre "userid" (sans underscore)
const res = await fetch(
`/api/compte-rendu-activites?userid=${selectedCollab.id}&annee=${annee}&mois=${mois}`,
{ headers: { 'Authorization': `Bearer ${token}` } }
);
if (!res.ok) throw new Error(`Erreur ${res.status}`);
const data = await res.json();
if (data.success) {
// ✅ NOUVEAU : on génère TOUS les jours du mois, y compris non saisis
const tousLesJours = genererJoursDuMois(annee, mois, data.jours || []);
setCompteRenduData(tousLesJours);
setMensuelData(data.mensuel);
}
} catch (err: any) {
console.error(err);
setCompteRenduData(genererJoursDuMois(annee, mois, []));
setMensuelData(null);
} finally {
setIsLoading(false);
}
};
useEffect(() => { loadCompteRendu(); }, [selectedCollab, annee, mois, token]);
// ── Cumul annuel
useEffect(() => {
if (!selectedCollab || !token) return;
fetch(`/api/compte-rendu/cumul-annuel?userid=${selectedCollab.id}&annee=${annee}`, {
headers: { 'Authorization': `Bearer ${token}` }
}).then(r => r.ok ? r.json() : null)
.then(data => setCumulAnnuel(data?.success ? data.cumul : null));
}, [selectedCollab, annee, token]);
// ── Verrouiller / Déverrouiller
const handleToggleLock = async (unlock = false) => {
if (!selectedCollab || !token) return;
const monthNames = ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'];
if (!confirm(`Confirmer ${unlock ? 'déverrouiller' : 'verrouiller'} ${monthNames[mois - 1]} ${annee} pour ${selectedCollab.nomComplet} ?`)) return;
try {
const res = await fetch(unlock ? '/api/compte-rendu/deverrouiller' : '/api/compte-rendu/verrouiller', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
// ✅ CORRECTION : collaborateur_id → collaborateurid
body: JSON.stringify({ collaborateurid: selectedCollab.id, annee, mois })
});
const data = await res.json();
if (data.success) {
toast.success(`Mois ${unlock ? 'déverrouillé' : 'verrouillé'} avec succès`);
await loadCompteRendu();
} else {
toast.error(data.message || 'Erreur');
}
} catch { toast.error('Erreur serveur'); }
};
// ── Export PDF
const handleExportPDF = async () => {
if (!selectedCollab || !token) { toast.error('Aucun collaborateur sélectionné'); return; }
const loadingToast = toast.loading('Génération du PDF...');
try {
const res = await fetch(
`/api/export-compte-rendu-pdf?user_id=${selectedCollab.id}&annee=${annee}&mois=${mois}`,
{ headers: { 'Authorization': `Bearer ${token}` } }
);
if (!res.ok) throw new Error(`Erreur ${res.status}`);
const blob = await res.blob();
if (blob.size === 0) throw new Error('PDF vide');
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = `compte-rendu-${selectedCollab.nom}-${annee}-${String(mois).padStart(2, '0')}.pdf`;
document.body.appendChild(a); a.click(); document.body.removeChild(a);
window.URL.revokeObjectURL(url);
toast.dismiss(loadingToast);
toast.success('PDF téléchargé');
} catch (err: any) {
toast.dismiss(loadingToast);
toast.error(err.message || 'Erreur export PDF');
}
};
const navigateMonth = (dir: string) => setCurrentDate(prev => {
const d = new Date(prev);
d.setMonth(prev.getMonth() + (dir === 'prev' ? -1 : 1));
return d;
});
const isVerrouille = () => mensuelData?.Verrouille === true || mensuelData?.Verrouille === 1;
const hasActiveFilters = filterSociete !== 'all' || filterCampus !== 'all' || filterService !== 'all';
const monthNames = ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'];
const nbNonSaisis = compteRenduData.filter(j => !j.DateSaisie).length;
if (isLoading && allCollaborateurs.length === 0) return (
<div className="min-h-screen bg-gradient-subtle flex items-center justify-center">
<div className="text-center">
<RefreshCw className="w-12 h-12 animate-spin text-cyan-600 mx-auto mb-4" />
<p className="text-gray-600">Chargement...</p>
</div>
</div>
);
return (
<div className="min-h-screen bg-gradient-subtle">
{/* Header */}
<header className="bg-card border-b border-border shadow-card">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="flex items-center space-x-3">
<Button variant="ghost" size="sm" onClick={() => navigate('/dashboard')}>
<ArrowLeft className="w-4 h-4 mr-2" /> Retour
</Button>
<div>
<h1 className="text-2xl font-bold text-foreground">
Gestion des Comptes-Rendus d'Activités
</h1>
<p className="text-sm text-muted-foreground">
Suivi et validation des forfaits jour · {allCollaborateurs.length} collaborateurs
</p>
</div>
</div>
</div>
</header>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Filtres */}
<Card className="shadow-card border-0 mb-6">
<CardContent className="p-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input placeholder="Rechercher..." value={searchTerm}
onChange={e => setSearchTerm(e.target.value)} className="pl-10" />
</div>
<Select value={filterSociete} onValueChange={setFilterSociete}>
<SelectTrigger><SelectValue placeholder="Toutes les sociétés" /></SelectTrigger>
<SelectContent>
<SelectItem value="all">Toutes les sociétés</SelectItem>
{societesUniques.map(s => <SelectItem key={s} value={s}>{s}</SelectItem>)}
</SelectContent>
</Select>
<Select value={filterCampus} onValueChange={setFilterCampus}>
<SelectTrigger><SelectValue placeholder="Tous les campus" /></SelectTrigger>
<SelectContent>
<SelectItem value="all">Tous les campus</SelectItem>
{campusUniques.map(c => <SelectItem key={c} value={c}>{c}</SelectItem>)}
</SelectContent>
</Select>
<Select value={filterService} onValueChange={setFilterService}>
<SelectTrigger><SelectValue placeholder="Tous les services" /></SelectTrigger>
<SelectContent>
<SelectItem value="all">Tous les services</SelectItem>
{servicesUniques.map(s => <SelectItem key={s} value={s}>{s}</SelectItem>)}
</SelectContent>
</Select>
</div>
{hasActiveFilters && (
<div className="mt-3 flex items-center gap-2">
<span className="text-xs text-muted-foreground">Filtres actifs :</span>
{filterSociete !== 'all' && <span className="px-2 py-0.5 bg-blue-100 text-blue-700 text-xs rounded-full flex items-center gap-1"><Building2 className="w-3 h-3" />{filterSociete}<button onClick={() => setFilterSociete('all')} className="ml-1">×</button></span>}
{filterCampus !== 'all' && <span className="px-2 py-0.5 bg-green-100 text-green-700 text-xs rounded-full flex items-center gap-1"><MapPin className="w-3 h-3" />{filterCampus}<button onClick={() => setFilterCampus('all')} className="ml-1">×</button></span>}
{filterService !== 'all' && <span className="px-2 py-0.5 bg-purple-100 text-purple-700 text-xs rounded-full flex items-center gap-1"><Briefcase className="w-3 h-3" />{filterService}<button onClick={() => setFilterService('all')} className="ml-1">×</button></span>}
<button onClick={() => { setFilterSociete('all'); setFilterCampus('all'); setFilterService('all'); }} className="text-xs text-muted-foreground underline">Réinitialiser</button>
</div>
)}
</CardContent>
</Card>
{/* Stats globales */}
{statsGlobales && (
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
<Card className="shadow-card"><CardContent className="p-6">
<div className="flex items-center gap-3">
<div className="p-3 bg-blue-100 rounded-lg"><Users className="w-6 h-6 text-blue-600" /></div>
<div><p className="text-sm text-muted-foreground">Forfaits jour</p><p className="text-2xl font-bold">{collaborateurs.length}</p></div>
</div>
</CardContent></Card>
<Card className="shadow-card"><CardContent className="p-6">
<div className="flex items-center gap-3">
<div className="p-3 bg-green-100 rounded-lg"><CheckCircle className="w-6 h-6 text-green-600" /></div>
<div><p className="text-sm text-muted-foreground">Mois validés</p><p className="text-2xl font-bold">{statsGlobales.mensuelsValides}</p></div>
</div>
</CardContent></Card>
<Card className="shadow-card"><CardContent className="p-6">
<div className="flex items-center gap-3">
<div className="p-3 bg-orange-100 rounded-lg"><Clock className="w-6 h-6 text-orange-600" /></div>
<div><p className="text-sm text-muted-foreground">En attente</p><p className="text-2xl font-bold">{statsGlobales.mensuelsEnAttente}</p></div>
</div>
</CardContent></Card>
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Liste collaborateurs */}
<div className="lg:col-span-1">
<Card className="shadow-card">
<CardHeader className="bg-gradient-to-r from-cyan-500 to-blue-500 text-white rounded-t-lg">
<h2 className="text-lg font-bold flex items-center gap-2">
<Users className="w-5 h-5" />
Collaborateurs ({collaborateurs.length})
{hasActiveFilters && <span className="text-xs bg-white/20 px-2 py-1 rounded">Filtré</span>}
</h2>
</CardHeader>
<div className="overflow-y-auto max-h-[600px]">
{collaborateurs.length === 0 ? (
<p className="p-4 text-center text-muted-foreground text-sm">
{searchTerm || hasActiveFilters ? 'Aucun résultat' : 'Aucun collaborateur en forfait jour'}
</p>
) : collaborateurs.map(collab => (
<div key={collab.id} onClick={() => setSelectedCollab(collab)}
className={`p-4 border-b cursor-pointer hover:bg-gray-50 transition-all
${selectedCollab?.id === collab.id ? 'bg-blue-50 border-l-4 border-blue-500' : ''}`}>
<p className="font-semibold text-foreground">{collab.nomComplet}</p>
<div className="flex flex-wrap gap-1 mt-1">
{collab.societe && <span className="text-xs text-blue-600 bg-blue-50 px-1.5 py-0.5 rounded">{collab.societe}</span>}
{collab.campus && <span className="text-xs text-green-600 bg-green-50 px-1.5 py-0.5 rounded">{collab.campus}</span>}
</div>
<p className="text-xs text-muted-foreground mt-1">{collab.service || 'Service non défini'}</p>
</div>
))}
</div>
</Card>
</div>
{/* Détails */}
<div className="lg:col-span-2">
{!selectedCollab ? (
<Card className="shadow-card p-8 text-center">
<Users className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<p className="text-muted-foreground">Sélectionnez un collaborateur pour voir ses comptes-rendus</p>
</Card>
) : (
<div className="space-y-6">
{/* En-tête + navigation */}
<Card className="shadow-card">
<CardContent className="p-6">
<div className="flex items-center justify-between mb-4">
<div>
<h2 className="text-xl font-bold">{selectedCollab.nomComplet}</h2>
<div className="flex flex-wrap gap-2 mt-1">
{selectedCollab.societe && <span className="text-xs text-blue-600 bg-blue-50 px-2 py-1 rounded">{selectedCollab.societe}</span>}
{selectedCollab.campus && <span className="text-xs text-green-600 bg-green-50 px-2 py-1 rounded">{selectedCollab.campus}</span>}
{selectedCollab.service && <span className="text-xs text-purple-600 bg-purple-50 px-2 py-1 rounded">{selectedCollab.service}</span>}
</div>
</div>
{isVerrouille() ? (
<span className="flex items-center gap-2 px-3 py-1.5 bg-red-100 text-red-700 rounded-lg text-sm font-medium">
<Lock className="w-4 h-4" /> Verrouillé
</span>
) : (
<span className="flex items-center gap-2 px-3 py-1.5 bg-green-100 text-green-700 rounded-lg text-sm font-medium">
<Unlock className="w-4 h-4" /> Ouvert
</span>
)}
</div>
{/* Navigation mois */}
<div className="flex items-center justify-between mb-4">
<button onClick={() => navigateMonth('prev')} className="p-2 hover:bg-gray-100 rounded">
<ChevronLeft className="w-5 h-5 text-blue-600" />
</button>
<h3 className="text-lg font-semibold">{monthNames[currentDate.getMonth()]} {currentDate.getFullYear()}</h3>
<button onClick={() => navigateMonth('next')} className="p-2 hover:bg-gray-100 rounded">
<ChevronRight className="w-5 h-5 text-blue-600" />
</button>
</div>
{/* Actions */}
<div className="flex gap-3">
{isVerrouille() ? (
<Button onClick={() => handleToggleLock(true)} className="flex-1 bg-orange-600 hover:bg-orange-700">
<Unlock className="w-4 h-4 mr-2" /> Déverrouiller
</Button>
) : (
<Button onClick={() => handleToggleLock(false)} className="flex-1 bg-green-600 hover:bg-green-700">
<Lock className="w-4 h-4 mr-2" /> Verrouiller
</Button>
)}
<Button onClick={handleExportPDF} className="flex-1 bg-purple-600 hover:bg-purple-700">
<Download className="w-4 h-4 mr-2" /> Export PDF
</Button>
</div>
</CardContent>
</Card>
{/* Cumul annuel */}
{cumulAnnuel && (
<Card className="shadow-card bg-gradient-to-br from-purple-50 to-blue-50 border-purple-200">
<CardContent className="p-6">
<div className="flex items-center gap-3 mb-4">
<div className="p-3 bg-purple-100 rounded-lg"><TrendingUp className="w-6 h-6 text-purple-600" /></div>
<div>
<h3 className="text-lg font-bold text-purple-900">Cumul Annuel {annee}</h3>
<p className="text-sm text-purple-700">Synthèse des activités sur l'année</p>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="bg-white rounded-lg p-4 border border-purple-200">
<p className="text-sm text-gray-600 mb-1">Total jours travaillés</p>
<p className="text-3xl font-bold text-purple-600">{cumulAnnuel.totalJoursTravailles || 0}</p>
<p className="text-xs text-gray-500 mt-1">jours</p>
</div>
<div className="bg-white rounded-lg p-4 border border-purple-200">
<p className="text-sm text-gray-600 mb-1">Mois validés</p>
<p className="text-3xl font-bold text-blue-600">{cumulAnnuel.moisValides || 0}</p>
<p className="text-xs text-gray-500 mt-1">/ 12 mois</p>
</div>
<div className="bg-white rounded-lg p-4 border border-orange-200">
<p className="text-sm text-gray-600 mb-1">Repos quotidien non respect.</p>
<p className="text-2xl font-bold text-orange-600">{cumulAnnuel.totalReposQuotidien || 0}</p>
</div>
<div className="bg-white rounded-lg p-4 border border-red-200">
<p className="text-sm text-gray-600 mb-1">Repos hebdo non respect.</p>
<p className="text-2xl font-bold text-red-600">{cumulAnnuel.totalReposHebdo || 0}</p>
</div>
</div>
<div className="mt-4 bg-white rounded-lg p-3 border border-purple-200">
<div className="flex justify-between mb-2">
<span className="text-sm font-medium text-gray-700">Progression annuelle</span>
<span className="text-sm font-bold text-purple-600">{Math.round(((cumulAnnuel.moisValides || 0) / 12) * 100)}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div className="bg-gradient-to-r from-purple-500 to-blue-500 h-2 rounded-full"
style={{ width: `${((cumulAnnuel.moisValides || 0) / 12) * 100}%` }} />
</div>
</div>
</CardContent>
</Card>
)}
{/* Stats du mois */}
{mensuelData && (
<Card className="shadow-card">
<CardContent className="p-6">
<h3 className="text-lg font-bold mb-4">Statistiques du mois</h3>
<div className="grid grid-cols-3 gap-4">
<div className="text-center">
<p className="text-2xl font-bold text-blue-600">{mensuelData.NbJoursTravailles || 0}</p>
<p className="text-sm text-muted-foreground">Jours travaillés</p>
</div>
<div className="text-center">
<p className="text-2xl font-bold text-orange-600">{mensuelData.NbJoursNonRespectsReposQuotidien || 0}</p>
<p className="text-sm text-muted-foreground">Non-respect repos quotidien</p>
</div>
<div className="text-center">
<p className="text-2xl font-bold text-red-600">{mensuelData.NbJoursNonRespectsReposHebdo || 0}</p>
<p className="text-sm text-muted-foreground">Non-respect repos hebdo</p>
</div>
</div>
</CardContent>
</Card>
)}
{/* Tableau éditable */}
<Card className="shadow-card">
<CardContent className="p-6">
<div className="flex items-center justify-between mb-2">
<h3 className="text-lg font-bold flex items-center gap-2">
<Edit3 className="w-5 h-5 text-blue-600" />
Édition des saisies accès RH
</h3>
{/* ✅ Badge jours non saisis */}
{nbNonSaisis > 0 && (
<span className="text-xs bg-orange-100 text-orange-700 px-3 py-1 rounded-full font-medium">
{nbNonSaisis} jour{nbNonSaisis > 1 ? 's' : ''} non saisi{nbNonSaisis > 1 ? 's' : ''}
</span>
)}
</div>
<p className="text-xs text-muted-foreground mb-4">
Les lignes en fond <span className="text-orange-600 font-medium">orange clair</span> sont des jours non saisis par le collaborateur.
Les lignes en fond <span className="text-yellow-600 font-medium">jaune</span> ont é modifiées.
</p>
{isLoading ? (
<div className="text-center py-8">
<RefreshCw className="w-8 h-8 animate-spin text-blue-500 mx-auto" />
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b text-xs uppercase text-muted-foreground">
<th className="text-left py-2 px-3">Date</th>
<th className="text-center py-2 px-3">Travaillé</th>
<th className="text-center py-2 px-3">Repos quotidien</th>
<th className="text-center py-2 px-3">Repos hebdo</th>
<th className="text-left py-2 px-3">Commentaire</th>
<th className="text-center py-2 px-3">Action</th>
</tr>
</thead>
<tbody>
{compteRenduData.map((jour: any) => (
<JourEditRow
key={jour.JourDate}
jour={jour}
collaborateurId={selectedCollab.id}
token={token}
onSaved={loadCompteRendu}
/>
))}
</tbody>
</table>
</div>
)}
</CardContent>
</Card>
</div>
)}
</div>
</div>
</main>
{/* Modal détail jour */}
{showDetailModal && selectedJour && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<Card className="w-full max-w-md">
<CardHeader className="bg-gradient-to-r from-cyan-500 to-blue-500 text-white rounded-t-lg">
<div className="flex items-center justify-between">
<h3 className="font-bold">Détail du {new Date(selectedJour.JourDate).toLocaleDateString('fr-FR')}</h3>
<button onClick={() => setShowDetailModal(false)} className="text-white hover:bg-white/20 p-1 rounded">
<XCircle className="w-5 h-5" />
</button>
</div>
</CardHeader>
<CardContent className="p-6 space-y-4">
<div>
<p className="text-sm text-muted-foreground">Statut</p>
<p className="font-semibold">{selectedJour.JourTravaille ? 'Jour travaillé' : 'Jour non travaillé'}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Repos quotidien respecté</p>
<p className="font-semibold">{selectedJour.ReposQuotidienRespect ? 'Oui' : 'Non'}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Repos hebdomadaire respecté</p>
<p className="font-semibold">{selectedJour.ReposHebdomadaireRespect ? 'Oui' : 'Non'}</p>
</div>
{selectedJour.CommentaireRepos && (
<div>
<p className="text-sm text-muted-foreground">Commentaire</p>
<p className="text-sm bg-gray-50 p-3 rounded">{selectedJour.CommentaireRepos}</p>
</div>
)}
{selectedJour.DateSaisie && (
<div>
<p className="text-sm text-muted-foreground">Date de saisie</p>
<p className="text-sm">{new Date(selectedJour.DateSaisie).toLocaleDateString('fr-FR')}</p>
</div>
)}
{selectedJour.DateModification && (
<div>
<p className="text-sm text-muted-foreground">Dernière modification</p>
<p className="text-sm">{new Date(selectedJour.DateModification).toLocaleDateString('fr-FR')}</p>
</div>
)}
</CardContent>
</Card>
</div>
)}
</div>
);
};
export default CompteRenduRH;