mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-09 02:28:18 +00:00
Merge branch 'dev' of github.com:Tria-plc/edr-platform into freight_feature/usermanagement
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { Edit, Eye, Plus, Search, Trash2 } from 'lucide-react';
|
||||
import { Edit, Eye, Plus, Trash2 } from 'lucide-react';
|
||||
import { FormEvent, ReactNode, useMemo, useState } from 'react';
|
||||
|
||||
import { api } from '@/services/api';
|
||||
@@ -44,6 +44,13 @@ import type { Train } from '@/services/trains.service';
|
||||
import type { WagonType } from '@/services/wagon-types.service';
|
||||
import type { Wagon } from '@/services/wagon.service';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@edr/ui-common';
|
||||
import {
|
||||
applyClientFilters,
|
||||
FilterBar,
|
||||
useFilters,
|
||||
type FilterDef,
|
||||
type FilterOption,
|
||||
} from '@/components/filters';
|
||||
|
||||
type FormValue = string | number | boolean | string[];
|
||||
|
||||
@@ -86,6 +93,8 @@ type FleetCrudPageProps<T extends { id: string }> = {
|
||||
hideViewAction?: boolean;
|
||||
/** Optional custom actions rendered before the view/edit/delete buttons in each row. */
|
||||
rowActions?: (item: T) => React.ReactNode;
|
||||
/** Enables the Status filter pill; the item's `status` field is matched against these. */
|
||||
statusOptions?: FilterOption[];
|
||||
};
|
||||
|
||||
const normalizePayload = (values: Record<string, FormValue>) =>
|
||||
@@ -172,9 +181,16 @@ function FleetCrudPage<T extends { id: string }>({
|
||||
removeSuccessMessage,
|
||||
hideViewAction = false,
|
||||
rowActions,
|
||||
statusOptions,
|
||||
}: FleetCrudPageProps<T>) {
|
||||
const [search, setSearch] = useState('');
|
||||
const [page, setPage] = useState(1);
|
||||
const filterDefs: FilterDef[] = useMemo(
|
||||
() =>
|
||||
statusOptions
|
||||
? [{ key: 'status', label: 'Status', type: 'enum', multiple: false, options: statusOptions }]
|
||||
: [],
|
||||
[statusOptions],
|
||||
);
|
||||
const controls = useFilters(filterDefs, { pageSize: 10 });
|
||||
const [sortKey, setSortKey] = useState<string>('');
|
||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
|
||||
const [formOpen, setFormOpen] = useState(false);
|
||||
@@ -184,11 +200,13 @@ function FleetCrudPage<T extends { id: string }>({
|
||||
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
|
||||
const { toast } = useToast();
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const query = search.trim().toLowerCase();
|
||||
if (!query) return data ?? [];
|
||||
return (data ?? []).filter((item) => searchText(item).toLowerCase().includes(query));
|
||||
}, [data, search, searchText]);
|
||||
const filtered = useMemo(
|
||||
() =>
|
||||
applyClientFilters(data ?? [], filterDefs, controls.values, controls.searchText, {
|
||||
searchValue: searchText,
|
||||
}),
|
||||
[data, filterDefs, controls.values, controls.searchText, searchText],
|
||||
);
|
||||
const sorted = useMemo(() => {
|
||||
if (!sortKey) return filtered;
|
||||
return [...filtered].sort((a, b) => {
|
||||
@@ -198,12 +216,13 @@ function FleetCrudPage<T extends { id: string }>({
|
||||
return sortDirection === 'asc' ? result : -result;
|
||||
});
|
||||
}, [filtered, sortDirection, sortKey]);
|
||||
const pageSize = 10;
|
||||
const pageSize = controls.pageSize;
|
||||
const page = controls.page;
|
||||
const pageCount = Math.max(1, Math.ceil(sorted.length / pageSize));
|
||||
const paged = sorted.slice((page - 1) * pageSize, page * pageSize);
|
||||
|
||||
const toggleSort = (key: string) => {
|
||||
setPage(1);
|
||||
controls.setPage(1);
|
||||
if (sortKey === key) {
|
||||
setSortDirection((current) => (current === 'asc' ? 'desc' : 'asc'));
|
||||
return;
|
||||
@@ -298,18 +317,11 @@ function FleetCrudPage<T extends { id: string }>({
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex max-w-md items-center gap-2 rounded-md border bg-background px-3">
|
||||
<Search className="size-4 text-muted-foreground" />
|
||||
<Input
|
||||
className="border-0 px-0 shadow-none focus-visible:ring-0"
|
||||
placeholder={`Search ${title.toLowerCase()}`}
|
||||
value={search}
|
||||
onChange={(event) => {
|
||||
setSearch(event.target.value);
|
||||
setPage(1);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<FilterBar
|
||||
defs={filterDefs}
|
||||
controls={controls}
|
||||
searchPlaceholder={`Search ${title.toLowerCase()}`}
|
||||
/>
|
||||
|
||||
<div className="overflow-hidden rounded-lg border bg-card">
|
||||
<Table>
|
||||
@@ -379,10 +391,10 @@ function FleetCrudPage<T extends { id: string }>({
|
||||
Showing {sorted.length === 0 ? 0 : (page - 1) * pageSize + 1}-{Math.min(page * pageSize, sorted.length)} of {sorted.length}
|
||||
</span>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm" disabled={page === 1} onClick={() => setPage((current) => current - 1)}>
|
||||
<Button variant="outline" size="sm" disabled={page === 1} onClick={() => controls.setPage(page - 1)}>
|
||||
Previous
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" disabled={page === pageCount} onClick={() => setPage((current) => current + 1)}>
|
||||
<Button variant="outline" size="sm" disabled={page === pageCount} onClick={() => controls.setPage(page + 1)}>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
@@ -487,6 +499,47 @@ const statusBadge = (status?: string) => <Badge variant="outline">{status ?? '-'
|
||||
const optionLabel = (options: { value: string; label: string }[], value?: string | null) =>
|
||||
options.find((option) => option.value === value)?.label ?? value ?? '-';
|
||||
|
||||
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 WAGON_STATUS_OPTIONS: FilterOption[] = [
|
||||
{ value: 'AVAILABLE', label: 'Available' },
|
||||
{ value: 'IMPORT_READY', label: 'Import ready' },
|
||||
{ value: 'EXPORT_READY', label: 'Export ready' },
|
||||
{ value: 'ASSIGNED', label: 'Assigned' },
|
||||
{ value: 'MAINTENANCE', label: 'Maintenance' },
|
||||
{ value: 'DETAINED', label: 'Detained' },
|
||||
];
|
||||
|
||||
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 LOCOMOTIVE_STATUS_OPTIONS: FilterOption[] = [
|
||||
{ value: 'AVAILABLE', label: 'Available' },
|
||||
{ value: 'MAINTENANCE', label: 'Maintenance' },
|
||||
{ value: 'ASSIGNED', label: 'Assigned' },
|
||||
{ value: 'OUT_OF_SERVICE', label: 'Out of service' },
|
||||
];
|
||||
|
||||
export function TrainMasterDataPage() {
|
||||
const query = useQuery(api.trains.list.queryOptions());
|
||||
return (
|
||||
@@ -499,6 +552,7 @@ export function TrainMasterDataPage() {
|
||||
create={useMutation(api.trains.create.mutationOptions())}
|
||||
update={useMutation(api.trains.update.mutationOptions())}
|
||||
remove={useMutation(api.trains.remove.mutationOptions())}
|
||||
statusOptions={TRAIN_STATUS_OPTIONS}
|
||||
searchText={(train) => [train.code, train.trainNumber, train.trainName, train.status].join(' ')}
|
||||
columns={[
|
||||
{ key: 'code', label: 'Code' },
|
||||
@@ -522,14 +576,17 @@ export function TrainMasterDataPage() {
|
||||
);
|
||||
}
|
||||
|
||||
const WAGON_TYPE_FILTER_DEFS: FilterDef[] = [
|
||||
{ key: 'isActive', label: 'Status', type: 'boolean', trueLabel: 'Active', falseLabel: 'Inactive' },
|
||||
];
|
||||
|
||||
export function WagonTypesCrudPage() {
|
||||
const query = useQuery(api.wagonTypes.list.queryOptions());
|
||||
const create = useMutation(api.wagonTypes.create.mutationOptions());
|
||||
const update = useMutation(api.wagonTypes.update.mutationOptions());
|
||||
const remove = useMutation(api.wagonTypes.remove.mutationOptions());
|
||||
const { toast } = useToast();
|
||||
const [search, setSearch] = useState('');
|
||||
const [page, setPage] = useState(1);
|
||||
const controls = useFilters(WAGON_TYPE_FILTER_DEFS, { pageSize: 10 });
|
||||
const [sortKey, setSortKey] = useState<keyof WagonType>('code');
|
||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
|
||||
const [formOpen, setFormOpen] = useState(false);
|
||||
@@ -546,18 +603,15 @@ export function WagonTypesCrudPage() {
|
||||
});
|
||||
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
|
||||
|
||||
const pageSize = 10;
|
||||
const filtered = useMemo(() => {
|
||||
const queryText = search.trim().toLowerCase();
|
||||
const rows = query.data ?? [];
|
||||
if (!queryText) return rows;
|
||||
return rows.filter((type) =>
|
||||
[type.code, type.name, type.supportedLoadTypes?.join(' '), type.isActive ? 'active' : 'inactive']
|
||||
.join(' ')
|
||||
.toLowerCase()
|
||||
.includes(queryText),
|
||||
);
|
||||
}, [query.data, search]);
|
||||
const pageSize = controls.pageSize;
|
||||
const page = controls.page;
|
||||
const filtered = useMemo(
|
||||
() =>
|
||||
applyClientFilters(query.data ?? [], WAGON_TYPE_FILTER_DEFS, controls.values, controls.searchText, {
|
||||
searchValue: (type) => [type.code, type.name, type.supportedLoadTypes?.join(' ')].join(' '),
|
||||
}),
|
||||
[query.data, controls.values, controls.searchText],
|
||||
);
|
||||
|
||||
const sorted = useMemo(() => {
|
||||
return [...filtered].sort((left, right) => {
|
||||
@@ -573,7 +627,7 @@ export function WagonTypesCrudPage() {
|
||||
const isSaving = create.isPending || update.isPending;
|
||||
|
||||
const toggleSort = (key: keyof WagonType) => {
|
||||
setPage(1);
|
||||
controls.setPage(1);
|
||||
if (sortKey === key) {
|
||||
setSortDirection((current) => (current === 'asc' ? 'desc' : 'asc'));
|
||||
return;
|
||||
@@ -689,16 +743,7 @@ export function WagonTypesCrudPage() {
|
||||
</MantineButton>
|
||||
</Group>
|
||||
|
||||
<TextInput
|
||||
maw={420}
|
||||
leftSection={<Search size={16} />}
|
||||
placeholder="Search wagon types"
|
||||
value={search}
|
||||
onChange={(event) => {
|
||||
setSearch(event.currentTarget.value);
|
||||
setPage(1);
|
||||
}}
|
||||
/>
|
||||
<FilterBar defs={WAGON_TYPE_FILTER_DEFS} controls={controls} searchPlaceholder="Search wagon types" />
|
||||
|
||||
<Paper withBorder radius="md">
|
||||
<ScrollArea>
|
||||
@@ -789,7 +834,7 @@ export function WagonTypesCrudPage() {
|
||||
Showing {sorted.length === 0 ? 0 : (page - 1) * pageSize + 1}-{Math.min(page * pageSize, sorted.length)} of{' '}
|
||||
{sorted.length}
|
||||
</Text>
|
||||
<Pagination total={pageCount} value={page} onChange={setPage} size="sm" />
|
||||
<Pagination total={pageCount} value={page} onChange={controls.setPage} size="sm" />
|
||||
</Group>
|
||||
</Stack>
|
||||
|
||||
@@ -906,6 +951,7 @@ export function WagonsCrudPage() {
|
||||
create={useMutation(api.wagons.create.mutationOptions())}
|
||||
update={useMutation(api.wagons.update.mutationOptions())}
|
||||
remove={useMutation(api.wagons.remove.mutationOptions())}
|
||||
statusOptions={WAGON_STATUS_OPTIONS}
|
||||
searchText={(wagon) => [
|
||||
wagon.wagonNumber,
|
||||
wagon.wagonTypeId,
|
||||
@@ -959,14 +1005,7 @@ export function WagonsCrudPage() {
|
||||
key: 'status',
|
||||
label: 'Status',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ value: 'AVAILABLE', label: 'Available' },
|
||||
{ value: 'IMPORT_READY', label: 'Import ready' },
|
||||
{ value: 'EXPORT_READY', label: 'Export ready' },
|
||||
{ value: 'ASSIGNED', label: 'Assigned' },
|
||||
{ value: 'MAINTENANCE', label: 'Maintenance' },
|
||||
{ value: 'DETAINED', label: 'Detained' },
|
||||
],
|
||||
options: WAGON_STATUS_OPTIONS,
|
||||
},
|
||||
{ key: 'notes', label: 'Notes' },
|
||||
]}
|
||||
@@ -999,6 +1038,7 @@ export function ContainersCrudPage() {
|
||||
create={useMutation(api.containers.create.mutationOptions())}
|
||||
update={useMutation(api.containers.update.mutationOptions())}
|
||||
remove={useMutation(api.containers.remove.mutationOptions())}
|
||||
statusOptions={CONTAINER_STATUS_OPTIONS}
|
||||
searchText={(container) => [container.containerNumber, container.containerTypeId, container.wagonId, container.status].join(' ')}
|
||||
columns={[
|
||||
{ key: 'containerNumber', label: 'Number' },
|
||||
@@ -1058,6 +1098,7 @@ export function CargoesCrudPage() {
|
||||
create={useMutation(api.cargoes.create.mutationOptions())}
|
||||
update={useMutation(api.cargoes.update.mutationOptions())}
|
||||
remove={useMutation(api.cargoes.remove.mutationOptions())}
|
||||
statusOptions={CARGO_STATUS_OPTIONS}
|
||||
searchText={(cargo) => [cargo.cargoReference, cargo.description, cargo.containerId, cargo.status].join(' ')}
|
||||
columns={[
|
||||
{ key: 'cargoReference', label: 'Reference' },
|
||||
@@ -1122,6 +1163,7 @@ export function LocomotivesCrudPage() {
|
||||
removeActionLabel="Decommission"
|
||||
removeConfirmMessage="Decommission this locomotive?"
|
||||
removeSuccessMessage="Locomotive decommissioned"
|
||||
statusOptions={LOCOMOTIVE_STATUS_OPTIONS}
|
||||
searchText={(locomotive) =>
|
||||
[
|
||||
locomotive.code,
|
||||
@@ -1166,12 +1208,7 @@ export function LocomotivesCrudPage() {
|
||||
label: 'Status',
|
||||
type: 'select',
|
||||
required: true,
|
||||
options: [
|
||||
{ value: 'AVAILABLE', label: 'Available' },
|
||||
{ value: 'MAINTENANCE', label: 'Maintenance' },
|
||||
{ value: 'ASSIGNED', label: 'Assigned' },
|
||||
{ value: 'OUT_OF_SERVICE', label: 'Out of service' },
|
||||
],
|
||||
options: LOCOMOTIVE_STATUS_OPTIONS,
|
||||
},
|
||||
{ key: 'maxPullWeightTons', label: 'Max pulling weight (tons)', type: 'number', required: true },
|
||||
{ key: 'maxTrainLengthMeters', label: 'Max train length (meters)', type: 'number', required: true },
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user