This commit is contained in:
Louis Mazin 2026-09-20 04:41:44 +02:00
parent 503172f537
commit ba5fd9bf85
2 changed files with 167 additions and 160 deletions

View File

@ -12,7 +12,7 @@ import {
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { import {
Alert, Alert,
AlertDescription, AlertDescription,
@ -334,54 +334,106 @@ const TimePicker = ({
onChange, onChange,
placeholder = 'Choisir une heure', placeholder = 'Choisir une heure',
}) => { }) => {
const [open, setOpen] = const [open, setOpen] = useState(false);
useState(false);
const [ const [hours = '', minutes = ''] = value
hours = '',
minutes = '',
] = value
? value.split(':') ? value.split(':')
: []; : [];
const setTimePart = ( const hourOptions = Array.from(
part, { length: 24 },
nextValue, (_, i) => String(i).padStart(2, '0'),
);
const minuteOptions = Array.from(
{ length: 60 },
(_, i) => String(i).padStart(2, '0'),
);
const updateTime = (
nextHours,
nextMinutes,
) => { ) => {
const nextHours =
part === 'hours'
? nextValue
: hours || '00';
const nextMinutes =
part === 'minutes'
? nextValue
: minutes || '00';
onChange( onChange(
`${nextHours}:${nextMinutes}`, `${String(nextHours).padStart(2, '0')}:${String(
nextMinutes,
).padStart(2, '0')}`,
); );
}; };
const hourOptions = const changeHour = (direction) => {
Array.from( const currentHour = Number(hours || 0);
{ length: 24 }, const currentMinute = Number(
(_, index) => minutes || 0,
String(index).padStart(
2,
'0',
),
); );
const minuteOptions = let nextHour =
Array.from( currentHour + direction;
{ length: 60 },
(_, index) => if (nextHour < 0) nextHour = 23;
String(index).padStart( if (nextHour > 23) nextHour = 0;
2,
'0', updateTime(
), nextHour,
currentMinute,
); );
};
const changeMinute = (direction) => {
const currentHour = Number(hours || 0);
const currentMinute = Number(
minutes || 0,
);
let nextMinute =
currentMinute + direction;
let nextHour = currentHour;
if (nextMinute < 0) {
nextMinute = 59;
nextHour =
currentHour === 0
? 23
: currentHour - 1;
}
if (nextMinute > 59) {
nextMinute = 0;
nextHour =
currentHour === 23
? 0
: currentHour + 1;
}
updateTime(
nextHour,
nextMinute,
);
};
const handleWheel = (
event,
type,
) => {
event.preventDefault();
event.stopPropagation();
// Un événement de molette = une seule unité
const direction =
event.deltaY > 0 ? 1 : -1;
if (type === 'hours') {
changeHour(direction);
} else {
changeMinute(direction);
}
};
const clearTime = () => {
onChange('');
setOpen(false);
};
return ( return (
<Popover <Popover
@ -402,7 +454,7 @@ const TimePicker = ({
<span <span
className={ className={
value value
? '' ? 'tabular-nums'
: 'text-muted-foreground' : 'text-muted-foreground'
} }
> >
@ -411,102 +463,106 @@ const TimePicker = ({
</PopoverTrigger> </PopoverTrigger>
<PopoverContent <PopoverContent
className="w-[280px] p-3" className="w-[190px] p-3"
align="start" align="start"
> >
<div className="mb-3 flex items-center justify-between border-b pb-3"> <div className="mb-2 flex items-center justify-between">
<span className="text-sm font-medium"> <span className="text-sm font-medium">
Choisir une heure Choisir une heure
</span> </span>
<span className="rounded-md bg-muted px-2 py-1 text-sm font-medium tabular-nums"> <span className="font-mono text-sm font-semibold tabular-nums">
{value || '--:--'} {value || '--:--'}
</span> </span>
</div> </div>
<div className="grid grid-cols-2 gap-3"> <div className="relative flex h-[144px] gap-2 overflow-hidden">
{/* Sélection centrale */}
<div className="pointer-events-none absolute inset-x-0 top-1/2 z-10 h-9 -translate-y-1/2 rounded-md border bg-muted/50" />
{/* HEURES */} {/* HEURES */}
<div
<div> className="flex-1"
<p className="mb-2 text-xs font-medium text-muted-foreground"> onWheel={(event) =>
Heure handleWheel(
</p> event,
<div className="grid max-h-56 grid-cols-4 gap-1 overflow-y-auto pr-1">
{hourOptions.map(
(hour) => (
<Button
key={hour}
type="button"
size="sm"
variant={
hours === hour
? 'default'
: 'ghost'
}
className="h-8 px-0 tabular-nums"
onClick={() =>
setTimePart(
'hours', 'hours',
hour,
) )
} }
> >
{hour} <div className="flex h-full flex-col items-center justify-center select-none">
</Button> <span className="h-9 text-sm text-muted-foreground/40">
), {hourOptions[
)} (Number(hours || 0) + 23) %
24
]}
</span>
<span className="flex h-9 w-full items-center justify-center rounded-md font-mono text-sm font-semibold tabular-nums">
{hours || '00'}
</span>
<span className="h-9 text-sm text-muted-foreground/40">
{hourOptions[
(Number(hours || 0) + 1) %
24
]}
</span>
</div> </div>
</div> </div>
{/* : */}
<div className="flex items-center justify-center">
<span className="font-bold">
:
</span>
</div>
{/* MINUTES */} {/* MINUTES */}
<div
<div> className="flex-1"
<p className="mb-2 text-xs font-medium text-muted-foreground"> onWheel={(event) =>
Minutes handleWheel(
</p> event,
<div className="grid max-h-56 grid-cols-4 gap-1 overflow-y-auto pr-1">
{minuteOptions.map(
(minute) => (
<Button
key={minute}
type="button"
size="sm"
variant={
minutes === minute
? 'default'
: 'ghost'
}
className="h-8 px-0 tabular-nums"
onClick={() =>
setTimePart(
'minutes', 'minutes',
minute,
) )
} }
> >
{minute} <div className="flex h-full flex-col items-center justify-center select-none">
</Button> <span className="h-9 text-sm text-muted-foreground/40">
), {minuteOptions[
)} (Number(minutes || 0) +
59) %
60
]}
</span>
<span className="flex h-9 w-full items-center justify-center rounded-md font-mono text-sm font-semibold tabular-nums">
{minutes || '00'}
</span>
<span className="h-9 text-sm text-muted-foreground/40">
{minuteOptions[
(Number(minutes || 0) + 1) %
60
]}
</span>
</div>
</div> </div>
</div> </div>
</div> <p className="mt-2 text-center text-[10px] text-muted-foreground">
Molette pour modifier
</p>
<div className="mt-3 border-t pt-3"> <div className="mt-2 border-t pt-2">
<Button <Button
type="button" type="button"
variant="secondary" variant="ghost"
className="w-full" size="sm"
className="h-7 w-full text-xs"
disabled={!value} disabled={!value}
onClick={() => { onClick={clearTime}
onChange('');
setOpen(false);
}}
> >
Effacer Effacer
</Button> </Button>

View File

@ -42,16 +42,6 @@ const MOBILE_TIME_COLUMN_WIDTH = 64;
const MOBILE_USER_COLUMN_WIDTH = 40; const MOBILE_USER_COLUMN_WIDTH = 40;
const MOBILE_DEFAULT_CONTENT_WIDTH = 236; const MOBILE_DEFAULT_CONTENT_WIDTH = 236;
const USER_COLORS = [
'#c56a3d',
'#3d7c8c',
'#8a5a9e',
'#5d8b55',
'#c08a32',
'#b04f66',
'#4772a8',
];
const expandOvernightAttendances = (entries) => { const expandOvernightAttendances = (entries) => {
return entries.map((entry) => { return entries.map((entry) => {
if (entry.start_time && !entry.end_time) { if (entry.start_time && !entry.end_time) {
@ -241,41 +231,6 @@ const WeeklySummaryPage = ({ userId }) => {
}; };
}; };
const getUserColor = (user) => {
if (user?.color) {
return user.color;
}
const id = getUserId(user);
if (id) {
const numericId = Number(id);
if (!Number.isNaN(numericId)) {
return USER_COLORS[
numericId % USER_COLORS.length
];
}
}
const name =
user?.first_name ||
user?.firstName ||
user?.username ||
user?.name ||
'';
const hash = [...String(name)].reduce(
(total, character) =>
total + character.charCodeAt(0),
0,
);
return USER_COLORS[
hash % USER_COLORS.length
];
};
const loadAttendances = async () => { const loadAttendances = async () => {
try { try {
setLoading(true); setLoading(true);
@ -1214,7 +1169,7 @@ const WeeklySummaryPage = ({ userId }) => {
).map((user) => ({ ).map((user) => ({
id: getUserId(user), id: getUserId(user),
label: getUserLabel(user), label: getUserLabel(user),
color: getUserColor(user), color: user.color,
})); }));
return ( return (
@ -1599,10 +1554,7 @@ const WeeklySummaryPage = ({ userId }) => {
user, user,
); );
const color = const color = user.color;
getUserColor(
user,
);
return ( return (
<div <div
@ -1720,6 +1672,7 @@ const WeeklySummaryPage = ({ userId }) => {
style={ style={
style style
} }
title={`${formatSegmentTime(start)} - ${formatSegmentTime(end)}`}
> >
<div className="p-1 text-xs text-white"> <div className="p-1 text-xs text-white">
{formatSegmentTime( {formatSegmentTime(
@ -1873,10 +1826,7 @@ const WeeklySummaryPage = ({ userId }) => {
user, user,
); );
const color = const color = user.color;
getUserColor(
user,
);
return ( return (
<div <div
@ -1993,6 +1943,7 @@ const WeeklySummaryPage = ({ userId }) => {
style={ style={
style style
} }
title={`${formatSegmentTime(start)} - ${formatSegmentTime(end)}`}
> >
<div className="p-2 text-xs text-white"> <div className="p-2 text-xs text-white">
<div className="font-medium"> <div className="font-medium">