fixes
This commit is contained in:
@@ -11,6 +11,12 @@ interface ConfirmActionDialogProps {
|
||||
intent?: "danger" | "primary";
|
||||
confirmationLabel?: string;
|
||||
confirmationValue?: string;
|
||||
extraFieldLabel?: string;
|
||||
extraFieldPlaceholder?: string;
|
||||
extraFieldValue?: string;
|
||||
extraFieldRequired?: boolean;
|
||||
extraFieldMultiline?: boolean;
|
||||
onExtraFieldChange?: (value: string) => void;
|
||||
isConfirming?: boolean;
|
||||
onConfirm: () => void | Promise<void>;
|
||||
onClose: () => void;
|
||||
@@ -27,6 +33,12 @@ export function ConfirmActionDialog({
|
||||
intent = "danger",
|
||||
confirmationLabel,
|
||||
confirmationValue,
|
||||
extraFieldLabel,
|
||||
extraFieldPlaceholder,
|
||||
extraFieldValue = "",
|
||||
extraFieldRequired = false,
|
||||
extraFieldMultiline = false,
|
||||
onExtraFieldChange,
|
||||
isConfirming = false,
|
||||
onConfirm,
|
||||
onClose,
|
||||
@@ -44,7 +56,11 @@ export function ConfirmActionDialog({
|
||||
}
|
||||
|
||||
const requiresTypedConfirmation = Boolean(confirmationLabel && confirmationValue);
|
||||
const isConfirmDisabled = isConfirming || (requiresTypedConfirmation && typedValue.trim() !== confirmationValue);
|
||||
const requiresExtraField = Boolean(extraFieldLabel);
|
||||
const isConfirmDisabled =
|
||||
isConfirming ||
|
||||
(requiresTypedConfirmation && typedValue.trim() !== confirmationValue) ||
|
||||
(requiresExtraField && extraFieldRequired && extraFieldValue.trim().length === 0);
|
||||
const confirmButtonClass =
|
||||
intent === "danger"
|
||||
? "bg-red-600 text-white hover:bg-red-700"
|
||||
@@ -81,6 +97,27 @@ export function ConfirmActionDialog({
|
||||
/>
|
||||
</label>
|
||||
) : null}
|
||||
{requiresExtraField ? (
|
||||
<label className="mt-4 block">
|
||||
<span className="mb-2 block text-sm font-semibold text-text">{extraFieldLabel}</span>
|
||||
{extraFieldMultiline ? (
|
||||
<textarea
|
||||
value={extraFieldValue}
|
||||
onChange={(event) => onExtraFieldChange?.(event.target.value)}
|
||||
placeholder={extraFieldPlaceholder}
|
||||
rows={4}
|
||||
className="w-full rounded-[18px] border border-line/70 bg-page px-3 py-2 text-text outline-none"
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
value={extraFieldValue}
|
||||
onChange={(event) => onExtraFieldChange?.(event.target.value)}
|
||||
placeholder={extraFieldPlaceholder}
|
||||
className="w-full rounded-2xl border border-line/70 bg-page px-3 py-2 text-text outline-none"
|
||||
/>
|
||||
)}
|
||||
</label>
|
||||
) : null}
|
||||
<div className="mt-5 flex flex-col-reverse gap-3 sm:flex-row sm:justify-end">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -75,6 +75,7 @@ import type {
|
||||
WorkOrderOperationTimerInput,
|
||||
WorkOrderMaterialIssueInput,
|
||||
WorkOrderStatus,
|
||||
WorkOrderStatusUpdateInput,
|
||||
WorkOrderSummaryDto,
|
||||
ManufacturingUserOptionDto,
|
||||
} from "@mrp/shared";
|
||||
@@ -83,6 +84,7 @@ import type {
|
||||
ProjectDetailDto,
|
||||
ProjectDocumentOptionDto,
|
||||
ProjectInput,
|
||||
ProjectMilestoneStatusUpdateInput,
|
||||
ProjectOwnerOptionDto,
|
||||
ProjectPriority,
|
||||
ProjectShipmentOptionDto,
|
||||
@@ -601,6 +603,13 @@ export const api = {
|
||||
updateProject(token: string, projectId: string, payload: ProjectInput) {
|
||||
return request<ProjectDetailDto>(`/api/v1/projects/${projectId}`, { method: "PUT", body: JSON.stringify(payload) }, token);
|
||||
},
|
||||
updateProjectMilestoneStatus(token: string, projectId: string, milestoneId: string, payload: ProjectMilestoneStatusUpdateInput) {
|
||||
return request<ProjectDetailDto>(
|
||||
`/api/v1/projects/${projectId}/milestones/${milestoneId}/status`,
|
||||
{ method: "PATCH", body: JSON.stringify(payload) },
|
||||
token
|
||||
);
|
||||
},
|
||||
getProjectCustomerOptions(token: string) {
|
||||
return request<ProjectCustomerOptionDto[]>("/api/v1/projects/customers/options", undefined, token);
|
||||
},
|
||||
@@ -667,10 +676,10 @@ export const api = {
|
||||
updateWorkOrder(token: string, workOrderId: string, payload: WorkOrderInput) {
|
||||
return request<WorkOrderDetailDto>(`/api/v1/manufacturing/work-orders/${workOrderId}`, { method: "PUT", body: JSON.stringify(payload) }, token);
|
||||
},
|
||||
updateWorkOrderStatus(token: string, workOrderId: string, status: WorkOrderStatus) {
|
||||
updateWorkOrderStatus(token: string, workOrderId: string, payload: WorkOrderStatusUpdateInput) {
|
||||
return request<WorkOrderDetailDto>(
|
||||
`/api/v1/manufacturing/work-orders/${workOrderId}/status`,
|
||||
{ method: "PATCH", body: JSON.stringify({ status }) },
|
||||
{ method: "PATCH", body: JSON.stringify(payload) },
|
||||
token
|
||||
);
|
||||
},
|
||||
|
||||
@@ -30,6 +30,7 @@ export function WorkOrderDetailPage() {
|
||||
const [operatorOptions, setOperatorOptions] = useState<ManufacturingUserOptionDto[]>([]);
|
||||
const [issueForm, setIssueForm] = useState<WorkOrderMaterialIssueInput>(emptyMaterialIssueInput);
|
||||
const [completionForm, setCompletionForm] = useState<WorkOrderCompletionInput>(emptyCompletionInput);
|
||||
const [holdReasonDraft, setHoldReasonDraft] = useState("");
|
||||
const [status, setStatus] = useState("Loading work order...");
|
||||
const [isUpdatingStatus, setIsUpdatingStatus] = useState(false);
|
||||
const [isPostingIssue, setIsPostingIssue] = useState(false);
|
||||
@@ -121,8 +122,12 @@ export function WorkOrderDetailPage() {
|
||||
setIsUpdatingStatus(true);
|
||||
setStatus("Updating work-order status...");
|
||||
try {
|
||||
const nextWorkOrder = await api.updateWorkOrderStatus(token, workOrder.id, nextStatus);
|
||||
const nextWorkOrder = await api.updateWorkOrderStatus(token, workOrder.id, {
|
||||
status: nextStatus,
|
||||
reason: nextStatus === "ON_HOLD" ? holdReasonDraft : null,
|
||||
});
|
||||
setWorkOrder(nextWorkOrder);
|
||||
setHoldReasonDraft("");
|
||||
setStatus("Work-order status updated. Review downstream planning and shipment readiness if this change affects execution timing.");
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof ApiError ? error.message : "Unable to update work-order status.";
|
||||
@@ -332,6 +337,8 @@ export function WorkOrderDetailPage() {
|
||||
impact:
|
||||
nextStatus === "CANCELLED"
|
||||
? "Cancelling a work order can invalidate planning assumptions, reservations, and operator expectations."
|
||||
: nextStatus === "ON_HOLD"
|
||||
? "Putting a work order on hold pauses expected execution and should capture the exact blocker so planning and shop-floor review stay aligned."
|
||||
: nextStatus === "COMPLETE"
|
||||
? "Completing the work order signals execution closure and can change readiness views across the system."
|
||||
: "This changes the execution state used by planning, dashboards, and downstream operational review.",
|
||||
@@ -341,6 +348,7 @@ export function WorkOrderDetailPage() {
|
||||
confirmationValue: nextStatus === "CANCELLED" ? workOrder.workOrderNumber : undefined,
|
||||
nextStatus,
|
||||
});
|
||||
setHoldReasonDraft(nextStatus === "ON_HOLD" ? workOrder.holdReason ?? "" : "");
|
||||
}
|
||||
|
||||
function handleIssueSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||||
@@ -391,6 +399,12 @@ export function WorkOrderDetailPage() {
|
||||
<h3 className="mt-2 text-xl font-bold text-text">{workOrder.workOrderNumber}</h3>
|
||||
<p className="mt-1 text-sm text-text">{workOrder.itemSku} - {workOrder.itemName}</p>
|
||||
<div className="mt-3"><WorkOrderStatusBadge status={workOrder.status} /></div>
|
||||
{workOrder.status === "ON_HOLD" && workOrder.holdReason ? (
|
||||
<div className="mt-3 max-w-2xl rounded-[18px] border border-amber-300/60 bg-amber-50 px-3 py-3 text-sm text-amber-900">
|
||||
<div className="text-xs font-semibold uppercase tracking-[0.18em]">Current Hold Reason</div>
|
||||
<div className="mt-2 whitespace-pre-line">{workOrder.holdReason}</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Link to="/manufacturing/work-orders" className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">Back to work orders</Link>
|
||||
@@ -793,6 +807,12 @@ export function WorkOrderDetailPage() {
|
||||
confirmLabel={pendingConfirmation?.confirmLabel ?? "Confirm"}
|
||||
confirmationLabel={pendingConfirmation?.confirmationLabel}
|
||||
confirmationValue={pendingConfirmation?.confirmationValue}
|
||||
extraFieldLabel={pendingConfirmation?.kind === "status" && pendingConfirmation?.nextStatus === "ON_HOLD" ? "Hold reason" : undefined}
|
||||
extraFieldPlaceholder={pendingConfirmation?.kind === "status" && pendingConfirmation?.nextStatus === "ON_HOLD" ? "Explain the blocker forcing this work order onto hold." : undefined}
|
||||
extraFieldValue={pendingConfirmation?.kind === "status" && pendingConfirmation?.nextStatus === "ON_HOLD" ? holdReasonDraft : undefined}
|
||||
extraFieldRequired={pendingConfirmation?.kind === "status" && pendingConfirmation?.nextStatus === "ON_HOLD"}
|
||||
extraFieldMultiline={pendingConfirmation?.kind === "status" && pendingConfirmation?.nextStatus === "ON_HOLD"}
|
||||
onExtraFieldChange={pendingConfirmation?.kind === "status" && pendingConfirmation?.nextStatus === "ON_HOLD" ? setHoldReasonDraft : undefined}
|
||||
isConfirming={
|
||||
(pendingConfirmation?.kind === "status" && isUpdatingStatus) ||
|
||||
(pendingConfirmation?.kind === "issue" && isPostingIssue) ||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { permissions } from "@mrp/shared";
|
||||
import type { WorkOrderSummaryDto } from "@mrp/shared";
|
||||
import type { ProjectMilestoneStatus, WorkOrderSummaryDto } from "@mrp/shared";
|
||||
import type { ProjectDetailDto } from "@mrp/shared/dist/projects/types.js";
|
||||
import type { SalesOrderPlanningDto } from "@mrp/shared/dist/sales/types.js";
|
||||
import { useEffect, useState } from "react";
|
||||
@@ -23,6 +23,7 @@ export function ProjectDetailPage() {
|
||||
const [workOrders, setWorkOrders] = useState<WorkOrderSummaryDto[]>([]);
|
||||
const [planning, setPlanning] = useState<SalesOrderPlanningDto | null>(null);
|
||||
const [status, setStatus] = useState("Loading project...");
|
||||
const [updatingMilestoneId, setUpdatingMilestoneId] = useState<string | null>(null);
|
||||
|
||||
const canManage = user?.permissions.includes(permissions.projectsWrite) ?? false;
|
||||
|
||||
@@ -107,6 +108,50 @@ export function ProjectDetailPage() {
|
||||
? "text-amber-600 dark:text-amber-300"
|
||||
: "text-rose-600 dark:text-rose-300";
|
||||
|
||||
async function updateMilestoneStatus(milestoneId: string, nextStatus: ProjectMilestoneStatus) {
|
||||
if (!token || !project) {
|
||||
return;
|
||||
}
|
||||
|
||||
setUpdatingMilestoneId(milestoneId);
|
||||
setStatus("Updating milestone status...");
|
||||
try {
|
||||
const nextProject = await api.updateProjectMilestoneStatus(token, project.id, milestoneId, { status: nextStatus });
|
||||
setProject(nextProject);
|
||||
setStatus("Milestone status updated.");
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof ApiError ? error.message : "Unable to update milestone status.";
|
||||
setStatus(message);
|
||||
} finally {
|
||||
setUpdatingMilestoneId(null);
|
||||
}
|
||||
}
|
||||
|
||||
function milestoneQuickActions(currentStatus: ProjectMilestoneStatus) {
|
||||
if (currentStatus === "PLANNED") {
|
||||
return [
|
||||
{ status: "IN_PROGRESS" as const, label: "Start" },
|
||||
{ status: "BLOCKED" as const, label: "Block" },
|
||||
{ status: "COMPLETE" as const, label: "Complete" },
|
||||
];
|
||||
}
|
||||
if (currentStatus === "IN_PROGRESS") {
|
||||
return [
|
||||
{ status: "BLOCKED" as const, label: "Block" },
|
||||
{ status: "COMPLETE" as const, label: "Complete" },
|
||||
{ status: "PLANNED" as const, label: "Reset" },
|
||||
];
|
||||
}
|
||||
if (currentStatus === "BLOCKED") {
|
||||
return [
|
||||
{ status: "IN_PROGRESS" as const, label: "Resume" },
|
||||
{ status: "COMPLETE" as const, label: "Complete" },
|
||||
{ status: "PLANNED" as const, label: "Reset" },
|
||||
];
|
||||
}
|
||||
return [{ status: "IN_PROGRESS" as const, label: "Reopen" }];
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="space-y-4">
|
||||
<div className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
||||
@@ -289,7 +334,7 @@ export function ProjectDetailPage() {
|
||||
<div><p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Milestones</p><p className="mt-2 text-sm text-muted">Track project checkpoints, blockers, and completion progress.</p></div>
|
||||
{canManage ? <Link to={`/projects/${project.id}/edit`} className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">Edit milestones</Link> : null}
|
||||
</div>
|
||||
{project.milestones.length === 0 ? <div className="mt-6 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted">No milestones are defined for this project yet.</div> : <div className="mt-6 space-y-3">{project.milestones.map((milestone) => (<div key={milestone.id} className="rounded-[18px] border border-line/70 bg-page/60 p-3"><div className="flex flex-wrap items-start justify-between gap-3"><div className="min-w-0"><div className="font-semibold text-text">{milestone.title}</div><div className="mt-2 flex flex-wrap items-center gap-2"><span className={`inline-flex rounded-full px-2 py-1 text-xs font-semibold uppercase tracking-[0.16em] ${projectMilestoneStatusPalette[milestone.status]}`}>{milestone.status.replace("_", " ")}</span><span className="text-xs text-muted">Due {milestone.dueDate ? new Date(milestone.dueDate).toLocaleDateString() : "not scheduled"}</span>{milestone.completedAt ? <span className="text-xs text-muted">Completed {new Date(milestone.completedAt).toLocaleDateString()}</span> : null}</div>{milestone.notes ? <div className="mt-3 whitespace-pre-line text-sm text-text">{milestone.notes}</div> : null}</div></div></div>))}</div>}
|
||||
{project.milestones.length === 0 ? <div className="mt-6 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted">No milestones are defined for this project yet.</div> : <div className="mt-6 space-y-3">{project.milestones.map((milestone) => (<div key={milestone.id} className="rounded-[18px] border border-line/70 bg-page/60 p-3"><div className="flex flex-wrap items-start justify-between gap-3"><div className="min-w-0"><div className="font-semibold text-text">{milestone.title}</div><div className="mt-2 flex flex-wrap items-center gap-2"><span className={`inline-flex rounded-full px-2 py-1 text-xs font-semibold uppercase tracking-[0.16em] ${projectMilestoneStatusPalette[milestone.status]}`}>{milestone.status.replace("_", " ")}</span><span className="text-xs text-muted">Due {milestone.dueDate ? new Date(milestone.dueDate).toLocaleDateString() : "not scheduled"}</span>{milestone.completedAt ? <span className="text-xs text-muted">Completed {new Date(milestone.completedAt).toLocaleDateString()}</span> : null}</div>{milestone.notes ? <div className="mt-3 whitespace-pre-line text-sm text-text">{milestone.notes}</div> : null}</div>{canManage ? <div className="flex flex-wrap gap-2">{milestoneQuickActions(milestone.status).map((action) => (<button key={action.status} type="button" onClick={() => void updateMilestoneStatus(milestone.id, action.status)} disabled={updatingMilestoneId === milestone.id} className="rounded-2xl border border-line/70 px-2 py-2 text-xs font-semibold text-text disabled:cursor-not-allowed disabled:opacity-60">{updatingMilestoneId === milestone.id ? "Saving..." : action.label}</button>))}</div> : null}</div></div>))}</div>}
|
||||
</section>
|
||||
{planning ? (
|
||||
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
||||
|
||||
@@ -300,7 +300,7 @@ export function WorkbenchPage() {
|
||||
return;
|
||||
}
|
||||
if (action.kind === "RELEASE_WORK_ORDER" && action.workOrderId) {
|
||||
await api.updateWorkOrderStatus(token, action.workOrderId, "RELEASED");
|
||||
await api.updateWorkOrderStatus(token, action.workOrderId, { status: "RELEASED" });
|
||||
await refreshWorkbench("Workbench refreshed after release.");
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user