Files
sap-erp/public/downtime-analysis.html
T
John eead8f5ffd
SAP-ERP Portal CI/CD / build (push) Successful in 3m57s
sale order
2026-10-05 18:45:17 +05:30

561 lines
30 KiB
HTML
Raw 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>
<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>