feat(filters): migrate FleetResourcePage to the pill filter bar

The real live fleet page — FleetCrudPages.tsx (previous commit) turned
out to be dead code; every fleet route (locomotives/trains/wagons/
containers/cargoes/vehicles/drivers) actually renders this one,
config-driven off resources.ts. Highest-leverage single file in the
remaining inventory: 7 routes at once.

- filterDefs are built per slug from `config.listFilters` (status/yard/
  wagon type/train…, wired to real server params via each def's
  default `{key: value}` toParams) plus a "Registered" date range. The
  3 slugs with no server list filters (trains/containers/cargoes) fall
  back to a plain client-only Status filter off a fixed enum, not
  "whatever status exists in the currently-loaded rows" — the latter
  would be circular (filterDefs feeds useFilters, which feeds the
  query that produces those rows).
- serverListFilters/pagedFilters derive straight from `controls.params`
  instead of hand-picking each field off local state — the def keys
  already match the API's param names, so this is mostly free.
- filteredRows keeps the original's exact "usesServerListFilters ⇒
  trust the server, don't re-check client-side" short-circuit — some
  server list filters (a wagon's `trainNumber` matches either of two
  different columns) aren't expressible as a plain client-side field
  equality, and re-applying them would silently break.
- Pagination moved from a local `usePagination()` (component state) to
  useFilters' URL-backed page/pageSize — the whole point of this pass.
  DataTable takes `controls.tableProps(total)` directly; FleetCardGrid
  (not a DataTable) is fed the same pieces by hand.
- The two manual "reset on slug/filter change" effects are dropped —
  a same-app nav Link to a bare path already clears the query string,
  and useFilters already deletes `page` on every filter/search change.
- FleetToolbar's view-mode SegmentedControl moves into FilterBar's
  `children` slot; its search/filters props are no longer used here.
This commit is contained in:
Nathnael
2026-08-15 08:39:59 +00:00
parent 69805315d9
commit 8ba8376f45

View File

@@ -1,7 +1,5 @@
import type { ColumnDef } from "@edr/ui-common"; import type { ColumnDef } from "@edr/ui-common";
import { Box, Button, Card, Container, Group, Modal, Select, Stack, Text, TextInput, Title } from "@mantine/core"; import { Box, Button, Card, Container, Group, Modal, SegmentedControl, Select, Stack, Text, TextInput, Title } from "@mantine/core";
import { DatePickerInput } from "@mantine/dates";
import { getDateRangePresets } from "@/components/common/dateRangePresets";
import { keepPreviousData, useMutation, useQuery } from "@tanstack/react-query"; import { keepPreviousData, useMutation, useQuery } from "@tanstack/react-query";
import { api } from "@/services/api"; import { api } from "@/services/api";
@@ -13,7 +11,7 @@ import {
FREIGHT_PERMS, FREIGHT_PERMS,
} from "@/lib/permissions"; } from "@/lib/permissions";
import Breadcrumbs from "@/components/ui/Breadcrumbs"; import Breadcrumbs from "@/components/ui/Breadcrumbs";
import { Inbox, Plus, Warehouse } from "lucide-react"; import { Inbox, LayoutGrid, Plus, Table2, Warehouse } from "lucide-react";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { Link, Navigate, useLocation } from "react-router-dom"; import { Link, Navigate, useLocation } from "react-router-dom";
@@ -21,13 +19,12 @@ import FleetCardGrid from "@/components/fleet/FleetCardGrid";
import FleetFormDialog from "@/components/fleet/FleetFormDialog"; import FleetFormDialog from "@/components/fleet/FleetFormDialog";
import FleetHistoryModal from "@/components/fleet/FleetHistoryModal"; import FleetHistoryModal from "@/components/fleet/FleetHistoryModal";
import FleetRecordActions from "@/components/fleet/FleetRecordActions"; import FleetRecordActions from "@/components/fleet/FleetRecordActions";
import FleetToolbar from "@/components/fleet/FleetToolbar";
import { matchesDayRange } from "@/hooks/useListControls"; import { matchesDayRange } from "@/hooks/useListControls";
import WagonMovementHistoryModal from "@/components/fleet/WagonMovementHistoryModal"; import WagonMovementHistoryModal from "@/components/fleet/WagonMovementHistoryModal";
import WagonStatusActions from "@/components/wagons/WagonStatusActions"; import WagonStatusActions from "@/components/wagons/WagonStatusActions";
import WagonYardWorkspaceModal from "@/components/wagons/WagonYardWorkspaceModal"; import WagonYardWorkspaceModal from "@/components/wagons/WagonYardWorkspaceModal";
import { formatFleetCell, registerFleetOptionLabels } from "@/components/fleet/fleetFormat"; import { formatFleetCell, registerFleetOptionLabels } from "@/components/fleet/fleetFormat";
import { useFleetViewMode } from "@/components/fleet/useFleetViewMode"; import { useFleetViewMode, type FleetViewMode } from "@/components/fleet/useFleetViewMode";
import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles"; import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { import {
@@ -43,11 +40,46 @@ import {
type FleetListFilters, type FleetListFilters,
type FleetRecord, type FleetRecord,
} from "@/services/fleet/fleet.service"; } from "@/services/fleet/fleet.service";
import { DataTable, DataTableFooter, usePagination } from "@edr/ui-common"; import { DataTable, DataTableFooter } from "@edr/ui-common";
import { useDebouncedValue } from "@mantine/hooks"; import { dateRangeParams, FilterBar, useFilters, type FilterDef, type FilterOption } from "@/components/filters";
const DEFAULT_SLUG: FleetResourceSlug = "locomotives"; const DEFAULT_SLUG: FleetResourceSlug = "locomotives";
const SERVER_FILTERED_SLUGS: FleetResourceSlug[] = ["wagons", "locomotives", "vehicles", "drivers"];
// trains/containers/cargoes have no server-side `listFilters` config (see
// resources.ts) — they get a plain client-only Status filter instead, off a
// fixed enum rather than "whatever status happens to exist in the currently
// loaded rows" (which would create a circular dependency: filterDefs feeds
// useFilters, which feeds the query that produces those rows).
const TRAIN_STATUS_OPTIONS: FilterOption[] = [
{ value: "AVAILABLE", label: "Available" },
{ value: "SCHEDULED", label: "Scheduled" },
{ value: "IN_SERVICE", label: "In service" },
{ value: "UNDER_MAINTENANCE", label: "Under maintenance" },
{ value: "OUT_OF_SERVICE", label: "Out of service" },
{ value: "DEACTIVATED", label: "Deactivated" },
];
const CONTAINER_STATUS_OPTIONS: FilterOption[] = [
{ value: "AVAILABLE", label: "Available" },
{ value: "LOADED", label: "Loaded" },
{ value: "IN_TRANSIT", label: "In transit" },
{ value: "MAINTENANCE", label: "Maintenance" },
{ value: "DAMAGED", label: "Damaged" },
];
const CARGO_STATUS_OPTIONS: FilterOption[] = [
{ value: "PENDING", label: "Pending" },
{ value: "LOADED", label: "Loaded" },
{ value: "IN_TRANSIT", label: "In transit" },
{ value: "DELIVERED", label: "Delivered" },
{ value: "UNLOADED", label: "Unloaded" },
];
const FALLBACK_STATUS_OPTIONS: Partial<Record<FleetResourceSlug, FilterOption[]>> = {
trains: TRAIN_STATUS_OPTIONS,
containers: CONTAINER_STATUS_OPTIONS,
cargoes: CARGO_STATUS_OPTIONS,
};
const FleetResourcePage = () => { const FleetResourcePage = () => {
const location = useLocation(); const location = useLocation();
const slug = getFleetSlugFromPath(location.pathname) ?? DEFAULT_SLUG; const slug = getFleetSlugFromPath(location.pathname) ?? DEFAULT_SLUG;
@@ -70,18 +102,9 @@ const FleetResourcePage = () => {
hasPermission(user, FREIGHT_PERMS.wagons.transferFulfill) || hasPermission(user, FREIGHT_PERMS.wagons.transferFulfill) ||
hasPermission(user, FREIGHT_PERMS.wagons.transferHistoryAll); hasPermission(user, FREIGHT_PERMS.wagons.transferHistoryAll);
const { pagination, setPagination } = usePagination({ pageSize: 10 });
const [search, setSearch] = useState("");
const [debouncedSearch] = useDebouncedValue(search, 300);
// Wagons and locomotives page in the database; the rest still list in full // Wagons and locomotives page in the database; the rest still list in full
// and page in the browser (see `pagedHandlers` in fleet.service). // and page in the browser (see `pagedHandlers` in fleet.service).
const serverPaged = isFleetServerPaginated(slug); const serverPaged = isFleetServerPaginated(slug);
const [statusFilter, setStatusFilter] = useState("ALL");
// Registration date range. Server-side list filters (status/yard/train) are
// applied by the API; this narrows what comes back, alongside search.
const [dateFrom, setDateFrom] = useState<string | null>(null);
const [dateTo, setDateTo] = useState<string | null>(null);
const [listFilterValues, setListFilterValues] = useState<Record<string, string>>({});
const [formOpen, setFormOpen] = useState(false); const [formOpen, setFormOpen] = useState(false);
const [editing, setEditing] = useState<FleetRecord | null>(null); const [editing, setEditing] = useState<FleetRecord | null>(null);
const [removeTarget, setRemoveTarget] = useState<FleetRecord | null>(null); const [removeTarget, setRemoveTarget] = useState<FleetRecord | null>(null);
@@ -95,77 +118,6 @@ const FleetResourcePage = () => {
const [wagonWorkspaceOpen, setWagonWorkspaceOpen] = useState(false); const [wagonWorkspaceOpen, setWagonWorkspaceOpen] = useState(false);
const { viewMode, setViewMode } = useFleetViewMode(slug); const { viewMode, setViewMode } = useFleetViewMode(slug);
const serverListFilters = useMemo((): FleetListFilters | undefined => {
const serverFilteredSlugs: FleetResourceSlug[] = ["wagons", "locomotives", "vehicles", "drivers"];
if (!serverFilteredSlugs.includes(slug)) return undefined;
const filters: FleetListFilters = {};
const status = listFilterValues.status;
const currentYardId = listFilterValues.currentYardId;
const availability = listFilterValues.availability;
const trainNumber = listFilterValues.trainNumber;
const trainId = listFilterValues.trainId;
if (status && status !== "ALL") {
(filters as { status?: string }).status = status;
}
if (currentYardId && currentYardId !== "ALL") {
filters.currentYardId = currentYardId;
}
if (availability && availability !== "ALL") {
(filters as { availability?: string }).availability = availability;
}
if (trainNumber && trainNumber !== "ALL") {
(filters as { trainNumber?: string }).trainNumber = trainNumber;
}
if (trainId && trainId !== "ALL") {
filters.trainId = trainId;
}
// Wagons only: narrow the fleet to one wagon type (the API filters on it).
const wagonTypeId = listFilterValues.wagonTypeId;
if (wagonTypeId && wagonTypeId !== "ALL") {
(filters as { wagonTypeId?: string }).wagonTypeId = wagonTypeId;
}
// The plain locomotives list has no server-side search — its page window
// does, so the term is only sent on the paginated path.
if ((serverPaged || slug !== "locomotives") && debouncedSearch.trim()) {
filters.search = debouncedSearch.trim();
}
return filters;
}, [slug, listFilterValues, debouncedSearch, serverPaged]);
// On the server-paged path the page window, the search and the registration
// date range are all resolved by the API — nothing is filtered client-side.
const pagedFilters = useMemo(
(): FleetListFilters => ({
...serverListFilters,
page: pagination.pageIndex + 1,
pageSize: pagination.pageSize,
...(dateFrom ? { createdFrom: dateFrom } : {}),
...(dateTo ? { createdTo: dateTo } : {}),
}),
[serverListFilters, pagination.pageIndex, pagination.pageSize, dateFrom, dateTo],
);
const listQuery = useQuery({
...api.fleet.list.queryOptions({ input: { slug, filters: serverListFilters } }),
enabled: !serverPaged,
});
const pagedQuery = useQuery({
...api.fleet.listPaged.queryOptions({ input: { slug, filters: pagedFilters } }),
enabled: serverPaged,
placeholderData: keepPreviousData,
});
const activeQuery = serverPaged ? pagedQuery : listQuery;
const { isLoading, isError, error } = activeQuery;
const allRows = useMemo(
() => (serverPaged ? (pagedQuery.data?.items ?? []) : (listQuery.data ?? [])),
[serverPaged, pagedQuery.data, listQuery.data],
);
const create = useMutation(api.fleet.create.mutationOptions());
const update = useMutation(api.fleet.update.mutationOptions());
const remove = useMutation(api.fleet.remove.mutationOptions());
const purge = useMutation(api.fleet.purge.mutationOptions());
const { data: wagonTypes = [], isLoading: wagonTypesLoading } = useQuery( const { data: wagonTypes = [], isLoading: wagonTypesLoading } = useQuery(
api.wagonTypes.list.queryOptions(), api.wagonTypes.list.queryOptions(),
); );
@@ -202,40 +154,8 @@ const FleetResourcePage = () => {
enabled: slug === "wagons", enabled: slug === "wagons",
}); });
useEffect(() => {
setPagination((prev) => ({ pageIndex: 0, pageSize: prev.pageSize }));
setSearch("");
setStatusFilter("ALL");
setListFilterValues({});
}, [slug, setPagination]);
useEffect(() => {
setPagination((prev) => ({ pageIndex: 0, pageSize: prev.pageSize }));
}, [search, listFilterValues, dateFrom, dateTo, setPagination]);
const hasStatusColumn = Boolean(config?.columns.some((col) => col.accessorKey === "status"));
const usesServerListFilters = Boolean(config?.listFilters?.length); const usesServerListFilters = Boolean(config?.listFilters?.length);
const statusFilterOptions = useMemo(() => {
if (!hasStatusColumn || usesServerListFilters) return [];
if (slug === "vehicles" || slug === "drivers") {
return [
{ value: "ALL", label: "All statuses" },
{ value: "ACTIVE", label: "Active" },
{ value: "INACTIVE", label: "Inactive" },
];
}
const statuses = new Set(
allRows
.map((row) => String((row as unknown as Record<string, unknown>).status ?? ""))
.filter(Boolean),
);
return [
{ value: "ALL", label: "All statuses" },
...[...statuses].sort().map((status) => ({ value: status, label: status })),
];
}, [allRows, hasStatusColumn, usesServerListFilters, slug]);
const dynamicOptions = useMemo(() => { const dynamicOptions = useMemo(() => {
const wagonTypeOpts = (wagonTypes as Array<{ id: string; code: string; name?: string }>).map( const wagonTypeOpts = (wagonTypes as Array<{ id: string; code: string; name?: string }>).map(
(t) => ({ value: t.id, label: `${t.code}${t.name ? ` - ${t.name}` : ""}` }), (t) => ({ value: t.id, label: `${t.code}${t.name ? ` - ${t.name}` : ""}` }),
@@ -291,25 +211,78 @@ const FleetResourcePage = () => {
}; };
}, [wagonTypes, containerTypes, cargoTypes, truckTypes, wagons, containers, yards, trains]); }, [wagonTypes, containerTypes, cargoTypes, truckTypes, wagons, containers, yards, trains]);
const listFilterSelects = useMemo(() => { // One pill per configured server list filter (status/yard/wagon type/train…),
if (!config?.listFilters?.length) return null; // built off `config.listFilters` — same source the old plain `<Select>` row
return config.listFilters.map((filter) => { // read, just reshaped into FilterDefs. Falls back to a plain client-only
const dynamicOpts = filter.dynamicOptions // Status filter for the 3 slugs with no server-side list filters at all.
? (dynamicOptions[filter.dynamicOptions] ?? []) const filterDefs: FilterDef[] = useMemo(() => {
: []; const dateDef: FilterDef = {
const staticOpts = key: "created",
filter.options?.map((opt) => ({ value: opt.value, label: opt.label })) ?? []; label: "Registered",
const opts = filter.dynamicOptions ? dynamicOpts : staticOpts; type: "date",
return { secondary: true,
...filter, toParams: dateRangeParams("createdFrom", "createdTo"),
value: listFilterValues[filter.key] ?? "ALL", };
data: [ if (config?.listFilters?.length) {
{ value: "ALL", label: filter.allLabel ?? `All ${filter.label.toLowerCase()}` }, return [
...opts, ...config.listFilters.map((filter): FilterDef => ({
], key: filter.key,
}; label: filter.label,
}); type: "enum",
}, [config?.listFilters, listFilterValues, dynamicOptions]); multiple: false,
options: filter.dynamicOptions
? (dynamicOptions[filter.dynamicOptions] ?? [])
: (filter.options ?? []),
})),
dateDef,
];
}
const fallback = FALLBACK_STATUS_OPTIONS[slug];
return fallback
? [{ key: "status", label: "Status", type: "enum", multiple: false, options: fallback }, dateDef]
: [dateDef];
}, [config, dynamicOptions, slug]);
const controls = useFilters(filterDefs, { pageSize: 10 });
// On the server-paged path the page window, the search and the registration
// date range are all resolved by the API — nothing is filtered client-side.
// `controls.params` already carries every filter's mapped param name (status/
// currentYardId/wagonTypeId/… default to `{key: value}`, "created" maps to
// createdFrom/createdTo) plus search/page/pageSize — it IS the paged filter
// object; the unpaged one is the same minus pagination and the date range
// (which stays client-only for the non-server-paged slugs, see below).
const serverListFilters = useMemo((): FleetListFilters | undefined => {
if (!SERVER_FILTERED_SLUGS.includes(slug)) return undefined;
const { page: _page, pageSize: _pageSize, createdFrom: _cf, createdTo: _ct, ...rest } = controls.params;
return rest as FleetListFilters;
}, [slug, controls.params]);
const pagedFilters = useMemo(
(): FleetListFilters => controls.params as unknown as FleetListFilters,
[controls.params],
);
const listQuery = useQuery({
...api.fleet.list.queryOptions({ input: { slug, filters: serverListFilters } }),
enabled: !serverPaged,
});
const pagedQuery = useQuery({
...api.fleet.listPaged.queryOptions({ input: { slug, filters: pagedFilters } }),
enabled: serverPaged,
placeholderData: keepPreviousData,
});
const activeQuery = serverPaged ? pagedQuery : listQuery;
const { isLoading, isError, error } = activeQuery;
const allRows = useMemo(
() => (serverPaged ? (pagedQuery.data?.items ?? []) : (listQuery.data ?? [])),
[serverPaged, pagedQuery.data, listQuery.data],
);
const create = useMutation(api.fleet.create.mutationOptions());
const update = useMutation(api.fleet.update.mutationOptions());
const remove = useMutation(api.fleet.remove.mutationOptions());
const purge = useMutation(api.fleet.purge.mutationOptions());
useEffect(() => { useEffect(() => {
registerFleetOptionLabels("wagonTypeId", dynamicOptions.wagonTypes); registerFleetOptionLabels("wagonTypeId", dynamicOptions.wagonTypes);
@@ -349,16 +322,22 @@ const FleetResourcePage = () => {
// The API already applied every filter and cut the page — re-filtering here // The API already applied every filter and cut the page — re-filtering here
// would drop rows the server deliberately returned. // would drop rows the server deliberately returned.
if (serverPaged) return allRows; if (serverPaged) return allRows;
const term = search.trim().toLowerCase();
return allRows.filter((row) => { return allRows.filter((row) => {
const record = row as unknown as Record<string, unknown>; const record = row as unknown as Record<string, unknown>;
// The date range applies even when the API already filtered the list — // The date range applies even when the API already filtered the list —
// it is not one of the server-side filters. // it is not one of the server-side filters.
if (!matchesDayRange(record.createdAt, dateFrom, dateTo)) return false; const created = controls.values.created;
if (usesServerListFilters) return true; if (created && !matchesDayRange(record.createdAt, created.v[0]?.slice(0, 10) ?? null, created.v[1]?.slice(0, 10) ?? null)) {
if (statusFilter !== "ALL" && String(record.status ?? "") !== statusFilter) {
return false; return false;
} }
// Every other filter (status/yard/wagon type/…) was already applied
// server-side for these slugs — re-checking here against a plain field
// equality would be wrong for one (a wagon's "trainNumber" filter
// matches either of two DIFFERENT columns server-side, not one).
if (usesServerListFilters) return true;
const status = controls.values.status;
if (status && String(record.status ?? "") !== status.v[0]) return false;
const term = controls.searchText.trim().toLowerCase();
if (!term) return true; if (!term) return true;
return config.searchKeys.some((key) => return config.searchKeys.some((key) =>
String(record[key] ?? "") String(record[key] ?? "")
@@ -366,19 +345,23 @@ const FleetResourcePage = () => {
.includes(term), .includes(term),
); );
}); });
}, [allRows, search, statusFilter, config, usesServerListFilters, dateFrom, dateTo, serverPaged]); }, [allRows, config, usesServerListFilters, controls.values, controls.searchText, serverPaged]);
const totalCount = serverPaged const totalCount = serverPaged
? (pagedQuery.data?.meta.total ?? 0) ? (pagedQuery.data?.meta.total ?? 0)
: filteredRows.length; : filteredRows.length;
const pageCount = serverPaged const pageCount = serverPaged
? Math.max(1, pagedQuery.data?.meta.totalPages ?? 1) ? Math.max(1, pagedQuery.data?.meta.totalPages ?? 1)
: Math.max(1, Math.ceil(filteredRows.length / pagination.pageSize)); : Math.max(1, Math.ceil(filteredRows.length / controls.pageSize));
const pagedRows = useMemo(() => { const pagedRows = useMemo(() => {
if (serverPaged) return filteredRows; if (serverPaged) return filteredRows;
const start = pagination.pageIndex * pagination.pageSize; const start = (controls.page - 1) * controls.pageSize;
return filteredRows.slice(start, start + pagination.pageSize); return filteredRows.slice(start, start + controls.pageSize);
}, [filteredRows, pagination.pageIndex, pagination.pageSize, serverPaged]); }, [filteredRows, controls.page, controls.pageSize, serverPaged]);
// Same {pagination, tableOptions} shape DataTable takes directly; FleetCardGrid
// (not a DataTable) just needs the raw pieces out of it below.
const { pagination: dtPagination, tableOptions: dtTableOptions } = controls.tableProps(totalCount);
const columns = useMemo((): ColumnDef<FleetRecord>[] => { const columns = useMemo((): ColumnDef<FleetRecord>[] => {
if (!config) return []; if (!config) return [];
@@ -604,77 +587,41 @@ const FleetResourcePage = () => {
<Card radius="lg" padding={0} withBorder style={{ borderColor: "var(--mantine-color-gray-2)" }}> <Card radius="lg" padding={0} withBorder style={{ borderColor: "var(--mantine-color-gray-2)" }}>
<Stack gap={0}> <Stack gap={0}>
<Box px="md" pt="md" pb="md" w="100%" style={{ borderBottom: "1px solid var(--mantine-color-gray-2)" }}> <Box px="md" pt="md" pb="md" w="100%" style={{ borderBottom: "1px solid var(--mantine-color-gray-2)" }}>
<FleetToolbar <FilterBar
search={search} defs={filterDefs}
onSearchChange={setSearch} controls={controls}
searchPlaceholder={config.searchPlaceholder} searchPlaceholder={config.searchPlaceholder}
showSearch={config.supportsSearch} showSearch={config.supportsSearch}
viewMode={viewMode} viewId={`fleet-${slug}`}
onViewModeChange={setViewMode} >
filters={ <SegmentedControl
<Group gap="sm" wrap="wrap" align="center"> value={viewMode}
<DatePickerInput onChange={(value) => setViewMode(value as FleetViewMode)}
type="range" size="sm"
aria-label="Created date range" radius="lg"
placeholder="Created date range" data={[
value={[dateFrom, dateTo]} {
onChange={([from, to]) => { value: "table",
setDateFrom(from); label: (
setDateTo(to); <Group gap={6} justify="center" wrap="nowrap">
}} <Table2 size={14} />
presets={getDateRangePresets()} <span>Table</span>
clearable </Group>
size="sm" ),
radius="lg" },
w={240} {
/> value: "cards",
{listFilterSelects ? ( label: (
<Group gap="sm" wrap="wrap" align="center"> <Group gap={6} justify="center" wrap="nowrap">
{listFilterSelects.map((filter) => ( <LayoutGrid size={14} />
<Select <span>Cards</span>
key={filter.key} </Group>
aria-label={filter.label} ),
placeholder={filter.data[0]?.label ?? filter.label} },
data={filter.data} ]}
value={filter.value} styles={{ root: { background: "var(--mantine-color-gray-1)" } }}
onChange={(value) => { />
setListFilterValues((prev) => ({ </FilterBar>
...prev,
[filter.key]: value ?? "ALL",
}));
setPagination((prev) => ({ ...prev, pageIndex: 0 }));
}}
size="sm"
radius="lg"
w={200}
searchable={filter.data.length > 8}
comboboxProps={{ withinPortal: true }}
styles={{ input: { borderColor: "var(--mantine-color-gray-3)" } }}
/>
))}
</Group>
) : hasStatusColumn && statusFilterOptions.length > 1 ? (
<Group gap={4} wrap="wrap">
<Text size="xs" fw={500} c="dimmed">Status:</Text>
<Group gap={4} wrap="wrap">
{[{ value: "ALL", label: "All" }, ...statusFilterOptions].map((option) => (
<Button
key={option.value}
size="xs"
radius="md"
variant={statusFilter === option.value ? "filled" : "outline"}
styles={{ label: { fontWeight: 500 } }}
onClick={() => setStatusFilter(option.value)}
>
{option.label}
</Button>
))}
</Group>
</Group>
) : null}
</Group>
}
/>
</Box> </Box>
{viewMode === "table" ? ( {viewMode === "table" ? (
@@ -696,18 +643,8 @@ const FleetResourcePage = () => {
: undefined : undefined
} }
emptyMessage={`No ${itemLabel} found`} emptyMessage={`No ${itemLabel} found`}
pagination={{ pagination={dtPagination}
pageIndex: pagination.pageIndex, tableOptions={dtTableOptions}
pageSize: pagination.pageSize,
pageCount,
totalCount,
}}
tableOptions={{
manualPagination: true,
pageCount,
state: { pagination },
onPaginationChange: setPagination,
}}
containerClassName="border-0 shadow-none bg-transparent" containerClassName="border-0 shadow-none bg-transparent"
footer={({ table, pagination: footerPagination }) => ( footer={({ table, pagination: footerPagination }) => (
<DataTableFooter <DataTableFooter
@@ -724,10 +661,10 @@ const FleetResourcePage = () => {
rows={pagedRows} rows={pagedRows}
status={tableStatus} status={tableStatus}
emptyMessage={`No ${itemLabel} found`} emptyMessage={`No ${itemLabel} found`}
pagination={pagination} pagination={{ pageIndex: controls.page - 1, pageSize: controls.pageSize }}
pageCount={pageCount} pageCount={pageCount}
totalCount={totalCount} totalCount={totalCount}
onPaginationChange={setPagination} onPaginationChange={dtTableOptions!.onPaginationChange!}
onEdit={ onEdit={
canUpdate canUpdate
? (record) => { ? (record) => {