Goods recieved notes and Booking delivery

This commit is contained in:
hagiye
2026-06-30 07:19:12 +03:00
parent 64cab3ecd0
commit 9f5c22c1ee
21 changed files with 1126 additions and 136 deletions

View File

@@ -1,4 +1,4 @@
import { Fragment, useEffect, useMemo, useState } from 'react';
import { Fragment, useEffect, useMemo, useState, type MouseEvent } from 'react';
import {
ActionIcon,
Alert,
@@ -79,6 +79,45 @@ interface ReceiveInventoryModalProps {
onReceived?: () => void;
}
function GrnDocumentButton({ inventoryId, grnNumber }: { inventoryId: string; grnNumber?: string | null }) {
const { toast } = useToast();
const [loading, setLoading] = useState(false);
const openDocument = async (event: MouseEvent<HTMLButtonElement>) => {
event.stopPropagation();
if (!grnNumber) {
toast({ variant: 'destructive', title: 'GRN document unavailable', description: 'This item has no GRN number yet.' });
return;
}
setLoading(true);
const pdfWindow = window.open('', '_blank');
try {
const response = await warehouseService.downloadGrnDocument(inventoryId);
const opened = openPdfBlob(response.data, `grn-${grnNumber}.pdf`, pdfWindow);
toast({ title: opened ? 'GRN document opened' : 'GRN document downloaded' });
} catch (error) {
pdfWindow?.close();
toast({ variant: 'destructive', title: 'GRN document failed', description: extractErrorMessage(error) });
} finally {
setLoading(false);
}
};
return (
<Button
size="compact-xs"
variant="subtle"
color="teal"
leftSection={<FileText size={12} />}
disabled={!grnNumber}
loading={loading}
onClick={openDocument}
>
{grnNumber ?? 'No GRN'}
</Button>
);
}
interface Location {
warehouseId: string;
yardId: string;
@@ -620,11 +659,13 @@ function EligibleTab({
const { toast } = useToast();
const qc = useQueryClient();
const { data: allRows = [], isLoading } = useQuery(
api.warehouses.eligibleBookings.queryOptions({ enabled }),
api.warehouses.eligibleBookings.queryOptions({
input: { direction },
enabled,
}),
);
const rows = useMemo(() => allRows.filter((r) => r.direction === direction), [allRows, direction]);
const bulkReceive = useMutation(api.warehouses.bulkReceive.mutationOptions());
const loadPassed = useMutation(api.warehouses.loadPassedExport.mutationOptions());
const requestFirstMile = useMutation({
mutationFn: (reference: string) => firstMileService.accept(reference),
onSuccess: () => {
@@ -782,10 +823,24 @@ function EligibleTab({
return;
}
const { form, lockedFields, packagingFreightType: nextPackagingFreightType } = truckEntranceFromBookings(selectedRows);
const totalContainerQuantity = selectedRows.reduce(
(sum, row) => sum + Number(row.containerQuantity ?? 0),
0,
);
const normalizedForm =
nextPackagingFreightType === 'CONTAINER' && totalContainerQuantity > 0
? {
...form,
unitCount: totalContainerQuantity,
}
: form;
setPendingReceiveIds(filteredIds);
setReceivedAt(new Date().toISOString());
setTruckForm(form);
setLockedTruckFields(lockedFields);
setTruckForm(normalizedForm);
setLockedTruckFields({
...lockedFields,
unitCount: nextPackagingFreightType === 'CONTAINER' && totalContainerQuantity > 0,
});
setPackagingFreightType(nextPackagingFreightType);
setTruckOpen(true);
};
@@ -798,18 +853,6 @@ function EligibleTab({
await receiveBookings(pendingReceiveIds, toTruckEntrancePayload(truckForm));
};
const loadPassedExport = async () => {
try {
const r = await loadPassed.mutateAsync(undefined);
toast({
title: `${r.loadedCount} loaded`,
description: r.skippedCount ? `${r.skippedCount} skipped — inspection not passed` : undefined,
});
onChanged?.();
} catch (error) {
toast({ variant: 'destructive', title: 'Load failed', description: extractErrorMessage(error) });
}
};
return (
<Stack gap="sm" mt="sm">
@@ -828,18 +871,6 @@ function EligibleTab({
Selected: <b>{selected.size}</b> / {statusFilteredRows.length} eligible
</Text>
<Group gap="xs">
{direction === 'EXPORT' && (
<Button
size="compact-sm"
variant="light"
color="teal"
leftSection={<Truck size={14} />}
loading={loadPassed.isPending}
onClick={loadPassedExport}
>
Load Passed Export Items
</Button>
)}
<Button
size="compact-sm"
color={direction === 'EXPORT' ? 'edr-green' : undefined}
@@ -849,7 +880,7 @@ function EligibleTab({
loading={bulkReceive.isPending}
onClick={() => openTruckReceive(selected.size > 0 ? [...selected] : selectableRows.map((r) => r.id))}
>
{direction === 'EXPORT' ? 'Receive to Warehouse' : 'Receive All to Warehouse'}
{direction === 'EXPORT' ? 'Receive All for Loading' : 'Receive All to Warehouse'}
</Button>
<Button
size="compact-sm"
@@ -858,7 +889,7 @@ function EligibleTab({
loading={bulkReceive.isPending}
onClick={() => openTruckReceive([...selected])}
>
Receive Selected
{direction === 'EXPORT' ? 'Receive Selected for Loading' : 'Receive Selected'}
</Button>
</Group>
</Group>
@@ -1000,7 +1031,7 @@ function EligibleTab({
loading={bulkReceive.isPending}
onClick={() => openTruckReceive([r.id])}
>
{canReceive ? 'Receive to Warehouse' : 'Await First Mile'}
{canReceive ? (direction === 'EXPORT' ? 'Receive for Loading' : 'Receive to Warehouse') : 'Await First Mile'}
</Button>
)}
</Table.Td>
@@ -1015,7 +1046,7 @@ function EligibleTab({
<Modal
opened={truckOpen}
onClose={() => setTruckOpen(false)}
title="Receive to Warehouse"
title={direction === 'EXPORT' ? 'Receive for Loading' : 'Receive to Warehouse'}
centered
size="lg"
>
@@ -1175,6 +1206,7 @@ function ExportReceivedTab({ enabled, onChanged }: { enabled: boolean; onChanged
/>
</Table.Th>
<Table.Th>Booking Ref</Table.Th>
<Table.Th>GRN</Table.Th>
<Table.Th>Booking ID</Table.Th>
<Table.Th>Customer ID</Table.Th>
<Table.Th>Customer Name</Table.Th>
@@ -1201,7 +1233,13 @@ function ExportReceivedTab({ enabled, onChanged }: { enabled: boolean; onChanged
/>
</Table.Td>
<Table.Td>
<Text size="sm" fw={600}>{r.bookingReference ?? '—'}</Text>
<Stack gap={2}>
<Text size="sm" fw={600}>{r.bookingReference ?? '—'}</Text>
<GrnDocumentButton inventoryId={r.id} grnNumber={r.grnNumber} />
</Stack>
</Table.Td>
<Table.Td>
<GrnDocumentButton inventoryId={r.id} grnNumber={r.grnNumber} />
</Table.Td>
<Table.Td>
<Text size="xs" c="dimmed">{r.bookingId ? `${r.bookingId.slice(0, 8)}` : '—'}</Text>
@@ -1322,6 +1360,7 @@ function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?:
/>
</Table.Th>
<Table.Th>Booking Ref</Table.Th>
<Table.Th>GRN</Table.Th>
<Table.Th>Booking ID</Table.Th>
<Table.Th>Customer ID</Table.Th>
<Table.Th>Customer Name</Table.Th>
@@ -1344,7 +1383,13 @@ function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?:
/>
</Table.Td>
<Table.Td>
<Text size="sm" fw={600}>{r.bookingReference ?? '—'}</Text>
<Stack gap={2}>
<Text size="sm" fw={600}>{r.bookingReference ?? '—'}</Text>
<GrnDocumentButton inventoryId={r.id} grnNumber={r.grnNumber} />
</Stack>
</Table.Td>
<Table.Td>
<GrnDocumentButton inventoryId={r.id} grnNumber={r.grnNumber} />
</Table.Td>
<Table.Td>
<Text size="xs" c="dimmed">{r.bookingId ? `${r.bookingId.slice(0, 8)}` : '—'}</Text>
@@ -1492,6 +1537,7 @@ function LoadedExportTab({
</Table.Th>
)}
<Table.Th>Booking Ref</Table.Th>
<Table.Th>GRN</Table.Th>
<Table.Th>Booking ID</Table.Th>
<Table.Th>Customer ID</Table.Th>
<Table.Th>Customer Name</Table.Th>
@@ -1516,7 +1562,13 @@ function LoadedExportTab({
</Table.Td>
)}
<Table.Td>
<Text size="sm" fw={600}>{r.bookingReference ?? '—'}</Text>
<Stack gap={2}>
<Text size="sm" fw={600}>{r.bookingReference ?? '—'}</Text>
<GrnDocumentButton inventoryId={r.id} grnNumber={r.grnNumber} />
</Stack>
</Table.Td>
<Table.Td>
<GrnDocumentButton inventoryId={r.id} grnNumber={r.grnNumber} />
</Table.Td>
<Table.Td>
<Text size="xs" c="dimmed">{r.bookingId ? `${r.bookingId.slice(0, 8)}` : '—'}</Text>
@@ -1866,12 +1918,15 @@ function ImportUnloadedQueueTab({ enabled }: { enabled: boolean }) {
bookingId: row.bookingId,
quantity: 1,
weight: Number(row.weight) || 0,
grnNumber: row.grnNumber,
status: row.currentStatus,
arrivedAt: row.arrivalTime,
unloadedAt: row.arrivalTime,
inspectionStatus: row.inspectionStatus,
releaseDate: row.releaseDate,
releaseOrderReference: row.releaseOrderReference,
handoverDocumentReference: row.handoverDocumentReference,
handoverDocumentDate: row.handoverDocumentDate,
deliveredAt: row.deliveredAt,
booking: row.bookingId
? {
@@ -1902,6 +1957,7 @@ function ImportUnloadedQueueTab({ enabled }: { enabled: boolean }) {
try {
const response = await warehouseService.downloadHandoverDocument(row.id);
openPdfBlob(response.data, `handover-${row.bookingReference ?? row.id}.pdf`, pdfWindow);
void qc.invalidateQueries({ queryKey: ['warehouse-inventory'] });
} catch (error) {
pdfWindow?.close();
toast({ variant: 'destructive', title: 'Handover document failed', description: extractErrorMessage(error) });
@@ -1910,6 +1966,20 @@ function ImportUnloadedQueueTab({ enabled }: { enabled: boolean }) {
}
};
const openReleaseDocument = async (row: ImportUnloadedItem) => {
setBusyId(row.id);
const pdfWindow = window.open('', '_blank');
try {
const response = await warehouseService.downloadReleaseDocument(row.id);
openPdfBlob(response.data, `release-${row.bookingReference ?? row.id}.pdf`, pdfWindow);
} catch (error) {
pdfWindow?.close();
toast({ variant: 'destructive', title: 'Exit paper failed', description: extractErrorMessage(error) });
} finally {
setBusyId(null);
}
};
return (
<Stack gap="sm" mt="sm">
<Group justify="space-between">
@@ -1959,6 +2029,7 @@ function ImportUnloadedQueueTab({ enabled }: { enabled: boolean }) {
</Table.Th>
<Table.Th>Booking ID</Table.Th>
<Table.Th>Booking Ref</Table.Th>
<Table.Th>GRN</Table.Th>
<Table.Th>Customer ID</Table.Th>
<Table.Th>Customer Name</Table.Th>
<Table.Th>Arrival Time</Table.Th>
@@ -1987,7 +2058,13 @@ function ImportUnloadedQueueTab({ enabled }: { enabled: boolean }) {
<Text size="xs" c="dimmed">{r.bookingId ? `${r.bookingId.slice(0, 8)}` : '—'}</Text>
</Table.Td>
<Table.Td>
<Text size="sm" fw={600}>{r.bookingReference ?? '—'}</Text>
<Stack gap={2}>
<Text size="sm" fw={600}>{r.bookingReference ?? '—'}</Text>
<GrnDocumentButton inventoryId={r.id} grnNumber={r.grnNumber} />
</Stack>
</Table.Td>
<Table.Td>
<GrnDocumentButton inventoryId={r.id} grnNumber={r.grnNumber} />
</Table.Td>
<Table.Td>
<Text size="xs" c="dimmed">{r.customerId ? `${r.customerId.slice(0, 8)}` : '—'}</Text>
@@ -2049,7 +2126,7 @@ function ImportUnloadedQueueTab({ enabled }: { enabled: boolean }) {
color="yellow"
onClick={() => setReleaseItem(toInventoryItem(r))}
>
Truck Arrival
{r.releaseOrderReference ? 'Truck Leaving' : 'Truck Arrival'}
</Button>
</>
)}
@@ -2064,6 +2141,18 @@ function ImportUnloadedQueueTab({ enabled }: { enabled: boolean }) {
Dispatch
</Button>
)}
{r.currentStatus === 'READY_FOR_PICKUP' && r.releaseDate && (
<Button
size="compact-xs"
variant="light"
color="orange"
leftSection={<FileText size={14} />}
loading={busyId === r.id}
onClick={() => openReleaseDocument(r)}
>
Exit Paper
</Button>
)}
{r.currentStatus === 'READY_FOR_PICKUP' && r.releaseDate && (
<Button
size="compact-xs"
@@ -2082,7 +2171,7 @@ function ImportUnloadedQueueTab({ enabled }: { enabled: boolean }) {
leftSection={<FileText size={14} />}
onClick={() => openHandoverDocument(r)}
>
Handover
{r.handoverDocumentReference ? 'View Handover' : 'Handover'}
</Button>
)}
<Button size="compact-xs" variant="light" color="orange" onClick={() => setInspectId(r.id)}>
@@ -2398,7 +2487,12 @@ function ExportWarehouseTabs({
onChanged?: () => void;
}) {
const [activeTab, setActiveTab] = useState<ExportWarehouseTab>('receive-queue');
const { data: eligibleRows = [] } = useQuery(api.warehouses.eligibleBookings.queryOptions({ enabled }));
const { data: eligibleRows = [] } = useQuery(
api.warehouses.eligibleBookings.queryOptions({
input: { direction: 'EXPORT' },
enabled,
}),
);
const { data: receivedRows = [] } = useQuery(api.warehouses.receivedExport.queryOptions({ enabled }));
const { data: readyRows = [] } = useQuery(api.warehouses.readyToLoadExport.queryOptions({ enabled }));
const { data: loadedRows = [] } = useQuery(api.warehouses.loadedExport.queryOptions({ enabled }));