TorbatYar/frontend/modules/sports-center/services/sports-center-api.ts
Mortezakoohjani 6f4a484051 Migrate Beauty, Healthcare, and Accounting frontend to modular src/modules architecture.
Move business logic out of App Router into module packages, add boundary validation scripts, and keep all routes as thin re-exports without changing URLs or API behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-26 22:28:27 +03:30

361 lines
14 KiB
TypeScript

/**
* Sports Center Service API client — real backend only.
* Browser → /api/sports-center BFF; SSR → direct upstream :8006
*/
import { getValidAccessToken, refreshSession } from "@/lib/auth";
import type { CapabilitiesResponse, HealthResponse, ListParams } from "@/modules/sports-center/types";
const SPORTS_CENTER_BASE =
typeof window !== "undefined"
? "/api/sports-center"
: process.env.SPORTS_CENTER_SERVICE_URL ||
process.env.NEXT_PUBLIC_SPORTS_CENTER_API_URL ||
"http://localhost:8006";
export class SportsCenterApiError extends Error {
constructor(
public status: number,
public code: string,
message: string
) {
super(message);
this.name = "SportsCenterApiError";
}
}
type RequestOptions = RequestInit & { tenantId: string; auth?: boolean };
async function request<T>(path: string, options: RequestOptions): Promise<T> {
const { tenantId, auth = true, headers: customHeaders, ...init } = options;
const headers = new Headers(customHeaders);
if (!headers.has("Content-Type") && init.body) {
headers.set("Content-Type", "application/json");
}
headers.set("X-Tenant-ID", tenantId);
if (auth) {
const token = await getValidAccessToken();
if (token) headers.set("Authorization", `Bearer ${token}`);
}
const url = path.startsWith("http")
? path
: `${SPORTS_CENTER_BASE.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
let res: Response;
try {
res = await fetch(url, { ...init, headers });
} catch (err) {
const detail = err instanceof Error ? err.message : "network_error";
throw new Error(`اتصال به سرویس مرکز ورزشی برقرار نشد (${url}) — ${detail}`);
}
if (res.status === 401 && auth) {
const refreshed = await refreshSession({ force: true });
if (refreshed) {
headers.set("Authorization", `Bearer ${refreshed}`);
res = await fetch(url, { ...init, headers });
}
}
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new SportsCenterApiError(
res.status,
body?.error?.code || body?.detail || "unknown_error",
body?.error?.message || body?.detail || res.statusText
);
}
if (res.status === 204) return undefined as T;
return res.json() as Promise<T>;
}
function qs(params?: ListParams) {
if (!params) return "";
const sp = new URLSearchParams();
Object.entries(params).forEach(([k, v]) => {
if (v !== undefined && v !== null && v !== "") sp.set(k, String(v));
});
const s = sp.toString();
return s ? `?${s}` : "";
}
function action(tenantId: string, apiPath: string, body?: Record<string, unknown>) {
return request<Record<string, unknown>>(apiPath, {
tenantId,
method: "POST",
body: body ? JSON.stringify(body) : undefined,
});
}
function crud(basePath: string, opts?: { upsert?: boolean }) {
return {
list: (tenantId: string, params?: ListParams) =>
request<Record<string, unknown>[]>(`${basePath}${qs(params)}`, { tenantId }),
get: (tenantId: string, id: string) =>
request<Record<string, unknown>>(`${basePath}/${id}`, { tenantId }),
create: (tenantId: string, body: Record<string, unknown>) =>
request<Record<string, unknown>>(basePath, {
tenantId,
method: "POST",
body: JSON.stringify(body),
}),
update: (tenantId: string, id: string, body: Record<string, unknown>) =>
request<Record<string, unknown>>(`${basePath}/${id}`, {
tenantId,
method: "PATCH",
body: JSON.stringify(body),
}),
remove: (tenantId: string, id: string) =>
action(tenantId, `${basePath}/${id}/delete`),
upsert: opts?.upsert
? (tenantId: string, body: Record<string, unknown>) =>
request<Record<string, unknown>>(basePath, {
tenantId,
method: "PUT",
body: JSON.stringify(body),
})
: undefined,
};
}
function createOnly(basePath: string) {
return {
list: (tenantId: string, params?: ListParams) =>
request<Record<string, unknown>[]>(`${basePath}${qs(params)}`, { tenantId }),
create: (tenantId: string, body: Record<string, unknown>) =>
request<Record<string, unknown>>(basePath, {
tenantId,
method: "POST",
body: JSON.stringify(body),
}),
};
}
function postOnly(basePath: string) {
return {
list: async (_tenantId: string, _params?: ListParams) => [] as Record<string, unknown>[],
create: (tenantId: string, body: Record<string, unknown>) =>
request<Record<string, unknown>>(basePath, {
tenantId,
method: "POST",
body: JSON.stringify(body),
}),
};
}
function listOnly(basePath: string) {
return {
list: (tenantId: string, params?: ListParams) =>
request<Record<string, unknown>[]>(`${basePath}${qs(params)}`, { tenantId }),
};
}
export const sportsCenterApi = {
health: (): Promise<HealthResponse> =>
fetch(`${SPORTS_CENTER_BASE}/health`).then((r) => r.json()),
capabilities: (): Promise<CapabilitiesResponse> =>
fetch(`${SPORTS_CENTER_BASE}/capabilities`).then((r) => r.json()),
// Foundation
sportsCenters: crud("/api/v1/sports-centers"),
branches: crud("/api/v1/branches"),
sports: crud("/api/v1/sports"),
membershipTypes: crud("/api/v1/membership-types"),
memberships: {
...crud("/api/v1/memberships"),
assignMember: (tenantId: string, id: string, body: Record<string, unknown>) =>
action(tenantId, `/api/v1/memberships/${id}/assign-member`, body),
activate: (tenantId: string, id: string, body?: Record<string, unknown>) =>
action(tenantId, `/api/v1/memberships/${id}/activate`, body),
suspend: (tenantId: string, id: string, body?: Record<string, unknown>) =>
action(tenantId, `/api/v1/memberships/${id}/suspend`, body),
cancel: (tenantId: string, id: string, body?: Record<string, unknown>) =>
action(tenantId, `/api/v1/memberships/${id}/cancel`, body),
freeze: (tenantId: string, id: string, body: Record<string, unknown>) =>
action(tenantId, `/api/v1/memberships/${id}/freeze`, body),
unfreeze: (tenantId: string, id: string, body?: Record<string, unknown>) =>
action(tenantId, `/api/v1/memberships/${id}/unfreeze`, body),
},
coaches: crud("/api/v1/coaches"),
roles: crud("/api/v1/roles"),
permissions: crud("/api/v1/permissions"),
facilities: crud("/api/v1/facilities"),
courts: crud("/api/v1/courts"),
rooms: crud("/api/v1/rooms"),
lockerRooms: crud("/api/v1/locker-rooms"),
lockers: {
...crud("/api/v1/lockers"),
assign: (tenantId: string, id: string, body: Record<string, unknown>) =>
action(tenantId, `/api/v1/lockers/${id}/assign`, body),
release: (tenantId: string, id: string, body?: Record<string, unknown>) =>
action(tenantId, `/api/v1/lockers/${id}/release`, body),
},
deviceProviders: crud("/api/v1/device-providers"),
devices: {
...crud("/api/v1/devices"),
connect: (tenantId: string, id: string) => action(tenantId, `/api/v1/devices/${id}/connect`),
disconnect: (tenantId: string, id: string) => action(tenantId, `/api/v1/devices/${id}/disconnect`),
},
attendanceGateways: crud("/api/v1/attendance-gateways"),
configurations: crud("/api/v1/configurations"),
events: crud("/api/v1/events"),
settings: {
list: (tenantId: string, params?: ListParams) =>
request<Record<string, unknown>[]>(`/api/v1/settings${qs(params)}`, { tenantId }),
upsert: (tenantId: string, body: Record<string, unknown>) =>
request<Record<string, unknown>>("/api/v1/settings", {
tenantId,
method: "POST",
body: JSON.stringify(body),
}),
remove: (tenantId: string, id: string) => action(tenantId, `/api/v1/settings/${id}/delete`),
},
// Catalog
sportCategories: crud("/api/v1/sport-categories"),
ageGroups: crud("/api/v1/age-groups"),
pricingModels: crud("/api/v1/pricing-models"),
membershipPackages: crud("/api/v1/membership-packages"),
membershipPlans: crud("/api/v1/membership-plans"),
membershipRules: crud("/api/v1/membership-rules"),
renewalPolicies: crud("/api/v1/renewal-policies"),
freezingRules: crud("/api/v1/freezing-rules"),
expirationPolicies: crud("/api/v1/expiration-policies"),
// Members
members: crud("/api/v1/members"),
familyMembers: crud("/api/v1/family-members"),
emergencyContacts: crud("/api/v1/emergency-contacts"),
medicalInformation: {
upsert: (tenantId: string, body: Record<string, unknown>) =>
request<Record<string, unknown>>("/api/v1/medical-information", {
tenantId,
method: "PUT",
body: JSON.stringify(body),
}),
byMember: (tenantId: string, memberId: string) =>
request<Record<string, unknown>>(`/api/v1/medical-information/by-member/${memberId}`, { tenantId }),
list: (tenantId: string, params?: ListParams) =>
request<Record<string, unknown>[]>(`/api/v1/members${qs(params)}`, { tenantId }),
create: (tenantId: string, body: Record<string, unknown>) =>
request<Record<string, unknown>>("/api/v1/medical-information", {
tenantId,
method: "PUT",
body: JSON.stringify(body),
}),
},
membershipCards: {
...createOnly("/api/v1/membership-cards"),
revoke: (tenantId: string, id: string) => action(tenantId, `/api/v1/membership-cards/${id}/revoke`),
},
digitalMemberships: {
list: (tenantId: string, params?: ListParams) =>
request<Record<string, unknown>[]>(`/api/v1/digital-memberships${qs(params)}`, { tenantId }),
create: (tenantId: string, body: Record<string, unknown>) =>
request<Record<string, unknown>>("/api/v1/digital-memberships", {
tenantId,
method: "POST",
body: JSON.stringify(body),
}),
update: (tenantId: string, id: string, body: Record<string, unknown>) =>
request<Record<string, unknown>>(`/api/v1/digital-memberships/${id}`, {
tenantId,
method: "PATCH",
body: JSON.stringify(body),
}),
},
waivers: {
...createOnly("/api/v1/waivers"),
sign: (tenantId: string, id: string, body?: Record<string, unknown>) =>
action(tenantId, `/api/v1/waivers/${id}/sign`, body),
},
memberDocuments: createOnly("/api/v1/member-documents"),
// Staff
staffCertificates: createOnly("/api/v1/staff-certificates"),
staffSkills: createOnly("/api/v1/staff-skills"),
staffWorkingHours: createOnly("/api/v1/staff-working-hours"),
staffAvailability: createOnly("/api/v1/staff-availability"),
staffAssignments: createOnly("/api/v1/staff-assignments"),
// Booking
equipment: createOnly("/api/v1/equipment"),
sessions: createOnly("/api/v1/sessions"),
bookings: {
create: (tenantId: string, body: Record<string, unknown>) =>
request<Record<string, unknown>>("/api/v1/bookings", {
tenantId,
method: "POST",
body: JSON.stringify(body),
}),
confirm: (tenantId: string, id: string) => action(tenantId, `/api/v1/bookings/${id}/confirm`),
cancel: (tenantId: string, id: string, body?: Record<string, unknown>) =>
action(tenantId, `/api/v1/bookings/${id}/cancel`, body),
list: (tenantId: string, params?: ListParams) =>
request<Record<string, unknown>[]>(`/api/v1/sessions${qs(params)}`, { tenantId }),
},
waitingList: createOnly("/api/v1/waiting-list"),
resourceReservations: createOnly("/api/v1/resource-reservations"),
// Attendance
checkIn: (tenantId: string, body: Record<string, unknown>) =>
action(tenantId, "/api/v1/check-in", body),
checkOut: (tenantId: string, body: Record<string, unknown>) =>
action(tenantId, "/api/v1/check-out", body),
attendanceRecords: createOnly("/api/v1/attendance-records"),
accessLogs: listOnly("/api/v1/access-logs"),
// Training
trainingPrograms: createOnly("/api/v1/training-programs"),
exercises: postOnly("/api/v1/exercises"),
workoutPlans: postOnly("/api/v1/workout-plans"),
workoutAssignments: postOnly("/api/v1/workout-assignments"),
trainingGoals: postOnly("/api/v1/training-goals"),
measurements: postOnly("/api/v1/measurements"),
progressRecords: postOnly("/api/v1/progress-records"),
nutritionPlans: postOnly("/api/v1/nutrition-plans"),
assessments: postOnly("/api/v1/assessments"),
performanceRecords: postOnly("/api/v1/performance-records"),
bodyCompositions: postOnly("/api/v1/body-compositions"),
// Competitions
competitions: {
list: (tenantId: string, params?: ListParams) =>
request<Record<string, unknown>[]>(`/api/v1/competitions${qs(params)}`, { tenantId }),
create: (tenantId: string, body: Record<string, unknown>) =>
request<Record<string, unknown>>("/api/v1/competitions", {
tenantId,
method: "POST",
body: JSON.stringify(body),
}),
update: (tenantId: string, id: string, body: Record<string, unknown>) =>
request<Record<string, unknown>>(`/api/v1/competitions/${id}`, {
tenantId,
method: "PATCH",
body: JSON.stringify(body),
}),
},
competitionTeams: createOnly("/api/v1/competition-teams"),
competitionGroups: createOnly("/api/v1/competition-groups"),
competitionRegistrations: {
create: (tenantId: string, body: Record<string, unknown>) =>
request<Record<string, unknown>>("/api/v1/competition-registrations", {
tenantId,
method: "POST",
body: JSON.stringify(body),
}),
confirm: (tenantId: string, id: string) =>
action(tenantId, `/api/v1/competition-registrations/${id}/confirm`),
list: (tenantId: string, params?: ListParams) =>
request<Record<string, unknown>[]>(`/api/v1/competition-registrations${qs(params)}`, { tenantId }),
},
competitionMatches: createOnly("/api/v1/competition-matches"),
competitionResults: createOnly("/api/v1/competition-results"),
competitionRankings: listOnly("/api/v1/competition-rankings"),
competitionMedals: createOnly("/api/v1/competition-medals"),
competitionJudges: createOnly("/api/v1/competition-judges"),
competitionCertificates: createOnly("/api/v1/competition-certificates"),
};