import { db } from "@/db"; import { clients, payments, time_entries, projects } from "@/db/schema"; import { sql, and, eq, inArray } from "drizzle-orm"; export async function getAnalyticsByYear(year: number) { const [contracted] = await db .select({ total: sql`coalesce(sum(${projects.accepted_total}::numeric), 0)` }) .from(projects) .innerJoin(clients, eq(projects.client_id, clients.id)) .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); } // ── Time tracking ───────────────────────────────────────────────────────────── export type ClientTimeRow = { clientId: string; clientName: string; totalSeconds: number; }; export async function getTimeByClient(year: number): Promise { const rows = await db .select({ project_id: time_entries.project_id, total: sql`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.project_id); if (rows.length === 0) return []; // Map project_id → client_id const projectIds = rows.map((r) => r.project_id); const projectRows = await db .select({ id: projects.id, client_id: projects.client_id }) .from(projects) .where(inArray(projects.id, projectIds)); const projectToClient = new Map(projectRows.map((p) => [p.id, p.client_id])); const allClients = await db.select({ id: clients.id, name: clients.name }).from(clients); const nameMap = new Map(allClients.map((c) => [c.id, c.name])); // Aggregate by client_id (a client may have multiple projects) const clientTotals = new Map(); for (const row of rows) { const clientId = projectToClient.get(row.project_id); if (!clientId) continue; clientTotals.set(clientId, (clientTotals.get(clientId) ?? 0) + parseInt(row.total)); } return [...clientTotals.entries()] .map(([clientId, totalSeconds]) => ({ clientId, clientName: nameMap.get(clientId) ?? clientId, totalSeconds, })) .sort((a, b) => b.totalSeconds - a.totalSeconds); } export async function getTotalTrackedHours(year: number): Promise { const [row] = await db .select({ total: sql`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; } // ── Redditività oraria per cliente ────────────────────────────────────────── /** Valore orario "target" di riferimento (€/h) — costante di consulenza. */ export const TARGET_HOURLY = 100; export type ProfitabilityMargin = "ottimo" | "in_linea" | "sotto"; export type ClientProfitabilityRow = { clientId: string; clientName: string; hours: number; // ore tracciate (anno), 1 decimale contracted: number; // totale contrattualizzato del cliente (accepted_total dei progetti) realRate: number; // contracted / hours, arrotondato a 2 decimali target: number; // TARGET_HOURLY margin: ProfitabilityMargin; }; /** * Redditività oraria reale per cliente: contrattualizzato ÷ ore tracciate (anno). * Solo clienti con ore tracciate > 0 (serve il denominatore). Ordinati per realRate desc. */ export async function getClientProfitability(year: number): Promise { const timeRows = await getTimeByClient(year); if (timeRows.length === 0) return []; const clientIds = timeRows.map((r) => r.clientId); // Contrattualizzato per cliente = somma accepted_total dei suoi progetti. const contractRows = await db .select({ client_id: projects.client_id, total: sql`coalesce(sum(${projects.accepted_total}::numeric), 0)`, }) .from(projects) .where(inArray(projects.client_id, clientIds)) .groupBy(projects.client_id); const contractedMap = new Map( contractRows.map((r) => [r.client_id, parseFloat(r.total)]) ); return timeRows .map((row) => { const hours = Math.round((row.totalSeconds / 3600) * 10) / 10; const contracted = contractedMap.get(row.clientId) ?? 0; const realRate = hours > 0 ? Math.round((contracted / hours) * 100) / 100 : 0; const margin: ProfitabilityMargin = realRate >= TARGET_HOURLY * 2 ? "ottimo" : realRate >= TARGET_HOURLY ? "in_linea" : "sotto"; return { clientId: row.clientId, clientName: row.clientName, hours, contracted, realRate, target: TARGET_HOURLY, margin, }; }) .sort((a, b) => b.realRate - a.realRate); }