This commit is contained in:
2026-03-15 19:22:20 -05:00
parent df041254da
commit 275c73b584
8 changed files with 268 additions and 23 deletions

View File

@@ -7,6 +7,7 @@ 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 { emptyPurchaseReceiptInput, purchaseStatusOptions } from "./config";
@@ -25,6 +26,20 @@ export function PurchaseDetailPage() {
const [isUpdatingStatus, setIsUpdatingStatus] = useState(false);
const [isOpeningPdf, setIsOpeningPdf] = useState(false);
const [planningRollup, setPlanningRollup] = useState<DemandPlanningRollupDto | null>(null);
const [pendingConfirmation, setPendingConfirmation] = useState<
| {
kind: "status" | "receipt";
title: string;
description: string;
impact: string;
recovery: string;
confirmLabel: string;
confirmationLabel?: string;
confirmationValue?: string;
nextStatus?: PurchaseOrderStatus;
}
| null
>(null);
const canManage = user?.permissions.includes("purchasing.write") ?? false;
const canReceive = canManage && (user?.permissions.includes(permissions.inventoryWrite) ?? false);
@@ -107,7 +122,7 @@ export function PurchaseDetailPage() {
}));
}
async function handleStatusChange(nextStatus: PurchaseOrderStatus) {
async function applyStatusChange(nextStatus: PurchaseOrderStatus) {
if (!token) {
return;
}
@@ -118,7 +133,7 @@ export function PurchaseDetailPage() {
try {
const nextDocument = await api.updatePurchaseOrderStatus(token, activeDocument.id, nextStatus);
setDocument(nextDocument);
setStatus("Purchase order status updated.");
setStatus("Purchase order status updated. Confirm vendor communication and receiving expectations if this moved the order into a terminal state.");
} catch (error: unknown) {
const message = error instanceof ApiError ? error.message : "Unable to update purchase order status.";
setStatus(message);
@@ -127,8 +142,7 @@ export function PurchaseDetailPage() {
}
}
async function handleReceiptSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
async function applyReceipt() {
if (!token || !canReceive) {
return;
}
@@ -155,7 +169,7 @@ export function PurchaseDetailPage() {
receivedAt: new Date().toISOString(),
notes: "",
}));
setReceiptStatus("Purchase receipt recorded.");
setReceiptStatus("Purchase receipt recorded. Inventory has been increased; verify stock balances and post a correcting movement if quantities were overstated.");
setStatus("Purchase order updated after receipt.");
} catch (error: unknown) {
const message = error instanceof ApiError ? error.message : "Unable to record purchase receipt.";
@@ -165,6 +179,39 @@ export function PurchaseDetailPage() {
}
}
function handleStatusChange(nextStatus: PurchaseOrderStatus) {
const label = purchaseStatusOptions.find((option) => option.value === nextStatus)?.label ?? nextStatus;
setPendingConfirmation({
kind: "status",
title: `Set purchase order to ${label}`,
description: `Update ${activeDocument.documentNumber} from ${activeDocument.status} to ${nextStatus}.`,
impact:
nextStatus === "CLOSED"
? "This closes the order operationally and can change inbound supply expectations, shortage coverage, and vendor follow-up."
: "This changes the purchasing state used by receiving, planning, and audit review.",
recovery: "If the status is wrong, set the order back to the correct state and verify any downstream receiving or planning assumptions.",
confirmLabel: `Set ${label}`,
confirmationLabel: nextStatus === "CLOSED" ? "Type purchase order number to confirm:" : undefined,
confirmationValue: nextStatus === "CLOSED" ? activeDocument.documentNumber : undefined,
nextStatus,
});
}
function handleReceiptSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
const totalReceiptQuantity = openLines.reduce((sum, line) => sum + Math.max(0, Math.floor(receiptQuantities[line.id] ?? 0)), 0);
setPendingConfirmation({
kind: "receipt",
title: "Post purchase receipt",
description: `Receive ${totalReceiptQuantity} total units into ${receiptForm.warehouseId && receiptForm.locationId ? "the selected stock location" : "inventory"} for ${activeDocument.documentNumber}.`,
impact: "This increases inventory immediately and becomes part of the PO receipt history.",
recovery: "If quantities are wrong, post the correcting inventory movement and review the remaining quantities on the purchase order.",
confirmLabel: "Post receipt",
confirmationLabel: totalReceiptQuantity > 0 ? "Type purchase order number to confirm:" : undefined,
confirmationValue: totalReceiptQuantity > 0 ? activeDocument.documentNumber : undefined,
});
}
async function handleOpenPdf() {
if (!token) {
return;
@@ -464,6 +511,41 @@ export function PurchaseDetailPage() {
emptyMessage="No vendor supporting documents have been uploaded for this purchase 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 purchasing 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 === "receipt" && isSavingReceipt)
}
onClose={() => {
if (!isUpdatingStatus && !isSavingReceipt) {
setPendingConfirmation(null);
}
}}
onConfirm={async () => {
if (!pendingConfirmation) {
return;
}
if (pendingConfirmation.kind === "status" && pendingConfirmation.nextStatus) {
await applyStatusChange(pendingConfirmation.nextStatus);
setPendingConfirmation(null);
return;
}
if (pendingConfirmation.kind === "receipt") {
await applyReceipt();
setPendingConfirmation(null);
}
}}
/>
</section>
);
}