planning payload
This commit is contained in:
@@ -1,6 +1,13 @@
|
||||
import type { DemandPlanningRollupDto, GanttTaskDto, PlanningExceptionDto, PlanningTimelineDto } from "@mrp/shared";
|
||||
import type {
|
||||
DemandPlanningRollupDto,
|
||||
GanttTaskDto,
|
||||
PlanningExceptionDto,
|
||||
PlanningStationLoadDto,
|
||||
PlanningTaskActionDto,
|
||||
PlanningTimelineDto,
|
||||
} from "@mrp/shared";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
|
||||
import { useAuth } from "../../auth/AuthProvider";
|
||||
import { ApiError, api } from "../../lib/api";
|
||||
@@ -16,6 +23,23 @@ type FocusRecord = {
|
||||
progress: number;
|
||||
detailHref: string | null;
|
||||
parentId: string | null;
|
||||
projectId: string | null;
|
||||
workOrderId: string | null;
|
||||
salesOrderId: string | null;
|
||||
itemId: string | null;
|
||||
itemSku: string | null;
|
||||
stationId: string | null;
|
||||
stationCode: string | null;
|
||||
readinessState: string;
|
||||
readinessScore: number;
|
||||
shortageItemCount: number;
|
||||
totalShortageQuantity: number;
|
||||
openSupplyQuantity: number;
|
||||
releaseReady: boolean;
|
||||
overdue: boolean;
|
||||
blockedReason: string | null;
|
||||
utilizationPercent: number | null;
|
||||
actions: PlanningTaskActionDto[];
|
||||
};
|
||||
|
||||
type HeatmapCell = {
|
||||
@@ -26,6 +50,9 @@ type HeatmapCell = {
|
||||
tasks: FocusRecord[];
|
||||
};
|
||||
|
||||
type WorkbenchGroup = "projects" | "stations" | "exceptions";
|
||||
type WorkbenchFilter = "all" | "release-ready" | "blocked" | "shortage" | "overdue";
|
||||
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
function formatDate(value: string | null, options?: Intl.DateTimeFormatOptions) {
|
||||
@@ -91,15 +118,50 @@ function buildFocusRecords(tasks: GanttTaskDto[]) {
|
||||
progress: task.progress,
|
||||
detailHref: task.detailHref ?? null,
|
||||
parentId: task.parentId ?? null,
|
||||
projectId: task.projectId ?? null,
|
||||
workOrderId: task.workOrderId ?? null,
|
||||
salesOrderId: task.salesOrderId ?? null,
|
||||
itemId: task.itemId ?? null,
|
||||
itemSku: task.itemSku ?? null,
|
||||
stationId: task.stationId ?? null,
|
||||
stationCode: task.stationCode ?? null,
|
||||
readinessState: task.readinessState ?? "READY",
|
||||
readinessScore: task.readinessScore ?? 0,
|
||||
shortageItemCount: task.shortageItemCount ?? 0,
|
||||
totalShortageQuantity: task.totalShortageQuantity ?? 0,
|
||||
openSupplyQuantity: task.openSupplyQuantity ?? 0,
|
||||
releaseReady: task.releaseReady ?? false,
|
||||
overdue: task.overdue ?? false,
|
||||
blockedReason: task.blockedReason ?? null,
|
||||
utilizationPercent: task.utilizationPercent ?? null,
|
||||
actions: task.actions ?? [],
|
||||
}));
|
||||
}
|
||||
|
||||
function matchesWorkbenchFilter(record: FocusRecord, filter: WorkbenchFilter) {
|
||||
switch (filter) {
|
||||
case "release-ready":
|
||||
return record.releaseReady;
|
||||
case "blocked":
|
||||
return record.readinessState === "BLOCKED" || record.readinessState === "PENDING_SUPPLY";
|
||||
case "shortage":
|
||||
return record.totalShortageQuantity > 0 || record.readinessState === "SHORTAGE";
|
||||
case "overdue":
|
||||
return record.overdue;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export function WorkbenchPage() {
|
||||
const navigate = useNavigate();
|
||||
const { token } = useAuth();
|
||||
const [timeline, setTimeline] = useState<PlanningTimelineDto | null>(null);
|
||||
const [planningRollup, setPlanningRollup] = useState<DemandPlanningRollupDto | null>(null);
|
||||
const [status, setStatus] = useState("Loading live planning timeline...");
|
||||
const [workbenchMode, setWorkbenchMode] = useState<WorkbenchMode>("overview");
|
||||
const [workbenchGroup, setWorkbenchGroup] = useState<WorkbenchGroup>("projects");
|
||||
const [workbenchFilter, setWorkbenchFilter] = useState<WorkbenchFilter>("all");
|
||||
const [selectedFocusId, setSelectedFocusId] = useState<string | null>(null);
|
||||
const [selectedHeatmapDate, setSelectedHeatmapDate] = useState<string | null>(null);
|
||||
|
||||
@@ -123,9 +185,11 @@ export function WorkbenchPage() {
|
||||
const tasks = timeline?.tasks ?? [];
|
||||
const summary = timeline?.summary;
|
||||
const exceptions = timeline?.exceptions ?? [];
|
||||
const stationLoads = timeline?.stationLoads ?? [];
|
||||
const focusRecords = useMemo(() => buildFocusRecords(tasks), [tasks]);
|
||||
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 : focusRecords[0] ?? null;
|
||||
const selectedFocus = selectedFocusId ? focusById.get(selectedFocusId) ?? null : filteredFocusRecords[0] ?? focusRecords[0] ?? null;
|
||||
|
||||
const heatmap = useMemo(() => {
|
||||
const start = summary ? startOfDay(new Date(summary.horizonStart)) : startOfDay(new Date());
|
||||
@@ -135,7 +199,7 @@ export function WorkbenchPage() {
|
||||
cells.set(dateKey(nextDate), { dateKey: dateKey(nextDate), count: 0, lateCount: 0, blockedCount: 0, tasks: [] });
|
||||
}
|
||||
|
||||
for (const record of focusRecords) {
|
||||
for (const record of filteredFocusRecords) {
|
||||
if (record.kind === "PROJECT") {
|
||||
continue;
|
||||
}
|
||||
@@ -159,15 +223,16 @@ export function WorkbenchPage() {
|
||||
}
|
||||
|
||||
return [...cells.values()];
|
||||
}, [focusRecords, summary]);
|
||||
}, [filteredFocusRecords, summary]);
|
||||
|
||||
const selectedHeatmapCell = selectedHeatmapDate ? heatmap.find((cell) => cell.dateKey === selectedHeatmapDate) ?? null : null;
|
||||
const agendaItems = useMemo(
|
||||
() => [...focusRecords]
|
||||
.filter((record) => record.kind !== "OPERATION")
|
||||
.filter((record) => matchesWorkbenchFilter(record, workbenchFilter))
|
||||
.sort((left, right) => new Date(left.end).getTime() - new Date(right.end).getTime())
|
||||
.slice(0, 18),
|
||||
[focusRecords]
|
||||
[focusRecords, workbenchFilter]
|
||||
);
|
||||
|
||||
const modeOptions: Array<{ value: WorkbenchMode; label: string; detail: string }> = [
|
||||
@@ -175,6 +240,34 @@ export function WorkbenchPage() {
|
||||
{ value: "heatmap", label: "Heatmap", detail: "Load by day" },
|
||||
{ value: "agenda", label: "Agenda", detail: "Upcoming due flow" },
|
||||
];
|
||||
const groupOptions: Array<{ value: WorkbenchGroup; label: string }> = [
|
||||
{ value: "projects", label: "Projects" },
|
||||
{ value: "stations", label: "Stations" },
|
||||
{ value: "exceptions", label: "Exceptions" },
|
||||
];
|
||||
const filterOptions: Array<{ value: WorkbenchFilter; label: string }> = [
|
||||
{ value: "all", label: "All" },
|
||||
{ value: "release-ready", label: "Release Ready" },
|
||||
{ value: "blocked", label: "Blocked" },
|
||||
{ value: "shortage", label: "Shortage" },
|
||||
{ value: "overdue", label: "Overdue" },
|
||||
];
|
||||
|
||||
async function handleTaskAction(action: PlanningTaskActionDto) {
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
if (action.kind === "RELEASE_WORK_ORDER" && action.workOrderId) {
|
||||
await api.updateWorkOrderStatus(token, action.workOrderId, "RELEASED");
|
||||
const refreshed = await api.getPlanningTimeline(token);
|
||||
setTimeline(refreshed);
|
||||
setStatus("Workbench refreshed after release.");
|
||||
return;
|
||||
}
|
||||
if (action.href) {
|
||||
navigate(action.href);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="space-y-4">
|
||||
@@ -198,15 +291,32 @@ export function WorkbenchPage() {
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
{groupOptions.map((option) => (
|
||||
<button key={option.value} type="button" onClick={() => setWorkbenchGroup(option.value)} className={`rounded-2xl border px-3 py-2 text-sm font-semibold ${workbenchGroup === option.value ? "border-brand bg-brand/10 text-text" : "border-line/70 bg-page/60 text-muted"}`}>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{filterOptions.map((option) => (
|
||||
<button key={option.value} type="button" onClick={() => setWorkbenchFilter(option.value)} className={`rounded-2xl border px-3 py-2 text-sm font-semibold ${workbenchFilter === option.value ? "border-brand bg-brand/10 text-text" : "border-line/70 bg-page/60 text-muted"}`}>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section className="grid gap-3 xl:grid-cols-8">
|
||||
<section className="grid gap-3 xl:grid-cols-10">
|
||||
<MetricCard label="Active Projects" value={summary?.activeProjects ?? 0} />
|
||||
<MetricCard label="At Risk" value={summary?.atRiskProjects ?? 0} />
|
||||
<MetricCard label="Overdue Projects" value={summary?.overdueProjects ?? 0} />
|
||||
<MetricCard label="Active Work" value={summary?.activeWorkOrders ?? 0} />
|
||||
<MetricCard label="Overdue Work" value={summary?.overdueWorkOrders ?? 0} />
|
||||
<MetricCard label="Unscheduled" value={summary?.unscheduledWorkOrders ?? 0} />
|
||||
<MetricCard label="Release Ready" value={summary?.releaseReadyWorkOrders ?? 0} />
|
||||
<MetricCard label="Blocked Work" value={summary?.blockedWorkOrders ?? 0} />
|
||||
<MetricCard label="Stations" value={`${summary?.stationCount ?? 0} / ${summary?.overloadedStations ?? 0} hot`} />
|
||||
<MetricCard label="Shortage Items" value={planningRollup?.summary.uncoveredItemCount ?? 0} />
|
||||
<MetricCard label="Build / Buy" value={planningRollup ? `${planningRollup.summary.totalBuildQuantity} / ${planningRollup.summary.totalPurchaseQuantity}` : "0 / 0"} />
|
||||
</section>
|
||||
@@ -229,9 +339,9 @@ export function WorkbenchPage() {
|
||||
<button key={exception.id} type="button" onClick={() => setSelectedFocusId(exception.id.startsWith("project-") ? exception.id : exception.id.replace("work-order-unscheduled-", "work-order-"))} className="block w-full rounded-[18px] border border-line/70 bg-page/60 p-3 text-left transition hover:bg-page/80">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<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-2 text-xs text-muted">{exception.ownerLabel ?? "No owner context"}</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-2 text-xs text-muted">{exception.ownerLabel ?? "No owner context"}</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>
|
||||
</div>
|
||||
@@ -246,6 +356,7 @@ export function WorkbenchPage() {
|
||||
<div className="mt-4 space-y-2 rounded-[18px] border border-line/70 bg-page/60 p-3 text-sm">
|
||||
<div className="flex items-center justify-between gap-3"><span className="text-muted">Uncovered quantity</span><span className="font-semibold text-text">{planningRollup?.summary.totalUncoveredQuantity ?? 0}</span></div>
|
||||
<div className="flex items-center justify-between gap-3"><span className="text-muted">Projects with linked demand</span><span className="font-semibold text-text">{planningRollup?.summary.projectCount ?? 0}</span></div>
|
||||
<div className="flex items-center justify-between gap-3"><span className="text-muted">Overloaded stations</span><span className="font-semibold text-text">{summary?.overloadedStations ?? 0}</span></div>
|
||||
</div>
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
<Link to="/projects" className="rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">Open projects</Link>
|
||||
@@ -256,7 +367,7 @@ export function WorkbenchPage() {
|
||||
</aside>
|
||||
|
||||
<div className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel">
|
||||
{workbenchMode === "overview" ? <OverviewBoard focusRecords={focusRecords} onSelect={setSelectedFocusId} /> : null}
|
||||
{workbenchMode === "overview" ? <OverviewBoard focusRecords={filteredFocusRecords} stationLoads={stationLoads} groupMode={workbenchGroup} onSelect={setSelectedFocusId} /> : null}
|
||||
{workbenchMode === "heatmap" ? <HeatmapBoard heatmap={heatmap} selectedDate={selectedHeatmapDate} onSelectDate={setSelectedHeatmapDate} /> : null}
|
||||
{workbenchMode === "agenda" ? <AgendaBoard records={agendaItems} onSelect={setSelectedFocusId} /> : null}
|
||||
</div>
|
||||
@@ -273,11 +384,24 @@ export function WorkbenchPage() {
|
||||
</div>
|
||||
<div className="rounded-[18px] border border-line/70 bg-page/60 p-3 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="mt-2 flex items-center justify-between gap-3"><span className="text-muted">Readiness</span><span className="font-semibold text-text">{selectedFocus.readinessState.replaceAll("_", " ")} ({selectedFocus.readinessScore}%)</span></div>
|
||||
<div className="mt-2 flex items-center justify-between gap-3"><span className="text-muted">Window</span><span className="font-semibold text-text">{formatDate(selectedFocus.start)} - {formatDate(selectedFocus.end)}</span></div>
|
||||
<div className="mt-2 flex items-center justify-between gap-3"><span className="text-muted">Progress</span><span className="font-semibold text-text">{selectedFocus.progress}%</span></div>
|
||||
<div className="mt-2 flex items-center justify-between gap-3"><span className="text-muted">Shortage</span><span className="font-semibold text-text">{selectedFocus.shortageItemCount} / {selectedFocus.totalShortageQuantity}</span></div>
|
||||
<div className="mt-2 flex items-center justify-between gap-3"><span className="text-muted">Open supply</span><span className="font-semibold text-text">{selectedFocus.openSupplyQuantity}</span></div>
|
||||
{selectedFocus.blockedReason ? <div className="mt-3 text-xs text-muted">{selectedFocus.blockedReason}</div> : null}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedFocus.detailHref ? <Link to={selectedFocus.detailHref} className="rounded-2xl bg-brand px-2 py-2 text-sm font-semibold text-white">Open record</Link> : null}
|
||||
{selectedFocus.actions.map((action, index) => (
|
||||
<button
|
||||
key={`${action.kind}-${index}`}
|
||||
type="button"
|
||||
onClick={() => void handleTaskAction(action)}
|
||||
className={`${index === 0 ? "bg-brand text-white" : "border border-line/70 text-text"} rounded-2xl px-2 py-2 text-sm font-semibold`}
|
||||
>
|
||||
{action.label}
|
||||
</button>
|
||||
))}
|
||||
<button type="button" onClick={() => setWorkbenchMode("heatmap")} className="rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">View load</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -306,54 +430,117 @@ function MetricCard({ label, value }: { label: string; value: string | number })
|
||||
);
|
||||
}
|
||||
|
||||
function OverviewBoard({ focusRecords, onSelect }: { focusRecords: FocusRecord[]; onSelect: (id: string) => void }) {
|
||||
function OverviewBoard({
|
||||
focusRecords,
|
||||
stationLoads,
|
||||
groupMode,
|
||||
onSelect,
|
||||
}: {
|
||||
focusRecords: FocusRecord[];
|
||||
stationLoads: PlanningStationLoadDto[];
|
||||
groupMode: WorkbenchGroup;
|
||||
onSelect: (id: string) => void;
|
||||
}) {
|
||||
const projects = focusRecords.filter((record) => record.kind === "PROJECT").slice(0, 6);
|
||||
const operations = focusRecords.filter((record) => record.kind === "OPERATION").slice(0, 10);
|
||||
const workOrders = focusRecords.filter((record) => record.kind === "WORK_ORDER").slice(0, 10);
|
||||
const exceptionRows = focusRecords
|
||||
.filter((record) => record.kind !== "PROJECT" && (record.overdue || record.totalShortageQuantity > 0 || record.readinessState === "BLOCKED" || record.readinessState === "PENDING_SUPPLY"))
|
||||
.slice(0, 10);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Overview</p>
|
||||
<p className="mt-2 text-sm text-muted">Scan project rollups, active work, and operation load without leaving the planner.</p>
|
||||
<p className="mt-2 text-sm text-muted">Scan project rollups, active work, station load, and release blockers without leaving the planner.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-3 xl:grid-cols-[minmax(0,1.15fr)_minmax(0,0.85fr)]">
|
||||
<section className="rounded-[18px] border border-line/70 bg-page/60 p-3">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Program Queue</p>
|
||||
<div className="mt-3 space-y-3">
|
||||
{projects.map((record) => (
|
||||
<button key={record.id} type="button" onClick={() => onSelect(record.id)} className="block w-full rounded-[16px] border border-line/70 bg-surface/80 p-3 text-left transition hover:bg-surface">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
{groupMode === "projects" ? (
|
||||
<div className="grid gap-3 xl:grid-cols-[minmax(0,1.15fr)_minmax(0,0.85fr)]">
|
||||
<section className="rounded-[18px] border border-line/70 bg-page/60 p-3">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Program Queue</p>
|
||||
<div className="mt-3 space-y-3">
|
||||
{projects.map((record) => (
|
||||
<button key={record.id} type="button" onClick={() => onSelect(record.id)} className="block w-full rounded-[16px] border border-line/70 bg-surface/80 p-3 text-left transition hover:bg-surface">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<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>
|
||||
<div className="text-right text-xs text-muted">
|
||||
<div>{record.readinessState}</div>
|
||||
<div>{record.progress}% progress</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
<section className="rounded-[18px] border border-line/70 bg-page/60 p-3">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Operation Load</p>
|
||||
<div className="mt-3 space-y-2">
|
||||
{operations.map((record) => (
|
||||
<button key={record.id} type="button" onClick={() => onSelect(record.id)} className="flex w-full items-center justify-between gap-3 rounded-[16px] border border-line/70 bg-surface/80 px-3 py-2 text-left transition hover:bg-surface">
|
||||
<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 parent work order"}</div>
|
||||
</div>
|
||||
<div className="text-right text-xs text-muted">
|
||||
<div>{record.status.replaceAll("_", " ")}</div>
|
||||
<div>{record.progress}% progress</div>
|
||||
<div>{record.stationCode ?? "No station"}</div>
|
||||
<div>{record.utilizationPercent ?? 0}% util</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
) : null}
|
||||
{groupMode === "stations" ? (
|
||||
<section className="rounded-[18px] border border-line/70 bg-page/60 p-3">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Work Center Load</p>
|
||||
<div className="mt-3 grid gap-3 xl:grid-cols-2">
|
||||
{stationLoads.slice(0, 10).map((station) => (
|
||||
<div key={station.stationId} className="rounded-[16px] border border-line/70 bg-surface/80 p-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<div className="font-semibold text-text">{station.stationCode} - {station.stationName}</div>
|
||||
<div className="mt-1 text-xs text-muted">{station.operationCount} ops across {station.workOrderCount} work orders</div>
|
||||
</div>
|
||||
<div className="text-right text-xs text-muted">
|
||||
<div>{station.utilizationPercent}% util</div>
|
||||
<div>{station.overloaded ? "Overloaded" : "Within load"}</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<div className="mt-3 grid grid-cols-3 gap-2 text-xs text-muted">
|
||||
<div>Ready {station.readyCount}</div>
|
||||
<div>Blocked {station.blockedCount}</div>
|
||||
<div>Late {station.lateCount}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
{groupMode === "exceptions" ? (
|
||||
<section className="rounded-[18px] border border-line/70 bg-page/60 p-3">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Operation Load</p>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Dispatch Exceptions</p>
|
||||
<div className="mt-3 space-y-2">
|
||||
{operations.map((record) => (
|
||||
{exceptionRows.map((record) => (
|
||||
<button key={record.id} type="button" onClick={() => onSelect(record.id)} className="flex w-full items-center justify-between gap-3 rounded-[16px] border border-line/70 bg-surface/80 px-3 py-2 text-left transition hover:bg-surface">
|
||||
<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.readinessState} - shortage {record.totalShortageQuantity}</div>
|
||||
</div>
|
||||
<div className="text-right text-xs text-muted">
|
||||
<div>{record.ownerLabel ?? "No context"}</div>
|
||||
<div>{record.overdue ? "Overdue" : "Open"}</div>
|
||||
</div>
|
||||
<div className="text-xs text-muted">{formatDate(record.start)} - {formatDate(record.end)}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
) : null}
|
||||
<section className="rounded-[18px] border border-line/70 bg-page/60 p-3">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Active Work Orders</p>
|
||||
<div className="mt-3 grid gap-3 xl:grid-cols-2">
|
||||
@@ -361,7 +548,7 @@ function OverviewBoard({ focusRecords, onSelect }: { focusRecords: FocusRecord[]
|
||||
<button key={record.id} type="button" onClick={() => onSelect(record.id)} className="rounded-[16px] border border-line/70 bg-surface/80 p-3 text-left transition hover:bg-surface">
|
||||
<div className="font-semibold text-text">{record.title}</div>
|
||||
<div className="mt-2 flex items-center justify-between gap-3 text-xs text-muted">
|
||||
<span>{record.status.replaceAll("_", " ")}</span>
|
||||
<span>{record.readinessState}</span>
|
||||
<span>{record.progress}%</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user