From 2a713716834cd91b5ba10d85cf3be40d4219625d Mon Sep 17 00:00:00 2001 From: Guillermo Pages Date: Sat, 6 Dec 2025 19:42:16 +0100 Subject: [PATCH] feat: complete competition manager UI with scheduling, results, and series - Add SchedulingTab with SlotPickerModal for linking matches to court slots - Add ResultsTab with dispute management (list, resolve, reject disputes) - Add Series management pages (list, create with RRULE configuration) - Add SaveAsTemplateModal to save competition config as reusable template - Add GroupAssignmentModal for assigning participants to groups - Enable approve button for waitlisted registrations - Add dispute types, API functions, and query/mutation hooks - Add series types, API functions, and query/mutation hooks --- .../CompetitionDetailComponent.tsx | 40 +- .../[competition_id]/SaveAsTemplateModal.tsx | 143 +++++ .../tabs/GroupAssignmentModal.tsx | 147 ++++++ .../[competition_id]/tabs/ParticipantsTab.tsx | 62 ++- .../tabs/RegistrationsTab.tsx | 9 +- .../[competition_id]/tabs/ResultsTab.tsx | 492 ++++++++++++++++++ .../[competition_id]/tabs/SchedulingTab.tsx | 360 +++++++++++++ .../[competition_id]/tabs/SlotPickerModal.tsx | 267 ++++++++++ .../competitions/series/CreateSeriesModal.tsx | 345 ++++++++++++ .../series/SeriesListComponent.tsx | 271 ++++++++++ .../[club_id]/competitions/series/page.tsx | 12 + .../mutations/useCompetitionMutations.ts | 200 +++++++ src/hooks/queries/useCompetitionQueries.ts | 100 ++++ src/lib/api/competition-admin.ts | 284 ++++++++++ src/types/competition.ts | 87 ++++ 15 files changed, 2798 insertions(+), 21 deletions(-) create mode 100644 src/app/[locale]/admin/clubs/[club_id]/competitions/[competition_id]/SaveAsTemplateModal.tsx create mode 100644 src/app/[locale]/admin/clubs/[club_id]/competitions/[competition_id]/tabs/GroupAssignmentModal.tsx create mode 100644 src/app/[locale]/admin/clubs/[club_id]/competitions/[competition_id]/tabs/ResultsTab.tsx create mode 100644 src/app/[locale]/admin/clubs/[club_id]/competitions/[competition_id]/tabs/SchedulingTab.tsx create mode 100644 src/app/[locale]/admin/clubs/[club_id]/competitions/[competition_id]/tabs/SlotPickerModal.tsx create mode 100644 src/app/[locale]/admin/clubs/[club_id]/competitions/series/CreateSeriesModal.tsx create mode 100644 src/app/[locale]/admin/clubs/[club_id]/competitions/series/SeriesListComponent.tsx create mode 100644 src/app/[locale]/admin/clubs/[club_id]/competitions/series/page.tsx diff --git a/src/app/[locale]/admin/clubs/[club_id]/competitions/[competition_id]/CompetitionDetailComponent.tsx b/src/app/[locale]/admin/clubs/[club_id]/competitions/[competition_id]/CompetitionDetailComponent.tsx index 0f4ee3a..7fcc406 100644 --- a/src/app/[locale]/admin/clubs/[club_id]/competitions/[competition_id]/CompetitionDetailComponent.tsx +++ b/src/app/[locale]/admin/clubs/[club_id]/competitions/[competition_id]/CompetitionDetailComponent.tsx @@ -16,6 +16,8 @@ import { Settings, Trophy, ListOrdered, + FileText, + CalendarClock, } from 'lucide-react'; import useTranslation from '@/src/hooks/useTranslation'; import Card from '@/src/components/cards/Card'; @@ -31,7 +33,10 @@ import OverviewTab from './tabs/OverviewTab'; import RegistrationsTab from './tabs/RegistrationsTab'; import ParticipantsTab from './tabs/ParticipantsTab'; import FixturesTab from './tabs/FixturesTab'; +import SchedulingTab from './tabs/SchedulingTab'; +import ResultsTab from './tabs/ResultsTab'; import StandingsTab from './tabs/StandingsTab'; +import SaveAsTemplateModal from './SaveAsTemplateModal'; import type { CompetitionStatus } from '@/src/types/competition'; interface CompetitionDetailComponentProps { @@ -39,13 +44,15 @@ interface CompetitionDetailComponentProps { competitionId: number; } -type TabKey = 'overview' | 'registrations' | 'participants' | 'fixtures' | 'standings'; +type TabKey = 'overview' | 'registrations' | 'participants' | 'fixtures' | 'scheduling' | 'results' | 'standings'; const tabs: { key: TabKey; label: string; icon: React.ComponentType<{ className?: string }> }[] = [ { key: 'overview', label: 'Overview', icon: Eye }, { key: 'registrations', label: 'Registrations', icon: Users }, { key: 'participants', label: 'Participants', icon: Trophy }, { key: 'fixtures', label: 'Fixtures', icon: Calendar }, + { key: 'scheduling', label: 'Scheduling', icon: CalendarClock }, + { key: 'results', label: 'Results', icon: CheckCircle }, { key: 'standings', label: 'Standings', icon: ListOrdered }, ]; @@ -157,6 +164,7 @@ export default function CompetitionDetailComponent({ }: CompetitionDetailComponentProps) { const { locale } = useTranslation(); const [activeTab, setActiveTab] = useState('overview'); + const [showSaveAsTemplateModal, setShowSaveAsTemplateModal] = useState(false); const { data: competition, isLoading, error } = useCompetition(competitionId); @@ -218,7 +226,16 @@ export default function CompetitionDetailComponent({

{competition.description}

)} - +
+ + +
{/* Quick Stats */} @@ -310,14 +327,31 @@ export default function CompetitionDetailComponent({ )} {activeTab === 'participants' && ( - + s.groups ?? []) ?? []} + /> )} {activeTab === 'fixtures' && ( )} + {activeTab === 'scheduling' && ( + + )} + {activeTab === 'results' && ( + + )} {activeTab === 'standings' && ( )} + + {/* Save as Template Modal */} + setShowSaveAsTemplateModal(false)} + competitionId={competitionId} + defaultName={competition.title} + /> ); } diff --git a/src/app/[locale]/admin/clubs/[club_id]/competitions/[competition_id]/SaveAsTemplateModal.tsx b/src/app/[locale]/admin/clubs/[club_id]/competitions/[competition_id]/SaveAsTemplateModal.tsx new file mode 100644 index 0000000..1e10a47 --- /dev/null +++ b/src/app/[locale]/admin/clubs/[club_id]/competitions/[competition_id]/SaveAsTemplateModal.tsx @@ -0,0 +1,143 @@ +'use client'; + +import React, { useState } from 'react'; +import { X, Loader2, FileText } from 'lucide-react'; +import { useSaveAsTemplate } from '@/src/hooks/mutations/useCompetitionMutations'; + +interface SaveAsTemplateModalProps { + isOpen: boolean; + onClose: () => void; + competitionId: number; + defaultName: string; +} + +export default function SaveAsTemplateModal({ + isOpen, + onClose, + competitionId, + defaultName, +}: SaveAsTemplateModalProps) { + const [name, setName] = useState(defaultName + ' Template'); + const [description, setDescription] = useState(''); + const saveAsTemplateMutation = useSaveAsTemplate(competitionId); + + if (!isOpen) return null; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!name.trim()) return; + + try { + await saveAsTemplateMutation.mutateAsync({ + name: name.trim(), + description: description.trim() || undefined, + }); + onClose(); + } catch { + // Error is handled by the mutation + } + }; + + return ( +
+ {/* Backdrop */} +
+ + {/* Modal */} +
+ {/* Header */} +
+
+
+ +
+
+

Save as Template

+

Create a reusable template from this competition

+
+
+ +
+ + {/* Form */} +
+
+
+ + setName(e.target.value)} + placeholder="e.g., Weekly Padel Tournament" + className="w-full px-4 py-2 border border-slate-300 rounded-xl focus:ring-2 focus:ring-purple-500 focus:border-purple-500 outline-none transition-all" + required + /> +
+ +
+ +