feat(frontend): refactor Remittances page to useRemittances + primitives
This commit is contained in:
+107
-47
@@ -1,3 +1,4 @@
|
|||||||
|
import { useState } from "react";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@@ -7,13 +8,37 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { RemitStatusBadge } from "@/components/StatusBadge";
|
import { RemitStatusBadge } from "@/components/StatusBadge";
|
||||||
import { useAppStore } from "@/store";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import { EmptyState } from "@/components/ui/empty-state";
|
||||||
|
import { ErrorState } from "@/components/ui/error-state";
|
||||||
|
import { FilterChips, type FilterChipOption } from "@/components/ui/filter-chips";
|
||||||
|
import { Pagination } from "@/components/ui/pagination";
|
||||||
|
import { useRemittances } from "@/hooks/useRemittances";
|
||||||
import { fmt } from "@/lib/format";
|
import { fmt } from "@/lib/format";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import type { RemittanceStatus } from "@/types";
|
||||||
|
|
||||||
|
const PAGE_SIZE = 25;
|
||||||
|
|
||||||
|
const STATUS_OPTIONS: FilterChipOption[] = [
|
||||||
|
{ value: "received", label: "Received" },
|
||||||
|
{ value: "posted", label: "Posted" },
|
||||||
|
{ value: "reconciled", label: "Reconciled" },
|
||||||
|
];
|
||||||
|
|
||||||
export function Remittances() {
|
export function Remittances() {
|
||||||
const remits = useAppStore((s) => s.remittances);
|
const [page, setPage] = useState(1);
|
||||||
|
const [status, setStatus] = useState<RemittanceStatus | null>(null);
|
||||||
|
|
||||||
const total = remits.reduce(
|
const { data, isLoading, isError, error, refetch } = useRemittances({
|
||||||
|
sort: "receivedDate",
|
||||||
|
order: "desc",
|
||||||
|
limit: PAGE_SIZE,
|
||||||
|
offset: (page - 1) * PAGE_SIZE,
|
||||||
|
});
|
||||||
|
const items = data?.items ?? [];
|
||||||
|
|
||||||
|
const total = items.reduce(
|
||||||
(acc, r) => ({
|
(acc, r) => ({
|
||||||
paid: acc.paid + r.paidAmount,
|
paid: acc.paid + r.paidAmount,
|
||||||
adjustments: acc.adjustments + r.adjustmentAmount,
|
adjustments: acc.adjustments + r.adjustmentAmount,
|
||||||
@@ -34,12 +59,20 @@ export function Remittances() {
|
|||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
{isError ? (
|
||||||
|
<ErrorState
|
||||||
|
message="Couldn't load remittances from the backend."
|
||||||
|
detail={error instanceof Error ? error.message : String(error)}
|
||||||
|
onRetry={() => refetch()}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<div className="grid gap-4 grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-4 grid-cols-2 lg:grid-cols-3">
|
||||||
<div className="surface rounded-xl p-5">
|
<div className="surface rounded-xl p-5">
|
||||||
<div className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground">
|
<div className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground">
|
||||||
Remits
|
Remits
|
||||||
</div>
|
</div>
|
||||||
<div className="display text-[28px] leading-none mt-3">{fmt.num(remits.length)}</div>
|
<div className="display text-[28px] leading-none mt-3">{fmt.num(data?.total ?? 0)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="surface rounded-xl p-5">
|
<div className="surface rounded-xl p-5">
|
||||||
<div className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground">
|
<div className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground">
|
||||||
@@ -55,51 +88,78 @@ export function Remittances() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="surface rounded-xl p-4">
|
||||||
|
<FilterChips
|
||||||
|
options={STATUS_OPTIONS}
|
||||||
|
value={status}
|
||||||
|
onChange={(v) => {
|
||||||
|
setStatus((v as RemittanceStatus | null) ?? null);
|
||||||
|
setPage(1);
|
||||||
|
}}
|
||||||
|
eyebrow="Status"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="surface rounded-xl overflow-hidden">
|
<div className="surface rounded-xl overflow-hidden">
|
||||||
<Table>
|
{isLoading ? (
|
||||||
<TableHeader>
|
<div className="p-4 space-y-2">
|
||||||
<TableRow>
|
{Array.from({ length: 5 }).map((_, i) => (
|
||||||
<TableHead>Remit</TableHead>
|
<Skeleton key={i} variant="row" />
|
||||||
<TableHead>Claim</TableHead>
|
))}
|
||||||
<TableHead>Payer</TableHead>
|
</div>
|
||||||
<TableHead className="text-right">Paid</TableHead>
|
) : items.length === 0 ? (
|
||||||
<TableHead className="text-right">Adjustment</TableHead>
|
<EmptyState
|
||||||
<TableHead>Status</TableHead>
|
eyebrow="Remittances · awaiting first 835"
|
||||||
<TableHead>Received</TableHead>
|
message="Drop an 835 file on the Upload page to populate this list."
|
||||||
</TableRow>
|
/>
|
||||||
</TableHeader>
|
) : (
|
||||||
<TableBody>
|
<>
|
||||||
{remits.length === 0 ? (
|
<Table>
|
||||||
<TableRow>
|
<TableHeader>
|
||||||
<TableCell colSpan={7} className="text-center py-12 text-muted-foreground">
|
<TableRow>
|
||||||
No remittances yet.
|
<TableHead>Remit</TableHead>
|
||||||
</TableCell>
|
<TableHead>Claim</TableHead>
|
||||||
</TableRow>
|
<TableHead>Payer</TableHead>
|
||||||
) : (
|
<TableHead className="text-right">Paid</TableHead>
|
||||||
remits.map((r) => (
|
<TableHead className="text-right">Adjustment</TableHead>
|
||||||
<TableRow key={r.id}>
|
<TableHead>Status</TableHead>
|
||||||
<TableCell className="display num text-[13px]">{r.id}</TableCell>
|
<TableHead>Received</TableHead>
|
||||||
<TableCell className="display num text-[13px] text-muted-foreground">
|
|
||||||
{r.claimId}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>{r.payerName}</TableCell>
|
|
||||||
<TableCell className="text-right display num">
|
|
||||||
{fmt.usdPrecise(r.paidAmount)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right display num text-muted-foreground">
|
|
||||||
{fmt.usdPrecise(r.adjustmentAmount)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<RemitStatusBadge status={r.status} />
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-muted-foreground num text-[13px]">
|
|
||||||
{fmt.dateShort(r.receivedDate)}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
</TableHeader>
|
||||||
)}
|
<TableBody>
|
||||||
</TableBody>
|
{items.map((r) => (
|
||||||
</Table>
|
<TableRow key={r.id} className={cn("animate-row-flash")}>
|
||||||
|
<TableCell className="display num text-[13px]">{r.id}</TableCell>
|
||||||
|
<TableCell className="display num text-[13px] text-muted-foreground">
|
||||||
|
{r.claimId}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{r.payerName}</TableCell>
|
||||||
|
<TableCell className="text-right display num">
|
||||||
|
{fmt.usdPrecise(r.paidAmount)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right display num text-muted-foreground">
|
||||||
|
{fmt.usdPrecise(r.adjustmentAmount)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<RemitStatusBadge status={r.status} />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-muted-foreground num text-[13px]">
|
||||||
|
{fmt.dateShort(r.receivedDate)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
<div className="px-4 pb-4">
|
||||||
|
<Pagination
|
||||||
|
page={page}
|
||||||
|
pageSize={PAGE_SIZE}
|
||||||
|
total={data?.total ?? 0}
|
||||||
|
onPageChange={setPage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user