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 && (
+ Premier du mois {displayName(winner)} {formatMinutes(winner.total_minutes)} {displayName(participant)} {formatMinutes(participant.total_minutes)}Podium
+
- {personLabel(participant)} +
+ {name}
Premier du mois
-{personLabel(winner)}
-{formatMinutes(winner.total_minutes)}
-{personLabel(participant)}
-{formatMinutes(participant.total_minutes)}
-