AgendaPro v1.0 - multi-tenant SaaS, mobile calendar, public booking
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.
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
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),
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user