first commit
SAP-ERP Portal CI/CD / build (push) Failing after 5m20s

This commit is contained in:
John
2026-09-23 17:31:02 +05:30
commit 69b4e68baf
51657 changed files with 3864077 additions and 0 deletions
+952
View File
@@ -0,0 +1,952 @@
<!DOCTYPE html>
<html lang="en">
<head>
<script src="/auth-guard.js?v=4"></script>
<script src="/company-list.js"></script>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0"/>
<title>OEE — SAP B1</title>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&family=Bebas+Neue&display=swap" rel="stylesheet"/>
<style>
:root{--ink:#080e18;--ink2:#0d1626;--ink3:#111f35;--purple:#9f5afd;--blue:#1a6fff;--amber:#f5a623;--green:#0dbb8a;--red:#e53e3e;--teal:#06b6d4;--border:#1c2e48;--border2:#243650;--text:#c8d6e8;--text2:#7a9ab8;--text3:#3d5470;--surface:#0f1d30;--surface2:#142338;--surface3:#0b1520;--radius:10px;--font:'Space Grotesk',sans-serif;--mono:'JetBrains Mono',monospace;--display:'Bebas Neue',sans-serif;--shadow:0 8px 32px rgba(0,0,0,.5)}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:var(--font);background:var(--ink);color:var(--text);min-height:100vh;-webkit-font-smoothing:antialiased}
nav{position:sticky;top:0;z-index:200;background:rgba(8,14,24,.92);backdrop-filter:blur(20px);border-bottom:1px solid var(--border);padding:0 12px;height:50px;display:flex;align-items:center;justify-content:space-between;gap:6px}
.nav-logo{background:var(--blue);color:#fff;font-family:var(--display);font-size:15px;letter-spacing:2px;padding:3px 8px;border-radius:5px}
.nav-links{display:flex;gap:2px;overflow-x:auto;flex:1;justify-content:center}
.nav-link{padding:4px 8px;border-radius:5px;font-size:10px;font-weight:600;color:var(--text2);text-decoration:none;white-space:nowrap;font-family:var(--font)}.nav-link:hover{color:var(--text);background:rgba(255,255,255,.05)}.nav-link.active{color:var(--blue);background:rgba(26,111,255,.1)}
.btn-sm{padding:4px 8px;border-radius:5px;font-size:10px;font-weight:700;border:1px solid var(--border2);color:var(--text3);background:none;cursor:pointer;font-family:var(--font)}.btn-sm:hover{border-color:var(--red);color:var(--red)}
main{padding:12px;max-width:1600px;margin:0 auto}
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);margin-bottom:12px;overflow:hidden}
.card-hd{background:var(--surface3);padding:10px 14px;font-size:12px;font-weight:700;color:var(--text);display:flex;align-items:center;gap:8px;border-bottom:1px solid var(--border)}
.card-hd .right{margin-left:auto;font-size:11px;color:var(--text3);font-weight:600}
.card-bd{padding:12px 14px}
.fg{margin-bottom:10px}
.fl{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.4px;color:var(--text3);margin-bottom:4px;display:block}
.fi{width:100%;padding:8px 10px;border:1.5px solid var(--border2);border-radius:7px;font-size:13px;font-family:var(--font);background:var(--surface2);color:var(--text);outline:none;height:36px}.fi:focus{border-color:var(--blue);box-shadow:0 0 0 2px rgba(26,111,255,.15)}.fi::placeholder{color:var(--text3)}
select.fi{cursor:pointer;appearance:none;background:var(--surface2) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%233d5470'/%3E%3C/svg%3E") no-repeat right 10px center;padding-right:26px}
.btn{padding:10px 18px;border-radius:8px;border:none;font-family:var(--font);font-size:13px;font-weight:700;cursor:pointer;transition:all .18s;display:inline-flex;align-items:center;gap:6px;justify-content:center}.btn:disabled{opacity:.4;cursor:not-allowed}
.b-primary{background:var(--blue);color:#fff}.b-primary:hover:not(:disabled){background:#155ad6}
.b-cancel{background:var(--surface2);color:var(--text2);border:1px solid var(--border2)}.b-cancel:hover{border-color:var(--text2)}
.b-danger{background:rgba(229,62,62,.1);color:var(--red);border:1px solid rgba(229,62,62,.3)}.b-danger:hover{background:rgba(229,62,62,.2)}
.empty-msg{text-align:center;padding:24px;color:var(--text3);font-size:12px}
.sp{width:14px;height:14px;border:2px solid rgba(255,255,255,.25);border-top-color:#fff;border-radius:50%;animation:spin .6s linear infinite;display:inline-block}
@keyframes spin{to{transform:rotate(360deg)}}
.toast-c{position:fixed;top:56px;right:10px;z-index:9999;display:flex;flex-direction:column;gap:6px;pointer-events:none}
.toast{padding:9px 13px;border-radius:8px;font-size:12px;font-weight:600;box-shadow:var(--shadow);max-width:320px}
.t-ok{background:var(--green);color:#fff}.t-err{background:var(--red);color:#fff}
.mp-head{align-items:center}
.mp-head-title{display:flex;align-items:center;gap:11px}
.mp-head-ico{width:34px;height:34px;border-radius:9px;background:rgba(26,111,255,.12);border:1px solid rgba(26,111,255,.25);display:flex;align-items:center;justify-content:center;font-size:17px;flex-shrink:0}
.mp-head-t{font-size:13px;font-weight:700;color:var(--text)}
.mp-head-s{font-size:10.5px;color:var(--text3);margin-top:1px;font-weight:400}
.mp-meta{display:flex;gap:14px;flex-wrap:wrap;align-items:flex-end;padding:14px;background:var(--surface3);border:1px solid var(--border);border-radius:10px;margin-bottom:14px}
.mp-meta .fg{margin:0}
.mp-meta-summary{margin-left:auto;align-self:center;font-size:11px;font-weight:600;color:var(--text2);background:var(--surface2);padding:6px 13px;border-radius:20px;border:1px solid var(--border2);white-space:nowrap}
.mp-tabs{display:flex;gap:5px;overflow-x:auto;padding:5px;background:var(--surface3);border:1px solid var(--border);border-radius:11px;margin-bottom:12px}
.mp-tabs::-webkit-scrollbar{height:0}
.mp-tab{padding:8px 13px;font-size:11px;font-weight:700;color:var(--text3);cursor:pointer;white-space:nowrap;border:none;background:none;border-radius:8px;font-family:var(--font);display:flex;align-items:center;gap:7px;transition:all .15s}
.mp-tab:hover{color:var(--text);background:var(--surface2)}
.mp-tab.on{color:#fff;background:var(--blue);box-shadow:0 2px 8px rgba(26,111,255,.35)}
.mp-tab:disabled{opacity:.45;cursor:not-allowed}
.mp-tab-soon{font-size:8px;font-weight:800;color:var(--text3);border:1px solid var(--border2);border-radius:6px;padding:0 4px}
.mp-addrow{margin-top:8px;padding:9px 14px;font-size:12px;font-weight:700;color:var(--blue);background:none;border:1.5px dashed var(--border2);border-radius:9px;cursor:pointer;font-family:var(--font);width:100%;transition:all .15s}
.mp-addrow:hover{border-color:var(--blue);background:rgba(26,111,255,.06)}
.mp-actions{display:flex;gap:8px;margin-top:16px;flex-wrap:wrap;padding-top:14px;border-top:1px solid var(--border)}
.mp-actions .grow{margin-left:auto}
.oee-grid-wrap{overflow-x:auto;border:1px solid var(--border);border-radius:8px}
table.oee{border-collapse:separate;border-spacing:0;font-size:11px;width:100%}
table.oee th{text-align:center;padding:5px 5px;font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.2px;color:var(--text3);background:var(--surface3);border-bottom:1px solid var(--border2);border-right:1px solid var(--border);white-space:normal;line-height:1.18;vertical-align:middle}
table.oee th.oee-band{font-size:10px;letter-spacing:.4px;padding:6px 4px;color:var(--text2)}
table.oee th.oee-sub{font-size:8.5px;color:var(--text2);background:var(--surface2)}
table.oee th.oee-name{font-size:8.5px;font-weight:700;color:var(--text2)}
table.oee th.oee-u2{font-size:8px;font-weight:700;color:var(--text3);text-transform:none;letter-spacing:0;background:var(--ink2);white-space:nowrap;padding:3px 4px}
table.oee th .oee-u{font-size:8px;font-weight:600;color:var(--text3);margin-top:2px;text-transform:none;letter-spacing:0}
table.oee td{padding:3px 4px;border-bottom:1px solid var(--border);border-right:1px solid var(--border);vertical-align:middle}
table.oee tbody tr:nth-child(even) td{background:rgba(255,255,255,.02)}
table.oee tbody tr:hover td{background:rgba(26,111,255,.05)}
table.oee th.oee-idx,table.oee td.oee-idx{width:32px;min-width:32px;text-align:center;color:var(--text3);font-size:10px;font-weight:700;font-family:var(--mono);position:sticky;left:0;background:var(--surface3);z-index:2}
table.oee tbody td.oee-idx{background:var(--ink2)}
table.oee input{width:100%;padding:4px 6px;border:1px solid var(--border2);border-radius:5px;font-size:11px;font-family:var(--mono);background:var(--surface2);color:var(--text);outline:none;height:26px;text-align:right}
table.oee input[type=date],table.oee input[type=text]{text-align:left}
table.oee input:focus{border-color:var(--blue)}
table.oee input.oee-calc{background:var(--ink2);cursor:not-allowed;color:var(--teal);font-weight:600;border-color:var(--border)}
table.oee input.oee-date.oee-missing{border-color:var(--red);box-shadow:0 0 0 2px rgba(229,62,62,.18)}
td.oee-oee input{color:var(--green);font-weight:700}
.oee-rm{width:24px;height:24px;border-radius:5px;border:1px solid var(--border2);background:var(--surface2);color:var(--red);cursor:pointer;font-size:11px}
.oee-rm:hover{background:var(--red);color:#fff;border-color:var(--red)}
.pill{font-size:9px;font-weight:700;padding:1px 6px;border-radius:3px}
.pill-ok{background:rgba(13,187,138,.12);color:var(--green)}
.pill-cx{background:rgba(229,62,62,.12);color:var(--red)}
.pill-lg{font-size:10px;padding:3px 10px;border-radius:20px}
.mp-list{display:flex;flex-direction:column;gap:8px}
.mp-doc{display:flex;align-items:center;gap:14px;padding:12px 14px;background:var(--surface2);border:1px solid var(--border);border-radius:12px;cursor:pointer;transition:all .15s}
.mp-doc:hover{border-color:var(--blue);transform:translateY(-1px);box-shadow:0 8px 22px rgba(0,0,0,.28)}
.mp-doc.cx{opacity:.72}
.mp-doc-badge{display:flex;flex-direction:column;align-items:center;justify-content:center;width:58px;height:58px;border-radius:12px;background:rgba(26,111,255,.10);border:1px solid rgba(26,111,255,.22);flex-shrink:0;text-align:center;padding:4px}
.mp-doc.cx .mp-doc-badge{background:rgba(229,62,62,.08);border-color:rgba(229,62,62,.22)}
.mp-doc-badge-t{font-size:9px;font-weight:800;text-transform:uppercase;letter-spacing:.4px;color:var(--blue);line-height:1.15}
.mp-doc.cx .mp-doc-badge-t{color:var(--red)}
.mp-doc-main{flex:1;min-width:0}
.mp-doc-title{font-size:14px;font-weight:700;color:var(--text)}
.mp-doc-sub{font-size:11px;color:var(--text3);margin-top:4px;display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.mp-doc-sub b{color:var(--text2);font-family:var(--mono)}
.mp-doc-right{display:flex;align-items:center;gap:12px;flex-shrink:0}
.mp-total{font-size:11px;font-weight:600;color:var(--text3);background:var(--surface3);border:1px solid var(--border2);border-radius:20px;padding:5px 12px;white-space:nowrap}
.mp-total b{color:var(--text);font-family:var(--mono)}
.mp-doc-chev{color:var(--text3);font-size:20px;line-height:1;transition:transform .15s,color .15s}
.mp-doc:hover .mp-doc-chev{color:var(--blue);transform:translateX(3px)}
.mp-pager{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:14px}
.mp-count{font-size:11px;font-weight:700;color:var(--blue);background:rgba(26,111,255,.1);padding:3px 11px;border-radius:20px}
.oee-stub{text-align:center;padding:40px 20px;color:var(--text3);font-size:13px;border:1.5px dashed var(--border2);border-radius:10px;background:var(--surface3)}
/* Compact summary table */
table.oee-sum{width:100%;border-collapse:separate;border-spacing:0;font-size:12.5px}
table.oee-sum th{text-align:left;padding:9px 12px;font-size:9.5px;font-weight:700;text-transform:uppercase;letter-spacing:.3px;color:var(--text3);border-bottom:2px solid var(--border2);white-space:nowrap}
table.oee-sum td{padding:9px 12px;border-bottom:1px solid var(--border);white-space:nowrap}
table.oee-sum tbody tr{cursor:pointer}
table.oee-sum tbody tr:hover td{background:rgba(26,111,255,.06)}
table.oee-sum .num{text-align:right;font-family:var(--mono)}
.oee-kpi{font-family:var(--mono);font-weight:700}
.oee-kpi.good{color:var(--green)}.oee-kpi.warn{color:var(--amber)}.oee-kpi.bad{color:var(--red)}
.oee-oeecell{font-family:var(--mono);font-weight:800;padding:2px 9px;border-radius:6px}
.oee-del{width:26px;height:26px;border-radius:6px;border:1px solid var(--border2);background:var(--surface2);color:var(--red);cursor:pointer;font-size:12px}
.oee-del:hover{background:var(--red);color:#fff;border-color:var(--red)}
/* Entry popup */
.oee-mv{position:fixed;inset:0;z-index:9998;background:rgba(4,8,14,.72);backdrop-filter:blur(4px);display:flex;align-items:flex-start;justify-content:center;padding:22px 14px;overflow:auto}
.oee-mw{width:100%;max-width:940px;background:var(--surface);border:1px solid var(--border2);border-radius:14px;box-shadow:var(--shadow);overflow:hidden}
.oee-mh{display:flex;align-items:center;gap:12px;padding:14px 18px;background:var(--surface3);border-bottom:1px solid var(--border)}
.oee-mh-t{font-size:14px;font-weight:700;color:var(--text)}
.oee-mh-s{font-size:11px;color:var(--text3);margin-top:1px}
.oee-badge{margin-left:auto;display:flex;align-items:center;gap:7px;background:rgba(0,176,80,.14);border:1px solid rgba(0,176,80,.45);padding:6px 15px;border-radius:30px}
.oee-badge-l{font-size:10px;font-weight:800;letter-spacing:.5px;color:#0aa457}
.oee-badge-v{font-size:21px;font-weight:800;font-family:var(--mono);color:#0aa457;line-height:1}
.oee-mx{width:30px;height:30px;border-radius:8px;border:1px solid var(--border2);background:none;color:var(--text2);cursor:pointer;font-size:14px}
.oee-mx:hover{border-color:var(--red);color:var(--red)}
.oee-mb{padding:14px 18px;max-height:calc(100vh - 210px);overflow:auto;display:flex;flex-direction:column;gap:12px}
.oee-sec{border:1px solid var(--border);border-radius:11px;overflow:hidden}
.oee-sec-hd{padding:7px 13px;font-size:11px;font-weight:800;letter-spacing:.4px;color:#0a0f18;background:var(--sec,#8aa0c0)}
.oee-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(148px,1fr));gap:9px;padding:12px 13px}
.oee-fld{display:flex;flex-direction:column;gap:4px;min-width:0}
.oee-fld label{font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.3px;color:var(--text3);display:flex;align-items:center;gap:5px;line-height:1.2;min-height:22px}
.oee-fu{font-size:8.5px;color:var(--text2);background:var(--surface2);padding:0 5px;border-radius:4px;text-transform:none;white-space:nowrap}
.oee-fld input{width:100%;height:34px;padding:6px 9px;border:1.5px solid var(--border2);border-radius:7px;font-size:13px;font-family:var(--mono);background:var(--surface2);color:var(--text);outline:none}
.oee-fld input:focus{border-color:var(--blue)}
.oee-fld input.miss{border-color:var(--red);box-shadow:0 0 0 2px rgba(229,62,62,.2)}
.oee-calcbox{height:34px;display:flex;align-items:center;justify-content:flex-end;padding:6px 10px;border-radius:7px;font-family:var(--mono);font-size:13px;font-weight:600;background:var(--ink2);color:var(--teal);border:1px dashed var(--border2)}
.oee-calcbox.hl{background:rgba(0,176,80,.12);color:#0aa457;border-style:solid;border-color:rgba(0,176,80,.4);font-weight:800}
.oee-mf{display:flex;gap:8px;padding:14px 18px;border-top:1px solid var(--border);background:var(--surface3)}
.oee-mf .grow{margin-left:auto}
</style>
<link rel="stylesheet" href="/theme.css?v=3"/>
</head>
<body>
<div class="toast-c" id="toast-c"></div>
<nav>
<span class="nav-logo">SAP</span>
<div class="nav-links">
<a href="/" class="nav-link">Home</a><a href="/production" class="nav-link">Production</a>
<a href="/man-power" class="nav-link">Man Power</a><a href="/oee" class="nav-link active">OEE</a>
</div>
<button class="btn-sm" id="btn-logout">Logout</button>
</nav>
<main id="app"></main>
<script>
'use strict';
let _tok=null,_user=null;
const BE='/api';
(function(){try{_tok=sessionStorage.getItem('portal_token');const u=sessionStorage.getItem('portal_user');if(u)_user=JSON.parse(u);}catch(_e){}})();
if(!_tok)window.location.href='/';
document.getElementById('btn-logout').onclick=()=>{sessionStorage.clear();document.cookie='portal_token=;path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT';document.cookie='portal_user=;path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT';localStorage.removeItem('token');localStorage.removeItem('portal_user');window.location.href='/';};
function hasStepPerm(fullKey,perm){
if(_user&&_user.role==='admin')return true;
const steps=Array.isArray(_user&&_user.approvalSteps)?_user.approvalSteps:[];
for(const s of steps){
if(typeof s==='string'){ if(s===fullKey) return true; }
else if(s&&s.step===fullKey){ return Array.isArray(s.perms)&&s.perms.includes(perm); }
}
return false;
}
const CAN_VIEW=hasStepPerm('oee:entry','view');
const CAN_ADD =hasStepPerm('oee:entry','add');
const CAN_EDIT=hasStepPerm('oee:entry','edit');
// ── OEE form schemas ──────────────────────────────────────────────
// Each form: grouped columns (colored bands like the source sheet) + a
// compute() that derives all 'calc' cells from the typed inputs.
// 'ebb', 'pd', 'sheet', 'moulding' and 'autoclave' are defined; 'needle' and
// 'packing' remain stubs until their source sheets are provided.
// Band fills matched to the source sheet's Excel colours.
const AMBER='#fff200',BLUE='#00b0f0',RED='#ff0000',GREEN='#00b050';
// EBB Production — flat leaf columns (body + units) drive rendering; `thead`
// is a hand-built 3-tier header that mirrors the source sheet exactly.
const OEE_SCHEMA={
ebb:{
ready:true,
leaves:[
{k:'date',type:'date',w:'116px'},
{k:'shift',type:'text',w:'70px'},
{k:'shiftLenHrs',type:'num',w:'54px',u:'Hrs'},
{k:'equMin',type:'calc',w:'62px',u:'Min'},
{k:'goldStd',type:'num',w:'62px',u:'Nos./min'},
{k:'waterTea',type:'num',w:'58px',u:'Min'},
{k:'meal',type:'num',w:'56px',u:'Min'},
{k:'breakdown',type:'num',w:'62px',u:'Min'},
{k:'cleaning',type:'num',w:'58px',u:'Min'},
{k:'changeOver',type:'num',w:'58px',u:'Min'},
{k:'dta',type:'calc',w:'58px',u:'Min'},
{k:'availTime',type:'calc',w:'66px',u:'Min'},
{k:'availability',type:'calc',w:'66px',u:'Min'},
{k:'availRate',type:'calc',w:'66px',u:'%'},
{k:'slowSpeed',type:'num',w:'60px',u:'Min'},
{k:'microStops',type:'num',w:'62px',u:'Min'},
{k:'emptyRun',type:'num',w:'62px',u:'Min'},
{k:'startShut',type:'num',w:'60px',u:'Min'},
{k:'dtp',type:'calc',w:'62px',u:'Min'},
{k:'apt',type:'calc',w:'62px',u:'Min'},
{k:'theoOutput',type:'calc',w:'66px',u:'Nos.'},
{k:'actualOutput',type:'num',w:'62px',u:'Nos.'},
{k:'perfRate',type:'calc',w:'64px',u:'Nos.'},
{k:'perfRate2',type:'calc',w:'64px',u:'%'},
{k:'scrap',type:'num',w:'54px',u:'Nos.'},
{k:'rework',type:'num',w:'56px',u:'Nos.'},
{k:'processLoss',type:'num',w:'62px',u:'Nos.'},
{k:'totalRej',type:'calc',w:'58px',u:'Nos.'},
{k:'netOutput',type:'calc',w:'66px',u:'Nos.'},
{k:'qualityRate',type:'calc',w:'62px',u:'Nos.'},
{k:'qualityRate2',type:'calc',w:'62px',u:'%'},
{k:'oee',type:'calc',w:'66px',u:'%'},
],
// Grouped fields for the entry popup (labels shown there).
sections:[
{title:'Shift Details (Potential Prod Time)',color:'#8aa0c0',fields:[
{k:'date',l:'Date',type:'date'},
{k:'shift',l:'Shift',type:'text'},
{k:'shiftLenHrs',l:'Shift Length (SL)',u:'Hrs',type:'num'},
{k:'equMin',l:'Shift Length',u:'Equ. Min',type:'calc'},
{k:'goldStd',l:'Prod rate (Gold Std)',u:'Nos./min',type:'num'},
]},
{title:'Availability Rating (Target 90%) (Downtime due to…)',color:AMBER,fields:[
{k:'waterTea',l:'Water / Tea Break',u:'Min',type:'num'},
{k:'meal',l:'Meal break',u:'Min',type:'num'},
{k:'breakdown',l:'Breakdown',u:'Min',type:'num'},
{k:'cleaning',l:'Cleaning',u:'Min',type:'num'},
{k:'changeOver',l:'Change over',u:'Min',type:'num'},
{k:'dta',l:'Down time (DTA)',u:'Min',type:'calc'},
{k:'availTime',l:'Available time (AT=SL−DTA)',u:'Min',type:'calc'},
{k:'availability',l:'Availability (AT/SL×100)',u:'Min',type:'calc'},
{k:'availRate',l:'Availability Rate',u:'%',type:'calc',hl:true},
]},
{title:'Performance Rating (Target 95%) (Downtime due to…)',color:BLUE,fields:[
{k:'slowSpeed',l:'Slow speed / High Cycle Time',u:'Min',type:'num'},
{k:'microStops',l:'Micro Stops < 10 min',u:'Min',type:'num'},
{k:'emptyRun',l:'Empty run (mat. shortage)',u:'Min',type:'num'},
{k:'startShut',l:'Start up / shut down',u:'Min',type:'num'},
{k:'dtp',l:'Perf. Down time (DTP)',u:'Min',type:'calc'},
{k:'apt',l:'Actual Prod time (APT=AT−DTP)',u:'Min',type:'calc'},
{k:'theoOutput',l:'Theoretical Output (APT×Gold Std)',u:'Nos.',type:'calc'},
{k:'actualOutput',l:'Actual Output (AO)',u:'Nos.',type:'num'},
{k:'perfRate',l:'Performance Rate',u:'Nos.',type:'calc'},
{k:'perfRate2',l:'Performance Rate',u:'%',type:'calc',hl:true},
]},
{title:'Quality Rating (Target 99.9%) (Rejection due to …)',color:RED,fields:[
{k:'scrap',l:'Scrap',u:'Nos.',type:'num'},
{k:'rework',l:'Rework',u:'Nos.',type:'num'},
{k:'processLoss',l:'Process loss',u:'Nos.',type:'num'},
{k:'totalRej',l:'Total Rej (TR)',u:'Nos.',type:'calc'},
{k:'netOutput',l:'Net output post Rej (AO−TR)',u:'Nos.',type:'calc'},
{k:'qualityRate',l:'Quality Rate',u:'Nos.',type:'calc'},
{k:'qualityRate2',l:'Quality Rate',u:'%',type:'calc',hl:true},
]},
],
thead:`<thead>
<tr>
<th class="oee-idx" rowspan="4">#</th>
<th class="oee-name" rowspan="4">Date</th>
<th class="oee-band oee-sub" colspan="3">Potential Prod Time</th>
<th class="oee-name" rowspan="3">Prod rate (Gold Std)</th>
<th class="oee-band" colspan="9" style="background:${AMBER} !important;color:#0a0f18 !important">Availability Rating (Target 90%) (Downtime due to…)</th>
<th class="oee-band" colspan="10" style="background:${BLUE} !important;color:#0a0f18 !important">Performance Rating (Target 95%) (Downtime due to…)</th>
<th class="oee-band" colspan="7" style="background:${RED} !important;color:#fff !important">Quality Rating (Target 99.9%) (Rejection due to …)</th>
<th class="oee-band" colspan="1" style="background:${GREEN} !important;color:#0a0f18 !important">OEE</th>
<th rowspan="4"></th>
</tr>
<tr>
<th class="oee-name" rowspan="3">Shift</th>
<th class="oee-sub" colspan="2" rowspan="2">Shift Length (SL)</th>
<th class="oee-sub" colspan="5">Downtime due to…</th>
<th class="oee-name" rowspan="2">Down time (DTA)</th>
<th class="oee-name" rowspan="2">Available time (AT = SL−DTA)</th>
<th class="oee-name" rowspan="2">Availability (AT/SL×100)</th>
<th class="oee-name" rowspan="2">Availability Rate</th>
<th class="oee-sub" colspan="4">Downtime due to…</th>
<th class="oee-name" rowspan="2">Performance Down time (DTP)</th>
<th class="oee-name" rowspan="2">Actual Prod time (APT = AT−DTP)</th>
<th class="oee-name" rowspan="2">Theoretical Output (APT×Gold Std)</th>
<th class="oee-name" rowspan="2">Actual Output (AO)</th>
<th class="oee-name" rowspan="2">Performance Rate</th>
<th class="oee-name" rowspan="2">Performance Rate</th>
<th class="oee-sub" colspan="3">Rejection due to …</th>
<th class="oee-name" rowspan="2">Total Rej (TR)</th>
<th class="oee-name" rowspan="2">Net output post Rej (AO−TR)</th>
<th class="oee-name" rowspan="2">Quality Rate</th>
<th class="oee-name" rowspan="2">Quality Rate</th>
<th class="oee-name" rowspan="2">A×P×Q (Target 85%)</th>
</tr>
<tr>
<th class="oee-name">Water / Tea Break</th>
<th class="oee-name">Meal break</th>
<th class="oee-name">Breakdown</th>
<th class="oee-name">Cleaning</th>
<th class="oee-name">Change over</th>
<th class="oee-name">Slow speed / High Cycle Time</th>
<th class="oee-name">Micro Stops &lt; 10 min (Utility failure etc)</th>
<th class="oee-name">Empty run (Shortage of material etc)</th>
<th class="oee-name">Start up / shut down</th>
<th class="oee-name">Scrap</th>
<th class="oee-name">Rework</th>
<th class="oee-name">Process loss</th>
</tr>
<tr>
<th class="oee-u2">Hrs</th><th class="oee-u2">Equ. Min</th><th class="oee-u2">Nos./min</th>
<th class="oee-u2">Min</th><th class="oee-u2">Min</th><th class="oee-u2">Min</th><th class="oee-u2">Min</th><th class="oee-u2">Min</th>
<th class="oee-u2">Min</th><th class="oee-u2">Min</th><th class="oee-u2">Min</th><th class="oee-u2">%</th>
<th class="oee-u2">Min</th><th class="oee-u2">Min</th><th class="oee-u2">Min</th><th class="oee-u2">Min</th>
<th class="oee-u2">Min</th><th class="oee-u2">Min</th><th class="oee-u2">Nos.</th><th class="oee-u2">Nos.</th><th class="oee-u2">Nos.</th><th class="oee-u2">%</th>
<th class="oee-u2">Nos.</th><th class="oee-u2">Nos.</th><th class="oee-u2">Nos.</th>
<th class="oee-u2">Nos.</th><th class="oee-u2">Nos.</th><th class="oee-u2">Nos.</th><th class="oee-u2">%</th>
<th class="oee-u2">%</th>
</tr>
</thead>`,
compute:function(r){
const n=k=>{const v=parseFloat(r[k]);return isNaN(v)?0:v;};
const dta=n('waterTea')+n('meal')+n('breakdown')+n('cleaning')+n('changeOver');
const slMin=n('shiftLenHrs')*60; // Shift Length in equivalent minutes
const at=slMin-dta;
const avail=slMin>0?at/slMin*100:0;
const dtp=n('slowSpeed')+n('microStops')+n('emptyRun')+n('startShut');
const apt=at-dtp;
const theo=apt*n('goldStd');
const ao=n('actualOutput');
const perf=theo>0?ao/theo*100:0;
const tr=n('scrap')+n('rework')+n('processLoss');
const net=ao-tr;
const qual=ao>0?net/ao*100:0;
const oee=avail*perf*qual/10000;
return {equMin:slMin,dta,availTime:at,availability:avail,availRate:avail,dtp,apt,theoOutput:theo,perfRate:perf,perfRate2:perf,totalRej:tr,netOutput:net,qualityRate:qual,qualityRate2:qual,oee};
},
},
// PD Production — identical shape/fields/formula to EBB Production (same
// sections, same downtime reasons, same compute()); just its own tab so PD
// shift data is stored separately from EBB's.
pd:{
ready:true,
sections:[
{title:'Shift Details (Potential Prod Time)',color:'#8aa0c0',fields:[
{k:'date',l:'Date',type:'date'},
{k:'shift',l:'Shift',type:'text'},
{k:'shiftLenHrs',l:'Shift Length (SL)',u:'Hrs',type:'num'},
{k:'equMin',l:'Shift Length',u:'Equ. Min',type:'calc'},
{k:'goldStd',l:'Prod rate (Gold Std)',u:'Nos./min',type:'num'},
]},
{title:'Availability Rating (Target 90%) (Downtime due to…)',color:AMBER,fields:[
{k:'waterTea',l:'Water / Tea Break',u:'Min',type:'num'},
{k:'meal',l:'Meal break',u:'Min',type:'num'},
{k:'breakdown',l:'Breakdown',u:'Min',type:'num'},
{k:'cleaning',l:'Cleaning',u:'Min',type:'num'},
{k:'changeOver',l:'Change over',u:'Min',type:'num'},
{k:'dta',l:'Down time (DTA)',u:'Min',type:'calc'},
{k:'availTime',l:'Available time (AT=SL−DTA)',u:'Min',type:'calc'},
{k:'availability',l:'Availability (AT/SL×100)',u:'Min',type:'calc'},
{k:'availRate',l:'Availability Rate',u:'%',type:'calc',hl:true},
]},
{title:'Performance Rating (Target 95%) (Downtime due to…)',color:BLUE,fields:[
{k:'slowSpeed',l:'Slow speed / High Cycle Time',u:'Min',type:'num'},
{k:'microStops',l:'Micro Stops < 10 min',u:'Min',type:'num'},
{k:'emptyRun',l:'Empty run (mat. shortage)',u:'Min',type:'num'},
{k:'startShut',l:'Start up / shut down',u:'Min',type:'num'},
{k:'dtp',l:'Perf. Down time (DTP)',u:'Min',type:'calc'},
{k:'apt',l:'Actual Prod time (APT=AT−DTP)',u:'Min',type:'calc'},
{k:'theoOutput',l:'Theoretical Output (APT×Gold Std)',u:'Nos.',type:'calc'},
{k:'actualOutput',l:'Actual Output (AO)',u:'Nos.',type:'num'},
{k:'perfRate',l:'Performance Rate',u:'Nos.',type:'calc'},
{k:'perfRate2',l:'Performance Rate',u:'%',type:'calc',hl:true},
]},
{title:'Quality Rating (Target 99.9%) (Rejection due to …)',color:RED,fields:[
{k:'scrap',l:'Scrap',u:'Nos.',type:'num'},
{k:'rework',l:'Rework',u:'Nos.',type:'num'},
{k:'processLoss',l:'Process loss',u:'Nos.',type:'num'},
{k:'totalRej',l:'Total Rej (TR)',u:'Nos.',type:'calc'},
{k:'netOutput',l:'Net output post Rej (AO−TR)',u:'Nos.',type:'calc'},
{k:'qualityRate',l:'Quality Rate',u:'Nos.',type:'calc'},
{k:'qualityRate2',l:'Quality Rate',u:'%',type:'calc',hl:true},
]},
],
compute:function(r){
const n=k=>{const v=parseFloat(r[k]);return isNaN(v)?0:v;};
const dta=n('waterTea')+n('meal')+n('breakdown')+n('cleaning')+n('changeOver');
const slMin=n('shiftLenHrs')*60; // Shift Length in equivalent minutes
const at=slMin-dta;
const avail=slMin>0?at/slMin*100:0;
const dtp=n('slowSpeed')+n('microStops')+n('emptyRun')+n('startShut');
const apt=at-dtp;
const theo=apt*n('goldStd');
const ao=n('actualOutput');
const perf=theo>0?ao/theo*100:0;
const tr=n('scrap')+n('rework')+n('processLoss');
const net=ao-tr;
const qual=ao>0?net/ao*100:0;
const oee=avail*perf*qual/10000;
return {equMin:slMin,dta,availTime:at,availability:avail,availRate:avail,dtp,apt,theoOutput:theo,perfRate:perf,perfRate2:perf,totalRej:tr,netOutput:net,qualityRate:qual,qualityRate2:qual,oee};
},
},
// Plastic Sheet Plant — same shape as EBB; differs in the Performance
// downtime reasons (Head Clean / Other Problem), "Rejection" in place of
// "Scrap", and output units measured in Nos. or Kg.
sheet:{
ready:true,
sections:[
{title:'Shift Details (Potential Prod Time)',color:'#8aa0c0',fields:[
{k:'date',l:'Date',type:'date'},
{k:'shift',l:'Shift',type:'text'},
{k:'shiftLenHrs',l:'Shift Length (SL)',u:'Hrs',type:'num'},
{k:'equMin',l:'Shift Length',u:'Equ. Min',type:'calc'},
{k:'goldStd',l:'Prod rate (Gold Std)',u:'Nos. or Kg/min',type:'num'},
]},
{title:'Availability Rating (Target 90%) (Downtime due to…)',color:AMBER,fields:[
{k:'waterTea',l:'Water / Tea Break',u:'Min',type:'num'},
{k:'meal',l:'Meal break',u:'Min',type:'num'},
{k:'breakdown',l:'Breakdown',u:'Min',type:'num'},
{k:'cleaning',l:'Cleaning',u:'Min',type:'num'},
{k:'changeOver',l:'Change over',u:'Min',type:'num'},
{k:'dta',l:'Down time (DTA)',u:'Min',type:'calc'},
{k:'availTime',l:'Available time (AT=SL−DTA)',u:'Min',type:'calc'},
{k:'availability',l:'Availability (AT/SL×100)',u:'Min',type:'calc'},
{k:'availRate',l:'Availability Rate',u:'%',type:'calc',hl:true},
]},
{title:'Performance Rating (Target 95%) (Downtime due to…)',color:BLUE,fields:[
{k:'slowSpeed',l:'Slow speed / High Cycle Time',u:'Min',type:'num'},
{k:'headClean',l:'Head Clean',u:'Min',type:'num'},
{k:'otherProblem',l:'Other Problem',u:'Min',type:'num'},
{k:'mcStartShut',l:'M/C Start up / shut down',u:'Min',type:'num'},
{k:'dtp',l:'Perf. Down time (DTP)',u:'Min',type:'calc'},
{k:'apt',l:'Actual Prod time (APT=AT−DTP)',u:'Min',type:'calc'},
{k:'theoOutput',l:'Theoretical Output (APT×Gold Std)',u:'Nos. or Kg',type:'calc'},
{k:'actualOutput',l:'Actual Output (AO)',u:'Nos. or Kg',type:'num'},
{k:'perfRate',l:'Performance Rate',u:'Nos. or Kg',type:'calc'},
{k:'perfRate2',l:'Performance Rate',u:'%',type:'calc',hl:true},
]},
{title:'Quality Rating (Target 99.9%) (Rejection due to …)',color:RED,fields:[
{k:'rejection',l:'Rejection',u:'Nos. or Kg',type:'num'},
{k:'rework',l:'Rework',u:'Nos. or Kg',type:'num'},
{k:'processLoss',l:'Process loss',u:'Nos. or Kg',type:'num'},
{k:'totalRej',l:'Total Rej (TR)',u:'Nos. or Kg',type:'calc'},
{k:'netOutput',l:'Net output post Rej (AO−TR)',u:'Nos. or Kg',type:'calc'},
{k:'qualityRate',l:'Quality Rate',u:'Nos. or Kg',type:'calc'},
{k:'qualityRate2',l:'Quality Rate',u:'%',type:'calc',hl:true},
]},
],
compute:function(r){
const n=k=>{const v=parseFloat(r[k]);return isNaN(v)?0:v;};
const dta=n('waterTea')+n('meal')+n('breakdown')+n('cleaning')+n('changeOver');
const slMin=n('shiftLenHrs')*60;
const at=slMin-dta;
const avail=slMin>0?at/slMin*100:0;
const dtp=n('slowSpeed')+n('headClean')+n('otherProblem')+n('mcStartShut');
const apt=at-dtp;
const theo=apt*n('goldStd');
const ao=n('actualOutput');
const perf=theo>0?ao/theo*100:0;
const tr=n('rejection')+n('rework')+n('processLoss');
const net=ao-tr;
const qual=ao>0?net/ao*100:0;
const oee=avail*perf*qual/10000;
return {equMin:slMin,dta,availTime:at,availability:avail,availRate:avail,dtp,apt,theoOutput:theo,perfRate:perf,perfRate2:perf,totalRej:tr,netOutput:net,qualityRate:qual,qualityRate2:qual,oee};
},
},
// Moulding — like EBB but with a Component Name field and Performance
// reasons Power Cut/Slow Speed, Head Clean, Nozzle Clean, M/C Start-up.
moulding:{
ready:true,
summaryCols:[{k:'componentName',l:'Component'}],
sections:[
{title:'Shift Details',color:'#8aa0c0',fields:[
{k:'date',l:'Date',type:'date'},
{k:'componentName',l:'Component Name',type:'text'},
{k:'shift',l:'Shift',type:'text'},
{k:'shiftLenHrs',l:'Shift Length (SL)',u:'Hrs',type:'num'},
{k:'equMin',l:'Shift Length',u:'Equ. Min',type:'calc'},
{k:'goldStd',l:'Prod rate (Gold Std)',u:'Nos./min',type:'num'},
]},
{title:'Availability Rating (Target 90%)',color:AMBER,fields:[
{k:'waterTea',l:'Water / Tea Break',u:'Min',type:'num'},
{k:'meal',l:'Meal break',u:'Min',type:'num'},
{k:'breakdown',l:'Breakdown',u:'Min',type:'num'},
{k:'cleaning',l:'Cleaning',u:'Min',type:'num'},
{k:'changeOver',l:'Change over',u:'Min',type:'num'},
{k:'dta',l:'Down time (DTA)',u:'Min',type:'calc'},
{k:'availTime',l:'Available time (AT=SL−DTA)',u:'Min',type:'calc'},
{k:'availability',l:'Availability (AT/SL×100)',u:'Min',type:'calc'},
{k:'availRate',l:'Availability Rate',u:'%',type:'calc',hl:true},
]},
{title:'Performance Rating (Target 95%)',color:BLUE,fields:[
{k:'powerCutSlow',l:'Power Cut / Slow Speed Time',u:'Min',type:'num'},
{k:'headClean',l:'Head Clean',u:'Min',type:'num'},
{k:'nozzleClean',l:'Nozzle Clean',u:'Min',type:'num'},
{k:'mcStartShut',l:'M/C Start up / shut down',u:'Min',type:'num'},
{k:'dtp',l:'Perf. Down time (DTP)',u:'Min',type:'calc'},
{k:'apt',l:'Actual Prod time (APT=AT−DTP)',u:'Min',type:'calc'},
{k:'theoOutput',l:'Theoretical Output (APT×Gold Std)',u:'Nos.',type:'calc'},
{k:'actualOutput',l:'Actual Output (AO)',u:'Nos.',type:'num'},
{k:'perfRate',l:'Performance Rate',u:'Nos. or Kg',type:'calc'},
{k:'perfRate2',l:'Performance Rate',u:'%',type:'calc',hl:true},
]},
{title:'Quality Rating (Target 99.9%)',color:RED,fields:[
{k:'scrap',l:'Scrap',u:'Nos.',type:'num'},
{k:'rework',l:'Rework',u:'Nos. or Kg',type:'num'},
{k:'processLoss',l:'Process loss',u:'Kg',type:'num'},
{k:'totalRej',l:'Total Rej (TR)',u:'Nos.',type:'calc'},
{k:'netOutput',l:'Net output post Rej (AO−TR)',u:'Nos.',type:'calc'},
{k:'qualityRate',l:'Quality Rate',u:'Nos. or Kg',type:'calc'},
{k:'qualityRate2',l:'Quality Rate',u:'%',type:'calc',hl:true},
]},
],
compute:function(r){
const n=k=>{const v=parseFloat(r[k]);return isNaN(v)?0:v;};
const dta=n('waterTea')+n('meal')+n('breakdown')+n('cleaning')+n('changeOver');
const slMin=n('shiftLenHrs')*60;
const at=slMin-dta;
const avail=slMin>0?at/slMin*100:0;
const dtp=n('powerCutSlow')+n('headClean')+n('nozzleClean')+n('mcStartShut');
const apt=at-dtp;
const theo=apt*n('goldStd');
const ao=n('actualOutput');
const perf=theo>0?ao/theo*100:0;
const tr=n('scrap')+n('rework')+n('processLoss');
const net=ao-tr;
const qual=ao>0?net/ao*100:0;
const oee=avail*perf*qual/10000;
return {equMin:slMin,dta,availTime:at,availability:avail,availRate:avail,dtp,apt,theoOutput:theo,perfRate:perf,perfRate2:perf,totalRej:tr,netOutput:net,qualityRate:qual,qualityRate2:qual,oee};
},
},
// Autoclave / Sterilizer — availability-only, cycle-based. No Performance or
// Quality rating blocks. OEE = running cycle time ÷ potential time × 100.
autoclave:{
ready:true,
kpiCols:[{k:'availRate',l:'Avail %',t:90}],
sections:[
{title:'Shift Details',color:'#8aa0c0',fields:[
{k:'date',l:'Date',type:'date'},
{k:'shift',l:'Shift',type:'text'},
{k:'shiftLenHrs',l:'Shift Length (SL)',u:'Hrs',type:'num'},
{k:'equMin',l:'Shift Length',u:'Equ. Min',type:'calc'},
{k:'cycleRun',l:'Total Cycle Run per day',u:"No's",type:'num'},
{k:'loadUnload',l:'Loading / Unloading time',u:'Min',type:'num'},
{k:'goldStd',l:'Prod rate (Gold Std)',u:'Nos./min',type:'num'},
]},
{title:'Availability Rating (Target 90%)',color:AMBER,fields:[
{k:'boiler',l:'Boiler breakdown',u:'Min',type:'num'},
{k:'waterSys',l:'Water system breakdown',u:'Min',type:'num'},
{k:'bopt',l:'BOPT breakdown',u:'Min',type:'num'},
{k:'coolingTower',l:'Cooling tower breakdown',u:'Min',type:'num'},
{k:'others',l:'Others',u:'Min',type:'num'},
{k:'dta',l:'Total Down time analysis (DTA)',u:'Min',type:'calc'},
{k:'totalCycleTime',l:'Total per cycle time per day',u:'Min',type:'calc'},
{k:'runningCycle',l:'TOTAL running cycle time',u:'Min',type:'calc'},
{k:'availRate',l:'Availability (AT/SL×100)',u:'%',type:'calc',hl:true},
{k:'oee',l:'OEE (Target 85%)',u:'%',type:'calc',hl:true},
]},
{title:'Remarks',color:'#8aa0c0',fields:[
{k:'remarks',l:'Remarks',type:'text'},
]},
],
compute:function(r){
const n=k=>{const v=parseFloat(r[k]);return isNaN(v)?0:v;};
const dta=n('boiler')+n('waterSys')+n('bopt')+n('coolingTower')+n('others');
const slMin=n('shiftLenHrs')*60;
const at=slMin-dta; // Available time
const totalCycleTime=n('loadUnload')*n('cycleRun');
const runningCycle=at-totalCycleTime;
const avail=slMin>0?at/slMin*100:0;
const oee=slMin>0?runningCycle/slMin*100:0;
return {equMin:slMin,dta,totalCycleTime,runningCycle,availRate:avail,availability:avail,oee};
},
},
};
// Tab list comes from the server (/oee/tabs) — key, label, ready.
let TABS=[];
function schemaFor(key){return OEE_SCHEMA[key]||null;}
function isReady(key){const s=schemaFor(key);return !!(s&&s.ready);}
function allCols(key){const s=schemaFor(key);if(!s)return [];return s.leaves||(s.sections?s.sections.reduce((a,x)=>a.concat(x.fields),[]):[]);}
function inputCols(key){return allCols(key).filter(c=>c.type!=='calc');}
function labelFor(key){const t=TABS.find(t=>t.key===key);return t?t.label:key;}
function num(v){const n=parseFloat(v);return isNaN(n)?0:n;}
function blankRow(key){const r={};allCols(key).forEach(c=>{if(c.type!=='calc')r[c.k]=c.type==='num'?0:'';});return r;}
function rowHasData(key,r){
return inputCols(key).some(c=>{
if(c.type==='num')return num(r[c.k])!==0;
return (r[c.k]||'').toString().trim()!=='';
});
}
// Format a computed value for display.
function fmtCalc(col,val){
if(val==null||isNaN(val))return '';
if(col.u==='%')return (Math.round(val*100)/100).toFixed(2);
const rounded=Math.round(val*1000)/1000;
return String(rounded);
}
let _co=window.__DEFAULT_COMPANY__;
let _companies=[{value:_co,label:_co}];
function coOptions(sel){return _companies.map(c=>`<option value="${esc(c.value)}" ${sel===c.value?'selected':''}>${esc(c.label)}</option>`).join('');}
let _mode='list'; // 'list' | 'form'
let _docs=[];let _listLoading=false;let _filterTab='';let _page=0,_pageSize=30,_rawCount=0;
let _editId=null; // doc id when editing, null for new
let _tab='ebb';let _month='';let _remark='';let _rows=[];
let _canceled=false;let _saving=false;let _formLoading=false;
let _allowedKeys=null; // null = all; else array of allowed tab keys
function esc(v){return(v||'').toString().replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');}
function q(s){return document.querySelector(s);}
function mk(c){const d=document.createElement('div');d.className=c;return d;}
function today(){return new Date().toISOString().slice(0,10);}
function thisMonth(){return new Date().toISOString().slice(0,7);}
const _MON=['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
function fmtMonth(m){if(!m)return '—';const p=String(m).split('-');if(p.length<2)return m;const mo=parseInt(p[1],10);return `${_MON[mo-1]||p[1]} ${p[0]}`;}
function visibleTabs(){return TABS.filter(t=>!_allowedKeys||_allowedKeys.includes(t.key));}
async function api(m,p,b=null){
const h={'Content-Type':'application/json'};if(_tok)h['Authorization']='Bearer '+_tok;
const o={method:m.toUpperCase(),headers:h};if(b)o.body=JSON.stringify(b);
let r,d;try{r=await fetch(BE+p,o);d=await r.json();}catch(_e){throw new Error('Cannot reach server');}
if(r.status===401){window.location.href='/';throw new Error('Session expired');}
if(!r.ok)throw new Error(d?.message||'HTTP '+r.status);return d;
}
function toast(msg,type='ok'){const tc=document.getElementById('toast-c');const el=document.createElement('div');el.className=`toast t-${type}`;el.textContent=(type==='ok'?'OK ':'ERR ')+msg;tc.appendChild(el);setTimeout(()=>el.remove(),4500);}
// ── LIST ──────────────────────────────────────────────
async function loadList(resetPage){
if(resetPage)_page=0;
_listLoading=true;render();
const tq=_filterTab?`&tab=${encodeURIComponent(_filterTab)}`:'';
try{const r=await api('GET',`/oee?top=${_pageSize}&skip=${_page*_pageSize}&company=${encodeURIComponent(_co)}${tq}`);_docs=r.data||[];_rawCount=_docs.length;}
catch(e){toast(e.message,'err');_docs=[];_rawCount=0;}
_listLoading=false;render();
}
function newEntry(){
_editId=null;_tab=(visibleTabs()[0]||TABS[0]||{key:'ebb'}).key;_month=thisMonth();_remark='';_canceled=false;
_rows=[];
_mode='form';render();
}
async function openEntry(id){
_mode='form';_formLoading=true;_editId=id;render();
try{
const r=await api('GET',`/oee/${id}?company=${encodeURIComponent(_co)}`);
const d=r.data||{};
_tab=d.tab;_month=(d.month||'').slice(0,7);_remark=d.remark||'';_canceled=!!d.canceled;
_rows=Array.isArray(d.rows)?d.rows:[];
}catch(e){toast(e.message,'err');_mode='list';}
_formLoading=false;render();
}
async function save(){
if(!_month){toast('Month is required','err');return;}
const editing=_editId!=null;
if(editing&&!CAN_EDIT){toast('You are not permitted to edit','err');return;}
if(!editing&&!CAN_ADD){toast('You are not permitted to add','err');return;}
if(!isReady(_tab)){toast('This OEE form is not available yet','err');return;}
// Every row with data needs a Date.
let firstBad=null,badCount=0;
_rows.forEach((r,i)=>{if(rowHasData(_tab,r)&&!(r.date||'').toString().trim()){badCount++;if(firstBad==null)firstBad=i;}});
if(firstBad!=null){render();toast(`Date is required in all filled rows — ${badCount} missing (row ${firstBad+1}).`,'err');return;}
const used=_rows.filter(r=>rowHasData(_tab,r)&&(r.date||'').toString().trim());
if(!used.length){toast('Add at least one row with a Date before saving.','err');return;}
// Merge computed values into each saved row.
const schema=schemaFor(_tab);
const outRows=used.map(r=>({...r,...(schema.compute?schema.compute(r):{})}));
_saving=true;render();
const body={company:_co,tab:_tab,month:_month,remark:_remark,rows:outRows};
try{
if(editing){await api('PATCH',`/oee/${_editId}`,body);toast('OEE data updated');}
else{await api('POST','/oee',body);toast('OEE data saved');}
_mode='list';_saving=false;loadList(true);
}catch(e){toast(e.message||'Failed','err');_saving=false;render();}
}
async function cancelDoc(){
if(_editId==null||!CAN_EDIT)return;
if(!confirm('Cancel this OEE document? This cannot be undone.'))return;
_saving=true;render();
try{await api('POST',`/oee/${_editId}/cancel`,{company:_co});toast('Document cancelled');_mode='list';_saving=false;loadList(true);}
catch(e){toast(e.message||'Failed','err');_saving=false;render();}
}
// ── RENDER ────────────────────────────────────────────
function render(){try{_doRender();}catch(e){console.error(e);document.getElementById('app').innerHTML=`<div style="padding:20px;color:var(--red)">Error: ${e.message}</div>`;}}
function _doRender(){
const app=document.getElementById('app');app.innerHTML='';
if(!CAN_VIEW){app.innerHTML='<div class="card"><div class="card-bd" style="text-align:center;padding:30px;color:var(--text3)">You are not assigned the "OEE" permission. Ask an admin to grant it (User Management → Approval Steps → Overall Equipment Efficiency).</div></div>';return;}
if(_mode==='form')renderForm(app);else renderList(app);
}
function renderList(app){
const c=mk('card');
const docsHtml=_docs.map(d=>{
const lbl=labelFor(d.tab);
return `<div class="mp-doc${d.canceled?' cx':''}" data-open="${d.id}">
<div class="mp-doc-badge"><div class="mp-doc-badge-t">${esc(lbl)}</div></div>
<div class="mp-doc-main">
<div class="mp-doc-title">${esc(fmtMonth(d.month))}</div>
<div class="mp-doc-sub">
<span>${esc(lbl)}</span><span>·</span>
<span>${d.createdBy?'By <b>'+esc(d.createdBy)+'</b>':''}</span>
<span>${d.canceled?'<span class="pill pill-cx pill-lg">CANCELLED</span>':'<span class="pill pill-ok pill-lg">ACTIVE</span>'}</span>
</div>
</div>
<div class="mp-doc-right">${d.rowCount?`<span class="mp-total"><b>${d.rowCount}</b> row${d.rowCount===1?'':'s'}</span>`:''}<span class="mp-doc-chev">›</span></div>
</div>`;
}).join('');
const tabOpts=`<option value="">All forms</option>`+visibleTabs().map(t=>`<option value="${esc(t.key)}" ${_filterTab===t.key?'selected':''}>${esc(t.label)}</option>`).join('');
c.innerHTML=`<div class="card-hd"><span>Overall Equipment Efficiency</span><span class="right">${_listLoading?'':`${_docs.length} on this page`}</span></div>
<div class="card-bd">
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:flex-end;margin-bottom:14px">
<div class="fg" style="margin:0;min-width:150px"><label class="fl">Company</label><select class="fi" id="sel-co">${coOptions(_co)}</select></div>
<div class="fg" style="margin:0;min-width:170px"><label class="fl">Filter by Form</label><select class="fi" id="f-tab">${tabOpts}</select></div>
${CAN_ADD?`<button class="btn b-primary" id="btn-new" style="height:36px;margin-left:auto">+ New Entry</button>`:''}
</div>
${_listLoading?`<div class="empty-msg"><span class="sp" style="border-color:rgba(26,111,255,.25);border-top-color:var(--blue)"></span> Loading…</div>`:
!_docs.length?`<div class="empty-msg">No OEE records${_filterTab?' for '+esc(labelFor(_filterTab)):''}.</div>`:
`<div class="mp-list">${docsHtml}</div>
<div class="mp-pager">
<button class="btn b-cancel" id="l-prev" ${_page===0?'disabled':''} style="padding:6px 14px;font-size:11px">‹ Prev</button>
<span class="mp-count">Page ${_page+1}</span>
<button class="btn b-cancel" id="l-next" ${_rawCount>=_pageSize?'':'disabled'} style="padding:6px 14px;font-size:11px">Next ›</button>
</div>`}
</div>`;
app.appendChild(c);
setTimeout(()=>{
q('#sel-co')&&(q('#sel-co').onchange=function(){_co=this.value;loadList(true);});
q('#f-tab')&&(q('#f-tab').onchange=function(){_filterTab=this.value;loadList(true);});
q('#btn-new')&&(q('#btn-new').onclick=newEntry);
q('#l-prev')&&(q('#l-prev').onclick=()=>{if(_page>0){_page--;loadList();}});
q('#l-next')&&(q('#l-next').onclick=()=>{if(_rawCount>=_pageSize){_page++;loadList();}});
document.querySelectorAll('[data-open]').forEach(tr=>tr.onclick=()=>openEntry(parseInt(tr.dataset.open)));
},0);
}
function switchTab(key){
if(_editId!=null)return; // form is fixed while editing
if(key===_tab)return;
_tab=key;_rows=[];render();
}
// ── Summary KPI cells ─────────────────────────────────
function pct1(v){return (v==null||!isFinite(v))?'—':(Math.round(v*10)/10).toFixed(1);}
function kpi(v,target){
if(v==null||!isFinite(v))return '<span class="oee-kpi">—</span>';
const cls=v>=target?'good':(v>=target*0.9?'warn':'bad');
return `<span class="oee-kpi ${cls}">${pct1(v)}</span>`;
}
function oeeCell(v){
if(v==null||!isFinite(v))return '—';
const g=v>=85,w=v>=70;
const bg=g?'rgba(0,176,80,.15)':w?'rgba(245,166,35,.15)':'rgba(229,62,62,.15)';
const col=g?'#0aa457':w?'#b47500':'#c81e1e';
return `<span class="oee-oeecell" style="background:${bg};color:${col}">${pct1(v)}</span>`;
}
function defaultRowDate(){return _month?`${_month}-01`:today();}
const DEFAULT_KPIS=[{k:'availRate',l:'Avail %',t:90},{k:'perfRate2',l:'Perf %',t:95},{k:'qualityRate2',l:'Qual %',t:99.9}];
function kpiColsFor(key){const s=schemaFor(key);return (s&&s.kpiCols)||DEFAULT_KPIS;}
// ── Single-shift entry popup ──────────────────────────
function openRowModal(index,readOnly){
const s=schemaFor(_tab);if(!s||!s.sections)return;
const editing=index!=null&&index>=0;
const work=editing?JSON.parse(JSON.stringify(_rows[index])):blankRow(_tab);
if(!editing&&!(work.date||'').trim())work.date=defaultRowDate();
const fieldByKey={};s.sections.forEach(sec=>sec.fields.forEach(f=>fieldByKey[f.k]=f));
const fldHtml=(f)=>{
const label=`<label>${esc(f.l)}${f.u?`<span class="oee-fu">${esc(f.u)}</span>`:''}</label>`;
if(f.type==='calc')return `<div class="oee-fld">${label}<div class="oee-calcbox${f.hl?' hl':''}" data-calc="${f.k}">—</div></div>`;
let inp;
if(f.type==='date')inp=`<input data-f="${f.k}" type="date" value="${esc(String(work[f.k]||''))}" ${readOnly?'disabled':''}/>`;
else if(f.type==='text')inp=`<input data-f="${f.k}" type="text" placeholder="e.g. A / B / C" value="${esc(String(work[f.k]||''))}" ${readOnly?'disabled':''}/>`;
else inp=`<input data-f="${f.k}" type="number" step="any" value="${esc(String(work[f.k]!=null?work[f.k]:0))}" ${readOnly?'disabled':''}/>`;
return `<div class="oee-fld">${label}${inp}</div>`;
};
const ov=document.createElement('div');ov.className='oee-mv';
ov.innerHTML=`<div class="oee-mw">
<div class="oee-mh">
<div>
<div class="oee-mh-t">${editing?'Edit Shift':'Add Shift'}</div>
<div class="oee-mh-s">${esc(labelFor(_tab))} · ${esc(fmtMonth(_month))}</div>
</div>
<div class="oee-badge" title="Live OEE = Availability × Performance × Quality"><span class="oee-badge-l">OEE</span><span class="oee-badge-v" data-calc="oee">0</span><span class="oee-badge-l">%</span></div>
<button class="oee-mx" data-x title="Close">✕</button>
</div>
<div class="oee-mb">
${s.sections.map(sec=>`<div class="oee-sec" style="--sec:${sec.color}">
<div class="oee-sec-hd">${esc(sec.title)}</div>
<div class="oee-grid">${sec.fields.map(fldHtml).join('')}</div>
</div>`).join('')}
</div>
<div class="oee-mf">
${readOnly?'':`<button class="btn b-primary" data-save>${editing?'Update Shift':'Add Shift'}</button>`}
<button class="btn b-cancel${readOnly?'':' grow'}" data-cancel>${readOnly?'Close':'Cancel'}</button>
</div>
</div>`;
document.body.appendChild(ov);
const recompute=()=>{
const comp=s.compute(work);
ov.querySelectorAll('[data-calc]').forEach(el=>{
const k=el.dataset.calc;const f=k==='oee'?{u:'%'}:(fieldByKey[k]||{});
const v=fmtCalc(f,comp[k]);el.textContent=(v===''?'0':v);
});
};
ov.querySelectorAll('input[data-f]').forEach(el=>{
el.oninput=function(){
const k=this.dataset.f;const f=fieldByKey[k];
work[k]=f&&f.type==='num'?(parseFloat(this.value)||0):this.value;
if(k==='date')this.classList.remove('miss');
recompute();
};
});
recompute();
const close=()=>ov.remove();
ov.querySelector('[data-x]').onclick=close;
ov.querySelector('[data-cancel]').onclick=close;
const saveBtn=ov.querySelector('[data-save]');
if(saveBtn)saveBtn.onclick=()=>{
if(!(work.date||'').toString().trim()){
const de=ov.querySelector('input[data-f="date"]');if(de){de.classList.add('miss');de.focus();}
toast('Date is required for this shift','err');return;
}
if(editing)_rows[index]=work;else _rows.push(work);
close();render();
};
}
function renderForm(app){
const editing=_editId!=null;
const readOnly=editing?!CAN_EDIT:!CAN_ADD;
const ready=isReady(_tab);
const c=mk('card');
const filled=_rows.filter(r=>rowHasData(_tab,r)).length;
let body;
if(!ready){
body=`<div class="oee-stub">The <strong>${esc(labelFor(_tab))}</strong> format has not been set up yet.<br/>It will be added once the sheet is provided.</div>`;
}else{
const extra=schemaFor(_tab).summaryCols||[]; // optional per-form extra columns
const kpis=kpiColsFor(_tab); // which rate KPIs to show
const rowsHtml=_rows.map((row,i)=>{
const cc=schemaFor(_tab).compute(row);
const extraTd=extra.map(x=>`<td>${esc(row[x.k]||'—')}</td>`).join('');
const kpiTd=kpis.map(kc=>`<td class="num">${kpi(cc[kc.k],kc.t)}</td>`).join('');
return `<tr data-edit="${i}">
<td>${i+1}</td>
<td>${esc(row.date||'—')}</td>
${extraTd}
<td>${esc(row.shift||'—')}</td>
<td class="num">${row.shiftLenHrs||0}</td>
${kpiTd}
<td class="num">${oeeCell(cc.oee)}</td>
${readOnly?'':`<td class="num"><button class="oee-del" data-del="${i}" title="Remove shift">✕</button></td>`}
</tr>`;
}).join('');
const extraTh=extra.map(x=>`<th>${esc(x.l)}</th>`).join('');
const kpiTh=kpis.map(kc=>`<th class="num">${esc(kc.l)}</th>`).join('');
const cols=5+extra.length+kpis.length+(readOnly?0:1);
body=`<div class="oee-grid-wrap"><table class="oee-sum">
<thead><tr><th>#</th><th>Date</th>${extraTh}<th>Shift</th><th class="num">SL (Hrs)</th>${kpiTh}<th class="num">OEE %</th>${readOnly?'':'<th></th>'}</tr></thead>
<tbody>${_rows.length?rowsHtml:`<tr><td colspan="${cols}" style="text-align:center;color:var(--text3);padding:24px">No shifts added yet.${readOnly?'':' Click <strong>“+ Add Shift”</strong> to enter one.'}</td></tr>`}</tbody>
</table></div>
${readOnly?'':`<button type="button" class="mp-addrow" id="btn-addrow">+ Add Shift</button>`}`;
}
c.innerHTML=`<div class="card-hd mp-head">
<div class="mp-head-title">
<span class="mp-head-ico">📈</span>
<div>
<div class="mp-head-t">${editing?'OEE Data':'New OEE Entry'}${_canceled?' <span class="pill pill-cx" style="margin-left:4px">CANCELLED</span>':''}</div>
<div class="mp-head-s">${editing?`Document #${_editId} · ${esc(labelFor(_tab))}`:'Record Overall Equipment Efficiency for a production area'}</div>
</div>
</div>
<button class="btn b-cancel" id="btn-back" style="padding:6px 14px;font-size:11px">← Back to list</button>
</div>
<div class="card-bd">
${_formLoading?`<div class="empty-msg"><span class="sp" style="border-color:rgba(26,111,255,.25);border-top-color:var(--blue)"></span> Loading…</div>`:`
<div class="mp-tabs">${visibleTabs().map(t=>{
const lock=editing&&t.key!==_tab;
return `<button class="mp-tab${_tab===t.key?' on':''}" data-tab="${t.key}" ${lock?'disabled':''} title="${lock?'Form is fixed while editing':''}">${esc(t.label)}${isReady(t.key)?'':`<span class="mp-tab-soon">SOON</span>`}</button>`;
}).join('')}</div>
<div class="mp-meta">
<div class="fg"><label class="fl">Month <span style="color:var(--red)">*</span></label><input class="fi" id="inp-month" type="month" value="${esc(_month)}" ${readOnly?'readonly':''} style="min-width:160px"/></div>
<div class="fg" style="flex:1;min-width:220px"><label class="fl">Remark</label><input class="fi" id="inp-remark" value="${esc(_remark)}" placeholder="Optional note for this sheet" ${readOnly?'readonly':''}/></div>
<div class="mp-meta-summary">${filled} shift${filled===1?'':'s'} entered</div>
</div>
${body}
<div class="mp-actions">
${(readOnly||!ready)?'':`<button class="btn b-primary" id="btn-save" ${_saving?'disabled':''}><span id="sp-save" style="display:${_saving?'inline-block':'none'}" class="sp"></span> ${editing?'Save Changes':'Save Entry'}</button>`}
${editing&&CAN_EDIT&&!_canceled?`<button class="btn b-danger" id="btn-cancel-doc" ${_saving?'disabled':''}>Cancel Document</button>`:''}
<button class="btn b-cancel${(readOnly||!ready)?'':' grow'}" id="btn-discard">${readOnly?'Close':'Discard'}</button>
</div>
`}
</div>`;
app.appendChild(c);
setTimeout(()=>{
q('#btn-back')&&(q('#btn-back').onclick=()=>{_mode='list';render();});
q('#btn-discard')&&(q('#btn-discard').onclick=()=>{_mode='list';render();});
if(_formLoading)return;
document.querySelectorAll('.mp-tab').forEach(b=>b.onclick=function(){if(!this.disabled)switchTab(this.dataset.tab);});
q('#inp-month')&&!readOnly&&(q('#inp-month').onchange=function(){_month=this.value;});
q('#inp-remark')&&!readOnly&&(q('#inp-remark').oninput=function(){_remark=this.value;});
if(!ready)return;
// Click a summary row → open the popup (edit, or view when read-only).
document.querySelectorAll('tr[data-edit]').forEach(tr=>tr.onclick=function(e){
if(e.target.closest('[data-del]'))return;
openRowModal(parseInt(this.dataset.edit),readOnly);
});
document.querySelectorAll('[data-del]').forEach(el=>el.onclick=function(e){
e.stopPropagation();_rows.splice(parseInt(this.dataset.del),1);render();
});
q('#btn-addrow')&&(q('#btn-addrow').onclick=()=>openRowModal(null,false));
q('#btn-save')&&(q('#btn-save').onclick=save);
q('#btn-cancel-doc')&&(q('#btn-cancel-doc').onclick=cancelDoc);
},0);
}
async function loadTabs(){
try{const r=await api('GET','/oee/tabs');TABS=(r.data||[]).map(t=>({key:String(t.key),label:t.label||String(t.key),ready:!!t.ready}));}
catch(_e){TABS=[{key:'ebb',label:'EBB Production',ready:true}];}
}
async function loadConfig(){
// Per-user form permission (Admin → user → OEE). Empty = all forms.
let userKeys=null;
try{const pr=await api('GET','/auth/profile');const ut=pr?.user?.oeeTabs;if(Array.isArray(ut)&&ut.length)userKeys=ut.map(String);}catch(_e){}
_allowedKeys=userKeys;
if(!visibleTabs().some(t=>t.key===_tab))_tab=(visibleTabs()[0]||TABS[0]||{key:'ebb'}).key;
}
async function init(){
try{
try{const list=await window.fetchCompanyList();if(list.length)_companies=list;}catch(_e){}
if(CAN_VIEW){ await Promise.all([loadTabs(),loadConfig()]); loadList(); } else render();
}catch(e){console.error('Init error:',e);render();}
}
init();
</script>
<script src="/sidebar.js?v=25"></script>
</body>
</html>