import type { Asset, MonthlyIncome, MonthlyRunningCost } from "./data"; import { computeProfit, type ProfitOptions } from "./profit"; export type IncomeRow = { id: string; code: string; name: string; months: (number | null)[]; // Part A.2: null = no data (forecast), 0 = earned zero fixedMonths: (number | null)[]; runningMonths: (number | null)[]; profit: number[]; income: number; fixed: number; running: number; ytdProfit: number; lastActualMonth: number; // Part A.3: clip x-axis }; // Per Part A.2: months beyond lastActualMonth are null in `months`/`fixedMonths`/`runningMonths`. // `running` is derived from monthly_running_costs; null means we have no running data for that month. export function buildIncomeRows( assets: Asset[], income: MonthlyIncome[], running?: MonthlyRunningCost[], profitOpts: ProfitOptions = {} ): IncomeRow[] { // Determine the last actual month globally (Part A.2/A.3) const lastActualMonth = income.reduce((m, r) => r.income > 0 && r.month > m ? r.month : m, 0); const byAsset = new Map(); for (const a of assets) { if (!a.code) continue; byAsset.set(a.id, { id: a.id, code: a.code, name: a.name, months: Array(12).fill(null), fixedMonths: Array(12).fill(null), runningMonths: Array(12).fill(null), profit: Array(12).fill(0), income: 0, fixed: 0, running: 0, ytdProfit: 0, lastActualMonth, }); } for (const r of income) { const row = byAsset.get(r.asset); if (!row) continue; const i = r.month - 1; // Part A.2: only null out months BEYOND the last actual month. Months up to and including // lastActualMonth get their real value (which may be 0 — a real plotted zero). if (r.month <= lastActualMonth) { row.months[i] = r.income; row.fixedMonths[i] = r.fixed_expenses; row.profit[i] = computeProfit({ income: r.income, fixed: r.fixed_expenses, running: 0 }, profitOpts); } row.income += r.income; row.fixed += r.fixed_expenses; } // Per Part 1.5: running cost is derived from monthly_running_costs, not stored on income. for (const c of running ?? []) { const row = byAsset.get(c.asset); if (!row) continue; row.running += c.amount; if (c.month <= lastActualMonth) { row.runningMonths[c.month - 1] = c.amount; } // Re-compute per-month profit so it includes running cost regardless of includeRunningCost flag. row.profit[c.month - 1] = computeProfit( { income: row.months[c.month - 1] ?? 0, fixed: row.fixedMonths[c.month - 1] ?? 0, running: c.amount }, profitOpts ); } for (const row of byAsset.values()) { row.ytdProfit = computeProfit({ income: row.income, fixed: row.fixed, running: row.running }, profitOpts); } return Array.from(byAsset.values()); } export type RunningRow = { id: string; code: string; name: string; months: number[]; ytd: number; }; export function buildRunningRows( assets: Asset[], costs: MonthlyRunningCost[] ): RunningRow[] { const byAsset = new Map(); for (const a of assets) { if (!a.code) continue; byAsset.set(a.id, { id: a.id, code: a.code, name: a.name, months: Array(12).fill(0), ytd: 0, }); } for (const c of costs) { const row = byAsset.get(c.asset); if (!row) continue; row.months[c.month - 1] = c.amount; row.ytd += c.amount; } return Array.from(byAsset.values()); }