'use client'; import { useState, useEffect, useMemo } 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 { getProtestRepository, getRaceRepository, getDriverRepository, getLeagueMembershipRepository, getReviewProtestUseCase, getApplyPenaltyUseCase, getRequestProtestDefenseUseCase, getSendNotificationUseCase } from '@/lib/di-container'; import { useEffectiveDriverId } from '@/lib/currentDriver'; import { isLeagueAdminOrHigherRole } from '@/lib/leagueRoles'; import type { Protest } from '@gridpilot/racing/domain/entities/Protest'; import type { Race } from '@gridpilot/racing/domain/entities/Race'; import type { DriverDTO } from '@gridpilot/racing/application/dto/DriverDTO'; import type { PenaltyType } from '@gridpilot/racing/domain/entities/Penalty'; import { EntityMappers } from '@gridpilot/racing/application/mappers/EntityMappers'; import { AlertCircle, Video, Clock, Grid3x3, TrendingDown, XCircle, CheckCircle, ArrowLeft, Flag, AlertTriangle, ShieldAlert, Ban, DollarSign, FileWarning, User, Calendar, MapPin, MessageCircle, Shield, Gavel, Send, ChevronDown, ExternalLink } from 'lucide-react'; // Timeline event types interface TimelineEvent { id: string; type: 'protest_filed' | 'defense_requested' | 'defense_submitted' | 'steward_comment' | 'decision' | 'penalty_applied'; timestamp: Date; actor: DriverDTO | null; content: string; metadata?: Record; } const PENALTY_TYPES = [ { type: 'time_penalty' as PenaltyType, label: 'Time Penalty', description: 'Add seconds to race result', icon: Clock, color: 'text-blue-400 bg-blue-500/10 border-blue-500/20', requiresValue: true, valueLabel: 'seconds', defaultValue: 5 }, { type: 'grid_penalty' as PenaltyType, label: 'Grid Penalty', description: 'Grid positions for next race', icon: Grid3x3, color: 'text-purple-400 bg-purple-500/10 border-purple-500/20', requiresValue: true, valueLabel: 'positions', defaultValue: 3 }, { type: 'points_deduction' as PenaltyType, label: 'Points Deduction', description: 'Deduct championship points', icon: TrendingDown, color: 'text-red-400 bg-red-500/10 border-red-500/20', requiresValue: true, valueLabel: 'points', defaultValue: 5 }, { type: 'disqualification' as PenaltyType, label: 'Disqualification', description: 'Disqualify from race', icon: XCircle, color: 'text-red-500 bg-red-500/10 border-red-500/20', requiresValue: false, valueLabel: '', defaultValue: 0 }, { type: 'warning' as PenaltyType, label: 'Warning', description: 'Official warning only', icon: AlertTriangle, color: 'text-yellow-400 bg-yellow-500/10 border-yellow-500/20', requiresValue: false, valueLabel: '', defaultValue: 0 }, { type: 'license_points' as PenaltyType, label: 'License Points', description: 'Safety rating penalty', icon: ShieldAlert, color: 'text-orange-400 bg-orange-500/10 border-orange-500/20', requiresValue: true, valueLabel: 'points', defaultValue: 2 }, ]; export default function ProtestReviewPage() { const params = useParams(); const router = useRouter(); const leagueId = params.id as string; const protestId = params.protestId as string; const currentDriverId = useEffectiveDriverId(); const [protest, setProtest] = useState(null); const [race, setRace] = useState(null); const [protestingDriver, setProtestingDriver] = useState(null); const [accusedDriver, setAccusedDriver] = useState(null); const [loading, setLoading] = useState(true); const [isAdmin, setIsAdmin] = useState(false); // Decision state const [showDecisionPanel, setShowDecisionPanel] = useState(false); const [decision, setDecision] = useState<'uphold' | 'dismiss' | null>(null); const [penaltyType, setPenaltyType] = useState('time_penalty'); const [penaltyValue, setPenaltyValue] = useState(5); const [stewardNotes, setStewardNotes] = useState(''); const [submitting, setSubmitting] = useState(false); // Comment state const [newComment, setNewComment] = useState(''); 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 loadProtest() { setLoading(true); try { const protestRepo = getProtestRepository(); const raceRepo = getRaceRepository(); const driverRepo = getDriverRepository(); const protestEntity = await protestRepo.findById(protestId); if (!protestEntity) { throw new Error('Protest not found'); } setProtest(protestEntity); const raceEntity = await raceRepo.findById(protestEntity.raceId); if (!raceEntity) { throw new Error('Race not found'); } setRace(raceEntity); const protestingDriverEntity = await driverRepo.findById(protestEntity.protestingDriverId); const accusedDriverEntity = await driverRepo.findById(protestEntity.accusedDriverId); setProtestingDriver(protestingDriverEntity ? EntityMappers.toDriverDTO(protestingDriverEntity) : null); setAccusedDriver(accusedDriverEntity ? EntityMappers.toDriverDTO(accusedDriverEntity) : null); } catch (err) { console.error('Failed to load protest:', err); alert('Failed to load protest details'); router.push(`/leagues/${leagueId}/stewarding`); } finally { setLoading(false); } } if (isAdmin) { loadProtest(); } }, [protestId, leagueId, isAdmin, currentDriverId, router]); // Build timeline from protest data const timeline = useMemo((): TimelineEvent[] => { if (!protest) return []; const events: TimelineEvent[] = [ { id: 'filed', type: 'protest_filed', timestamp: new Date(protest.filedAt), actor: protestingDriver, content: protest.incident.description, metadata: { lap: protest.incident.lap, comment: protest.comment } } ]; // Add decision event if resolved if (protest.status === 'upheld' || protest.status === 'dismissed') { events.push({ id: 'decision', type: 'decision', timestamp: protest.reviewedAt ? new Date(protest.reviewedAt) : new Date(), actor: null, // Would need to load steward driver content: protest.decisionNotes || (protest.status === 'upheld' ? 'Protest upheld' : 'Protest dismissed'), metadata: { decision: protest.status } }); } return events.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime()); }, [protest, protestingDriver]); const handleSubmitDecision = async () => { if (!decision || !stewardNotes.trim() || !protest) return; setSubmitting(true); try { const reviewUseCase = getReviewProtestUseCase(); const penaltyUseCase = getApplyPenaltyUseCase(); if (decision === 'uphold') { await reviewUseCase.execute({ protestId: protest.id, stewardId: currentDriverId, decision: 'uphold', decisionNotes: stewardNotes, }); const selectedPenalty = PENALTY_TYPES.find(p => p.type === penaltyType); const penaltyValueToUse = selectedPenalty && selectedPenalty.requiresValue ? penaltyValue : 0; await penaltyUseCase.execute({ raceId: protest.raceId, driverId: protest.accusedDriverId, stewardId: currentDriverId, type: penaltyType, value: penaltyValueToUse, reason: protest.incident.description, protestId: protest.id, notes: stewardNotes, }); } else { await reviewUseCase.execute({ protestId: protest.id, stewardId: currentDriverId, decision: 'dismiss', decisionNotes: stewardNotes, }); } router.push(`/leagues/${leagueId}/stewarding`); } catch (err) { alert(err instanceof Error ? err.message : 'Failed to submit decision'); } finally { setSubmitting(false); } }; const handleRequestDefense = async () => { if (!protest) return; try { const requestDefenseUseCase = getRequestProtestDefenseUseCase(); const sendNotificationUseCase = getSendNotificationUseCase(); // Request defense const result = await requestDefenseUseCase.execute({ protestId: protest.id, stewardId: currentDriverId, }); // Send notification to accused driver await sendNotificationUseCase.execute({ recipientId: result.accusedDriverId, type: 'protest_filed', title: 'Defense Requested', body: `A steward has requested your defense for a protest filed against you.`, actionUrl: `/leagues/${leagueId}/stewarding/protests/${protest.id}`, data: { protestId: protest.id, raceId: protest.raceId, leagueId, }, }); // Reload page to show updated status window.location.reload(); } catch (err) { alert(err instanceof Error ? err.message : 'Failed to request defense'); } }; const getStatusConfig = (status: string) => { switch (status) { case 'pending': return { label: 'Pending Review', color: 'bg-warning-amber/20 text-warning-amber border-warning-amber/30', icon: Clock }; case 'under_review': return { label: 'Under Review', color: 'bg-blue-500/20 text-blue-400 border-blue-500/30', icon: Shield }; case 'awaiting_defense': return { label: 'Awaiting Defense', color: 'bg-purple-500/20 text-purple-400 border-purple-500/30', icon: MessageCircle }; case 'upheld': return { label: 'Upheld', color: 'bg-red-500/20 text-red-400 border-red-500/30', icon: CheckCircle }; case 'dismissed': return { label: 'Dismissed', color: 'bg-gray-500/20 text-gray-400 border-gray-500/30', icon: XCircle }; default: return { label: status, color: 'bg-gray-500/20 text-gray-400 border-gray-500/30', icon: AlertCircle }; } }; if (!isAdmin) { return (

Admin Access Required

Only league admins can review protests.

); } if (loading || !protest || !race) { return (
Loading protest details...
); } const statusConfig = getStatusConfig(protest.status); const StatusIcon = statusConfig.icon; const isPending = protest.status === 'pending' || protest.status === 'under_review' || protest.status === 'awaiting_defense'; const daysSinceFiled = Math.floor((Date.now() - new Date(protest.filedAt).getTime()) / (1000 * 60 * 60 * 24)); return (
{/* Compact Header */}

Protest Review

{statusConfig.label}
{daysSinceFiled > 2 && isPending && ( {daysSinceFiled}d old )}
{/* Main Layout: Feed + Sidebar */}
{/* Left Sidebar - Incident Info */}
{/* Drivers Involved */}

Parties Involved

{/* Protesting Driver */}

Protesting

{protestingDriver?.name || 'Unknown'}

{/* Accused Driver */}

Accused

{accusedDriver?.name || 'Unknown'}

{/* Race Info */}

Race Details

{race.track}
{race.track}
{race.scheduledAt.toLocaleDateString()}
Lap {protest.incident.lap}
{/* Evidence */} {protest.proofVideoUrl && (

Evidence

)} {/* Quick Stats */}

Timeline

Filed {new Date(protest.filedAt).toLocaleDateString()}
Age 2 ? 'text-red-400' : 'text-gray-300'}>{daysSinceFiled} days
{protest.reviewedAt && (
Resolved {new Date(protest.reviewedAt).toLocaleDateString()}
)}
{/* Center - Discussion Feed */}
{/* Timeline / Feed */}

Discussion

{/* Initial Protest Filing */}
{protestingDriver?.name || 'Unknown'} filed protest {new Date(protest.filedAt).toLocaleString()}

{protest.incident.description}

{protest.comment && (

Additional details:

{protest.comment}

)}
{/* Defense placeholder - will be populated when defense system is implemented */} {protest.status === 'awaiting_defense' && (

Defense Requested

Waiting for {accusedDriver?.name || 'the accused driver'} to submit their defense...

)} {/* Decision (if resolved) */} {(protest.status === 'upheld' || protest.status === 'dismissed') && protest.decisionNotes && (
Steward Decision {protest.status === 'upheld' ? 'Protest Upheld' : 'Protest Dismissed'} {protest.reviewedAt && ( <> {new Date(protest.reviewedAt).toLocaleString()} )}

{protest.decisionNotes}

)}
{/* Add Comment (future feature) */} {isPending && (