1025 lines
29 KiB
JavaScript
1025 lines
29 KiB
JavaScript
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 (
|
|
<Card className="page-layout page-state weekly-schedule flex h-full w-full">
|
|
<CardContent className="p-6">
|
|
<div className="flex items-center justify-center h-32">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500" />
|
|
<span className="ml-3">
|
|
Chargement...
|
|
</span>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<Card className="page-layout page-state weekly-schedule schedule-error flex h-full w-full">
|
|
<CardContent className="p-6">
|
|
<div className="text-red-500 text-center">
|
|
<div className="flex items-center justify-center mb-2">
|
|
<div className="bg-red-100 p-2 rounded-full">
|
|
<div className="bg-red-500 text-white p-1 rounded-full">
|
|
<div className="w-4 h-4" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<p>{error}</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
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 (
|
|
<Card
|
|
className={`flex h-full w-full weekly-schedule page-layout${
|
|
isAdmin ? ' multi-user-schedule' : ''
|
|
}`}
|
|
>
|
|
<CardHeader className="border-b">
|
|
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
|
<div>
|
|
<CardTitle className="flex items-center gap-2">
|
|
{isAdmin ? (
|
|
<>
|
|
<Users className="h-5 w-5" />
|
|
Récapitulatif de tous les utilisateurs
|
|
</>
|
|
) : (
|
|
<>
|
|
<CalendarDays className="h-5 w-5" />
|
|
Emploi du temps hebdomadaire
|
|
</>
|
|
)}
|
|
</CardTitle>
|
|
</div>
|
|
|
|
{!isMobile && (
|
|
<div className="flex items-center gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => navigateWeek(-1)}
|
|
className="flex items-center gap-1"
|
|
>
|
|
<ChevronLeft className="h-4 w-4" />
|
|
Précédente
|
|
</Button>
|
|
|
|
<Badge
|
|
variant="secondary"
|
|
className="text-sm"
|
|
>
|
|
Du{' '}
|
|
{weekDays[0].toLocaleDateString(
|
|
'fr-FR',
|
|
{
|
|
day: '2-digit',
|
|
month: 'short',
|
|
},
|
|
)}{' '}
|
|
au{' '}
|
|
{weekDays[6].toLocaleDateString(
|
|
'fr-FR',
|
|
{
|
|
day: '2-digit',
|
|
month: 'short',
|
|
},
|
|
)}
|
|
</Badge>
|
|
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => navigateWeek(1)}
|
|
className="flex items-center gap-1"
|
|
>
|
|
Suivante
|
|
<ChevronRight className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
{isMobile && (
|
|
<div className="flex items-center justify-between gap-2 pt-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => navigateDay(-1)}
|
|
>
|
|
<ChevronLeft className="h-4 w-4" />
|
|
</Button>
|
|
|
|
<Badge
|
|
variant="secondary"
|
|
className="text-sm"
|
|
>
|
|
{displayDay.toLocaleDateString(
|
|
'fr-FR',
|
|
{
|
|
weekday: 'long',
|
|
day: '2-digit',
|
|
month: 'short',
|
|
},
|
|
)}
|
|
</Badge>
|
|
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => navigateDay(1)}
|
|
>
|
|
<ChevronRight className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</CardHeader>
|
|
|
|
<CardContent className="p-4">
|
|
{isAdmin && legendUsers.length > 0 && (
|
|
<div className="mb-4 p-3 bg-muted rounded-lg">
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<span className="text-sm font-medium mr-2">
|
|
Légende :
|
|
</span>
|
|
|
|
{legendUsers.map(
|
|
({ username, color }) => (
|
|
<div
|
|
key={username}
|
|
className="flex items-center gap-1"
|
|
>
|
|
<div
|
|
className="w-3 h-3 rounded-full"
|
|
style={{
|
|
backgroundColor: color,
|
|
}}
|
|
/>
|
|
|
|
<span className="text-sm">
|
|
{username}
|
|
</span>
|
|
</div>
|
|
),
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div
|
|
ref={scheduleTableRef}
|
|
className="relative overflow-auto border rounded-lg"
|
|
style={{
|
|
height: `${Math.max(
|
|
180,
|
|
Math.min(
|
|
availableScheduleHeight,
|
|
viewportHeight - 180,
|
|
),
|
|
)}px`,
|
|
}}
|
|
>
|
|
{isMobile ? (
|
|
<div className="grid grid-cols-1 gap-0 min-w-[300px]">
|
|
<div className="p-2 text-center font-medium border-b sticky top-0 bg-background z-10">
|
|
{displayDay.toLocaleDateString(
|
|
'fr-FR',
|
|
{
|
|
weekday: 'long',
|
|
day: '2-digit',
|
|
month: 'short',
|
|
},
|
|
)}
|
|
</div>
|
|
|
|
<div className="grid grid-cols-[64px_1fr] gap-0">
|
|
<div className="border-r">
|
|
{hours.map((hour) => (
|
|
<div
|
|
key={hour}
|
|
className="flex items-center justify-center text-xs border-b"
|
|
style={{
|
|
height: `${hourHeight}px`,
|
|
}}
|
|
>
|
|
{formatHour(hour)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div
|
|
className="relative"
|
|
style={{
|
|
height: `${hourCount * hourHeight}px`,
|
|
}}
|
|
>
|
|
{hours.map((hour) => (
|
|
<div
|
|
key={hour}
|
|
className="border-b border-gray-200"
|
|
style={{
|
|
height: `${hourHeight}px`,
|
|
}}
|
|
/>
|
|
))}
|
|
|
|
{isAdmin
|
|
? getAttendanceColumns(
|
|
getAttendancesForDay(
|
|
displayDay,
|
|
),
|
|
).map(
|
|
({
|
|
start,
|
|
end,
|
|
username,
|
|
color,
|
|
}, columnIndex) => {
|
|
const style =
|
|
getAttendanceStyle(
|
|
start,
|
|
end,
|
|
startHour,
|
|
endHour,
|
|
hourHeight,
|
|
);
|
|
|
|
return (
|
|
<div
|
|
key={`${formatDateKey(
|
|
displayDay,
|
|
)}-${username}`}
|
|
className="absolute rounded-sm"
|
|
style={{
|
|
...style,
|
|
left: `${
|
|
5 +
|
|
columnIndex * 9
|
|
}px`,
|
|
width: '6px',
|
|
backgroundColor:
|
|
color,
|
|
}}
|
|
title={`${username} : ${formatSegmentTime(
|
|
start,
|
|
)} - ${formatSegmentTime(
|
|
end,
|
|
)}`}
|
|
/>
|
|
);
|
|
},
|
|
)
|
|
: getAttendanceSegments(
|
|
getAttendancesForDay(
|
|
displayDay,
|
|
),
|
|
).map(
|
|
({
|
|
start,
|
|
end,
|
|
users,
|
|
}) => {
|
|
const style =
|
|
getAttendanceStyle(
|
|
start,
|
|
end,
|
|
startHour,
|
|
endHour,
|
|
hourHeight,
|
|
);
|
|
|
|
const segmentLabel =
|
|
users
|
|
.map(
|
|
({
|
|
username,
|
|
}) => username,
|
|
)
|
|
.join(', ');
|
|
|
|
return (
|
|
<div
|
|
key={`${formatDateKey(
|
|
displayDay,
|
|
)}-${start}-${end}`}
|
|
className="absolute bg-blue-500/80 rounded-sm border border-blue-600/30 left-0 right-0"
|
|
style={style}
|
|
title={segmentLabel}
|
|
>
|
|
<div className="p-1 text-xs text-white">
|
|
{formatSegmentTime(
|
|
start,
|
|
)}{' '}
|
|
-{' '}
|
|
{formatSegmentTime(
|
|
end,
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
},
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div
|
|
className="grid min-w-[900px]"
|
|
style={{
|
|
gridTemplateColumns: `64px repeat(7, minmax(120px, 1fr))`,
|
|
}}
|
|
>
|
|
{/* En-tête */}
|
|
<div className="sticky top-0 z-20 bg-background border-b border-r p-2" />
|
|
|
|
{weekDays.map((day, dayIndex) => (
|
|
<div
|
|
key={formatDateKey(day)}
|
|
className={`sticky top-0 z-20 bg-background border-b border-r p-2 text-center font-medium ${
|
|
dayIndex === selectedDayIndex
|
|
? 'bg-muted'
|
|
: ''
|
|
}`}
|
|
>
|
|
<div className="text-sm capitalize">
|
|
{day.toLocaleDateString(
|
|
'fr-FR',
|
|
{
|
|
weekday: 'long',
|
|
},
|
|
)}
|
|
</div>
|
|
|
|
<div className="text-xs text-muted-foreground">
|
|
{day.toLocaleDateString(
|
|
'fr-FR',
|
|
{
|
|
day: '2-digit',
|
|
month: 'short',
|
|
},
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{/* Heures */}
|
|
<div className="border-r">
|
|
{hours.map((hour) => (
|
|
<div
|
|
key={hour}
|
|
className="flex items-start justify-center pt-1 text-xs text-muted-foreground border-b"
|
|
style={{
|
|
height: `${hourHeight}px`,
|
|
}}
|
|
>
|
|
{formatHour(hour)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Jours */}
|
|
{weekDays.map((day) => {
|
|
const dayAttendances =
|
|
getAttendancesForDay(day);
|
|
|
|
return (
|
|
<div
|
|
key={formatDateKey(day)}
|
|
className="relative border-r"
|
|
style={{
|
|
height: `${hourCount * hourHeight}px`,
|
|
}}
|
|
>
|
|
{hours.map((hour) => (
|
|
<div
|
|
key={hour}
|
|
className="border-b border-gray-200"
|
|
style={{
|
|
height: `${hourHeight}px`,
|
|
}}
|
|
/>
|
|
))}
|
|
|
|
{isAdmin
|
|
? getAttendanceColumns(
|
|
dayAttendances,
|
|
).map(
|
|
({
|
|
start,
|
|
end,
|
|
username,
|
|
color,
|
|
}) => {
|
|
const style =
|
|
getAttendanceStyle(
|
|
start,
|
|
end,
|
|
startHour,
|
|
endHour,
|
|
hourHeight,
|
|
);
|
|
|
|
return (
|
|
<div
|
|
key={`${formatDateKey(
|
|
day,
|
|
)}-${username}`}
|
|
className="absolute left-2 right-2 rounded-md border"
|
|
style={{
|
|
...style,
|
|
backgroundColor: `${color}22`,
|
|
borderColor: color,
|
|
}}
|
|
title={`${username} : ${formatSegmentTime(
|
|
start,
|
|
)} - ${formatSegmentTime(
|
|
end,
|
|
)}`}
|
|
>
|
|
<div
|
|
className="absolute left-0 top-0 bottom-0 w-1 rounded-l-md"
|
|
style={{
|
|
backgroundColor:
|
|
color,
|
|
}}
|
|
/>
|
|
|
|
<div className="px-3 py-1 text-xs">
|
|
<div
|
|
className="font-medium"
|
|
style={{
|
|
color,
|
|
}}
|
|
>
|
|
{username}
|
|
</div>
|
|
|
|
<div className="text-muted-foreground">
|
|
{formatSegmentTime(
|
|
start,
|
|
)}{' '}
|
|
-{' '}
|
|
{formatSegmentTime(
|
|
end,
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
},
|
|
)
|
|
: getAttendanceSegments(
|
|
dayAttendances,
|
|
).map(
|
|
({
|
|
start,
|
|
end,
|
|
users,
|
|
}) => {
|
|
const style =
|
|
getAttendanceStyle(
|
|
start,
|
|
end,
|
|
startHour,
|
|
endHour,
|
|
hourHeight,
|
|
);
|
|
|
|
const segmentLabel =
|
|
users
|
|
.map(
|
|
({
|
|
username,
|
|
}) =>
|
|
username,
|
|
)
|
|
.join(', ');
|
|
|
|
return (
|
|
<div
|
|
key={`${formatDateKey(
|
|
day,
|
|
)}-${start}-${end}`}
|
|
className="absolute left-1 right-1 bg-blue-500/80 rounded-md border border-blue-600/30 overflow-hidden"
|
|
style={style}
|
|
title={segmentLabel}
|
|
>
|
|
<div className="p-2 text-xs text-white">
|
|
<div className="font-medium">
|
|
{formatSegmentTime(
|
|
start,
|
|
)}{' '}
|
|
-{' '}
|
|
{formatSegmentTime(
|
|
end,
|
|
)}
|
|
</div>
|
|
|
|
{isAdmin &&
|
|
users.length >
|
|
0 && (
|
|
<div className="mt-1">
|
|
{segmentLabel}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
},
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
};
|
|
|
|
export default WeeklySummaryPage;
|