Files
edr-platform/apps/edr-freight-web/backoffice/src/pages/fleet/FleetResourcePage.tsx
Nathnael 58b47318e9 feat(freight-backoffice): consolidate Mantine from/to date filters into range pickers
Replaces every remaining split from/to Mantine DateInput/DatePickerInput pair
with a single DatePickerInput type="range", sharing one preset list (Today,
Last 7/30 days, this/last month, YTD) via getDateRangePresets(). Covers
ListControls (18 consumers), FleetResourcePage, ContractRequestsPage,
BookingRequestsPage (created + scheduled ranges), WagonCancellationsPage,
BatchBoardPage, ClearanceDocumentsPage, ShipmentRequestsPage.

Native Mantine range picker, not the shadcn DateRangePicker, to match each
page's existing design system instead of clashing with it.

Reports date-range filter (ReportFilters.tsx) intentionally left untouched.
2026-08-13 08:45:29 +00:00

900 lines
34 KiB
TypeScript

import type { ColumnDef } from "@edr/ui-common";
import { Box, Button, Card, Container, Group, Modal, 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 { api } from "@/services/api";
import { useAuth } from "@/auth/useAuth";
import {
canFleetAction,
canFleetHardDelete,
hasPermission,
FREIGHT_PERMS,
} from "@/lib/permissions";
import Breadcrumbs from "@/components/ui/Breadcrumbs";
import { Inbox, Plus, Warehouse } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { Link, Navigate, useLocation } from "react-router-dom";
import FleetCardGrid from "@/components/fleet/FleetCardGrid";
import FleetFormDialog from "@/components/fleet/FleetFormDialog";
import FleetHistoryModal from "@/components/fleet/FleetHistoryModal";
import FleetRecordActions from "@/components/fleet/FleetRecordActions";
import FleetToolbar from "@/components/fleet/FleetToolbar";
import { matchesDayRange } from "@/hooks/useListControls";
import WagonMovementHistoryModal from "@/components/fleet/WagonMovementHistoryModal";
import WagonStatusActions from "@/components/wagons/WagonStatusActions";
import WagonYardWorkspaceModal from "@/components/wagons/WagonYardWorkspaceModal";
import { formatFleetCell, registerFleetOptionLabels } from "@/components/fleet/fleetFormat";
import { useFleetViewMode } from "@/components/fleet/useFleetViewMode";
import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles";
import { useToast } from "@/hooks/use-toast";
import {
FLEET_SELECT_NONE,
getFleetResource,
getFleetSlugFromPath,
type FleetFormFieldDef,
type FleetResourceSlug,
} from "@/pages/fleet/config/resources";
import {
isFleetPurgeable,
isFleetServerPaginated,
type FleetListFilters,
type FleetRecord,
} from "@/services/fleet/fleet.service";
import { DataTable, DataTableFooter, usePagination } from "@edr/ui-common";
import { useDebouncedValue } from "@mantine/hooks";
const DEFAULT_SLUG: FleetResourceSlug = "locomotives";
const FleetResourcePage = () => {
const location = useLocation();
const slug = getFleetSlugFromPath(location.pathname) ?? DEFAULT_SLUG;
const config = getFleetResource(slug);
const { toast } = useToast();
const { user } = useAuth();
const canCreate = canFleetAction(user, slug, "create");
const canUpdate = canFleetAction(user, slug, "update");
const canDelete = canFleetAction(user, slug, "delete");
// Irreversible purge: only locomotives/wagons expose it, and it needs its own
// grant — the coarse fleet:manage key deliberately does not unlock it.
const canPurge =
isFleetPurgeable(slug) &&
(slug === "locomotives" || slug === "wagons") &&
canFleetHardDelete(user, slug);
// Wagon transfer workspace: shown only to holders of a transfer capability
// (raise a request, fulfill one, or see the cross-yard history).
const canTransfer =
hasPermission(user, FREIGHT_PERMS.wagons.transferRequest) ||
hasPermission(user, FREIGHT_PERMS.wagons.transferFulfill) ||
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
// and page in the browser (see `pagedHandlers` in fleet.service).
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 [editing, setEditing] = useState<FleetRecord | null>(null);
const [removeTarget, setRemoveTarget] = useState<FleetRecord | null>(null);
const [purgeTarget, setPurgeTarget] = useState<FleetRecord | null>(null);
// Typing the record's own code is the confirmation — a purge cannot be undone,
// so a single misplaced click must not be enough to trigger it.
const [purgeConfirmText, setPurgeConfirmText] = useState("");
const [assigningDriver, setAssigningDriver] = useState<FleetRecord | null>(null);
const [historyTarget, setHistoryTarget] = useState<FleetRecord | null>(null);
const [selectedDriver, setSelectedDriver] = useState<string>("");
const [wagonWorkspaceOpen, setWagonWorkspaceOpen] = useState(false);
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(
api.wagonTypes.list.queryOptions(),
);
const { data: truckTypes = [], isLoading: truckTypesLoading } = useQuery(
api.truckTypes.list.queryOptions(),
);
const { data: containerTypes = [], isLoading: containerTypesLoading } = useQuery(
api.containerTypes.list.queryOptions({ staleTime: Infinity }),
);
const { data: cargoTypes = [], isLoading: cargoTypesLoading } = useQuery(
api.cargoTypes.list.queryOptions({ staleTime: Infinity }),
);
// Whole-fleet list for the "Wagon" form select — page-walked, so only fetch it
// where a form actually offers that select (containers), not on every slug.
const needsWagonOptions = Boolean(
config?.formFields.some((field) => field.dynamicOptions === "wagons"),
);
const { data: wagons = [], isLoading: wagonsLoading } = useQuery({
...api.wagons.list.queryOptions({ input: {} }),
enabled: needsWagonOptions,
});
const { data: containers = [], isLoading: containersLoading } = useQuery(
api.containers.list.queryOptions(),
);
const { data: yards = [], isLoading: yardsLoading } = useQuery(
api.routes.yards.queryOptions(),
);
const { data: drivers = [] } = useQuery(
api.fleet.list.queryOptions({ input: { slug: "drivers" } }),
);
// Wagons-only: "Train" list filter needs every train's code to pick from.
const { data: trains = [], isLoading: trainsLoading } = useQuery({
...api.trains.list.queryOptions(),
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 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 wagonTypeOpts = (wagonTypes as Array<{ id: string; code: string; name?: string }>).map(
(t) => ({ value: t.id, label: `${t.code}${t.name ? ` - ${t.name}` : ""}` }),
);
const containerTypeOpts = (
containerTypes as Array<{ id: string; label?: string; code?: string }>
).map((t) => ({ value: t.id, label: t.label ?? t.code ?? t.id }));
const cargoTypeOpts = (
cargoTypes as Array<{ id: string; cargoTypeName?: string; code?: string }>
).map((t) => ({ value: t.id, label: t.cargoTypeName ?? t.code ?? t.id }));
const wagonOpts = (wagons as Array<{ id: string; wagonNumber: string }>).map((w) => ({
value: w.id,
label: w.wagonNumber,
}));
const containerOpts = (containers as Array<{ id: string; containerNumber: string }>).map(
(c) => ({ value: c.id, label: c.containerNumber }),
);
const yardOpts = (yards as Array<{ id: string; label?: string; code?: string }>).map(
(y) => ({ value: y.id, label: y.label ?? y.code ?? y.id }),
);
const trainOpts = (trains as Array<{ id: string; code: string; trainName?: string | null }>).map(
(t) => ({ value: t.id, label: t.trainName ? `${t.code} - ${t.trainName}` : t.code }),
);
// Carries capacity + trailer configuration so picking a truck type can
// pre-fill the vehicle's capacity and drop the trailer plate on a rigid type.
const truckTypeOpts = (
truckTypes as Array<{
id: string;
code: string;
name?: string;
capacityTons?: number | null;
hasTrailer?: boolean;
}>
).map((t) => ({
value: t.id,
label: t.name ? `${t.name} (${t.code})` : t.code,
meta: { capacityTons: t.capacityTons, hasTrailer: t.hasTrailer },
}));
registerFleetOptionLabels("currentYardId", yardOpts);
return {
wagonTypes: wagonTypeOpts,
containerTypes: containerTypeOpts,
cargoTypes: [{ label: "None", value: FLEET_SELECT_NONE }, ...cargoTypeOpts],
truckTypes: truckTypeOpts,
wagons: [{ label: "Unassigned", value: FLEET_SELECT_NONE }, ...wagonOpts],
containers: containerOpts,
yards: yardOpts,
trains: trainOpts,
};
}, [wagonTypes, containerTypes, cargoTypes, truckTypes, wagons, containers, yards, trains]);
const listFilterSelects = useMemo(() => {
if (!config?.listFilters?.length) return null;
return config.listFilters.map((filter) => {
const dynamicOpts = filter.dynamicOptions
? (dynamicOptions[filter.dynamicOptions] ?? [])
: [];
const staticOpts =
filter.options?.map((opt) => ({ value: opt.value, label: opt.label })) ?? [];
const opts = filter.dynamicOptions ? dynamicOpts : staticOpts;
return {
...filter,
value: listFilterValues[filter.key] ?? "ALL",
data: [
{ value: "ALL", label: filter.allLabel ?? `All ${filter.label.toLowerCase()}` },
...opts,
],
};
});
}, [config?.listFilters, listFilterValues, dynamicOptions]);
useEffect(() => {
registerFleetOptionLabels("wagonTypeId", dynamicOptions.wagonTypes);
registerFleetOptionLabels("containerTypeId", dynamicOptions.containerTypes);
registerFleetOptionLabels(
"cargoTypeId",
dynamicOptions.cargoTypes.filter((o) => o.value !== FLEET_SELECT_NONE),
);
registerFleetOptionLabels("wagonId", dynamicOptions.wagons);
registerFleetOptionLabels("containerId", dynamicOptions.containers);
registerFleetOptionLabels("currentYardId", dynamicOptions.yards);
registerFleetOptionLabels("locationId", dynamicOptions.yards);
registerFleetOptionLabels("truckTypeId", dynamicOptions.truckTypes);
}, [dynamicOptions]);
const formFields = useMemo((): FleetFormFieldDef[] => {
if (!config) return [];
return config.formFields.map((field) => {
if (!field.dynamicOptions) return field;
const options = dynamicOptions[field.dynamicOptions] ?? [];
return { ...field, type: "select" as const, options };
});
}, [config, dynamicOptions]);
const selectOptionsLoading =
wagonTypesLoading ||
containerTypesLoading ||
cargoTypesLoading ||
truckTypesLoading ||
wagonsLoading ||
containersLoading ||
yardsLoading ||
trainsLoading;
const filteredRows = useMemo(() => {
if (!config) return allRows;
// The API already applied every filter and cut the page — re-filtering here
// would drop rows the server deliberately returned.
if (serverPaged) return allRows;
const term = search.trim().toLowerCase();
return allRows.filter((row) => {
const record = row as unknown as Record<string, unknown>;
// The date range applies even when the API already filtered the list —
// it is not one of the server-side filters.
if (!matchesDayRange(record.createdAt, dateFrom, dateTo)) return false;
if (usesServerListFilters) return true;
if (statusFilter !== "ALL" && String(record.status ?? "") !== statusFilter) {
return false;
}
if (!term) return true;
return config.searchKeys.some((key) =>
String(record[key] ?? "")
.toLowerCase()
.includes(term),
);
});
}, [allRows, search, statusFilter, config, usesServerListFilters, dateFrom, dateTo, serverPaged]);
const totalCount = serverPaged
? (pagedQuery.data?.meta.total ?? 0)
: filteredRows.length;
const pageCount = serverPaged
? Math.max(1, pagedQuery.data?.meta.totalPages ?? 1)
: Math.max(1, Math.ceil(filteredRows.length / pagination.pageSize));
const pagedRows = useMemo(() => {
if (serverPaged) return filteredRows;
const start = pagination.pageIndex * pagination.pageSize;
return filteredRows.slice(start, start + pagination.pageSize);
}, [filteredRows, pagination.pageIndex, pagination.pageSize, serverPaged]);
const columns = useMemo((): ColumnDef<FleetRecord>[] => {
if (!config) return [];
const headerClassName = ruleEngineTable.headerCell;
const cellClassName = ruleEngineTable.bodyCell;
const base: ColumnDef<FleetRecord>[] = config.columns.map((col) => ({
id: col.id,
accessorKey: col.accessorKey,
header: col.header,
size: col.size || 150,
minSize: col.size ? Math.max(col.size - 20, 80) : 80,
meta: { headerClassName, cellClassName },
cell: ({ row }) => {
const value = (row.original as unknown as Record<string, unknown>)[col.accessorKey];
return formatFleetCell(value, col.format, col.accessorKey);
},
}));
base.push({
id: "actions",
header: "Actions",
// Wagons carry the inline maintenance toggle, which needs more room.
size: config.slug === "wagons" ? 240 : 160,
meta: { headerClassName, cellClassName: `${cellClassName} whitespace-nowrap` },
cell: ({ row }) => (
<div onClick={(e) => e.stopPropagation()} data-stop-row-click>
<Group gap={4} wrap="nowrap">
{config.slug === "wagons" ? (
<WagonStatusActions record={row.original} canUpdate={canUpdate} />
) : null}
<FleetRecordActions
record={row.original}
config={config}
layout="compact"
onEdit={
canUpdate
? (record) => {
setEditing(record);
setFormOpen(true);
}
: undefined
}
onRemove={canDelete ? setRemoveTarget : undefined}
onPurge={canPurge ? setPurgeTarget : undefined}
onAssignDriver={canUpdate ? setAssigningDriver : undefined}
onHistory={setHistoryTarget}
/>
</Group>
</div>
),
});
return base;
}, [config, dynamicOptions.yards, canUpdate, canDelete]);
const tableStatus = isLoading ? "loading" : isError ? "error" : "success";
if (!config) {
return <Navigate to="/dashboard/locomotives" replace />;
}
const handleFormSubmit = async (values: Record<string, unknown>) => {
try {
if (editing && "id" in editing) {
// PATCH only the fields the user actually changed. Re-sending the whole
// form used to re-submit status/currentYardId on every save — which,
// for a locomotive/wagon coupled to a built train, silently diverged
// the consist (editing a name could move the loco to another yard).
const editingRecord = editing as unknown as Record<string, unknown>;
const changed = Object.fromEntries(
Object.entries(values).filter(
([key, value]) => value !== editingRecord[key],
),
);
await update.mutateAsync({ slug, id: String(editing.id), data: changed });
toast({ title: `${config.entityLabel} updated` });
} else {
await create.mutateAsync({ slug, data: values });
toast({ title: `${config.entityLabel} created` });
}
setFormOpen(false);
setEditing(null);
} catch (err: unknown) {
const message =
(err as { response?: { data?: { message?: string } } })?.response?.data?.message ??
"Save failed";
toast({ title: "Save failed", description: String(message), variant: "destructive" });
}
};
const handleRemove = async () => {
if (!removeTarget || !("id" in removeTarget)) return;
try {
await remove.mutateAsync({ slug, id: String(removeTarget.id) });
toast({
title: config.removeSuccessMessage ?? `${config.entityLabel} removed`,
});
setRemoveTarget(null);
} catch (err: unknown) {
const message =
(err as { response?: { data?: { message?: string } } })?.response?.data?.message ??
"Remove failed";
toast({ title: "Remove failed", description: String(message), variant: "destructive" });
}
};
/** The code the operator must retype to confirm a purge. */
const purgeFields = purgeTarget
? (purgeTarget as unknown as Record<string, unknown>)
: null;
const purgeLabel = purgeFields
? String(purgeFields.wagonNumber ?? purgeFields.code ?? "")
: "";
const closePurge = () => {
setPurgeTarget(null);
setPurgeConfirmText("");
};
const handlePurge = async () => {
if (!purgeTarget || !("id" in purgeTarget)) return;
try {
await purge.mutateAsync({ slug, id: String(purgeTarget.id) });
toast({ title: `${config.entityLabel} permanently deleted` });
closePurge();
} catch (err: unknown) {
const message =
(err as { response?: { data?: { message?: string } } })?.response?.data?.message ??
"Permanent delete failed";
toast({
title: "Permanent delete failed",
description: String(message),
variant: "destructive",
});
}
};
const handleAssignDriver = async () => {
if (!assigningDriver || !("id" in assigningDriver) || !selectedDriver) return;
try {
const selectedDriverRecord = (drivers as unknown as Array<Record<string, unknown>>).find(
(d) => String(d.id) === selectedDriver
);
if (!selectedDriverRecord) return;
const driverName = `${selectedDriverRecord.firstName} ${selectedDriverRecord.lastName}`;
await update.mutateAsync({
slug,
id: String(assigningDriver.id),
data: {
assignedDriverId: selectedDriver,
assignedDriverName: driverName,
},
});
toast({ title: "Driver assigned successfully" });
setAssigningDriver(null);
setSelectedDriver("");
} catch (err: unknown) {
const message =
(err as { response?: { data?: { message?: string } } })?.response?.data?.message ??
"Assignment failed";
toast({ title: "Assignment failed", description: String(message), variant: "destructive" });
}
};
const itemLabel = config.label.toLowerCase();
return (
<Container size="xxl" py="lg" px="lg">
<Breadcrumbs items={[{ label: config.label }]} />
<Stack gap="lg" mt="sm">
<Group justify="space-between" align="flex-end">
<div>
<Title order={2}>{config.label}</Title>
<Text c="dimmed" size="sm">
{config.subtitle}
</Text>
</div>
<Group gap="sm">
{slug === "wagons" ? (
<>
{canUpdate ? (
<Button
variant="light"
color="edr-green"
leftSection={<Warehouse size={16} />}
styles={{ label: { fontWeight: 500 } }}
onClick={() => setWagonWorkspaceOpen(true)}
>
Yard Workspace
</Button>
) : null}
{canTransfer ? (
// The desk is its own page now (list + fulfil + history with
// pagination); this is just the way in from the fleet list.
<Button
component={Link}
to="/dashboard/wagon-transfers"
variant="light"
color="grape"
leftSection={<Inbox size={16} />}
styles={{ label: { fontWeight: 500 } }}
>
Transfer Requests
</Button>
) : null}
</>
) : null}
{canCreate ? (
<Button leftSection={<Plus size={16} />} styles={{ label: { fontWeight: 500 } }} onClick={() => {
setEditing(null);
setFormOpen(true);
}}>
{config.addLabel}
</Button>
) : null}
</Group>
</Group>
<Card radius="lg" padding={0} withBorder style={{ borderColor: "var(--mantine-color-gray-2)" }}>
<Stack gap={0}>
<Box px="md" pt="md" pb="md" w="100%" style={{ borderBottom: "1px solid var(--mantine-color-gray-2)" }}>
<FleetToolbar
search={search}
onSearchChange={setSearch}
searchPlaceholder={config.searchPlaceholder}
showSearch={config.supportsSearch}
viewMode={viewMode}
onViewModeChange={setViewMode}
filters={
<Group gap="sm" wrap="wrap" align="center">
<DatePickerInput
type="range"
aria-label="Created date range"
placeholder="Created date range"
value={[dateFrom, dateTo]}
onChange={([from, to]) => {
setDateFrom(from);
setDateTo(to);
}}
presets={getDateRangePresets()}
clearable
size="sm"
radius="lg"
w={240}
/>
{listFilterSelects ? (
<Group gap="sm" wrap="wrap" align="center">
{listFilterSelects.map((filter) => (
<Select
key={filter.key}
aria-label={filter.label}
placeholder={filter.data[0]?.label ?? filter.label}
data={filter.data}
value={filter.value}
onChange={(value) => {
setListFilterValues((prev) => ({
...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>
{viewMode === "table" ? (
<Box style={{
overflowX: "auto",
width: "100%",
WebkitOverflowScrolling: "touch",
}}>
<DataTable
columns={columns}
data={pagedRows}
status={tableStatus}
error={
isError
? {
message: "Failed to load data",
description: error instanceof Error ? error.message : "Unknown error",
}
: undefined
}
emptyMessage={`No ${itemLabel} found`}
pagination={{
pageIndex: pagination.pageIndex,
pageSize: pagination.pageSize,
pageCount,
totalCount,
}}
tableOptions={{
manualPagination: true,
pageCount,
state: { pagination },
onPaginationChange: setPagination,
}}
containerClassName="border-0 shadow-none bg-transparent"
footer={({ table, pagination: footerPagination }) => (
<DataTableFooter
table={table}
pagination={footerPagination}
options={{ labels: { items: itemLabel } }}
/>
)}
/>
</Box>
) : (
<FleetCardGrid
config={config}
rows={pagedRows}
status={tableStatus}
emptyMessage={`No ${itemLabel} found`}
pagination={pagination}
pageCount={pageCount}
totalCount={totalCount}
onPaginationChange={setPagination}
onEdit={
canUpdate
? (record) => {
setEditing(record);
setFormOpen(true);
}
: undefined
}
onRemove={canDelete ? setRemoveTarget : undefined}
onPurge={canPurge ? setPurgeTarget : undefined}
/>
)}
</Stack>
</Card>
</Stack>
<FleetFormDialog
open={formOpen}
onOpenChange={(open) => {
setFormOpen(open);
if (!open) setEditing(null);
}}
title={editing ? `Edit ${config.entityLabel}` : config.addLabel}
fields={formFields}
initialRecord={editing}
emptyValues={config.emptyValues}
isSubmitting={create.isPending || update.isPending}
selectOptionsLoading={selectOptionsLoading}
onSubmit={handleFormSubmit}
verifyWithFayda={Boolean(config.faydaVerification)}
/>
<Modal
opened={Boolean(removeTarget)}
onClose={() => setRemoveTarget(null)}
title={<Text fw={600}>{config.removeActionLabel ?? "Delete"}</Text>}
radius="lg"
centered
>
<Stack gap="md">
<Text size="sm">
{config.removeConfirmMessage ??
`Are you sure you want to ${config.removeAction} this ${config.entityLabel.toLowerCase()}?`}
</Text>
<Group justify="flex-end">
<Button variant="default" onClick={() => setRemoveTarget(null)}>
Cancel
</Button>
<Button color="red" loading={remove.isPending} onClick={handleRemove}>
{config.removeActionLabel ?? "Delete"}
</Button>
</Group>
</Stack>
</Modal>
<Modal
opened={Boolean(purgeTarget)}
onClose={closePurge}
title={<Text fw={600}>Delete permanently</Text>}
radius="lg"
centered
>
<Stack gap="md">
<Text size="sm">
This permanently removes{" "}
<Text span fw={700}>
{purgeLabel || `this ${config.entityLabel.toLowerCase()}`}
</Text>{" "}
from the database. It cannot be undone.
</Text>
<Text size="sm" c="dimmed">
Only unused records can be purged if it has any history or is still
referenced, the request is refused and you should use{" "}
{(config.removeActionLabel ?? "Delete").toLowerCase()} instead.
</Text>
<TextInput
label={`Type ${purgeLabel} to confirm`}
placeholder={purgeLabel}
value={purgeConfirmText}
onChange={(e) => setPurgeConfirmText(e.currentTarget.value)}
/>
<Group justify="flex-end">
<Button variant="default" onClick={closePurge}>
Cancel
</Button>
<Button
color="red"
loading={purge.isPending}
disabled={purgeConfirmText.trim() !== purgeLabel || !purgeLabel}
onClick={handlePurge}
>
Delete permanently
</Button>
</Group>
</Stack>
</Modal>
<Modal
opened={Boolean(assigningDriver)}
onClose={() => {
setAssigningDriver(null);
setSelectedDriver("");
}}
title={<Text fw={600}>Assign Driver</Text>}
radius="lg"
centered
>
<Stack gap="md">
<Text size="sm">
{assigningDriver && "id" in assigningDriver ?
`Assign a driver to vehicle: ${(assigningDriver as unknown as Record<string, unknown>).plateNumber}`
: "Select a driver to assign"}
</Text>
<Select
label="Driver"
placeholder="Select a driver"
searchable
clearable
value={selectedDriver}
onChange={(value) => setSelectedDriver(value || "")}
data={(drivers as unknown as Array<Record<string, unknown>>).map((driver) => ({
value: String(driver.id || ""),
label: `${driver.firstName} ${driver.lastName} (${driver.licenseNumber})`,
}))}
/>
<Group justify="flex-end">
<Button variant="default" onClick={() => {
setAssigningDriver(null);
setSelectedDriver("");
}}>
Cancel
</Button>
<Button
onClick={handleAssignDriver}
disabled={!selectedDriver}
loading={update.isPending}
>
Assign
</Button>
</Group>
</Stack>
</Modal>
{slug === "wagons" ? (
<WagonYardWorkspaceModal
opened={wagonWorkspaceOpen}
onClose={() => setWagonWorkspaceOpen(false)}
/>
) : null}
{slug === "wagons" ? (
<WagonMovementHistoryModal
opened={Boolean(historyTarget)}
onClose={() => setHistoryTarget(null)}
record={historyTarget}
/>
) : (
<FleetHistoryModal
opened={Boolean(historyTarget)}
onClose={() => setHistoryTarget(null)}
entity={slug === "vehicles" ? "vehicle" : "driver"}
record={historyTarget}
/>
)}
</Container>
);
};
export default FleetResourcePage;