Files
AgendaPro/server/routes/me.ts
T
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

135 lines
4.6 KiB
TypeScript

import { Router } from "express";
import { db } from "../db.ts";
import type { AuthedRequest } from "../lib/auth.ts";
import { authRequired } from "../lib/auth.ts";
import { bizDayBoundsSqlite, bizDayBoundsIso, toIsoUtc } from "../lib/time.ts";
export const meRouter = Router();
meRouter.use(authRequired);
const DEFAULT_TZ = "America/Mexico_City";
/** Fetch the business timezone once per handler. */
function bizTz(bid: number | null): string {
const row = db.prepare(`SELECT timezone t FROM businesses WHERE id = ?`).get(bid) as { t?: string } | undefined;
return row?.t || DEFAULT_TZ;
}
const EMPTY_PERF = {
appointments_today: 0,
upcoming: 0,
revenue_30d: 0,
avg_rating: 0,
share_pct: 0,
next_appointment: null,
};
/** Employee self-service analytics, tz-correct via the business timezone. */
meRouter.get("/performance", (req: AuthedRequest, res) => {
const eid = req.user!.employee_id;
const bid = req.user!.business_id;
if (!eid) return res.json(EMPTY_PERF);
const tz = bizTz(bid);
// start_at is stored ISO-Z → pair with ISO bounds.
const todayIso = bizDayBoundsIso(tz, 0);
const rangeStartIso = bizDayBoundsIso(tz, -30).start;
const nowIso = toIsoUtc(new Date());
// created_at is stored in a mixed format → wrap with datetime() and use SQLite bounds.
const rangeStartSql = bizDayBoundsSqlite(tz, -30).start;
const ratingStartSql = bizDayBoundsSqlite(tz, -90).start;
const scalar = (sql: string, ...args: any[]) => (db.prepare(sql).get(...args) as any)?.v ?? 0;
const appointments_today = scalar(
`SELECT COUNT(*) v FROM appointments
WHERE employee_id = ? AND start_at >= ? AND start_at <= ? AND status != 'cancelled'`,
eid, todayIso.start, todayIso.end
);
const upcoming = scalar(
`SELECT COUNT(*) v FROM appointments
WHERE employee_id = ? AND status = 'scheduled' AND start_at >= ?`,
eid, nowIso
);
const revenue_30d = scalar(
`SELECT COALESCE(SUM(amount + tip), 0) v FROM tickets
WHERE employee_id = ? AND datetime(created_at) >= ?`,
eid, rangeStartSql
);
// Avg of last-90d reviews, falling back to the employee's stored rating.
const avg_rating = scalar(
`SELECT COALESCE(
(SELECT AVG(rating) FROM reviews WHERE employee_id = ? AND datetime(created_at) >= ?),
(SELECT rating FROM employees WHERE id = ?)
) v`,
eid, ratingStartSql, eid
);
const empAppts = scalar(
`SELECT COUNT(*) v FROM appointments WHERE employee_id = ? AND start_at >= ?`,
eid, rangeStartIso
);
const bizAppts = scalar(
`SELECT COUNT(*) v FROM appointments WHERE business_id = ? AND start_at >= ?`,
bid, rangeStartIso
);
const share_pct = bizAppts > 0 ? Math.round((empAppts / bizAppts) * 100) : 0;
const next = db
.prepare(
`SELECT a.start_at, s.name service_name, c.name client_name
FROM appointments a
JOIN services s ON s.id = a.service_id
JOIN clients c ON c.id = a.client_id
WHERE a.employee_id = ? AND a.status = 'scheduled' AND a.start_at >= ?
ORDER BY a.start_at ASC
LIMIT 1`
)
.get(eid, nowIso) as { start_at: string; service_name: string; client_name: string } | undefined;
res.json({
appointments_today,
upcoming,
revenue_30d: Math.round(revenue_30d * 100) / 100,
avg_rating: Math.round((avg_rating ?? 0) * 10) / 10,
share_pct,
next_appointment: next
? { start_at: next.start_at, service_name: next.service_name, client_name: next.client_name }
: null,
});
});
/** Per-ticket commissions for the signed-in employee, last 30 business-local days. */
meRouter.get("/commissions", (req: AuthedRequest, res) => {
const eid = req.user!.employee_id;
if (!eid) return res.json({ rows: [], total: 0 });
const bid = req.user!.business_id;
const tz = bizTz(bid);
const rangeStartSql = bizDayBoundsSqlite(tz, -30).start;
const rows = db
.prepare(
`SELECT t.appointment_id, s.name service_name, c.name client_name,
t.amount, t.commission, t.created_at
FROM tickets t
JOIN services s ON s.id = t.service_id
JOIN clients c ON c.id = t.client_id
WHERE t.employee_id = ? AND datetime(t.created_at) >= ?
ORDER BY datetime(t.created_at) DESC
LIMIT 100`
)
.all(eid, rangeStartSql) as any[];
const out = rows.map((r) => ({
appointment_id: r.appointment_id,
service_name: r.service_name,
client_name: r.client_name,
amount: Math.round(Number(r.amount) * 100) / 100,
commission: Math.round(Number(r.commission) * 100) / 100,
created_at: r.created_at,
}));
const total = Math.round(out.reduce((a, r) => a + r.commission, 0) * 100) / 100;
res.json({ rows: out, total });
});