From 457656a2a9a525cac809ebb55de3735fbb96258d Mon Sep 17 00:00:00 2001 From: Simone Cavalli Date: Sat, 16 May 2026 12:49:21 +0200 Subject: [PATCH] feat: add analytics query helpers for admin financial reporting Provides getAnalyticsByYear, getMonthlyCollected, and getAvailableYears to power the admin dashboard analytics view (not yet wired to a page). Co-Authored-By: Claude Sonnet 4.6 --- src/lib/analytics-queries.ts | 71 ++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 src/lib/analytics-queries.ts diff --git a/src/lib/analytics-queries.ts b/src/lib/analytics-queries.ts new file mode 100644 index 0000000..d9381b8 --- /dev/null +++ b/src/lib/analytics-queries.ts @@ -0,0 +1,71 @@ +import { db } from "@/db"; +import { clients, payments } from "@/db/schema"; +import { sql, and, eq } from "drizzle-orm"; + +export async function getAnalyticsByYear(year: number) { + const [contracted] = await db + .select({ total: sql`coalesce(sum(${clients.accepted_total}::numeric), 0)` }) + .from(clients) + .where(sql`extract(year from ${clients.created_at}) = ${year}`); + + const [clientsRow] = await db + .select({ count: sql`count(*)` }) + .from(clients) + .where(sql`extract(year from ${clients.created_at}) = ${year}`); + + const [collected] = await db + .select({ total: sql`coalesce(sum(${payments.amount}::numeric), 0)` }) + .from(payments) + .where( + and( + eq(payments.status, "saldato"), + sql`${payments.paid_at} is not null and extract(year from ${payments.paid_at}) = ${year}` + ) + ); + + const [pending] = await db + .select({ total: sql`coalesce(sum(${payments.amount}::numeric), 0)` }) + .from(payments) + .where(sql`${payments.status} in ('da_saldare', 'inviata')`); + + return { + contracted: parseFloat(contracted?.total ?? "0"), + collected: parseFloat(collected?.total ?? "0"), + clientsAcquired: parseInt(clientsRow?.count ?? "0"), + pending: parseFloat(pending?.total ?? "0"), + }; +} + +export async function getMonthlyCollected(year: number): Promise { + const rows = await db + .select({ + month: sql`extract(month from ${payments.paid_at})::int`, + total: sql`coalesce(sum(${payments.amount}::numeric), 0)`, + }) + .from(payments) + .where( + and( + eq(payments.status, "saldato"), + sql`${payments.paid_at} is not null and extract(year from ${payments.paid_at}) = ${year}` + ) + ) + .groupBy(sql`extract(month from ${payments.paid_at})`); + + const byMonth: number[] = Array(12).fill(0); + for (const row of rows) { + byMonth[(row.month as number) - 1] = parseFloat(row.total); + } + return byMonth; +} + +export async function getAvailableYears(): Promise { + const rows = await db + .select({ year: sql`extract(year from ${clients.created_at})::int` }) + .from(clients) + .groupBy(sql`extract(year from ${clients.created_at})`); + + const years = rows.map((r) => r.year as number); + const currentYear = new Date().getFullYear(); + if (!years.includes(currentYear)) years.push(currentYear); + return years.sort((a, b) => b - a); +} \ No newline at end of file