cleanup
This commit is contained in:
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user