feat(documents): the vault surface

The page becomes one wide, calm filing-cabinet view: PageHeader with an
Upload action, a drop-target upload zone whose accept list and per-file
cap come from the usage endpoint (quota bar only above 80 %), a sticky
filter rail (200 ms debounced search focused by '/', nine type chips,
condition chips from linked episodes, a year segmenter, one-tap clear),
and the virtualized timeline below. Filter state lives in the URL so
every view is shareable and back-button-safe; uploads filtered to an
episode arrive pre-linked. Hovering a card prefetches detail metadata —
never the blob. Designed states: a skeleton grid that matches the loaded
layout, a teaching empty state, a distinct no-matches state, and a
retryable error card. /documents joins the shell's wide-container list;
the shell stays the single owner of the page frame. Locale bundles gain
the vault vocabulary in all six languages and drop the retired
extraction-review strings.

Plan-Task: W2-T1, W2-T4 (zone), W2-T5 (prefetch), W2-T6
This commit is contained in:
Marc-André Bombeck committed 2026-07-07 12:26:06 +02:00
1 parent 4014d77a21
commit b8009d2e73
12 files changed
+1268 -754

No files matched your search

+12 -39
View File
@@ -1,45 +1,18 @@
"use client";
import { Suspense } from "react";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
import { Loader2 } from "lucide-react";
import { useAuth } from "@/hooks/use-auth";
import { InboundDocumentsView } from "@/components/documents/inbound-documents-view";
import { DocumentsView } from "@/components/documents/documents-view";
/**
* v1.25.0 (W-DOCS-IN) — inbound clinical documents entry.
*
* Born-gated on the resolved `modules.inboundDocuments` flag from
* `GET /api/auth/me` (opt-in / default-off — the per-user opt-in AND the
* operator server-wide kill-switch). An unauthenticated visitor is bounced to
* login; an authenticated account without the module opted in is bounced home
* (the nav entry is already hidden for them, so this only catches a direct URL
* hit). Every `/api/documents/inbound/*` route also enforces the gate
* server-side, so this is a UX redirect, not the security boundary.
* The Dokumente vault — server shell only. The client surface owns the
* module gate, the URL-backed filter state (which needs `useSearchParams`,
* hence the Suspense boundary), the virtualized timeline, and the upload
* queue. The route rides the AuthShell's wide-container flag: the shell
* owns the page frame, this file never re-implements paddings.
*/
export default function DocumentsPage() {
const { user, isLoading, isAuthenticated } = useAuth();
const router = useRouter();
const enabled = user?.modules?.inboundDocuments === true;
useEffect(() => {
if (isLoading) return;
if (!isAuthenticated) {
router.push("/auth/login");
} else if (!enabled) {
router.push("/");
}
}, [isLoading, isAuthenticated, enabled, router]);
if (isLoading || !isAuthenticated || !enabled) {
return (
<div className="flex h-64 items-center justify-center">
<Loader2 className="text-primary h-8 w-8 animate-spin motion-reduce:animate-none" />
</div>
);
}
return <InboundDocumentsView />;
return (
<Suspense fallback={null}>
<DocumentsView />
</Suspense>
);
}