diff --git a/AGENTS.md b/AGENTS.md index 4883098..2223f10 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 - 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 -- 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 - 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 @@ -131,7 +131,7 @@ If implementation changes invalidate those docs, update them in the same change Near-term priorities are: 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. diff --git a/CHANGELOG.md b/CHANGELOG.md index 867261b..8f0ed8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 - 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 - 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 @@ -50,7 +51,7 @@ This file is the running release and change log for MRP Codex. Keep it updated w ### 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 - 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 diff --git a/INSTRUCTIONS.md b/INSTRUCTIONS.md index e794525..1c4f5a0 100644 --- a/INSTRUCTIONS.md +++ b/INSTRUCTIONS.md @@ -33,7 +33,7 @@ This repository implements the platform foundation milestone: - 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 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 - backup/restore guidance, richer startup diagnostics, and exportable support bundles 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 - 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 diff --git a/README.md b/README.md index e7888c6..ee19d81 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Current foundation scope includes: - 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 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 - 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 @@ -59,7 +59,7 @@ Current completed foundation areas: Near-term priorities: 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: @@ -378,7 +378,7 @@ The current admin operations slice supports: Current follow-up direction: - 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 diff --git a/ROADMAP.md b/ROADMAP.md index c17fdfd..bedd2cf 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -291,7 +291,7 @@ Foundation slice shipped: - 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 - 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 - 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 @@ -306,7 +306,7 @@ QOL subfeatures: - Admin diagnostics screen for permissions, migrations, storage, and PDF health - 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 - Support-log filtering, retention controls, and broader support-package polish - Backup verification checklist and restore drill guidance @@ -330,4 +330,4 @@ QOL subfeatures: ## Near-term priority order 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 diff --git a/client/src/modules/purchasing/PurchaseDetailPage.tsx b/client/src/modules/purchasing/PurchaseDetailPage.tsx index ccfeec2..1388636 100644 --- a/client/src/modules/purchasing/PurchaseDetailPage.tsx +++ b/client/src/modules/purchasing/PurchaseDetailPage.tsx @@ -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(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) { - 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) { + 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." />
{status}
+ { + 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); + } + }} + /> ); } diff --git a/client/src/modules/sales/SalesDetailPage.tsx b/client/src/modules/sales/SalesDetailPage.tsx index 2153d4a..5938f5d 100644 --- a/client/src/modules/sales/SalesDetailPage.tsx +++ b/client/src/modules/sales/SalesDetailPage.tsx @@ -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([]); const [planning, setPlanning] = useState(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 (
@@ -570,6 +629,48 @@ export function SalesDetailPage({ entity }: { entity: SalesDocumentEntity }) { )}
) : null} + { + 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); + } + }} + /> ); } diff --git a/client/src/modules/shipping/ShipmentDetailPage.tsx b/client/src/modules/shipping/ShipmentDetailPage.tsx index 825fe26..721ebca 100644 --- a/client/src/modules/shipping/ShipmentDetailPage.tsx +++ b/client/src/modules/shipping/ShipmentDetailPage.tsx @@ -5,6 +5,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 { FileAttachmentsPanel } from "../../components/FileAttachmentsPanel"; import { shipmentStatusOptions } from "./config"; import { ShipmentStatusBadge } from "./ShipmentStatusBadge"; @@ -17,6 +18,19 @@ export function ShipmentDetailPage() { const [status, setStatus] = useState("Loading shipment..."); const [isUpdatingStatus, setIsUpdatingStatus] = useState(false); 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; @@ -38,7 +52,7 @@ export function ShipmentDetailPage() { }); }, [shipmentId, token]); - async function handleStatusChange(nextStatus: ShipmentStatus) { + async function applyStatusChange(nextStatus: ShipmentStatus) { if (!token || !shipment) { return; } @@ -48,7 +62,7 @@ export function ShipmentDetailPage() { try { const nextShipment = await api.updateShipmentStatus(token, shipment.id, nextStatus); 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) { const message = error instanceof ApiError ? error.message : "Unable to update shipment status."; 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") { if (!token || !shipment) { 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." emptyMessage="No logistics attachments have been uploaded for this shipment yet." /> + { + if (!isUpdatingStatus) { + setPendingConfirmation(null); + } + }} + onConfirm={async () => { + if (!pendingConfirmation) { + return; + } + + await applyStatusChange(pendingConfirmation.nextStatus); + setPendingConfirmation(null); + }} + /> ); }