2026-03-15 14:00:12 -05:00
import type {
InventoryItemDetailDto ,
InventoryReservationInput ,
InventoryTransactionInput ,
InventoryTransferInput ,
WarehouseLocationOptionDto ,
} from "@mrp/shared/dist/inventory/types.js" ;
2026-03-15 22:59:16 -05:00
import type { FileAttachmentDto } from "@mrp/shared" ;
2026-03-14 21:10:35 -05:00
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" ;
2026-03-15 18:59:37 -05:00
import { ConfirmActionDialog } from "../../components/ConfirmActionDialog" ;
2026-03-15 22:59:16 -05:00
import { emptyInventoryTransactionInput , inventoryThumbnailOwnerType , inventoryTransactionOptions } from "./config" ;
2026-03-14 23:03:17 -05:00
import { InventoryAttachmentsPanel } from "./InventoryAttachmentsPanel" ;
2026-03-14 21:10:35 -05:00
import { InventoryStatusBadge } from "./InventoryStatusBadge" ;
2026-03-14 22:37:09 -05:00
import { InventoryTransactionTypeBadge } from "./InventoryTransactionTypeBadge" ;
2026-03-14 21:10:35 -05:00
import { InventoryTypeBadge } from "./InventoryTypeBadge" ;
2026-03-15 14:00:12 -05:00
const emptyTransferInput : InventoryTransferInput = {
quantity : 1 ,
fromWarehouseId : "" ,
fromLocationId : "" ,
toWarehouseId : "" ,
toLocationId : "" ,
notes : "" ,
} ;
const emptyReservationInput : InventoryReservationInput = {
quantity : 1 ,
warehouseId : null ,
locationId : null ,
notes : "" ,
} ;
2026-03-14 21:10:35 -05:00
export function InventoryDetailPage() {
const { token , user } = useAuth ( ) ;
const { itemId } = useParams ( ) ;
const [ item , setItem ] = useState < InventoryItemDetailDto | null > ( null ) ;
2026-03-14 22:37:09 -05:00
const [ locationOptions , setLocationOptions ] = useState < WarehouseLocationOptionDto [ ] > ( [ ] ) ;
const [ transactionForm , setTransactionForm ] = useState < InventoryTransactionInput > ( emptyInventoryTransactionInput ) ;
2026-03-15 14:00:12 -05:00
const [ transferForm , setTransferForm ] = useState < InventoryTransferInput > ( emptyTransferInput ) ;
const [ reservationForm , setReservationForm ] = useState < InventoryReservationInput > ( emptyReservationInput ) ;
2026-03-14 22:37:09 -05:00
const [ transactionStatus , setTransactionStatus ] = useState ( "Record receipts, issues, and adjustments against this item." ) ;
2026-03-15 14:00:12 -05:00
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." ) ;
2026-03-14 22:37:09 -05:00
const [ isSavingTransaction , setIsSavingTransaction ] = useState ( false ) ;
2026-03-15 14:00:12 -05:00
const [ isSavingTransfer , setIsSavingTransfer ] = useState ( false ) ;
const [ isSavingReservation , setIsSavingReservation ] = useState ( false ) ;
2026-03-14 21:10:35 -05:00
const [ status , setStatus ] = useState ( "Loading inventory item..." ) ;
2026-03-15 22:59:16 -05:00
const [ thumbnailAttachment , setThumbnailAttachment ] = useState < FileAttachmentDto | null > ( null ) ;
const [ thumbnailPreviewUrl , setThumbnailPreviewUrl ] = useState < string | null > ( null ) ;
2026-03-15 18:59:37 -05:00
const [ pendingConfirmation , setPendingConfirmation ] = useState <
| {
kind : "transaction" | "transfer" | "reservation" ;
title : string ;
description : string ;
impact : string ;
recovery : string ;
confirmLabel : string ;
confirmationLabel? : string ;
confirmationValue? : string ;
}
| null
> ( null ) ;
2026-03-14 21:10:35 -05:00
const canManage = user ? . permissions . includes ( permissions . inventoryWrite ) ? ? false ;
2026-03-15 22:59:16 -05:00
const canReadFiles = user ? . permissions . includes ( permissions . filesRead ) ? ? false ;
function replaceThumbnailPreview ( nextUrl : string | null ) {
setThumbnailPreviewUrl ( ( current ) = > {
if ( current ) {
window . URL . revokeObjectURL ( current ) ;
}
return nextUrl ;
} ) ;
}
2026-03-14 21:10:35 -05:00
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 ) ;
} ) ;
2026-03-14 22:37:09 -05:00
api
. getWarehouseLocationOptions ( token )
. then ( ( options ) = > {
setLocationOptions ( options ) ;
2026-03-15 14:00:12 -05:00
const firstOption = options [ 0 ] ;
if ( ! firstOption ) {
return ;
}
2026-03-14 22:37:09 -05:00
2026-03-15 14:00:12 -05:00
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 ,
} ) ) ;
2026-03-14 22:37:09 -05:00
} )
. catch ( ( ) = > setLocationOptions ( [ ] ) ) ;
2026-03-14 21:10:35 -05:00
} , [ itemId , token ] ) ;
2026-03-15 22:59:16 -05:00
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 ] ) ;
2026-03-14 22:37:09 -05:00
function updateTransactionField < Key extends keyof InventoryTransactionInput > ( key : Key , value : InventoryTransactionInput [ Key ] ) {
setTransactionForm ( ( current ) = > ( { . . . current , [ key ] : value } ) ) ;
}
2026-03-15 14:00:12 -05:00
function updateTransferField < Key extends keyof InventoryTransferInput > ( key : Key , value : InventoryTransferInput [ Key ] ) {
setTransferForm ( ( current ) = > ( { . . . current , [ key ] : value } ) ) ;
}
2026-03-15 18:59:37 -05:00
async function submitTransaction() {
2026-03-14 22:37:09 -05:00
if ( ! token || ! itemId ) {
return ;
}
setIsSavingTransaction ( true ) ;
setTransactionStatus ( "Saving stock transaction..." ) ;
try {
const nextItem = await api . createInventoryTransaction ( token , itemId , transactionForm ) ;
setItem ( nextItem ) ;
2026-03-15 18:59:37 -05:00
setTransactionStatus ( "Stock transaction recorded. If this was posted in error, create an offsetting stock entry and verify the result in Recent Movements." ) ;
2026-03-14 22:37:09 -05:00
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 ) ;
}
}
2026-03-15 18:59:37 -05:00
async function submitTransfer() {
2026-03-15 14:00:12 -05:00
if ( ! token || ! itemId ) {
return ;
}
setIsSavingTransfer ( true ) ;
setTransferStatus ( "Saving transfer..." ) ;
try {
const nextItem = await api . createInventoryTransfer ( token , itemId , transferForm ) ;
setItem ( nextItem ) ;
2026-03-15 18:59:37 -05:00
setTransferStatus ( "Transfer recorded. Review stock balances on both locations and post a return transfer if this movement was entered incorrectly." ) ;
2026-03-15 14:00:12 -05:00
} catch ( error : unknown ) {
const message = error instanceof ApiError ? error . message : "Unable to save transfer." ;
setTransferStatus ( message ) ;
} finally {
setIsSavingTransfer ( false ) ;
}
}
2026-03-15 18:59:37 -05:00
async function submitReservation() {
2026-03-15 14:00:12 -05:00
if ( ! token || ! itemId ) {
return ;
}
setIsSavingReservation ( true ) ;
setReservationStatus ( "Saving reservation..." ) ;
try {
const nextItem = await api . createInventoryReservation ( token , itemId , reservationForm ) ;
setItem ( nextItem ) ;
2026-03-15 18:59:37 -05:00
setReservationStatus ( "Reservation recorded. Verify available stock and add a compensating reservation change if this demand hold was entered incorrectly." ) ;
2026-03-15 14:00:12 -05:00
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 ) ;
}
}
2026-03-15 18:59:37 -05:00
function handleTransactionSubmit ( event : React.FormEvent < HTMLFormElement > ) {
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 < HTMLFormElement > ) {
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 < HTMLFormElement > ) {
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" ,
} ) ;
}
2026-03-14 21:10:35 -05:00
if ( ! item ) {
2026-03-15 20:07:48 -05:00
return < div className = "rounded-[20px] border border-line/70 bg-surface/90 p-4 text-sm text-muted shadow-panel" > { status } < / div > ;
2026-03-14 21:10:35 -05:00
}
return (
< section className = "space-y-4" >
2026-03-15 20:07:48 -05:00
< div className = "rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5" >
2026-03-14 22:21:31 -05:00
< div className = "flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between" >
2026-03-14 21:10:35 -05:00
< div >
< p className = "text-xs font-semibold uppercase tracking-[0.24em] text-muted" > Inventory Detail < / p >
2026-03-14 22:21:31 -05:00
< h3 className = "mt-2 text-xl font-bold text-text" > { item . sku } < / h3 >
< p className = "mt-1 text-sm text-text" > { item . name } < / p >
2026-03-14 21:10:35 -05:00
< div className = "mt-4 flex flex-wrap gap-3" >
< InventoryTypeBadge type = { item . type } / >
< InventoryStatusBadge status = { item . status } / >
< / div >
< p className = "mt-3 text-sm text-muted" > Last updated { new Date ( item . updatedAt ) . toLocaleString ( ) } . < / p >
< / div >
< div className = "flex flex-wrap gap-3" >
2026-03-15 14:00:12 -05:00
< Link to = "/inventory/items" className = "inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text" >
2026-03-14 21:10:35 -05:00
Back to items
< / Link >
{ canManage ? (
2026-03-14 22:03:51 -05:00
< Link to = { ` /inventory/items/ ${ item . id } /edit ` } className = "inline-flex items-center justify-center rounded-2xl bg-brand px-3 py-2 text-sm font-semibold text-white" >
2026-03-14 21:10:35 -05:00
Edit item
< / Link >
) : null }
< / div >
< / div >
< / div >
2026-03-15 14:00:12 -05:00
< section className = "grid gap-3 xl:grid-cols-7" >
2026-03-15 20:07:48 -05:00
< article className = "rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel" >
2026-03-14 22:37:09 -05:00
< p className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted" > On Hand < / p >
< div className = "mt-2 text-base font-bold text-text" > { item . onHandQuantity } < / div >
< / article >
2026-03-15 20:07:48 -05:00
< article className = "rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel" >
2026-03-15 14:00:12 -05:00
< p className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted" > Reserved < / p >
< div className = "mt-2 text-base font-bold text-text" > { item . reservedQuantity } < / div >
< / article >
2026-03-15 20:07:48 -05:00
< article className = "rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel" >
2026-03-15 14:00:12 -05:00
< p className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted" > Available < / p >
< div className = "mt-2 text-base font-bold text-text" > { item . availableQuantity } < / div >
< / article >
2026-03-15 20:07:48 -05:00
< article className = "rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel" >
2026-03-14 22:37:09 -05:00
< p className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted" > Stock Locations < / p >
< div className = "mt-2 text-base font-bold text-text" > { item . stockBalances . length } < / div >
< / article >
2026-03-15 20:07:48 -05:00
< article className = "rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel" >
2026-03-14 22:37:09 -05:00
< p className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted" > Transactions < / p >
< div className = "mt-2 text-base font-bold text-text" > { item . recentTransactions . length } < / div >
< / article >
2026-03-15 20:07:48 -05:00
< article className = "rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel" >
2026-03-15 14:00:12 -05:00
< p className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted" > Transfers < / p >
< div className = "mt-2 text-base font-bold text-text" > { item . transfers . length } < / div >
2026-03-14 22:37:09 -05:00
< / article >
2026-03-15 20:07:48 -05:00
< article className = "rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel" >
2026-03-15 14:00:12 -05:00
< p className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted" > Reservations < / p >
< div className = "mt-2 text-base font-bold text-text" > { item . reservations . length } < / div >
2026-03-15 12:11:46 -05:00
< / article >
2026-03-14 22:37:09 -05:00
< / section >
2026-03-15 14:00:12 -05:00
2026-03-14 22:21:31 -05:00
< div className = "grid gap-3 xl:grid-cols-[minmax(0,1.05fr)_minmax(340px,0.95fr)]" >
2026-03-15 20:07:48 -05:00
< article className = "rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5" >
2026-03-14 21:10:35 -05:00
< p className = "text-xs font-semibold uppercase tracking-[0.24em] text-muted" > Item Definition < / p >
2026-03-14 22:21:31 -05:00
< dl className = "mt-5 grid gap-3 xl:grid-cols-2" >
2026-03-14 21:10:35 -05:00
< div >
< dt className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted" > Description < / dt >
2026-03-14 22:21:31 -05:00
< dd className = "mt-1 text-sm leading-6 text-text" > { item . description || "No description provided." } < / dd >
2026-03-14 21:10:35 -05:00
< / div >
< div >
< dt className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted" > Unit of measure < / dt >
< dd className = "mt-2 text-sm text-text" > { item . unitOfMeasure } < / dd >
< / div >
< div >
< dt className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted" > Default cost < / dt >
< dd className = "mt-2 text-sm text-text" > { item . defaultCost == null ? "Not set" : ` $ ${ item . defaultCost . toFixed ( 2 ) } ` } < / dd >
< / div >
2026-03-14 23:23:43 -05:00
< div >
< dt className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted" > Default price < / dt >
< dd className = "mt-2 text-sm text-text" > { item . defaultPrice == null ? "Not set" : ` $ ${ item . defaultPrice . toFixed ( 2 ) } ` } < / dd >
< / div >
2026-03-15 16:40:25 -05:00
< div >
< dt className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted" > Preferred vendor < / dt >
< dd className = "mt-2 text-sm text-text" > { item . preferredVendorName ? ? "Not set" } < / dd >
< / div >
2026-03-14 21:10:35 -05:00
< div >
< dt className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted" > Flags < / dt >
< dd className = "mt-2 text-sm text-text" >
{ item . isSellable ? "Sellable" : "Not sellable" } / { item . isPurchasable ? "Purchasable" : "Not purchasable" }
< / dd >
< / div >
< / dl >
< / article >
2026-03-15 22:59:16 -05:00
< article className = "rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5" >
< p className = "text-xs font-semibold uppercase tracking-[0.24em] text-muted" > Thumbnail < / p >
< div className = "mt-4 overflow-hidden rounded-[18px] border border-line/70 bg-page/70" >
{ thumbnailPreviewUrl ? (
< img src = { thumbnailPreviewUrl } alt = { ` ${ item . sku } thumbnail ` } className = "aspect-square w-full object-cover" / >
) : (
< div className = "flex aspect-square items-center justify-center px-4 text-center text-sm text-muted" >
No thumbnail image has been attached to this item .
< / div >
) }
< / div >
< div className = "mt-3 text-xs text-muted" >
{ thumbnailAttachment ? ` Current file: ${ thumbnailAttachment . originalName } ` : "Add or replace the thumbnail from the item edit page." }
< / div >
< / article >
< / div >
< div className = "grid gap-3 xl:grid-cols-[minmax(0,1.05fr)_minmax(340px,0.95fr)]" >
2026-03-15 20:07:48 -05:00
< article className = "rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5" >
2026-03-15 14:00:12 -05:00
< p className = "text-xs font-semibold uppercase tracking-[0.24em] text-muted" > Stock By Location < / p >
{ item . stockBalances . length === 0 ? (
< p className = "mt-4 text-sm text-muted" > No stock or reservation balances have been posted for this item yet . < / p >
) : (
< div className = "mt-4 space-y-2" >
{ item . stockBalances . map ( ( balance ) = > (
< div key = { ` ${ balance . warehouseId } - ${ balance . locationId } ` } className = "flex items-center justify-between rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm" >
< div className = "min-w-0" >
< div className = "font-semibold text-text" >
{ balance . warehouseCode } / { balance . locationCode }
< / div >
< div className = "text-xs text-muted" >
{ balance . warehouseName } / { balance . locationName }
2026-03-14 22:37:09 -05:00
< / div >
< / div >
2026-03-15 14:00:12 -05:00
< div className = "text-right" >
< div className = "font-semibold text-text" > { balance . quantityOnHand } on hand < / div >
< div className = "text-xs text-muted" > { balance . quantityReserved } reserved / { balance . quantityAvailable } available < / div >
< / div >
< / div >
) ) }
2026-03-15 12:11:46 -05:00
< / div >
) }
2026-03-15 14:00:12 -05:00
< / article >
< / div >
< section className = "grid gap-3 xl:grid-cols-2" >
2026-03-14 22:37:09 -05:00
{ canManage ? (
2026-03-15 20:07:48 -05:00
< form className = "rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5" onSubmit = { handleTransactionSubmit } >
2026-03-14 22:37:09 -05:00
< p className = "text-xs font-semibold uppercase tracking-[0.24em] text-muted" > Stock Transactions < / p >
2026-03-15 14:00:12 -05:00
< div className = "mt-5 grid gap-3" >
2026-03-14 22:37:09 -05:00
< div className = "grid gap-3 xl:grid-cols-2" >
< label className = "block" >
< span className = "mb-2 block text-sm font-semibold text-text" > Transaction type < / span >
2026-03-15 14:00:12 -05:00
< select value = { transactionForm . transactionType } onChange = { ( event ) = > updateTransactionField ( "transactionType" , event . target . value as InventoryTransactionInput [ "transactionType" ] ) } className = "w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand" >
2026-03-14 22:37:09 -05:00
{ inventoryTransactionOptions . map ( ( option ) = > (
< option key = { option . value } value = { option . value } >
{ option . label }
< / option >
) ) }
< / select >
< / label >
< label className = "block" >
< span className = "mb-2 block text-sm font-semibold text-text" > Quantity < / span >
2026-03-15 14:00:12 -05:00
< input type = "number" min = { 1 } step = { 1 } value = { transactionForm . quantity } onChange = { ( event ) = > updateTransactionField ( "quantity" , Number . parseInt ( event . target . value , 10 ) || 0 ) } className = "w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand" / >
2026-03-14 22:37:09 -05:00
< / label >
< / div >
< label className = "block" >
< span className = "mb-2 block text-sm font-semibold text-text" > Stock location < / span >
2026-03-15 14:00:12 -05:00
< select value = { transactionForm . locationId } onChange = { ( event ) = > {
const nextLocation = locationOptions . find ( ( option ) = > option . locationId === event . target . value ) ;
updateTransactionField ( "locationId" , event . target . value ) ;
if ( nextLocation ) {
updateTransactionField ( "warehouseId" , nextLocation . warehouseId ) ;
}
} } className = "w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand" >
2026-03-14 22:37:09 -05:00
{ locationOptions . map ( ( option ) = > (
< option key = { option . locationId } value = { option . locationId } >
{ option . warehouseCode } / { option . locationCode }
< / option >
) ) }
< / select >
< / label >
< label className = "block" >
< span className = "mb-2 block text-sm font-semibold text-text" > Reference < / span >
2026-03-15 14:00:12 -05:00
< input value = { transactionForm . reference } onChange = { ( event ) = > updateTransactionField ( "reference" , event . target . value ) } placeholder = "PO, WO, adjustment note, etc." className = "w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand" / >
2026-03-14 22:37:09 -05:00
< / label >
< label className = "block" >
< span className = "mb-2 block text-sm font-semibold text-text" > Notes < / span >
2026-03-15 20:07:48 -05:00
< textarea value = { transactionForm . notes } onChange = { ( event ) = > updateTransactionField ( "notes" , event . target . value ) } rows = { 3 } className = "w-full rounded-[18px] border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand" / >
2026-03-14 22:37:09 -05:00
< / label >
2026-03-15 14:00:12 -05:00
< div className = "flex flex-col gap-3 rounded-2xl border border-line/70 bg-page/70 px-2 py-2" >
< span className = "text-sm text-muted" > { transactionStatus } < / span >
< button type = "submit" disabled = { isSavingTransaction } className = "rounded-2xl bg-brand px-2 py-2 text-sm font-semibold text-white disabled:cursor-not-allowed disabled:opacity-60" >
2026-03-14 22:37:09 -05:00
{ isSavingTransaction ? "Posting..." : "Post transaction" }
< / button >
< / div >
2026-03-15 14:00:12 -05:00
< / div >
< / form >
2026-03-14 22:37:09 -05:00
) : null }
2026-03-15 20:07:48 -05:00
< article className = "rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5" >
2026-03-15 14:00:12 -05:00
< p className = "text-xs font-semibold uppercase tracking-[0.24em] text-muted" > Recent Movements < / p >
2026-03-14 22:37:09 -05:00
{ item . recentTransactions . length === 0 ? (
2026-03-15 20:07:48 -05:00
< div className = "mt-6 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted" >
2026-03-14 22:37:09 -05:00
No stock transactions have been recorded for this item yet .
< / div >
) : (
< div className = "mt-6 space-y-3" >
{ item . recentTransactions . map ( ( transaction ) = > (
2026-03-15 20:07:48 -05:00
< article key = { transaction . id } className = "rounded-[18px] border border-line/70 bg-page/60 p-3" >
2026-03-14 22:37:09 -05:00
< div className = "flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between" >
< div >
< div className = "flex flex-wrap items-center gap-2" >
< InventoryTransactionTypeBadge type = { transaction . transactionType } / >
< span className = { ` text-sm font-semibold ${ transaction . signedQuantity >= 0 ? "text-emerald-700 dark:text-emerald-300" : "text-rose-700 dark:text-rose-300" } ` } >
{ transaction . signedQuantity >= 0 ? "+" : "" }
{ transaction . signedQuantity }
< / span >
< / div >
< div className = "mt-2 text-sm font-semibold text-text" >
{ transaction . warehouseCode } / { transaction . locationCode }
< / div >
{ transaction . reference ? < div className = "mt-2 text-xs text-muted" > Ref : { transaction . reference } < / div > : null }
{ transaction . notes ? < p className = "mt-2 whitespace-pre-line text-sm leading-6 text-text" > { transaction . notes } < / p > : null }
< / div >
< div className = "text-sm text-muted lg:text-right" >
< div > { new Date ( transaction . createdAt ) . toLocaleString ( ) } < / div >
< div className = "mt-1" > { transaction . createdByName } < / div >
< / div >
< / div >
< / article >
) ) }
< / div >
) }
< / article >
< / section >
2026-03-15 14:00:12 -05:00
{ canManage ? (
< section className = "grid gap-3 xl:grid-cols-2" >
2026-03-15 20:07:48 -05:00
< form className = "rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5" onSubmit = { handleTransferSubmit } >
2026-03-15 14:00:12 -05:00
< p className = "text-xs font-semibold uppercase tracking-[0.24em] text-muted" > Inventory Transfer < / p >
< div className = "mt-5 grid gap-3" >
< label className = "block" >
< span className = "mb-2 block text-sm font-semibold text-text" > Quantity < / span >
< input type = "number" min = { 1 } step = { 1 } value = { transferForm . quantity } onChange = { ( event ) = > updateTransferField ( "quantity" , Number . parseInt ( event . target . value , 10 ) || 1 ) } className = "w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand" / >
< / label >
< div className = "grid gap-3 sm:grid-cols-2" >
< label className = "block" >
< span className = "mb-2 block text-sm font-semibold text-text" > From < / span >
< select value = { transferForm . fromLocationId } onChange = { ( event ) = > {
const option = locationOptions . find ( ( entry ) = > entry . locationId === event . target . value ) ;
updateTransferField ( "fromLocationId" , event . target . value ) ;
if ( option ) {
updateTransferField ( "fromWarehouseId" , option . warehouseId ) ;
}
} } className = "w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand" >
{ locationOptions . map ( ( option ) = > (
< option key = { ` from- ${ option . locationId } ` } value = { option . locationId } >
{ option . warehouseCode } / { option . locationCode }
< / option >
) ) }
< / select >
< / label >
< label className = "block" >
< span className = "mb-2 block text-sm font-semibold text-text" > To < / span >
< select value = { transferForm . toLocationId } onChange = { ( event ) = > {
const option = locationOptions . find ( ( entry ) = > entry . locationId === event . target . value ) ;
updateTransferField ( "toLocationId" , event . target . value ) ;
if ( option ) {
updateTransferField ( "toWarehouseId" , option . warehouseId ) ;
}
} } className = "w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand" >
{ locationOptions . map ( ( option ) = > (
< option key = { ` to- ${ option . locationId } ` } value = { option . locationId } >
{ option . warehouseCode } / { option . locationCode }
< / option >
) ) }
< / select >
< / label >
< / div >
< label className = "block" >
< span className = "mb-2 block text-sm font-semibold text-text" > Notes < / span >
2026-03-15 20:07:48 -05:00
< textarea value = { transferForm . notes } onChange = { ( event ) = > updateTransferField ( "notes" , event . target . value ) } rows = { 3 } className = "w-full rounded-[18px] border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand" / >
2026-03-15 14:00:12 -05:00
< / label >
< div className = "flex flex-col gap-3 rounded-2xl border border-line/70 bg-page/70 px-2 py-2" >
< span className = "text-sm text-muted" > { transferStatus } < / span >
< button type = "submit" disabled = { isSavingTransfer } className = "rounded-2xl bg-brand px-2 py-2 text-sm font-semibold text-white disabled:cursor-not-allowed disabled:opacity-60" >
{ isSavingTransfer ? "Posting transfer..." : "Post transfer" }
< / button >
< / div >
< / div >
< / form >
2026-03-15 20:07:48 -05:00
< form className = "rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5" onSubmit = { handleReservationSubmit } >
2026-03-15 14:00:12 -05:00
< p className = "text-xs font-semibold uppercase tracking-[0.24em] text-muted" > Manual Reservation < / p >
< div className = "mt-5 grid gap-3" >
< label className = "block" >
< span className = "mb-2 block text-sm font-semibold text-text" > Quantity < / span >
< input type = "number" min = { 1 } step = { 1 } value = { reservationForm . quantity } onChange = { ( event ) = > setReservationForm ( ( current ) = > ( { . . . current , quantity : Number.parseInt ( event . target . value , 10 ) || 1 } ) ) } className = "w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand" / >
< / label >
< label className = "block" >
< span className = "mb-2 block text-sm font-semibold text-text" > Location < / span >
< select value = { reservationForm . locationId ? ? "" } onChange = { ( event ) = > {
const option = locationOptions . find ( ( entry ) = > entry . locationId === event . target . value ) ;
setReservationForm ( ( current ) = > ( {
. . . current ,
locationId : event.target.value || null ,
warehouseId : option ? option.warehouseId : null ,
} ) ) ;
} } className = "w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand" >
< option value = "" > Global / not location - specific < / option >
{ locationOptions . map ( ( option ) = > (
< option key = { option . locationId } value = { option . locationId } >
{ option . warehouseCode } / { option . locationCode }
< / option >
) ) }
< / select >
< / label >
< label className = "block" >
< span className = "mb-2 block text-sm font-semibold text-text" > Notes < / span >
2026-03-15 20:07:48 -05:00
< textarea value = { reservationForm . notes } onChange = { ( event ) = > setReservationForm ( ( current ) = > ( { . . . current , notes : event.target.value } ) ) } rows = { 3 } className = "w-full rounded-[18px] border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand" / >
2026-03-15 14:00:12 -05:00
< / label >
< div className = "flex flex-col gap-3 rounded-2xl border border-line/70 bg-page/70 px-2 py-2" >
< span className = "text-sm text-muted" > { reservationStatus } < / span >
< button type = "submit" disabled = { isSavingReservation } className = "rounded-2xl bg-brand px-2 py-2 text-sm font-semibold text-white disabled:cursor-not-allowed disabled:opacity-60" >
{ isSavingReservation ? "Saving reservation..." : "Create reservation" }
< / button >
< / div >
< / div >
< / form >
< / section >
) : null }
< section className = "grid gap-3 xl:grid-cols-2" >
2026-03-15 20:07:48 -05:00
< article className = "rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5" >
2026-03-15 14:00:12 -05:00
< p className = "text-xs font-semibold uppercase tracking-[0.24em] text-muted" > Reservations < / p >
{ item . reservations . length === 0 ? (
2026-03-15 20:07:48 -05:00
< div className = "mt-6 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted" >
2026-03-15 14:00:12 -05:00
No reservations have been recorded for this item .
< / div >
) : (
< div className = "mt-5 space-y-3" >
{ item . reservations . map ( ( reservation ) = > (
2026-03-15 20:07:48 -05:00
< article key = { reservation . id } className = "rounded-[18px] border border-line/70 bg-page/60 p-3" >
2026-03-15 14:00:12 -05:00
< div className = "flex items-center justify-between gap-3" >
< div >
< div className = "font-semibold text-text" > { reservation . quantity } reserved < / div >
< div className = "mt-1 text-xs text-muted" > { reservation . sourceLabel ? ? reservation . sourceType } < / div >
< / div >
< div className = "text-xs text-muted" > { reservation . status } < / div >
< / div >
< div className = "mt-2 text-xs text-muted" >
{ reservation . warehouseCode && reservation . locationCode ? ` ${ reservation . warehouseCode } / ${ reservation . locationCode } ` : "Not location-specific" }
< / div >
< div className = "mt-2 text-sm text-text" > { reservation . notes || "No notes recorded." } < / div >
< / article >
) ) }
< / div >
) }
< / article >
2026-03-15 20:07:48 -05:00
< article className = "rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5" >
2026-03-15 14:00:12 -05:00
< p className = "text-xs font-semibold uppercase tracking-[0.24em] text-muted" > Transfers < / p >
{ item . transfers . length === 0 ? (
2026-03-15 20:07:48 -05:00
< div className = "mt-6 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted" >
2026-03-15 14:00:12 -05:00
No transfers have been recorded for this item .
< / div >
) : (
< div className = "mt-5 space-y-3" >
{ item . transfers . map ( ( transfer ) = > (
2026-03-15 20:07:48 -05:00
< article key = { transfer . id } className = "rounded-[18px] border border-line/70 bg-page/60 p-3" >
2026-03-15 14:00:12 -05:00
< div className = "flex items-center justify-between gap-3" >
< div className = "font-semibold text-text" > { transfer . quantity } moved < / div >
< div className = "text-xs text-muted" > { new Date ( transfer . createdAt ) . toLocaleString ( ) } < / div >
< / div >
< div className = "mt-2 text-xs text-muted" >
{ transfer . fromWarehouseCode } / { transfer . fromLocationCode } to { transfer . toWarehouseCode } / { transfer . toLocationCode }
< / div >
< div className = "mt-2 text-sm text-text" > { transfer . notes || "No notes recorded." } < / div >
< / article >
) ) }
< / div >
) }
< / article >
< / section >
2026-03-14 23:03:17 -05:00
< InventoryAttachmentsPanel itemId = { item . id } / >
2026-03-15 18:59:37 -05:00
< ConfirmActionDialog
open = { pendingConfirmation != null }
title = { pendingConfirmation ? . title ? ? "Confirm inventory action" }
description = { pendingConfirmation ? . description ? ? "" }
impact = { pendingConfirmation ? . impact }
recovery = { pendingConfirmation ? . recovery }
confirmLabel = { pendingConfirmation ? . confirmLabel ? ? "Confirm" }
confirmationLabel = { pendingConfirmation ? . confirmationLabel }
confirmationValue = { pendingConfirmation ? . confirmationValue }
isConfirming = {
( pendingConfirmation ? . kind === "transaction" && isSavingTransaction ) ||
( pendingConfirmation ? . kind === "transfer" && isSavingTransfer ) ||
( pendingConfirmation ? . kind === "reservation" && isSavingReservation )
}
onClose = { ( ) = > {
if ( ! isSavingTransaction && ! isSavingTransfer && ! isSavingReservation ) {
setPendingConfirmation ( null ) ;
}
} }
onConfirm = { async ( ) = > {
if ( ! pendingConfirmation ) {
return ;
}
if ( pendingConfirmation . kind === "transaction" ) {
await submitTransaction ( ) ;
} else if ( pendingConfirmation . kind === "transfer" ) {
await submitTransfer ( ) ;
} else {
await submitReservation ( ) ;
}
setPendingConfirmation ( null ) ;
} }
/ >
2026-03-14 21:10:35 -05:00
< / section >
) ;
}
2026-03-15 20:07:48 -05:00