'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'}
)}
);
}}
);
}