Files
clienthub/src/lib/analytics-queries.ts
T
Simone Cavalli 0f48570cd7 feat: client edit/delete/archive + time tracker + analytics time section
Schema:
- clients.archived boolean (default false)
- time_entries table (client_id, started_at, ended_at, duration_seconds)

Client management:
- /admin/clients/[id]/edit — form pre-compilato con nome, brand, brief
- ClientActions: Modifica / Archivia / Elimina con doppia conferma
- setClientArchived: toggle archiviazione senza perdere dati
- deleteClient: elimina con cascade, redirect a /admin
- Admin list: toggle "Mostra archiviati" via ?archived=1, righe archiviate opache

Time tracker:
- startTimer: crea sessione, ferma automaticamente quella precedente
- stopTimer: chiude sessione, calcola duration_seconds
- TimerCell: ▶/⏹ per ogni cliente, contatore live in secondi, totale cumulativo
- Una sola sessione attiva alla volta su tutta la lista

Analytics:
- Sezione "Fatturato" (invariata) + sezione "Tempo tracciato" separata
- Ore totali per anno + barre orizzontali per cliente
- getTotalTrackedHours, getTimeByClient queries

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 21:28:01 +02:00

117 lines
3.9 KiB
TypeScript

import { db } from "@/db";
import { clients, payments, time_entries } from "@/db/schema";
import { sql, and, eq } from "drizzle-orm";
export async function getAnalyticsByYear(year: number) {
const [contracted] = await db
.select({ total: sql<string>`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<string>`count(*)` })
.from(clients)
.where(sql`extract(year from ${clients.created_at}) = ${year}`);
const [collected] = await db
.select({ total: sql<string>`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<string>`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<number[]> {
const rows = await db
.select({
month: sql<number>`extract(month from ${payments.paid_at})::int`,
total: sql<string>`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<number[]> {
const rows = await db
.select({ year: sql<number>`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);
}
// ── Time tracking ─────────────────────────────────────────────────────────────
export type ClientTimeRow = {
clientId: string;
clientName: string;
totalSeconds: number;
};
export async function getTimeByClient(year: number): Promise<ClientTimeRow[]> {
const rows = await db
.select({
client_id: time_entries.client_id,
total: sql<string>`coalesce(sum(${time_entries.duration_seconds}), 0)`,
})
.from(time_entries)
.where(
sql`${time_entries.ended_at} is not null
and extract(year from ${time_entries.started_at}) = ${year}`
)
.groupBy(time_entries.client_id);
if (rows.length === 0) return [];
const allClients = await db.select({ id: clients.id, name: clients.name }).from(clients);
const nameMap = new Map(allClients.map((c) => [c.id, c.name]));
return rows
.map((r) => ({
clientId: r.client_id,
clientName: nameMap.get(r.client_id) ?? r.client_id,
totalSeconds: parseInt(r.total),
}))
.sort((a, b) => b.totalSeconds - a.totalSeconds);
}
export async function getTotalTrackedHours(year: number): Promise<number> {
const [row] = await db
.select({ total: sql<string>`coalesce(sum(${time_entries.duration_seconds}), 0)` })
.from(time_entries)
.where(
sql`${time_entries.ended_at} is not null
and extract(year from ${time_entries.started_at}) = ${year}`
);
return Math.round(parseInt(row?.total ?? "0") / 3600 * 10) / 10;
}