diff --git a/backend/app/routers/imports.py b/backend/app/routers/imports.py index bd88306..73fe43a 100644 --- a/backend/app/routers/imports.py +++ b/backend/app/routers/imports.py @@ -32,6 +32,12 @@ def _to_out(tpl: ImportTemplate) -> ImportTemplateOut: # --- Templates ------------------------------------------------------------- +@router.get("/presets") +def list_presets(_: User = Depends(get_current_user)) -> list[dict]: + """Vorgefertigte Bank-Vorlagen (füllen die Format-/Spalten-Einstellungen).""" + return csv_import.PRESETS + + @router.get("/templates", response_model=list[ImportTemplateOut]) def list_templates(db: Session = Depends(get_db), user: User = Depends(get_current_user)): rows = db.query(ImportTemplate).filter(ImportTemplate.owner_id == user.id).order_by(ImportTemplate.name).all() diff --git a/backend/app/services/csv_import.py b/backend/app/services/csv_import.py index 3ca8d5e..cb1198c 100644 --- a/backend/app/services/csv_import.py +++ b/backend/app/services/csv_import.py @@ -30,6 +30,39 @@ from ..models import ( from . import currency +# Vorgefertigte Bank-Vorlagen (aus echten Export-Beispielen). Erscheinen im +# Import als auswählbare Presets und füllen die Format-/Spalten-Einstellungen. +PRESETS: list[dict] = [ + { + "name": "Raiffeisen (CH)", + "delimiter": ";", + "decimal_sep": ".", + "date_format": "%Y-%m-%d %H:%M:%S.%f", + "encoding": "utf-8", + "has_header": False, + "skip_rows": 0, + "amount_mode": "signed", + # Spalten: 0=eigene IBAN, 1=Datum, 2=Text, 3=Betrag, 4=Saldo, 5=Valuta. + "column_map": {"date": 1, "amount": 3, "payee": 2, + "purpose": None, "counterparty_iban": None}, + }, + { + "name": "Revolut", + "delimiter": ",", + "decimal_sep": ".", + "date_format": "%Y-%m-%d %H:%M:%S", + "encoding": "utf-8", + "has_header": True, + "skip_rows": 0, + "amount_mode": "signed", + # Spalten: 0=Typ, 1=Produkt, 2=Start, 3=Abgeschlossen, 4=Beschreibung, + # 5=Betrag, 6=Gebühr, 7=Währung, 8=Status, 9=Saldo. + "column_map": {"date": 3, "amount": 5, "payee": 4, + "purpose": None, "counterparty_iban": None}, + }, +] + + # --- Kleine Parser-Helfer -------------------------------------------------- diff --git a/web/src/api.js b/web/src/api.js index 50e3ba8..96736cd 100644 --- a/web/src/api.js +++ b/web/src/api.js @@ -188,6 +188,7 @@ export const api = { cardRecent: (limit) => request(`/dashboard/recent${limit ? `?limit=${limit}` : ""}`), // CSV-Import + importPresets: () => request("/imports/presets"), importTemplates: () => request("/imports/templates"), createImportTemplate: (body) => request("/imports/templates", { method: "POST", body }), updateImportTemplate: (id, body) => request(`/imports/templates/${id}`, { method: "PATCH", body }), diff --git a/web/src/locales/de.json b/web/src/locales/de.json index bf30644..5b395d4 100644 --- a/web/src/locales/de.json +++ b/web/src/locales/de.json @@ -204,7 +204,8 @@ "step4": "4 · Prüfen & buchen", "account": "Zielkonto", "file": "CSV-Datei", - "template": "Vorlage", + "preset": "Bank-Vorlage (vorgefertigt)", + "template": "Eigene Vorlage", "templateManual": "Manuell einstellen", "delimiter": "Trennzeichen", "decimal": "Dezimaltrenner", diff --git a/web/src/locales/en.json b/web/src/locales/en.json index 1942b8a..27620be 100644 --- a/web/src/locales/en.json +++ b/web/src/locales/en.json @@ -204,7 +204,8 @@ "step4": "4 · Review & import", "account": "Target account", "file": "CSV file", - "template": "Template", + "preset": "Bank preset", + "template": "My template", "templateManual": "Configure manually", "delimiter": "Delimiter", "decimal": "Decimal separator", diff --git a/web/src/pages/Import.jsx b/web/src/pages/Import.jsx index d2433e1..4a8ebb5 100644 --- a/web/src/pages/Import.jsx +++ b/web/src/pages/Import.jsx @@ -38,6 +38,7 @@ export default function Import() { const [accounts, setAccounts] = useState([]); const [groups, setGroups] = useState([]); const [templates, setTemplates] = useState([]); + const [presets, setPresets] = useState([]); const [batches, setBatches] = useState([]); const [accountId, setAccountId] = useState(""); @@ -52,10 +53,10 @@ export default function Import() { async function loadStamm() { try { - const [a, g, tp, b] = await Promise.all([ - api.listAccounts(), api.listCategories(), api.importTemplates(), api.importBatches(), + const [a, g, tp, pr, b] = await Promise.all([ + api.listAccounts(), api.listCategories(), api.importTemplates(), api.importPresets(), api.importBatches(), ]); - setAccounts(a); setGroups(g); setTemplates(tp); setBatches(b); + setAccounts(a); setGroups(g); setTemplates(tp); setPresets(pr); setBatches(b); if (a[0] && !accountId) setAccountId(String(a[0].id)); } catch (e) { toast(e.message, "error"); } } @@ -66,14 +67,20 @@ export default function Import() { setConfig((c) => ({ ...c, column_map: { ...c.column_map, [field]: value === "" ? null : Number(value) } })); } + function applyConfig(src) { + setConfig({ + delimiter: src.delimiter, decimal_sep: src.decimal_sep, date_format: src.date_format, + encoding: src.encoding, has_header: src.has_header, skip_rows: src.skip_rows, + amount_mode: src.amount_mode, column_map: src.column_map || {}, + }); + } function applyTemplate(id) { const tpl = templates.find((x) => String(x.id) === String(id)); - if (!tpl) return; - setConfig({ - delimiter: tpl.delimiter, decimal_sep: tpl.decimal_sep, date_format: tpl.date_format, - encoding: tpl.encoding, has_header: tpl.has_header, skip_rows: tpl.skip_rows, - amount_mode: tpl.amount_mode, column_map: tpl.column_map || {}, - }); + if (tpl) applyConfig(tpl); + } + function applyPreset(idx) { + const p = presets[Number(idx)]; + if (p) applyConfig(p); } function baseForm() { @@ -179,15 +186,26 @@ export default function Import() { onChange={(e) => { setFile(e.target.files?.[0] || null); setRaw(null); setRows(null); }} /> - {templates.length > 0 && ( - - )} +