From 1714b788d26937a8ba558835ba510dac8f974087 Mon Sep 17 00:00:00 2001
From: mAi
Date: Mon, 25 May 2026 13:33:14 +0200
Subject: [PATCH] =?UTF-8?q?feat(projects-detail):=20t-paliad-245=20?=
=?UTF-8?q?=E2=80=94=20demote=20Daten=20Export=20into=20Verwaltung=20tab?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
m/paliad#76. The export button no longer pokes out of the tabs nav with a
non-tab styling — instead it lives inside a new "Verwaltung" tab (last in
the project tab list) as a normal section with heading, description, and a
plain btn-secondary trigger. Same gate as before (canExportProject).
Archive co-locates in the same tab as a pointer to the Edit-modal danger
zone: click "Bearbeiten öffnen" → modal opens scrolled to the archive
button. Single source of truth for the destructive action stays in the
modal; the Verwaltung pointer just gives it discoverability.
If neither sub-section is visible to the caller (no export entitlement,
not global_admin), the Verwaltung tab hides itself — an empty tab is
worse UX than no tab.
---
frontend/src/client/i18n.ts | 12 ++++++
frontend/src/client/projects-detail.ts | 57 ++++++++++++++++++++------
frontend/src/i18n-keys.ts | 6 +++
frontend/src/projects-detail.tsx | 50 +++++++++++++++-------
frontend/src/styles/global.css | 14 +++++++
5 files changed, 112 insertions(+), 27 deletions(-)
diff --git a/frontend/src/client/i18n.ts b/frontend/src/client/i18n.ts
index 5b03e60..c85bbc9 100644
--- a/frontend/src/client/i18n.ts
+++ b/frontend/src/client/i18n.ts
@@ -1426,8 +1426,14 @@ const translations: Record> = {
"projects.detail.tab.notizen": "Notizen",
"projects.detail.tab.checklisten": "Checklisten",
"projects.detail.tab.submissions": "Schriftsätze",
+ "projects.detail.tab.settings": "Verwaltung",
"projects.detail.export.button": "Daten exportieren",
"projects.detail.export.tooltip": "Daten dieses Projekts (mit Unter-Projekten) als Excel + JSON + CSV herunterladen.",
+ "projects.detail.settings.export.heading": "Daten exportieren",
+ "projects.detail.settings.export.description": "Lade alle Daten dieses Projekts (inkl. Unter-Projekten) als Excel + JSON + CSV-Archiv herunter.",
+ "projects.detail.settings.archive.heading": "Projekt archivieren",
+ "projects.detail.settings.archive.description": "Archivieren erfolgt aus dem Bearbeiten-Dialog (Gefahrenbereich).",
+ "projects.detail.settings.archive.cta": "Bearbeiten öffnen",
"projects.detail.submissions.empty": "Es sind aktuell keine Schriftsatzvorlagen hinterlegt.",
"projects.detail.submissions.empty.no_proceeding": "Für dieses Projekt ist noch kein Verfahrenstyp gesetzt — der Katalog unten zeigt trotzdem alle Vorlagen.",
"projects.detail.submissions.empty.no_proceeding.cta": "Projekt bearbeiten",
@@ -4347,8 +4353,14 @@ const translations: Record> = {
"projects.detail.tab.notizen": "Notes",
"projects.detail.tab.checklisten": "Checklists",
"projects.detail.tab.submissions": "Submissions",
+ "projects.detail.tab.settings": "Settings",
"projects.detail.export.button": "Export data",
"projects.detail.export.tooltip": "Download this project's data (including sub-projects) as Excel + JSON + CSV.",
+ "projects.detail.settings.export.heading": "Export data",
+ "projects.detail.settings.export.description": "Download all data for this project (including sub-projects) as an Excel + JSON + CSV archive.",
+ "projects.detail.settings.archive.heading": "Archive project",
+ "projects.detail.settings.archive.description": "Archiving happens in the edit dialog (danger zone).",
+ "projects.detail.settings.archive.cta": "Open edit dialog",
"projects.detail.submissions.empty": "No submission templates are configured yet.",
"projects.detail.submissions.empty.no_proceeding": "No proceeding type is set for this project yet — the catalog below still lists every template.",
"projects.detail.submissions.empty.no_proceeding.cta": "Edit project",
diff --git a/frontend/src/client/projects-detail.ts b/frontend/src/client/projects-detail.ts
index b0f860d..1f0116d 100644
--- a/frontend/src/client/projects-detail.ts
+++ b/frontend/src/client/projects-detail.ts
@@ -175,7 +175,8 @@ type TabId =
| "appointments"
| "notes"
| "checklists"
- | "submissions";
+ | "submissions"
+ | "settings";
const VALID_TABS: TabId[] = [
"history",
@@ -187,6 +188,7 @@ const VALID_TABS: TabId[] = [
"notes",
"checklists",
"submissions",
+ "settings",
];
// Legacy German tab slugs that may appear in bookmarked URLs after the
@@ -1185,13 +1187,16 @@ function renderHeader() {
netdocs.style.display = "none";
}
- // Delete visibility: partner/admin only
+ // Delete visibility: partner/admin only. The Verwaltung tab's archive
+ // sub-section mirrors the same gate (t-paliad-245) — it only points at
+ // the Edit-modal danger zone, so it's pointless to show when the danger
+ // zone itself is hidden.
const deleteWrap = document.getElementById("project-delete-wrap")!;
- if (me && (me.global_role === "global_admin")) {
- deleteWrap.style.display = "";
- } else {
- deleteWrap.style.display = "none";
- }
+ const archiveSection = document.getElementById("project-settings-archive");
+ const canArchive = !!me && me.global_role === "global_admin";
+ deleteWrap.style.display = canArchive ? "" : "none";
+ if (archiveSection) archiveSection.style.display = canArchive ? "" : "none";
+ updateSettingsTabVisibility();
}
// wrapEventTitleLink — kept for the dashboard activity feed which reuses
@@ -2045,6 +2050,17 @@ function initEditModal() {
});
}
+ // Verwaltung → Projekt archivieren — opens the edit modal scrolled to
+ // the danger-zone archive button (t-paliad-245).
+ const archiveLink = document.getElementById(
+ "project-settings-archive-link",
+ ) as HTMLButtonElement | null;
+ if (archiveLink) {
+ archiveLink.addEventListener("click", () => {
+ openEditModal("project-delete-btn");
+ });
+ }
+
form.addEventListener("submit", async (e) => {
e.preventDefault();
if (!project) return;
@@ -2991,17 +3007,21 @@ function canExportProject(): boolean {
);
}
-// wireExportButton reveals + hooks up the project-export button on the
-// tabs nav. Triggers a download via a transient — same
-// pattern as the personal export in client/settings.ts.
+// wireExportButton reveals the Export sub-section of the Verwaltung tab
+// (t-paliad-245) and hooks up the project-export button. Triggers a
+// download via a transient — same pattern as the personal
+// export in client/settings.ts.
function wireExportButton(projectID: string): void {
+ const section = document.getElementById("project-settings-export") as HTMLElement | null;
const btn = document.getElementById("project-export-btn") as HTMLButtonElement | null;
- if (!btn) return;
+ if (!section || !btn) return;
if (!canExportProject()) {
- btn.style.display = "none";
+ section.style.display = "none";
+ updateSettingsTabVisibility();
return;
}
- btn.style.display = "";
+ section.style.display = "";
+ updateSettingsTabVisibility();
btn.addEventListener("click", () => {
const a = document.createElement("a");
a.href = `/api/projects/${encodeURIComponent(projectID)}/export`;
@@ -3012,6 +3032,17 @@ function wireExportButton(projectID: string): void {
});
}
+// updateSettingsTabVisibility hides the Verwaltung tab when none of its
+// sub-sections are visible to the current user — an empty tab is worse
+// UX than no tab. Called whenever a sub-section's visibility flips.
+function updateSettingsTabVisibility(): void {
+ const tab = document.querySelector('.entity-tab[data-tab="settings"]');
+ if (!tab) return;
+ const exportShown = document.getElementById("project-settings-export")?.style.display !== "none";
+ const archiveShown = document.getElementById("project-settings-archive")?.style.display !== "none";
+ tab.style.display = exportShown || archiveShown ? "" : "none";
+}
+
function canRemoveTeamMember(m: ProjectTeamMember): boolean {
if (!me) return false;
if (m.user_id === me.id) return true;
diff --git a/frontend/src/i18n-keys.ts b/frontend/src/i18n-keys.ts
index de511f0..4528ce9 100644
--- a/frontend/src/i18n-keys.ts
+++ b/frontend/src/i18n-keys.ts
@@ -2188,6 +2188,11 @@ export type I18nKey =
| "projects.detail.parteien.role.defendant"
| "projects.detail.parteien.role.thirdparty"
| "projects.detail.save"
+ | "projects.detail.settings.archive.cta"
+ | "projects.detail.settings.archive.description"
+ | "projects.detail.settings.archive.heading"
+ | "projects.detail.settings.export.description"
+ | "projects.detail.settings.export.heading"
| "projects.detail.smarttimeline.add.cancel"
| "projects.detail.smarttimeline.add.choice.amend"
| "projects.detail.smarttimeline.add.choice.appointment"
@@ -2277,6 +2282,7 @@ export type I18nKey =
| "projects.detail.tab.kinder"
| "projects.detail.tab.notizen"
| "projects.detail.tab.parteien"
+ | "projects.detail.tab.settings"
| "projects.detail.tab.submissions"
| "projects.detail.tab.team"
| "projects.detail.tab.termine"
diff --git a/frontend/src/projects-detail.tsx b/frontend/src/projects-detail.tsx
index dd1ff70..74f07f6 100644
--- a/frontend/src/projects-detail.tsx
+++ b/frontend/src/projects-detail.tsx
@@ -89,20 +89,9 @@ export function renderProjectsDetail(): string {
Notizen
Checklisten
Schriftsätze
- {/* t-paliad-214 Slice 2 — project-subtree export button.
- Sits at the end of the tab nav. Hidden by default; the
- client unhides it after /api/me confirms the caller can
- extract (responsibility ∈ {lead, member} OR global_admin). */}
-
+ {/* Verwaltung — rare admin actions (export, archive). Sits
+ last in the tab list per t-paliad-245. */}
+ Verwaltung
{/* History (Verlauf) — t-paliad-171 SmartTimeline Slice 1.
@@ -666,6 +655,39 @@ export function renderProjectsDetail(): string {
Schriftsätze werden direkt aus dem Projekt heraus als .docx generiert. Anpassen, drucken, einreichen.
+
+ {/* Verwaltung — rare admin actions (export, archive). Each
+ sub-section hides itself if the caller is not entitled
+ (export: §4 gate; archive: global_admin). */}
+
+
+
Daten exportieren
+
+ Lade alle Daten dieses Projekts (inkl. Unter-Projekten) als Excel + JSON + CSV-Archiv herunter.
+
+
+
+
+
+
Projekt archivieren
+
+ Archivieren erfolgt aus dem Bearbeiten-Dialog (Gefahrenbereich).
+
+
+
+
{/* Full edit modal — same form as /projects/new, pre-filled. */}
diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css
index f00ef82..d22c866 100644
--- a/frontend/src/styles/global.css
+++ b/frontend/src/styles/global.css
@@ -7291,6 +7291,20 @@ dialog.modal::backdrop {
padding: 0.5rem 0 2rem;
}
+/* Verwaltung tab — rare admin actions (export, archive) live here as
+ stacked sections. No accent, no oversized buttons (t-paliad-245). */
+.settings-section {
+ margin-bottom: 2rem;
+}
+
+.settings-section:last-child {
+ margin-bottom: 0;
+}
+
+.settings-section .tool-subtitle {
+ margin-bottom: 0.75rem;
+}
+
.entity-events {
list-style: none;
padding: 0;