sale order
SAP-ERP Portal CI/CD / build (push) Successful in 3m57s

This commit is contained in:
John
2026-10-05 18:45:17 +05:30
parent e725a6571b
commit eead8f5ffd
129 changed files with 14252 additions and 452 deletions
+560
View File
@@ -0,0 +1,560 @@
<!DOCTYPE html>
<html lang="en">
<head>
<script src="/auth-guard.js?v=4"></script>
<script src="/company-list.js"></script>
<script src="https://cdn.jsdelivr.net/npm/exceljs@4.4.0/dist/exceljs.min.js"></script>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0"/>
<title>Downtime Analysis — 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;--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}
main{padding:12px;max-width:1700px;margin:0 auto}
.page-head{display:flex;align-items:center;gap:10px;margin-bottom:12px}
.page-icon{width:34px;height:34px;border-radius:9px;background:rgba(245,166,35,.14);color:var(--amber);display:flex;align-items:center;justify-content:center}
.page-title{font-size:16px;font-weight:700}
.page-sub{font-size:11px;color:var(--text3)}
.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;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)}
.card-bd{padding:12px 14px}
.fl{font-size:9.5px;font-weight:700;text-transform:uppercase;letter-spacing:.4px;color:var(--text3);margin-bottom:3px;display:block}
.fi{width:100%;padding:7px 9px;border:1.5px solid var(--border2);border-radius:7px;font-size:12.5px;font-family:var(--font);background:var(--surface2);color:var(--text);outline:none;height:34px}
.fi:focus{border-color:var(--amber)}
select.fi{cursor:pointer}
.filters{display:flex;gap:8px;flex-wrap:wrap;align-items:flex-end}
.filters>div{min-width:140px}
.btn{padding:8px 15px;border-radius:8px;border:none;font-family:var(--font);font-size:12px;font-weight:700;cursor:pointer;height:34px}
.b-primary{background:var(--amber);color:#1a1200}
.b-primary:disabled{opacity:.5;cursor:not-allowed}
.b-cancel{background:var(--surface2);color:var(--text2);border:1px solid var(--border2)}
.lst{width:100%;border-collapse:separate;border-spacing:0;font-size:11.5px}
.lst th{text-align:left;padding:7px 9px;font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.3px;color:var(--text3);border-bottom:2px solid var(--border2);white-space:nowrap;background:var(--surface3);position:sticky;top:0}
.lst td{padding:6px 9px;border-bottom:1px solid var(--border);vertical-align:top;white-space:nowrap}
.lst td.num,.lst th.num{text-align:right;font-family:var(--mono)}
.lst tr.sec td{background:var(--surface3);font-weight:700;color:var(--text2)}
.lst tr.hi td{color:#fca5a5}
.lst td.hi-cell{background:rgba(229,62,62,.14);color:#fca5a5;font-weight:700}
.lst tbody tr:hover td{background:rgba(245,166,35,.05)}
.mono{font-family:var(--mono)}
.empty-msg{text-align:center;padding:26px;color:var(--text3);font-size:12px}
.sp{width:14px;height:14px;border:2px solid rgba(245,166,35,.25);border-top-color:var(--amber);border-radius:50%;animation:spin .6s linear infinite;display:inline-block}
@keyframes spin{to{transform:rotate(360deg)}}
.toast-c{position:fixed;top:14px;right:10px;z-index:9999;display:flex;flex-direction:column;gap:6px}
.toast{padding:9px 13px;border-radius:8px;font-size:12px;font-weight:600;box-shadow:var(--shadow)}
.t-err{background:var(--red);color:#fff}
.t-ok{background:var(--green);color:#fff}
.tbl-wrap{overflow-x:auto;max-height:70vh;overflow-y:auto;border-radius:8px;border:1px solid var(--border2)}
.note{font-size:11px;color:var(--text3);margin-top:8px;line-height:1.5}
</style>
<link rel="stylesheet" href="/theme.css?v=3"/>
</head>
<body>
<div class="toast-c" id="toast-c"></div>
<main id="app"></main>
<script>
'use strict';
let _tok=null;const BE='/api';
try{_tok=sessionStorage.getItem('portal_token');}catch(_e){}
if(!_tok)location.href='/';
function esc(v){return(v==null?'':String(v)).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');}
function q(s){return document.querySelector(s);}
function toast(m,kind){const tc=q('#toast-c');const e=document.createElement('div');e.className='toast '+(kind==='ok'?'t-ok':'t-err');e.textContent=m;tc.appendChild(e);setTimeout(()=>e.remove(),4500);}
function num0(v){if(v==null)return'';const n=Number(v);if(isNaN(n))return'';return Math.round(n).toLocaleString('en-IN');}
function pct(v){if(v==null||!isFinite(v))return'—';return v.toFixed(2)+'%';}
async function api(p,method,body){
const headers={Authorization:'Bearer '+_tok};
const opts={method:method||'GET',headers};
if(body!=null){headers['Content-Type']='application/json';opts.body=JSON.stringify(body);}
const r=await fetch(BE+p,opts);
if(r.status===401){location.href='/';throw new Error('Session expired');}
const d=await r.json();
if(!r.ok||d.success===false)throw new Error(d.message||('HTTP '+r.status));
return d;
}
let _co=window.__DEFAULT_COMPANY__||'';
let _companies=[];
let _loading=false,_ran=false,_report=null,_monthly=null,_detailByTab=null;
const _f={periodFrom:'',periodTo:'',baseDays:''};
function coOptions(sel){return _companies.map(c=>`<option value="${esc(c.value)}" ${sel===c.value?'selected':''}>${esc(c.label)}</option>`).join('');}
async function loadConfig(){
try{
const r=await api('/settings');
const cfg=JSON.parse(r.data?.downtimeAnalysisConfig||'{}');
if(!_f.periodFrom)_f.periodFrom=cfg.periodFrom||'';
if(!_f.periodTo)_f.periodTo=cfg.periodTo||'';
if(!_f.baseDays)_f.baseDays=cfg.baseDays||'';
}catch(_e){ /* non-admin users can't read /settings — fall back to blank, user fills in manually */ }
}
async function runReport(){
if(!_f.periodFrom||!_f.periodTo){toast('Period From/To are required');return;}
_loading=true;_ran=true;render();
try{
const qs=new URLSearchParams();
qs.set('company',_co);qs.set('periodFrom',_f.periodFrom);qs.set('periodTo',_f.periodTo);
if(_f.baseDays)qs.set('baseDays',_f.baseDays);
const r=await api('/downtime-analysis/report?'+qs.toString());
_report=r.data;
const dr=await api('/downtime-analysis/detail-by-tab?company='+encodeURIComponent(_co)+'&periodFrom='+_f.periodFrom+'&periodTo='+_f.periodTo);
_detailByTab=dr.data;
const mr=await api('/downtime-analysis/monthly?company='+encodeURIComponent(_co)+'&periodFrom='+_f.periodFrom+'&periodTo='+_f.periodTo);
_monthly=mr.data;
}catch(e){toast(e.message,'err');_report=null;_detailByTab=null;_monthly=null;}
_loading=false;render();
}
function fmtMin0(v){return v==null?'':Math.round(v).toLocaleString('en-IN');}
function monthlySummaryHtml(mo){
if(!mo||!mo.rows.length)return `<div class="empty-msg">No OEE entries found for this period.</div>`;
const rows=mo.rows.map(r=>`<tr>
<td class="num">${r.no}</td>
<td>${esc(r.monthLabel)}</td>
<td class="num">${fmtMin0(r.downMin)}</td>
<td class="num">${fmtMin0(r.runMin)}</td>
<td class="num">${fmtMin0(r.netRunMin)}</td>
<td class="num">${r.pctDown.toFixed(0)}%</td>
</tr>`).join('');
return `<div style="display:flex;gap:18px;align-items:flex-start;flex-wrap:wrap">
<div class="tbl-wrap" style="flex:1;min-width:420px">
<table class="lst">
<thead><tr><th>S.No</th><th>Month</th><th class="num">Total Down Time<br><span style="font-weight:400">MIN</span></th><th class="num">Total M/C Run Time<br><span style="font-weight:400">MIN</span></th><th class="num" style="color:var(--red)">Total Run Time − Total Down Time<br><span style="font-weight:400">MIN</span></th><th class="num">% Of Down Time</th></tr></thead>
<tbody>${rows}
<tr style="background:rgba(245,166,35,.12);font-weight:700">
<td></td><td>TOTAL</td>
<td class="num">${fmtMin0(mo.total.downMin)}</td>
<td class="num">${fmtMin0(mo.total.runMin)}</td>
<td class="num">${fmtMin0(mo.total.netRunMin)}</td>
<td class="num">${mo.total.pctDown.toFixed(0)}%</td>
</tr>
</tbody>
</table>
</div>
<div style="width:110px;height:110px;border-radius:50%;background:var(--red);color:#fff;display:flex;align-items:center;justify-content:center;font-family:var(--display);font-size:32px;flex-shrink:0;margin-top:30px">${mo.total.pctDown.toFixed(0)}</div>
</div>`;
}
// Per-tab shift-level detail table — one row per OEE shift entry, matching
// the Master Cause Sheet's per-form layout: Date/Shift/Shift Length/Equ.Min/
// Gold Std, one column per cause, TOTAL DOWN TIME, % OF DOWN TIME. The TOTAL
// row's % column is the SUM of every row's own % (not a recomputed ratio) —
// matches the screenshot's arithmetic exactly.
function detailByTabCardsHtml(dbt){
const tabKeys=Object.keys(dbt.tabs||{});
if(!tabKeys.length)return '';
return tabKeys.map(tab=>{
const t=dbt.tabs[tab];
if(!t.rows.length)return '';
const causeHead=dbt.causes.map(c=>`<th class="num">${esc(c)}</th>`).join('');
const rowsHtml=t.rows.map(r=>`<tr>
<td>${esc(r.date)}</td>
<td>${esc(r.shift)}</td>
<td class="num">${r.shiftLenHrs}</td>
<td class="num">${num0(r.equMin)}</td>
<td class="num">${r.goldStd||''}</td>
${dbt.causes.map(c=>`<td class="num">${num0(r.causeMins[c]||0)}</td>`).join('')}
<td class="num">${num0(r.totalDownMin)}</td>
<td class="num">${r.pctDown.toFixed(0)}%</td>
</tr>`).join('');
return `<div class="card">
<div class="card-hd"><span>Downtime Analysis — ${esc(t.tabLabel)}</span><span class="right">${t.rows.length} shift(s)</span></div>
<div class="card-bd">
<div class="tbl-wrap"><table class="lst">
<thead><tr>
<th>Date</th><th>Shift</th><th class="num">Shift Length<br>(Hrs)</th><th class="num">Equ. Min</th><th class="num">Prod Rate<br>(Gold Std)</th>
${causeHead}
<th class="num">Total Down<br>Time (Min)</th><th class="num">% Of<br>Down Time</th>
</tr></thead>
<tbody>${rowsHtml}
<tr style="background:rgba(245,166,35,.12);font-weight:700">
<td colspan="3">TOTAL</td>
<td class="num">${num0(t.totals.equMin)}</td>
<td></td>
${dbt.causes.map(c=>`<td class="num">${num0(t.rows.reduce((s,r)=>s+(r.causeMins[c]||0),0))}</td>`).join('')}
<td class="num">${num0(t.totals.totalDownMin)}</td>
<td class="num">${t.totals.pctDownSum.toFixed(0)}%</td>
</tr>
</tbody>
</table></div>
</div>
</div>`;
}).join('');
}
// Highlight rule: a cell is flagged if its % value is at or above 70% of
// that row's highest value among the machine columns (NET excluded from the
// threshold calc, but still highlighted if it also clears it) — a simple,
// row-relative "stands out" rule rather than a fixed universal cutoff, since
// what counts as "high" genuinely differs per cause.
function rowThreshold(machines,getter){
const vals=machines.map(getter).filter(v=>isFinite(v));
if(!vals.length)return Infinity;
return Math.max(...vals)*0.7;
}
// Shared by both the Machine-wise and OEE-Form-wise summary tables — `groups`
// is the list of per-group metric objects (machines, or tabs), `net` the
// combined column, `labelOf` how to read each column's header text.
function summaryTableHtml(causes,groups,net,labelOf){
const cols=[...groups,net];
const causeRows=causes.map(c=>({label:c,get:g=>g.causeTotals[c]||0,getPct:g=>g.causePcts[c]||0}));
const rowsHtml=(label,cells)=>`<tr><td>${esc(label)}</td>${cells}</tr>`;
const headCols=cols.map(g=>`<th class="num">${esc(labelOf(g))}</th>`).join('');
let body='';
body+=`<tr class="sec"><td colspan="${cols.length+1}">1. BASE (WORKING) DAYS USED</td></tr>`;
body+=rowsHtml('Base Days',cols.map(g=>`<td class="num">${num0(g.baseDays)}</td>`).join(''));
body+=rowsHtml('Base Days × 1440 (min)',cols.map(g=>`<td class="num">${num0(g.baseMin)}</td>`).join(''));
body+=`<tr class="sec"><td colspan="${cols.length+1}">2. DOWNTIME BY CAUSE (min / %) — from OEE entries</td></tr>`;
causeRows.forEach(cr=>{
body+=rowsHtml(cr.label,cols.map(g=>`<td class="num">${num0(cr.get(g))}</td>`).join(''));
});
causeRows.forEach(cr=>{
const th=rowThreshold(groups,cr.getPct);
body+=rowsHtml('% '+cr.label,cols.map(g=>{
const v=cr.getPct(g);
return `<td class="num${v>=th?' hi-cell':''}">${pct(v)}</td>`;
}).join(''));
});
body+=`<tr class="sec"><td colspan="${cols.length+1}">3. ACTUAL ACTIVE DAYS</td></tr>`;
body+=rowsHtml('Actual Active Days',cols.map(g=>`<td class="num">${num0(g.actualActiveDays)}</td>`).join(''));
body+=`<tr class="sec"><td colspan="${cols.length+1}">4. TOTAL SHIFT TIME (Scheduled, incl. downtime)</td></tr>`;
body+=rowsHtml('Total Shift Time (min)',cols.map(g=>`<td class="num">${num0(g.totalShiftMin)}</td>`).join(''));
body+=`<tr class="sec"><td colspan="${cols.length+1}">5. DOWNTIME DUE TO PLANT INOPERATION</td></tr>`;
body+=rowsHtml('Minutes (Base Days×1440 − Total Shift Time)',cols.map(g=>`<td class="num">${num0(g.plantInoperationMin)}</td>`).join(''));
body+=rowsHtml('%',cols.map(g=>`<td class="num">${pct(g.plantInoperationPct)}</td>`).join(''));
body+=`<tr class="sec"><td colspan="${cols.length+1}">6. NET DOWNTIME % (Causes Only)</td></tr>`;
body+=rowsHtml('Net Downtime %',cols.map(g=>`<td class="num">${pct(g.netDowntimePct)}</td>`).join(''));
body+=rowsHtml('Total % (Causes + Plant Inoperative)',cols.map(g=>`<td class="num">${pct(g.totalPct)}</td>`).join(''));
body+=rowsHtml('Remainder: Actual Run %',cols.map(g=>`<td class="num">${pct(g.remainderPct)}</td>`).join(''));
return `<div class="tbl-wrap"><table class="lst"><thead><tr><th>Metric</th>${headCols}</tr></thead><tbody>${body}</tbody></table></div>`;
}
function render(){
const app=q('#app');
app.innerHTML=`
<div class="page-head">
<div class="page-icon">⏱</div>
<div><div class="page-title">Downtime Analysis</div><div class="page-sub">Machine-wise downtime by cause, aggregated from OEE entries — matches the Master Cause Sheet layout</div></div>
</div>
<div class="card">
<div class="card-hd"><span>Selection Criteria</span></div>
<div class="card-bd">
<div class="filters">
<div style="min-width:180px"><label class="fl">Company</label><select class="fi" id="sel-co">${coOptions(_co)}</select></div>
<div><label class="fl">Period From</label><input class="fi" id="f-from" type="date" value="${esc(_f.periodFrom)}"/></div>
<div><label class="fl">Period To</label><input class="fi" id="f-to" type="date" value="${esc(_f.periodTo)}"/></div>
<div style="min-width:110px"><label class="fl">Base Days</label><input class="fi fi-mono" id="f-basedays" type="number" min="1" value="${esc(_f.baseDays)}" placeholder="330"/></div>
<div><button class="btn b-primary" id="btn-run" ${_loading?'disabled':''}>${_loading?'Running…':'Run Report'}</button></div>
</div>
<div class="note">Base Days = the common working-day calendar for this period (set once in Admin → System Settings → Downtime Analysis, or override here). A machine whose own OEE entries only start partway through the period gets this prorated down automatically.</div>
</div>
</div>
<div class="card">
<div class="card-hd"><span>Downtime Analysis — All Machines</span>${_ran&&!_loading&&_report?`<span class="right">${_report.machines.length} machine(s)</span>`:''}
${_ran&&!_loading&&_report?`<button class="btn b-cancel" id="btn-export" style="margin-left:10px">⬇ Export Excel</button>`:''}
</div>
<div class="card-bd">
${_loading?`<div class="empty-msg"><span class="sp"></span> Running report…</div>`
:!_ran?`<div class="empty-msg">Set the period above and click "Run Report".</div>`
:!_report||!_report.machines.length?`<div class="empty-msg">No OEE entries with a Machine Name found for this period.</div>`
:summaryTableHtml(_report.causes,_report.machines,_report.net,g=>g.machine)}
</div>
</div>
${_ran&&!_loading&&_monthly?`<div class="card">
<div class="card-hd"><span>Monthly Downtime Summary — All Machines</span></div>
<div class="card-bd">${monthlySummaryHtml(_monthly)}</div>
</div>`:''}
${_ran&&!_loading&&_detailByTab?detailByTabCardsHtml(_detailByTab):''}`;
wire();
}
function wire(){
q('#sel-co').onchange=function(){_co=this.value;};
q('#f-from').onchange=function(){_f.periodFrom=this.value;};
q('#f-to').onchange=function(){_f.periodTo=this.value;};
q('#f-basedays').onchange=function(){_f.baseDays=this.value;};
q('#btn-run').onclick=runReport;
if(q('#btn-export'))q('#btn-export').onclick=exportExcel;
}
// ── Excel export — "All Machines" summary sheet (same layout as the on-page
// table) + one sheet per machine (cause breakdown, matching the Master
// Sheet's own per-machine tab layout). ──
const XLSX_BLUE='FF1A6FFF', XLSX_AMBER='FFF5A623', XLSX_INK='FF1E2A44', XLSX_WHITE='FFFFFFFF', XLSX_ZEBRA='FFFFF7EC', XLSX_HI='FFF8D7DA', XLSX_SEC='FFEFF4FF';
function xlsxBorder(color){ const b={style:'thin',color:{argb:color||'FFB9CCEA'}}; return {top:b,left:b,bottom:b,right:b}; }
// Builds one "summary" sheet (Base Days / cause mins & %s / active days /
// shift time / plant inoperation / net downtime %) for a given set of
// groups (machines, or OEE tabs) + their combined NET column — shared by
// both the "All Machines" and "By OEE Form" sheets below.
function addSummarySheet(wb,{sheetName,titleText,causes,baseDaysCommon,periodFrom,periodTo,groups,net,labelOf}){
const cols=[...groups,net];
const ws=wb.addWorksheet(sheetName,{views:[{state:'frozen',ySplit:4,xSplit:1}]});
ws.mergeCells(1,1,1,cols.length+1);
const title=ws.getCell(1,1);
title.value=`${titleText} (${periodFrom} to ${periodTo})`;
title.font={bold:true,size:14,color:{argb:XLSX_WHITE}};
title.fill={type:'pattern',pattern:'solid',fgColor:{argb:XLSX_AMBER}};
ws.getRow(1).height=24;
ws.getRow(2).getCell(1).value=`Base Days (common): ${baseDaysCommon}`;
ws.getRow(2).getCell(1).font={italic:true,color:{argb:'FF64748B'}};
const hdrRowNum=4;
const headLabels=['Metric',...cols.map(labelOf)];
headLabels.forEach((h,i)=>{
const cell=ws.getRow(hdrRowNum).getCell(i+1);
cell.value=h;
cell.font={bold:true,color:{argb:XLSX_WHITE}};
cell.fill={type:'pattern',pattern:'solid',fgColor:{argb:XLSX_INK}};
cell.border=xlsxBorder('FF0F172A');
cell.alignment={horizontal:i===0?'left':'right',vertical:'middle'};
});
ws.getRow(hdrRowNum).height=20;
let r=hdrRowNum+1;
const addSection=(label)=>{
ws.mergeCells(r,1,r,cols.length+1);
const c=ws.getRow(r).getCell(1);
c.value=label; c.font={bold:true,color:{argb:'FF1E2A44'}};
c.fill={type:'pattern',pattern:'solid',fgColor:{argb:XLSX_SEC}};
r++;
};
const addRow=(label,vals,fmt,hiMask)=>{
const row=ws.getRow(r);
row.getCell(1).value=label;row.getCell(1).font={bold:true};row.getCell(1).border=xlsxBorder();
vals.forEach((v,i)=>{
const cell=row.getCell(i+2);
cell.value=v; cell.numFmt=fmt||'#,##0';
cell.alignment={horizontal:'right'};
cell.border=xlsxBorder();
if(hiMask&&hiMask[i])cell.fill={type:'pattern',pattern:'solid',fgColor:{argb:XLSX_HI}};
});
r++;
};
addSection('1. BASE (WORKING) DAYS USED');
addRow('Base Days',cols.map(g=>g.baseDays));
addRow('Base Days × 1440 (min)',cols.map(g=>g.baseMin));
addSection('2. DOWNTIME BY CAUSE (min / %)');
causes.forEach(c=>addRow(c,cols.map(g=>g.causeTotals[c]||0)));
causes.forEach(c=>{
const th=rowThreshold(groups,g=>g.causePcts[c]||0);
const vals=cols.map(g=>(g.causePcts[c]||0)/100);
const mask=cols.map(g=>(g.causePcts[c]||0)>=th);
addRow('% '+c,vals,'0.00%',mask);
});
addSection('3. ACTUAL ACTIVE DAYS');
addRow('Actual Active Days',cols.map(g=>g.actualActiveDays));
addSection('4. TOTAL SHIFT TIME (Scheduled, incl. downtime)');
addRow('Total Shift Time (min)',cols.map(g=>g.totalShiftMin));
addSection('5. DOWNTIME DUE TO PLANT INOPERATION');
addRow('Minutes (Base Days×1440 − Total Shift Time)',cols.map(g=>g.plantInoperationMin));
addRow('%',cols.map(g=>g.plantInoperationPct/100),'0.00%');
addSection('6. NET DOWNTIME % (Causes Only)');
addRow('Net Downtime %',cols.map(g=>g.netDowntimePct/100),'0.00%');
addRow('Total % (Causes + Plant Inoperative)',cols.map(g=>g.totalPct/100),'0.00%');
addRow('Remainder: Actual Run %',cols.map(g=>g.remainderPct/100),'0.00%');
ws.getColumn(1).width=42;
for(let i=2;i<=cols.length+1;i++)ws.getColumn(i).width=16;
}
async function exportExcel(){
const rep=_report;
if(!rep||!rep.machines.length){toast('Nothing to export');return;}
if(typeof ExcelJS==='undefined'){toast('Export library failed to load — check your connection and retry');return;}
const wb=new ExcelJS.Workbook();
wb.creator='SAP ERP Portal';wb.created=new Date();
addSummarySheet(wb,{
sheetName:'All Machines',titleText:'Downtime Analysis — All Machines',
causes:rep.causes,baseDaysCommon:rep.baseDaysCommon,periodFrom:rep.periodFrom,periodTo:rep.periodTo,
groups:rep.machines,net:rep.net,labelOf:g=>g.machine,
});
// ── One sheet per machine ──
rep.machines.forEach(m=>{
const safeName=m.machine.replace(/[\\/*?:[\]]/g,' ').slice(0,31)||'Machine';
const mws=wb.addWorksheet(safeName);
mws.mergeCells(1,1,1,2);
const t=mws.getCell(1,1);
t.value=`${m.machine} — Downtime Analysis`;
t.font={bold:true,size:13,color:{argb:XLSX_WHITE}};
t.fill={type:'pattern',pattern:'solid',fgColor:{argb:XLSX_AMBER}};
mws.getRow(1).height=22;
mws.getRow(2).getCell(1).value=`Base days = ${m.baseDays} (${rep.periodFrom} to ${rep.periodTo})${m.firstActiveDate&&m.firstActiveDate>rep.periodFrom?` — prorated from first entry ${m.firstActiveDate}`:''}`;
mws.getRow(2).getCell(1).font={italic:true,color:{argb:'FF64748B'}};
let mr=4;
const mAdd=(label,val,fmt)=>{
mws.getRow(mr).getCell(1).value=label; mws.getRow(mr).getCell(1).border=xlsxBorder();
const c=mws.getRow(mr).getCell(2); c.value=val; c.numFmt=fmt||'#,##0'; c.alignment={horizontal:'right'}; c.border=xlsxBorder();
mr++;
};
const mSection=(label)=>{
mws.mergeCells(mr,1,mr,2);
const c=mws.getRow(mr).getCell(1); c.value=label; c.font={bold:true}; c.fill={type:'pattern',pattern:'solid',fgColor:{argb:XLSX_SEC}};
mr++;
};
mSection('1. BASE (WORKING) DAYS USED');
mAdd('Base Days',m.baseDays);
mAdd('Base Days × 1440 (min)',m.baseMin);
mr++;
mSection('2. DOWNTIME BY CAUSE');
const causeHdr=mws.getRow(mr);
causeHdr.getCell(1).value='Cause';causeHdr.getCell(2).value='Minutes';causeHdr.getCell(3).value='%';
[1,2,3].forEach(ci=>{const c=causeHdr.getCell(ci);c.font={bold:true,color:{argb:XLSX_WHITE}};c.fill={type:'pattern',pattern:'solid',fgColor:{argb:XLSX_INK}};c.border=xlsxBorder('FF0F172A');});
mr++;
rep.causes.forEach((c,i)=>{
const row=mws.getRow(mr);
row.getCell(1).value=c;row.getCell(1).border=xlsxBorder();
row.getCell(2).value=m.causeTotals[c]||0;row.getCell(2).numFmt='#,##0';row.getCell(2).border=xlsxBorder();row.getCell(2).alignment={horizontal:'right'};
row.getCell(3).value=(m.causePcts[c]||0)/100;row.getCell(3).numFmt='0.00%';row.getCell(3).border=xlsxBorder();row.getCell(3).alignment={horizontal:'right'};
if(i%2)[1,2,3].forEach(ci=>row.getCell(ci).fill={type:'pattern',pattern:'solid',fgColor:{argb:XLSX_ZEBRA}});
mr++;
});
mr++;
mSection('3. ACTUAL ACTIVE DAYS (machine ran)');
mAdd('Actual Active Days',m.actualActiveDays);
mSection('4. TOTAL SHIFT TIME (Scheduled, incl. downtime)');
mAdd('Total Shift Time (min)',m.totalShiftMin);
mSection('5. DOWNTIME DUE TO PLANT INOPERATION');
mAdd('Minutes',m.plantInoperationMin);
mAdd('%',m.plantInoperationPct/100,'0.00%');
mSection('6. NET DOWNTIME % (Causes Only)');
mAdd('Net Downtime %',m.netDowntimePct/100,'0.00%');
mAdd('Total % (Causes + Plant Inoperative)',m.totalPct/100,'0.00%');
mAdd('Remainder: Actual Run %',m.remainderPct/100,'0.00%');
mws.getColumn(1).width=42;mws.getColumn(2).width=16;mws.getColumn(3).width=12;
});
// ── Monthly Downtime Summary sheet (all machines combined) ──
if(_monthly&&_monthly.rows.length){
const mo=_monthly;
const mws2=wb.addWorksheet('Monthly Summary',{views:[{state:'frozen',ySplit:3}]});
mws2.mergeCells(1,1,1,6);
const t2=mws2.getCell(1,1);
t2.value=`Monthly Downtime Summary (${mo.periodFrom} to ${mo.periodTo})`;
t2.font={bold:true,size:14,color:{argb:XLSX_WHITE}};
t2.fill={type:'pattern',pattern:'solid',fgColor:{argb:XLSX_AMBER}};
mws2.getRow(1).height=24;
const hdr2=['S.No','Month','Total Down Time (MIN)','Total M/C Run Time (MIN)','Total Run Time − Total Down Time (MIN)','% Of Down Time'];
hdr2.forEach((h,i)=>{
const c=mws2.getRow(3).getCell(i+1);
c.value=h;c.font={bold:true,color:{argb:XLSX_WHITE}};c.fill={type:'pattern',pattern:'solid',fgColor:{argb:XLSX_INK}};
c.border=xlsxBorder('FF0F172A');c.alignment={horizontal:i<2?'left':'right',vertical:'middle',wrapText:true};
});
mws2.getRow(3).height=28;
let mr2=4;
mo.rows.forEach(r=>{
const row=mws2.getRow(mr2);
row.getCell(1).value=r.no;row.getCell(2).value=r.monthLabel;
row.getCell(3).value=r.downMin;row.getCell(4).value=r.runMin;row.getCell(5).value=r.netRunMin;
row.getCell(6).value=r.pctDown/100;row.getCell(6).numFmt='0%';
[1,2,3,4,5,6].forEach(ci=>{row.getCell(ci).border=xlsxBorder();if(ci>=3)row.getCell(ci).alignment={horizontal:'right'};});
mr2++;
});
const totalRow=mws2.getRow(mr2);
totalRow.getCell(2).value='TOTAL';
totalRow.getCell(3).value=mo.total.downMin;totalRow.getCell(4).value=mo.total.runMin;totalRow.getCell(5).value=mo.total.netRunMin;
totalRow.getCell(6).value=mo.total.pctDown/100;totalRow.getCell(6).numFmt='0%';
[1,2,3,4,5,6].forEach(ci=>{
const c=totalRow.getCell(ci);
c.font={bold:true};c.border=xlsxBorder();c.fill={type:'pattern',pattern:'solid',fgColor:{argb:'FFFFF176'}};
if(ci>=3)c.alignment={horizontal:'right'};
});
mws2.getColumn(1).width=6;mws2.getColumn(2).width=12;
for(let i=3;i<=6;i++)mws2.getColumn(i).width=22;
}
// ── One detail sheet per OEE tab — shift-level rows (Date/Shift/Shift
// Length/Equ.Min/Gold Std/cause columns/Total Down Time/% Down Time),
// matching the on-page detailByTabCardsHtml tables exactly. Placed after
// "Monthly Summary" per request. ──
if(_detailByTab&&Object.keys(_detailByTab.tabs||{}).length){
const dbt=_detailByTab;
Object.keys(dbt.tabs).forEach(tab=>{
const t=dbt.tabs[tab];
if(!t.rows.length)return;
const safeName=('OEE - '+t.tabLabel).replace(/[\\/*?:[\]]/g,' ').slice(0,31);
const dws=wb.addWorksheet(safeName,{views:[{state:'frozen',ySplit:3,xSplit:2}]});
const hdr=['Date','Shift','Shift Length (Hrs)','Equ. Min','Prod Rate (Gold Std)',...dbt.causes,'Total Down Time (Min)','% Of Down Time'];
dws.mergeCells(1,1,1,hdr.length);
const dt=dws.getCell(1,1);
dt.value=`Downtime Analysis — ${t.tabLabel} (${dbt.periodFrom} to ${dbt.periodTo})`;
dt.font={bold:true,size:13,color:{argb:XLSX_WHITE}};
dt.fill={type:'pattern',pattern:'solid',fgColor:{argb:XLSX_AMBER}};
dws.getRow(1).height=22;
hdr.forEach((h,i)=>{
const c=dws.getRow(3).getCell(i+1);
c.value=h;c.font={bold:true,color:{argb:XLSX_WHITE}};c.fill={type:'pattern',pattern:'solid',fgColor:{argb:XLSX_INK}};
c.border=xlsxBorder('FF0F172A');c.alignment={horizontal:'center',vertical:'middle',wrapText:true};
});
dws.getRow(3).height=30;
let dr=4;
t.rows.forEach((row,i)=>{
const xr=dws.getRow(dr);
xr.getCell(1).value=row.date;xr.getCell(2).value=row.shift;
xr.getCell(3).value=row.shiftLenHrs;xr.getCell(4).value=row.equMin;xr.getCell(5).value=row.goldStd||0;
dbt.causes.forEach((c,ci)=>{xr.getCell(6+ci).value=row.causeMins[c]||0;});
xr.getCell(6+dbt.causes.length).value=row.totalDownMin;
const pc=xr.getCell(7+dbt.causes.length);pc.value=row.pctDown/100;pc.numFmt='0%';
for(let ci=1;ci<=hdr.length;ci++){
const cell=xr.getCell(ci);
cell.border=xlsxBorder();
if(ci>=3)cell.alignment={horizontal:'right'};
if(i%2)cell.fill={type:'pattern',pattern:'solid',fgColor:{argb:XLSX_ZEBRA}};
}
dr++;
});
const totalRow=dws.getRow(dr);
dws.mergeCells(dr,1,dr,3);
totalRow.getCell(1).value='TOTAL';
totalRow.getCell(4).value=t.totals.equMin;
dbt.causes.forEach((c,ci)=>{
totalRow.getCell(6+ci).value=t.rows.reduce((s,r)=>s+(r.causeMins[c]||0),0);
});
totalRow.getCell(6+dbt.causes.length).value=t.totals.totalDownMin;
const ptc=totalRow.getCell(7+dbt.causes.length);ptc.value=t.totals.pctDownSum/100;ptc.numFmt='0%';
for(let ci=1;ci<=hdr.length;ci++){
const cell=totalRow.getCell(ci);
cell.font={bold:true};cell.border=xlsxBorder();cell.fill={type:'pattern',pattern:'solid',fgColor:{argb:'FFFFF176'}};
if(ci>=3)cell.alignment={horizontal:'right'};
}
dws.getColumn(1).width=12;dws.getColumn(2).width=10;
for(let ci=3;ci<=hdr.length;ci++)dws.getColumn(ci).width=14;
});
}
const buf=await wb.xlsx.writeBuffer();
const blob=new Blob([buf],{type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'});
const a=document.createElement('a');
a.href=URL.createObjectURL(blob);
a.download=`Downtime_Analysis_${rep.periodFrom}_to_${rep.periodTo}.xlsx`;
a.click();
URL.revokeObjectURL(a.href);
}
async function init(){
try{const list=await window.fetchCompanyList();if(list.length)_companies=list;}catch(_e){}
await loadConfig();
render();
}
init();
</script>
<script src="/sidebar.js?v=31"></script>
</body>
</html>