Files
cyclone/src/hooks/useClaims.ts
T
Tyler 87cd51e1fc fix(frontend): replay row-flash on every refetch via dataUpdatedAt key
The previous static key={c.id} let React reuse the same <TableRow>
element across refetches, so animate-row-flash only played on
initial mount and on the first render of a brand-new claim_id.
Updated rows whose claim_id was unchanged never re-flashed.

Re-keying on `${c.id}-${dataUpdatedAt}` re-mounts the row on
every refetch (initial load, filter change, parse invalidation),
so the 1.2s accent-tint flash replays each time.

Also: README's 'Frontend unit tests (when added)' is stale — npm
test now exists and runs 3 passing tests.

hooks: add dataUpdatedAt: 0 to the !isConfigured fallback return
so the pages type-check.
2026-06-19 20:00:05 -06:00

62 lines
1.9 KiB
TypeScript

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(),
dataUpdatedAt: 0,
} as const;
}
return q;
}