Show unqualified downtime for qualification
This commit is contained in:
@@ -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:
|
||||
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
|
||||
result.append(
|
||||
{
|
||||
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": None,
|
||||
"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,
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@app.post("/api/downtimes/{downtime_id}/qualify")
|
||||
|
||||
@@ -40,7 +40,7 @@ const moduleTabs: Array<{ key: ModuleKey; label: string; description: string }>
|
||||
export default function App() {
|
||||
const [dashboard, setDashboard] = useState<DashboardSnapshot | null>(null);
|
||||
const [orders, setOrders] = useState<ProductionOrder[]>([]);
|
||||
const [openDowntimes, setOpenDowntimes] = useState<Downtime[]>([]);
|
||||
const [unqualifiedDowntimes, setUnqualifiedDowntimes] = useState<Downtime[]>([]);
|
||||
const [scraps, setScraps] = useState<Scrap[]>([]);
|
||||
const [references, setReferences] = useState<ReferenceData | null>(null);
|
||||
const [dailyOee, setDailyOee] = useState<DailyOee | null>(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<ProductionOrder[]>("/api/production-orders"),
|
||||
fetchJson<Downtime[]>("/api/downtimes/open"),
|
||||
fetchJson<Downtime[]>("/api/downtimes/unqualified"),
|
||||
fetchJson<Scrap[]>("/api/scraps"),
|
||||
fetchJson<ReferenceData>("/api/reference-data"),
|
||||
fetchJson<DailyOee>(`/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() {
|
||||
<ul className="list">
|
||||
{detail.downtimes.slice(0, 8).map((item) => (
|
||||
<li key={item.id}>
|
||||
<strong>{item.reason_code ?? "Waiting qualification"}</strong>
|
||||
<span>{new Date(item.start_time).toLocaleString()}</span>
|
||||
<strong>{downtimeStateLabel(item)}</strong>
|
||||
<span>
|
||||
{new Date(item.start_time).toLocaleString()}
|
||||
{item.end_time ? ` -> ${new Date(item.end_time).toLocaleString()}` : ""}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -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)`
|
||||
}
|
||||
>
|
||||
<div className="module-grid module-grid--form-table">
|
||||
@@ -675,9 +691,9 @@ export default function App() {
|
||||
onChangeCapture={(event) => validateForm(event.currentTarget, "downtime")}
|
||||
noValidate
|
||||
>
|
||||
<select name="downtime_id" required defaultValue={selectedMachineOpenDowntimes[0]?.id ?? ""}>
|
||||
<select name="downtime_id" required defaultValue={selectedMachineUnqualifiedDowntimes[0]?.id ?? ""}>
|
||||
<option value="" disabled>
|
||||
Open downtime
|
||||
Downtime to qualify
|
||||
</option>
|
||||
{downtimeOptions.map((downtime) => (
|
||||
<option key={downtime.id} value={downtime.id}>
|
||||
@@ -712,26 +728,26 @@ export default function App() {
|
||||
<section className="downtime-focus">
|
||||
<span className="meta-label">Selected machine</span>
|
||||
<strong>{selectedMachine.code}</strong>
|
||||
{selectedMachineOpenDowntimes.length > 0 ? (
|
||||
{selectedMachineUnqualifiedDowntimes.length > 0 ? (
|
||||
<ul className="timeline-list timeline-list--compact">
|
||||
{selectedMachineOpenDowntimes.map((item) => (
|
||||
{selectedMachineUnqualifiedDowntimes.map((item) => (
|
||||
<li key={item.id}>
|
||||
<div>
|
||||
<strong>{downtimeOptionLabel(item)}</strong>
|
||||
<span>{item.reason_code ?? "Waiting qualification"}</span>
|
||||
<span>{downtimeStateLabel(item)}</span>
|
||||
</div>
|
||||
<time>{new Date(item.start_time).toLocaleString()}</time>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<div className="empty-state">No open downtime for {selectedMachine.code}.</div>
|
||||
<div className="empty-state">No unqualified downtime for {selectedMachine.code}.</div>
|
||||
)}
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<section>
|
||||
<span className="meta-label">All open stops</span>
|
||||
<span className="meta-label">All stops waiting qualification</span>
|
||||
<ul className="timeline-list">
|
||||
{downtimeOptions.map((item) => (
|
||||
<li key={item.id} className={item.machine_id === selectedMachineId ? "is-current-machine" : ""}>
|
||||
@@ -739,7 +755,7 @@ export default function App() {
|
||||
<strong>{downtimeMachineLabel(item)}</strong>
|
||||
<span>
|
||||
{item.order_number ? `${item.order_number} / ` : ""}
|
||||
{item.reason_code ?? "Waiting qualification"}
|
||||
{downtimeStateLabel(item)}
|
||||
</span>
|
||||
</div>
|
||||
<time>{new Date(item.start_time).toLocaleString()}</time>
|
||||
@@ -748,7 +764,7 @@ export default function App() {
|
||||
</ul>
|
||||
</section>
|
||||
</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>
|
||||
</Panel>
|
||||
);
|
||||
@@ -891,7 +907,7 @@ export default function App() {
|
||||
<section className="summary-ribbon" aria-label="Workshop summary">
|
||||
<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="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" />
|
||||
</section>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user