feat(dashboard): KPI tiles drillable — navigate to /claims with filter
This commit is contained in:
+86
-4
@@ -1,4 +1,5 @@
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { Search, X } from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
@@ -48,14 +49,80 @@ const STATUS_OPTIONS: FilterChipOption[] = [
|
||||
{ value: "pending", label: "Pending" },
|
||||
];
|
||||
|
||||
// Whitelist of valid `?status=` values — anything else falls back to ALL.
|
||||
// Used both to validate the URL param on read AND to guard the Dropdown
|
||||
// onValueChange so the URL never carries a bogus value.
|
||||
const VALID_STATUSES: ReadonlySet<ClaimStatus> = new Set<ClaimStatus>([
|
||||
"draft",
|
||||
"submitted",
|
||||
"accepted",
|
||||
"denied",
|
||||
"paid",
|
||||
"pending",
|
||||
]);
|
||||
|
||||
export function Claims() {
|
||||
const [status, setStatus] = useState<ClaimStatus | typeof ALL>(ALL);
|
||||
// -------------------------------------------------------------------------
|
||||
// URL-driven filter state.
|
||||
//
|
||||
// The page reads `?status=` and `?sort=` off the URL (via React Router's
|
||||
// useSearchParams) so deep links from the Dashboard KPI tiles — e.g.
|
||||
// navigate("/claims?status=denied") — actually filter, not just land on
|
||||
// a default view. Status and sort/order derive from the URL; pagination
|
||||
// (`?page=`) and search (`?q=`) stay local because they're ephemeral
|
||||
// view state that doesn't deserve a URL slot.
|
||||
//
|
||||
// Sort syntax: `?sort=billedAmount` → asc, `?sort=-billedAmount` → desc.
|
||||
// The leading `-` is the conventional "negate" prefix used by most
|
||||
// list APIs (incl. our own /api/claims), so we mirror it instead of
|
||||
// using a separate `?order=` param.
|
||||
// -------------------------------------------------------------------------
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const rawStatus = searchParams.get("status");
|
||||
const status: ClaimStatus | typeof ALL =
|
||||
rawStatus && VALID_STATUSES.has(rawStatus as ClaimStatus)
|
||||
? (rawStatus as ClaimStatus)
|
||||
: ALL;
|
||||
|
||||
const rawSort = searchParams.get("sort");
|
||||
let sort: string;
|
||||
let order: "asc" | "desc";
|
||||
if (rawSort && rawSort.length > 1 && rawSort.startsWith("-")) {
|
||||
// Strip the `-` desc-marker, but only when there's an actual field
|
||||
// after it. `?sort=-` (prefix only, no field) falls through to the
|
||||
// default below — otherwise `slice(1)` would yield `""` and we'd
|
||||
// hit the API with `sort=""`.
|
||||
sort = rawSort.slice(1);
|
||||
order = "desc";
|
||||
} else if (rawSort && rawSort !== "-") {
|
||||
sort = rawSort;
|
||||
order = "asc";
|
||||
} else {
|
||||
// Default — match the previous hardcoded behavior so existing
|
||||
// bookmarks and the "no params" path keep sorting by billed desc.
|
||||
// Also catches `?sort=-` (no field after the prefix) and any other
|
||||
// degenerate value.
|
||||
sort = "billedAmount";
|
||||
order = "desc";
|
||||
}
|
||||
|
||||
const [npi, setNpi] = useState<string>(ALL);
|
||||
const [query, setQuery] = useState("");
|
||||
const [page, setPage] = useState(1);
|
||||
const [helpOpen, setHelpOpen] = useState(false);
|
||||
const searchRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Reset pagination whenever the URL-driven filters change. Without
|
||||
// this, a deep link like `/claims?status=denied` lands on whatever
|
||||
// page the user was last on (e.g. page 5 of an unfiltered list) and
|
||||
// shows an empty view. Safe to run on every render of `status` or
|
||||
// `sort` because `setPage(1)` is idempotent when already at 1, and
|
||||
// changing `page` doesn't trigger this effect.
|
||||
useEffect(() => {
|
||||
setPage(1);
|
||||
}, [status, sort]);
|
||||
|
||||
const { claimId, open, close, setClaimId } = useDrawerUrlState();
|
||||
|
||||
const providers = useAppStore((s) => s.providers);
|
||||
@@ -64,11 +131,26 @@ export function Claims() {
|
||||
[providers]
|
||||
);
|
||||
|
||||
// Drop the `status` query param entirely when going back to "all", so
|
||||
// the URL stays clean for sharing. Caller still owns `setPage(1)` to
|
||||
// reset pagination.
|
||||
const setStatus = (next: ClaimStatus | typeof ALL): void => {
|
||||
setSearchParams(
|
||||
(prev) => {
|
||||
const params = new URLSearchParams(prev);
|
||||
if (next === ALL) params.delete("status");
|
||||
else params.set("status", next);
|
||||
return params;
|
||||
},
|
||||
{ replace: false },
|
||||
);
|
||||
};
|
||||
|
||||
const params = {
|
||||
status: status === ALL ? undefined : status,
|
||||
provider_npi: npi === ALL ? undefined : npi,
|
||||
sort: "billedAmount",
|
||||
order: "desc" as const,
|
||||
sort,
|
||||
order,
|
||||
limit: PAGE_SIZE,
|
||||
offset: (page - 1) * PAGE_SIZE,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user