This commit is contained in:
2026-03-18 23:42:30 -05:00
parent 7b65fe06cf
commit 061057339b
3 changed files with 309 additions and 12 deletions

View File

@@ -38,6 +38,8 @@ This file is the running release and change log for CODEXIUM. Keep it updated wh
- 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
- 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
- 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

View File

@@ -112,6 +112,7 @@ This file tracks work that still needs to be completed. Shipped phase history an
### Planning and scheduling
- Standardize dense UI primitives and shared page shells so future Workbench, dashboard, and operational screens reuse the same cards, filter bars, empty states, and section wrappers instead of reintroducing ad hoc layout patterns
- Task dependencies, milestones, and progress updates
- Manufacturing calendar views and deeper bottleneck visibility beyond the shipped station load and overload workbench summaries
- Labor and machine scheduling support beyond the shipped station calendar/capacity foundation

View File

@@ -62,8 +62,16 @@ type DraggingOperation = {
start: string;
loadMinutes: number;
};
type SavedWorkbenchView = {
id: string;
name: string;
mode: WorkbenchMode;
group: WorkbenchGroup;
filter: WorkbenchFilter;
};
const DAY_MS = 24 * 60 * 60 * 1000;
const WORKBENCH_VIEW_STORAGE_KEY = "codexium.workbench.savedViews";
function formatDate(value: string | null, options?: Intl.DateTimeFormatOptions) {
if (!value) {
@@ -201,6 +209,10 @@ function exceptionTargetId(exceptionId: string) {
return exceptionId.startsWith("project-") ? exceptionId : exceptionId.replace("work-order-unscheduled-", "work-order-");
}
function canQueueRelease(record: FocusRecord) {
return record.kind === "WORK_ORDER" && record.releaseReady && record.actions.some((action) => action.kind === "RELEASE_WORK_ORDER" && action.workOrderId);
}
export function WorkbenchPage() {
const navigate = useNavigate();
const { token } = useAuth();
@@ -218,6 +230,36 @@ export function WorkbenchPage() {
const [stations, setStations] = useState<ManufacturingStationDto[]>([]);
const [draggingOperation, setDraggingOperation] = useState<DraggingOperation | null>(null);
const [dropStationId, setDropStationId] = useState<string | null>(null);
const [queuedWorkOrderIds, setQueuedWorkOrderIds] = useState<string[]>([]);
const [savedViews, setSavedViews] = useState<SavedWorkbenchView[]>([]);
const [isBatchReleasing, setIsBatchReleasing] = useState(false);
const [selectedOperationIds, setSelectedOperationIds] = useState<string[]>([]);
const [batchRescheduleStart, setBatchRescheduleStart] = useState("");
const [batchStationId, setBatchStationId] = useState("");
const [isBatchRescheduling, setIsBatchRescheduling] = useState(false);
useEffect(() => {
if (typeof window === "undefined") {
return;
}
const stored = window.localStorage.getItem(WORKBENCH_VIEW_STORAGE_KEY);
if (!stored) {
return;
}
try {
const parsed = JSON.parse(stored) as SavedWorkbenchView[];
setSavedViews(Array.isArray(parsed) ? parsed : []);
} catch {
setSavedViews([]);
}
}, []);
useEffect(() => {
if (typeof window === "undefined") {
return;
}
window.localStorage.setItem(WORKBENCH_VIEW_STORAGE_KEY, JSON.stringify(savedViews));
}, [savedViews]);
useEffect(() => {
if (!token) {
@@ -245,6 +287,33 @@ export function WorkbenchPage() {
const filteredFocusRecords = useMemo(() => focusRecords.filter((record) => matchesWorkbenchFilter(record, workbenchFilter)), [focusRecords, workbenchFilter]);
const focusById = useMemo(() => new Map(focusRecords.map((record) => [record.id, record])), [focusRecords]);
const selectedFocus = selectedFocusId ? focusById.get(selectedFocusId) ?? null : filteredFocusRecords[0] ?? focusRecords[0] ?? null;
const queuedRecords = useMemo(() => queuedWorkOrderIds.reduce<FocusRecord[]>((records, id) => {
const match = focusRecords.find((record) => (record.workOrderId ?? record.id) === id);
if (match) {
records.push(match);
}
return records;
}, []), [focusRecords, queuedWorkOrderIds]);
const releasableQueuedRecords = useMemo(() => queuedRecords.filter((record) => canQueueRelease(record)), [queuedRecords]);
const visibleReleasableRecords = useMemo(() => filteredFocusRecords.filter((record) => canQueueRelease(record)), [filteredFocusRecords]);
const selectedOperations = useMemo(() => selectedOperationIds.reduce<FocusRecord[]>((records, id) => {
const match = focusRecords.find((record) => record.id === id && record.kind === "OPERATION");
if (match) {
records.push(match);
}
return records;
}, []), [focusRecords, selectedOperationIds]);
useEffect(() => {
setQueuedWorkOrderIds((current) => current.filter((id) => {
const record = focusRecords.find((candidate) => (candidate.workOrderId ?? candidate.id) === id);
return record ? canQueueRelease(record) : false;
}));
}, [focusRecords]);
useEffect(() => {
setSelectedOperationIds((current) => current.filter((id) => focusRecords.some((record) => record.id === id && record.kind === "OPERATION")));
}, [focusRecords]);
useEffect(() => {
if (selectedFocus?.kind === "OPERATION") {
@@ -256,6 +325,20 @@ export function WorkbenchPage() {
}
}, [selectedFocus?.id, selectedFocus?.kind, selectedFocus?.start, selectedFocus?.stationId]);
useEffect(() => {
if (selectedOperations.length === 0) {
setBatchRescheduleStart("");
setBatchStationId("");
return;
}
const first = selectedOperations[0];
if (!first) {
return;
}
setBatchRescheduleStart((current) => current || first.start.slice(0, 16));
setBatchStationId((current) => current || first.stationId || "");
}, [selectedOperations]);
const heatmap = useMemo(() => {
const start = summary ? startOfDay(new Date(summary.horizonStart)) : startOfDay(new Date());
const cells = new Map<string, HeatmapCell>();
@@ -340,6 +423,7 @@ export function WorkbenchPage() {
.slice(0, 10)
.flatMap((station) => (stationBuckets.get(station.stationId) ?? []).slice(0, 5));
}, [agendaItems, filteredFocusRecords, selectedHeatmapCell?.dateKey, selectedHeatmapCell?.tasks, stationLoads, workbenchGroup, workbenchMode]);
const visibleOperations = useMemo(() => keyboardRecords.filter((record) => record.kind === "OPERATION"), [keyboardRecords]);
useEffect(() => {
function handleKeydown(event: KeyboardEvent) {
@@ -429,6 +513,110 @@ export function WorkbenchPage() {
}
}
function addRecordToQueue(record: FocusRecord) {
if (!canQueueRelease(record)) {
return;
}
const queueId = record.workOrderId ?? record.id;
setQueuedWorkOrderIds((current) => (current.includes(queueId) ? current : [...current, queueId]));
setStatus(`Queued ${record.title} for batch release.`);
}
function queueVisibleReady() {
const nextIds = visibleReleasableRecords.map((record) => record.workOrderId ?? record.id);
if (nextIds.length === 0) {
return;
}
setQueuedWorkOrderIds((current) => [...new Set([...current, ...nextIds])]);
setStatus(`Queued ${nextIds.length} visible release-ready work orders.`);
}
async function releaseQueuedWorkOrders() {
if (!token || releasableQueuedRecords.length === 0) {
return;
}
setIsBatchReleasing(true);
try {
await Promise.all(releasableQueuedRecords.map((record) => api.updateWorkOrderStatus(token, record.workOrderId!, { status: "RELEASED" })));
setQueuedWorkOrderIds([]);
await refreshWorkbench(`Released ${releasableQueuedRecords.length} queued work orders from Workbench.`);
} catch (error: unknown) {
const message = error instanceof ApiError ? error.message : "Unable to release queued work orders.";
setStatus(message);
} finally {
setIsBatchReleasing(false);
}
}
function saveCurrentView() {
if (typeof window === "undefined") {
return;
}
const suggested = `${workbenchMode.toUpperCase()} ${workbenchGroup.toUpperCase()}`;
const name = window.prompt("Save workbench view as:", suggested)?.trim();
if (!name) {
return;
}
const nextView: SavedWorkbenchView = {
id: `${Date.now()}`,
name,
mode: workbenchMode,
group: workbenchGroup,
filter: workbenchFilter,
};
setSavedViews((current) => [nextView, ...current].slice(0, 8));
setStatus(`Saved workbench view: ${name}.`);
}
function applySavedView(view: SavedWorkbenchView) {
setWorkbenchMode(view.mode);
setWorkbenchGroup(view.group);
setWorkbenchFilter(view.filter);
setStatus(`Loaded saved view: ${view.name}.`);
}
function toggleOperationSelection(record: FocusRecord) {
if (record.kind !== "OPERATION") {
return;
}
setSelectedOperationIds((current) => current.includes(record.id) ? current.filter((id) => id !== record.id) : [...current, record.id]);
}
function selectVisibleOperations() {
const nextIds = visibleOperations.map((record) => record.id);
if (nextIds.length === 0) {
return;
}
setSelectedOperationIds((current) => [...new Set([...current, ...nextIds])]);
setStatus(`Selected ${nextIds.length} visible operations for batch rebalance.`);
}
async function applyBatchReschedule() {
if (!token || selectedOperations.length === 0 || !batchRescheduleStart) {
return;
}
setIsBatchRescheduling(true);
try {
const plannedStart = new Date(batchRescheduleStart).toISOString();
await Promise.all(selectedOperations.map((record) => {
if (!record.workOrderId || !record.entityId) {
return Promise.resolve();
}
return api.updateWorkOrderOperationSchedule(token, record.workOrderId, record.entityId, {
plannedStart,
stationId: batchStationId || record.stationId || null,
});
}));
setSelectedOperationIds([]);
await refreshWorkbench(`Rebalanced ${selectedOperations.length} operations from Workbench.`);
} catch (error: unknown) {
const message = error instanceof ApiError ? error.message : "Unable to rebalance selected operations.";
setStatus(message);
} finally {
setIsBatchRescheduling(false);
}
}
async function rebalanceOperation(record: FocusRecord, nextStartIso?: string, nextStationId?: string | null) {
if (!token || record.kind !== "OPERATION" || !record.workOrderId || !record.entityId) {
return;
@@ -544,6 +732,8 @@ export function WorkbenchPage() {
<span className="rounded-full border border-line/70 bg-page/60 px-2 py-1">Up/Down navigate</span>
<span className="rounded-full border border-line/70 bg-page/60 px-2 py-1">Enter open</span>
<span className="rounded-full border border-line/70 bg-page/60 px-2 py-1">Esc clear</span>
<span className="rounded-full border border-line/70 bg-page/60 px-2 py-1">{releasableQueuedRecords.length} queued</span>
<span className="rounded-full border border-line/70 bg-page/60 px-2 py-1">{selectedOperations.length} ops selected</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}
@@ -555,6 +745,90 @@ export function WorkbenchPage() {
</button>
) : null}
</div>
<div className="mt-3 flex flex-wrap items-center gap-2">
<button type="button" onClick={saveCurrentView} className="rounded-2xl border border-line/70 bg-page/60 px-3 py-2 text-sm font-semibold text-text">
Save view
</button>
<button type="button" onClick={queueVisibleReady} disabled={visibleReleasableRecords.length === 0} className="rounded-2xl border border-line/70 bg-page/60 px-3 py-2 text-sm font-semibold text-text disabled:cursor-not-allowed disabled:opacity-60">
Queue visible ready
</button>
<button type="button" onClick={() => void releaseQueuedWorkOrders()} disabled={isBatchReleasing || releasableQueuedRecords.length === 0} className="rounded-2xl bg-brand px-3 py-2 text-sm font-semibold text-white disabled:cursor-not-allowed disabled:opacity-60">
{isBatchReleasing ? "Releasing..." : "Release queued"}
</button>
{queuedWorkOrderIds.length > 0 ? (
<button type="button" onClick={() => setQueuedWorkOrderIds([])} className="rounded-2xl border border-line/70 bg-page/60 px-3 py-2 text-sm font-semibold text-text">
Clear queue
</button>
) : null}
<button type="button" onClick={selectVisibleOperations} disabled={visibleOperations.length === 0} className="rounded-2xl border border-line/70 bg-page/60 px-3 py-2 text-sm font-semibold text-text disabled:cursor-not-allowed disabled:opacity-60">
Select visible ops
</button>
{selectedOperationIds.length > 0 ? (
<button type="button" onClick={() => setSelectedOperationIds([])} className="rounded-2xl border border-line/70 bg-page/60 px-3 py-2 text-sm font-semibold text-text">
Clear op selection
</button>
) : null}
</div>
{savedViews.length > 0 ? (
<div className="mt-3 flex flex-wrap items-center gap-2">
{savedViews.map((view) => (
<div key={view.id} className="flex items-center overflow-hidden rounded-2xl border border-line/70 bg-page/60">
<button type="button" onClick={() => applySavedView(view)} className="px-3 py-2 text-sm font-semibold text-text">
{view.name}
</button>
<button type="button" onClick={() => setSavedViews((current) => current.filter((candidate) => candidate.id !== view.id))} className="border-l border-line/70 px-2 py-2 text-xs font-semibold uppercase tracking-[0.16em] text-muted">
X
</button>
</div>
))}
</div>
) : null}
{queuedRecords.length > 0 ? (
<div className="mt-3 flex flex-wrap items-center gap-2 text-xs text-muted">
{queuedRecords.slice(0, 6).map((record) => (
<span key={record.workOrderId ?? record.id} className="rounded-full border border-brand/40 bg-brand/10 px-2 py-1 text-text">
{record.title}
</span>
))}
{queuedRecords.length > 6 ? <span className="rounded-full border border-line/70 bg-page/60 px-2 py-1">+{queuedRecords.length - 6} more</span> : null}
</div>
) : null}
{selectedOperations.length > 0 ? (
<div className="mt-3 rounded-[18px] border border-line/70 bg-page/60 px-3 py-3">
<div className="section-kicker">BATCH REBALANCE</div>
<div className="mt-3 grid gap-2 xl:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto]">
<input
type="datetime-local"
value={batchRescheduleStart}
onChange={(event) => setBatchRescheduleStart(event.target.value)}
className="rounded-2xl border border-line/70 bg-surface px-2 py-2 text-sm text-text outline-none transition focus:border-brand"
/>
<select
value={batchStationId}
onChange={(event) => setBatchStationId(event.target.value)}
className="rounded-2xl border border-line/70 bg-surface px-2 py-2 text-sm text-text outline-none transition focus:border-brand"
>
<option value="">Keep current station</option>
{stations.filter((station) => station.isActive).map((station) => (
<option key={station.id} value={station.id}>
{station.code} - {station.name}
</option>
))}
</select>
<button type="button" onClick={() => void applyBatchReschedule()} disabled={isBatchRescheduling || !batchRescheduleStart} className="rounded-2xl bg-brand px-3 py-2 text-sm font-semibold text-white disabled:cursor-not-allowed disabled:opacity-60">
{isBatchRescheduling ? "Applying..." : "Apply batch"}
</button>
</div>
<div className="mt-3 flex flex-wrap gap-2 text-xs text-muted">
{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">
{record.title}
</span>
))}
{selectedOperations.length > 6 ? <span className="rounded-full border border-line/70 bg-surface px-2 py-1">+{selectedOperations.length - 6} more</span> : null}
</div>
</div>
) : null}
</div>
<section className="grid gap-3 xl:grid-cols-10">
@@ -630,6 +904,7 @@ export function WorkbenchPage() {
stationLoads={stationLoads}
groupMode={workbenchGroup}
onSelect={setSelectedFocusId}
selectedOperationIds={selectedOperationIds}
selectedId={selectedFocus?.id ?? null}
draggingOperation={draggingOperation}
dropStationId={dropStationId}
@@ -640,7 +915,7 @@ export function WorkbenchPage() {
/>
) : null}
{workbenchMode === "heatmap" ? <HeatmapBoard heatmap={heatmap} selectedDate={selectedHeatmapDate} onSelectDate={setSelectedHeatmapDate} /> : null}
{workbenchMode === "agenda" ? <AgendaBoard records={agendaItems} onSelect={setSelectedFocusId} selectedId={selectedFocus?.id ?? null} /> : null}
{workbenchMode === "agenda" ? <AgendaBoard records={agendaItems} onSelect={setSelectedFocusId} selectedOperationIds={selectedOperationIds} selectedId={selectedFocus?.id ?? null} /> : null}
</div>
<aside className="space-y-3">
@@ -653,7 +928,7 @@ export function WorkbenchPage() {
<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-3 flex flex-wrap gap-2">
<RecordSignals record={selectedFocus} />
<RecordSignals record={selectedFocus} queued={queuedWorkOrderIds.includes(selectedFocus.workOrderId ?? selectedFocus.id)} selected={selectedOperationIds.includes(selectedFocus.id)} />
</div>
</div>
<div className="rounded-[18px] border border-line/70 bg-page/60 px-2 py-2 text-sm">
@@ -755,6 +1030,21 @@ export function WorkbenchPage() {
</div>
) : null}
<div className="flex flex-wrap gap-2">
{canQueueRelease(selectedFocus) ? (
<button
type="button"
onClick={() => addRecordToQueue(selectedFocus)}
disabled={queuedWorkOrderIds.includes(selectedFocus.workOrderId ?? selectedFocus.id)}
className="rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text disabled:cursor-not-allowed disabled:opacity-60"
>
{queuedWorkOrderIds.includes(selectedFocus.workOrderId ?? selectedFocus.id) ? "Queued" : "Queue release"}
</button>
) : null}
{selectedFocus.kind === "OPERATION" ? (
<button type="button" onClick={() => toggleOperationSelection(selectedFocus)} className="rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">
{selectedOperationIds.includes(selectedFocus.id) ? "Deselect op" : "Select op"}
</button>
) : null}
{selectedFocus.actions.map((action, index) => (
<button
key={`${action.kind}-${index}`}
@@ -776,8 +1066,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} 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 />}
? (selectedHeatmapCell ? <SelectedDayPanel cell={selectedHeatmapCell} onSelect={setSelectedFocusId} selectedOperationIds={selectedOperationIds} 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} selectedOperationIds={selectedOperationIds} selectedId={selectedFocus?.id ?? null} compact />}
</section>
</aside>
</div>
@@ -794,12 +1084,14 @@ function MetricCard({ label, value }: { label: string; value: string | number })
);
}
function RecordSignals({ record }: { record: FocusRecord }) {
function RecordSignals({ record, queued = false, selected = false }: { record: FocusRecord; queued?: boolean; selected?: boolean }) {
return (
<>
<span className={`rounded-full border px-2 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] ${readinessTone(record)}`}>
{readinessLabel(record)}
</span>
{selected ? <span className="rounded-full border border-cyan-300/60 bg-cyan-400/10 px-2 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-cyan-300">SELECTED</span> : null}
{queued ? <span className="rounded-full border border-brand/40 bg-brand/10 px-2 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-text">QUEUED</span> : null}
{record.releaseReady ? <span className="rounded-full border border-emerald-300/60 bg-emerald-500/10 px-2 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-emerald-300">RELEASE</span> : null}
{record.totalShortageQuantity > 0 ? <span className="rounded-full border border-orange-300/60 bg-orange-400/10 px-2 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-orange-300">SHORT {record.totalShortageQuantity}</span> : null}
{record.blockedReason ? <span className="rounded-full border border-amber-300/60 bg-amber-400/10 px-2 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-amber-300">HOLD</span> : null}
@@ -812,6 +1104,7 @@ function OverviewBoard({
stationLoads,
groupMode,
onSelect,
selectedOperationIds,
selectedId,
draggingOperation,
dropStationId,
@@ -824,6 +1117,7 @@ function OverviewBoard({
stationLoads: PlanningStationLoadDto[];
groupMode: WorkbenchGroup;
onSelect: (id: string) => void;
selectedOperationIds: string[];
selectedId: string | null;
draggingOperation: DraggingOperation | null;
dropStationId: string | null;
@@ -870,7 +1164,7 @@ function OverviewBoard({
<div className="font-semibold text-text">{record.title}</div>
<div className="mt-1 text-xs text-muted">{record.ownerLabel ?? "No owner context"}</div>
<div className="mt-2 flex flex-wrap gap-2">
<RecordSignals record={record} />
<RecordSignals record={record} selected={selectedOperationIds.includes(record.id)} />
</div>
</div>
<div className="text-right text-xs text-muted">
@@ -891,7 +1185,7 @@ function OverviewBoard({
<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-2 flex flex-wrap gap-2">
<RecordSignals record={record} />
<RecordSignals record={record} selected={selectedOperationIds.includes(record.id)} />
</div>
</div>
<div className="text-right text-xs text-muted">
@@ -1007,7 +1301,7 @@ function OverviewBoard({
<div className="font-semibold text-text">{record.ownerLabel ?? record.title}</div>
<div className="mt-1 text-xs text-muted">{formatDate(record.start, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" })}</div>
<div className="mt-2 flex flex-wrap gap-2">
<RecordSignals record={record} />
<RecordSignals record={record} selected={selectedOperationIds.includes(record.id)} />
</div>
</div>
<div className="text-right text-xs text-muted">
@@ -1111,7 +1405,7 @@ function HeatmapBoard({ heatmap, selectedDate, onSelectDate }: { heatmap: Heatma
);
}
function AgendaBoard({ records, onSelect, selectedId, compact = false }: { records: FocusRecord[]; onSelect: (id: string) => void; selectedId: string | null; compact?: boolean }) {
function AgendaBoard({ records, onSelect, selectedOperationIds, selectedId, compact = false }: { records: FocusRecord[]; onSelect: (id: string) => void; selectedOperationIds: string[]; selectedId: string | null; compact?: boolean }) {
return (
<div className={compact ? "mt-3 space-y-2" : "space-y-3"}>
{!compact ? (
@@ -1126,7 +1420,7 @@ function AgendaBoard({ records, onSelect, selectedId, compact = false }: { recor
<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-2 flex flex-wrap gap-2">
<RecordSignals record={record} />
<RecordSignals record={record} selected={selectedOperationIds.includes(record.id)} />
</div>
</div>
<div className="text-right text-xs text-muted">
@@ -1140,7 +1434,7 @@ function AgendaBoard({ records, onSelect, selectedId, compact = false }: { recor
);
}
function SelectedDayPanel({ cell, onSelect, selectedId }: { cell: HeatmapCell; onSelect: (id: string) => void; selectedId: string | null }) {
function SelectedDayPanel({ cell, onSelect, selectedOperationIds, selectedId }: { cell: HeatmapCell; onSelect: (id: string) => void; selectedOperationIds: string[]; 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">
@@ -1157,7 +1451,7 @@ function SelectedDayPanel({ cell, onSelect, selectedId }: { cell: HeatmapCell; o
<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-2 flex flex-wrap gap-2">
<RecordSignals record={task} />
<RecordSignals record={task} selected={selectedOperationIds.includes(task.id)} />
</div>
</button>
))}