'use client'; import { useEffectiveDriverId } from "@/hooks/useEffectiveDriverId"; import { ProtestDecisionCommandModel } from '@/lib/command-models/protests/ProtestDecisionCommandModel'; import { useInject } from '@/lib/di/hooks/useInject'; import { PROTEST_SERVICE_TOKEN } from '@/lib/di/tokens'; import { Button } from '@/ui/Button'; import { Card } from '@/ui/Card'; import { AlertCircle, AlertTriangle, ArrowLeft, Calendar, CheckCircle, ChevronDown, Clock, ExternalLink, Flag, Gavel, Grid3x3, MapPin, MessageCircle, Send, Shield, ShieldAlert, TrendingDown, User, Video, XCircle, type LucideIcon } from 'lucide-react'; import { useParams, useRouter } from 'next/navigation'; import { useEffect, useMemo, useState } from 'react'; // Shared state components import { LoadingWrapper } from '@/ui/LoadingWrapper'; import { StateContainer } from '@/components/shared/state/StateContainer'; import { useLeagueAdminStatus } from "@/hooks/league/useLeagueAdminStatus"; import { useProtestDetail } from "@/hooks/league/useProtestDetail"; import { routes } from '@/lib/routing/RouteConfig'; import { Box } from '@/ui/Box'; import { Heading } from '@/ui/Heading'; import { Icon as UIIcon } from '@/ui/Icon'; import { Link as UILink } from '@/ui/Link'; import { Grid } from '@/ui/Grid'; import { Stack } from '@/ui/Stack'; import { Text } from '@/ui/Text'; const GridItem = ({ children, colSpan, lgSpan }: { children: React.ReactNode; colSpan?: number; lgSpan?: number }) => ( {children} ); type PenaltyUiConfig = { label: string; description: string; icon: LucideIcon; 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 function ProtestDetailPageClient({ initialViewData }: { initialViewData: unknown }) { 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); // Use initial data if available // eslint-disable-next-line @typescript-eslint/no-explicit-any const protestDetail = (detail || initialViewData) as any; // Set initial penalty values when data loads useEffect(() => { if (protestDetail?.initialPenaltyType) { setPenaltyType(protestDetail.initialPenaltyType); setPenaltyValue(protestDetail.initialPenaltyValue); } }, [protestDetail]); const penaltyTypes = useMemo(() => { const referenceItems = protestDetail?.penaltyTypes ?? []; // eslint-disable-next-line @typescript-eslint/no-explicit-any return referenceItems.map((ref: any) => { 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, }; }); }, [protestDetail?.penaltyTypes]); const selectedPenalty = useMemo(() => { // eslint-disable-next-line @typescript-eslint/no-explicit-any return penaltyTypes.find((p: any) => p.type === penaltyType); }, [penaltyTypes, penaltyType]); const handleSubmitDecision = async () => { if (!decision || !stewardNotes.trim() || !protestDetail || !currentDriverId) return; setSubmitting(true); try { const protest = protestDetail.protest || protestDetail; const defaultUpheldReason = protestDetail.defaultReasons?.upheld; const defaultDismissedReason = protestDetail.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 || protestDetail.race?.id, protest.accusedDriverId || protestDetail.accusedDriver?.id, currentDriverId, protest.id || protestDetail.protestId, options, ); const result = await protestService.applyPenalty(penaltyCommand); if (result.isErr()) { throw new Error(result.getError().message); } } else { // eslint-disable-next-line @typescript-eslint/no-explicit-any const warningRef = protestDetail.penaltyTypes.find((p: any) => 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 || protestDetail.race?.id, protest.accusedDriverId || protestDetail.accusedDriver?.id, currentDriverId, protest.id || protestDetail.protestId, options, ); const result = await protestService.applyPenalty(penaltyCommand); if (result.isErr()) { throw new Error(result.getError().message); } } router.push(routes.league.stewarding(leagueId)); } catch (err) { alert(err instanceof Error ? err.message : 'Failed to submit decision'); } finally { setSubmitting(false); } }; const handleRequestDefense = async () => { if (!protestDetail || !currentDriverId) return; try { // Request defense const result = await protestService.requestDefense({ protestId: protestDetail.protest?.id || protestDetail.protestId, stewardId: currentDriverId, }); if (result.isErr()) { throw new Error(result.getError().message); } // 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', bg: 'bg-warning-amber/20', color: 'text-warning-amber', borderColor: 'border-warning-amber/30', icon: Clock }; case 'under_review': return { label: 'Under Review', bg: 'bg-blue-500/20', color: 'text-blue-400', borderColor: 'border-blue-500/30', icon: Shield }; case 'awaiting_defense': return { label: 'Awaiting Defense', bg: 'bg-purple-500/20', color: 'text-purple-400', borderColor: 'border-purple-500/30', icon: MessageCircle }; case 'upheld': return { label: 'Upheld', bg: 'bg-red-500/20', color: 'text-red-400', borderColor: 'border-red-500/30', icon: CheckCircle }; case 'dismissed': return { label: 'Dismissed', bg: 'bg-gray-500/20', color: 'text-gray-400', borderColor: 'border-gray-500/30', icon: XCircle }; default: return { label: status, bg: 'bg-gray-500/20', color: 'text-gray-400', borderColor: '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 ( {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} {(pd: any) => { if (!pd) return null; const protest = pd.protest || pd; const race = pd.race; const protestingDriver = pd.protestingDriver; const accusedDriver = pd.accusedDriver; const statusConfig = getStatusConfig(protest.status); const StatusIcon = statusConfig.icon; const isPending = protest.status === 'pending'; const submittedAt = protest.submittedAt || pd.submittedAt; const daysSinceFiled = Math.floor((Date.now() - new Date(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 || 'Unknown Race'} {race?.name || 'Unknown Track'} {race?.formattedDate || (race?.scheduledAt ? new Date(race.scheduledAt).toLocaleDateString() : 'Unknown Date')} {protest.incident?.lap && ( Lap {protest.incident.lap} )} {protest.proofVideoUrl && ( Evidence Watch Video )} {/* Quick Stats */} Timeline Filed {new Date(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(submittedAt).toLocaleString()} {protest.description || pd.incident?.description} {(protest.comment || pd.comment) && ( Additional details: {protest.comment || pd.comment} )} {/* Defense placeholder */} {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 */} {isPending && ( ) => setNewComment(e.target.value)} placeholder="Add a comment or request more information..." style={{ height: '4rem' }} w="full" px={4} py={3} bg="bg-deep-graphite" border borderColor="border-charcoal-outline" rounded="lg" color="text-white" fontSize="sm" /> )} {/* Right Sidebar - Actions */} {isPending && ( <> {/* Quick Actions */} Actions {/* Decision Panel */} {showDecisionPanel && ( Stewarding Decision {/* Decision Selection */} {/* Penalty Selection (if upholding) */} {decision === 'uphold' && ( Penalty Type {penaltyTypes.length === 0 ? ( Loading penalty types... ) : ( <> {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} {penaltyTypes.map((penalty: any) => { const Icon = penalty.icon; const isSelected = penaltyType === penalty.type; return ( ); })} {selectedPenalty?.requiresValue && ( Value ({selectedPenalty.valueLabel}) ) => setPenaltyValue(Number(e.target.value))} min="1" w="full" px={3} py={2} bg="bg-deep-graphite" border borderColor="border-charcoal-outline" rounded="lg" color="text-white" fontSize="sm" /> )} )} )} {/* Steward Notes */} Decision Reasoning * ) => setStewardNotes(e.target.value)} placeholder="Explain your decision..." style={{ height: '8rem' }} w="full" px={3} py={2} bg="bg-deep-graphite" border borderColor="border-charcoal-outline" rounded="lg" color="text-white" fontSize="sm" /> {/* Submit */} )} )} {/* Already Resolved Info */} {!isPending && ( Case Closed {protest.status === 'upheld' ? 'Protest was upheld' : 'Protest was dismissed'} )} ); }} ); }