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

@@ -7,6 +7,7 @@ import { Link, useParams } from "react-router-dom";
import { FileAttachmentsPanel } from "../../components/FileAttachmentsPanel";
import { useAuth } from "../../auth/AuthProvider";
import { api, ApiError } from "../../lib/api";
import { ConfirmActionDialog } from "../../components/ConfirmActionDialog";
import { emptyCompletionInput, emptyMaterialIssueInput, workOrderStatusOptions } from "./config";
import { WorkOrderStatusBadge } from "./WorkOrderStatusBadge";
@@ -21,6 +22,20 @@ export function WorkOrderDetailPage() {
const [isUpdatingStatus, setIsUpdatingStatus] = useState(false);
const [isPostingIssue, setIsPostingIssue] = useState(false);
const [isPostingCompletion, setIsPostingCompletion] = useState(false);
const [pendingConfirmation, setPendingConfirmation] = useState<
| {
kind: "status" | "issue" | "completion";
title: string;
description: string;
impact: string;
recovery: string;
confirmLabel: string;
confirmationLabel?: string;
confirmationValue?: string;
nextStatus?: WorkOrderStatus;
}
| null
>(null);
const canManage = user?.permissions.includes(permissions.manufacturingWrite) ?? false;
@@ -56,7 +71,7 @@ export function WorkOrderDetailPage() {
[issueForm.warehouseId, locationOptions]
);
async function handleStatusChange(nextStatus: WorkOrderStatus) {
async function applyStatusChange(nextStatus: WorkOrderStatus) {
if (!token || !workOrder) {
return;
}
@@ -66,7 +81,7 @@ export function WorkOrderDetailPage() {
try {
const nextWorkOrder = await api.updateWorkOrderStatus(token, workOrder.id, nextStatus);
setWorkOrder(nextWorkOrder);
setStatus("Work-order status updated.");
setStatus("Work-order status updated. Review downstream planning and shipment readiness if this change affects execution timing.");
} catch (error: unknown) {
const message = error instanceof ApiError ? error.message : "Unable to update work-order status.";
setStatus(message);
@@ -75,8 +90,7 @@ export function WorkOrderDetailPage() {
}
}
async function handleIssueSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
async function submitIssue() {
if (!token || !workOrder) {
return;
}
@@ -91,7 +105,7 @@ export function WorkOrderDetailPage() {
warehouseId: nextWorkOrder.warehouseId,
locationId: nextWorkOrder.locationId,
});
setStatus("Material issue posted.");
setStatus("Material issue posted. This consumed inventory immediately; post a correcting stock movement if the issue quantity was wrong.");
} catch (error: unknown) {
const message = error instanceof ApiError ? error.message : "Unable to post material issue.";
setStatus(message);
@@ -100,8 +114,7 @@ export function WorkOrderDetailPage() {
}
}
async function handleCompletionSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
async function submitCompletion() {
if (!token || !workOrder) {
return;
}
@@ -115,7 +128,7 @@ export function WorkOrderDetailPage() {
...emptyCompletionInput,
quantity: Math.max(nextWorkOrder.dueQuantity, 1),
});
setStatus("Completion posted.");
setStatus("Completion posted. Finished-goods stock has been received; verify the remaining quantity and post a correcting transaction if this completion was overstated.");
} catch (error: unknown) {
const message = error instanceof ApiError ? error.message : "Unable to post completion.";
setStatus(message);
@@ -124,6 +137,64 @@ export function WorkOrderDetailPage() {
}
}
function handleStatusChange(nextStatus: WorkOrderStatus) {
if (!workOrder) {
return;
}
const option = workOrderStatusOptions.find((entry) => entry.value === nextStatus);
setPendingConfirmation({
kind: "status",
title: `Change status to ${option?.label ?? nextStatus}`,
description: `Update work order ${workOrder.workOrderNumber} from ${workOrder.status} to ${nextStatus}.`,
impact:
nextStatus === "CANCELLED"
? "Cancelling a work order can invalidate planning assumptions, reservations, and operator expectations."
: nextStatus === "COMPLETE"
? "Completing the work order signals execution closure and can change readiness views across the system."
: "This changes the execution state used by planning, dashboards, and downstream operational review.",
recovery: "If this status was selected in error, set the work order back to the correct state immediately after review.",
confirmLabel: `Set ${option?.label ?? nextStatus}`,
confirmationLabel: nextStatus === "CANCELLED" ? "Type work-order number to confirm:" : undefined,
confirmationValue: nextStatus === "CANCELLED" ? workOrder.workOrderNumber : undefined,
nextStatus,
});
}
function handleIssueSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!workOrder) {
return;
}
const component = workOrder.materialRequirements.find((requirement) => requirement.componentItemId === issueForm.componentItemId);
setPendingConfirmation({
kind: "issue",
title: "Post material issue",
description: `Issue ${issueForm.quantity} units of ${component?.componentSku ?? "the selected component"} to work order ${workOrder.workOrderNumber}.`,
impact: "This consumes component inventory immediately and updates work-order material history.",
recovery: "If the wrong quantity was issued, post a correcting stock transaction and note the reason on the work order.",
confirmLabel: "Post issue",
confirmationLabel: "Type work-order number to confirm:",
confirmationValue: workOrder.workOrderNumber,
});
}
function handleCompletionSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!workOrder) {
return;
}
setPendingConfirmation({
kind: "completion",
title: "Post production completion",
description: `Receive ${completionForm.quantity} finished units into ${workOrder.warehouseCode} / ${workOrder.locationCode}.`,
impact: "This increases finished-goods inventory immediately and advances the execution history for this work order.",
recovery: "If the completion quantity is wrong, post the correcting inventory movement and verify the work-order remaining quantity.",
confirmLabel: "Post completion",
confirmationLabel: completionForm.quantity >= workOrder.dueQuantity ? "Type work-order number to confirm:" : undefined,
confirmationValue: completionForm.quantity >= workOrder.dueQuantity ? workOrder.workOrderNumber : undefined,
});
}
if (!workOrder) {
return <div className="rounded-[28px] border border-line/70 bg-surface/90 p-4 text-sm text-muted shadow-panel">{status}</div>;
}
@@ -375,6 +446,41 @@ export function WorkOrderDetailPage() {
emptyMessage="No manufacturing attachments have been uploaded for this work order yet."
/>
<div className="rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm text-muted">{status}</div>
<ConfirmActionDialog
open={pendingConfirmation != null}
title={pendingConfirmation?.title ?? "Confirm manufacturing action"}
description={pendingConfirmation?.description ?? ""}
impact={pendingConfirmation?.impact}
recovery={pendingConfirmation?.recovery}
confirmLabel={pendingConfirmation?.confirmLabel ?? "Confirm"}
confirmationLabel={pendingConfirmation?.confirmationLabel}
confirmationValue={pendingConfirmation?.confirmationValue}
isConfirming={
(pendingConfirmation?.kind === "status" && isUpdatingStatus) ||
(pendingConfirmation?.kind === "issue" && isPostingIssue) ||
(pendingConfirmation?.kind === "completion" && isPostingCompletion)
}
onClose={() => {
if (!isUpdatingStatus && !isPostingIssue && !isPostingCompletion) {
setPendingConfirmation(null);
}
}}
onConfirm={async () => {
if (!pendingConfirmation) {
return;
}
if (pendingConfirmation.kind === "status" && pendingConfirmation.nextStatus) {
await applyStatusChange(pendingConfirmation.nextStatus);
} else if (pendingConfirmation.kind === "issue") {
await submitIssue();
} else if (pendingConfirmation.kind === "completion") {
await submitCompletion();
}
setPendingConfirmation(null);
}}
/>
</section>
);
}