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

@@ -29,7 +29,7 @@ MRP Codex is a modular Manufacturing Resource Planning platform intended to be a
- shared shortage and readiness rollups across dashboard, planning, projects, purchasing, and manufacturing - shared shortage and readiness rollups across dashboard, planning, projects, purchasing, and manufacturing
- admin diagnostics with runtime footprint, record counts, and persisted audit-trail visibility - admin diagnostics with runtime footprint, record counts, and persisted audit-trail visibility
- admin user management with account creation, activation, role assignment, role-permission editing, and session visibility/revocation - admin user management with account creation, activation, role assignment, role-permission editing, and session visibility/revocation
- safer destructive-action confirmations and recovery messaging across admin, inventory, manufacturing, and attachment workflows - safer destructive-action confirmations and recovery messaging across admin, sales, purchasing, shipping, inventory, manufacturing, and attachment workflows
- CRM/shipping audit coverage and startup validation surfaced through the admin diagnostics workflow - CRM/shipping audit coverage and startup validation surfaced through the admin diagnostics workflow
- backup/restore guidance, richer startup diagnostics, and exportable support bundles in the admin diagnostics workflow - backup/restore guidance, richer startup diagnostics, and exportable support bundles in the admin diagnostics workflow
- backup verification checklist and restore-drill runbook in the admin diagnostics workflow - backup verification checklist and restore-drill runbook in the admin diagnostics workflow
@@ -131,7 +131,7 @@ If implementation changes invalidate those docs, update them in the same change
Near-term priorities are: Near-term priorities are:
1. Deeper session history, filtering, and admin-side access review polish 1. Deeper session history, filtering, and admin-side access review polish
2. Extend destructive-action safety coverage across sales, purchasing, shipping, and project workflows 2. Extend destructive-action safety coverage into remaining project and form-edit removal workflows
When adding new modules, preserve the ability to extend the system without refactoring the existing app shell. When adding new modules, preserve the ability to extend the system without refactoring the existing app shell.

View File

@@ -8,6 +8,7 @@ This file is the running release and change log for MRP Codex. Keep it updated w
- Shared destructive-action confirmation dialog with impact and recovery guidance for high-risk operational actions - Shared destructive-action confirmation dialog with impact and recovery guidance for high-risk operational actions
- Typed confirmation for sensitive admin actions such as account deactivation, current-session revocation, and terminal manufacturing/inventory postings - Typed confirmation for sensitive admin actions such as account deactivation, current-session revocation, and terminal manufacturing/inventory postings
- Destructive-action confirmation and recovery coverage for sales approvals, quote conversion, purchase receiving, purchase status changes, and shipment status changes
- Persisted auth-session tracking with admin visibility into active, expired, and revoked sign-ins - Persisted auth-session tracking with admin visibility into active, expired, and revoked sign-ins
- Admin-side session revocation controls plus server-side logout that invalidates the current JWT-backed session - Admin-side session revocation controls plus server-side logout that invalidates the current JWT-backed session
- Shared shortage and readiness rollups across dashboard, planning, project detail, purchasing detail, and manufacturing detail - Shared shortage and readiness rollups across dashboard, planning, project detail, purchasing detail, and manufacturing detail
@@ -50,7 +51,7 @@ This file is the running release and change log for MRP Codex. Keep it updated w
### Changed ### Changed
- Admin, inventory, manufacturing, and attachment workflows now use explicit destructive-action confirmation and recovery messaging instead of immediate irreversible clicks - Admin, sales, purchasing, shipping, inventory, manufacturing, and attachment workflows now use explicit destructive-action confirmation and recovery messaging instead of immediate irreversible clicks
- Admin operations now combine user management with live session visibility so operators can inspect and revoke sign-ins without changing user records - Admin operations now combine user management with live session visibility so operators can inspect and revoke sign-ins without changing user records
- JWT authentication now validates against persisted session records and inactive users lose access immediately instead of waiting for token expiry - JWT authentication now validates against persisted session records and inactive users lose access immediately instead of waiting for token expiry
- The dashboard now treats Projects as a live first-class module alongside CRM, inventory, sales, and shipping - The dashboard now treats Projects as a live first-class module alongside CRM, inventory, sales, and shipping

View File

@@ -33,7 +33,7 @@ This repository implements the platform foundation milestone:
- shared shortage and readiness rollups across dashboard, planning, projects, purchasing, and manufacturing - shared shortage and readiness rollups across dashboard, planning, projects, purchasing, and manufacturing
- admin diagnostics with runtime footprint, storage visibility, record counts, and recent audit activity - admin diagnostics with runtime footprint, storage visibility, record counts, and recent audit activity
- admin user management with account creation, activation, role assignment, role-permission editing, and session visibility/revocation - admin user management with account creation, activation, role assignment, role-permission editing, and session visibility/revocation
- safer destructive-action confirmations and recovery messaging across admin, inventory, manufacturing, and attachment workflows - safer destructive-action confirmations and recovery messaging across admin, sales, purchasing, shipping, inventory, manufacturing, and attachment workflows
- CRM/shipping audit coverage and startup validation surfaced through diagnostics - CRM/shipping audit coverage and startup validation surfaced through diagnostics
- backup/restore guidance, richer startup diagnostics, and exportable support bundles in diagnostics - backup/restore guidance, richer startup diagnostics, and exportable support bundles in diagnostics
- backup verification checklist and restore-drill runbook in diagnostics - backup verification checklist and restore-drill runbook in diagnostics
@@ -74,4 +74,4 @@ This repository implements the platform foundation milestone:
## Next roadmap candidates ## Next roadmap candidates
- deeper session history, filtering, and admin-side access review polish - deeper session history, filtering, and admin-side access review polish
- extend destructive-action safety coverage across sales, purchasing, shipping, and project workflows - extend destructive-action safety coverage into remaining project and form-edit removal workflows

View File

@@ -32,7 +32,7 @@ Current foundation scope includes:
- shared shortage and readiness rollups across dashboard, planning, projects, purchasing, and manufacturing - shared shortage and readiness rollups across dashboard, planning, projects, purchasing, and manufacturing
- admin diagnostics with runtime footprint, record counts, and recent audit-trail visibility - admin diagnostics with runtime footprint, record counts, and recent audit-trail visibility
- admin user management with account creation, activation, role assignment, role-permission editing, and session visibility/revocation - admin user management with account creation, activation, role assignment, role-permission editing, and session visibility/revocation
- safer destructive-action confirmations and recovery messaging across admin, inventory, manufacturing, and attachment workflows - safer destructive-action confirmations and recovery messaging across admin, sales, purchasing, shipping, inventory, manufacturing, and attachment workflows
- CRM and shipping audit coverage plus startup validation surfaced through the admin diagnostics page - CRM and shipping audit coverage plus startup validation surfaced through the admin diagnostics page
- backup/restore guidance, richer startup diagnostics, and exportable support bundles in the admin diagnostics workflow - backup/restore guidance, richer startup diagnostics, and exportable support bundles in the admin diagnostics workflow
- backup verification checklist and restore-drill runbook surfaced in admin diagnostics - backup verification checklist and restore-drill runbook surfaced in admin diagnostics
@@ -59,7 +59,7 @@ Current completed foundation areas:
Near-term priorities: Near-term priorities:
1. Deeper session history, filtering, and admin-side access review polish 1. Deeper session history, filtering, and admin-side access review polish
2. Extend destructive-action safety coverage across sales, purchasing, shipping, and project workflows 2. Extend destructive-action safety coverage into remaining project and form-edit removal workflows
Revisit / deferred items: Revisit / deferred items:
@@ -378,7 +378,7 @@ The current admin operations slice supports:
Current follow-up direction: Current follow-up direction:
- deeper session history, filtering, and admin-side access review polish - deeper session history, filtering, and admin-side access review polish
- extend destructive-action safety coverage across sales, purchasing, shipping, and project workflows - extend destructive-action safety coverage into remaining project and form-edit removal workflows
## UI Notes ## UI Notes

View File

@@ -291,7 +291,7 @@ Foundation slice shipped:
- Expanded role-management UI with account creation, activation, role assignment, and permission administration - Expanded role-management UI with account creation, activation, role assignment, and permission administration
- Persisted auth-session tracking with admin visibility into active, expired, and revoked sign-ins - Persisted auth-session tracking with admin visibility into active, expired, and revoked sign-ins
- Server-side logout and admin session revocation for JWT-backed access - Server-side logout and admin session revocation for JWT-backed access
- Shared destructive-action confirmation and recovery messaging for admin, inventory, manufacturing, and attachment workflows - Shared destructive-action confirmation and recovery messaging for admin, sales, purchasing, shipping, inventory, manufacturing, and attachment workflows
- CRM customer/vendor changes and shipping mutations covered by the shared audit trail - CRM customer/vendor changes and shipping mutations covered by the shared audit trail
- Startup validation during server boot with checks for storage paths, writable directories, database connectivity, client bundle readiness, Chromium availability, and risky production defaults - Startup validation during server boot with checks for storage paths, writable directories, database connectivity, client bundle readiness, Chromium availability, and risky production defaults
- Backup/restore guidance, support-bundle exports, and support-log viewing surfaced through the admin diagnostics workflow - Backup/restore guidance, support-bundle exports, and support-log viewing surfaced through the admin diagnostics workflow
@@ -306,7 +306,7 @@ QOL subfeatures:
- Admin diagnostics screen for permissions, migrations, storage, and PDF health - Admin diagnostics screen for permissions, migrations, storage, and PDF health
- Better session filtering, review history, and unusual-access cues for operational admins - Better session filtering, review history, and unusual-access cues for operational admins
- Extend destructive-action safety coverage across sales, purchasing, shipping, and project workflows - Extend destructive-action safety coverage into remaining project and form-edit removal workflows
- More explicit environment validation on startup - More explicit environment validation on startup
- Support-log filtering, retention controls, and broader support-package polish - Support-log filtering, retention controls, and broader support-package polish
- Backup verification checklist and restore drill guidance - Backup verification checklist and restore drill guidance
@@ -330,4 +330,4 @@ QOL subfeatures:
## Near-term priority order ## Near-term priority order
1. Better session filtering, review history, and unusual-access cues for operational admins 1. Better session filtering, review history, and unusual-access cues for operational admins
2. Extend destructive-action safety coverage across sales, purchasing, shipping, and project workflows 2. Extend destructive-action safety coverage into remaining project and form-edit removal workflows

View File

@@ -7,6 +7,7 @@ import { useEffect, useState } from "react";
import { Link, useParams } from "react-router-dom"; import { Link, useParams } from "react-router-dom";
import { useAuth } from "../../auth/AuthProvider"; import { useAuth } from "../../auth/AuthProvider";
import { ConfirmActionDialog } from "../../components/ConfirmActionDialog";
import { FileAttachmentsPanel } from "../../components/FileAttachmentsPanel"; import { FileAttachmentsPanel } from "../../components/FileAttachmentsPanel";
import { api, ApiError } from "../../lib/api"; import { api, ApiError } from "../../lib/api";
import { emptyPurchaseReceiptInput, purchaseStatusOptions } from "./config"; import { emptyPurchaseReceiptInput, purchaseStatusOptions } from "./config";
@@ -25,6 +26,20 @@ export function PurchaseDetailPage() {
const [isUpdatingStatus, setIsUpdatingStatus] = useState(false); const [isUpdatingStatus, setIsUpdatingStatus] = useState(false);
const [isOpeningPdf, setIsOpeningPdf] = useState(false); const [isOpeningPdf, setIsOpeningPdf] = useState(false);
const [planningRollup, setPlanningRollup] = useState<DemandPlanningRollupDto | null>(null); 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 canManage = user?.permissions.includes("purchasing.write") ?? false;
const canReceive = canManage && (user?.permissions.includes(permissions.inventoryWrite) ?? 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) { if (!token) {
return; return;
} }
@@ -118,7 +133,7 @@ export function PurchaseDetailPage() {
try { try {
const nextDocument = await api.updatePurchaseOrderStatus(token, activeDocument.id, nextStatus); const nextDocument = await api.updatePurchaseOrderStatus(token, activeDocument.id, nextStatus);
setDocument(nextDocument); 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) { } catch (error: unknown) {
const message = error instanceof ApiError ? error.message : "Unable to update purchase order status."; const message = error instanceof ApiError ? error.message : "Unable to update purchase order status.";
setStatus(message); setStatus(message);
@@ -127,8 +142,7 @@ export function PurchaseDetailPage() {
} }
} }
async function handleReceiptSubmit(event: React.FormEvent<HTMLFormElement>) { async function applyReceipt() {
event.preventDefault();
if (!token || !canReceive) { if (!token || !canReceive) {
return; return;
} }
@@ -155,7 +169,7 @@ export function PurchaseDetailPage() {
receivedAt: new Date().toISOString(), receivedAt: new Date().toISOString(),
notes: "", 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."); setStatus("Purchase order updated after receipt.");
} catch (error: unknown) { } catch (error: unknown) {
const message = error instanceof ApiError ? error.message : "Unable to record purchase receipt."; 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() { async function handleOpenPdf() {
if (!token) { if (!token) {
return; return;
@@ -464,6 +511,41 @@ export function PurchaseDetailPage() {
emptyMessage="No vendor supporting documents have been uploaded for this purchase order yet." 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> <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> </section>
); );
} }

View File

@@ -6,6 +6,7 @@ import { Link, useNavigate, useParams } from "react-router-dom";
import { useAuth } from "../../auth/AuthProvider"; import { useAuth } from "../../auth/AuthProvider";
import { api, ApiError } from "../../lib/api"; import { api, ApiError } from "../../lib/api";
import { ConfirmActionDialog } from "../../components/ConfirmActionDialog";
import { salesConfigs, salesStatusOptions, type SalesDocumentEntity } from "./config"; import { salesConfigs, salesStatusOptions, type SalesDocumentEntity } from "./config";
import { SalesStatusBadge } from "./SalesStatusBadge"; import { SalesStatusBadge } from "./SalesStatusBadge";
import { ShipmentStatusBadge } from "../shipping/ShipmentStatusBadge"; import { ShipmentStatusBadge } from "../shipping/ShipmentStatusBadge";
@@ -59,6 +60,20 @@ export function SalesDetailPage({ entity }: { entity: SalesDocumentEntity }) {
const [isApproving, setIsApproving] = useState(false); const [isApproving, setIsApproving] = useState(false);
const [shipments, setShipments] = useState<ShipmentSummaryDto[]>([]); const [shipments, setShipments] = useState<ShipmentSummaryDto[]>([]);
const [planning, setPlanning] = useState<SalesOrderPlanningDto | null>(null); 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 canManage = user?.permissions.includes(permissions.salesWrite) ?? false;
const canManageShipping = user?.permissions.includes(permissions.shippingWrite) ?? 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()}`; return `/purchasing/orders/new?${params.toString()}`;
} }
async function handleStatusChange(nextStatus: SalesDocumentStatus) { async function applyStatusChange(nextStatus: SalesDocumentStatus) {
if (!token) { if (!token) {
return; return;
} }
@@ -133,7 +148,7 @@ export function SalesDetailPage({ entity }: { entity: SalesDocumentEntity }) {
? await api.updateQuoteStatus(token, activeDocument.id, nextStatus) ? await api.updateQuoteStatus(token, activeDocument.id, nextStatus)
: await api.updateSalesOrderStatus(token, activeDocument.id, nextStatus); : await api.updateSalesOrderStatus(token, activeDocument.id, nextStatus);
setDocument(nextDocument); 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) { } catch (error: unknown) {
const message = error instanceof ApiError ? error.message : `Unable to update ${config.singularLabel.toLowerCase()} status.`; const message = error instanceof ApiError ? error.message : `Unable to update ${config.singularLabel.toLowerCase()} status.`;
setStatus(message); setStatus(message);
@@ -142,7 +157,7 @@ export function SalesDetailPage({ entity }: { entity: SalesDocumentEntity }) {
} }
} }
async function handleConvert() { async function applyConvert() {
if (!token || entity !== "quote") { if (!token || entity !== "quote") {
return; return;
} }
@@ -185,7 +200,7 @@ export function SalesDetailPage({ entity }: { entity: SalesDocumentEntity }) {
} }
} }
async function handleApprove() { async function applyApprove() {
if (!token) { if (!token) {
return; return;
} }
@@ -197,7 +212,7 @@ export function SalesDetailPage({ entity }: { entity: SalesDocumentEntity }) {
const nextDocument = const nextDocument =
entity === "quote" ? await api.approveQuote(token, activeDocument.id) : await api.approveSalesOrder(token, activeDocument.id); entity === "quote" ? await api.approveQuote(token, activeDocument.id) : await api.approveSalesOrder(token, activeDocument.id);
setDocument(nextDocument); 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) { } catch (error: unknown) {
const message = error instanceof ApiError ? error.message : `Unable to approve ${config.singularLabel.toLowerCase()}.`; const message = error instanceof ApiError ? error.message : `Unable to approve ${config.singularLabel.toLowerCase()}.`;
setStatus(message); 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 ( return (
<section className="space-y-4"> <section className="space-y-4">
<div className="rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5"> <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> </section>
) : null} ) : 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> </section>
); );
} }

View File

@@ -5,6 +5,7 @@ import { Link, useParams } from "react-router-dom";
import { useAuth } from "../../auth/AuthProvider"; import { useAuth } from "../../auth/AuthProvider";
import { api, ApiError } from "../../lib/api"; import { api, ApiError } from "../../lib/api";
import { ConfirmActionDialog } from "../../components/ConfirmActionDialog";
import { FileAttachmentsPanel } from "../../components/FileAttachmentsPanel"; import { FileAttachmentsPanel } from "../../components/FileAttachmentsPanel";
import { shipmentStatusOptions } from "./config"; import { shipmentStatusOptions } from "./config";
import { ShipmentStatusBadge } from "./ShipmentStatusBadge"; import { ShipmentStatusBadge } from "./ShipmentStatusBadge";
@@ -17,6 +18,19 @@ export function ShipmentDetailPage() {
const [status, setStatus] = useState("Loading shipment..."); const [status, setStatus] = useState("Loading shipment...");
const [isUpdatingStatus, setIsUpdatingStatus] = useState(false); const [isUpdatingStatus, setIsUpdatingStatus] = useState(false);
const [activeDocumentAction, setActiveDocumentAction] = useState<"packing-slip" | "label" | "bol" | null>(null); const [activeDocumentAction, setActiveDocumentAction] = useState<"packing-slip" | "label" | "bol" | null>(null);
const [pendingConfirmation, setPendingConfirmation] = useState<
| {
title: string;
description: string;
impact: string;
recovery: string;
confirmLabel: string;
confirmationLabel?: string;
confirmationValue?: string;
nextStatus: ShipmentStatus;
}
| null
>(null);
const canManage = user?.permissions.includes(permissions.shippingWrite) ?? false; const canManage = user?.permissions.includes(permissions.shippingWrite) ?? false;
@@ -38,7 +52,7 @@ export function ShipmentDetailPage() {
}); });
}, [shipmentId, token]); }, [shipmentId, token]);
async function handleStatusChange(nextStatus: ShipmentStatus) { async function applyStatusChange(nextStatus: ShipmentStatus) {
if (!token || !shipment) { if (!token || !shipment) {
return; return;
} }
@@ -48,7 +62,7 @@ export function ShipmentDetailPage() {
try { try {
const nextShipment = await api.updateShipmentStatus(token, shipment.id, nextStatus); const nextShipment = await api.updateShipmentStatus(token, shipment.id, nextStatus);
setShipment(nextShipment); setShipment(nextShipment);
setStatus("Shipment status updated."); setStatus("Shipment status updated. Verify carrier paperwork and sales-order expectations if the shipment moved into a terminal state.");
} catch (error: unknown) { } catch (error: unknown) {
const message = error instanceof ApiError ? error.message : "Unable to update shipment status."; const message = error instanceof ApiError ? error.message : "Unable to update shipment status.";
setStatus(message); setStatus(message);
@@ -57,6 +71,29 @@ export function ShipmentDetailPage() {
} }
} }
function handleStatusChange(nextStatus: ShipmentStatus) {
if (!shipment) {
return;
}
const label = shipmentStatusOptions.find((option) => option.value === nextStatus)?.label ?? nextStatus;
setPendingConfirmation({
title: `Set shipment to ${label}`,
description: `Update shipment ${shipment.shipmentNumber} from ${shipment.status} to ${nextStatus}.`,
impact:
nextStatus === "DELIVERED"
? "This marks delivery complete and can affect customer communication and project/shipping readiness views."
: nextStatus === "SHIPPED"
? "This marks the shipment as outbound and can trigger customer-facing tracking and downstream delivery expectations."
: "This changes the logistics state used by related shipping and sales workflows.",
recovery: "If the status is wrong, return the shipment to the correct state and confirm the linked sales order still reflects reality.",
confirmLabel: `Set ${label}`,
confirmationLabel: nextStatus === "DELIVERED" ? "Type shipment number to confirm:" : undefined,
confirmationValue: nextStatus === "DELIVERED" ? shipment.shipmentNumber : undefined,
nextStatus,
});
}
async function handleOpenDocument(kind: "packing-slip" | "label" | "bol") { async function handleOpenDocument(kind: "packing-slip" | "label" | "bol") {
if (!token || !shipment) { if (!token || !shipment) {
return; return;
@@ -207,6 +244,30 @@ export function ShipmentDetailPage() {
description="Store carrier paperwork, signed delivery records, bills of lading, and related logistics support files on the shipment record." description="Store carrier paperwork, signed delivery records, bills of lading, and related logistics support files on the shipment record."
emptyMessage="No logistics attachments have been uploaded for this shipment yet." emptyMessage="No logistics attachments have been uploaded for this shipment yet."
/> />
<ConfirmActionDialog
open={pendingConfirmation != null}
title={pendingConfirmation?.title ?? "Confirm shipment action"}
description={pendingConfirmation?.description ?? ""}
impact={pendingConfirmation?.impact}
recovery={pendingConfirmation?.recovery}
confirmLabel={pendingConfirmation?.confirmLabel ?? "Confirm"}
confirmationLabel={pendingConfirmation?.confirmationLabel}
confirmationValue={pendingConfirmation?.confirmationValue}
isConfirming={isUpdatingStatus}
onClose={() => {
if (!isUpdatingStatus) {
setPendingConfirmation(null);
}
}}
onConfirm={async () => {
if (!pendingConfirmation) {
return;
}
await applyStatusChange(pendingConfirmation.nextStatus);
setPendingConfirmation(null);
}}
/>
</section> </section>
); );
} }