mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 16:35:42 +00:00
update locaomotive and fix wagon transfer issue
This commit is contained in:
@@ -41,7 +41,12 @@ export default function AvailableWagonsPanel({
|
||||
const wagonsQuery = useQuery(
|
||||
api.wagons.list.queryOptions({
|
||||
input: {
|
||||
filters: { status: Freight.WagonStatus.Available, currentYardId: yardId },
|
||||
filters: {
|
||||
status: Freight.WagonStatus.Available,
|
||||
currentYardId: yardId,
|
||||
// Loose wagons only — one already on another train cannot be coupled.
|
||||
unassigned: true,
|
||||
},
|
||||
},
|
||||
enabled: Boolean(yardId),
|
||||
}),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ColumnDef } from "@edr/ui-common";
|
||||
import { Box, Button, Card, Container, Group, Modal, Select, Stack, Text, Title } from "@mantine/core";
|
||||
import { DatePickerInput } from "@mantine/dates";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { keepPreviousData, useMutation, useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { api } from "@/services/api";
|
||||
import { useAuth } from "@/auth/useAuth";
|
||||
@@ -30,8 +30,13 @@ import {
|
||||
type FleetFormFieldDef,
|
||||
type FleetResourceSlug,
|
||||
} from "@/pages/fleet/config/resources";
|
||||
import type { FleetListFilters, FleetRecord } from "@/services/fleet/fleet.service";
|
||||
import {
|
||||
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";
|
||||
|
||||
@@ -53,6 +58,10 @@ const FleetResourcePage = () => {
|
||||
|
||||
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.
|
||||
@@ -93,14 +102,42 @@ const FleetResourcePage = () => {
|
||||
if (wagonTypeId && wagonTypeId !== "ALL") {
|
||||
(filters as { wagonTypeId?: string }).wagonTypeId = wagonTypeId;
|
||||
}
|
||||
if (slug !== "locomotives" && search.trim()) {
|
||||
filters.search = search.trim();
|
||||
// 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, search]);
|
||||
}, [slug, listFilterValues, debouncedSearch, serverPaged]);
|
||||
|
||||
const { data: allRows = [], isLoading, isError, error } = useQuery(
|
||||
api.fleet.list.queryOptions({ input: { slug, filters: serverListFilters } }),
|
||||
// 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());
|
||||
@@ -270,6 +307,9 @@ const FleetResourcePage = () => {
|
||||
|
||||
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>;
|
||||
@@ -287,13 +327,19 @@ const FleetResourcePage = () => {
|
||||
.includes(term),
|
||||
);
|
||||
});
|
||||
}, [allRows, search, statusFilter, config, usesServerListFilters, dateFrom, dateTo]);
|
||||
}, [allRows, search, statusFilter, config, usesServerListFilters, dateFrom, dateTo, serverPaged]);
|
||||
|
||||
const pageCount = Math.max(1, Math.ceil(filteredRows.length / pagination.pageSize));
|
||||
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]);
|
||||
}, [filteredRows, pagination.pageIndex, pagination.pageSize, serverPaged]);
|
||||
|
||||
const columns = useMemo((): ColumnDef<FleetRecord>[] => {
|
||||
if (!config) return [];
|
||||
@@ -584,7 +630,7 @@ const FleetResourcePage = () => {
|
||||
pageIndex: pagination.pageIndex,
|
||||
pageSize: pagination.pageSize,
|
||||
pageCount,
|
||||
totalCount: filteredRows.length,
|
||||
totalCount,
|
||||
}}
|
||||
tableOptions={{
|
||||
manualPagination: true,
|
||||
@@ -610,7 +656,7 @@ const FleetResourcePage = () => {
|
||||
emptyMessage={`No ${itemLabel} found`}
|
||||
pagination={pagination}
|
||||
pageCount={pageCount}
|
||||
totalCount={filteredRows.length}
|
||||
totalCount={totalCount}
|
||||
onPaginationChange={setPagination}
|
||||
onEdit={
|
||||
canUpdate
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FormEvent, useMemo, useState } from "react";
|
||||
import { FormEvent, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
ArrowRight,
|
||||
Ban,
|
||||
@@ -27,7 +27,8 @@ import {
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { useDebouncedValue } from "@mantine/hooks";
|
||||
import { keepPreviousData, useMutation, useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { KpiStrip, PageContainer, PageHeader } from "@/components/page";
|
||||
import FleetToolbar from "@/components/fleet/FleetToolbar";
|
||||
@@ -155,6 +156,7 @@ function RouteTimeline({ route }: { route: RouteRecord }) {
|
||||
|
||||
export default function RoutesPage() {
|
||||
const [search, setSearch] = useState("");
|
||||
const [debouncedSearch] = useDebouncedValue(search, 300);
|
||||
const [formOpen, setFormOpen] = useState(false);
|
||||
const [viewing, setViewing] = useState<RouteRecord | null>(null);
|
||||
const [editing, setEditing] = useState<RouteRecord | null>(null);
|
||||
@@ -167,7 +169,26 @@ export default function RoutesPage() {
|
||||
const canUpdate = canFleetAction(user, "routes", "update");
|
||||
const canDelete = canFleetAction(user, "routes", "delete");
|
||||
|
||||
const routesQuery = useQuery(api.routes.list.queryOptions());
|
||||
const routesQuery = useQuery({
|
||||
...api.routes.listPaged.queryOptions({
|
||||
input: {
|
||||
page: pagination.pageIndex + 1,
|
||||
pageSize: pagination.pageSize,
|
||||
...(debouncedSearch.trim() ? { search: debouncedSearch.trim() } : {}),
|
||||
},
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
// KPI counts stay whole-fleet (they must not move with the search box), so
|
||||
// they come from two count-only pages rather than the visible one.
|
||||
const totalCountQuery = useQuery(
|
||||
api.routes.listPaged.queryOptions({ input: { page: 1, pageSize: 1 } }),
|
||||
);
|
||||
const availableCountQuery = useQuery(
|
||||
api.routes.listPaged.queryOptions({
|
||||
input: { page: 1, pageSize: 1, status: "AVAILABLE" },
|
||||
}),
|
||||
);
|
||||
const yardsQuery = useQuery(api.routes.yards.queryOptions());
|
||||
// Segment km are configured in Configuration → Yard Distances and resolved
|
||||
// by the API on save; this fetch is only to preview them in the form.
|
||||
@@ -182,35 +203,19 @@ export default function RoutesPage() {
|
||||
const updateMutation = useMutation(api.routes.update.mutationOptions());
|
||||
const deactivateMutation = useMutation(api.routes.deactivate.mutationOptions());
|
||||
|
||||
const filteredRoutes = useMemo(() => {
|
||||
const query = search.trim().toLowerCase();
|
||||
if (!query) return routesQuery.data ?? [];
|
||||
return (routesQuery.data ?? []).filter((route) => {
|
||||
const searchable = [
|
||||
formatRouteLabel(route),
|
||||
route.originYard?.label,
|
||||
route.originYard?.code,
|
||||
route.destinationYard?.label,
|
||||
route.destinationYard?.code,
|
||||
...(route.milestones ?? []).map(
|
||||
(m) => m.yard?.label ?? m.yard?.code ?? m.yardId,
|
||||
),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")
|
||||
.toLowerCase();
|
||||
return searchable.includes(query);
|
||||
});
|
||||
}, [routesQuery.data, search]);
|
||||
// Narrowing the result set can strand the user on a page that no longer
|
||||
// exists (search down to 3 rows while on page 5 → empty table).
|
||||
useEffect(() => {
|
||||
setPagination((prev) => (prev.pageIndex === 0 ? prev : { ...prev, pageIndex: 0 }));
|
||||
}, [debouncedSearch, setPagination]);
|
||||
|
||||
const pageCount = Math.max(1, Math.ceil(filteredRoutes.length / pagination.pageSize));
|
||||
const pagedRoutes = useMemo(() => {
|
||||
const start = pagination.pageIndex * pagination.pageSize;
|
||||
return filteredRoutes.slice(start, start + pagination.pageSize);
|
||||
}, [filteredRoutes, pagination.pageIndex, pagination.pageSize]);
|
||||
// Filtering, sorting and the page window all happen server-side.
|
||||
const pagedRoutes = routesQuery.data?.items ?? [];
|
||||
const matchCount = routesQuery.data?.meta.total ?? 0;
|
||||
const pageCount = Math.max(1, routesQuery.data?.meta.totalPages ?? 1);
|
||||
|
||||
const allRoutes = routesQuery.data ?? [];
|
||||
const availableCount = allRoutes.filter((r) => r.status === "AVAILABLE").length;
|
||||
const totalRoutes = totalCountQuery.data?.meta.total ?? 0;
|
||||
const availableCount = availableCountQuery.data?.meta.total ?? 0;
|
||||
|
||||
const yardOptions = useMemo(
|
||||
() =>
|
||||
@@ -489,11 +494,11 @@ export default function RoutesPage() {
|
||||
<KpiStrip
|
||||
loading={routesQuery.isLoading}
|
||||
items={[
|
||||
{ label: "Total routes", value: allRoutes.length, icon: RouteIcon },
|
||||
{ label: "Total routes", value: totalRoutes, icon: RouteIcon },
|
||||
{ label: "Available", value: availableCount, icon: CircleCheck, color: "edr-green" },
|
||||
{
|
||||
label: "Unavailable",
|
||||
value: allRoutes.length - availableCount,
|
||||
value: totalRoutes - availableCount,
|
||||
icon: Ban,
|
||||
color: "gray",
|
||||
},
|
||||
@@ -522,7 +527,7 @@ export default function RoutesPage() {
|
||||
pageIndex: pagination.pageIndex,
|
||||
pageSize: pagination.pageSize,
|
||||
pageCount,
|
||||
totalCount: filteredRoutes.length,
|
||||
totalCount: matchCount,
|
||||
}}
|
||||
tableOptions={{
|
||||
manualPagination: true,
|
||||
@@ -581,7 +586,7 @@ export default function RoutesPage() {
|
||||
<RuleEngineListFooter
|
||||
pagination={pagination}
|
||||
pageCount={pageCount}
|
||||
totalCount={filteredRoutes.length}
|
||||
totalCount={matchCount}
|
||||
itemLabel="routes"
|
||||
onPaginationChange={setPagination}
|
||||
/>
|
||||
|
||||
@@ -48,6 +48,8 @@ export function TransferFulfillModal({
|
||||
currentYardId: request.fromYardId,
|
||||
wagonTypeId: request.wagonTypeId,
|
||||
status: Freight.WagonStatus.Available,
|
||||
// A coupled wagon cannot be moved out of its train by a transfer.
|
||||
unassigned: true,
|
||||
}
|
||||
: {},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,342 @@
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
Group,
|
||||
SegmentedControl,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Switch,
|
||||
Text,
|
||||
ThemeIcon,
|
||||
Timeline,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
ArrowRight,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
History,
|
||||
Inbox,
|
||||
Truck,
|
||||
} from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { useAuth } from "@/auth/useAuth";
|
||||
import { FREIGHT_PERMS, hasPermission } from "@/lib/permissions";
|
||||
import { api } from "@/services/api";
|
||||
import type {
|
||||
WagonMovementRecord,
|
||||
WagonTransferRequest,
|
||||
} from "@/services/wagon.service";
|
||||
|
||||
import {
|
||||
STATUS_META,
|
||||
TransferProgress,
|
||||
TransferStatusBadge,
|
||||
wagonTypeLabel,
|
||||
yardLabel,
|
||||
} from "./wagon-transfer-ui";
|
||||
|
||||
const fmtTime = (iso?: string | null) =>
|
||||
iso
|
||||
? new Date(iso).toLocaleTimeString("en-GB", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
})
|
||||
: "—";
|
||||
|
||||
/** "Today" / "Yesterday" / "Mon 12 Jul 2026" — the header of one timeline block. */
|
||||
const dayLabel = (iso: string) => {
|
||||
const d = new Date(iso);
|
||||
const days = Math.round(
|
||||
(new Date().setHours(0, 0, 0, 0) - new Date(iso).setHours(0, 0, 0, 0)) /
|
||||
86_400_000,
|
||||
);
|
||||
if (days === 0) return "Today";
|
||||
if (days === 1) return "Yesterday";
|
||||
return d.toLocaleDateString("en-GB", {
|
||||
weekday: "short",
|
||||
day: "numeric",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
});
|
||||
};
|
||||
|
||||
/** Bucket an already-DESC-sorted list into day blocks, order preserved. */
|
||||
function groupByDay<T>(items: T[], at: (item: T) => string) {
|
||||
const groups: Array<{ key: string; label: string; items: T[] }> = [];
|
||||
for (const item of items) {
|
||||
const iso = at(item);
|
||||
const key = new Date(iso).toDateString();
|
||||
const last = groups[groups.length - 1];
|
||||
if (last?.key === key) last.items.push(item);
|
||||
else groups.push({ key, label: dayLabel(iso), items: [item] });
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
const MOVEMENT_KIND_LABEL: Record<string, string> = {
|
||||
LOADED: "Carried cargo",
|
||||
EMPTY_REPOSITION: "Repositioned empty",
|
||||
MANUAL: "Manual move",
|
||||
};
|
||||
|
||||
function EmptyState({ label }: { label: string }) {
|
||||
return (
|
||||
<Stack align="center" gap={6} py="xl">
|
||||
<ThemeIcon variant="light" color="gray" radius="xl" size={44}>
|
||||
<History size={20} />
|
||||
</ThemeIcon>
|
||||
<Text size="sm" c="dimmed">
|
||||
{label}
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
function RequestItem({ request }: { request: WagonTransferRequest }) {
|
||||
const meta = STATUS_META[request.status];
|
||||
return (
|
||||
<Timeline.Item
|
||||
bullet={<Inbox size={12} />}
|
||||
color={meta?.color ?? "gray"}
|
||||
lineVariant="dotted"
|
||||
>
|
||||
<Group justify="space-between" align="flex-start" gap="md" wrap="wrap">
|
||||
<Stack gap={4} style={{ flex: 1, minWidth: 220 }}>
|
||||
<Group gap={6} wrap="nowrap">
|
||||
<Text size="sm" fw={600}>
|
||||
{yardLabel(request.fromYard)}
|
||||
</Text>
|
||||
<ArrowRight size={13} className="shrink-0 opacity-60" />
|
||||
<Text size="sm" fw={600}>
|
||||
{yardLabel(request.toYard)}
|
||||
</Text>
|
||||
<Badge variant="default" radius="sm" size="sm">
|
||||
{wagonTypeLabel(request.wagonType)}
|
||||
</Badge>
|
||||
</Group>
|
||||
{request.reason ? (
|
||||
<Text size="xs" c="dimmed" lineClamp={1}>
|
||||
{request.reason}
|
||||
</Text>
|
||||
) : null}
|
||||
</Stack>
|
||||
<Group gap="sm" wrap="nowrap">
|
||||
<TransferProgress request={request} />
|
||||
<TransferStatusBadge status={request.status} />
|
||||
<Text size="xs" c="dimmed" w={38} ta="right">
|
||||
{fmtTime(request.createdAt)}
|
||||
</Text>
|
||||
</Group>
|
||||
</Group>
|
||||
</Timeline.Item>
|
||||
);
|
||||
}
|
||||
|
||||
function MovementItem({ movement }: { movement: WagonMovementRecord }) {
|
||||
return (
|
||||
<Timeline.Item
|
||||
bullet={<Truck size={12} />}
|
||||
color={movement.transferRequestId ? "edr-green" : "gray"}
|
||||
lineVariant="dotted"
|
||||
>
|
||||
<Group justify="space-between" align="center" gap="md" wrap="wrap">
|
||||
<Group gap={8} wrap="nowrap" style={{ flex: 1, minWidth: 220 }}>
|
||||
<Badge variant="light" color="gray" radius="sm" ff="monospace">
|
||||
{movement.wagon?.wagonNumber ?? "Wagon"}
|
||||
</Badge>
|
||||
<Text size="sm" fw={500}>
|
||||
{yardLabel(movement.fromYard)}
|
||||
</Text>
|
||||
<ArrowRight size={13} className="shrink-0 opacity-60" />
|
||||
<Text size="sm" fw={500}>
|
||||
{yardLabel(movement.toYard)}
|
||||
</Text>
|
||||
</Group>
|
||||
<Group gap="sm" wrap="nowrap">
|
||||
{movement.transferRequestId ? (
|
||||
<Tooltip label="Delivered against a transfer request" withArrow>
|
||||
<Badge variant="dot" color="teal" radius="sm" size="sm">
|
||||
Transfer
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Badge variant="light" color="gray" radius="sm" size="sm">
|
||||
{MOVEMENT_KIND_LABEL[movement.kind] ?? movement.kind}
|
||||
</Badge>
|
||||
)}
|
||||
<Text size="xs" c="dimmed" w={38} ta="right">
|
||||
{fmtTime(movement.occurredAt)}
|
||||
</Text>
|
||||
</Group>
|
||||
</Group>
|
||||
</Timeline.Item>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Who moved what. A staffer sees their own activity; holders of
|
||||
* `transfer_history_all` can widen it to every staffer (the backend enforces
|
||||
* the scope regardless of the toggle).
|
||||
*/
|
||||
export default function TransferHistoryPanel() {
|
||||
const { user } = useAuth();
|
||||
const canSeeAll = hasPermission(user, FREIGHT_PERMS.wagons.transferHistoryAll);
|
||||
const [allStaff, setAllStaff] = useState(false);
|
||||
const [view, setView] = useState<"requests" | "movements">("requests");
|
||||
const [page, setPage] = useState(1);
|
||||
const scopeAll = canSeeAll && allStaff;
|
||||
|
||||
const mine = useQuery({
|
||||
...api.wagonTransferRequests.history.queryOptions({
|
||||
input: { page, pageSize: 20 },
|
||||
}),
|
||||
enabled: !scopeAll,
|
||||
});
|
||||
const all = useQuery({
|
||||
...api.wagonTransferRequests.historyAll.queryOptions({
|
||||
input: { page, pageSize: 20 },
|
||||
}),
|
||||
enabled: scopeAll,
|
||||
});
|
||||
const source = scopeAll ? all : mine;
|
||||
const requests = source.data?.requests ?? [];
|
||||
const movements = source.data?.movements ?? [];
|
||||
const meta = source.data?.meta;
|
||||
|
||||
const showingRequests = view === "requests";
|
||||
const total = showingRequests
|
||||
? (meta?.requestsTotal ?? 0)
|
||||
: (meta?.movementsTotal ?? 0);
|
||||
// Each list pages independently on the server; the pager follows the one on screen.
|
||||
const pageSize = meta?.pageSize ?? 20;
|
||||
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
||||
const groups = showingRequests
|
||||
? groupByDay(requests, (r) => r.createdAt)
|
||||
: groupByDay(movements, (m) => m.occurredAt);
|
||||
|
||||
return (
|
||||
<Card withBorder radius="md" p="md">
|
||||
<Stack gap="md">
|
||||
<Group justify="space-between" align="flex-start" gap="md" wrap="wrap">
|
||||
<Stack gap={2}>
|
||||
<Text fw={600}>Transfer history</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{scopeAll
|
||||
? "Every staffer's requests and wagon moves"
|
||||
: "Requests you filed or fulfilled, and the wagons you moved"}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Group gap="sm" wrap="wrap">
|
||||
<SegmentedControl
|
||||
size="xs"
|
||||
radius="md"
|
||||
value={view}
|
||||
onChange={(v) => {
|
||||
setView(v as "requests" | "movements");
|
||||
setPage(1);
|
||||
}}
|
||||
data={[
|
||||
{
|
||||
value: "requests",
|
||||
label: `Requests ${meta?.requestsTotal ?? 0}`,
|
||||
},
|
||||
{
|
||||
value: "movements",
|
||||
label: `Wagons moved ${meta?.movementsTotal ?? 0}`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{canSeeAll ? (
|
||||
<Switch
|
||||
label="All staff"
|
||||
checked={allStaff}
|
||||
onChange={(e) => {
|
||||
setAllStaff(e.currentTarget.checked);
|
||||
setPage(1);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
{source.isLoading ? (
|
||||
<Stack gap="sm">
|
||||
{[0, 1, 2, 3].map((i) => (
|
||||
<Skeleton key={i} height={44} radius="md" />
|
||||
))}
|
||||
</Stack>
|
||||
) : groups.length === 0 ? (
|
||||
<EmptyState
|
||||
label={
|
||||
showingRequests
|
||||
? "No transfer requests recorded yet."
|
||||
: "No wagon moves recorded yet."
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<Stack gap="lg">
|
||||
{groups.map((group) => (
|
||||
<Stack key={group.key} gap="xs">
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<Text size="xs" fw={700} c="dimmed" tt="uppercase">
|
||||
{group.label}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
· {group.items.length}
|
||||
</Text>
|
||||
</Group>
|
||||
<Timeline
|
||||
bulletSize={22}
|
||||
lineWidth={2}
|
||||
active={group.items.length}
|
||||
>
|
||||
{showingRequests
|
||||
? (group.items as WagonTransferRequest[]).map((r) => (
|
||||
<RequestItem key={r.id} request={r} />
|
||||
))
|
||||
: (group.items as WagonMovementRecord[]).map((m) => (
|
||||
<MovementItem key={m.id} movement={m} />
|
||||
))}
|
||||
</Timeline>
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<Group justify="space-between" gap="sm" wrap="wrap">
|
||||
<Text size="xs" c="dimmed">
|
||||
{total} {showingRequests ? "request(s)" : "move(s)"} · page{" "}
|
||||
{meta?.page ?? page} of {totalPages}
|
||||
</Text>
|
||||
<Group gap="xs">
|
||||
<Button
|
||||
variant="default"
|
||||
size="xs"
|
||||
radius="md"
|
||||
leftSection={<ChevronLeft size={14} />}
|
||||
disabled={page <= 1}
|
||||
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
size="xs"
|
||||
radius="md"
|
||||
rightSection={<ChevronRight size={14} />}
|
||||
disabled={page >= totalPages}
|
||||
onClick={() => setPage((p) => p + 1)}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -4,10 +4,9 @@ import {
|
||||
Button,
|
||||
Card,
|
||||
Group,
|
||||
Loader,
|
||||
Modal,
|
||||
Select,
|
||||
Stack,
|
||||
Switch,
|
||||
Tabs,
|
||||
Text,
|
||||
TextInput,
|
||||
@@ -46,6 +45,7 @@ import {
|
||||
} from "@edr/ui-common";
|
||||
|
||||
import TransferFulfillModal from "./TransferFulfillModal";
|
||||
import TransferHistoryPanel from "./TransferHistoryPanel";
|
||||
import {
|
||||
TransferCloseShortModal,
|
||||
TransferRequestFormModal,
|
||||
@@ -103,6 +103,9 @@ export default function WagonTransfersPage() {
|
||||
const [formOpen, setFormOpen] = useState(false);
|
||||
const [carryOver, setCarryOver] = useState<WagonTransferRequest | null>(null);
|
||||
const [fulfilling, setFulfilling] = useState<WagonTransferRequest | null>(null);
|
||||
const [withdrawing, setWithdrawing] = useState<WagonTransferRequest | null>(
|
||||
null,
|
||||
);
|
||||
const [closingShort, setClosingShort] = useState<WagonTransferRequest | null>(
|
||||
null,
|
||||
);
|
||||
@@ -269,15 +272,7 @@ export default function WagonTransfersPage() {
|
||||
radius="md"
|
||||
variant="subtle"
|
||||
color="red"
|
||||
loading={cancel.isPending}
|
||||
onClick={async () => {
|
||||
try {
|
||||
await cancel.mutateAsync({ id: r.id });
|
||||
toast.success("Request withdrawn");
|
||||
} catch {
|
||||
// interceptor surfaces the reason
|
||||
}
|
||||
}}
|
||||
onClick={() => setWithdrawing(r)}
|
||||
>
|
||||
Withdraw
|
||||
</Button>
|
||||
@@ -494,132 +489,53 @@ export default function WagonTransfersPage() {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Modal
|
||||
opened={Boolean(withdrawing)}
|
||||
onClose={() => setWithdrawing(null)}
|
||||
radius="md"
|
||||
title="Withdraw this request?"
|
||||
>
|
||||
{!withdrawing ? null : (
|
||||
<Stack gap="sm">
|
||||
<Text size="sm">
|
||||
{yardLabel(withdrawing.fromYard)} →{" "}
|
||||
{yardLabel(withdrawing.toYard)} ·{" "}
|
||||
{wagonTypeLabel(withdrawing.wagonType)} ·{" "}
|
||||
{withdrawing.quantity} wagon(s)
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
The source yard stops seeing it. Withdrawing can't be undone —
|
||||
raise a new request if you still need the wagons.
|
||||
</Text>
|
||||
<Group justify="flex-end" gap="sm">
|
||||
<Button
|
||||
variant="default"
|
||||
radius="md"
|
||||
onClick={() => setWithdrawing(null)}
|
||||
>
|
||||
Keep it
|
||||
</Button>
|
||||
<Button
|
||||
color="red"
|
||||
radius="md"
|
||||
leftSection={<XCircle size={15} />}
|
||||
loading={cancel.isPending}
|
||||
onClick={async () => {
|
||||
try {
|
||||
await cancel.mutateAsync({ id: withdrawing.id });
|
||||
toast.success("Request withdrawn");
|
||||
setWithdrawing(null);
|
||||
} catch {
|
||||
// interceptor surfaces the reason
|
||||
}
|
||||
}}
|
||||
>
|
||||
Withdraw
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
)}
|
||||
</Modal>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Who moved what. A staffer sees their own activity; holders of
|
||||
* `transfer_history_all` can widen it to every staffer (the backend enforces
|
||||
* the scope regardless of the toggle).
|
||||
*/
|
||||
function TransferHistoryPanel() {
|
||||
const { user } = useAuth();
|
||||
const canSeeAll = hasPermission(user, FREIGHT_PERMS.wagons.transferHistoryAll);
|
||||
const [allStaff, setAllStaff] = useState(false);
|
||||
const [page, setPage] = useState(1);
|
||||
const scopeAll = canSeeAll && allStaff;
|
||||
|
||||
const mine = useQuery({
|
||||
...api.wagonTransferRequests.history.queryOptions({
|
||||
input: { page, pageSize: 20 },
|
||||
}),
|
||||
enabled: !scopeAll,
|
||||
});
|
||||
const all = useQuery({
|
||||
...api.wagonTransferRequests.historyAll.queryOptions({
|
||||
input: { page, pageSize: 20 },
|
||||
}),
|
||||
enabled: scopeAll,
|
||||
});
|
||||
const source = scopeAll ? all : mine;
|
||||
const requests = source.data?.requests ?? [];
|
||||
const movements = source.data?.movements ?? [];
|
||||
const meta = source.data?.meta;
|
||||
|
||||
return (
|
||||
<Card withBorder radius="md" p="md">
|
||||
<Stack gap="md">
|
||||
<Group justify="space-between" wrap="wrap">
|
||||
<Text fw={600}>Transfer history</Text>
|
||||
{canSeeAll ? (
|
||||
<Switch
|
||||
label="All staff"
|
||||
checked={allStaff}
|
||||
onChange={(e) => {
|
||||
setAllStaff(e.currentTarget.checked);
|
||||
setPage(1);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</Group>
|
||||
|
||||
{source.isLoading ? (
|
||||
<Group justify="center" py="lg">
|
||||
<Loader size="sm" />
|
||||
</Group>
|
||||
) : (
|
||||
<Group align="flex-start" grow gap="lg" wrap="wrap">
|
||||
<Stack gap={6} miw={280}>
|
||||
<Text size="sm" fw={700} c="dimmed" tt="uppercase">
|
||||
Requests ({meta?.requestsTotal ?? 0})
|
||||
</Text>
|
||||
{requests.length === 0 ? (
|
||||
<Text size="sm" c="dimmed">
|
||||
Nothing yet.
|
||||
</Text>
|
||||
) : (
|
||||
requests.map((r) => (
|
||||
<Group key={r.id} gap={8} wrap="nowrap" justify="space-between">
|
||||
<Text size="sm" truncate>
|
||||
{yardLabel(r.fromYard)} → {yardLabel(r.toYard)} ·{" "}
|
||||
{r.fulfilledQuantity}/{r.quantity}
|
||||
</Text>
|
||||
<TransferStatusBadge status={r.status} />
|
||||
</Group>
|
||||
))
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Stack gap={6} miw={280}>
|
||||
<Text size="sm" fw={700} c="dimmed" tt="uppercase">
|
||||
Wagons moved ({meta?.movementsTotal ?? 0})
|
||||
</Text>
|
||||
{movements.length === 0 ? (
|
||||
<Text size="sm" c="dimmed">
|
||||
Nothing yet.
|
||||
</Text>
|
||||
) : (
|
||||
movements.map((m) => (
|
||||
<Group key={m.id} gap={8} wrap="nowrap" justify="space-between">
|
||||
<Text size="sm" truncate>
|
||||
{m.wagon?.wagonNumber ?? "Wagon"} · {yardLabel(m.fromYard)} →{" "}
|
||||
{yardLabel(m.toYard)}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{fmtDateTime(m.occurredAt)}
|
||||
</Text>
|
||||
</Group>
|
||||
))
|
||||
)}
|
||||
</Stack>
|
||||
</Group>
|
||||
)}
|
||||
|
||||
<Group justify="center" gap="sm">
|
||||
<Button
|
||||
variant="default"
|
||||
size="xs"
|
||||
radius="md"
|
||||
disabled={page <= 1}
|
||||
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
<Text size="sm" c="dimmed">
|
||||
Page {meta?.page ?? page} of {meta?.totalPages ?? 1}
|
||||
</Text>
|
||||
<Button
|
||||
variant="default"
|
||||
size="xs"
|
||||
radius="md"
|
||||
disabled={page >= (meta?.totalPages ?? 1)}
|
||||
onClick={() => setPage((p) => p + 1)}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -170,6 +170,7 @@ import {
|
||||
} from "./payments.service";
|
||||
import {
|
||||
routesService,
|
||||
type RouteListFilters,
|
||||
type RouteRecord,
|
||||
type SaveRoutePayload,
|
||||
type YardRef,
|
||||
@@ -1507,6 +1508,13 @@ export const api = {
|
||||
(input) => ["routes", input?.status ?? "all"],
|
||||
),
|
||||
|
||||
listPaged: endpoint<RouteListFilters, PaginatedResponse<RouteRecord>>(
|
||||
"routes",
|
||||
"listPaged",
|
||||
(filters) => routesService.getPaged(filters).then((r) => r.data),
|
||||
(filters) => ["routes", "paged", filters],
|
||||
),
|
||||
|
||||
yards: endpoint<void, YardRef[]>(
|
||||
"routes",
|
||||
"yards",
|
||||
@@ -2082,6 +2090,16 @@ export const api = {
|
||||
({ slug, filters }) => [...QUERY_KEYS.FLEET.list(slug), filters ?? {}],
|
||||
),
|
||||
|
||||
listPaged: endpoint<
|
||||
{ slug: FleetResourceSlug; filters?: FleetListFilters },
|
||||
PaginatedResponse<FleetRecord>
|
||||
>(
|
||||
"fleet",
|
||||
"listPaged",
|
||||
({ slug, filters }) => fleetService.listPaged(slug, filters),
|
||||
({ slug, filters }) => [...QUERY_KEYS.FLEET.list(slug), "paged", filters ?? {}],
|
||||
),
|
||||
|
||||
create: endpoint<
|
||||
{ slug: FleetResourceSlug; data: Record<string, unknown> },
|
||||
unknown
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { PaginatedResponse } from "@edr/types";
|
||||
|
||||
import { cargoService, type Cargo } from "@/services/cargoService";
|
||||
import { containerService, type Container } from "@/services/containerService";
|
||||
import {
|
||||
@@ -28,6 +30,20 @@ const listHandlers: Record<
|
||||
drivers: (filters) => driversService.getAll(filters ?? {}).then((r) => r.data),
|
||||
};
|
||||
|
||||
/**
|
||||
* Server-paginated slugs. Everything else still lists in full and pages in the
|
||||
* browser — add an entry here once its API grows a `/paged` endpoint.
|
||||
*/
|
||||
const pagedHandlers: Partial<
|
||||
Record<FleetResourceSlug, (filters: FleetListFilters) => Promise<PaginatedResponse<FleetRecord>>>
|
||||
> = {
|
||||
locomotives: (filters) => locomotivesService.getPaged(filters).then((r) => r.data),
|
||||
wagons: (filters) => wagonService.getPaged(filters).then((r) => r.data),
|
||||
};
|
||||
|
||||
export const isFleetServerPaginated = (slug: FleetResourceSlug): boolean =>
|
||||
slug in pagedHandlers;
|
||||
|
||||
const createHandlers: Record<FleetResourceSlug, (data: Record<string, unknown>) => Promise<unknown>> = {
|
||||
locomotives: (data) => locomotivesService.create(data),
|
||||
trains: (data) => trainService.create(data),
|
||||
@@ -63,6 +79,12 @@ const removeHandlers: Record<FleetResourceSlug, (id: string) => Promise<unknown>
|
||||
|
||||
export const fleetService = {
|
||||
list: (slug: FleetResourceSlug, filters?: FleetListFilters) => listHandlers[slug](filters),
|
||||
/** Only for slugs in `pagedHandlers` — guard with `isFleetServerPaginated`. */
|
||||
listPaged: (slug: FleetResourceSlug, filters: FleetListFilters = {}) => {
|
||||
const handler = pagedHandlers[slug];
|
||||
if (!handler) throw new Error(`Fleet resource "${slug}" has no paginated list endpoint`);
|
||||
return handler(filters);
|
||||
},
|
||||
create: (slug: FleetResourceSlug, data: Record<string, unknown>) => createHandlers[slug](data),
|
||||
update: (slug: FleetResourceSlug, id: string, data: Record<string, unknown>) =>
|
||||
updateHandlers[slug](id, data),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { PaginatedResponse } from '@edr/types';
|
||||
|
||||
import { api as apiClient } from '../auth/http';
|
||||
|
||||
import { URL_CONSTANTS } from '@/constants/URLS';
|
||||
@@ -19,8 +21,31 @@ export interface LocomotiveListFilters {
|
||||
excludeCoupled?: boolean;
|
||||
/** With excludeCoupled: keep THIS train's own coupled locos in the list. */
|
||||
excludeTrainId?: string;
|
||||
/** Free-text over code + name — only honoured by `getPaged`. */
|
||||
search?: string;
|
||||
/** Registration day range (YYYY-MM-DD), both ends inclusive. */
|
||||
createdFrom?: string;
|
||||
createdTo?: string;
|
||||
/** Only read by `getPaged`. */
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
const locomotiveListQuery = (filters: LocomotiveListFilters): string => {
|
||||
const params = new URLSearchParams();
|
||||
if (filters.status) params.set('status', filters.status);
|
||||
if (filters.currentYardId) params.set('currentYardId', filters.currentYardId);
|
||||
if (filters.excludeCoupled) params.set('excludeCoupled', 'true');
|
||||
if (filters.excludeTrainId) params.set('excludeTrainId', filters.excludeTrainId);
|
||||
if (filters.search?.trim()) params.set('search', filters.search.trim());
|
||||
if (filters.createdFrom) params.set('createdFrom', filters.createdFrom);
|
||||
if (filters.createdTo) params.set('createdTo', filters.createdTo);
|
||||
if (filters.page) params.set('page', String(filters.page));
|
||||
if (filters.pageSize) params.set('pageSize', String(filters.pageSize));
|
||||
const qs = params.toString();
|
||||
return qs ? `?${qs}` : '';
|
||||
};
|
||||
|
||||
export interface Locomotive {
|
||||
id: string;
|
||||
code: string;
|
||||
@@ -45,17 +70,15 @@ export type SaveLocomotivePayload = Omit<
|
||||
>;
|
||||
|
||||
export const locomotivesService = {
|
||||
getAll: (filters: LocomotiveListFilters = {}) => {
|
||||
const params = new URLSearchParams();
|
||||
if (filters.status) params.set('status', filters.status);
|
||||
if (filters.currentYardId) params.set('currentYardId', filters.currentYardId);
|
||||
if (filters.excludeCoupled) params.set('excludeCoupled', 'true');
|
||||
if (filters.excludeTrainId) params.set('excludeTrainId', filters.excludeTrainId);
|
||||
const qs = params.toString();
|
||||
return apiClient.get<Locomotive[]>(
|
||||
`${URL_CONSTANTS.LOCOMOTIVES.BASE}${qs ? `?${qs}` : ''}`,
|
||||
);
|
||||
},
|
||||
getAll: (filters: LocomotiveListFilters = {}) =>
|
||||
apiClient.get<Locomotive[]>(
|
||||
`${URL_CONSTANTS.LOCOMOTIVES.BASE}${locomotiveListQuery(filters)}`,
|
||||
),
|
||||
/** Same filters as `getAll` plus search, server-paginated ({items, meta}). */
|
||||
getPaged: (filters: LocomotiveListFilters = {}) =>
|
||||
apiClient.get<PaginatedResponse<Locomotive>>(
|
||||
`${URL_CONSTANTS.LOCOMOTIVES.BASE}/paged${locomotiveListQuery(filters)}`,
|
||||
),
|
||||
getById: (id: string) => apiClient.get<Locomotive>(URL_CONSTANTS.LOCOMOTIVES.BY_ID(id)),
|
||||
create: (data: Partial<SaveLocomotivePayload>) =>
|
||||
apiClient.post(URL_CONSTANTS.LOCOMOTIVES.BASE, data),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { PaginatedResponse } from '@edr/types';
|
||||
|
||||
import { api as apiClient } from '../auth/http';
|
||||
|
||||
import { URL_CONSTANTS } from '@/constants/URLS';
|
||||
@@ -78,9 +80,22 @@ export const ROUTE_STATUS_OPTIONS: Array<{ value: RouteStatus; label: string }>
|
||||
{ value: 'STOP_WORKING', label: 'Stop working' },
|
||||
];
|
||||
|
||||
export interface RouteListFilters {
|
||||
status?: RouteStatus;
|
||||
search?: string;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
export const routesService = {
|
||||
getAll: (params?: { status?: RouteStatus; search?: string }) =>
|
||||
apiClient.get<RouteRecord[]>(URL_CONSTANTS.ROUTES.BASE, { params }),
|
||||
/** Same filters as `getAll`, server-paginated ({items, meta}). */
|
||||
getPaged: (params: RouteListFilters = {}) =>
|
||||
apiClient.get<PaginatedResponse<RouteRecord>>(
|
||||
`${URL_CONSTANTS.ROUTES.BASE}/paged`,
|
||||
{ params },
|
||||
),
|
||||
getById: (id: string) => apiClient.get<RouteRecord>(URL_CONSTANTS.ROUTES.BY_ID(id)),
|
||||
create: (data: SaveRoutePayload) => apiClient.post(URL_CONSTANTS.ROUTES.BASE, data),
|
||||
update: (id: string, data: Partial<SaveRoutePayload>) =>
|
||||
|
||||
@@ -41,10 +41,35 @@ export interface WagonListFilters {
|
||||
currentYardId?: string;
|
||||
wagonTypeId?: string;
|
||||
trainId?: string;
|
||||
/** Drop wagons already coupled to a built train — only loose ones can be taken. */
|
||||
unassigned?: boolean;
|
||||
/** Run number — matches a wagon whose export OR import run equals it. */
|
||||
trainNumber?: string;
|
||||
/** Registration day range (YYYY-MM-DD), both ends inclusive. */
|
||||
createdFrom?: string;
|
||||
createdTo?: string;
|
||||
/** Only read by `getPaged`. */
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
const wagonListQuery = (filters: WagonListFilters): string => {
|
||||
const params = new URLSearchParams();
|
||||
if (filters.search?.trim()) params.set('search', filters.search.trim());
|
||||
if (filters.status) params.set('status', filters.status);
|
||||
if (filters.currentYardId) params.set('currentYardId', filters.currentYardId);
|
||||
if (filters.wagonTypeId) params.set('wagonTypeId', filters.wagonTypeId);
|
||||
if (filters.trainId) params.set('trainId', filters.trainId);
|
||||
if (filters.unassigned) params.set('unassigned', 'true');
|
||||
if (filters.trainNumber) params.set('trainNumber', filters.trainNumber);
|
||||
if (filters.createdFrom) params.set('createdFrom', filters.createdFrom);
|
||||
if (filters.createdTo) params.set('createdTo', filters.createdTo);
|
||||
if (filters.page) params.set('page', String(filters.page));
|
||||
if (filters.pageSize) params.set('pageSize', String(filters.pageSize));
|
||||
const qs = params.toString();
|
||||
return qs ? `?${qs}` : '';
|
||||
};
|
||||
|
||||
/**
|
||||
* One row of the wagon_movements ledger: every physical relocation between
|
||||
* yards — a booking's loaded leg, an empty reposition ride, or a manual staff
|
||||
@@ -70,17 +95,11 @@ export interface WagonMovementRecord {
|
||||
}
|
||||
|
||||
export const wagonService = {
|
||||
getAll: (filters: WagonListFilters = {}) => {
|
||||
const params = new URLSearchParams();
|
||||
if (filters.search?.trim()) params.set('search', filters.search.trim());
|
||||
if (filters.status) params.set('status', filters.status);
|
||||
if (filters.currentYardId) params.set('currentYardId', filters.currentYardId);
|
||||
if (filters.wagonTypeId) params.set('wagonTypeId', filters.wagonTypeId);
|
||||
if (filters.trainId) params.set('trainId', filters.trainId);
|
||||
if (filters.trainNumber) params.set('trainNumber', filters.trainNumber);
|
||||
const qs = params.toString();
|
||||
return apiClient.get<Wagon[]>(`/wagons${qs ? `?${qs}` : ''}`);
|
||||
},
|
||||
getAll: (filters: WagonListFilters = {}) =>
|
||||
apiClient.get<Wagon[]>(`/wagons${wagonListQuery(filters)}`),
|
||||
/** Same filters as `getAll`, server-paginated ({items, meta}). */
|
||||
getPaged: (filters: WagonListFilters = {}) =>
|
||||
apiClient.get<PaginatedResponse<Wagon>>(`/wagons/paged${wagonListQuery(filters)}`),
|
||||
getById: (id: string) => apiClient.get<Wagon>(`/wagons/${id}`),
|
||||
getMovements: (id: string) =>
|
||||
apiClient.get<WagonMovementRecord[]>(`/wagons/${id}/movements`),
|
||||
|
||||
Reference in New Issue
Block a user