"use client"; /** * Owner document picker for the clinician-share create flow. A * `ResponsiveSheet` (bottom sheet on phones) with a debounced search over the * user's own vault — the SAME recall path as the vault page — and one * multi-select toggle row per document. Selection is client-only state; the * chosen ids POST with the share link, which freezes them write-once. The set * is bounded at `max` (mirrors `SHARE_LINK_MAX_DOCUMENTS`): once the cap is * reached, unselected rows disable so the client never posts an oversized set. * * Reuses the vault list endpoint + the `inboundDocumentPicker` query key from * the central factory; reads unwrap `(await res.json()).data` via `apiGet`. * No write happens here — attaching is the create mutation's job. */ import { useQuery } from "@tanstack/react-query"; import { Check, Search } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { ResponsiveSheet } from "@/components/ui/responsive-sheet"; import { Skeleton } from "@/components/ui/skeleton"; import { DOCUMENT_KIND_ICONS } from "@/components/documents/document-kind-meta"; import { documentDateKey } from "@/components/documents/vault-utils"; import { apiGet } from "@/lib/api/api-fetch"; import { useFormatters, useTranslations } from "@/lib/i18n/context"; import { queryKeys } from "@/lib/query-keys"; import { cn } from "@/lib/utils"; import type { InboundDocumentDto } from "@/lib/validations/inbound-documents"; /** A picked document, carried by id + a display title (for the owner chips). */ export interface PickedDocument { id: string; title: string; } interface ListPage { documents: InboundDocumentDto[]; nextCursor: string | null; } export function ShareDocumentPicker({ open, onOpenChange, selected, onSelectedChange, max, }: { open: boolean; onOpenChange: (open: boolean) => void; selected: PickedDocument[]; onSelectedChange: (next: PickedDocument[]) => void; max: number; }) { const { t } = useTranslations(); const format = useFormatters(); const [searchDraft, setSearchDraft] = useState(""); const [query, setQuery] = useState(""); // 200 ms debounce, matching the vault's search feel. const debounceRef = useRef | null>(null); useEffect( () => () => { if (debounceRef.current) clearTimeout(debounceRef.current); }, [], ); const onSearchChange = (value: string) => { setSearchDraft(value); if (debounceRef.current) clearTimeout(debounceRef.current); debounceRef.current = setTimeout(() => setQuery(value.trim()), 200); }; const list = useQuery({ queryKey: queryKeys.inboundDocumentPicker(query), enabled: open, queryFn: () => { const sp = new URLSearchParams(); if (query) sp.set("q", query); sp.set("sort", "documentDate"); sp.set("order", "desc"); sp.set("limit", "50"); return apiGet(`/api/documents/inbound?${sp.toString()}`); }, }); const selectedIds = new Set(selected.map((s) => s.id)); const atCap = selected.length >= max; function toggle(doc: InboundDocumentDto, title: string) { if (selectedIds.has(doc.id)) { onSelectedChange(selected.filter((s) => s.id !== doc.id)); return; } if (atCap) return; onSelectedChange([...selected, { id: doc.id, title }]); } const documents = list.data?.documents ?? []; return ( {t("settings.sharing.pickerSelectedCount", { count: selected.length, max, })} } >
onSearchChange(e.target.value)} placeholder={t("settings.sharing.pickerSearch")} aria-label={t("settings.sharing.pickerSearch")} className="pl-9" />
{atCap ? (

{t("settings.sharing.pickerMaxReached", { max })}

) : null} {list.isPending && open ? (
{Array.from({ length: 4 }, (_, i) => ( ))}
) : list.isError ? (

{t("documents.list.loadError")}

) : documents.length === 0 ? (

{t("settings.sharing.pickerEmpty")}

) : (
    {documents.map((doc) => { const picked = selectedIds.has(doc.id); const title = doc.title ?? doc.filename ?? t("documents.card.untitled"); const Icon = DOCUMENT_KIND_ICONS[doc.kind]; const disabled = !picked && atCap; return (
  • ); })}
)}
); }