Factory console: inspection feeds in fixed columns
Deploy test site / deploy (push) Successful in 45s
Deploy test site / deploy (push) Successful in 45s
The live inspection feeds (floor, quality, press, customer's latest copies) were one run-on string, so a shorter press ID (C1 vs D1-A) shifted everything after it and the cycle time showed twice. Each row is now a grid of fixed columns sized in characters of the mono font: time, stamper, press, order, result, with a header row (the row keeps the mono font so its columns match; only the labels change font). On phones the order column (factory) or press column (customer) drops. The full record key stays in the stamper cell's tooltip. Work order cards: station chip left, work order ID right-aligned in the header, the assignee moves to the time line, so card headers no longer shift with the ID length.
This commit is contained in:
1 parent
f9b8407561
commit
1cceedc8a4
1 file changed
+17
-10
+17
-10
@@ -144,11 +144,16 @@ document.documentElement.setAttribute("data-theme",t);})();
|
||||
|
||||
/* feed */
|
||||
.feed { display: flex; flex-direction: column; font-size: 12.5px; }
|
||||
.fr { display: grid; grid-template-columns: 62px minmax(0, 1.5fr) 82px minmax(0, 1fr); gap: 10px; padding: 4px 0; border-bottom: 1px dashed var(--line); align-items: baseline; }
|
||||
/* fixed columns sized in characters of the mono font, so a longer ID never pushes the next column */
|
||||
.fr { display: grid; grid-template-columns: 8.4ch 10.6ch 5ch 9ch minmax(0, 1fr); column-gap: 14px; padding: 4px 0; border-bottom: 1px dashed var(--line); align-items: baseline; font-family: var(--mono); font-variant-numeric: tabular-nums; }
|
||||
.fr > span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
|
||||
.fr:last-child { border-bottom: 0; }
|
||||
.fr .k { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--mute); }
|
||||
.fr .fo { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.fr .v { color: var(--ok); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.fr .k, .fr .pr { color: var(--mute); }
|
||||
.fr .v { color: var(--ok); font-family: system-ui, -apple-system, "Segoe UI", sans-serif; }
|
||||
/* the header row keeps the mono font on the row itself, so its ch-based columns match the data rows; only the labels change font */
|
||||
.fr.hd { color: var(--faint); border-bottom: 1px solid var(--line); padding-top: 0; }
|
||||
.fr.hd > span { font-family: system-ui, -apple-system, "Segoe UI", sans-serif; font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.06em; }
|
||||
.feed.cf .fr { grid-template-columns: 8.4ch 7ch 10.6ch 5ch minmax(0, 1fr); }
|
||||
.fr.bad .v { color: var(--bad); font-weight: 650; }
|
||||
|
||||
.al { display: flex; gap: 10px; padding: 8px 0; border-bottom: 1px solid var(--line); color: var(--ink); text-decoration: none; }
|
||||
@@ -235,7 +240,7 @@ document.documentElement.setAttribute("data-theme",t);})();
|
||||
.wo.todo { border-left-color: var(--inv); }
|
||||
.wo.done { border-left-color: var(--ok); opacity: 0.86; }
|
||||
.wo-h { display: flex; align-items: center; gap: 8px; font-size: 11.5px; min-width: 0; }
|
||||
.wo-h .who { margin-left: auto; color: var(--mute); white-space: nowrap; }
|
||||
.wo-h .wid { margin-left: auto; color: var(--faint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
|
||||
.wo b { font-size: 13.5px; font-weight: 650; }
|
||||
.wo .rf { font-size: 12px; color: var(--mute); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.wo small { color: var(--mute); }
|
||||
@@ -349,8 +354,10 @@ document.documentElement.setAttribute("data-theme",t);})();
|
||||
.chero .dw { max-width: 170px; margin: 0 auto; }
|
||||
.chero h1 { font-size: 27px; }
|
||||
.big { font-size: 42px; }
|
||||
.fr { grid-template-columns: 58px minmax(0, 1fr) minmax(0, 0.9fr); }
|
||||
.fr { grid-template-columns: 8.4ch 10.6ch 4.4ch minmax(0, 1fr); column-gap: 9px; }
|
||||
.fr .fo { display: none; }
|
||||
.feed.cf .fr { grid-template-columns: 8.4ch 6.4ch 10.6ch minmax(0, 1fr); }
|
||||
.feed.cf .fr .pr { display: none; }
|
||||
.pipe a { grid-template-columns: 120px minmax(0, 1fr) 34px; }
|
||||
.pareto div { grid-template-columns: 112px minmax(0, 1fr) 40px; }
|
||||
ol.ks li { grid-template-columns: 30px minmax(0, 1fr); }
|
||||
@@ -820,7 +827,7 @@ function audioTable(list,showOrder){const L=list.slice(-10).reverse();if(!L.leng
|
||||
function paretoHTML(by){const L=Object.entries(by).sort((a,b)=>b[1]-a[1]);if(!L.length)return '<p class="mute">No rejects yet.</p>';const mx=L[0][1];
|
||||
return `<div class="pareto">${L.map(([r,n])=>`<div><span>${esc(r)}</span>${bar(n/mx,"bad")}<span class="num">${fI(n)}</span></div>`).join("")}</div>`;}
|
||||
function feedHTML(list,n){if(!list.length)return '<p class="mute">The presses are off for the night. Records appear here as they leave the press.</p>';
|
||||
return `<div class="feed">${list.slice(-n).reverse().map(r=>`<div class="fr ${r.ok?"":"bad"}"><span class="mono">${fTs(r.t)}</span><span class="mono k" title="${esc(r.key)}">${esc(r.key)}</span><span class="fo"><a href="#order=${r.o}">${r.o}</a></span><span class="v">${r.ok?"✓ pass":"✕ "+esc(r.r)}</span></div>`).join("")}</div>`;}
|
||||
return `<div class="feed"><div class="fr hd"><span>Time</span><span>Stamper</span><span class="pr">Press</span><span class="fo">Order</span><span>Result</span></div>${list.slice(-n).reverse().map(r=>{const [sid,pid]=r.key.split(" · ");return `<div class="fr ${r.ok?"":"bad"}"><span>${fTs(r.t)}</span><span class="k" title="Record key: ${esc(r.key)}">${esc(sid)}</span><span class="pr">${esc(pid)}</span><span class="fo"><a href="#order=${r.o}">${r.o}</a></span><span class="v">${r.ok?"✓ pass":"✕ "+esc(r.r)}</span></div>`;}).join("")}</div>`;}
|
||||
const kpi=(l,v,s,c)=>`<div class="kpi ${c||""}"><span>${l}</span><b>${v}</b><small>${s||" "}</small></div>`;
|
||||
|
||||
/* ================= factory pages ================= */
|
||||
@@ -1081,13 +1088,13 @@ function woById(id){const W=workOrders().find(w=>w.id===id);if(W)return W;
|
||||
function woCur(w){const n=w.steps.length;return w.status==="done"?n:w.status==="todo"?0:Math.min(n-1,Math.floor(w.pr*n));}
|
||||
function woTime(w){
|
||||
if(w.status==="done")return "done "+fDT(w.end)+(w.who?" · "+w.who:"");
|
||||
if(w.status==="doing")return "started "+fDT(w.start)+(w.end&&isFinite(w.end)?", done about "+fDT(w.end):"");
|
||||
if(w.status==="doing")return "started "+fDT(w.start)+(w.end&&isFinite(w.end)?", done about "+fDT(w.end):"")+(w.who?" · "+w.who:"");
|
||||
if(w.prio&&w.plan)return "now, waiting for the lab";
|
||||
return w.plan?(w.st==="press"?"planned ":"arrives about ")+fDT(w.plan)+(w.where?" on "+w.where:""):"later";
|
||||
}
|
||||
const stChip=k=>`<span class="chip ${WSTN[k][2]}">${WSTN[k][1]}</span>`;
|
||||
function woCard(w){const c=woCur(w),n=w.steps.length;
|
||||
return `<a class="wo ${w.status}" href="#wo=${w.id}"><div class="wo-h"><span class="mono">${w.id}</span>${stChip(w.st)}<span class="who">${esc(w.who||"")}</span></div><b>${esc(w.title)}</b><div class="rf">${esc(w.ref)}${w.refT?" · "+esc(w.refT):""}</div>${w.status==="doing"?bar(w.pr,w.st==="press"?"acc":w.st==="maint"?"warn":"")+`<small class="cs">Step ${c+1} of ${n}: ${esc(w.steps[c][0])}</small>`:""}<small>${esc(woTime(w))}</small></a>`;}
|
||||
return `<a class="wo ${w.status}" href="#wo=${w.id}"><div class="wo-h">${stChip(w.st)}<span class="mono wid">${w.id}</span></div><b>${esc(w.title)}</b><div class="rf">${esc(w.ref)}${w.refT?" · "+esc(w.refT):""}</div>${w.status==="doing"?bar(w.pr,w.st==="press"?"acc":w.st==="maint"?"warn":"")+`<small class="cs">Step ${c+1} of ${n}: ${esc(w.steps[c][0])}</small>`:""}<small>${esc(woTime(w))}</small></a>`;}
|
||||
PAGES.work={
|
||||
layout:()=>`<div class="ph"><div><div class="eyebrow">Factory console</div><h1>Work orders</h1></div><p class="mute" style="max-width:560px">What every station does now and next. On the floor each station shows its own list on a kiosk: the operator taps the access badge and scans what each step asks for, and every confirmation lands in the event log.</p></div><div class="kpis" id="wK"></div><div class="filters" id="wF" style="margin-top:14px"></div><div class="kan" id="wB"></div>`,
|
||||
blocks(){
|
||||
@@ -1182,7 +1189,7 @@ const CUST={
|
||||
const g=STAGES[o.si].gate;
|
||||
let live="";
|
||||
if(o.si===SI.run||(o.si>SI.run&&o.feed.length)){
|
||||
live=`<div class="lc"><h3>Your latest copies</h3>${o.feed.length?`<div class="feed">${o.feed.slice(-8).reverse().map(r=>`<div class="fr ${r.ok?"":"bad"}" style="grid-template-columns:62px 74px minmax(0,1fr) minmax(0,0.9fr)"><span class="mono">${fTs(r.t)}</span><span class="mono">${r.ok?"#"+fI(r.copy):"-"}</span><span class="mono k">${esc(r.key.split(" · ").slice(0,2).join(" · "))}</span><span class="v">${r.ok?"✓ passed":"✕ "+esc(r.r)}</span></div>`).join("")}</div>`:'<p class="mute">The first copies appear here as they leave the press.</p>'}<p class="note">Record key: stamper, press and the second it was pressed. It is printed on the sleeve as a code and opens the record passport.</p></div>
|
||||
live=`<div class="lc"><h3>Your latest copies</h3>${o.feed.length?`<div class="feed cf"><div class="fr hd"><span>Time</span><span>Copy</span><span>Stamper</span><span class="pr">Press</span><span>Result</span></div>${o.feed.slice(-8).reverse().map(r=>{const [sid,pid]=r.key.split(" · ");return `<div class="fr ${r.ok?"":"bad"}"><span>${fTs(r.t)}</span><span>${r.ok?"#"+fI(r.copy):"-"}</span><span class="k" title="Record key: ${esc(r.key)}">${esc(sid)}</span><span class="pr">${esc(pid)}</span><span class="v">${r.ok?"✓ passed":"✕ "+esc(r.r)}</span></div>`;}).join("")}</div>`:'<p class="mute">The first copies appear here as they leave the press.</p>'}<p class="note">Record key: stamper, press and the second it was pressed. It is printed on the sleeve as a code and opens the record passport.</p></div>
|
||||
<div class="lc"><h3>Right now</h3><div class="stats3"><div><b>${fI(o.good)}</b><span>pressed and passed</span></div><div><b>${fI(o.rej)}</b><span>caught by the camera</span></div><div><b>${fI(o.audio.filter(a=>a.rt<=S.t).length)}</b><span>listening checks passed</span></div></div>${p&&p.tel.length?`<p class="note">Press ${p.id} last cycle: ${f1(p.tel[p.tel.length-1].cyc)} s, steam ${f1(p.tel[p.tel.length-1].steam)} °C, cake ${f1(p.tel[p.tel.length-1].cake)} g.</p>`:""}${o.audio.some(a=>a.rt>S.t)?`<p class="note">${(n=>n+(n>1?" copies":" copy"))(o.audio.filter(a=>a.rt>S.t).length)} at the listening station now.</p>`:""}</div>`;}
|
||||
return {
|
||||
cDisc:discSVG(o,250,spin)+"<!--"+(spin?"s":"")+"-->",
|
||||
|
||||
Reference in new issue
Block a user