FlowBudget MVP: Chrome extension with SKU-level parsing, sinking funds, seasonal baselines, lifestyle creep heatmap, net worth dashboard
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user