Files
gridpilot.gg/apps/website/client-wrapper/ProtestDetailPageClient.tsx
2026-01-19 02:14:53 +01:00

272 lines
9.2 KiB
TypeScript

'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<string, any> = {
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<ClientWrapperProps<ViewData>>) {
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<string>('time_penalty');
const [penaltyValue, setPenaltyValue] = useState<number>(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 (
<ProtestDetailTemplate
viewData={{}}
protestDetail={protestDetail}
leagueId={leagueId}
showDecisionPanel={showDecisionPanel}
setShowDecisionPanel={setShowDecisionPanel}
decision={decision}
setDecision={setDecision}
penaltyType={penaltyType}
setPenaltyType={setPenaltyType}
penaltyValue={penaltyValue}
setPenaltyValue={setPenaltyValue}
stewardNotes={stewardNotes}
setStewardNotes={setStewardNotes}
submitting={submitting}
newComment={newComment}
setNewComment={setNewComment}
penaltyTypes={penaltyTypes}
selectedPenalty={selectedPenalty}
onSubmitDecision={handleSubmitDecision}
onRequestDefense={handleRequestDefense}
getStatusConfig={getStatusConfig}
/>
);
}