Files
John eead8f5ffd
SAP-ERP Portal CI/CD / build (push) Successful in 3m57s
sale order
2026-10-05 18:45:17 +05:30

1026 lines
64 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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;flex-wrap:wrap;row-gap:6px;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;flex:1;min-width:0}
.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;flex-shrink:0}
.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,.oee-fld select{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 select{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 9px center;padding-right:24px}
.oee-fld input:focus,.oee-fld select: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}
/* ── Small screens ─────────────────────────────────────────────── */
@media (max-width:600px){
main{padding:8px}
nav{padding:0 8px}
.card-bd{padding:10px}
.mp-meta{gap:8px;padding:10px}
.mp-meta .fg{min-width:0;flex:1 1 140px}
.mp-meta-summary{margin-left:0;flex:1 1 100%;text-align:center}
.mp-doc{flex-wrap:wrap;row-gap:8px;padding:10px 12px}
.mp-doc-badge{width:44px;height:44px}
.mp-doc-main{flex:1 1 auto;min-width:0}
.mp-doc-right{flex:0 0 auto}
.mp-doc-title{font-size:13px}
.oee-grid{grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:7px;padding:10px}
.oee-mv{padding:0}
.oee-mw{max-width:100%;height:100%;border-radius:0}
.oee-mb{max-height:calc(100vh - 160px);padding:10px 12px}
.oee-mh{padding:10px 12px;flex-wrap:wrap;row-gap:8px}
.oee-badge{margin-left:0}
.oee-mf{padding:10px 12px;flex-wrap:wrap}
.oee-mf button{flex:1 1 auto}
.oee-mf .grow{margin-left:0}
table.oee-sum th,table.oee-sum td{padding:6px 7px;font-size:11px}
.mp-tabs{gap:4px;padding:4px}
.mp-tab{padding:6px 10px;font-size:10px}
}
</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,
summaryCols:[{k:'machineName',l:'Machine'}],
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:'machineName',l:'Machine Name',type:'machine'},
{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},
]},
{title:'Remarks',color:'#8aa0c0',fields:[
{k:'remarks',l:'Remarks',type:'text',ph:'Optional note...'},
]},
],
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,
summaryCols:[{k:'machineName',l:'Machine'}],
sections:[
{title:'Shift Details (Potential Prod Time)',color:'#8aa0c0',fields:[
{k:'date',l:'Date',type:'date'},
{k:'shift',l:'Shift',type:'text'},
{k:'machineName',l:'Machine Name',type:'machine'},
{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},
]},
{title:'Remarks',color:'#8aa0c0',fields:[
{k:'remarks',l:'Remarks',type:'text',ph:'Optional note...'},
]},
],
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,
summaryCols:[{k:'machineName',l:'Machine'}],
sections:[
{title:'Shift Details (Potential Prod Time)',color:'#8aa0c0',fields:[
{k:'date',l:'Date',type:'date'},
{k:'shift',l:'Shift',type:'text'},
{k:'machineName',l:'Machine Name',type:'machine'},
{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},
]},
{title:'Remarks',color:'#8aa0c0',fields:[
{k:'remarks',l:'Remarks',type:'text',ph:'Optional note...'},
]},
],
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'},{k:'machineName',l:'Machine'}],
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:'machineName',l:'Machine Name',type:'machine'},
{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},
]},
{title:'Remarks',color:'#8aa0c0',fields:[
{k:'remarks',l:'Remarks',type:'text',ph:'Optional note...'},
]},
],
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,
summaryCols:[{k:'machineName',l:'Machine'}],
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:'machineName',l:'Machine Name',type:'machine'},
{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',ph:'Optional note...'},
]},
],
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 _docsTotal=null;
let _filterMonthFrom='',_filterMonthTo='';
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
let _machineNamesByType={}; // Admin → System Settings → OEE → Machine Names, PER TYPE — {tabKey:[names]}; a type absent/empty here stays free text
function machineNamesForTab(key){return _machineNamesByType[key]||[];}
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)}`:'';
const mq=(_filterMonthFrom?`&monthFrom=${_filterMonthFrom}`:'')+(_filterMonthTo?`&monthTo=${_filterMonthTo}`:'');
try{const r=await api('GET',`/oee?top=${_pageSize}&skip=${_page*_pageSize}&company=${encodeURIComponent(_co)}${tq}${mq}`);_docs=r.data||[];_rawCount=_docs.length;_docsTotal=typeof r.total==='number'?r.total:null;}
catch(e){toast(e.message,'err');_docs=[];_rawCount=0;_docsTotal=null;}
_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>
<div class="fg" style="margin:0"><label class="fl">Month</label><div style="display:flex;gap:6px;align-items:center">
<input class="fi" id="f-monthfrom" type="month" value="${esc(_filterMonthFrom)}" style="width:130px"/>
<span style="font-size:11px;color:var(--text3)">to</span>
<input class="fi" id="f-monthto" type="month" value="${esc(_filterMonthTo)}" style="width:130px"/>
</div></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">${_docsTotal!=null?`Page ${_page+1} of ${Math.max(1,Math.ceil(_docsTotal/_pageSize))} · ${_docsTotal} record${_docsTotal===1?'':'s'}`:`Page ${_page+1}`}</span>
<button class="btn b-cancel" id="l-next" ${(_docsTotal!=null?(_page+1)*_pageSize<_docsTotal:_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('#f-monthfrom')&&(q('#f-monthfrom').onchange=function(){_filterMonthFrom=this.value;loadList(true);});
q('#f-monthto')&&(q('#f-monthto').onchange=function(){_filterMonthTo=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==='machine'&&machineNamesForTab(_tab).length){
const cur=String(work[f.k]||'');
const opts=machineNamesForTab(_tab).map(m=>`<option value="${esc(m)}" ${cur===m?'selected':''}>${esc(m)}</option>`).join('');
inp=`<select data-f="${f.k}" ${readOnly?'disabled':''}><option value="">— Select —</option>${opts}</select>`;
}
else if(f.type==='text'||f.type==='machine')inp=`<input data-f="${f.k}" type="text" placeholder="${esc(f.ph||(f.type==='machine'?'Machine name':'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('[data-f]').forEach(el=>{
const handler=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();
};
el.oninput=handler;
if(el.tagName==='SELECT')el.onchange=handler; // <select> (Machine Name dropdown) doesn't fire 'input' in all browsers
});
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;
try{
const cfg=await api('GET','/config');
const map=cfg?.oeeMachineNames&&typeof cfg.oeeMachineNames==='object'?cfg.oeeMachineNames:{};
_machineNamesByType={};
Object.keys(map).forEach(k=>{ _machineNamesByType[k]=String(map[k]||'').split(',').map(s=>s.trim()).filter(Boolean); });
}catch(_e){}
}
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=31"></script>
</body>
</html>