mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 16:28:12 +00:00
258 lines
8.7 KiB
TypeScript
258 lines
8.7 KiB
TypeScript
import { useMemo } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { Badge, Button, Card, Group, Tabs, Text } from '@mantine/core';
|
|
import { CreditCard, Eye, Truck, TrainFront } from 'lucide-react';
|
|
import { DataTable, type ColumnDef } from '@edr/ui-common';
|
|
|
|
import { PageContainer, PageHeader } from '@/components/page';
|
|
import {
|
|
InventoryWorkbench,
|
|
VisualEmptyState,
|
|
formatNumber,
|
|
} from '@/components/warehouses';
|
|
import { LoadToTrainPanel } from '@/components/warehouses/LoadToTrainPanel';
|
|
import { useMutation, useQuery } from '@tanstack/react-query';
|
|
|
|
import { api } from '@/services/api';
|
|
import { useToast } from '@/hooks/use-toast';
|
|
import type { WarehouseInventoryItem } from '@/types/warehouse';
|
|
|
|
const isPaid = (item: WarehouseInventoryItem) => item.booking?.status === 'PAID';
|
|
|
|
/**
|
|
* Loading Queue — manage inventory through the loading workflow.
|
|
* Tabs:
|
|
* - Ready to Load: READY_FOR_LOADING + booking PAID (can Mark as Loaded)
|
|
* - Pending Payment: READY_FOR_LOADING + booking not PAID (no Load action)
|
|
* - Loaded Inventory: LOADED (can Dispatch)
|
|
* - Dispatch Queue: LOADED (can Dispatch)
|
|
*/
|
|
export default function LoadingQueuePage() {
|
|
const navigate = useNavigate();
|
|
const { toast } = useToast();
|
|
const autoLoad = useMutation(api.warehouses.autoLoadReady.mutationOptions());
|
|
const { data: readyData, isLoading: readyLoading } = useQuery(
|
|
api.warehouses.listInventory.queryOptions({
|
|
input: { filter: { status: 'READY_FOR_LOADING' } },
|
|
}),
|
|
);
|
|
const { data: loadedData, isLoading: loadedLoading } = useQuery(
|
|
api.warehouses.listInventory.queryOptions({ input: { filter: { status: 'LOADED' } } }),
|
|
);
|
|
|
|
const handleAutoLoad = async () => {
|
|
try {
|
|
const r = await autoLoad.mutateAsync();
|
|
toast({
|
|
title: 'Auto-load complete',
|
|
description: `Loaded ${r.loadedCount} PAID item(s); skipped ${r.skippedCount} (unpaid stay pending).`,
|
|
});
|
|
} catch {
|
|
toast({ variant: 'destructive', title: 'Auto-load failed' });
|
|
}
|
|
};
|
|
|
|
const readyItems = readyData ?? [];
|
|
const loadedItems = loadedData ?? [];
|
|
|
|
const paidItems = useMemo(() => readyItems.filter(isPaid), [readyItems]);
|
|
const unpaidItems = useMemo(() => readyItems.filter((i) => !isPaid(i)), [readyItems]);
|
|
|
|
return (
|
|
<PageContainer>
|
|
<PageHeader
|
|
title="Loading Queue"
|
|
subtitle="Manage bookings and inventory through the loading workflow."
|
|
action={
|
|
<Button
|
|
leftSection={<Truck size={16} />}
|
|
loading={autoLoad.isPending}
|
|
onClick={handleAutoLoad}
|
|
>
|
|
Auto Load Ready Items
|
|
</Button>
|
|
}
|
|
/>
|
|
|
|
<Card>
|
|
<Tabs defaultValue="ready">
|
|
<Tabs.List>
|
|
<Tabs.Tab
|
|
value="ready"
|
|
leftSection={
|
|
<Badge size="xs" color="edr-green">
|
|
{paidItems.length}
|
|
</Badge>
|
|
}
|
|
>
|
|
Ready to Load
|
|
</Tabs.Tab>
|
|
<Tabs.Tab
|
|
value="pending"
|
|
leftSection={
|
|
<Badge size="xs" color="orange">
|
|
{unpaidItems.length}
|
|
</Badge>
|
|
}
|
|
>
|
|
Pending Payment
|
|
</Tabs.Tab>
|
|
<Tabs.Tab
|
|
value="loaded"
|
|
leftSection={
|
|
<Badge size="xs" color="teal">
|
|
{loadedItems.length}
|
|
</Badge>
|
|
}
|
|
>
|
|
Loaded Inventory
|
|
</Tabs.Tab>
|
|
<Tabs.Tab
|
|
value="dispatch"
|
|
leftSection={
|
|
<Badge size="xs" color="blue">
|
|
{loadedItems.length}
|
|
</Badge>
|
|
}
|
|
>
|
|
Dispatch Queue
|
|
</Tabs.Tab>
|
|
<Tabs.Tab value="load-train" leftSection={<TrainFront size={14} />}>
|
|
Load to Train
|
|
</Tabs.Tab>
|
|
</Tabs.List>
|
|
|
|
{/* Ready to Load — PAID bookings, can be marked Loaded */}
|
|
<Tabs.Panel value="ready" pt="md">
|
|
{!readyLoading && paidItems.length === 0 ? (
|
|
<VisualEmptyState
|
|
variant="wagon"
|
|
title="Nothing ready to load"
|
|
description="Paid bookings marked Ready For Loading appear here, ready to load onto a wagon."
|
|
/>
|
|
) : (
|
|
<InventoryWorkbench items={paidItems} isLoading={readyLoading} />
|
|
)}
|
|
</Tabs.Panel>
|
|
|
|
{/* Pending Payment — unpaid bookings, read-only (no Load action) */}
|
|
<Tabs.Panel value="pending" pt="md">
|
|
{!readyLoading && unpaidItems.length === 0 ? (
|
|
<VisualEmptyState
|
|
variant="cargo"
|
|
title="No unpaid bookings"
|
|
description="Ready-for-loading items whose booking is not yet PAID appear here."
|
|
/>
|
|
) : (
|
|
<PendingPaymentTable items={unpaidItems} onNavigate={navigate} />
|
|
)}
|
|
</Tabs.Panel>
|
|
|
|
{/* Loaded Inventory — LOADED items, can Dispatch */}
|
|
<Tabs.Panel value="loaded" pt="md">
|
|
{!loadedLoading && loadedItems.length === 0 ? (
|
|
<VisualEmptyState
|
|
variant="container"
|
|
title="No loaded inventory yet"
|
|
description="Items loaded onto a wagon appear here, ready to dispatch."
|
|
/>
|
|
) : (
|
|
<InventoryWorkbench items={loadedItems} isLoading={loadedLoading} />
|
|
)}
|
|
</Tabs.Panel>
|
|
|
|
{/* Dispatch Queue — LOADED items awaiting departure */}
|
|
<Tabs.Panel value="dispatch" pt="md">
|
|
{!loadedLoading && loadedItems.length === 0 ? (
|
|
<VisualEmptyState
|
|
variant="train"
|
|
title="Nothing to dispatch"
|
|
description="Loaded items appear here, ready to mark as dispatched."
|
|
/>
|
|
) : (
|
|
<InventoryWorkbench items={loadedItems} isLoading={loadedLoading} />
|
|
)}
|
|
</Tabs.Panel>
|
|
|
|
{/* Load to Train — per-train arrived containers/cargoes, multiselect → load onto wagons */}
|
|
<Tabs.Panel value="load-train" pt="md">
|
|
<LoadToTrainPanel />
|
|
</Tabs.Panel>
|
|
</Tabs>
|
|
</Card>
|
|
</PageContainer>
|
|
);
|
|
}
|
|
|
|
interface PendingPaymentTableProps {
|
|
items: WarehouseInventoryItem[];
|
|
onNavigate: (path: string) => void;
|
|
}
|
|
|
|
/** Read-only view of unpaid ready-for-loading items. No Mark-as-Loaded action. */
|
|
function PendingPaymentTable({ items, onNavigate }: PendingPaymentTableProps) {
|
|
const columns: ColumnDef<WarehouseInventoryItem>[] = [
|
|
{
|
|
id: 'booking',
|
|
header: 'Booking',
|
|
cell: ({ row }) => (
|
|
<Text fw={600} size="sm">
|
|
{row.original.booking?.reference ?? row.original.bookingId?.slice(0, 8) ?? '—'}
|
|
</Text>
|
|
),
|
|
},
|
|
{ id: 'warehouse', header: 'Warehouse', cell: ({ row }) => row.original.warehouse?.code ?? '—' },
|
|
{ id: 'zone', header: 'Zone', cell: ({ row }) => row.original.zone?.code ?? '—' },
|
|
{ id: 'weight', header: 'Weight (kg)', cell: ({ row }) => formatNumber(row.original.weight) },
|
|
{
|
|
id: 'payment',
|
|
header: 'Payment',
|
|
cell: ({ row }) => (
|
|
<Badge color="orange" variant="light" size="sm">
|
|
{row.original.booking?.status ?? row.original.booking?.paymentStatus ?? 'UNPAID'}
|
|
</Badge>
|
|
),
|
|
},
|
|
{
|
|
id: 'actions',
|
|
header: '',
|
|
cell: ({ row }) => {
|
|
const item = row.original;
|
|
return (
|
|
<Group gap="xs" justify="flex-end" wrap="nowrap" onClick={(e) => e.stopPropagation()}>
|
|
<Button
|
|
size="compact-xs"
|
|
variant="light"
|
|
color="gray"
|
|
leftSection={<Eye size={14} />}
|
|
disabled={!item.bookingId}
|
|
onClick={() => item.bookingId && onNavigate(`/dashboard/booking-requests/${item.bookingId}`)}
|
|
>
|
|
Booking
|
|
</Button>
|
|
<Button
|
|
size="compact-xs"
|
|
variant="light"
|
|
leftSection={<CreditCard size={14} />}
|
|
disabled={!item.bookingId}
|
|
onClick={() => item.bookingId && onNavigate(`/dashboard/booking-requests/${item.bookingId}`)}
|
|
>
|
|
Payment
|
|
</Button>
|
|
</Group>
|
|
);
|
|
},
|
|
},
|
|
];
|
|
|
|
return (
|
|
<DataTable
|
|
columns={columns}
|
|
data={items}
|
|
status="success"
|
|
emptyMessage="No unpaid items."
|
|
containerClassName="border-0 shadow-none"
|
|
/>
|
|
);
|
|
}
|