Fix budget CSV import — handle quoted fields, Aug-25 month format, currency symbols

This commit is contained in:
jtricerolph 2026-07-23 11:41:12 +00:00
parent 95103280c3
commit bbc9a23da0

View file

@ -103,32 +103,53 @@ export default function Budgets() {
const reader = new FileReader() const reader = new FileReader()
reader.onload = async (ev) => { reader.onload = async (ev) => {
const text = ev.target?.result as string const text = ev.target?.result as string
const lines = text.split(/\r?\n/).filter(l => l.trim() && !l.startsWith('Month'))
// Proper CSV field parser — handles quoted fields containing commas
function parseCSVLine(line: string): string[] {
const fields: string[] = []
let cur = '', inQ = false
for (const ch of line) {
if (ch === '"') { inQ = !inQ }
else if (ch === ',' && !inQ) { fields.push(cur.trim()); cur = '' }
else { cur += ch }
}
fields.push(cur.trim())
return fields
}
// Parse a month string in any common format → 'YYYY-MM' or null
function parseMonth(raw: string): string | null {
const s = raw.trim()
// YYYY-MM
const iso = s.match(/^(\d{4})-(\d{2})$/)
if (iso) return `${iso[1]}-${iso[2]}`
// Sep-25 / Sep-2025 / Sep 25 / Sep 2025 / September 2025
const m = s.match(/^([A-Za-z]+)[\s\-](\d{2,4})$/)
if (m) {
const mi = MONTH_SHORT.findIndex(n => n.toLowerCase() === m[1].toLowerCase().slice(0, 3))
if (mi < 0) return null
const yr = parseInt(m[2])
const fullYr = yr < 100 ? (yr >= 50 ? 1900 + yr : 2000 + yr) : yr
return `${fullYr}-${String(mi + 1).padStart(2, '0')}`
}
return null
}
const lines = text.split(/\r?\n/).filter(l => l.trim() && !/^month/i.test(l.trim()))
let saved = 0, skipped = 0 let saved = 0, skipped = 0
for (const line of lines) { for (const line of lines) {
const [monthRaw, amountRaw] = line.split(',').map(s => s.trim()) const fields = parseCSVLine(line)
const monthRaw = fields[0] ?? ''
const amountRaw = fields[1] ?? ''
if (!monthRaw || !amountRaw) { skipped++; continue } if (!monthRaw || !amountRaw) { skipped++; continue }
const amount = parseFloat(amountRaw) // Strip currency symbols, spaces, commas (thousands separators)
if (isNaN(amount)) { skipped++; continue } const amount = parseFloat(amountRaw.replace(/[£$€,\s]/g, ''))
if (isNaN(amount) || amount < 0) { skipped++; continue }
let key: string | null = null const key = parseMonth(monthRaw)
const isoMatch = monthRaw.match(/^(\d{4})-(\d{2})$/)
if (isoMatch) {
key = `${isoMatch[1]}-${isoMatch[2]}`
} else {
const parts = monthRaw.split(' ')
if (parts.length === 2) {
const yr = parseInt(parts[1])
const mi = MONTH_SHORT.findIndex(m => m.toLowerCase() === parts[0].toLowerCase().slice(0, 3))
?? MONTH_LABELS.findIndex(m => m.toLowerCase() === parts[0].toLowerCase())
if (!isNaN(yr) && mi >= 0) key = `${yr}-${String(mi + 1).padStart(2, '0')}`
}
}
if (!key) { skipped++; continue } if (!key) { skipped++; continue }
try { try {
await saveBudget(key, amount) await saveBudget(key, amount)
setBudgets(b => ({ ...b, [key!]: amount })) setBudgets(b => ({ ...b, [key]: amount }))
saved++ saved++
} catch { } catch {
skipped++ skipped++