import type { AppState, Budget, ToolEntry, UsageRecord } from '../types' import { loadState, saveState, addTool, addUsage, removeTool, removeUsage, setBudget, sumMonthUsages, sumToolMonthUsages, dailyTotals, projectedMonthEnd, clearAll, } from '../storage' import { newId, DEFAULT_BUDGET, getMonthKey, getTodayKey } from '../types' // --- DOM refs --- const el = (id: string) => document.getElementById(id)! let state: AppState = { version: 1, tools: [], usages: [], budget: { ...DEFAULT_BUDGET } } async function init() { state = await loadState() render() wireEvents() } function render() { const month = getMonthKey() const total = sumMonthUsages(state.usages, month) const limit = state.budget.monthlyLimit const projected = projectedMonthEnd(total) // Overview el('total-spend').textContent = `$${total.toFixed(2)}` el('budget-sub').textContent = `Budget: $${limit.toFixed(2)}` el('projected-sub').textContent = `Projected: $${projected.toFixed(2)}` const pct = limit > 0 ? Math.min((total / limit) * 100, 100) : 0 el('budget-bar').style.width = `${pct}%` el('budget-bar').style.background = pct > 100 ? 'var(--danger)' : pct > 80 ? 'var(--warning)' : 'var(--accent)' // Tools list const toolsList = el('tools-list') toolsList.innerHTML = '' if (state.tools.length === 0) { el('empty-state').classList.remove('hidden') el('tools-section').classList.add('hidden') el('add-usage-section').classList.add('hidden') el('trend-section').classList.add('hidden') } else { el('empty-state').classList.add('hidden') el('tools-section').classList.remove('hidden') el('add-usage-section').classList.remove('hidden') el('trend-section').classList.remove('hidden') for (const t of state.tools) { const toolSpent = sumToolMonthUsages(state.usages, t.id, month) const div = document.createElement('div') div.className = 'tool-item' div.innerHTML = `
${escapeHtml(t.name)}
${escapeHtml(t.billingModel)}${t.planName ? ' · ' + escapeHtml(t.planName) : ''}
$${toolSpent.toFixed(2)}
${t.monthlyCost > 0 ? 'Est: $' + t.monthlyCost.toFixed(2) + '/mo' : 'No estimate'}
` toolsList.appendChild(div) } } // Usage tool selector const usageTool = el('usage-tool') as HTMLSelectElement const prevVal = usageTool.value usageTool.innerHTML = '' for (const t of state.tools) { const opt = document.createElement('option') opt.value = t.id opt.textContent = t.name usageTool.appendChild(opt) } if (prevVal) usageTool.value = prevVal // Daily trend const trendList = el('trend-list') trendList.innerHTML = '' const days = dailyTotals(state.usages, month) if (days.length === 0) { const empty = document.createElement('div') empty.className = 'trend-row' empty.innerHTML = 'No data yet' trendList.appendChild(empty) } else { for (const d of days) { const row = document.createElement('div') row.className = 'trend-row' row.innerHTML = `${d.day}$${d.total.toFixed(2)}` trendList.appendChild(row) } } // Settings form values ;(el('budget-limit') as HTMLInputElement).value = String(state.budget.monthlyLimit) ;(el('budget-threshold') as HTMLInputElement).value = String(Math.round(state.budget.alertThreshold * 100)) ;(el('budget-enabled') as HTMLInputElement).checked = state.budget.enabled } function wireEvents() { el('add-tool-form').addEventListener('submit', async (e) => { e.preventDefault() const name = (el('tool-name') as HTMLInputElement).value.trim() const cost = parseFloat((el('tool-cost') as HTMLInputElement).value) const model = (el('tool-model') as HTMLSelectElement).value as ToolEntry['billingModel'] if (!name || isNaN(cost) || cost < 0) return const tool: ToolEntry = { id: newId(), name, monthlyCost: cost, currency: 'USD', billingModel: model, lastUpdated: Date.now(), } await addTool(tool) state = await loadState() ;(el('add-tool-form') as HTMLFormElement).reset() render() }) el('add-usage-form').addEventListener('submit', async (e) => { e.preventDefault() const toolId = (el('usage-tool') as HTMLSelectElement).value const date = (el('usage-date') as HTMLInputElement).value const amount = parseFloat((el('usage-amount') as HTMLInputElement).value) const note = (el('usage-note') as HTMLInputElement).value.trim() if (!toolId || !date || isNaN(amount) || amount < 0) return const record: UsageRecord = { id: newId(), toolId, date, amount, note: note || undefined, source: 'manual', createdAt: Date.now(), } await addUsage(record) state = await loadState() ;(el('add-usage-form') as HTMLFormElement).reset() ;(el('usage-date') as HTMLInputElement).value = getTodayKey() render() }) el('tools-list').addEventListener('click', async (e) => { const btn = (e.target as HTMLElement).closest('button') as HTMLButtonElement | null if (!btn) return const action = btn.dataset.action const toolId = btn.dataset.tool if (!action || !toolId) return if (action === 'delete-tool') { await removeTool(toolId) state = await loadState() render() } else if (action === 'add-usage') { ;(el('usage-tool') as HTMLSelectElement).value = toolId el('add-usage-section').scrollIntoView({ behavior: 'smooth' }) } }) el('btn-settings').addEventListener('click', () => { el('settings-modal').classList.remove('hidden') }) el('btn-close-settings').addEventListener('click', () => { el('settings-modal').classList.add('hidden') }) el('budget-form').addEventListener('submit', async (e) => { e.preventDefault() const limit = parseFloat((el('budget-limit') as HTMLInputElement).value) const threshold = parseInt((el('budget-threshold') as HTMLInputElement).value, 10) / 100 const enabled = (el('budget-enabled') as HTMLInputElement).checked if (isNaN(limit) || isNaN(threshold)) return const budget: Budget = { monthlyLimit: limit, currency: 'USD', alertThreshold: threshold, enabled } await setBudget(budget) state = await loadState() el('settings-modal').classList.add('hidden') render() }) el('btn-reset').addEventListener('click', async () => { if (!confirm('Delete ALL data? This cannot be undone.')) return await clearAll() state = await loadState() render() }) } function escapeHtml(s: string): string { const d = document.createElement('div') d.textContent = s return d.innerHTML } // Init default date ;(el('usage-date') as HTMLInputElement).value = getTodayKey() init()