diff --git a/e2e/doctor-report.spec.ts b/e2e/doctor-report.spec.ts
index e9524688d..3abf401a7 100644
--- a/e2e/doctor-report.spec.ts
+++ b/e2e/doctor-report.spec.ts
@@ -4,14 +4,16 @@ import { STORAGE_STATE_PATH } from "./setup/global-setup";
/**
* Doctor-report PDF flow. The real flow:
- * 1. User clicks "Doctor report (PDF)" on `/settings/advanced`
- * 2. Frontend POSTs `/api/doctor-report` to fetch aggregated data
- * 3. Frontend dynamic-imports `src/lib/doctor-report-pdf.ts` and
+ * 1. User clicks "Configure & generate" on `/settings/export`
+ * (relocated from `/settings/advanced` in v1.4.16 phase B7)
+ * 2. The dialog opens; user picks dates and submits.
+ * 3. Frontend POSTs `/api/doctor-report` to fetch aggregated data
+ * 4. Frontend dynamic-imports `src/lib/doctor-report-pdf.ts` and
* generates the PDF client-side via jsPDF; calls `doc.save(...)`
* which triggers a download.
*
- * This spec mocks step 2 (so we don't need 90 days of seeded data) and
- * captures step 3's download via Playwright's download API. We then
+ * This spec mocks step 3 (so we don't need 90 days of seeded data) and
+ * captures step 4's download via Playwright's download API. We then
* confirm the file is non-empty and starts with `%PDF` magic bytes.
*/
test.describe("doctor report PDF generation", () => {
@@ -59,19 +61,24 @@ test.describe("doctor report PDF generation", () => {
pageErrors.push(`console.error: ${msg.text()}`);
});
- await page.goto("/settings/advanced", { waitUntil: "domcontentloaded" });
+ await page.goto("/settings/export", { waitUntil: "domcontentloaded" });
- // The button label varies by locale; match the EN string but keep
- // the regex permissive so a future i18n rename still passes if the
- // word "report" survives.
- const reportBtn = page.getByRole("button", {
- name: /doctor report|arztbericht|gesundheitsbericht/i,
- });
+ // The card carries a stable testid so the e2e suite can target it
+ // without depending on the localised "Configure & generate" label.
+ const reportBtn = page.getByTestId("export-action-doctor-report");
await expect(reportBtn).toBeVisible({ timeout: 10_000 });
- // Trigger the download and capture the resulting Download object.
+ // Click opens the dialog; the dialog's submit button triggers the PDF
+ // generation. Trigger the download and capture the resulting Download
+ // object once the user confirms.
const downloadPromise = page.waitForEvent("download", { timeout: 30_000 });
await reportBtn.click();
+ // Submit the dialog with the default dates (last 90 days).
+ const submitBtn = page.getByRole("button", {
+ name: /generate pdf|pdf generieren/i,
+ });
+ await expect(submitBtn).toBeVisible({ timeout: 5_000 });
+ await submitBtn.click();
const download = await downloadPromise.catch((err) => {
throw new Error(
`Download did not fire. Page errors:\n${pageErrors.join("\n")}\n\nOriginal: ${err}`,
diff --git a/src/components/settings/__tests__/sections.test.tsx b/src/components/settings/__tests__/sections.test.tsx
index 4f2769e8c..51e6ff0db 100644
--- a/src/components/settings/__tests__/sections.test.tsx
+++ b/src/components/settings/__tests__/sections.test.tsx
@@ -155,9 +155,15 @@ describe("settings sections — SSR smoke", () => {
expect(html).toContain("API & Tokens");
});
- it(" renders export + danger-zone cards", () => {
+ it(" renders the danger-zone card only (export moved to /settings/export)", () => {
const html = render();
+ // v1.4.16 phase B7: every export path moved out into the dedicated
+ // `` so what remains is the irreversible delete-all
+ // path. The section heading + dangerZone surface are the only
+ // primary surfaces here now.
expect(html).toContain("Advanced");
+ expect(html).not.toContain("settings.export");
+ expect(html).not.toContain("doctorReport");
});
it(" resolves about.* keys in German", () => {
diff --git a/src/components/settings/advanced-section.tsx b/src/components/settings/advanced-section.tsx
index 675743833..70d4c373e 100644
--- a/src/components/settings/advanced-section.tsx
+++ b/src/components/settings/advanced-section.tsx
@@ -1,8 +1,8 @@
"use client";
-import { useEffect, useState } from "react";
+import { useState } from "react";
import { useQueryClient } from "@tanstack/react-query";
-import { AlertTriangle, Download, Loader2, Trash2 } from "lucide-react";
+import { AlertTriangle, Loader2, Trash2 } from "lucide-react";
import {
AlertDialog,
@@ -16,12 +16,17 @@ import {
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
-import {
- DoctorReportDialog,
- type DoctorReportSubmitPayload,
-} from "@/components/doctor-report/doctor-report-dialog";
import { useTranslations } from "@/lib/i18n/context";
+/**
+ * Settings → Advanced.
+ *
+ * v1.4.16 phase B7: every export path (CSV, JSON, doctor-report PDF)
+ * moved out into the dedicated `` under `/settings/export`.
+ * What stays here is the irreversible danger-zone — the "wipe all my
+ * data" surface that should never live next to a single-click export
+ * button.
+ */
export function AdvancedSection() {
const { t } = useTranslations();
@@ -42,139 +47,11 @@ export function AdvancedSection() {
-
);
}
-function ExportCard() {
- const { t, locale } = useTranslations();
- const [exporting, setExporting] = useState(false);
- const [generatingReport, setGeneratingReport] = useState(false);
- const [reportDialogOpen, setReportDialogOpen] = useState(false);
- const [lastPracticeName, setLastPracticeName] = useState(null);
-
- // Fetch the previously-stored practice name so the dialog can pre-fill it.
- // `useEffect` is the right place for a one-shot side-effect on mount; the
- // ESLint `set-state-in-effect` rule is fine here because we are not
- // setting state on every render — we are reacting to a fetch.
- useEffect(() => {
- let cancelled = false;
- fetch("/api/auth/me")
- .then((res) => (res.ok ? res.json() : null))
- .then((json) => {
- if (cancelled || !json) return;
- const name = json?.data?.lastReportPracticeName;
- if (typeof name === "string") setLastPracticeName(name);
- })
- .catch(() => {
- // Best-effort; the dialog still works without the pre-fill.
- });
- return () => {
- cancelled = true;
- };
- }, []);
-
- async function handleExport(format: "csv" | "json") {
- setExporting(true);
- try {
- const res = await fetch(`/api/export?format=${format}&type=all`);
- if (!res.ok) return;
-
- const blob = await res.blob();
- const url = URL.createObjectURL(blob);
- const a = document.createElement("a");
- a.href = url;
- a.download = `healthlog-export-${new Date().toISOString().slice(0, 10)}.${format}`;
- document.body.appendChild(a);
- a.click();
- a.remove();
- URL.revokeObjectURL(url);
- } finally {
- setExporting(false);
- }
- }
-
- async function handleDoctorReport(payload: DoctorReportSubmitPayload) {
- setGeneratingReport(true);
- try {
- const res = await fetch("/api/doctor-report", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- startDate: payload.startDate,
- endDate: payload.endDate,
- practiceName: payload.practiceName,
- }),
- });
- if (!res.ok) return;
- const json = await res.json();
-
- const { generateDoctorReportPDF } =
- await import("@/lib/doctor-report-pdf");
- const doc = generateDoctorReportPDF(json.data, { t, locale });
- const fileSlug = locale === "de" ? "gesundheitsbericht" : "health-report";
- doc.save(`${fileSlug}-${new Date().toISOString().slice(0, 10)}.pdf`);
- // Remember the last-used practice name so the next open of the dialog
- // pre-fills it without a roundtrip to /api/auth/me.
- if (payload.practiceName) setLastPracticeName(payload.practiceName);
- setReportDialogOpen(false);
- } finally {
- setGeneratingReport(false);
- }
- }
-
- return (
-
-
-
-
{t("settings.export")}
-
-
- {t("settings.exportDescription")}
-
-
-
-
-
-
-
-
- );
-}
-
function DataResetCard() {
const { t } = useTranslations();
const queryClient = useQueryClient();