feat: vorgefertigte Import-Presets für Raiffeisen (CH) und Revolut

- services/csv_import: PRESETS-Liste (aus echten Beispiel-Exporten des Nutzers),
  GET /imports/presets.
- Frontend: Preset-Auswahl im Import füllt Format/Spalten automatisch.
Beide Presets per TestClient gegen echte Zeilenformate verifiziert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Guido
2026-08-19 20:14:39 +02:00
co-authored by Claude Opus 4.8
parent 1381967aae
commit ddf4f1f5ed
6 changed files with 80 additions and 20 deletions
+1
View File
@@ -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 }),
+2 -1
View File
@@ -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",
+2 -1
View File
@@ -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",
+36 -18
View File
@@ -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); }} />
</label>
</div>
{templates.length > 0 && (
<label>
<span>{t("import.template")}</span>
<select defaultValue="" onChange={(e) => applyTemplate(e.target.value)}>
<option value="">{t("import.templateManual")}</option>
{templates.map((tp) => <option key={tp.id} value={tp.id}>{tp.name}</option>)}
</select>
</label>
)}
<div className="row">
{presets.length > 0 && (
<label className="grow">
<span>{t("import.preset")}</span>
<select defaultValue="" onChange={(e) => applyPreset(e.target.value)}>
<option value="">{t("import.templateManual")}</option>
{presets.map((p, i) => <option key={i} value={i}>{p.name}</option>)}
</select>
</label>
)}
{templates.length > 0 && (
<label className="grow">
<span>{t("import.template")}</span>
<select defaultValue="" onChange={(e) => applyTemplate(e.target.value)}>
<option value="">{t("import.templateManual")}</option>
{templates.map((tp) => <option key={tp.id} value={tp.id}>{tp.name}</option>)}
</select>
</label>
)}
</div>
</div>
{/* Schritt 2: Format */}