mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 07:15:45 +00:00
feat(warehouses): gate loading queues on the train's station loading window
Warehouse loading queues and the train schedule page now share one truth: the schedule's per-yard stationWorkLogs. A booking's items aren't loadable until "Start loading" has been clicked for their boarding yard, mirroring the same assertStationWorkStarted check the train schedule page's own Load button already enforces. - LoadableTrainRow/LoadableTrain carry originStationId + stationWorkLogs. - TrainLoadableItem carries originYardId/originYardLabel/loadingWindowStarted, computed from station_work_logs in the same query. - New YardLoadingWindows component surfaces the per-yard windows on the loading panel; ReceiveInventoryModal and LoadToTrainPanel wire it in. - Invalidate loadable-trains/train-loadable-items/warehouse-inventory queries alongside train-scheduling ones, since they render off the same data. Fixes the StationWorkLogJson typo that broke the freight-api build.
This commit is contained in:
@@ -0,0 +1,77 @@
|
|||||||
|
import { WarehouseInventoryService } from './warehouse-inventory.service';
|
||||||
|
import type { TrainLoadableItemRow } from './warehouse-inventory.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cargo may only go onto a wagon inside a STARTED loading window at its
|
||||||
|
* boarding yard — the same rule the train schedule's own Load button enforces
|
||||||
|
* (assertStationWorkStarted). The warehouse loading queues load through a
|
||||||
|
* different service, so the rule is mirrored here; without it the two surfaces
|
||||||
|
* disagree and the queue offers a Load the schedule would refuse.
|
||||||
|
*
|
||||||
|
* Only the DataSource is touched, so the instance is built off the prototype
|
||||||
|
* rather than stubbing every collaborator.
|
||||||
|
*/
|
||||||
|
const row = (over: Partial<TrainLoadableItemRow> = {}): TrainLoadableItemRow =>
|
||||||
|
({
|
||||||
|
id: 'inv-1',
|
||||||
|
bookingId: 'b-1',
|
||||||
|
bookingReference: 'BK-1',
|
||||||
|
customerName: 'Acme',
|
||||||
|
containerNumber: 'CN-1',
|
||||||
|
cargoType: 'General',
|
||||||
|
weight: 20,
|
||||||
|
grnNumber: 'GRN-1',
|
||||||
|
inspectionStatus: 'PASSED',
|
||||||
|
status: 'READY_FOR_LOADING',
|
||||||
|
wagonId: 'w-1',
|
||||||
|
wagonNumber: 'W-001',
|
||||||
|
sequenceNo: 1,
|
||||||
|
originYardId: 'yard-1',
|
||||||
|
originYardLabel: 'Modjo',
|
||||||
|
loadingWindowStarted: true,
|
||||||
|
loadable: true,
|
||||||
|
...over,
|
||||||
|
}) as TrainLoadableItemRow;
|
||||||
|
|
||||||
|
function makeService(items: TrainLoadableItemRow[]) {
|
||||||
|
const query = jest.fn().mockResolvedValue([
|
||||||
|
{ trainNumber: 'T-100', origin: 'Modjo', destination: 'Djibouti', departure: null },
|
||||||
|
]);
|
||||||
|
const load = jest.fn().mockResolvedValue(undefined);
|
||||||
|
const service = Object.create(WarehouseInventoryService.prototype) as Record<string, unknown>;
|
||||||
|
service.dataSource = { query };
|
||||||
|
service.load = load;
|
||||||
|
service.trainLoadableItems = jest.fn().mockResolvedValue(items);
|
||||||
|
return { service: service as unknown as WarehouseInventoryService, load };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('loadItemsOntoTrain() — station loading window gate', () => {
|
||||||
|
it('skips an item whose boarding yard has no started loading window', async () => {
|
||||||
|
const { service, load } = makeService([row({ loadingWindowStarted: false })]);
|
||||||
|
|
||||||
|
const result = await service.loadItemsOntoTrain('sched-1', ['inv-1']);
|
||||||
|
|
||||||
|
expect(load).not.toHaveBeenCalled();
|
||||||
|
expect(result.loadedCount).toBe(0);
|
||||||
|
expect(result.skippedCount).toBe(1);
|
||||||
|
expect(result.results[0].reason).toContain('Start loading at Modjo first');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads once the window is started', async () => {
|
||||||
|
const { service, load } = makeService([row()]);
|
||||||
|
|
||||||
|
const result = await service.loadItemsOntoTrain('sched-1', ['inv-1']);
|
||||||
|
|
||||||
|
expect(load).toHaveBeenCalledTimes(1);
|
||||||
|
expect(result.loadedCount).toBe(1);
|
||||||
|
expect(result.skippedCount).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('still reports the wagon blocker first — the window is not the only gate', async () => {
|
||||||
|
const { service } = makeService([row({ wagonId: null, loadingWindowStarted: false })]);
|
||||||
|
|
||||||
|
const result = await service.loadItemsOntoTrain('sched-1', ['inv-1']);
|
||||||
|
|
||||||
|
expect(result.results[0].reason).toContain('No wagon allocated');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -19,6 +19,7 @@ import { deriveTradeDirection } from '../../common/derive-trade-direction.util';
|
|||||||
import { generateGrnNumber } from '../../common/grn.util';
|
import { generateGrnNumber } from '../../common/grn.util';
|
||||||
import { SCHEDULE_BOOKINGS_CTE } from '../../common/schedule-bookings.sql';
|
import { SCHEDULE_BOOKINGS_CTE } from '../../common/schedule-bookings.sql';
|
||||||
import { Booking } from '../bookings/entities/booking.entity';
|
import { Booking } from '../bookings/entities/booking.entity';
|
||||||
|
import type { StationWorkLog } from '../train-schedules/entities/train-schedule.entity';
|
||||||
import { Cargo } from '../cargoes/entities/cargoes.entity';
|
import { Cargo } from '../cargoes/entities/cargoes.entity';
|
||||||
import { Company } from '../companies/entities/company.entity';
|
import { Company } from '../companies/entities/company.entity';
|
||||||
import { Container } from '../container-management/entities/container.entity';
|
import { Container } from '../container-management/entities/container.entity';
|
||||||
@@ -326,7 +327,7 @@ export interface LoadableTrainRow {
|
|||||||
* schedule page stores them. The warehouse loading queues render the same
|
* schedule page stores them. The warehouse loading queues render the same
|
||||||
* Start/End controls off this, so both surfaces show one truth.
|
* Start/End controls off this, so both surfaces show one truth.
|
||||||
*/
|
*/
|
||||||
stationWorkLogs: Record<string, StationWorkLogJson> | null;
|
stationWorkLogs: Record<string, StationWorkLog> | null;
|
||||||
/** Received/ready inventory not yet loaded onto this train. */
|
/** Received/ready inventory not yet loaded onto this train. */
|
||||||
readyCount: number;
|
readyCount: number;
|
||||||
/** Inventory already loaded onto this train. */
|
/** Inventory already loaded onto this train. */
|
||||||
@@ -1855,6 +1856,8 @@ export class WarehouseInventoryService {
|
|||||||
dy.country AS "destinationCountry",
|
dy.country AS "destinationCountry",
|
||||||
ts.status AS "status",
|
ts.status AS "status",
|
||||||
ts.scheduled_departure_date AS "departureTime",
|
ts.scheduled_departure_date AS "departureTime",
|
||||||
|
ts.origin_station_id AS "originStationId",
|
||||||
|
ts.station_work_logs AS "stationWorkLogs",
|
||||||
(SELECT count(*) FROM sched_bookings sb
|
(SELECT count(*) FROM sched_bookings sb
|
||||||
JOIN freight.warehouse_inventory inv
|
JOIN freight.warehouse_inventory inv
|
||||||
ON inv.booking_id = sb.booking_id AND inv.deleted_at IS NULL
|
ON inv.booking_id = sb.booking_id AND inv.deleted_at IS NULL
|
||||||
@@ -1918,7 +1921,15 @@ export class WarehouseInventoryService {
|
|||||||
inv.status AS "status",
|
inv.status AS "status",
|
||||||
wl.wagon_id AS "wagonId",
|
wl.wagon_id AS "wagonId",
|
||||||
wl.wagon_number AS "wagonNumber",
|
wl.wagon_number AS "wagonNumber",
|
||||||
wl.sequence_no AS "sequenceNo"
|
wl.sequence_no AS "sequenceNo",
|
||||||
|
COALESCE(b.origin_yard_id, ts.origin_station_id) AS "originYardId",
|
||||||
|
COALESCE(oy.label, oy.code) AS "originYardLabel",
|
||||||
|
-- Same rule the train schedule's own Load button obeys
|
||||||
|
-- (assertStationWorkStarted): the yard's loading window must have
|
||||||
|
-- been started before its cargo may go on a wagon.
|
||||||
|
(ts.station_work_logs #>> ARRAY[
|
||||||
|
COALESCE(b.origin_yard_id, ts.origin_station_id)::text, 'loading', 'startedAt'
|
||||||
|
]) IS NOT NULL AS "loadingWindowStarted"
|
||||||
FROM sched_bookings sb
|
FROM sched_bookings sb
|
||||||
JOIN freight.train_schedules ts ON ts.id = sb.schedule_id
|
JOIN freight.train_schedules ts ON ts.id = sb.schedule_id
|
||||||
JOIN freight.bookings b ON b.id = sb.booking_id AND b.deleted_at IS NULL
|
JOIN freight.bookings b ON b.id = sb.booking_id AND b.deleted_at IS NULL
|
||||||
@@ -1926,6 +1937,7 @@ export class WarehouseInventoryService {
|
|||||||
LEFT JOIN freight.companies company ON company.id = b.company_id
|
LEFT JOIN freight.companies company ON company.id = b.company_id
|
||||||
LEFT JOIN freight.cargo_types cgt ON cgt.id = b.cargo_type_id
|
LEFT JOIN freight.cargo_types cgt ON cgt.id = b.cargo_type_id
|
||||||
LEFT JOIN freight.containers ct ON ct.id = inv.container_id
|
LEFT JOIN freight.containers ct ON ct.id = inv.container_id
|
||||||
|
LEFT JOIN freight.yards oy ON oy.id = COALESCE(b.origin_yard_id, ts.origin_station_id)
|
||||||
LEFT JOIN LATERAL (
|
LEFT JOIN LATERAL (
|
||||||
SELECT w.id AS wagon_id, w.wagon_number, tsw.sequence_no
|
SELECT w.id AS wagon_id, w.wagon_number, tsw.sequence_no
|
||||||
FROM freight.wagon_booking_allocations wba
|
FROM freight.wagon_booking_allocations wba
|
||||||
@@ -1948,9 +1960,14 @@ export class WarehouseInventoryService {
|
|||||||
...r,
|
...r,
|
||||||
// Export flow: received at the warehouse -> GRN -> loaded onto its wagon.
|
// Export flow: received at the warehouse -> GRN -> loaded onto its wagon.
|
||||||
// The row only exists once the goods were received, so requiring a GRN and
|
// The row only exists once the goods were received, so requiring a GRN and
|
||||||
// an allocated wagon completes the chain.
|
// an allocated wagon completes the chain. The yard's loading window is the
|
||||||
|
// fourth link — the warehouse queue must not offer what the train
|
||||||
|
// schedule's own Load button would refuse.
|
||||||
loadable:
|
loadable:
|
||||||
r.status === 'READY_FOR_LOADING' && Boolean(r.wagonId) && Boolean(r.grnNumber),
|
r.status === 'READY_FOR_LOADING' &&
|
||||||
|
Boolean(r.wagonId) &&
|
||||||
|
Boolean(r.grnNumber) &&
|
||||||
|
r.loadingWindowStarted,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2007,6 +2024,11 @@ export class WarehouseInventoryService {
|
|||||||
// nothing rides a train without one.
|
// nothing rides a train without one.
|
||||||
if (!item.grnNumber) { skip('No GRN — receive the goods and generate the GRN first'); continue; }
|
if (!item.grnNumber) { skip('No GRN — receive the goods and generate the GRN first'); continue; }
|
||||||
if (!item.wagonId) { skip('No wagon allocated — allocate a wagon first'); continue; }
|
if (!item.wagonId) { skip('No wagon allocated — allocate a wagon first'); continue; }
|
||||||
|
// Mirrors assertStationWorkStarted on the train-schedule load path.
|
||||||
|
if (!item.loadingWindowStarted) {
|
||||||
|
skip(`Start loading at ${item.originYardLabel ?? 'the boarding yard'} first — the loading time window has not been started`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.load(inventoryId, {
|
await this.load(inventoryId, {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
type TrainLoadableItem,
|
type TrainLoadableItem,
|
||||||
} from '@/services/warehouse.service';
|
} from '@/services/warehouse.service';
|
||||||
import { extractErrorMessage } from './options';
|
import { extractErrorMessage } from './options';
|
||||||
|
import { YardLoadingWindows } from './YardLoadingWindows';
|
||||||
|
|
||||||
const STAGE_COLOR: Record<string, string> = {
|
const STAGE_COLOR: Record<string, string> = {
|
||||||
RECEIVED: 'blue',
|
RECEIVED: 'blue',
|
||||||
@@ -161,6 +162,11 @@ function TrainRow({ train, expanded, onToggle }: { train: LoadableTrain; expande
|
|||||||
</Alert>
|
</Alert>
|
||||||
) : (
|
) : (
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
|
<YardLoadingWindows
|
||||||
|
scheduleId={train.scheduleId}
|
||||||
|
items={items}
|
||||||
|
logs={train.stationWorkLogs}
|
||||||
|
/>
|
||||||
{bookings.map((b) => (
|
{bookings.map((b) => (
|
||||||
<BookingBlock key={b.bookingId ?? b.bookingReference} scheduleId={train.scheduleId} booking={b} />
|
<BookingBlock key={b.bookingId ?? b.bookingReference} scheduleId={train.scheduleId} booking={b} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ import { MoveInventoryModal } from './MoveInventoryModal';
|
|||||||
import { ReleaseOrderModal } from './ReleaseOrderModal';
|
import { ReleaseOrderModal } from './ReleaseOrderModal';
|
||||||
import { StoreInventoryModal } from './StoreInventoryModal';
|
import { StoreInventoryModal } from './StoreInventoryModal';
|
||||||
import { WarehouseInquiryTable } from './WarehouseInquiryTable';
|
import { WarehouseInquiryTable } from './WarehouseInquiryTable';
|
||||||
|
import { YardLoadingWindows } from './YardLoadingWindows';
|
||||||
import { extractDownloadErrorMessage, extractErrorMessage, formatDate, formatNumber, inventoryStatusOptions, warehousesAtStation, yardsForBooking } from './options';
|
import { extractDownloadErrorMessage, extractErrorMessage, formatDate, formatNumber, inventoryStatusOptions, warehousesAtStation, yardsForBooking } from './options';
|
||||||
import { openPdfBlob } from './pdf';
|
import { openPdfBlob } from './pdf';
|
||||||
import ListControls from '@/components/common/ListControls';
|
import ListControls from '@/components/common/ListControls';
|
||||||
@@ -1598,6 +1599,11 @@ function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?:
|
|||||||
queryFn: () => warehouseService.getLoadableTrains(),
|
queryFn: () => warehouseService.getLoadableTrains(),
|
||||||
enabled: enabled && trainPickerOpen,
|
enabled: enabled && trainPickerOpen,
|
||||||
});
|
});
|
||||||
|
const { data: pickerItems = [] } = useQuery({
|
||||||
|
queryKey: ['train-loadable-items', targetScheduleId],
|
||||||
|
queryFn: () => warehouseService.getTrainLoadableItems(targetScheduleId!),
|
||||||
|
enabled: Boolean(targetScheduleId) && trainPickerOpen,
|
||||||
|
});
|
||||||
const loadOntoTrain = useMutation({
|
const loadOntoTrain = useMutation({
|
||||||
mutationFn: async ({ scheduleId, onlyIds }: { scheduleId: string; onlyIds: string[] }) => {
|
mutationFn: async ({ scheduleId, onlyIds }: { scheduleId: string; onlyIds: string[] }) => {
|
||||||
const items = await warehouseService.getTrainLoadableItems(scheduleId);
|
const items = await warehouseService.getTrainLoadableItems(scheduleId);
|
||||||
@@ -1608,6 +1614,17 @@ function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?:
|
|||||||
loadableIds = loadableIds.filter((id) => picked.has(id));
|
loadableIds = loadableIds.filter((id) => picked.has(id));
|
||||||
}
|
}
|
||||||
if (!loadableIds.length) {
|
if (!loadableIds.length) {
|
||||||
|
const scope = onlyIds.length
|
||||||
|
? items.filter((i) => onlyIds.includes(i.id))
|
||||||
|
: items.filter((i) => i.status === 'READY_FOR_LOADING');
|
||||||
|
// A closed loading window is the blocker staff hit most, and the old
|
||||||
|
// wagon-only message sent them to fix the wrong thing.
|
||||||
|
const shut = scope.find((i) => !i.loadingWindowStarted);
|
||||||
|
if (shut) {
|
||||||
|
throw new Error(
|
||||||
|
`Start loading at ${shut.originYardLabel ?? 'the boarding yard'} first — the loading time window has not been started`,
|
||||||
|
);
|
||||||
|
}
|
||||||
throw new Error(
|
throw new Error(
|
||||||
onlyIds.length
|
onlyIds.length
|
||||||
? 'None of the selected items have an allocated wagon on this train'
|
? 'None of the selected items have an allocated wagon on this train'
|
||||||
@@ -1701,6 +1718,13 @@ function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?:
|
|||||||
searchable
|
searchable
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{targetScheduleId ? (
|
||||||
|
<YardLoadingWindows
|
||||||
|
scheduleId={targetScheduleId}
|
||||||
|
items={pickerItems}
|
||||||
|
logs={trains.find((t) => t.scheduleId === targetScheduleId)?.stationWorkLogs}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
<Group justify="flex-end">
|
<Group justify="flex-end">
|
||||||
<Button variant="default" onClick={() => setTrainPickerOpen(false)} disabled={loadOntoTrain.isPending}>
|
<Button variant="default" onClick={() => setTrainPickerOpen(false)} disabled={loadOntoTrain.isPending}>
|
||||||
Cancel
|
Cancel
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { Alert, Badge, Group, Stack, Text } from '@mantine/core';
|
||||||
|
import { Info, MapPin } from 'lucide-react';
|
||||||
|
|
||||||
|
import { StationWorkControls } from '@/components/trainScheduling/StationWorkControls';
|
||||||
|
import type { TrainLoadableItem } from '@/services/warehouse.service';
|
||||||
|
import type { StationWorkLog } from '@/types/trainScheduling';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The train schedule's per-yard loading window, shown where the warehouse
|
||||||
|
* actually loads. Cargo may only go onto a wagon inside a started window
|
||||||
|
* (assertStationWorkStarted on the API side), so the same Start/End loading
|
||||||
|
* controls the train schedule page carries belong here too — otherwise the
|
||||||
|
* warehouse operator sees a Load button that the server refuses.
|
||||||
|
*
|
||||||
|
* One block per boarding yard of the items waiting to load, since a train can
|
||||||
|
* pick cargo up at more than one stop and each stop has its own window.
|
||||||
|
*/
|
||||||
|
export function YardLoadingWindows({
|
||||||
|
scheduleId,
|
||||||
|
items,
|
||||||
|
logs,
|
||||||
|
}: {
|
||||||
|
scheduleId: string;
|
||||||
|
items: TrainLoadableItem[];
|
||||||
|
logs: Record<string, StationWorkLog> | null | undefined;
|
||||||
|
}) {
|
||||||
|
const yards = useMemo(() => {
|
||||||
|
const byYard = new Map<string, { label: string; count: number }>();
|
||||||
|
for (const item of items) {
|
||||||
|
// Everything still queued for this train, not only the ready ones — the
|
||||||
|
// operator starts the window before the cargo finishes becoming ready.
|
||||||
|
if (item.status === 'LOADED' || !item.originYardId) continue;
|
||||||
|
const entry = byYard.get(item.originYardId);
|
||||||
|
if (entry) entry.count += 1;
|
||||||
|
else byYard.set(item.originYardId, { label: item.originYardLabel ?? 'Boarding yard', count: 1 });
|
||||||
|
}
|
||||||
|
return [...byYard.entries()].map(([yardId, v]) => ({ yardId, ...v }));
|
||||||
|
}, [items]);
|
||||||
|
|
||||||
|
if (yards.length === 0) return null;
|
||||||
|
|
||||||
|
const anyOpen = yards.some((y) => logs?.[y.yardId]?.loading?.startedAt);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Text size="sm" fw={600}>
|
||||||
|
Loading window
|
||||||
|
</Text>
|
||||||
|
{!anyOpen ? (
|
||||||
|
<Alert color="yellow" variant="light" icon={<Info size={16} />}>
|
||||||
|
Start loading at the yard before loading cargo — the train schedule records the same
|
||||||
|
window, and the server refuses cargo outside it.
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
{yards.map((yard) => (
|
||||||
|
<Stack key={yard.yardId} gap={6} p="xs" style={{ border: '1px solid var(--mantine-color-gray-3)', borderRadius: 8 }}>
|
||||||
|
<Group gap={8} align="center">
|
||||||
|
<MapPin size={13} />
|
||||||
|
<Text size="xs" fw={700}>
|
||||||
|
{yard.label}
|
||||||
|
</Text>
|
||||||
|
<Badge size="sm" radius="sm" variant="light" color="gray">
|
||||||
|
{yard.count} queued
|
||||||
|
</Badge>
|
||||||
|
</Group>
|
||||||
|
<StationWorkControls
|
||||||
|
scheduleId={scheduleId}
|
||||||
|
yardId={yard.yardId}
|
||||||
|
phase="loading"
|
||||||
|
log={logs?.[yard.yardId]?.loading}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -295,6 +295,11 @@ const INVENTORY_INVALIDATIONS: ReadonlyArray<readonly unknown[]> = [
|
|||||||
const TRAIN_SCHEDULING_INVALIDATIONS: ReadonlyArray<readonly unknown[]> = [
|
const TRAIN_SCHEDULING_INVALIDATIONS: ReadonlyArray<readonly unknown[]> = [
|
||||||
QUERY_KEYS.TRAIN_SCHEDULING.ROOT,
|
QUERY_KEYS.TRAIN_SCHEDULING.ROOT,
|
||||||
QUERY_KEYS.BOOKINGS.ROOT,
|
QUERY_KEYS.BOOKINGS.ROOT,
|
||||||
|
// A station's loading window gates the warehouse loading queues too — they
|
||||||
|
// render the same Start/End controls, so they must refresh on the same click.
|
||||||
|
["loadable-trains"],
|
||||||
|
["train-loadable-items"],
|
||||||
|
["warehouse-inventory"],
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ import type {
|
|||||||
WarehouseZone,
|
WarehouseZone,
|
||||||
ZoneContentItem,
|
ZoneContentItem,
|
||||||
} from '@/types/warehouse';
|
} from '@/types/warehouse';
|
||||||
|
import type { StationWorkLog } from '@/types/trainScheduling';
|
||||||
|
|
||||||
export type ContainerItemStage = 'PENDING' | 'RECEIVED' | 'GRN' | 'ASSIGNED' | 'LOADED' | 'LEFT' | 'DELIVERED';
|
export type ContainerItemStage = 'PENDING' | 'RECEIVED' | 'GRN' | 'ASSIGNED' | 'LOADED' | 'LEFT' | 'DELIVERED';
|
||||||
|
|
||||||
@@ -115,6 +116,10 @@ export interface LoadableTrain {
|
|||||||
departureTime: string | null;
|
departureTime: string | null;
|
||||||
readyCount: number;
|
readyCount: number;
|
||||||
loadedCount: number;
|
loadedCount: number;
|
||||||
|
/** freight.yards.id the train departs from. */
|
||||||
|
originStationId: string | null;
|
||||||
|
/** The schedule's per-yard loading/unloading windows — same store the train schedule page writes. */
|
||||||
|
stationWorkLogs: Record<string, StationWorkLog> | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A container/cargo inventory item assigned to a train, with its allocated wagon. */
|
/** A container/cargo inventory item assigned to a train, with its allocated wagon. */
|
||||||
@@ -132,6 +137,11 @@ export interface TrainLoadableItem {
|
|||||||
wagonId: string | null;
|
wagonId: string | null;
|
||||||
wagonNumber: string | null;
|
wagonNumber: string | null;
|
||||||
sequenceNo: number | null;
|
sequenceNo: number | null;
|
||||||
|
/** The booking's boarding yard — the yard whose loading window gates this item. */
|
||||||
|
originYardId: string | null;
|
||||||
|
originYardLabel: string | null;
|
||||||
|
/** True once "Start loading" was clicked for this item's boarding yard on this train. */
|
||||||
|
loadingWindowStarted: boolean;
|
||||||
loadable: boolean;
|
loadable: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user