feat(#18): import de extratos OFX e CSV — parser, dedup, preview e confirmação

Parser OFX tolerante ao formato SGML de bancos brasileiros (sem closing tags, BOM,
timezone suffix). Parser CSV com separador de campo/decimal configurável. Dedup por
FITID (OFX) ou date+amount+desc normalizado. Fluxo: POST /preview → revisão na UI
→ POST /confirm salva e loga. Migration 002 adiciona external_id com unique index.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
2026-05-26 20:17:46 -03:00
co-authored by Claude Sonnet 4.6
parent d6782d51a5
commit 83dff2de78
15 changed files with 1065 additions and 3 deletions
+96
View File
@@ -0,0 +1,96 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
export interface ImportRow {
date: string
amount: number
description: string
type: string
external_id?: string
is_duplicate: boolean
}
export interface ImportResult {
imported: number
duplicates: number
errors: number
}
export interface CSVMapping {
date_column: number
amount_column: number
description_column: number
date_format: string
has_header: boolean
decimal_separator: string
field_separator: string
}
export const useImportStore = defineStore('import', () => {
const rows = ref<ImportRow[]>([])
const parseErrors = ref<string[]>([])
const filename = ref('')
const loading = ref(false)
const error = ref<string | null>(null)
const result = ref<ImportResult | null>(null)
async function preview(file: File, csvMapping?: CSVMapping) {
loading.value = true
error.value = null
result.value = null
filename.value = file.name
const form = new FormData()
form.append('file', file)
if (csvMapping) {
form.append('csv_mapping', JSON.stringify(csvMapping))
}
try {
const res = await fetch('/api/imports/preview', { method: 'POST', body: form })
const data = await res.json()
if (!res.ok) throw new Error(data.error ?? 'Erro no preview')
rows.value = data.rows ?? []
parseErrors.value = data.parse_errors ?? []
} catch (e: any) {
error.value = e.message
} finally {
loading.value = false
}
}
async function confirm() {
loading.value = true
error.value = null
try {
const res = await fetch('/api/imports/confirm', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
filename: filename.value,
rows: rows.value,
parse_error_count: parseErrors.value.length,
}),
})
const data = await res.json()
if (!res.ok) throw new Error(data.error ?? 'Erro ao confirmar')
result.value = data
rows.value = []
parseErrors.value = []
} catch (e: any) {
error.value = e.message
} finally {
loading.value = false
}
}
function reset() {
rows.value = []
parseErrors.value = []
filename.value = ''
error.value = null
result.value = null
}
return { rows, parseErrors, filename, loading, error, result, preview, confirm, reset }
})