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:
@@ -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 }
|
||||
})
|
||||
Reference in New Issue
Block a user