- Redesign session header: title on top, badge + date/map/scenario below, participants aligned - Add 3-column layout for standard sessions: KPIs | Global+Personal Stats | BarChart - FireRange/LongRange: per-variant target sizing (human=320px, longRange=480px) - Challenge: hide target and objectives, show reaction time chart - Add TargetVisualization component with SVG hit markers - Compute success/failed from debrief data (civilKilled, policeKilled, hitsReceived) - Apply computed success across Dashboard, Sessions list, UserDetail, SessionDetail - Add useComputedSuccess hook for batch debrief loading - Unified participant combo box in session header with auto-select for single player - Dark theme: lighter dropdown arrows, brighter muted text, larger ScoreBadge - Add i18n keys for new stats labels (FR/EN) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
61 lines
2.4 KiB
TypeScript
61 lines
2.4 KiB
TypeScript
import { useState, useEffect, useRef } from 'react';
|
|
import { getDebrief } from '../api/client';
|
|
import type { Session, DebriefRow } from '../types';
|
|
|
|
/**
|
|
* Computes success/failure for a list of sessions based on debrief data.
|
|
* A session is failed if any: civilKilled > 0, policeKilled > 0, or hitsReceived > 0.
|
|
* Only fetches debriefs for the given sessions (use with paginated/visible sessions).
|
|
* Returns a Map<sessionId, computedSuccess>.
|
|
*/
|
|
export function useComputedSuccess(sessions: Session[]): Map<number, boolean> {
|
|
const [successMap, setSuccessMap] = useState<Map<number, boolean>>(new Map());
|
|
const fetchedRef = useRef<Set<number>>(new Set());
|
|
|
|
useEffect(() => {
|
|
if (sessions.length === 0) return;
|
|
|
|
// Only fetch sessions we haven't already fetched
|
|
const toFetch = sessions.filter(s => !fetchedRef.current.has(s.id));
|
|
if (toFetch.length === 0) return;
|
|
|
|
Promise.all(
|
|
toFetch.map(async (session) => {
|
|
try {
|
|
const debrief = await getDebrief(session.id);
|
|
return { id: session.id, success: computeSuccess(session.success, debrief) };
|
|
} catch {
|
|
return { id: session.id, success: session.success };
|
|
}
|
|
}),
|
|
).then((results) => {
|
|
setSuccessMap(prev => {
|
|
const next = new Map(prev);
|
|
for (const r of results) {
|
|
next.set(r.id, r.success);
|
|
fetchedRef.current.add(r.id);
|
|
}
|
|
return next;
|
|
});
|
|
});
|
|
}, [sessions]);
|
|
|
|
return successMap;
|
|
}
|
|
|
|
/** Compute success from debrief rows */
|
|
export function computeSuccess(originalSuccess: boolean, debrief: DebriefRow[]): boolean {
|
|
const totalCivilKilled = debrief.reduce((s, r) => s + (Number(r.totalCivilKilled) || 0), 0);
|
|
const totalPoliceKilled = debrief.reduce((s, r) => s + (Number(r.totalPoliceKilled) || 0), 0);
|
|
const totalHitsReceived = debrief.reduce(
|
|
(s, r) => s + (Number(r.nbReceivedHitsFromEnemyIA) || 0) + (Number(r.nbReceivedHitsFromEnemyUser) || 0) + (Number(r.nbReceivedHitsFromPoliceUser) || 0),
|
|
0,
|
|
);
|
|
return originalSuccess && totalCivilKilled === 0 && totalPoliceKilled === 0 && totalHitsReceived === 0;
|
|
}
|
|
|
|
/** Helper: get computed success for a single session from the map, fallback to session.success */
|
|
export function getComputedSuccess(successMap: Map<number, boolean>, session: Session): boolean {
|
|
return successMap.has(session.id) ? successMap.get(session.id)! : session.success;
|
|
}
|