You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

224 lines
5.8 KiB
TypeScript

/**
* Admin Clubs API Client
* Handles all /admin/clubs endpoints
*
* Based on: docs/VENUE_ADMIN_DESIGN.md (Phase 0)
* Owner: Frontend Faye
* Created: 2025-11-05
*/
import type {
AdminClubsResponse,
AdminClubDetail,
AdminApiResult,
AdminApiError,
} from '@/src/types/admin-api';
import { getPathnameLocale } from '@/src/utils/getLocale';
// ============================================================================
// Configuration
// ============================================================================
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 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
// ============================================================================
async function handleApiResponse<T>(response: Response): Promise<AdminApiResult<T>> {
if (response.ok) {
const data = await response.json();
return { success: true, data };
}
// Handle error responses
try {
const error: AdminApiError = await response.json();
return { success: false, error };
} catch {
// Fallback for non-JSON errors
return {
success: false,
error: {
type: 'about:blank',
title: 'API Error',
status: response.status,
detail: response.statusText || 'An unexpected error occurred',
instance: response.url,
code: `HTTP_${response.status}`,
},
};
}
}
// ============================================================================
// API Functions
// ============================================================================
/**
* GET /admin/clubs
* Lists all clubs the authenticated user can manage
*
* @param cookieHeader - Optional cookie header to forward (for SSR)
* @returns Array of clubs or error
*/
export async function getAdminClubs(cookieHeader?: string): Promise<AdminApiResult<AdminClubsResponse>> {
try {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...getLocaleHeaders(),
};
// Forward cookies for server-side rendering
if (cookieHeader) {
headers['Cookie'] = cookieHeader;
}
const response = await fetch(`${API_BASE_URL}/admin/clubs`, {
method: 'GET',
headers,
credentials: cookieHeader ? 'omit' : 'include', // Use 'omit' when manually setting Cookie header
});
return handleApiResponse<AdminClubsResponse>(response);
} catch (error) {
return {
success: false,
error: {
type: 'about:blank',
title: 'Network Error',
status: 0,
detail: error instanceof Error ? error.message : 'Failed to connect to API',
instance: `${API_BASE_URL}/admin/clubs`,
code: 'NETWORK_ERROR',
},
};
}
}
/**
* GET /admin/clubs/{club_id}
* Gets detailed information about a specific club
*
* @param clubId - The club ID to fetch
* @param cookieHeader - Optional cookie header to forward (for SSR)
* @returns Club details or error
*/
export async function getAdminClubDetail(
clubId: number,
cookieHeader?: string
): Promise<AdminApiResult<AdminClubDetail>> {
try {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...getLocaleHeaders(),
};
// Forward cookies for server-side rendering
if (cookieHeader) {
headers['Cookie'] = cookieHeader;
}
const response = await fetch(`${API_BASE_URL}/admin/clubs/${clubId}`, {
method: 'GET',
headers,
credentials: cookieHeader ? 'omit' : 'include', // Use 'omit' when manually setting Cookie header
});
return handleApiResponse<AdminClubDetail>(response);
} catch (error) {
return {
success: false,
error: {
type: 'about:blank',
title: 'Network Error',
status: 0,
detail: error instanceof Error ? error.message : 'Failed to connect to API',
instance: `${API_BASE_URL}/admin/clubs/${clubId}`,
code: 'NETWORK_ERROR',
},
};
}
}
// ============================================================================
// Mock Data (for local development until staging stubs are ready)
// ============================================================================
export const MOCK_CLUBS: AdminClubsResponse = [
{
club_id: 1,
name: 'Central Padel Geneva',
timezone: 'Europe/Zurich',
courts: 4,
},
{
club_id: 2,
name: 'Riverside Tennis Lausanne',
timezone: 'Europe/Zurich',
courts: 6,
},
];
export const MOCK_CLUB_DETAIL: AdminClubDetail = {
club: {
club_id: 1,
name: 'Central Padel Geneva',
timezone: 'Europe/Zurich',
},
courts: [
{
court_id: 1,
name: 'Court 1',
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-01-01T00:00:00Z',
},
{
court_id: 2,
name: 'Court 2',
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-01-01T00:00:00Z',
},
{
court_id: 3,
name: 'Court 3',
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-01-01T00:00:00Z',
},
],
slot_definitions: [],
upcoming_slots: [],
provider: {
remote_type: 'local',
capabilities: {
manages_slot_storage: true,
supports_payment_verification: false,
},
},
};