diff --git a/frontend/src/pages/Budgets.tsx b/frontend/src/pages/Budgets.tsx index ae2f7b0..9c572a0 100644 --- a/frontend/src/pages/Budgets.tsx +++ b/frontend/src/pages/Budgets.tsx @@ -103,32 +103,53 @@ export default function Budgets() { const reader = new FileReader() reader.onload = async (ev) => { 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 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 } - const amount = parseFloat(amountRaw) - if (isNaN(amount)) { skipped++; continue } - - let key: string | null = null - 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')}` - } - } + // Strip currency symbols, spaces, commas (thousands separators) + const amount = parseFloat(amountRaw.replace(/[£$€,\s]/g, '')) + if (isNaN(amount) || amount < 0) { skipped++; continue } + const key = parseMonth(monthRaw) if (!key) { skipped++; continue } - try { await saveBudget(key, amount) - setBudgets(b => ({ ...b, [key!]: amount })) + setBudgets(b => ({ ...b, [key]: amount })) saved++ } catch { skipped++