From 9f059bcbfe0f15692b45e1919d7436b2fa93e0f2 Mon Sep 17 00:00:00 2001 From: Guillermo Pages Date: Thu, 27 Nov 2025 10:29:46 +0100 Subject: [PATCH] feat: add plan template picker for membership plan creation - Add PlanTemplate type and listPlanTemplates API function - Create TemplateCard and TemplatePicker components - Modify PlanFormModal to accept initialValues from template - Update MembershipPlansComponent to show template picker first - Add missing translations (Venue Management, Club Management, etc.) --- dictionaries/de.json | 33 ++++- dictionaries/en.json | 1 + dictionaries/fr.json | 33 ++++- .../plans/MembershipPlansComponent.tsx | 42 +++++- src/components/plans/PlanFormModal.tsx | 27 +++- src/components/plans/TemplateCard.tsx | 66 ++++++++++ src/components/plans/TemplatePicker.tsx | 122 ++++++++++++++++++ src/lib/api/facility-admin.ts | 44 +++++++ src/types/facility-admin.ts | 15 +++ 9 files changed, 370 insertions(+), 13 deletions(-) create mode 100644 src/components/plans/TemplateCard.tsx create mode 100644 src/components/plans/TemplatePicker.tsx diff --git a/dictionaries/de.json b/dictionaries/de.json index 3763648..4c63231 100644 --- a/dictionaries/de.json +++ b/dictionaries/de.json @@ -836,5 +836,36 @@ "approval(s)": "Genehmigung(en)", "All approved": "Alle genehmigt", "Slot": "Zeitfenster", - "© 2024 Playchoo Manager. Venue administration portal.": "© 2024 Playchoo Manager. Verwaltungsportal für Veranstaltungsorte." + "© 2024 Playchoo Manager. Venue administration portal.": "© 2024 Playchoo Manager. Verwaltungsportal für Veranstaltungsorte.", + "Club Management": "Clubverwaltung", + "Venue Management": "Anlagenverwaltung", + "View and manage your venue locations": "Ihre Veranstaltungsorte anzeigen und verwalten", + "Clubs": "Clubs", + "Coming soon": "Demnächst", + "Schedules": "Zeitpläne", + "Bookings": "Buchungen", + "Loading clubs...": "Clubs werden geladen...", + "Error Loading Clubs": "Fehler beim Laden der Clubs", + "Error code": "Fehlercode", + "No Clubs Assigned": "Keine Clubs zugewiesen", + "You are not currently assigned as an administrator for any clubs. Contact your organization to request access.": "Sie sind derzeit keinem Club als Administrator zugewiesen. Kontaktieren Sie Ihre Organisation, um Zugang zu beantragen.", + "Courts": "Plätze", + "Loading club details...": "Club-Details werden geladen...", + "Access Denied": "Zugriff verweigert", + "Back to clubs": "Zurück zu Clubs", + "Error Loading Club": "Fehler beim Laden des Clubs", + "Provider Information": "Anbieterinformationen", + "Type": "Typ", + "Manages Slot Storage": "Verwaltet Slot-Speicher", + "Supports Payment Verification": "Unterstützt Zahlungsverifizierung", + "No courts configured": "Keine Plätze konfiguriert", + "Slot Definitions": "Slot-Definitionen", + "No slot definitions configured": "Keine Slot-Definitionen konfiguriert", + "Slot definition": "Slot-Definition", + "Upcoming Slots": "Kommende Slots", + "No upcoming slots available": "Keine kommenden Slots verfügbar", + "Checking authentication...": "Authentifizierung wird überprüft...", + "Redirecting to login...": "Weiterleitung zur Anmeldung...", + "Please log in to access the venue management portal.": "Bitte melden Sie sich an, um auf das Verwaltungsportal zuzugreifen.", + "If you are a venue administrator and do not have access, please contact support.": "Wenn Sie ein Anlagen-Administrator sind und keinen Zugang haben, kontaktieren Sie bitte den Support." } diff --git a/dictionaries/en.json b/dictionaries/en.json index 99551c4..cd2e830 100644 --- a/dictionaries/en.json +++ b/dictionaries/en.json @@ -838,6 +838,7 @@ "approval(s)": "approval(s)", "All approved": "All approved", "Club Management": "Club Management", + "Venue Management": "Venue Management", "View and manage your venue locations": "View and manage your venue locations", "Clubs": "Clubs", "Coming soon": "Coming soon", diff --git a/dictionaries/fr.json b/dictionaries/fr.json index 300e257..bca79c8 100644 --- a/dictionaries/fr.json +++ b/dictionaries/fr.json @@ -832,5 +832,36 @@ "approval(s)": "approbation(s)", "All approved": "Toutes approuvées", "Slot": "Créneau", - "© 2024 Playchoo Manager. Venue administration portal.": "© 2024 Playchoo Manager. Portail d'administration de lieu." + "© 2024 Playchoo Manager. Venue administration portal.": "© 2024 Playchoo Manager. Portail d'administration de lieu.", + "Club Management": "Gestion de club", + "Venue Management": "Gestion de lieu", + "View and manage your venue locations": "Voir et gérer vos sites", + "Clubs": "Clubs", + "Coming soon": "Bientôt disponible", + "Schedules": "Horaires", + "Bookings": "Réservations", + "Loading clubs...": "Chargement des clubs...", + "Error Loading Clubs": "Erreur de chargement des clubs", + "Error code": "Code d'erreur", + "No Clubs Assigned": "Aucun club assigné", + "You are not currently assigned as an administrator for any clubs. Contact your organization to request access.": "Vous n'êtes actuellement assigné comme administrateur d'aucun club. Contactez votre organisation pour demander l'accès.", + "Courts": "Terrains", + "Loading club details...": "Chargement des détails du club...", + "Access Denied": "Accès refusé", + "Back to clubs": "Retour aux clubs", + "Error Loading Club": "Erreur de chargement du club", + "Provider Information": "Informations du fournisseur", + "Type": "Type", + "Manages Slot Storage": "Gère le stockage des créneaux", + "Supports Payment Verification": "Prend en charge la vérification des paiements", + "No courts configured": "Aucun terrain configuré", + "Slot Definitions": "Définitions de créneaux", + "No slot definitions configured": "Aucune définition de créneau configurée", + "Slot definition": "Définition de créneau", + "Upcoming Slots": "Créneaux à venir", + "No upcoming slots available": "Aucun créneau à venir disponible", + "Checking authentication...": "Vérification de l'authentification...", + "Redirecting to login...": "Redirection vers la connexion...", + "Please log in to access the venue management portal.": "Veuillez vous connecter pour accéder au portail de gestion.", + "If you are a venue administrator and do not have access, please contact support.": "Si vous êtes administrateur de lieu et n'avez pas accès, veuillez contacter le support." } diff --git a/src/app/[locale]/admin/clubs/[club_id]/plans/MembershipPlansComponent.tsx b/src/app/[locale]/admin/clubs/[club_id]/plans/MembershipPlansComponent.tsx index b0eeb1f..38e32b3 100644 --- a/src/app/[locale]/admin/clubs/[club_id]/plans/MembershipPlansComponent.tsx +++ b/src/app/[locale]/admin/clubs/[club_id]/plans/MembershipPlansComponent.tsx @@ -2,10 +2,11 @@ import { useState, useEffect } from 'react'; import { listPlans, deletePlan } from '@/src/lib/api/facility-admin'; -import type { MembershipPlan } from '@/src/types/facility-admin'; +import type { MembershipPlan, PlanTemplate } from '@/src/types/facility-admin'; import PlanCard from '@/src/components/plans/PlanCard'; import PlanFormModal from '@/src/components/plans/PlanFormModal'; import PlanListSkeleton from '@/src/components/plans/PlanListSkeleton'; +import TemplatePicker from '@/src/components/plans/TemplatePicker'; interface MembershipPlansComponentProps { clubId: number; @@ -15,7 +16,9 @@ export default function MembershipPlansComponent({ clubId }: MembershipPlansComp const [plans, setPlans] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [templatePickerOpen, setTemplatePickerOpen] = useState(false); const [createModalOpen, setCreateModalOpen] = useState(false); + const [selectedTemplate, setSelectedTemplate] = useState(null); const [editingPlan, setEditingPlan] = useState(null); useEffect(() => { @@ -49,6 +52,22 @@ export default function MembershipPlansComponent({ clubId }: MembershipPlansComp } } + function handleCreateClick() { + // Open template picker first + setTemplatePickerOpen(true); + } + + function handleTemplateSelect(template: PlanTemplate | null) { + setSelectedTemplate(template); + setTemplatePickerOpen(false); + setCreateModalOpen(true); + } + + function handleCreateModalClose() { + setCreateModalOpen(false); + setSelectedTemplate(null); + } + return (
{/* Header */} @@ -58,7 +77,7 @@ export default function MembershipPlansComponent({ clubId }: MembershipPlansComp

Create and manage subscription plans for your facility

+ + ); +} diff --git a/src/components/plans/TemplatePicker.tsx b/src/components/plans/TemplatePicker.tsx new file mode 100644 index 0000000..2bca030 --- /dev/null +++ b/src/components/plans/TemplatePicker.tsx @@ -0,0 +1,122 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import Modal from '@/src/components/modals/Modal'; +import ModalHeader from '@/src/components/modals/ModalHeader'; +import ModalBody from '@/src/components/modals/ModalBody'; +import ModalFooter from '@/src/components/modals/ModalFooter'; +import { listPlanTemplates } from '@/src/lib/api/facility-admin'; +import type { PlanTemplate } from '@/src/types/facility-admin'; +import TemplateCard from './TemplateCard'; + +interface TemplatePickerProps { + isOpen: boolean; + onClose: () => void; + onSelect: (template: PlanTemplate | null) => void; +} + +export default function TemplatePicker({ + isOpen, + onClose, + onSelect +}: TemplatePickerProps) { + const [templates, setTemplates] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + if (isOpen) { + fetchTemplates(); + } + }, [isOpen]); + + async function fetchTemplates() { + setLoading(true); + setError(null); + + const result = await listPlanTemplates(); + + if (result.success) { + setTemplates(result.data); + } else { + setError(result.error.detail || 'Failed to load templates'); + } + + setLoading(false); + } + + function handleSelectTemplate(template: PlanTemplate) { + onSelect(template); + onClose(); + } + + function handleStartFromScratch() { + onSelect(null); + onClose(); + } + + return ( + + + Choose a Template + + + + {error && ( +
+ {error} +
+ )} + + {loading ? ( +
+
+
+ ) : templates.length === 0 ? ( +
+

No templates available

+
+ ) : ( +
+ {templates.map(template => ( + handleSelectTemplate(template)} + /> + ))} +
+ )} + + {/* Divider */} +
+
+
+
+
+ or +
+
+ + {/* Start from Scratch */} +
+ +
+ + + + + + + ); +} diff --git a/src/lib/api/facility-admin.ts b/src/lib/api/facility-admin.ts index cdb6e1a..61403ff 100644 --- a/src/lib/api/facility-admin.ts +++ b/src/lib/api/facility-admin.ts @@ -20,6 +20,7 @@ import type { UpdatePolicyRequest, FacilityAdminError, FacilityAdminApiResult, + PlanTemplate, } from '@/src/types/facility-admin'; import apiFetch from '@/src/utils/apiFetch'; @@ -234,6 +235,49 @@ export async function deletePlan( } } +// ============================================================================ +// Plan Templates +// ============================================================================ + +/** + * GET /admin/plan-templates + * Get all available plan templates + */ +export async function listPlanTemplates( + filters?: { sport_id?: number } +): Promise> { + try { + const params = new URLSearchParams(); + if (filters?.sport_id !== undefined) params.set('sport_id', String(filters.sport_id)); + + const queryString = params.toString(); + const endpoint = `/admin/plan-templates${queryString ? `?${queryString}` : ''}`; + + const response = await apiFetch(endpoint, { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + }); + + const result = await handleApiResponse(response); + // Backend returns { templates: [...] } - extract the templates array + if (result.success && result.data.templates) { + return { success: true, data: result.data.templates }; + } + return result as FacilityAdminApiResult; + } catch (error) { + return { + success: false, + error: { + type: 'about:blank', + title: 'Network Error', + status: 0, + detail: error instanceof Error ? error.message : 'Failed to list templates', + code: 'internal_error', + }, + }; + } +} + // ============================================================================ // Entitlements // ============================================================================ diff --git a/src/types/facility-admin.ts b/src/types/facility-admin.ts index 75a46c1..2a68967 100644 --- a/src/types/facility-admin.ts +++ b/src/types/facility-admin.ts @@ -41,6 +41,21 @@ export interface UpdatePlanRequest { is_active?: boolean; } +// ============================================================================ +// Plan Templates +// ============================================================================ + +export interface PlanTemplate { + plan_template_id: number; + name: string; + description: string | null; + suggested_price_cents: number; + billing_period: BillingPeriod; + default_entitlements: PlanEntitlements; + sport_id: number | null; + display_order: number; +} + // ============================================================================ // Entitlements // ============================================================================