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

@@ -6,6 +6,7 @@ import { Link, useNavigate, useParams } from "react-router-dom";
import { useAuth } from "../../auth/AuthProvider";
import { api, ApiError } from "../../lib/api";
import { ConfirmActionDialog } from "../../components/ConfirmActionDialog";
import { salesConfigs, salesStatusOptions, type SalesDocumentEntity } from "./config";
import { SalesStatusBadge } from "./SalesStatusBadge";
import { ShipmentStatusBadge } from "../shipping/ShipmentStatusBadge";
@@ -59,6 +60,20 @@ export function SalesDetailPage({ entity }: { entity: SalesDocumentEntity }) {
const [isApproving, setIsApproving] = useState(false);
const [shipments, setShipments] = useState<ShipmentSummaryDto[]>([]);
const [planning, setPlanning] = useState<SalesOrderPlanningDto | null>(null);
const [pendingConfirmation, setPendingConfirmation] = useState<
| {
kind: "status" | "approve" | "convert";
title: string;
description: string;
impact: string;
recovery: string;
confirmLabel: string;
confirmationLabel?: string;
confirmationValue?: string;
nextStatus?: SalesDocumentStatus;
}
| null
>(null);
const canManage = user?.permissions.includes(permissions.salesWrite) ?? false;
const canManageShipping = user?.permissions.includes(permissions.shippingWrite) ?? false;
@@ -119,7 +134,7 @@ export function SalesDetailPage({ entity }: { entity: SalesDocumentEntity }) {
return `/purchasing/orders/new?${params.toString()}`;
}
async function handleStatusChange(nextStatus: SalesDocumentStatus) {
async function applyStatusChange(nextStatus: SalesDocumentStatus) {
if (!token) {
return;
}
@@ -133,7 +148,7 @@ export function SalesDetailPage({ entity }: { entity: SalesDocumentEntity }) {
? await api.updateQuoteStatus(token, activeDocument.id, nextStatus)
: await api.updateSalesOrderStatus(token, activeDocument.id, nextStatus);
setDocument(nextDocument);
setStatus(`${config.singularLabel} status updated.`);
setStatus(`${config.singularLabel} status updated. Review revisions and downstream workflows if the document moved into a terminal or customer-visible state.`);
} catch (error: unknown) {
const message = error instanceof ApiError ? error.message : `Unable to update ${config.singularLabel.toLowerCase()} status.`;
setStatus(message);
@@ -142,7 +157,7 @@ export function SalesDetailPage({ entity }: { entity: SalesDocumentEntity }) {
}
}
async function handleConvert() {
async function applyConvert() {
if (!token || entity !== "quote") {
return;
}
@@ -185,7 +200,7 @@ export function SalesDetailPage({ entity }: { entity: SalesDocumentEntity }) {
}
}
async function handleApprove() {
async function applyApprove() {
if (!token) {
return;
}
@@ -197,7 +212,7 @@ export function SalesDetailPage({ entity }: { entity: SalesDocumentEntity }) {
const nextDocument =
entity === "quote" ? await api.approveQuote(token, activeDocument.id) : await api.approveSalesOrder(token, activeDocument.id);
setDocument(nextDocument);
setStatus(`${config.singularLabel} approved.`);
setStatus(`${config.singularLabel} approved. The approval stamp is now part of the document history and downstream teams can act on it immediately.`);
} catch (error: unknown) {
const message = error instanceof ApiError ? error.message : `Unable to approve ${config.singularLabel.toLowerCase()}.`;
setStatus(message);
@@ -206,6 +221,50 @@ export function SalesDetailPage({ entity }: { entity: SalesDocumentEntity }) {
}
}
function handleStatusChange(nextStatus: SalesDocumentStatus) {
const label = salesStatusOptions.find((option) => option.value === nextStatus)?.label ?? nextStatus;
setPendingConfirmation({
kind: "status",
title: `Set ${config.singularLabel.toLowerCase()} to ${label}`,
description: `Update ${activeDocument.documentNumber} from ${activeDocument.status} to ${nextStatus}.`,
impact:
nextStatus === "CLOSED"
? "This closes the document operationally and can change customer-facing execution assumptions and downstream follow-up expectations."
: nextStatus === "APPROVED"
? "This marks the document ready for downstream action and becomes part of the approval history."
: "This changes the operational state used by downstream workflows and audit/revision history.",
recovery: "If this status is set in error, return the document to the correct state and verify the latest revision history.",
confirmLabel: `Set ${label}`,
confirmationLabel: nextStatus === "CLOSED" ? "Type document number to confirm:" : undefined,
confirmationValue: nextStatus === "CLOSED" ? activeDocument.documentNumber : undefined,
nextStatus,
});
}
function handleApprove() {
setPendingConfirmation({
kind: "approve",
title: `Approve ${config.singularLabel.toLowerCase()}`,
description: `Approve ${activeDocument.documentNumber} for ${activeDocument.customerName}.`,
impact: "Approval records the approver and timestamp and signals that downstream execution can proceed.",
recovery: "If approval was granted by mistake, change the document status and review the revision trail for follow-up.",
confirmLabel: "Approve document",
});
}
function handleConvert() {
setPendingConfirmation({
kind: "convert",
title: "Convert quote to sales order",
description: `Create a sales order from quote ${activeDocument.documentNumber}.`,
impact: "This creates a new sales order record and can trigger planning, purchasing, manufacturing, and shipping follow-up work.",
recovery: "Review the new order immediately after creation. If conversion was premature, move the resulting order to the correct status and coordinate with downstream teams.",
confirmLabel: "Convert quote",
confirmationLabel: "Type quote number to confirm:",
confirmationValue: activeDocument.documentNumber,
});
}
return (
<section className="space-y-4">
<div className="rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
@@ -570,6 +629,48 @@ export function SalesDetailPage({ entity }: { entity: SalesDocumentEntity }) {
)}
</section>
) : null}
<ConfirmActionDialog
open={pendingConfirmation != null}
title={pendingConfirmation?.title ?? "Confirm sales 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 === "approve" && isApproving) ||
(pendingConfirmation?.kind === "convert" && isConverting)
}
onClose={() => {
if (!isUpdatingStatus && !isApproving && !isConverting) {
setPendingConfirmation(null);
}
}}
onConfirm={async () => {
if (!pendingConfirmation) {
return;
}
if (pendingConfirmation.kind === "status" && pendingConfirmation.nextStatus) {
await applyStatusChange(pendingConfirmation.nextStatus);
setPendingConfirmation(null);
return;
}
if (pendingConfirmation.kind === "approve") {
await applyApprove();
setPendingConfirmation(null);
return;
}
if (pendingConfirmation.kind === "convert") {
await applyConvert();
setPendingConfirmation(null);
}
}}
/>
</section>
);
}