import { db } from "@/db"; import { project_offers, offer_micros, offer_macros, projects } from "@/db/schema"; import { eq, sql, desc } from "drizzle-orm"; export type ForecastMonth = { year: number; month: number; // 1-12 label: string; // "giu 2026" total: number; }; export type OffersSoldRow = { offer_name: string; // offer_macros.internal_name tier_letter: string | null; count: number; }; export type OffersSoldBreakdown = { rows: OffersSoldRow[]; total: number; }; // Quante offerte sono state vendute (assegnate a un progetto), per offerta + tier. export async function getOffersSoldBreakdown(): Promise { const rows = await db .select({ offer_name: offer_macros.internal_name, tier_letter: offer_micros.tier_letter, count: sql`count(*)::int`, }) .from(project_offers) .innerJoin(offer_micros, eq(project_offers.micro_id, offer_micros.id)) .innerJoin(offer_macros, eq(offer_micros.macro_id, offer_macros.id)) .groupBy(offer_macros.internal_name, offer_micros.tier_letter) .orderBy(desc(sql`count(*)`)); const total = rows.reduce((sum, r) => sum + Number(r.count), 0); return { rows: rows.map((r) => ({ offer_name: r.offer_name, tier_letter: r.tier_letter, count: Number(r.count), })), total, }; } export async function getRevenueForecast12Months(): Promise { // Active offers only (non-archived projects). offer_type drives the math: // - retainer: accepted_total è il CANONE MENSILE → ogni mese coperto riceve l'intero importo // - una_tantum: accepted_total è il prezzo TOTALE → spalmato su duration_months const offers = await db .select({ start_date: project_offers.start_date, duration_months: offer_micros.duration_months, accepted_total: project_offers.accepted_total, offer_type: offer_macros.offer_type, }) .from(project_offers) .innerJoin(offer_micros, eq(project_offers.micro_id, offer_micros.id)) .innerJoin(offer_macros, eq(offer_micros.macro_id, offer_macros.id)) .innerJoin(projects, eq(project_offers.project_id, projects.id)) .where(eq(projects.archived, false)); // Build 12-month bucket array starting from current month const now = new Date(); const buckets: ForecastMonth[] = Array.from({ length: 12 }, (_, i) => { const d = new Date(now.getFullYear(), now.getMonth() + i, 1); return { year: d.getFullYear(), month: d.getMonth() + 1, label: d.toLocaleDateString("it-IT", { month: "short", year: "numeric" }), total: 0, }; }); for (const offer of offers) { if (!offer.accepted_total) continue; const total = parseFloat(String(offer.accepted_total)); if (isNaN(total) || total <= 0) continue; const start = new Date(offer.start_date); const startMonthKey = start.getFullYear() * 12 + start.getMonth(); if (offer.offer_type === "retainer") { // Canone ricorrente: contribuisce ad OGNI mese dell'orizzonte da start in poi, // senza limite di duration_months (un retainer è continuativo). for (const b of buckets) { const bucketMonthKey = b.year * 12 + (b.month - 1); if (bucketMonthKey >= startMonthKey) b.total += total; } } else { // Una tantum: prezzo totale ripartito su duration_months a partire da start. const months = offer.duration_months > 0 ? offer.duration_months : 1; const perMonth = total / months; for (let m = 0; m < months; m++) { const offerMonth = new Date(start.getFullYear(), start.getMonth() + m, 1); const bucket = buckets.find( (b) => b.year === offerMonth.getFullYear() && b.month === offerMonth.getMonth() + 1 ); if (bucket) bucket.total += perMonth; } } } // Round totals to 2 decimal places buckets.forEach((b) => { b.total = Math.round(b.total * 100) / 100; }); return buckets; }