changes:
This commit is contained in:
@@ -122,7 +122,7 @@ const TOL = 0.10;
|
||||
const DEFAULT_MICRO_MULT = 1.5;
|
||||
const DEFAULT_MACRO_MULT = 5;
|
||||
const NORMAL_TOL_SEC = 0.1;
|
||||
const LIVE_REFRESH_MS = 5000;
|
||||
const LIVE_REFRESH_MS = 15000;
|
||||
|
||||
const BUCKET = {
|
||||
normal: {
|
||||
@@ -686,17 +686,34 @@ export default function MachineDetailClient() {
|
||||
function MachineActivityTimeline({ machineId }: { machineId: string | undefined }) {
|
||||
const [timeline, setTimeline] = useState<RecapTimelineResponse | null>(null);
|
||||
const [timelineLoading, setTimelineLoading] = useState(true);
|
||||
const timelineHashRef = useRef("");
|
||||
|
||||
useEffect(() => {
|
||||
if (!machineId) return;
|
||||
let alive = true;
|
||||
timelineHashRef.current = "";
|
||||
setTimeline(null);
|
||||
setTimelineLoading(true);
|
||||
|
||||
async function loadTimeline() {
|
||||
try {
|
||||
const res = await fetch(`/api/recap/${machineId}/timeline?range=1h`, { cache: "no-store" });
|
||||
const json = await res.json().catch(() => null);
|
||||
if (!alive || !res.ok || !json) return;
|
||||
setTimeline(json as RecapTimelineResponse);
|
||||
const nextTimeline = json as RecapTimelineResponse;
|
||||
const nextHash = JSON.stringify({
|
||||
start: nextTimeline.range?.start ?? "",
|
||||
end: nextTimeline.range?.end ?? "",
|
||||
hasData: nextTimeline.hasData,
|
||||
segments: nextTimeline.segments.map((segment) => ({
|
||||
type: segment.type,
|
||||
startMs: segment.startMs,
|
||||
endMs: segment.endMs,
|
||||
})),
|
||||
});
|
||||
if (timelineHashRef.current === nextHash) return;
|
||||
timelineHashRef.current = nextHash;
|
||||
setTimeline(nextTimeline);
|
||||
} finally {
|
||||
if (alive) setTimelineLoading(false);
|
||||
}
|
||||
@@ -1065,7 +1082,7 @@ export default function MachineDetailClient() {
|
||||
<>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<div className="rounded-2xl border border-white/10 bg-white/5 p-5">
|
||||
<div className="text-xs text-zinc-400">OEE</div>
|
||||
<div className="text-xs text-zinc-400">{t("machine.detail.kpi.oeeCurrent")}</div>
|
||||
{kpi?.oee == null || Number.isNaN(kpi.oee) ? (
|
||||
<div className="mt-2 text-3xl font-bold text-zinc-400">—</div>
|
||||
) : (
|
||||
|
||||
@@ -27,6 +27,29 @@ function asRecord(value: unknown): Record<string, unknown> | null {
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function toFiniteNumber(value: unknown): number | null {
|
||||
if (typeof value === "number" && Number.isFinite(value)) return value;
|
||||
if (typeof value === "string") {
|
||||
const parsed = Number(value);
|
||||
if (Number.isFinite(parsed)) return parsed;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function toFiniteInt(value: unknown): number | null {
|
||||
const parsed = toFiniteNumber(value);
|
||||
if (parsed == null) return null;
|
||||
return Math.trunc(parsed);
|
||||
}
|
||||
|
||||
function pickFirstNumber(...values: unknown[]) {
|
||||
for (const value of values) {
|
||||
const parsed = toFiniteNumber(value);
|
||||
if (parsed != null) return parsed;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function readPath(root: unknown, path: string[]): unknown {
|
||||
let current = root;
|
||||
for (const key of path) {
|
||||
@@ -160,28 +183,37 @@ export async function POST(req: Request) {
|
||||
orgId = machine.orgId;
|
||||
|
||||
const woRecord = (body.activeWorkOrder ?? {}) as Record<string, unknown>;
|
||||
const good =
|
||||
typeof woRecord.good === "number"
|
||||
? woRecord.good
|
||||
: typeof woRecord.goodParts === "number"
|
||||
? woRecord.goodParts
|
||||
: typeof woRecord.good_parts === "number"
|
||||
? woRecord.good_parts
|
||||
: null;
|
||||
const scrap =
|
||||
typeof woRecord.scrap === "number"
|
||||
? woRecord.scrap
|
||||
: typeof woRecord.scrapParts === "number"
|
||||
? woRecord.scrapParts
|
||||
: typeof woRecord.scrap_parts === "number"
|
||||
? woRecord.scrap_parts
|
||||
: null;
|
||||
const activeWorkOrderId = woRecord.id != null ? String(woRecord.id).trim() : "";
|
||||
const activeSku = woRecord.sku != null ? String(woRecord.sku).trim() : "";
|
||||
const activeStatus = woRecord.status != null ? String(woRecord.status).trim() : "";
|
||||
const activeTargetQty = toFiniteInt(woRecord.target);
|
||||
const activeCycleTime = toFiniteNumber(woRecord.cycleTime);
|
||||
const good = pickFirstNumber(woRecord.good, woRecord.goodParts, woRecord.good_parts);
|
||||
const scrap = pickFirstNumber(woRecord.scrap, woRecord.scrapParts, woRecord.scrap_parts);
|
||||
const activeGoodParts = Math.max(0, Math.trunc(good ?? 0));
|
||||
const activeScrapParts = Math.max(0, Math.trunc(scrap ?? 0));
|
||||
const activeCycleCount = Math.max(
|
||||
0,
|
||||
toFiniteInt(woRecord.cycleCount ?? woRecord.cycle_count ?? body.cycle_count) ?? 0
|
||||
);
|
||||
const snapshotCycleCount =
|
||||
toFiniteInt(body.cycle_count) ??
|
||||
toFiniteInt(woRecord.cycle_count) ??
|
||||
toFiniteInt(woRecord.cycleCount);
|
||||
const snapshotGoodParts =
|
||||
toFiniteInt(body.good_parts) ??
|
||||
toFiniteInt(woRecord.good_parts) ??
|
||||
toFiniteInt(woRecord.goodParts);
|
||||
const snapshotScrapParts =
|
||||
toFiniteInt(body.scrap_parts) ??
|
||||
toFiniteInt(woRecord.scrap_parts) ??
|
||||
toFiniteInt(woRecord.scrapParts);
|
||||
const k = body.kpis ?? {};
|
||||
const safeCycleTime =
|
||||
typeof body.cycleTime === "number" && body.cycleTime > 0
|
||||
? body.cycleTime
|
||||
: typeof woRecord.cycleTime === "number" && woRecord.cycleTime > 0
|
||||
? woRecord.cycleTime
|
||||
: activeCycleTime != null && activeCycleTime > 0
|
||||
? activeCycleTime
|
||||
: null;
|
||||
|
||||
const safeCavities =
|
||||
@@ -202,16 +234,16 @@ export async function POST(req: Request) {
|
||||
ts: tsDeviceDate, // store device-time in ts; server-time goes to ts_server
|
||||
|
||||
// Work order fields
|
||||
workOrderId: woRecord.id != null ? String(woRecord.id) : null,
|
||||
sku: woRecord.sku != null ? String(woRecord.sku) : null,
|
||||
target: typeof woRecord.target === "number" ? Math.trunc(woRecord.target) : null,
|
||||
workOrderId: activeWorkOrderId || null,
|
||||
sku: activeSku || null,
|
||||
target: activeTargetQty,
|
||||
good: good != null ? Math.trunc(good) : null,
|
||||
scrap: scrap != null ? Math.trunc(scrap) : null,
|
||||
|
||||
// Counters
|
||||
cycleCount: typeof body.cycle_count === "number" ? body.cycle_count : null,
|
||||
goodParts: typeof body.good_parts === "number" ? body.good_parts : null,
|
||||
scrapParts: typeof body.scrap_parts === "number" ? body.scrap_parts : null,
|
||||
cycleCount: snapshotCycleCount,
|
||||
goodParts: snapshotGoodParts,
|
||||
scrapParts: snapshotScrapParts,
|
||||
cavities: safeCavities,
|
||||
|
||||
// Cycle times
|
||||
@@ -229,6 +261,38 @@ export async function POST(req: Request) {
|
||||
},
|
||||
});
|
||||
|
||||
if (activeWorkOrderId) {
|
||||
await prisma.machineWorkOrder.upsert({
|
||||
where: {
|
||||
machineId_workOrderId: {
|
||||
machineId: machine.id,
|
||||
workOrderId: activeWorkOrderId,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
orgId: machine.orgId,
|
||||
machineId: machine.id,
|
||||
workOrderId: activeWorkOrderId,
|
||||
sku: activeSku || null,
|
||||
targetQty: activeTargetQty,
|
||||
cycleTime: activeCycleTime,
|
||||
status: activeStatus || "RUNNING",
|
||||
goodParts: activeGoodParts,
|
||||
scrapParts: activeScrapParts,
|
||||
cycleCount: activeCycleCount,
|
||||
},
|
||||
update: {
|
||||
sku: activeSku || undefined,
|
||||
targetQty: activeTargetQty ?? undefined,
|
||||
cycleTime: activeCycleTime ?? undefined,
|
||||
status: activeStatus || undefined,
|
||||
goodParts: activeGoodParts,
|
||||
scrapParts: activeScrapParts,
|
||||
cycleCount: activeCycleCount,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Optional but useful: update machine "last seen" meta fields
|
||||
await prisma.machine.update({
|
||||
where: { id: machine.id },
|
||||
|
||||
Reference in New Issue
Block a user