Improve downtime qualification visibility
This commit is contained in:
@@ -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 [
|
||||
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,
|
||||
}
|
||||
for row in rows
|
||||
]
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@app.post("/api/downtimes/{downtime_id}/qualify")
|
||||
|
||||
@@ -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] ? <div className="inline-toast">{toasts[key]}</div> : 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 (
|
||||
<Panel title="Qualification Arret" subtitle={`${openDowntimes.length} open downtime(s)`}>
|
||||
<Panel
|
||||
title="Qualification Arret"
|
||||
subtitle={
|
||||
selectedMachine
|
||||
? `${selectedMachineOpenDowntimes.length} open for ${selectedMachine.code} / ${openDowntimes.length} total`
|
||||
: `${openDowntimes.length} open downtime(s)`
|
||||
}
|
||||
>
|
||||
<div className="module-grid module-grid--form-table">
|
||||
<form
|
||||
className="form-grid module-form"
|
||||
key={selectedMachine?.id ?? "all-downtimes"}
|
||||
onSubmit={submitDowntimeQualification}
|
||||
onBlurCapture={(event) => handleFormBlur("downtime", event.currentTarget)}
|
||||
onInputCapture={(event) => validateForm(event.currentTarget, "downtime")}
|
||||
onChangeCapture={(event) => validateForm(event.currentTarget, "downtime")}
|
||||
noValidate
|
||||
>
|
||||
<select name="downtime_id" required defaultValue="">
|
||||
<select name="downtime_id" required defaultValue={selectedMachineOpenDowntimes[0]?.id ?? ""}>
|
||||
<option value="" disabled>
|
||||
Open downtime
|
||||
</option>
|
||||
{openDowntimes.map((downtime) => (
|
||||
{downtimeOptions.map((downtime) => (
|
||||
<option key={downtime.id} value={downtime.id}>
|
||||
#{downtime.id} machine {downtime.machine_id}
|
||||
{downtimeOptionLabel(downtime)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
@@ -673,17 +707,47 @@ export default function App() {
|
||||
{toast("downtime")}
|
||||
</form>
|
||||
|
||||
<ul className="timeline-list">
|
||||
{openDowntimes.map((item) => (
|
||||
<div className="downtime-board">
|
||||
{selectedMachine ? (
|
||||
<section className="downtime-focus">
|
||||
<span className="meta-label">Selected machine</span>
|
||||
<strong>{selectedMachine.code}</strong>
|
||||
{selectedMachineOpenDowntimes.length > 0 ? (
|
||||
<ul className="timeline-list timeline-list--compact">
|
||||
{selectedMachineOpenDowntimes.map((item) => (
|
||||
<li key={item.id}>
|
||||
<div>
|
||||
<strong>Machine {item.machine_id}</strong>
|
||||
<span>Waiting qualification</span>
|
||||
<strong>{downtimeOptionLabel(item)}</strong>
|
||||
<span>{item.reason_code ?? "Waiting qualification"}</span>
|
||||
</div>
|
||||
<time>{new Date(item.start_time).toLocaleString()}</time>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<div className="empty-state">No open downtime for {selectedMachine.code}.</div>
|
||||
)}
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<section>
|
||||
<span className="meta-label">All open stops</span>
|
||||
<ul className="timeline-list">
|
||||
{downtimeOptions.map((item) => (
|
||||
<li key={item.id} className={item.machine_id === selectedMachineId ? "is-current-machine" : ""}>
|
||||
<div>
|
||||
<strong>{downtimeMachineLabel(item)}</strong>
|
||||
<span>
|
||||
{item.order_number ? `${item.order_number} / ` : ""}
|
||||
{item.reason_code ?? "Waiting qualification"}
|
||||
</span>
|
||||
</div>
|
||||
<time>{new Date(item.start_time).toLocaleString()}</time>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
{openDowntimes.length === 0 ? <div className="empty-state">No open downtime currently needs qualification.</div> : null}
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user