mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
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:
@@ -3,6 +3,7 @@ export * from "./url";
|
|||||||
export * from "./dates";
|
export * from "./dates";
|
||||||
export * from "./format";
|
export * from "./format";
|
||||||
export * from "./clientFilter";
|
export * from "./clientFilter";
|
||||||
|
export * from "./ruleEngineFooterProps";
|
||||||
export * from "./useFilters";
|
export * from "./useFilters";
|
||||||
export * from "./useSavedViews";
|
export * from "./useSavedViews";
|
||||||
export { FilterBar } from "./FilterBar";
|
export { FilterBar } from "./FilterBar";
|
||||||
|
|||||||
@@ -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);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -18,11 +18,16 @@ import {
|
|||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { Plus, AlertTriangle } from "lucide-react";
|
import { Plus, AlertTriangle } from "lucide-react";
|
||||||
import Breadcrumbs from "@/components/ui/Breadcrumbs";
|
import Breadcrumbs from "@/components/ui/Breadcrumbs";
|
||||||
import ListControls from "@/components/common/ListControls";
|
|
||||||
// Generic list footer — already shared by the fleet and train-scheduling lists
|
// Generic list footer — already shared by the fleet and train-scheduling lists
|
||||||
// despite the ruleEngine path.
|
// despite the ruleEngine path.
|
||||||
import RuleEngineListFooter from "@/components/ruleEngine/RuleEngineListFooter";
|
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 { useToast } from "@/hooks/use-toast";
|
||||||
import {
|
import {
|
||||||
complianceService,
|
complianceService,
|
||||||
@@ -52,6 +57,8 @@ const statusColor = (status: ComplianceRecord["status"]) => {
|
|||||||
const formatDate = (value?: string | null) =>
|
const formatDate = (value?: string | null) =>
|
||||||
value ? new Date(value).toLocaleDateString() : "—";
|
value ? new Date(value).toLocaleDateString() : "—";
|
||||||
|
|
||||||
|
const COMPLIANCE_FILTER_DEFS: FilterDef[] = [{ key: "expiryDate", label: "Expiry", type: "date" }];
|
||||||
|
|
||||||
const emptyForm = {
|
const emptyForm = {
|
||||||
vehicleId: "",
|
vehicleId: "",
|
||||||
type: "INSPECTION" as ComplianceType,
|
type: "INSPECTION" as ComplianceType,
|
||||||
@@ -91,10 +98,18 @@ export default function CompliancePage() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const controls = useListControls(records as ComplianceRecord[], {
|
const controls = useFilters(COMPLIANCE_FILTER_DEFS, { pageSize: 10 });
|
||||||
searchKeys: ["type", "status", "documentNumber"],
|
const filteredRecords = applyClientFilters(
|
||||||
dateKey: "expiryDate",
|
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({
|
const createMutation = useMutation({
|
||||||
mutationFn: async (data: typeof formData) => {
|
mutationFn: async (data: typeof formData) => {
|
||||||
@@ -220,17 +235,11 @@ export default function CompliancePage() {
|
|||||||
Compliance Records
|
Compliance Records
|
||||||
</Title>
|
</Title>
|
||||||
<Card withBorder>
|
<Card withBorder>
|
||||||
<ListControls
|
<FilterBar
|
||||||
search={controls.search}
|
defs={COMPLIANCE_FILTER_DEFS}
|
||||||
onSearchChange={controls.setSearch}
|
controls={controls}
|
||||||
searchPlaceholder="Search type, status, document no…"
|
searchPlaceholder="Search type, status, document no…"
|
||||||
dateFrom={controls.dateFrom}
|
viewId="fleet-compliance"
|
||||||
onDateFromChange={controls.setDateFrom}
|
|
||||||
dateTo={controls.dateTo}
|
|
||||||
onDateToChange={controls.setDateTo}
|
|
||||||
dateLabel="Expiry"
|
|
||||||
hasFilters={controls.hasFilters}
|
|
||||||
onReset={controls.reset}
|
|
||||||
/>
|
/>
|
||||||
<Table striped highlightOnHover>
|
<Table striped highlightOnHover>
|
||||||
<Table.Thead>
|
<Table.Thead>
|
||||||
@@ -261,7 +270,7 @@ export default function CompliancePage() {
|
|||||||
</Table.Td>
|
</Table.Td>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
) : null}
|
) : null}
|
||||||
{controls.pagedRows.map((record) => (
|
{pagedRecords.map((record) => (
|
||||||
<Table.Tr key={record.id}>
|
<Table.Tr key={record.id}>
|
||||||
<Table.Td>{vehicleLabel(record)}</Table.Td>
|
<Table.Td>{vehicleLabel(record)}</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
@@ -282,11 +291,8 @@ export default function CompliancePage() {
|
|||||||
</Table.Tbody>
|
</Table.Tbody>
|
||||||
</Table>
|
</Table>
|
||||||
<RuleEngineListFooter
|
<RuleEngineListFooter
|
||||||
pagination={controls.pagination}
|
|
||||||
pageCount={controls.pageCount}
|
|
||||||
totalCount={controls.totalCount}
|
|
||||||
itemLabel="records"
|
itemLabel="records"
|
||||||
onPaginationChange={controls.setPagination}
|
{...toRuleEngineFooterProps(controls, filteredRecords.length)}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|||||||
@@ -19,11 +19,16 @@ import {
|
|||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { Plus } from "lucide-react";
|
import { Plus } from "lucide-react";
|
||||||
import Breadcrumbs from "@/components/ui/Breadcrumbs";
|
import Breadcrumbs from "@/components/ui/Breadcrumbs";
|
||||||
import ListControls from "@/components/common/ListControls";
|
|
||||||
// Generic list footer — already shared by the fleet and train-scheduling lists
|
// Generic list footer — already shared by the fleet and train-scheduling lists
|
||||||
// despite the ruleEngine path.
|
// despite the ruleEngine path.
|
||||||
import RuleEngineListFooter from "@/components/ruleEngine/RuleEngineListFooter";
|
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 { useToast } from "@/hooks/use-toast";
|
||||||
import { api } from "@/auth/http";
|
import { api } from "@/auth/http";
|
||||||
import { QUERY_KEYS } from "@/constants/QUERY_KEYS";
|
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() {
|
export default function FuelPurchasePage() {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
@@ -123,10 +130,18 @@ export default function FuelPurchasePage() {
|
|||||||
const totalCost = formData.liters * formData.costPerLiter;
|
const totalCost = formData.liters * formData.costPerLiter;
|
||||||
|
|
||||||
// Aggregate stats (guarded against divide-by-zero when there are no purchases)
|
// Aggregate stats (guarded against divide-by-zero when there are no purchases)
|
||||||
const controls = useListControls(purchasesData as FuelPurchase[], {
|
const controls = useFilters(FUEL_FILTER_DEFS, { pageSize: 10 });
|
||||||
searchKeys: ["fuelStation", "paymentMethod"],
|
const filteredPurchases = applyClientFilters(
|
||||||
dateKey: "purchaseDate",
|
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(
|
const totalLiters = (purchasesData as FuelPurchase[]).reduce(
|
||||||
(sum, p) => sum + Number(p.liters),
|
(sum, p) => sum + Number(p.liters),
|
||||||
@@ -195,17 +210,11 @@ export default function FuelPurchasePage() {
|
|||||||
|
|
||||||
{/* Purchases Table */}
|
{/* Purchases Table */}
|
||||||
<Card withBorder>
|
<Card withBorder>
|
||||||
<ListControls
|
<FilterBar
|
||||||
search={controls.search}
|
defs={FUEL_FILTER_DEFS}
|
||||||
onSearchChange={controls.setSearch}
|
controls={controls}
|
||||||
searchPlaceholder="Search station or payment method…"
|
searchPlaceholder="Search station or payment method…"
|
||||||
dateFrom={controls.dateFrom}
|
viewId="fleet-fuel-purchases"
|
||||||
onDateFromChange={controls.setDateFrom}
|
|
||||||
dateTo={controls.dateTo}
|
|
||||||
onDateToChange={controls.setDateTo}
|
|
||||||
dateLabel="Purchased"
|
|
||||||
hasFilters={controls.hasFilters}
|
|
||||||
onReset={controls.reset}
|
|
||||||
/>
|
/>
|
||||||
<Table striped highlightOnHover>
|
<Table striped highlightOnHover>
|
||||||
<Table.Thead>
|
<Table.Thead>
|
||||||
@@ -237,7 +246,7 @@ export default function FuelPurchasePage() {
|
|||||||
</Table.Td>
|
</Table.Td>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
) : null}
|
) : null}
|
||||||
{controls.pagedRows.map((purchase) => (
|
{pagedPurchases.map((purchase) => (
|
||||||
<Table.Tr key={purchase.id}>
|
<Table.Tr key={purchase.id}>
|
||||||
<Table.Td>{(purchase as any).vehicle?.registrationNumber || (purchase as any).vehicle?.plateNumber || purchase.vehicleId}</Table.Td>
|
<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>
|
<Table.Td>{new Date(purchase.purchaseDate).toLocaleDateString()}</Table.Td>
|
||||||
@@ -253,11 +262,8 @@ export default function FuelPurchasePage() {
|
|||||||
</Table.Tbody>
|
</Table.Tbody>
|
||||||
</Table>
|
</Table>
|
||||||
<RuleEngineListFooter
|
<RuleEngineListFooter
|
||||||
pagination={controls.pagination}
|
|
||||||
pageCount={controls.pageCount}
|
|
||||||
totalCount={controls.totalCount}
|
|
||||||
itemLabel="purchases"
|
itemLabel="purchases"
|
||||||
onPaginationChange={controls.setPagination}
|
{...toRuleEngineFooterProps(controls, filteredPurchases.length)}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|||||||
@@ -20,11 +20,16 @@ import {
|
|||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { Plus } from "lucide-react";
|
import { Plus } from "lucide-react";
|
||||||
import Breadcrumbs from "@/components/ui/Breadcrumbs";
|
import Breadcrumbs from "@/components/ui/Breadcrumbs";
|
||||||
import ListControls from "@/components/common/ListControls";
|
|
||||||
// Generic list footer — already shared by the fleet and train-scheduling lists
|
// Generic list footer — already shared by the fleet and train-scheduling lists
|
||||||
// despite the ruleEngine path.
|
// despite the ruleEngine path.
|
||||||
import RuleEngineListFooter from "@/components/ruleEngine/RuleEngineListFooter";
|
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 { useToast } from "@/hooks/use-toast";
|
||||||
import {
|
import {
|
||||||
incidentsService,
|
incidentsService,
|
||||||
@@ -89,6 +94,8 @@ const initialForm = {
|
|||||||
reportedBy: "",
|
reportedBy: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const INCIDENT_FILTER_DEFS: FilterDef[] = [{ key: "occurredAt", label: "Occurred", type: "date" }];
|
||||||
|
|
||||||
export default function IncidentsPage() {
|
export default function IncidentsPage() {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
@@ -166,10 +173,18 @@ export default function IncidentsPage() {
|
|||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
const incidents = incidentsData as Incident[];
|
const incidents = incidentsData as Incident[];
|
||||||
const controls = useListControls(incidents, {
|
const controls = useFilters(INCIDENT_FILTER_DEFS, { pageSize: 10 });
|
||||||
searchKeys: ["type", "severity", "status"],
|
const filteredIncidents = applyClientFilters(
|
||||||
dateKey: "occurredAt",
|
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 totalCount = incidents.length;
|
||||||
const openCount = incidents.filter((i) => OPEN_STATUSES.includes(i.status)).length;
|
const openCount = incidents.filter((i) => OPEN_STATUSES.includes(i.status)).length;
|
||||||
const underReviewCount = incidents.filter((i) => i.status === "UNDER_REVIEW").length;
|
const underReviewCount = incidents.filter((i) => i.status === "UNDER_REVIEW").length;
|
||||||
@@ -246,17 +261,11 @@ export default function IncidentsPage() {
|
|||||||
|
|
||||||
{/* Incidents Table */}
|
{/* Incidents Table */}
|
||||||
<Card withBorder>
|
<Card withBorder>
|
||||||
<ListControls
|
<FilterBar
|
||||||
search={controls.search}
|
defs={INCIDENT_FILTER_DEFS}
|
||||||
onSearchChange={controls.setSearch}
|
controls={controls}
|
||||||
searchPlaceholder="Search type, severity, status…"
|
searchPlaceholder="Search type, severity, status…"
|
||||||
dateFrom={controls.dateFrom}
|
viewId="fleet-incidents"
|
||||||
onDateFromChange={controls.setDateFrom}
|
|
||||||
dateTo={controls.dateTo}
|
|
||||||
onDateToChange={controls.setDateTo}
|
|
||||||
dateLabel="Occurred"
|
|
||||||
hasFilters={controls.hasFilters}
|
|
||||||
onReset={controls.reset}
|
|
||||||
/>
|
/>
|
||||||
<Table striped highlightOnHover>
|
<Table striped highlightOnHover>
|
||||||
<Table.Thead>
|
<Table.Thead>
|
||||||
@@ -288,7 +297,7 @@ export default function IncidentsPage() {
|
|||||||
</Table.Td>
|
</Table.Td>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
) : null}
|
) : null}
|
||||||
{controls.pagedRows.map((incident) => (
|
{pagedIncidents.map((incident) => (
|
||||||
<Table.Tr key={incident.id}>
|
<Table.Tr key={incident.id}>
|
||||||
<Table.Td>{new Date(incident.occurredAt).toLocaleDateString()}</Table.Td>
|
<Table.Td>{new Date(incident.occurredAt).toLocaleDateString()}</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
@@ -316,11 +325,8 @@ export default function IncidentsPage() {
|
|||||||
</Table.Tbody>
|
</Table.Tbody>
|
||||||
</Table>
|
</Table>
|
||||||
<RuleEngineListFooter
|
<RuleEngineListFooter
|
||||||
pagination={controls.pagination}
|
|
||||||
pageCount={controls.pageCount}
|
|
||||||
totalCount={controls.totalCount}
|
|
||||||
itemLabel="incidents"
|
itemLabel="incidents"
|
||||||
onPaginationChange={controls.setPagination}
|
{...toRuleEngineFooterProps(controls, filteredIncidents.length)}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,13 @@ import { PageContainer, PageHeader } from "@/components/page";
|
|||||||
// Generic list footer — already shared by the fleet and train-scheduling lists
|
// Generic list footer — already shared by the fleet and train-scheduling lists
|
||||||
// despite the ruleEngine path; reused here rather than adding a second one.
|
// despite the ruleEngine path; reused here rather than adding a second one.
|
||||||
import RuleEngineListFooter from "@/components/ruleEngine/RuleEngineListFooter";
|
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 { useTrucksOnSite } from "@/hooks/useWarehouses";
|
||||||
import type { TruckOnSite } from "@/types/warehouse";
|
import type { TruckOnSite } from "@/types/warehouse";
|
||||||
import { formatDateTime } from "@/lib/format";
|
import { formatDateTime } from "@/lib/format";
|
||||||
@@ -231,16 +237,8 @@ export default function TrucksOnSitePage() {
|
|||||||
<>
|
<>
|
||||||
<Rows rows={pagedTrucks} />
|
<Rows rows={pagedTrucks} />
|
||||||
<RuleEngineListFooter
|
<RuleEngineListFooter
|
||||||
pagination={{ pageIndex: controls.page - 1, pageSize: controls.pageSize }}
|
|
||||||
pageCount={Math.max(1, Math.ceil(filteredTrucks.length / controls.pageSize))}
|
|
||||||
totalCount={filteredTrucks.length}
|
|
||||||
itemLabel="trucks"
|
itemLabel="trucks"
|
||||||
onPaginationChange={(updater) => {
|
{...toRuleEngineFooterProps(controls, filteredTrucks.length)}
|
||||||
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);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user