feat: Backup/Export + Import-UX klarer + Raiffeisen-Preset-Fix

Backup & Export:
- routers/backup: /backup/db (Admin, konsistentes SQLite-Online-Backup),
  /export/transactions.csv|json und /export/all.json (owner-gescoped).
- Einstellungen: Bereich „Backup & Export" mit Download-Buttons. docs/BACKUP.md
  (Wiederherstellung + LXC-Sicherung). pytest für Export + Admin-Schutz.

Import-Verbesserungen (Nutzer-Feedback zum Ablauf):
- Klarer Hinweis, dass die Zuordnungstabelle nur Beispielzeilen zeigt und
  „Vorschau erstellen" die vollständige Prüfliste + Buchen-Button erzeugt.
- Zusammenfassung (erkannt/gebucht) und Warnung, wenn keine gültige Zeile erkannt
  wurde (Hinweis auf Format/Bank-Vorlage).
- Fix: Raiffeisen-Preset hat has_header=true (der Export hat eine Kopfzeile).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Guido
2026-08-19 20:35:02 +02:00
co-authored by Claude Opus 4.8
parent 7461fddb7a
commit f375fc5eaa
11 changed files with 385 additions and 5 deletions
+9
View File
@@ -315,6 +315,7 @@ export default function Import() {
{raw && (
<div className="card">
<h2>{t("import.step3")}</h2>
<p className="muted small">{t("import.rawHint")}</p>
<div className="table-scroll">
<table className="table raw-table">
<thead>
@@ -357,6 +358,7 @@ export default function Import() {
<button className="btn sm" onClick={saveTemplate}>{t("import.saveTemplate")}</button>
</div>
</div>
<p className="muted small" style={{ marginTop: "var(--sp-2)" }}>{t("import.makePreviewNote")}</p>
</div>
)}
@@ -369,6 +371,13 @@ export default function Import() {
<Icon name="download" size={15} /> {t("import.commit", { n: includeCount })}
</button>
</div>
<p className="muted small">{t("import.summary", { total: rows.length, n: includeCount })}</p>
{rows.length > 0 && includeCount === 0 && (
<div className="alert error" style={{ marginBottom: "var(--sp-3)" }}>
<Icon name="alert" size={16} />
<span>{t("import.noneValid")}</span>
</div>
)}
<div className="table-scroll">
<table className="table">
<thead>
+36
View File
@@ -1,10 +1,14 @@
import { useTranslation } from "react-i18next";
import { useSettings } from "../settings";
import { useAuth } from "../auth";
import { useToast } from "../toast";
import { api } from "../api";
import Icon from "../components/Icon";
export default function Settings() {
const { t } = useTranslation();
const s = useSettings();
const { isAdmin } = useAuth();
const toast = useToast();
async function set(key, value) {
@@ -16,6 +20,14 @@ export default function Settings() {
}
}
async function dl(fn) {
try {
await fn();
} catch (err) {
toast(err.message, "error");
}
}
return (
<div>
<h1>{t("settings.title")}</h1>
@@ -62,6 +74,30 @@ export default function Settings() {
</label>
<p className="muted small" style={{ marginTop: -8 }}>{t("settings.budgetStartDayHint")}</p>
</div>
<div className="card" style={{ maxWidth: 520, marginTop: "var(--sp-4)" }}>
<h2>{t("settings.backup")}</h2>
<p className="muted small">{t("settings.exportHint")}</p>
<div className="field-inline" style={{ flexWrap: "wrap" }}>
<button className="btn" onClick={() => dl(api.exportTransactionsCsv)}>
<Icon name="download" size={15} /> {t("settings.exportTxnCsv")}
</button>
<button className="btn" onClick={() => dl(api.exportTransactionsJson)}>
<Icon name="download" size={15} /> {t("settings.exportTxnJson")}
</button>
<button className="btn" onClick={() => dl(api.exportAllJson)}>
<Icon name="download" size={15} /> {t("settings.exportAll")}
</button>
</div>
{isAdmin && (
<>
<p className="muted small" style={{ marginTop: "var(--sp-3)" }}>{t("settings.dbBackupHint")}</p>
<button className="btn primary" onClick={() => dl(api.downloadDbBackup)}>
<Icon name="download" size={15} /> {t("settings.dbBackup")}
</button>
</>
)}
</div>
</div>
);
}