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.

640 lines
18 KiB
TypeScript

/**
* Facility Admin API Client
*
* Handles all facility admin operations: membership plans, entitlements, members, policy
* Build: 495
* Date: 2025-11-24
*/
import type {
MembershipPlan,
CreatePlanRequest,
UpdatePlanRequest,
PlanEntitlements,
SetEntitlementRequest,
FacilityMember,
AddMemberRequest,
UpdateMemberRequest,
MemberListFilters,
FacilityPolicy,
UpdatePolicyRequest,
FacilityAdminError,
FacilityAdminApiResult,
PlanTemplate,
} from '@/src/types/facility-admin';
import apiFetch from '@/src/utils/apiFetch';
// ============================================================================
// Helper Functions
// ============================================================================
/**
* Handle API response
* Flask backend wraps responses in: { status: 'success'|'fail', data: {...} }
*/
async function handleApiResponse<T>(response: Response): Promise<FacilityAdminApiResult<T>> {
if (response.ok) {
// Handle 204 No Content - DELETE endpoints return empty body
if (response.status === 204) {
return { success: true, data: undefined as T };
}
const json = await response.json();
// Flask returns { status: 'success', data: {...} }
// Extract the actual data from the wrapper
const data = json.data !== undefined ? json.data : json;
return { success: true, data };
}
// Handle error responses (RFC-7807)
try {
const error: FacilityAdminError = 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',
code: 'internal_error',
},
};
}
}
// ============================================================================
// Membership Plans
// ============================================================================
/**
* POST /admin/facilities/{facility_id}/plans
* Create a new membership plan
*/
export async function createPlan(
facilityId: number,
request: CreatePlanRequest
): Promise<FacilityAdminApiResult<MembershipPlan>> {
try {
const response = await apiFetch(`/admin/facilities/${facilityId}/plans`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(request),
});
return handleApiResponse<MembershipPlan>(response);
} catch (error) {
return {
success: false,
error: {
type: 'about:blank',
title: 'Network Error',
status: 0,
detail: error instanceof Error ? error.message : 'Failed to create plan',
code: 'internal_error',
},
};
}
}
/**
* GET /admin/facilities/{facility_id}/plans
* List all membership plans for a facility
*/
export async function listPlans(
facilityId: number,
filters?: { sport_id?: number; include_inactive?: boolean }
): Promise<FacilityAdminApiResult<MembershipPlan[]>> {
try {
const params = new URLSearchParams();
if (filters?.sport_id !== undefined) params.set('sport_id', String(filters.sport_id));
if (filters?.include_inactive !== undefined) params.set('include_inactive', String(filters.include_inactive));
const queryString = params.toString();
const endpoint = `/admin/facilities/${facilityId}/plans${queryString ? `?${queryString}` : ''}`;
const response = await apiFetch(endpoint, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
});
const result = await handleApiResponse<any>(response);
// Backend returns { plans: [...] } - extract the plans array
if (result.success && result.data.plans) {
return { success: true, data: result.data.plans };
}
return result as FacilityAdminApiResult<MembershipPlan[]>;
} catch (error) {
return {
success: false,
error: {
type: 'about:blank',
title: 'Network Error',
status: 0,
detail: error instanceof Error ? error.message : 'Failed to list plans',
code: 'internal_error',
},
};
}
}
/**
* GET /admin/facilities/{facility_id}/plans/{plan_id}
* Get a single membership plan
*/
export async function getPlan(
facilityId: number,
planId: number
): Promise<FacilityAdminApiResult<MembershipPlan>> {
try {
const response = await apiFetch(`/admin/facilities/${facilityId}/plans/${planId}`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
});
return handleApiResponse<MembershipPlan>(response);
} catch (error) {
return {
success: false,
error: {
type: 'about:blank',
title: 'Network Error',
status: 0,
detail: error instanceof Error ? error.message : 'Failed to get plan',
code: 'internal_error',
},
};
}
}
/**
* PATCH /admin/facilities/{facility_id}/plans/{plan_id}
* Update a membership plan (partial update)
*/
export async function updatePlan(
facilityId: number,
planId: number,
request: UpdatePlanRequest
): Promise<FacilityAdminApiResult<MembershipPlan>> {
try {
const response = await apiFetch(`/admin/facilities/${facilityId}/plans/${planId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(request),
});
return handleApiResponse<MembershipPlan>(response);
} catch (error) {
return {
success: false,
error: {
type: 'about:blank',
title: 'Network Error',
status: 0,
detail: error instanceof Error ? error.message : 'Failed to update plan',
code: 'internal_error',
},
};
}
}
/**
* DELETE /admin/facilities/{facility_id}/plans/{plan_id}
* Delete a membership plan (soft delete by default)
*/
export async function deletePlan(
facilityId: number,
planId: number,
hardDelete: boolean = false
): Promise<FacilityAdminApiResult<void>> {
try {
const params = new URLSearchParams();
if (hardDelete) params.set('hard_delete', 'true');
const queryString = params.toString();
const endpoint = `/admin/facilities/${facilityId}/plans/${planId}${queryString ? `?${queryString}` : ''}`;
const response = await apiFetch(endpoint, {
method: 'DELETE',
});
return handleApiResponse<void>(response);
} catch (error) {
return {
success: false,
error: {
type: 'about:blank',
title: 'Network Error',
status: 0,
detail: error instanceof Error ? error.message : 'Failed to delete plan',
code: 'internal_error',
},
};
}
}
// ============================================================================
// Plan Templates
// ============================================================================
/**
* GET /admin/plan-templates
* Get all available plan templates
*/
export async function listPlanTemplates(
filters?: { sport_id?: number }
): Promise<FacilityAdminApiResult<PlanTemplate[]>> {
try {
const params = new URLSearchParams();
if (filters?.sport_id !== undefined) params.set('sport_id', String(filters.sport_id));
const queryString = params.toString();
const endpoint = `/admin/plan-templates${queryString ? `?${queryString}` : ''}`;
const response = await apiFetch(endpoint, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
});
const result = await handleApiResponse<any>(response);
// Backend returns { templates: [...] } - extract the templates array
if (result.success && result.data.templates) {
return { success: true, data: result.data.templates };
}
return result as FacilityAdminApiResult<PlanTemplate[]>;
} catch (error) {
return {
success: false,
error: {
type: 'about:blank',
title: 'Network Error',
status: 0,
detail: error instanceof Error ? error.message : 'Failed to list templates',
code: 'internal_error',
},
};
}
}
// ============================================================================
// Entitlements
// ============================================================================
/**
* GET /admin/facilities/{facility_id}/plans/{plan_id}/entitlements
* Get all entitlements for a plan
*/
export async function getEntitlements(
facilityId: number,
planId: number
): Promise<FacilityAdminApiResult<PlanEntitlements>> {
try {
const response = await apiFetch(`/admin/facilities/${facilityId}/plans/${planId}/entitlements`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
});
return handleApiResponse<PlanEntitlements>(response);
} catch (error) {
return {
success: false,
error: {
type: 'about:blank',
title: 'Network Error',
status: 0,
detail: error instanceof Error ? error.message : 'Failed to get entitlements',
code: 'internal_error',
},
};
}
}
/**
* PUT /admin/facilities/{facility_id}/plans/{plan_id}/entitlements
* Replace all entitlements for a plan (bulk update)
*/
export async function updateEntitlements(
facilityId: number,
planId: number,
entitlements: PlanEntitlements
): Promise<FacilityAdminApiResult<PlanEntitlements>> {
try {
const response = await apiFetch(`/admin/facilities/${facilityId}/plans/${planId}/entitlements`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(entitlements),
});
return handleApiResponse<PlanEntitlements>(response);
} catch (error) {
return {
success: false,
error: {
type: 'about:blank',
title: 'Network Error',
status: 0,
detail: error instanceof Error ? error.message : 'Failed to update entitlements',
code: 'internal_error',
},
};
}
}
/**
* PUT /admin/facilities/{facility_id}/plans/{plan_id}/entitlements/{key}
* Set or update a single entitlement
*/
export async function setEntitlement(
facilityId: number,
planId: number,
key: string,
request: SetEntitlementRequest
): Promise<FacilityAdminApiResult<{ key: string; value: string | number | boolean }>> {
try {
const response = await apiFetch(`/admin/facilities/${facilityId}/plans/${planId}/entitlements/${key}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(request),
});
return handleApiResponse<{ key: string; value: string | number | boolean }>(response);
} catch (error) {
return {
success: false,
error: {
type: 'about:blank',
title: 'Network Error',
status: 0,
detail: error instanceof Error ? error.message : 'Failed to set entitlement',
code: 'internal_error',
},
};
}
}
/**
* DELETE /admin/facilities/{facility_id}/plans/{plan_id}/entitlements/{key}
* Delete a specific entitlement
*/
export async function deleteEntitlement(
facilityId: number,
planId: number,
key: string
): Promise<FacilityAdminApiResult<void>> {
try {
const response = await apiFetch(`/admin/facilities/${facilityId}/plans/${planId}/entitlements/${key}`, {
method: 'DELETE',
});
return handleApiResponse<void>(response);
} catch (error) {
return {
success: false,
error: {
type: 'about:blank',
title: 'Network Error',
status: 0,
detail: error instanceof Error ? error.message : 'Failed to delete entitlement',
code: 'internal_error',
},
};
}
}
// ============================================================================
// Members
// ============================================================================
/**
* POST /admin/facilities/{facility_id}/members
* Add a member to a facility
*/
export async function addMember(
facilityId: number,
request: AddMemberRequest
): Promise<FacilityAdminApiResult<FacilityMember>> {
try {
const response = await apiFetch(`/admin/facilities/${facilityId}/members`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(request),
});
return handleApiResponse<FacilityMember>(response);
} catch (error) {
return {
success: false,
error: {
type: 'about:blank',
title: 'Network Error',
status: 0,
detail: error instanceof Error ? error.message : 'Failed to add member',
code: 'internal_error',
},
};
}
}
/**
* GET /admin/facilities/{facility_id}/members
* List all members for a facility
*/
export async function listMembers(
facilityId: number,
filters?: MemberListFilters
): Promise<FacilityAdminApiResult<FacilityMember[]>> {
try {
const params = new URLSearchParams();
if (filters?.sport_id !== undefined) params.set('sport_id', String(filters.sport_id));
if (filters?.status) params.set('status', filters.status);
if (filters?.role) params.set('role', filters.role);
if (filters?.plan_id !== undefined) params.set('plan_id', String(filters.plan_id));
const queryString = params.toString();
const endpoint = `/admin/facilities/${facilityId}/members${queryString ? `?${queryString}` : ''}`;
const response = await apiFetch(endpoint, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
});
const result = await handleApiResponse<any>(response);
// Backend returns { members: [...] } - extract the members array
if (result.success && result.data.members) {
return { success: true, data: result.data.members };
}
return result as FacilityAdminApiResult<FacilityMember[]>;
} catch (error) {
return {
success: false,
error: {
type: 'about:blank',
title: 'Network Error',
status: 0,
detail: error instanceof Error ? error.message : 'Failed to list members',
code: 'internal_error',
},
};
}
}
/**
* GET /admin/facilities/{facility_id}/members/{member_id}
* Get a single member
*/
export async function getMember(
facilityId: number,
memberId: number
): Promise<FacilityAdminApiResult<FacilityMember>> {
try {
const response = await apiFetch(`/admin/facilities/${facilityId}/members/${memberId}`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
});
return handleApiResponse<FacilityMember>(response);
} catch (error) {
return {
success: false,
error: {
type: 'about:blank',
title: 'Network Error',
status: 0,
detail: error instanceof Error ? error.message : 'Failed to get member',
code: 'internal_error',
},
};
}
}
/**
* PATCH /admin/facilities/{facility_id}/members/{member_id}
* Update a member (partial update)
*/
export async function updateMember(
facilityId: number,
memberId: number,
request: UpdateMemberRequest
): Promise<FacilityAdminApiResult<FacilityMember>> {
try {
const response = await apiFetch(`/admin/facilities/${facilityId}/members/${memberId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(request),
});
return handleApiResponse<FacilityMember>(response);
} catch (error) {
return {
success: false,
error: {
type: 'about:blank',
title: 'Network Error',
status: 0,
detail: error instanceof Error ? error.message : 'Failed to update member',
code: 'internal_error',
},
};
}
}
/**
* DELETE /admin/facilities/{facility_id}/members/{member_id}
* Delete a member (soft delete by default)
*/
export async function deleteMember(
facilityId: number,
memberId: number,
hardDelete: boolean = false
): Promise<FacilityAdminApiResult<void>> {
try {
const params = new URLSearchParams();
if (hardDelete) params.set('hard_delete', 'true');
const queryString = params.toString();
const endpoint = `/admin/facilities/${facilityId}/members/${memberId}${queryString ? `?${queryString}` : ''}`;
const response = await apiFetch(endpoint, {
method: 'DELETE',
});
return handleApiResponse<void>(response);
} catch (error) {
return {
success: false,
error: {
type: 'about:blank',
title: 'Network Error',
status: 0,
detail: error instanceof Error ? error.message : 'Failed to delete member',
code: 'internal_error',
},
};
}
}
// ============================================================================
// Policy
// ============================================================================
/**
* GET /admin/facilities/{facility_id}/policy
* Get facility policy (creates default if doesn't exist)
*/
export async function getPolicy(
facilityId: number
): Promise<FacilityAdminApiResult<FacilityPolicy>> {
try {
const response = await apiFetch(`/admin/facilities/${facilityId}/policy`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
});
// Backend returns the policy object directly, not wrapped
return handleApiResponse<FacilityPolicy>(response);
} catch (error) {
return {
success: false,
error: {
type: 'about:blank',
title: 'Network Error',
status: 0,
detail: error instanceof Error ? error.message : 'Failed to get policy',
code: 'internal_error',
},
};
}
}
/**
* PATCH /admin/facilities/{facility_id}/policy
* Update facility policy (partial update, upsert)
*/
export async function updatePolicy(
facilityId: number,
request: UpdatePolicyRequest
): Promise<FacilityAdminApiResult<FacilityPolicy>> {
try {
const response = await apiFetch(`/admin/facilities/${facilityId}/policy`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(request),
});
return handleApiResponse<FacilityPolicy>(response);
} catch (error) {
return {
success: false,
error: {
type: 'about:blank',
title: 'Network Error',
status: 0,
detail: error instanceof Error ? error.message : 'Failed to update policy',
code: 'internal_error',
},
};
}
}