import type { InventoryItemDetailDto, InventoryReservationInput, InventoryTransactionInput, InventoryTransferInput, WarehouseLocationOptionDto, } from "@mrp/shared/dist/inventory/types.js"; import type { FileAttachmentDto } from "@mrp/shared"; import { permissions } from "@mrp/shared"; import { useEffect, useState } from "react"; import { Link, useParams } from "react-router-dom"; import { useAuth } from "../../auth/AuthProvider"; import { api, ApiError } from "../../lib/api"; import { ConfirmActionDialog } from "../../components/ConfirmActionDialog"; import { emptyInventoryTransactionInput, inventoryThumbnailOwnerType, inventoryTransactionOptions } from "./config"; import { InventoryAttachmentsPanel } from "./InventoryAttachmentsPanel"; import { InventoryStatusBadge } from "./InventoryStatusBadge"; import { InventoryTransactionTypeBadge } from "./InventoryTransactionTypeBadge"; import { InventoryTypeBadge } from "./InventoryTypeBadge"; const emptyTransferInput: InventoryTransferInput = { quantity: 1, fromWarehouseId: "", fromLocationId: "", toWarehouseId: "", toLocationId: "", notes: "", }; const emptyReservationInput: InventoryReservationInput = { quantity: 1, warehouseId: null, locationId: null, notes: "", }; export function InventoryDetailPage() { const { token, user } = useAuth(); const { itemId } = useParams(); const [item, setItem] = useState(null); const [locationOptions, setLocationOptions] = useState([]); const [transactionForm, setTransactionForm] = useState(emptyInventoryTransactionInput); const [transferForm, setTransferForm] = useState(emptyTransferInput); const [reservationForm, setReservationForm] = useState(emptyReservationInput); const [transactionStatus, setTransactionStatus] = useState("Record receipts, issues, and adjustments against this item."); const [transferStatus, setTransferStatus] = useState("Move physical stock between warehouses or locations without manual paired entries."); const [reservationStatus, setReservationStatus] = useState("Reserve stock manually while active work orders reserve component demand automatically."); const [isSavingTransaction, setIsSavingTransaction] = useState(false); const [isSavingTransfer, setIsSavingTransfer] = useState(false); const [isSavingReservation, setIsSavingReservation] = useState(false); const [status, setStatus] = useState("Loading inventory item..."); const [thumbnailAttachment, setThumbnailAttachment] = useState(null); const [thumbnailPreviewUrl, setThumbnailPreviewUrl] = useState(null); const [pendingConfirmation, setPendingConfirmation] = useState< | { kind: "transaction" | "transfer" | "reservation"; title: string; description: string; impact: string; recovery: string; confirmLabel: string; confirmationLabel?: string; confirmationValue?: string; } | null >(null); const canManage = user?.permissions.includes(permissions.inventoryWrite) ?? false; const canReadFiles = user?.permissions.includes(permissions.filesRead) ?? false; function replaceThumbnailPreview(nextUrl: string | null) { setThumbnailPreviewUrl((current) => { if (current) { window.URL.revokeObjectURL(current); } return nextUrl; }); } useEffect(() => { if (!token || !itemId) { return; } api .getInventoryItem(token, itemId) .then((nextItem) => { setItem(nextItem); setStatus("Inventory item loaded."); }) .catch((error: unknown) => { const message = error instanceof ApiError ? error.message : "Unable to load inventory item."; setStatus(message); }); api .getWarehouseLocationOptions(token) .then((options) => { setLocationOptions(options); const firstOption = options[0]; if (!firstOption) { return; } setTransactionForm((current) => ({ ...current, warehouseId: current.warehouseId || firstOption.warehouseId, locationId: current.locationId || firstOption.locationId, })); setTransferForm((current) => ({ ...current, fromWarehouseId: current.fromWarehouseId || firstOption.warehouseId, fromLocationId: current.fromLocationId || firstOption.locationId, toWarehouseId: current.toWarehouseId || firstOption.warehouseId, toLocationId: current.toLocationId || firstOption.locationId, })); }) .catch(() => setLocationOptions([])); }, [itemId, token]); useEffect(() => { return () => { if (thumbnailPreviewUrl) { window.URL.revokeObjectURL(thumbnailPreviewUrl); } }; }, [thumbnailPreviewUrl]); useEffect(() => { if (!token || !itemId || !canReadFiles) { setThumbnailAttachment(null); replaceThumbnailPreview(null); return; } let cancelled = false; const activeToken: string = token; const activeItemId: string = itemId; async function loadThumbnail() { const attachments = await api.getAttachments(activeToken, inventoryThumbnailOwnerType, activeItemId); const latestAttachment = attachments[0] ?? null; if (!latestAttachment) { if (!cancelled) { setThumbnailAttachment(null); replaceThumbnailPreview(null); } return; } const blob = await api.getFileContentBlob(activeToken, latestAttachment.id); if (!cancelled) { setThumbnailAttachment(latestAttachment); replaceThumbnailPreview(window.URL.createObjectURL(blob)); } } void loadThumbnail().catch(() => { if (!cancelled) { setThumbnailAttachment(null); replaceThumbnailPreview(null); } }); return () => { cancelled = true; }; }, [canReadFiles, itemId, token]); function updateTransactionField(key: Key, value: InventoryTransactionInput[Key]) { setTransactionForm((current) => ({ ...current, [key]: value })); } function updateTransferField(key: Key, value: InventoryTransferInput[Key]) { setTransferForm((current) => ({ ...current, [key]: value })); } async function submitTransaction() { if (!token || !itemId) { return; } setIsSavingTransaction(true); setTransactionStatus("Saving stock transaction..."); try { const nextItem = await api.createInventoryTransaction(token, itemId, transactionForm); setItem(nextItem); setTransactionStatus("Stock transaction recorded. If this was posted in error, create an offsetting stock entry and verify the result in Recent Movements."); setTransactionForm((current) => ({ ...emptyInventoryTransactionInput, transactionType: current.transactionType, warehouseId: current.warehouseId, locationId: current.locationId, })); } catch (error: unknown) { const message = error instanceof ApiError ? error.message : "Unable to save stock transaction."; setTransactionStatus(message); } finally { setIsSavingTransaction(false); } } async function submitTransfer() { if (!token || !itemId) { return; } setIsSavingTransfer(true); setTransferStatus("Saving transfer..."); try { const nextItem = await api.createInventoryTransfer(token, itemId, transferForm); setItem(nextItem); setTransferStatus("Transfer recorded. Review stock balances on both locations and post a return transfer if this movement was entered incorrectly."); } catch (error: unknown) { const message = error instanceof ApiError ? error.message : "Unable to save transfer."; setTransferStatus(message); } finally { setIsSavingTransfer(false); } } async function submitReservation() { if (!token || !itemId) { return; } setIsSavingReservation(true); setReservationStatus("Saving reservation..."); try { const nextItem = await api.createInventoryReservation(token, itemId, reservationForm); setItem(nextItem); setReservationStatus("Reservation recorded. Verify available stock and add a compensating reservation change if this demand hold was entered incorrectly."); setReservationForm((current) => ({ ...current, quantity: 1, notes: "" })); } catch (error: unknown) { const message = error instanceof ApiError ? error.message : "Unable to save reservation."; setReservationStatus(message); } finally { setIsSavingReservation(false); } } function handleTransactionSubmit(event: React.FormEvent) { event.preventDefault(); if (!item) { return; } const transactionLabel = inventoryTransactionOptions.find((option) => option.value === transactionForm.transactionType)?.label ?? "transaction"; setPendingConfirmation({ kind: "transaction", title: `Post ${transactionLabel.toLowerCase()}`, description: `Post a ${transactionLabel.toLowerCase()} of ${transactionForm.quantity} units for ${item.sku} at the selected stock location.`, impact: transactionForm.transactionType === "ISSUE" || transactionForm.transactionType === "ADJUSTMENT_OUT" ? "This reduces available inventory immediately and affects downstream shortage and readiness calculations." : "This updates the stock ledger immediately and becomes part of the item transaction history.", recovery: "If this is incorrect, post an explicit offsetting transaction instead of editing history.", confirmLabel: `Post ${transactionLabel.toLowerCase()}`, confirmationLabel: transactionForm.transactionType === "ISSUE" || transactionForm.transactionType === "ADJUSTMENT_OUT" ? "Type item SKU to confirm:" : undefined, confirmationValue: transactionForm.transactionType === "ISSUE" || transactionForm.transactionType === "ADJUSTMENT_OUT" ? item.sku : undefined, }); } function handleTransferSubmit(event: React.FormEvent) { event.preventDefault(); if (!item) { return; } setPendingConfirmation({ kind: "transfer", title: "Post inventory transfer", description: `Move ${transferForm.quantity} units of ${item.sku} between the selected source and destination locations.`, impact: "This creates paired stock movement entries and changes both source and destination availability immediately.", recovery: "If the move was entered incorrectly, post a reversing transfer back to the original location.", confirmLabel: "Post transfer", }); } function handleReservationSubmit(event: React.FormEvent) { event.preventDefault(); if (!item) { return; } setPendingConfirmation({ kind: "reservation", title: "Create manual reservation", description: `Reserve ${reservationForm.quantity} units of ${item.sku}${reservationForm.locationId ? " at the selected location" : ""}.`, impact: "This reduces available quantity used by planning, purchasing, manufacturing, and readiness views.", recovery: "Add the correcting reservation entry if this hold should be reduced or removed.", confirmLabel: "Create reservation", }); } if (!item) { return
{status}
; } return (

Inventory Detail

{item.sku}

{item.name}

Last updated {new Date(item.updatedAt).toLocaleString()}.

Back to items {canManage ? ( Edit item ) : null}

On Hand

{item.onHandQuantity}

Reserved

{item.reservedQuantity}

Available

{item.availableQuantity}

Stock Locations

{item.stockBalances.length}

Transactions

{item.recentTransactions.length}

Transfers

{item.transfers.length}

Reservations

{item.reservations.length}

Item Definition

Description
{item.description || "No description provided."}
Unit of measure
{item.unitOfMeasure}
Default cost
{item.defaultCost == null ? "Not set" : `$${item.defaultCost.toFixed(2)}`}
Default price
{item.defaultPrice == null ? "Not set" : `$${item.defaultPrice.toFixed(2)}`}
Preferred vendor
{item.preferredVendorName ?? "Not set"}
Flags
{item.isSellable ? "Sellable" : "Not sellable"} / {item.isPurchasable ? "Purchasable" : "Not purchasable"}

Thumbnail

{thumbnailPreviewUrl ? ( {`${item.sku} ) : (
No thumbnail image has been attached to this item.
)}
{thumbnailAttachment ? `Current file: ${thumbnailAttachment.originalName}` : "Add or replace the thumbnail from the item edit page."}

Stock By Location

{item.stockBalances.length === 0 ? (

No stock or reservation balances have been posted for this item yet.

) : (
{item.stockBalances.map((balance) => (
{balance.warehouseCode} / {balance.locationCode}
{balance.warehouseName} / {balance.locationName}
{balance.quantityOnHand} on hand
{balance.quantityReserved} reserved / {balance.quantityAvailable} available
))}
)}
{canManage ? (

Stock Transactions