Files
AgendaPro Dev e8d2435cc2 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.
2026-07-25 13:45:53 -06:00

114 lines
4.0 KiB
TypeScript

import { Router } from "express";
import { db } from "../db.ts";
import type { AuthedRequest } from "../lib/auth.ts";
import { err, ownerOnly } from "../lib/auth.ts";
export const servicesRouter = Router();
function attachEmployees(svc: any) {
const ids = db
.prepare(`SELECT employee_id FROM employee_services WHERE service_id = ?`)
.all(svc.id)
.map((r: any) => r.employee_id);
svc.employee_ids = ids;
return svc;
}
servicesRouter.get("/", (req: AuthedRequest, res) => {
const { employee_id, active } = req.query;
let rows: any[];
if (employee_id) {
rows = db
.prepare(
`SELECT s.* FROM services s
JOIN employee_services es ON es.service_id = s.id
WHERE s.business_id = ? AND es.employee_id = ?
ORDER BY s.category, s.name`
)
.all(req.user!.business_id, Number(employee_id));
} else {
rows = db
.prepare(
`SELECT * FROM services WHERE business_id = ? ORDER BY category, name`
)
.all(req.user!.business_id);
}
if (active === "true") rows = rows.filter((r) => r.active === 1);
rows.forEach(attachEmployees);
res.json({ services: rows });
});
servicesRouter.post("/", ownerOnly, (req: AuthedRequest, res) => {
const { name, category, description, duration_min, price, color, employee_ids, active } = req.body ?? {};
if (!name) return err(res, 400, "El nombre es obligatorio");
const r = db
.prepare(
`INSERT INTO services (business_id, name, category, description, duration_min, price, color, active)
VALUES (?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`
)
.get(
req.user!.business_id,
name,
category || "General",
description || null,
Number(duration_min) || 60,
Number(price) || 0,
color || "#3b66ff",
active === false ? 0 : 1
) as any;
if (Array.isArray(employee_ids)) {
const ins = db.prepare(`INSERT OR IGNORE INTO employee_services (employee_id, service_id) VALUES (?, ?)`);
for (const eid of employee_ids) ins.run(Number(eid), r.id);
}
attachEmployees(r);
res.status(201).json({ service: r });
});
servicesRouter.patch("/:id", ownerOnly, (req: AuthedRequest, res) => {
const id = Number(req.params.id);
const existing = db
.prepare(`SELECT * FROM services WHERE id = ? AND business_id = ?`)
.get(id, req.user!.business_id) as any;
if (!existing) return err(res, 404, "Servicio no encontrado");
const { name, category, description, duration_min, price, color, active, employee_ids } = req.body ?? {};
const merged = {
name: name ?? existing.name,
category: category ?? existing.category,
description: description ?? existing.description,
duration_min: duration_min === undefined ? existing.duration_min : Number(duration_min),
price: price === undefined ? existing.price : Number(price),
color: color ?? existing.color,
active: active === undefined ? existing.active : active ? 1 : 0,
};
db.prepare(
`UPDATE services SET name = ?, category = ?, description = ?, duration_min = ?, price = ?, color = ?, active = ? WHERE id = ?`
).run(
merged.name,
merged.category,
merged.description,
merged.duration_min,
merged.price,
merged.color,
merged.active,
id
);
if (Array.isArray(employee_ids)) {
db.prepare(`DELETE FROM employee_services WHERE service_id = ?`).run(id);
const ins = db.prepare(`INSERT OR IGNORE INTO employee_services (employee_id, service_id) VALUES (?, ?)`);
for (const eid of employee_ids) ins.run(Number(eid), id);
}
const updated = db.prepare(`SELECT * FROM services WHERE id = ?`).get(id) as any;
attachEmployees(updated);
res.json({ service: updated });
});
servicesRouter.delete("/:id", ownerOnly, (req: AuthedRequest, res) => {
const id = Number(req.params.id);
const existing = db
.prepare(`SELECT id FROM services WHERE id = ? AND business_id = ?`)
.get(id, req.user!.business_id);
if (!existing) return err(res, 404, "Servicio no encontrado");
db.prepare(`DELETE FROM services WHERE id = ?`).run(id);
res.json({ ok: true });
});