import React, { useState, useEffect, useCallback } from 'react'; import { useAuth } from '../context/AuthContext'; import Sidebar from '../components/Sidebar'; import { Calendar, Clock, Check, X, Save, Lock, Unlock, Download, Menu, AlertCircle, ChevronLeft, ChevronRight, FileText, TrendingUp, RefreshCw, Info } from 'lucide-react'; const CompteRenduActivites = () => { const { user } = useAuth(); const userId = user?.id || user?.CollaborateurADId || user?.ID; const [sidebarOpen, setSidebarOpen] = useState(false); const [currentDate, setCurrentDate] = useState(new Date()); const [joursActifs, setJoursActifs] = useState([]); const [statsAnnuelles, setStatsAnnuelles] = useState(null); const [mensuelData, setMensuelData] = useState(null); const [congesData, setCongesData] = useState([]); const [holidays, setHolidays] = useState([]); const [isLoading, setIsLoading] = useState(true); const [showSaisieModal, setShowSaisieModal] = useState(false); const [selectedJour, setSelectedJour] = useState(null); const [showSaisieMasse, setShowSaisieMasse] = useState(false); const [isSaving, setIsSaving] = useState(false); const [infoMessage, setInfoMessage] = useState(null); // Vérifier l'accès : forfait jour, directeur campus, ou RH const hasAccess = () => { const userRole = user?.role; const typeContrat = user?.TypeContrat || user?.typeContrat; return ( typeContrat === 'forfait-jour' || typeContrat === 'forfait_jour' || userRole === 'Directeur Campus' || userRole === 'Directrice Campus' || userRole === 'RH' || userRole === 'Admin' ); }; const isRH = user?.role === 'RH' || user?.role === 'Admin'; const isDirecteurCampus = user?.role === 'Directeur Campus' || user?.role === 'Directrice Campus'; const annee = currentDate.getFullYear(); const mois = currentDate.getMonth() + 1; // Charger les jours fériés français const fetchFrenchHolidays = async (year) => { try { const response = await fetch(`https://calendrier.api.gouv.fr/jours-feries/metropole/${year}.json`); if (!response.ok) throw new Error(`Erreur API: ${response.status}`); const data = await response.json(); const holidayDates = Object.keys(data).map(dateStr => { const [year, month, day] = dateStr.split('-').map(Number); return { date: new Date(year, month - 1, day), name: data[dateStr] }; }); return holidayDates; } catch (error) { console.error('Erreur jours fériés:', error); return []; } }; useEffect(() => { const year = currentDate.getFullYear(); fetchFrenchHolidays(year).then(setHolidays); }, [currentDate]); const isHoliday = (date) => { if (!date) return false; return holidays.some(holiday => holiday.date.getDate() === date.getDate() && holiday.date.getMonth() === date.getMonth() && holiday.date.getFullYear() === date.getFullYear() ); }; const getHolidayName = (date) => { const holiday = holidays.find(h => h.date.getDate() === date.getDate() && h.date.getMonth() === date.getMonth() && h.date.getFullYear() === date.getFullYear() ); return holiday?.name; }; // Charger les données du mois const loadCompteRendu = useCallback(async () => { if (!userId || !hasAccess()) return; setIsLoading(true); try { const response = await fetch(`/getCompteRenduActivites?user_id=${userId}&annee=${annee}&mois=${mois}`); const data = await response.json(); if (data.success) { setJoursActifs(data.jours || []); setMensuelData(data.mensuel); console.log('📅 Jours chargés:', data.jours?.length || 0, 'jours'); console.log('📊 Détail des jours:', data.jours); } const congesResponse = await fetch(`/getTeamLeaves?user_id=${userId}&role=${user.role}`); const congesData = await congesResponse.json(); if (congesData.success) { setCongesData(congesData.leaves || []); } } catch (error) { console.error('Erreur chargement compte-rendu:', error); } finally { setIsLoading(false); } }, [userId, annee, mois, user?.role]); // Charger les stats annuelles const loadStatsAnnuelles = useCallback(async () => { if (!userId || !hasAccess()) return; try { const response = await fetch(`/getStatsAnnuelles?user_id=${userId}&annee=${annee}`); const data = await response.json(); if (data.success) { setStatsAnnuelles(data.stats); } } catch (error) { console.error('Erreur stats annuelles:', error); } }, [userId, annee]); useEffect(() => { loadCompteRendu(); loadStatsAnnuelles(); }, [loadCompteRendu, loadStatsAnnuelles]); // Vérifier si le mois est autorisé (mois en cours + mois précédent) const isMoisAutorise = () => { const today = new Date(); const currentYear = today.getFullYear(); const currentMonth = today.getMonth() + 1; const selectedYear = currentDate.getFullYear(); const selectedMonth = currentDate.getMonth() + 1; // RH peut tout voir if (isRH) return true; // Mois en cours autorisé if (selectedYear === currentYear && selectedMonth === currentMonth) return true; // Mois précédent autorisé const previousMonth = currentMonth === 1 ? 12 : currentMonth - 1; const previousYear = currentMonth === 1 ? currentYear - 1 : currentYear; return selectedYear === previousYear && selectedMonth === previousMonth; }; // Générer les jours du mois (lundi-samedi) avec décalage correct const getDaysInMonth = () => { const year = currentDate.getFullYear(); const month = currentDate.getMonth(); const firstDay = new Date(year, month, 1); const lastDay = new Date(year, month + 1, 0); const daysInMonth = lastDay.getDate(); // Jour de la semaine du 1er (0=dimanche, 1=lundi, ..., 6=samedi) let firstDayOfWeek = firstDay.getDay(); // Convertir pour que lundi = 0, mardi = 1, ..., samedi = 5, dimanche = 6 firstDayOfWeek = firstDayOfWeek === 0 ? 6 : firstDayOfWeek - 1; const days = []; // Ajouter des cases vides pour le décalage initial for (let i = 0; i < firstDayOfWeek; i++) { days.push(null); } // Ajouter tous les jours du mois (lundi-samedi uniquement) for (let day = 1; day <= daysInMonth; day++) { const currentDay = new Date(year, month, day); const dayOfWeek = currentDay.getDay(); // Exclure les dimanches (0) if (dayOfWeek !== 0) { days.push(currentDay); } } return days; }; const getJourData = (date) => { const dateStr = formatDateToString(date); const found = joursActifs.find(j => { // Normaliser la date de la BDD (peut être un objet Date ou une string) let jourDateStr = j.JourDate; if (j.JourDate instanceof Date) { jourDateStr = formatDateToString(j.JourDate); } else if (typeof j.JourDate === 'string') { // Si c'est déjà une string, extraire juste la partie date (YYYY-MM-DD) jourDateStr = j.JourDate.split('T')[0]; } const match = jourDateStr === dateStr; console.log('Comparaison:', jourDateStr, 'vs', dateStr, 'match:', match); return match; }); if (found) { console.log('✅ Jour trouvé:', dateStr, found); } return found; }; const isJourEnConge = (date) => { return congesData.some(conge => { const start = new Date(conge.startdate); const end = new Date(conge.enddate); return date >= start && date <= end && conge.statut === 'Valide'; }); }; // Vérifier si le jour est STRICTEMENT dans le passé (pas aujourd'hui) const isPastOnly = (date) => { const today = new Date(); today.setHours(0, 0, 0, 0); const checkDate = new Date(date); checkDate.setHours(0, 0, 0, 0); return checkDate < today; }; // Vérifier si un jour spécifique est verrouillé const isJourVerrouille = (date) => { const jourData = getJourData(date); return jourData?.Verrouille === true || jourData?.Verrouille === 1; }; // Afficher message d'info const showInfo = (message, type = 'info') => { setInfoMessage({ message, type }); setTimeout(() => setInfoMessage(null), 5000); }; // Ouvrir le modal de saisie const handleJourClick = (date) => { if (!date) return; // Ignorer les cases vides if (!isMoisAutorise() && !isRH) { showInfo('Vous ne pouvez saisir que pour le mois en cours ou le mois précédent', 'warning'); return; } if (!isPastOnly(date)) { showInfo('Vous ne pouvez pas saisir le jour actuel. Veuillez attendre demain.', 'warning'); return; } if (isHoliday(date)) { showInfo(`Jour férié : ${getHolidayName(date)} - Saisie impossible`, 'info'); return; } if (isJourEnConge(date)) { showInfo('Vous êtes en congé ce jour - Saisie impossible', 'info'); return; } if (isJourVerrouille(date) && !isRH) { showInfo('Ce jour est déjà verrouillé - Contactez les RH pour modification', 'error'); return; } const jourData = getJourData(date); setSelectedJour({ date: date, dateStr: formatDateToString(date), jourTravaille: jourData?.JourTravaille !== false, reposQuotidien: jourData?.ReposQuotidienRespect !== false, reposHebdo: jourData?.ReposHebdomadaireRespect !== false, commentaire: jourData?.CommentaireRepos || '' }); setShowSaisieModal(true); }; // Sauvegarder un jour const handleSaveJour = async () => { if ((!selectedJour.reposQuotidien || !selectedJour.reposHebdo)) { if (!selectedJour.commentaire || selectedJour.commentaire.trim() === '') { showInfo('Commentaire obligatoire en cas de non-respect des repos', 'warning'); return; } } setIsSaving(true); try { const response = await fetch('/saveCompteRenduJour', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user_id: userId, date: selectedJour.dateStr, jour_travaille: selectedJour.jourTravaille, repos_quotidien: selectedJour.reposQuotidien, repos_hebdo: selectedJour.reposHebdo, commentaire: selectedJour.commentaire, rh_override: isRH }) }); const data = await response.json(); if (data.success) { await loadCompteRendu(); await loadStatsAnnuelles(); setShowSaisieModal(false); showInfo('✅ Jour enregistré', 'success'); console.log('Données rechargées après sauvegarde'); } else { showInfo(data.message || 'Erreur lors de la sauvegarde', 'error'); } } catch (error) { console.error('Erreur sauvegarde jour:', error); showInfo('Erreur serveur', 'error'); } finally { setIsSaving(false); } }; // Saisie en masse const handleSaisieMasse = async (joursTravailles) => { if (!isMoisAutorise() && !isRH) { showInfo('Vous ne pouvez saisir que pour le mois en cours ou le mois précédent', 'warning'); return; } setIsSaving(true); try { const response = await fetch('/saveCompteRenduMasse', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user_id: userId, annee: annee, mois: mois, jours: joursTravailles, rh_override: isRH }) }); const data = await response.json(); if (data.success) { await loadCompteRendu(); await loadStatsAnnuelles(); setShowSaisieMasse(false); showInfo(data.message || `✅ ${data.count || 0} jours enregistrés`, 'success'); } else { showInfo(data.message || 'Erreur', 'error'); } } catch (error) { console.error('Erreur saisie masse:', error); showInfo('Erreur serveur', 'error'); } finally { setIsSaving(false); } }; const formatDateToString = (date) => { if (!date) return null; const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; }; const navigateMonth = (direction) => { setCurrentDate(prev => { const newDate = new Date(prev); if (direction === 'prev') { newDate.setMonth(prev.getMonth() - 1); } else { newDate.setMonth(prev.getMonth() + 1); } return newDate; }); }; const monthNames = ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre']; const days = getDaysInMonth(); const moisAutorise = isMoisAutorise(); // Vérifier l'accès if (!hasAccess()) { return (
setSidebarOpen(!sidebarOpen)} />

Accès restreint

Cette fonctionnalité est réservée aux :

  • Collaborateurs en forfait jour
  • Directeurs et directrices de campus
  • Service RH
); } if (isLoading) { return (
setSidebarOpen(!sidebarOpen)} />

Chargement...

); } return (
setSidebarOpen(!sidebarOpen)} />
{/* Message d'information */} {infoMessage && (

{infoMessage.message}

)} {/* Header */}

Compte-Rendu d'Activités

Forfait jour - Suivi des jours travaillés et repos obligatoires

{/* Stats annuelles */} {statsAnnuelles && (

Cumul annuel {annee}

Jours travaillés

{statsAnnuelles.totalJoursTravailles || 0}

)}
{/* Bandeau mois non autorisé */} {!moisAutorise && !isRH && (

Mois non accessible

Vous pouvez saisir uniquement le mois en cours et le mois précédent (mais pas le jour actuel).

)} {/* Navigation + Actions */}

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

{mensuelData && ( {mensuelData.NbJoursTravailles || 0} jours saisis )}
{/* Calendrier */}
{['Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi'].map(day => (
{day}
))}
{days.map((date, index) => { // Case vide pour le décalage if (date === null) { return (
); } const jourData = getJourData(date); const enConge = isJourEnConge(date); const ferie = isHoliday(date); const isPast = isPastOnly(date); const isToday = date.toDateString() === new Date().toDateString(); const jourVerrouille = isJourVerrouille(date); // Déterminer la classe de fond let bgClass = 'bg-white hover:bg-gray-50'; let cursorClass = 'cursor-pointer hover:shadow-md'; if (ferie) { bgClass = 'bg-gray-700 text-white'; cursorClass = 'cursor-not-allowed'; } else if (enConge) { bgClass = 'bg-purple-100'; cursorClass = 'cursor-not-allowed'; } else if (jourData) { bgClass = 'bg-gray-300 hover:bg-gray-400'; cursorClass = 'cursor-pointer hover:shadow-md'; } return (
handleJourClick(date)} className={` min-h-[100px] p-3 rounded-lg border-2 transition-all ${isToday ? 'border-cyan-500' : 'border-gray-200'} ${bgClass} ${cursorClass} ${!isPast ? 'opacity-40 cursor-not-allowed' : ''} ${!moisAutorise && !isRH ? 'opacity-60 cursor-not-allowed' : ''} `} title={ ferie ? getHolidayName(date) : enConge ? 'En congé' : jourData ? 'Jour saisi - Cliquer pour modifier' : '' } >
{date.getDate()} {jourData && !ferie && !enConge && ( )}
{ferie ? (
{getHolidayName(date)}
) : enConge ? (
En congé
) : jourData ? (
{jourData.JourTravaille ? ( ) : ( )} {jourData.JourTravaille ? 'Travaillé' : 'Non travaillé'}
{!jourData.ReposQuotidienRespect && (
Repos quotidien
)} {!jourData.ReposHebdomadaireRespect && (
Repos hebdo
)}
Saisi
) : (
{isPast && moisAutorise ? 'Cliquer pour saisir' : 'Non disponible'}
)}
); })}
{/* Légende */}
Aujourd'hui
Jour saisi (grisé)
En congé
Jour férié
Non-respect repos
{/* Modal saisie jour */} {showSaisieModal && selectedJour && (

Saisie du {selectedJour.date.toLocaleDateString('fr-FR', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}

{selectedJour.jourTravaille && ( <>
{(!selectedJour.reposQuotidien || !selectedJour.reposHebdo) && (