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