'use client'; import Button from '@/components/ui/Button'; import Card from '@/components/ui/Card'; import { useEffectiveDriverId } from "@/lib/hooks/useEffectiveDriverId"; import { LeagueRoleUtility } from '@/lib/utilities/LeagueRoleUtility'; import { useInject } from '@/lib/di/hooks/useInject'; import { PROTEST_SERVICE_TOKEN } from '@/lib/di/tokens'; import type { ProtestDetailViewModel } from '@/lib/view-models/ProtestDetailViewModel'; import { ProtestDriverViewModel } from '@/lib/view-models/ProtestDriverViewModel'; import { ProtestDecisionCommandModel } 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 { useMemo, useState } from 'react'; // Shared state components import { StateContainer } from '@/components/shared/state/StateContainer'; import { LoadingWrapper } from '@/components/shared/state/LoadingWrapper'; import { useLeagueAdminStatus } from "@/lib/hooks/league/useLeagueAdminStatus"; import { useProtestDetail } from "@/lib/hooks/league/useProtestDetail"; // 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; } type PenaltyUiConfig = { label: string; description: string; icon: typeof Gavel; color: string; defaultValue?: number; }; const PENALTY_UI: Record = { time_penalty: { label: 'Time Penalty', description: 'Add seconds to race result', icon: Clock, color: 'text-blue-400 bg-blue-500/10 border-blue-500/20', defaultValue: 5, }, grid_penalty: { label: 'Grid Penalty', description: 'Grid positions for next race', icon: Grid3x3, color: 'text-purple-400 bg-purple-500/10 border-purple-500/20', defaultValue: 3, }, points_deduction: { label: 'Points Deduction', description: 'Deduct championship points', icon: TrendingDown, color: 'text-red-400 bg-red-500/10 border-red-500/20', defaultValue: 5, }, disqualification: { label: 'Disqualification', description: 'Disqualify from race', icon: XCircle, color: 'text-red-500 bg-red-500/10 border-red-500/20', defaultValue: 0, }, warning: { label: 'Warning', description: 'Official warning only', icon: AlertTriangle, color: 'text-yellow-400 bg-yellow-500/10 border-yellow-500/20', defaultValue: 0, }, license_points: { label: 'License Points', description: 'Safety rating penalty', icon: ShieldAlert, color: 'text-orange-400 bg-orange-500/10 border-orange-500/20', 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 = useInject(PROTEST_SERVICE_TOKEN); // 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); const [newComment, setNewComment] = useState(''); // Check admin status using hook const { data: isAdmin, isLoading: adminLoading } = useLeagueAdminStatus(leagueId, currentDriverId || ''); // Load protest detail using hook const { data: detail, isLoading: detailLoading, error, retry } = useProtestDetail(leagueId, protestId, isAdmin || false); // Set initial penalty values when data loads useMemo(() => { if (detail?.initialPenaltyType) { setPenaltyType(detail.initialPenaltyType); setPenaltyValue(detail.initialPenaltyValue); } }, [detail]); const penaltyTypes = useMemo(() => { const referenceItems = detail?.penaltyTypes ?? []; return referenceItems.map((ref) => { const ui = PENALTY_UI[ref.type] ?? { icon: Gavel, color: 'text-gray-400 bg-gray-500/10 border-gray-500/20', }; return { ...ref, icon: ui.icon, color: ui.color, }; }); }, [detail?.penaltyTypes]); const selectedPenalty = useMemo(() => { return penaltyTypes.find((p) => p.type === penaltyType); }, [penaltyTypes, penaltyType]); const handleSubmitDecision = async () => { if (!decision || !stewardNotes.trim() || !detail || !currentDriverId) return; setSubmitting(true); try { const protest = detail.protest; const defaultUpheldReason = detail.defaultReasons?.upheld; const defaultDismissedReason = detail.defaultReasons?.dismissed; if (decision === 'uphold') { const requiresValue = selectedPenalty?.requiresValue ?? true; const commandModel = new ProtestDecisionCommandModel({ decision, penaltyType, penaltyValue, stewardNotes, }); const options: { requiresValue?: boolean; defaultUpheldReason?: string; defaultDismissedReason?: string; } = { requiresValue }; if (defaultUpheldReason) { options.defaultUpheldReason = defaultUpheldReason; } if (defaultDismissedReason) { options.defaultDismissedReason = defaultDismissedReason; } const penaltyCommand = commandModel.toApplyPenaltyCommand( protest.raceId, protest.accusedDriverId, currentDriverId, protest.id, options, ); await protestService.applyPenalty(penaltyCommand); } else { const warningRef = detail.penaltyTypes.find((p) => p.type === 'warning'); const requiresValue = warningRef?.requiresValue ?? false; const commandModel = new ProtestDecisionCommandModel({ decision, penaltyType: 'warning', penaltyValue: 0, stewardNotes, }); const options: { requiresValue?: boolean; defaultUpheldReason?: string; defaultDismissedReason?: string; } = { requiresValue }; if (defaultUpheldReason) { options.defaultUpheldReason = defaultUpheldReason; } if (defaultDismissedReason) { options.defaultDismissedReason = defaultDismissedReason; } const penaltyCommand = commandModel.toApplyPenaltyCommand( protest.raceId, protest.accusedDriverId, currentDriverId, protest.id, options, ); 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 (!detail || !currentDriverId) return; try { // Request defense await protestService.requestDefense({ protestId: detail.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 }; } }; // Show loading for admin check if (adminLoading) { return ; } // Show access denied if not admin if (!isAdmin) { return (

Admin Access Required

Only league admins can review protests.

); } return ( {(protestDetail) => { if (!protestDetail) return null; const protest = protestDetail.protest; const race = protestDetail.race; const protestingDriver = protestDetail.protestingDriver; const accusedDriver = protestDetail.accusedDriver; 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 && (