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

@@ -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>