workbench
This commit is contained in:
@@ -45,6 +45,7 @@ This file is the running release and change log for CODEXIUM. Keep it updated wh
|
|||||||
- Planning timeline now includes station day-load rollups, and Workbench slot suggestions use that server-backed per-day capacity data instead of only summary-level utilization heuristics
|
- Planning timeline now includes station day-load rollups, and Workbench slot suggestions use that server-backed per-day capacity data instead of only summary-level utilization heuristics
|
||||||
- Workbench now surfaces day-level capacity directly in the planner, including hot-station day counts on heatmap cells, selected-day station load breakdowns, and per-station hot-day chips in station grouping mode
|
- Workbench now surfaces day-level capacity directly in the planner, including hot-station day counts on heatmap cells, selected-day station load breakdowns, and per-station hot-day chips in station grouping mode
|
||||||
- Workbench exception prioritization now scores and ranks projects, work orders, agenda rows, and dispatch exceptions by lateness, blockage, shortage, readiness, and overload pressure, with inline priority chips for faster triage
|
- Workbench exception prioritization now scores and ranks projects, work orders, agenda rows, and dispatch exceptions by lateness, blockage, shortage, readiness, and overload pressure, with inline priority chips for faster triage
|
||||||
|
- Workbench now surfaces top-priority action lanes for `DO NOW`, `UNBLOCK`, and `RELEASE READY` records so planners can jump straight into ranked dispatch queues before working deeper lists
|
||||||
- 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
|
||||||
|
|||||||
@@ -551,6 +551,18 @@ export function WorkbenchPage() {
|
|||||||
.slice(0, 18),
|
.slice(0, 18),
|
||||||
[focusRecords, workbenchFilter]
|
[focusRecords, workbenchFilter]
|
||||||
);
|
);
|
||||||
|
const prioritizedRecords = useMemo(() => [...filteredFocusRecords].sort(comparePriority), [filteredFocusRecords]);
|
||||||
|
const actionLanes = useMemo(() => ({
|
||||||
|
doNow: prioritizedRecords
|
||||||
|
.filter((record) => record.overdue || (record.kind === "OPERATION" && (record.utilizationPercent ?? 0) > 100))
|
||||||
|
.slice(0, 4),
|
||||||
|
unblock: prioritizedRecords
|
||||||
|
.filter((record) => record.readinessState === "BLOCKED" || record.readinessState === "SHORTAGE" || record.readinessState === "PENDING_SUPPLY" || Boolean(record.blockedReason))
|
||||||
|
.slice(0, 4),
|
||||||
|
releaseReady: prioritizedRecords
|
||||||
|
.filter((record) => canQueueRelease(record))
|
||||||
|
.slice(0, 4),
|
||||||
|
}), [prioritizedRecords]);
|
||||||
const keyboardRecords = useMemo(() => {
|
const keyboardRecords = useMemo(() => {
|
||||||
if (workbenchMode === "agenda") {
|
if (workbenchMode === "agenda") {
|
||||||
return agendaItems;
|
return agendaItems;
|
||||||
@@ -1064,6 +1076,30 @@ export function WorkbenchPage() {
|
|||||||
<MetricCard label="Build / Buy" value={planningRollup ? `${planningRollup.summary.totalBuildQuantity} / ${planningRollup.summary.totalPurchaseQuantity}` : "0 / 0"} />
|
<MetricCard label="Build / Buy" value={planningRollup ? `${planningRollup.summary.totalBuildQuantity} / ${planningRollup.summary.totalPurchaseQuantity}` : "0 / 0"} />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section className="grid gap-3 xl:grid-cols-3">
|
||||||
|
<ActionLane
|
||||||
|
title="DO NOW"
|
||||||
|
accent="border-rose-300/60 bg-rose-500/10 text-rose-200"
|
||||||
|
records={actionLanes.doNow}
|
||||||
|
selectedId={selectedFocus?.id ?? null}
|
||||||
|
onSelect={setSelectedFocusId}
|
||||||
|
/>
|
||||||
|
<ActionLane
|
||||||
|
title="UNBLOCK"
|
||||||
|
accent="border-amber-300/60 bg-amber-400/10 text-amber-300"
|
||||||
|
records={actionLanes.unblock}
|
||||||
|
selectedId={selectedFocus?.id ?? null}
|
||||||
|
onSelect={setSelectedFocusId}
|
||||||
|
/>
|
||||||
|
<ActionLane
|
||||||
|
title="RELEASE READY"
|
||||||
|
accent="border-emerald-300/60 bg-emerald-500/10 text-emerald-300"
|
||||||
|
records={actionLanes.releaseReady}
|
||||||
|
selectedId={selectedFocus?.id ?? null}
|
||||||
|
onSelect={setSelectedFocusId}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
<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="surface-panel">
|
<section className="surface-panel">
|
||||||
@@ -1304,6 +1340,57 @@ function MetricCard({ label, value }: { label: string; value: string | number })
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ActionLane({
|
||||||
|
title,
|
||||||
|
accent,
|
||||||
|
records,
|
||||||
|
selectedId,
|
||||||
|
onSelect,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
accent: string;
|
||||||
|
records: FocusRecord[];
|
||||||
|
selectedId: string | null;
|
||||||
|
onSelect: (id: string) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<section className="surface-panel">
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
<p className="section-kicker">{title}</p>
|
||||||
|
<span className={`rounded-full border px-2 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] ${accent}`}>{records.length}</span>
|
||||||
|
</div>
|
||||||
|
{records.length === 0 ? (
|
||||||
|
<div className="mt-3 rounded-[16px] border border-dashed border-line/70 bg-page/60 px-3 py-4 text-sm text-muted">No items in this lane.</div>
|
||||||
|
) : (
|
||||||
|
<div className="mt-3 space-y-2">
|
||||||
|
{records.map((record) => (
|
||||||
|
<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-page/80 ${selectedId === record.id ? "border-brand bg-brand/10" : "border-line/70 bg-page/60"}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<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>
|
||||||
|
<div className="text-right text-xs text-muted">
|
||||||
|
<div>{formatDate(record.end)}</div>
|
||||||
|
<div>P{priorityScore(record)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 flex flex-wrap gap-2">
|
||||||
|
<RecordSignals record={record} />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function RecordSignals({ record, queued = false, selected = false }: { record: FocusRecord; queued?: boolean; selected?: boolean }) {
|
function RecordSignals({ record, queued = false, selected = false }: { record: FocusRecord; queued?: boolean; selected?: boolean }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
Reference in New Issue
Block a user