Files
clienthub/src/lib/forecast-queries.ts
T
simone c398d6de34 feat(05-04): extend client-view with activeOffers + create forecast-queries.ts
- Add activeOffers field to ProjectView and ClientView interfaces (public_name only — never internal_name)
- Add offer queries to getProjectView(): project_offers JOIN offer_micros (explicit public_name selection), cumulative price via offer_micro_services JOIN offer_services
- Create src/lib/forecast-queries.ts with ForecastMonth type and getRevenueForecast12Months() 12-bucket JS algorithm
- Forecast spreads accepted_total evenly across duration_months from start_date
2026-05-30 16:41:43 +02:00

58 lines
1.9 KiB
TypeScript

import { db } from "@/db";
import { project_offers, offer_micros } from "@/db/schema";
import { eq } from "drizzle-orm";
export type ForecastMonth = {
year: number;
month: number; // 1-12
label: string; // "giu 2026"
total: number;
};
export async function getRevenueForecast12Months(): Promise<ForecastMonth[]> {
// Load all project offers with micro duration info
const offers = await db
.select({
start_date: project_offers.start_date,
duration_months: offer_micros.duration_months,
accepted_total: project_offers.accepted_total,
})
.from(project_offers)
.innerJoin(offer_micros, eq(project_offers.micro_id, offer_micros.id));
// 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) {
// Skip offers with no accepted_total — they contribute 0 to forecast
if (!offer.accepted_total) continue;
const total = parseFloat(String(offer.accepted_total));
if (isNaN(total) || total <= 0) continue;
const perMonth = total / offer.duration_months;
const start = new Date(offer.start_date);
for (let m = 0; m < offer.duration_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;
}