Files
gridpilot.gg/apps/website/client-wrapper/LeagueAdminSchedulePageClient.tsx
2026-01-19 18:34:01 +01:00

249 lines
7.6 KiB
TypeScript

'use client';
import {
createRaceAction,
deleteRaceAction,
publishScheduleAction,
unpublishScheduleAction,
updateRaceAction
} from '@/app/actions/leagueScheduleActions';
import { StatefulPageWrapper } from '@/components/shared/state/StatefulPageWrapper';
import { ConfirmDialog } from '@/components/shared/ConfirmDialog';
import { Stack } from '@/ui/Stack';
import { Card } from '@/ui/Card';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import {
useLeagueAdminSchedule,
useLeagueAdminStatus,
useLeagueSeasons
} from "@/hooks/league/useLeagueScheduleAdminPageData";
import { useEffectiveDriverId } from "@/hooks/useEffectiveDriverId";
import { RaceScheduleCommandModel } from '@/lib/command-models/leagues/RaceScheduleCommandModel';
import { LeagueAdminScheduleTemplate } from '@/templates/LeagueAdminScheduleTemplate';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
export function LeagueAdminSchedulePageClient() {
const params = useParams();
const leagueId = params.id as string;
const currentDriverId = useEffectiveDriverId() || '';
const router = useRouter();
// Form state
const [seasonId, setSeasonId] = useState<string>('');
const [form, setForm] = useState(() => new RaceScheduleCommandModel());
const [editingRaceId, setEditingRaceId] = useState<string | null>(null);
// Action state
const [isPublishing, setIsPublishing] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [deletingRaceId, setDeletingRaceId] = useState<string | null>(null);
const [raceToDelete, setRaceToDelete] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
// Check admin status using domain hook
const { data: isAdmin, isLoading: isAdminLoading } = useLeagueAdminStatus(leagueId, currentDriverId);
// Load seasons using domain hook
const { data: seasonsData, isLoading: seasonsLoading } = useLeagueSeasons(leagueId, !!isAdmin);
// Auto-select season
const selectedSeasonId = seasonId || (seasonsData && seasonsData.length > 0
? (seasonsData.find((s) => s.status === 'active') ?? seasonsData[0])?.seasonId || ''
: '');
// Load schedule using domain hook
const { data: schedule, isLoading: scheduleLoading } = useLeagueAdminSchedule(leagueId, selectedSeasonId, !!isAdmin);
// Handlers
const handleSeasonChange = (newSeasonId: string) => {
setSeasonId(newSeasonId);
setEditingRaceId(null);
setForm(new RaceScheduleCommandModel());
};
const handlePublishToggle = async () => {
if (!schedule || !selectedSeasonId) return;
setIsPublishing(true);
setError(null);
try {
const result = schedule.published
? await unpublishScheduleAction(leagueId, selectedSeasonId)
: await publishScheduleAction(leagueId, selectedSeasonId);
if (result.isOk()) {
router.refresh();
} else {
setError(result.getError());
}
} finally {
setIsPublishing(false);
}
};
const handleAddOrSave = async () => {
if (!selectedSeasonId) return;
const validationErrors = form.validate();
if (Object.keys(validationErrors).length > 0) {
return;
}
setIsSaving(true);
setError(null);
try {
const result = !editingRaceId
? await createRaceAction(leagueId, selectedSeasonId, form.toCommand())
: await updateRaceAction(leagueId, selectedSeasonId, editingRaceId, form.toCommand());
if (result.isOk()) {
// Reset form
setForm(new RaceScheduleCommandModel());
setEditingRaceId(null);
router.refresh();
} else {
setError(result.getError());
}
} finally {
setIsSaving(false);
}
};
const handleEdit = (raceId: string) => {
if (!schedule) return;
const race = schedule.races.find((r) => r.id === raceId);
if (!race) return;
setEditingRaceId(raceId);
setForm(new RaceScheduleCommandModel({
track: race.track || '',
car: race.car || '',
scheduledAtIso: race.scheduledAt.toISOString(),
}));
};
const handleDelete = (raceId: string) => {
setRaceToDelete(raceId);
};
const confirmDelete = async () => {
if (!selectedSeasonId || !raceToDelete) return;
setDeletingRaceId(raceToDelete);
setError(null);
try {
const result = await deleteRaceAction(leagueId, selectedSeasonId, raceToDelete);
if (result.isOk()) {
router.refresh();
setRaceToDelete(null);
} else {
setError(result.getError());
}
} finally {
setDeletingRaceId(null);
}
};
const handleCancelEdit = () => {
setEditingRaceId(null);
setForm(new RaceScheduleCommandModel());
};
// Derived states
const isLoading = isAdminLoading || seasonsLoading || scheduleLoading;
// Prepare template data
const templateData = schedule && seasonsData && selectedSeasonId
? {
published: schedule.published,
races: schedule.races.map(r => ({
id: r.id,
name: r.name,
track: r.track || '',
car: r.car || '',
scheduledAt: r.scheduledAt.toISOString(),
})),
seasons: seasonsData.map(s => ({
seasonId: s.seasonId,
name: s.name,
})),
seasonId: selectedSeasonId,
}
: undefined;
// Render admin access required if not admin
if (!isLoading && !isAdmin) {
return (
<Stack gap={6}>
<Card>
<Box p={6} textAlign="center">
<Heading level={3}>Admin Access Required</Heading>
<Box mt={2}>
<Text size="sm" color="text-gray-400">Only league admins can manage the schedule.</Text>
</Box>
</Box>
</Card>
</Stack>
);
}
return (
<StatefulPageWrapper
data={templateData}
isLoading={isLoading}
error={null}
Template={({ viewData }) => (
<>
<LeagueAdminScheduleTemplate
viewData={viewData}
onSeasonChange={handleSeasonChange}
onPublishToggle={handlePublishToggle}
onAddOrSave={handleAddOrSave}
onEdit={handleEdit}
onDelete={handleDelete}
onCancelEdit={handleCancelEdit}
track={form.track}
car={form.car}
scheduledAtIso={form.scheduledAtIso}
editingRaceId={editingRaceId}
isPublishing={isPublishing}
isSaving={isSaving}
isDeleting={deletingRaceId}
error={error}
setTrack={(val) => {
form.track = val;
setForm(new RaceScheduleCommandModel(form.toCommand()));
}}
setCar={(val) => {
form.car = val;
setForm(new RaceScheduleCommandModel(form.toCommand()));
}}
setScheduledAtIso={(val) => {
form.scheduledAtIso = val;
setForm(new RaceScheduleCommandModel(form.toCommand()));
}}
/>
<ConfirmDialog
isOpen={!!raceToDelete}
onClose={() => setRaceToDelete(null)}
onConfirm={confirmDelete}
title="Delete Race"
description="Are you sure you want to delete this race? This will remove it from the schedule and cannot be undone."
confirmLabel="Delete Race"
variant="danger"
isLoading={!!deletingRaceId}
/>
</>
)}
loading={{ variant: 'full-screen', message: 'Loading schedule admin...' }}
empty={{
title: 'No schedule data available',
description: 'Unable to load schedule administration data',
}}
/>
);
}