From 565e5ca4cbdf809c581e8566f671d2349353e658 Mon Sep 17 00:00:00 2001 From: masterdev Date: Wed, 17 Jun 2026 02:37:45 +0100 Subject: [PATCH] Show unqualified downtime for qualification --- backend/app/main.py | 49 +++++++++++++++++++--------------- frontend/src/App.tsx | 62 ++++++++++++++++++++++++++++---------------- 2 files changed, 67 insertions(+), 44 deletions(-) diff --git a/backend/app/main.py b/backend/app/main.py index 9a89445..f0ad8be 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -554,27 +554,34 @@ def close_production_order(order_id: int, db: Session = Depends(get_db)) -> dict @app.get("/api/downtimes/open") def get_open_downtimes(db: Session = Depends(get_db)) -> list[dict]: rows = db.execute(select(Downtime).where(Downtime.end_time.is_(None)).order_by(Downtime.start_time.asc())).scalars() - result = [] - for row in rows: - machine = db.get(Machine, row.machine_id) - order = db.get(ProductionOrder, row.production_order_id) if row.production_order_id else None - result.append( - { - "id": row.id, - "machine_id": row.machine_id, - "machine_code": machine.code if machine else None, - "production_order_id": row.production_order_id, - "order_number": order.order_number if order else None, - "start_time": row.start_time.isoformat(), - "end_time": None, - "duration_sec": row.duration_sec, - "reason_code": row.reason_code, - "comment": row.comment, - "auto_detected": row.auto_detected, - "qualified_by": row.qualified_by, - } - ) - return result + return [_serialize_downtime_for_qualification(db, row) for row in rows] + + +@app.get("/api/downtimes/unqualified") +def get_unqualified_downtimes(db: Session = Depends(get_db)) -> list[dict]: + rows = db.execute( + select(Downtime).where(Downtime.reason_code.is_(None)).order_by(Downtime.start_time.desc()).limit(200) + ).scalars() + return [_serialize_downtime_for_qualification(db, row) for row in rows] + + +def _serialize_downtime_for_qualification(db: Session, row: Downtime) -> dict: + machine = db.get(Machine, row.machine_id) + order = db.get(ProductionOrder, row.production_order_id) if row.production_order_id else None + return { + "id": row.id, + "machine_id": row.machine_id, + "machine_code": machine.code if machine else None, + "production_order_id": row.production_order_id, + "order_number": order.order_number if order else None, + "start_time": row.start_time.isoformat(), + "end_time": row.end_time.isoformat() if row.end_time else None, + "duration_sec": row.duration_sec, + "reason_code": row.reason_code, + "comment": row.comment, + "auto_detected": row.auto_detected, + "qualified_by": row.qualified_by, + } @app.post("/api/downtimes/{downtime_id}/qualify") diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b90799c..8ab6ae2 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -40,7 +40,7 @@ const moduleTabs: Array<{ key: ModuleKey; label: string; description: string }> export default function App() { const [dashboard, setDashboard] = useState(null); const [orders, setOrders] = useState([]); - const [openDowntimes, setOpenDowntimes] = useState([]); + const [unqualifiedDowntimes, setUnqualifiedDowntimes] = useState([]); const [scraps, setScraps] = useState([]); const [references, setReferences] = useState(null); const [dailyOee, setDailyOee] = useState(null); @@ -78,16 +78,16 @@ export default function App() { } async function loadCollections() { - const [loadedOrders, loadedOpenDowntimes, loadedScraps, loadedReferences, loadedDailyOee] = await Promise.all([ + const [loadedOrders, loadedUnqualifiedDowntimes, loadedScraps, loadedReferences, loadedDailyOee] = await Promise.all([ fetchJson("/api/production-orders"), - fetchJson("/api/downtimes/open"), + fetchJson("/api/downtimes/unqualified"), fetchJson("/api/scraps"), fetchJson("/api/reference-data"), fetchJson(`/api/oee/daily?date=${today}`), ]); setOrders(loadedOrders); - setOpenDowntimes(loadedOpenDowntimes); + setUnqualifiedDowntimes(loadedUnqualifiedDowntimes); setScraps(loadedScraps); setReferences(loadedReferences); setDailyOee(loadedDailyOee); @@ -192,19 +192,24 @@ export default function App() { const selectedMachine = dashboard?.machines.find((machine) => machine.id === selectedMachineId) ?? null; const machines = dashboard?.machines ?? []; - const selectedMachineOpenDowntimes = - selectedMachineId === null ? [] : openDowntimes.filter((downtime) => downtime.machine_id === selectedMachineId); - const downtimeOptions = [...openDowntimes].sort((left, right) => { + const openDowntimeCount = unqualifiedDowntimes.filter((downtime) => downtime.end_time === null).length; + const selectedMachineUnqualifiedDowntimes = + selectedMachineId === null ? [] : unqualifiedDowntimes.filter((downtime) => downtime.machine_id === selectedMachineId); + const downtimeOptions = [...unqualifiedDowntimes].sort((left, right) => { const leftSelected = selectedMachineId !== null && left.machine_id === selectedMachineId; const rightSelected = selectedMachineId !== null && right.machine_id === selectedMachineId; if (leftSelected !== rightSelected) { return Number(rightSelected) - Number(leftSelected); } + if ((left.end_time === null) !== (right.end_time === null)) { + return Number(right.end_time === null) - Number(left.end_time === null); + } return new Date(left.start_time).getTime() - new Date(right.start_time).getTime(); }); const summaryMetrics = { production: machines.filter((machine) => machine.status === "production").length, - openDowntimes: openDowntimes.length, + openDowntimes: openDowntimeCount, + unqualifiedDowntimes: unqualifiedDowntimes.length, producedQty: machines.reduce((total, machine) => total + machine.produced_qty, 0), scrapQty: machines.reduce((total, machine) => total + machine.scrap_qty, 0), oee: Math.round((dailyOee?.overall.oee ?? 0) * 100), @@ -422,13 +427,21 @@ export default function App() { } function downtimeOptionLabel(downtime: Downtime) { - const parts = [`#${downtime.id}`, downtimeMachineLabel(downtime)]; + const state = downtime.end_time === null ? "Open" : "Ended"; + const parts = [`#${downtime.id}`, state, downtimeMachineLabel(downtime)]; if (downtime.order_number) { parts.push(downtime.order_number); } return parts.join(" / "); } + function downtimeStateLabel(downtime: Downtime) { + if (downtime.reason_code) { + return downtime.reason_code; + } + return downtime.end_time === null ? "Open - waiting qualification" : "Ended - waiting qualification"; + } + function renderModule(): ReactNode { if (activeModule === "atelier") { return ( @@ -532,8 +545,11 @@ export default function App() {
    {detail.downtimes.slice(0, 8).map((item) => (
  • - {item.reason_code ?? "Waiting qualification"} - {new Date(item.start_time).toLocaleString()} + {downtimeStateLabel(item)} + + {new Date(item.start_time).toLocaleString()} + {item.end_time ? ` -> ${new Date(item.end_time).toLocaleString()}` : ""} +
  • ))}
@@ -661,8 +677,8 @@ export default function App() { title="Qualification Arret" subtitle={ selectedMachine - ? `${selectedMachineOpenDowntimes.length} open for ${selectedMachine.code} / ${openDowntimes.length} total` - : `${openDowntimes.length} open downtime(s)` + ? `${selectedMachineUnqualifiedDowntimes.length} unqualified for ${selectedMachine.code} / ${unqualifiedDowntimes.length} total` + : `${unqualifiedDowntimes.length} unqualified downtime(s)` } >
@@ -675,9 +691,9 @@ export default function App() { onChangeCapture={(event) => validateForm(event.currentTarget, "downtime")} noValidate > - {downtimeOptions.map((downtime) => (
@@ -748,7 +764,7 @@ export default function App() { - {openDowntimes.length === 0 ?
No open downtime currently needs qualification.
: null} + {unqualifiedDowntimes.length === 0 ?
No downtime currently needs qualification.
: null} ); @@ -891,7 +907,7 @@ export default function App() {
- +