Reapply "V1_Sans_Congé_Anticipéfemini collaboratrice"
This reverts commit 7f15e380e3.
This commit is contained in:
984
project/src/pages/CompteRenduActivite.jsx
Normal file
984
project/src/pages/CompteRenduActivite.jsx
Normal file
@@ -0,0 +1,984 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user