import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; import { timeApi } from '../api/time'; import { useAuth } from '../context/useAuth'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Calendar, ChevronLeft, ChevronRight, Clock, Users, User, CalendarDays, } from 'lucide-react'; const DEFAULT_START_HOUR = 8; const DEFAULT_END_HOUR = 18; const HOUR_HEIGHT = 64; const USER_COLORS = [ '#c56a3d', '#3d7c8c', '#8a5a9e', '#5d8b55', '#c08a32', '#b04f66', '#4772a8', ]; const expandOvernightAttendances = (entries) => { return entries.map((entry) => { if (entry.start_time && !entry.end_time) { return { ...entry, end_time: '24:00:00' }; } if (!entry.start_time && entry.end_time) { return { ...entry, start_time: '00:00:00' }; } return entry; }); }; const WeeklySummaryPage = ({ userId }) => { const { isAdmin } = useAuth(); const [attendances, setAttendances] = useState([]); const [currentWeek, setCurrentWeek] = useState(new Date()); const [selectedDayIndex, setSelectedDayIndex] = useState(() => { const day = new Date().getDay(); return day === 0 ? 6 : day - 1; }); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [viewportHeight, setViewportHeight] = useState(() => window.innerHeight); const [availableScheduleHeight, setAvailableScheduleHeight] = useState(620); const [isMobile, setIsMobile] = useState(false); const scheduleTableRef = useRef(null); useEffect(() => { const updateViewportHeight = () => { setViewportHeight(window.innerHeight); }; const checkMobile = () => { setIsMobile(window.innerWidth < 768); }; updateViewportHeight(); checkMobile(); window.addEventListener('resize', updateViewportHeight); window.addEventListener('resize', checkMobile); return () => { window.removeEventListener('resize', updateViewportHeight); window.removeEventListener('resize', checkMobile); }; }, []); useLayoutEffect(() => { const updateScheduleHeight = () => { const scheduleTable = scheduleTableRef.current; const main = document.querySelector('main'); const footer = document.querySelector('footer'); if (!scheduleTable || !main || !footer) { return; } const mainStyles = window.getComputedStyle(main); const bottomPadding = parseFloat(mainStyles.paddingBottom) || 0; const footerHeight = footer.getBoundingClientRect().height; const tableTop = scheduleTable.getBoundingClientRect().top; const tableHeaderHeight = 58; const remainingHeight = window.innerHeight - tableTop - footerHeight - bottomPadding - tableHeaderHeight; setAvailableScheduleHeight(Math.max(180, remainingHeight)); }; updateScheduleHeight(); window.addEventListener('resize', updateScheduleHeight); return () => { window.removeEventListener('resize', updateScheduleHeight); }; }, [loading, error]); useEffect(() => { const loadAttendances = async () => { try { setLoading(true); setError(''); const entries = await timeApi.getTimeEntries(); setAttendances(entries); } catch (loadError) { setError('Impossible de charger les pointages.'); console.error(loadError); } finally { setLoading(false); } }; loadAttendances(); }, [userId]); const getWeekDays = (date) => { const startOfWeek = new Date(date); const day = startOfWeek.getDay() || 7; startOfWeek.setDate(startOfWeek.getDate() - day + 1); return Array.from({ length: 7 }, (_, index) => { const weekDay = new Date(startOfWeek); weekDay.setDate(startOfWeek.getDate() + index); return weekDay; }); }; const formatDateKey = (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 getTimeValue = (attendance, key) => { const legacyKey = key === 'start_time' ? 'startTime' : 'endTime'; const value = attendance[key] || attendance[legacyKey] || ''; return value.slice(0, 5); }; const getMinutes = (time) => { if (!time) { return null; } const [hours, minutes] = time.split(':').map(Number); if (Number.isNaN(hours) || Number.isNaN(minutes)) { return null; } return (hours * 60) + minutes; }; const formatSegmentTime = (minutes) => { if (minutes === 1440) { return '23h59'; } return `${String(Math.floor(minutes / 60)).padStart(2, '0')}h${String( minutes % 60, ).padStart(2, '0')}`; }; const getUserColor = (username) => { const hash = [...username].reduce( (total, character) => total + character.charCodeAt(0), 0, ); return USER_COLORS[hash % USER_COLORS.length]; }; const getAttendancesForDay = (date) => { const dateKey = formatDateKey(date); return expandOvernightAttendances(attendances).filter( (attendance) => String(attendance.date).slice(0, 10) === dateKey, ); }; const getAttendanceSegments = (dayAttendances) => { const validAttendances = dayAttendances .map((attendance) => ({ attendance, start: getMinutes( getTimeValue(attendance, 'start_time'), ), end: getMinutes( getTimeValue(attendance, 'end_time'), ), })) .filter( ({ start, end }) => start !== null && end !== null && end > start, ); const boundaries = [ ...new Set( validAttendances.flatMap(({ start, end }) => [ start, end, ]), ), ].sort((a, b) => a - b); return boundaries .slice(0, -1) .map((start, index) => { const end = boundaries[index + 1]; const activeAttendances = validAttendances.filter( ({ start: entryStart, end: entryEnd, }) => entryStart <= start && entryEnd >= end, ); if (!activeAttendances.length) { return null; } return { start, end, users: activeAttendances .map(({ attendance }) => attendance.username) .filter(Boolean) .filter( (username, usernameIndex, names) => names.indexOf(username) === usernameIndex, ) .map((username) => ({ username, color: getUserColor(username), })), }; }) .filter(Boolean); }; const getAttendanceColumns = (dayAttendances) => { const columns = new Map(); dayAttendances.forEach((attendance) => { const start = getMinutes( getTimeValue(attendance, 'start_time'), ); const end = getMinutes( getTimeValue(attendance, 'end_time'), ); const username = attendance.username; if ( start === null || end === null || end <= start || !username ) { return; } const currentColumn = columns.get(username); columns.set(username, { username, color: getUserColor(username), start: currentColumn ? Math.min(currentColumn.start, start) : start, end: currentColumn ? Math.max(currentColumn.end, end) : end, }); }); return [...columns.values()]; }; const navigateWeek = (direction) => { const newWeek = new Date(currentWeek); newWeek.setDate( currentWeek.getDate() + direction * 7, ); setCurrentWeek(newWeek); }; const navigateDay = (direction) => { const nextDayIndex = selectedDayIndex + direction; if ( nextDayIndex < 0 || nextDayIndex > 6 ) { navigateWeek(direction); setSelectedDayIndex( direction < 0 ? 6 : 0, ); return; } setSelectedDayIndex(nextDayIndex); }; const getScheduleBounds = (weekDays) => { const weekDateKeys = new Set( weekDays.map(formatDateKey), ); const weekAttendances = expandOvernightAttendances(attendances).filter( (attendance) => weekDateKeys.has( String(attendance.date).slice(0, 10), ), ); const times = weekAttendances .flatMap((attendance) => [ getMinutes( getTimeValue( attendance, 'start_time', ), ), getMinutes( getTimeValue( attendance, 'end_time', ), ), ]) .filter((time) => time !== null); if (!times.length) { return { startHour: DEFAULT_START_HOUR, endHour: DEFAULT_END_HOUR, }; } const firstHour = Math.floor( Math.min(...times) / 60, ); const lastHour = Math.ceil( Math.max(...times) / 60, ); return { startHour: firstHour, endHour: Math.max( lastHour, firstHour + 1, ), }; }; const getAttendanceStyle = ( start, end, startHour, endHour, hourHeight, ) => { const visibleStart = Math.max( startHour * 60, start, ); const visibleEnd = Math.min( endHour * 60, end, ); return { top: `${ ((visibleStart - startHour * 60) / 60) * hourHeight }px`, height: `${Math.max( ((visibleEnd - visibleStart) / 60) * hourHeight, 1, )}px`, }; }; if (loading) { return (
Chargement...
); } if (error) { return (

{error}

); } const weekDays = getWeekDays(currentWeek); const { startHour, endHour } = getScheduleBounds(weekDays); const hourCount = endHour - startHour; const measuredHeight = Math.min( availableScheduleHeight, Math.max(180, viewportHeight - 180), ); const scheduleHeight = Math.max( 180, measuredHeight, ); const hourHeight = Math.min( HOUR_HEIGHT, Math.max( 12, scheduleHeight / hourCount, ), ); const hours = Array.from( { length: hourCount }, (_, index) => startHour + index, ); const formatHour = (hour) => `${String(hour).padStart(2, '0')}:00`; const legendUsers = [ ...new Set( attendances .map((attendance) => attendance.username) .filter(Boolean), ), ] .sort((first, second) => first.localeCompare(second, 'fr'), ) .map((username) => ({ username, color: getUserColor(username), })); const displayDay = isMobile ? weekDays[selectedDayIndex] : null; return (
{isAdmin ? ( <> Récapitulatif de tous les utilisateurs ) : ( <> Emploi du temps hebdomadaire )}
{!isMobile && (
Du{' '} {weekDays[0].toLocaleDateString( 'fr-FR', { day: '2-digit', month: 'short', }, )}{' '} au{' '} {weekDays[6].toLocaleDateString( 'fr-FR', { day: '2-digit', month: 'short', }, )}
)} {isMobile && (
{displayDay.toLocaleDateString( 'fr-FR', { weekday: 'long', day: '2-digit', month: 'short', }, )}
)}
{isAdmin && legendUsers.length > 0 && (
Légende : {legendUsers.map( ({ username, color }) => (
{username}
), )}
)}
{isMobile ? (
{displayDay.toLocaleDateString( 'fr-FR', { weekday: 'long', day: '2-digit', month: 'short', }, )}
{hours.map((hour) => (
{formatHour(hour)}
))}
{hours.map((hour) => (
))} {isAdmin ? getAttendanceColumns( getAttendancesForDay( displayDay, ), ).map( ({ start, end, username, color, }, columnIndex) => { const style = getAttendanceStyle( start, end, startHour, endHour, hourHeight, ); return (
); }, ) : getAttendanceSegments( getAttendancesForDay( displayDay, ), ).map( ({ start, end, users, }) => { const style = getAttendanceStyle( start, end, startHour, endHour, hourHeight, ); const segmentLabel = users .map( ({ username, }) => username, ) .join(', '); return (
{formatSegmentTime( start, )}{' '} -{' '} {formatSegmentTime( end, )}
); }, )}
) : (
{/* En-tête */}
{weekDays.map((day, dayIndex) => (
{day.toLocaleDateString( 'fr-FR', { weekday: 'long', }, )}
{day.toLocaleDateString( 'fr-FR', { day: '2-digit', month: 'short', }, )}
))} {/* Heures */}
{hours.map((hour) => (
{formatHour(hour)}
))}
{/* Jours */} {weekDays.map((day) => { const dayAttendances = getAttendancesForDay(day); return (
{hours.map((hour) => (
))} {isAdmin ? getAttendanceColumns( dayAttendances, ).map( ({ start, end, username, color, }) => { const style = getAttendanceStyle( start, end, startHour, endHour, hourHeight, ); return (
{username}
{formatSegmentTime( start, )}{' '} -{' '} {formatSegmentTime( end, )}
); }, ) : getAttendanceSegments( dayAttendances, ).map( ({ start, end, users, }) => { const style = getAttendanceStyle( start, end, startHour, endHour, hourHeight, ); const segmentLabel = users .map( ({ username, }) => username, ) .join(', '); return (
{formatSegmentTime( start, )}{' '} -{' '} {formatSegmentTime( end, )}
{isAdmin && users.length > 0 && (
{segmentLabel}
)}
); }, )}
); })}
)}
); }; export default WeeklySummaryPage;