workbench rebalance

This commit is contained in:
2026-03-18 00:10:15 -05:00
parent 14708d7013
commit abc795b4a7
14 changed files with 640 additions and 48 deletions

View File

@@ -1,5 +1,5 @@
import { permissions } from "@mrp/shared";
import type { WorkOrderCompletionInput, WorkOrderDetailDto, WorkOrderMaterialIssueInput, WorkOrderStatus } from "@mrp/shared";
import type { WorkOrderCompletionInput, WorkOrderDetailDto, WorkOrderMaterialIssueInput, WorkOrderOperationScheduleInput, WorkOrderStatus } from "@mrp/shared";
import type { WarehouseLocationOptionDto } from "@mrp/shared/dist/inventory/types.js";
import { useEffect, useMemo, useState } from "react";
import { Link, useParams } from "react-router-dom";
@@ -22,6 +22,8 @@ export function WorkOrderDetailPage() {
const [isUpdatingStatus, setIsUpdatingStatus] = useState(false);
const [isPostingIssue, setIsPostingIssue] = useState(false);
const [isPostingCompletion, setIsPostingCompletion] = useState(false);
const [operationScheduleForm, setOperationScheduleForm] = useState<Record<string, WorkOrderOperationScheduleInput>>({});
const [reschedulingOperationId, setReschedulingOperationId] = useState<string | null>(null);
const [pendingConfirmation, setPendingConfirmation] = useState<
| {
kind: "status" | "issue" | "completion";
@@ -56,6 +58,11 @@ export function WorkOrderDetailPage() {
...emptyCompletionInput,
quantity: Math.max(nextWorkOrder.dueQuantity, 1),
});
setOperationScheduleForm(
Object.fromEntries(
nextWorkOrder.operations.map((operation) => [operation.id, { plannedStart: operation.plannedStart }])
)
);
setStatus("Work order loaded.");
})
.catch((error: unknown) => {
@@ -137,6 +144,35 @@ export function WorkOrderDetailPage() {
}
}
async function submitOperationReschedule(operationId: string) {
if (!token || !workOrder) {
return;
}
const payload = operationScheduleForm[operationId];
if (!payload?.plannedStart) {
return;
}
setReschedulingOperationId(operationId);
setStatus("Rebuilding operation schedule...");
try {
const nextWorkOrder = await api.updateWorkOrderOperationSchedule(token, workOrder.id, operationId, payload);
setWorkOrder(nextWorkOrder);
setOperationScheduleForm(
Object.fromEntries(
nextWorkOrder.operations.map((operation) => [operation.id, { plannedStart: operation.plannedStart }])
)
);
setStatus("Operation schedule updated with station calendar constraints.");
} catch (error: unknown) {
const message = error instanceof ApiError ? error.message : "Unable to reschedule operation.";
setStatus(message);
} finally {
setReschedulingOperationId(null);
}
}
function handleStatusChange(nextStatus: WorkOrderStatus) {
if (!workOrder) {
return;
@@ -271,9 +307,11 @@ export function WorkOrderDetailPage() {
<tr className="text-left text-xs font-semibold uppercase tracking-[0.18em] text-muted">
<th className="px-3 py-3">Seq</th>
<th className="px-3 py-3">Station</th>
<th className="px-3 py-3">Capacity</th>
<th className="px-3 py-3">Start</th>
<th className="px-3 py-3">End</th>
<th className="px-3 py-3">Minutes</th>
{canManage ? <th className="px-3 py-3">Reschedule</th> : null}
</tr>
</thead>
<tbody className="divide-y divide-line/70">
@@ -284,9 +322,38 @@ export function WorkOrderDetailPage() {
<div className="font-semibold text-text">{operation.stationCode}</div>
<div className="mt-1 text-xs text-muted">{operation.stationName}</div>
</td>
<td className="px-3 py-3 text-xs text-muted">
<div>{operation.stationDailyCapacityMinutes} min/day x {operation.stationParallelCapacity}</div>
<div>{operation.stationWorkingDays.join(",")}</div>
</td>
<td className="px-3 py-3 text-text">{new Date(operation.plannedStart).toLocaleString()}</td>
<td className="px-3 py-3 text-text">{new Date(operation.plannedEnd).toLocaleString()}</td>
<td className="px-3 py-3 text-text">{operation.plannedMinutes}</td>
{canManage ? (
<td className="px-3 py-3">
<div className="flex min-w-[220px] items-center gap-2">
<input
type="datetime-local"
value={(operationScheduleForm[operation.id]?.plannedStart ?? operation.plannedStart).slice(0, 16)}
onChange={(event) =>
setOperationScheduleForm((current) => ({
...current,
[operation.id]: { plannedStart: new Date(event.target.value).toISOString() },
}))
}
className="w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-xs text-text outline-none transition focus:border-brand"
/>
<button
type="button"
onClick={() => void submitOperationReschedule(operation.id)}
disabled={reschedulingOperationId === operation.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"
>
{reschedulingOperationId === operation.id ? "Saving..." : "Apply"}
</button>
</div>
</td>
) : null}
</tr>
))}
</tbody>