"use client"; import { useMemo, useState } from "react"; import { usePenaltyTypesReference } from "@/hooks/usePenaltyTypesReference"; import type { PenaltyValueKindDTO } from "@/lib/types/PenaltyTypesReferenceDTO"; import { ProtestViewModel } from "../../lib/view-models/ProtestViewModel"; import { Modal } from "@/ui/Modal"; import { Button } from "@/ui/Button"; import { Card } from "@/ui/Card"; import { Box } from "@/ui/Box"; import { Text } from "@/ui/Text"; import { Stack } from "@/ui/Stack"; import { Heading } from "@/ui/Heading"; import { Icon } from "@/ui/Icon"; import { TextArea } from "@/ui/TextArea"; import { Input } from "@/ui/Input"; import { AlertCircle, Video, Clock, Grid3x3, TrendingDown, CheckCircle, XCircle, AlertTriangle, ShieldAlert, Ban, DollarSign, FileWarning, type LucideIcon, } from "lucide-react"; type PenaltyType = string; interface ReviewProtestModalProps { protest: ProtestViewModel | null; onClose: () => void; onAccept: ( protestId: string, penaltyType: PenaltyType, penaltyValue: number, stewardNotes: string ) => Promise; onReject: (protestId: string, stewardNotes: string) => Promise; } export function ReviewProtestModal({ protest, onClose, onAccept, onReject, }: ReviewProtestModalProps) { const [decision, setDecision] = useState<"accept" | "reject" | null>(null); const [penaltyType, setPenaltyType] = useState("time_penalty"); const [penaltyValue, setPenaltyValue] = useState(5); const [stewardNotes, setStewardNotes] = useState(""); const [submitting, setSubmitting] = useState(false); const [showConfirmation, setShowConfirmation] = useState(false); const { data: penaltyTypesReferenceResult, isLoading: penaltyTypesLoading } = usePenaltyTypesReference(); const penaltyOptions = useMemo(() => { const refs = penaltyTypesReferenceResult?.isOk() ? penaltyTypesReferenceResult.unwrap().penaltyTypes : []; // eslint-disable-next-line @typescript-eslint/no-explicit-any return refs.map((ref: any) => ({ type: ref.type as PenaltyType, name: getPenaltyName(ref.type), requiresValue: ref.requiresValue, valueLabel: getPenaltyValueLabel(ref.valueKind), defaultValue: getPenaltyDefaultValue(ref.type, ref.valueKind), Icon: getPenaltyIcon(ref.type), colorClass: getPenaltyColor(ref.type), })); }, [penaltyTypesReferenceResult]); const selectedPenalty = useMemo(() => { // eslint-disable-next-line @typescript-eslint/no-explicit-any return penaltyOptions.find((p: any) => p.type === penaltyType); }, [penaltyOptions, penaltyType]); if (!protest) return null; const handleSubmit = async () => { if (!decision || !stewardNotes.trim()) return; setSubmitting(true); try { if (decision === "accept") { await onAccept(protest.id, penaltyType, penaltyValue, stewardNotes); } else { await onReject(protest.id, stewardNotes); } onClose(); } catch (err) { alert(err instanceof Error ? err.message : "Failed to submit decision"); } finally { setSubmitting(false); setShowConfirmation(false); } }; function getPenaltyIcon(type: PenaltyType) { switch (type) { case "time_penalty": return Clock; case "grid_penalty": return Grid3x3; case "points_deduction": return TrendingDown; case "disqualification": return XCircle; case "warning": return AlertTriangle; case "license_points": return ShieldAlert; case "probation": return FileWarning; case "fine": return DollarSign; case "race_ban": return Ban; default: return AlertCircle; } } function getPenaltyName(type: PenaltyType) { switch (type) { case "time_penalty": return "Time Penalty"; case "grid_penalty": return "Grid Penalty"; case "points_deduction": return "Points Deduction"; case "disqualification": return "Disqualification"; case "warning": return "Warning"; case "license_points": return "License Points"; case "probation": return "Probation"; case "fine": return "Fine"; case "race_ban": return "Race Ban"; default: return type.replaceAll("_", " "); } } function getPenaltyValueLabel(valueKind: PenaltyValueKindDTO): string { switch (valueKind) { case "seconds": return "seconds"; case "grid_positions": return "grid positions"; case "points": return "points"; case "races": return "races"; case "none": return ""; } } function getPenaltyDefaultValue(type: PenaltyType, valueKind: PenaltyValueKindDTO): number { if (type === "license_points") return 2; if (type === "race_ban") return 1; switch (valueKind) { case "seconds": return 5; case "grid_positions": return 3; case "points": return 5; case "races": return 1; case "none": return 0; } } function getPenaltyColor(type: PenaltyType) { switch (type) { case "time_penalty": return "text-blue-400 bg-blue-500/10 border-blue-500/30"; case "grid_penalty": return "text-purple-400 bg-purple-500/10 border-purple-500/30"; case "points_deduction": return "text-red-400 bg-red-500/10 border-red-500/30"; case "disqualification": return "text-red-500 bg-red-500/10 border-red-500/30"; case "warning": return "text-yellow-400 bg-yellow-500/10 border-yellow-500/30"; case "license_points": return "text-orange-400 bg-orange-500/10 border-orange-500/30"; case "probation": return "text-amber-400 bg-amber-500/10 border-amber-500/30"; case "fine": return "text-green-400 bg-green-500/10 border-green-500/30"; case "race_ban": return "text-red-600 bg-red-600/10 border-red-600/30"; default: return "text-warning-amber bg-warning-amber/10 border-warning-amber/30"; } } if (showConfirmation) { return ( setShowConfirmation(false)}> {decision === "accept" ? ( ) : ( )} Confirm Decision {decision === "accept" ? (selectedPenalty?.requiresValue ? `Issue ${penaltyValue} ${selectedPenalty.valueLabel} penalty?` : `Issue ${selectedPenalty?.name ?? penaltyType} penalty?`) : "Reject this protest?"} {stewardNotes} ); } return ( Review Protest Protest #{protest.id.substring(0, 8)} Filed Date {new Date(protest.filedAt || protest.submittedAt).toLocaleString()} Incident Lap Lap {protest.incident?.lap || 'N/A'} Status {protest.status} Description {protest.incident?.description || protest.description} {protest.comment && ( Additional Comment {protest.comment} )} {protest.proofVideoUrl && ( Evidence View video evidence )} Stewarding Decision {decision === "accept" && ( Penalty Type {penaltyTypesLoading ? ( Loading penalty types… ) : ( {penaltyOptions.map(({ type, name, Icon: PenaltyIcon, colorClass, defaultValue }: { type: string; name: string; Icon: LucideIcon; colorClass: string; defaultValue: number }) => { const isSelected = penaltyType === type; return ( { setPenaltyType(type); setPenaltyValue(defaultValue); }} p={3} rounded="lg" border borderWidth={isSelected ? "2px" : "1px"} transition borderColor={isSelected ? undefined : "border-charcoal-outline"} bg={isSelected ? undefined : "bg-iron-gray/50"} hoverBorderColor={!isSelected ? "border-gray-600" : undefined} // eslint-disable-next-line gridpilot-rules/component-classification className={isSelected ? colorClass : ""} > {name} ); })} )} {selectedPenalty?.requiresValue && ( Penalty Value ({selectedPenalty.valueLabel}) ) => setPenaltyValue(Number(e.target.value))} min={1} /> )} )} Steward Notes *