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()
|
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++
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue