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

@@ -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>
))}