import { Router } from "express"; import { db } from "../db.ts"; import { adminOnly, err, type AuthedRequest } from "../lib/auth.ts"; import { TEMPLATES, getTemplate, type TemplateDef } from "../lib/templates.ts"; import { seedBusiness, clearBusinessData } from "../scripts/seed.ts"; export const adminRouter = Router(); adminRouter.use(adminOnly); function businessStats(businessId: number) { const scalar = (sql: string) => (db.prepare(sql).get(businessId) as any)?.v ?? 0; return { users: scalar(`SELECT COUNT(*) v FROM users WHERE business_id = ?`), employees: scalar(`SELECT COUNT(*) v FROM employees WHERE business_id = ?`), services: scalar(`SELECT COUNT(*) v FROM services WHERE business_id = ?`), clients: scalar(`SELECT COUNT(*) v FROM clients WHERE business_id = ?`), appointments: scalar(`SELECT COUNT(*) v FROM appointments WHERE business_id = ?`), appointments_upcoming: scalar( `SELECT COUNT(*) v FROM appointments WHERE business_id = ? AND status='scheduled' AND start_at >= datetime('now')` ), revenue_30d: scalar( `SELECT COALESCE(SUM(amount+tip),0) v FROM tickets WHERE business_id = ? AND created_at >= datetime('now','-30 days')` ), tickets: scalar(`SELECT COUNT(*) v FROM tickets WHERE business_id = ?`), }; } function businessRow(id: number) { return db .prepare( `SELECT id, name, industry, currency, currency_symbol, phone, address, slug, plan, status, template, trial_ends_at, created_at FROM businesses WHERE id = ?` ) .get(id) as any; } // Platform overview adminRouter.get("/overview", (_req: AuthedRequest, res) => { const scalar = (sql: string) => (db.prepare(sql).get() as any)?.v ?? 0; res.json({ businesses: scalar(`SELECT COUNT(*) v FROM businesses`), active_businesses: scalar(`SELECT COUNT(*) v FROM businesses WHERE status='active'`), trial_businesses: scalar(`SELECT COUNT(*) v FROM businesses WHERE plan='trial'`), total_users: scalar(`SELECT COUNT(*) v FROM users WHERE role IN ('owner','employee')`), total_appointments: scalar(`SELECT COUNT(*) v FROM appointments`), revenue_30d: scalar(`SELECT COALESCE(SUM(amount+tip),0) v FROM tickets WHERE created_at >= datetime('now','-30 days')`), upcoming_appointments: scalar( `SELECT COUNT(*) v FROM appointments WHERE status='scheduled' AND start_at >= datetime('now')` ), templates: TEMPLATES.length, }); }); // List templates adminRouter.get("/templates", (_req: AuthedRequest, res) => { const out = TEMPLATES.map((t) => ({ key: t.key, name: t.name, industry: t.industry, description: t.description, currency: t.currency, currency_symbol: t.currency_symbol, employees: t.employees.length, services: t.services.length, clients: t.clients.length, })); res.json({ templates: out }); }); // List all businesses with stats adminRouter.get("/businesses", (_req: AuthedRequest, res) => { const rows = db .prepare( `SELECT id, name, industry, currency, currency_symbol, phone, address, slug, plan, status, template, trial_ends_at, created_at FROM businesses ORDER BY created_at DESC` ) .all() as any[]; const out = rows.map((b) => ({ business: b, stats: businessStats(b.id) })); res.json({ businesses: out }); }); // Business detail adminRouter.get("/businesses/:id", (req: AuthedRequest, res) => { const id = Number(req.params.id); const b = businessRow(id); if (!b) return err(res, 404, "Negocio no encontrado"); res.json({ business: b, stats: businessStats(id) }); }); // Create business + owner + template seed adminRouter.post("/businesses", (req: AuthedRequest, res) => { const { name, industry, template, ownerName, ownerEmail, ownerPassword, plan, currency, currency_symbol } = req.body ?? {}; if (!name) return err(res, 400, "El nombre del negocio es obligatorio"); if (!ownerEmail) return err(res, 400, "El correo del dueño es obligatorio"); const tpl: TemplateDef | undefined = template ? getTemplate(template) : getTemplate("blank"); if (!tpl) return err(res, 400, "Plantilla no válida"); const email = String(ownerEmail).toLowerCase().trim(); const existing = db.prepare(`SELECT id FROM users WHERE email = ?`).get(email); if (existing) return err(res, 409, "Ya existe un usuario con ese correo"); const biz = db .prepare( `INSERT INTO businesses (name, industry, currency, currency_symbol, plan, status, template) VALUES (?, ?, ?, ?, ?, 'active', ?) RETURNING id` ) .get( name, industry || tpl.industry, currency || tpl.currency || "MXN", currency_symbol || tpl.currency_symbol || "$", plan || "trial", tpl.key ) as { id: number }; const result = seedBusiness({ businessId: biz.id, template: tpl, ownerEmail: email, ownerName: ownerName || "Dueño", ownerPassword: ownerPassword || "demo1234", withHistory: tpl.key !== "blank", withUpcoming: tpl.key !== "blank", }); res.status(201).json({ business: businessRow(biz.id), stats: businessStats(biz.id), seeded: result, message: `Negocio "${name}" creado con plantilla "${tpl.name}"`, }); }); // Update business adminRouter.patch("/businesses/:id", (req: AuthedRequest, res) => { const id = Number(req.params.id); const existing = businessRow(id); if (!existing) return err(res, 404, "Negocio no encontrado"); const { name, industry, phone, address, slug, plan, status, trial_ends_at } = req.body ?? {}; db.prepare( `UPDATE businesses SET name=?, industry=?, phone=?, address=?, slug=?, plan=?, status=?, trial_ends_at=? WHERE id=?` ).run( name ?? existing.name, industry ?? existing.industry, phone ?? existing.phone, address ?? existing.address, slug ?? existing.slug, plan ?? existing.plan, status ?? existing.status, trial_ends_at ?? existing.trial_ends_at, id ); res.json({ business: businessRow(id), stats: businessStats(id) }); }); // Delete business + all its data adminRouter.delete("/businesses/:id", (req: AuthedRequest, res) => { const id = Number(req.params.id); const existing = businessRow(id); if (!existing) return err(res, 404, "Negocio no encontrado"); clearBusinessData(id); // clearBusinessData preserves user accounts; a full delete removes them too db.prepare(`DELETE FROM users WHERE business_id = ?`).run(id); db.prepare(`DELETE FROM businesses WHERE id = ?`).run(id); res.json({ ok: true }); }); // Load template data into an existing business (clearFirst optional) adminRouter.post("/businesses/:id/seed-template", (req: AuthedRequest, res) => { const id = Number(req.params.id); const b = businessRow(id); if (!b) return err(res, 404, "Negocio no encontrado"); const { template, clearFirst = true, ownerEmail, ownerName, ownerPassword, withHistory = true, withUpcoming = true } = req.body ?? {}; const tpl: TemplateDef | undefined = template ? getTemplate(template) : b.template ? getTemplate(b.template) : getTemplate("blank"); if (!tpl) return err(res, 400, "Plantilla no válida"); const result = seedBusiness({ businessId: id, template: tpl, ownerEmail: ownerEmail ?? (db.prepare(`SELECT email FROM users WHERE business_id=? AND role='owner'`).get(id) as any)?.email, ownerName: ownerName ?? "Dueño", ownerPassword: ownerPassword ?? "demo1234", clearFirst: clearFirst !== false, withHistory, withUpcoming, }); res.json({ business: businessRow(id), stats: businessStats(id), seeded: result }); }); // Reset demo data (clear + reseed with current template) adminRouter.post("/businesses/:id/reset-demo", (req: AuthedRequest, res) => { const id = Number(req.params.id); const b = businessRow(id); if (!b) return err(res, 404, "Negocio no encontrado"); const tplKey = req.body?.template || b.template || "blank"; const tpl = getTemplate(tplKey); if (!tpl) return err(res, 400, "Plantilla no válida"); const owner = db.prepare(`SELECT email, name FROM users WHERE business_id=? AND role='owner'`).get(id) as any; const result = seedBusiness({ businessId: id, template: tpl, ownerEmail: owner?.email, ownerName: owner?.name ?? "Dueño", clearFirst: true, withHistory: tpl.key !== "blank", withUpcoming: tpl.key !== "blank", }); res.json({ business: businessRow(id), stats: businessStats(id), seeded: result }); });