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>
1447 lines
40 KiB
TypeScript
1447 lines
40 KiB
TypeScript
/**
|
|
* Beauty Business Service API client — real backend only.
|
|
* Base: NEXT_PUBLIC_BEAUTY_BUSINESS_API_URL (default http://localhost:8011)
|
|
* Always sends Authorization + X-Tenant-ID.
|
|
*/
|
|
|
|
import { getValidAccessToken, refreshSession } from "@/lib/auth";
|
|
|
|
const BEAUTY_BASE =
|
|
typeof window !== "undefined"
|
|
? "/api/beauty-business"
|
|
: process.env.BEAUTY_BUSINESS_SERVICE_URL ||
|
|
process.env.NEXT_PUBLIC_BEAUTY_BUSINESS_API_URL ||
|
|
"http://localhost:8011";
|
|
|
|
export class BeautyBusinessApiError extends Error {
|
|
constructor(
|
|
public status: number,
|
|
public code: string,
|
|
message: string
|
|
) {
|
|
super(message);
|
|
this.name = "BeautyBusinessApiError";
|
|
}
|
|
}
|
|
|
|
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);
|
|
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
|
|
: `${BEAUTY_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 BeautyBusinessApiError(
|
|
res.status,
|
|
body?.error?.code || "unknown_error",
|
|
body?.error?.message || res.statusText
|
|
);
|
|
}
|
|
if (res.status === 204) return undefined as T;
|
|
return res.json() as Promise<T>;
|
|
}
|
|
|
|
function qs(params?: Record<string, string | number | undefined | null>) {
|
|
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}` : "";
|
|
}
|
|
|
|
export type LifecycleStatus = "draft" | "active" | "inactive" | "suspended" | "archived";
|
|
export type BusinessFormat = "salon" | "clinic" | "spa" | "barbershop" | "mixed";
|
|
export type ProviderKind =
|
|
| "communication"
|
|
| "crm"
|
|
| "loyalty"
|
|
| "accounting"
|
|
| "delivery"
|
|
| "storage"
|
|
| "ai"
|
|
| "identity"
|
|
| "custom";
|
|
export type ProviderStatus = "registered" | "active" | "inactive" | "failed" | "retired";
|
|
export type BookingEngineStatus = "registered" | "active" | "inactive" | "failed" | "retired";
|
|
export type AppointmentStatus =
|
|
| "requested"
|
|
| "confirmed"
|
|
| "checked_in"
|
|
| "in_progress"
|
|
| "completed"
|
|
| "cancelled"
|
|
| "no_show";
|
|
export type ServiceStatus = "draft" | "active" | "inactive" | "archived";
|
|
export type ResourceStatus = "available" | "occupied" | "maintenance" | "inactive";
|
|
export type StaffKind = "stylist" | "therapist" | "receptionist" | "manager" | "other";
|
|
export type DayOfWeek =
|
|
| "monday"
|
|
| "tuesday"
|
|
| "wednesday"
|
|
| "thursday"
|
|
| "friday"
|
|
| "saturday"
|
|
| "sunday";
|
|
export type ScheduleExceptionKind = "closed" | "modified_hours" | "holiday";
|
|
export type IntegrationDispatchStatus = "pending" | "sent" | "failed" | "skipped";
|
|
|
|
export type HealthResponse = {
|
|
status: string;
|
|
service: string;
|
|
version: string;
|
|
};
|
|
|
|
export type CapabilitiesResponse = {
|
|
service: string;
|
|
version: string;
|
|
phase: string;
|
|
commercial_product: string;
|
|
features: Record<string, boolean>;
|
|
independence: {
|
|
standalone: boolean;
|
|
superapp: boolean;
|
|
integrations: string[];
|
|
integration_mode: string;
|
|
};
|
|
};
|
|
|
|
export type MetricsResponse = {
|
|
service: string;
|
|
version: string;
|
|
phase: string;
|
|
metrics: Record<string, string | number>;
|
|
note?: string;
|
|
};
|
|
|
|
export type Organization = {
|
|
id: string;
|
|
tenant_id: string;
|
|
code: string;
|
|
name: string;
|
|
description: string | null;
|
|
status: LifecycleStatus;
|
|
business_format: BusinessFormat;
|
|
legal_name: string | null;
|
|
timezone: string;
|
|
language: string;
|
|
currency_code: string;
|
|
settings: Record<string, unknown> | null;
|
|
version: number;
|
|
is_deleted: boolean;
|
|
created_at: string;
|
|
updated_at: string;
|
|
};
|
|
|
|
export type Branch = {
|
|
id: string;
|
|
tenant_id: string;
|
|
organization_id: string;
|
|
code: string;
|
|
name: string;
|
|
description: string | null;
|
|
status: LifecycleStatus;
|
|
business_format: BusinessFormat | null;
|
|
address: Record<string, unknown> | null;
|
|
timezone: string | null;
|
|
working_hours: Record<string, unknown> | null;
|
|
metadata_json: Record<string, unknown> | null;
|
|
is_deleted: boolean;
|
|
created_at: string;
|
|
updated_at: string;
|
|
};
|
|
|
|
export type ExternalProvider = {
|
|
id: string;
|
|
tenant_id: string;
|
|
organization_id: string | null;
|
|
code: string;
|
|
name: string;
|
|
provider_kind: ProviderKind;
|
|
adapter_key: string;
|
|
status: ProviderStatus;
|
|
priority: number;
|
|
credentials_ref: string | null;
|
|
config: Record<string, unknown> | null;
|
|
capabilities: Record<string, unknown> | null;
|
|
version: number;
|
|
is_deleted: boolean;
|
|
created_at: string;
|
|
updated_at: string;
|
|
};
|
|
|
|
export type BookingEngine = {
|
|
id: string;
|
|
tenant_id: string;
|
|
organization_id: string | null;
|
|
code: string;
|
|
name: string;
|
|
adapter_key: string;
|
|
status: BookingEngineStatus;
|
|
is_default: boolean;
|
|
config: Record<string, unknown> | null;
|
|
capabilities: Record<string, unknown> | null;
|
|
version: number;
|
|
is_deleted: boolean;
|
|
created_at: string;
|
|
updated_at: string;
|
|
};
|
|
|
|
export type Configuration = {
|
|
id: string;
|
|
tenant_id: string;
|
|
organization_id: string;
|
|
branch_id: string | null;
|
|
code: string;
|
|
working_hours: Record<string, unknown> | null;
|
|
booking_policies: Record<string, unknown> | null;
|
|
cancellation_policies: Record<string, unknown> | null;
|
|
reminder_policies: Record<string, unknown> | null;
|
|
custom_fields: Record<string, unknown> | null;
|
|
timezone: string;
|
|
language: string;
|
|
currency_code: string;
|
|
version: number;
|
|
is_deleted: boolean;
|
|
created_at: string;
|
|
updated_at: string;
|
|
};
|
|
|
|
export type Setting = {
|
|
id: string;
|
|
tenant_id: string;
|
|
organization_id: string | null;
|
|
branch_id: string | null;
|
|
key: string;
|
|
value: Record<string, unknown> | null;
|
|
description: string | null;
|
|
is_deleted: boolean;
|
|
created_at: string;
|
|
updated_at: string;
|
|
};
|
|
|
|
export type AuditLog = {
|
|
id: string;
|
|
tenant_id: string;
|
|
entity_type: string;
|
|
entity_id: string;
|
|
action: string;
|
|
actor_user_id: string | null;
|
|
changes: Record<string, unknown> | null;
|
|
message: string | null;
|
|
created_at: string;
|
|
};
|
|
|
|
export type PermissionCatalog = {
|
|
prefix: string;
|
|
prefixes: string[];
|
|
permissions: string[];
|
|
count: number;
|
|
};
|
|
|
|
export type StaffSchedule = {
|
|
id: string;
|
|
tenant_id: string;
|
|
organization_id: string;
|
|
branch_id: string;
|
|
staff_ref: string;
|
|
code: string;
|
|
day_of_week: DayOfWeek;
|
|
start_time: string;
|
|
end_time: string;
|
|
is_active: boolean;
|
|
metadata_json: Record<string, unknown> | null;
|
|
is_deleted: boolean;
|
|
created_at: string;
|
|
updated_at: string;
|
|
};
|
|
|
|
export type StaffAvailability = {
|
|
id: string;
|
|
tenant_id: string;
|
|
organization_id: string;
|
|
branch_id: string;
|
|
staff_ref: string;
|
|
code: string;
|
|
starts_at: string;
|
|
ends_at: string;
|
|
is_bookable: boolean;
|
|
metadata_json: Record<string, unknown> | null;
|
|
is_deleted: boolean;
|
|
created_at: string;
|
|
updated_at: string;
|
|
};
|
|
|
|
export type ScheduleException = {
|
|
id: string;
|
|
tenant_id: string;
|
|
organization_id: string;
|
|
branch_id: string;
|
|
staff_ref: string | null;
|
|
code: string;
|
|
kind: ScheduleExceptionKind;
|
|
exception_date: string;
|
|
start_time: string | null;
|
|
end_time: string | null;
|
|
reason: string | null;
|
|
is_deleted: boolean;
|
|
created_at: string;
|
|
updated_at: string;
|
|
};
|
|
|
|
export type AppointmentType = {
|
|
id: string;
|
|
tenant_id: string;
|
|
organization_id: string;
|
|
code: string;
|
|
name: string;
|
|
description: string | null;
|
|
default_duration_minutes: number;
|
|
status: LifecycleStatus;
|
|
is_deleted: boolean;
|
|
created_at: string;
|
|
updated_at: string;
|
|
};
|
|
|
|
export type Appointment = {
|
|
id: string;
|
|
tenant_id: string;
|
|
organization_id: string;
|
|
branch_id: string;
|
|
appointment_type_id: string | null;
|
|
code: string;
|
|
customer_ref: string | null;
|
|
staff_ref: string | null;
|
|
room_ref: string | null;
|
|
service_ref: string | null;
|
|
status: AppointmentStatus;
|
|
scheduled_start: string;
|
|
scheduled_end: string;
|
|
notes: string | null;
|
|
status_reason: string | null;
|
|
status_changed_at: string | null;
|
|
metadata_json: Record<string, unknown> | null;
|
|
version: number;
|
|
is_deleted: boolean;
|
|
created_at: string;
|
|
updated_at: string;
|
|
};
|
|
|
|
export type AppointmentStatusHistory = {
|
|
id: string;
|
|
tenant_id: string;
|
|
appointment_id: string;
|
|
action: string;
|
|
from_status: AppointmentStatus;
|
|
to_status: AppointmentStatus;
|
|
reason: string | null;
|
|
actor_user_id: string | null;
|
|
metadata_json: Record<string, unknown> | null;
|
|
created_at: string;
|
|
};
|
|
|
|
export type WaitingListEntry = {
|
|
id: string;
|
|
tenant_id: string;
|
|
organization_id: string;
|
|
branch_id: string;
|
|
code: string;
|
|
customer_ref: string | null;
|
|
service_ref: string | null;
|
|
preferred_staff_ref: string | null;
|
|
requested_date: string | null;
|
|
notes: string | null;
|
|
priority: number;
|
|
is_deleted: boolean;
|
|
created_at: string;
|
|
updated_at: string;
|
|
};
|
|
|
|
export type TreatmentRoom = {
|
|
id: string;
|
|
tenant_id: string;
|
|
organization_id: string;
|
|
branch_id: string;
|
|
code: string;
|
|
name: string;
|
|
description: string | null;
|
|
status: ResourceStatus;
|
|
capacity: number;
|
|
metadata_json: Record<string, unknown> | null;
|
|
is_deleted: boolean;
|
|
created_at: string;
|
|
updated_at: string;
|
|
};
|
|
|
|
export type Station = {
|
|
id: string;
|
|
tenant_id: string;
|
|
organization_id: string;
|
|
branch_id: string;
|
|
room_id: string | null;
|
|
code: string;
|
|
name: string;
|
|
status: ResourceStatus;
|
|
metadata_json: Record<string, unknown> | null;
|
|
is_deleted: boolean;
|
|
created_at: string;
|
|
updated_at: string;
|
|
};
|
|
|
|
export type BranchPolicy = {
|
|
id: string;
|
|
tenant_id: string;
|
|
organization_id: string;
|
|
branch_id: string;
|
|
code: string;
|
|
name: string;
|
|
policy_type: string;
|
|
rules: Record<string, unknown> | null;
|
|
status: LifecycleStatus;
|
|
is_deleted: boolean;
|
|
created_at: string;
|
|
updated_at: string;
|
|
};
|
|
|
|
export type ServiceCategory = {
|
|
id: string;
|
|
tenant_id: string;
|
|
organization_id: string;
|
|
code: string;
|
|
name: string;
|
|
description: string | null;
|
|
status: LifecycleStatus;
|
|
sort_order: number;
|
|
is_deleted: boolean;
|
|
created_at: string;
|
|
updated_at: string;
|
|
};
|
|
|
|
export type BeautyService = {
|
|
id: string;
|
|
tenant_id: string;
|
|
organization_id: string;
|
|
category_id: string | null;
|
|
code: string;
|
|
name: string;
|
|
description: string | null;
|
|
status: ServiceStatus;
|
|
base_duration_minutes: number;
|
|
base_price: number | null;
|
|
metadata_json: Record<string, unknown> | null;
|
|
is_deleted: boolean;
|
|
created_at: string;
|
|
updated_at: string;
|
|
};
|
|
|
|
export type CustomerProfile = {
|
|
id: string;
|
|
tenant_id: string;
|
|
organization_id: string;
|
|
code: string;
|
|
display_name: string;
|
|
external_crm_contact_ref: string | null;
|
|
external_user_ref: string | null;
|
|
mobile: string | null;
|
|
email: string | null;
|
|
status: LifecycleStatus;
|
|
metadata_json: Record<string, unknown> | null;
|
|
is_deleted: boolean;
|
|
created_at: string;
|
|
updated_at: string;
|
|
};
|
|
|
|
export type PackageDefinition = {
|
|
id: string;
|
|
tenant_id: string;
|
|
organization_id: string;
|
|
code: string;
|
|
name: string;
|
|
description: string | null;
|
|
total_sessions: number;
|
|
price: number | null;
|
|
valid_days: number | null;
|
|
status: LifecycleStatus;
|
|
is_deleted: boolean;
|
|
created_at: string;
|
|
updated_at: string;
|
|
};
|
|
|
|
export type MembershipPlan = {
|
|
id: string;
|
|
tenant_id: string;
|
|
organization_id: string;
|
|
code: string;
|
|
name: string;
|
|
description: string | null;
|
|
billing_interval_days: number;
|
|
price: number | null;
|
|
benefits: Record<string, unknown> | null;
|
|
status: LifecycleStatus;
|
|
is_deleted: boolean;
|
|
created_at: string;
|
|
updated_at: string;
|
|
};
|
|
|
|
export type StaffProfile = {
|
|
id: string;
|
|
tenant_id: string;
|
|
organization_id: string;
|
|
branch_id: string | null;
|
|
code: string;
|
|
display_name: string;
|
|
kind: StaffKind;
|
|
external_user_ref: string | null;
|
|
mobile: string | null;
|
|
email: string | null;
|
|
status: LifecycleStatus;
|
|
hire_date: string | null;
|
|
metadata_json: Record<string, unknown> | null;
|
|
is_deleted: boolean;
|
|
created_at: string;
|
|
updated_at: string;
|
|
};
|
|
|
|
export type CommissionRule = {
|
|
id: string;
|
|
tenant_id: string;
|
|
organization_id: string;
|
|
code: string;
|
|
name: string;
|
|
rule_type: string;
|
|
rate_percent: number | null;
|
|
flat_amount: number | null;
|
|
applies_to_service_id: string | null;
|
|
status: LifecycleStatus;
|
|
config: Record<string, unknown> | null;
|
|
is_deleted: boolean;
|
|
created_at: string;
|
|
updated_at: string;
|
|
};
|
|
|
|
export type MarketingCampaign = {
|
|
id: string;
|
|
tenant_id: string;
|
|
organization_id: string | null;
|
|
branch_id: string | null;
|
|
code: string;
|
|
name: string;
|
|
external_campaign_ref: string | null;
|
|
channel: string | null;
|
|
status: LifecycleStatus;
|
|
config: Record<string, unknown> | null;
|
|
is_deleted: boolean;
|
|
created_at: string;
|
|
updated_at: string;
|
|
};
|
|
|
|
export type LoyaltyIntegration = {
|
|
id: string;
|
|
tenant_id: string;
|
|
organization_id: string | null;
|
|
code: string;
|
|
name: string;
|
|
external_program_ref: string | null;
|
|
earn_rules: Record<string, unknown> | null;
|
|
status: LifecycleStatus;
|
|
is_deleted: boolean;
|
|
created_at: string;
|
|
updated_at: string;
|
|
};
|
|
|
|
export type CommunicationIntegration = {
|
|
id: string;
|
|
tenant_id: string;
|
|
organization_id: string | null;
|
|
code: string;
|
|
name: string;
|
|
template_mappings: Record<string, unknown> | null;
|
|
status: LifecycleStatus;
|
|
is_deleted: boolean;
|
|
created_at: string;
|
|
updated_at: string;
|
|
};
|
|
|
|
export type IntegrationDispatch = {
|
|
id: string;
|
|
tenant_id: string;
|
|
organization_id: string | null;
|
|
integration_kind: string;
|
|
config_id: string | null;
|
|
event_type: string;
|
|
payload_ref: string | null;
|
|
status: IntegrationDispatchStatus;
|
|
response_json: Record<string, unknown> | null;
|
|
notes: string | null;
|
|
is_deleted: boolean;
|
|
created_at: string;
|
|
updated_at: string;
|
|
};
|
|
|
|
type PaginatedList = { page?: number; pageSize?: number };
|
|
|
|
function listPath(base: string, { page = 1, pageSize = 50 }: PaginatedList = {}) {
|
|
return `${base}${qs({ page, page_size: pageSize })}`;
|
|
}
|
|
|
|
export const beautyBusinessApi = {
|
|
health: () =>
|
|
fetch(`${BEAUTY_BASE}/health`).then((r) => r.json() as Promise<HealthResponse>),
|
|
|
|
capabilities: () =>
|
|
fetch(`${BEAUTY_BASE}/capabilities`).then((r) => r.json() as Promise<CapabilitiesResponse>),
|
|
|
|
metrics: () =>
|
|
fetch(`${BEAUTY_BASE}/metrics`).then((r) => r.json() as Promise<MetricsResponse>),
|
|
|
|
organizations: {
|
|
list: (tenantId: string, page = 1, pageSize = 50) =>
|
|
request<Organization[]>(listPath("/api/v1/organizations", { page, pageSize }), { tenantId }),
|
|
get: (tenantId: string, id: string) =>
|
|
request<Organization>(`/api/v1/organizations/${id}`, { tenantId }),
|
|
create: (
|
|
tenantId: string,
|
|
body: {
|
|
code: string;
|
|
name: string;
|
|
description?: string;
|
|
status?: LifecycleStatus;
|
|
business_format?: BusinessFormat;
|
|
legal_name?: string;
|
|
timezone?: string;
|
|
language?: string;
|
|
currency_code?: string;
|
|
}
|
|
) =>
|
|
request<Organization>("/api/v1/organizations", {
|
|
tenantId,
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
update: (
|
|
tenantId: string,
|
|
id: string,
|
|
body: Partial<{
|
|
name: string;
|
|
description: string;
|
|
status: LifecycleStatus;
|
|
business_format: BusinessFormat;
|
|
legal_name: string;
|
|
timezone: string;
|
|
language: string;
|
|
currency_code: string;
|
|
version: number;
|
|
}>
|
|
) =>
|
|
request<Organization>(`/api/v1/organizations/${id}`, {
|
|
tenantId,
|
|
method: "PATCH",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
delete: (tenantId: string, id: string) =>
|
|
request<Organization>(`/api/v1/organizations/${id}/delete`, { tenantId, method: "POST" }),
|
|
},
|
|
|
|
branches: {
|
|
list: (tenantId: string, page = 1, pageSize = 50) =>
|
|
request<Branch[]>(listPath("/api/v1/branches", { page, pageSize }), { tenantId }),
|
|
get: (tenantId: string, id: string) =>
|
|
request<Branch>(`/api/v1/branches/${id}`, { tenantId }),
|
|
create: (
|
|
tenantId: string,
|
|
body: {
|
|
organization_id: string;
|
|
code: string;
|
|
name: string;
|
|
description?: string;
|
|
status?: LifecycleStatus;
|
|
}
|
|
) =>
|
|
request<Branch>("/api/v1/branches", {
|
|
tenantId,
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
update: (
|
|
tenantId: string,
|
|
id: string,
|
|
body: Partial<{
|
|
name: string;
|
|
description: string;
|
|
status: LifecycleStatus;
|
|
}>
|
|
) =>
|
|
request<Branch>(`/api/v1/branches/${id}`, {
|
|
tenantId,
|
|
method: "PATCH",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
delete: (tenantId: string, id: string) =>
|
|
request<Branch>(`/api/v1/branches/${id}/delete`, { tenantId, method: "POST" }),
|
|
},
|
|
|
|
externalProviders: {
|
|
list: (tenantId: string, page = 1, pageSize = 50) =>
|
|
request<ExternalProvider[]>(listPath("/api/v1/external-providers", { page, pageSize }), {
|
|
tenantId,
|
|
}),
|
|
get: (tenantId: string, id: string) =>
|
|
request<ExternalProvider>(`/api/v1/external-providers/${id}`, { tenantId }),
|
|
create: (
|
|
tenantId: string,
|
|
body: {
|
|
organization_id?: string;
|
|
code: string;
|
|
name: string;
|
|
provider_kind?: ProviderKind;
|
|
adapter_key: string;
|
|
status?: ProviderStatus;
|
|
priority?: number;
|
|
}
|
|
) =>
|
|
request<ExternalProvider>("/api/v1/external-providers", {
|
|
tenantId,
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
update: (
|
|
tenantId: string,
|
|
id: string,
|
|
body: Partial<{
|
|
name: string;
|
|
provider_kind: ProviderKind;
|
|
adapter_key: string;
|
|
status: ProviderStatus;
|
|
priority: number;
|
|
version: number;
|
|
}>
|
|
) =>
|
|
request<ExternalProvider>(`/api/v1/external-providers/${id}`, {
|
|
tenantId,
|
|
method: "PATCH",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
delete: (tenantId: string, id: string) =>
|
|
request<ExternalProvider>(`/api/v1/external-providers/${id}/delete`, {
|
|
tenantId,
|
|
method: "POST",
|
|
}),
|
|
},
|
|
|
|
bookingEngines: {
|
|
list: (tenantId: string, page = 1, pageSize = 50) =>
|
|
request<BookingEngine[]>(listPath("/api/v1/booking-engines", { page, pageSize }), {
|
|
tenantId,
|
|
}),
|
|
get: (tenantId: string, id: string) =>
|
|
request<BookingEngine>(`/api/v1/booking-engines/${id}`, { tenantId }),
|
|
create: (
|
|
tenantId: string,
|
|
body: {
|
|
organization_id?: string;
|
|
code: string;
|
|
name: string;
|
|
adapter_key: string;
|
|
status?: BookingEngineStatus;
|
|
is_default?: boolean;
|
|
}
|
|
) =>
|
|
request<BookingEngine>("/api/v1/booking-engines", {
|
|
tenantId,
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
update: (
|
|
tenantId: string,
|
|
id: string,
|
|
body: Partial<{
|
|
name: string;
|
|
adapter_key: string;
|
|
status: BookingEngineStatus;
|
|
is_default: boolean;
|
|
version: number;
|
|
}>
|
|
) =>
|
|
request<BookingEngine>(`/api/v1/booking-engines/${id}`, {
|
|
tenantId,
|
|
method: "PATCH",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
delete: (tenantId: string, id: string) =>
|
|
request<BookingEngine>(`/api/v1/booking-engines/${id}/delete`, {
|
|
tenantId,
|
|
method: "POST",
|
|
}),
|
|
},
|
|
|
|
configurations: {
|
|
list: (tenantId: string, page = 1, pageSize = 50) =>
|
|
request<Configuration[]>(listPath("/api/v1/configurations", { page, pageSize }), { tenantId }),
|
|
get: (tenantId: string, id: string) =>
|
|
request<Configuration>(`/api/v1/configurations/${id}`, { tenantId }),
|
|
create: (
|
|
tenantId: string,
|
|
body: {
|
|
organization_id: string;
|
|
branch_id?: string;
|
|
code: string;
|
|
timezone?: string;
|
|
language?: string;
|
|
currency_code?: string;
|
|
}
|
|
) =>
|
|
request<Configuration>("/api/v1/configurations", {
|
|
tenantId,
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
update: (
|
|
tenantId: string,
|
|
id: string,
|
|
body: Partial<{
|
|
branch_id: string | null;
|
|
timezone: string;
|
|
language: string;
|
|
currency_code: string;
|
|
version: number;
|
|
}>
|
|
) =>
|
|
request<Configuration>(`/api/v1/configurations/${id}`, {
|
|
tenantId,
|
|
method: "PATCH",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
delete: (tenantId: string, id: string) =>
|
|
request<Configuration>(`/api/v1/configurations/${id}/delete`, {
|
|
tenantId,
|
|
method: "POST",
|
|
}),
|
|
},
|
|
|
|
settings: {
|
|
list: (tenantId: string, page = 1, pageSize = 100) =>
|
|
request<Setting[]>(listPath("/api/v1/settings", { page, pageSize }), { tenantId }),
|
|
upsert: (
|
|
tenantId: string,
|
|
body: {
|
|
organization_id?: string;
|
|
branch_id?: string;
|
|
key: string;
|
|
value?: Record<string, unknown>;
|
|
description?: string;
|
|
}
|
|
) =>
|
|
request<Setting>("/api/v1/settings", {
|
|
tenantId,
|
|
method: "PUT",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
},
|
|
|
|
audit: {
|
|
list: (tenantId: string, entityType: string, entityId: string, limit = 100) =>
|
|
request<AuditLog[]>(
|
|
`/api/v1/audit${qs({ entity_type: entityType, entity_id: entityId, limit })}`,
|
|
{ tenantId }
|
|
),
|
|
},
|
|
|
|
permissions: {
|
|
catalog: (tenantId: string) =>
|
|
request<PermissionCatalog>("/api/v1/permissions/catalog", { tenantId }),
|
|
},
|
|
|
|
staffSchedules: {
|
|
list: (tenantId: string, page = 1, pageSize = 50) =>
|
|
request<StaffSchedule[]>(listPath("/api/v1/staff-schedules", { page, pageSize }), {
|
|
tenantId,
|
|
}),
|
|
get: (tenantId: string, id: string) =>
|
|
request<StaffSchedule>(`/api/v1/staff-schedules/${id}`, { tenantId }),
|
|
create: (
|
|
tenantId: string,
|
|
body: {
|
|
organization_id: string;
|
|
branch_id: string;
|
|
staff_ref: string;
|
|
code: string;
|
|
day_of_week: DayOfWeek;
|
|
start_time: string;
|
|
end_time: string;
|
|
is_active?: boolean;
|
|
}
|
|
) =>
|
|
request<StaffSchedule>("/api/v1/staff-schedules", {
|
|
tenantId,
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
},
|
|
|
|
staffAvailabilities: {
|
|
list: (tenantId: string, page = 1, pageSize = 50) =>
|
|
request<StaffAvailability[]>(listPath("/api/v1/staff-availabilities", { page, pageSize }), {
|
|
tenantId,
|
|
}),
|
|
get: (tenantId: string, id: string) =>
|
|
request<StaffAvailability>(`/api/v1/staff-availabilities/${id}`, { tenantId }),
|
|
create: (
|
|
tenantId: string,
|
|
body: {
|
|
organization_id: string;
|
|
branch_id: string;
|
|
staff_ref: string;
|
|
code: string;
|
|
starts_at: string;
|
|
ends_at: string;
|
|
is_bookable?: boolean;
|
|
}
|
|
) =>
|
|
request<StaffAvailability>("/api/v1/staff-availabilities", {
|
|
tenantId,
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
},
|
|
|
|
scheduleExceptions: {
|
|
list: (tenantId: string, page = 1, pageSize = 50) =>
|
|
request<ScheduleException[]>(listPath("/api/v1/schedule-exceptions", { page, pageSize }), {
|
|
tenantId,
|
|
}),
|
|
get: (tenantId: string, id: string) =>
|
|
request<ScheduleException>(`/api/v1/schedule-exceptions/${id}`, { tenantId }),
|
|
create: (
|
|
tenantId: string,
|
|
body: {
|
|
organization_id: string;
|
|
branch_id: string;
|
|
staff_ref?: string;
|
|
code: string;
|
|
kind: ScheduleExceptionKind;
|
|
exception_date: string;
|
|
start_time?: string;
|
|
end_time?: string;
|
|
reason?: string;
|
|
}
|
|
) =>
|
|
request<ScheduleException>("/api/v1/schedule-exceptions", {
|
|
tenantId,
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
},
|
|
|
|
appointmentTypes: {
|
|
list: (tenantId: string, page = 1, pageSize = 50) =>
|
|
request<AppointmentType[]>(listPath("/api/v1/appointment-types", { page, pageSize }), {
|
|
tenantId,
|
|
}),
|
|
get: (tenantId: string, id: string) =>
|
|
request<AppointmentType>(`/api/v1/appointment-types/${id}`, { tenantId }),
|
|
create: (
|
|
tenantId: string,
|
|
body: {
|
|
organization_id: string;
|
|
code: string;
|
|
name: string;
|
|
description?: string;
|
|
default_duration_minutes?: number;
|
|
status?: LifecycleStatus;
|
|
}
|
|
) =>
|
|
request<AppointmentType>("/api/v1/appointment-types", {
|
|
tenantId,
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
},
|
|
|
|
appointments: {
|
|
list: (tenantId: string, page = 1, pageSize = 50) =>
|
|
request<Appointment[]>(listPath("/api/v1/appointments", { page, pageSize }), { tenantId }),
|
|
get: (tenantId: string, id: string) =>
|
|
request<Appointment>(`/api/v1/appointments/${id}`, { tenantId }),
|
|
create: (
|
|
tenantId: string,
|
|
body: {
|
|
organization_id: string;
|
|
branch_id: string;
|
|
code: string;
|
|
scheduled_start: string;
|
|
scheduled_end: string;
|
|
appointment_type_id?: string;
|
|
customer_ref?: string;
|
|
staff_ref?: string;
|
|
room_ref?: string;
|
|
service_ref?: string;
|
|
status?: AppointmentStatus;
|
|
notes?: string;
|
|
}
|
|
) =>
|
|
request<Appointment>("/api/v1/appointments", {
|
|
tenantId,
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
update: (
|
|
tenantId: string,
|
|
id: string,
|
|
body: Partial<{
|
|
staff_ref: string;
|
|
room_ref: string;
|
|
service_ref: string;
|
|
scheduled_start: string;
|
|
scheduled_end: string;
|
|
notes: string;
|
|
version: number;
|
|
}>
|
|
) =>
|
|
request<Appointment>(`/api/v1/appointments/${id}`, {
|
|
tenantId,
|
|
method: "PATCH",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
confirm: (tenantId: string, id: string, body: { reason?: string; version: number }) =>
|
|
request<Appointment>(`/api/v1/appointments/${id}/confirm`, {
|
|
tenantId,
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
checkIn: (tenantId: string, id: string, body: { reason?: string; version: number }) =>
|
|
request<Appointment>(`/api/v1/appointments/${id}/check-in`, {
|
|
tenantId,
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
complete: (tenantId: string, id: string, body: { reason?: string; version: number }) =>
|
|
request<Appointment>(`/api/v1/appointments/${id}/complete`, {
|
|
tenantId,
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
cancel: (tenantId: string, id: string, body: { reason?: string; version: number }) =>
|
|
request<Appointment>(`/api/v1/appointments/${id}/cancel`, {
|
|
tenantId,
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
noShow: (tenantId: string, id: string, body: { reason?: string; version: number }) =>
|
|
request<Appointment>(`/api/v1/appointments/${id}/no-show`, {
|
|
tenantId,
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
history: (tenantId: string, id: string) =>
|
|
request<AppointmentStatusHistory[]>(`/api/v1/appointments/${id}/history`, { tenantId }),
|
|
},
|
|
|
|
waitingList: {
|
|
list: (tenantId: string, page = 1, pageSize = 50) =>
|
|
request<WaitingListEntry[]>(listPath("/api/v1/waiting-list", { page, pageSize }), {
|
|
tenantId,
|
|
}),
|
|
get: (tenantId: string, id: string) =>
|
|
request<WaitingListEntry>(`/api/v1/waiting-list/${id}`, { tenantId }),
|
|
create: (
|
|
tenantId: string,
|
|
body: {
|
|
organization_id: string;
|
|
branch_id: string;
|
|
code: string;
|
|
customer_ref?: string;
|
|
service_ref?: string;
|
|
preferred_staff_ref?: string;
|
|
requested_date?: string;
|
|
notes?: string;
|
|
priority?: number;
|
|
}
|
|
) =>
|
|
request<WaitingListEntry>("/api/v1/waiting-list", {
|
|
tenantId,
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
},
|
|
|
|
treatmentRooms: {
|
|
list: (tenantId: string, page = 1, pageSize = 50) =>
|
|
request<TreatmentRoom[]>(listPath("/api/v1/treatment-rooms", { page, pageSize }), {
|
|
tenantId,
|
|
}),
|
|
get: (tenantId: string, id: string) =>
|
|
request<TreatmentRoom>(`/api/v1/treatment-rooms/${id}`, { tenantId }),
|
|
create: (
|
|
tenantId: string,
|
|
body: {
|
|
organization_id: string;
|
|
branch_id: string;
|
|
code: string;
|
|
name: string;
|
|
description?: string;
|
|
status?: ResourceStatus;
|
|
capacity?: number;
|
|
}
|
|
) =>
|
|
request<TreatmentRoom>("/api/v1/treatment-rooms", {
|
|
tenantId,
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
},
|
|
|
|
stations: {
|
|
list: (tenantId: string, page = 1, pageSize = 50) =>
|
|
request<Station[]>(listPath("/api/v1/stations", { page, pageSize }), { tenantId }),
|
|
get: (tenantId: string, id: string) =>
|
|
request<Station>(`/api/v1/stations/${id}`, { tenantId }),
|
|
create: (
|
|
tenantId: string,
|
|
body: {
|
|
organization_id: string;
|
|
branch_id: string;
|
|
room_id?: string;
|
|
code: string;
|
|
name: string;
|
|
status?: ResourceStatus;
|
|
}
|
|
) =>
|
|
request<Station>("/api/v1/stations", {
|
|
tenantId,
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
},
|
|
|
|
branchPolicies: {
|
|
list: (tenantId: string, page = 1, pageSize = 50) =>
|
|
request<BranchPolicy[]>(listPath("/api/v1/branch-policies", { page, pageSize }), {
|
|
tenantId,
|
|
}),
|
|
get: (tenantId: string, id: string) =>
|
|
request<BranchPolicy>(`/api/v1/branch-policies/${id}`, { tenantId }),
|
|
create: (
|
|
tenantId: string,
|
|
body: {
|
|
organization_id: string;
|
|
branch_id: string;
|
|
code: string;
|
|
name: string;
|
|
policy_type: string;
|
|
rules?: Record<string, unknown>;
|
|
status?: LifecycleStatus;
|
|
}
|
|
) =>
|
|
request<BranchPolicy>("/api/v1/branch-policies", {
|
|
tenantId,
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
},
|
|
|
|
serviceCategories: {
|
|
list: (tenantId: string, page = 1, pageSize = 50) =>
|
|
request<ServiceCategory[]>(listPath("/api/v1/service-categories", { page, pageSize }), {
|
|
tenantId,
|
|
}),
|
|
get: (tenantId: string, id: string) =>
|
|
request<ServiceCategory>(`/api/v1/service-categories/${id}`, { tenantId }),
|
|
create: (
|
|
tenantId: string,
|
|
body: {
|
|
organization_id: string;
|
|
code: string;
|
|
name: string;
|
|
description?: string;
|
|
status?: LifecycleStatus;
|
|
sort_order?: number;
|
|
}
|
|
) =>
|
|
request<ServiceCategory>("/api/v1/service-categories", {
|
|
tenantId,
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
},
|
|
|
|
services: {
|
|
list: (tenantId: string, page = 1, pageSize = 50) =>
|
|
request<BeautyService[]>(listPath("/api/v1/services", { page, pageSize }), { tenantId }),
|
|
get: (tenantId: string, id: string) =>
|
|
request<BeautyService>(`/api/v1/services/${id}`, { tenantId }),
|
|
create: (
|
|
tenantId: string,
|
|
body: {
|
|
organization_id: string;
|
|
category_id?: string;
|
|
code: string;
|
|
name: string;
|
|
description?: string;
|
|
status?: ServiceStatus;
|
|
base_duration_minutes?: number;
|
|
base_price?: number;
|
|
}
|
|
) =>
|
|
request<BeautyService>("/api/v1/services", {
|
|
tenantId,
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
},
|
|
|
|
customers: {
|
|
list: (tenantId: string, page = 1, pageSize = 50) =>
|
|
request<CustomerProfile[]>(listPath("/api/v1/customers", { page, pageSize }), { tenantId }),
|
|
get: (tenantId: string, id: string) =>
|
|
request<CustomerProfile>(`/api/v1/customers/${id}`, { tenantId }),
|
|
create: (
|
|
tenantId: string,
|
|
body: {
|
|
organization_id: string;
|
|
code: string;
|
|
display_name: string;
|
|
mobile?: string;
|
|
email?: string;
|
|
status?: LifecycleStatus;
|
|
}
|
|
) =>
|
|
request<CustomerProfile>("/api/v1/customers", {
|
|
tenantId,
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
},
|
|
|
|
packageDefinitions: {
|
|
list: (tenantId: string, page = 1, pageSize = 50) =>
|
|
request<PackageDefinition[]>(listPath("/api/v1/package-definitions", { page, pageSize }), {
|
|
tenantId,
|
|
}),
|
|
get: (tenantId: string, id: string) =>
|
|
request<PackageDefinition>(`/api/v1/package-definitions/${id}`, { tenantId }),
|
|
create: (
|
|
tenantId: string,
|
|
body: {
|
|
organization_id: string;
|
|
code: string;
|
|
name: string;
|
|
description?: string;
|
|
total_sessions?: number;
|
|
price?: number;
|
|
valid_days?: number;
|
|
status?: LifecycleStatus;
|
|
}
|
|
) =>
|
|
request<PackageDefinition>("/api/v1/package-definitions", {
|
|
tenantId,
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
},
|
|
|
|
membershipPlans: {
|
|
list: (tenantId: string, page = 1, pageSize = 50) =>
|
|
request<MembershipPlan[]>(listPath("/api/v1/membership-plans", { page, pageSize }), {
|
|
tenantId,
|
|
}),
|
|
get: (tenantId: string, id: string) =>
|
|
request<MembershipPlan>(`/api/v1/membership-plans/${id}`, { tenantId }),
|
|
create: (
|
|
tenantId: string,
|
|
body: {
|
|
organization_id: string;
|
|
code: string;
|
|
name: string;
|
|
description?: string;
|
|
billing_interval_days?: number;
|
|
price?: number;
|
|
status?: LifecycleStatus;
|
|
}
|
|
) =>
|
|
request<MembershipPlan>("/api/v1/membership-plans", {
|
|
tenantId,
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
},
|
|
|
|
staff: {
|
|
list: (tenantId: string, page = 1, pageSize = 50) =>
|
|
request<StaffProfile[]>(listPath("/api/v1/staff", { page, pageSize }), { tenantId }),
|
|
get: (tenantId: string, id: string) =>
|
|
request<StaffProfile>(`/api/v1/staff/${id}`, { tenantId }),
|
|
create: (
|
|
tenantId: string,
|
|
body: {
|
|
organization_id: string;
|
|
branch_id?: string;
|
|
code: string;
|
|
display_name: string;
|
|
kind?: StaffKind;
|
|
mobile?: string;
|
|
email?: string;
|
|
status?: LifecycleStatus;
|
|
}
|
|
) =>
|
|
request<StaffProfile>("/api/v1/staff", {
|
|
tenantId,
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
},
|
|
|
|
commissionRules: {
|
|
list: (tenantId: string, page = 1, pageSize = 50) =>
|
|
request<CommissionRule[]>(listPath("/api/v1/commission-rules", { page, pageSize }), {
|
|
tenantId,
|
|
}),
|
|
get: (tenantId: string, id: string) =>
|
|
request<CommissionRule>(`/api/v1/commission-rules/${id}`, { tenantId }),
|
|
create: (
|
|
tenantId: string,
|
|
body: {
|
|
organization_id: string;
|
|
code: string;
|
|
name: string;
|
|
rule_type: string;
|
|
rate_percent?: number;
|
|
flat_amount?: number;
|
|
applies_to_service_id?: string;
|
|
status?: LifecycleStatus;
|
|
}
|
|
) =>
|
|
request<CommissionRule>("/api/v1/commission-rules", {
|
|
tenantId,
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
},
|
|
|
|
marketingCampaigns: {
|
|
list: (tenantId: string, page = 1, pageSize = 50) =>
|
|
request<MarketingCampaign[]>(listPath("/api/v1/marketing-campaigns", { page, pageSize }), {
|
|
tenantId,
|
|
}),
|
|
get: (tenantId: string, id: string) =>
|
|
request<MarketingCampaign>(`/api/v1/marketing-campaigns/${id}`, { tenantId }),
|
|
create: (
|
|
tenantId: string,
|
|
body: {
|
|
organization_id?: string;
|
|
branch_id?: string;
|
|
code: string;
|
|
name: string;
|
|
external_campaign_ref?: string;
|
|
channel?: string;
|
|
status?: LifecycleStatus;
|
|
}
|
|
) =>
|
|
request<MarketingCampaign>("/api/v1/marketing-campaigns", {
|
|
tenantId,
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
},
|
|
|
|
loyaltyIntegrations: {
|
|
list: (tenantId: string, page = 1, pageSize = 50) =>
|
|
request<LoyaltyIntegration[]>(listPath("/api/v1/loyalty-integrations", { page, pageSize }), {
|
|
tenantId,
|
|
}),
|
|
get: (tenantId: string, id: string) =>
|
|
request<LoyaltyIntegration>(`/api/v1/loyalty-integrations/${id}`, { tenantId }),
|
|
create: (
|
|
tenantId: string,
|
|
body: {
|
|
organization_id?: string;
|
|
code: string;
|
|
name: string;
|
|
external_program_ref?: string;
|
|
status?: LifecycleStatus;
|
|
}
|
|
) =>
|
|
request<LoyaltyIntegration>("/api/v1/loyalty-integrations", {
|
|
tenantId,
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
},
|
|
|
|
communicationIntegrations: {
|
|
list: (tenantId: string, page = 1, pageSize = 50) =>
|
|
request<CommunicationIntegration[]>(
|
|
listPath("/api/v1/communication-integrations", { page, pageSize }),
|
|
{ tenantId }
|
|
),
|
|
get: (tenantId: string, id: string) =>
|
|
request<CommunicationIntegration>(`/api/v1/communication-integrations/${id}`, { tenantId }),
|
|
create: (
|
|
tenantId: string,
|
|
body: {
|
|
organization_id?: string;
|
|
code: string;
|
|
name: string;
|
|
status?: LifecycleStatus;
|
|
}
|
|
) =>
|
|
request<CommunicationIntegration>("/api/v1/communication-integrations", {
|
|
tenantId,
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
},
|
|
|
|
integrationDispatches: {
|
|
list: (tenantId: string, page = 1, pageSize = 50) =>
|
|
request<IntegrationDispatch[]>(
|
|
listPath("/api/v1/integration-dispatches", { page, pageSize }),
|
|
{ tenantId }
|
|
),
|
|
get: (tenantId: string, id: string) =>
|
|
request<IntegrationDispatch>(`/api/v1/integration-dispatches/${id}`, { tenantId }),
|
|
create: (
|
|
tenantId: string,
|
|
body: {
|
|
organization_id?: string;
|
|
integration_kind: string;
|
|
config_id?: string;
|
|
event_type: string;
|
|
payload_ref?: string;
|
|
}
|
|
) =>
|
|
request<IntegrationDispatch>("/api/v1/integration-dispatches", {
|
|
tenantId,
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
},
|
|
};
|