From 7b65fe06cf5a48e509429cfabed949090a58e867 Mon Sep 17 00:00:00 2001 From: jason Date: Wed, 18 Mar 2026 23:32:12 -0500 Subject: [PATCH] more workbench --- CHANGELOG.md | 1 + .../src/modules/workbench/WorkbenchPage.tsx | 170 +++++++++++++++++- 2 files changed, 163 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 561882c..bf0b4cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ This file is the running release and change log for CODEXIUM. Keep it updated wh - Continued density standardization across project cockpit/detail internals, including tighter cockpit cards, denser purchasing and readiness panels, and compact milestone, manufacturing-link, and activity-timeline surfaces - Continued density standardization across admin diagnostics, user management, and CRM contacts, including tighter filter/forms, denser summary cards, and compact contact/account management surfaces - Workbench usability pass with sticky planner controls, stronger selected-row and selected-day state, clearer heatmap/day context, and more explicit dispatch-oriented action affordances +- 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 - 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 - Thumbnail image attachment staging on inventory item create/edit pages, with upload-on-save and replacement/removal support diff --git a/client/src/modules/workbench/WorkbenchPage.tsx b/client/src/modules/workbench/WorkbenchPage.tsx index 4db79f1..c764d7d 100644 --- a/client/src/modules/workbench/WorkbenchPage.tsx +++ b/client/src/modules/workbench/WorkbenchPage.tsx @@ -116,6 +116,38 @@ function densityTone(cell: HeatmapCell) { return "border-line/60 bg-surface/70"; } +function readinessTone(record: FocusRecord) { + if (record.overdue) { + return "border-rose-300/60 bg-rose-500/10 text-rose-200 dark:text-rose-200"; + } + if (record.readinessState === "BLOCKED" || record.blockedReason) { + return "border-amber-300/60 bg-amber-400/10 text-amber-300"; + } + if (record.totalShortageQuantity > 0 || record.readinessState === "SHORTAGE" || record.readinessState === "PENDING_SUPPLY") { + return "border-orange-300/60 bg-orange-400/10 text-orange-300"; + } + if (record.releaseReady || record.readinessState === "READY") { + return "border-emerald-300/60 bg-emerald-500/10 text-emerald-300"; + } + return "border-line/70 bg-page/60 text-muted"; +} + +function readinessLabel(record: FocusRecord) { + if (record.overdue) { + return "OVERDUE"; + } + if (record.readinessState === "BLOCKED" || record.blockedReason) { + return "BLOCKED"; + } + if (record.totalShortageQuantity > 0 || record.readinessState === "SHORTAGE" || record.readinessState === "PENDING_SUPPLY") { + return "SHORTAGE"; + } + if (record.releaseReady || record.readinessState === "READY") { + return "READY"; + } + return record.readinessState.replaceAll("_", " "); +} + function buildFocusRecords(tasks: GanttTaskDto[]) { return tasks.map((task) => ({ id: task.id, @@ -270,6 +302,90 @@ export function WorkbenchPage() { .slice(0, 18), [focusRecords, workbenchFilter] ); + const keyboardRecords = useMemo(() => { + if (workbenchMode === "agenda") { + return agendaItems; + } + if (workbenchMode === "heatmap") { + return (selectedHeatmapCell?.tasks ?? filteredFocusRecords.filter((record) => record.kind !== "PROJECT")).slice(0, 18); + } + + const projects = filteredFocusRecords.filter((record) => record.kind === "PROJECT").slice(0, 6); + const operations = filteredFocusRecords.filter((record) => record.kind === "OPERATION"); + const workOrders = filteredFocusRecords.filter((record) => record.kind === "WORK_ORDER").slice(0, 10); + const exceptionRows = filteredFocusRecords + .filter((record) => record.kind !== "PROJECT" && (record.overdue || record.totalShortageQuantity > 0 || record.readinessState === "BLOCKED" || record.readinessState === "PENDING_SUPPLY")) + .slice(0, 10); + + if (workbenchGroup === "projects") { + return [...projects, ...operations.slice(0, 10), ...workOrders].filter((record, index, array) => array.findIndex((candidate) => candidate.id === record.id) === index); + } + if (workbenchGroup === "exceptions") { + return [...exceptionRows, ...workOrders].filter((record, index, array) => array.findIndex((candidate) => candidate.id === record.id) === index); + } + + const stationBuckets = new Map(); + for (const record of operations) { + if (!record.stationId) { + continue; + } + const bucket = stationBuckets.get(record.stationId) ?? []; + bucket.push(record); + stationBuckets.set(record.stationId, bucket); + } + for (const bucket of stationBuckets.values()) { + bucket.sort((left, right) => new Date(left.start).getTime() - new Date(right.start).getTime()); + } + return stationLoads + .slice(0, 10) + .flatMap((station) => (stationBuckets.get(station.stationId) ?? []).slice(0, 5)); + }, [agendaItems, filteredFocusRecords, selectedHeatmapCell?.dateKey, selectedHeatmapCell?.tasks, stationLoads, workbenchGroup, workbenchMode]); + + useEffect(() => { + function handleKeydown(event: KeyboardEvent) { + const target = event.target; + if (target instanceof HTMLElement) { + const tagName = target.tagName; + if (target.isContentEditable || tagName === "INPUT" || tagName === "TEXTAREA" || tagName === "SELECT" || tagName === "BUTTON") { + return; + } + } + if (event.altKey || event.ctrlKey || event.metaKey || keyboardRecords.length === 0) { + return; + } + + if (event.key === "ArrowDown" || event.key === "j") { + event.preventDefault(); + const currentIndex = selectedFocus ? keyboardRecords.findIndex((record) => record.id === selectedFocus.id) : -1; + const nextIndex = currentIndex >= 0 ? Math.min(currentIndex + 1, keyboardRecords.length - 1) : 0; + setSelectedFocusId(keyboardRecords[nextIndex]?.id ?? null); + } + + if (event.key === "ArrowUp" || event.key === "k") { + event.preventDefault(); + const currentIndex = selectedFocus ? keyboardRecords.findIndex((record) => record.id === selectedFocus.id) : -1; + const nextIndex = currentIndex >= 0 ? Math.max(currentIndex - 1, 0) : 0; + setSelectedFocusId(keyboardRecords[nextIndex]?.id ?? null); + } + + if (event.key === "Enter" && selectedFocus?.detailHref) { + event.preventDefault(); + navigate(selectedFocus.detailHref); + } + + if (event.key === "Escape") { + event.preventDefault(); + if (selectedHeatmapDate) { + setSelectedHeatmapDate(null); + return; + } + setSelectedFocusId(null); + } + } + + window.addEventListener("keydown", handleKeydown); + return () => window.removeEventListener("keydown", handleKeydown); + }, [keyboardRecords, navigate, selectedFocus, selectedHeatmapDate]); const modeOptions: Array<{ value: WorkbenchMode; label: string; detail: string }> = [ { value: "overview", label: "Overview", detail: "Dense planner board" }, @@ -425,6 +541,9 @@ export function WorkbenchPage() {
{filteredFocusRecords.length} visible rows + Up/Down navigate + Enter open + Esc clear {selectedFocus ? ( Selected {selectedFocus.kind.toLowerCase()}: {selectedFocus.title} @@ -469,9 +588,9 @@ export function WorkbenchPage() {
Status{selectedFocus.status.replaceAll("_", " ")}
@@ -672,6 +794,19 @@ function MetricCard({ label, value }: { label: string; value: string | number }) ); } +function RecordSignals({ record }: { record: FocusRecord }) { + return ( + <> + + {readinessLabel(record)} + + {record.releaseReady ? RELEASE : null} + {record.totalShortageQuantity > 0 ? SHORT {record.totalShortageQuantity} : null} + {record.blockedReason ? HOLD : null} + + ); +} + function OverviewBoard({ focusRecords, stationLoads, @@ -734,10 +869,13 @@ function OverviewBoard({
{record.title}
{record.ownerLabel ?? "No owner context"}
+
+ +
-
{record.readinessState}
{record.progress}% progress
+
{formatDate(record.end)}
@@ -752,6 +890,9 @@ function OverviewBoard({
{record.title}
{record.ownerLabel ?? "No parent work order"}
+
+ +
{record.stationCode ?? "No station"}
@@ -865,9 +1006,11 @@ function OverviewBoard({
{record.ownerLabel ?? record.title}
{formatDate(record.start, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" })}
+
+ +
-
{record.readinessState}
{record.utilizationPercent ?? station.utilizationPercent}% util
@@ -891,10 +1034,12 @@ function OverviewBoard({ @@ -908,8 +1053,11 @@ function OverviewBoard({ {workOrders.map((record) => ( @@ -977,6 +1125,9 @@ function AgendaBoard({ records, onSelect, selectedId, compact = false }: { recor
{record.title}
{record.kind} - {record.ownerLabel ?? "No context"}
+
+ +
{formatDate(record.end)}
@@ -1005,6 +1156,9 @@ function SelectedDayPanel({ cell, onSelect, selectedId }: { cell: HeatmapCell; o ))}