Fix budget CSV import — handle quoted fields, Aug-25 month format, currency symbols
This commit is contained in:
parent
95103280c3
commit
bbc9a23da0
1 changed files with 41 additions and 20 deletions
|
|
@ -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++
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue