fix(reports): add error handler and improve error messages for diagnosis #51

Merged
ghost merged 2 commits from fix/reports-error-handling into main 2026-03-18 13:36:32 +00:00
2 changed files with 24 additions and 3 deletions
+7 -1
View File
@@ -16,6 +16,11 @@ import {
export const reportsRouter = new Hono(); export const reportsRouter = new Hono();
reportsRouter.onError((err, c) => {
console.error("[reports] unhandled error:", err);
return c.json({ error: "Internal server error", message: err.message }, 500);
});
// ─── Helpers ────────────────────────────────────────────────────────────────── // ─── Helpers ──────────────────────────────────────────────────────────────────
function parseDate(value: string | undefined, fallback: Date): Date { function parseDate(value: string | undefined, fallback: Date): Date {
@@ -279,6 +284,7 @@ reportsRouter.get("/clients", async (c) => {
// Clients with no appointment in last 90 days (churn risk) // Clients with no appointment in last 90 days (churn risk)
const ninetyDaysAgo = new Date(); const ninetyDaysAgo = new Date();
ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90); ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90);
const ninetyDaysAgoISO = ninetyDaysAgo.toISOString();
const churnRisk = await db const churnRisk = await db
.select({ .select({
@@ -290,7 +296,7 @@ reportsRouter.get("/clients", async (c) => {
.leftJoin(appointments, eq(appointments.clientId, clients.id)) .leftJoin(appointments, eq(appointments.clientId, clients.id))
.groupBy(clients.id, clients.name) .groupBy(clients.id, clients.name)
.having( .having(
sql`MAX(${appointments.startTime}) < ${ninetyDaysAgo} OR MAX(${appointments.startTime}) IS NULL` sql`MAX(${appointments.startTime}) < ${ninetyDaysAgoISO}::timestamptz OR MAX(${appointments.startTime}) IS NULL`
) )
.orderBy(sql`MAX(${appointments.startTime}) ASC NULLS FIRST`); .orderBy(sql`MAX(${appointments.startTime}) ASC NULLS FIRST`);
+17 -2
View File
@@ -176,8 +176,23 @@ export function ReportsPage() {
fetch(`/api/reports/clients?${qs}`), fetch(`/api/reports/clients?${qs}`),
]); ]);
if (!summRes.ok || !revRes.ok || !apptRes.ok || !svcRes.ok || !clientRes.ok) { const failures = [
throw new Error("Failed to load report data"); ["summary", summRes],
["revenue", revRes],
["appointments", apptRes],
["services", svcRes],
["clients", clientRes],
].filter(([, r]) => !(r as Response).ok);
if (failures.length > 0) {
const details = await Promise.all(
failures.map(async ([name, r]) => {
const res = r as Response;
let body = "";
try { body = await res.text(); } catch { /* ignore */ }
return `${name} (HTTP ${res.status}${body ? `: ${body.slice(0, 120)}` : ""})`;
})
);
throw new Error(`Failed to load report data — ${details.join(", ")}`);
} }
const [summData, revData, apptData, svcData, clientData] = await Promise.all([ const [summData, revData, apptData, svcData, clientData] = await Promise.all([