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,94 @@
|
||||
import express from "express";
|
||||
import cors from "cors";
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { db } from "./db.ts";
|
||||
import { ensurePlatformAdmin, seedBusiness, getTemplate, DEFAULT_TEMPLATE_KEY } from "./scripts/seed.ts";
|
||||
import { authRouter } from "./routes/auth.ts";
|
||||
import { businessRouter } from "./routes/business.ts";
|
||||
import { employeesRouter } from "./routes/employees.ts";
|
||||
import { servicesRouter } from "./routes/services.ts";
|
||||
import { clientsRouter } from "./routes/clients.ts";
|
||||
import { appointmentsRouter } from "./routes/appointments.ts";
|
||||
import { dashboardRouter } from "./routes/dashboard.ts";
|
||||
import { adminRouter } from "./routes/admin.ts";
|
||||
import { settingsRouter } from "./routes/settings.ts";
|
||||
import { cashRouter } from "./routes/cash.ts";
|
||||
import { notificationsRouter, scheduleReminders } from "./routes/notifications.ts";
|
||||
import { bookingRouter } from "./routes/booking.ts";
|
||||
import { authRequired } from "./lib/auth.ts";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = path.resolve(__dirname, "..");
|
||||
|
||||
// Auto-seed demo data if the database is empty (first run). Preserves existing data.
|
||||
function ensureSeed() {
|
||||
ensurePlatformAdmin();
|
||||
const row = db.prepare(`SELECT COUNT(*) c FROM businesses`).get() as { c: number };
|
||||
if (row.c === 0) {
|
||||
console.log("[seed] Base de datos vacía — creando negocio demo y admin…");
|
||||
const biz = db
|
||||
.prepare(
|
||||
`INSERT INTO businesses (name, industry, currency, currency_symbol, phone, address, plan, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 'trial', 'active') RETURNING id`
|
||||
)
|
||||
.get("Lumière Estética & Spa", "Estética y Spa", "MXN", "$", "+52 55 1234 5678", "Av. Reforma 245, CDMX") as { id: number };
|
||||
seedBusiness({
|
||||
businessId: biz.id,
|
||||
template: getTemplate(DEFAULT_TEMPLATE_KEY)!,
|
||||
ownerEmail: "[email protected]",
|
||||
ownerName: "Daniela Reyes",
|
||||
});
|
||||
}
|
||||
}
|
||||
ensureSeed();
|
||||
// On startup, refresh pending reminders for all active businesses (idempotent).
|
||||
try {
|
||||
const bids = db.prepare(`SELECT id FROM businesses WHERE status = 'active'`).all() as { id: number }[];
|
||||
for (const b of bids) scheduleReminders(b.id);
|
||||
} catch (e) {
|
||||
console.warn("[reminders] no se pudieron generar al inicio:", (e as Error).message);
|
||||
}
|
||||
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
app.use(express.json({ limit: "1mb" }));
|
||||
|
||||
app.use("/api/auth", authRouter);
|
||||
// Public booking (no auth)
|
||||
app.use("/api/public", bookingRouter);
|
||||
// Protected APIs
|
||||
app.use("/api/business", authRequired, businessRouter);
|
||||
app.use("/api/employees", authRequired, employeesRouter);
|
||||
app.use("/api/services", authRequired, servicesRouter);
|
||||
app.use("/api/clients", authRequired, clientsRouter);
|
||||
app.use("/api/appointments", authRequired, appointmentsRouter);
|
||||
app.use("/api/dashboard", authRequired, dashboardRouter);
|
||||
app.use("/api/admin", authRequired, adminRouter);
|
||||
app.use("/api/settings", authRequired, settingsRouter);
|
||||
app.use("/api/cash", authRequired, cashRouter);
|
||||
app.use("/api/notifications", authRequired, notificationsRouter);
|
||||
|
||||
app.get("/api/health", (_req, res) => res.json({ ok: true, ts: Date.now() }));
|
||||
|
||||
// Serve built frontend in production
|
||||
const distDir = path.join(ROOT, "dist");
|
||||
if (fs.existsSync(distDir)) {
|
||||
app.use(express.static(distDir));
|
||||
app.get("*", (req, res, next) => {
|
||||
if (req.path.startsWith("/api/")) return next();
|
||||
res.sendFile(path.join(distDir, "index.html"));
|
||||
});
|
||||
}
|
||||
|
||||
app.use((err: any, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
|
||||
console.error("[server error]", err);
|
||||
res.status(err.status || 500).json({ error: err.message || "Error interno" });
|
||||
});
|
||||
|
||||
const port = Number(process.env.PORT) || 3000;
|
||||
const host = process.env.HOST || "0.0.0.0";
|
||||
app.listen(port, host, () => {
|
||||
console.log(`AgendaPro API en http://${host}:${port}`);
|
||||
});
|
||||
Reference in New Issue
Block a user