FlowBudget MVP: Chrome extension with SKU-level parsing, sinking funds, seasonal baselines, lifestyle creep heatmap, net worth dashboard

This commit is contained in:
Bun Bun
2026-06-19 12:31:22 +00:00
commit d6b9fe0ac6
24 changed files with 3465 additions and 0 deletions
+119
View File
@@ -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)
}