mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
187 lines
6.5 KiB
TypeScript
187 lines
6.5 KiB
TypeScript
import { useState } from 'react';
|
|
import { Badge, Button, Card, Divider, Group, Stack, Text, Tooltip } from '@mantine/core';
|
|
import { PackagePlus, Train as TrainIcon, Warehouse as WarehouseIcon } from 'lucide-react';
|
|
|
|
import { useQuery } from '@tanstack/react-query';
|
|
|
|
import { api } from '@/services/api';
|
|
import { InventoryStatusBadge } from './badges';
|
|
import { FreightVisual } from './FreightVisual';
|
|
import { formatDate } from './options';
|
|
import { ReceiveInventoryModal } from './ReceiveInventoryModal';
|
|
|
|
interface WarehouseInfoCardProps {
|
|
bookingId: string;
|
|
bookingReference?: string;
|
|
/**
|
|
* Booking payment status. Export cargo is received into the warehouse only
|
|
* after the booking is paid — receiving an unpaid booking starts storage and
|
|
* GRN against cargo the customer has not settled. Optional so existing callers
|
|
* that do not have the booking to hand keep their current behaviour.
|
|
*/
|
|
paymentStatus?: string | null;
|
|
/**
|
|
* IMPORT | EXPORT | DOMESTIC. The payment gate is export-only: import cargo
|
|
* arrives OFF a train, so blocking its receive would strand cargo already at
|
|
* the yard.
|
|
*/
|
|
tradeDirection?: string | null;
|
|
}
|
|
|
|
function Row({ label, value }: { label: string; value: React.ReactNode }) {
|
|
return (
|
|
<Group justify="space-between" wrap="nowrap">
|
|
<Text size="sm" c="dimmed">
|
|
{label}
|
|
</Text>
|
|
<Text size="sm" fw={500} ta="right">
|
|
{value}
|
|
</Text>
|
|
</Group>
|
|
);
|
|
}
|
|
|
|
export function WarehouseInfoCard({
|
|
bookingId,
|
|
bookingReference,
|
|
paymentStatus,
|
|
tradeDirection,
|
|
}: WarehouseInfoCardProps) {
|
|
const [modalOpen, setModalOpen] = useState(false);
|
|
const { data, isLoading } = useQuery(
|
|
api.warehouses.listInventory.queryOptions({ input: { filter: { bookingId } } }),
|
|
);
|
|
const { data: scheduleView } = useQuery(
|
|
api.warehouses.bookingSchedule.queryOptions({
|
|
input: { bookingId },
|
|
enabled: Boolean(bookingId),
|
|
}),
|
|
);
|
|
|
|
const items = data ?? [];
|
|
const latest = items[0];
|
|
const schedule = scheduleView?.schedule;
|
|
const wagon = scheduleView?.wagon;
|
|
const isLoadedOrDispatched =
|
|
latest?.status === 'LOADED' || latest?.status === 'DISPATCHED';
|
|
// Export only, and only when we were actually told the status — an absent prop
|
|
// means the caller cannot answer, and guessing "unpaid" would disable a valid
|
|
// action. Mirrors the server guard on receive().
|
|
const awaitingPayment =
|
|
tradeDirection?.toUpperCase() === 'EXPORT' &&
|
|
paymentStatus != null &&
|
|
paymentStatus.toUpperCase() !== 'PAID';
|
|
|
|
return (
|
|
<Card withBorder radius="md" padding="lg">
|
|
<Stack gap="md">
|
|
<Group justify="space-between">
|
|
<Group gap="xs">
|
|
<WarehouseIcon size={18} />
|
|
<Text fw={700}>Warehouse Information</Text>
|
|
</Group>
|
|
{items.length > 0 && (
|
|
<Badge variant="light" color="gray">
|
|
{items.length} item{items.length > 1 ? 's' : ''}
|
|
</Badge>
|
|
)}
|
|
</Group>
|
|
|
|
<Divider />
|
|
|
|
{isLoading ? (
|
|
<Text size="sm" c="dimmed">
|
|
Loading…
|
|
</Text>
|
|
) : !latest ? (
|
|
<Text size="sm" c="dimmed">
|
|
This booking has not been received at any warehouse yet.
|
|
</Text>
|
|
) : (
|
|
<Stack gap="xs">
|
|
<Row label="Warehouse" value={latest.warehouse ? `${latest.warehouse.name} (${latest.warehouse.code})` : '—'} />
|
|
<Row label="Yard" value={latest.yard ? `${latest.yard.name} (${latest.yard.code})` : '—'} />
|
|
<Row label="Zone" value={latest.zone ? `${latest.zone.name} (${latest.zone.code})` : '—'} />
|
|
<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>
|
|
</>
|
|
)}
|
|
|
|
<Tooltip
|
|
label={
|
|
latest
|
|
? 'This booking is already received at the warehouse'
|
|
: 'This booking is not paid yet — cargo can only be received once payment is settled'
|
|
}
|
|
disabled={!latest && !awaitingPayment}
|
|
withArrow
|
|
>
|
|
{/* span wrapper so the tooltip still fires on the disabled button */}
|
|
<span style={{ display: 'block' }}>
|
|
<Button
|
|
variant="light"
|
|
leftSection={<PackagePlus size={16} />}
|
|
onClick={() => setModalOpen(true)}
|
|
fullWidth
|
|
disabled={Boolean(latest) || awaitingPayment}
|
|
>
|
|
{latest ? 'Received At Warehouse' : 'Receive At Warehouse'}
|
|
</Button>
|
|
</span>
|
|
</Tooltip>
|
|
</Stack>
|
|
|
|
<ReceiveInventoryModal
|
|
opened={modalOpen}
|
|
onClose={() => setModalOpen(false)}
|
|
bookingId={bookingId}
|
|
bookingLabel={bookingReference}
|
|
/>
|
|
</Card>
|
|
);
|
|
}
|