feat: add bulk operations API support (generate/clone)
continuous-integration/drone/push Build is passing Details

Added TypeScript types and API client functions for slot definition bulk operations.

Types:
- SlotDefinitionPreset enum with 4 presets
- PRESET_OPTIONS metadata for UI display
- GenerateSlotDefinitionsRequest/Response
- CloneSlotDefinitionRequest/Response
- PatternOverrides for preset customization
- SkippedSlot interface for overlap handling

API Functions:
- generateSlotDefinitions() - bulk create from presets
- cloneSlotDefinition() - copy to multiple courts/days

Based on Backend Brooke's spec (BUILD:345, staging).
Next: Build UI modals for generate and clone flows.
master
Guillermo Pages 1 month ago
parent bd2cbb3a0d
commit 13ec348fb2

@ -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<ApiResult<GenerateSlotDefinitionsResponse>> {
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<ApiResult<CloneSlotDefinitionResponse>> {
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 [

@ -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[];
};
}

Loading…
Cancel
Save