Files
gridpilot.gg/apps/website/app/leagues/[id]/schedule/admin/page.tsx
2025-12-28 12:04:12 +01:00

331 lines
11 KiB
TypeScript

'use client';
import Card from '@/components/ui/Card';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
import type { LeagueAdminScheduleViewModel } from '@/lib/view-models/LeagueAdminScheduleViewModel';
import type { LeagueSeasonSummaryViewModel } from '@/lib/view-models/LeagueSeasonSummaryViewModel';
import { useServices } from '@/lib/services/ServiceProvider';
import { LeagueRoleUtility } from '@/lib/utilities/LeagueRoleUtility';
import { useParams } from 'next/navigation';
import { useEffect, useMemo, useState } from 'react';
export default function LeagueAdminSchedulePage() {
const params = useParams();
const leagueId = params.id as string;
const currentDriverId = useEffectiveDriverId();
const { leagueService, leagueMembershipService } = useServices();
const [isAdmin, setIsAdmin] = useState(false);
const [membershipLoading, setMembershipLoading] = useState(true);
const [seasons, setSeasons] = useState<LeagueSeasonSummaryViewModel[]>([]);
const [seasonId, setSeasonId] = useState<string>('');
const [schedule, setSchedule] = useState<LeagueAdminScheduleViewModel | null>(null);
const [loading, setLoading] = useState(false);
const [track, setTrack] = useState('');
const [car, setCar] = useState('');
const [scheduledAtIso, setScheduledAtIso] = useState('');
const [editingRaceId, setEditingRaceId] = useState<string | null>(null);
const isEditing = editingRaceId !== null;
const publishedLabel = schedule?.published ? 'Published' : 'Unpublished';
const selectedSeasonLabel = useMemo(() => {
const selected = seasons.find((s) => s.seasonId === seasonId);
return selected?.name ?? seasonId;
}, [seasons, seasonId]);
const loadSchedule = async (leagueIdToLoad: string, seasonIdToLoad: string) => {
setLoading(true);
try {
const vm = await leagueService.getAdminSchedule(leagueIdToLoad, seasonIdToLoad);
setSchedule(vm);
} finally {
setLoading(false);
}
};
useEffect(() => {
async function checkAdmin() {
setMembershipLoading(true);
try {
await leagueMembershipService.fetchLeagueMemberships(leagueId);
} finally {
const membership = leagueMembershipService.getMembership(leagueId, currentDriverId);
setIsAdmin(membership ? LeagueRoleUtility.isLeagueAdminOrHigherRole(membership.role) : false);
setMembershipLoading(false);
}
}
checkAdmin();
}, [leagueId, currentDriverId, leagueMembershipService]);
useEffect(() => {
async function loadSeasons() {
const loaded = await leagueService.getLeagueSeasonSummaries(leagueId);
setSeasons(loaded);
if (loaded.length > 0) {
const active = loaded.find((s) => s.status === 'active') ?? loaded[0];
setSeasonId(active?.seasonId ?? '');
}
}
if (isAdmin) {
loadSeasons();
}
}, [leagueId, isAdmin, leagueService]);
useEffect(() => {
if (!isAdmin) return;
if (!seasonId) return;
loadSchedule(leagueId, seasonId);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [leagueId, seasonId, isAdmin]);
const handlePublishToggle = async () => {
if (!schedule) return;
if (schedule.published) {
const vm = await leagueService.unpublishAdminSchedule(leagueId, seasonId);
setSchedule(vm);
return;
}
const vm = await leagueService.publishAdminSchedule(leagueId, seasonId);
setSchedule(vm);
};
const handleAddOrSave = async () => {
if (!seasonId) return;
if (!scheduledAtIso) return;
if (!isEditing) {
const vm = await leagueService.createAdminScheduleRace(leagueId, seasonId, {
track,
car,
scheduledAtIso,
});
setSchedule(vm);
setTrack('');
setCar('');
setScheduledAtIso('');
return;
}
const vm = await leagueService.updateAdminScheduleRace(leagueId, seasonId, editingRaceId, {
...(track ? { track } : {}),
...(car ? { car } : {}),
...(scheduledAtIso ? { scheduledAtIso } : {}),
});
setSchedule(vm);
setEditingRaceId(null);
};
const handleEdit = (raceId: string) => {
if (!schedule) return;
const race = schedule.races.find((r) => r.id === raceId);
if (!race) return;
setEditingRaceId(raceId);
setTrack('');
setCar('');
setScheduledAtIso(race.scheduledAt.toISOString());
};
const handleDelete = async (raceId: string) => {
const confirmed = window.confirm('Delete this race?');
if (!confirmed) return;
const vm = await leagueService.deleteAdminScheduleRace(leagueId, seasonId, raceId);
setSchedule(vm);
};
if (membershipLoading) {
return (
<Card>
<div className="py-6 text-sm text-gray-400">Loading</div>
</Card>
);
}
if (!isAdmin) {
return (
<Card>
<div className="text-center py-12">
<h3 className="text-lg font-medium text-white mb-2">Admin Access Required</h3>
<p className="text-sm text-gray-400">Only league admins can manage the schedule.</p>
</div>
</Card>
);
}
return (
<div className="space-y-6">
<Card>
<div className="space-y-4">
<div>
<h1 className="text-2xl font-bold text-white">Schedule Admin</h1>
<p className="text-sm text-gray-400">Create, edit, and publish season races.</p>
</div>
<div className="flex flex-col gap-2">
<label className="text-sm text-gray-300" htmlFor="seasonId">
Season
</label>
{seasons.length > 0 ? (
<select
id="seasonId"
value={seasonId}
onChange={(e) => setSeasonId(e.target.value)}
className="bg-iron-gray text-white px-3 py-2 rounded"
>
{seasons.map((s) => (
<option key={s.seasonId} value={s.seasonId}>
{s.name}
</option>
))}
</select>
) : (
<input
id="seasonId"
value={seasonId}
onChange={(e) => setSeasonId(e.target.value)}
className="bg-iron-gray text-white px-3 py-2 rounded"
placeholder="season-id"
/>
)}
<p className="text-xs text-gray-500">Selected: {selectedSeasonLabel}</p>
</div>
<div className="flex items-center justify-between gap-3">
<p className="text-sm text-gray-300">
Status: <span className="font-medium text-white">{publishedLabel}</span>
</p>
<button
type="button"
onClick={handlePublishToggle}
disabled={!schedule}
className="px-3 py-1.5 rounded bg-primary-blue text-white disabled:opacity-50"
>
{schedule?.published ? 'Unpublish' : 'Publish'}
</button>
</div>
<div className="border-t border-charcoal-outline pt-4 space-y-3">
<h2 className="text-lg font-semibold text-white">{isEditing ? 'Edit race' : 'Add race'}</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<div className="flex flex-col gap-1">
<label htmlFor="track" className="text-sm text-gray-300">
Track
</label>
<input
id="track"
value={track}
onChange={(e) => setTrack(e.target.value)}
className="bg-iron-gray text-white px-3 py-2 rounded"
/>
</div>
<div className="flex flex-col gap-1">
<label htmlFor="car" className="text-sm text-gray-300">
Car
</label>
<input
id="car"
value={car}
onChange={(e) => setCar(e.target.value)}
className="bg-iron-gray text-white px-3 py-2 rounded"
/>
</div>
<div className="flex flex-col gap-1">
<label htmlFor="scheduledAtIso" className="text-sm text-gray-300">
Scheduled At (ISO)
</label>
<input
id="scheduledAtIso"
value={scheduledAtIso}
onChange={(e) => setScheduledAtIso(e.target.value)}
className="bg-iron-gray text-white px-3 py-2 rounded"
placeholder="2025-01-01T12:00:00.000Z"
/>
</div>
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={handleAddOrSave}
className="px-3 py-1.5 rounded bg-primary-blue text-white"
>
{isEditing ? 'Save' : 'Add race'}
</button>
{isEditing && (
<button
type="button"
onClick={() => setEditingRaceId(null)}
className="px-3 py-1.5 rounded bg-iron-gray text-gray-200"
>
Cancel
</button>
)}
</div>
</div>
<div className="border-t border-charcoal-outline pt-4 space-y-3">
<h2 className="text-lg font-semibold text-white">Races</h2>
{loading ? (
<div className="py-4 text-sm text-gray-400">Loading schedule</div>
) : schedule?.races.length ? (
<div className="space-y-2">
{schedule.races.map((race) => (
<div
key={race.id}
className="flex items-center justify-between gap-3 bg-deep-graphite border border-charcoal-outline rounded p-3"
>
<div className="min-w-0">
<p className="text-white font-medium truncate">{race.name}</p>
<p className="text-xs text-gray-400 truncate">{race.scheduledAt.toISOString()}</p>
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => handleEdit(race.id)}
className="px-3 py-1.5 rounded bg-iron-gray text-gray-200"
>
Edit
</button>
<button
type="button"
onClick={() => handleDelete(race.id)}
className="px-3 py-1.5 rounded bg-iron-gray text-gray-200"
>
Delete
</button>
</div>
</div>
))}
</div>
) : (
<div className="py-4 text-sm text-gray-500">No races yet.</div>
)}
</div>
</div>
</Card>
</div>
);
}