feat: migrate admin API clients to send locale/timezone headers
continuous-integration/drone/push Build is passing Details

Migrated all admin API clients to automatically send X-Locale and X-Timezone headers
with every request for proper internationalization and timezone-aware operations.

Changes:
- Migrated slot-definitions.ts, materialisation.ts, courts.ts, booking-admin.ts to use apiFetch utility
- Added getLocaleHeaders() helper to admin-clubs.ts for SSR compatibility
- Added getSlotDefinitionPresets() endpoint to fetch localized preset metadata
- Updated slot-definitions types to support API-based preset loading
- Refactored GenerateSlotDefinitionsModal to fetch presets from API with localized titles/descriptions

Benefits:
- All API requests now include user's locale (e.g., en-US, fr-CH) for i18n
- Timezone-aware operations use browser's Intl.DateTimeFormat timezone
- Preset titles and descriptions are localized per user language
- Consistent header management across all admin endpoints

Technical Details:
- apiFetch extracts locale from URL pathname via getPathnameLocale()
- apiFetch automatically adds credentials: 'include' for session auth
- admin-clubs.ts uses manual headers for SSR cookie forwarding compatibility
- GenerateSlotDefinitionsModal shows loading state while fetching presets
master
Guillermo Pages 1 month ago
parent 1ebe61fa40
commit 9c802ea2aa

@ -1,16 +1,17 @@
'use client'; 'use client';
import { useState } from 'react'; import { useState, useEffect } from 'react';
import { X, Loader2, AlertCircle, ChevronDown, ChevronUp } from 'lucide-react'; import { X, Loader2, AlertCircle, ChevronDown, ChevronUp } from 'lucide-react';
import { generateSlotDefinitions } from '@/src/lib/api/slot-definitions'; import { generateSlotDefinitions, getSlotDefinitionPresets } from '@/src/lib/api/slot-definitions';
import type { Court } from '@/src/types/courts'; import type { Court } from '@/src/types/courts';
import type { import type {
SlotDefinitionPreset, SlotDefinitionPreset,
GenerateSlotDefinitionsRequest, GenerateSlotDefinitionsRequest,
PatternOverrides, PatternOverrides,
DayOfWeek, DayOfWeek,
PresetInfo,
} from '@/src/types/slot-definitions'; } from '@/src/types/slot-definitions';
import { PRESET_OPTIONS, DAY_NAMES } from '@/src/types/slot-definitions'; import { DAY_NAMES } from '@/src/types/slot-definitions';
interface GenerateSlotDefinitionsModalProps { interface GenerateSlotDefinitionsModalProps {
clubId: number; clubId: number;
@ -25,6 +26,10 @@ export default function GenerateSlotDefinitionsModal({
onClose, onClose,
onSuccess, onSuccess,
}: GenerateSlotDefinitionsModalProps) { }: GenerateSlotDefinitionsModalProps) {
// Preset data
const [presets, setPresets] = useState<PresetInfo[]>([]);
const [loadingPresets, setLoadingPresets] = useState(true);
// Form state // Form state
const [preset, setPreset] = useState<SlotDefinitionPreset>('workday_standard'); const [preset, setPreset] = useState<SlotDefinitionPreset>('workday_standard');
const [selectedCourtIds, setSelectedCourtIds] = useState<number[]>([]); const [selectedCourtIds, setSelectedCourtIds] = useState<number[]>([]);
@ -44,6 +49,21 @@ export default function GenerateSlotDefinitionsModal({
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
// Fetch presets on mount
useEffect(() => {
async function loadPresets() {
const result = await getSlotDefinitionPresets();
if (result.success) {
setPresets(result.data.data.presets);
setLoadingPresets(false);
} else {
setError('Failed to load presets');
setLoadingPresets(false);
}
}
loadPresets();
}, []);
function handleSelectAllCourts() { function handleSelectAllCourts() {
if (selectedCourtIds.length === courts.length) { if (selectedCourtIds.length === courts.length) {
setSelectedCourtIds([]); setSelectedCourtIds([]);
@ -113,7 +133,7 @@ export default function GenerateSlotDefinitionsModal({
setLoading(false); setLoading(false);
} }
const selectedPreset = PRESET_OPTIONS.find(p => p.id === preset); const selectedPreset = presets.find((p) => p.key === preset);
return ( return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"> <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
@ -131,7 +151,16 @@ export default function GenerateSlotDefinitionsModal({
{/* Body */} {/* Body */}
<div className="p-6 space-y-6"> <div className="p-6 space-y-6">
{/* Loading State */}
{loadingPresets && (
<div className="flex items-center justify-center py-8">
<Loader2 className="w-8 h-8 text-slate-900 animate-spin" />
<span className="ml-3 text-slate-600">Loading presets...</span>
</div>
)}
{/* Preset Selection */} {/* Preset Selection */}
{!loadingPresets && presets.length > 0 && (
<div> <div>
<label className="block text-sm font-semibold text-slate-700 mb-2"> <label className="block text-sm font-semibold text-slate-700 mb-2">
Preset Schedule Preset Schedule
@ -141,9 +170,9 @@ export default function GenerateSlotDefinitionsModal({
onChange={(e) => setPreset(e.target.value as SlotDefinitionPreset)} onChange={(e) => setPreset(e.target.value as SlotDefinitionPreset)}
className="w-full px-4 py-3 border-2 border-slate-200 rounded-lg focus:outline-none focus:border-slate-900 font-medium" className="w-full px-4 py-3 border-2 border-slate-200 rounded-lg focus:outline-none focus:border-slate-900 font-medium"
> >
{PRESET_OPTIONS.map(opt => ( {presets.map((p) => (
<option key={opt.id} value={opt.id}> <option key={p.key} value={p.key}>
{opt.name} ({opt.days}, {opt.hours}, {opt.duration}) {p.title}
</option> </option>
))} ))}
</select> </select>
@ -151,7 +180,11 @@ export default function GenerateSlotDefinitionsModal({
<p className="mt-2 text-sm text-slate-600">{selectedPreset.description}</p> <p className="mt-2 text-sm text-slate-600">{selectedPreset.description}</p>
)} )}
</div> </div>
)}
{/* Form Elements */}
{!loadingPresets && (
<>
{/* Court Selection */} {/* Court Selection */}
<div> <div>
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
@ -323,6 +356,8 @@ export default function GenerateSlotDefinitionsModal({
</div> </div>
)} )}
</div> </div>
</>
)}
{/* Error Message */} {/* Error Message */}
{error && ( {error && (
@ -343,7 +378,7 @@ export default function GenerateSlotDefinitionsModal({
</button> </button>
<button <button
onClick={handleSubmit} onClick={handleSubmit}
disabled={loading || selectedCourtIds.length === 0} disabled={loading || loadingPresets || selectedCourtIds.length === 0}
className="px-6 py-2 bg-slate-900 text-white font-semibold rounded-lg hover:bg-slate-800 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-2" className="px-6 py-2 bg-slate-900 text-white font-semibold rounded-lg hover:bg-slate-800 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-2"
> >
{loading ? ( {loading ? (

@ -13,6 +13,7 @@ import type {
AdminApiResult, AdminApiResult,
AdminApiError, AdminApiError,
} from '@/src/types/admin-api'; } from '@/src/types/admin-api';
import { getPathnameLocale } from '@/src/utils/getLocale';
// ============================================================================ // ============================================================================
// Configuration // Configuration
@ -24,6 +25,26 @@ if (!API_BASE_URL) {
throw new Error('NEXT_PUBLIC_PYTHON_API_URL environment variable is not set'); throw new Error('NEXT_PUBLIC_PYTHON_API_URL environment variable is not set');
} }
// ============================================================================
// Helpers
// ============================================================================
/**
* Add locale and timezone headers to requests
*/
function getLocaleHeaders(): Record<string, string> {
const locale = typeof window !== 'undefined'
? getPathnameLocale(window.location.pathname) || 'en-US'
: 'en-US';
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
return {
'X-Locale': locale,
'X-Timezone': timezone,
};
}
// ============================================================================ // ============================================================================
// Error Handling // Error Handling
// ============================================================================ // ============================================================================
@ -69,6 +90,7 @@ export async function getAdminClubs(cookieHeader?: string): Promise<AdminApiResu
try { try {
const headers: Record<string, string> = { const headers: Record<string, string> = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...getLocaleHeaders(),
}; };
// Forward cookies for server-side rendering // Forward cookies for server-side rendering
@ -113,6 +135,7 @@ export async function getAdminClubDetail(
try { try {
const headers: Record<string, string> = { const headers: Record<string, string> = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...getLocaleHeaders(),
}; };
// Forward cookies for server-side rendering // Forward cookies for server-side rendering

@ -17,12 +17,7 @@ import type {
BookingAdminError, BookingAdminError,
BookingApiResult, BookingApiResult,
} from '@/src/types/booking-admin'; } from '@/src/types/booking-admin';
import apiFetch from '@/src/utils/apiFetch';
const API_BASE_URL = process.env.NEXT_PUBLIC_PYTHON_API_URL;
if (!API_BASE_URL) {
throw new Error('NEXT_PUBLIC_PYTHON_API_URL environment variable is not defined');
}
// ============================================================================ // ============================================================================
// Helper Functions // Helper Functions
@ -92,11 +87,10 @@ export async function getBookingDetail(
} }
const queryString = params.toString(); const queryString = params.toString();
const url = `${API_BASE_URL}/admin/bookings/${bookingId}${queryString ? `?${queryString}` : ''}`; const endpoint = `/admin/bookings/${bookingId}${queryString ? `?${queryString}` : ''}`;
const response = await fetch(url, { const response = await apiFetch(endpoint, {
method: 'GET', method: 'GET',
credentials: 'include',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
@ -144,9 +138,8 @@ export async function cancelBooking(
headers['X-Idempotency-Key'] = options.idempotencyKey; headers['X-Idempotency-Key'] = options.idempotencyKey;
} }
const response = await fetch(`${API_BASE_URL}/admin/bookings/${bookingId}`, { const response = await apiFetch(`/admin/bookings/${bookingId}`, {
method: 'PATCH', method: 'PATCH',
credentials: 'include',
headers, headers,
body: JSON.stringify(request), body: JSON.stringify(request),
}); });
@ -194,9 +187,8 @@ export async function moveBooking(
headers['X-Idempotency-Key'] = options.idempotencyKey; headers['X-Idempotency-Key'] = options.idempotencyKey;
} }
const response = await fetch(`${API_BASE_URL}/admin/bookings/${bookingId}/move`, { const response = await apiFetch(`/admin/bookings/${bookingId}/move`, {
method: 'PATCH', method: 'PATCH',
credentials: 'include',
headers, headers,
body: JSON.stringify(request), body: JSON.stringify(request),
}); });
@ -243,9 +235,8 @@ export async function updateAttendees(
headers['X-Idempotency-Key'] = options.idempotencyKey; headers['X-Idempotency-Key'] = options.idempotencyKey;
} }
const response = await fetch(`${API_BASE_URL}/admin/bookings/${bookingId}/attendees`, { const response = await apiFetch(`/admin/bookings/${bookingId}/attendees`, {
method: 'PATCH', method: 'PATCH',
credentials: 'include',
headers, headers,
body: JSON.stringify(request), body: JSON.stringify(request),
}); });

@ -2,7 +2,6 @@
* Court Management API Client * Court Management API Client
* *
* Handles club profile and court inventory CRUD operations. * Handles club profile and court inventory CRUD operations.
* Uses mock data until backend endpoints are ready.
*/ */
import type { import type {
@ -14,12 +13,7 @@ import type {
CourtError, CourtError,
SportsResponse, SportsResponse,
} from '@/src/types/courts'; } from '@/src/types/courts';
import apiFetch from '@/src/utils/apiFetch';
const API_BASE_URL = process.env.NEXT_PUBLIC_PYTHON_API_URL;
if (!API_BASE_URL) {
throw new Error('NEXT_PUBLIC_PYTHON_API_URL environment variable is not defined');
}
type ApiResult<T> = type ApiResult<T> =
| { success: true; data: T } | { success: true; data: T }
@ -30,9 +24,8 @@ type ApiResult<T> =
*/ */
export async function getClubProfile(clubId: number): Promise<ApiResult<ClubProfile>> { export async function getClubProfile(clubId: number): Promise<ApiResult<ClubProfile>> {
try { try {
const response = await fetch(`${API_BASE_URL}/admin/clubs/${clubId}`, { const response = await apiFetch(`/admin/clubs/${clubId}`, {
method: 'GET', method: 'GET',
credentials: 'include',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
@ -70,9 +63,8 @@ export async function updateClubProfile(
request: ClubProfileUpdateRequest request: ClubProfileUpdateRequest
): Promise<ApiResult<ClubProfile>> { ): Promise<ApiResult<ClubProfile>> {
try { try {
const response = await fetch(`${API_BASE_URL}/admin/clubs/${clubId}`, { const response = await apiFetch(`/admin/clubs/${clubId}`, {
method: 'PATCH', method: 'PATCH',
credentials: 'include',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
@ -105,9 +97,8 @@ export async function updateClubProfile(
*/ */
export async function getCourts(clubId: number): Promise<ApiResult<Court[]>> { export async function getCourts(clubId: number): Promise<ApiResult<Court[]>> {
try { try {
const response = await fetch(`${API_BASE_URL}/admin/clubs/${clubId}/courts`, { const response = await apiFetch(`/admin/clubs/${clubId}/courts`, {
method: 'GET', method: 'GET',
credentials: 'include',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
@ -142,9 +133,8 @@ export async function createCourt(
request: CourtRequest request: CourtRequest
): Promise<ApiResult<Court>> { ): Promise<ApiResult<Court>> {
try { try {
const response = await fetch(`${API_BASE_URL}/admin/clubs/${clubId}/courts`, { const response = await apiFetch(`/admin/clubs/${clubId}/courts`, {
method: 'POST', method: 'POST',
credentials: 'include',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
@ -181,9 +171,8 @@ export async function updateCourt(
request: CourtRequest request: CourtRequest
): Promise<ApiResult<Court>> { ): Promise<ApiResult<Court>> {
try { try {
const response = await fetch(`${API_BASE_URL}/admin/clubs/${clubId}/courts/${courtId}`, { const response = await apiFetch(`/admin/clubs/${clubId}/courts/${courtId}`, {
method: 'PATCH', method: 'PATCH',
credentials: 'include',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
@ -219,9 +208,8 @@ export async function getCourtDependencies(
courtId: number courtId: number
): Promise<ApiResult<CourtDependencies>> { ): Promise<ApiResult<CourtDependencies>> {
try { try {
const response = await fetch(`${API_BASE_URL}/admin/clubs/${clubId}/courts/${courtId}/dependencies`, { const response = await apiFetch(`/admin/clubs/${clubId}/courts/${courtId}/dependencies`, {
method: 'GET', method: 'GET',
credentials: 'include',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
@ -256,9 +244,8 @@ export async function deleteCourt(
courtId: number courtId: number
): Promise<ApiResult<void>> { ): Promise<ApiResult<void>> {
try { try {
const response = await fetch(`${API_BASE_URL}/admin/clubs/${clubId}/courts/${courtId}`, { const response = await apiFetch(`/admin/clubs/${clubId}/courts/${courtId}`, {
method: 'DELETE', method: 'DELETE',
credentials: 'include',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
@ -289,9 +276,8 @@ export async function deleteCourt(
*/ */
export async function getSports(): Promise<ApiResult<SportsResponse>> { export async function getSports(): Promise<ApiResult<SportsResponse>> {
try { try {
const response = await fetch(`${API_BASE_URL}/admin/sports`, { const response = await apiFetch(`/admin/sports`, {
method: 'GET', method: 'GET',
credentials: 'include',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },

@ -2,7 +2,6 @@
* Materialisation API Client * Materialisation API Client
* *
* Handles slot materialisation status polling and manual trigger requests. * Handles slot materialisation status polling and manual trigger requests.
* Uses mock data until backend endpoints are ready.
*/ */
import type { import type {
@ -11,12 +10,7 @@ import type {
MaterialisationTriggerResponse, MaterialisationTriggerResponse,
MaterialisationError, MaterialisationError,
} from '@/src/types/materialisation'; } from '@/src/types/materialisation';
import apiFetch from '@/src/utils/apiFetch';
const API_BASE_URL = process.env.NEXT_PUBLIC_PYTHON_API_URL;
if (!API_BASE_URL) {
throw new Error('NEXT_PUBLIC_PYTHON_API_URL environment variable is not defined');
}
type ApiResult<T> = type ApiResult<T> =
| { success: true; data: T } | { success: true; data: T }
@ -29,9 +23,8 @@ export async function getMaterialisationStatus(
clubId: number clubId: number
): Promise<ApiResult<MaterialisationStatus>> { ): Promise<ApiResult<MaterialisationStatus>> {
try { try {
const response = await fetch(`${API_BASE_URL}/admin/clubs/${clubId}/materialisation-status`, { const response = await apiFetch(`/admin/clubs/${clubId}/materialisation-status`, {
method: 'GET', method: 'GET',
credentials: 'include',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
@ -66,9 +59,8 @@ export async function triggerMaterialisation(
request: MaterialisationTriggerRequest request: MaterialisationTriggerRequest
): Promise<ApiResult<MaterialisationTriggerResponse>> { ): Promise<ApiResult<MaterialisationTriggerResponse>> {
try { try {
const response = await fetch(`${API_BASE_URL}/admin/clubs/${clubId}/slot-materialize`, { const response = await apiFetch(`/admin/clubs/${clubId}/slot-materialize`, {
method: 'POST', method: 'POST',
credentials: 'include',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },

@ -6,13 +6,9 @@ import type {
GenerateSlotDefinitionsResponse, GenerateSlotDefinitionsResponse,
CloneSlotDefinitionRequest, CloneSlotDefinitionRequest,
CloneSlotDefinitionResponse, CloneSlotDefinitionResponse,
GetPresetsResponse,
} from '@/src/types/slot-definitions'; } from '@/src/types/slot-definitions';
import apiFetch from '@/src/utils/apiFetch';
const API_BASE_URL = process.env.NEXT_PUBLIC_PYTHON_API_URL;
if (!API_BASE_URL) {
throw new Error('NEXT_PUBLIC_PYTHON_API_URL environment variable is not defined');
}
type ApiResult<T> = type ApiResult<T> =
| { success: true; data: T } | { success: true; data: T }
@ -29,11 +25,10 @@ export async function getSlotDefinitions(
if (filters?.active_on) params.append('active_on', filters.active_on); if (filters?.active_on) params.append('active_on', filters.active_on);
const queryString = params.toString(); const queryString = params.toString();
const url = `${API_BASE_URL}/admin/clubs/${clubId}/slot-definitions${queryString ? `?${queryString}` : ''}`; const endpoint = `/admin/clubs/${clubId}/slot-definitions${queryString ? `?${queryString}` : ''}`;
const response = await fetch(url, { const response = await apiFetch(endpoint, {
method: 'GET', method: 'GET',
credentials: 'include',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
@ -66,9 +61,8 @@ export async function createSlotDefinition(
request: SlotDefinitionRequest request: SlotDefinitionRequest
): Promise<ApiResult<SlotDefinition>> { ): Promise<ApiResult<SlotDefinition>> {
try { try {
const response = await fetch(`${API_BASE_URL}/admin/clubs/${clubId}/slot-definitions`, { const response = await apiFetch(`/admin/clubs/${clubId}/slot-definitions`, {
method: 'POST', method: 'POST',
credentials: 'include',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
@ -103,11 +97,10 @@ export async function updateSlotDefinition(
request: Partial<SlotDefinitionRequest> request: Partial<SlotDefinitionRequest>
): Promise<ApiResult<SlotDefinition>> { ): Promise<ApiResult<SlotDefinition>> {
try { try {
const response = await fetch( const response = await apiFetch(
`${API_BASE_URL}/admin/clubs/${clubId}/slot-definitions/${slotDefinitionId}`, `/admin/clubs/${clubId}/slot-definitions/${slotDefinitionId}`,
{ {
method: 'PATCH', method: 'PATCH',
credentials: 'include',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
@ -142,11 +135,10 @@ export async function deleteSlotDefinition(
slotDefinitionId: number slotDefinitionId: number
): Promise<ApiResult<void>> { ): Promise<ApiResult<void>> {
try { try {
const response = await fetch( const response = await apiFetch(
`${API_BASE_URL}/admin/clubs/${clubId}/slot-definitions/${slotDefinitionId}`, `/admin/clubs/${clubId}/slot-definitions/${slotDefinitionId}`,
{ {
method: 'DELETE', method: 'DELETE',
credentials: 'include',
} }
); );
@ -170,15 +162,42 @@ export async function deleteSlotDefinition(
} }
} }
// GET /admin/slot-definition-presets
export async function getSlotDefinitionPresets(): Promise<ApiResult<GetPresetsResponse>> {
try {
const response = await apiFetch('/admin/slot-definition-presets', {
method: 'GET',
});
if (!response.ok) {
const error: SlotDefinitionError = await response.json();
return { success: false, error };
}
const data: GetPresetsResponse = await response.json();
return { success: true, data };
} catch (error) {
return {
success: false,
error: {
type: 'about:blank',
title: 'Network Error',
status: 0,
detail: 'Failed to fetch slot definition presets. Please check your connection.',
code: 'network_error',
},
};
}
}
// POST /admin/clubs/{club_id}/slot-definitions/generate // POST /admin/clubs/{club_id}/slot-definitions/generate
export async function generateSlotDefinitions( export async function generateSlotDefinitions(
clubId: number, clubId: number,
request: GenerateSlotDefinitionsRequest request: GenerateSlotDefinitionsRequest
): Promise<ApiResult<GenerateSlotDefinitionsResponse>> { ): Promise<ApiResult<GenerateSlotDefinitionsResponse>> {
try { try {
const response = await fetch(`${API_BASE_URL}/admin/clubs/${clubId}/slot-definitions/generate`, { const response = await apiFetch(`/admin/clubs/${clubId}/slot-definitions/generate`, {
method: 'POST', method: 'POST',
credentials: 'include',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
@ -213,11 +232,10 @@ export async function cloneSlotDefinition(
request: CloneSlotDefinitionRequest request: CloneSlotDefinitionRequest
): Promise<ApiResult<CloneSlotDefinitionResponse>> { ): Promise<ApiResult<CloneSlotDefinitionResponse>> {
try { try {
const response = await fetch( const response = await apiFetch(
`${API_BASE_URL}/admin/clubs/${clubId}/slot-definitions/${slotDefinitionId}/clone`, `/admin/clubs/${clubId}/slot-definitions/${slotDefinitionId}/clone`,
{ {
method: 'POST', method: 'POST',
credentials: 'include',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },

@ -99,51 +99,37 @@ export type SlotDefinitionPreset =
| 'hourly_daytime'; // Mon-Fri, 9am-6pm, 60min | 'hourly_daytime'; // Mon-Fri, 9am-6pm, 60min
/** /**
* Preset metadata for UI display * Pattern details from preset
*/
export interface PresetPattern {
days: DayOfWeek[];
start_time: string; // HH:MM:SS
end_time: string; // HH:MM:SS
duration_minutes: number;
interval_minutes: number;
capacity: number;
}
/**
* Preset info from API (localized)
*/ */
export interface PresetInfo { export interface PresetInfo {
id: SlotDefinitionPreset; key: SlotDefinitionPreset;
name: string; title: string; // Localized title
description: string; description: string; // Localized description
days: string; pattern: PresetPattern;
hours: string; }
duration: string;
} /**
* Get presets endpoint response
export const PRESET_OPTIONS: PresetInfo[] = [ */
{ export interface GetPresetsResponse {
id: 'workday_standard', status: 'success';
name: 'Weekday Standard', data: {
description: 'Standard weekday operations', locale: string;
days: 'Mon-Fri', presets: PresetInfo[];
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 * Pattern overrides for customizing presets

Loading…
Cancel
Save