120 lines
4.0 KiB
TypeScript
120 lines
4.0 KiB
TypeScript
import type { Category, Transaction, MonthlySnapshot } from '../types'
|
|
|
|
export function getTotalBudget(categories: Category[]): number {
|
|
return categories.reduce((sum, c) => sum + getEffectiveBudget(c), 0)
|
|
}
|
|
|
|
export function getTotalSpent(categories: Category[]): number {
|
|
return categories.reduce((sum, c) => sum + c.spent, 0)
|
|
}
|
|
|
|
export function getEffectiveBudget(category: Category, month?: number): number {
|
|
const base = month !== undefined && category.seasonalBaseline
|
|
? getSeasonalValue(category.seasonalBaseline, month)
|
|
: category.budget
|
|
return base + (category.rolloverEnabled ? category.rolloverAmount : 0)
|
|
}
|
|
|
|
function getSeasonalValue(baseline: { jan: number; feb: number; mar: number; apr: number; may: number; jun: number; jul: number; aug: number; sep: number; oct: number; nov: number; dec: number }, month: number): number {
|
|
const map = [baseline.jan, baseline.feb, baseline.mar, baseline.apr, baseline.may, baseline.jun, baseline.jul, baseline.aug, baseline.sep, baseline.oct, baseline.nov, baseline.dec]
|
|
return map[month] ?? baseline.jan
|
|
}
|
|
|
|
export function getRemaining(category: Category): number {
|
|
return getEffectiveBudget(category) - category.spent
|
|
}
|
|
|
|
export function getProgressPercent(category: Category): number {
|
|
const budget = getEffectiveBudget(category)
|
|
if (budget <= 0) return 0
|
|
return Math.min(100, Math.round((category.spent / budget) * 100))
|
|
}
|
|
|
|
export function addTransaction(
|
|
categories: Category[],
|
|
transactions: Transaction[],
|
|
tx: Transaction
|
|
): { categories: Category[]; transactions: Transaction[] } {
|
|
const cats = categories.map(c => ({ ...c }))
|
|
const txs = [...transactions, tx]
|
|
|
|
if (tx.isSplit && tx.items) {
|
|
for (const item of tx.items) {
|
|
const cat = cats.find(c => c.id === item.categoryId)
|
|
if (cat) {
|
|
cat.spent += item.amount
|
|
}
|
|
}
|
|
} else {
|
|
const cat = cats.find(c => c.id === tx.categoryId)
|
|
if (cat) {
|
|
cat.spent += tx.amount
|
|
}
|
|
}
|
|
|
|
return { categories: cats, transactions: txs }
|
|
}
|
|
|
|
export function removeTransaction(
|
|
categories: Category[],
|
|
transactions: Transaction[],
|
|
txId: string
|
|
): { categories: Category[]; transactions: Transaction[] } {
|
|
const tx = transactions.find(t => t.id === txId)
|
|
if (!tx) return { categories, transactions }
|
|
|
|
const cats = categories.map(c => ({ ...c }))
|
|
const txs = transactions.filter(t => t.id !== txId)
|
|
|
|
if (tx.isSplit && tx.items) {
|
|
for (const item of tx.items) {
|
|
const cat = cats.find(c => c.id === item.categoryId)
|
|
if (cat) {
|
|
cat.spent = Math.max(0, cat.spent - item.amount)
|
|
}
|
|
}
|
|
} else {
|
|
const cat = cats.find(c => c.id === tx.categoryId)
|
|
if (cat) {
|
|
cat.spent = Math.max(0, cat.spent - tx.amount)
|
|
}
|
|
}
|
|
|
|
return { categories: cats, transactions: txs }
|
|
}
|
|
|
|
export function rollOverMonth(categories: Category[]): Category[] {
|
|
return categories.map(c => {
|
|
const remaining = getEffectiveBudget(c) - c.spent
|
|
return {
|
|
...c,
|
|
rolloverAmount: c.rolloverEnabled ? c.rolloverAmount + remaining : c.rolloverAmount,
|
|
spent: 0,
|
|
}
|
|
})
|
|
}
|
|
|
|
export function computeLifestyleCreep(
|
|
snapshots: MonthlySnapshot[],
|
|
currentMonth: string
|
|
): { categoryId: string; changePercent: number; isInflation: boolean }[] {
|
|
const current = snapshots.find(s => s.month === currentMonth)
|
|
const lastYear = currentMonth.slice(0, 4)
|
|
const prevYear = String(Number(lastYear) - 1)
|
|
const prevMonthKey = prevYear + currentMonth.slice(4)
|
|
const previous = snapshots.find(s => s.month === prevMonthKey)
|
|
|
|
if (!current || !previous) return []
|
|
|
|
return Object.entries(current.categories).map(([catId, curr]) => {
|
|
const prev = previous.categories[catId]
|
|
if (!prev || prev.budget <= 0) return { categoryId: catId, changePercent: 0, isInflation: false }
|
|
const change = ((curr.budget - prev.budget) / prev.budget) * 100
|
|
return { categoryId: catId, changePercent: Math.round(change * 10) / 10, isInflation: change > 5 }
|
|
})
|
|
}
|
|
|
|
export function getNetWorth(accounts: { balance: number }[]): number {
|
|
return accounts.reduce((sum, a) => sum + a.balance, 0)
|
|
}
|