workbench rebalance
This commit is contained in:
@@ -61,6 +61,7 @@ import type {
|
||||
WorkOrderCompletionInput,
|
||||
WorkOrderDetailDto,
|
||||
WorkOrderInput,
|
||||
WorkOrderOperationScheduleInput,
|
||||
WorkOrderMaterialIssueInput,
|
||||
WorkOrderStatus,
|
||||
WorkOrderSummaryDto,
|
||||
@@ -639,6 +640,13 @@ export const api = {
|
||||
token
|
||||
);
|
||||
},
|
||||
updateWorkOrderOperationSchedule(token: string, workOrderId: string, operationId: string, payload: WorkOrderOperationScheduleInput) {
|
||||
return request<WorkOrderDetailDto>(
|
||||
`/api/v1/manufacturing/work-orders/${workOrderId}/operations/${operationId}/schedule`,
|
||||
{ method: "PATCH", body: JSON.stringify(payload) },
|
||||
token
|
||||
);
|
||||
},
|
||||
issueWorkOrderMaterial(token: string, workOrderId: string, payload: WorkOrderMaterialIssueInput) {
|
||||
return request<WorkOrderDetailDto>(
|
||||
`/api/v1/manufacturing/work-orders/${workOrderId}/issues`,
|
||||
|
||||
@@ -10,6 +10,9 @@ const emptyStationInput: ManufacturingStationInput = {
|
||||
name: "",
|
||||
description: "",
|
||||
queueDays: 0,
|
||||
dailyCapacityMinutes: 480,
|
||||
parallelCapacity: 1,
|
||||
workingDays: [1, 2, 3, 4, 5],
|
||||
isActive: true,
|
||||
};
|
||||
|
||||
@@ -72,6 +75,8 @@ export function ManufacturingPage() {
|
||||
</div>
|
||||
<div className="text-right text-xs text-muted">
|
||||
<div>{station.queueDays} expected wait day(s)</div>
|
||||
<div>{station.dailyCapacityMinutes} min/day x {station.parallelCapacity}</div>
|
||||
<div>Days {station.workingDays.join(",")}</div>
|
||||
<div className="mt-1">{station.isActive ? "Active" : "Inactive"}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -96,6 +101,46 @@ export function ManufacturingPage() {
|
||||
<span className="mb-2 block text-sm font-semibold text-text">Expected Wait (Days)</span>
|
||||
<input type="number" min={0} step={1} value={form.queueDays} onChange={(event) => setForm((current) => ({ ...current, queueDays: Number.parseInt(event.target.value, 10) || 0 }))} className="w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand" />
|
||||
</label>
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<label className="block">
|
||||
<span className="mb-2 block text-sm font-semibold text-text">Capacity Minutes / Day</span>
|
||||
<input type="number" min={60} step={30} value={form.dailyCapacityMinutes} onChange={(event) => setForm((current) => ({ ...current, dailyCapacityMinutes: Number.parseInt(event.target.value, 10) || 480 }))} className="w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand" />
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="mb-2 block text-sm font-semibold text-text">Parallel Capacity</span>
|
||||
<input type="number" min={1} step={1} value={form.parallelCapacity} onChange={(event) => setForm((current) => ({ ...current, parallelCapacity: Number.parseInt(event.target.value, 10) || 1 }))} className="w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand" />
|
||||
</label>
|
||||
</div>
|
||||
<label className="block">
|
||||
<span className="mb-2 block text-sm font-semibold text-text">Working Days</span>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{[
|
||||
{ value: 1, label: "Mon" },
|
||||
{ value: 2, label: "Tue" },
|
||||
{ value: 3, label: "Wed" },
|
||||
{ value: 4, label: "Thu" },
|
||||
{ value: 5, label: "Fri" },
|
||||
{ value: 6, label: "Sat" },
|
||||
{ value: 0, label: "Sun" },
|
||||
].map((day) => (
|
||||
<label key={day.value} className="flex items-center gap-2 rounded-2xl border border-line/70 bg-page px-3 py-2 text-sm text-text">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={form.workingDays.includes(day.value)}
|
||||
onChange={(event) =>
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
workingDays: event.target.checked
|
||||
? [...current.workingDays, day.value].sort((left, right) => left - right)
|
||||
: current.workingDays.filter((value) => value !== day.value),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<span>{day.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="mb-2 block text-sm font-semibold text-text">Description</span>
|
||||
<textarea value={form.description} onChange={(event) => setForm((current) => ({ ...current, description: event.target.value }))} rows={3} className="w-full rounded-[18px] border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type {
|
||||
DemandPlanningRollupDto,
|
||||
GanttTaskDto,
|
||||
ManufacturingStationDto,
|
||||
PlanningExceptionDto,
|
||||
PlanningStationLoadDto,
|
||||
PlanningTaskActionDto,
|
||||
@@ -14,6 +15,7 @@ import { ApiError, api } from "../../lib/api";
|
||||
type WorkbenchMode = "overview" | "heatmap" | "agenda";
|
||||
type FocusRecord = {
|
||||
id: string;
|
||||
entityId: string | null;
|
||||
title: string;
|
||||
kind: "PROJECT" | "WORK_ORDER" | "OPERATION" | "MILESTONE";
|
||||
status: string;
|
||||
@@ -109,6 +111,7 @@ function densityTone(cell: HeatmapCell) {
|
||||
function buildFocusRecords(tasks: GanttTaskDto[]) {
|
||||
return tasks.map((task) => ({
|
||||
id: task.id,
|
||||
entityId: task.entityId ?? null,
|
||||
title: task.text,
|
||||
kind: parseFocusKind(task),
|
||||
status: task.status ?? "PLANNED",
|
||||
@@ -164,16 +167,21 @@ export function WorkbenchPage() {
|
||||
const [workbenchFilter, setWorkbenchFilter] = useState<WorkbenchFilter>("all");
|
||||
const [selectedFocusId, setSelectedFocusId] = useState<string | null>(null);
|
||||
const [selectedHeatmapDate, setSelectedHeatmapDate] = useState<string | null>(null);
|
||||
const [rescheduleStart, setRescheduleStart] = useState("");
|
||||
const [rescheduleStationId, setRescheduleStationId] = useState("");
|
||||
const [isRescheduling, setIsRescheduling] = useState(false);
|
||||
const [stations, setStations] = useState<ManufacturingStationDto[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
|
||||
Promise.all([api.getPlanningTimeline(token), api.getDemandPlanningRollup(token)])
|
||||
.then(([data, rollup]) => {
|
||||
Promise.all([api.getPlanningTimeline(token), api.getDemandPlanningRollup(token), api.getManufacturingStations(token)])
|
||||
.then(([data, rollup, stationOptions]) => {
|
||||
setTimeline(data);
|
||||
setPlanningRollup(rollup);
|
||||
setStations(stationOptions);
|
||||
setStatus("Planning workbench loaded.");
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
@@ -191,6 +199,16 @@ export function WorkbenchPage() {
|
||||
const focusById = useMemo(() => new Map(focusRecords.map((record) => [record.id, record])), [focusRecords]);
|
||||
const selectedFocus = selectedFocusId ? focusById.get(selectedFocusId) ?? null : filteredFocusRecords[0] ?? focusRecords[0] ?? null;
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedFocus?.kind === "OPERATION") {
|
||||
setRescheduleStart(selectedFocus.start.slice(0, 16));
|
||||
setRescheduleStationId(selectedFocus.stationId ?? "");
|
||||
} else {
|
||||
setRescheduleStart("");
|
||||
setRescheduleStationId("");
|
||||
}
|
||||
}, [selectedFocus?.id, selectedFocus?.kind, selectedFocus?.start, selectedFocus?.stationId]);
|
||||
|
||||
const heatmap = useMemo(() => {
|
||||
const start = summary ? startOfDay(new Date(summary.horizonStart)) : startOfDay(new Date());
|
||||
const cells = new Map<string, HeatmapCell>();
|
||||
@@ -226,6 +244,9 @@ export function WorkbenchPage() {
|
||||
}, [filteredFocusRecords, summary]);
|
||||
|
||||
const selectedHeatmapCell = selectedHeatmapDate ? heatmap.find((cell) => cell.dateKey === selectedHeatmapDate) ?? null : null;
|
||||
const stationLoadById = useMemo(() => new Map(stationLoads.map((station) => [station.stationId, station])), [stationLoads]);
|
||||
const selectedRescheduleStation = rescheduleStationId ? stations.find((station) => station.id === rescheduleStationId) ?? null : null;
|
||||
const selectedRescheduleLoad = selectedRescheduleStation ? stationLoadById.get(selectedRescheduleStation.id) ?? null : null;
|
||||
const agendaItems = useMemo(
|
||||
() => [...focusRecords]
|
||||
.filter((record) => record.kind !== "OPERATION")
|
||||
@@ -253,15 +274,23 @@ export function WorkbenchPage() {
|
||||
{ value: "overdue", label: "Overdue" },
|
||||
];
|
||||
|
||||
async function refreshWorkbench(message: string) {
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
const [refreshed, stationOptions] = await Promise.all([api.getPlanningTimeline(token), api.getManufacturingStations(token)]);
|
||||
setTimeline(refreshed);
|
||||
setStations(stationOptions);
|
||||
setStatus(message);
|
||||
}
|
||||
|
||||
async function handleTaskAction(action: PlanningTaskActionDto) {
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
if (action.kind === "RELEASE_WORK_ORDER" && action.workOrderId) {
|
||||
await api.updateWorkOrderStatus(token, action.workOrderId, "RELEASED");
|
||||
const refreshed = await api.getPlanningTimeline(token);
|
||||
setTimeline(refreshed);
|
||||
setStatus("Workbench refreshed after release.");
|
||||
await refreshWorkbench("Workbench refreshed after release.");
|
||||
return;
|
||||
}
|
||||
if (action.href) {
|
||||
@@ -269,6 +298,50 @@ export function WorkbenchPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRescheduleOperation(nextStartIso?: string, nextStationId?: string | null) {
|
||||
if (!token || !selectedFocus || selectedFocus.kind !== "OPERATION" || !selectedFocus.workOrderId || !selectedFocus.entityId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const plannedStart = nextStartIso ?? (rescheduleStart ? new Date(rescheduleStart).toISOString() : "");
|
||||
if (!plannedStart) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsRescheduling(true);
|
||||
try {
|
||||
await api.updateWorkOrderOperationSchedule(token, selectedFocus.workOrderId, selectedFocus.entityId, {
|
||||
plannedStart,
|
||||
stationId: (nextStationId ?? rescheduleStationId) || null,
|
||||
});
|
||||
await refreshWorkbench("Workbench refreshed after operation rebalance.");
|
||||
setRescheduleStart(plannedStart.slice(0, 16));
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof ApiError ? error.message : "Unable to rebalance operation from Workbench.";
|
||||
setStatus(message);
|
||||
} finally {
|
||||
setIsRescheduling(false);
|
||||
}
|
||||
}
|
||||
|
||||
function shiftRescheduleDraft(hours: number) {
|
||||
if (!rescheduleStart) {
|
||||
return;
|
||||
}
|
||||
const next = new Date(rescheduleStart);
|
||||
next.setHours(next.getHours() + hours);
|
||||
setRescheduleStart(next.toISOString().slice(0, 16));
|
||||
}
|
||||
|
||||
function moveDraftToSelectedHeatmapDay() {
|
||||
if (!selectedHeatmapDate) {
|
||||
return;
|
||||
}
|
||||
const current = rescheduleStart ? new Date(rescheduleStart) : new Date(`${selectedHeatmapDate}T08:00:00`);
|
||||
const target = new Date(`${selectedHeatmapDate}T${String(current.getHours()).padStart(2, "0")}:${String(current.getMinutes()).padStart(2, "0")}:00`);
|
||||
setRescheduleStart(target.toISOString().slice(0, 16));
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="space-y-4">
|
||||
<div className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
||||
@@ -391,6 +464,95 @@ export function WorkbenchPage() {
|
||||
<div className="mt-2 flex items-center justify-between gap-3"><span className="text-muted">Open supply</span><span className="font-semibold text-text">{selectedFocus.openSupplyQuantity}</span></div>
|
||||
{selectedFocus.blockedReason ? <div className="mt-3 text-xs text-muted">{selectedFocus.blockedReason}</div> : null}
|
||||
</div>
|
||||
{selectedFocus.kind === "OPERATION" ? (
|
||||
<div className="rounded-[18px] border border-line/70 bg-page/60 p-3 text-sm">
|
||||
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Workbench Rebalance</div>
|
||||
<div className="mt-3">
|
||||
<label className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Target Station</label>
|
||||
<select
|
||||
value={rescheduleStationId}
|
||||
onChange={(event) => setRescheduleStationId(event.target.value)}
|
||||
className="w-full rounded-2xl border border-line/70 bg-surface px-2 py-2 text-text outline-none transition focus:border-brand"
|
||||
>
|
||||
{stations
|
||||
.filter((station) => station.isActive || station.id === selectedFocus.stationId)
|
||||
.map((station) => (
|
||||
<option key={station.id} value={station.id}>
|
||||
{station.code} - {station.name}{station.isActive ? "" : " (Inactive)"}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<label className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Planned Start</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
value={rescheduleStart}
|
||||
onChange={(event) => setRescheduleStart(event.target.value)}
|
||||
className="w-full rounded-2xl border border-line/70 bg-surface px-2 py-2 text-text outline-none transition focus:border-brand"
|
||||
/>
|
||||
</div>
|
||||
{selectedRescheduleStation ? (
|
||||
<div className="mt-3 rounded-[16px] border border-line/70 bg-surface/80 p-3 text-xs text-muted">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span>Capacity</span>
|
||||
<span className="font-semibold text-text">
|
||||
{selectedRescheduleStation.dailyCapacityMinutes} min/day x {selectedRescheduleStation.parallelCapacity}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center justify-between gap-3">
|
||||
<span>Working days</span>
|
||||
<span className="font-semibold text-text">
|
||||
{selectedRescheduleStation.workingDays.map((day) => ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][day]).join(", ")}
|
||||
</span>
|
||||
</div>
|
||||
{selectedRescheduleLoad ? (
|
||||
<div className="mt-2 flex items-center justify-between gap-3">
|
||||
<span>Current load</span>
|
||||
<span className="font-semibold text-text">
|
||||
{selectedRescheduleLoad.utilizationPercent}% util / {selectedRescheduleLoad.overloaded ? "Overloaded" : "Within load"}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
<button type="button" onClick={() => shiftRescheduleDraft(1)} className="rounded-2xl border border-line/70 px-2 py-2 text-xs font-semibold text-text">+1h</button>
|
||||
<button type="button" onClick={() => shiftRescheduleDraft(8)} className="rounded-2xl border border-line/70 px-2 py-2 text-xs font-semibold text-text">+1 shift</button>
|
||||
<button type="button" onClick={() => shiftRescheduleDraft(24)} className="rounded-2xl border border-line/70 px-2 py-2 text-xs font-semibold text-text">+1 day</button>
|
||||
{selectedHeatmapDate ? (
|
||||
<button type="button" onClick={moveDraftToSelectedHeatmapDay} className="rounded-2xl border border-line/70 px-2 py-2 text-xs font-semibold text-text">
|
||||
Move to {formatDate(selectedHeatmapDate)}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleRescheduleOperation()}
|
||||
disabled={isRescheduling || !rescheduleStart}
|
||||
className="rounded-2xl bg-brand px-2 py-2 text-sm font-semibold text-white disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
{isRescheduling ? "Rebalancing..." : "Apply rebalance"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const originalStationId = selectedFocus.stationId ?? "";
|
||||
setRescheduleStationId(originalStationId);
|
||||
void handleRescheduleOperation(new Date(selectedFocus.start).toISOString(), originalStationId);
|
||||
}}
|
||||
disabled={isRescheduling}
|
||||
className="rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-3 text-xs text-muted">
|
||||
Rebalance starts from this operation, can move it onto another active station, and rebuilds downstream operations using station calendars and capacity.
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedFocus.actions.map((action, index) => (
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user