Files
GTA/project/src/pages/CompteRenduActivite.jsx

984 lines
45 KiB
JavaScript

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(`http://localhost:3000/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(`http://localhost:3000/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(`http://localhost:3000/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-vendredi)
const getDaysInMonth = () => {
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
const lastDay = new Date(year, month + 1, 0);
const daysInMonth = lastDay.getDate();
const days = [];
for (let day = 1; day <= daysInMonth; day++) {
const currentDay = new Date(year, month, day);
const dayOfWeek = currentDay.getDay();
if (dayOfWeek >= 1 && dayOfWeek <= 5) {
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 (!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('http://localhost:3000/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('http://localhost:3000/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 (
<div className="min-h-screen bg-gray-50 flex">
<Sidebar isOpen={sidebarOpen} onToggle={() => setSidebarOpen(!sidebarOpen)} />
<div className="flex-1 lg:ml-60 p-8">
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-6 text-center">
<AlertCircle className="w-12 h-12 text-yellow-600 mx-auto mb-4" />
<h2 className="text-xl font-bold text-gray-900 mb-2">Accès restreint</h2>
<p className="text-gray-600 mb-4">
Cette fonctionnalité est réservée aux :
</p>
<ul className="text-gray-700 space-y-2 text-left max-w-md mx-auto">
<li className="flex items-center gap-2">
<Check className="w-4 h-4 text-green-600" />
Collaborateurs en forfait jour
</li>
<li className="flex items-center gap-2">
<Check className="w-4 h-4 text-green-600" />
Directeurs et directrices de campus
</li>
<li className="flex items-center gap-2">
<Check className="w-4 h-4 text-green-600" />
Service RH
</li>
</ul>
</div>
</div>
</div>
);
}
if (isLoading) {
return (
<div className="min-h-screen bg-gray-50 flex">
<Sidebar isOpen={sidebarOpen} onToggle={() => setSidebarOpen(!sidebarOpen)} />
<div className="flex-1 lg:ml-60 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>
</div>
);
}
return (
<div className="min-h-screen bg-gray-50 flex">
<Sidebar isOpen={sidebarOpen} onToggle={() => setSidebarOpen(!sidebarOpen)} />
<div className="flex-1 lg:ml-60 p-4 lg:p-8">
{/* Message d'information */}
{infoMessage && (
<div className={`mb-4 p-4 rounded-lg border-l-4 flex items-center gap-3 animate-slideIn ${infoMessage.type === 'success' ? 'bg-green-50 border-green-500' :
infoMessage.type === 'error' ? 'bg-red-50 border-red-500' :
infoMessage.type === 'warning' ? 'bg-orange-50 border-orange-500' :
'bg-blue-50 border-blue-500'
}`}>
<Info className={`w-5 h-5 flex-shrink-0 ${infoMessage.type === 'success' ? 'text-green-600' :
infoMessage.type === 'error' ? 'text-red-600' :
infoMessage.type === 'warning' ? 'text-orange-600' :
'text-blue-600'
}`} />
<p className={`text-sm font-medium ${infoMessage.type === 'success' ? 'text-green-800' :
infoMessage.type === 'error' ? 'text-red-800' :
infoMessage.type === 'warning' ? 'text-orange-800' :
'text-blue-800'
}`}>{infoMessage.message}</p>
</div>
)}
{/* Header */}
<div className="mb-6">
<div className="flex items-center justify-between mb-4">
<div>
<h1 className="text-2xl lg:text-3xl font-bold text-gray-900">
Compte-Rendu d'Activités
</h1>
<p className="text-sm text-gray-600 mt-1">
Forfait jour - Suivi des jours travaillés et repos obligatoires
</p>
</div>
<button
onClick={() => setSidebarOpen(true)}
className="lg:hidden p-2 rounded-lg bg-white border"
>
<Menu className="w-6 h-6" />
</button>
</div>
{/* Stats annuelles */}
{statsAnnuelles && (
<div className="bg-gradient-to-r from-cyan-500 to-blue-500 rounded-xl shadow-md p-6 text-white">
<div className="flex items-center gap-2 mb-3">
<TrendingUp className="w-6 h-6" />
<h3 className="text-lg font-bold">Cumul annuel {annee}</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="bg-white bg-opacity-20 rounded-lg p-4">
<p className="text-sm opacity-90">Jours travaillés</p>
<p className="text-3xl font-bold">{statsAnnuelles.totalJoursTravailles || 0}</p>
</div>
<div className="bg-white bg-opacity-20 rounded-lg p-4">
<p className="text-sm opacity-90">Non-respect repos quotidien</p>
<p className="text-3xl font-bold">{statsAnnuelles.totalNonRespectQuotidien || 0}</p>
</div>
<div className="bg-white bg-opacity-20 rounded-lg p-4">
<p className="text-sm opacity-90">Non-respect repos hebdo</p>
<p className="text-3xl font-bold">{statsAnnuelles.totalNonRespectHebdo || 0}</p>
</div>
</div>
</div>
)}
</div>
{/* Bandeau mois non autorisé */}
{!moisAutorise && !isRH && (
<div className="bg-yellow-50 border-l-4 border-yellow-500 p-4 mb-6 rounded">
<div className="flex items-center gap-3">
<AlertCircle className="w-6 h-6 text-yellow-600" />
<div>
<p className="font-semibold text-yellow-900">Mois non accessible</p>
<p className="text-sm text-yellow-700">
Vous pouvez saisir uniquement le mois en cours et le mois précédent (mais pas le jour actuel).
</p>
</div>
</div>
</div>
)}
{/* Navigation + Actions */}
<div className="bg-white rounded-lg border p-4 mb-6">
<div className="flex flex-col lg:flex-row items-center justify-between gap-4">
<div className="flex items-center gap-4">
<button
onClick={() => navigateMonth('prev')}
className="p-2 hover:bg-gray-100 rounded"
>
<ChevronLeft className="w-5 h-5 text-blue-600" />
</button>
<h2 className="text-xl font-semibold min-w-[200px] text-center">
{monthNames[currentDate.getMonth()]} {currentDate.getFullYear()}
</h2>
<button
onClick={() => navigateMonth('next')}
className="p-2 hover:bg-gray-100 rounded"
>
<ChevronRight className="w-5 h-5 text-blue-600" />
</button>
</div>
<div className="flex items-center gap-3 flex-wrap">
{mensuelData && (
<span className="text-sm text-gray-600">
{mensuelData.NbJoursTravailles || 0} jours saisis
</span>
)}
<button
onClick={() => setShowSaisieMasse(true)}
disabled={!moisAutorise && !isRH}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
>
<FileText className="w-4 h-4" />
<span className="hidden sm:inline">Saisie en masse</span>
</button>
</div>
</div>
</div>
{/* Calendrier */}
<div className="bg-white rounded-lg border overflow-hidden shadow-sm">
<div className="grid grid-cols-5 gap-2 p-4 bg-gray-50">
{['Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi'].map(day => (
<div key={day} className="text-center font-semibold text-gray-700 text-sm">
{day}
</div>
))}
</div>
<div className="grid grid-cols-5 gap-2 p-4">
{days.map((date, index) => {
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 (
<div
key={index}
onClick={() => 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' :
''
}
>
<div className={`text-right text-sm font-semibold mb-2 flex items-center justify-end gap-1 ${ferie ? 'text-white' :
jourData ? 'text-gray-700' :
'text-gray-700'
}`}>
{date.getDate()}
{jourData && !ferie && !enConge && (
<Lock className="w-3 h-3 text-gray-600" />
)}
</div>
{ferie ? (
<div className="text-center">
<div className="text-xs text-white font-bold truncate">{getHolidayName(date)}</div>
</div>
) : enConge ? (
<div className="text-center">
<div className="text-xs text-purple-700 font-semibold">En congé</div>
</div>
) : jourData ? (
<div className="space-y-1">
<div className="flex items-center gap-1 text-xs">
{jourData.JourTravaille ? (
<Check className="w-3 h-3 text-gray-700" />
) : (
<X className="w-3 h-3 text-gray-700" />
)}
<span className="text-gray-700 truncate">
{jourData.JourTravaille ? 'Travaillé' : 'Non travaillé'}
</span>
</div>
{!jourData.ReposQuotidienRespect && (
<div className="flex items-center gap-1 text-xs text-gray-700">
<AlertCircle className="w-3 h-3" />
<span className="truncate">Repos quotidien</span>
</div>
)}
{!jourData.ReposHebdomadaireRespect && (
<div className="flex items-center gap-1 text-xs text-gray-700">
<AlertCircle className="w-3 h-3" />
<span className="truncate">Repos hebdo</span>
</div>
)}
<div className="text-center text-xs text-gray-600 font-semibold mt-2">
Saisi
</div>
</div>
) : (
<div className="text-center text-xs text-gray-400">
{isPast && moisAutorise ? 'Cliquer pour saisir' : 'Non disponible'}
</div>
)}
</div>
);
})}
</div>
</div>
{/* Légende */}
<div className="mt-4 flex flex-wrap items-center gap-4 text-sm text-gray-600">
<div className="flex items-center gap-2">
<div className="w-4 h-4 bg-cyan-50 border-2 border-cyan-500 rounded"></div>
<span>Aujourd'hui</span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 bg-gray-300 border-2 border-gray-200 rounded"></div>
<Lock className="w-3 h-3 text-gray-600" />
<span>Jour saisi (grisé)</span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 bg-purple-100 border-2 border-gray-200 rounded"></div>
<span>En congé</span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 bg-gray-700 rounded"></div>
<span>Jour férié</span>
</div>
<div className="flex items-center gap-2">
<AlertCircle className="w-4 h-4 text-orange-600" />
<span>Non-respect repos</span>
</div>
</div>
</div>
{/* Modal saisie jour */}
{showSaisieModal && selectedJour && (
<div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
<div className="bg-white rounded-xl shadow-xl max-w-lg w-full p-6 max-h-[90vh] overflow-y-auto">
<h3 className="text-xl font-bold text-gray-900 mb-4">
Saisie du {selectedJour.date.toLocaleDateString('fr-FR', {
weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'
})}
</h3>
<div className="space-y-4">
<div>
<label className="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={selectedJour.jourTravaille}
onChange={(e) => setSelectedJour({ ...selectedJour, jourTravaille: e.target.checked })}
className="w-5 h-5 text-blue-600 rounded"
/>
<span className="text-gray-700 font-medium">Jour travaillé</span>
</label>
</div>
{selectedJour.jourTravaille && (
<>
<div className="border-t pt-4">
<label className="flex items-start gap-3 cursor-pointer">
<input
type="checkbox"
checked={selectedJour.reposQuotidien}
onChange={(e) => setSelectedJour({ ...selectedJour, reposQuotidien: e.target.checked })}
className="w-5 h-5 text-blue-600 rounded mt-0.5"
/>
<div>
<span className="text-gray-700 font-medium block">
Respect du repos quotidien
</span>
<span className="text-xs text-gray-500">
11 heures consécutives minimum
</span>
</div>
</label>
</div>
<div>
<label className="flex items-start gap-3 cursor-pointer">
<input
type="checkbox"
checked={selectedJour.reposHebdo}
onChange={(e) => setSelectedJour({ ...selectedJour, reposHebdo: e.target.checked })}
className="w-5 h-5 text-blue-600 rounded mt-0.5"
/>
<div>
<span className="text-gray-700 font-medium block">
Respect du repos hebdomadaire
</span>
<span className="text-xs text-gray-500">
35 heures consécutives minimum (24h + 11h)
</span>
</div>
</label>
</div>
{(!selectedJour.reposQuotidien || !selectedJour.reposHebdo) && (
<div className="bg-orange-50 border border-orange-200 rounded-lg p-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
Commentaire obligatoire
</label>
<textarea
value={selectedJour.commentaire}
onChange={(e) => setSelectedJour({ ...selectedJour, commentaire: e.target.value })}
placeholder="Précisez les jours/semaines concernés et les raisons..."
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
rows={4}
/>
<p className="text-xs text-orange-700 mt-2">
Veuillez préciser les circonstances du non-respect des repos obligatoires
</p>
</div>
)}
</>
)}
</div>
<div className="flex gap-3 mt-6">
<button
onClick={() => setShowSaisieModal(false)}
className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50"
disabled={isSaving}
>
Annuler
</button>
<button
onClick={handleSaveJour}
disabled={isSaving}
className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 flex items-center justify-center gap-2 disabled:opacity-50"
>
{isSaving ? (
<>
<RefreshCw className="w-4 h-4 animate-spin" />
Enregistrement...
</>
) : (
<>
<Save className="w-4 h-4" />
Enregistrer
</>
)}
</button>
</div>
</div>
</div>
)}
{/* Modal saisie en masse */}
{showSaisieMasse && (
<SaisieMasseModal
mois={mois}
annee={annee}
days={days}
congesData={congesData}
holidays={holidays}
onClose={() => setShowSaisieMasse(false)}
onSave={handleSaisieMasse}
isSaving={isSaving}
/>
)}
</div>
);
};
// Modal de saisie en masse
const SaisieMasseModal = ({ mois, annee, days, congesData, holidays, onClose, onSave, isSaving }) => {
const [selectedDays, setSelectedDays] = useState([]);
const monthNames = ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin',
'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'];
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';
});
};
const isHoliday = (date) => {
return holidays.some(holiday =>
holiday.date.getDate() === date.getDate() &&
holiday.date.getMonth() === date.getMonth() &&
holiday.date.getFullYear() === date.getFullYear()
);
};
const isPastOnly = (date) => {
const today = new Date();
today.setHours(0, 0, 0, 0);
date = new Date(date);
date.setHours(0, 0, 0, 0);
return date < today;
};
const toggleDay = (date) => {
const dateStr = formatDateToString(date);
if (selectedDays.includes(dateStr)) {
setSelectedDays(selectedDays.filter(d => d !== dateStr));
} else {
setSelectedDays([...selectedDays, dateStr]);
}
};
const selectAllWorkingDays = () => {
const workingDays = days
.filter(date => isPastOnly(date) && !isJourEnConge(date) && !isHoliday(date))
.map(date => formatDateToString(date));
setSelectedDays(workingDays);
};
const formatDateToString = (date) => {
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 handleSave = () => {
const joursTravailles = selectedDays.map(dateStr => ({
date: dateStr,
jour_travaille: true,
repos_quotidien: true,
repos_hebdo: true,
commentaire: ''
}));
onSave(joursTravailles);
};
return (
<div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
<div className="bg-white rounded-xl shadow-xl max-w-4xl w-full p-6 max-h-[90vh] overflow-y-auto">
<h3 className="text-xl font-bold text-gray-900 mb-4">
Saisie en masse - {monthNames[mois - 1]} {annee}
</h3>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-4">
<p className="text-sm text-blue-800">
Sélectionnez tous les jours travaillés du mois. Le jour actuel, les jours fériés et les congés sont automatiquement exclus.
Les repos quotidien et hebdomadaire seront considérés comme respectés.
</p>
</div>
<button
onClick={selectAllWorkingDays}
className="mb-4 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Sélectionner tous les jours ouvrés disponibles
</button>
<div className="grid grid-cols-5 gap-2 mb-6">
{days.map((date, index) => {
const dateStr = formatDateToString(date);
const enConge = isJourEnConge(date);
const ferie = isHoliday(date);
const isPast = isPastOnly(date);
const isSelected = selectedDays.includes(dateStr);
const isToday = date.toDateString() === new Date().toDateString();
return (
<div
key={index}
onClick={() => !enConge && !ferie && isPast && toggleDay(date)}
className={`
p-3 rounded-lg border-2 text-center cursor-pointer transition-all
${isToday ? 'border-cyan-500 bg-cyan-100' : ''}
${ferie ? 'bg-gray-700 text-white cursor-not-allowed' : ''}
${enConge ? 'bg-purple-100 cursor-not-allowed' : ''}
${!isPast ? 'opacity-30 cursor-not-allowed' : ''}
${isSelected ? 'bg-green-500 border-green-600 text-white' : 'bg-white border-gray-200 hover:bg-gray-50'}
`}
>
<div className="font-semibold">{date.getDate()}</div>
{isToday && <div className="text-xs mt-1">Aujourd'hui</div>}
{ferie && <div className="text-xs mt-1">Férié</div>}
{enConge && <div className="text-xs mt-1 text-purple-700">Congé</div>}
</div>
);
})}
</div>
<div className="text-sm text-gray-600 mb-4">
{selectedDays.length} jour{selectedDays.length > 1 ? 's' : ''} sélectionné{selectedDays.length > 1 ? 's' : ''}
</div>
<div className="flex gap-3">
<button
onClick={onClose}
className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50"
disabled={isSaving}
>
Annuler
</button>
<button
onClick={handleSave}
disabled={isSaving || selectedDays.length === 0}
className="flex-1 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSaving ? (
<>
<RefreshCw className="w-4 h-4 animate-spin" />
Enregistrement...
</>
) : (
<>
<Save className="w-4 h-4" />
Enregistrer {selectedDays.length} jours
</>
)}
</button>
</div>
</div>
</div>
);
};
export default CompteRenduActivites;