/** * Attached-precedent hooks — user-supplied case-law quotes that * justify chair positions in the compose screen. * * Backed by POST/GET/DELETE /api/cases/{n}/precedents and the * cross-case library search at GET /api/precedents/search. The * optional PDF archive chains through POST .../upload-pdf before * precedent creation; that's a plain async function, not a mutation * hook, because it has no cache invalidation of its own. */ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { apiRequest, ApiError } from "./client"; import type { PracticeArea } from "@/lib/practice-area"; export type CasePrecedent = { id: string; case_id: string; section_id: string | null; quote: string; citation: string; chair_note: string; pdf_document_id: string | null; practice_area: PracticeArea | null; created_at: string; updated_at: string; }; export type PrecedentCreateInput = { quote: string; citation: string; section_id?: string; chair_note?: string; pdf_document_id?: string; }; export type LibraryMatch = { id: string; citation: string; quote: string; chair_note: string; practice_area: PracticeArea | null; created_at: string; }; export const precedentKeys = { all: ["precedents"] as const, forCase: (caseNumber: string) => [...precedentKeys.all, "case", caseNumber] as const, librarySearch: (q: string, area: string) => [...precedentKeys.all, "library", area, q] as const, }; export function useCasePrecedents(caseNumber: string | undefined) { return useQuery({ queryKey: precedentKeys.forCase(caseNumber ?? ""), queryFn: ({ signal }) => apiRequest( `/api/cases/${caseNumber}/precedents`, { signal }, ), enabled: Boolean(caseNumber), staleTime: 30_000, }); } export function useCreatePrecedent(caseNumber: string) { const qc = useQueryClient(); return useMutation({ mutationFn: (input: PrecedentCreateInput) => apiRequest(`/api/cases/${caseNumber}/precedents`, { method: "POST", body: input, }), onSuccess: () => { qc.invalidateQueries({ queryKey: precedentKeys.forCase(caseNumber) }); qc.invalidateQueries({ queryKey: [...precedentKeys.all, "library"] }); }, }); } export function useDeletePrecedent(caseNumber: string) { const qc = useQueryClient(); return useMutation({ mutationFn: (precedentId: string) => apiRequest<{ deleted: boolean }>(`/api/precedents/${precedentId}`, { method: "DELETE", }), onSuccess: () => { qc.invalidateQueries({ queryKey: precedentKeys.forCase(caseNumber) }); }, }); } export function usePrecedentLibrarySearch( query: string, practiceArea: PracticeArea | null | undefined, enabled: boolean, ) { return useQuery({ queryKey: precedentKeys.librarySearch(query, practiceArea ?? ""), queryFn: ({ signal }) => { const params = new URLSearchParams({ q: query }); if (practiceArea) params.set("practice_area", practiceArea); return apiRequest( `/api/precedents/search?${params.toString()}`, { signal }, ); }, enabled: enabled && query.trim().length >= 2, staleTime: 10_000, placeholderData: (prev) => prev, }); } /** * One-shot PDF archive upload. Returns the new document_id so the * caller can pass it into useCreatePrecedent. No cache invalidation * — we only care about the id as a handle. */ export async function uploadPrecedentPdf( caseNumber: string, file: File, ): Promise<{ document_id: string; filename: string }> { const fd = new FormData(); fd.append("file", file); const res = await fetch( `/api/cases/${encodeURIComponent(caseNumber)}/precedents/upload-pdf`, { method: "POST", body: fd }, ); const parsed = await res.json().catch(() => null); if (!res.ok) { throw new ApiError( `Upload failed with ${res.status}`, res.status, parsed, ); } return parsed as { document_id: string; filename: string }; }