Multi-tenant scheduling SaaS (AgendaPro-equivalent): - Backend: Node + Express + node:sqlite, multi-tenant (admin/owner/employee), versioned migrations, 4 templates (estetica-spa, barberia, clinica, blank). - Frontend: React 18 + TS + Vite + Tailwind, FullCalendar drag-and-drop, Recharts dashboard, mobile-first (day/list views on mobile). - Features: calendar+services+employees+clients+tickets, dashboard with best employee/service, top tickets, top/frequent clients, commissions, cancellation policy + no-show tracking, public online booking (/b/:slug), cash register (cierre de caja), reminders/notifications center. - Verification: 65/65 e2e API, 58/58 visual (0 overflow, 0 console errors), typecheck clean, vite build OK.
120 lines
2.6 KiB
TypeScript
120 lines
2.6 KiB
TypeScript
const BASE = "/api/public";
|
|
|
|
export interface PublicBusiness {
|
|
id: number;
|
|
name: string;
|
|
industry: string;
|
|
currency_symbol: string;
|
|
phone: string | null;
|
|
address: string | null;
|
|
booking_enabled: boolean;
|
|
cancel_window_hours: number;
|
|
cancel_penalty_pct: number;
|
|
require_deposit: boolean;
|
|
deposit_pct: number;
|
|
}
|
|
|
|
export interface PublicService {
|
|
id: number;
|
|
name: string;
|
|
description: string | null;
|
|
category: string;
|
|
duration_min: number;
|
|
price: number;
|
|
color: string;
|
|
}
|
|
|
|
export interface PublicEmployee {
|
|
id: number;
|
|
name: string;
|
|
role: string;
|
|
color: string;
|
|
}
|
|
|
|
export interface PublicBusinessResponse {
|
|
business: PublicBusiness;
|
|
services: PublicService[];
|
|
employees: PublicEmployee[];
|
|
}
|
|
|
|
export interface PublicSlot {
|
|
time: string;
|
|
iso: string;
|
|
employee_id: number;
|
|
employee_name: string;
|
|
}
|
|
|
|
export interface PublicSlotsResponse {
|
|
slots: PublicSlot[];
|
|
service: Pick<PublicService, "id" | "name" | "price" | "duration_min">;
|
|
}
|
|
|
|
export interface BookClient {
|
|
name: string;
|
|
email?: string;
|
|
phone?: string;
|
|
notes?: string;
|
|
}
|
|
|
|
export interface BookPayload {
|
|
service_id: number;
|
|
employee_id: number;
|
|
start_at: string;
|
|
client: BookClient;
|
|
}
|
|
|
|
export interface BookResponse {
|
|
appointment: {
|
|
id: number;
|
|
start_at: string;
|
|
price: number;
|
|
service_name: string;
|
|
employee_name: string;
|
|
};
|
|
business: { name: string; currency_symbol: string };
|
|
client_created: boolean;
|
|
}
|
|
|
|
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
|
const res = await fetch(`${BASE}${path}`, {
|
|
...init,
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
...(init?.headers as Record<string, string> | undefined),
|
|
},
|
|
});
|
|
if (!res.ok) {
|
|
let message = `Error ${res.status}`;
|
|
try {
|
|
const body = await res.json();
|
|
if (body?.error) message = body.error;
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
throw new Error(message);
|
|
}
|
|
return (await res.json()) as T;
|
|
}
|
|
|
|
export function getBusiness(slug: string): Promise<PublicBusinessResponse> {
|
|
return request<PublicBusinessResponse>(`/${slug}`);
|
|
}
|
|
|
|
export function getSlots(
|
|
slug: string,
|
|
serviceId: number,
|
|
date: string,
|
|
employeeId?: number
|
|
): Promise<PublicSlotsResponse> {
|
|
const q = new URLSearchParams({ service_id: String(serviceId), date });
|
|
if (employeeId) q.set("employee_id", String(employeeId));
|
|
return request<PublicSlotsResponse>(`/${slug}/slots?${q.toString()}`);
|
|
}
|
|
|
|
export function book(slug: string, payload: BookPayload): Promise<BookResponse> {
|
|
return request<BookResponse>(`/${slug}/book`, {
|
|
method: "POST",
|
|
body: JSON.stringify(payload),
|
|
});
|
|
}
|