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

409 lines
23 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>
<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>Inventory Status Report — 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:1500px;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(26,111,255,.14);color:var(--blue);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(--blue)}
.fi-mono{font-family:var(--mono)}
select.fi{cursor:pointer}
.filters{display:flex;gap:8px;flex-wrap:wrap;align-items:flex-end}
.filters>div{min-width:130px}
.range-pair{display:flex;align-items:flex-end;gap:6px}
.range-pair span{font-size:11px;color:var(--text3);padding-bottom:8px}
.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(--blue);color:#fff}
.b-primary:disabled{opacity:.5;cursor:not-allowed}
.b-cancel{background:var(--surface2);color:var(--text2);border:1px solid var(--border2)}
.chk-row{display:flex;align-items:center;gap:7px;font-size:12px;cursor:pointer;padding:6px 0}
.wh-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:4px 14px;max-height:190px;overflow-y:auto;border:1px solid var(--border2);border-radius:8px;padding:8px 10px;background:var(--surface2)}
.wh-toolbar{display:flex;gap:10px;margin-bottom:6px;font-size:10.5px}
.wh-toolbar a{color:var(--blue);cursor:pointer;text-decoration:none}
.search-wrap{position:relative}
.search-dd{position:absolute;top:100%;left:0;right:0;background:var(--surface2);border:1px solid var(--blue);border-radius:7px;box-shadow:0 12px 40px rgba(0,0,0,.7);z-index:300;max-height:220px;overflow-y:auto;display:none;margin-top:4px}
.sd-row{padding:10px 12px;cursor:pointer;border-bottom:1px solid var(--border);display:flex;gap:8px;align-items:center}
.sd-row:hover{background:rgba(26,111,255,.1)}
.sd-code{font-family:var(--mono);font-size:11px;color:var(--blue);font-weight:600;min-width:90px}
.sd-name{font-size:11px;color:var(--text2)}
.sd-msg{padding:12px;text-align:center;color:var(--text3);font-size:12px}
.lst{width:100%;border-collapse:separate;border-spacing:0;font-size:12px}
.lst th{text-align:left;padding:8px 10px;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 th.sortable{cursor:pointer;user-select:none}
.lst th.sortable:hover{color:var(--blue)}
.lst td{padding:7px 10px;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 td.neg{color:var(--red)!important}
.lst tbody tr:hover td{background:rgba(26,111,255,.05)}
.mono{font-family:var(--mono)}
.empty-msg{text-align:center;padding:26px;color:var(--text3);font-size:12px}
.count{font-size:11px;font-weight:700;color:var(--blue);background:rgba(26,111,255,.1);padding:3px 11px;border-radius:20px}
.sp{width:14px;height:14px;border:2px solid rgba(26,111,255,.25);border-top-color:var(--blue);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:65vh;overflow-y:auto;border-radius:8px;border:1px solid var(--border2)}
</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='/';
// Preferred Vendor is only meaningful/visible to Admin & System Admin — same
// sessionStorage user blob auth-guard.js itself already decodes from, no
// extra API round trip needed.
let _role='';
try{const raw=sessionStorage.getItem('portal_user');const u=raw?JSON.parse(raw):null;_role=u&&u.role||'';}catch(_e){}
const CAN_SEE_VENDOR=_role==='admin'||_role==='system_admin';
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 num3(v){if(v==null)return'';const n=Number(v);if(isNaN(n))return'';return n.toLocaleString('en-IN',{maximumFractionDigits:6});}
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 _itemGroups=[];
let _wh=[];
let _selectedWh=new Set(); // empty = every warehouse, matching SAP's own "nothing ticked = everything" behavior
const _f={itemFrom:'',itemTo:'',vendorFrom:'',vendorTo:'',itemGroup:'',hideZeroStock:false};
let _loading=false,_ran=false,_rows=[],_search='';
let _page=0;
const PAGE_SIZE=20;
function coOptions(sel){return _companies.map(c=>`<option value="${esc(c.value)}" ${sel===c.value?'selected':''}>${esc(c.label)}</option>`).join('');}
function grpOptions(sel){return '<option value="">All</option>'+_itemGroups.map(g=>`<option value="${esc(g.code)}" ${String(sel)===String(g.code)?'selected':''}>${esc(g.name)}</option>`).join('');}
async function loadItemGroups(){
try{const r=await api('/sap/lookup/item-groups?company='+encodeURIComponent(_co));_itemGroups=r.data||[];}
catch(_e){_itemGroups=[];}
}
async function loadWarehouses(){
try{const r=await api('/sap/lookup/warehouses?company='+encodeURIComponent(_co));_wh=r.data||[];}
catch(_e){_wh=[];}
}
// Same code-search-and-pick pattern as New Production Order's own Product
// No. field (public/production.html's wireSearch()/fetchItems()) — shared
// here by both Item Code From/To (against /sap/lookup/items) and Vendor
// From/To (against /sap/lookup/vendors), so each can be searched by
// name too, not just typed exactly, instead of the plain text inputs they
// started as.
async function fetchItems(v){
try{const r=await api(`/sap/lookup/items?q=${encodeURIComponent(v)}&company=${encodeURIComponent(_co)}`);if(r.success&&r.data?.length)return r.data;}catch(_e){}
return [];
}
async function fetchVendors(v){
try{const r=await api(`/sap/lookup/vendors?q=${encodeURIComponent(v)}&company=${encodeURIComponent(_co)}`);if(r.success&&r.data?.length)return r.data;}catch(_e){}
return [];
}
let _codeSearchTimer=null;
function wireCodeSearch(inputId,ddId,field,fetchFn,codeKey,nameKey){
const inp=q('#'+inputId),dd=q('#'+ddId);if(!inp||!dd)return;
inp.oninput=function(){
_f[field]=this.value.toUpperCase();
const v=this.value.trim();
if(v.length<2){dd.style.display='none';return;}
clearTimeout(_codeSearchTimer);
_codeSearchTimer=setTimeout(async()=>{
dd.innerHTML='<div class="sd-msg"><span class="sp"></span></div>';dd.style.display='block';
const items=await fetchFn(v);
if(_f[field]!==this.value.toUpperCase())return; // stale — a newer keystroke already superseded this
if(!items.length){dd.innerHTML='<div class="sd-msg">No results</div>';return;}
dd.innerHTML='';
items.forEach(it=>{
const row=document.createElement('div');row.className='sd-row';
row.innerHTML=`<span class="sd-code">${esc(it[codeKey])}</span><span class="sd-name">${esc(it[nameKey])}</span>`;
row.onclick=()=>{_f[field]=it[codeKey];inp.value=it[codeKey];dd.style.display='none';};
dd.appendChild(row);
});
},250);
};
}
document.addEventListener('click',e=>{
if(!e.target.closest('.search-wrap'))document.querySelectorAll('.search-dd').forEach(d=>d.style.display='none');
});
async function runReport(){
_loading=true;_ran=true;_page=0;render();
const qs=new URLSearchParams();
qs.set('company',_co);
if(_f.itemFrom)qs.set('itemFrom',_f.itemFrom);
if(_f.itemTo)qs.set('itemTo',_f.itemTo);
if(_f.vendorFrom)qs.set('vendorFrom',_f.vendorFrom);
if(_f.vendorTo)qs.set('vendorTo',_f.vendorTo);
if(_f.itemGroup)qs.set('itemGroup',_f.itemGroup);
if(_f.hideZeroStock)qs.set('hideZeroStock','1');
if(_selectedWh.size)qs.set('warehouses',[..._selectedWh].join(','));
try{
const r=await api('/inventory-status-report?'+qs.toString());
_rows=r.data||[];
}catch(e){toast(e.message);_rows=[];}
_loading=false;render();
}
let _sortKey='',_sortDir=1; // '' = server order (unsorted); 1=asc, -1=desc
const SORT_COLS={itemCode:'string',itemName:'string',inStock:'num',committed:'num',ordered:'num',available:'num',uom:'string'};
function filteredRows(){
let list=_rows;
if(_search.trim()){
const s=_search.trim().toUpperCase();
list=list.filter(r=>(r.itemCode||'').toUpperCase().includes(s)||(r.itemName||'').toUpperCase().includes(s));
}
if(_sortKey){
const type=SORT_COLS[_sortKey];
list=[...list].sort((a,b)=>{
const av=a[_sortKey],bv=b[_sortKey];
const cmp=type==='num'?(Number(av)||0)-(Number(bv)||0):String(av||'').localeCompare(String(bv||''));
return cmp*_sortDir;
});
}
return list;
}
function sortBy(key){
if(_sortKey===key)_sortDir=-_sortDir;else{_sortKey=key;_sortDir=1;}
_page=0;render();
}
function sortArrow(key){ return _sortKey===key?(_sortDir===1?' ▲':' ▼'):''; }
function whToolbar(){
return `<div class="wh-toolbar"><a id="wh-all">Select all</a><a id="wh-none">Clear all</a></div>`;
}
function whGrid(){
return `<div class="wh-grid">${_wh.map(w=>`
<label class="chk-row"><input type="checkbox" class="wh-chk" value="${esc(w.code)}" ${_selectedWh.has(w.code)?'checked':''}/> <span class="mono">${esc(w.code)}</span> — ${esc(w.name||'')}</label>
`).join('')}</div>`;
}
function render(){
const app=q('#app');
const list=filteredRows();
const totalPages=Math.max(1,Math.ceil(list.length/PAGE_SIZE));
if(_page>=totalPages)_page=totalPages-1;
if(_page<0)_page=0;
const pageList=list.slice(_page*PAGE_SIZE,(_page+1)*PAGE_SIZE);
app.innerHTML=`
<div class="page-head">
<div class="page-icon">📋</div>
<div><div class="page-title">Inventory Status Report</div><div class="page-sub">Per-item In Stock / Committed / Ordered / Available — same as SAP B1's own Inventory Status report</div></div>
</div>
<div class="card">
<div class="card-hd"><span>Selection Criteria</span></div>
<div class="card-bd">
<div class="filters" style="margin-bottom:10px">
<div style="min-width:180px"><label class="fl">Company</label><select class="fi" id="sel-co">${coOptions(_co)}</select></div>
<div><label class="fl">Item Code</label><div class="range-pair">
<div class="search-wrap" style="flex:1"><input class="fi fi-mono" id="f-itemfrom" placeholder="Search item code / name…" value="${esc(_f.itemFrom)}" autocomplete="off"/><div class="search-dd" id="dd-itemfrom"></div></div>
<span>to</span>
<div class="search-wrap" style="flex:1"><input class="fi fi-mono" id="f-itemto" placeholder="Search item code / name…" value="${esc(_f.itemTo)}" autocomplete="off"/><div class="search-dd" id="dd-itemto"></div></div>
</div></div>
${CAN_SEE_VENDOR?`<div><label class="fl">Vendor</label><div class="range-pair">
<div class="search-wrap" style="flex:1"><input class="fi fi-mono" id="f-vendorfrom" placeholder="Search vendor code / name…" value="${esc(_f.vendorFrom)}" autocomplete="off"/><div class="search-dd" id="dd-vendorfrom"></div></div>
<span>to</span>
<div class="search-wrap" style="flex:1"><input class="fi fi-mono" id="f-vendorto" placeholder="Search vendor code / name…" value="${esc(_f.vendorTo)}" autocomplete="off"/><div class="search-dd" id="dd-vendorto"></div></div>
</div></div>`:''}
<div><label class="fl">Item Group</label><select class="fi" id="f-itemgroup">${grpOptions(_f.itemGroup)}</select></div>
<div><button class="btn b-primary" id="btn-run" ${_loading?'disabled':''}>${_loading?'Running…':'Run Report'}</button></div>
</div>
<label class="chk-row"><input type="checkbox" id="f-hidezero" ${_f.hideZeroStock?'checked':''}/> Hide Items with No Quantity in Stock</label>
<div style="margin-top:8px">
<label class="fl">Warehouses <span style="font-weight:400;text-transform:none;color:var(--text3)">(none selected = all warehouses)</span></label>
${whToolbar()}
${whGrid()}
</div>
</div>
</div>
<div class="card">
<div class="card-hd"><span>Results</span><span class="right">${_ran&&!_loading?`<span class="count">${list.length} item(s)</span>`:''}</span></div>
<div class="card-bd">
<div style="margin-bottom:10px;display:flex;gap:8px;align-items:center">
<input class="fi" id="f-search" placeholder="Item No. / Description…" value="${esc(_search)}" style="max-width:320px"/>
${_ran&&list.length?`<button class="btn b-cancel" id="btn-export" style="margin-left:auto">⬇ Export Excel</button>`:''}
</div>
${_loading?`<div class="empty-msg"><span class="sp"></span> Running report…</div>`
:!_ran?`<div class="empty-msg">Set your filters above and click "Run Report".</div>`
:!list.length?`<div class="empty-msg">No items found for these filters.</div>`
:`<div class="tbl-wrap"><table class="lst"><thead><tr>
<th class="num">#</th>
<th class="sortable" data-sort="itemCode">Item No.${sortArrow('itemCode')}</th>
<th class="sortable" data-sort="itemName">Item Description${sortArrow('itemName')}</th>
<th class="num sortable" data-sort="inStock">In Stock${sortArrow('inStock')}</th>
<th class="num sortable" data-sort="committed">Committed${sortArrow('committed')}</th>
<th class="num sortable" data-sort="ordered">Ordered${sortArrow('ordered')}</th>
<th class="num sortable" data-sort="available">Available${sortArrow('available')}</th>
<th class="sortable" data-sort="uom">UoM${sortArrow('uom')}</th>
</tr></thead><tbody>${pageList.map((r,i)=>`
<tr>
<td class="num">${_page*PAGE_SIZE+i+1}</td>
<td class="mono">${esc(r.itemCode)}</td>
<td>${esc(r.itemName)}</td>
<td class="num">${num3(r.inStock)}</td>
<td class="num">${num3(r.committed)}</td>
<td class="num">${num3(r.ordered)}</td>
<td class="num${r.available<0?' neg':''}">${num3(r.available)}</td>
<td>${esc(r.uom)}</td>
</tr>`).join('')}</tbody></table></div>
<div style="display:flex;align-items:center;gap:10px;margin-top:10px;justify-content:flex-end">
<span style="font-size:11px;color:var(--text3)">Page ${_page+1} of ${totalPages}</span>
<button class="btn b-cancel" id="btn-prev" ${_page===0?'disabled':''} style="padding:6px 14px;height:auto">‹ Prev</button>
<button class="btn b-cancel" id="btn-next" ${_page>=totalPages-1?'disabled':''} style="padding:6px 14px;height:auto">Next ›</button>
</div>`}
</div>
</div>`;
wire();
}
function wire(){
q('#sel-co').onchange=async function(){_co=this.value;_ran=false;_rows=[];await Promise.all([loadItemGroups(),loadWarehouses()]);_selectedWh=new Set();render();};
wireCodeSearch('f-itemfrom','dd-itemfrom','itemFrom',fetchItems,'ItemCode','ItemName');
wireCodeSearch('f-itemto','dd-itemto','itemTo',fetchItems,'ItemCode','ItemName');
wireCodeSearch('f-vendorfrom','dd-vendorfrom','vendorFrom',fetchVendors,'CardCode','CardName');
wireCodeSearch('f-vendorto','dd-vendorto','vendorTo',fetchVendors,'CardCode','CardName');
q('#f-itemgroup').onchange=function(){_f.itemGroup=this.value;};
q('#f-hidezero').onchange=function(){_f.hideZeroStock=this.checked;};
q('#btn-run').onclick=runReport;
q('#f-search').oninput=function(){
_search=this.value;_page=0;
const pos=this.selectionStart;
render();
const el=q('#f-search');if(el){el.focus();try{el.setSelectionRange(pos,pos);}catch(_e){}}
};
if(q('#btn-prev'))q('#btn-prev').onclick=()=>{_page--;render();};
if(q('#btn-next'))q('#btn-next').onclick=()=>{_page++;render();};
document.querySelectorAll('th.sortable').forEach(th=>{th.onclick=()=>sortBy(th.dataset.sort);});
document.querySelectorAll('.wh-chk').forEach(el=>{
el.onchange=function(){
if(this.checked)_selectedWh.add(this.value);else _selectedWh.delete(this.value);
};
});
q('#wh-all').onclick=()=>{_selectedWh=new Set(_wh.map(w=>w.code));render();};
q('#wh-none').onclick=()=>{_selectedWh=new Set();render();};
if(q('#btn-export'))q('#btn-export').onclick=exportExcel;
}
// Styled .xlsx export via ExcelJS — same look/border convention as the PWO
// Source Report's own export (bordered header banner + criteria summary,
// bordered/zebra-striped data grid, frozen header row). Exports exactly
// what's currently filtered on screen (filteredRows()), not the whole
// unfiltered fetch.
const XLSX_BLUE='FF1A6FFF', XLSX_INK='FF1E2A44', XLSX_WHITE='FFFFFFFF', XLSX_ZEBRA='FFEFF4FF', XLSX_RED='FFE53E3E';
function xlsxBorder(color){ const b={style:'thin',color:{argb:color||'FFB9CCEA'}}; return {top:b,left:b,bottom:b,right:b}; }
async function exportExcel(){
const rows=filteredRows();
if(!rows.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();
const ws=wb.addWorksheet('Inventory Status',{views:[{state:'frozen',ySplit:6}]});
ws.mergeCells('A1:H1');
const title=ws.getCell('A1');
title.value='Inventory Status Report — Export';
title.font={bold:true,size:16,color:{argb:XLSX_WHITE}};
title.fill={type:'pattern',pattern:'solid',fgColor:{argb:XLSX_BLUE}};
title.alignment={vertical:'middle',horizontal:'left'};
ws.getRow(1).height=26;
const whLabel=_selectedWh.size?[..._selectedWh].join(', '):'All';
const metaRows=[
['Company:',_co,'Item Code:',`${_f.itemFrom||'—'} to ${_f.itemTo||'—'}`],
['Warehouse(s):',whLabel,'Rows:',rows.length],
];
metaRows.forEach((r,i)=>{
const row=ws.getRow(2+i);
row.getCell(1).value=r[0];row.getCell(1).font={bold:true,color:{argb:'FF64748B'}};
row.getCell(2).value=r[1];row.getCell(2).font={color:{argb:'FF0F172A'}};
row.getCell(3).value=r[2];row.getCell(3).font={bold:true,color:{argb:'FF64748B'}};
row.getCell(4).value=r[3];row.getCell(4).font={color:{argb:'FF0F172A'}};
});
const hdrRowNum=6;
const cols=['#','Item No.','Item Description','In Stock','Committed','Ordered','Available','UoM'];
const hdrRow=ws.getRow(hdrRowNum);
cols.forEach((h,i)=>{
const cell=hdrRow.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.alignment={vertical:'middle',horizontal:i===0||(i>=3&&i<=6)?'right':'center'};
cell.border=xlsxBorder('FF0F172A');
});
hdrRow.height=20;
rows.forEach((r,i)=>{
const row=ws.getRow(hdrRowNum+1+i);
const vals=[i+1,r.itemCode,r.itemName,r.inStock,r.committed,r.ordered,r.available,r.uom];
vals.forEach((v,ci)=>{
const cell=row.getCell(ci+1);
cell.value=v;
cell.border=xlsxBorder();
cell.fill={type:'pattern',pattern:'solid',fgColor:{argb:i%2?XLSX_ZEBRA:XLSX_WHITE}};
if(ci===0)cell.alignment={horizontal:'right'};
if(ci>=3&&ci<=6){
cell.numFmt='#,##0.######';
cell.alignment={horizontal:'right'};
if(ci===6&&r.available<0)cell.font={bold:true,color:{argb:XLSX_RED}};
}
});
});
ws.columns=[{width:6},{width:16},{width:34},{width:14},{width:14},{width:14},{width:14},{width:10}];
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=`Inventory_Status_Report_${_co||'all'}.xlsx`;
a.click();
URL.revokeObjectURL(a.href);
}
async function init(){
try{const list=await window.fetchCompanyList();if(list.length)_companies=list;}catch(_e){}
await Promise.all([loadItemGroups(),loadWarehouses()]);
render();
}
init();
</script>
<script src="/sidebar.js?v=31"></script>
</body>
</html>