This commit is contained in:
2025-12-11 00:57:32 +01:00
parent 1303a14493
commit 6a427eab57
112 changed files with 6148 additions and 2272 deletions

View File

@@ -3,27 +3,6 @@
import { useState, useEffect } from 'react';
import { useParams, useRouter } from 'next/navigation';
import Link from 'next/link';
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import Breadcrumbs from '@/components/layout/Breadcrumbs';
import {
getRaceRepository,
getLeagueRepository,
getProtestRepository,
getDriverRepository,
getPenaltyRepository,
getLeagueMembershipRepository,
getReviewProtestUseCase,
getApplyPenaltyUseCase,
} from '@/lib/di-container';
import { useEffectiveDriverId } from '@/lib/currentDriver';
import { isLeagueAdminOrHigherRole } from '@/lib/leagueRoles';
import type { Race } from '@gridpilot/racing/domain/entities/Race';
import type { League } from '@gridpilot/racing/domain/entities/League';
import type { Protest } from '@gridpilot/racing/domain/entities/Protest';
import type { Penalty, PenaltyType } from '@gridpilot/racing/domain/entities/Penalty';
import type { DriverDTO } from '@gridpilot/racing/application/dto/DriverDTO';
import { EntityMappers } from '@gridpilot/racing/application/mappers/EntityMappers';
import {
AlertTriangle,
Clock,
@@ -40,6 +19,22 @@ import {
Users,
Trophy,
} from 'lucide-react';
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import Breadcrumbs from '@/components/layout/Breadcrumbs';
import {
getGetRaceProtestsUseCase,
getGetRacePenaltiesUseCase,
getRaceRepository,
getLeagueRepository,
getLeagueMembershipRepository,
} from '@/lib/di-container';
import { useEffectiveDriverId } from '@/lib/currentDriver';
import { isLeagueAdminOrHigherRole } from '@/lib/leagueRoles';
import type { RaceProtestViewModel } from '@gridpilot/racing/application/presenters/IRaceProtestsPresenter';
import type { RacePenaltyViewModel } from '@gridpilot/racing/application/presenters/IRacePenaltiesPresenter';
import type { League } from '@gridpilot/racing/domain/entities/League';
import type { Race } from '@gridpilot/racing/domain/entities/Race';
export default function RaceStewardingPage() {
const params = useParams();
@@ -49,9 +44,8 @@ export default function RaceStewardingPage() {
const [race, setRace] = useState<Race | null>(null);
const [league, setLeague] = useState<League | null>(null);
const [protests, setProtests] = useState<Protest[]>([]);
const [penalties, setPenalties] = useState<Penalty[]>([]);
const [driversById, setDriversById] = useState<Record<string, DriverDTO>>({});
const [protests, setProtests] = useState<RaceProtestViewModel[]>([]);
const [penalties, setPenalties] = useState<RacePenaltyViewModel[]>([]);
const [loading, setLoading] = useState(true);
const [isAdmin, setIsAdmin] = useState(false);
const [activeTab, setActiveTab] = useState<'pending' | 'resolved' | 'penalties'>('pending');
@@ -62,12 +56,10 @@ export default function RaceStewardingPage() {
try {
const raceRepo = getRaceRepository();
const leagueRepo = getLeagueRepository();
const protestRepo = getProtestRepository();
const penaltyRepo = getPenaltyRepository();
const driverRepo = getDriverRepository();
const membershipRepo = getLeagueMembershipRepository();
const protestsUseCase = getGetRaceProtestsUseCase();
const penaltiesUseCase = getGetRacePenaltiesUseCase();
// Get race
const raceData = await raceRepo.findById(raceId);
if (!raceData) {
setLoading(false);
@@ -75,48 +67,24 @@ export default function RaceStewardingPage() {
}
setRace(raceData);
// Get league
const leagueData = await leagueRepo.findById(raceData.leagueId);
setLeague(leagueData);
// Check admin status
if (leagueData) {
const membership = await membershipRepo.getMembership(leagueData.id, currentDriverId);
const membership = await membershipRepo.getMembership(
leagueData.id,
currentDriverId,
);
setIsAdmin(membership ? isLeagueAdminOrHigherRole(membership.role) : false);
}
// Get protests for this race
const raceProtests = await protestRepo.findByRaceId(raceId);
setProtests(raceProtests);
await protestsUseCase.execute(raceId);
const protestsViewModel = protestsUseCase.presenter.getViewModel();
setProtests(protestsViewModel.protests);
// Get penalties for this race
const racePenalties = await penaltyRepo.findByRaceId(raceId);
setPenalties(racePenalties);
// Collect driver IDs
const driverIds = new Set<string>();
raceProtests.forEach((p) => {
driverIds.add(p.protestingDriverId);
driverIds.add(p.accusedDriverId);
});
racePenalties.forEach((p) => {
driverIds.add(p.driverId);
});
// Load driver info
const driverEntities = await Promise.all(
Array.from(driverIds).map((id) => driverRepo.findById(id))
);
const byId: Record<string, DriverDTO> = {};
driverEntities.forEach((driver) => {
if (driver) {
const dto = EntityMappers.toDriverDTO(driver);
if (dto) {
byId[dto.id] = dto;
}
}
});
setDriversById(byId);
await penaltiesUseCase.execute(raceId);
const penaltiesViewModel = penaltiesUseCase.presenter.getViewModel();
setPenalties(penaltiesViewModel.penalties);
} catch (err) {
console.error('Failed to load data:', err);
} finally {
@@ -128,10 +96,13 @@ export default function RaceStewardingPage() {
}, [raceId, currentDriverId]);
const pendingProtests = protests.filter(
(p) => p.status === 'pending' || p.status === 'under_review'
(p) => p.status === 'pending' || p.status === 'under_review',
);
const resolvedProtests = protests.filter(
(p) => p.status === 'upheld' || p.status === 'dismissed' || p.status === 'withdrawn'
(p) =>
p.status === 'upheld' ||
p.status === 'dismissed' ||
p.status === 'withdrawn',
);
const getStatusBadge = (status: string) => {
@@ -166,8 +137,9 @@ export default function RaceStewardingPage() {
}
};
const formatDate = (date: Date) => {
return new Date(date).toLocaleDateString('en-US', {
const formatDate = (date: Date | string) => {
const d = typeof date === 'string' ? new Date(date) : date;
return d.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',