feat(filter-bar): migrate 3 more fleet pages, extract footer adapter

- ruleEngineFooterProps.ts: toRuleEngineFooterProps() — the
  useFilters-to-RuleEngineListFooter pagination adapter had already been
  hand-written twice (TrucksOnSitePage, CompliancePage) with the same
  pageSize-drop risk useFilters itself just had fixed; extracted before a
  third copy could drift.
- CompliancePage, FuelPurchasePage, IncidentsPage: ListControls ->
  FilterBar + applyClientFilters, same mechanical pattern as the
  warehouse pages (search + one date range, endpoints take no params at
  all per the inventory sweep — confirmed correct bucket, not assumed).
This commit is contained in:
Nathnael
2026-08-14 14:13:11 +00:00
parent fa32fa6a96
commit e5e75b4166
6 changed files with 122 additions and 73 deletions

View File

@@ -3,6 +3,7 @@ export * from "./url";
export * from "./dates";
export * from "./format";
export * from "./clientFilter";
export * from "./ruleEngineFooterProps";
export * from "./useFilters";
export * from "./useSavedViews";
export { FilterBar } from "./FilterBar";

View File

@@ -0,0 +1,32 @@
import type { OnChangeFn, PaginationState } from "@edr/ui-common";
import type { UseFilters } from "./useFilters";
/**
* Adapts `useFilters`'s URL-backed page/pageSize to `RuleEngineListFooter`'s
* prop shape, for the client-bridge pages that render a plain `<Table>` +
* that footer instead of `<DataTable>` (which has `tableProps()` for this).
* Routes page-index vs page-size changes to the right setter — the same
* pageSize-gets-silently-dropped bug `tableProps()` had before it was fixed.
*/
export function toRuleEngineFooterProps(
controls: Pick<UseFilters, "page" | "pageSize" | "setPage" | "setPageSize">,
totalCount: number,
): {
pagination: PaginationState;
pageCount: number;
totalCount: number;
onPaginationChange: OnChangeFn<PaginationState>;
} {
const { page, pageSize, setPage, setPageSize } = controls;
return {
pagination: { pageIndex: page - 1, pageSize },
pageCount: Math.max(1, Math.ceil(totalCount / pageSize)),
totalCount,
onPaginationChange: (updater) => {
const current = { pageIndex: page - 1, pageSize };
const next = typeof updater === "function" ? updater(current) : updater;
if (next.pageSize !== pageSize) setPageSize(next.pageSize);
else if (next.pageIndex !== current.pageIndex) setPage(next.pageIndex + 1);
},
};
}

View File

@@ -18,11 +18,16 @@ import {
} from "@mantine/core";
import { Plus, AlertTriangle } from "lucide-react";
import Breadcrumbs from "@/components/ui/Breadcrumbs";
import ListControls from "@/components/common/ListControls";
// Generic list footer — already shared by the fleet and train-scheduling lists
// despite the ruleEngine path.
import RuleEngineListFooter from "@/components/ruleEngine/RuleEngineListFooter";
import { useListControls } from "@/hooks/useListControls";
import {
applyClientFilters,
FilterBar,
toRuleEngineFooterProps,
useFilters,
type FilterDef,
} from "@/components/filters";
import { useToast } from "@/hooks/use-toast";
import {
complianceService,
@@ -52,6 +57,8 @@ const statusColor = (status: ComplianceRecord["status"]) => {
const formatDate = (value?: string | null) =>
value ? new Date(value).toLocaleDateString() : "—";
const COMPLIANCE_FILTER_DEFS: FilterDef[] = [{ key: "expiryDate", label: "Expiry", type: "date" }];
const emptyForm = {
vehicleId: "",
type: "INSPECTION" as ComplianceType,
@@ -91,10 +98,18 @@ export default function CompliancePage() {
},
});
const controls = useListControls(records as ComplianceRecord[], {
searchKeys: ["type", "status", "documentNumber"],
dateKey: "expiryDate",
});
const controls = useFilters(COMPLIANCE_FILTER_DEFS, { pageSize: 10 });
const filteredRecords = applyClientFilters(
records as ComplianceRecord[],
COMPLIANCE_FILTER_DEFS,
controls.values,
controls.searchText,
{ searchKeys: ["type", "status", "documentNumber"] },
);
const pagedRecords = filteredRecords.slice(
(controls.page - 1) * controls.pageSize,
controls.page * controls.pageSize,
);
const createMutation = useMutation({
mutationFn: async (data: typeof formData) => {
@@ -220,17 +235,11 @@ export default function CompliancePage() {
Compliance Records
</Title>
<Card withBorder>
<ListControls
search={controls.search}
onSearchChange={controls.setSearch}
<FilterBar
defs={COMPLIANCE_FILTER_DEFS}
controls={controls}
searchPlaceholder="Search type, status, document no…"
dateFrom={controls.dateFrom}
onDateFromChange={controls.setDateFrom}
dateTo={controls.dateTo}
onDateToChange={controls.setDateTo}
dateLabel="Expiry"
hasFilters={controls.hasFilters}
onReset={controls.reset}
viewId="fleet-compliance"
/>
<Table striped highlightOnHover>
<Table.Thead>
@@ -261,7 +270,7 @@ export default function CompliancePage() {
</Table.Td>
</Table.Tr>
) : null}
{controls.pagedRows.map((record) => (
{pagedRecords.map((record) => (
<Table.Tr key={record.id}>
<Table.Td>{vehicleLabel(record)}</Table.Td>
<Table.Td>
@@ -282,11 +291,8 @@ export default function CompliancePage() {
</Table.Tbody>
</Table>
<RuleEngineListFooter
pagination={controls.pagination}
pageCount={controls.pageCount}
totalCount={controls.totalCount}
itemLabel="records"
onPaginationChange={controls.setPagination}
{...toRuleEngineFooterProps(controls, filteredRecords.length)}
/>
</Card>

View File

@@ -19,11 +19,16 @@ import {
} from "@mantine/core";
import { Plus } from "lucide-react";
import Breadcrumbs from "@/components/ui/Breadcrumbs";
import ListControls from "@/components/common/ListControls";
// Generic list footer — already shared by the fleet and train-scheduling lists
// despite the ruleEngine path.
import RuleEngineListFooter from "@/components/ruleEngine/RuleEngineListFooter";
import { useListControls } from "@/hooks/useListControls";
import {
applyClientFilters,
FilterBar,
toRuleEngineFooterProps,
useFilters,
type FilterDef,
} from "@/components/filters";
import { useToast } from "@/hooks/use-toast";
import { api } from "@/auth/http";
import { QUERY_KEYS } from "@/constants/QUERY_KEYS";
@@ -45,6 +50,8 @@ interface FuelPurchase {
}
const FUEL_FILTER_DEFS: FilterDef[] = [{ key: "purchaseDate", label: "Purchased", type: "date" }];
export default function FuelPurchasePage() {
const { toast } = useToast();
const qc = useQueryClient();
@@ -123,10 +130,18 @@ export default function FuelPurchasePage() {
const totalCost = formData.liters * formData.costPerLiter;
// Aggregate stats (guarded against divide-by-zero when there are no purchases)
const controls = useListControls(purchasesData as FuelPurchase[], {
searchKeys: ["fuelStation", "paymentMethod"],
dateKey: "purchaseDate",
});
const controls = useFilters(FUEL_FILTER_DEFS, { pageSize: 10 });
const filteredPurchases = applyClientFilters(
purchasesData as FuelPurchase[],
FUEL_FILTER_DEFS,
controls.values,
controls.searchText,
{ searchKeys: ["fuelStation", "paymentMethod"] },
);
const pagedPurchases = filteredPurchases.slice(
(controls.page - 1) * controls.pageSize,
controls.page * controls.pageSize,
);
const totalLiters = (purchasesData as FuelPurchase[]).reduce(
(sum, p) => sum + Number(p.liters),
@@ -195,17 +210,11 @@ export default function FuelPurchasePage() {
{/* Purchases Table */}
<Card withBorder>
<ListControls
search={controls.search}
onSearchChange={controls.setSearch}
<FilterBar
defs={FUEL_FILTER_DEFS}
controls={controls}
searchPlaceholder="Search station or payment method…"
dateFrom={controls.dateFrom}
onDateFromChange={controls.setDateFrom}
dateTo={controls.dateTo}
onDateToChange={controls.setDateTo}
dateLabel="Purchased"
hasFilters={controls.hasFilters}
onReset={controls.reset}
viewId="fleet-fuel-purchases"
/>
<Table striped highlightOnHover>
<Table.Thead>
@@ -237,7 +246,7 @@ export default function FuelPurchasePage() {
</Table.Td>
</Table.Tr>
) : null}
{controls.pagedRows.map((purchase) => (
{pagedPurchases.map((purchase) => (
<Table.Tr key={purchase.id}>
<Table.Td>{(purchase as any).vehicle?.registrationNumber || (purchase as any).vehicle?.plateNumber || purchase.vehicleId}</Table.Td>
<Table.Td>{new Date(purchase.purchaseDate).toLocaleDateString()}</Table.Td>
@@ -253,11 +262,8 @@ export default function FuelPurchasePage() {
</Table.Tbody>
</Table>
<RuleEngineListFooter
pagination={controls.pagination}
pageCount={controls.pageCount}
totalCount={controls.totalCount}
itemLabel="purchases"
onPaginationChange={controls.setPagination}
{...toRuleEngineFooterProps(controls, filteredPurchases.length)}
/>
</Card>

View File

@@ -20,11 +20,16 @@ import {
} from "@mantine/core";
import { Plus } from "lucide-react";
import Breadcrumbs from "@/components/ui/Breadcrumbs";
import ListControls from "@/components/common/ListControls";
// Generic list footer — already shared by the fleet and train-scheduling lists
// despite the ruleEngine path.
import RuleEngineListFooter from "@/components/ruleEngine/RuleEngineListFooter";
import { useListControls } from "@/hooks/useListControls";
import {
applyClientFilters,
FilterBar,
toRuleEngineFooterProps,
useFilters,
type FilterDef,
} from "@/components/filters";
import { useToast } from "@/hooks/use-toast";
import {
incidentsService,
@@ -89,6 +94,8 @@ const initialForm = {
reportedBy: "",
};
const INCIDENT_FILTER_DEFS: FilterDef[] = [{ key: "occurredAt", label: "Occurred", type: "date" }];
export default function IncidentsPage() {
const { toast } = useToast();
const qc = useQueryClient();
@@ -166,10 +173,18 @@ export default function IncidentsPage() {
})) || [];
const incidents = incidentsData as Incident[];
const controls = useListControls(incidents, {
searchKeys: ["type", "severity", "status"],
dateKey: "occurredAt",
});
const controls = useFilters(INCIDENT_FILTER_DEFS, { pageSize: 10 });
const filteredIncidents = applyClientFilters(
incidents,
INCIDENT_FILTER_DEFS,
controls.values,
controls.searchText,
{ searchKeys: ["type", "severity", "status"] },
);
const pagedIncidents = filteredIncidents.slice(
(controls.page - 1) * controls.pageSize,
controls.page * controls.pageSize,
);
const totalCount = incidents.length;
const openCount = incidents.filter((i) => OPEN_STATUSES.includes(i.status)).length;
const underReviewCount = incidents.filter((i) => i.status === "UNDER_REVIEW").length;
@@ -246,17 +261,11 @@ export default function IncidentsPage() {
{/* Incidents Table */}
<Card withBorder>
<ListControls
search={controls.search}
onSearchChange={controls.setSearch}
<FilterBar
defs={INCIDENT_FILTER_DEFS}
controls={controls}
searchPlaceholder="Search type, severity, status…"
dateFrom={controls.dateFrom}
onDateFromChange={controls.setDateFrom}
dateTo={controls.dateTo}
onDateToChange={controls.setDateTo}
dateLabel="Occurred"
hasFilters={controls.hasFilters}
onReset={controls.reset}
viewId="fleet-incidents"
/>
<Table striped highlightOnHover>
<Table.Thead>
@@ -288,7 +297,7 @@ export default function IncidentsPage() {
</Table.Td>
</Table.Tr>
) : null}
{controls.pagedRows.map((incident) => (
{pagedIncidents.map((incident) => (
<Table.Tr key={incident.id}>
<Table.Td>{new Date(incident.occurredAt).toLocaleDateString()}</Table.Td>
<Table.Td>
@@ -316,11 +325,8 @@ export default function IncidentsPage() {
</Table.Tbody>
</Table>
<RuleEngineListFooter
pagination={controls.pagination}
pageCount={controls.pageCount}
totalCount={controls.totalCount}
itemLabel="incidents"
onPaginationChange={controls.setPagination}
{...toRuleEngineFooterProps(controls, filteredIncidents.length)}
/>
</Card>

View File

@@ -14,7 +14,13 @@ import { PageContainer, PageHeader } from "@/components/page";
// Generic list footer — already shared by the fleet and train-scheduling lists
// despite the ruleEngine path; reused here rather than adding a second one.
import RuleEngineListFooter from "@/components/ruleEngine/RuleEngineListFooter";
import { applyClientFilters, FilterBar, useFilters, type FilterDef } from "@/components/filters";
import {
applyClientFilters,
FilterBar,
toRuleEngineFooterProps,
useFilters,
type FilterDef,
} from "@/components/filters";
import { useTrucksOnSite } from "@/hooks/useWarehouses";
import type { TruckOnSite } from "@/types/warehouse";
import { formatDateTime } from "@/lib/format";
@@ -231,16 +237,8 @@ export default function TrucksOnSitePage() {
<>
<Rows rows={pagedTrucks} />
<RuleEngineListFooter
pagination={{ pageIndex: controls.page - 1, pageSize: controls.pageSize }}
pageCount={Math.max(1, Math.ceil(filteredTrucks.length / controls.pageSize))}
totalCount={filteredTrucks.length}
itemLabel="trucks"
onPaginationChange={(updater) => {
const current = { pageIndex: controls.page - 1, pageSize: controls.pageSize };
const next = typeof updater === "function" ? updater(current) : updater;
if (next.pageSize !== controls.pageSize) controls.setPageSize(next.pageSize);
else controls.setPage(next.pageIndex + 1);
}}
{...toRuleEngineFooterProps(controls, filteredTrucks.length)}
/>
</>
)}