wip league admin tools

This commit is contained in:
2025-12-28 12:04:12 +01:00
parent 5dc8c2399c
commit 6edf12fda8
401 changed files with 15365 additions and 6047 deletions

View File

@@ -5,11 +5,9 @@ import Card from '@/components/ui/Card';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
import { LeagueRoleUtility } from '@/lib/utilities/LeagueRoleUtility';
import { useServices } from '@/lib/services/ServiceProvider';
import { ProtestViewModel } from '@/lib/view-models/ProtestViewModel';
import { RaceViewModel } from '@/lib/view-models/RaceViewModel';
import type { ProtestDetailViewModel } from '@/lib/view-models/ProtestDetailViewModel';
import { ProtestDriverViewModel } from '@/lib/view-models/ProtestDriverViewModel';
import { ProtestDecisionCommandModel } from '@/lib/command-models/protests/ProtestDecisionCommandModel';
import type { PenaltyTypesReferenceDTO, PenaltyValueKindDTO } from '@/lib/types/PenaltyTypesReferenceDTO';
import {
AlertCircle,
AlertTriangle,
@@ -99,54 +97,18 @@ const PENALTY_UI: Record<string, PenaltyUiConfig> = {
},
};
function getPenaltyValueLabel(valueKind: PenaltyValueKindDTO): string {
switch (valueKind) {
case 'seconds':
return 'seconds';
case 'grid_positions':
return 'positions';
case 'points':
return 'points';
case 'races':
return 'races';
case 'none':
return '';
}
}
function getFallbackDefaultValue(valueKind: PenaltyValueKindDTO): number {
switch (valueKind) {
case 'seconds':
return 5;
case 'grid_positions':
return 3;
case 'points':
return 5;
case 'races':
return 1;
case 'none':
return 0;
}
}
export default function ProtestReviewPage() {
const params = useParams();
const router = useRouter();
const leagueId = params.id as string;
const protestId = params.protestId as string;
const currentDriverId = useEffectiveDriverId();
const { protestService, leagueMembershipService, penaltyService } = useServices();
const { leagueStewardingService, protestService, leagueMembershipService } = useServices();
const [protest, setProtest] = useState<ProtestViewModel | null>(null);
const [race, setRace] = useState<RaceViewModel | null>(null);
const [protestingDriver, setProtestingDriver] = useState<ProtestDriverViewModel | null>(null);
const [accusedDriver, setAccusedDriver] = useState<ProtestDriverViewModel | null>(null);
const [detail, setDetail] = useState<ProtestDetailViewModel | null>(null);
const [loading, setLoading] = useState(true);
const [isAdmin, setIsAdmin] = useState(false);
const [penaltyTypesReference, setPenaltyTypesReference] = useState<PenaltyTypesReferenceDTO | null>(null);
const [penaltyTypesLoading, setPenaltyTypesLoading] = useState(false);
// Decision state
const [showDecisionPanel, setShowDecisionPanel] = useState(false);
const [decision, setDecision] = useState<'uphold' | 'dismiss' | null>(null);
@@ -156,24 +118,20 @@ export default function ProtestReviewPage() {
const [submitting, setSubmitting] = useState(false);
const penaltyTypes = useMemo(() => {
const referenceItems = penaltyTypesReference?.penaltyTypes ?? [];
const referenceItems = detail?.penaltyTypes ?? [];
return referenceItems.map((ref) => {
const ui = PENALTY_UI[ref.type] ?? {
label: ref.type.replaceAll('_', ' '),
description: '',
icon: Gavel,
color: 'text-gray-400 bg-gray-500/10 border-gray-500/20',
defaultValue: getFallbackDefaultValue(ref.valueKind),
};
return {
...ref,
...ui,
valueLabel: getPenaltyValueLabel(ref.valueKind),
defaultValue: ui.defaultValue ?? getFallbackDefaultValue(ref.valueKind),
icon: ui.icon,
color: ui.color,
};
});
}, [penaltyTypesReference]);
}, [detail?.penaltyTypes]);
const selectedPenalty = useMemo(() => {
return penaltyTypes.find((p) => p.type === penaltyType);
@@ -195,15 +153,14 @@ export default function ProtestReviewPage() {
async function loadProtest() {
setLoading(true);
try {
const protestData = await protestService.getProtestById(leagueId, protestId);
if (!protestData) {
throw new Error('Protest not found');
}
const protestDetail = await leagueStewardingService.getProtestDetailViewModel(leagueId, protestId);
setProtest(protestData.protest);
setRace(protestData.race);
setProtestingDriver(protestData.protestingDriver);
setAccusedDriver(protestData.accusedDriver);
setDetail(protestDetail);
if (protestDetail.initialPenaltyType) {
setPenaltyType(protestDetail.initialPenaltyType);
setPenaltyValue(protestDetail.initialPenaltyValue);
}
} catch (err) {
console.error('Failed to load protest:', err);
alert('Failed to load protest details');
@@ -216,43 +173,18 @@ export default function ProtestReviewPage() {
if (isAdmin) {
loadProtest();
}
}, [protestId, leagueId, isAdmin, router, protestService]);
useEffect(() => {
async function loadPenaltyTypes() {
if (!isAdmin) return;
if (penaltyTypesReference) return;
setPenaltyTypesLoading(true);
try {
const ref = await penaltyService.getPenaltyTypesReference();
setPenaltyTypesReference(ref);
const hasSelected = ref.penaltyTypes.some((p) => p.type === penaltyType);
const [first] = ref.penaltyTypes;
if (!hasSelected && first) {
setPenaltyType(first.type);
setPenaltyValue(PENALTY_UI[first.type]?.defaultValue ?? getFallbackDefaultValue(first.valueKind));
}
} catch (err) {
console.error('Failed to load penalty types reference:', err);
} finally {
setPenaltyTypesLoading(false);
}
}
loadPenaltyTypes();
}, [isAdmin, penaltyService, penaltyTypesReference, penaltyType]);
}, [protestId, leagueId, isAdmin, router, leagueStewardingService]);
const handleSubmitDecision = async () => {
if (!decision || !stewardNotes.trim() || !protest) return;
if (penaltyTypesLoading) return;
if (!decision || !stewardNotes.trim() || !detail) return;
setSubmitting(true);
try {
const defaultUpheldReason = penaltyTypesReference?.defaultReasons?.upheld;
const defaultDismissedReason = penaltyTypesReference?.defaultReasons?.dismissed;
const protest = detail.protest;
const defaultUpheldReason = detail.defaultReasons?.upheld;
const defaultDismissedReason = detail.defaultReasons?.dismissed;
if (decision === 'uphold') {
const requiresValue = selectedPenalty?.requiresValue ?? true;
@@ -287,7 +219,7 @@ export default function ProtestReviewPage() {
await protestService.applyPenalty(penaltyCommand);
} else {
const warningRef = penaltyTypesReference?.penaltyTypes.find((p) => p.type === 'warning');
const warningRef = detail.penaltyTypes.find((p) => p.type === 'warning');
const requiresValue = warningRef?.requiresValue ?? false;
const commandModel = new ProtestDecisionCommandModel({
@@ -330,12 +262,12 @@ export default function ProtestReviewPage() {
};
const handleRequestDefense = async () => {
if (!protest) return;
if (!detail) return;
try {
// Request defense
await protestService.requestDefense({
protestId: protest.id,
protestId: detail.protest.id,
stewardId: currentDriverId,
});
@@ -379,7 +311,7 @@ export default function ProtestReviewPage() {
);
}
if (loading || !protest || !race) {
if (loading || !detail) {
return (
<Card>
<div className="text-center py-12">
@@ -389,6 +321,11 @@ export default function ProtestReviewPage() {
);
}
const protest = detail.protest;
const race = detail.race;
const protestingDriver = detail.protestingDriver;
const accusedDriver = detail.accusedDriver;
const statusConfig = getStatusConfig(protest.status);
const StatusIcon = statusConfig.icon;
const isPending = protest.status === 'pending';