fix data flow issues

This commit is contained in:
2025-12-19 21:58:03 +01:00
parent 94fc538f44
commit ec177a75ce
37 changed files with 1336 additions and 534 deletions

View File

@@ -7,6 +7,7 @@ import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
import { useServices } from '@/lib/services/ServiceProvider';
import { LeagueStewardingViewModel } from '@/lib/view-models/LeagueStewardingViewModel';
import { LeagueRoleUtility } from '@/lib/utilities/LeagueRoleUtility';
import {
AlertCircle,
@@ -27,32 +28,13 @@ import { useEffect, useMemo, useState } from 'react';
// Local type definitions to replace core imports
type PenaltyType = 'time_penalty' | 'grid_penalty' | 'points_deduction' | 'disqualification' | 'warning' | 'license_points';
type DriverDTO = {
id: string;
name: string;
avatarUrl?: string;
iracingId?: string;
rating?: number;
};
interface RaceWithProtests {
race: any;
pendingProtests: any[];
resolvedProtests: any[];
penalties: any[];
}
export default function LeagueStewardingPage() {
const params = useParams();
const leagueId = params.id as string;
const currentDriverId = useEffectiveDriverId();
const { raceService, protestService, driverService, leagueMembershipService, penaltyService } = useServices();
const { leagueStewardingService, leagueMembershipService } = useServices();
const [races, setRaces] = useState<any[]>([]);
const [protestsByRace, setProtestsByRace] = useState<Record<string, any[]>>({});
const [penaltiesByRace, setPenaltiesByRace] = useState<Record<string, any[]>>({});
const [driversById, setDriversById] = useState<Record<string, DriverDTO>>({});
const [allDrivers, setAllDrivers] = useState<DriverDTO[]>([]);
const [stewardingData, setStewardingData] = useState<LeagueStewardingViewModel | null>(null);
const [loading, setLoading] = useState(true);
const [isAdmin, setIsAdmin] = useState(false);
const [activeTab, setActiveTab] = useState<'pending' | 'history'>('pending');
@@ -72,52 +54,13 @@ export default function LeagueStewardingPage() {
async function loadData() {
setLoading(true);
try {
// Get all races for this league
const leagueRaces = await raceService.findByLeagueId(leagueId);
setRaces(leagueRaces);
// Get protests and penalties for each race
const protestsMap: Record<string, any[]> = {};
const penaltiesMap: Record<string, any[]> = {};
const driverIds = new Set<string>();
for (const race of leagueRaces) {
const raceProtests = await protestService.findByRaceId(race.id);
const racePenalties = await penaltyService.findByRaceId(race.id);
protestsMap[race.id] = raceProtests;
penaltiesMap[race.id] = racePenalties;
// Collect driver IDs
raceProtests.forEach((p: any) => {
driverIds.add(p.protestingDriverId);
driverIds.add(p.accusedDriverId);
});
racePenalties.forEach((p: any) => {
driverIds.add(p.driverId);
});
}
setProtestsByRace(protestsMap);
setPenaltiesByRace(penaltiesMap);
// Load driver info
const driverEntities = await driverService.findByIds(Array.from(driverIds));
const byId: Record<string, any> = {};
driverEntities.forEach((driver) => {
if (driver) {
byId[driver.id] = driver;
}
});
setDriversById(byId);
setAllDrivers(Object.values(byId));
const data = await leagueStewardingService.getLeagueStewardingData(leagueId);
setStewardingData(data);
// Auto-expand races with pending protests
const racesWithPending = new Set<string>();
Object.entries(protestsMap).forEach(([raceId, protests]) => {
if (protests.some((p: any) => p.status === 'pending' || p.status === 'under_review')) {
racesWithPending.add(raceId);
}
data.pendingRaces.forEach(race => {
racesWithPending.add(race.race.id);
});
setExpandedRaces(racesWithPending);
} catch (err) {
@@ -130,34 +73,12 @@ export default function LeagueStewardingPage() {
if (isAdmin) {
loadData();
}
}, [leagueId, isAdmin, raceService, protestService, driverService, penaltyService]);
// Compute race data with protest/penalty info
const racesWithData = useMemo((): RaceWithProtests[] => {
return races.map(race => {
const protests = protestsByRace[race.id] || [];
const penalties = penaltiesByRace[race.id] || [];
return {
race,
pendingProtests: protests.filter(p => p.status === 'pending' || p.status === 'under_review'),
resolvedProtests: protests.filter(p => p.status === 'upheld' || p.status === 'dismissed' || p.status === 'withdrawn'),
penalties
};
}).sort((a, b) => b.race.scheduledAt.getTime() - a.race.scheduledAt.getTime());
}, [races, protestsByRace, penaltiesByRace]);
}, [leagueId, isAdmin, leagueStewardingService]);
// Filter races based on active tab
const filteredRaces = useMemo(() => {
if (activeTab === 'pending') {
return racesWithData.filter(r => r.pendingProtests.length > 0);
}
return racesWithData.filter(r => r.resolvedProtests.length > 0 || r.penalties.length > 0);
}, [racesWithData, activeTab]);
// Stats
const totalPending = racesWithData.reduce((sum, r) => sum + r.pendingProtests.length, 0);
const totalResolved = racesWithData.reduce((sum, r) => sum + r.resolvedProtests.length, 0);
const totalPenalties = racesWithData.reduce((sum, r) => sum + r.penalties.length, 0);
return activeTab === 'pending' ? stewardingData?.pendingRaces ?? [] : stewardingData?.historyRaces ?? [];
}, [stewardingData, activeTab]);
const handleAcceptProtest = async (
protestId: string,
@@ -165,22 +86,23 @@ export default function LeagueStewardingPage() {
penaltyValue: number,
stewardNotes: string
) => {
await protestService.reviewProtest({
await leagueStewardingService.reviewProtest({
protestId,
stewardId: currentDriverId,
decision: 'uphold',
decisionNotes: stewardNotes,
});
// Find the protest
// Find the protest to get details for penalty
let foundProtest: any | undefined;
Object.values(protestsByRace).forEach(protests => {
const p = protests.find(pr => pr.id === protestId);
if (p) foundProtest = p;
stewardingData?.racesWithData.forEach(raceData => {
const p = raceData.pendingProtests.find(pr => pr.id === protestId) ||
raceData.resolvedProtests.find(pr => pr.id === protestId);
if (p) foundProtest = { ...p, raceId: raceData.race.id };
});
if (foundProtest) {
await penaltyService.applyPenalty({
await leagueStewardingService.applyPenalty({
raceId: foundProtest.raceId,
driverId: foundProtest.accusedDriverId,
stewardId: currentDriverId,
@@ -194,7 +116,7 @@ export default function LeagueStewardingPage() {
};
const handleRejectProtest = async (protestId: string, stewardNotes: string) => {
await protestService.reviewProtest({
await leagueStewardingService.reviewProtest({
protestId,
stewardId: currentDriverId,
decision: 'dismiss',
@@ -260,28 +182,28 @@ export default function LeagueStewardingPage() {
</div>
{/* Stats summary */}
{!loading && (
{!loading && stewardingData && (
<div className="grid grid-cols-3 gap-4 mb-6">
<div className="rounded-lg bg-iron-gray/50 border border-charcoal-outline p-4">
<div className="flex items-center gap-2 text-warning-amber mb-1">
<Clock className="w-4 h-4" />
<span className="text-xs font-medium uppercase">Pending Review</span>
</div>
<div className="text-2xl font-bold text-white">{totalPending}</div>
<div className="text-2xl font-bold text-white">{stewardingData.totalPending}</div>
</div>
<div className="rounded-lg bg-iron-gray/50 border border-charcoal-outline p-4">
<div className="flex items-center gap-2 text-performance-green mb-1">
<CheckCircle className="w-4 h-4" />
<span className="text-xs font-medium uppercase">Resolved</span>
</div>
<div className="text-2xl font-bold text-white">{totalResolved}</div>
<div className="text-2xl font-bold text-white">{stewardingData.totalResolved}</div>
</div>
<div className="rounded-lg bg-iron-gray/50 border border-charcoal-outline p-4">
<div className="flex items-center gap-2 text-red-400 mb-1">
<Gavel className="w-4 h-4" />
<span className="text-xs font-medium uppercase">Penalties</span>
</div>
<div className="text-2xl font-bold text-white">{totalPenalties}</div>
<div className="text-2xl font-bold text-white">{stewardingData.totalPenalties}</div>
</div>
</div>
)}
@@ -298,9 +220,9 @@ export default function LeagueStewardingPage() {
}`}
>
Pending Protests
{totalPending > 0 && (
{stewardingData && stewardingData.totalPending > 0 && (
<span className="ml-2 px-2 py-0.5 text-xs bg-warning-amber/20 text-warning-amber rounded-full">
{totalPending}
{stewardingData.totalPending}
</span>
)}
</button>
@@ -380,8 +302,8 @@ export default function LeagueStewardingPage() {
) : (
<>
{displayProtests.map((protest) => {
const protester = driversById[protest.protestingDriverId];
const accused = driversById[protest.accusedDriverId];
const protester = stewardingData!.driverMap[protest.protestingDriverId];
const accused = stewardingData!.driverMap[protest.accusedDriverId];
const daysSinceFiled = Math.floor((Date.now() - new Date(protest.filedAt).getTime()) / (1000 * 60 * 60 * 24));
const isUrgent = daysSinceFiled > 2 && (protest.status === 'pending' || protest.status === 'under_review');
@@ -443,7 +365,7 @@ export default function LeagueStewardingPage() {
})}
{activeTab === 'history' && penalties.map((penalty) => {
const driver = driversById[penalty.driverId];
const driver = stewardingData!.driverMap[penalty.driverId];
return (
<div
key={penalty.id}
@@ -500,12 +422,12 @@ export default function LeagueStewardingPage() {
/>
)}
{showQuickPenaltyModal && (
{showQuickPenaltyModal && stewardingData && (
<QuickPenaltyModal
drivers={allDrivers}
drivers={stewardingData.allDrivers}
onClose={() => setShowQuickPenaltyModal(false)}
adminId={currentDriverId}
races={races.map(r => ({ id: r.id, track: r.track, scheduledAt: r.scheduledAt }))}
races={stewardingData.racesWithData.map(r => ({ id: r.race.id, track: r.race.track, scheduledAt: r.race.scheduledAt }))}
/>
)}
</div>