'use client'; import { useState, useEffect, useMemo } from 'react'; import { useParams } from 'next/navigation'; import Link from 'next/link'; import Card from '@/components/ui/Card'; import Button from '@/components/ui/Button'; import { ReviewProtestModal } from '@/components/leagues/ReviewProtestModal'; import QuickPenaltyModal from '@/components/leagues/QuickPenaltyModal'; import PenaltyFAB from '@/components/leagues/PenaltyFAB'; import { useEffectiveDriverId } from '@/lib/currentDriver'; import { isLeagueAdminOrHigherRole } from '@/lib/leagueRoles'; import type { Protest } from '@core/racing/domain/entities/Protest'; import type { Race } from '@core/racing/domain/entities/Race'; import type { Penalty, PenaltyType } from '@core/racing/domain/entities/Penalty'; import type { DriverDTO } from '@core/racing/application/dto/DriverDTO'; import { EntityMappers } from '@core/racing/application/mappers/EntityMappers'; import { AlertTriangle, Clock, CheckCircle, Flag, ChevronRight, Calendar, MapPin, AlertCircle, Video, Gavel } from 'lucide-react'; interface RaceWithProtests { race: Race; pendingProtests: Protest[]; resolvedProtests: Protest[]; penalties: Penalty[]; } export default function LeagueStewardingPage() { const params = useParams(); const leagueId = params.id as string; const currentDriverId = useEffectiveDriverId(); const [races, setRaces] = useState([]); const [protestsByRace, setProtestsByRace] = useState>({}); const [penaltiesByRace, setPenaltiesByRace] = useState>({}); const [driversById, setDriversById] = useState>({}); const [allDrivers, setAllDrivers] = useState([]); const [loading, setLoading] = useState(true); const [isAdmin, setIsAdmin] = useState(false); const [activeTab, setActiveTab] = useState<'pending' | 'history'>('pending'); const [selectedProtest, setSelectedProtest] = useState(null); const [expandedRaces, setExpandedRaces] = useState>(new Set()); const [showQuickPenaltyModal, setShowQuickPenaltyModal] = useState(false); useEffect(() => { async function checkAdmin() { const membershipRepo = getLeagueMembershipRepository(); const membership = await membershipRepo.getMembership(leagueId, currentDriverId); setIsAdmin(membership ? isLeagueAdminOrHigherRole(membership.role) : false); } checkAdmin(); }, [leagueId, currentDriverId]); useEffect(() => { async function loadData() { setLoading(true); try { const raceRepo = getRaceRepository(); const protestRepo = getProtestRepository(); const penaltyRepo = getPenaltyRepository(); const driverRepo = getDriverRepository(); // Get all races for this league const leagueRaces = await raceRepo.findByLeagueId(leagueId); setRaces(leagueRaces); // Get protests and penalties for each race const protestsMap: Record = {}; const penaltiesMap: Record = {}; const driverIds = new Set(); for (const race of leagueRaces) { const raceProtests = await protestRepo.findByRaceId(race.id); const racePenalties = await penaltyRepo.findByRaceId(race.id); protestsMap[race.id] = raceProtests; penaltiesMap[race.id] = racePenalties; // Collect driver IDs raceProtests.forEach((p) => { driverIds.add(p.protestingDriverId); driverIds.add(p.accusedDriverId); }); racePenalties.forEach((p) => { driverIds.add(p.driverId); }); } setProtestsByRace(protestsMap); setPenaltiesByRace(penaltiesMap); // Load driver info const driverEntities = await Promise.all( Array.from(driverIds).map((id) => driverRepo.findById(id)), ); const byId: Record = {}; driverEntities.forEach((driver) => { if (driver) { const dto = EntityMappers.toDriverDTO(driver); if (dto) { byId[dto.id] = dto; } } }); setDriversById(byId); setAllDrivers(Object.values(byId)); // Auto-expand races with pending protests const racesWithPending = new Set(); Object.entries(protestsMap).forEach(([raceId, protests]) => { if (protests.some(p => p.status === 'pending' || p.status === 'under_review')) { racesWithPending.add(raceId); } }); setExpandedRaces(racesWithPending); } catch (err) { console.error('Failed to load data:', err); } finally { setLoading(false); } } if (isAdmin) { loadData(); } }, [leagueId, isAdmin]); // 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]); // 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); const handleAcceptProtest = async ( protestId: string, penaltyType: PenaltyType, penaltyValue: number, stewardNotes: string ) => { const reviewUseCase = getReviewProtestUseCase(); const penaltyUseCase = getApplyPenaltyUseCase(); await reviewUseCase.execute({ protestId, stewardId: currentDriverId, decision: 'uphold', decisionNotes: stewardNotes, }); // Find the protest let foundProtest: Protest | undefined; Object.values(protestsByRace).forEach(protests => { const p = protests.find(pr => pr.id === protestId); if (p) foundProtest = p; }); if (foundProtest) { await penaltyUseCase.execute({ raceId: foundProtest.raceId, driverId: foundProtest.accusedDriverId, stewardId: currentDriverId, type: penaltyType, value: penaltyValue, reason: foundProtest.incident.description, protestId, notes: stewardNotes, }); } }; const handleRejectProtest = async (protestId: string, stewardNotes: string) => { const reviewUseCase = getReviewProtestUseCase(); await reviewUseCase.execute({ protestId, stewardId: currentDriverId, decision: 'dismiss', decisionNotes: stewardNotes, }); }; const handleProtestReviewed = () => { setSelectedProtest(null); window.location.reload(); }; const toggleRaceExpanded = (raceId: string) => { setExpandedRaces(prev => { const next = new Set(prev); if (next.has(raceId)) { next.delete(raceId); } else { next.add(raceId); } return next; }); }; const getStatusBadge = (status: string) => { switch (status) { case 'pending': case 'under_review': return Pending; case 'upheld': return Upheld; case 'dismissed': return Dismissed; case 'withdrawn': return Withdrawn; default: return null; } }; if (!isAdmin) { return (

Admin Access Required

Only league admins can access stewarding functions.

); } return (

Stewarding

Quick overview of protests and penalties across all races

{/* Stats summary */} {!loading && (
Pending Review
{totalPending}
Resolved
{totalResolved}
Penalties
{totalPenalties}
)} {/* Tab navigation */}
{/* Content */} {loading ? (
Loading stewarding data...
) : filteredRaces.length === 0 ? (

{activeTab === 'pending' ? 'All Clear!' : 'No History Yet'}

{activeTab === 'pending' ? 'No pending protests to review' : 'No resolved protests or penalties'}

) : (
{filteredRaces.map(({ race, pendingProtests, resolvedProtests, penalties }) => { const isExpanded = expandedRaces.has(race.id); const displayProtests = activeTab === 'pending' ? pendingProtests : resolvedProtests; return (
{/* Race Header */} {/* Expanded Content */} {isExpanded && (
{displayProtests.length === 0 && penalties.length === 0 ? (

No items to display

) : ( <> {displayProtests.map((protest) => { const protester = driversById[protest.protestingDriverId]; const accused = driversById[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'); return (
{protester?.name || 'Unknown'} vs {accused?.name || 'Unknown'} {getStatusBadge(protest.status)} {isUrgent && ( {daysSinceFiled}d old )}
Lap {protest.incident.lap} Filed {new Date(protest.filedAt).toLocaleDateString()} {protest.proofVideoUrl && ( <> )}

{protest.incident.description}

{protest.decisionNotes && (

Steward: {protest.decisionNotes}

)}
{(protest.status === 'pending' || protest.status === 'under_review') && ( )}
); })} {activeTab === 'history' && penalties.map((penalty) => { const driver = driversById[penalty.driverId]; return (
{driver?.name || 'Unknown'} {penalty.type.replace('_', ' ')}

{penalty.reason}

{penalty.type === 'time_penalty' && `+${penalty.value}s`} {penalty.type === 'grid_penalty' && `+${penalty.value} grid`} {penalty.type === 'points_deduction' && `-${penalty.value} pts`} {penalty.type === 'disqualification' && 'DSQ'} {penalty.type === 'warning' && 'Warning'} {penalty.type === 'license_points' && `${penalty.value} LP`}
); })} )}
)}
); })}
)}
{activeTab === 'history' && ( setShowQuickPenaltyModal(true)} /> )} {selectedProtest && ( setSelectedProtest(null)} onAccept={handleAcceptProtest} onReject={handleRejectProtest} /> )} {showQuickPenaltyModal && ( setShowQuickPenaltyModal(false)} adminId={currentDriverId} races={races.map(r => ({ id: r.id, track: r.track, scheduledAt: r.scheduledAt }))} /> )}
); }