This commit is contained in:
2026-03-18 23:06:44 -05:00
parent 17b73a4597
commit 52bc98c16e
3 changed files with 93 additions and 92 deletions

View File

@@ -31,6 +31,7 @@ This file is the running release and change log for CODEXIUM. Keep it updated wh
- Continued density standardization across sales, purchasing, shipping, and manufacturing editor internals, and standardized form-header cancel actions onto button-driven navigation to avoid in-form route-transition edge cases - Continued density standardization across sales, purchasing, shipping, and manufacturing editor internals, and standardized form-header cancel actions onto button-driven navigation to avoid in-form route-transition edge cases
- Continued density standardization across sales, purchasing, shipping, and manufacturing detail internals, including denser KPI strips, tighter side panels, shorter empty states, and less redundant context copy on high-traffic record views - Continued density standardization across sales, purchasing, shipping, and manufacturing detail internals, including denser KPI strips, tighter side panels, shorter empty states, and less redundant context copy on high-traffic record views
- Continued density standardization across shared attachment and revision-comparison surfaces, and changed inventory item-editor exit actions to hard navigation so SKU master and cancel transitions no longer depend on client-side router state - Continued density standardization across shared attachment and revision-comparison surfaces, and changed inventory item-editor exit actions to hard navigation so SKU master and cancel transitions no longer depend on client-side router state
- Continued density standardization across the SKU master builder and planning workbench, including tighter tree and board panels, denser exception and focus surfaces, shorter empty states, and less helper copy on those operational screens
- 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

View File

@@ -87,7 +87,7 @@ export function InventorySkuMasterPage() {
return ( return (
<div key={node.id} className="space-y-2"> <div key={node.id} className="space-y-2">
<div className="rounded-[18px] border border-line/70 bg-page/60 px-3 py-3" style={{ marginLeft: `${depth * 16}px` }}> <div className="rounded-[18px] border border-line/70 bg-page/60 px-2 py-2" style={{ marginLeft: `${depth * 16}px` }}>
<div className="flex flex-wrap items-center justify-between gap-2"> <div className="flex flex-wrap items-center justify-between gap-2">
<div className="min-w-0"> <div className="min-w-0">
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
@@ -107,8 +107,10 @@ export function InventorySkuMasterPage() {
</span> </span>
)} )}
<div className="min-w-0"> <div className="min-w-0">
<div className="text-sm font-semibold text-text">{node.code} <span className="text-muted">- {node.label}</span></div> <div className="text-sm font-semibold text-text">
<div className="mt-1 text-xs text-muted">Level {node.level} {node.childCount} child branch(es)</div> {node.code} <span className="text-muted">- {node.label}</span>
</div>
<div className="mt-1 text-xs text-muted">Level {node.level} - {node.childCount} child branch(es)</div>
</div> </div>
</div> </div>
</div> </div>
@@ -193,13 +195,12 @@ export function InventorySkuMasterPage() {
} }
return ( return (
<section className="space-y-6"> <section className="page-stack">
<div className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5"> <div className="surface-panel">
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between"> <div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div> <div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Inventory Master Data</p> <p className="section-kicker">INVENTORY MASTER DATA</p>
<h3 className="mt-2 text-xl font-bold text-text">SKU Master Builder</h3> <h3 className="module-title">SKU MASTER BUILDER</h3>
<p className="mt-2 max-w-3xl text-sm text-muted">Define family roots, branch-specific child codes, and the family-scoped short-code suffix that finishes each generated SKU.</p>
</div> </div>
<Link to="/inventory/items" className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text"> <Link to="/inventory/items" className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">
Back to items Back to items
@@ -207,13 +208,13 @@ export function InventorySkuMasterPage() {
</div> </div>
</div> </div>
<div className="grid gap-6 xl:grid-cols-[0.9fr_1.5fr]"> <div className="grid gap-3 xl:grid-cols-[0.9fr_1.5fr]">
<div className="space-y-6"> <div className="space-y-3">
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5"> <section className="surface-panel">
<div className="text-sm font-semibold text-text">Families</div> <p className="section-kicker">FAMILIES</p>
<div className="mt-4 space-y-2"> <div className="mt-3 space-y-2">
{catalog.families.length === 0 ? ( {catalog.families.length === 0 ? (
<div className="rounded-[18px] border border-dashed border-line/70 bg-page/60 px-4 py-6 text-sm text-muted">No SKU families defined yet.</div> <div className="rounded-[18px] border border-dashed border-line/70 bg-page/60 px-3 py-5 text-sm text-muted">No SKU families defined yet.</div>
) : ( ) : (
catalog.families.map((family) => ( catalog.families.map((family) => (
<button <button
@@ -224,13 +225,18 @@ export function InventorySkuMasterPage() {
setExpandedNodeIds([]); setExpandedNodeIds([]);
setNodeForm((current) => ({ ...current, familyId: family.id, parentNodeId: null })); setNodeForm((current) => ({ ...current, familyId: family.id, parentNodeId: null }));
}} }}
className={`block w-full rounded-[18px] border px-3 py-3 text-left transition ${ className={`block w-full rounded-[18px] border px-2 py-2 text-left transition ${
selectedFamilyId === family.id ? "border-brand bg-brand/8" : "border-line/70 bg-page/60 hover:bg-page/80" selectedFamilyId === family.id ? "border-brand bg-brand/8" : "border-line/70 bg-page/60 hover:bg-page/80"
}`} }`}
> >
<div className="text-sm font-semibold text-text">{family.code} <span className="text-muted">({family.sequenceCode})</span></div> <div className="text-sm font-semibold text-text">
{family.code} <span className="text-muted">({family.sequenceCode})</span>
</div>
<div className="mt-1 text-xs text-muted">{family.name}</div> <div className="mt-1 text-xs text-muted">{family.name}</div>
<div className="mt-2 text-xs text-muted">{family.childNodeCount} branch nodes next {family.sequenceCode}{String(family.nextSequenceNumber).padStart(4, "0")}</div> <div className="mt-2 text-xs text-muted">
{family.childNodeCount} branch nodes - next {family.sequenceCode}
{String(family.nextSequenceNumber).padStart(4, "0")}
</div>
</button> </button>
)) ))
)} )}
@@ -238,9 +244,9 @@ export function InventorySkuMasterPage() {
</section> </section>
{canManage ? ( {canManage ? (
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5"> <section className="surface-panel">
<div className="text-sm font-semibold text-text">Add family</div> <p className="section-kicker">ADD FAMILY</p>
<form className="mt-4 space-y-3" onSubmit={handleCreateFamily}> <form className="mt-3 space-y-3" onSubmit={handleCreateFamily}>
<div className="grid gap-3 sm:grid-cols-2"> <div className="grid gap-3 sm:grid-cols-2">
<label className="block"> <label className="block">
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Family code</span> <span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Family code</span>
@@ -265,26 +271,26 @@ export function InventorySkuMasterPage() {
) : null} ) : null}
</div> </div>
<div className="space-y-6"> <div className="space-y-3">
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5"> <section className="surface-panel">
<div className="flex flex-col gap-2 lg:flex-row lg:items-center lg:justify-between"> <div className="flex flex-col gap-2 lg:flex-row lg:items-center lg:justify-between">
<div> <div>
<div className="text-sm font-semibold text-text">Branch tree</div> <p className="section-kicker">BRANCH TREE</p>
<div className="mt-1 text-xs text-muted">{status}</div> <div className="mt-1 text-xs text-muted">{status}</div>
</div> </div>
{selectedFamilyId ? ( {selectedFamilyId ? (
<div className="text-xs text-muted">Up to 6 total SKU levels including family root.</div> <div className="text-xs text-muted">Up to 6 total SKU levels.</div>
) : null} ) : null}
</div> </div>
<div className="mt-4 space-y-3"> <div className="mt-3 space-y-2">
{selectedFamilyId ? renderNodes(null) : <div className="rounded-[18px] border border-dashed border-line/70 bg-page/60 px-4 py-6 text-sm text-muted">Select a family to inspect or extend its branch tree.</div>} {selectedFamilyId ? renderNodes(null) : <div className="rounded-[18px] border border-dashed border-line/70 bg-page/60 px-3 py-5 text-sm text-muted">Select a family to inspect or extend its branch tree.</div>}
</div> </div>
</section> </section>
{canManage ? ( {canManage ? (
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5"> <section className="surface-panel">
<div className="text-sm font-semibold text-text">Add branch node</div> <p className="section-kicker">ADD BRANCH NODE</p>
<form className="mt-4 space-y-3" onSubmit={handleCreateNode}> <form className="mt-3 space-y-3" onSubmit={handleCreateNode}>
<div className="grid gap-3 sm:grid-cols-2"> <div className="grid gap-3 sm:grid-cols-2">
<label className="block"> <label className="block">
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Family</span> <span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Family</span>

View File

@@ -385,24 +385,23 @@ export function WorkbenchPage() {
} }
return ( return (
<section className="space-y-4"> <section className="page-stack">
<div className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5"> <div className="surface-panel">
<div className="flex flex-col gap-3 xl:flex-row xl:items-start xl:justify-between"> <div className="flex flex-col gap-3 xl:flex-row xl:items-start xl:justify-between">
<div> <div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Planning</p> <p className="section-kicker">PLANNING</p>
<h3 className="mt-2 text-2xl font-bold text-text">Planning Workbench</h3> <h3 className="module-title">PLANNING WORKBENCH</h3>
<p className="mt-2 max-w-4xl text-sm text-muted">A reactive planning surface for projects, work orders, operations, shortages, and schedule risk. Use it as the daily planner cockpit, not just a chart.</p>
</div> </div>
<div className="rounded-[18px] border border-line/70 bg-page/60 px-3 py-3 text-sm"> <div className="rounded-[18px] border border-line/70 bg-page/60 px-2 py-2 text-sm">
<div className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Workbench Status</div> <div className="section-kicker">WORKBENCH STATUS</div>
<div className="mt-2 font-semibold text-text">{status}</div> <div className="mt-1 font-semibold text-text">{status}</div>
</div> </div>
</div> </div>
<div className="mt-5 grid gap-3 xl:grid-cols-4"> <div className="mt-3 grid gap-2 xl:grid-cols-4">
{modeOptions.map((option) => ( {modeOptions.map((option) => (
<button key={option.value} type="button" onClick={() => setWorkbenchMode(option.value)} className={`rounded-[18px] border px-3 py-3 text-left transition ${workbenchMode === option.value ? "border-brand bg-brand/10" : "border-line/70 bg-page/60 hover:border-brand/40"}`}> <button key={option.value} type="button" onClick={() => setWorkbenchMode(option.value)} className={`rounded-[18px] border px-2 py-2 text-left transition ${workbenchMode === option.value ? "border-brand bg-brand/10" : "border-line/70 bg-page/60 hover:border-brand/40"}`}>
<div className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">{option.label}</div> <div className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">{option.label}</div>
<div className="mt-2 text-sm font-semibold text-text">{option.detail}</div> <div className="mt-1 text-sm font-semibold text-text">{option.detail}</div>
</button> </button>
))} ))}
</div> </div>
@@ -438,20 +437,19 @@ export function WorkbenchPage() {
<div className="grid gap-3 xl:grid-cols-[320px_minmax(0,1fr)_360px]"> <div className="grid gap-3 xl:grid-cols-[320px_minmax(0,1fr)_360px]">
<aside className="space-y-3"> <aside className="space-y-3">
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel"> <section className="surface-panel">
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-3">
<div> <div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Exception Rail</p> <p className="section-kicker">EXCEPTION RAIL</p>
<p className="mt-2 text-sm text-muted">Late, at-risk, and unscheduled items that require planner attention.</p>
</div> </div>
<span className="rounded-full border border-line/70 px-2 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-muted">{exceptions.length}</span> <span className="rounded-full border border-line/70 px-2 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-muted">{exceptions.length}</span>
</div> </div>
{exceptions.length === 0 ? ( {exceptions.length === 0 ? (
<div className="mt-5 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted">No planning exceptions are active.</div> <div className="mt-3 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-3 py-5 text-center text-sm text-muted">No planning exceptions are active.</div>
) : ( ) : (
<div className="mt-5 space-y-3"> <div className="mt-3 space-y-2">
{exceptions.map((exception: PlanningExceptionDto) => ( {exceptions.map((exception: PlanningExceptionDto) => (
<button key={exception.id} type="button" onClick={() => setSelectedFocusId(exception.id.startsWith("project-") ? exception.id : exception.id.replace("work-order-unscheduled-", "work-order-"))} className="block w-full rounded-[18px] border border-line/70 bg-page/60 p-3 text-left transition hover:bg-page/80"> <button key={exception.id} type="button" onClick={() => setSelectedFocusId(exception.id.startsWith("project-") ? exception.id : exception.id.replace("work-order-unscheduled-", "work-order-"))} className="block w-full rounded-[18px] border border-line/70 bg-page/60 px-2 py-2 text-left transition hover:bg-page/80">
<div className="flex items-start justify-between gap-3"> <div className="flex items-start justify-between gap-3">
<div> <div>
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">{exception.kind === "PROJECT" ? "Project" : "Work Order"}</div> <div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">{exception.kind === "PROJECT" ? "Project" : "Work Order"}</div>
@@ -466,14 +464,14 @@ export function WorkbenchPage() {
</div> </div>
)} )}
</section> </section>
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel"> <section className="surface-panel">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Planner Actions</p> <p className="section-kicker">PLANNER ACTIONS</p>
<div className="mt-4 space-y-2 rounded-[18px] border border-line/70 bg-page/60 p-3 text-sm"> <div className="mt-3 space-y-2 rounded-[18px] border border-line/70 bg-page/60 px-2 py-2 text-sm">
<div className="flex items-center justify-between gap-3"><span className="text-muted">Uncovered quantity</span><span className="font-semibold text-text">{planningRollup?.summary.totalUncoveredQuantity ?? 0}</span></div> <div className="flex items-center justify-between gap-3"><span className="text-muted">Uncovered quantity</span><span className="font-semibold text-text">{planningRollup?.summary.totalUncoveredQuantity ?? 0}</span></div>
<div className="flex items-center justify-between gap-3"><span className="text-muted">Projects with linked demand</span><span className="font-semibold text-text">{planningRollup?.summary.projectCount ?? 0}</span></div> <div className="flex items-center justify-between gap-3"><span className="text-muted">Projects with linked demand</span><span className="font-semibold text-text">{planningRollup?.summary.projectCount ?? 0}</span></div>
<div className="flex items-center justify-between gap-3"><span className="text-muted">Overloaded stations</span><span className="font-semibold text-text">{summary?.overloadedStations ?? 0}</span></div> <div className="flex items-center justify-between gap-3"><span className="text-muted">Overloaded stations</span><span className="font-semibold text-text">{summary?.overloadedStations ?? 0}</span></div>
</div> </div>
<div className="mt-4 flex flex-wrap gap-2"> <div className="mt-3 flex flex-wrap gap-2">
<Link to="/projects" className="rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">Open projects</Link> <Link to="/projects" className="rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">Open projects</Link>
<Link to="/manufacturing/work-orders" className="rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">Open work orders</Link> <Link to="/manufacturing/work-orders" className="rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">Open work orders</Link>
<Link to="/purchasing/orders" className="rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">Open purchasing</Link> <Link to="/purchasing/orders" className="rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">Open purchasing</Link>
@@ -481,7 +479,7 @@ export function WorkbenchPage() {
</section> </section>
</aside> </aside>
<div className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel"> <div className="surface-panel">
{workbenchMode === "overview" ? ( {workbenchMode === "overview" ? (
<OverviewBoard <OverviewBoard
focusRecords={filteredFocusRecords} focusRecords={filteredFocusRecords}
@@ -501,16 +499,16 @@ export function WorkbenchPage() {
</div> </div>
<aside className="space-y-3"> <aside className="space-y-3">
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel"> <section className="surface-panel">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Focus Drawer</p> <p className="section-kicker">FOCUS DRAWER</p>
{selectedFocus ? ( {selectedFocus ? (
<div className="mt-4 space-y-3"> <div className="mt-3 space-y-2">
<div className="rounded-[18px] border border-line/70 bg-page/60 p-3"> <div className="rounded-[18px] border border-line/70 bg-page/60 px-2 py-2">
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">{selectedFocus.kind}</div> <div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">{selectedFocus.kind}</div>
<div className="mt-2 text-base font-bold text-text">{selectedFocus.title}</div> <div className="mt-2 text-base font-bold text-text">{selectedFocus.title}</div>
<div className="mt-2 text-xs text-muted">{selectedFocus.ownerLabel ?? "No context label"}</div> <div className="mt-2 text-xs text-muted">{selectedFocus.ownerLabel ?? "No context label"}</div>
</div> </div>
<div className="rounded-[18px] border border-line/70 bg-page/60 p-3 text-sm"> <div className="rounded-[18px] border border-line/70 bg-page/60 px-2 py-2 text-sm">
<div className="flex items-center justify-between gap-3"><span className="text-muted">Status</span><span className="font-semibold text-text">{selectedFocus.status.replaceAll("_", " ")}</span></div> <div className="flex items-center justify-between gap-3"><span className="text-muted">Status</span><span className="font-semibold text-text">{selectedFocus.status.replaceAll("_", " ")}</span></div>
<div className="mt-2 flex items-center justify-between gap-3"><span className="text-muted">Readiness</span><span className="font-semibold text-text">{selectedFocus.readinessState.replaceAll("_", " ")} ({selectedFocus.readinessScore}%)</span></div> <div className="mt-2 flex items-center justify-between gap-3"><span className="text-muted">Readiness</span><span className="font-semibold text-text">{selectedFocus.readinessState.replaceAll("_", " ")} ({selectedFocus.readinessScore}%)</span></div>
<div className="mt-2 flex items-center justify-between gap-3"><span className="text-muted">Window</span><span className="font-semibold text-text">{formatDate(selectedFocus.start)} - {formatDate(selectedFocus.end)}</span></div> <div className="mt-2 flex items-center justify-between gap-3"><span className="text-muted">Window</span><span className="font-semibold text-text">{formatDate(selectedFocus.start)} - {formatDate(selectedFocus.end)}</span></div>
@@ -520,8 +518,8 @@ export function WorkbenchPage() {
{selectedFocus.blockedReason ? <div className="mt-3 text-xs text-muted">{selectedFocus.blockedReason}</div> : null} {selectedFocus.blockedReason ? <div className="mt-3 text-xs text-muted">{selectedFocus.blockedReason}</div> : null}
</div> </div>
{selectedFocus.kind === "OPERATION" ? ( {selectedFocus.kind === "OPERATION" ? (
<div className="rounded-[18px] border border-line/70 bg-page/60 p-3 text-sm"> <div className="rounded-[18px] border border-line/70 bg-page/60 px-2 py-2 text-sm">
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Workbench Rebalance</div> <div className="section-kicker">WORKBENCH REBALANCE</div>
<div className="mt-3"> <div className="mt-3">
<label className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Target Station</label> <label className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Target Station</label>
<select <select
@@ -548,7 +546,7 @@ export function WorkbenchPage() {
/> />
</div> </div>
{selectedRescheduleStation ? ( {selectedRescheduleStation ? (
<div className="mt-3 rounded-[16px] border border-line/70 bg-surface/80 p-3 text-xs text-muted"> <div className="mt-3 rounded-[16px] border border-line/70 bg-surface/80 px-2 py-2 text-xs text-muted">
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-3">
<span>Capacity</span> <span>Capacity</span>
<span className="font-semibold text-text"> <span className="font-semibold text-text">
@@ -623,13 +621,13 @@ export function WorkbenchPage() {
</div> </div>
</div> </div>
) : ( ) : (
<div className="mt-5 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted">Select a project or work order to inspect it.</div> <div className="mt-3 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-3 py-5 text-center text-sm text-muted">Select a project or work order to inspect it.</div>
)} )}
</section> </section>
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel"> <section className="surface-panel">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">{workbenchMode === "heatmap" ? "Selected Day" : "Upcoming Agenda"}</p> <p className="section-kicker">{workbenchMode === "heatmap" ? "SELECTED DAY" : "UPCOMING AGENDA"}</p>
{workbenchMode === "heatmap" {workbenchMode === "heatmap"
? (selectedHeatmapCell ? <SelectedDayPanel cell={selectedHeatmapCell} onSelect={setSelectedFocusId} /> : <div className="mt-5 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted">Select a day in the heatmap to inspect its load.</div>) ? (selectedHeatmapCell ? <SelectedDayPanel cell={selectedHeatmapCell} onSelect={setSelectedFocusId} /> : <div className="mt-3 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-3 py-5 text-center text-sm text-muted">Select a day in the heatmap to inspect its load.</div>)
: <AgendaBoard records={agendaItems.slice(0, 8)} onSelect={setSelectedFocusId} compact />} : <AgendaBoard records={agendaItems.slice(0, 8)} onSelect={setSelectedFocusId} compact />}
</section> </section>
</aside> </aside>
@@ -640,9 +638,9 @@ export function WorkbenchPage() {
function MetricCard({ label, value }: { label: string; value: string | number }) { function MetricCard({ label, value }: { label: string; value: string | number }) {
return ( return (
<article className="rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel"> <article className="surface-panel-tight">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">{label}</p> <p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">{label}</p>
<div className="mt-2 text-xl font-extrabold text-text">{value}</div> <div className="mt-1 text-xl font-extrabold text-text">{value}</div>
</article> </article>
); );
} }
@@ -690,20 +688,19 @@ function OverviewBoard({
} }
return ( return (
<div className="space-y-4"> <div className="space-y-3">
<div className="flex flex-wrap items-center justify-between gap-3"> <div className="flex flex-wrap items-center justify-between gap-3">
<div> <div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Overview</p> <p className="section-kicker">OVERVIEW</p>
<p className="mt-2 text-sm text-muted">Scan project rollups, active work, station load, and release blockers without leaving the planner.</p>
</div> </div>
</div> </div>
{groupMode === "projects" ? ( {groupMode === "projects" ? (
<div className="grid gap-3 xl:grid-cols-[minmax(0,1.15fr)_minmax(0,0.85fr)]"> <div className="grid gap-3 xl:grid-cols-[minmax(0,1.15fr)_minmax(0,0.85fr)]">
<section className="rounded-[18px] border border-line/70 bg-page/60 p-3"> <section className="rounded-[18px] border border-line/70 bg-page/60 px-2 py-2">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Program Queue</p> <p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Program Queue</p>
<div className="mt-3 space-y-3"> <div className="mt-3 space-y-3">
{projects.map((record) => ( {projects.map((record) => (
<button key={record.id} type="button" onClick={() => onSelect(record.id)} className="block w-full rounded-[16px] border border-line/70 bg-surface/80 p-3 text-left transition hover:bg-surface"> <button key={record.id} type="button" onClick={() => onSelect(record.id)} className="block w-full rounded-[16px] border border-line/70 bg-surface/80 px-2 py-2 text-left transition hover:bg-surface">
<div className="flex flex-wrap items-center justify-between gap-3"> <div className="flex flex-wrap items-center justify-between gap-3">
<div> <div>
<div className="font-semibold text-text">{record.title}</div> <div className="font-semibold text-text">{record.title}</div>
@@ -718,11 +715,11 @@ function OverviewBoard({
))} ))}
</div> </div>
</section> </section>
<section className="rounded-[18px] border border-line/70 bg-page/60 p-3"> <section className="rounded-[18px] border border-line/70 bg-page/60 px-2 py-2">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Operation Load</p> <p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Operation Load</p>
<div className="mt-3 space-y-2"> <div className="mt-3 space-y-2">
{operations.slice(0, 10).map((record) => ( {operations.slice(0, 10).map((record) => (
<button key={record.id} type="button" onClick={() => onSelect(record.id)} className="flex w-full items-center justify-between gap-3 rounded-[16px] border border-line/70 bg-surface/80 px-3 py-2 text-left transition hover:bg-surface"> <button key={record.id} type="button" onClick={() => onSelect(record.id)} className="flex w-full items-center justify-between gap-3 rounded-[16px] border border-line/70 bg-surface/80 px-2 py-2 text-left transition hover:bg-surface">
<div> <div>
<div className="font-semibold text-text">{record.title}</div> <div className="font-semibold text-text">{record.title}</div>
<div className="mt-1 text-xs text-muted">{record.ownerLabel ?? "No parent work order"}</div> <div className="mt-1 text-xs text-muted">{record.ownerLabel ?? "No parent work order"}</div>
@@ -738,11 +735,10 @@ function OverviewBoard({
</div> </div>
) : null} ) : null}
{groupMode === "stations" ? ( {groupMode === "stations" ? (
<section className="rounded-[18px] border border-line/70 bg-page/60 p-3"> <section className="rounded-[18px] border border-line/70 bg-page/60 px-2 py-2">
<div className="flex flex-wrap items-center justify-between gap-3"> <div className="flex flex-wrap items-center justify-between gap-3">
<div> <div>
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Work Center Load</p> <p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Work Center Load</p>
<p className="mt-2 text-sm text-muted">Drag operations between stations to rebalance capacity. If a heatmap day is selected, drops target that date on the new station.</p>
</div> </div>
{draggingOperation ? ( {draggingOperation ? (
<div className="rounded-2xl border border-brand/40 bg-brand/10 px-3 py-2 text-xs font-semibold text-text"> <div className="rounded-2xl border border-brand/40 bg-brand/10 px-3 py-2 text-xs font-semibold text-text">
@@ -770,7 +766,7 @@ function OverviewBoard({
event.preventDefault(); event.preventDefault();
void onDropStation(station.stationId); void onDropStation(station.stationId);
}} }}
className={`rounded-[16px] border bg-surface/80 p-3 transition ${ className={`rounded-[16px] border bg-surface/80 px-2 py-2 transition ${
dropStationId === station.stationId dropStationId === station.stationId
? "border-brand bg-brand/10" ? "border-brand bg-brand/10"
: station.overloaded : station.overloaded
@@ -798,7 +794,7 @@ function OverviewBoard({
<div>Actual {station.totalActualMinutes} min</div> <div>Actual {station.totalActualMinutes} min</div>
</div> </div>
{draggingOperation ? ( {draggingOperation ? (
<div className="mt-3 rounded-[14px] border border-line/70 bg-page/60 p-2 text-xs text-muted"> <div className="mt-2 rounded-[14px] border border-line/70 bg-page/60 px-2 py-2 text-xs text-muted">
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-3">
<span>Projected util after drop</span> <span>Projected util after drop</span>
<span className="font-semibold text-text"> <span className="font-semibold text-text">
@@ -833,7 +829,7 @@ function OverviewBoard({
onDragOperation(null); onDragOperation(null);
onDropStationChange(null); onDropStationChange(null);
}} }}
className="cursor-grab rounded-[14px] border border-line/70 bg-page/60 p-2 active:cursor-grabbing" className="cursor-grab rounded-[14px] border border-line/70 bg-page/60 px-2 py-2 active:cursor-grabbing"
> >
<button type="button" onClick={() => onSelect(record.id)} className="block w-full text-left"> <button type="button" onClick={() => onSelect(record.id)} className="block w-full text-left">
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-3">
@@ -859,11 +855,11 @@ function OverviewBoard({
</section> </section>
) : null} ) : null}
{groupMode === "exceptions" ? ( {groupMode === "exceptions" ? (
<section className="rounded-[18px] border border-line/70 bg-page/60 p-3"> <section className="rounded-[18px] border border-line/70 bg-page/60 px-2 py-2">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Dispatch Exceptions</p> <p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Dispatch Exceptions</p>
<div className="mt-3 space-y-2"> <div className="mt-3 space-y-2">
{exceptionRows.map((record) => ( {exceptionRows.map((record) => (
<button key={record.id} type="button" onClick={() => onSelect(record.id)} className="flex w-full items-center justify-between gap-3 rounded-[16px] border border-line/70 bg-surface/80 px-3 py-2 text-left transition hover:bg-surface"> <button key={record.id} type="button" onClick={() => onSelect(record.id)} className="flex w-full items-center justify-between gap-3 rounded-[16px] border border-line/70 bg-surface/80 px-2 py-2 text-left transition hover:bg-surface">
<div> <div>
<div className="font-semibold text-text">{record.title}</div> <div className="font-semibold text-text">{record.title}</div>
<div className="mt-1 text-xs text-muted">{record.readinessState} - shortage {record.totalShortageQuantity}</div> <div className="mt-1 text-xs text-muted">{record.readinessState} - shortage {record.totalShortageQuantity}</div>
@@ -877,11 +873,11 @@ function OverviewBoard({
</div> </div>
</section> </section>
) : null} ) : null}
<section className="rounded-[18px] border border-line/70 bg-page/60 p-3"> <section className="rounded-[18px] border border-line/70 bg-page/60 px-2 py-2">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Active Work Orders</p> <p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Active Work Orders</p>
<div className="mt-3 grid gap-3 xl:grid-cols-2"> <div className="mt-3 grid gap-3 xl:grid-cols-2">
{workOrders.map((record) => ( {workOrders.map((record) => (
<button key={record.id} type="button" onClick={() => onSelect(record.id)} className="rounded-[16px] border border-line/70 bg-surface/80 p-3 text-left transition hover:bg-surface"> <button key={record.id} type="button" onClick={() => onSelect(record.id)} className="rounded-[16px] border border-line/70 bg-surface/80 px-2 py-2 text-left transition hover:bg-surface">
<div className="font-semibold text-text">{record.title}</div> <div className="font-semibold text-text">{record.title}</div>
<div className="mt-2 flex items-center justify-between gap-3 text-xs text-muted"> <div className="mt-2 flex items-center justify-between gap-3 text-xs text-muted">
<span>{record.readinessState}</span> <span>{record.readinessState}</span>
@@ -902,12 +898,11 @@ function HeatmapBoard({ heatmap, selectedDate, onSelectDate }: { heatmap: Heatma
} }
return ( return (
<div className="space-y-4"> <div className="space-y-3">
<div> <div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Load Heatmap</p> <p className="section-kicker">LOAD HEATMAP</p>
<p className="mt-2 text-sm text-muted">Dense daily load scan for operations and work orders, with late and blocked pressure highlighted.</p>
</div> </div>
<div className="overflow-x-auto rounded-[18px] border border-line/70 bg-page/60 p-4"> <div className="overflow-x-auto rounded-[18px] border border-line/70 bg-page/60 px-3 py-3">
<div className="flex gap-2"> <div className="flex gap-2">
<div className="flex flex-col gap-2 pt-7"> <div className="flex flex-col gap-2 pt-7">
{["M", "T", "W", "T", "F", "S", "S"].map((label) => <div key={label} className="h-9 text-xs font-semibold text-muted">{label}</div>)} {["M", "T", "W", "T", "F", "S", "S"].map((label) => <div key={label} className="h-9 text-xs font-semibold text-muted">{label}</div>)}
@@ -936,16 +931,15 @@ function HeatmapBoard({ heatmap, selectedDate, onSelectDate }: { heatmap: Heatma
function AgendaBoard({ records, onSelect, compact = false }: { records: FocusRecord[]; onSelect: (id: string) => void; compact?: boolean }) { function AgendaBoard({ records, onSelect, compact = false }: { records: FocusRecord[]; onSelect: (id: string) => void; compact?: boolean }) {
return ( return (
<div className={compact ? "mt-4 space-y-3" : "space-y-4"}> <div className={compact ? "mt-3 space-y-2" : "space-y-3"}>
{!compact ? ( {!compact ? (
<div> <div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Agenda</p> <p className="section-kicker">AGENDA</p>
<p className="mt-2 text-sm text-muted">Upcoming projects, work orders, and milestones ordered by due date.</p>
</div> </div>
) : null} ) : null}
<div className="space-y-2"> <div className="space-y-2">
{records.map((record) => ( {records.map((record) => (
<button key={record.id} type="button" onClick={() => onSelect(record.id)} className="flex w-full items-center justify-between gap-3 rounded-[16px] border border-line/70 bg-page/60 px-3 py-3 text-left transition hover:bg-page/80"> <button key={record.id} type="button" onClick={() => onSelect(record.id)} className="flex w-full items-center justify-between gap-3 rounded-[16px] border border-line/70 bg-page/60 px-2 py-2 text-left transition hover:bg-page/80">
<div> <div>
<div className="font-semibold text-text">{record.title}</div> <div className="font-semibold text-text">{record.title}</div>
<div className="mt-1 text-xs text-muted">{record.kind} - {record.ownerLabel ?? "No context"}</div> <div className="mt-1 text-xs text-muted">{record.kind} - {record.ownerLabel ?? "No context"}</div>
@@ -963,8 +957,8 @@ function AgendaBoard({ records, onSelect, compact = false }: { records: FocusRec
function SelectedDayPanel({ cell, onSelect }: { cell: HeatmapCell; onSelect: (id: string) => void }) { function SelectedDayPanel({ cell, onSelect }: { cell: HeatmapCell; onSelect: (id: string) => void }) {
return ( return (
<div className="mt-4 space-y-3"> <div className="mt-3 space-y-2">
<div className="rounded-[18px] border border-line/70 bg-page/60 p-3"> <div className="rounded-[18px] border border-line/70 bg-page/60 px-2 py-2">
<div className="text-sm font-semibold text-text">{formatDate(cell.dateKey, { weekday: "short", month: "short", day: "numeric" })}</div> <div className="text-sm font-semibold text-text">{formatDate(cell.dateKey, { weekday: "short", month: "short", day: "numeric" })}</div>
<div className="mt-2 flex items-center justify-between gap-3 text-xs text-muted"> <div className="mt-2 flex items-center justify-between gap-3 text-xs text-muted">
<span>{cell.count} scheduled</span> <span>{cell.count} scheduled</span>
@@ -973,7 +967,7 @@ function SelectedDayPanel({ cell, onSelect }: { cell: HeatmapCell; onSelect: (id
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
{cell.tasks.slice(0, 8).map((task) => ( {cell.tasks.slice(0, 8).map((task) => (
<button key={task.id} type="button" onClick={() => onSelect(task.id)} className="block w-full rounded-[16px] border border-line/70 bg-page/60 p-3 text-left transition hover:bg-page/80"> <button key={task.id} type="button" onClick={() => onSelect(task.id)} className="block w-full rounded-[16px] border border-line/70 bg-page/60 px-2 py-2 text-left transition hover:bg-page/80">
<div className="font-semibold text-text">{task.title}</div> <div className="font-semibold text-text">{task.title}</div>
<div className="mt-1 text-xs text-muted">{task.status.replaceAll("_", " ")} - {task.ownerLabel ?? "No context"}</div> <div className="mt-1 text-xs text-muted">{task.status.replaceAll("_", " ")} - {task.ownerLabel ?? "No context"}</div>
</button> </button>