diff --git a/src/app/settings/[section]/page.tsx b/src/app/settings/[section]/page.tsx index 1210e2074..38e5fadae 100644 --- a/src/app/settings/[section]/page.tsx +++ b/src/app/settings/[section]/page.tsx @@ -11,7 +11,6 @@ import { ExportSection } from "@/components/settings/export-section"; import { IntegrationsSection } from "@/components/settings/integrations-section"; import { NotificationsSection } from "@/components/settings/notifications-section"; import { SectionPlaceholder } from "@/components/settings/section-placeholder"; -import { SharingSection } from "@/components/settings/sharing-section"; // v1.8.7.1 — `thresholds` (Targets) and `sources` (Sources) are two // separate sections again. `ThresholdsSection` renders the target-range // editor; `SourcesSection` (standalone mode) renders the source-priority @@ -53,7 +52,6 @@ const SECTION_COMPONENTS: Record< thresholds: ThresholdsSection, sources: () => , api: ApiSection, - sharing: SharingSection, export: ExportSection, advanced: AdvancedSection, }; diff --git a/src/components/settings/__tests__/arztbericht-hero-card.test.tsx b/src/components/settings/__tests__/arztbericht-hero-card.test.tsx deleted file mode 100644 index 06c1076cc..000000000 --- a/src/components/settings/__tests__/arztbericht-hero-card.test.tsx +++ /dev/null @@ -1,121 +0,0 @@ -/** - * Arztbericht (doctor-report) card contract. - * - * v1.4.37 W7a promoted this surface to the page hero; v1.12 demotes it - * back to a small secondary card at the bottom of `/settings/export` - * (the health-record export now owns the hero). The card stays fully - * functional — only the framing and visual weight changed. This suite - * pins the SSR contract: - * - * 1. The card renders with the expected `data-testid` slot. - * 2. The value statement is present (Marc-Voice copy, no AI mention). - * 3. The CTA button is present, clears the 44 px touch floor, and - * carries an `aria-describedby` link to the value statement. - * 4. The CTA is wired to a click handler (the prop `data-testid` - * surface is the e2e contract). - * 5. Both DE and EN locales resolve their copy without leaking the - * raw i18n key. - * - * Project convention is SSR-only tests (no `@testing-library/react`). - * The actual click-flow + PDF generation is exercised by the e2e - * Playwright path and the dialog's own integration tests. - */ - -import { describe, expect, it, vi } from "vitest"; -import { renderToStaticMarkup } from "react-dom/server"; - -vi.mock("next/navigation", () => ({ - useRouter: () => ({ push: vi.fn(), replace: vi.fn() }), - usePathname: () => "/settings/export", -})); - -import { I18nProvider } from "@/lib/i18n/context"; -import { ArztberichtHeroCard } from "../arztbericht-hero-card"; - -function renderSSR(node: React.ReactElement, locale: "en" | "de" = "en") { - return renderToStaticMarkup( - {node}, - ); -} - -describe(" — SSR contract", () => { - it("renders the card slot with eyebrow + title", () => { - const html = renderSSR(); - expect(html).toContain('data-testid="export-hero-doctor-report"'); - // Title falls back to the existing doctorReport.title key — keeps - // a single source-of-truth for the brand name. - expect(html).toContain("Doctor Report"); - // Eyebrow surfaces the use-case framing. - expect(html).toContain("Doctor visit"); - }); - - it("renders the value statement as a Marc-Voice one-liner", () => { - const html = renderSSR(); - expect(html).toContain('data-testid="export-hero-doctor-report-value"'); - expect(html).toContain("printable PDF report"); - // No AI/Coach name-drop in the user-facing artefact (Marc-Voice). - expect(html.toLowerCase()).not.toContain("coach"); - expect(html.toLowerCase()).not.toContain(" ai "); - }); - - it("renders the CTA with min-h-11 touch target + aria-describedby", () => { - const html = renderSSR(); - expect(html).toContain('data-testid="export-hero-doctor-report-action"'); - // 44 px touch floor (WCAG 2.5.5) — the demoted CTA keeps `min-h-11` - // on mobile and falls back to the compact `sm:min-h-9` on pointer - // devices. - expect(html).toContain("min-h-11"); - // Aria-describedby links the CTA to the value statement so screen - // readers announce the framing alongside the action label. - expect(html).toMatch(/aria-describedby="[^"]+"/); - }); - - it("does NOT leak raw i18n keys", () => { - const html = renderSSR(); - expect(html).not.toContain("settings.sections.export.hero"); - expect(html).not.toContain("settings.sections.export.cards.doctorReport"); - }); - - it("renders German copy when the DE locale is active", () => { - const html = renderSSR(, "de"); - expect(html).toContain("Arztbericht"); - expect(html).toContain("Arzttermin"); - // Umlauts encoded as their literal UTF-8 character (not their HTML - // entity), matching the rest of the codebase. - expect(html).toContain("druckbarer PDF-Bericht"); - expect(html).not.toContain("settings.sections.export.hero"); - }); - - it("renders as a plain secondary card, not a hero", () => { - const html = renderSSR(); - // v1.12 demoted the doctor-report from the page hero — the - // health-record export now owns the hero treatment, so this card - // must NOT carry the hero gradient/glow utilities. - expect(html).not.toContain("hero-gradient"); - expect(html).not.toContain("glow-purple"); - // It renders with the standard secondary-card surface. - expect(html).toContain("bg-card"); - }); - - it("does not paint dialog markup in the closed-state SSR pass", () => { - // Radix Dialog renders its portal content lazily, so the closed - // SSR pass never carries `role="dialog"`. The CTA testid IS - // present so the e2e suite can drive the open from there. Pinning - // this contract guards against an accidental `defaultOpen={true}` - // regression that would briefly paint the dialog above the page on - // every cold mount. - const html = renderSSR(); - expect(html).toContain('data-testid="export-hero-doctor-report-action"'); - expect(html).not.toContain('role="dialog"'); - }); -}); - -describe(" — heading hierarchy", () => { - it("uses an

so it nests cleanly below the page-level

", () => { - const html = renderSSR(); - // Page-level h1 (settings-section-export-title) wraps the section; - // hero owns the h2 slot. The export-section test pins the outer - // h1; this assertion pins the hero's contribution. - expect(html).toMatch(/]*id="export-hero-doctor-report-title"/); - }); -}); diff --git a/src/components/settings/__tests__/export-section.test.tsx b/src/components/settings/__tests__/export-section.test.tsx index 1cfdfd331..7f6496218 100644 --- a/src/components/settings/__tests__/export-section.test.tsx +++ b/src/components/settings/__tests__/export-section.test.tsx @@ -7,18 +7,16 @@ * - Medications CSV (with optional intake-history toggle) * - Mood CSV * - Full JSON Backup - * - Doctor Report (PDF) — small secondary card at the bottom (v1.12) * * Each card must surface a title, a 1-line description, and a * download/generate button. SSR-only smoke test — interaction is * exercised by the e2e suite. * - * v1.12 — the health-record export takes the page hero; the - * doctor-report card (``) is demoted to a small - * secondary card at the bottom of the page. The four CSV/JSON tiles - * stay under a "Weitere Export-Optionen" / "Other export options" - * sub-heading. Each component owns its own contract test; this suite - * pins the page-level shape. + * The health-record export takes the page hero. The doctor-report PDF + * now lives under the health-record export and is no longer offered as + * a separate card here. The four CSV/JSON tiles stay under a "Weitere + * Export-Optionen" / "Other export options" sub-heading. Each component + * owns its own contract test; this suite pins the page-level shape. */ import { describe, expect, it, vi } from "vitest"; import { renderToStaticMarkup } from "react-dom/server"; @@ -68,12 +66,11 @@ describe(" — SSR smoke", () => { expect(html).toContain("glow-purple"); }); - it("mounts the demoted doctor-report card", () => { + it("no longer mounts a separate doctor-report card", () => { const html = render(); - // The card owns its own contract suite; here we pin that the page - // mounts it, and that the legacy in-grid doctor-report card is - // gone. The card no longer carries the hero gradient/glow (v1.12). - expect(html).toContain('data-testid="export-hero-doctor-report"'); + // The doctor-report PDF now lives under the health-record export; + // the Export page no longer offers it as a separate surface. + expect(html).not.toContain('data-testid="export-hero-doctor-report"'); expect(html).not.toContain('data-testid="export-card-doctor-report"'); }); @@ -107,19 +104,16 @@ describe(" — SSR smoke", () => { expect(html).toContain('data-testid="export-medications-include-intake"'); }); - it("renders a download/generate button per secondary card + the hero CTA", () => { + it("renders a download/generate button per secondary card", () => { const html = render(); - // Four secondary cards each carry an `export-action-*` testid; the - // hero owns the `export-hero-doctor-report-action` slot. + // Four secondary cards each carry an `export-action-*` testid. const buttonMatches = html.match(/data-testid="export-action-/g); expect(buttonMatches?.length ?? 0).toBe(4); - expect(html).toContain('data-testid="export-hero-doctor-report-action"'); }); it("German locale renders the DE heading + hero copy", () => { const html = render(, "de"); expect(html).toContain("Export"); - expect(html).toContain("Arztbericht"); expect(html).toContain("Weitere Export-Optionen"); expect(html).not.toContain("settings.sections.export."); }); diff --git a/src/components/settings/__tests__/settings-shell.test.tsx b/src/components/settings/__tests__/settings-shell.test.tsx index 5584e3330..25e3fcb6b 100644 --- a/src/components/settings/__tests__/settings-shell.test.tsx +++ b/src/components/settings/__tests__/settings-shell.test.tsx @@ -48,8 +48,6 @@ describe("SETTINGS_SECTION_SLUGS", () => { // IW-D merged it into `thresholds`. // v1.8.7.1 — `sources` (Sources) is its own slug again, sitting // between `thresholds` (Targets) and `ai`. - // v1.11.0 — `sharing` (clinician share links) added between `api` and - // `export`. Section count: 12. expect([...SETTINGS_SECTION_SLUGS]).toEqual([ "account", "integrations", @@ -59,7 +57,6 @@ describe("SETTINGS_SECTION_SLUGS", () => { "sources", "ai", "api", - "sharing", "export", "advanced", "about", diff --git a/src/components/settings/__tests__/sharing-section.test.tsx b/src/components/settings/__tests__/sharing-section.test.tsx deleted file mode 100644 index f09fa06e4..000000000 --- a/src/components/settings/__tests__/sharing-section.test.tsx +++ /dev/null @@ -1,125 +0,0 @@ -/** - * v1.11.0 — Settings → Sharing (Epic C, C7). - * - * The owner share-link surface. The security-load-bearing guarantees the - * test pins: - * 1. The list never carries a raw `hls_` token — the server stores only the - * hash, so the list response shape has no token field and the rendered - * markup must not surface one. - * 2. The one-time token reveal card (`share-token-reveal`) is absent until a - * create succeeds — on first paint (no token in state) it must not render. - * 3. Active links render with a revoke control; revoked/expired links fold - * into the inactive list. - * 4. The expiry input is bounded at the server cap (90 days). - * - * SSR-static render only (no jsdom), matching the sibling settings-section - * tests. The TanStack hooks are mocked so the query returns a fixed link list. - */ -import { describe, expect, it, vi } from "vitest"; -import { renderToStaticMarkup } from "react-dom/server"; - -vi.mock("@/hooks/use-auth", () => ({ - useAuth: () => ({ - user: { id: "u1", role: "USER" }, - isAuthenticated: true, - isLoading: false, - refetch: vi.fn(), - }), -})); - -const FIXTURE_LINKS = [ - { - id: "link-active", - label: "Cardiology referral", - rangeStart: "2026-01-01T00:00:00.000Z", - rangeEnd: null, - resourceTypes: ["Patient", "Observation"], - allowFhirApi: true, - expiresAt: "2099-01-01T00:00:00.000Z", - createdAt: "2026-06-01T00:00:00.000Z", - revokedAt: null, - lastAccessAt: "2026-06-02T10:00:00.000Z", - accessCount: 3, - active: true, - }, - { - id: "link-revoked", - label: "Old GP link", - rangeStart: "2026-01-01T00:00:00.000Z", - rangeEnd: null, - resourceTypes: [], - allowFhirApi: false, - expiresAt: "2099-01-01T00:00:00.000Z", - createdAt: "2026-05-01T00:00:00.000Z", - revokedAt: "2026-05-15T00:00:00.000Z", - lastAccessAt: null, - accessCount: 0, - active: false, - }, -]; - -vi.mock("@tanstack/react-query", () => ({ - useQuery: () => ({ data: FIXTURE_LINKS, isLoading: false }), - useMutation: () => ({ mutate: vi.fn(), isPending: false }), - useQueryClient: () => ({ invalidateQueries: vi.fn() }), -})); - -import { I18nProvider } from "@/lib/i18n/context"; -import { SharingSection } from "../sharing-section"; - -function render(locale: "en" | "de" = "en") { - return renderToStaticMarkup( - - - , - ); -} - -describe(" — owner share-link surface (C7)", () => { - it("renders the create form with a 90-day-capped expiry input", () => { - const html = render(); - expect(html).toContain("New share link"); - // The expiry field carries the server's max-days cap. - expect(html).toContain('max="90"'); - expect(html).toContain("Create link"); - }); - - it("does not reveal a token before a create succeeds", () => { - const html = render(); - // The one-time reveal card only mounts once `newToken` state is set. - expect(html).not.toContain('data-testid="share-token-reveal"'); - }); - - it("lists the active link with a revoke control and never a raw token", () => { - const html = render(); - expect(html).toContain('data-testid="share-active-list"'); - expect(html).toContain("Cardiology referral"); - expect(html).toContain("Revoke"); - // The list response shape carries no token; the markup must not surface - // an `hls_` string anywhere. - expect(html).not.toContain("hls_"); - // The FHIR badge surfaces for the FHIR-enabled link. - expect(html).toContain("FHIR"); - }); - - it("folds revoked links into the inactive list (not the active list)", () => { - const html = render(); - // The active list must NOT contain the revoked link's label. - const activeListStart = html.indexOf('data-testid="share-active-list"'); - const activeListEnd = html.indexOf('data-testid="share-inactive-list"'); - // Inactive list is collapsed by default, so the label only appears once - // the disclosure is open — but the active list must never carry it. - const activeSlice = - activeListEnd > activeListStart - ? html.slice(activeListStart, activeListEnd) - : html.slice(activeListStart); - expect(activeSlice).not.toContain("Old GP link"); - // The inactive-count disclosure surfaces. - expect(html).toContain("Revoked and expired"); - }); - - it("renders localized section copy", () => { - const html = render("de"); - expect(html).toContain("Neuer Freigabe-Link"); - }); -}); diff --git a/src/components/settings/arztbericht-hero-card.tsx b/src/components/settings/arztbericht-hero-card.tsx deleted file mode 100644 index 7783457da..000000000 --- a/src/components/settings/arztbericht-hero-card.tsx +++ /dev/null @@ -1,178 +0,0 @@ -"use client"; - -/** - * Settings → Export Arztbericht (doctor-report) card. - * - * The doctor-report PDF is the artefact a user carries into a doctor's - * appointment. v1.4.37 W7a promoted it to the page hero; v1.12 demotes - * it back to a small secondary card at the bottom of the export page — - * the health-record export now owns the page hero. The card is fully - * functional: the CTA routes through the existing `` - * + `/api/doctor-report` flow exactly as before. Only the framing and - * visual weight changed. - * - * Layout (compact card, no hero gradient): - * - * ┌─ card ────────────────────────────────────────────┐ - * │ [icon] H2 title eyebrow │ - * │ one-line value statement │ - * │ [Configure & generate] PDF · printable │ - * └───────────────────────────────────────────────────┘ - * - * a11y: - * - The CTA button clears the 44 px touch floor (`min-h-11`). - * - `focus-visible:ring` ships via the default ` - - {t("settings.sections.export.hero.formatHint")} - - - - {error && ( -

- {error} -

- )} - - - - - ); -} diff --git a/src/components/settings/export-section.tsx b/src/components/settings/export-section.tsx index 4330603aa..c4d0be2a4 100644 --- a/src/components/settings/export-section.tsx +++ b/src/components/settings/export-section.tsx @@ -16,13 +16,11 @@ * 3. Medications CSV — optional intake-history toggle * 4. Mood CSV — optional `since`/`until` * 5. Full JSON Backup — single-file user-scoped dump - * 6. Doctor Report (PDF) — small secondary card at the bottom - * (``) * * v1.12 — the health-record export is the headline "data out" path; it - * takes the page hero. The doctor-report PDF is demoted to a small - * secondary card at the bottom of the page — fully functional, just no - * longer the flagship surface it was in v1.4.37 W7a. + * takes the page hero. The doctor-report PDF lives under the + * health-record export now and is no longer offered as a separate card + * here (it used to be a small secondary card at the bottom). * * Mobile-first: cards stack on `=md`. */ @@ -42,7 +40,6 @@ import { Button } from "@/components/ui/button"; import { DateInput } from "@/components/ui/date-input"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; -import { ArztberichtHeroCard } from "@/components/settings/arztbericht-hero-card"; import { HealthRecordExportPanel } from "@/components/settings/health-record-export-panel"; import { useTranslations } from "@/lib/i18n/context"; @@ -114,11 +111,6 @@ export function ExportSection() { - - {/* v1.12 — doctor-report PDF, demoted from the former page hero to a - small secondary card at the bottom. Fully functional — the same - `` + `/api/doctor-report` flow runs from here. */} - ); } diff --git a/src/components/settings/health-record-export-panel.tsx b/src/components/settings/health-record-export-panel.tsx index cefedf981..54fe6e37c 100644 --- a/src/components/settings/health-record-export-panel.tsx +++ b/src/components/settings/health-record-export-panel.tsx @@ -433,7 +433,7 @@ export function HealthRecordExportPanel() {

)} -
+
- - - {newToken && ( -
-

- {t("settings.sharing.tokenCreated")} -

-

- {t("settings.sharing.tokenOnce")} -

-
- - {`${typeof window !== "undefined" ? window.location.origin : ""}/c/${newToken}`} - - -
- {copied && ( -

- {t("settings.sharing.copied")} -

- )} -
- )} - -
-

- {t("settings.sharing.activeTitle")} -

- {activeLinks.length === 0 ? ( -

- {t("settings.sharing.noActive")} -

- ) : ( -
    - {activeLinks.map((link) => ( -
  • -
    -

    - {link.label} -

    -
    - - {t("settings.sharing.statusActive")} - - {link.allowFhirApi && ( - - FHIR - - )} -
    -
    -

    - - {t("settings.sharing.created")}: - {" "} - {formatDate(link.createdAt)} -

    -

    - - {t("settings.sharing.expires")}: - {" "} - {formatDateTime(link.expiresAt)} -

    -

    - - {t("settings.sharing.accessCount")}: - {" "} - {link.accessCount} - {link.lastAccessAt - ? ` · ${formatDateTime(link.lastAccessAt)}` - : ""} -

    - - - - - - - - {t("settings.sharing.revoke")} - - - {t("settings.sharing.revokeDescription")} - - - - - {t("common.cancel")} - - revokeMutation.mutate(link.id)} - > - {t("settings.sharing.revoke")} - - - - -
  • - ))} -
- )} -
- - {inactiveLinks.length > 0 && ( -
- - {showRevoked && ( -
    - {inactiveLinks.map((link) => ( -
  • -
    -

    - {link.label} -

    - - {link.revokedAt - ? t("settings.sharing.statusRevoked") - : t("settings.sharing.statusExpired")} - -
    -

    - - {t("settings.sharing.accessCount")}: - {" "} - {link.accessCount} -

    -
  • - ))} -
- )} -
- )} -
- ); -}