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
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,
|
|
},
|
|
},
|
|
};
|
|
|