diff --git a/src/lib/api/slot-definitions.ts b/src/lib/api/slot-definitions.ts index ffe8138..2c6494c 100644 --- a/src/lib/api/slot-definitions.ts +++ b/src/lib/api/slot-definitions.ts @@ -2,6 +2,10 @@ import type { SlotDefinition, SlotDefinitionRequest, SlotDefinitionError, + GenerateSlotDefinitionsRequest, + GenerateSlotDefinitionsResponse, + CloneSlotDefinitionRequest, + CloneSlotDefinitionResponse, } from '@/src/types/slot-definitions'; const API_BASE_URL = process.env.NEXT_PUBLIC_PYTHON_API_URL; @@ -166,6 +170,82 @@ export async function deleteSlotDefinition( } } +// POST /admin/clubs/{club_id}/slot-definitions/generate +export async function generateSlotDefinitions( + clubId: number, + request: GenerateSlotDefinitionsRequest +): Promise> { + try { + const response = await fetch(`${API_BASE_URL}/admin/clubs/${clubId}/slot-definitions/generate`, { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(request), + }); + + if (!response.ok) { + const error: SlotDefinitionError = await response.json(); + return { success: false, error }; + } + + const data: GenerateSlotDefinitionsResponse = await response.json(); + return { success: true, data }; + } catch (error) { + return { + success: false, + error: { + type: 'about:blank', + title: 'Network Error', + status: 0, + detail: 'Failed to generate slot definitions. Please check your connection.', + code: 'network_error', + }, + }; + } +} + +// POST /admin/clubs/{club_id}/slot-definitions/{slot_definition_id}/clone +export async function cloneSlotDefinition( + clubId: number, + slotDefinitionId: number, + request: CloneSlotDefinitionRequest +): Promise> { + try { + const response = await fetch( + `${API_BASE_URL}/admin/clubs/${clubId}/slot-definitions/${slotDefinitionId}/clone`, + { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(request), + } + ); + + if (!response.ok) { + const error: SlotDefinitionError = await response.json(); + return { success: false, error }; + } + + const data: CloneSlotDefinitionResponse = await response.json(); + return { success: true, data }; + } catch (error) { + return { + success: false, + error: { + type: 'about:blank', + title: 'Network Error', + status: 0, + detail: 'Failed to clone slot definition. Please check your connection.', + code: 'network_error', + }, + }; + } +} + // Mock data for local development export function getMockSlotDefinitions(): SlotDefinition[] { return [ diff --git a/src/types/slot-definitions.ts b/src/types/slot-definitions.ts index 5c8b771..7e73938 100644 --- a/src/types/slot-definitions.ts +++ b/src/types/slot-definitions.ts @@ -84,3 +84,132 @@ export function calculateEndTime(startTime: string, durationMinutes: number): st const endMinutes = totalMinutes % 60; return `${String(endHours).padStart(2, '0')}:${String(endMinutes).padStart(2, '0')}`; } + +// ============================================================================ +// Bulk Operations (Generate & Clone) +// ============================================================================ + +/** + * Available presets for generate endpoint + */ +export type SlotDefinitionPreset = + | 'workday_standard' // Mon-Fri, 8am-10pm, 90min + | 'weekend_extended' // Sat-Sun, 7am-11pm, 90min + | 'all_week_uniform' // Every day, 8am-10pm, 90min + | 'hourly_daytime'; // Mon-Fri, 9am-6pm, 60min + +/** + * Preset metadata for UI display + */ +export interface PresetInfo { + id: SlotDefinitionPreset; + name: string; + description: string; + days: string; + hours: string; + duration: string; +} + +export const PRESET_OPTIONS: PresetInfo[] = [ + { + id: 'workday_standard', + name: 'Weekday Standard', + description: 'Standard weekday operations', + days: 'Mon-Fri', + hours: '8am-10pm', + duration: '90min slots', + }, + { + id: 'weekend_extended', + name: 'Weekend Extended', + description: 'Weekend with early/late slots', + days: 'Sat-Sun', + hours: '7am-11pm', + duration: '90min slots', + }, + { + id: 'all_week_uniform', + name: 'All Week Uniform', + description: 'Uniform schedule year-round', + days: 'Every day', + hours: '8am-10pm', + duration: '90min slots', + }, + { + id: 'hourly_daytime', + name: 'Hourly Daytime', + description: 'Short slots for busy periods', + days: 'Mon-Fri', + hours: '9am-6pm', + duration: '60min slots', + }, +]; + +/** + * Pattern overrides for customizing presets + */ +export interface PatternOverrides { + days?: DayOfWeek[]; + start_time?: string; // HH:MM:SS + end_time?: string; // HH:MM:SS + duration_minutes?: number; + interval_minutes?: number; + capacity?: number; +} + +/** + * Generate endpoint request + */ +export interface GenerateSlotDefinitionsRequest { + preset: SlotDefinitionPreset; + court_ids: number[]; + pattern_overrides?: PatternOverrides; + valid_from?: string; // YYYY-MM-DD + valid_to?: string; // YYYY-MM-DD +} + +/** + * Skipped slot info + */ +export interface SkippedSlot { + court_id: number; + dow: DayOfWeek; + starts_at: string; + reason: string; +} + +/** + * Generate endpoint response + */ +export interface GenerateSlotDefinitionsResponse { + status: 'success'; + data: { + created_count: number; + skipped_count?: number; + created_definitions: SlotDefinition[]; + skipped?: SkippedSlot[]; + }; +} + +/** + * Clone endpoint request + */ +export interface CloneSlotDefinitionRequest { + target_court_ids: number[]; + target_days?: DayOfWeek[]; + valid_from?: string; // YYYY-MM-DD + valid_to?: string; // YYYY-MM-DD +} + +/** + * Clone endpoint response + */ +export interface CloneSlotDefinitionResponse { + status: 'success'; + data: { + created_count: number; + skipped_count?: number; + created_definitions: SlotDefinition[]; + skipped?: SkippedSlot[]; + }; +}