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>
361 lines
14 KiB
TypeScript
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"),
|
|
};
|