'use client'; import Button from '@/components/ui/Button'; import Card from '@/components/ui/Card'; import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId'; import { LeagueRoleUtility } from '@/lib/utilities/LeagueRoleUtility'; import { useServices } from '@/lib/services/ServiceProvider'; import { ProtestViewModel } from '@/lib/view-models/ProtestViewModel'; import { RaceViewModel } from '@/lib/view-models/RaceViewModel'; import { ProtestDriverViewModel } from '@/lib/view-models/ProtestDriverViewModel'; import { ProtestDecisionCommandModel, type PenaltyType } from '@/lib/command-models/protests/ProtestDecisionCommandModel'; import { AlertCircle, AlertTriangle, ArrowLeft, Calendar, CheckCircle, ChevronDown, Clock, ExternalLink, Flag, Gavel, Grid3x3, MapPin, MessageCircle, Send, Shield, ShieldAlert, TrendingDown, User, Video, XCircle } from 'lucide-react'; import Link from 'next/link'; import { useParams, useRouter } from 'next/navigation'; import { useEffect, useMemo, useState } from 'react'; // Timeline event types interface TimelineEvent { id: string; type: 'protest_filed' | 'defense_requested' | 'defense_submitted' | 'steward_comment' | 'decision' | 'penalty_applied'; timestamp: Date; actor: ProtestDriverViewModel | 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 { protestService, leagueMembershipService } = useServices(); 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() { await leagueMembershipService.fetchLeagueMemberships(leagueId); const membership = leagueMembershipService.getMembership(leagueId, currentDriverId); setIsAdmin(membership ? LeagueRoleUtility.isLeagueAdminOrHigherRole(membership.role) : false); } checkAdmin(); }, [leagueId, currentDriverId, leagueMembershipService]); useEffect(() => { async function loadProtest() { setLoading(true); try { const protestData = await protestService.getProtestById(leagueId, protestId); if (!protestData) { throw new Error('Protest not found'); } setProtest(protestData.protest); setRace(protestData.race); setProtestingDriver(protestData.protestingDriver); setAccusedDriver(protestData.accusedDriver); } 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, 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.submittedAt), actor: protestingDriver, content: protest.description, metadata: {} } ]; // Add decision event when status/decisions are available in view model 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 { if (decision === 'uphold') { const commandModel = new ProtestDecisionCommandModel({ decision, penaltyType, penaltyValue, stewardNotes, }); const penaltyCommand = commandModel.toApplyPenaltyCommand( protest.raceId, protest.accusedDriverId, currentDriverId, protest.id ); await protestService.applyPenalty(penaltyCommand); } else { // For dismiss, we might need a separate endpoint // For now, just apply a warning penalty with 0 value or create a separate endpoint const commandModel = new ProtestDecisionCommandModel({ decision, penaltyType: 'warning', penaltyValue: 0, stewardNotes, }); const penaltyCommand = commandModel.toApplyPenaltyCommand( protest.raceId, protest.accusedDriverId, currentDriverId, protest.id ); penaltyCommand.reason = stewardNotes || 'Protest dismissed'; await protestService.applyPenalty(penaltyCommand); } 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 { // Request defense await protestService.requestDefense({ protestId: protest.id, stewardId: currentDriverId, }); // 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'; const daysSinceFiled = Math.floor((Date.now() - new Date(protest.submittedAt).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.name}
{race.name}
{race.formattedDate}
{protest.incident?.lap && (
Lap {protest.incident.lap}
)}
{protest.proofVideoUrl && (

Evidence

)} {/* Quick Stats */}

Timeline

Filed {new Date(protest.submittedAt).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.submittedAt).toLocaleString()}

{protest.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 && (