Files
edr-platform/apps/edr-freight-web/backoffice/src/pages/warehouses/LoadingQueuePage.tsx
2026-07-06 19:00:04 +00:00

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"
/>
);
}