/** * Cases domain hooks. * * Note on types: the FastAPI `/api/cases` endpoint doesn't declare a response * model, so openapi-typescript emits `unknown` for its payload. Until the * backend is annotated (see out-of-scope in the rewrite plan), we maintain a * small local type that matches what the running API returns today. Any drift * surfaces as a runtime TypeScript error the first time a property is touched. */ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "./client"; import type { CaseCreateInput, CaseUpdateInput } from "@/lib/schemas/case"; import type { PracticeArea, AppealSubtype } from "@/lib/practice-area"; export type CaseStatus = | "new" | "uploading" | "processing" | "documents_ready" | "analyst_verified" | "research_complete" | "outcome_set" | "brainstorming" | "direction_approved" | "analysis_enriched" | "ready_for_writing" | "drafting" | "qa_review" | "drafted" | "exported" | "reviewed" | "final"; export type Case = { case_number: string; title: string; status: CaseStatus; subject?: string | null; expected_outcome?: string | null; created_at?: string; updated_at?: string; /* Multi-tenant axis — populated by backfill + server-side derive */ practice_area?: PracticeArea; appeal_subtype?: AppealSubtype; /* Present when loaded with detail=true */ document_count?: number; processing_count?: number; committee_type?: string | null; hearing_date?: string | null; }; export type CaseDocument = { id: string; case_id: string; doc_type: string; title: string; file_path: string; page_count: number | null; extraction_status: string; created_at: string; practice_area?: PracticeArea; appeal_subtype?: AppealSubtype; }; export type CaseDetail = Case & { documents?: CaseDocument[]; blocks?: Array<{ code: string; status?: string; char_count?: number }>; }; export const casesKeys = { all: ["cases"] as const, list: (detail: boolean) => [...casesKeys.all, "list", { detail }] as const, detail: (caseNumber: string) => [...casesKeys.all, "detail", caseNumber] as const, }; export function useCases(detail = false) { return useQuery({ queryKey: casesKeys.list(detail), queryFn: ({ signal }) => apiRequest(`/api/cases${detail ? "?detail=true" : ""}`, { signal, }), }); } export function useCase(caseNumber: string | undefined) { return useQuery({ queryKey: casesKeys.detail(caseNumber ?? ""), queryFn: ({ signal }) => apiRequest(`/api/cases/${caseNumber}/details`, { signal }), enabled: Boolean(caseNumber), /* Replaces the old 5s polling from vanilla index.html */ staleTime: 5_000, refetchInterval: 5_000, }); } export type WorkflowStatus = { case_number?: string; status?: CaseStatus | string; current_step?: string; steps?: Array<{ key: string; label?: string; status: "done" | "current" | "pending" | string; }>; /* FastAPI returns free-form JSON; keep it permissive */ [key: string]: unknown; }; export function useCreateCase() { const qc = useQueryClient(); return useMutation({ mutationFn: (input: CaseCreateInput) => apiRequest<{ case_number?: string; message?: string; [k: string]: unknown }>( `/api/cases/create`, { method: "POST", body: input }, ), onSuccess: () => { qc.invalidateQueries({ queryKey: casesKeys.all }); }, }); } export function useUpdateCase(caseNumber: string | undefined) { const qc = useQueryClient(); return useMutation({ mutationFn: (input: CaseUpdateInput) => apiRequest(`/api/cases/${caseNumber}`, { method: "PUT", body: input, }), onSuccess: (data) => { /* Patch cached detail and nudge the list to refetch on next focus */ if (caseNumber) { qc.setQueryData( casesKeys.detail(caseNumber), (prev) => (prev ? { ...prev, ...data } : prev), ); } qc.invalidateQueries({ queryKey: casesKeys.all }); }, }); } export type GitSyncStatus = { synced: boolean; has_remote: boolean; remote_url?: string | null; dirty_files: number; commits_ahead: number; last_commit_time?: string | null; last_commit_msg?: string | null; error?: string; }; export function useGitStatus(caseNumber: string | undefined) { return useQuery({ queryKey: [...casesKeys.all, "git-status", caseNumber ?? ""] as const, queryFn: ({ signal }) => apiRequest(`/api/cases/${caseNumber}/git-status`, { signal }), enabled: Boolean(caseNumber), staleTime: 30_000, refetchInterval: 60_000, }); } export type StartWorkflowResult = { case_number: string; status: string; issue_id: string; issue_identifier: string; project_url: string; wakeup: Record; }; export function useStartWorkflow(caseNumber: string | undefined) { const qc = useQueryClient(); return useMutation({ mutationFn: () => apiRequest( `/api/cases/${caseNumber}/start-workflow`, { method: "POST" }, ), onSuccess: () => { qc.invalidateQueries({ queryKey: casesKeys.all }); if (caseNumber) { qc.invalidateQueries({ queryKey: casesKeys.detail(caseNumber) }); } }, }); } export function useWorkflowStatus(caseNumber: string | undefined) { return useQuery({ queryKey: [...casesKeys.all, "workflow", caseNumber ?? ""] as const, queryFn: ({ signal }) => apiRequest(`/api/cases/${caseNumber}/status`, { signal }), enabled: Boolean(caseNumber), staleTime: 5_000, refetchInterval: 5_000, }); }