From 7fb3538a8ce996e27bce5e9a77a44ee32ff69a2a Mon Sep 17 00:00:00 2001 From: mAi Date: Tue, 19 May 2026 12:51:23 +0200 Subject: [PATCH] =?UTF-8?q?feat(export):=20t-paliad-214=20Slice=201=20fron?= =?UTF-8?q?tend=20=E2=80=94=20Datenexport=20tab=20on=20/settings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a 4th tab "Datenexport" to /settings (after Profil / Benachrichtigungen / CalDAV) with a single-button card that triggers GET /api/me/export. Browser handles the download via Content-Disposition: attachment. i18n: 12 new keys under einstellungen.export.* (DE primary, EN secondary) — subtitle, bullets per format, scope notice, audit notice, button label, post-click hint. The tab is loaded lazily (idempotent loadExportTab) like every other settings tab, and the runExport handler swaps in a transient to use the browser's normal download pipeline. --- frontend/src/client/i18n.ts | 22 +++++++++++++++ frontend/src/client/settings.ts | 49 +++++++++++++++++++++++++++++++-- frontend/src/i18n-keys.ts | 11 ++++++++ frontend/src/settings.tsx | 44 +++++++++++++++++++++++++++++ 4 files changed, 124 insertions(+), 2 deletions(-) diff --git a/frontend/src/client/i18n.ts b/frontend/src/client/i18n.ts index e285921..78d75ec 100644 --- a/frontend/src/client/i18n.ts +++ b/frontend/src/client/i18n.ts @@ -1126,6 +1126,17 @@ const translations: Record> = { "einstellungen.tab.profil": "Profil", "einstellungen.tab.benachrichtigungen": "Benachrichtigungen", "einstellungen.tab.caldav": "CalDAV", + "einstellungen.tab.export": "Datenexport", + "einstellungen.export.subtitle": "Laden Sie Ihre pers\u00f6nlichen Paliad-Daten als Excel- + JSON- + CSV-Paket herunter. Enthalten ist alles, was Sie aktuell sehen k\u00f6nnen \u2014 Ihre Projekte, Fristen, Termine, Notizen, Genehmigungen und Einstellungen.", + "einstellungen.export.heading": "Pers\u00f6nlicher Datenexport", + "einstellungen.export.what": "Das Paket enth\u00e4lt Ihre sichtbaren Daten in drei Formaten in einem .zip:", + "einstellungen.export.bullet.xlsx": "paliad-export.xlsx \u2014 eine Excel-Mappe pro Entit\u00e4t.", + "einstellungen.export.bullet.json": "paliad-export.json \u2014 maschinenlesbare Kopie f\u00fcr Skripte und Tools.", + "einstellungen.export.bullet.csv": "csv/.csv \u2014 Tabellen einzeln als CSV (UTF-8 mit BOM).", + "einstellungen.export.scope": "Umfang: alles, was Sie aktuell in Paliad sehen k\u00f6nnen (Sichtbarkeit zum Zeitpunkt des Exports). Passw\u00f6rter, CalDAV-Zugangsdaten und andere Geheimnisse werden nie exportiert.", + "einstellungen.export.audit": "Jeder Export wird im Audit-Log protokolliert.", + "einstellungen.export.button": "Daten exportieren", + "einstellungen.export.started": "Download gestartet. Falls nichts passiert, pr\u00fcfen Sie Ihren Browser-Downloadordner.", "projects.title": "Projekte \u2014 Paliad", "projects.heading": "Projekte", "projects.subtitle": "Mandanten, Streitsachen, Patente und Verfahren \u2014 hierarchisch organisiert.", @@ -3702,6 +3713,17 @@ const translations: Record> = { "einstellungen.tab.profil": "Profile", "einstellungen.tab.benachrichtigungen": "Notifications", "einstellungen.tab.caldav": "CalDAV", + "einstellungen.tab.export": "Data export", + "einstellungen.export.subtitle": "Download your personal Paliad data as an Excel + JSON + CSV bundle. The package contains everything you can currently see \u2014 your projects, deadlines, appointments, notes, approvals and settings.", + "einstellungen.export.heading": "Personal data export", + "einstellungen.export.what": "The package contains your visible data in three formats in one .zip:", + "einstellungen.export.bullet.xlsx": "paliad-export.xlsx \u2014 one Excel sheet per entity.", + "einstellungen.export.bullet.json": "paliad-export.json \u2014 machine-readable copy for scripts and tools.", + "einstellungen.export.bullet.csv": "csv/.csv \u2014 individual tables as CSV (UTF-8 with BOM).", + "einstellungen.export.scope": "Scope: everything you can currently see in Paliad (visibility at the moment of export). Passwords, CalDAV credentials and other secrets are never exported.", + "einstellungen.export.audit": "Every export is logged in the audit log.", + "einstellungen.export.button": "Export data", + "einstellungen.export.started": "Download started. If nothing happens, check your browser's downloads folder.", "projects.title": "Projects \u2014 Paliad", "projects.heading": "Projects", "projects.subtitle": "Clients, litigations, patents and cases \u2014 organised hierarchically.", diff --git a/frontend/src/client/settings.ts b/frontend/src/client/settings.ts index 46f3f08..71093b2 100644 --- a/frontend/src/client/settings.ts +++ b/frontend/src/client/settings.ts @@ -51,8 +51,8 @@ interface SyncLogEntry { duration_ms?: number; } -type TabName = "profil" | "benachrichtigungen" | "caldav"; -const TABS: TabName[] = ["profil", "benachrichtigungen", "caldav"]; +type TabName = "profil" | "benachrichtigungen" | "caldav" | "export"; +const TABS: TabName[] = ["profil", "benachrichtigungen", "caldav", "export"]; const DEFAULT_TAB: TabName = "profil"; let me: Me | null = null; @@ -115,6 +115,7 @@ function showTab(tab: TabName, pushHistory: boolean) { if (tab === "profil") void loadProfilTab(); else if (tab === "benachrichtigungen") void loadPrefsTab(); else if (tab === "caldav") void loadCalDAVTab(); + else if (tab === "export") void loadExportTab(); } } @@ -662,6 +663,48 @@ async function renderMyPartnerUnits(): Promise { } } +// --- Export tab (t-paliad-214 Slice 1) ------------------------------------- + +// Personal data export. One button; on click hits GET /api/me/export and the +// browser handles the download via Content-Disposition. We use an anchor + +// hidden iframe pattern so any non-200 response can surface inline instead +// of silently triggering a save dialog with an error-html body. +async function loadExportTab(): Promise { + // Nothing to fetch on render; the tab is static text + button. Wired in + // the DOMContentLoaded handler. +} + +function runExport(): void { + const msg = document.getElementById("export-msg"); + const btn = document.getElementById("export-btn") as HTMLButtonElement | null; + if (msg) msg.textContent = ""; + if (btn) btn.disabled = true; + // Trigger a navigation to the endpoint. The server sets + // Content-Disposition: attachment which the browser respects. + // We use a transient so the click goes through the + // normal download path even on browsers that try to render text/json. + const a = document.createElement("a"); + a.href = "/api/me/export"; + // download="" tells the browser to keep the server-provided filename + // when one is set via Content-Disposition. + a.download = ""; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + // Re-enable after a short timeout so users can re-trigger if needed. + // We don't try to detect download completion — there's no portable + // browser API for it. + if (btn) { + setTimeout(() => { + btn.disabled = false; + if (msg) + msg.textContent = + t("einstellungen.export.started") || + "Download gestartet. Falls nichts passiert, prüfen Sie Ihren Browser-Downloadordner."; + }, 500); + } +} + // --- Init ------------------------------------------------------------------- document.addEventListener("DOMContentLoaded", () => { @@ -674,6 +717,8 @@ document.addEventListener("DOMContentLoaded", () => { document.getElementById("caldav-form")!.addEventListener("submit", saveCalDAV); document.getElementById("caldav-test-btn")!.addEventListener("click", testCalDAVConnection); document.getElementById("caldav-delete-btn")!.addEventListener("click", deleteCalDAVConfig); + const exportBtn = document.getElementById("export-btn"); + if (exportBtn) exportBtn.addEventListener("click", runExport); onLangChange(() => { if (loadedTabs.has("profil")) renderOfficeOptions(); diff --git a/frontend/src/i18n-keys.ts b/frontend/src/i18n-keys.ts index 85dadb6..2cf22ea 100644 --- a/frontend/src/i18n-keys.ts +++ b/frontend/src/i18n-keys.ts @@ -1229,6 +1229,16 @@ export type I18nKey = | "downloads.subtitle" | "downloads.title" | "einstellungen.error.generic" + | "einstellungen.export.audit" + | "einstellungen.export.bullet.csv" + | "einstellungen.export.bullet.json" + | "einstellungen.export.bullet.xlsx" + | "einstellungen.export.button" + | "einstellungen.export.heading" + | "einstellungen.export.scope" + | "einstellungen.export.started" + | "einstellungen.export.subtitle" + | "einstellungen.export.what" | "einstellungen.heading" | "einstellungen.loading" | "einstellungen.optional" @@ -1272,6 +1282,7 @@ export type I18nKey = | "einstellungen.subtitle" | "einstellungen.tab.benachrichtigungen" | "einstellungen.tab.caldav" + | "einstellungen.tab.export" | "einstellungen.tab.profil" | "einstellungen.title" | "event.description.appointment_approval_approved" diff --git a/frontend/src/settings.tsx b/frontend/src/settings.tsx index 516d6bd..3146722 100644 --- a/frontend/src/settings.tsx +++ b/frontend/src/settings.tsx @@ -40,6 +40,7 @@ export function renderSettings(): string { Profil Benachrichtigungen CalDAV + Datenexport {/* --- Profil tab ---------------------------------------- */} @@ -342,6 +343,49 @@ export function renderSettings(): string { + {/* --- Datenexport tab (t-paliad-214 Slice 1) ----------- */} + +