feat(documents): vault list contract on the client

URL-backed facets (q / kind / episode / year) parse and serialise through
one pure helper pair so deep links round-trip byte-stable; the API search
builder pins the timeline sort. Month bucketing + row chunking feed the
virtualized grid as a flat item list. The upload-response parser owns the
duplicate contract: HTTP 200 with envelope-level meta.duplicate returns
the existing row, every 413/415 reason maps to a translated-copy class,
and proxy-stripped bodies fall back to the HTTP status. Query keys move
to the vault filter shape and gain the usage read.

Plan-Task: W2-T1 (URL state), W2-T5 (keys)
This commit is contained in:
Marc-André Bombeck committed 2026-07-07 12:25:45 +02:00
1 parent b1004958c3
commit 37e9d933e2
4 files changed
+636 -24

No files matched your search

+29 -24
View File
@@ -1,42 +1,47 @@
/**
* Query keys — clinical documents library (v1.25, W-DOCS-IN): the browsable
* document list (search / category filter / sort, keyset-paginated) and a
* single document's detail (document + staged facts). Part of the centralized
* factory; aggregated in `./index.ts`. Any document write (upload / edit /
* extract / confirm / discard) invalidates the `["documents"]` prefix so the
* list and the open detail repaint in lockstep.
* Query keys — the Dokumente vault: the filtered timeline list (keyset-
* paginated), a single document's detail, and the usage/limits read the
* upload path pre-flights against. Part of the centralized factory;
* aggregated in `./index.ts`. Any document write (upload / edit / link /
* delete / restore / bulk) invalidates the `["documents"]` prefix so the
* timeline, an open detail sheet, and the quota bar repaint in lockstep.
*/
import type {
DocumentListSort,
InboundDocumentKindValue,
} from "@/lib/validations/inbound-documents";
import type { InboundDocumentKindValue } from "@/lib/validations/inbound-documents";
/**
* The parameter set that distinguishes one library list view from another.
* Lives in the key so two different filter/sort combinations never share a
* cache bucket. `undefined` fields are dropped by TanStack's stable hash, so a
* bare `{ sort, order }` and `{ q: undefined, kind: undefined, sort, order }`
* resolve to the same key.
* The parameter set that distinguishes one vault timeline view from another —
* mirrors the URL facets (`?q&kind&episode&year`) one-to-one so a deep link
* and an in-page filter change resolve to the same cache bucket. `undefined`
* fields are dropped by TanStack's stable hash, so an all-defaults view and
* an explicit `{ q: undefined, … }` share a key. Sort is pinned server-side
* (filing date, newest first) and is deliberately NOT part of the key.
*/
export interface DocumentListParams {
export interface DocumentVaultFilters {
q?: string;
kind?: InboundDocumentKindValue;
from?: string;
to?: string;
sort: DocumentListSort;
order: "asc" | "desc";
/** Type facet — OR inside the facet, sorted before keying by the caller. */
kinds?: InboundDocumentKindValue[];
/** Only documents linked to this illness/condition episode. */
episodeId?: string;
/** Only documents whose filing date falls in this calendar year. */
year?: number;
}
export const documentKeys = {
documents: () => ["documents"] as const,
inboundDocuments: () => ["documents", "inbound"] as const,
/**
* The parameterised library list (keyset-paginated via `useInfiniteQuery`).
* The parameterised vault list (keyset-paginated via `useInfiniteQuery`).
* The literal `"list"` segment keeps it distinct from a single-document key
* (`inboundDocument(id)`); cuid ids never collide with it. Stays under the
* `["documents", "inbound"]` prefix so prefix invalidation still reaches it.
*/
inboundDocumentList: (params: DocumentListParams) =>
["documents", "inbound", "list", params] as const,
inboundDocumentList: (filters: DocumentVaultFilters) =>
["documents", "inbound", "list", filters] as const,
inboundDocument: (id: string) => ["documents", "inbound", id] as const,
/**
* Storage usage + effective limits (`GET /api/documents/inbound/usage`).
* Invalidated through the `["documents"]` prefix after every upload /
* delete so the quota bar tracks reality.
*/
inboundDocumentUsage: () => ["documents", "inbound", "usage"] as const,
};