confirm actions

This commit is contained in:
2026-03-15 18:59:37 -05:00
parent 59754c7657
commit df041254da
28 changed files with 999 additions and 63 deletions

View File

@@ -11,6 +11,7 @@ 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, inventoryTransactionOptions } from "./config";
import { InventoryAttachmentsPanel } from "./InventoryAttachmentsPanel";
import { InventoryStatusBadge } from "./InventoryStatusBadge";
@@ -48,6 +49,19 @@ export function InventoryDetailPage() {
const [isSavingTransfer, setIsSavingTransfer] = useState(false);
const [isSavingReservation, setIsSavingReservation] = useState(false);
const [status, setStatus] = useState("Loading inventory item...");
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;
@@ -100,8 +114,7 @@ export function InventoryDetailPage() {
setTransferForm((current) => ({ ...current, [key]: value }));
}
async function handleTransactionSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
async function submitTransaction() {
if (!token || !itemId) {
return;
}
@@ -112,7 +125,7 @@ export function InventoryDetailPage() {
try {
const nextItem = await api.createInventoryTransaction(token, itemId, transactionForm);
setItem(nextItem);
setTransactionStatus("Stock transaction recorded.");
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,
@@ -127,8 +140,7 @@ export function InventoryDetailPage() {
}
}
async function handleTransferSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
async function submitTransfer() {
if (!token || !itemId) {
return;
}
@@ -139,7 +151,7 @@ export function InventoryDetailPage() {
try {
const nextItem = await api.createInventoryTransfer(token, itemId, transferForm);
setItem(nextItem);
setTransferStatus("Transfer recorded.");
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);
@@ -148,8 +160,7 @@ export function InventoryDetailPage() {
}
}
async function handleReservationSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
async function submitReservation() {
if (!token || !itemId) {
return;
}
@@ -160,7 +171,7 @@ export function InventoryDetailPage() {
try {
const nextItem = await api.createInventoryReservation(token, itemId, reservationForm);
setItem(nextItem);
setReservationStatus("Reservation recorded.");
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.";
@@ -170,6 +181,64 @@ export function InventoryDetailPage() {
}
}
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",
});
}
if (!item) {
return <div className="rounded-[28px] border border-line/70 bg-surface/90 p-4 text-sm text-muted shadow-panel">{status}</div>;
}
@@ -530,6 +599,41 @@ export function InventoryDetailPage() {
</section>
<InventoryAttachmentsPanel itemId={item.id} />
<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);
}}
/>
</section>
);
}