Show unqualified downtime for qualification

This commit is contained in:
masterdev
2026-06-17 02:37:45 +01:00
parent 4e6286b1a3
commit 565e5ca4cb
2 changed files with 67 additions and 44 deletions

View File

@@ -554,27 +554,34 @@ def close_production_order(order_id: int, db: Session = Depends(get_db)) -> dict
@app.get("/api/downtimes/open") @app.get("/api/downtimes/open")
def get_open_downtimes(db: Session = Depends(get_db)) -> list[dict]: 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() rows = db.execute(select(Downtime).where(Downtime.end_time.is_(None)).order_by(Downtime.start_time.asc())).scalars()
result = [] return [_serialize_downtime_for_qualification(db, row) for row in rows]
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) machine = db.get(Machine, row.machine_id)
order = db.get(ProductionOrder, row.production_order_id) if row.production_order_id else None order = db.get(ProductionOrder, row.production_order_id) if row.production_order_id else None
result.append( return {
{
"id": row.id, "id": row.id,
"machine_id": row.machine_id, "machine_id": row.machine_id,
"machine_code": machine.code if machine else None, "machine_code": machine.code if machine else None,
"production_order_id": row.production_order_id, "production_order_id": row.production_order_id,
"order_number": order.order_number if order else None, "order_number": order.order_number if order else None,
"start_time": row.start_time.isoformat(), "start_time": row.start_time.isoformat(),
"end_time": None, "end_time": row.end_time.isoformat() if row.end_time else None,
"duration_sec": row.duration_sec, "duration_sec": row.duration_sec,
"reason_code": row.reason_code, "reason_code": row.reason_code,
"comment": row.comment, "comment": row.comment,
"auto_detected": row.auto_detected, "auto_detected": row.auto_detected,
"qualified_by": row.qualified_by, "qualified_by": row.qualified_by,
} }
)
return result
@app.post("/api/downtimes/{downtime_id}/qualify") @app.post("/api/downtimes/{downtime_id}/qualify")

View File

@@ -40,7 +40,7 @@ const moduleTabs: Array<{ key: ModuleKey; label: string; description: string }>
export default function App() { export default function App() {
const [dashboard, setDashboard] = useState<DashboardSnapshot | null>(null); const [dashboard, setDashboard] = useState<DashboardSnapshot | null>(null);
const [orders, setOrders] = useState<ProductionOrder[]>([]); const [orders, setOrders] = useState<ProductionOrder[]>([]);
const [openDowntimes, setOpenDowntimes] = useState<Downtime[]>([]); const [unqualifiedDowntimes, setUnqualifiedDowntimes] = useState<Downtime[]>([]);
const [scraps, setScraps] = useState<Scrap[]>([]); const [scraps, setScraps] = useState<Scrap[]>([]);
const [references, setReferences] = useState<ReferenceData | null>(null); const [references, setReferences] = useState<ReferenceData | null>(null);
const [dailyOee, setDailyOee] = useState<DailyOee | null>(null); const [dailyOee, setDailyOee] = useState<DailyOee | null>(null);
@@ -78,16 +78,16 @@ export default function App() {
} }
async function loadCollections() { async function loadCollections() {
const [loadedOrders, loadedOpenDowntimes, loadedScraps, loadedReferences, loadedDailyOee] = await Promise.all([ const [loadedOrders, loadedUnqualifiedDowntimes, loadedScraps, loadedReferences, loadedDailyOee] = await Promise.all([
fetchJson<ProductionOrder[]>("/api/production-orders"), fetchJson<ProductionOrder[]>("/api/production-orders"),
fetchJson<Downtime[]>("/api/downtimes/open"), fetchJson<Downtime[]>("/api/downtimes/unqualified"),
fetchJson<Scrap[]>("/api/scraps"), fetchJson<Scrap[]>("/api/scraps"),
fetchJson<ReferenceData>("/api/reference-data"), fetchJson<ReferenceData>("/api/reference-data"),
fetchJson<DailyOee>(`/api/oee/daily?date=${today}`), fetchJson<DailyOee>(`/api/oee/daily?date=${today}`),
]); ]);
setOrders(loadedOrders); setOrders(loadedOrders);
setOpenDowntimes(loadedOpenDowntimes); setUnqualifiedDowntimes(loadedUnqualifiedDowntimes);
setScraps(loadedScraps); setScraps(loadedScraps);
setReferences(loadedReferences); setReferences(loadedReferences);
setDailyOee(loadedDailyOee); setDailyOee(loadedDailyOee);
@@ -192,19 +192,24 @@ export default function App() {
const selectedMachine = dashboard?.machines.find((machine) => machine.id === selectedMachineId) ?? null; const selectedMachine = dashboard?.machines.find((machine) => machine.id === selectedMachineId) ?? null;
const machines = dashboard?.machines ?? []; const machines = dashboard?.machines ?? [];
const selectedMachineOpenDowntimes = const openDowntimeCount = unqualifiedDowntimes.filter((downtime) => downtime.end_time === null).length;
selectedMachineId === null ? [] : openDowntimes.filter((downtime) => downtime.machine_id === selectedMachineId); const selectedMachineUnqualifiedDowntimes =
const downtimeOptions = [...openDowntimes].sort((left, right) => { selectedMachineId === null ? [] : unqualifiedDowntimes.filter((downtime) => downtime.machine_id === selectedMachineId);
const downtimeOptions = [...unqualifiedDowntimes].sort((left, right) => {
const leftSelected = selectedMachineId !== null && left.machine_id === selectedMachineId; const leftSelected = selectedMachineId !== null && left.machine_id === selectedMachineId;
const rightSelected = selectedMachineId !== null && right.machine_id === selectedMachineId; const rightSelected = selectedMachineId !== null && right.machine_id === selectedMachineId;
if (leftSelected !== rightSelected) { if (leftSelected !== rightSelected) {
return Number(rightSelected) - Number(leftSelected); 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(); return new Date(left.start_time).getTime() - new Date(right.start_time).getTime();
}); });
const summaryMetrics = { const summaryMetrics = {
production: machines.filter((machine) => machine.status === "production").length, 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), producedQty: machines.reduce((total, machine) => total + machine.produced_qty, 0),
scrapQty: machines.reduce((total, machine) => total + machine.scrap_qty, 0), scrapQty: machines.reduce((total, machine) => total + machine.scrap_qty, 0),
oee: Math.round((dailyOee?.overall.oee ?? 0) * 100), oee: Math.round((dailyOee?.overall.oee ?? 0) * 100),
@@ -422,13 +427,21 @@ export default function App() {
} }
function downtimeOptionLabel(downtime: Downtime) { 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) { if (downtime.order_number) {
parts.push(downtime.order_number); parts.push(downtime.order_number);
} }
return parts.join(" / "); 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 { function renderModule(): ReactNode {
if (activeModule === "atelier") { if (activeModule === "atelier") {
return ( return (
@@ -532,8 +545,11 @@ export default function App() {
<ul className="list"> <ul className="list">
{detail.downtimes.slice(0, 8).map((item) => ( {detail.downtimes.slice(0, 8).map((item) => (
<li key={item.id}> <li key={item.id}>
<strong>{item.reason_code ?? "Waiting qualification"}</strong> <strong>{downtimeStateLabel(item)}</strong>
<span>{new Date(item.start_time).toLocaleString()}</span> <span>
{new Date(item.start_time).toLocaleString()}
{item.end_time ? ` -> ${new Date(item.end_time).toLocaleString()}` : ""}
</span>
</li> </li>
))} ))}
</ul> </ul>
@@ -661,8 +677,8 @@ export default function App() {
title="Qualification Arret" title="Qualification Arret"
subtitle={ subtitle={
selectedMachine selectedMachine
? `${selectedMachineOpenDowntimes.length} open for ${selectedMachine.code} / ${openDowntimes.length} total` ? `${selectedMachineUnqualifiedDowntimes.length} unqualified for ${selectedMachine.code} / ${unqualifiedDowntimes.length} total`
: `${openDowntimes.length} open downtime(s)` : `${unqualifiedDowntimes.length} unqualified downtime(s)`
} }
> >
<div className="module-grid module-grid--form-table"> <div className="module-grid module-grid--form-table">
@@ -675,9 +691,9 @@ export default function App() {
onChangeCapture={(event) => validateForm(event.currentTarget, "downtime")} onChangeCapture={(event) => validateForm(event.currentTarget, "downtime")}
noValidate noValidate
> >
<select name="downtime_id" required defaultValue={selectedMachineOpenDowntimes[0]?.id ?? ""}> <select name="downtime_id" required defaultValue={selectedMachineUnqualifiedDowntimes[0]?.id ?? ""}>
<option value="" disabled> <option value="" disabled>
Open downtime Downtime to qualify
</option> </option>
{downtimeOptions.map((downtime) => ( {downtimeOptions.map((downtime) => (
<option key={downtime.id} value={downtime.id}> <option key={downtime.id} value={downtime.id}>
@@ -712,26 +728,26 @@ export default function App() {
<section className="downtime-focus"> <section className="downtime-focus">
<span className="meta-label">Selected machine</span> <span className="meta-label">Selected machine</span>
<strong>{selectedMachine.code}</strong> <strong>{selectedMachine.code}</strong>
{selectedMachineOpenDowntimes.length > 0 ? ( {selectedMachineUnqualifiedDowntimes.length > 0 ? (
<ul className="timeline-list timeline-list--compact"> <ul className="timeline-list timeline-list--compact">
{selectedMachineOpenDowntimes.map((item) => ( {selectedMachineUnqualifiedDowntimes.map((item) => (
<li key={item.id}> <li key={item.id}>
<div> <div>
<strong>{downtimeOptionLabel(item)}</strong> <strong>{downtimeOptionLabel(item)}</strong>
<span>{item.reason_code ?? "Waiting qualification"}</span> <span>{downtimeStateLabel(item)}</span>
</div> </div>
<time>{new Date(item.start_time).toLocaleString()}</time> <time>{new Date(item.start_time).toLocaleString()}</time>
</li> </li>
))} ))}
</ul> </ul>
) : ( ) : (
<div className="empty-state">No open downtime for {selectedMachine.code}.</div> <div className="empty-state">No unqualified downtime for {selectedMachine.code}.</div>
)} )}
</section> </section>
) : null} ) : null}
<section> <section>
<span className="meta-label">All open stops</span> <span className="meta-label">All stops waiting qualification</span>
<ul className="timeline-list"> <ul className="timeline-list">
{downtimeOptions.map((item) => ( {downtimeOptions.map((item) => (
<li key={item.id} className={item.machine_id === selectedMachineId ? "is-current-machine" : ""}> <li key={item.id} className={item.machine_id === selectedMachineId ? "is-current-machine" : ""}>
@@ -739,7 +755,7 @@ export default function App() {
<strong>{downtimeMachineLabel(item)}</strong> <strong>{downtimeMachineLabel(item)}</strong>
<span> <span>
{item.order_number ? `${item.order_number} / ` : ""} {item.order_number ? `${item.order_number} / ` : ""}
{item.reason_code ?? "Waiting qualification"} {downtimeStateLabel(item)}
</span> </span>
</div> </div>
<time>{new Date(item.start_time).toLocaleString()}</time> <time>{new Date(item.start_time).toLocaleString()}</time>
@@ -748,7 +764,7 @@ export default function App() {
</ul> </ul>
</section> </section>
</div> </div>
{openDowntimes.length === 0 ? <div className="empty-state">No open downtime currently needs qualification.</div> : null} {unqualifiedDowntimes.length === 0 ? <div className="empty-state">No downtime currently needs qualification.</div> : null}
</div> </div>
</Panel> </Panel>
); );
@@ -891,7 +907,7 @@ export default function App() {
<section className="summary-ribbon" aria-label="Workshop summary"> <section className="summary-ribbon" aria-label="Workshop summary">
<KpiBlock label="Global OEE" value={`${summaryMetrics.oee}%`} subLabel="Daily aggregate" tone="dark" /> <KpiBlock label="Global OEE" value={`${summaryMetrics.oee}%`} subLabel="Daily aggregate" tone="dark" />
<KpiBlock label="Production" value={summaryMetrics.production} subLabel={`${machines.length} machines monitored`} /> <KpiBlock label="Production" value={summaryMetrics.production} subLabel={`${machines.length} machines monitored`} />
<KpiBlock label="Open stops" value={summaryMetrics.openDowntimes} subLabel="Awaiting qualification" /> <KpiBlock label="Open stops" value={summaryMetrics.openDowntimes} subLabel={`${summaryMetrics.unqualifiedDowntimes} unqualified`} />
<KpiBlock label="Output / scrap" value={`${summaryMetrics.producedQty} / ${summaryMetrics.scrapQty}`} subLabel="Pieces today" /> <KpiBlock label="Output / scrap" value={`${summaryMetrics.producedQty} / ${summaryMetrics.scrapQty}`} subLabel="Pieces today" />
</section> </section>