export type ChartPoint = { date: string; value: number }; export type ChartSeries = { label: string; color: string; points: ChartPoint[] }; export function balanceSeries( accounts: { id: number; name: string; openingBalanceCents: number }[], transactions: { accountId: number; date: string; amountCents: number }[] ): ChartSeries[] { const palette = ['#0070f3', '#e5484d', '#46a758', '#ad7f00', '#8e4ec6', '#0091ad', '#e56910']; const lastDate = transactions.at(-1)?.date; return accounts.map((account, index) => { let balance = account.openingBalanceCents; const entries = transactions.filter((transaction) => transaction.accountId === account.id); const points: ChartPoint[] = []; if (entries.length) points.push({ date: entries[0].date, value: balance }); for (const entry of entries) { balance += entry.amountCents; points.push({ date: entry.date, value: balance }); } if (lastDate && entries.length && lastDate > entries.at(-1)!.date) points.push({ date: lastDate, value: balance }); return { label: account.name, color: palette[index % palette.length], points }; }); } export function accountBalances( accounts: { id: number; openingBalanceCents: number }[], transactions: { accountId: number; amountCents: number }[] ): Map { const balances = new Map(accounts.map((account) => [account.id, account.openingBalanceCents])); for (const transaction of transactions) balances.set( transaction.accountId, (balances.get(transaction.accountId) ?? 0) + transaction.amountCents ); return balances; } export function monthlyCents(amountCents: number, cadence: string): number { return cadence === 'weekly' ? (amountCents * 52) / 12 : cadence === 'yearly' ? amountCents / 12 : amountCents; } export function forecastSeries( currentCents: number, forecasts: { date: string; amountCents: number }[], today: string ): ChartSeries[] { let projected = currentCents; const points: ChartPoint[] = [{ date: today, value: currentCents }]; for (const item of forecasts .filter((entry) => entry.date >= today) .sort((a, b) => a.date.localeCompare(b.date))) { projected += item.amountCents; points.push({ date: item.date, value: projected }); } return [{ label: 'Projected total', color: '#0070f3', points }]; } export function utilityEstimate( readings: { date: string; reading: string; kind: string }[], kind: string, ratePerUnit: string, monthlyAdvanceCents: number, billingStart?: string | null ) { const byKind = readings.filter( (reading) => reading.kind === kind && (!billingStart || reading.date >= billingStart) ); if (byKind.length < 2) return null; const first = byKind[0]; const last = byKind.at(-1)!; const usage = Number(last.reading) - Number(first.reading); const months = (Date.parse(`${last.date}T12:00:00Z`) - Date.parse(`${first.date}T12:00:00Z`)) / ((365.25 / 12) * 86_400_000); if (months <= 0 || usage < 0) return null; const estimatedCents = Math.round(usage * Number(ratePerUnit) * 100); const advancesCents = Math.round(months * monthlyAdvanceCents); return { usage, months, estimatedCents, advancesCents, differenceCents: advancesCents - estimatedCents }; }