mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 02:58:11 +00:00
feat(warehouse): batch 3 — loading, dispatch & train-departure visibility
- WarehouseLoading entity + Batch3 migration (wagon loading records) - wagon-aware load() with validation; dispatch moved to PATCH - read-only SchedulingReadFacade (schedule/wagon/departure) — never writes scheduling - new endpoints: GET /warehouse-loadings, loadable-wagons, booking schedule - Loading Queue / Loaded Inventory / Dispatch Queue pages + routes + sidebar - FreightVisual illustrations (page heroes + empty states) - booking detail: loaded/dispatched/wagon + read-only train schedule Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
import type { CSSProperties, ReactElement } from 'react';
|
||||
|
||||
export type FreightVisualVariant =
|
||||
| 'train'
|
||||
| 'warehouse'
|
||||
| 'container'
|
||||
| 'wagon'
|
||||
| 'cargo'
|
||||
| 'route'
|
||||
| 'empty';
|
||||
|
||||
interface FreightVisualProps {
|
||||
variant: FreightVisualVariant;
|
||||
/** Pixel size of the (square) artwork. Defaults to 64. */
|
||||
size?: number;
|
||||
style?: CSSProperties;
|
||||
className?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lightweight railway/freight illustrations — minimal, enterprise-logistics style.
|
||||
* Inline SVG (no network cost) using EDR brand colors: green, yellow, dark text,
|
||||
* light gray. Purposely low-contrast so it never overpowers tables/forms.
|
||||
*
|
||||
* Use only in page headers, empty states, and KPI cards.
|
||||
*/
|
||||
const EDR = {
|
||||
green: '#2F9E44',
|
||||
greenSoft: '#D3F9D8',
|
||||
yellow: '#F59F00',
|
||||
yellowSoft: '#FFF3BF',
|
||||
dark: '#343A40',
|
||||
gray: '#ADB5BD',
|
||||
graySoft: '#E9ECEF',
|
||||
};
|
||||
|
||||
function Train() {
|
||||
return (
|
||||
<>
|
||||
{/* track */}
|
||||
<rect x="2" y="52" width="60" height="3" rx="1.5" fill={EDR.graySoft} />
|
||||
{/* locomotive body */}
|
||||
<rect x="6" y="20" width="26" height="26" rx="3" fill={EDR.green} />
|
||||
<rect x="10" y="24" width="8" height="8" rx="1.5" fill={EDR.greenSoft} />
|
||||
<rect x="22" y="24" width="6" height="8" rx="1.5" fill={EDR.greenSoft} />
|
||||
{/* cab roof */}
|
||||
<rect x="9" y="15" width="14" height="6" rx="2" fill={EDR.dark} />
|
||||
{/* wagon */}
|
||||
<rect x="36" y="26" width="22" height="20" rx="2.5" fill={EDR.yellow} />
|
||||
<rect x="40" y="30" width="14" height="6" rx="1" fill={EDR.yellowSoft} />
|
||||
{/* wheels */}
|
||||
{[12, 24, 42, 52].map((cx) => (
|
||||
<circle key={cx} cx={cx} cy={48} r={3.2} fill={EDR.dark} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Warehouse() {
|
||||
return (
|
||||
<>
|
||||
{/* ground */}
|
||||
<rect x="4" y="50" width="56" height="3" rx="1.5" fill={EDR.graySoft} />
|
||||
{/* roof */}
|
||||
<path d="M10 24 L32 12 L54 24 Z" fill={EDR.green} />
|
||||
{/* body */}
|
||||
<rect x="14" y="24" width="36" height="26" rx="1.5" fill={EDR.greenSoft} />
|
||||
{/* shutter door */}
|
||||
<rect x="26" y="32" width="12" height="18" rx="1" fill={EDR.dark} />
|
||||
<rect x="27.5" y="35" width="9" height="2" fill={EDR.gray} />
|
||||
<rect x="27.5" y="39" width="9" height="2" fill={EDR.gray} />
|
||||
<rect x="27.5" y="43" width="9" height="2" fill={EDR.gray} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Container() {
|
||||
return (
|
||||
<>
|
||||
{/* stacked containers */}
|
||||
<rect x="8" y="34" width="22" height="16" rx="1.5" fill={EDR.green} />
|
||||
<rect x="34" y="34" width="22" height="16" rx="1.5" fill={EDR.yellow} />
|
||||
<rect x="20" y="16" width="24" height="16" rx="1.5" fill={EDR.dark} />
|
||||
{/* corrugation lines */}
|
||||
{[12, 16, 20, 24].map((x) => (
|
||||
<rect key={`a${x}`} x={x} y="37" width="1.5" height="10" fill={EDR.greenSoft} />
|
||||
))}
|
||||
{[38, 42, 46, 50].map((x) => (
|
||||
<rect key={`b${x}`} x={x} y="37" width="1.5" height="10" fill={EDR.yellowSoft} />
|
||||
))}
|
||||
{[25, 29, 33, 37].map((x) => (
|
||||
<rect key={`c${x}`} x={x} y="19" width="1.5" height="10" fill={EDR.gray} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Wagon() {
|
||||
return (
|
||||
<>
|
||||
<rect x="4" y="50" width="56" height="3" rx="1.5" fill={EDR.graySoft} />
|
||||
{/* flatbed wagon */}
|
||||
<rect x="8" y="38" width="48" height="8" rx="1.5" fill={EDR.dark} />
|
||||
{/* cargo on wagon */}
|
||||
<rect x="14" y="22" width="16" height="16" rx="1.5" fill={EDR.green} />
|
||||
<rect x="34" y="26" width="16" height="12" rx="1.5" fill={EDR.yellow} />
|
||||
{/* wheels */}
|
||||
{[16, 26, 40, 50].map((cx) => (
|
||||
<circle key={cx} cx={cx} cy={48} r={3.2} fill={EDR.dark} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Cargo() {
|
||||
return (
|
||||
<>
|
||||
{/* cargo boxes */}
|
||||
<rect x="12" y="30" width="20" height="20" rx="2" fill={EDR.yellow} />
|
||||
<rect x="34" y="34" width="18" height="16" rx="2" fill={EDR.green} />
|
||||
{/* tape */}
|
||||
<rect x="21" y="30" width="2" height="20" fill={EDR.yellowSoft} />
|
||||
<rect x="12" y="38" width="20" height="2" fill={EDR.yellowSoft} />
|
||||
<rect x="42" y="34" width="2" height="16" fill={EDR.greenSoft} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Route() {
|
||||
return (
|
||||
<>
|
||||
{/* track line with stations */}
|
||||
<rect x="6" y="31" width="52" height="2" rx="1" fill={EDR.gray} />
|
||||
{[10, 22, 34, 46, 58].map((x) => (
|
||||
<rect key={x} x={x - 0.5} y="28" width="1.5" height="8" fill={EDR.graySoft} />
|
||||
))}
|
||||
<circle cx="10" cy="32" r="5" fill={EDR.green} />
|
||||
<circle cx="54" cy="32" r="5" fill={EDR.yellow} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Empty() {
|
||||
return (
|
||||
<>
|
||||
{/* empty open box */}
|
||||
<path d="M14 28 L32 22 L50 28 L50 30 L32 24 L14 30 Z" fill={EDR.gray} />
|
||||
<path d="M14 30 L32 36 L32 50 L14 44 Z" fill={EDR.graySoft} />
|
||||
<path d="M50 30 L32 36 L32 50 L50 44 Z" fill={EDR.graySoft} />
|
||||
<path d="M14 30 L32 24 L50 30 L32 36 Z" fill="#F8F9FA" />
|
||||
<circle cx="32" cy="14" r="2.5" fill={EDR.yellow} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const VARIANTS: Record<FreightVisualVariant, () => ReactElement> = {
|
||||
train: Train,
|
||||
warehouse: Warehouse,
|
||||
container: Container,
|
||||
wagon: Wagon,
|
||||
cargo: Cargo,
|
||||
route: Route,
|
||||
empty: Empty,
|
||||
};
|
||||
|
||||
export function FreightVisual({ variant, size = 64, style, className, title }: FreightVisualProps) {
|
||||
const Art = VARIANTS[variant];
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 64 64"
|
||||
fill="none"
|
||||
role="img"
|
||||
aria-label={title ?? `${variant} illustration`}
|
||||
className={className}
|
||||
style={style}
|
||||
>
|
||||
{title ? <title>{title}</title> : null}
|
||||
<Art />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -4,12 +4,12 @@ import { Center, Loader } from '@mantine/core';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import {
|
||||
useDispatchInventory,
|
||||
useLoadInventory,
|
||||
useMarkReadyForLoading,
|
||||
useStoreInventory,
|
||||
} from '@/hooks/useWarehouses';
|
||||
import type { InventoryAction, WarehouseInventoryItem } from '@/types/warehouse';
|
||||
import { InventoryHistoryModal } from './InventoryHistoryModal';
|
||||
import { LoadInventoryModal } from './LoadInventoryModal';
|
||||
import { MoveInventoryModal } from './MoveInventoryModal';
|
||||
import { ReserveInventoryModal } from './ReserveInventoryModal';
|
||||
import { WarehouseInventoryTable } from './WarehouseInventoryTable';
|
||||
@@ -26,11 +26,11 @@ export function InventoryWorkbench({ items, isLoading }: InventoryWorkbenchProps
|
||||
const [busyId, setBusyId] = useState<string | null>(null);
|
||||
const [moveItem, setMoveItem] = useState<WarehouseInventoryItem | null>(null);
|
||||
const [reserveItem, setReserveItem] = useState<WarehouseInventoryItem | null>(null);
|
||||
const [loadItem, setLoadItem] = useState<WarehouseInventoryItem | null>(null);
|
||||
const [historyItem, setHistoryItem] = useState<WarehouseInventoryItem | null>(null);
|
||||
|
||||
const storeMutation = useStoreInventory();
|
||||
const readyMutation = useMarkReadyForLoading();
|
||||
const loadMutation = useLoadInventory();
|
||||
const dispatchMutation = useDispatchInventory();
|
||||
|
||||
const runDirect = async (item: WarehouseInventoryItem, fn: () => Promise<unknown>, label: string) => {
|
||||
@@ -55,7 +55,8 @@ export function InventoryWorkbench({ items, isLoading }: InventoryWorkbenchProps
|
||||
case 'ready-for-loading':
|
||||
return runDirect(item, () => readyMutation.mutateAsync(item.id), 'Ready for loading');
|
||||
case 'load':
|
||||
return runDirect(item, () => loadMutation.mutateAsync(item.id), 'Inventory loaded');
|
||||
setLoadItem(item);
|
||||
return;
|
||||
case 'dispatch':
|
||||
return runDirect(item, () => dispatchMutation.mutateAsync(item.id), 'Inventory dispatched');
|
||||
default:
|
||||
@@ -87,6 +88,7 @@ export function InventoryWorkbench({ items, isLoading }: InventoryWorkbenchProps
|
||||
onClose={() => setReserveItem(null)}
|
||||
item={reserveItem}
|
||||
/>
|
||||
<LoadInventoryModal opened={Boolean(loadItem)} onClose={() => setLoadItem(null)} item={loadItem} />
|
||||
<InventoryHistoryModal
|
||||
opened={Boolean(historyItem)}
|
||||
onClose={() => setHistoryItem(null)}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Alert, Button, Group, Modal, NumberInput, Stack, Text, Textarea } from '@mantine/core';
|
||||
import { Info } from 'lucide-react';
|
||||
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { useLoadInventory } from '@/hooks/useWarehouses';
|
||||
import type { WarehouseInventoryItem } from '@/types/warehouse';
|
||||
import { WagonSelect } from './WagonSelect';
|
||||
import { extractErrorMessage } from './options';
|
||||
|
||||
interface LoadInventoryModalProps {
|
||||
opened: boolean;
|
||||
onClose: () => void;
|
||||
item: WarehouseInventoryItem | null;
|
||||
}
|
||||
|
||||
/** Load READY_FOR_LOADING inventory onto a wagon (creates a loading record). */
|
||||
export function LoadInventoryModal({ opened, onClose, item }: LoadInventoryModalProps) {
|
||||
const { toast } = useToast();
|
||||
const loadMutation = useLoadInventory();
|
||||
const [wagonId, setWagonId] = useState('');
|
||||
const [loadedWeight, setLoadedWeight] = useState<number | ''>('');
|
||||
const [notes, setNotes] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (opened) {
|
||||
setWagonId('');
|
||||
setLoadedWeight(item?.weight ?? '');
|
||||
setNotes('');
|
||||
}
|
||||
}, [opened, item]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!item) return;
|
||||
if (!wagonId.trim()) {
|
||||
toast({ variant: 'destructive', title: 'Select a wagon' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await loadMutation.mutateAsync({
|
||||
id: item.id,
|
||||
payload: {
|
||||
wagonId: wagonId.trim(),
|
||||
loadedWeight: loadedWeight === '' ? undefined : Number(loadedWeight),
|
||||
notes: notes.trim() || undefined,
|
||||
},
|
||||
});
|
||||
toast({ title: 'Inventory loaded', description: 'Status set to LOADED' });
|
||||
onClose();
|
||||
} catch (error) {
|
||||
toast({ variant: 'destructive', title: 'Load failed', description: extractErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal opened={opened} onClose={onClose} title="Load onto wagon" centered size="md">
|
||||
<Stack gap="md">
|
||||
<Alert icon={<Info size={16} />} color="blue" variant="light">
|
||||
<Text size="sm">
|
||||
The item must be <b>READY_FOR_LOADING</b> and the wagon must be available or already on a
|
||||
train schedule.
|
||||
</Text>
|
||||
</Alert>
|
||||
|
||||
<WagonSelect label="Wagon" required value={wagonId} onChange={setWagonId} />
|
||||
|
||||
<NumberInput
|
||||
label="Loaded weight (kg)"
|
||||
placeholder="Defaults to item weight"
|
||||
min={0}
|
||||
value={loadedWeight}
|
||||
onChange={(v) => setLoadedWeight(v === '' ? '' : Number(v))}
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
label="Notes"
|
||||
placeholder="Optional"
|
||||
autosize
|
||||
minRows={2}
|
||||
value={notes}
|
||||
onChange={(e) => {
|
||||
const v = e.currentTarget.value;
|
||||
setNotes(v);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Group justify="flex-end" mt="sm">
|
||||
<Button variant="default" onClick={onClose} disabled={loadMutation.isPending}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSubmit} loading={loadMutation.isPending}>
|
||||
Load
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { Center, Stack, Text } from '@mantine/core';
|
||||
|
||||
import { FreightVisual, type FreightVisualVariant } from './FreightVisual';
|
||||
|
||||
interface VisualEmptyStateProps {
|
||||
variant?: FreightVisualVariant;
|
||||
title: string;
|
||||
description?: string;
|
||||
action?: ReactNode;
|
||||
}
|
||||
|
||||
/** Friendly empty state with a small freight illustration. */
|
||||
export function VisualEmptyState({
|
||||
variant = 'empty',
|
||||
title,
|
||||
description,
|
||||
action,
|
||||
}: VisualEmptyStateProps) {
|
||||
return (
|
||||
<Center py="xl">
|
||||
<Stack align="center" gap="xs" maw={360}>
|
||||
<FreightVisual variant={variant} size={88} style={{ opacity: 0.85 }} />
|
||||
<Text fw={600} ta="center">
|
||||
{title}
|
||||
</Text>
|
||||
{description && (
|
||||
<Text size="sm" c="dimmed" ta="center">
|
||||
{description}
|
||||
</Text>
|
||||
)}
|
||||
{action}
|
||||
</Stack>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Select } from '@mantine/core';
|
||||
|
||||
import { useLoadableWagons } from '@/hooks/useWarehouses';
|
||||
|
||||
interface WagonSelectProps {
|
||||
value: string;
|
||||
onChange: (wagonId: string) => void;
|
||||
label?: string;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
/** Searchable wagon picker. Lists wagons that are loadable (read-only from scheduling). */
|
||||
export function WagonSelect({ value, onChange, label = 'Wagon', required }: WagonSelectProps) {
|
||||
const { data, isLoading } = useLoadableWagons();
|
||||
|
||||
const options = (data ?? []).map((w) => ({
|
||||
value: w.id,
|
||||
label: `${w.wagonNumber} · ${w.status}`,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Select
|
||||
label={label}
|
||||
required={required}
|
||||
searchable
|
||||
clearable
|
||||
data={options}
|
||||
value={value || null}
|
||||
onChange={(v) => onChange(v ?? '')}
|
||||
placeholder={isLoading ? 'Loading wagons…' : 'Search wagon number'}
|
||||
nothingFoundMessage="No loadable wagons found"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { Box, Group, Stack, Text, Title } from '@mantine/core';
|
||||
|
||||
import { FreightVisual, type FreightVisualVariant } from './FreightVisual';
|
||||
|
||||
interface WarehouseHeroProps {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
/** Primary illustration shown on the right of the hero. */
|
||||
variant?: FreightVisualVariant;
|
||||
/** Optional secondary illustration tucked behind the primary. */
|
||||
secondaryVariant?: FreightVisualVariant;
|
||||
actions?: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Page header hero with a lightweight freight illustration. Low-contrast,
|
||||
* minimal — sets context without overpowering the data below.
|
||||
*/
|
||||
export function WarehouseHero({
|
||||
title,
|
||||
subtitle,
|
||||
variant = 'warehouse',
|
||||
secondaryVariant,
|
||||
actions,
|
||||
}: WarehouseHeroProps) {
|
||||
return (
|
||||
<Box
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #F8F9FA 0%, #F1F3F5 100%)',
|
||||
border: '1px solid #E9ECEF',
|
||||
borderRadius: 'var(--mantine-radius-md)',
|
||||
padding: 'var(--mantine-spacing-lg)',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Group justify="space-between" wrap="nowrap" align="center">
|
||||
<Stack gap={4}>
|
||||
<Title order={3}>{title}</Title>
|
||||
{subtitle && (
|
||||
<Text size="sm" c="dimmed">
|
||||
{subtitle}
|
||||
</Text>
|
||||
)}
|
||||
{actions && <Group mt="sm">{actions}</Group>}
|
||||
</Stack>
|
||||
|
||||
<Group gap="xs" wrap="nowrap" style={{ opacity: 0.95 }}>
|
||||
{secondaryVariant && (
|
||||
<FreightVisual variant={secondaryVariant} size={56} style={{ opacity: 0.7 }} />
|
||||
)}
|
||||
<FreightVisual variant={variant} size={84} />
|
||||
</Group>
|
||||
</Group>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useState } from 'react';
|
||||
import { Badge, Button, Card, Divider, Group, Stack, Text } from '@mantine/core';
|
||||
import { PackagePlus, Warehouse as WarehouseIcon } from 'lucide-react';
|
||||
import { PackagePlus, Train as TrainIcon, Warehouse as WarehouseIcon } from 'lucide-react';
|
||||
|
||||
import { useWarehouseInventory } from '@/hooks/useWarehouses';
|
||||
import { useBookingSchedule, useWarehouseInventory } from '@/hooks/useWarehouses';
|
||||
import { InventoryStatusBadge } from './badges';
|
||||
import { FreightVisual } from './FreightVisual';
|
||||
import { formatDate } from './options';
|
||||
import { ReceiveInventoryModal } from './ReceiveInventoryModal';
|
||||
|
||||
@@ -28,9 +29,14 @@ function Row({ label, value }: { label: string; value: React.ReactNode }) {
|
||||
export function WarehouseInfoCard({ bookingId, bookingReference }: WarehouseInfoCardProps) {
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const { data, isLoading } = useWarehouseInventory({ bookingId });
|
||||
const { data: scheduleView } = useBookingSchedule(bookingId);
|
||||
|
||||
const items = data ?? [];
|
||||
const latest = items[0];
|
||||
const schedule = scheduleView?.schedule;
|
||||
const wagon = scheduleView?.wagon;
|
||||
const isLoadedOrDispatched =
|
||||
latest?.status === 'LOADED' || latest?.status === 'DISPATCHED';
|
||||
|
||||
return (
|
||||
<Card withBorder radius="md" padding="lg">
|
||||
@@ -65,9 +71,52 @@ export function WarehouseInfoCard({ bookingId, bookingReference }: WarehouseInfo
|
||||
<Row label="Inventory Status" value={<InventoryStatusBadge status={latest.status} />} />
|
||||
<Row label="Arrived At" value={formatDate(latest.arrivedAt)} />
|
||||
<Row label="Ready For Loading At" value={formatDate(latest.readyForLoadingAt)} />
|
||||
{isLoadedOrDispatched && (
|
||||
<>
|
||||
<Row
|
||||
label="Wagon"
|
||||
value={wagon?.wagonNumber ?? '—'}
|
||||
/>
|
||||
<Row label="Loaded At" value={formatDate(latest.loadedAt)} />
|
||||
<Row label="Dispatched At" value={formatDate(latest.dispatchedAt)} />
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{schedule && (
|
||||
<>
|
||||
<Divider
|
||||
label={
|
||||
<Group gap={6}>
|
||||
<TrainIcon size={14} />
|
||||
<Text size="xs" c="dimmed">
|
||||
Train schedule (read-only)
|
||||
</Text>
|
||||
</Group>
|
||||
}
|
||||
labelPosition="left"
|
||||
/>
|
||||
<Group gap="sm" wrap="nowrap" align="flex-start">
|
||||
<FreightVisual variant="train" size={40} />
|
||||
<Stack gap="xs" style={{ flex: 1 }}>
|
||||
<Row
|
||||
label="Departure Status"
|
||||
value={
|
||||
<Badge variant="light" color="blue" size="sm">
|
||||
{schedule.status}
|
||||
</Badge>
|
||||
}
|
||||
/>
|
||||
<Row label="Scheduled Departure" value={formatDate(schedule.scheduledDepartureDate)} />
|
||||
<Row label="Scheduled Arrival" value={formatDate(schedule.scheduledArrivalDate)} />
|
||||
{wagon?.wagonNumber && <Row label="Assigned Wagon" value={wagon.wagonNumber} />}
|
||||
{wagon?.sequenceNo != null && <Row label="Wagon Position" value={`#${wagon.sequenceNo}`} />}
|
||||
</Stack>
|
||||
</Group>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="light"
|
||||
leftSection={<PackagePlus size={16} />}
|
||||
|
||||
@@ -18,3 +18,9 @@ export { ActivityTimeline } from './ActivityTimeline';
|
||||
export { InventoryHistoryModal } from './InventoryHistoryModal';
|
||||
export { InventoryWorkbench } from './InventoryWorkbench';
|
||||
export { BookingSelect } from './BookingSelect';
|
||||
export { WagonSelect } from './WagonSelect';
|
||||
export { LoadInventoryModal } from './LoadInventoryModal';
|
||||
export { FreightVisual } from './FreightVisual';
|
||||
export type { FreightVisualVariant } from './FreightVisual';
|
||||
export { WarehouseHero } from './WarehouseHero';
|
||||
export { VisualEmptyState } from './VisualEmptyState';
|
||||
|
||||
Reference in New Issue
Block a user