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(!!jour.JourTravaille); const [reposQ, setReposQ] = useState(!!jour.ReposQuotidienRespect); const [reposH, setReposH] = useState(!!jour.ReposHebdomadaireRespect); const [commentaire, setCommentaire] = useState(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 ( {/* Date */} {dateStr} {isWeekend && (WE)} {/* ✅ Badge "Non saisi" pour les jours vides */} {nonSaisi && ( Non saisi )} {/* Jour travaillé */} mark(() => { const v = e.target.checked; setTravaille(v); if (!v) { setReposQ(false); setReposH(false); } })} className="w-4 h-4 accent-blue-600 cursor-pointer" /> {/* Repos quotidien */} mark(() => setReposQ(e.target.checked))} disabled={!travaille} className="w-4 h-4 accent-green-600 cursor-pointer disabled:opacity-30 disabled:cursor-not-allowed" /> {/* Repos hebdo */} mark(() => setReposH(e.target.checked))} disabled={!travaille} className="w-4 h-4 accent-green-600 cursor-pointer disabled:opacity-30 disabled:cursor-not-allowed" /> {/* Commentaire */} 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" /> {/* Bouton sauvegarde */} {saved ? ( ) : ( )} ); }; // ─── 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(null); const [token, setToken] = useState(''); const [currentDate, setCurrentDate] = useState(new Date()); const [isLoading, setIsLoading] = useState(true); const [collaborateurs, setCollaborateurs] = useState([]); const [allCollaborateurs, setAllCollaborateurs] = useState([]); const [selectedCollab, setSelectedCollab] = useState(null); const [compteRenduData, setCompteRenduData] = useState([]); const [mensuelData, setMensuelData] = useState(null); const [statsGlobales, setStatsGlobales] = useState(null); const [searchTerm, setSearchTerm] = useState(''); const [showDetailModal, setShowDetailModal] = useState(false); const [selectedJour, setSelectedJour] = useState(null); const [cumulAnnuel, setCumulAnnuel] = useState(null); // Filtres const [filterSociete, setFilterSociete] = useState('all'); const [filterCampus, setFilterCampus] = useState('all'); const [filterService, setFilterService] = useState('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 (

Chargement...

); return (
{/* Header */}

Gestion des Comptes-Rendus d'Activités

Suivi et validation des forfaits jour · {allCollaborateurs.length} collaborateurs

{/* Filtres */}
setSearchTerm(e.target.value)} className="pl-10" />
{hasActiveFilters && (
Filtres actifs : {filterSociete !== 'all' && {filterSociete}} {filterCampus !== 'all' && {filterCampus}} {filterService !== 'all' && {filterService}}
)}
{/* Stats globales */} {statsGlobales && (

Forfaits jour

{collaborateurs.length}

Mois validés

{statsGlobales.mensuelsValides}

En attente

{statsGlobales.mensuelsEnAttente}

)}
{/* Liste collaborateurs */}

Collaborateurs ({collaborateurs.length}) {hasActiveFilters && Filtré}

{collaborateurs.length === 0 ? (

{searchTerm || hasActiveFilters ? 'Aucun résultat' : 'Aucun collaborateur en forfait jour'}

) : collaborateurs.map(collab => (
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' : ''}`}>

{collab.nomComplet}

{collab.societe && {collab.societe}} {collab.campus && {collab.campus}}

{collab.service || 'Service non défini'}

))}
{/* Détails */}
{!selectedCollab ? (

Sélectionnez un collaborateur pour voir ses comptes-rendus

) : (
{/* En-tête + navigation */}

{selectedCollab.nomComplet}

{selectedCollab.societe && {selectedCollab.societe}} {selectedCollab.campus && {selectedCollab.campus}} {selectedCollab.service && {selectedCollab.service}}
{isVerrouille() ? ( Verrouillé ) : ( Ouvert )}
{/* Navigation mois */}

{monthNames[currentDate.getMonth()]} {currentDate.getFullYear()}

{/* Actions */}
{isVerrouille() ? ( ) : ( )}
{/* Cumul annuel */} {cumulAnnuel && (

Cumul Annuel {annee}

Synthèse des activités sur l'année

Total jours travaillés

{cumulAnnuel.totalJoursTravailles || 0}

jours

Mois validés

{cumulAnnuel.moisValides || 0}

/ 12 mois

Repos quotidien non respect.

{cumulAnnuel.totalReposQuotidien || 0}

Repos hebdo non respect.

{cumulAnnuel.totalReposHebdo || 0}

Progression annuelle {Math.round(((cumulAnnuel.moisValides || 0) / 12) * 100)}%
)} {/* Stats du mois */} {mensuelData && (

Statistiques du mois

{mensuelData.NbJoursTravailles || 0}

Jours travaillés

{mensuelData.NbJoursNonRespectsReposQuotidien || 0}

Non-respect repos quotidien

{mensuelData.NbJoursNonRespectsReposHebdo || 0}

Non-respect repos hebdo

)} {/* Tableau éditable */}

Édition des saisies — accès RH

{/* ✅ Badge jours non saisis */} {nbNonSaisis > 0 && ( {nbNonSaisis} jour{nbNonSaisis > 1 ? 's' : ''} non saisi{nbNonSaisis > 1 ? 's' : ''} )}

Les lignes en fond orange clair sont des jours non saisis par le collaborateur. Les lignes en fond jaune ont été modifiées.

{isLoading ? (
) : (
{compteRenduData.map((jour: any) => ( ))}
Date Travaillé Repos quotidien Repos hebdo Commentaire Action
)}
)}
{/* Modal détail jour */} {showDetailModal && selectedJour && (

Détail du {new Date(selectedJour.JourDate).toLocaleDateString('fr-FR')}

Statut

{selectedJour.JourTravaille ? 'Jour travaillé' : 'Jour non travaillé'}

Repos quotidien respecté

{selectedJour.ReposQuotidienRespect ? 'Oui' : 'Non'}

Repos hebdomadaire respecté

{selectedJour.ReposHebdomadaireRespect ? 'Oui' : 'Non'}

{selectedJour.CommentaireRepos && (

Commentaire

{selectedJour.CommentaireRepos}

)} {selectedJour.DateSaisie && (

Date de saisie

{new Date(selectedJour.DateSaisie).toLocaleDateString('fr-FR')}

)} {selectedJour.DateModification && (

Dernière modification

{new Date(selectedJour.DateModification).toLocaleDateString('fr-FR')}

)}
)}
); }; export default CompteRenduRH;