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 = `
`
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()