This commit is contained in:
mdares
2026-01-11 22:07:01 +00:00
parent d0ab254dd7
commit f231d87ae3
2 changed files with 78 additions and 18 deletions

View File

@@ -90,6 +90,13 @@ type TimelineSeg = {
state: TimelineState; state: TimelineState;
}; };
type ActiveStoppage = {
state: "microstop" | "macrostop";
startedAt: string;
durationSec: number;
theoreticalCycleTime: number;
};
type UploadState = { type UploadState = {
status: "idle" | "parsing" | "uploading" | "success" | "error"; status: "idle" | "parsing" | "uploading" | "success" | "error";
message?: string; message?: string;
@@ -246,9 +253,12 @@ export default function MachineDetailClient() {
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [cycles, setCycles] = useState<CycleRow[]>([]); const [cycles, setCycles] = useState<CycleRow[]>([]);
const [thresholds, setThresholds] = useState<Thresholds | null>(null); const [thresholds, setThresholds] = useState<Thresholds | null>(null);
const [activeStoppage, setActiveStoppage] = useState<ActiveStoppage | null>(null);
const [open, setOpen] = useState<null | "events" | "deviation" | "impact">(null); const [open, setOpen] = useState<null | "events" | "deviation" | "impact">(null);
const fileInputRef = useRef<HTMLInputElement | null>(null); const fileInputRef = useRef<HTMLInputElement | null>(null);
const [uploadState, setUploadState] = useState<UploadState>({ status: "idle" }); const [uploadState, setUploadState] = useState<UploadState>({ status: "idle" });
const [nowMs, setNowMs] = useState(() => Date.now());
const BUCKET = { const BUCKET = {
normal: { normal: {
@@ -308,6 +318,7 @@ export default function MachineDetailClient() {
setEvents(json.events ?? []); setEvents(json.events ?? []);
setCycles(json.cycles ?? []); setCycles(json.cycles ?? []);
setThresholds(json.thresholds ?? null); setThresholds(json.thresholds ?? null);
setActiveStoppage(json.activeStoppage ?? null);
setError(null); setError(null);
setLoading(false); setLoading(false);
} catch { } catch {
@@ -325,6 +336,11 @@ export default function MachineDetailClient() {
}; };
}, [machineId, t]); }, [machineId, t]);
useEffect(() => {
const timer = setInterval(() => setNowMs(Date.now()), 1000);
return () => clearInterval(timer);
}, []);
async function parseWorkOrdersFile(file: File) { async function parseWorkOrdersFile(file: File) {
const name = file.name.toLowerCase(); const name = file.name.toLowerCase();
if (name.endsWith(".csv")) { if (name.endsWith(".csv")) {
@@ -430,6 +446,20 @@ export default function MachineDetailClient() {
return `${v}`; return `${v}`;
} }
function formatDurationShort(totalSec?: number | null) {
if (totalSec === null || totalSec === undefined || Number.isNaN(totalSec)) {
return t("common.na");
}
const sec = Math.max(0, Math.floor(totalSec));
const h = Math.floor(sec / 3600);
const m = Math.floor((sec % 3600) / 60);
const s = sec % 60;
if (h > 0) return `${h}h ${m}m`;
if (m > 0) return `${m}m ${s}s`;
return `${s}s`;
}
function timeAgo(ts?: string) { function timeAgo(ts?: string) {
if (!ts) return t("common.never"); if (!ts) return t("common.never");
const diff = Math.floor((Date.now() - new Date(ts).getTime()) / 1000); const diff = Math.floor((Date.now() - new Date(ts).getTime()) / 1000);
@@ -815,6 +845,8 @@ export default function MachineDetailClient() {
}); });
} }
return { windowSec, segments: segs, start, end }; return { windowSec, segments: segs, start, end };
}, [cycles, cycleTarget, thresholds]); }, [cycles, cycleTarget, thresholds]);

View File

@@ -312,6 +312,34 @@ const rawCycles = await prisma.machineCycle.findMany({
sku: true, sku: true,
}, },
}); });
const latestCycle = rawCycles[0] ?? null;
let activeStoppage: {
state: "microstop" | "macrostop";
startedAt: string;
durationSec: number;
theoreticalCycleTime: number;
} | null = null;
if (latestCycle?.ts && effectiveCycleTime && effectiveCycleTime > 0) {
const elapsedSec = (Date.now() - latestCycle.ts.getTime()) / 1000;
const microThresholdSec = effectiveCycleTime * microMultiplier;
const macroThresholdSec = effectiveCycleTime * macroMultiplier;
if (elapsedSec >= microThresholdSec) {
const isMacro = elapsedSec >= macroThresholdSec;
const state = isMacro ? "macrostop" : "microstop";
const thresholdSec = isMacro ? macroThresholdSec : microThresholdSec;
const startedAtMs = latestCycle.ts.getTime() + thresholdSec * 1000;
activeStoppage = {
state,
startedAt: new Date(startedAtMs).toISOString(),
durationSec: Math.max(0, Math.floor(elapsedSec - thresholdSec)),
theoreticalCycleTime: effectiveCycleTime,
};
}
}
// chart-friendly: oldest -> newest + numeric timestamps // chart-friendly: oldest -> newest + numeric timestamps
const cycles = rawCycles const cycles = rawCycles
@@ -331,23 +359,23 @@ const cycles = rawCycles
return NextResponse.json({ return NextResponse.json({
ok: true, ok: true,
machine: { machine: {
id: machine.id, id: machine.id,
name: machine.name, name: machine.name,
code: machine.code, code: machine.code,
location: machine.location, location: machine.location,
latestHeartbeat: machine.heartbeats[0] ?? null, latestHeartbeat: machine.heartbeats[0] ?? null,
latestKpi: machine.kpiSnapshots[0] ?? null, latestKpi: machine.kpiSnapshots[0] ?? null,
effectiveCycleTime, effectiveCycleTime,
},
}, thresholds: {
thresholds: { stoppageMultiplier: microMultiplier,
stoppageMultiplier: microMultiplier, macroStoppageMultiplier: macroMultiplier,
macroStoppageMultiplier: macroMultiplier, },
}, activeStoppage,
events, events,
cycles cycles,
}); });
} }