From 4014d77a2189bd6dd230e00cdc151a07c197f90b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Tue, 7 Jul 2026 12:26:06 +0200 Subject: [PATCH] feat(documents): detail sheet with inline preview and undo-able delete MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ResponsiveSheet (bottom sheet on phones, dialog on desktop): inline-class documents preview against the owner-scoped serve route — skeleton first, internal scroll only; attachment-class documents get no fake preview, just glyph, filename, size, and a prominent Download. Title edits click-to-edit and commit on Enter/blur, kind and filing date save on change, condition links PATCH as a replace-set with chips navigating to the episode page. Delete soft-deletes behind an Undo toast; a refused restore translates the purged / duplicate reasons. Mutation errors stay inline. No share affordance — deliberately. Plan-Task: W2-T3 --- .../documents/document-detail-sheet.tsx | 463 ++++++++++++++++++ 1 file changed, 463 insertions(+) create mode 100644 src/components/documents/document-detail-sheet.tsx diff --git a/src/components/documents/document-detail-sheet.tsx b/src/components/documents/document-detail-sheet.tsx new file mode 100644 index 000000000..e376ba082 --- /dev/null +++ b/src/components/documents/document-detail-sheet.tsx @@ -0,0 +1,463 @@ +"use client"; + +/** + * Document detail on `ResponsiveSheet` (bottom sheet on phones, dialog on + * desktop). Inline-class documents (PDF / browser-native images) preview + * inline against the owner-scoped decrypt-and-serve route — skeleton first, + * content on load, internal scroll only (`overscroll-contain`). Attachment- + * class documents get NO fake preview: type glyph, filename, size, and a + * prominent Download button. + * + * Metadata edits save on commit (title on Enter/blur, kind and filing date + * on change, condition links on toggle — replace-set PATCH). Mutation + * errors surface inline (`role="alert"`), never as a QueryErrorCard. + * Delete soft-deletes with an Undo toast (restore endpoint); a restore + * refusal maps `meta.reason` (`purged`, `duplicateExists`) to translated + * copy. No share affordance anywhere — deliberately. + */ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { Check, Download, Pencil, Plus, Trash2 } from "lucide-react"; +import Link from "next/link"; +import { useState } from "react"; +import { toast } from "sonner"; + +import { useIllnessEpisodes } from "@/components/illness/use-illness"; +import { Button } from "@/components/ui/button"; +import { DateField } from "@/components/ui/date-field"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { ResponsiveSheet } from "@/components/ui/responsive-sheet"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Skeleton } from "@/components/ui/skeleton"; +import { + apiDelete, + ApiError, + apiGet, + apiPatch, + apiPost, +} from "@/lib/api/api-fetch"; +import { useFormatters, useTranslations } from "@/lib/i18n/context"; +import { invalidateKeys, queryKeys } from "@/lib/query-keys"; +import { cn } from "@/lib/utils"; +import { + INBOUND_DOCUMENT_KINDS, + type InboundDocumentDetailDto, + type InboundDocumentKindValue, +} from "@/lib/validations/inbound-documents"; +import { DOCUMENT_KIND_ICONS } from "./document-kind-meta"; +import { formatBytes } from "./vault-utils"; + +type PatchInput = { + title?: string | null; + kind?: InboundDocumentKindValue; + documentDate?: string | null; + episodeIds?: string[]; +}; + +/** Inline preview for Class A documents; skeleton until the blob paints. */ +function InlinePreview({ + documentId, + mimeType, + title, +}: { + documentId: string; + mimeType: string; + title: string; +}) { + const [loaded, setLoaded] = useState(false); + const src = `/api/documents/inbound/${documentId}/original`; + const isPdf = mimeType === "application/pdf"; + + return ( +
+ {!loaded ? ( + + ) : null} + {isPdf ? ( +