workbench rebalance
This commit is contained in:
@@ -10,8 +10,6 @@ import type {
|
||||
import { prisma } from "../../lib/prisma.js";
|
||||
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
const SHIFT_MINUTES_PER_DAY = 8 * 60;
|
||||
|
||||
type PlanningProjectRecord = {
|
||||
id: string;
|
||||
projectNumber: string;
|
||||
@@ -59,7 +57,7 @@ type PlanningWorkOrderRecord = {
|
||||
plannedStart: Date;
|
||||
plannedEnd: Date;
|
||||
plannedMinutes: number;
|
||||
station: { id: string; code: string; name: string };
|
||||
station: { id: string; code: string; name: string; dailyCapacityMinutes: number; parallelCapacity: number; workingDays: string };
|
||||
}>;
|
||||
materialIssues: Array<{ componentItemId: string; quantity: number }>;
|
||||
};
|
||||
@@ -88,6 +86,9 @@ type StationAccumulator = {
|
||||
readyCount: number;
|
||||
lateCount: number;
|
||||
dayKeys: Set<string>;
|
||||
dailyCapacityMinutes: number;
|
||||
parallelCapacity: number;
|
||||
workingDays: number[];
|
||||
};
|
||||
|
||||
function clampProgress(value: number) {
|
||||
@@ -153,6 +154,14 @@ function encodeQuery(params: Record<string, string | null | undefined>) {
|
||||
return query.length > 0 ? `?${query}` : "";
|
||||
}
|
||||
|
||||
function parseWorkingDays(value: string) {
|
||||
const parsed = value
|
||||
.split(",")
|
||||
.map((entry) => Number.parseInt(entry.trim(), 10))
|
||||
.filter((entry) => Number.isInteger(entry) && entry >= 0 && entry <= 6);
|
||||
return parsed.length > 0 ? parsed : [1, 2, 3, 4, 5];
|
||||
}
|
||||
|
||||
function isBuildItem(type: string) {
|
||||
return type === "ASSEMBLY" || type === "MANUFACTURED";
|
||||
}
|
||||
@@ -166,7 +175,7 @@ function getAvailabilityKey(itemId: string, warehouseId: string, locationId: str
|
||||
}
|
||||
|
||||
function createStationLoad(record: StationAccumulator): PlanningStationLoadDto {
|
||||
const capacityMinutes = Math.max(record.dayKeys.size, 1) * SHIFT_MINUTES_PER_DAY;
|
||||
const capacityMinutes = Math.max(record.dayKeys.size, 1) * Math.max(record.dailyCapacityMinutes, 60) * Math.max(record.parallelCapacity, 1);
|
||||
const utilizationPercent = capacityMinutes > 0 ? Math.round((record.totalPlannedMinutes / capacityMinutes) * 100) : 0;
|
||||
return {
|
||||
stationId: record.stationId,
|
||||
@@ -299,7 +308,7 @@ export async function getPlanningTimeline(): Promise<PlanningTimelineDto> {
|
||||
plannedStart: true,
|
||||
plannedEnd: true,
|
||||
plannedMinutes: true,
|
||||
station: { select: { id: true, code: true, name: true } },
|
||||
station: { select: { id: true, code: true, name: true, dailyCapacityMinutes: true, parallelCapacity: true, workingDays: true } },
|
||||
},
|
||||
orderBy: [{ sequence: "asc" }],
|
||||
},
|
||||
@@ -489,6 +498,9 @@ export async function getPlanningTimeline(): Promise<PlanningTimelineDto> {
|
||||
readyCount: 0,
|
||||
lateCount: 0,
|
||||
dayKeys: new Set<string>(),
|
||||
dailyCapacityMinutes: operation.station.dailyCapacityMinutes,
|
||||
parallelCapacity: operation.station.parallelCapacity,
|
||||
workingDays: parseWorkingDays(operation.station.workingDays),
|
||||
};
|
||||
current.operationCount += 1;
|
||||
current.workOrderIds.add(workOrder.id);
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
listWorkOrders,
|
||||
recordWorkOrderCompletion,
|
||||
updateWorkOrder,
|
||||
updateWorkOrderOperationSchedule,
|
||||
updateWorkOrderStatus,
|
||||
} from "./service.js";
|
||||
|
||||
@@ -24,6 +25,9 @@ const stationSchema = z.object({
|
||||
name: z.string().trim().min(1).max(160),
|
||||
description: z.string(),
|
||||
queueDays: z.number().int().min(0).max(365),
|
||||
dailyCapacityMinutes: z.number().int().min(60).max(1440),
|
||||
parallelCapacity: z.number().int().min(1).max(24),
|
||||
workingDays: z.array(z.number().int().min(0).max(6)).min(1).max(7),
|
||||
isActive: z.boolean(),
|
||||
});
|
||||
|
||||
@@ -64,6 +68,11 @@ const completionSchema = z.object({
|
||||
notes: z.string(),
|
||||
});
|
||||
|
||||
const operationScheduleSchema = z.object({
|
||||
plannedStart: z.string().datetime(),
|
||||
stationId: z.string().trim().min(1).nullable().optional(),
|
||||
});
|
||||
|
||||
function getRouteParam(value: unknown) {
|
||||
return typeof value === "string" ? value : null;
|
||||
}
|
||||
@@ -166,6 +175,26 @@ manufacturingRouter.patch("/work-orders/:workOrderId/status", requirePermissions
|
||||
return ok(response, result.workOrder);
|
||||
});
|
||||
|
||||
manufacturingRouter.patch("/work-orders/:workOrderId/operations/:operationId/schedule", requirePermissions([permissions.manufacturingWrite]), async (request, response) => {
|
||||
const workOrderId = getRouteParam(request.params.workOrderId);
|
||||
const operationId = getRouteParam(request.params.operationId);
|
||||
if (!workOrderId || !operationId) {
|
||||
return fail(response, 400, "INVALID_INPUT", "Work-order or operation id is invalid.");
|
||||
}
|
||||
|
||||
const parsed = operationScheduleSchema.safeParse(request.body);
|
||||
if (!parsed.success) {
|
||||
return fail(response, 400, "INVALID_INPUT", "Operation schedule payload is invalid.");
|
||||
}
|
||||
|
||||
const result = await updateWorkOrderOperationSchedule(workOrderId, operationId, parsed.data, request.authUser?.id);
|
||||
if (!result.ok) {
|
||||
return fail(response, 400, "INVALID_INPUT", result.reason);
|
||||
}
|
||||
|
||||
return ok(response, result.workOrder);
|
||||
});
|
||||
|
||||
manufacturingRouter.post("/work-orders/:workOrderId/issues", requirePermissions([permissions.manufacturingWrite]), async (request, response) => {
|
||||
const workOrderId = getRouteParam(request.params.workOrderId);
|
||||
if (!workOrderId) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
WorkOrderDetailDto,
|
||||
WorkOrderInput,
|
||||
WorkOrderOperationDto,
|
||||
WorkOrderOperationScheduleInput,
|
||||
WorkOrderMaterialIssueInput,
|
||||
WorkOrderStatus,
|
||||
WorkOrderSummaryDto,
|
||||
@@ -23,6 +24,9 @@ type StationRecord = {
|
||||
name: string;
|
||||
description: string;
|
||||
queueDays: number;
|
||||
dailyCapacityMinutes: number;
|
||||
parallelCapacity: number;
|
||||
workingDays: string;
|
||||
isActive: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
@@ -55,6 +59,9 @@ type WorkOrderRecord = {
|
||||
code: string;
|
||||
name: string;
|
||||
queueDays: number;
|
||||
dailyCapacityMinutes: number;
|
||||
parallelCapacity: number;
|
||||
workingDays: string;
|
||||
};
|
||||
}>;
|
||||
bomLines: Array<{
|
||||
@@ -106,6 +113,9 @@ type WorkOrderRecord = {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
dailyCapacityMinutes: number;
|
||||
parallelCapacity: number;
|
||||
workingDays: string;
|
||||
};
|
||||
}>;
|
||||
materialIssues: Array<{
|
||||
@@ -146,12 +156,16 @@ type WorkOrderRecord = {
|
||||
};
|
||||
|
||||
function mapStation(record: StationRecord): ManufacturingStationDto {
|
||||
const workingDays = record.workingDays.split(",").map((value) => Number.parseInt(value, 10)).filter((value) => Number.isInteger(value) && value >= 0 && value <= 6);
|
||||
return {
|
||||
id: record.id,
|
||||
code: record.code,
|
||||
name: record.name,
|
||||
description: record.description,
|
||||
queueDays: record.queueDays,
|
||||
dailyCapacityMinutes: record.dailyCapacityMinutes,
|
||||
parallelCapacity: record.parallelCapacity,
|
||||
workingDays: workingDays.length > 0 ? workingDays : [1, 2, 3, 4, 5],
|
||||
isActive: record.isActive,
|
||||
createdAt: record.createdAt.toISOString(),
|
||||
updatedAt: record.updatedAt.toISOString(),
|
||||
@@ -170,6 +184,9 @@ function buildInclude() {
|
||||
code: true,
|
||||
name: true,
|
||||
queueDays: true,
|
||||
dailyCapacityMinutes: true,
|
||||
parallelCapacity: true,
|
||||
workingDays: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -230,6 +247,9 @@ function buildInclude() {
|
||||
id: true,
|
||||
code: true,
|
||||
name: true,
|
||||
dailyCapacityMinutes: true,
|
||||
parallelCapacity: true,
|
||||
workingDays: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -337,6 +357,9 @@ function mapDetail(
|
||||
stationId: operation.station.id,
|
||||
stationCode: operation.station.code,
|
||||
stationName: operation.station.name,
|
||||
stationDailyCapacityMinutes: operation.station.dailyCapacityMinutes,
|
||||
stationParallelCapacity: operation.station.parallelCapacity,
|
||||
stationWorkingDays: parseWorkingDays(operation.station.workingDays),
|
||||
sequence: operation.sequence,
|
||||
setupMinutes: operation.setupMinutes,
|
||||
runMinutesPerUnit: operation.runMinutesPerUnit,
|
||||
@@ -400,6 +423,68 @@ function addMinutes(value: Date, minutes: number) {
|
||||
return new Date(value.getTime() + minutes * 60 * 1000);
|
||||
}
|
||||
|
||||
function parseWorkingDays(value: string) {
|
||||
const parsed = value
|
||||
.split(",")
|
||||
.map((entry) => Number.parseInt(entry.trim(), 10))
|
||||
.filter((entry) => Number.isInteger(entry) && entry >= 0 && entry <= 6);
|
||||
return parsed.length > 0 ? parsed : [1, 2, 3, 4, 5];
|
||||
}
|
||||
|
||||
function normalizeStationWorkingDays(value: number[]) {
|
||||
const normalized = [...new Set(value.filter((entry) => Number.isInteger(entry) && entry >= 0 && entry <= 6))].sort((left, right) => left - right);
|
||||
return normalized.length > 0 ? normalized : [1, 2, 3, 4, 5];
|
||||
}
|
||||
|
||||
function alignToWorkingWindow(value: Date, workingDays: number[]) {
|
||||
let next = new Date(value);
|
||||
while (!workingDays.includes(next.getDay())) {
|
||||
next = new Date(next.getFullYear(), next.getMonth(), next.getDate() + 1, 8, 0, 0, 0);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function addWorkingMinutes(
|
||||
start: Date,
|
||||
minutes: number,
|
||||
station: { dailyCapacityMinutes: number; parallelCapacity: number; workingDays: string; queueDays: number }
|
||||
) {
|
||||
const workingDays = parseWorkingDays(station.workingDays);
|
||||
const dailyCapacityMinutes = Math.max(station.dailyCapacityMinutes * Math.max(station.parallelCapacity, 1), 60);
|
||||
let cursor = alignToWorkingWindow(start, workingDays);
|
||||
let remaining = Math.max(minutes, 0);
|
||||
|
||||
while (remaining > 0) {
|
||||
const dayStart = new Date(cursor.getFullYear(), cursor.getMonth(), cursor.getDate(), 8, 0, 0, 0);
|
||||
const dayEnd = addMinutes(dayStart, dailyCapacityMinutes);
|
||||
const effectiveStart = cursor < dayStart ? dayStart : cursor;
|
||||
const availableToday = Math.max(Math.round((dayEnd.getTime() - effectiveStart.getTime()) / 60000), 0);
|
||||
|
||||
if (availableToday <= 0) {
|
||||
cursor = alignToWorkingWindow(new Date(cursor.getFullYear(), cursor.getMonth(), cursor.getDate() + 1, 8, 0, 0, 0), workingDays);
|
||||
continue;
|
||||
}
|
||||
|
||||
const consumed = Math.min(availableToday, remaining);
|
||||
cursor = addMinutes(effectiveStart, consumed);
|
||||
remaining -= consumed;
|
||||
|
||||
if (remaining > 0) {
|
||||
cursor = alignToWorkingWindow(new Date(cursor.getFullYear(), cursor.getMonth(), cursor.getDate() + 1, 8, 0, 0, 0), workingDays);
|
||||
}
|
||||
}
|
||||
|
||||
if (station.queueDays > 0) {
|
||||
let queued = new Date(cursor);
|
||||
for (let day = 0; day < station.queueDays; day += 1) {
|
||||
queued = alignToWorkingWindow(new Date(queued.getFullYear(), queued.getMonth(), queued.getDate() + 1, queued.getHours(), queued.getMinutes(), 0, 0), workingDays);
|
||||
}
|
||||
return queued;
|
||||
}
|
||||
|
||||
return cursor;
|
||||
}
|
||||
|
||||
function shouldReserveForStatus(status: string) {
|
||||
return status === "RELEASED" || status === "IN_PROGRESS" || status === "ON_HOLD";
|
||||
}
|
||||
@@ -415,7 +500,7 @@ function buildWorkOrderOperationPlan(
|
||||
}
|
||||
|
||||
const operationDurations = itemOperations.map((operation) => {
|
||||
const plannedMinutes = Math.max(operation.setupMinutes + operation.runMinutesPerUnit * quantity + operation.moveMinutes + operation.station.queueDays * 8 * 60, 1);
|
||||
const plannedMinutes = Math.max(operation.setupMinutes + operation.runMinutesPerUnit * quantity + operation.moveMinutes, 1);
|
||||
return {
|
||||
stationId: operation.station.id,
|
||||
sequence: operation.position,
|
||||
@@ -424,33 +509,17 @@ function buildWorkOrderOperationPlan(
|
||||
moveMinutes: operation.moveMinutes,
|
||||
plannedMinutes,
|
||||
notes: operation.notes,
|
||||
station: operation.station,
|
||||
};
|
||||
});
|
||||
|
||||
if (dueDate) {
|
||||
let nextEnd = new Date(dueDate);
|
||||
return operationDurations
|
||||
.slice()
|
||||
.sort((left, right) => right.sequence - left.sequence)
|
||||
.map((operation) => {
|
||||
const plannedStart = addMinutes(nextEnd, -operation.plannedMinutes);
|
||||
const planned = {
|
||||
...operation,
|
||||
plannedStart,
|
||||
plannedEnd: nextEnd,
|
||||
};
|
||||
nextEnd = plannedStart;
|
||||
return planned;
|
||||
})
|
||||
.reverse();
|
||||
}
|
||||
|
||||
let nextStart = new Date(fallbackStart);
|
||||
let nextStart = new Date(dueDate ?? fallbackStart);
|
||||
return operationDurations.map((operation) => {
|
||||
const plannedEnd = addMinutes(nextStart, operation.plannedMinutes);
|
||||
const plannedStart = alignToWorkingWindow(nextStart, parseWorkingDays(operation.station.workingDays));
|
||||
const plannedEnd = addWorkingMinutes(plannedStart, operation.plannedMinutes, operation.station);
|
||||
const planned = {
|
||||
...operation,
|
||||
plannedStart: nextStart,
|
||||
plannedStart,
|
||||
plannedEnd,
|
||||
};
|
||||
nextStart = plannedEnd;
|
||||
@@ -501,6 +570,70 @@ async function regenerateWorkOrderOperations(workOrderId: string) {
|
||||
});
|
||||
}
|
||||
|
||||
async function rescheduleWorkOrderOperations(workOrderId: string, operationId: string, plannedStart: Date) {
|
||||
const workOrder = await workOrderModel.findUnique({
|
||||
where: { id: workOrderId },
|
||||
include: buildInclude(),
|
||||
});
|
||||
|
||||
if (!workOrder) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const operations = (workOrder as WorkOrderRecord).operations;
|
||||
const anchorIndex = operations.findIndex((operation) => operation.id === operationId);
|
||||
if (anchorIndex < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return rescheduleWorkOrderOperationsToStation(workOrderId, operations, anchorIndex, plannedStart, null);
|
||||
}
|
||||
|
||||
async function rescheduleWorkOrderOperationsToStation(
|
||||
workOrderId: string,
|
||||
operations: WorkOrderRecord["operations"],
|
||||
anchorIndex: number,
|
||||
plannedStart: Date,
|
||||
targetStation: Pick<StationRecord, "id" | "queueDays" | "dailyCapacityMinutes" | "parallelCapacity" | "workingDays"> | null
|
||||
) {
|
||||
let nextStart = plannedStart;
|
||||
const updates = operations.slice(anchorIndex).map((operation) => {
|
||||
const station = operation.id === operations[anchorIndex]?.id && targetStation
|
||||
? targetStation
|
||||
: {
|
||||
id: operation.station.id,
|
||||
dailyCapacityMinutes: operation.station.dailyCapacityMinutes,
|
||||
parallelCapacity: operation.station.parallelCapacity,
|
||||
workingDays: operation.station.workingDays,
|
||||
queueDays: 0,
|
||||
};
|
||||
const alignedStart = alignToWorkingWindow(nextStart, parseWorkingDays(station.workingDays));
|
||||
const plannedEnd = addWorkingMinutes(alignedStart, operation.plannedMinutes, station);
|
||||
nextStart = plannedEnd;
|
||||
return {
|
||||
id: operation.id,
|
||||
stationId: station.id,
|
||||
plannedStart: alignedStart,
|
||||
plannedEnd,
|
||||
};
|
||||
});
|
||||
|
||||
await prisma.$transaction(
|
||||
updates.map((update) =>
|
||||
prisma.workOrderOperation.update({
|
||||
where: { id: update.id },
|
||||
data: {
|
||||
stationId: update.stationId,
|
||||
plannedStart: update.plannedStart,
|
||||
plannedEnd: update.plannedEnd,
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
return getWorkOrderById(workOrderId);
|
||||
}
|
||||
|
||||
async function syncWorkOrderReservations(workOrderId: string) {
|
||||
const workOrder = await getWorkOrderById(workOrderId);
|
||||
if (!workOrder) {
|
||||
@@ -760,12 +893,16 @@ export async function listManufacturingStations(): Promise<ManufacturingStationD
|
||||
}
|
||||
|
||||
export async function createManufacturingStation(payload: ManufacturingStationInput, actorId?: string | null) {
|
||||
const workingDays = normalizeStationWorkingDays(payload.workingDays);
|
||||
const station = await prisma.manufacturingStation.create({
|
||||
data: {
|
||||
code: payload.code.trim(),
|
||||
name: payload.name.trim(),
|
||||
description: payload.description,
|
||||
queueDays: payload.queueDays,
|
||||
dailyCapacityMinutes: payload.dailyCapacityMinutes,
|
||||
parallelCapacity: payload.parallelCapacity,
|
||||
workingDays: workingDays.join(","),
|
||||
isActive: payload.isActive,
|
||||
},
|
||||
});
|
||||
@@ -777,12 +914,15 @@ export async function createManufacturingStation(payload: ManufacturingStationIn
|
||||
action: "created",
|
||||
summary: `Created manufacturing station ${station.code}.`,
|
||||
metadata: {
|
||||
code: station.code,
|
||||
name: station.name,
|
||||
queueDays: station.queueDays,
|
||||
isActive: station.isActive,
|
||||
},
|
||||
});
|
||||
code: station.code,
|
||||
name: station.name,
|
||||
queueDays: station.queueDays,
|
||||
dailyCapacityMinutes: station.dailyCapacityMinutes,
|
||||
parallelCapacity: station.parallelCapacity,
|
||||
workingDays,
|
||||
isActive: station.isActive,
|
||||
},
|
||||
});
|
||||
|
||||
return mapStation(station);
|
||||
}
|
||||
@@ -1017,6 +1157,110 @@ export async function updateWorkOrderStatus(workOrderId: string, status: WorkOrd
|
||||
return workOrder ? { ok: true as const, workOrder } : { ok: false as const, reason: "Unable to load saved work order." };
|
||||
}
|
||||
|
||||
export async function updateWorkOrderOperationSchedule(
|
||||
workOrderId: string,
|
||||
operationId: string,
|
||||
payload: WorkOrderOperationScheduleInput,
|
||||
actorId?: string | null
|
||||
) {
|
||||
const existing = await prisma.workOrderOperation.findUnique({
|
||||
where: { id: operationId },
|
||||
select: {
|
||||
id: true,
|
||||
workOrderId: true,
|
||||
sequence: true,
|
||||
stationId: true,
|
||||
station: {
|
||||
select: {
|
||||
code: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!existing || existing.workOrderId !== workOrderId) {
|
||||
return { ok: false as const, reason: "Work-order operation was not found." };
|
||||
}
|
||||
|
||||
const workOrder = await getWorkOrderById(workOrderId);
|
||||
if (!workOrder) {
|
||||
return { ok: false as const, reason: "Work order was not found." };
|
||||
}
|
||||
|
||||
if (workOrder.status === "COMPLETE" || workOrder.status === "CANCELLED") {
|
||||
return { ok: false as const, reason: "Completed or cancelled work orders cannot be rescheduled." };
|
||||
}
|
||||
|
||||
let targetStation:
|
||||
| Pick<StationRecord, "id" | "queueDays" | "dailyCapacityMinutes" | "parallelCapacity" | "workingDays">
|
||||
| null = null;
|
||||
if (payload.stationId && payload.stationId !== existing.stationId) {
|
||||
const station = await prisma.manufacturingStation.findUnique({
|
||||
where: { id: payload.stationId },
|
||||
select: {
|
||||
id: true,
|
||||
queueDays: true,
|
||||
dailyCapacityMinutes: true,
|
||||
parallelCapacity: true,
|
||||
workingDays: true,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!station || !station.isActive) {
|
||||
return { ok: false as const, reason: "Selected manufacturing station was not found or is inactive." };
|
||||
}
|
||||
|
||||
targetStation = station;
|
||||
}
|
||||
|
||||
const workOrderRecord = await workOrderModel.findUnique({
|
||||
where: { id: workOrderId },
|
||||
include: buildInclude(),
|
||||
});
|
||||
if (!workOrderRecord) {
|
||||
return { ok: false as const, reason: "Work order was not found." };
|
||||
}
|
||||
|
||||
const operations = (workOrderRecord as WorkOrderRecord).operations;
|
||||
const anchorIndex = operations.findIndex((operation) => operation.id === operationId);
|
||||
if (anchorIndex < 0) {
|
||||
return { ok: false as const, reason: "Work-order operation was not found." };
|
||||
}
|
||||
|
||||
const rescheduled = await rescheduleWorkOrderOperationsToStation(
|
||||
workOrderId,
|
||||
operations,
|
||||
anchorIndex,
|
||||
new Date(payload.plannedStart),
|
||||
targetStation
|
||||
);
|
||||
if (!rescheduled) {
|
||||
return { ok: false as const, reason: "Unable to reschedule the requested operation." };
|
||||
}
|
||||
|
||||
await logAuditEvent({
|
||||
actorId,
|
||||
entityType: "work-order",
|
||||
entityId: workOrderId,
|
||||
action: "operations.rescheduled",
|
||||
summary: `Rescheduled work order ${rescheduled.workOrderNumber} from operation ${existing.sequence}.`,
|
||||
metadata: {
|
||||
workOrderNumber: rescheduled.workOrderNumber,
|
||||
operationId,
|
||||
sequence: existing.sequence,
|
||||
plannedStart: payload.plannedStart,
|
||||
previousStationId: existing.stationId,
|
||||
previousStationCode: existing.station.code,
|
||||
previousStationName: existing.station.name,
|
||||
stationId: payload.stationId ?? existing.stationId,
|
||||
},
|
||||
});
|
||||
|
||||
return { ok: true as const, workOrder: rescheduled };
|
||||
}
|
||||
|
||||
export async function issueWorkOrderMaterial(workOrderId: string, payload: WorkOrderMaterialIssueInput, createdById?: string | null) {
|
||||
const workOrder = await workOrderModel.findUnique({
|
||||
where: { id: workOrderId },
|
||||
|
||||
Reference in New Issue
Block a user