workbench
This commit is contained in:
@@ -36,6 +36,7 @@ This file is the running release and change log for CODEXIUM. Keep it updated wh
|
||||
- Continued density standardization across company settings and deeper manufacturing detail surfaces, including tighter admin/profile/theme sections, denser work-order execution panels, and compact issue/completion history cards
|
||||
- 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
|
||||
- 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
|
||||
|
||||
@@ -28,6 +28,10 @@
|
||||
.module-title {
|
||||
@apply mt-1 text-xl font-bold uppercase tracking-[0.08em] text-text;
|
||||
}
|
||||
|
||||
.planner-sticky-bar {
|
||||
@apply sticky top-3 z-20 rounded-[18px] border border-line/70 bg-surface/90 p-3 shadow-panel backdrop-blur;
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
|
||||
@@ -165,6 +165,10 @@ function matchesWorkbenchFilter(record: FocusRecord, filter: WorkbenchFilter) {
|
||||
}
|
||||
}
|
||||
|
||||
function exceptionTargetId(exceptionId: string) {
|
||||
return exceptionId.startsWith("project-") ? exceptionId : exceptionId.replace("work-order-unscheduled-", "work-order-");
|
||||
}
|
||||
|
||||
export function WorkbenchPage() {
|
||||
const navigate = useNavigate();
|
||||
const { token } = useAuth();
|
||||
@@ -386,7 +390,7 @@ export function WorkbenchPage() {
|
||||
|
||||
return (
|
||||
<section className="page-stack">
|
||||
<div className="surface-panel">
|
||||
<div className="planner-sticky-bar">
|
||||
<div className="flex flex-col gap-3 xl:flex-row xl:items-start xl:justify-between">
|
||||
<div>
|
||||
<p className="section-kicker">PLANNING</p>
|
||||
@@ -419,6 +423,19 @@ export function WorkbenchPage() {
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap items-center gap-2 text-xs text-muted">
|
||||
<span className="rounded-full border border-line/70 bg-page/60 px-2 py-1">{filteredFocusRecords.length} visible rows</span>
|
||||
{selectedFocus ? (
|
||||
<span className="rounded-full border border-brand/40 bg-brand/10 px-2 py-1 text-text">
|
||||
Selected {selectedFocus.kind.toLowerCase()}: {selectedFocus.title}
|
||||
</span>
|
||||
) : null}
|
||||
{selectedHeatmapDate ? (
|
||||
<button type="button" onClick={() => setSelectedHeatmapDate(null)} className="rounded-full border border-line/70 bg-page/60 px-2 py-1 text-text">
|
||||
Day {formatDate(selectedHeatmapDate)} selected - clear
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section className="grid gap-3 xl:grid-cols-10">
|
||||
@@ -449,7 +466,7 @@ export function WorkbenchPage() {
|
||||
) : (
|
||||
<div className="mt-3 space-y-2">
|
||||
{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 px-2 py-2 text-left transition hover:bg-page/80">
|
||||
<button key={exception.id} type="button" onClick={() => setSelectedFocusId(exceptionTargetId(exception.id))} className={`block w-full rounded-[18px] border px-2 py-2 text-left transition hover:bg-page/80 ${selectedFocusId === exceptionTargetId(exception.id) ? "border-brand bg-brand/10" : "border-line/70 bg-page/60"}`}>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">{exception.kind === "PROJECT" ? "Project" : "Work Order"}</div>
|
||||
@@ -473,19 +490,28 @@ export function WorkbenchPage() {
|
||||
</div>
|
||||
<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="/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="/manufacturing/work-orders" className="rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">Dispatch build</Link>
|
||||
<Link to="/purchasing/orders" className="rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">Dispatch buy</Link>
|
||||
</div>
|
||||
</section>
|
||||
</aside>
|
||||
|
||||
<div className="surface-panel">
|
||||
<div className="mb-3 flex flex-wrap items-center justify-between gap-2 rounded-[16px] border border-line/70 bg-page/60 px-2 py-2 text-xs text-muted">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="rounded-full border border-line/70 px-2 py-1">{workbenchMode.toUpperCase()}</span>
|
||||
<span className="rounded-full border border-line/70 px-2 py-1">{workbenchGroup.toUpperCase()}</span>
|
||||
<span className="rounded-full border border-line/70 px-2 py-1">{workbenchFilter.replace("-", " ").toUpperCase()}</span>
|
||||
</div>
|
||||
{selectedFocus ? <div className="font-semibold text-text">{selectedFocus.title}</div> : <div>Select a record to inspect and act.</div>}
|
||||
</div>
|
||||
{workbenchMode === "overview" ? (
|
||||
<OverviewBoard
|
||||
focusRecords={filteredFocusRecords}
|
||||
stationLoads={stationLoads}
|
||||
groupMode={workbenchGroup}
|
||||
onSelect={setSelectedFocusId}
|
||||
selectedId={selectedFocus?.id ?? null}
|
||||
draggingOperation={draggingOperation}
|
||||
dropStationId={dropStationId}
|
||||
selectedHeatmapDate={selectedHeatmapDate}
|
||||
@@ -495,7 +521,7 @@ export function WorkbenchPage() {
|
||||
/>
|
||||
) : null}
|
||||
{workbenchMode === "heatmap" ? <HeatmapBoard heatmap={heatmap} selectedDate={selectedHeatmapDate} onSelectDate={setSelectedHeatmapDate} /> : null}
|
||||
{workbenchMode === "agenda" ? <AgendaBoard records={agendaItems} onSelect={setSelectedFocusId} /> : null}
|
||||
{workbenchMode === "agenda" ? <AgendaBoard records={agendaItems} onSelect={setSelectedFocusId} selectedId={selectedFocus?.id ?? null} /> : null}
|
||||
</div>
|
||||
|
||||
<aside className="space-y-3">
|
||||
@@ -617,6 +643,7 @@ export function WorkbenchPage() {
|
||||
{action.label}
|
||||
</button>
|
||||
))}
|
||||
{selectedFocus.detailHref ? <button type="button" onClick={() => navigate(selectedFocus.detailHref!)} className="rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">Open record</button> : null}
|
||||
<button type="button" onClick={() => setWorkbenchMode("heatmap")} className="rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">View load</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -627,8 +654,8 @@ export function WorkbenchPage() {
|
||||
<section className="surface-panel">
|
||||
<p className="section-kicker">{workbenchMode === "heatmap" ? "SELECTED DAY" : "UPCOMING AGENDA"}</p>
|
||||
{workbenchMode === "heatmap"
|
||||
? (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 />}
|
||||
? (selectedHeatmapCell ? <SelectedDayPanel cell={selectedHeatmapCell} onSelect={setSelectedFocusId} selectedId={selectedFocus?.id ?? null} /> : <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} selectedId={selectedFocus?.id ?? null} compact />}
|
||||
</section>
|
||||
</aside>
|
||||
</div>
|
||||
@@ -650,6 +677,7 @@ function OverviewBoard({
|
||||
stationLoads,
|
||||
groupMode,
|
||||
onSelect,
|
||||
selectedId,
|
||||
draggingOperation,
|
||||
dropStationId,
|
||||
selectedHeatmapDate,
|
||||
@@ -661,6 +689,7 @@ function OverviewBoard({
|
||||
stationLoads: PlanningStationLoadDto[];
|
||||
groupMode: WorkbenchGroup;
|
||||
onSelect: (id: string) => void;
|
||||
selectedId: string | null;
|
||||
draggingOperation: DraggingOperation | null;
|
||||
dropStationId: string | null;
|
||||
selectedHeatmapDate: string | null;
|
||||
@@ -700,7 +729,7 @@ function OverviewBoard({
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Program Queue</p>
|
||||
<div className="mt-3 space-y-3">
|
||||
{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 px-2 py-2 text-left transition hover:bg-surface">
|
||||
<button key={record.id} type="button" onClick={() => onSelect(record.id)} className={`block w-full rounded-[16px] border px-2 py-2 text-left transition hover:bg-surface ${selectedId === record.id ? "border-brand bg-brand/10" : "border-line/70 bg-surface/80"}`}>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<div className="font-semibold text-text">{record.title}</div>
|
||||
@@ -719,7 +748,7 @@ function OverviewBoard({
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Operation Load</p>
|
||||
<div className="mt-3 space-y-2">
|
||||
{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-2 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 px-2 py-2 text-left transition hover:bg-surface ${selectedId === record.id ? "border-brand bg-brand/10" : "border-line/70 bg-surface/80"}`}>
|
||||
<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>
|
||||
@@ -829,7 +858,7 @@ function OverviewBoard({
|
||||
onDragOperation(null);
|
||||
onDropStationChange(null);
|
||||
}}
|
||||
className="cursor-grab rounded-[14px] border border-line/70 bg-page/60 px-2 py-2 active:cursor-grabbing"
|
||||
className={`cursor-grab rounded-[14px] border px-2 py-2 active:cursor-grabbing ${selectedId === record.id ? "border-brand bg-brand/10" : "border-line/70 bg-page/60"}`}
|
||||
>
|
||||
<button type="button" onClick={() => onSelect(record.id)} className="block w-full text-left">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
@@ -859,7 +888,7 @@ function OverviewBoard({
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Dispatch Exceptions</p>
|
||||
<div className="mt-3 space-y-2">
|
||||
{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-2 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 px-2 py-2 text-left transition hover:bg-surface ${selectedId === record.id ? "border-brand bg-brand/10" : "border-line/70 bg-surface/80"}`}>
|
||||
<div>
|
||||
<div className="font-semibold text-text">{record.title}</div>
|
||||
<div className="mt-1 text-xs text-muted">{record.readinessState} - shortage {record.totalShortageQuantity}</div>
|
||||
@@ -877,7 +906,7 @@ function OverviewBoard({
|
||||
<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">
|
||||
{workOrders.map((record) => (
|
||||
<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">
|
||||
<button key={record.id} type="button" onClick={() => onSelect(record.id)} className={`rounded-[16px] border px-2 py-2 text-left transition hover:bg-surface ${selectedId === record.id ? "border-brand bg-brand/10" : "border-line/70 bg-surface/80"}`}>
|
||||
<div className="font-semibold text-text">{record.title}</div>
|
||||
<div className="mt-2 flex items-center justify-between gap-3 text-xs text-muted">
|
||||
<span>{record.readinessState}</span>
|
||||
@@ -902,6 +931,11 @@ function HeatmapBoard({ heatmap, selectedDate, onSelectDate }: { heatmap: Heatma
|
||||
<div>
|
||||
<p className="section-kicker">LOAD HEATMAP</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs text-muted">
|
||||
<span className="rounded-full border border-line/70 bg-page/60 px-2 py-1">84-day horizon</span>
|
||||
<span className="rounded-full border border-line/70 bg-page/60 px-2 py-1">{heatmap.reduce((sum, cell) => sum + cell.count, 0)} scheduled touchpoints</span>
|
||||
{selectedDate ? <span className="rounded-full border border-brand/40 bg-brand/10 px-2 py-1 text-text">Selected {formatDate(selectedDate, { weekday: "short", month: "short", day: "numeric" })}</span> : null}
|
||||
</div>
|
||||
<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 flex-col gap-2 pt-7">
|
||||
@@ -929,7 +963,7 @@ 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, selectedId, compact = false }: { records: FocusRecord[]; onSelect: (id: string) => void; selectedId: string | null; compact?: boolean }) {
|
||||
return (
|
||||
<div className={compact ? "mt-3 space-y-2" : "space-y-3"}>
|
||||
{!compact ? (
|
||||
@@ -939,7 +973,7 @@ function AgendaBoard({ records, onSelect, compact = false }: { records: FocusRec
|
||||
) : null}
|
||||
<div className="space-y-2">
|
||||
{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-2 py-2 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 px-2 py-2 text-left transition hover:bg-page/80 ${selectedId === record.id ? "border-brand bg-brand/10" : "border-line/70 bg-page/60"}`}>
|
||||
<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>
|
||||
@@ -955,7 +989,7 @@ function AgendaBoard({ records, onSelect, compact = false }: { records: FocusRec
|
||||
);
|
||||
}
|
||||
|
||||
function SelectedDayPanel({ cell, onSelect }: { cell: HeatmapCell; onSelect: (id: string) => void }) {
|
||||
function SelectedDayPanel({ cell, onSelect, selectedId }: { cell: HeatmapCell; onSelect: (id: string) => void; selectedId: string | null }) {
|
||||
return (
|
||||
<div className="mt-3 space-y-2">
|
||||
<div className="rounded-[18px] border border-line/70 bg-page/60 px-2 py-2">
|
||||
@@ -964,10 +998,11 @@ function SelectedDayPanel({ cell, onSelect }: { cell: HeatmapCell; onSelect: (id
|
||||
<span>{cell.count} scheduled</span>
|
||||
<span>{cell.lateCount} late</span>
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-muted">{cell.blockedCount} blocked</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{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 px-2 py-2 text-left transition hover:bg-page/80">
|
||||
<button key={task.id} type="button" onClick={() => onSelect(task.id)} className={`block w-full rounded-[16px] border px-2 py-2 text-left transition hover:bg-page/80 ${selectedId === task.id ? "border-brand bg-brand/10" : "border-line/70 bg-page/60"}`}>
|
||||
<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>
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user