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:
@@ -201,4 +201,10 @@ export const api = {
|
||||
|
||||
// PDF/Bild-Beleg
|
||||
documentPreview: (formData) => request("/documents/preview", { method: "POST", formData }),
|
||||
|
||||
// Backup & Export
|
||||
downloadDbBackup: () => downloadFile("/backup/db", `certain-finance-${zeitstempel()}.db`),
|
||||
exportTransactionsCsv: () => downloadFile("/export/transactions.csv", `transaktionen_${zeitstempel()}.csv`),
|
||||
exportTransactionsJson: () => downloadFile("/export/transactions.json", `transaktionen_${zeitstempel()}.json`),
|
||||
exportAllJson: () => downloadFile("/export/all.json", `certain-finance-export_${zeitstempel()}.json`),
|
||||
};
|
||||
|
||||
+12
-1
@@ -72,7 +72,14 @@
|
||||
"dateFormat": "Datumsformat",
|
||||
"saved": "Gespeichert.",
|
||||
"languageDe": "Deutsch",
|
||||
"languageEn": "Englisch"
|
||||
"languageEn": "Englisch",
|
||||
"backup": "Backup & Export",
|
||||
"exportHint": "Exportiere deine eigenen Daten zum Archivieren oder Weiterverarbeiten.",
|
||||
"exportTxnCsv": "Buchungen (CSV)",
|
||||
"exportTxnJson": "Buchungen (JSON)",
|
||||
"exportAll": "Alle Daten (JSON)",
|
||||
"dbBackup": "Komplettes DB-Backup herunterladen",
|
||||
"dbBackupHint": "Vollständige Datenbank aller Nutzer (nur Admin) – zum Wiederherstellen sichern."
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Übersicht",
|
||||
@@ -234,6 +241,10 @@
|
||||
"colPurpose": "Verwendungszweck",
|
||||
"colIban": "Gegen-IBAN",
|
||||
"makePreviewHint": "Ordne mindestens Datum und Betrag zu.",
|
||||
"rawHint": "Nur die ersten Zeilen – sie dienen nur der Spaltenzuordnung.",
|
||||
"makePreviewNote": "„Vorschau erstellen“ zeigt danach ALLE Buchungen zum Prüfen und Buchen.",
|
||||
"summary": "{{total}} Zeilen erkannt · {{n}} werden gebucht (Duplikate/ungültige ausgenommen).",
|
||||
"noneValid": "Keine gültige Buchung erkannt. Prüfe Datumsformat, Dezimaltrenner und Spaltenzuordnung – am einfachsten oben die passende Bank-Vorlage wählen.",
|
||||
"commit": "{{n}} Buchungen übernehmen",
|
||||
"imported": "{{n}} Buchungen importiert.",
|
||||
"importedCount": "{{n}} gebucht",
|
||||
|
||||
+12
-1
@@ -72,7 +72,14 @@
|
||||
"dateFormat": "Date format",
|
||||
"saved": "Saved.",
|
||||
"languageDe": "German",
|
||||
"languageEn": "English"
|
||||
"languageEn": "English",
|
||||
"backup": "Backup & export",
|
||||
"exportHint": "Export your own data for archiving or further processing.",
|
||||
"exportTxnCsv": "Transactions (CSV)",
|
||||
"exportTxnJson": "Transactions (JSON)",
|
||||
"exportAll": "All data (JSON)",
|
||||
"dbBackup": "Download full DB backup",
|
||||
"dbBackupHint": "Complete database of all users (admin only) — keep it for restore."
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Overview",
|
||||
@@ -234,6 +241,10 @@
|
||||
"colPurpose": "Purpose",
|
||||
"colIban": "Counterparty IBAN",
|
||||
"makePreviewHint": "Map at least date and amount.",
|
||||
"rawHint": "Only the first rows – just for mapping the columns.",
|
||||
"makePreviewNote": "“Build preview” then shows ALL transactions to review and import.",
|
||||
"summary": "{{total}} rows detected · {{n}} will be imported (duplicates/invalid excluded).",
|
||||
"noneValid": "No valid transaction detected. Check date format, decimal separator and column mapping – easiest is to pick the matching bank preset above.",
|
||||
"commit": "Import {{n}} transactions",
|
||||
"imported": "Imported {{n}} transactions.",
|
||||
"importedCount": "{{n}} imported",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user