usability workbench
This commit is contained in:
@@ -40,6 +40,7 @@ This file is the running release and change log for CODEXIUM. Keep it updated wh
|
|||||||
- Workbench usability depth with keyboard row navigation, enter-to-open behavior, escape-to-clear, and inline readiness/shortage/hold signal pills across planner rows and day-detail cards
|
- Workbench usability depth with keyboard row navigation, enter-to-open behavior, escape-to-clear, and inline readiness/shortage/hold signal pills across planner rows and day-detail cards
|
||||||
- Workbench dispatch workflow depth with saved planner views, a release queue for visible ready work, queued-record visibility in the sticky control bar, and batch release directly from the workbench
|
- Workbench dispatch workflow depth with saved planner views, a release queue for visible ready work, queued-record visibility in the sticky control bar, and batch release directly from the workbench
|
||||||
- Workbench batch operation rebalance with multi-operation selection, sticky-bar batch reschedule controls, station reassignment across selected operations, and selected-operation visibility in row signals and focus context
|
- Workbench batch operation rebalance with multi-operation selection, sticky-bar batch reschedule controls, station reassignment across selected operations, and selected-operation visibility in row signals and focus context
|
||||||
|
- Workbench conflict-intelligence pass with projected batch target load, overload warnings before batch station moves, and best-alternate-station suggestions inside the sticky rebalance controls
|
||||||
- Project-side milestone and work-order rollups surfaced on project list and detail pages
|
- Project-side milestone and work-order rollups surfaced on project list and detail pages
|
||||||
- Inventory SKU master builder with family-level sequence codes, branch-aware taxonomy management, and generated SKU previews on the item form
|
- Inventory SKU master builder with family-level sequence codes, branch-aware taxonomy management, and generated SKU previews on the item form
|
||||||
- Thumbnail image attachment staging on inventory item create/edit pages, with upload-on-save and replacement/removal support
|
- Thumbnail image attachment staging on inventory item create/edit pages, with upload-on-save and replacement/removal support
|
||||||
|
|||||||
@@ -377,6 +377,46 @@ export function WorkbenchPage() {
|
|||||||
const stationLoadById = useMemo(() => new Map(stationLoads.map((station) => [station.stationId, station])), [stationLoads]);
|
const stationLoadById = useMemo(() => new Map(stationLoads.map((station) => [station.stationId, station])), [stationLoads]);
|
||||||
const selectedRescheduleStation = rescheduleStationId ? stations.find((station) => station.id === rescheduleStationId) ?? null : null;
|
const selectedRescheduleStation = rescheduleStationId ? stations.find((station) => station.id === rescheduleStationId) ?? null : null;
|
||||||
const selectedRescheduleLoad = selectedRescheduleStation ? stationLoadById.get(selectedRescheduleStation.id) ?? null : null;
|
const selectedRescheduleLoad = selectedRescheduleStation ? stationLoadById.get(selectedRescheduleStation.id) ?? null : null;
|
||||||
|
const selectedOperationLoadMinutes = useMemo(() => selectedOperations.reduce((sum, record) => sum + Math.max(record.loadMinutes, 1), 0), [selectedOperations]);
|
||||||
|
const batchTargetLoad = useMemo(() => {
|
||||||
|
if (!batchStationId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const station = stations.find((candidate) => candidate.id === batchStationId) ?? null;
|
||||||
|
const load = stationLoadById.get(batchStationId) ?? null;
|
||||||
|
if (!station || !load) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const projectedMinutes = load.totalPlannedMinutes + selectedOperationLoadMinutes;
|
||||||
|
const projectedUtilizationPercent = Math.round((projectedMinutes / Math.max(load.capacityMinutes, 1)) * 100);
|
||||||
|
return {
|
||||||
|
station,
|
||||||
|
load,
|
||||||
|
projectedMinutes,
|
||||||
|
projectedUtilizationPercent,
|
||||||
|
overloaded: projectedUtilizationPercent > 100,
|
||||||
|
};
|
||||||
|
}, [batchStationId, selectedOperationLoadMinutes, stationLoadById, stations]);
|
||||||
|
const batchStationSuggestions = useMemo(() => {
|
||||||
|
if (selectedOperations.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return stations
|
||||||
|
.filter((station) => station.isActive)
|
||||||
|
.map((station) => {
|
||||||
|
const load = stationLoadById.get(station.id);
|
||||||
|
const projectedMinutes = (load?.totalPlannedMinutes ?? 0) + selectedOperationLoadMinutes;
|
||||||
|
const capacityMinutes = Math.max(load?.capacityMinutes ?? station.dailyCapacityMinutes * station.parallelCapacity, 1);
|
||||||
|
const projectedUtilizationPercent = Math.round((projectedMinutes / capacityMinutes) * 100);
|
||||||
|
return {
|
||||||
|
station,
|
||||||
|
projectedUtilizationPercent,
|
||||||
|
overloaded: projectedUtilizationPercent > 100,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((left, right) => left.projectedUtilizationPercent - right.projectedUtilizationPercent)
|
||||||
|
.slice(0, 3);
|
||||||
|
}, [selectedOperationLoadMinutes, selectedOperations.length, stationLoadById, stations]);
|
||||||
const agendaItems = useMemo(
|
const agendaItems = useMemo(
|
||||||
() => [...focusRecords]
|
() => [...focusRecords]
|
||||||
.filter((record) => record.kind !== "OPERATION")
|
.filter((record) => record.kind !== "OPERATION")
|
||||||
@@ -819,6 +859,37 @@ export function WorkbenchPage() {
|
|||||||
{isBatchRescheduling ? "Applying..." : "Apply batch"}
|
{isBatchRescheduling ? "Applying..." : "Apply batch"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mt-3 grid gap-2 xl:grid-cols-2">
|
||||||
|
<div className={`rounded-[16px] border px-2 py-2 text-xs ${batchTargetLoad?.overloaded ? "border-amber-300/60 bg-amber-400/10 text-amber-300" : "border-line/70 bg-surface text-muted"}`}>
|
||||||
|
<div className="section-kicker">TARGET LOAD</div>
|
||||||
|
{batchTargetLoad ? (
|
||||||
|
<div className="mt-2 space-y-1">
|
||||||
|
<div className="font-semibold text-text">{batchTargetLoad.station.code} - {batchTargetLoad.station.name}</div>
|
||||||
|
<div>Projected util: <span className="font-semibold text-text">{batchTargetLoad.projectedUtilizationPercent}%</span></div>
|
||||||
|
<div>Projected minutes: <span className="font-semibold text-text">{batchTargetLoad.projectedMinutes}</span></div>
|
||||||
|
<div>{batchTargetLoad.overloaded ? "This batch move will overload the target station." : "This batch move stays within summarized station capacity."}</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="mt-2">Keeping current stations. Pick a station to preview the batch landing load.</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="rounded-[16px] border border-line/70 bg-surface px-2 py-2 text-xs text-muted">
|
||||||
|
<div className="section-kicker">BEST ALTERNATES</div>
|
||||||
|
<div className="mt-2 space-y-2">
|
||||||
|
{batchStationSuggestions.map((suggestion) => (
|
||||||
|
<button
|
||||||
|
key={suggestion.station.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setBatchStationId(suggestion.station.id)}
|
||||||
|
className={`flex w-full items-center justify-between rounded-[14px] border px-2 py-2 text-left ${batchStationId === suggestion.station.id ? "border-brand bg-brand/10" : "border-line/70 bg-page/60"}`}
|
||||||
|
>
|
||||||
|
<span className="font-semibold text-text">{suggestion.station.code} - {suggestion.station.name}</span>
|
||||||
|
<span className={suggestion.overloaded ? "text-amber-300" : "text-text"}>{suggestion.projectedUtilizationPercent}%</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="mt-3 flex flex-wrap gap-2 text-xs text-muted">
|
<div className="mt-3 flex flex-wrap gap-2 text-xs text-muted">
|
||||||
{selectedOperations.slice(0, 6).map((record) => (
|
{selectedOperations.slice(0, 6).map((record) => (
|
||||||
<span key={record.id} className="rounded-full border border-brand/40 bg-brand/10 px-2 py-1 text-text">
|
<span key={record.id} className="rounded-full border border-brand/40 bg-brand/10 px-2 py-1 text-text">
|
||||||
|
|||||||
Reference in New Issue
Block a user