'use client'; import { useEffectiveDriverId } from "@/hooks/useEffectiveDriverId"; import { useInject } from '@/lib/di/hooks/useInject'; import { PROTEST_SERVICE_TOKEN } from '@/lib/di/tokens'; import { AlertCircle, AlertTriangle, CheckCircle, Clock, Gavel, Grid3x3, MessageCircle, Shield, ShieldAlert, TrendingDown, XCircle, } from 'lucide-react'; import { useParams, useRouter } from 'next/navigation'; import { useEffect, useMemo, useState } from 'react'; import { useLeagueAdminStatus } from "@/hooks/league/useLeagueAdminStatus"; import { useProtestDetail } from "@/hooks/league/useProtestDetail"; import { routes } from '@/lib/routing/RouteConfig'; import { ProtestDetailTemplate } from '@/templates/ProtestDetailTemplate'; import { ProtestDecisionCommandModel } from '@/lib/command-models/protests/ProtestDecisionCommandModel'; import { ClientWrapperProps } from '@/lib/contracts/components/ComponentContracts'; import { ViewData } from '@/lib/contracts/view-data/ViewData'; 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({ viewData: initialViewData }: Partial>) { 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 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 ?? []; 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(() => { 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: any = { 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 { 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: any = { 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 { const result = await protestService.requestDefense({ protestId: protestDetail.protest?.id || protestDetail.protestId, stewardId: currentDriverId, }); if (result.isErr()) { throw new Error(result.getError().message); } 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 }; } }; return ( ); }