import { permissions } from "@mrp/shared"; import type { WarehouseLocationOptionDto } from "@mrp/shared/dist/inventory/types.js"; import type { ShipmentDetailDto, ShipmentPickInput, ShipmentStatus, ShipmentSummaryDto } from "@mrp/shared/dist/shipping/types.js"; import { useEffect, useState } from "react"; import { Link, useParams } from "react-router-dom"; import { useAuth } from "../../auth/AuthProvider"; import { ConfirmActionDialog } from "../../components/ConfirmActionDialog"; import { FileAttachmentsPanel } from "../../components/FileAttachmentsPanel"; import { api, ApiError } from "../../lib/api"; import { shipmentStatusOptions } from "./config"; import { ShipmentStatusBadge } from "./ShipmentStatusBadge"; function buildInitialPickForm( shipment: ShipmentDetailDto | null, locationOptions: WarehouseLocationOptionDto[], current?: ShipmentPickInput ): ShipmentPickInput { const remainingLine = shipment?.lines.find((line) => line.remainingQuantity > 0) ?? shipment?.lines[0] ?? null; const fallbackLocation = locationOptions.find((location) => location.warehouseId === current?.warehouseId) ?? locationOptions[0] ?? null; return { salesOrderLineId: current?.salesOrderLineId && shipment?.lines.some((line) => line.salesOrderLineId === current.salesOrderLineId) ? current.salesOrderLineId : remainingLine?.salesOrderLineId ?? "", warehouseId: current?.warehouseId || fallbackLocation?.warehouseId || "", locationId: current?.locationId || fallbackLocation?.locationId || "", quantity: current?.quantity ?? Math.min(remainingLine?.remainingQuantity ?? 1, 1), notes: current?.notes ?? "", }; } function formatDateTime(value: string) { return new Date(value).toLocaleString(); } export function ShipmentDetailPage() { const { token, user } = useAuth(); const { shipmentId } = useParams(); const [shipment, setShipment] = useState(null); const [relatedShipments, setRelatedShipments] = useState([]); const [locationOptions, setLocationOptions] = useState([]); const [pickForm, setPickForm] = useState({ salesOrderLineId: "", warehouseId: "", locationId: "", quantity: 1, notes: "", }); const [status, setStatus] = useState("Loading shipment..."); const [isUpdatingStatus, setIsUpdatingStatus] = useState(false); const [isPostingPick, setIsPostingPick] = useState(false); const [activeDocumentAction, setActiveDocumentAction] = useState<"packing-slip" | "label" | "bol" | null>(null); const [pendingConfirmation, setPendingConfirmation] = useState< | { title: string; description: string; impact: string; recovery: string; confirmLabel: string; confirmationLabel?: string; confirmationValue?: string; nextStatus: ShipmentStatus; } | null >(null); const canManage = user?.permissions.includes(permissions.shippingWrite) ?? false; async function loadShipmentDetail(activeToken: string, activeShipmentId: string) { const [nextShipment, nextLocationOptions] = await Promise.all([ api.getShipment(activeToken, activeShipmentId), canManage ? api.getWarehouseLocationOptions(activeToken) : Promise.resolve([]), ]); const shipments = await api.getShipments(activeToken, { salesOrderId: nextShipment.salesOrderId }); setShipment(nextShipment); setLocationOptions(nextLocationOptions); setRelatedShipments(shipments.filter((candidate) => candidate.id !== activeShipmentId)); setPickForm((current) => buildInitialPickForm(nextShipment, nextLocationOptions, current)); setStatus("Shipment loaded."); } useEffect(() => { if (!token || !shipmentId) { return; } loadShipmentDetail(token, shipmentId).catch((error: unknown) => { const message = error instanceof ApiError ? error.message : "Unable to load shipment."; setStatus(message); }); }, [shipmentId, token, canManage]); const selectedLine = shipment?.lines.find((line) => line.salesOrderLineId === pickForm.salesOrderLineId) ?? null; const availableLocations = locationOptions.filter((location) => !pickForm.warehouseId || location.warehouseId === pickForm.warehouseId); const warehouseOptions = Array.from( new Map(locationOptions.map((location) => [location.warehouseId, { id: location.warehouseId, label: `${location.warehouseCode} ยท ${location.warehouseName}` }])).values() ); const totalOrderedQuantity = shipment?.lines.reduce((sum, line) => sum + line.orderedQuantity, 0) ?? 0; const totalPickedQuantity = shipment?.lines.reduce((sum, line) => sum + line.pickedQuantity, 0) ?? 0; async function applyStatusChange(nextStatus: ShipmentStatus) { if (!token || !shipment) { return; } setIsUpdatingStatus(true); setStatus("Updating shipment status..."); try { const nextShipment = await api.updateShipmentStatus(token, shipment.id, nextStatus); setShipment(nextShipment); setPickForm((current) => buildInitialPickForm(nextShipment, locationOptions, current)); setStatus("Shipment status updated. Verify carrier paperwork, inventory issue progress, and sales-order expectations."); } catch (error: unknown) { const message = error instanceof ApiError ? error.message : "Unable to update shipment status."; setStatus(message); } finally { setIsUpdatingStatus(false); } } function handleStatusChange(nextStatus: ShipmentStatus) { if (!shipment) { return; } const label = shipmentStatusOptions.find((option) => option.value === nextStatus)?.label ?? nextStatus; setPendingConfirmation({ title: `Set shipment to ${label}`, description: `Update shipment ${shipment.shipmentNumber} from ${shipment.status} to ${nextStatus}.`, impact: nextStatus === "DELIVERED" ? "This marks delivery complete and can affect customer communication, project delivery status, and shipment closeout review." : nextStatus === "SHIPPED" ? "This marks the shipment as outbound and should only happen after stock has been picked and packed from real inventory locations." : "This changes the logistics state used by related shipping and sales workflows.", recovery: "If the status is wrong, return the shipment to the correct state and confirm pick quantities still match the physical shipment.", confirmLabel: `Set ${label}`, confirmationLabel: nextStatus === "DELIVERED" ? "Type shipment number to confirm:" : undefined, confirmationValue: nextStatus === "DELIVERED" ? shipment.shipmentNumber : undefined, nextStatus, }); } async function handleOpenDocument(kind: "packing-slip" | "label" | "bol") { if (!token || !shipment) { return; } setActiveDocumentAction(kind); setStatus( kind === "packing-slip" ? "Rendering packing slip PDF..." : kind === "label" ? "Rendering shipping label PDF..." : "Rendering bill of lading PDF..." ); try { const blob = kind === "packing-slip" ? await api.getShipmentPackingSlipPdf(token, shipment.id) : kind === "label" ? await api.getShipmentLabelPdf(token, shipment.id) : await api.getShipmentBillOfLadingPdf(token, shipment.id); const objectUrl = URL.createObjectURL(blob); window.open(objectUrl, "_blank", "noopener,noreferrer"); window.setTimeout(() => URL.revokeObjectURL(objectUrl), 60_000); setStatus( kind === "packing-slip" ? "Packing slip PDF rendered." : kind === "label" ? "Shipping label PDF rendered." : "Bill of lading PDF rendered." ); } catch (error: unknown) { const message = error instanceof ApiError ? error.message : kind === "packing-slip" ? "Unable to render packing slip PDF." : kind === "label" ? "Unable to render shipping label PDF." : "Unable to render bill of lading PDF."; setStatus(message); } finally { setActiveDocumentAction(null); } } async function handlePostPick() { if (!token || !shipment || !selectedLine) { return; } setIsPostingPick(true); setStatus("Posting shipment pick and issuing stock..."); try { const nextShipment = await api.postShipmentPick(token, shipment.id, { ...pickForm, quantity: Number(pickForm.quantity), }); setShipment(nextShipment); setPickForm(buildInitialPickForm(nextShipment, locationOptions)); setStatus("Shipment pick posted. Inventory was issued from the selected stock location."); } catch (error: unknown) { const message = error instanceof ApiError ? error.message : "Unable to post shipment pick."; setStatus(message); } finally { setIsPostingPick(false); } } if (!shipment) { return
{status}
; } return (

SHIPMENT

{shipment.shipmentNumber}

{shipment.salesOrderNumber} / {shipment.customerName}

{status}
Back to shipments Open sales order {canManage ? ( Edit shipment ) : null}
{canManage ? (

QUICK ACTIONS

{shipmentStatusOptions.map((option) => ( ))}
) : null}

Carrier

{shipment.carrier || "Not set"}

Ordered Units

{totalOrderedQuantity}

Picked Units

{totalPickedQuantity}

Packages

{shipment.packageCount}

SHIPMENT LINES

{shipment.lines.map((line) => ( ))}
Item Description Ordered Picked Remaining
{line.itemSku}
{line.itemName}
{line.description} {line.orderedQuantity} {line.unitOfMeasure} {line.pickedQuantity} {line.unitOfMeasure} 0 ? "bg-amber-500/15 text-amber-700 dark:text-amber-300" : "bg-emerald-500/15 text-emerald-700 dark:text-emerald-300"}`}> {line.remainingQuantity} {line.unitOfMeasure}

Timing

Ship Date
{shipment.shipDate ? new Date(shipment.shipDate).toLocaleDateString() : "Not set"}
Created
{formatDateTime(shipment.createdAt)}
Updated
{formatDateTime(shipment.updatedAt)}
Tracking
{shipment.trackingNumber || "Not set"}
{canManage ? (

PICK AND ISSUE FROM STOCK

Select the sales-order line, source location, and quantity you are physically picking.
{selectedLine ? `Remaining on selected line: ${selectedLine.remainingQuantity} ${selectedLine.unitOfMeasure}.` : "Select a shipment line to issue inventory."}
) : null}

PICK HISTORY

{shipment.picks.length === 0 ? (
No shipment picks have been posted yet.
) : (
{shipment.picks.map((pick) => (
{pick.itemSku} / {pick.itemName}
{pick.quantity} issued from {pick.warehouseCode} / {pick.locationCode}
{pick.createdByName}
{formatDateTime(pick.createdAt)}
{pick.notes || "No pick notes."}
))}
)}

Shipment Notes

{shipment.notes || "No notes recorded for this shipment."}

RELATED SHIPMENTS

{canManage ? ( Add another shipment ) : null}
{relatedShipments.length === 0 ? (
No additional shipments exist for this sales order.
) : (
{relatedShipments.map((related) => (
{related.shipmentNumber}
{related.carrier || "Carrier not set"} / {related.trackingNumber || "No tracking"}
))}
)}
{ if (!isUpdatingStatus) { setPendingConfirmation(null); } }} onConfirm={async () => { if (!pendingConfirmation) { return; } await applyStatusChange(pendingConfirmation.nextStatus); setPendingConfirmation(null); }} />
); }