"use client"; /** * The vault document picker sheet: a `ResponsiveSheet` (bottom sheet on * phones) with a debounced search over the person's own vault — the same * recall path as the vault page, one page (50) of matches, the search input * as the way to an older document — and one toggle row per document. * * Two pickers used to carry this whole body as copies: linking documents to * an illness episode (each toggle writes at once) and choosing documents for * a share link (the choice is local state, capped, posted with the link). * What a toggle means stays with each caller; the list, the search, the rows * and their states live here once. */ import { useQuery } from "@tanstack/react-query"; import { Check, Search } from "lucide-react"; import { useEffect, useRef, useState, type ReactNode } from "react"; import { useDocumentDate } from "@/components/documents/use-document-date"; import { DOCUMENT_KIND_ICONS } from "@/components/documents/document-kind-meta"; import { EmptyState } from "@/components/ui/empty-state"; import { Input } from "@/components/ui/input"; import { QueryErrorRow } from "@/components/ui/query-error-row"; import { ResponsiveSheet } from "@/components/ui/responsive-sheet"; import { Skeleton } from "@/components/ui/skeleton"; import { apiGet } from "@/lib/api/api-fetch"; import { useTranslations } from "@/lib/i18n/context"; import { queryKeys } from "@/lib/query-keys"; import { cn } from "@/lib/utils"; import type { InboundDocumentDto } from "@/lib/validations/inbound-documents"; interface ListPage { documents: InboundDocumentDto[]; nextCursor: string | null; } export function DocumentPickerSheet({ open, onOpenChange, title, description, searchPlaceholder, emptyLabel, isPicked, onToggle, isDisabled, notice, footer, listTestId, }: { open: boolean; onOpenChange: (open: boolean) => void; title: string; description: string; searchPlaceholder: string; emptyLabel: string; isPicked: (doc: InboundDocumentDto) => boolean; /** `title` is the row's display title (title, filename or "untitled"). */ onToggle: (doc: InboundDocumentDto, title: string, picked: boolean) => void; isDisabled?: (doc: InboundDocumentDto, picked: boolean) => boolean; /** A status line above the list (the share cap). */ notice?: ReactNode; footer?: ReactNode; listTestId?: string; }) { const { t } = useTranslations(); const documentDate = useDocumentDate(); 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 documents = list.data?.documents ?? []; return (
onSearchChange(e.target.value)} placeholder={searchPlaceholder} aria-label={searchPlaceholder} className="pl-9" />
{notice} {list.isPending && open ? (
{Array.from({ length: 4 }, (_, i) => ( ))}
) : list.isError ? ( list.refetch()} /> ) : documents.length === 0 ? ( ) : (
    {documents.map((doc) => { const picked = isPicked(doc); const rowTitle = doc.title ?? doc.filename ?? t("documents.card.untitled"); const Icon = DOCUMENT_KIND_ICONS[doc.kind]; const disabled = isDisabled?.(doc, picked) ?? false; return (
  • ); })}
)}
); }