This commit is contained in:
2025-12-13 11:43:09 +01:00
parent 4b6fc668b5
commit bb0497f429
38 changed files with 3838 additions and 55 deletions

View File

@@ -9,8 +9,9 @@ import Heading from '@/components/ui/Heading';
import Breadcrumbs from '@/components/layout/Breadcrumbs';
import FileProtestModal from '@/components/races/FileProtestModal';
import SponsorInsightsCard, { useSponsorMode, MetricBuilders, SlotTemplates } from '@/components/sponsors/SponsorInsightsCard';
import { getGetRaceDetailUseCase, getRegisterForRaceUseCase, getWithdrawFromRaceUseCase, getCancelRaceUseCase } from '@/lib/di-container';
import { getGetRaceDetailUseCase, getRegisterForRaceUseCase, getWithdrawFromRaceUseCase, getCancelRaceUseCase, getCompleteRaceUseCase } from '@/lib/di-container';
import { useEffectiveDriverId } from '@/lib/currentDriver';
import { getMembership, isOwnerOrAdmin } from '@/lib/leagueMembership';
import type {
RaceDetailViewModel,
RaceDetailEntryViewModel,
@@ -49,6 +50,7 @@ export default function RaceDetailPage() {
const [ratingChange, setRatingChange] = useState<number | null>(null);
const [animatedRatingChange, setAnimatedRatingChange] = useState(0);
const [showProtestModal, setShowProtestModal] = useState(false);
const [membership, setMembership] = useState<any>(null);
const currentDriverId = useEffectiveDriverId();
const isSponsorMode = useSponsorMode();
@@ -65,6 +67,13 @@ export default function RaceDetailPage() {
throw new Error('Race detail not available');
}
setViewModel(vm);
// Fetch league membership for admin controls
if (vm.league) {
const leagueMembership = getMembership(vm.league.id, currentDriverId);
setMembership(leagueMembership);
}
const userResultRatingChange = vm.userResult?.ratingChange ?? null;
setRatingChange(userResultRatingChange);
if (userResultRatingChange === null) {
@@ -529,7 +538,7 @@ export default function RaceDetailPage() {
{animatedRatingChange > 0 ? '+' : ''}
{animatedRatingChange}
</div>
<div className="text-xs text-gray-400 mt-0.5">iRating</div>
<div className="text-xs text-gray-400 mt-0.5">Rating</div>
</div>
)}
@@ -717,11 +726,11 @@ export default function RaceDetailPage() {
className={`
flex items-center justify-center w-8 h-8 rounded-lg font-bold text-sm
${
index === 0
race.status === 'completed' && index === 0
? 'bg-yellow-500/20 text-yellow-400'
: index === 1
: race.status === 'completed' && index === 1
? 'bg-gray-400/20 text-gray-300'
: index === 2
: race.status === 'completed' && index === 2
? 'bg-amber-600/20 text-amber-500'
: 'bg-iron-gray text-gray-500'
}
@@ -892,9 +901,55 @@ export default function RaceDetailPage() {
<Scale className="w-4 h-4" />
Stewarding
</Button>
{membership && isOwnerOrAdmin(viewModel.league?.id || '', currentDriverId) && (
<>
<Button
variant="outline"
className="w-full flex items-center justify-center gap-2"
onClick={async () => {
const confirmed = window.confirm(
'Re-open this race? This will allow re-registration and re-running. Results will be archived.'
);
if (!confirmed) return;
// TODO: Implement re-open race functionality
alert('Re-open race functionality not yet implemented');
}}
>
<PlayCircle className="w-4 h-4" />
Re-open Race
</Button>
</>
)}
</>
)}
{race.status === 'running' && membership && isOwnerOrAdmin(viewModel.league?.id || '', currentDriverId) && (
<Button
variant="primary"
className="w-full flex items-center justify-center gap-2"
onClick={async () => {
const confirmed = window.confirm(
'Are you sure you want to end this race and process results?\n\nThis will mark the race as completed and calculate final standings.'
);
if (!confirmed) return;
try {
const completeRace = getCompleteRaceUseCase();
await completeRace.execute({ raceId: race.id });
// Reload race data to reflect the completed race
await loadRaceData();
} catch (err) {
alert(err instanceof Error ? err.message : 'Failed to complete race');
}
}}
>
<CheckCircle2 className="w-4 h-4" />
End Race & Process Results
</Button>
)}
{race.status === 'scheduled' && (
<Button
variant="secondary"