fixes
This commit is contained in:
@@ -0,0 +1 @@
|
||||
ALTER TABLE "WorkOrder" ADD COLUMN "holdReason" TEXT;
|
||||
@@ -653,6 +653,7 @@ model WorkOrder {
|
||||
warehouseId String
|
||||
locationId String
|
||||
status String
|
||||
holdReason String?
|
||||
quantity Int
|
||||
completedQuantity Int @default(0)
|
||||
dueDate DateTime?
|
||||
|
||||
@@ -59,6 +59,7 @@ const workOrderFiltersSchema = z.object({
|
||||
|
||||
const statusUpdateSchema = z.object({
|
||||
status: z.enum(workOrderStatuses),
|
||||
reason: z.string().nullable().optional(),
|
||||
});
|
||||
|
||||
const materialIssueSchema = z.object({
|
||||
@@ -215,7 +216,7 @@ manufacturingRouter.patch("/work-orders/:workOrderId/status", requirePermissions
|
||||
return fail(response, 400, "INVALID_INPUT", "Work-order status payload is invalid.");
|
||||
}
|
||||
|
||||
const result = await updateWorkOrderStatus(workOrderId, parsed.data.status, request.authUser?.id);
|
||||
const result = await updateWorkOrderStatus(workOrderId, parsed.data, request.authUser?.id);
|
||||
if (!result.ok) {
|
||||
return fail(response, 400, "INVALID_INPUT", result.reason);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import type {
|
||||
WorkOrderOperationTimerInput,
|
||||
WorkOrderMaterialIssueInput,
|
||||
WorkOrderStatus,
|
||||
WorkOrderStatusUpdateInput,
|
||||
WorkOrderSummaryDto,
|
||||
} from "@mrp/shared";
|
||||
|
||||
@@ -41,6 +42,7 @@ type WorkOrderRecord = {
|
||||
id: string;
|
||||
workOrderNumber: string;
|
||||
status: string;
|
||||
holdReason: string | null;
|
||||
quantity: number;
|
||||
completedQuantity: number;
|
||||
dueDate: Date | null;
|
||||
@@ -390,6 +392,7 @@ function mapDetail(
|
||||
return {
|
||||
...mapSummary(record),
|
||||
notes: record.notes,
|
||||
holdReason: record.holdReason,
|
||||
createdAt: record.createdAt.toISOString(),
|
||||
itemType: record.item.type,
|
||||
itemUnitOfMeasure: record.item.unitOfMeasure,
|
||||
@@ -1294,12 +1297,18 @@ export async function updateWorkOrder(workOrderId: string, payload: WorkOrderInp
|
||||
return workOrder ? { ok: true as const, workOrder } : { ok: false as const, reason: "Unable to load saved work order." };
|
||||
}
|
||||
|
||||
export async function updateWorkOrderStatus(workOrderId: string, status: WorkOrderStatus, actorId?: string | null) {
|
||||
export async function updateWorkOrderStatus(
|
||||
workOrderId: string,
|
||||
payload: WorkOrderStatusUpdateInput,
|
||||
actorId?: string | null
|
||||
) {
|
||||
const existing = await workOrderModel.findUnique({
|
||||
where: { id: workOrderId },
|
||||
select: {
|
||||
id: true,
|
||||
workOrderNumber: true,
|
||||
status: true,
|
||||
holdReason: true,
|
||||
quantity: true,
|
||||
completedQuantity: true,
|
||||
},
|
||||
@@ -1309,18 +1318,24 @@ export async function updateWorkOrderStatus(workOrderId: string, status: WorkOrd
|
||||
return { ok: false as const, reason: "Work order was not found." };
|
||||
}
|
||||
|
||||
if (existing.status === "COMPLETE" && status !== "COMPLETE") {
|
||||
if (existing.status === "COMPLETE" && payload.status !== "COMPLETE") {
|
||||
return { ok: false as const, reason: "Completed work orders cannot be reopened from quick actions." };
|
||||
}
|
||||
|
||||
if (status === "COMPLETE" && existing.completedQuantity < existing.quantity) {
|
||||
if (payload.status === "COMPLETE" && existing.completedQuantity < existing.quantity) {
|
||||
return { ok: false as const, reason: "Use the completion action to finish a work order." };
|
||||
}
|
||||
|
||||
const nextHoldReason = payload.reason?.trim() ?? "";
|
||||
if (payload.status === "ON_HOLD" && nextHoldReason.length === 0) {
|
||||
return { ok: false as const, reason: "An On Hold reason is required before the work order can be paused." };
|
||||
}
|
||||
|
||||
await workOrderModel.update({
|
||||
where: { id: workOrderId },
|
||||
data: {
|
||||
status,
|
||||
status: payload.status,
|
||||
holdReason: payload.status === "ON_HOLD" ? nextHoldReason : null,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1333,10 +1348,12 @@ export async function updateWorkOrderStatus(workOrderId: string, status: WorkOrd
|
||||
entityType: "work-order",
|
||||
entityId: workOrderId,
|
||||
action: "status.updated",
|
||||
summary: `Updated work order ${workOrder.workOrderNumber} to ${status}.`,
|
||||
summary: `Updated work order ${workOrder.workOrderNumber} to ${payload.status}.`,
|
||||
metadata: {
|
||||
workOrderNumber: workOrder.workOrderNumber,
|
||||
status,
|
||||
previousStatus: existing.status,
|
||||
status: payload.status,
|
||||
holdReason: payload.status === "ON_HOLD" ? nextHoldReason : null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
listProjectQuoteOptions,
|
||||
listProjectShipmentOptions,
|
||||
updateProject,
|
||||
updateProjectMilestoneStatus,
|
||||
} from "./service.js";
|
||||
|
||||
const projectSchema = z.object({
|
||||
@@ -51,6 +52,10 @@ const projectOptionQuerySchema = z.object({
|
||||
customerId: z.string().optional(),
|
||||
});
|
||||
|
||||
const milestoneStatusSchema = z.object({
|
||||
status: z.enum(projectMilestoneStatuses),
|
||||
});
|
||||
|
||||
function getRouteParam(value: unknown) {
|
||||
return typeof value === "string" ? value : null;
|
||||
}
|
||||
@@ -147,3 +152,23 @@ projectsRouter.put("/:projectId", requirePermissions([permissions.projectsWrite]
|
||||
|
||||
return ok(response, result.project);
|
||||
});
|
||||
|
||||
projectsRouter.patch("/:projectId/milestones/:milestoneId/status", requirePermissions([permissions.projectsWrite]), async (request, response) => {
|
||||
const projectId = getRouteParam(request.params.projectId);
|
||||
const milestoneId = getRouteParam(request.params.milestoneId);
|
||||
if (!projectId || !milestoneId) {
|
||||
return fail(response, 400, "INVALID_INPUT", "Project or milestone id is invalid.");
|
||||
}
|
||||
|
||||
const parsed = milestoneStatusSchema.safeParse(request.body);
|
||||
if (!parsed.success) {
|
||||
return fail(response, 400, "INVALID_INPUT", "Project milestone status payload is invalid.");
|
||||
}
|
||||
|
||||
const result = await updateProjectMilestoneStatus(projectId, milestoneId, parsed.data, request.authUser?.id);
|
||||
if (!result.ok) {
|
||||
return fail(response, 400, "INVALID_INPUT", result.reason);
|
||||
}
|
||||
|
||||
return ok(response, result.project);
|
||||
});
|
||||
|
||||
@@ -11,6 +11,8 @@ import type {
|
||||
ProjectInput,
|
||||
ProjectMilestoneDto,
|
||||
ProjectMilestoneInput,
|
||||
ProjectMilestoneStatus,
|
||||
ProjectMilestoneStatusUpdateInput,
|
||||
ProjectOwnerOptionDto,
|
||||
ProjectPriority,
|
||||
ProjectRollupDto,
|
||||
@@ -1266,3 +1268,60 @@ export async function updateProject(projectId: string, payload: ProjectInput, ac
|
||||
}
|
||||
return project ? { ok: true as const, project } : { ok: false as const, reason: "Unable to load saved project." };
|
||||
}
|
||||
|
||||
export async function updateProjectMilestoneStatus(
|
||||
projectId: string,
|
||||
milestoneId: string,
|
||||
payload: ProjectMilestoneStatusUpdateInput,
|
||||
actorId?: string | null
|
||||
) {
|
||||
const existing = await prisma.projectMilestone.findUnique({
|
||||
where: { id: milestoneId },
|
||||
select: {
|
||||
id: true,
|
||||
projectId: true,
|
||||
title: true,
|
||||
status: true,
|
||||
completedAt: true,
|
||||
project: {
|
||||
select: {
|
||||
id: true,
|
||||
projectNumber: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!existing || existing.projectId !== projectId) {
|
||||
return { ok: false as const, reason: "Project milestone was not found." };
|
||||
}
|
||||
|
||||
const nextStatus = payload.status as ProjectMilestoneStatus;
|
||||
await prisma.projectMilestone.update({
|
||||
where: { id: milestoneId },
|
||||
data: {
|
||||
status: nextStatus,
|
||||
completedAt: nextStatus === "COMPLETE" ? existing.completedAt ?? new Date() : null,
|
||||
},
|
||||
});
|
||||
|
||||
const project = await getProjectById(projectId);
|
||||
if (project) {
|
||||
await logAuditEvent({
|
||||
actorId,
|
||||
entityType: "project",
|
||||
entityId: projectId,
|
||||
action: "milestone.status.updated",
|
||||
summary: `Updated milestone ${existing.title} on ${existing.project.projectNumber} to ${nextStatus}.`,
|
||||
metadata: {
|
||||
projectNumber: existing.project.projectNumber,
|
||||
milestoneId,
|
||||
milestoneTitle: existing.title,
|
||||
previousStatus: existing.status,
|
||||
status: nextStatus,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return project ? { ok: true as const, project } : { ok: false as const, reason: "Unable to load saved project." };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user