more workbench
This commit is contained in:
@@ -37,6 +37,7 @@ This file is the running release and change log for CODEXIUM. Keep it updated wh
|
|||||||
- 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 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
|
- 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 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
|
||||||
- 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
|
||||||
|
|||||||
@@ -116,6 +116,38 @@ function densityTone(cell: HeatmapCell) {
|
|||||||
return "border-line/60 bg-surface/70";
|
return "border-line/60 bg-surface/70";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readinessTone(record: FocusRecord) {
|
||||||
|
if (record.overdue) {
|
||||||
|
return "border-rose-300/60 bg-rose-500/10 text-rose-200 dark:text-rose-200";
|
||||||
|
}
|
||||||
|
if (record.readinessState === "BLOCKED" || record.blockedReason) {
|
||||||
|
return "border-amber-300/60 bg-amber-400/10 text-amber-300";
|
||||||
|
}
|
||||||
|
if (record.totalShortageQuantity > 0 || record.readinessState === "SHORTAGE" || record.readinessState === "PENDING_SUPPLY") {
|
||||||
|
return "border-orange-300/60 bg-orange-400/10 text-orange-300";
|
||||||
|
}
|
||||||
|
if (record.releaseReady || record.readinessState === "READY") {
|
||||||
|
return "border-emerald-300/60 bg-emerald-500/10 text-emerald-300";
|
||||||
|
}
|
||||||
|
return "border-line/70 bg-page/60 text-muted";
|
||||||
|
}
|
||||||
|
|
||||||
|
function readinessLabel(record: FocusRecord) {
|
||||||
|
if (record.overdue) {
|
||||||
|
return "OVERDUE";
|
||||||
|
}
|
||||||
|
if (record.readinessState === "BLOCKED" || record.blockedReason) {
|
||||||
|
return "BLOCKED";
|
||||||
|
}
|
||||||
|
if (record.totalShortageQuantity > 0 || record.readinessState === "SHORTAGE" || record.readinessState === "PENDING_SUPPLY") {
|
||||||
|
return "SHORTAGE";
|
||||||
|
}
|
||||||
|
if (record.releaseReady || record.readinessState === "READY") {
|
||||||
|
return "READY";
|
||||||
|
}
|
||||||
|
return record.readinessState.replaceAll("_", " ");
|
||||||
|
}
|
||||||
|
|
||||||
function buildFocusRecords(tasks: GanttTaskDto[]) {
|
function buildFocusRecords(tasks: GanttTaskDto[]) {
|
||||||
return tasks.map((task) => ({
|
return tasks.map((task) => ({
|
||||||
id: task.id,
|
id: task.id,
|
||||||
@@ -270,6 +302,90 @@ export function WorkbenchPage() {
|
|||||||
.slice(0, 18),
|
.slice(0, 18),
|
||||||
[focusRecords, workbenchFilter]
|
[focusRecords, workbenchFilter]
|
||||||
);
|
);
|
||||||
|
const keyboardRecords = useMemo(() => {
|
||||||
|
if (workbenchMode === "agenda") {
|
||||||
|
return agendaItems;
|
||||||
|
}
|
||||||
|
if (workbenchMode === "heatmap") {
|
||||||
|
return (selectedHeatmapCell?.tasks ?? filteredFocusRecords.filter((record) => record.kind !== "PROJECT")).slice(0, 18);
|
||||||
|
}
|
||||||
|
|
||||||
|
const projects = filteredFocusRecords.filter((record) => record.kind === "PROJECT").slice(0, 6);
|
||||||
|
const operations = filteredFocusRecords.filter((record) => record.kind === "OPERATION");
|
||||||
|
const workOrders = filteredFocusRecords.filter((record) => record.kind === "WORK_ORDER").slice(0, 10);
|
||||||
|
const exceptionRows = filteredFocusRecords
|
||||||
|
.filter((record) => record.kind !== "PROJECT" && (record.overdue || record.totalShortageQuantity > 0 || record.readinessState === "BLOCKED" || record.readinessState === "PENDING_SUPPLY"))
|
||||||
|
.slice(0, 10);
|
||||||
|
|
||||||
|
if (workbenchGroup === "projects") {
|
||||||
|
return [...projects, ...operations.slice(0, 10), ...workOrders].filter((record, index, array) => array.findIndex((candidate) => candidate.id === record.id) === index);
|
||||||
|
}
|
||||||
|
if (workbenchGroup === "exceptions") {
|
||||||
|
return [...exceptionRows, ...workOrders].filter((record, index, array) => array.findIndex((candidate) => candidate.id === record.id) === index);
|
||||||
|
}
|
||||||
|
|
||||||
|
const stationBuckets = new Map<string, FocusRecord[]>();
|
||||||
|
for (const record of operations) {
|
||||||
|
if (!record.stationId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const bucket = stationBuckets.get(record.stationId) ?? [];
|
||||||
|
bucket.push(record);
|
||||||
|
stationBuckets.set(record.stationId, bucket);
|
||||||
|
}
|
||||||
|
for (const bucket of stationBuckets.values()) {
|
||||||
|
bucket.sort((left, right) => new Date(left.start).getTime() - new Date(right.start).getTime());
|
||||||
|
}
|
||||||
|
return stationLoads
|
||||||
|
.slice(0, 10)
|
||||||
|
.flatMap((station) => (stationBuckets.get(station.stationId) ?? []).slice(0, 5));
|
||||||
|
}, [agendaItems, filteredFocusRecords, selectedHeatmapCell?.dateKey, selectedHeatmapCell?.tasks, stationLoads, workbenchGroup, workbenchMode]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function handleKeydown(event: KeyboardEvent) {
|
||||||
|
const target = event.target;
|
||||||
|
if (target instanceof HTMLElement) {
|
||||||
|
const tagName = target.tagName;
|
||||||
|
if (target.isContentEditable || tagName === "INPUT" || tagName === "TEXTAREA" || tagName === "SELECT" || tagName === "BUTTON") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (event.altKey || event.ctrlKey || event.metaKey || keyboardRecords.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === "ArrowDown" || event.key === "j") {
|
||||||
|
event.preventDefault();
|
||||||
|
const currentIndex = selectedFocus ? keyboardRecords.findIndex((record) => record.id === selectedFocus.id) : -1;
|
||||||
|
const nextIndex = currentIndex >= 0 ? Math.min(currentIndex + 1, keyboardRecords.length - 1) : 0;
|
||||||
|
setSelectedFocusId(keyboardRecords[nextIndex]?.id ?? null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === "ArrowUp" || event.key === "k") {
|
||||||
|
event.preventDefault();
|
||||||
|
const currentIndex = selectedFocus ? keyboardRecords.findIndex((record) => record.id === selectedFocus.id) : -1;
|
||||||
|
const nextIndex = currentIndex >= 0 ? Math.max(currentIndex - 1, 0) : 0;
|
||||||
|
setSelectedFocusId(keyboardRecords[nextIndex]?.id ?? null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === "Enter" && selectedFocus?.detailHref) {
|
||||||
|
event.preventDefault();
|
||||||
|
navigate(selectedFocus.detailHref);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
event.preventDefault();
|
||||||
|
if (selectedHeatmapDate) {
|
||||||
|
setSelectedHeatmapDate(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSelectedFocusId(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("keydown", handleKeydown);
|
||||||
|
return () => window.removeEventListener("keydown", handleKeydown);
|
||||||
|
}, [keyboardRecords, navigate, selectedFocus, selectedHeatmapDate]);
|
||||||
|
|
||||||
const modeOptions: Array<{ value: WorkbenchMode; label: string; detail: string }> = [
|
const modeOptions: Array<{ value: WorkbenchMode; label: string; detail: string }> = [
|
||||||
{ value: "overview", label: "Overview", detail: "Dense planner board" },
|
{ value: "overview", label: "Overview", detail: "Dense planner board" },
|
||||||
@@ -425,6 +541,9 @@ export function WorkbenchPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="mt-3 flex flex-wrap items-center gap-2 text-xs text-muted">
|
<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>
|
<span className="rounded-full border border-line/70 bg-page/60 px-2 py-1">{filteredFocusRecords.length} visible rows</span>
|
||||||
|
<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>
|
||||||
{selectedFocus ? (
|
{selectedFocus ? (
|
||||||
<span className="rounded-full border border-brand/40 bg-brand/10 px-2 py-1 text-text">
|
<span className="rounded-full border border-brand/40 bg-brand/10 px-2 py-1 text-text">
|
||||||
Selected {selectedFocus.kind.toLowerCase()}: {selectedFocus.title}
|
Selected {selectedFocus.kind.toLowerCase()}: {selectedFocus.title}
|
||||||
@@ -469,9 +588,9 @@ export function WorkbenchPage() {
|
|||||||
<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"}`}>
|
<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 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>
|
||||||
<div className="mt-1 font-semibold text-text">{exception.title}</div>
|
<div className="mt-1 font-semibold text-text">{exception.title}</div>
|
||||||
<div className="mt-2 text-xs text-muted">{exception.ownerLabel ?? "No owner context"}</div>
|
<div className="mt-2 text-xs text-muted">{exception.ownerLabel ?? "No owner context"}</div>
|
||||||
</div>
|
</div>
|
||||||
<span className="rounded-full border border-line/70 px-2 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-muted">{exception.status.replaceAll("_", " ")}</span>
|
<span className="rounded-full border border-line/70 px-2 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-muted">{exception.status.replaceAll("_", " ")}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -533,6 +652,9 @@ export function WorkbenchPage() {
|
|||||||
<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 className="mt-3 flex flex-wrap gap-2">
|
||||||
|
<RecordSignals record={selectedFocus} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-[18px] border border-line/70 bg-page/60 px-2 py-2 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>
|
||||||
@@ -672,6 +794,19 @@ function MetricCard({ label, value }: { label: string; value: string | number })
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function RecordSignals({ record }: { record: FocusRecord }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span className={`rounded-full border px-2 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] ${readinessTone(record)}`}>
|
||||||
|
{readinessLabel(record)}
|
||||||
|
</span>
|
||||||
|
{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}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function OverviewBoard({
|
function OverviewBoard({
|
||||||
focusRecords,
|
focusRecords,
|
||||||
stationLoads,
|
stationLoads,
|
||||||
@@ -734,10 +869,13 @@ function OverviewBoard({
|
|||||||
<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 owner context"}</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} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right text-xs text-muted">
|
<div className="text-right text-xs text-muted">
|
||||||
<div>{record.readinessState}</div>
|
|
||||||
<div>{record.progress}% progress</div>
|
<div>{record.progress}% progress</div>
|
||||||
|
<div>{formatDate(record.end)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
@@ -752,6 +890,9 @@ function OverviewBoard({
|
|||||||
<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>
|
||||||
|
<div className="mt-2 flex flex-wrap gap-2">
|
||||||
|
<RecordSignals record={record} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right text-xs text-muted">
|
<div className="text-right text-xs text-muted">
|
||||||
<div>{record.stationCode ?? "No station"}</div>
|
<div>{record.stationCode ?? "No station"}</div>
|
||||||
@@ -865,9 +1006,11 @@ function OverviewBoard({
|
|||||||
<div>
|
<div>
|
||||||
<div className="font-semibold text-text">{record.ownerLabel ?? record.title}</div>
|
<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-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} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right text-xs text-muted">
|
<div className="text-right text-xs text-muted">
|
||||||
<div>{record.readinessState}</div>
|
|
||||||
<div>{record.utilizationPercent ?? station.utilizationPercent}% util</div>
|
<div>{record.utilizationPercent ?? station.utilizationPercent}% util</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -891,10 +1034,12 @@ function OverviewBoard({
|
|||||||
<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"}`}>
|
<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>
|
||||||
<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.ownerLabel ?? "No context"}</div>
|
||||||
|
<div className="mt-2 flex flex-wrap gap-2">
|
||||||
|
<RecordSignals record={record} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right text-xs text-muted">
|
<div className="text-right text-xs text-muted">
|
||||||
<div>{record.ownerLabel ?? "No context"}</div>
|
|
||||||
<div>{record.overdue ? "Overdue" : "Open"}</div>
|
<div>{record.overdue ? "Overdue" : "Open"}</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
@@ -908,8 +1053,11 @@ function OverviewBoard({
|
|||||||
{workOrders.map((record) => (
|
{workOrders.map((record) => (
|
||||||
<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"}`}>
|
<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="font-semibold text-text">{record.title}</div>
|
||||||
|
<div className="mt-2 flex flex-wrap gap-2">
|
||||||
|
<RecordSignals record={record} />
|
||||||
|
</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.ownerLabel ?? "No context"}</span>
|
||||||
<span>{record.progress}%</span>
|
<span>{record.progress}%</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
@@ -977,6 +1125,9 @@ function AgendaBoard({ records, onSelect, selectedId, compact = false }: { recor
|
|||||||
<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>
|
||||||
|
<div className="mt-2 flex flex-wrap gap-2">
|
||||||
|
<RecordSignals record={record} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right text-xs text-muted">
|
<div className="text-right text-xs text-muted">
|
||||||
<div>{formatDate(record.end)}</div>
|
<div>{formatDate(record.end)}</div>
|
||||||
@@ -1005,6 +1156,9 @@ function SelectedDayPanel({ cell, onSelect, selectedId }: { cell: HeatmapCell; o
|
|||||||
<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"}`}>
|
<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="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>
|
||||||
|
<div className="mt-2 flex flex-wrap gap-2">
|
||||||
|
<RecordSignals record={task} />
|
||||||
|
</div>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user