From d7bd061ee0c28da667aa99cf4b02a9e2dea3f146 Mon Sep 17 00:00:00 2001 From: Tyler Date: Fri, 19 Jun 2026 19:41:42 -0600 Subject: [PATCH] feat(frontend): add 6 query/mutation hooks (batches, claims, remits, providers, activity, parse) --- src/hooks/useActivity.ts | 41 +++++++++++++++++++++++++ src/hooks/useBatches.ts | 15 ++++++++++ src/hooks/useClaims.ts | 60 +++++++++++++++++++++++++++++++++++++ src/hooks/useParse.ts | 31 +++++++++++++++++++ src/hooks/useProviders.ts | 40 +++++++++++++++++++++++++ src/hooks/useRemittances.ts | 43 ++++++++++++++++++++++++++ 6 files changed, 230 insertions(+) create mode 100644 src/hooks/useActivity.ts create mode 100644 src/hooks/useBatches.ts create mode 100644 src/hooks/useClaims.ts create mode 100644 src/hooks/useParse.ts create mode 100644 src/hooks/useProviders.ts create mode 100644 src/hooks/useRemittances.ts diff --git a/src/hooks/useActivity.ts b/src/hooks/useActivity.ts new file mode 100644 index 0000000..b1c3ed6 --- /dev/null +++ b/src/hooks/useActivity.ts @@ -0,0 +1,41 @@ +import { useQuery } from "@tanstack/react-query"; +import { useSyncExternalStore } from "react"; +import { api, type ListActivityParams, type PaginatedResponse } from "@/lib/api"; +import { useAppStore } from "@/store"; +import type { Activity } from "@/types"; + +/** + * Lists activity. Polls every 30s when a backend is configured (so the + * Activity Log page reflects new events without a manual refresh); in + * sample-data mode it returns the in-memory store directly. + */ +export function useActivity(params: ListActivityParams = {}) { + const fallback = useSyncExternalStore( + (cb) => useAppStore.subscribe(cb), + () => useAppStore.getState().activity, + () => useAppStore.getState().activity + ); + + const q = useQuery>({ + queryKey: ["activity", params], + queryFn: () => api.listActivity(params), + enabled: api.isConfigured, + refetchInterval: api.isConfigured ? 30_000 : false, + }); + + if (!api.isConfigured) { + return { + data: { + items: fallback, + total: fallback.length, + returned: fallback.length, + has_more: false, + } satisfies PaginatedResponse, + isLoading: false, + isError: false, + error: null, + refetch: () => Promise.resolve(), + } as const; + } + return q; +} diff --git a/src/hooks/useBatches.ts b/src/hooks/useBatches.ts new file mode 100644 index 0000000..650f5f9 --- /dev/null +++ b/src/hooks/useBatches.ts @@ -0,0 +1,15 @@ +import { useQuery } from "@tanstack/react-query"; +import { api, type BatchSummary } from "@/lib/api"; + +/** + * Lists parsed batches. Batches are only available when a backend is + * configured — when running in sample-data mode there's no persisted + * history, so we leave the query disabled. + */ +export function useBatches(limit?: number) { + return useQuery({ + queryKey: ["batches", limit ?? null], + queryFn: () => api.listBatches(limit), + enabled: api.isConfigured, + }); +} diff --git a/src/hooks/useClaims.ts b/src/hooks/useClaims.ts new file mode 100644 index 0000000..1ddeb25 --- /dev/null +++ b/src/hooks/useClaims.ts @@ -0,0 +1,60 @@ +import { useQuery } from "@tanstack/react-query"; +import { useSyncExternalStore } from "react"; +import { api, type ListClaimsParams, type PaginatedResponse } from "@/lib/api"; +import { useAppStore } from "@/store"; +import type { Claim } from "@/types"; + +export const EMPTY_CLAIMS: PaginatedResponse = { + items: [], + total: 0, + returned: 0, + has_more: false, +}; + +/** + * Lists claims. Falls back to the in-memory zustand store when no backend + * is configured, applying the same `status` / `provider_npi` / `batch_id` + * / pagination parameters the live endpoint would accept. Note: the UI + * `Claim` shape doesn't carry a `batchId` yet, so the `batch_id` filter is + * a no-op in fallback mode — it'll start working once the Claim type + * gains that field. + */ +export function useClaims(params: ListClaimsParams) { + const fallback = useSyncExternalStore( + (cb) => useAppStore.subscribe(cb), + () => useAppStore.getState().claims, + () => useAppStore.getState().claims + ); + + const q = useQuery>({ + queryKey: ["claims", params], + queryFn: () => api.listClaims(params), + enabled: api.isConfigured, + }); + + if (!api.isConfigured) { + const offset = params.offset ?? 0; + const limit = params.limit ?? 100; + const filtered = fallback.filter((c) => { + if (params.status && c.status !== params.status) return false; + if (params.provider_npi && c.providerNpi !== params.provider_npi) { + return false; + } + return true; + }); + const sliced = filtered.slice(offset, offset + limit); + return { + data: { + items: sliced, + total: filtered.length, + returned: sliced.length, + has_more: filtered.length > offset + limit, + } satisfies PaginatedResponse, + isLoading: false, + isError: false, + error: null, + refetch: () => Promise.resolve(), + } as const; + } + return q; +} diff --git a/src/hooks/useParse.ts b/src/hooks/useParse.ts new file mode 100644 index 0000000..18b720e --- /dev/null +++ b/src/hooks/useParse.ts @@ -0,0 +1,31 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { api, type ParseOptions } from "@/lib/api"; +import type { + BatchSummary as ParserBatchSummary, + ParseResult837, + ParseResult835, +} from "@/types"; + +type ParseResult = ParseResult837 | ParseResult835 | ParserBatchSummary; + +/** + * Mutation that runs an EDI file through the parser. On success, invalidates + * every list query that might have been affected by the new batch so pages + * showing stale data re-fetch. + */ +export function useParse(kind: "837p" | "835") { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (opts) => { + const fn = kind === "837p" ? api.parse837 : api.parse835; + return fn(opts.file, opts.options) as Promise; + }, + onSuccess: () => { + qc.invalidateQueries({ queryKey: ["batches"] }); + qc.invalidateQueries({ queryKey: ["claims"] }); + qc.invalidateQueries({ queryKey: ["remittances"] }); + qc.invalidateQueries({ queryKey: ["providers"] }); + qc.invalidateQueries({ queryKey: ["activity"] }); + }, + }); +} diff --git a/src/hooks/useProviders.ts b/src/hooks/useProviders.ts new file mode 100644 index 0000000..6ec924d --- /dev/null +++ b/src/hooks/useProviders.ts @@ -0,0 +1,40 @@ +import { useQuery } from "@tanstack/react-query"; +import { useSyncExternalStore } from "react"; +import { api, type ListProvidersParams, type PaginatedResponse } from "@/lib/api"; +import { useAppStore } from "@/store"; +import type { Provider } from "@/types"; + +/** + * Lists providers. Falls back to the in-memory zustand store when no + * backend is configured; the store carries the full provider directory + * so no pagination is applied in fallback mode. + */ +export function useProviders(params: ListProvidersParams = {}) { + const fallback = useSyncExternalStore( + (cb) => useAppStore.subscribe(cb), + () => useAppStore.getState().providers, + () => useAppStore.getState().providers + ); + + const q = useQuery>({ + queryKey: ["providers", params], + queryFn: () => api.listProviders(params), + enabled: api.isConfigured, + }); + + if (!api.isConfigured) { + return { + data: { + items: fallback, + total: fallback.length, + returned: fallback.length, + has_more: false, + } satisfies PaginatedResponse, + isLoading: false, + isError: false, + error: null, + refetch: () => Promise.resolve(), + } as const; + } + return q; +} diff --git a/src/hooks/useRemittances.ts b/src/hooks/useRemittances.ts new file mode 100644 index 0000000..84afd58 --- /dev/null +++ b/src/hooks/useRemittances.ts @@ -0,0 +1,43 @@ +import { useQuery } from "@tanstack/react-query"; +import { useSyncExternalStore } from "react"; +import { api, type ListRemittancesParams, type PaginatedResponse } from "@/lib/api"; +import { useAppStore } from "@/store"; +import type { Remittance } from "@/types"; + +/** + * Lists remittances. Falls back to the in-memory zustand store when no + * backend is configured; the fallback honors `offset` / `limit` and + * returns the full list otherwise. + */ +export function useRemittances(params: ListRemittancesParams) { + const fallback = useSyncExternalStore( + (cb) => useAppStore.subscribe(cb), + () => useAppStore.getState().remittances, + () => useAppStore.getState().remittances + ); + + const q = useQuery>({ + queryKey: ["remittances", params], + queryFn: () => api.listRemittances(params), + enabled: api.isConfigured, + }); + + if (!api.isConfigured) { + const offset = params.offset ?? 0; + const limit = params.limit ?? 100; + const sliced = fallback.slice(offset, offset + limit); + return { + data: { + items: sliced, + total: fallback.length, + returned: sliced.length, + has_more: fallback.length > offset + limit, + } satisfies PaginatedResponse, + isLoading: false, + isError: false, + error: null, + refetch: () => Promise.resolve(), + } as const; + } + return q; +}