feat(frontend): add 6 query/mutation hooks (batches, claims, remits, providers, activity, parse)

This commit is contained in:
Tyler
2026-06-19 19:41:42 -06:00
parent 3ddf962da2
commit d7bd061ee0
6 changed files with 230 additions and 0 deletions
+41
View File
@@ -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<PaginatedResponse<Activity>>({
queryKey: ["activity", params],
queryFn: () => api.listActivity<Activity>(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<Activity>,
isLoading: false,
isError: false,
error: null,
refetch: () => Promise.resolve(),
} as const;
}
return q;
}
+15
View File
@@ -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<BatchSummary[]>({
queryKey: ["batches", limit ?? null],
queryFn: () => api.listBatches(limit),
enabled: api.isConfigured,
});
}
+60
View File
@@ -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<Claim> = {
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<PaginatedResponse<Claim>>({
queryKey: ["claims", params],
queryFn: () => api.listClaims<Claim>(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<Claim>,
isLoading: false,
isError: false,
error: null,
refetch: () => Promise.resolve(),
} as const;
}
return q;
}
+31
View File
@@ -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<ParseResult, Error, { file: File; options?: ParseOptions }>({
mutationFn: (opts) => {
const fn = kind === "837p" ? api.parse837 : api.parse835;
return fn(opts.file, opts.options) as Promise<ParseResult>;
},
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["batches"] });
qc.invalidateQueries({ queryKey: ["claims"] });
qc.invalidateQueries({ queryKey: ["remittances"] });
qc.invalidateQueries({ queryKey: ["providers"] });
qc.invalidateQueries({ queryKey: ["activity"] });
},
});
}
+40
View File
@@ -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<PaginatedResponse<Provider>>({
queryKey: ["providers", params],
queryFn: () => api.listProviders<Provider>(params),
enabled: api.isConfigured,
});
if (!api.isConfigured) {
return {
data: {
items: fallback,
total: fallback.length,
returned: fallback.length,
has_more: false,
} satisfies PaginatedResponse<Provider>,
isLoading: false,
isError: false,
error: null,
refetch: () => Promise.resolve(),
} as const;
}
return q;
}
+43
View File
@@ -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<PaginatedResponse<Remittance>>({
queryKey: ["remittances", params],
queryFn: () => api.listRemittances<Remittance>(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<Remittance>,
isLoading: false,
isError: false,
error: null,
refetch: () => Promise.resolve(),
} as const;
}
return q;
}