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 ? ( +