- 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)
112 lines
3.7 KiB
TypeScript
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 });
|
|
});
|