From aa3b5e6ae3c33a4a09739fa69ac535631e4e180a Mon Sep 17 00:00:00 2001 From: Louis Mazin Date: Mon, 5 Oct 2026 19:04:11 +0200 Subject: [PATCH] =?UTF-8?q?l=C3=A9ch=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../statistics/MonthlyHoursRace.jsx | 140 ++++++++++++------ 1 file changed, 92 insertions(+), 48 deletions(-) diff --git a/frontend/src/components/statistics/MonthlyHoursRace.jsx b/frontend/src/components/statistics/MonthlyHoursRace.jsx index 3639a2b..245ab2d 100644 --- a/frontend/src/components/statistics/MonthlyHoursRace.jsx +++ b/frontend/src/components/statistics/MonthlyHoursRace.jsx @@ -8,7 +8,7 @@ import { DialogTitle, } from '@/components/ui/dialog'; import { timeApi } from '@/api/time'; -import { formatMinutes, personLabel } from '@/components/planning/utils'; +import { formatMinutes } from '@/components/planning/utils'; const RACE_DURATION_MS = 8000; @@ -30,6 +30,14 @@ const dateLabel = (value) => { const clamp = (value, min, max) => Math.min(max, Math.max(min, value)); +const firstNameLabel = (participant) => { + const firstName = String(participant?.first_name || '').trim(); + if (firstName) return firstName; + + const username = String(participant?.username || '').trim(); + return username || 'Utilisateur'; +}; + export default function MonthlyHoursRace() { const [open, setOpen] = useState(false); const [race, setRace] = useState(null); @@ -40,6 +48,40 @@ export default function MonthlyHoursRace() { const frameRef = useRef(null); const replayFrameRef = useRef(null); + const participants = useMemo( + () => (race?.participants || []).filter((participant) => Number(participant.total_minutes || 0) > 0), + [race], + ); + + const participantLabels = useMemo(() => { + const firstNameCounts = new Map(); + + participants.forEach((participant) => { + const firstName = firstNameLabel(participant); + const key = firstName.toLocaleLowerCase('fr-FR'); + firstNameCounts.set(key, (firstNameCounts.get(key) || 0) + 1); + }); + + return new Map(participants.map((participant) => { + const firstName = firstNameLabel(participant); + const key = firstName.toLocaleLowerCase('fr-FR'); + let label = firstName; + + if ((firstNameCounts.get(key) || 0) > 1) { + const lastName = String(participant.last_name || '').trim(); + const initial = lastName.charAt(0).toLocaleUpperCase('fr-FR'); + if (initial) label = `${firstName} ${initial}.`; + } + + return [String(participant.id), label]; + })); + }, [participants]); + + const displayName = useCallback( + (participant) => participantLabels.get(String(participant?.id)) || firstNameLabel(participant), + [participantLabels], + ); + const loadRace = useCallback(async () => { setLoading(true); setError(''); @@ -90,7 +132,7 @@ export default function MonthlyHoursRace() { }; useEffect(() => { - if (!open || !race || loading || !race.participants?.length) return undefined; + if (!open || !race || loading || !participants.length) return undefined; let startTime = null; const reduceMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches; @@ -116,9 +158,8 @@ export default function MonthlyHoursRace() { if (frameRef.current) cancelAnimationFrame(frameRef.current); frameRef.current = null; }; - }, [open, race, loading, runId]); + }, [open, race, loading, runId, participants.length]); - const participants = race?.participants || []; const daysCount = race?.days_count || participants[0]?.daily_minutes?.length || 0; const maxFinalMinutes = useMemo( () => Math.max(0, ...participants.map((row) => Number(row.total_minutes || 0))), @@ -126,8 +167,8 @@ export default function MonthlyHoursRace() { ); const laneRows = useMemo(() => ( - [...participants].sort((a, b) => personLabel(a).localeCompare(personLabel(b), 'fr', { sensitivity: 'base' })) - ), [participants]); + [...participants].sort((a, b) => displayName(a).localeCompare(displayName(b), 'fr', { sensitivity: 'base' })) + ), [participants, displayName]); const currentState = useMemo(() => { if (!daysCount) return { dayIndex: 0, dayFraction: 0, currentDate: race?.start || null }; @@ -160,6 +201,43 @@ export default function MonthlyHoursRace() { const podium = [...participants].sort((a, b) => Number(a.rank) - Number(b.rank)).slice(0, 3); const winner = podium[0]; + const finalRanking = finished && podium.length > 0 && ( +
+ {winner && ( +
+ + + +

Premier du mois

+

{displayName(winner)}

+

{formatMinutes(winner.total_minutes)}

+
+ )} + +
+
+
+ {[1, 0, 2].map((index) => { + const participant = podium[index]; + if (!participant) return
; + const rank = index + 1; + const height = rank === 1 ? 'min-h-36' : rank === 2 ? 'min-h-28' : 'min-h-24'; + return ( +
+ +

{displayName(participant)}

+

{formatMinutes(participant.total_minutes)}

+
+ ); + })} +
+
+ ); + return (
+ {finalRanking} +
{animatedRows.map((participant) => { const barHeight = maxFinalMinutes > 0 ? clamp((participant.animatedMinutes / maxFinalMinutes) * 100, 0, 100) : 0; + const name = displayName(participant); return (
{formatMinutes(Math.round(participant.animatedMinutes))} @@ -232,8 +313,8 @@ export default function MonthlyHoursRace() {
-

- {personLabel(participant)} +

+ {name}

@@ -241,47 +322,10 @@ export default function MonthlyHoursRace() { })}
- - {finished && podium.length > 0 && ( -
- {winner && Number(winner.total_minutes || 0) > 0 && ( -
- - - -

Premier du mois

-

{personLabel(winner)}

-

{formatMinutes(winner.total_minutes)}

-
- )} - -
-
-
- {[1, 0, 2].map((index) => { - const participant = podium[index]; - if (!participant) return
; - const rank = index + 1; - const height = rank === 1 ? 'min-h-36' : rank === 2 ? 'min-h-28' : 'min-h-24'; - return ( -
- -

{personLabel(participant)}

-

{formatMinutes(participant.total_minutes)}

-
- ); - })} -
-
- )} )} -
+
{race && participants.length > 0 && maxFinalMinutes > 0 && !loading && !error && (