mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 13:10:56 +00:00
Invoice genaration and seal
This commit is contained in:
@@ -21,6 +21,9 @@ export interface ImportTrainRow {
|
||||
totalBookings: number;
|
||||
totalContainers: number;
|
||||
totalCargoes: number;
|
||||
unloadedBookings: number;
|
||||
pendingUnloadBookings: number;
|
||||
fullyUnloaded: boolean;
|
||||
status: string;
|
||||
}
|
||||
|
||||
@@ -205,7 +208,24 @@ export class SchedulingReadFacade {
|
||||
WHERE tsbc.train_schedule_id = ts.id AND c.deleted_at IS NULL) AS "totalContainers",
|
||||
(SELECT count(*) FROM freight.cargoes cg
|
||||
JOIN freight.train_schedule_bookings tsbg ON tsbg.booking_id = cg.booking_id AND tsbg.deleted_at IS NULL
|
||||
WHERE tsbg.train_schedule_id = ts.id AND cg.deleted_at IS NULL) AS "totalCargoes"
|
||||
WHERE tsbg.train_schedule_id = ts.id AND cg.deleted_at IS NULL) AS "totalCargoes",
|
||||
(SELECT count(*) FROM freight.train_schedule_bookings tsbp
|
||||
JOIN freight.bookings bp ON bp.id = tsbp.booking_id AND bp.deleted_at IS NULL
|
||||
WHERE tsbp.train_schedule_id = ts.id
|
||||
AND tsbp.deleted_at IS NULL
|
||||
AND bp.status NOT IN ('DRAFT', 'REJECTED', 'CANCELLED', 'COMPLETED')
|
||||
AND (
|
||||
NOT EXISTS (
|
||||
SELECT 1 FROM freight.warehouse_inventory invp
|
||||
WHERE invp.booking_id = bp.id AND invp.deleted_at IS NULL
|
||||
)
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM freight.warehouse_inventory invr
|
||||
WHERE invr.booking_id = bp.id
|
||||
AND invr.deleted_at IS NULL
|
||||
AND invr.status = 'RECEIVED'
|
||||
)
|
||||
)) AS "pendingUnloadBookings"
|
||||
FROM freight.train_schedules ts
|
||||
LEFT JOIN freight.yards oy ON oy.id = ts.origin_station_id
|
||||
LEFT JOIN freight.yards dy ON dy.id = ts.destination_station_id
|
||||
@@ -219,13 +239,22 @@ export class SchedulingReadFacade {
|
||||
(r) =>
|
||||
deriveTradeDirection({ country: r.originCountry }, { country: r.destinationCountry }) === 'IMPORT',
|
||||
)
|
||||
.map(({ originCountry: _oc, destinationCountry: _dc, ...rest }) => ({
|
||||
...rest,
|
||||
totalBookings: Number(rest.totalBookings) || 0,
|
||||
totalContainers: Number(rest.totalContainers) || 0,
|
||||
totalCargoes: Number(rest.totalCargoes) || 0,
|
||||
route: rest.origin || rest.destination ? `${rest.origin ?? '?'} → ${rest.destination ?? '?'}` : null,
|
||||
}));
|
||||
.map(({ originCountry: _oc, destinationCountry: _dc, ...rest }) => {
|
||||
const totalBookings = Number(rest.totalBookings) || 0;
|
||||
const pendingUnloadBookings = Number(rest.pendingUnloadBookings) || 0;
|
||||
const unloadedBookings = Math.max(totalBookings - pendingUnloadBookings, 0);
|
||||
|
||||
return {
|
||||
...rest,
|
||||
totalBookings,
|
||||
totalContainers: Number(rest.totalContainers) || 0,
|
||||
totalCargoes: Number(rest.totalCargoes) || 0,
|
||||
unloadedBookings,
|
||||
pendingUnloadBookings,
|
||||
fullyUnloaded: totalBookings > 0 && pendingUnloadBookings === 0,
|
||||
route: rest.origin || rest.destination ? `${rest.origin ?? '?'} → ${rest.destination ?? '?'}` : null,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** Assigned bookings/items for an arrived import train (one row per booking). Read-only. */
|
||||
|
||||
@@ -726,6 +726,12 @@ function ImportTrainDetailTable({ scheduleId }: { scheduleId: string }) {
|
||||
}
|
||||
|
||||
/** Import Arrive Queue: arrived IMPORT trains, with Open (detail) + Auto Unload per train. */
|
||||
const getPendingUnloadBookings = (train: ImportTrain) =>
|
||||
train.pendingUnloadBookings ?? train.totalBookings;
|
||||
|
||||
const isFullyUnloaded = (train: ImportTrain) =>
|
||||
Boolean(train.fullyUnloaded) || (train.totalBookings > 0 && getPendingUnloadBookings(train) === 0);
|
||||
|
||||
function ImportArriveQueueTab({
|
||||
enabled,
|
||||
onChanged,
|
||||
@@ -744,9 +750,19 @@ function ImportArriveQueueTab({
|
||||
const [busyId, setBusyId] = useState<string | null>(null);
|
||||
|
||||
const autoUnload = async (train: ImportTrain) => {
|
||||
if (isFullyUnloaded(train)) {
|
||||
toast({
|
||||
title: 'Already unloaded',
|
||||
description: `${train.trainNumber ?? 'This train'} has no remaining bookings to auto unload.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setBusyId(train.scheduleId);
|
||||
try {
|
||||
const r = await autoUnloadMutation.mutateAsync(train.scheduleId);
|
||||
const alreadyUnloaded = r.unloadedCount === 0 && r.skippedCount > 0 && r.failedCount === 0;
|
||||
const firstReason = r.results.find((item) => item.reason)?.reason;
|
||||
const extra = [
|
||||
r.skippedCount ? `${r.skippedCount} skipped` : '',
|
||||
r.failedCount ? `${r.failedCount} failed` : '',
|
||||
@@ -754,8 +770,8 @@ function ImportArriveQueueTab({
|
||||
.filter(Boolean)
|
||||
.join(', ');
|
||||
toast({
|
||||
title: `${r.unloadedCount} unloaded`,
|
||||
description: extra || undefined,
|
||||
title: alreadyUnloaded ? 'Already unloaded' : `${r.unloadedCount} unloaded`,
|
||||
description: alreadyUnloaded ? firstReason ?? 'This train is already in warehouse inventory.' : extra || undefined,
|
||||
});
|
||||
onChanged?.();
|
||||
} catch (error) {
|
||||
@@ -800,6 +816,8 @@ function ImportArriveQueueTab({
|
||||
<Table.Tbody>
|
||||
{trains.map((t: ImportTrain) => {
|
||||
const isOpen = openId === t.scheduleId;
|
||||
const fullyUnloaded = isFullyUnloaded(t);
|
||||
const unloadedBookings = t.unloadedBookings ?? t.totalBookings - getPendingUnloadBookings(t);
|
||||
return (
|
||||
<Fragment key={t.scheduleId}>
|
||||
<Table.Tr>
|
||||
@@ -817,7 +835,14 @@ function ImportArriveQueueTab({
|
||||
<Table.Td ta="center">{t.totalContainers}</Table.Td>
|
||||
<Table.Td ta="center">{t.totalCargoes}</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge color="indigo" variant="light" size="sm">{t.status}</Badge>
|
||||
<Stack gap={2}>
|
||||
<Badge color={fullyUnloaded ? 'green' : 'indigo'} variant="light" size="sm">
|
||||
{fullyUnloaded ? 'UNLOADED' : t.status}
|
||||
</Badge>
|
||||
<Text size="xs" c="dimmed">
|
||||
{Math.max(unloadedBookings, 0)}/{t.totalBookings} unloaded
|
||||
</Text>
|
||||
</Stack>
|
||||
</Table.Td>
|
||||
<Table.Td ta="right">
|
||||
<Group gap="xs" justify="flex-end" wrap="nowrap">
|
||||
@@ -831,12 +856,13 @@ function ImportArriveQueueTab({
|
||||
</Button>
|
||||
<Button
|
||||
size="compact-xs"
|
||||
color="indigo"
|
||||
color={fullyUnloaded ? 'gray' : 'indigo'}
|
||||
leftSection={<Truck size={14} />}
|
||||
loading={busyId === t.scheduleId}
|
||||
disabled={fullyUnloaded || t.totalBookings === 0}
|
||||
onClick={() => autoUnload(t)}
|
||||
>
|
||||
Auto Unload Arrived Bookings
|
||||
{fullyUnloaded ? 'Already Unloaded' : 'Auto Unload Arrived Bookings'}
|
||||
</Button>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
|
||||
@@ -37,6 +37,12 @@ const getErrorMessage = (error: unknown) => {
|
||||
return error instanceof Error ? error.message : undefined;
|
||||
};
|
||||
|
||||
const getPendingUnloadBookings = (train: ImportTrain) =>
|
||||
train.pendingUnloadBookings ?? train.totalBookings;
|
||||
|
||||
const isFullyUnloaded = (train: ImportTrain) =>
|
||||
Boolean(train.fullyUnloaded) || (train.totalBookings > 0 && getPendingUnloadBookings(train) === 0);
|
||||
|
||||
function ImportTrainDetailRows({ scheduleId }: { scheduleId: string }) {
|
||||
const { data: items = [], isLoading } = useImportTrainItems(scheduleId);
|
||||
|
||||
@@ -105,10 +111,20 @@ export default function ArrivalQueuePage() {
|
||||
const [busyScheduleId, setBusyScheduleId] = useState<string | null>(null);
|
||||
|
||||
const unloadTrain = async (train: ImportTrain) => {
|
||||
if (isFullyUnloaded(train)) {
|
||||
toast({
|
||||
title: 'Already unloaded',
|
||||
description: `${train.trainNumber ?? 'This train'} has no remaining bookings to auto unload.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setBusyScheduleId(train.scheduleId);
|
||||
try {
|
||||
const res = (await autoUnload.mutateAsync(train.scheduleId)) as { data: AutoUnloadArrivedResult };
|
||||
const result = res.data;
|
||||
const alreadyUnloaded = result.unloadedCount === 0 && result.skippedCount > 0 && result.failedCount === 0;
|
||||
const firstReason = result.results.find((item) => item.reason)?.reason;
|
||||
const details = [
|
||||
result.skippedCount ? `${result.skippedCount} skipped` : '',
|
||||
result.failedCount ? `${result.failedCount} failed` : '',
|
||||
@@ -117,8 +133,10 @@ export default function ArrivalQueuePage() {
|
||||
.join(', ');
|
||||
|
||||
toast({
|
||||
title: `${result.unloadedCount} booking(s) unloaded`,
|
||||
description: details || `${train.trainNumber ?? 'Train'} moved into warehouse inventory.`,
|
||||
title: alreadyUnloaded ? 'Already unloaded' : `${result.unloadedCount} booking(s) unloaded`,
|
||||
description: alreadyUnloaded
|
||||
? firstReason ?? `${train.trainNumber ?? 'Train'} is already in warehouse inventory.`
|
||||
: details || `${train.trainNumber ?? 'Train'} moved into warehouse inventory.`,
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
@@ -181,6 +199,8 @@ export default function ArrivalQueuePage() {
|
||||
<Table.Tbody>
|
||||
{trains.map((train: ImportTrain) => {
|
||||
const isOpen = openScheduleId === train.scheduleId;
|
||||
const fullyUnloaded = isFullyUnloaded(train);
|
||||
const unloadedBookings = train.unloadedBookings ?? train.totalBookings - getPendingUnloadBookings(train);
|
||||
return (
|
||||
<Fragment key={train.scheduleId}>
|
||||
<Table.Tr>
|
||||
@@ -204,9 +224,14 @@ export default function ArrivalQueuePage() {
|
||||
<Table.Td ta="center">{train.totalContainers}</Table.Td>
|
||||
<Table.Td ta="center">{train.totalCargoes}</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge variant="light" color="teal" size="sm">
|
||||
{train.status}
|
||||
</Badge>
|
||||
<Stack gap={2}>
|
||||
<Badge variant="light" color={fullyUnloaded ? 'green' : 'teal'} size="sm">
|
||||
{fullyUnloaded ? 'UNLOADED' : train.status}
|
||||
</Badge>
|
||||
<Text size="xs" c="dimmed">
|
||||
{Math.max(unloadedBookings, 0)}/{train.totalBookings} unloaded
|
||||
</Text>
|
||||
</Stack>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Group gap="xs" justify="flex-end" wrap="nowrap">
|
||||
@@ -220,12 +245,13 @@ export default function ArrivalQueuePage() {
|
||||
</Button>
|
||||
<Button
|
||||
size="compact-xs"
|
||||
color="orange"
|
||||
color={fullyUnloaded ? 'gray' : 'orange'}
|
||||
leftSection={busyScheduleId === train.scheduleId ? <PackageOpen size={14} /> : <Truck size={14} />}
|
||||
loading={busyScheduleId === train.scheduleId}
|
||||
disabled={fullyUnloaded || train.totalBookings === 0}
|
||||
onClick={() => unloadTrain(train)}
|
||||
>
|
||||
Auto Unload
|
||||
{fullyUnloaded ? 'Already Unloaded' : 'Auto Unload'}
|
||||
</Button>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
|
||||
@@ -430,6 +430,9 @@ export interface ImportTrain {
|
||||
totalBookings: number;
|
||||
totalContainers: number;
|
||||
totalCargoes: number;
|
||||
unloadedBookings?: number;
|
||||
pendingUnloadBookings?: number;
|
||||
fullyUnloaded?: boolean;
|
||||
status: string;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user