Files
AgendaPro Dev d796538fd9 feat: rebrand AgendaPro -> AgendaMax + calendar fixes + current-week demo seed
- Rebrand all user-facing text to AgendaMax
- Fix sidebar scrolling: card container no longer overflows html (flex h-full min-h-0)
- Fix calendar toolbar hover: active buttons keep readable contrast
- Sticky calendar toolbar (month/week/day always visible on scroll)
- Seed guarantees 2-4 appointments per day for current week (Mon-Sat)
2026-07-27 10:11:16 -06:00

112 lines
3.7 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";
import type { ClientStats } from "../../shared/types.ts";
export const clientsRouter = Router();
function statsFor(clientId: number): ClientStats {
const agg = db
.prepare(
`SELECT
COUNT(*) c,
COALESCE(SUM(amount+tip),0) s,
MAX(created_at) last,
COALESCE(SUM(CASE WHEN amount > 0 THEN 1 ELSE 0 END)*1.0 / NULLIF(COUNT(*),0),0) ar
FROM tickets WHERE client_id = ?`
)
.get(clientId) as { c: number; s: number; last: string | null; ar: number };
const noShow = db
.prepare(`SELECT COUNT(*) c FROM appointments WHERE client_id = ? AND status = 'no_show'`)
.get(clientId) as { c: number };
return {
visits: agg.c,
total_spent: Math.round(agg.s * 100) / 100,
last_visit: agg.last,
avg_ticket: agg.c > 0 ? Math.round((agg.s / agg.c) * 100) / 100 : 0,
no_show_count: noShow.c,
};
}
clientsRouter.get("/", (req: AuthedRequest, res) => {
const q = (req.query.q as string | undefined)?.trim();
let rows: any[];
if (q) {
const like = `%${q.replace(/[%_]/g, (m) => "\\" + m)}%`;
rows = db
.prepare(
`SELECT * FROM clients
WHERE business_id = ? AND (name LIKE ? ESCAPE '\\' OR phone LIKE ? ESCAPE '\\' OR email LIKE ? ESCAPE '\\')
ORDER BY name LIMIT 50`
)
.all(req.user!.business_id, like, like, like);
} else {
rows = db
.prepare(`SELECT * FROM clients WHERE business_id = ? ORDER BY name LIMIT 50`)
.all(req.user!.business_id);
}
rows.forEach((r) => (r.stats = statsFor(r.id)));
res.json({ clients: rows });
});
clientsRouter.get("/:id", (req: AuthedRequest, res) => {
const c = db
.prepare(`SELECT * FROM clients WHERE id = ? AND business_id = ?`)
.get(Number(req.params.id), req.user!.business_id) as any;
if (!c) return err(res, 404, "Cliente no encontrado");
c.stats = statsFor(c.id);
res.json({ client: c });
});
clientsRouter.post("/", (req: AuthedRequest, res) => {
const { name, email, phone, notes, tags } = req.body ?? {};
if (!name) return err(res, 400, "El nombre es obligatorio");
const r = db
.prepare(
`INSERT INTO clients (business_id, name, email, phone, notes, tags) VALUES (?, ?, ?, ?, ?, ?) RETURNING *`
)
.get(
req.user!.business_id,
name,
email || null,
phone || null,
notes || null,
tags || null
) as any;
r.stats = statsFor(r.id);
res.status(201).json({ client: r });
});
clientsRouter.patch("/:id", ownerOnly, (req: AuthedRequest, res) => {
const id = Number(req.params.id);
const existing = db
.prepare(`SELECT * FROM clients WHERE id = ? AND business_id = ?`)
.get(id, req.user!.business_id) as any;
if (!existing) return err(res, 404, "Cliente no encontrado");
const { name, email, phone, notes, tags } = req.body ?? {};
db.prepare(
`UPDATE clients SET name = ?, email = ?, phone = ?, notes = ?, tags = ? WHERE id = ?`
).run(
name ?? existing.name,
email ?? existing.email,
phone ?? existing.phone,
notes ?? existing.notes,
tags ?? existing.tags,
id
);
const updated = db.prepare(`SELECT * FROM clients WHERE id = ?`).get(id) as any;
updated.stats = statsFor(id);
res.json({ client: updated });
});
clientsRouter.delete("/:id", ownerOnly, (req: AuthedRequest, res) => {
const id = Number(req.params.id);
const existing = db
.prepare(`SELECT id FROM clients WHERE id = ? AND business_id = ?`)
.get(id, req.user!.business_id);
if (!existing) return err(res, 404, "Cliente no encontrado");
db.prepare(`DELETE FROM clients WHERE id = ?`).run(id);
res.json({ ok: true });
});