diff --git a/backend/app/main.py b/backend/app/main.py index 7b214bb..9a89445 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -440,6 +440,9 @@ def get_machine_cycles(machine_id: int, limit: int = Query(default=100, le=500), @app.get("/api/machines/{machine_id}/downtimes") def get_machine_downtimes(machine_id: int, limit: int = Query(default=100, le=500), db: Session = Depends(get_db)) -> list[dict]: + machine = db.get(Machine, machine_id) + if machine is None: + raise HTTPException(status_code=404, detail="Machine not found") rows = db.execute( select(Downtime).where(Downtime.machine_id == machine_id).order_by(Downtime.start_time.desc()).limit(limit) ).scalars() @@ -449,6 +452,8 @@ def get_machine_downtimes(machine_id: int, limit: int = Query(default=100, le=50 "start_time": row.start_time.isoformat(), "end_time": row.end_time.isoformat() if row.end_time else None, "duration_sec": row.duration_sec, + "machine_id": row.machine_id, + "machine_code": machine.code, "reason_code": row.reason_code, "comment": row.comment, "auto_detected": row.auto_detected, @@ -549,19 +554,27 @@ 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() - return [ - { - "id": row.id, - "machine_id": row.machine_id, - "production_order_id": row.production_order_id, - "start_time": row.start_time.isoformat(), - "reason_code": row.reason_code, - "comment": row.comment, - "auto_detected": row.auto_detected, - "qualified_by": row.qualified_by, - } - for row in rows - ] + 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 @app.post("/api/downtimes/{downtime_id}/qualify") diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 0f55450..b90799c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -192,6 +192,16 @@ 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 leftSelected = selectedMachineId !== null && left.machine_id === selectedMachineId; + const rightSelected = selectedMachineId !== null && right.machine_id === selectedMachineId; + if (leftSelected !== rightSelected) { + return Number(rightSelected) - Number(leftSelected); + } + 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, @@ -403,6 +413,22 @@ export default function App() { return toasts[key] ?
{toasts[key]}
: null; } + function downtimeMachineLabel(downtime: Downtime) { + if (downtime.machine_code) { + return downtime.machine_code; + } + const machine = machines.find((item) => item.id === downtime.machine_id); + return machine?.code ?? `Machine ${downtime.machine_id ?? "-"}`; + } + + function downtimeOptionLabel(downtime: Downtime) { + const parts = [`#${downtime.id}`, downtimeMachineLabel(downtime)]; + if (downtime.order_number) { + parts.push(downtime.order_number); + } + return parts.join(" / "); + } + function renderModule(): ReactNode { if (activeModule === "atelier") { return ( @@ -631,23 +657,31 @@ export default function App() { if (activeModule === "downtimes") { return ( - +
handleFormBlur("downtime", event.currentTarget)} onInputCapture={(event) => validateForm(event.currentTarget, "downtime")} onChangeCapture={(event) => validateForm(event.currentTarget, "downtime")} noValidate > - - {openDowntimes.map((downtime) => ( + {downtimeOptions.map((downtime) => ( ))} @@ -673,17 +707,47 @@ export default function App() { {toast("downtime")}
-
    - {openDowntimes.map((item) => ( -
  • -
    - Machine {item.machine_id} - Waiting qualification -
    - -
  • - ))} -
+
+ {selectedMachine ? ( +
+ Selected machine + {selectedMachine.code} + {selectedMachineOpenDowntimes.length > 0 ? ( +
    + {selectedMachineOpenDowntimes.map((item) => ( +
  • +
    + {downtimeOptionLabel(item)} + {item.reason_code ?? "Waiting qualification"} +
    + +
  • + ))} +
+ ) : ( +
No open downtime for {selectedMachine.code}.
+ )} +
+ ) : null} + +
+ All open stops +
    + {downtimeOptions.map((item) => ( +
  • +
    + {downtimeMachineLabel(item)} + + {item.order_number ? `${item.order_number} / ` : ""} + {item.reason_code ?? "Waiting qualification"} + +
    + +
  • + ))} +
+
+
{openDowntimes.length === 0 ?
No open downtime currently needs qualification.
: null}
diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index 3ff773e..f02ebe3 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -83,7 +83,9 @@ export type Cycle = { export type Downtime = { id: number; machine_id?: number; + machine_code?: string | null; production_order_id: number | null; + order_number?: string | null; start_time: string; end_time: string | null; duration_sec: number | null; diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 29bd8f8..42305c3 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -708,6 +708,11 @@ th { border-bottom: 1px solid var(--border); } +.timeline-list li.is-current-machine { + border-radius: 8px; + background: var(--accent-light); +} + .timeline-list li::before { position: absolute; top: 18px; @@ -753,6 +758,32 @@ th { text-align: right; } +.timeline-list--compact { + margin-top: 10px; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--bg-panel); +} + +.downtime-board { + display: grid; + gap: 14px; +} + +.downtime-focus { + padding: 12px; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--bg-panel-alt); +} + +.downtime-focus > strong { + display: block; + margin-top: 4px; + font-family: "Space Grotesk", sans-serif; + font-size: var(--text-lg); +} + .oee-mini-list { display: grid; gap: 8px;