Factory console: ERPNext link page and ERPNext documents on orders
Deploy test site / deploy (push) Successful in 45s
Deploy test site / deploy (push) Successful in 45s
Follows the decision that the whole plant runs ERPNext and every tool
we build interacts with it (e12ad2e).
- New page #erp (nav: ERPNext): every posting the floor makes, derived
from the same event log: sales orders in by webhook, work orders,
job cards per station, serial numbers for lacquers, fathers and
mothers (warehouse Archive), purchase receipts, quality inspections
and stock entries per compound batch, manufacture entries per run,
downtime entries per stop, delivery notes and invoices. Outbox status
per row (sent within a minute, held during the nightly backup
01:00 to 01:15), KPIs, who-owns-what table, counts by document type.
Rows use fixed ch grid columns; on phones a two-line row.
- Order page: ERPNext card (customer, sales order, work order with
produced quantity, job cards completed, batches, delivery note and
invoice). Work order page: its job card, quality inspection or asset
maintenance log.
- Names follow ERPNext's default naming series and are simulated;
nothing uses the simulation's random stream, so the seed is unchanged.
- Phones: the nav strip scrolls the current tab into view. Work order
IDs on the order page no longer wrap (wider fixed column).
digital-factory.md demo paragraph. Checked: node --check, Edge dark and
light at 1440/1024/390 (no overflow, no page errors, rows aligned), and
the pw_fc, pw_wo and pw_feed regressions.
This commit is contained in:
1 parent
e12ad2ed7b
commit
a101c9f7db
2 files changed
+89
-6
No files matched your search
@@ -521,6 +521,10 @@ simulated plant day: the press floor with live counters per press, work
|
||||
orders per station in the kiosk form described below (badge in, scan
|
||||
what the step asks for, confirm), the optical inspection stream, audio samples, the material lab gate with a
|
||||
lot on hold, stamper metrology and genealogy, the hash-chained event log,
|
||||
the ERPNext link (every posting the floor makes: sales orders in by
|
||||
webhook, job cards, batches and quality inspections, stock entries,
|
||||
downtime entries, delivery notes and invoices, through the outbox, with
|
||||
the ERPNext documents on each order and work order),
|
||||
and the customer portal of the same orders (progress through the three
|
||||
gates, copies pressed so far, the lab certificate of the compound lot,
|
||||
listening checks). All orders, values and IDs are simulated; limits are
|
||||
|
||||
@@ -155,6 +155,15 @@ document.documentElement.setAttribute("data-theme",t);})();
|
||||
.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; }
|
||||
/* ERPNext postings: fixed ch columns, IDs never shift the row */
|
||||
.er { display: grid; grid-template-columns: 8.4ch 18ch 19ch minmax(0, 1fr) 10ch; grid-template-areas: "t d n e s"; column-gap: 14px; padding: 4px 0; border-bottom: 1px dashed var(--line); align-items: baseline; font-family: var(--mono); font-size: 12.5px; font-variant-numeric: tabular-nums; }
|
||||
.er:last-child { border-bottom: 0; }
|
||||
.er > span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
|
||||
.er .t { grid-area: t; } .er .d { grid-area: d; color: var(--mute); } .er .n { grid-area: n; } .er .e { grid-area: e; font-family: system-ui, -apple-system, "Segoe UI", sans-serif; } .er .s { grid-area: s; font-family: system-ui, -apple-system, "Segoe UI", sans-serif; color: var(--ok); }
|
||||
.er .s.q { color: var(--warn); } .er .s.in { color: var(--mute); }
|
||||
.er.hd { color: var(--faint); border-bottom: 1px solid var(--line); padding-top: 0; }
|
||||
.er.hd > span { font-family: system-ui, -apple-system, "Segoe UI", sans-serif; font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--faint); }
|
||||
.own td:first-child { font-weight: 650; white-space: nowrap; }
|
||||
|
||||
.al { display: flex; gap: 10px; padding: 8px 0; border-bottom: 1px solid var(--line); color: var(--ink); text-decoration: none; }
|
||||
.al:last-child { border-bottom: 0; }
|
||||
@@ -260,8 +269,9 @@ document.documentElement.setAttribute("data-theme",t);})();
|
||||
.scan::before { content: "▦ "; }
|
||||
li.now .scan { border-color: var(--acc); color: var(--ink); }
|
||||
.wol { display: flex; flex-direction: column; }
|
||||
.wol a { display: grid; grid-template-columns: 96px minmax(0, 1fr) auto; gap: 10px; align-items: center; padding: 6px 0; border-bottom: 1px dashed var(--line); color: var(--ink); font-size: 13px; text-decoration: none; }
|
||||
.wol a { display: grid; grid-template-columns: 112px minmax(0, 1fr) auto; gap: 10px; align-items: center; padding: 6px 0; border-bottom: 1px dashed var(--line); color: var(--ink); font-size: 13px; text-decoration: none; }
|
||||
.wol a:last-child { border-bottom: 0; }
|
||||
.wol .mono { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
|
||||
.wol a:hover b { text-decoration: underline; }
|
||||
.wol b { font-weight: 560; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
||||
@@ -358,6 +368,9 @@ document.documentElement.setAttribute("data-theme",t);})();
|
||||
.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; }
|
||||
.er { grid-template-columns: 8.4ch minmax(0, 1fr) 9ch; grid-template-areas: "t n s" ". e e"; column-gap: 9px; }
|
||||
.er .d, .er.hd .e { display: none; }
|
||||
.er .e { white-space: normal; color: var(--mute); font-size: 12px; }
|
||||
.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); }
|
||||
@@ -391,7 +404,7 @@ document.documentElement.setAttribute("data-theme",t);})();
|
||||
<div class="fac" id="factory">
|
||||
<aside class="side">
|
||||
<nav id="fnav" aria-label="Factory console"></nav>
|
||||
<div class="sidenote"><b>What this is.</b> The factory application from the digital factory plan (working name cnx-factory): every machine, instrument and operator check writes to one append-only log, and the customer portal is a scoped view of the same records.<br><br><b>Simulated:</b> all orders, values and IDs. <b>From the plan:</b> stages and the three artist gates, the inspection layers, Newbilt cycle times, two shifts 06:00 to 22:00. The demo runs one Duplex system and one Classic. Limits are illustrative: the acceptance specifications are still to be written.</div>
|
||||
<div class="sidenote"><b>What this is.</b> The factory application from the digital factory plan (working name cnx-factory): every machine, instrument and operator check writes to one append-only log, and the customer portal is a scoped view of the same records.<br><br><b>Simulated:</b> all orders, values and IDs. <b>From the plan:</b> stages and the three artist gates, the inspection layers, Newbilt cycle times, two shifts 06:00 to 22:00. The demo runs one Duplex system and one Classic. Limits are illustrative: the acceptance specifications are still to be written.<br><br><b>ERPNext</b> runs the whole plant: orders, job cards, batches and invoices are posted there (see ERPNext).</div>
|
||||
</aside>
|
||||
<main id="fmain"></main>
|
||||
</div>
|
||||
@@ -470,7 +483,7 @@ const PST={run:"Running",warm:"Warm-up",change:"Changeover",stop:"Stopped",off:"
|
||||
const NOW0=(()=>{const d=new Date();d.setHours(10,5,0,0);return d.getTime();})();
|
||||
|
||||
/* ================= state ================= */
|
||||
const S={t:NOW0-60*DAY,speed:20,run:true,warm:true,orders:[],byId:{},presses:[],lots:[],use:{black:null,clear:null},pend:{black:0,clear:0},regrind:[],feed:[],aud:[],led:[],ledN:0,day:{k:0},days:[],nextArr:0,seq:{o:1040,p:300,lot:2400,rg:880},
|
||||
const S={t:NOW0-60*DAY,speed:20,run:true,warm:true,orders:[],byId:{},presses:[],lots:[],use:{black:null,clear:null},pend:{black:0,clear:0},regrind:[],feed:[],aud:[],led:[],ledN:0,erp:[],erpN:0,eseq:{},day:{k:0},days:[],nextArr:0,seq:{o:1040,p:300,lot:2400,rg:880},
|
||||
ui:{view:"factory",fpage:"floor",arg:null,fhash:"floor",filter:"active",q:"",cust:null}};
|
||||
function mkPress(id,sys,kind){return {id,sys,kind,st:"off",next:0,until:0,order:null,set:null,since:0,why:"",tel:[],lc:30000,op:"",res:0,stops:[]};}
|
||||
/* demo fleet for now: one Duplex system (two presses on one extruder set) and one Classic */
|
||||
@@ -481,9 +494,51 @@ const pById=id=>S.presses.find(p=>p.id===id);
|
||||
function log(t,k,txt,actor,oid){
|
||||
S.ledN++;const prev=S.led.length?S.led[S.led.length-1].h:"0000000000000000";
|
||||
const e={t,k,txt,actor,oid,p:prev};e.h=hx(prev+t+k+txt);S.led.push(e);
|
||||
erpFrom(t,k,txt,oid);
|
||||
if(oid&&S.byId[oid]){const o=S.byId[oid];o.ev.push(e);if(o.ev.length>60)o.ev.shift();}
|
||||
if(!S.warm&&S.led.length>900)S.led.splice(0,S.led.length-800);
|
||||
}
|
||||
/* ================= ERPNext link: business documents go to ERPNext through an outbox (names simulated, ERPNext default naming series) ================= */
|
||||
const p5=n=>String(n).padStart(5,"0"),yr=t=>new Date(t).getFullYear();
|
||||
const jcName=key=>"PO-JOB"+p5(10000+fnv(key,2166136261)%89990);
|
||||
const erpO=(o,k)=>({so:"SAL-ORD-",wo:"MFG-WO-",dn:"MAT-DN-",si:"ACC-SINV-"})[k]+yr(o.created)+"-"+p5(o.n);
|
||||
const eNum=(k,base)=>(S.eseq[k]=(S.eseq[k]||base)+1);
|
||||
let STF=null;
|
||||
function ePost(t,dir,dt,name,txt,oid){S.erpN++;S.erp.push({t,dir,dt,name,txt,oid});if(S.erp.length>900)S.erp.splice(0,S.erp.length-600);}
|
||||
/* when a posting left the outbox: within a minute, or after the nightly ERPNext backup window 01:00 to 01:15 (hash-based, never the simulation's random stream) */
|
||||
function eSent(e){if(e.dir==="in")return e.t;const d=new Date(e.t),lag=(15+fnv(e.name+e.t,2166136261)%45)*SEC;
|
||||
if(d.getHours()===1&&d.getMinutes()<15){d.setMinutes(15,0,0);return d.getTime()+lag;}return e.t+lag;}
|
||||
function erpFrom(t,k,txt,oid){
|
||||
if(!STF)STF=Object.fromEntries(STAGES.map(s=>[s.f,s.id]));
|
||||
const o=oid&&S.byId[oid],Y=yr(t);let m;
|
||||
if(o){
|
||||
if(k==="order"&&/ received: /.test(txt)){ePost(t,"in","Sales Order",erpO(o,"so"),`submitted by the order platform: ${fI(o.qty)} × ${fmtS(o)}`,o.id);
|
||||
ePost(t+SEC,"out","Work Order",erpO(o,"wo"),`created from ${erpO(o,"so")}${o.repress?", repress routing, no cut":""}`,o.id);return;}
|
||||
if(k==="order"&&(m=/ → (.+)$/.exec(txt))){const nid=STF[m[1]],h=o.hist,prev=h.length>1?h[h.length-2].id:null;
|
||||
if(prev&&STG_WO[prev]&&prev!=="run")ePost(t,"out","Job Card",jcName(`WO-${o.n}-${prev}`),`completed: ${STAGES[SI[prev]].f}, time logs from the kiosk`,o.id);
|
||||
if(prev==="plate"&&o.mo&&!o.repress)ePost(t,"out","Serial No",o.mo[0].id,`fathers and mothers ${o.fa.map(x=>x.id).join(", ")}, ${o.mo.map(x=>x.id).join(", ")} into warehouse Archive`,o.id);
|
||||
if(nid==="ship"){ePost(t,"out","Delivery Note",erpO(o,"dn"),`${fI(o.good||o.qty)} records, against ${erpO(o,"so")}`,o.id);ePost(t+2*SEC,"out","Sales Invoice",erpO(o,"si"),`from ${erpO(o,"dn")}`,o.id);}
|
||||
return;}
|
||||
if(k==="metal"&&/: lacquers /.test(txt)){ePost(t,"out","Serial No",o.lac[0].id,`lacquers ${o.lac.map(x=>x.id).join(", ")} registered, warehouse Cutting room`,o.id);return;}
|
||||
if(k==="press"&&(m=/^(\S+) set up for /.exec(txt))){ePost(t,"out","Job Card",jcName(`WO-${o.n}-run`),`started on ${m[1]}, stamper set ${o.setIdx+1}`,o.id);return;}
|
||||
if(k==="press"&&(m=/^(\S+) finished /.exec(txt))){ePost(t,"out","Job Card",jcName(`WO-${o.n}-run`),`completed on ${m[1]}: ${fI(o.good)} good, ${fI(o.rej)} rejected`,o.id);
|
||||
ePost(t+SEC,"out","Stock Entry","MAT-STE-"+Y+"-"+p5(eNum("ste",3100)),`Manufacture: ${fI(o.good)} into Finished goods, rejects to regrind`,o.id);return;}
|
||||
if(k==="press"&&(m=/^(\S+) stopped: (.+)$/.exec(txt))){ePost(t,"out","Downtime Entry","DT-"+Y+"-"+p5(eNum("dt",200)),`${m[1]}: ${m[2]}`,o.id);return;}
|
||||
return;}
|
||||
if(k==="lab"&&(m=/^(PVC-\d+) (.*)$/.exec(txt))){const id=m[1],r=m[2],L=S.lots.find(x=>x.id===id);
|
||||
if(/^received/.test(r))ePost(t,"out","Purchase Receipt","MAT-PRE-"+Y+"-"+p5(eNum("pre",400)),`batch ${id}${L?", "+fI(L.kg)+" kg":""}, warehouse Quarantine`,null);
|
||||
else if(/^on hold/.test(r)||/^passed the lab/.test(r)){const qi="MAT-QA-"+Y+"-"+p5(eNum("qa",500));if(L)L.qi=qi;ePost(t,"out","Quality Inspection",qi,/^on hold/.test(r)?`Rejected, batch ${id} held: ${r.replace(/^on hold: /,"")}`:`Accepted, batch ${id}: incoming lab checks`,null);}
|
||||
else if(/^released/.test(r))ePost(t,"out","Stock Entry","MAT-STE-"+Y+"-"+p5(eNum("ste",3100)),`Material Transfer: batch ${id} Quarantine to Stores, released by test pressing`,null);
|
||||
else if(/^into the/.test(r))ePost(t,"out","Stock Entry","MAT-STE-"+Y+"-"+p5(eNum("ste",3100)),`Material Transfer for Manufacture: batch ${id} to the ${/black/.test(r)?"black":"colour"} hopper`,null);
|
||||
}
|
||||
}
|
||||
function erpWO(w){
|
||||
if(w.o)return `Job Card <span class="mono">${jcName(w.id)}</span> of <span class="mono">${erpO(w.o,"wo")}</span>`;
|
||||
if(w.L)return w.L.qi?`Quality Inspection <span class="mono">${w.L.qi}</span>, batch <span class="mono">${w.L.id}</span>`:`Batch <span class="mono">${w.L.id}</span>; the Quality Inspection is posted with the result`;
|
||||
if(w.p)return `Asset Maintenance Log <span class="mono">ACC-AML-${yr(w.start||S.t)}-${p5(1000+fnv(w.id,2166136261)%8999)}</span>`;
|
||||
return "-";}
|
||||
function erpCard(o){const wos=orderWOs(o),dn=wos.filter(w=>w.status==="done").length;
|
||||
return `<dl class="kv"><dt>Customer</dt><dd>${esc(o.artist)}</dd><dt>Sales order</dt><dd><span class="mono">${erpO(o,"so")}</span>, submitted ${fDT(o.created)}</dd><dt>Work order</dt><dd><span class="mono">${erpO(o,"wo")}</span>, ${fI(Math.min(o.good,o.qty))} of ${fI(o.qty)} produced</dd><dt>Job cards</dt><dd>${dn} of ${wos.length} completed</dd><dt>Batches</dt><dd class="mono">${o.lots.length?o.lots.join(", "):"-"}</dd><dt>Delivery</dt><dd>${o.si>=SI.ship?`<span class="mono">${erpO(o,"dn")}</span>, invoice <span class="mono">${erpO(o,"si")}</span>`:'<span class="faint">note and invoice when it ships</span>'}</dd></dl><p class="note"><a href="#erp">ERPNext link</a>: what this order posted. Document names are simulated.</p>`;}
|
||||
function rechain(){S.led.sort((a,b)=>a.t-b.t);if(S.led.length>800)S.led=S.led.slice(-800);let prev=hx("genesis "+(S.ledN-S.led.length));for(const e of S.led){e.p=prev;e.h=hx(prev+e.t+e.k+e.txt);prev=e.h;}}
|
||||
|
||||
/* ================= orders ================= */
|
||||
@@ -700,7 +755,7 @@ function backfill(o,untilId,tUntil,fix){
|
||||
const seq=[];for(let i=0;i<=SI[untilId];i++)if(stageOn(o,STAGES[i]))seq.push(i);
|
||||
const ds=seq.slice(0,-1).map(i=>{const s=STAGES[i];if(fix[s.id]!=null)return fix[s.id]*DAY;const d=s.d||[0.5,1.5];return U(d[0],d[1])*DAY*(s.gate?0.45:1);});
|
||||
let t=tUntil-ds.reduce((a,b)=>a+b,0);
|
||||
o.hist=[];o.ev=[];o.created=t;o.ss=t;
|
||||
o.hist=[];o.ev=[];o.created=t;o.ss=t;S.erp=S.erp.filter(e=>e.oid!==o.id);
|
||||
log(t,"order",`${o.id} received: ${o.title}, ${fI(o.qty)} × ${fmtS(o)}`,"Customer portal",o.id);
|
||||
seq.forEach((i,k)=>{if(k>0&&STAGES[seq[k-1]].gate)approve(o,STAGES[seq[k-1]].gate,t,"Customer portal");enter(o,i,t);if(k<ds.length)t+=ds[k];});
|
||||
}
|
||||
@@ -833,7 +888,7 @@ function feedHTML(list,n){if(!list.length)return '<p class="mute">The presses ar
|
||||
const kpi=(l,v,s,c)=>`<div class="kpi ${c||""}"><span>${l}</span><b>${v}</b><small>${s||" "}</small></div>`;
|
||||
|
||||
/* ================= factory pages ================= */
|
||||
const NAV=[["floor","◉","Floor"],["orders","☰","Orders"],["work","⚒","Work orders"],["quality","✓","Quality"],["lab","⚗","Lab"],["metal","◎","Metal"],["ledger","⛓","Ledger"]];
|
||||
const NAV=[["floor","◉","Floor"],["orders","☰","Orders"],["work","⚒","Work orders"],["quality","✓","Quality"],["lab","⚗","Lab"],["metal","◎","Metal"],["ledger","⛓","Ledger"],["erp","⇄","ERPNext"]];
|
||||
function navHTML(){const cur=S.ui.fpage==="order"?"orders":S.ui.fpage==="press"?"floor":S.ui.fpage==="wo"?"work":S.ui.fpage;
|
||||
const bd={floor:S.presses.filter(p=>p.st==="stop"||p.st==="nopvc").length,orders:cnt(o=>o.si!==SI.done),lab:S.lots.filter(L=>L.st==="hold").length,quality:S.aud.filter(x=>x.a.rev&&x.a.rt<=S.t&&S.t-x.a.rt<6*HR).length};
|
||||
return NAV.map(([k,ic,l])=>{const b=bd[k];const bad=(k==="lab"||k==="floor"||k==="quality")&&b>0;return `<a href="#${k}" class="${cur===k?"on":""}"><span class="ic">${ic}</span>${l}${b?`<span class="bd ${bad?"bad":""}">${fI(b)}</span>`:""}</a>`;}).join("");}
|
||||
@@ -941,6 +996,7 @@ PAGES.order={
|
||||
<section class="card"><div class="ch"><h3>Compound lot</h3><span class="mute">lab gate</span></div>${b.comp}</section>
|
||||
<section class="card"><div class="ch"><h3>Packing and shipping</h3></div>${b.pack}</section>
|
||||
<section class="card" style="grid-column:1/-1"><div class="ch"><h3>Audio comparison, sampled</h3><span class="mute">first-off per stamper set, about every 50th record, every 25th near end of life</span></div>${audioTable(o.audio,false)}</section>
|
||||
<section class="card"><div class="ch"><h3>ERPNext</h3><span class="mute">business documents of this order</span></div>${erpCard(o)}</section>
|
||||
<section class="card"><div class="ch"><h3>Work orders</h3><span class="mute">one per station</span></div><div class="wol">${orderWOs(o).map(w=>`<a href="#wo=${w.id}"><span class="mono mute">${w.id}</span><b>${esc(w.title)}</b><span class="chip ${w.status==="doing"?"g-press":w.status==="done"?"g-out":"g-queue"}">${w.status==="doing"?"in progress":w.status==="done"?"done":"to do"}</span></a>`).join("")}</div></section>
|
||||
<section class="card" style="grid-column:1/-1"><div class="ch"><h3>Events for this order</h3><span class="mute">from the hash-chained log</span></div>${ledgerTable(o.ev.slice(-14).reverse(),false)}</section>`
|
||||
};
|
||||
@@ -1003,6 +1059,27 @@ PAGES.ledger={
|
||||
blocks:()=>({gL:ledgerTable(S.led.slice(-60).reverse(),true)+`<p class="note">Showing the latest 60 of ${fI(S.ledN)} events since the simulation started. The demo uses a short hash; the factory application uses SHA-256.</p>`})
|
||||
};
|
||||
|
||||
const OWN=[["Orders","Sales Order, Delivery Note, Sales Invoice","the three artist gates, the portal view"],["Production","Work Order, a Job Card per station, Downtime Entry","cycles, telemetry, kiosk scans, stamper changes"],["Materials","Batch, Purchase Receipt, Quality Inspection, Stock Entry","instrument files, CoA scans, which lot fed which cycle"],["Metal parts","Serial No in a warehouse: archive, rack, press","genealogy, metrology, play counts, sets per mother"],["Quality","inspection results per batch and run","every record's optical result, audio samples"],["Plant","Asset, maintenance schedule, accounts, payroll","run hours, alarms, the hash-chained log"]];
|
||||
PAGES.erp={
|
||||
layout:()=>`<div class="ph"><div><div class="eyebrow">Factory console</div><h1>ERPNext link</h1></div><p class="mute" style="max-width:600px">The whole plant runs ERPNext. It owns orders, stock, inspections, deliveries and money; this application owns the floor and posts what happens there in business-sized pieces, through an outbox, so no press waits for the ERP.</p></div>
|
||||
<div class="kpis" id="eK"></div>
|
||||
<section class="card"><div class="ch"><h3>Postings and webhooks</h3><span class="mute">latest 40, newest first</span></div><div id="eL"></div></section>
|
||||
<div class="cols2"><section class="card"><div class="ch"><h3>Who owns what</h3></div><div class="tw" id="eO"></div></section>
|
||||
<section class="card"><div class="ch"><h3>Today by document type</h3><span class="mute">since 00:00</span></div><div id="eD"></div></section></div>`,
|
||||
blocks(){
|
||||
const T=S.t,d0=S.day.t0,L=S.erp.filter(e=>e.t<=T).sort((a,b)=>b.t-a.t),today=L.filter(e=>e.t>=d0);
|
||||
const q=L.filter(e=>e.dir==="out"&&eSent(e)>T),inT=today.filter(e=>e.dir==="in").length;
|
||||
const rc=new Date(T);rc.setHours(2,0,0,0);if(rc.getTime()>T)rc.setDate(rc.getDate()-1);
|
||||
const by={};for(const e of today)by[e.dt]=(by[e.dt]||0)+1;const BL=Object.entries(by).sort((a,b)=>b[1]-a[1]),mx=BL.length?BL[0][1]:1;
|
||||
const row=e=>{const qd=e.dir==="out"&&eSent(e)>T;return `<div class="er"><span class="t">${fTs(e.t)}</span><span class="d">${esc(e.dt)}</span><span class="n" title="${esc(e.dt)} ${esc(e.name)}">${esc(e.name)}</span><span class="e">${e.oid?`<a href="#order=${e.oid}">${e.oid}</a> `:""}${esc(e.txt)}</span><span class="s ${qd?"q":e.dir==="in"?"in":""}">${qd?"… outbox":e.dir==="in"?"← webhook":"✓ posted"}</span></div>`;};
|
||||
return {
|
||||
eK:kpi("Posted today",fI(today.filter(e=>e.dir==="out").length),"documents created or updated")+kpi("In the outbox",fI(q.length),q.length?"sent within a minute":"held only in the nightly backup, 01:00 to 01:15",q.length>40?"warn":"")+kpi("Webhooks in today",fI(inT),"sales orders from the order platform")+kpi("Reconciled",fT(rc.getTime()),"work order and batch totals match","ok"),
|
||||
eL:L.length?`<div class="feed"><div class="er hd"><span class="t">Time</span><span class="d">Document type</span><span class="n">Document</span><span class="e">Event</span><span class="s">Status</span></div>${L.slice(0,40).map(row).join("")}</div>`:'<p class="mute">Nothing posted yet.</p>',
|
||||
eO:`<table class="t own"><tr><th></th><th>ERPNext</th><th>This application</th></tr>${OWN.map(([a,b,c])=>`<tr><td>${a}</td><td>${b}</td><td class="mute">${c}</td></tr>`).join("")}</table>`,
|
||||
eD:(BL.length?`<div class="pareto">${BL.map(([r,n])=>`<div><span>${esc(r)}</span>${bar(n/mx,"acc")}<span class="num">${fI(n)}</span></div>`).join("")}</div>`:'<p class="mute">Nothing posted today yet.</p>')+`<p class="note">Postings are per order, run, lot, stop and shipment, never per record or cycle. Document names follow ERPNext's default naming series and are simulated; the real link is the Frappe REST API with an integration user per tool, and a nightly reconciliation of totals. ${fI(S.erpN)} postings since the simulation started.</p>`
|
||||
};
|
||||
}
|
||||
};
|
||||
/* ================= work orders: derived from the same records the stations write ================= */
|
||||
const WST=[["mast","Mastering suite","g-pre"],["cut","Cutting room","g-metal"],["galv","Galvanics","g-metal"],["prep","Stamper prep","g-metal"],["press","Press floor","g-press"],["qc","Audio QC","g-out"],["lab","Material lab","g-pre"],["pack","Packing","g-out"],["ship","Dispatch","g-out"],["maint","Maintenance","g-gate"]];
|
||||
const WSTN=Object.fromEntries(WST.map(x=>[x[0],x]));
|
||||
@@ -1131,7 +1208,7 @@ PAGES.wo={
|
||||
const act=w.L&&w.L.st==="hold"&&w.status==="todo"?`<button type="button" class="btn pri" data-act="redry" data-arg="${w.L.id}">Start re-drying</button>`:"";
|
||||
return {
|
||||
kH:`<div class="ph"><div><div class="eyebrow"><span class="mono">${w.id}</span></div><h1>${esc(w.title)}</h1><div style="margin-top:6px;display:flex;gap:8px;flex-wrap:wrap;align-items:center">${stChip(w.st)}<span class="chip ${w.status==="doing"?"g-press":w.status==="done"?"g-out":"g-pre"}">${w.status==="doing"?"In progress":w.status==="done"?"Done":"Next up"}</span><span class="mute">${esc(woTime(w))}</span></div></div><div class="acts" style="display:flex;gap:8px">${act}${link}</div></div>`,
|
||||
kG:`<section class="card"><div class="ch"><h3>${w.o?"Order":w.L?"Compound lot":"Press"}</h3><span class="mute">${esc(w.ref)}</span></div>${ref}<dl class="kv"><dt>Station</dt><dd>${WSTN[w.st][1]}${w.where?" · "+esc(w.where):""}</dd><dt>Assigned</dt><dd>${esc(w.who||"next free operator")}</dd>${w.start?`<dt>Started</dt><dd>${fDT(w.start)}</dd>`:""}${w.plan&&w.status==="todo"&&!w.prio?`<dt>Planned</dt><dd>${fDT(w.plan)}</dd>`:""}${w.end&&isFinite(w.end)?`<dt>${w.status==="done"?"Done":"Done about"}</dt><dd>${fDT(w.end)}</dd>`:""}</dl>${w.status==="doing"?`<div style="margin-top:12px">${bar(w.pr,"acc")}</div>`:""}<p class="note">Writes to the log: ${esc(WO_LOG[w.st])}.</p></section>
|
||||
kG:`<section class="card"><div class="ch"><h3>${w.o?"Order":w.L?"Compound lot":"Press"}</h3><span class="mute">${esc(w.ref)}</span></div>${ref}<dl class="kv"><dt>Station</dt><dd>${WSTN[w.st][1]}${w.where?" · "+esc(w.where):""}</dd><dt>Assigned</dt><dd>${esc(w.who||"next free operator")}</dd><dt>ERPNext</dt><dd>${erpWO(w)}</dd>${w.start?`<dt>Started</dt><dd>${fDT(w.start)}</dd>`:""}${w.plan&&w.status==="todo"&&!w.prio?`<dt>Planned</dt><dd>${fDT(w.plan)}</dd>`:""}${w.end&&isFinite(w.end)?`<dt>${w.status==="done"?"Done":"Done about"}</dt><dd>${fDT(w.end)}</dd>`:""}</dl>${w.status==="doing"?`<div style="margin-top:12px">${bar(w.pr,"acc")}</div>`:""}<p class="note">Writes to the log: ${esc(WO_LOG[w.st])}.</p></section>
|
||||
<section class="card"><div class="ch"><h3>Steps</h3><span class="mute">kiosk view: badge in, scan, confirm</span></div>${steps}<p class="note">Steps follow the simulation in this demo. At the station each confirmation is an event in the hash-chained log, so the order history and the customer portal update as the work is done.</p></section>`
|
||||
};
|
||||
}
|
||||
@@ -1235,6 +1312,8 @@ function route(){
|
||||
else{S.ui.view="factory";S.ui.fpage=PAGES[k]?k:"floor";S.ui.arg=v;S.ui.fhash=h||"floor";}
|
||||
if(prev!==S.ui.view+S.ui.fpage+S.ui.arg+S.ui.cust)window.scrollTo(0,0);
|
||||
layout();
|
||||
/* phones: the nav is a horizontal strip; bring the current page's tab into view */
|
||||
requestAnimationFrame(()=>{const n=$("#fnav"),a=n&&n.querySelector("a.on");if(a&&n.scrollWidth>n.clientWidth)n.scrollLeft=a.offsetLeft-(n.clientWidth-a.offsetWidth)/2;});
|
||||
}
|
||||
function toast(msg){const t=$("#toast");t.textContent=msg;t.hidden=false;clearTimeout(toast.h);toast.h=setTimeout(()=>{t.hidden=true;},4200);}
|
||||
const ACT={
|
||||
|
||||
Reference in new issue
Block a user