Files
AgendaPro/server/index.ts
T

97 lines
3.9 KiB
TypeScript

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 { meRouter } from "./routes/me.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/me", authRequired, meRouter);
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(`AgendaMax API en http://${host}:${port}`);
});