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:
AgendaPro Dev
2026-07-25 13:45:53 -06:00
commit e8d2435cc2
63 changed files with 16539 additions and 0 deletions
+96
View File
@@ -0,0 +1,96 @@
import { Loader2 } from "lucide-react";
export function Spinner({ className = "" }: { className?: string }) {
return <Loader2 className={`h-4 w-4 animate-spin ${className}`} />;
}
export function EmptyState({
icon: Icon,
title,
description,
action,
}: {
icon?: React.ComponentType<{ className?: string }>;
title: string;
description?: string;
action?: React.ReactNode;
}) {
return (
<div className="flex flex-col items-center justify-center gap-3 px-6 py-12 text-center">
{Icon && (
<div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-slate-100 text-slate-400">
<Icon className="h-6 w-6" />
</div>
)}
<div>
<div className="text-sm font-bold text-slate-900">{title}</div>
{description && <p className="mt-1 text-sm text-slate-500">{description}</p>}
</div>
{action}
</div>
);
}
export function PageHeader({
title,
subtitle,
actions,
}: {
title: string;
subtitle?: string;
actions?: React.ReactNode;
}) {
return (
<div className="flex flex-col gap-3 px-5 pt-5 sm:px-7 sm:pt-7 md:flex-row md:items-end md:justify-between">
<div>
<h1 className="text-xl font-extrabold tracking-tight text-slate-900 sm:text-2xl">{title}</h1>
{subtitle && <p className="mt-1 text-sm text-slate-500">{subtitle}</p>}
</div>
{actions && <div className="flex flex-wrap items-center gap-2">{actions}</div>}
</div>
);
}
export function Badge({
children,
bg,
fg,
dot,
}: {
children: React.ReactNode;
bg?: string;
fg?: string;
dot?: string;
}) {
return (
<span className="chip" style={{ background: bg ?? "#f3f4f6", color: fg ?? "#374151" }}>
{dot && <span className="h-1.5 w-1.5 rounded-full" style={{ background: dot }} />}
{children}
</span>
);
}
export function Avatar({
name,
color,
size = 36,
}: {
name: string;
color?: string;
size?: number;
}) {
const init = name
.split(" ")
.filter(Boolean)
.slice(0, 2)
.map((p) => p[0]?.toUpperCase())
.join("");
return (
<div
className="flex shrink-0 items-center justify-center rounded-full text-xs font-bold text-white"
style={{ background: color ?? "#3b66ff", width: size, height: size, fontSize: size * 0.35 }}
>
{init}
</div>
);
}