Compare commits

..

5 Commits

Author SHA1 Message Date
Guillermo Pages c9f153c345 Add DELETE endpoint for competitions (draft/cancelled only)
continuous-integration/drone/push Build is passing Details
2 weeks ago
Guillermo Pages 447b2125a8 feat: add default stage configs when creating competitions from scratch
When creating a competition without a template, the modal now provides
sensible default configurations for each competition type:

- league: round_robin stage with standard points (3/1/0)
- tournament: single_elimination stage
- challenge: rating_delta_challenge stage with sum metric (leaderboard)
- hybrid: group stage + knockout stage

This ensures stages are created automatically instead of leaving
competitions with 'No stages configured yet'.
2 weeks ago
Guillermo Pages fbf4bdd55b fix: update remaining navigation links from /admin/clubs to /admin/facilities 2 weeks ago
Guillermo Pages 2899e1fd58 refactor: rename clubs to facilities in admin routes
- Rename /admin/clubs to /admin/facilities
- Rename [club_id] param to [facility_id]
- Update all component names: Club* -> Facility*
- Update CompetitionCard to use correct admin route with locale
- Keep API function names as-is (getAdminClubDetail) since they match backend
2 weeks ago
Guillermo Pages b91f2d47bc fix: add missing scope and config_snapshot fields to createCompetition
Backend requires scope and config_snapshot when creating competition
without a template. Frontend now:
- Defaults scope to 'facility' when facility_id is provided
- Renames config to config_snapshot for backend compatibility
- Provides empty config_snapshot when no template_id
2 weeks ago

@ -0,0 +1,39 @@
import { spawn } from 'child_process';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
const __dirname = dirname(fileURLToPath(import.meta.url));
const proxies = [
{ name: 'API', script: 'proxy-api.mjs' },
{ name: 'Auth', script: 'proxy-auth.mjs' },
];
const processes = [];
for (const proxy of proxies) {
const proc = spawn('node', [join(__dirname, proxy.script)], {
stdio: 'inherit',
});
proc.on('error', (err) => {
console.error(`[${proxy.name}] Failed to start:`, err);
});
proc.on('exit', (code) => {
console.log(`[${proxy.name}] Exited with code ${code}`);
});
processes.push(proc);
}
// Handle Ctrl+C
process.on('SIGINT', () => {
console.log('\nShutting down proxies...');
for (const proc of processes) {
proc.kill();
}
process.exit(0);
});
console.log('All proxies started. Press Ctrl+C to stop.');

@ -0,0 +1,108 @@
import 'dotenv/config';
import http from 'http';
import https from 'https';
const TARGET = process.env.PROXY_API_TARGET;
const PORT = process.env.PROXY_API_PORT;
const LOCAL_ORIGIN = process.env.PROXY_LOCAL_ORIGIN;
const PROD_ORIGIN = process.env.PROXY_PROD_ORIGIN;
if (!TARGET) throw new Error('PROXY_API_TARGET environment variable is required');
if (!PORT) throw new Error('PROXY_API_PORT environment variable is required');
if (!LOCAL_ORIGIN) throw new Error('PROXY_LOCAL_ORIGIN environment variable is required');
if (!PROD_ORIGIN) throw new Error('PROXY_PROD_ORIGIN environment variable is required');
const parsedPort = parseInt(PORT, 10);
const server = http.createServer((req, res) => {
// Handle preflight OPTIONS requests
if (req.method === 'OPTIONS') {
res.setHeader('Access-Control-Allow-Origin', LOCAL_ORIGIN);
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS, PATCH');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Timezone, X-Locale');
res.writeHead(204);
res.end();
return;
}
// Rewrite origin to match production so backend CORS accepts it
const headers = { ...req.headers, host: TARGET };
if (headers.origin === LOCAL_ORIGIN) {
headers.origin = PROD_ORIGIN;
}
const options = {
hostname: TARGET,
port: 443,
path: req.url,
method: req.method,
headers,
};
const proxyReq = https.request(options, (proxyRes) => {
// Merge CORS headers with response headers
const headers = { ...proxyRes.headers };
headers['access-control-allow-origin'] = LOCAL_ORIGIN;
headers['access-control-allow-credentials'] = 'true';
res.writeHead(proxyRes.statusCode, headers);
proxyRes.pipe(res);
});
proxyReq.on('error', (err) => {
console.error('Proxy error:', err);
res.writeHead(500);
res.end('Proxy error');
});
req.pipe(proxyReq);
});
// Handle WebSocket upgrades for Socket.IO
server.on('upgrade', (req, socket, head) => {
// Rewrite origin to match production so backend CORS accepts it
const headers = { ...req.headers, host: TARGET };
if (headers.origin === LOCAL_ORIGIN) {
headers.origin = PROD_ORIGIN;
}
const options = {
hostname: TARGET,
port: 443,
path: req.url,
method: 'GET',
headers,
};
const proxyReq = https.request(options);
proxyReq.on('upgrade', (proxyRes, proxySocket, proxyHead) => {
let response = 'HTTP/1.1 101 Switching Protocols\r\n' +
'Upgrade: websocket\r\n' +
'Connection: Upgrade\r\n' +
`Sec-WebSocket-Accept: ${proxyRes.headers['sec-websocket-accept']}\r\n`;
// Forward compression extension if backend negotiated it
if (proxyRes.headers['sec-websocket-extensions']) {
response += `Sec-WebSocket-Extensions: ${proxyRes.headers['sec-websocket-extensions']}\r\n`;
}
response += '\r\n';
socket.write(response);
proxySocket.pipe(socket);
socket.pipe(proxySocket);
});
proxyReq.on('error', (err) => {
console.error('WebSocket proxy error:', err);
socket.end();
});
proxyReq.end();
});
server.listen(parsedPort, () => {
console.log(`API Proxy running: localhost:${parsedPort} -> ${TARGET} (HTTP + WebSocket)`);
});

@ -0,0 +1,59 @@
import 'dotenv/config';
import http from 'http';
import https from 'https';
const TARGET = process.env.PROXY_AUTH_TARGET;
const PORT = process.env.PROXY_AUTH_PORT;
const LOCAL_ORIGIN = process.env.PROXY_LOCAL_ORIGIN;
if (!TARGET) throw new Error('PROXY_AUTH_TARGET environment variable is required');
if (!PORT) throw new Error('PROXY_AUTH_PORT environment variable is required');
if (!LOCAL_ORIGIN) throw new Error('PROXY_LOCAL_ORIGIN environment variable is required');
const parsedPort = parseInt(PORT, 10);
const server = http.createServer((req, res) => {
// Handle preflight OPTIONS requests
if (req.method === 'OPTIONS') {
res.setHeader('Access-Control-Allow-Origin', LOCAL_ORIGIN);
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS, PATCH');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Timezone, X-Locale');
res.writeHead(204);
res.end();
return;
}
const options = {
hostname: TARGET,
port: 443,
path: req.url,
method: req.method,
headers: {
...req.headers,
host: TARGET,
},
};
const proxyReq = https.request(options, (proxyRes) => {
// Copy response headers but add CORS
const headers = { ...proxyRes.headers };
headers['access-control-allow-origin'] = LOCAL_ORIGIN;
headers['access-control-allow-credentials'] = 'true';
res.writeHead(proxyRes.statusCode, headers);
proxyRes.pipe(res);
});
proxyReq.on('error', (err) => {
console.error('Proxy error:', err);
res.writeHead(500);
res.end('Proxy error');
});
req.pipe(proxyReq);
});
server.listen(parsedPort, () => {
console.log(`Auth Proxy running: localhost:${parsedPort} -> ${TARGET}`);
});

@ -1,11 +0,0 @@
'use client';
import ClubProfileTab from './tabs/ClubProfileTab';
interface ClubDetailTabsProps {
clubId: number;
}
export default function ClubDetailTabs({ clubId }: ClubDetailTabsProps) {
return <ClubProfileTab clubId={clubId} />;
}

@ -1,13 +0,0 @@
import CompetitionDetailComponent from './CompetitionDetailComponent';
export default async function CompetitionDetailPage({
params,
}: {
params: Promise<{ club_id: string; competition_id: string }>;
}) {
const { club_id, competition_id } = await params;
const clubId = parseInt(club_id, 10);
const competitionId = parseInt(competition_id, 10);
return <CompetitionDetailComponent clubId={clubId} competitionId={competitionId} />;
}

@ -1,12 +0,0 @@
import CompetitionsPageComponent from './CompetitionsPageComponent';
export default async function CompetitionsPage({
params,
}: {
params: Promise<{ club_id: string }>;
}) {
const { club_id } = await params;
const clubId = parseInt(club_id, 10);
return <CompetitionsPageComponent clubId={clubId} />;
}

@ -1,12 +0,0 @@
import SeriesListComponent from './SeriesListComponent';
export default async function SeriesPage({
params,
}: {
params: Promise<{ club_id: string }>;
}) {
const { club_id } = await params;
const clubId = parseInt(club_id, 10);
return <SeriesListComponent clubId={clubId} />;
}

@ -1,12 +0,0 @@
import TemplatesPageComponent from './TemplatesPageComponent';
export default async function TemplatesPage({
params,
}: {
params: Promise<{ club_id: string }>;
}) {
const { club_id } = await params;
const clubId = parseInt(club_id, 10);
return <TemplatesPageComponent clubId={clubId} />;
}

@ -1,12 +0,0 @@
import CourtsComponent from './CourtsComponent';
export default async function CourtsPage({
params
}: {
params: Promise<{ club_id: string }>
}) {
const { club_id } = await params;
const clubId = parseInt(club_id, 10);
return <CourtsComponent clubId={clubId} />;
}

@ -1,12 +0,0 @@
import CreditsManagementComponent from './CreditsManagementComponent';
export default async function CreditsManagementPage({
params
}: {
params: Promise<{ club_id: string }>;
}) {
const { club_id } = await params;
const clubId = parseInt(club_id, 10);
return <CreditsManagementComponent clubId={clubId} />;
}

@ -1,21 +0,0 @@
import AdminAuthGuard from '@/src/components/AdminAuthGuard';
import ClubDetailHeader from '@/src/components/ClubDetailHeader';
export default async function ClubDetailLayout({
children,
params,
}: {
children: React.ReactNode;
params: Promise<{ club_id: string }>;
}) {
const { club_id } = await params;
const clubId = parseInt(club_id, 10);
return (
<AdminAuthGuard>
<ClubDetailHeader clubId={clubId}>
{children}
</ClubDetailHeader>
</AdminAuthGuard>
);
}

@ -1,12 +0,0 @@
import MembersManagementComponent from './MembersManagementComponent';
export default async function MembersManagementPage({
params
}: {
params: Promise<{ club_id: string }>;
}) {
const { club_id } = await params;
const clubId = parseInt(club_id, 10);
return <MembersManagementComponent clubId={clubId} />;
}

@ -1,12 +0,0 @@
import ClubDetailTabs from './ClubDetailTabs';
export default async function AdminClubDetailPage({
params
}: {
params: Promise<{ club_id: string }>
}) {
const { club_id } = await params;
const clubId = parseInt(club_id, 10);
return <ClubDetailTabs clubId={clubId} />;
}

@ -1,12 +0,0 @@
import MembershipPlansComponent from './MembershipPlansComponent';
export default async function MembershipPlansPage({
params
}: {
params: Promise<{ club_id: string }>;
}) {
const { club_id } = await params;
const clubId = parseInt(club_id, 10);
return <MembershipPlansComponent clubId={clubId} />;
}

@ -1,12 +0,0 @@
import FacilitySettingsComponent from './FacilitySettingsComponent';
export default async function FacilitySettingsPage({
params
}: {
params: Promise<{ club_id: string }>;
}) {
const { club_id } = await params;
const clubId = parseInt(club_id, 10);
return <FacilitySettingsComponent clubId={clubId} />;
}

@ -1,12 +0,0 @@
import SlotDefinitionsComponent from './SlotDefinitionsComponent';
export default async function SlotDefinitionsPage({
params
}: {
params: Promise<{ club_id: string }>;
}) {
const { club_id } = await params;
const clubId = parseInt(club_id, 10);
return <SlotDefinitionsComponent clubId={clubId} />;
}

@ -1,12 +0,0 @@
import SlotInstancesComponent from './SlotInstancesComponent';
export default async function SlotInstancesPage({
params
}: {
params: Promise<{ club_id: string }>;
}) {
const { club_id } = await params;
const clubId = parseInt(club_id, 10);
return <SlotInstancesComponent clubId={clubId} />;
}

@ -1,12 +0,0 @@
import TransfersManagementComponent from './TransfersManagementComponent';
export default async function TransfersManagementPage({
params
}: {
params: Promise<{ club_id: string }>;
}) {
const { club_id } = await params;
const clubId = parseInt(club_id, 10);
return <TransfersManagementComponent clubId={clubId} />;
}

@ -7,7 +7,7 @@ import { getAdminClubs } from '@/src/lib/api/admin-clubs';
import type { AdminClubsResponse, AdminApiError } from '@/src/types/admin-api'; import type { AdminClubsResponse, AdminApiError } from '@/src/types/admin-api';
import useTranslation from '@/src/hooks/useTranslation'; import useTranslation from '@/src/hooks/useTranslation';
export default function AdminClubsList() { export default function AdminFacilitiesList() {
const { t, locale } = useTranslation(); const { t, locale } = useTranslation();
const [clubs, setClubs] = useState<AdminClubsResponse | null>(null); const [clubs, setClubs] = useState<AdminClubsResponse | null>(null);
const [error, setError] = useState<AdminApiError | null>(null); const [error, setError] = useState<AdminApiError | null>(null);
@ -140,7 +140,7 @@ export default function AdminClubsList() {
{clubs.map((club) => ( {clubs.map((club) => (
<Link <Link
key={club.facility_id} key={club.facility_id}
href={`/${locale}/admin/clubs/${club.facility_id}`} href={`/${locale}/admin/facilities/${club.facility_id}`}
className="group bg-white border-2 border-slate-200 rounded-2xl p-6 hover:border-slate-300 hover:shadow-xl transition-all duration-200" className="group bg-white border-2 border-slate-200 rounded-2xl p-6 hover:border-slate-300 hover:shadow-xl transition-all duration-200"
> >
<div className="flex items-start justify-between mb-5"> <div className="flex items-start justify-between mb-5">

@ -8,10 +8,10 @@ import type { AdminClubDetail, AdminApiError } from '@/src/types/admin-api';
import useTranslation from '@/src/hooks/useTranslation'; import useTranslation from '@/src/hooks/useTranslation';
interface AdminClubDetailProps { interface AdminClubDetailProps {
clubId: number; facilityId: number;
} }
export default function AdminClubDetailComponent({ clubId }: AdminClubDetailProps) { export default function AdminClubDetailComponent({ facilityId }: AdminClubDetailProps) {
const { t, locale } = useTranslation(); const { t, locale } = useTranslation();
const [clubDetail, setClubDetail] = useState<AdminClubDetail | null>(null); const [clubDetail, setClubDetail] = useState<AdminClubDetail | null>(null);
const [error, setError] = useState<AdminApiError | null>(null); const [error, setError] = useState<AdminApiError | null>(null);
@ -20,7 +20,7 @@ export default function AdminClubDetailComponent({ clubId }: AdminClubDetailProp
useEffect(() => { useEffect(() => {
async function loadClubDetail() { async function loadClubDetail() {
setLoading(true); setLoading(true);
const result = await getAdminClubDetail(clubId); const result = await getAdminClubDetail(facilityId);
if (result.success) { if (result.success) {
setClubDetail(result.data); setClubDetail(result.data);
@ -34,7 +34,7 @@ export default function AdminClubDetailComponent({ clubId }: AdminClubDetailProp
} }
loadClubDetail(); loadClubDetail();
}, [clubId]); }, [facilityId]);
// Loading state // Loading state
if (loading) { if (loading) {
@ -87,7 +87,7 @@ export default function AdminClubDetailComponent({ clubId }: AdminClubDetailProp
{error.detail} {error.detail}
</p> </p>
<Link <Link
href={`/${locale}/admin/clubs`} href={`/${locale}/admin/facilities`}
className="inline-flex items-center px-4 py-2 bg-slate-900 text-white font-semibold rounded-lg hover:bg-slate-800 transition-colors" className="inline-flex items-center px-4 py-2 bg-slate-900 text-white font-semibold rounded-lg hover:bg-slate-800 transition-colors"
> >
<ArrowLeft className="w-4 h-4 mr-2" /> <ArrowLeft className="w-4 h-4 mr-2" />
@ -120,7 +120,7 @@ export default function AdminClubDetailComponent({ clubId }: AdminClubDetailProp
{t('Error code')}: {error.code} {t('Error code')}: {error.code}
</p> </p>
<Link <Link
href={`/${locale}/admin/clubs`} href={`/${locale}/admin/facilities`}
className="inline-flex items-center px-4 py-2 bg-slate-900 text-white font-semibold rounded-lg hover:bg-slate-800 transition-colors" className="inline-flex items-center px-4 py-2 bg-slate-900 text-white font-semibold rounded-lg hover:bg-slate-800 transition-colors"
> >
<ArrowLeft className="w-4 h-4 mr-2" /> <ArrowLeft className="w-4 h-4 mr-2" />
@ -144,7 +144,7 @@ export default function AdminClubDetailComponent({ clubId }: AdminClubDetailProp
{/* Breadcrumb */} {/* Breadcrumb */}
<div className="mb-8"> <div className="mb-8">
<Link <Link
href={`/${locale}/admin/clubs`} href={`/${locale}/admin/facilities`}
className="inline-flex items-center text-slate-600 hover:text-slate-900 font-medium transition-colors" className="inline-flex items-center text-slate-600 hover:text-slate-900 font-medium transition-colors"
> >
<ArrowLeft className="w-4 h-4 mr-2" /> <ArrowLeft className="w-4 h-4 mr-2" />

@ -0,0 +1,11 @@
'use client';
import FacilityProfileTab from './tabs/FacilityProfileTab';
interface FacilityDetailTabsProps {
facilityId: number;
}
export default function FacilityDetailTabs({ facilityId }: FacilityDetailTabsProps) {
return <FacilityProfileTab facilityId={facilityId} />;
}

@ -8,7 +8,7 @@ import CreateCompetitionModal from './CreateCompetitionModal';
import type { CompetitionStatus, CompetitionType } from '@/src/types/competition'; import type { CompetitionStatus, CompetitionType } from '@/src/types/competition';
interface CompetitionsPageComponentProps { interface CompetitionsPageComponentProps {
clubId: number; facilityId: number;
} }
const statusOptions: { value: CompetitionStatus | 'all'; label: string }[] = [ const statusOptions: { value: CompetitionStatus | 'all'; label: string }[] = [
@ -28,12 +28,12 @@ const typeOptions: { value: CompetitionType | 'all'; label: string }[] = [
{ value: 'hybrid', label: 'Hybrid' }, { value: 'hybrid', label: 'Hybrid' },
]; ];
export default function CompetitionsPageComponent({ clubId }: CompetitionsPageComponentProps) { export default function CompetitionsPageComponent({ facilityId }: CompetitionsPageComponentProps) {
const [statusFilter, setStatusFilter] = useState<CompetitionStatus | 'all'>('all'); const [statusFilter, setStatusFilter] = useState<CompetitionStatus | 'all'>('all');
const [typeFilter, setTypeFilter] = useState<CompetitionType | 'all'>('all'); const [typeFilter, setTypeFilter] = useState<CompetitionType | 'all'>('all');
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const { data: competitions, isLoading, error } = useCompetitions(clubId, { const { data: competitions, isLoading, error } = useCompetitions(facilityId, {
status: statusFilter === 'all' ? undefined : statusFilter, status: statusFilter === 'all' ? undefined : statusFilter,
type: typeFilter === 'all' ? undefined : typeFilter, type: typeFilter === 'all' ? undefined : typeFilter,
}); });
@ -110,7 +110,7 @@ export default function CompetitionsPageComponent({ clubId }: CompetitionsPageCo
{/* Competition List */} {/* Competition List */}
<CompetitionList <CompetitionList
competitions={competitions ?? []} competitions={competitions ?? []}
facilityId={clubId} facilityId={facilityId}
isLoading={isLoading} isLoading={isLoading}
emptyMessage="No competitions yet. Create your first competition to get started." emptyMessage="No competitions yet. Create your first competition to get started."
/> />
@ -119,7 +119,7 @@ export default function CompetitionsPageComponent({ clubId }: CompetitionsPageCo
<CreateCompetitionModal <CreateCompetitionModal
isOpen={isCreateModalOpen} isOpen={isCreateModalOpen}
onClose={() => setIsCreateModalOpen(false)} onClose={() => setIsCreateModalOpen(false)}
facilityId={clubId} facilityId={facilityId}
/> />
</div> </div>
); );

@ -59,6 +59,62 @@ const visibilityOptions: { value: CompetitionVisibility; label: string; descript
{ value: 'private', label: 'Private', description: 'Invite only' }, { value: 'private', label: 'Private', description: 'Invite only' },
]; ];
// Default configs for each competition type when creating from scratch
const defaultConfigs: Record<CompetitionType, Record<string, unknown>> = {
league: {
format: {
stages: [
{
type: 'round_robin',
name: 'League Stage',
rounds: 1,
points_win: 3,
points_draw: 1,
points_loss: 0,
},
],
},
},
tournament: {
format: {
stages: [
{
type: 'single_elimination',
name: 'Main Draw',
third_place_match: false,
},
],
},
},
challenge: {
format: {
stages: [
{
type: 'rating_delta_challenge',
name: 'Leaderboard',
metric: 'rating_delta_sum', // Total rating gained
// Alternative metrics: 'rating_delta_avg', 'rating_delta_max', 'matches_played'
},
],
},
},
hybrid: {
format: {
stages: [
{
type: 'round_robin',
name: 'Group Stage',
rounds: 1,
},
{
type: 'single_elimination',
name: 'Knockout Stage',
},
],
},
},
};
export default function CreateCompetitionModal({ export default function CreateCompetitionModal({
isOpen, isOpen,
onClose, onClose,
@ -144,6 +200,9 @@ export default function CreateCompetitionModal({
return; return;
} }
// Use template config if selected, otherwise use default config for the type
const config = selectedTemplate?.config ?? defaultConfigs[formData.type];
const request: CreateCompetitionRequest = { const request: CreateCompetitionRequest = {
type: formData.type, type: formData.type,
sport_id: formData.sport_id, sport_id: formData.sport_id,
@ -159,7 +218,7 @@ export default function CreateCompetitionModal({
? new Date(formData.registration_close_at).toISOString() ? new Date(formData.registration_close_at).toISOString()
: undefined, : undefined,
template_id: selectedTemplate?.template_id, template_id: selectedTemplate?.template_id,
config: selectedTemplate?.config, config,
}; };
try { try {

@ -18,6 +18,7 @@ import {
ListOrdered, ListOrdered,
FileText, FileText,
CalendarClock, CalendarClock,
Trash2,
} from 'lucide-react'; } from 'lucide-react';
import useTranslation from '@/src/hooks/useTranslation'; import useTranslation from '@/src/hooks/useTranslation';
import Card from '@/src/components/cards/Card'; import Card from '@/src/components/cards/Card';
@ -28,7 +29,9 @@ import {
useStartCompetition, useStartCompetition,
useFinishCompetition, useFinishCompetition,
useCancelCompetition, useCancelCompetition,
useDeleteCompetition,
} from '@/src/hooks/mutations/useCompetitionMutations'; } from '@/src/hooks/mutations/useCompetitionMutations';
import { useRouter } from 'next/navigation';
import OverviewTab from './tabs/OverviewTab'; import OverviewTab from './tabs/OverviewTab';
import RegistrationsTab from './tabs/RegistrationsTab'; import RegistrationsTab from './tabs/RegistrationsTab';
import ParticipantsTab from './tabs/ParticipantsTab'; import ParticipantsTab from './tabs/ParticipantsTab';
@ -40,7 +43,7 @@ import SaveAsTemplateModal from './SaveAsTemplateModal';
import type { CompetitionStatus } from '@/src/types/competition'; import type { CompetitionStatus } from '@/src/types/competition';
interface CompetitionDetailComponentProps { interface CompetitionDetailComponentProps {
clubId: number; facilityId: number;
competitionId: number; competitionId: number;
} }
@ -71,20 +74,76 @@ function formatDate(dateString: string): string {
function StatusTransitionButton({ function StatusTransitionButton({
status, status,
competitionId, competitionId,
facilityId,
locale,
}: { }: {
status: CompetitionStatus; status: CompetitionStatus;
competitionId: number; competitionId: number;
facilityId: number;
locale: string;
}) { }) {
const router = useRouter();
const publishMutation = usePublishCompetition(competitionId); const publishMutation = usePublishCompetition(competitionId);
const startMutation = useStartCompetition(competitionId); const startMutation = useStartCompetition(competitionId);
const finishMutation = useFinishCompetition(competitionId); const finishMutation = useFinishCompetition(competitionId);
const cancelMutation = useCancelCompetition(competitionId); const cancelMutation = useCancelCompetition(competitionId);
const deleteMutation = useDeleteCompetition(competitionId);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const isPending = const isPending =
publishMutation.isPending || publishMutation.isPending ||
startMutation.isPending || startMutation.isPending ||
finishMutation.isPending || finishMutation.isPending ||
cancelMutation.isPending; cancelMutation.isPending ||
deleteMutation.isPending;
const canDelete = status === 'draft' || status === 'cancelled';
const handleDelete = () => {
deleteMutation.mutate(undefined, {
onSuccess: () => {
router.push(`/${locale}/admin/facilities/${facilityId}/competitions`);
},
});
};
const DeleteButton = () => (
<>
{showDeleteConfirm ? (
<div className="flex items-center gap-2">
<span className="text-sm text-red-700">Delete permanently?</span>
<button
onClick={handleDelete}
disabled={isPending}
className="inline-flex items-center px-3 py-1.5 bg-red-600 text-white text-sm font-semibold rounded-lg hover:bg-red-700 transition-colors disabled:opacity-50"
>
{deleteMutation.isPending ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
'Yes, delete'
)}
</button>
<button
onClick={() => setShowDeleteConfirm(false)}
disabled={isPending}
className="inline-flex items-center px-3 py-1.5 bg-slate-100 text-slate-700 text-sm font-semibold rounded-lg hover:bg-slate-200 transition-colors disabled:opacity-50"
>
No
</button>
</div>
) : (
<button
onClick={() => setShowDeleteConfirm(true)}
disabled={isPending}
className="inline-flex items-center px-4 py-2 bg-red-100 text-red-700 font-semibold rounded-xl hover:bg-red-200 transition-colors disabled:opacity-50"
title="Delete competition permanently"
>
<Trash2 className="w-4 h-4 mr-2" />
Delete
</button>
)}
</>
);
if (status === 'draft') { if (status === 'draft') {
return ( return (
@ -101,6 +160,7 @@ function StatusTransitionButton({
)} )}
Publish Publish
</button> </button>
<DeleteButton />
</div> </div>
); );
} }
@ -155,11 +215,20 @@ function StatusTransitionButton({
); );
} }
// For cancelled status, show delete button
if (canDelete) {
return (
<div className="flex gap-2">
<DeleteButton />
</div>
);
}
return null; return null;
} }
export default function CompetitionDetailComponent({ export default function CompetitionDetailComponent({
clubId, facilityId,
competitionId, competitionId,
}: CompetitionDetailComponentProps) { }: CompetitionDetailComponentProps) {
const { locale } = useTranslation(); const { locale } = useTranslation();
@ -181,7 +250,7 @@ export default function CompetitionDetailComponent({
return ( return (
<div className="py-8"> <div className="py-8">
<Link <Link
href={`/${locale}/admin/clubs/${clubId}/competitions`} href={`/${locale}/admin/facilities/${facilityId}/competitions`}
className="inline-flex items-center text-slate-600 hover:text-slate-900 font-medium transition-colors mb-6" className="inline-flex items-center text-slate-600 hover:text-slate-900 font-medium transition-colors mb-6"
> >
<ArrowLeft className="w-4 h-4 mr-2" /> <ArrowLeft className="w-4 h-4 mr-2" />
@ -206,7 +275,7 @@ export default function CompetitionDetailComponent({
<div> <div>
{/* Back Link */} {/* Back Link */}
<Link <Link
href={`/${locale}/admin/clubs/${clubId}/competitions`} href={`/${locale}/admin/facilities/${facilityId}/competitions`}
className="inline-flex items-center text-slate-600 hover:text-slate-900 font-medium transition-colors mb-6" className="inline-flex items-center text-slate-600 hover:text-slate-900 font-medium transition-colors mb-6"
> >
<ArrowLeft className="w-4 h-4 mr-2" /> <ArrowLeft className="w-4 h-4 mr-2" />
@ -234,7 +303,7 @@ export default function CompetitionDetailComponent({
<FileText className="w-4 h-4 mr-2" /> <FileText className="w-4 h-4 mr-2" />
Save as Template Save as Template
</button> </button>
<StatusTransitionButton status={competition.status} competitionId={competitionId} /> <StatusTransitionButton status={competition.status} competitionId={competitionId} facilityId={facilityId} locale={locale} />
</div> </div>
</div> </div>
@ -321,7 +390,7 @@ export default function CompetitionDetailComponent({
{/* Tab Content */} {/* Tab Content */}
{activeTab === 'overview' && ( {activeTab === 'overview' && (
<OverviewTab competition={competition} clubId={clubId} /> <OverviewTab competition={competition} facilityId={facilityId} />
)} )}
{activeTab === 'registrations' && ( {activeTab === 'registrations' && (
<RegistrationsTab competitionId={competitionId} /> <RegistrationsTab competitionId={competitionId} />
@ -336,10 +405,10 @@ export default function CompetitionDetailComponent({
<FixturesTab competition={competition} /> <FixturesTab competition={competition} />
)} )}
{activeTab === 'scheduling' && ( {activeTab === 'scheduling' && (
<SchedulingTab competition={competition} clubId={clubId} /> <SchedulingTab competition={competition} facilityId={facilityId} />
)} )}
{activeTab === 'results' && ( {activeTab === 'results' && (
<ResultsTab competition={competition} clubId={clubId} /> <ResultsTab competition={competition} facilityId={facilityId} />
)} )}
{activeTab === 'standings' && ( {activeTab === 'standings' && (
<StandingsTab competition={competition} /> <StandingsTab competition={competition} />

@ -0,0 +1,13 @@
import CompetitionDetailComponent from './CompetitionDetailComponent';
export default async function CompetitionDetailPage({
params,
}: {
params: Promise<{ facility_id: string; competition_id: string }>;
}) {
const { facility_id, competition_id } = await params;
const facilityId = parseInt(facility_id, 10);
const competitionId = parseInt(competition_id, 10);
return <CompetitionDetailComponent facilityId={facilityId} competitionId={competitionId} />;
}

@ -7,7 +7,7 @@ import type { Competition, CompetitionStage } from '@/src/types/competition';
interface OverviewTabProps { interface OverviewTabProps {
competition: Competition; competition: Competition;
clubId: number; facilityId: number;
} }
function formatDate(dateString: string | null): string { function formatDate(dateString: string | null): string {
@ -60,7 +60,7 @@ function StageCard({ stage, index }: { stage: CompetitionStage; index: number })
); );
} }
export default function OverviewTab({ competition, clubId }: OverviewTabProps) { export default function OverviewTab({ competition, facilityId }: OverviewTabProps) {
const visibilityIcons = { const visibilityIcons = {
public: Globe, public: Globe,
unlisted: LinkIcon, unlisted: LinkIcon,

@ -30,7 +30,7 @@ import type {
interface ResultsTabProps { interface ResultsTabProps {
competition: Competition; competition: Competition;
clubId: number; facilityId: number;
} }
const disputeStatusConfig: Record<DisputeStatus, { label: string; bgColor: string; textColor: string }> = { const disputeStatusConfig: Record<DisputeStatus, { label: string; bgColor: string; textColor: string }> = {
@ -280,7 +280,7 @@ function MatchResultsSkeleton() {
); );
} }
export default function ResultsTab({ competition, clubId }: ResultsTabProps) { export default function ResultsTab({ competition, facilityId }: ResultsTabProps) {
const [disputeFilter, setDisputeFilter] = useState<DisputeStatus | 'all'>('all'); const [disputeFilter, setDisputeFilter] = useState<DisputeStatus | 'all'>('all');
const [activeStageId, setActiveStageId] = useState<number | null>( const [activeStageId, setActiveStageId] = useState<number | null>(
competition.stages?.[0]?.stage_id ?? null competition.stages?.[0]?.stage_id ?? null

@ -24,7 +24,7 @@ import type {
interface SchedulingTabProps { interface SchedulingTabProps {
competition: Competition; competition: Competition;
clubId: number; facilityId: number;
} }
const schedulingStatusConfig: Record< const schedulingStatusConfig: Record<
@ -145,11 +145,11 @@ function MatchSchedulingSkeleton() {
function StageScheduling({ function StageScheduling({
stage, stage,
competitionId, competitionId,
clubId, facilityId,
}: { }: {
stage: CompetitionStage; stage: CompetitionStage;
competitionId: number; competitionId: number;
clubId: number; facilityId: number;
}) { }) {
const [selectedMatch, setSelectedMatch] = useState<CompetitionMatch | null>(null); const [selectedMatch, setSelectedMatch] = useState<CompetitionMatch | null>(null);
const { data: fixtures, isLoading, error } = useCompetitionFixtures(competitionId, stage.stage_id); const { data: fixtures, isLoading, error } = useCompetitionFixtures(competitionId, stage.stage_id);
@ -290,7 +290,7 @@ function StageScheduling({
away: selectedMatch.away_participant?.display_name ?? 'TBD', away: selectedMatch.away_participant?.display_name ?? 'TBD',
round: selectedMatch.round_index + 1, round: selectedMatch.round_index + 1,
}} }}
clubId={clubId} facilityId={facilityId}
isLoading={linkMutation.isPending} isLoading={linkMutation.isPending}
error={linkMutation.error instanceof Error ? linkMutation.error.message : null} error={linkMutation.error instanceof Error ? linkMutation.error.message : null}
/> />
@ -301,7 +301,7 @@ function StageScheduling({
export default function SchedulingTab({ export default function SchedulingTab({
competition, competition,
clubId, facilityId,
}: SchedulingTabProps) { }: SchedulingTabProps) {
const [selectedStageId, setSelectedStageId] = useState<number | null>( const [selectedStageId, setSelectedStageId] = useState<number | null>(
competition.stages?.[0]?.stage_id ?? null competition.stages?.[0]?.stage_id ?? null
@ -352,7 +352,7 @@ export default function SchedulingTab({
<StageScheduling <StageScheduling
stage={selectedStage} stage={selectedStage}
competitionId={competition.competition_id} competitionId={competition.competition_id}
clubId={clubId} facilityId={facilityId}
/> />
)} )}
</div> </div>

@ -15,7 +15,7 @@ interface SlotPickerModalProps {
away: string; away: string;
round: number; round: number;
}; };
clubId: number; facilityId: number;
isLoading?: boolean; isLoading?: boolean;
error?: string | null; error?: string | null;
} }
@ -50,7 +50,7 @@ export default function SlotPickerModal({
onClose, onClose,
onSelect, onSelect,
matchInfo, matchInfo,
clubId, facilityId,
isLoading: isSaving, isLoading: isSaving,
error: saveError, error: saveError,
}: SlotPickerModalProps) { }: SlotPickerModalProps) {
@ -60,15 +60,15 @@ export default function SlotPickerModal({
const dateString = formatDate(selectedDate); const dateString = formatDate(selectedDate);
const { data: slotsData, isLoading, error } = useQuery({ const { data: slotsData, isLoading, error } = useQuery({
queryKey: ['slotInstances', clubId, dateString], queryKey: ['slotInstances', facilityId, dateString],
queryFn: async () => { queryFn: async () => {
const result = await getSlotInstances(clubId, dateString, { status: 'open' }); const result = await getSlotInstances(facilityId, dateString, { status: 'open' });
if (!result.success) { if (!result.success) {
throw new Error(result.error.detail); throw new Error(result.error.detail);
} }
return result.data; return result.data;
}, },
enabled: isOpen && clubId > 0, enabled: isOpen && facilityId > 0,
}); });
// Filter to only show future slots that are open // Filter to only show future slots that are open

@ -0,0 +1,12 @@
import CompetitionsPageComponent from './CompetitionsPageComponent';
export default async function CompetitionsPage({
params,
}: {
params: Promise<{ facility_id: string }>;
}) {
const { facility_id } = await params;
const facilityId = parseInt(facility_id, 10);
return <CompetitionsPageComponent facilityId={facilityId} />;
}

@ -8,7 +8,7 @@ import type { CompetitionTemplate, CreateSeriesRequest, CarryOverPolicy } from '
interface CreateSeriesModalProps { interface CreateSeriesModalProps {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
clubId: number; facilityId: number;
templates: CompetitionTemplate[]; templates: CompetitionTemplate[];
} }
@ -33,10 +33,10 @@ const dayOptions = [
export default function CreateSeriesModal({ export default function CreateSeriesModal({
isOpen, isOpen,
onClose, onClose,
clubId, facilityId,
templates, templates,
}: CreateSeriesModalProps) { }: CreateSeriesModalProps) {
const createMutation = useCreateSeries(clubId); const createMutation = useCreateSeries(facilityId);
// Form state // Form state
const [templateId, setTemplateId] = useState<number | null>(null); const [templateId, setTemplateId] = useState<number | null>(null);

@ -23,7 +23,7 @@ import CreateSeriesModal from './CreateSeriesModal';
import type { CompetitionSeries, SeriesStatus } from '@/src/types/competition'; import type { CompetitionSeries, SeriesStatus } from '@/src/types/competition';
interface SeriesListComponentProps { interface SeriesListComponentProps {
clubId: number; facilityId: number;
} }
const statusConfig: Record<SeriesStatus, { const statusConfig: Record<SeriesStatus, {
@ -38,10 +38,10 @@ const statusConfig: Record<SeriesStatus, {
function SeriesCard({ function SeriesCard({
series, series,
clubId, facilityId,
}: { }: {
series: CompetitionSeries; series: CompetitionSeries;
clubId: number; facilityId: number;
}) { }) {
const { locale } = useTranslation(); const { locale } = useTranslation();
const updateMutation = useUpdateSeries(series.series_id); const updateMutation = useUpdateSeries(series.series_id);
@ -162,17 +162,17 @@ function SeriesCardSkeleton() {
); );
} }
export default function SeriesListComponent({ clubId }: SeriesListComponentProps) { export default function SeriesListComponent({ facilityId }: SeriesListComponentProps) {
const { locale } = useTranslation(); const { locale } = useTranslation();
const [showCreateModal, setShowCreateModal] = useState(false); const [showCreateModal, setShowCreateModal] = useState(false);
const [statusFilter, setStatusFilter] = useState<SeriesStatus | 'all'>('all'); const [statusFilter, setStatusFilter] = useState<SeriesStatus | 'all'>('all');
const { data: series, isLoading, error } = useCompetitionSeries( const { data: series, isLoading, error } = useCompetitionSeries(
clubId, facilityId,
statusFilter === 'all' ? undefined : { status: statusFilter } statusFilter === 'all' ? undefined : { status: statusFilter }
); );
const { data: templates } = useCompetitionTemplates(clubId); const { data: templates } = useCompetitionTemplates(facilityId);
const activeCount = series?.filter((s) => s.status === 'active').length ?? 0; const activeCount = series?.filter((s) => s.status === 'active').length ?? 0;
const pausedCount = series?.filter((s) => s.status === 'paused').length ?? 0; const pausedCount = series?.filter((s) => s.status === 'paused').length ?? 0;
@ -181,7 +181,7 @@ export default function SeriesListComponent({ clubId }: SeriesListComponentProps
<div className="py-8"> <div className="py-8">
{/* Back Link */} {/* Back Link */}
<Link <Link
href={`/${locale}/admin/clubs/${clubId}/competitions`} href={`/${locale}/admin/facilities/${facilityId}/competitions`}
className="inline-flex items-center text-slate-600 hover:text-slate-900 font-medium transition-colors mb-6" className="inline-flex items-center text-slate-600 hover:text-slate-900 font-medium transition-colors mb-6"
> >
<ArrowLeft className="w-4 h-4 mr-2" /> <ArrowLeft className="w-4 h-4 mr-2" />
@ -237,7 +237,7 @@ export default function SeriesListComponent({ clubId }: SeriesListComponentProps
) : series && series.length > 0 ? ( ) : series && series.length > 0 ? (
<div className="space-y-4"> <div className="space-y-4">
{series.map((s) => ( {series.map((s) => (
<SeriesCard key={s.series_id} series={s} clubId={clubId} /> <SeriesCard key={s.series_id} series={s} facilityId={facilityId} />
))} ))}
</div> </div>
) : ( ) : (
@ -263,7 +263,7 @@ export default function SeriesListComponent({ clubId }: SeriesListComponentProps
<CreateSeriesModal <CreateSeriesModal
isOpen={showCreateModal} isOpen={showCreateModal}
onClose={() => setShowCreateModal(false)} onClose={() => setShowCreateModal(false)}
clubId={clubId} facilityId={facilityId}
templates={templates ?? []} templates={templates ?? []}
/> />
</div> </div>

@ -0,0 +1,12 @@
import SeriesListComponent from './SeriesListComponent';
export default async function SeriesPage({
params,
}: {
params: Promise<{ facility_id: string }>;
}) {
const { facility_id } = await params;
const facilityId = parseInt(facility_id, 10);
return <SeriesListComponent facilityId={facilityId} />;
}

@ -11,7 +11,7 @@ import CreateTemplateModal from './CreateTemplateModal';
import type { CompetitionTemplate, CompetitionType } from '@/src/types/competition'; import type { CompetitionTemplate, CompetitionType } from '@/src/types/competition';
interface TemplatesPageComponentProps { interface TemplatesPageComponentProps {
clubId: number; facilityId: number;
} }
const typeIcons: Record<CompetitionType, React.ComponentType<{ className?: string }>> = { const typeIcons: Record<CompetitionType, React.ComponentType<{ className?: string }>> = {
@ -141,11 +141,11 @@ function TemplateCardSkeleton() {
); );
} }
export default function TemplatesPageComponent({ clubId }: TemplatesPageComponentProps) { export default function TemplatesPageComponent({ facilityId }: TemplatesPageComponentProps) {
const { locale } = useTranslation(); const { locale } = useTranslation();
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const { data: templates, isLoading, error } = useCompetitionTemplates(clubId); const { data: templates, isLoading, error } = useCompetitionTemplates(facilityId);
const updateMutation = useUpdateTemplate(); const updateMutation = useUpdateTemplate();
function handleToggleActive(template: CompetitionTemplate) { function handleToggleActive(template: CompetitionTemplate) {
@ -161,7 +161,7 @@ export default function TemplatesPageComponent({ clubId }: TemplatesPageComponen
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-8"> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-8">
<div> <div>
<Link <Link
href={`/${locale}/admin/clubs/${clubId}/competitions`} href={`/${locale}/admin/facilities/${facilityId}/competitions`}
className="inline-flex items-center text-slate-600 hover:text-slate-900 font-medium transition-colors mb-2" className="inline-flex items-center text-slate-600 hover:text-slate-900 font-medium transition-colors mb-2"
> >
<ArrowLeft className="w-4 h-4 mr-2" /> <ArrowLeft className="w-4 h-4 mr-2" />
@ -229,7 +229,7 @@ export default function TemplatesPageComponent({ clubId }: TemplatesPageComponen
<CreateTemplateModal <CreateTemplateModal
isOpen={isCreateModalOpen} isOpen={isCreateModalOpen}
onClose={() => setIsCreateModalOpen(false)} onClose={() => setIsCreateModalOpen(false)}
facilityId={clubId} facilityId={facilityId}
/> />
</div> </div>
); );

@ -0,0 +1,12 @@
import TemplatesPageComponent from './TemplatesPageComponent';
export default async function TemplatesPage({
params,
}: {
params: Promise<{ facility_id: string }>;
}) {
const { facility_id } = await params;
const facilityId = parseInt(facility_id, 10);
return <TemplatesPageComponent facilityId={facilityId} />;
}

@ -4,23 +4,23 @@ import { useState, useEffect } from 'react';
import { Loader2 } from 'lucide-react'; import { Loader2 } from 'lucide-react';
import { getAdminClubDetail } from '@/src/lib/api/admin-clubs'; import { getAdminClubDetail } from '@/src/lib/api/admin-clubs';
import type { Court } from '@/src/types/courts'; import type { Court } from '@/src/types/courts';
import ClubCourtsTab from '../tabs/ClubCourtsTab'; import FacilityCourtsTab from '../tabs/FacilityCourtsTab';
interface CourtsComponentProps { interface CourtsComponentProps {
clubId: number; facilityId: number;
} }
export default function CourtsComponent({ clubId }: CourtsComponentProps) { export default function CourtsComponent({ facilityId }: CourtsComponentProps) {
const [courts, setCourts] = useState<Court[]>([]); const [courts, setCourts] = useState<Court[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
loadCourts(); loadCourts();
}, [clubId]); }, [facilityId]);
async function loadCourts() { async function loadCourts() {
setLoading(true); setLoading(true);
const result = await getAdminClubDetail(clubId); const result = await getAdminClubDetail(facilityId);
if (result.success) { if (result.success) {
setCourts(result.data.courts); setCourts(result.data.courts);
@ -38,8 +38,8 @@ export default function CourtsComponent({ clubId }: CourtsComponentProps) {
} }
return ( return (
<ClubCourtsTab <FacilityCourtsTab
clubId={clubId} facilityId={facilityId}
courts={courts} courts={courts}
onUpdate={loadCourts} onUpdate={loadCourts}
/> />

@ -0,0 +1,12 @@
import CourtsComponent from './CourtsComponent';
export default async function CourtsPage({
params
}: {
params: Promise<{ facility_id: string }>
}) {
const { facility_id } = await params;
const facilityId = parseInt(facility_id, 10);
return <CourtsComponent facilityId={facilityId} />;
}

@ -7,10 +7,10 @@ import type { MemberCreditBalance } from '@/src/types/facility-admin';
import CreditDetailModal from './CreditDetailModal'; import CreditDetailModal from './CreditDetailModal';
interface CreditsManagementComponentProps { interface CreditsManagementComponentProps {
clubId: number; facilityId: number;
} }
export default function CreditsManagementComponent({ clubId }: CreditsManagementComponentProps) { export default function CreditsManagementComponent({ facilityId }: CreditsManagementComponentProps) {
const [credits, setCredits] = useState<MemberCreditBalance[]>([]); const [credits, setCredits] = useState<MemberCreditBalance[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@ -20,14 +20,14 @@ export default function CreditsManagementComponent({ clubId }: CreditsManagement
useEffect(() => { useEffect(() => {
fetchCredits(); fetchCredits();
}, [clubId, showZeroBalances]); }, [facilityId, showZeroBalances]);
async function fetchCredits() { async function fetchCredits() {
setLoading(true); setLoading(true);
setError(null); setError(null);
const minBalance = showZeroBalances ? 0 : 1; const minBalance = showZeroBalances ? 0 : 1;
const result = await listMemberCredits(clubId, minBalance); const result = await listMemberCredits(facilityId, minBalance);
if (result.success) { if (result.success) {
setCredits(result.data); setCredits(result.data);
@ -177,7 +177,7 @@ export default function CreditsManagementComponent({ clubId }: CreditsManagement
<CreditDetailModal <CreditDetailModal
isOpen={!!selectedUser} isOpen={!!selectedUser}
onClose={() => setSelectedUser(null)} onClose={() => setSelectedUser(null)}
facilityId={clubId} facilityId={facilityId}
user={selectedUser} user={selectedUser}
onBalanceChange={fetchCredits} onBalanceChange={fetchCredits}
/> />

@ -0,0 +1,12 @@
import CreditsManagementComponent from './CreditsManagementComponent';
export default async function CreditsManagementPage({
params
}: {
params: Promise<{ facility_id: string }>;
}) {
const { facility_id } = await params;
const facilityId = parseInt(facility_id, 10);
return <CreditsManagementComponent facilityId={facilityId} />;
}

@ -0,0 +1,21 @@
import AdminAuthGuard from '@/src/components/AdminAuthGuard';
import FacilityDetailHeader from '@/src/components/FacilityDetailHeader';
export default async function FacilityDetailLayout({
children,
params,
}: {
children: React.ReactNode;
params: Promise<{ facility_id: string }>;
}) {
const { facility_id } = await params;
const facilityId = parseInt(facility_id, 10);
return (
<AdminAuthGuard>
<FacilityDetailHeader facilityId={facilityId}>
{children}
</FacilityDetailHeader>
</AdminAuthGuard>
);
}

@ -9,10 +9,10 @@ import EditMemberModal from '@/src/components/members/EditMemberModal';
import MemberListSkeleton from '@/src/components/members/MemberListSkeleton'; import MemberListSkeleton from '@/src/components/members/MemberListSkeleton';
interface MembersManagementComponentProps { interface MembersManagementComponentProps {
clubId: number; facilityId: number;
} }
export default function MembersManagementComponent({ clubId }: MembersManagementComponentProps) { export default function MembersManagementComponent({ facilityId }: MembersManagementComponentProps) {
const [members, setMembers] = useState<FacilityMember[]>([]); const [members, setMembers] = useState<FacilityMember[]>([]);
const [filteredMembers, setFilteredMembers] = useState<FacilityMember[]>([]); const [filteredMembers, setFilteredMembers] = useState<FacilityMember[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@ -27,7 +27,7 @@ export default function MembersManagementComponent({ clubId }: MembersManagement
useEffect(() => { useEffect(() => {
fetchMembers(); fetchMembers();
}, [clubId]); }, [facilityId]);
useEffect(() => { useEffect(() => {
applyFilters(); applyFilters();
@ -37,7 +37,7 @@ export default function MembersManagementComponent({ clubId }: MembersManagement
setLoading(true); setLoading(true);
setError(null); setError(null);
const result = await listMembers(clubId); const result = await listMembers(facilityId);
if (result.success) { if (result.success) {
setMembers(result.data); setMembers(result.data);
@ -77,7 +77,7 @@ export default function MembersManagementComponent({ clubId }: MembersManagement
async function handleDelete(memberId: number, memberName: string) { async function handleDelete(memberId: number, memberName: string) {
if (!confirm(`Are you sure you want to remove "${memberName}" from this facility?`)) return; if (!confirm(`Are you sure you want to remove "${memberName}" from this facility?`)) return;
const result = await deleteMember(clubId, memberId); const result = await deleteMember(facilityId, memberId);
if (result.success) { if (result.success) {
fetchMembers(); fetchMembers();
@ -211,7 +211,7 @@ export default function MembersManagementComponent({ clubId }: MembersManagement
<AddMemberModal <AddMemberModal
isOpen={addModalOpen} isOpen={addModalOpen}
onClose={() => setAddModalOpen(false)} onClose={() => setAddModalOpen(false)}
facilityId={clubId} facilityId={facilityId}
onSuccess={fetchMembers} onSuccess={fetchMembers}
/> />
)} )}
@ -221,7 +221,7 @@ export default function MembersManagementComponent({ clubId }: MembersManagement
<EditMemberModal <EditMemberModal
isOpen={!!editingMember} isOpen={!!editingMember}
onClose={() => setEditingMember(null)} onClose={() => setEditingMember(null)}
facilityId={clubId} facilityId={facilityId}
member={editingMember} member={editingMember}
onSuccess={fetchMembers} onSuccess={fetchMembers}
/> />

@ -0,0 +1,12 @@
import MembersManagementComponent from './MembersManagementComponent';
export default async function MembersManagementPage({
params
}: {
params: Promise<{ facility_id: string }>;
}) {
const { facility_id } = await params;
const facilityId = parseInt(facility_id, 10);
return <MembersManagementComponent facilityId={facilityId} />;
}

@ -0,0 +1,12 @@
import FacilityDetailTabs from './FacilityDetailTabs';
export default async function AdminClubDetailPage({
params
}: {
params: Promise<{ facility_id: string }>
}) {
const { facility_id } = await params;
const facilityId = parseInt(facility_id, 10);
return <FacilityDetailTabs facilityId={facilityId} />;
}

@ -10,10 +10,10 @@ import TemplatePicker from '@/src/components/plans/TemplatePicker';
import EntitlementsConfigModal from '@/src/components/plans/EntitlementsConfigModal'; import EntitlementsConfigModal from '@/src/components/plans/EntitlementsConfigModal';
interface MembershipPlansComponentProps { interface MembershipPlansComponentProps {
clubId: number; facilityId: number;
} }
export default function MembershipPlansComponent({ clubId }: MembershipPlansComponentProps) { export default function MembershipPlansComponent({ facilityId }: MembershipPlansComponentProps) {
const [plans, setPlans] = useState<MembershipPlan[]>([]); const [plans, setPlans] = useState<MembershipPlan[]>([]);
const [entitlementsMap, setEntitlementsMap] = useState<Record<number, PlanEntitlements>>({}); const [entitlementsMap, setEntitlementsMap] = useState<Record<number, PlanEntitlements>>({});
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@ -26,13 +26,13 @@ export default function MembershipPlansComponent({ clubId }: MembershipPlansComp
useEffect(() => { useEffect(() => {
fetchPlans(); fetchPlans();
}, [clubId]); }, [facilityId]);
async function fetchPlans() { async function fetchPlans() {
setLoading(true); setLoading(true);
setError(null); setError(null);
const result = await listPlans(clubId, { include_inactive: false }); const result = await listPlans(facilityId, { include_inactive: false });
if (result.success) { if (result.success) {
setPlans(result.data); setPlans(result.data);
@ -47,7 +47,7 @@ export default function MembershipPlansComponent({ clubId }: MembershipPlansComp
async function fetchAllEntitlements(plansList: MembershipPlan[]) { async function fetchAllEntitlements(plansList: MembershipPlan[]) {
const entitlementPromises = plansList.map(async (plan) => { const entitlementPromises = plansList.map(async (plan) => {
const result = await getEntitlements(clubId, plan.facility_membership_plan_id); const result = await getEntitlements(facilityId, plan.facility_membership_plan_id);
if (result.success) { if (result.success) {
return { planId: plan.facility_membership_plan_id, entitlements: result.data }; return { planId: plan.facility_membership_plan_id, entitlements: result.data };
} }
@ -69,7 +69,7 @@ export default function MembershipPlansComponent({ clubId }: MembershipPlansComp
async function handleDelete(planId: number, planName: string) { async function handleDelete(planId: number, planName: string) {
if (!confirm(`Are you sure you want to deactivate "${planName}"?`)) return; if (!confirm(`Are you sure you want to deactivate "${planName}"?`)) return;
const result = await deletePlan(clubId, planId); const result = await deletePlan(facilityId, planId);
if (result.success) { if (result.success) {
fetchPlans(); fetchPlans();
@ -159,7 +159,7 @@ export default function MembershipPlansComponent({ clubId }: MembershipPlansComp
<PlanFormModal <PlanFormModal
isOpen={createModalOpen} isOpen={createModalOpen}
onClose={handleCreateModalClose} onClose={handleCreateModalClose}
facilityId={clubId} facilityId={facilityId}
initialValues={selectedTemplate ? { initialValues={selectedTemplate ? {
name: selectedTemplate.name, name: selectedTemplate.name,
billing_period: selectedTemplate.billing_period, billing_period: selectedTemplate.billing_period,
@ -177,7 +177,7 @@ export default function MembershipPlansComponent({ clubId }: MembershipPlansComp
<PlanFormModal <PlanFormModal
isOpen={!!editingPlan} isOpen={!!editingPlan}
onClose={() => setEditingPlan(null)} onClose={() => setEditingPlan(null)}
facilityId={clubId} facilityId={facilityId}
plan={editingPlan} plan={editingPlan}
onSuccess={fetchPlans} onSuccess={fetchPlans}
/> />
@ -188,7 +188,7 @@ export default function MembershipPlansComponent({ clubId }: MembershipPlansComp
<EntitlementsConfigModal <EntitlementsConfigModal
isOpen={!!configuringPlan} isOpen={!!configuringPlan}
onClose={() => setConfiguringPlan(null)} onClose={() => setConfiguringPlan(null)}
facilityId={clubId} facilityId={facilityId}
plan={configuringPlan} plan={configuringPlan}
onSuccess={fetchPlans} onSuccess={fetchPlans}
/> />

@ -0,0 +1,12 @@
import MembershipPlansComponent from './MembershipPlansComponent';
export default async function MembershipPlansPage({
params
}: {
params: Promise<{ facility_id: string }>;
}) {
const { facility_id } = await params;
const facilityId = parseInt(facility_id, 10);
return <MembershipPlansComponent facilityId={facilityId} />;
}

@ -8,10 +8,10 @@ import BookingLimitsForm from '@/src/components/policy/BookingLimitsForm';
import GuestPricingInput from '@/src/components/policy/GuestPricingInput'; import GuestPricingInput from '@/src/components/policy/GuestPricingInput';
interface FacilitySettingsComponentProps { interface FacilitySettingsComponentProps {
clubId: number; facilityId: number;
} }
export default function FacilitySettingsComponent({ clubId }: FacilitySettingsComponentProps) { export default function FacilitySettingsComponent({ facilityId }: FacilitySettingsComponentProps) {
const [policy, setPolicy] = useState<FacilityPolicy | null>(null); const [policy, setPolicy] = useState<FacilityPolicy | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
@ -20,13 +20,13 @@ export default function FacilitySettingsComponent({ clubId }: FacilitySettingsCo
useEffect(() => { useEffect(() => {
fetchPolicy(); fetchPolicy();
}, [clubId]); }, [facilityId]);
async function fetchPolicy() { async function fetchPolicy() {
setLoading(true); setLoading(true);
setError(null); setError(null);
const result = await getPolicy(clubId); const result = await getPolicy(facilityId);
if (result.success) { if (result.success) {
setPolicy(result.data); setPolicy(result.data);
@ -44,7 +44,7 @@ export default function FacilitySettingsComponent({ clubId }: FacilitySettingsCo
setError(null); setError(null);
setSuccessMessage(null); setSuccessMessage(null);
const result = await updatePolicy(clubId, { const result = await updatePolicy(facilityId, {
access_model: policy.access_model, access_model: policy.access_model,
guest_price_cents: policy.guest_price_cents, guest_price_cents: policy.guest_price_cents,
default_price_group: policy.default_price_group, default_price_group: policy.default_price_group,

@ -0,0 +1,12 @@
import FacilitySettingsComponent from './FacilitySettingsComponent';
export default async function FacilitySettingsPage({
params
}: {
params: Promise<{ facility_id: string }>;
}) {
const { facility_id } = await params;
const facilityId = parseInt(facility_id, 10);
return <FacilitySettingsComponent facilityId={facilityId} />;
}

@ -12,7 +12,7 @@ import type {
import { DAY_NAMES, formatTime, calculateEndTime } from '@/src/types/slot-definitions'; import { DAY_NAMES, formatTime, calculateEndTime } from '@/src/types/slot-definitions';
interface CloneSlotDefinitionModalProps { interface CloneSlotDefinitionModalProps {
clubId: number; facilityId: number;
sourceDefinition: SlotDefinition; sourceDefinition: SlotDefinition;
courts: Court[]; courts: Court[];
onClose: () => void; onClose: () => void;
@ -20,7 +20,7 @@ interface CloneSlotDefinitionModalProps {
} }
export default function CloneSlotDefinitionModal({ export default function CloneSlotDefinitionModal({
clubId, facilityId,
sourceDefinition, sourceDefinition,
courts, courts,
onClose, onClose,
@ -82,7 +82,7 @@ export default function CloneSlotDefinitionModal({
if (validFrom) request.valid_from = validFrom; if (validFrom) request.valid_from = validFrom;
if (validTo) request.valid_to = validTo; if (validTo) request.valid_to = validTo;
const result = await cloneSlotDefinition(clubId, sourceDefinition.slot_definition_id, request); const result = await cloneSlotDefinition(facilityId, sourceDefinition.slot_definition_id, request);
if (result.success) { if (result.success) {
onSuccess(); onSuccess();

@ -14,14 +14,14 @@ import type {
import { DAY_NAMES } from '@/src/types/slot-definitions'; import { DAY_NAMES } from '@/src/types/slot-definitions';
interface GenerateSlotDefinitionsModalProps { interface GenerateSlotDefinitionsModalProps {
clubId: number; facilityId: number;
courts: Court[]; courts: Court[];
onClose: () => void; onClose: () => void;
onSuccess: () => void; onSuccess: () => void;
} }
export default function GenerateSlotDefinitionsModal({ export default function GenerateSlotDefinitionsModal({
clubId, facilityId,
courts, courts,
onClose, onClose,
onSuccess, onSuccess,
@ -130,7 +130,7 @@ export default function GenerateSlotDefinitionsModal({
if (validTo) request.valid_to = validTo; if (validTo) request.valid_to = validTo;
const result = await generateSlotDefinitions(clubId, request); const result = await generateSlotDefinitions(facilityId, request);
if (result.success) { if (result.success) {
onSuccess(); onSuccess();

@ -15,10 +15,10 @@ import {
} from '@/src/types/materialisation'; } from '@/src/types/materialisation';
interface MaterialisationStatusPanelProps { interface MaterialisationStatusPanelProps {
clubId: number; facilityId: number;
} }
export default function MaterialisationStatusPanel({ clubId }: MaterialisationStatusPanelProps) { export default function MaterialisationStatusPanel({ facilityId }: MaterialisationStatusPanelProps) {
const [status, setStatus] = useState<MaterialisationStatus | null>(null); const [status, setStatus] = useState<MaterialisationStatus | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [triggering, setTriggering] = useState(false); const [triggering, setTriggering] = useState(false);
@ -30,7 +30,7 @@ export default function MaterialisationStatusPanel({ clubId }: MaterialisationSt
// Poll status on mount and when triggering completes // Poll status on mount and when triggering completes
useEffect(() => { useEffect(() => {
loadStatus(); loadStatus();
}, [clubId]); }, [facilityId]);
// Poll status every 2 seconds while job is active (queued/running) // Poll status every 2 seconds while job is active (queued/running)
useEffect(() => { useEffect(() => {
@ -96,7 +96,7 @@ export default function MaterialisationStatusPanel({ clubId }: MaterialisationSt
}, [status?.rate_limit.next_available_at]); }, [status?.rate_limit.next_available_at]);
async function loadStatus() { async function loadStatus() {
const result = await getMaterialisationStatus(clubId); const result = await getMaterialisationStatus(facilityId);
if (result.success) { if (result.success) {
setStatus(result.data); setStatus(result.data);
@ -116,7 +116,7 @@ export default function MaterialisationStatusPanel({ clubId }: MaterialisationSt
const idempotencyKey = generateIdempotencyKey(); const idempotencyKey = generateIdempotencyKey();
const result = await triggerMaterialisation(clubId, { const result = await triggerMaterialisation(facilityId, {
idempotency_key: idempotencyKey, idempotency_key: idempotencyKey,
policy_profile: selectedPolicy, policy_profile: selectedPolicy,
}); });

@ -7,7 +7,7 @@ import { DAY_NAMES, formatTimeForAPI } from '@/src/types/slot-definitions';
import { createSlotDefinition, updateSlotDefinition } from '@/src/lib/api/slot-definitions'; import { createSlotDefinition, updateSlotDefinition } from '@/src/lib/api/slot-definitions';
interface SlotDefinitionFormProps { interface SlotDefinitionFormProps {
clubId: number; facilityId: number;
courts: { court_id: number; name: string }[]; courts: { court_id: number; name: string }[];
definition?: SlotDefinition; definition?: SlotDefinition;
onClose: () => void; onClose: () => void;
@ -15,7 +15,7 @@ interface SlotDefinitionFormProps {
} }
export default function SlotDefinitionForm({ export default function SlotDefinitionForm({
clubId, facilityId,
courts, courts,
definition, definition,
onClose, onClose,
@ -113,8 +113,8 @@ export default function SlotDefinitionForm({
}; };
const result = isEditing const result = isEditing
? await updateSlotDefinition(clubId, definition.slot_definition_id, request) ? await updateSlotDefinition(facilityId, definition.slot_definition_id, request)
: await createSlotDefinition(clubId, request); : await createSlotDefinition(facilityId, request);
if (result.success) { if (result.success) {
onSuccess(); onSuccess();

@ -13,10 +13,10 @@ import GenerateSlotDefinitionsModal from './GenerateSlotDefinitionsModal';
import CloneSlotDefinitionModal from './CloneSlotDefinitionModal'; import CloneSlotDefinitionModal from './CloneSlotDefinitionModal';
interface SlotDefinitionsComponentProps { interface SlotDefinitionsComponentProps {
clubId: number; facilityId: number;
} }
export default function SlotDefinitionsComponent({ clubId }: SlotDefinitionsComponentProps) { export default function SlotDefinitionsComponent({ facilityId }: SlotDefinitionsComponentProps) {
const [definitions, setDefinitions] = useState<SlotDefinition[]>([]); const [definitions, setDefinitions] = useState<SlotDefinition[]>([]);
const [courts, setCourts] = useState<Court[]>([]); const [courts, setCourts] = useState<Court[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@ -28,18 +28,18 @@ export default function SlotDefinitionsComponent({ clubId }: SlotDefinitionsComp
useEffect(() => { useEffect(() => {
loadData(); loadData();
}, [clubId]); }, [facilityId]);
async function loadData() { async function loadData() {
setLoading(true); setLoading(true);
// Fetch club detail to get courts with actual names // Fetch club detail to get courts with actual names
const clubResult = await getAdminClubDetail(clubId); const clubResult = await getAdminClubDetail(facilityId);
if (clubResult.success) { if (clubResult.success) {
setCourts(clubResult.data.courts); setCourts(clubResult.data.courts);
} }
const result = await getSlotDefinitions(clubId); const result = await getSlotDefinitions(facilityId);
if (result.success) { if (result.success) {
setDefinitions(result.data); setDefinitions(result.data);
setError(null); setError(null);
@ -52,7 +52,7 @@ export default function SlotDefinitionsComponent({ clubId }: SlotDefinitionsComp
async function loadDefinitions() { async function loadDefinitions() {
// Reload just definitions (for after create/edit/delete) // Reload just definitions (for after create/edit/delete)
const result = await getSlotDefinitions(clubId); const result = await getSlotDefinitions(facilityId);
if (result.success) { if (result.success) {
setDefinitions(result.data); setDefinitions(result.data);
} }
@ -73,7 +73,7 @@ export default function SlotDefinitionsComponent({ clubId }: SlotDefinitionsComp
return; return;
} }
const result = await deleteSlotDefinition(clubId, definition.slot_definition_id); const result = await deleteSlotDefinition(facilityId, definition.slot_definition_id);
if (result.success) { if (result.success) {
loadDefinitions(); loadDefinitions();
} else { } else {
@ -158,7 +158,7 @@ export default function SlotDefinitionsComponent({ clubId }: SlotDefinitionsComp
</div> </div>
{/* Materialisation Status Panel */} {/* Materialisation Status Panel */}
<MaterialisationStatusPanel clubId={clubId} /> <MaterialisationStatusPanel facilityId={facilityId} />
{/* Empty state */} {/* Empty state */}
{definitions.length === 0 ? ( {definitions.length === 0 ? (
@ -272,7 +272,7 @@ export default function SlotDefinitionsComponent({ clubId }: SlotDefinitionsComp
{/* Create/Edit Modal */} {/* Create/Edit Modal */}
{showCreateModal && ( {showCreateModal && (
<SlotDefinitionForm <SlotDefinitionForm
clubId={clubId} facilityId={facilityId}
courts={courts} courts={courts}
definition={editingDefinition} definition={editingDefinition}
onClose={handleFormClose} onClose={handleFormClose}
@ -283,7 +283,7 @@ export default function SlotDefinitionsComponent({ clubId }: SlotDefinitionsComp
{/* Generate Modal */} {/* Generate Modal */}
{showGenerateModal && ( {showGenerateModal && (
<GenerateSlotDefinitionsModal <GenerateSlotDefinitionsModal
clubId={clubId} facilityId={facilityId}
courts={courts} courts={courts}
onClose={() => setShowGenerateModal(false)} onClose={() => setShowGenerateModal(false)}
onSuccess={loadDefinitions} onSuccess={loadDefinitions}
@ -293,7 +293,7 @@ export default function SlotDefinitionsComponent({ clubId }: SlotDefinitionsComp
{/* Clone Modal */} {/* Clone Modal */}
{cloningDefinition && ( {cloningDefinition && (
<CloneSlotDefinitionModal <CloneSlotDefinitionModal
clubId={clubId} facilityId={facilityId}
sourceDefinition={cloningDefinition} sourceDefinition={cloningDefinition}
courts={courts} courts={courts}
onClose={() => setCloningDefinition(undefined)} onClose={() => setCloningDefinition(undefined)}

@ -0,0 +1,12 @@
import SlotDefinitionsComponent from './SlotDefinitionsComponent';
export default async function SlotDefinitionsPage({
params
}: {
params: Promise<{ facility_id: string }>;
}) {
const { facility_id } = await params;
const facilityId = parseInt(facility_id, 10);
return <SlotDefinitionsComponent facilityId={facilityId} />;
}

@ -7,7 +7,7 @@ import type { CreateSlotInstanceRequest, SlotInstanceError } from '@/src/types/s
import type { Court } from '@/src/types/courts'; import type { Court } from '@/src/types/courts';
interface ManualSlotModalProps { interface ManualSlotModalProps {
clubId: number; facilityId: number;
courts: Court[]; courts: Court[];
initialDate: string; initialDate: string;
onClose: () => void; onClose: () => void;
@ -15,7 +15,7 @@ interface ManualSlotModalProps {
} }
export default function ManualSlotModal({ export default function ManualSlotModal({
clubId, facilityId,
courts, courts,
initialDate, initialDate,
onClose, onClose,
@ -87,7 +87,7 @@ export default function ManualSlotModal({
reason: reason.trim() || undefined, reason: reason.trim() || undefined,
}; };
const result = await createSlotInstance(clubId, request); const result = await createSlotInstance(facilityId, request);
if (result.success) { if (result.success) {
onSuccess(); onSuccess();

@ -13,14 +13,14 @@ import {
} from '@/src/types/slot-instances'; } from '@/src/types/slot-instances';
interface SlotInstanceEditModalProps { interface SlotInstanceEditModalProps {
clubId: number; facilityId: number;
slot: SlotInstance; slot: SlotInstance;
onClose: () => void; onClose: () => void;
onSuccess: () => void; onSuccess: () => void;
} }
export default function SlotInstanceEditModal({ export default function SlotInstanceEditModal({
clubId, facilityId,
slot, slot,
onClose, onClose,
onSuccess, onSuccess,
@ -93,7 +93,7 @@ export default function SlotInstanceEditModal({
return; return;
} }
const result = await updateSlotInstance(clubId, slot.slot_instance_id, request); const result = await updateSlotInstance(facilityId, slot.slot_instance_id, request);
if (result.success) { if (result.success) {
onSuccess(); onSuccess();
@ -113,7 +113,7 @@ export default function SlotInstanceEditModal({
setError(null); setError(null);
const result = await convertToManualSlot( const result = await convertToManualSlot(
clubId, facilityId,
slot.slot_instance_id, slot.slot_instance_id,
`Converted to manual by admin (was: definition ${slot.origin_definition_id})` `Converted to manual by admin (was: definition ${slot.origin_definition_id})`
); );

@ -28,10 +28,10 @@ import SlotInstanceEditModal from './SlotInstanceEditModal';
import ManualSlotModal from './ManualSlotModal'; import ManualSlotModal from './ManualSlotModal';
interface SlotInstancesComponentProps { interface SlotInstancesComponentProps {
clubId: number; facilityId: number;
} }
export default function SlotInstancesComponent({ clubId }: SlotInstancesComponentProps) { export default function SlotInstancesComponent({ facilityId }: SlotInstancesComponentProps) {
const [slots, setSlots] = useState<SlotInstance[]>([]); const [slots, setSlots] = useState<SlotInstance[]>([]);
const [courts, setCourts] = useState<Court[]>([]); const [courts, setCourts] = useState<Court[]>([]);
const [timezone, setTimezone] = useState<string>('UTC'); const [timezone, setTimezone] = useState<string>('UTC');
@ -54,14 +54,14 @@ export default function SlotInstancesComponent({ clubId }: SlotInstancesComponen
useEffect(() => { useEffect(() => {
loadCourts(); loadCourts();
}, [clubId]); }, [facilityId]);
useEffect(() => { useEffect(() => {
loadSlots(); loadSlots();
}, [clubId, selectedDate, selectedCourtId, showCancelled]); }, [facilityId, selectedDate, selectedCourtId, showCancelled]);
async function loadCourts() { async function loadCourts() {
const result = await getAdminClubDetail(clubId); const result = await getAdminClubDetail(facilityId);
if (result.success) { if (result.success) {
setCourts(result.data.courts); setCourts(result.data.courts);
} }
@ -69,7 +69,7 @@ export default function SlotInstancesComponent({ clubId }: SlotInstancesComponen
async function loadSlots() { async function loadSlots() {
setLoading(true); setLoading(true);
const result = await getSlotInstances(clubId, selectedDate, { const result = await getSlotInstances(facilityId, selectedDate, {
court_id: selectedCourtId ?? undefined, court_id: selectedCourtId ?? undefined,
include_cancelled: showCancelled, include_cancelled: showCancelled,
}); });
@ -100,7 +100,7 @@ export default function SlotInstancesComponent({ clubId }: SlotInstancesComponen
return; return;
} }
const result = await deleteSlotInstance(clubId, slot.slot_instance_id); const result = await deleteSlotInstance(facilityId, slot.slot_instance_id);
if (result.success) { if (result.success) {
loadSlots(); loadSlots();
} else { } else {
@ -113,7 +113,7 @@ export default function SlotInstancesComponent({ clubId }: SlotInstancesComponen
return; return;
} }
const result = await cancelSlotInstance(clubId, slot.slot_instance_id); const result = await cancelSlotInstance(facilityId, slot.slot_instance_id);
if (result.success) { if (result.success) {
loadSlots(); loadSlots();
} else { } else {
@ -431,7 +431,7 @@ export default function SlotInstancesComponent({ clubId }: SlotInstancesComponen
{/* Edit Modal */} {/* Edit Modal */}
{editingSlot && ( {editingSlot && (
<SlotInstanceEditModal <SlotInstanceEditModal
clubId={clubId} facilityId={facilityId}
slot={editingSlot} slot={editingSlot}
onClose={() => setEditingSlot(null)} onClose={() => setEditingSlot(null)}
onSuccess={() => { onSuccess={() => {
@ -444,7 +444,7 @@ export default function SlotInstancesComponent({ clubId }: SlotInstancesComponen
{/* Create Modal */} {/* Create Modal */}
{showCreateModal && ( {showCreateModal && (
<ManualSlotModal <ManualSlotModal
clubId={clubId} facilityId={facilityId}
courts={courts} courts={courts}
initialDate={selectedDate} initialDate={selectedDate}
onClose={() => setShowCreateModal(false)} onClose={() => setShowCreateModal(false)}

@ -0,0 +1,12 @@
import SlotInstancesComponent from './SlotInstancesComponent';
export default async function SlotInstancesPage({
params
}: {
params: Promise<{ facility_id: string }>;
}) {
const { facility_id } = await params;
const facilityId = parseInt(facility_id, 10);
return <SlotInstancesComponent facilityId={facilityId} />;
}

@ -12,13 +12,13 @@ import {
import type { Court, CourtRequest, CourtDependencies, Sport, SportVariation } from '@/src/types/courts'; import type { Court, CourtRequest, CourtDependencies, Sport, SportVariation } from '@/src/types/courts';
import { formatTimestamp } from '@/src/types/courts'; import { formatTimestamp } from '@/src/types/courts';
interface ClubCourtsTabProps { interface FacilityCourtsTabProps {
clubId: number; facilityId: number;
courts: Court[]; courts: Court[];
onUpdate: () => void; onUpdate: () => void;
} }
export default function ClubCourtsTab({ clubId, courts, onUpdate }: ClubCourtsTabProps) { export default function FacilityCourtsTab({ facilityId, courts, onUpdate }: FacilityCourtsTabProps) {
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
// Modal state // Modal state
@ -43,7 +43,7 @@ export default function ClubCourtsTab({ clubId, courts, onUpdate }: ClubCourtsTa
setSelectedCourt(court); setSelectedCourt(court);
// Check dependencies first // Check dependencies first
const result = await getCourtDependencies(clubId, court.court_id); const result = await getCourtDependencies(facilityId, court.court_id);
if (result.success) { if (result.success) {
setDependencies(result.data); setDependencies(result.data);
@ -173,7 +173,7 @@ export default function ClubCourtsTab({ clubId, courts, onUpdate }: ClubCourtsTa
{/* Add Modal */} {/* Add Modal */}
{showAddModal && ( {showAddModal && (
<CourtFormModal <CourtFormModal
clubId={clubId} facilityId={facilityId}
onClose={closeModals} onClose={closeModals}
onSuccess={handleSuccess} onSuccess={handleSuccess}
/> />
@ -182,7 +182,7 @@ export default function ClubCourtsTab({ clubId, courts, onUpdate }: ClubCourtsTa
{/* Edit Modal */} {/* Edit Modal */}
{showEditModal && selectedCourt && ( {showEditModal && selectedCourt && (
<CourtFormModal <CourtFormModal
clubId={clubId} facilityId={facilityId}
court={selectedCourt} court={selectedCourt}
onClose={closeModals} onClose={closeModals}
onSuccess={handleSuccess} onSuccess={handleSuccess}
@ -192,7 +192,7 @@ export default function ClubCourtsTab({ clubId, courts, onUpdate }: ClubCourtsTa
{/* Delete Confirmation Modal */} {/* Delete Confirmation Modal */}
{showDeleteModal && selectedCourt && ( {showDeleteModal && selectedCourt && (
<DeleteConfirmationModal <DeleteConfirmationModal
clubId={clubId} facilityId={facilityId}
court={selectedCourt} court={selectedCourt}
onClose={closeModals} onClose={closeModals}
onSuccess={handleSuccess} onSuccess={handleSuccess}
@ -215,13 +215,13 @@ export default function ClubCourtsTab({ clubId, courts, onUpdate }: ClubCourtsTa
* Court Form Modal (Add/Edit) * Court Form Modal (Add/Edit)
*/ */
interface CourtFormModalProps { interface CourtFormModalProps {
clubId: number; facilityId: number;
court?: Court; court?: Court;
onClose: () => void; onClose: () => void;
onSuccess: () => void; onSuccess: () => void;
} }
function CourtFormModal({ clubId, court, onClose, onSuccess }: CourtFormModalProps) { function CourtFormModal({ facilityId, court, onClose, onSuccess }: CourtFormModalProps) {
const isEditing = !!court; const isEditing = !!court;
const [name, setName] = useState(court?.name || ''); const [name, setName] = useState(court?.name || '');
// Get sport_variation_id from nested structure or fall back to deprecated flat field // Get sport_variation_id from nested structure or fall back to deprecated flat field
@ -276,8 +276,8 @@ function CourtFormModal({ clubId, court, onClose, onSuccess }: CourtFormModalPro
}; };
const result = isEditing const result = isEditing
? await updateCourt(clubId, court!.court_id, request) ? await updateCourt(facilityId, court!.court_id, request)
: await createCourt(clubId, request); : await createCourt(facilityId, request);
if (result.success) { if (result.success) {
onSuccess(); onSuccess();
@ -418,13 +418,13 @@ function CourtFormModal({ clubId, court, onClose, onSuccess }: CourtFormModalPro
* Delete Confirmation Modal * Delete Confirmation Modal
*/ */
interface DeleteConfirmationModalProps { interface DeleteConfirmationModalProps {
clubId: number; facilityId: number;
court: Court; court: Court;
onClose: () => void; onClose: () => void;
onSuccess: () => void; onSuccess: () => void;
} }
function DeleteConfirmationModal({ clubId, court, onClose, onSuccess }: DeleteConfirmationModalProps) { function DeleteConfirmationModal({ facilityId, court, onClose, onSuccess }: DeleteConfirmationModalProps) {
const [deleting, setDeleting] = useState(false); const [deleting, setDeleting] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
@ -432,7 +432,7 @@ function DeleteConfirmationModal({ clubId, court, onClose, onSuccess }: DeleteCo
setDeleting(true); setDeleting(true);
setError(''); setError('');
const result = await deleteCourt(clubId, court.court_id); const result = await deleteCourt(facilityId, court.court_id);
if (result.success) { if (result.success) {
onSuccess(); onSuccess();

@ -66,12 +66,12 @@ function EditableField({
); );
} }
interface ClubProfileTabProps { interface FacilityProfileTabProps {
clubId: number; facilityId: number;
onUpdate?: () => void; onUpdate?: () => void;
} }
export default function ClubProfileTab({ clubId, onUpdate }: ClubProfileTabProps) { export default function FacilityProfileTab({ facilityId, onUpdate }: FacilityProfileTabProps) {
const [profile, setProfile] = useState<ClubProfile | null>(null); const [profile, setProfile] = useState<ClubProfile | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
@ -96,11 +96,11 @@ export default function ClubProfileTab({ clubId, onUpdate }: ClubProfileTabProps
useEffect(() => { useEffect(() => {
loadProfile(); loadProfile();
}, [clubId]); }, [facilityId]);
async function loadProfile() { async function loadProfile() {
setLoading(true); setLoading(true);
const result = await getClubProfile(clubId); const result = await getClubProfile(facilityId);
if (result.success) { if (result.success) {
const prof = result.data; const prof = result.data;
@ -182,7 +182,7 @@ export default function ClubProfileTab({ clubId, onUpdate }: ClubProfileTabProps
}, },
}; };
const result = await updateClubProfile(clubId, request); const result = await updateClubProfile(facilityId, request);
if (result.success) { if (result.success) {
setProfile(result.data); setProfile(result.data);

@ -6,12 +6,12 @@ import { listFacilityTransfers } from '@/src/lib/api/facility-admin';
import type { AdminTransferOffer, TransferListFilters } from '@/src/types/facility-admin'; import type { AdminTransferOffer, TransferListFilters } from '@/src/types/facility-admin';
interface TransfersManagementComponentProps { interface TransfersManagementComponentProps {
clubId: number; facilityId: number;
} }
type TransferStatus = 'all' | 'pending' | 'accepted' | 'declined' | 'expired' | 'cancelled'; type TransferStatus = 'all' | 'pending' | 'accepted' | 'declined' | 'expired' | 'cancelled';
export default function TransfersManagementComponent({ clubId }: TransfersManagementComponentProps) { export default function TransfersManagementComponent({ facilityId }: TransfersManagementComponentProps) {
const [transfers, setTransfers] = useState<AdminTransferOffer[]>([]); const [transfers, setTransfers] = useState<AdminTransferOffer[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@ -23,7 +23,7 @@ export default function TransfersManagementComponent({ clubId }: TransfersManage
useEffect(() => { useEffect(() => {
fetchTransfers(); fetchTransfers();
}, [clubId, statusFilter, dateRange]); }, [facilityId, statusFilter, dateRange]);
async function fetchTransfers() { async function fetchTransfers() {
setLoading(true); setLoading(true);
@ -40,7 +40,7 @@ export default function TransfersManagementComponent({ clubId }: TransfersManage
filters.to_date = dateRange.to; filters.to_date = dateRange.to;
} }
const result = await listFacilityTransfers(clubId, filters); const result = await listFacilityTransfers(facilityId, filters);
if (result.success) { if (result.success) {
setTransfers(result.data); setTransfers(result.data);

@ -0,0 +1,12 @@
import TransfersManagementComponent from './TransfersManagementComponent';
export default async function TransfersManagementPage({
params
}: {
params: Promise<{ facility_id: string }>;
}) {
const { facility_id } = await params;
const facilityId = parseInt(facility_id, 10);
return <TransfersManagementComponent facilityId={facilityId} />;
}

@ -1,10 +1,10 @@
import AdminClubsList from './AdminClubsList'; import AdminFacilitiesList from './AdminFacilitiesList';
import AdminAuthGuard from '@/src/components/AdminAuthGuard'; import AdminAuthGuard from '@/src/components/AdminAuthGuard';
export default async function AdminClubsPage() { export default async function AdminClubsPage() {
return ( return (
<AdminAuthGuard> <AdminAuthGuard>
<AdminClubsList /> <AdminFacilitiesList />
</AdminAuthGuard> </AdminAuthGuard>
); );
} }

@ -9,5 +9,5 @@ export default async function DashboardPage({
const { locale } = await params; const { locale } = await params;
// Manager portal dashboard redirects to club management // Manager portal dashboard redirects to club management
redirect(`/${locale}/admin/clubs`); redirect(`/${locale}/admin/facilities`);
} }

@ -6,33 +6,33 @@ import Link from 'next/link';
import { getAdminClubDetail } from '@/src/lib/api/admin-clubs'; import { getAdminClubDetail } from '@/src/lib/api/admin-clubs';
import type { AdminClubDetail, AdminApiError } from '@/src/types/admin-api'; import type { AdminClubDetail, AdminApiError } from '@/src/types/admin-api';
import useTranslation from '@/src/hooks/useTranslation'; import useTranslation from '@/src/hooks/useTranslation';
import ClubTabNavigation from './ClubTabNavigation'; import FacilityTabNavigation from './FacilityTabNavigation';
interface ClubDetailHeaderProps { interface FacilityDetailHeaderProps {
clubId: number; facilityId: number;
children: React.ReactNode; children: React.ReactNode;
} }
export default function ClubDetailHeader({ clubId, children }: ClubDetailHeaderProps) { export default function FacilityDetailHeader({ facilityId, children }: FacilityDetailHeaderProps) {
const { locale } = useTranslation(); const { locale } = useTranslation();
const [clubDetail, setClubDetail] = useState<AdminClubDetail | null>(null); const [facilityDetail, setFacilityDetail] = useState<AdminClubDetail | null>(null);
const [error, setError] = useState<AdminApiError | null>(null); const [error, setError] = useState<AdminApiError | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
loadClubDetail(); loadFacilityDetail();
}, [clubId]); }, [facilityId]);
async function loadClubDetail() { async function loadFacilityDetail() {
setLoading(true); setLoading(true);
const result = await getAdminClubDetail(clubId); const result = await getAdminClubDetail(facilityId);
if (result.success) { if (result.success) {
setClubDetail(result.data); setFacilityDetail(result.data);
setError(null); setError(null);
} else { } else {
setError(result.error); setError(result.error);
setClubDetail(null); setFacilityDetail(null);
} }
setLoading(false); setLoading(false);
@ -44,7 +44,7 @@ export default function ClubDetailHeader({ clubId, children }: ClubDetailHeaderP
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
<div className="flex flex-col items-center justify-center space-y-4"> <div className="flex flex-col items-center justify-center space-y-4">
<Loader2 className="w-12 h-12 text-slate-900 animate-spin" /> <Loader2 className="w-12 h-12 text-slate-900 animate-spin" />
<p className="text-slate-600 font-medium">Loading club details...</p> <p className="text-slate-600 font-medium">Loading facility details...</p>
</div> </div>
</div> </div>
); );
@ -89,11 +89,11 @@ export default function ClubDetailHeader({ clubId, children }: ClubDetailHeaderP
{error.detail} {error.detail}
</p> </p>
<Link <Link
href={`/${locale}/admin/clubs`} href={`/${locale}/admin/facilities`}
className="inline-flex items-center px-4 py-2 bg-slate-900 text-white font-semibold rounded-lg hover:bg-slate-800 transition-colors" className="inline-flex items-center px-4 py-2 bg-slate-900 text-white font-semibold rounded-lg hover:bg-slate-800 transition-colors"
> >
<ArrowLeft className="w-4 h-4 mr-2" /> <ArrowLeft className="w-4 h-4 mr-2" />
Back to clubs Back to facilities
</Link> </Link>
</div> </div>
</div> </div>
@ -113,7 +113,7 @@ export default function ClubDetailHeader({ clubId, children }: ClubDetailHeaderP
<AlertCircle className="w-8 h-8 text-red-600 flex-shrink-0 mt-1" /> <AlertCircle className="w-8 h-8 text-red-600 flex-shrink-0 mt-1" />
<div className="flex-1"> <div className="flex-1">
<h2 className="text-2xl font-bold text-slate-900 mb-4"> <h2 className="text-2xl font-bold text-slate-900 mb-4">
Error Loading Club Error Loading Facility
</h2> </h2>
<p className="text-slate-700 mb-4 leading-relaxed"> <p className="text-slate-700 mb-4 leading-relaxed">
{error.detail} {error.detail}
@ -122,11 +122,11 @@ export default function ClubDetailHeader({ clubId, children }: ClubDetailHeaderP
Error code: {error.code} Error code: {error.code}
</p> </p>
<Link <Link
href={`/${locale}/admin/clubs`} href={`/${locale}/admin/facilities`}
className="inline-flex items-center px-4 py-2 bg-slate-900 text-white font-semibold rounded-lg hover:bg-slate-800 transition-colors" className="inline-flex items-center px-4 py-2 bg-slate-900 text-white font-semibold rounded-lg hover:bg-slate-800 transition-colors"
> >
<ArrowLeft className="w-4 h-4 mr-2" /> <ArrowLeft className="w-4 h-4 mr-2" />
Back to clubs Back to facilities
</Link> </Link>
</div> </div>
</div> </div>
@ -136,7 +136,7 @@ export default function ClubDetailHeader({ clubId, children }: ClubDetailHeaderP
); );
} }
if (!clubDetail) { if (!facilityDetail) {
return null; return null;
} }
@ -145,26 +145,26 @@ export default function ClubDetailHeader({ clubId, children }: ClubDetailHeaderP
{/* Breadcrumb */} {/* Breadcrumb */}
<div className="mb-8"> <div className="mb-8">
<Link <Link
href={`/${locale}/admin/clubs`} href={`/${locale}/admin/facilities`}
className="inline-flex items-center text-slate-600 hover:text-slate-900 font-medium transition-colors" className="inline-flex items-center text-slate-600 hover:text-slate-900 font-medium transition-colors"
> >
<ArrowLeft className="w-4 h-4 mr-2" /> <ArrowLeft className="w-4 h-4 mr-2" />
Back to clubs Back to facilities
</Link> </Link>
</div> </div>
{/* Club Header */} {/* Facility Header */}
<div className="mb-8"> <div className="mb-8">
<h1 className="text-4xl font-bold text-slate-900 mb-2 tracking-tight"> <h1 className="text-4xl font-bold text-slate-900 mb-2 tracking-tight">
{clubDetail.facility.name} {facilityDetail.facility.name}
</h1> </h1>
<p className="text-lg text-slate-600 font-light"> <p className="text-lg text-slate-600 font-light">
{clubDetail.facility.timezone} {facilityDetail.facility.timezone}
</p> </p>
</div> </div>
{/* Tab Navigation */} {/* Tab Navigation */}
<ClubTabNavigation clubId={clubId} /> <FacilityTabNavigation facilityId={facilityId} />
{/* Content */} {/* Content */}
{children} {children}

@ -4,8 +4,8 @@ import Link from 'next/link';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import useTranslation from '@/src/hooks/useTranslation'; import useTranslation from '@/src/hooks/useTranslation';
interface ClubTabNavigationProps { interface FacilityTabNavigationProps {
clubId: number; facilityId: number;
} }
interface TabConfig { interface TabConfig {
@ -14,11 +14,11 @@ interface TabConfig {
href: string; href: string;
} }
export default function ClubTabNavigation({ clubId }: ClubTabNavigationProps) { export default function FacilityTabNavigation({ facilityId }: FacilityTabNavigationProps) {
const { locale } = useTranslation(); const { locale } = useTranslation();
const pathname = usePathname(); const pathname = usePathname();
const basePath = `/${locale}/admin/clubs/${clubId}`; const basePath = `/${locale}/admin/facilities/${facilityId}`;
const tabs: TabConfig[] = [ const tabs: TabConfig[] = [
{ key: 'profile', label: 'Profile', href: basePath }, { key: 'profile', label: 'Profile', href: basePath },
@ -34,7 +34,7 @@ export default function ClubTabNavigation({ clubId }: ClubTabNavigationProps) {
]; ];
function isActive(tab: TabConfig): boolean { function isActive(tab: TabConfig): boolean {
// For the base club path (profile), check if we're exactly on it // For the base facility path (profile), check if we're exactly on it
if (tab.key === 'profile') { if (tab.key === 'profile') {
// Active if pathname is exactly the basePath (no sub-routes) // Active if pathname is exactly the basePath (no sub-routes)
return pathname === basePath; return pathname === basePath;

@ -329,7 +329,7 @@ export default function Navigation({ pageTitle }: NavigationProps) {
animate="open" animate="open"
> >
<Link <Link
href={localizedLink("/admin/clubs")} href={localizedLink("/admin/facilities")}
onClick={closeMenu} onClick={closeMenu}
className="flex items-center space-x-3 p-4 rounded-xl hover:bg-slate-50 transition-colors duration-200 group" className="flex items-center space-x-3 p-4 rounded-xl hover:bg-slate-50 transition-colors duration-200 group"
> >

@ -6,6 +6,7 @@ import { Calendar, Users, MapPin, ChevronRight } from 'lucide-react';
import Card from '@/src/components/cards/Card'; import Card from '@/src/components/cards/Card';
import CompetitionStatusBadge from './CompetitionStatusBadge'; import CompetitionStatusBadge from './CompetitionStatusBadge';
import CompetitionTypeBadge from './CompetitionTypeBadge'; import CompetitionTypeBadge from './CompetitionTypeBadge';
import useTranslation from '@/src/hooks/useTranslation';
import type { Competition } from '@/src/types/competition'; import type { Competition } from '@/src/types/competition';
interface CompetitionCardProps { interface CompetitionCardProps {
@ -35,7 +36,8 @@ export default function CompetitionCard({
facilityId, facilityId,
className = '', className = '',
}: CompetitionCardProps) { }: CompetitionCardProps) {
const href = `/facility/${facilityId}/competitions/${competition.competition_id}`; const { locale } = useTranslation();
const href = `/${locale}/admin/facilities/${facilityId}/competitions/${competition.competition_id}`;
return ( return (
<Link href={href} className={`block group ${className}`}> <Link href={href} className={`block group ${className}`}>

@ -8,6 +8,7 @@ import {
startCompetition, startCompetition,
finishCompetition, finishCompetition,
cancelCompetition, cancelCompetition,
deleteCompetition,
saveCompetitionAsTemplate, saveCompetitionAsTemplate,
approveRegistration, approveRegistration,
rejectRegistration, rejectRegistration,
@ -237,6 +238,30 @@ export function useCancelCompetition(competitionId: number) {
}); });
} }
/**
* Delete a competition (draft or cancelled only)
*/
export function useDeleteCompetition(competitionId: number) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async () => {
const result = await deleteCompetition(competitionId);
if (!result.success) {
throw new Error(result.error.detail);
}
return result.data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: competitionQueryKeys.lists() });
// Remove the detail query since the competition no longer exists
queryClient.removeQueries({
queryKey: competitionQueryKeys.detail(competitionId),
});
},
});
}
/** /**
* Save competition as template * Save competition as template
*/ */

@ -263,10 +263,26 @@ export async function createCompetition(
request: CreateCompetitionRequest request: CreateCompetitionRequest
): Promise<CompetitionApiResult<Competition>> { ): Promise<CompetitionApiResult<Competition>> {
try { try {
// Build request body with proper field names for backend
const { config, ...rest } = request;
const body: Record<string, unknown> = {
...rest,
facility_id: facilityId,
scope: request.scope ?? 'facility', // Default to facility scope
};
// Backend expects config_snapshot, not config
if (config) {
body.config_snapshot = config;
} else if (!request.template_id) {
// Provide empty config_snapshot when creating without template
body.config_snapshot = {};
}
const response = await apiFetch('/competitions', { const response = await apiFetch('/competitions', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...request, facility_id: facilityId }), body: JSON.stringify(body),
}); });
const result = await handleApiResponse<{ competition: Competition }>(response); const result = await handleApiResponse<{ competition: Competition }>(response);
@ -396,6 +412,25 @@ export async function cancelCompetition(
} }
} }
/**
* DELETE /competitions/{competition_id}
* Permanently delete a competition (draft or cancelled only)
*/
export async function deleteCompetition(
competitionId: number
): Promise<CompetitionApiResult<void>> {
try {
const response = await apiFetch(`/competitions/${competitionId}`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
});
return handleApiResponse<void>(response);
} catch (error) {
return buildNetworkError(error instanceof Error ? error.message : 'Failed to delete competition');
}
}
/** /**
* POST /competitions/{competition_id}/save-as-template * POST /competitions/{competition_id}/save-as-template
* Create a template from an existing competition's config * Create a template from an existing competition's config

@ -156,4 +156,86 @@ export class BookingService {
throw BookingError.networkError('Network error while changing match type'); throw BookingError.networkError('Network error while changing match type');
} }
} }
/**
* Check if user can cancel their booking on this slot
*
* For native facilities: checks transfer-before-cancel policy
* For remote facilities: always returns can_cancel=true
*/
async canCancelBooking(slotId: string): Promise<CanCancelResponse> {
try {
const response = await this.apiClient(`/booking/${encodeURIComponent(slotId)}/can-cancel`);
if (!response.ok) {
if (response.status === 404) {
throw BookingError.notFound(slotId);
}
const data = await response.json().catch(() => ({ message: 'Failed to check cancel status' }));
throw BookingError.serverError(data.message || 'Failed to check cancel status');
}
const result = await response.json();
return result.data;
} catch (error) {
if (error instanceof BookingError) throw error;
throw BookingError.networkError('Network error while checking cancel status');
}
}
/**
* Cancel booking on this slot for current user
*
* For native facilities: handles refund via booking_transfer service
* For remote facilities: forwards to external provider
*/
async cancelBooking(slotId: string): Promise<CancelBookingResponse> {
try {
const response = await this.apiClient(
`/booking/${encodeURIComponent(slotId)}/cancel`,
{ method: 'POST' }
);
if (!response.ok) {
if (response.status === 404) {
throw BookingError.notFound(slotId);
}
const data = await response.json().catch(() => ({ message: 'Failed to cancel booking' }));
if (response.status === 400) {
throw BookingError.badRequest(data.message || 'Cancellation not allowed');
}
throw BookingError.serverError(data.message || 'Failed to cancel booking');
}
const result = await response.json();
return result.data;
} catch (error) {
if (error instanceof BookingError) throw error;
throw BookingError.networkError('Network error while cancelling booking');
}
}
}
/**
* Response from /booking/{slot_id}/can-cancel
*/
export interface CanCancelResponse {
can_cancel: boolean;
reason: string | null;
requires_transfer: boolean;
refund_eligible: boolean;
}
/**
* Response from /booking/{slot_id}/cancel
*/
export interface CancelBookingResponse {
cancelled: boolean;
refund_amount?: number;
refund_issued?: boolean;
booking_id?: string;
} }

Loading…
Cancel
Save