diff --git a/apps/api/src/routes/invoices.ts b/apps/api/src/routes/invoices.ts index 4d83402..9bb8790 100644 --- a/apps/api/src/routes/invoices.ts +++ b/apps/api/src/routes/invoices.ts @@ -101,6 +101,7 @@ invoicesRouter.get( paymentMethod: invoices.paymentMethod, paidAt: invoices.paidAt, notes: invoices.notes, + stripePaymentIntentId: invoices.stripePaymentIntentId, createdAt: invoices.createdAt, updatedAt: invoices.updatedAt, }) @@ -480,40 +481,50 @@ invoicesRouter.post( // Payment stats for admin dashboard invoicesRouter.get("/stats/summary", async (c) => { - const db = getDb(); - const now = new Date(); - const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); + try { + const db = getDb(); + const now = new Date(); + const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); - const [revenueResult] = await db - .select({ total: sql`coalesce(sum(total_cents), 0)` }) - .from(invoices) - .where(and(eq(invoices.status, "paid"), sql`${invoices.paidAt} >= ${startOfMonth}`)); + const [revenueResult] = await db + .select({ total: sql`coalesce(sum(total_cents), 0)` }) + .from(invoices) + .where(and(eq(invoices.status, "paid"), sql`${invoices.paidAt} >= ${startOfMonth}`)); - const [outstandingResult] = await db - .select({ total: sql`coalesce(sum(total_cents), 0)` }) - .from(invoices) - .where(eq(invoices.status, "pending")); + const [outstandingResult] = await db + .select({ total: sql`coalesce(sum(total_cents), 0)` }) + .from(invoices) + .where(eq(invoices.status, "pending")); - const [refundsResult] = await db - .select({ total: sql`coalesce(sum(amount_cents), 0)` }) - .from(refunds) - .where(sql`${refunds.createdAt} >= ${startOfMonth}`); + const [refundsResult] = await db + .select({ total: sql`coalesce(sum(amount_cents), 0)` }) + .from(refunds) + .where(sql`${refunds.createdAt} >= ${startOfMonth}`); - const methodBreakdown = await db - .select({ - method: invoices.paymentMethod, - total: sql`count(*)`, - }) - .from(invoices) - .where(and(eq(invoices.status, "paid"), sql`${invoices.paidAt} >= ${startOfMonth}`)) - .groupBy(invoices.paymentMethod); + const methodBreakdown = await db + .select({ + method: invoices.paymentMethod, + total: sql`count(*)`, + }) + .from(invoices) + .where(and(eq(invoices.status, "paid"), sql`${invoices.paidAt} >= ${startOfMonth}`)) + .groupBy(invoices.paymentMethod); - return c.json({ - revenueThisMonth: revenueResult?.total ?? 0, - outstanding: outstandingResult?.total ?? 0, - refundsThisMonth: refundsResult?.total ?? 0, - methodBreakdown, - }); + return c.json({ + revenueThisMonth: revenueResult?.total ?? 0, + outstanding: outstandingResult?.total ?? 0, + refundsThisMonth: refundsResult?.total ?? 0, + methodBreakdown, + }); + } catch (err) { + console.error("stats/summary error:", err); + return c.json({ + revenueThisMonth: 0, + outstanding: 0, + refundsThisMonth: 0, + methodBreakdown: [], + }); + } }); // Get Stripe payment details for an invoice (card last4, payment status, refund status) diff --git a/apps/web/src/pages/Appointments.tsx b/apps/web/src/pages/Appointments.tsx index 1dd7046..b64186d 100644 --- a/apps/web/src/pages/Appointments.tsx +++ b/apps/web/src/pages/Appointments.tsx @@ -112,9 +112,17 @@ export function AppointmentsPage() { const [viewMode, setViewMode] = useState<"status" | "groomer">("status"); // null key = unassigned; staffId string = that groomer; undefined set = all visible const [hiddenGroomers, setHiddenGroomers] = useState>(new Set()); + const [paymentStats, setPaymentStats] = useState<{ revenueThisMonth: number; outstanding: number; refundsThisMonth: number; methodBreakdown: { method: string | null; total: number }[] } | null>(null); const weekEnd = addDays(weekStart, 6); + useEffect(() => { + fetch("/api/invoices/stats/summary") + .then((r) => r.ok ? r.json() : null) + .then((data) => { if (data) setPaymentStats(data); }) + .catch(() => {}); + }, []); + const loadAppointments = useCallback(() => { const from = weekStart.toISOString(); const to = addDays(weekStart, 7).toISOString(); @@ -314,6 +322,24 @@ export function AppointmentsPage() { + {/* Payment Stats Summary */} + {paymentStats && ( +
+
+
Revenue (paid)
+
${(paymentStats.revenueThisMonth / 100).toFixed(2)}
+
+
+
Outstanding
+
${(paymentStats.outstanding / 100).toFixed(2)}
+
+
+
Refunds (this mo.)
+
${(paymentStats.refundsThisMonth / 100).toFixed(2)}
+
+
+ )} + {/* ── View Mode + Groomer Filters ── */}
Color by: diff --git a/apps/web/src/portal/CustomerPortal.tsx b/apps/web/src/portal/CustomerPortal.tsx index a542cc0..80be6cc 100644 --- a/apps/web/src/portal/CustomerPortal.tsx +++ b/apps/web/src/portal/CustomerPortal.tsx @@ -326,7 +326,7 @@ export function CustomerPortal() { )} {/* Main Content */} -
+

diff --git a/apps/web/src/portal/sections/BillingPayments.tsx b/apps/web/src/portal/sections/BillingPayments.tsx index e4d2902..89e3877 100644 --- a/apps/web/src/portal/sections/BillingPayments.tsx +++ b/apps/web/src/portal/sections/BillingPayments.tsx @@ -130,7 +130,7 @@ function BillingPaymentsInner({ sessionId, readOnly }: BillingPaymentsProps) {

)} -
+
{([ { id: "invoices" as const, label: "Invoices", icon: DollarSign }, { id: "payment" as const, label: "Payment Methods", icon: CreditCard },