more
This commit is contained in:
@@ -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>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user