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
+519
View File
@@ -0,0 +1,519 @@
<!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>Batch No. Transaction 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: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(13,187,138,.14);color:var(--green);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(--green)}
.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(--green);color:#06281f}
.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(--green);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(--green);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(13,187,138,.1)}
.sd-code{font-family:var(--mono);font-size:11px;color:var(--green);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(--green)}
.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(13,187,138,.05)}
.lst tbody tr.sel td{background:rgba(26,111,255,.16)!important}
.lst tbody tr.batch-row{cursor:pointer}
.lst tfoot td{background:var(--surface3);font-weight:700;border-top:2px solid var(--border2)}
.dir-badge{display:inline-block;padding:2px 9px;border-radius:5px;font-size:10px;font-weight:700;letter-spacing:.3px}
.dir-in{background:rgba(13,187,138,.18);color:var(--green)}
.dir-out{background:rgba(229,62,62,.18);color:#fca5a5}
.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(--green);background:rgba(13,187,138,.1);padding:3px 11px;border-radius:20px}
.sp{width:14px;height:14px;border:2px solid rgba(13,187,138,.25);border-top-color:var(--green);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:46vh;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 num3(v){if(v==null)return'';const n=Number(v);if(isNaN(n))return'';return n.toLocaleString('en-IN',{maximumFractionDigits:6});}
function fmtDate(v){if(!v)return'';const d=new Date(v);if(isNaN(d))return'';return d.toLocaleDateString('en-IN',{day:'2-digit',month:'short',year:'numeric'});}
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
const _f={itemFrom:'',itemTo:'',batchNo:'',itemGroup:'',includeZeroQty:false,allTxns:false};
let _loading=false,_ran=false,_rows=[],_search='';
let _page=0;
const PAGE_SIZE=20;
// Selected batch (drives the "Transactions for Batch" panel below)
let _selItem=null,_selBatch=null;
let _txnLoading=false,_txns=[];
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=[];}
}
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 [];
}
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;
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;_selItem=null;_selBatch=null;_txns=[];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.batchNo)qs.set('batchNo',_f.batchNo);
if(_f.itemGroup)qs.set('itemGroup',_f.itemGroup);
qs.set('includeZeroQty',_f.includeZeroQty?'1':'0');
if(_selectedWh.size)qs.set('warehouses',[..._selectedWh].join(','));
try{
const r=await api('/batch-no-transaction?'+qs.toString());
_rows=r.data||[];
}catch(e){toast(e.message);_rows=[];}
_loading=false;render();
}
let _sortKey='',_sortDir=1;
const SORT_COLS={itemCode:'string',itemName:'string',batchNo:'string',warehouse:'string',inStock:'num',mfgDate:'date',expDate:'date',project:'string',status:'string',coaStatus:'string',saleType:'string',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)||(r.batchNo||'').toUpperCase().includes(s));
}
if(_sortKey){
const type=SORT_COLS[_sortKey];
list=[...list].sort((a,b)=>{
const av=a[_sortKey],bv=b[_sortKey];
let cmp;
if(type==='num')cmp=(Number(av)||0)-(Number(bv)||0);
else if(type==='date')cmp=new Date(av||0)-new Date(bv||0);
else cmp=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>`;
}
// ── Transactions-for-Batch panel ──
async function selectBatch(itemCode,batchNo){
_selItem=itemCode;_selBatch=batchNo;_txnLoading=true;_txns=[];render();
try{
const r=await api(`/batch-no-transaction/transactions?itemCode=${encodeURIComponent(itemCode)}&batchNo=${encodeURIComponent(batchNo)}&company=${encodeURIComponent(_co)}`);
_txns=r.data||[];
}catch(e){toast(e.message);_txns=[];}
_txnLoading=false;render();
}
// "Display All Transactions for Selected Batches" — loads transactions for
// every batch currently in the (filtered) list, capped to avoid hammering
// the server with an unbounded fan-out when the filters are too broad.
async function loadAllTxns(){
const list=filteredRows();
const pairs=[...new Map(list.map(r=>[r.itemCode+'||'+r.batchNo,r])).values()];
if(pairs.length>50){toast('Too many batches ('+pairs.length+') to load all transactions at once — narrow your filters first (max 50)','err');_f.allTxns=false;render();return;}
_selItem=null;_selBatch=null;_txnLoading=true;_txns=[];render();
try{
const results=await Promise.all(pairs.map(r=>
api(`/batch-no-transaction/transactions?itemCode=${encodeURIComponent(r.itemCode)}&batchNo=${encodeURIComponent(r.batchNo)}&company=${encodeURIComponent(_co)}`).then(x=>x.data||[]).catch(()=>[])
));
_txns=results.flat().sort((a,b)=>new Date(a.date||0)-new Date(b.date||0));
}catch(e){toast(e.message);_txns=[];}
_txnLoading=false;render();
}
function txnPanelHtml(){
const title=_f.allTxns?'Transactions for All Listed Batches':(_selBatch?`Transactions for Batch: ${esc(_selBatch)} (${esc(_selItem)})`:'Transactions for Batch');
let body;
if(_txnLoading)body=`<div class="empty-msg"><span class="sp"></span> Loading transactions…</div>`;
else if(!_f.allTxns&&!_selBatch)body=`<div class="empty-msg">Click a batch row above to see its transaction history.</div>`;
else if(!_txns.length)body=`<div class="empty-msg">No transactions found for this batch.</div>`;
else{
const totalQty=_txns.reduce((s,t)=>s+(Number(t.qty)||0),0);
body=`<div class="tbl-wrap"><table class="lst"><thead><tr>
<th class="num">#</th><th>Document</th><th class="num">Doc Row</th><th>Date</th>
<th>Whse</th><th>Whse Name</th><th>G/L Acct/BP Name</th>
<th class="num">Qty</th><th>Direction</th><th>Item No.</th><th>Batch</th>
</tr></thead><tbody>${_txns.map(t=>`
<tr>
<td class="num">${t.no}</td>
<td class="mono">${esc(t.docTypeLabel)} ${esc(t.docNum)}</td>
<td class="num">${t.docRow}</td>
<td>${fmtDate(t.date)}</td>
<td class="mono">${esc(t.warehouse)}</td>
<td>${esc(t.warehouseName)}</td>
<td>${esc(t.bpName)}</td>
<td class="num${t.qty<0?' neg':''}">${num3(t.qty)}</td>
<td><span class="dir-badge ${t.direction==='In'?'dir-in':'dir-out'}">${t.direction}</span></td>
<td class="mono">${esc(t.itemCode)}</td>
<td class="mono">${esc(t.batchNo)}</td>
</tr>`).join('')}</tbody>
<tfoot><tr><td colspan="7">TOTAL</td><td class="num${totalQty<0?' neg':''}">${num3(totalQty)}</td><td colspan="3"></td></tr></tfoot>
</table></div>`;
}
return `<div class="card">
<div class="card-hd"><span>${title}</span>${_txns.length?`<span class="right">${_txns.length} transaction(s)</span>`:''}</div>
<div class="card-bd">
<label class="chk-row"><input type="checkbox" id="f-alltxns" ${_f.allTxns?'checked':''}/> Display All Transactions for Selected (listed) Batches</label>
${body}
</div>
</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);
const grandTotalQty=list.reduce((s,r)=>s+(Number(r.inStock)||0),0);
app.innerHTML=`
<div class="page-head">
<div class="page-icon">🧪</div>
<div><div class="page-title">Batch No. Transaction Report</div><div class="page-sub">Same layout as SAP B1's own Batch Number Transactions Report — batch stock on top, per-batch transaction history below</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>
<div><label class="fl">Batch No.</label><input class="fi fi-mono" id="f-batchno" placeholder="Contains…" value="${esc(_f.batchNo)}"/></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-zeroqty" ${_f.includeZeroQty?'checked':''}/> Display Batches with Zero Qty</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>Batches</span><span class="right">${_ran&&!_loading?`<span class="count">${list.length} row(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 / Batch No…" value="${esc(_search)}" style="max-width:340px"/>
${_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 batches 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="sortable" data-sort="batchNo">Batch${sortArrow('batchNo')}</th>
<th class="sortable" data-sort="warehouse">Whse${sortArrow('warehouse')}</th>
<th class="num sortable" data-sort="inStock">Quantity${sortArrow('inStock')}</th>
<th class="sortable" data-sort="expDate">Expiration Date${sortArrow('expDate')}</th>
<th class="sortable" data-sort="mfgDate">Manufacturing Date${sortArrow('mfgDate')}</th>
<th class="sortable" data-sort="project">Project${sortArrow('project')}</th>
<th class="sortable" data-sort="status">Status${sortArrow('status')}</th>
<th class="sortable" data-sort="coaStatus">COA Status${sortArrow('coaStatus')}</th>
<th class="sortable" data-sort="saleType">Sale Type${sortArrow('saleType')}</th>
<th class="num">Committed</th><th class="num">Ordered</th><th class="num">Available</th>
<th class="sortable" data-sort="uom">UoM${sortArrow('uom')}</th>
</tr></thead><tbody>${pageList.map((r,i)=>{
const isSel=!_f.allTxns&&_selItem===r.itemCode&&_selBatch===r.batchNo;
return `<tr class="batch-row${isSel?' sel':''}" data-item="${esc(r.itemCode)}" data-batch="${esc(r.batchNo)}">
<td class="num">${_page*PAGE_SIZE+i+1}</td>
<td class="mono">${esc(r.itemCode)}</td>
<td>${esc(r.itemName)}</td>
<td class="mono">${esc(r.batchNo)}</td>
<td class="mono">${esc(r.warehouse)}</td>
<td class="num">${num3(r.inStock)}</td>
<td>${fmtDate(r.expDate)}</td>
<td>${fmtDate(r.mfgDate)}</td>
<td>${esc(r.project)}</td>
<td>${esc(r.status)}</td>
<td>${esc(r.coaStatus)}</td>
<td>${esc(r.saleType)}</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>
<tfoot><tr><td colspan="5">TOTAL</td><td class="num">${num3(grandTotalQty)}</td><td colspan="10"></td></tr></tfoot>
</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 class="note">Click a batch row to view its transaction history below. Committed/Ordered/Available are extra columns not in SAP's native report — available here since the data is already on hand.</div>`}
</div>
</div>
${_ran&&!_loading?txnPanelHtml():''}`;
wire();
}
function wire(){
q('#sel-co').onchange=async function(){_co=this.value;_ran=false;_rows=[];_selItem=null;_selBatch=null;_txns=[];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');
q('#f-batchno').oninput=function(){_f.batchNo=this.value;};
q('#f-itemgroup').onchange=function(){_f.itemGroup=this.value;};
q('#f-zeroqty').onchange=function(){_f.includeZeroQty=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;
document.querySelectorAll('.batch-row').forEach(row=>{
row.onclick=()=>{ if(_f.allTxns){_f.allTxns=false;} selectBatch(row.dataset.item,row.dataset.batch); };
});
const allTxnsEl=q('#f-alltxns');
if(allTxnsEl)allTxnsEl.onchange=function(){
_f.allTxns=this.checked;
if(_f.allTxns)loadAllTxns();else{_selItem=null;_selBatch=null;_txns=[];render();}
};
}
const XLSX_GREEN='FF0DBB8A', 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('Batches',{views:[{state:'frozen',ySplit:6}]});
ws.mergeCells('A1:P1');
const title=ws.getCell('A1');
title.value='Batch No. Transaction Report — Export';
title.font={bold:true,size:16,color:{argb:XLSX_WHITE}};
title.fill={type:'pattern',pattern:'solid',fgColor:{argb:XLSX_GREEN}};
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','Batch','Whse','Quantity','Expiration Date','Manufacturing Date','Project','Status','COA Status','Sale Type','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>=5&&i<=5)||(i>=12&&i<=14)?'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.batchNo,r.warehouse,r.inStock,
r.expDate?new Date(r.expDate):null,r.mfgDate?new Date(r.mfgDate):null,
r.project,r.status,r.coaStatus,r.saleType,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||ci===5||(ci>=12&&ci<=14))cell.alignment={horizontal:'right'};
if(ci===6||ci===7)cell.numFmt='dd-mmm-yyyy';
if(ci===5||(ci>=12&&ci<=14)){
cell.numFmt='#,##0.######';
if(ci===14&&r.available<0)cell.font={bold:true,color:{argb:XLSX_RED}};
}
});
});
ws.columns=[{width:6},{width:16},{width:30},{width:16},{width:8},{width:14},{width:14},{width:16},{width:12},{width:10},{width:12},{width:12},{width:12},{width:12},{width:12},{width:10}];
if(_txns.length){
const ws2=wb.addWorksheet('Transactions',{views:[{state:'frozen',ySplit:3}]});
ws2.mergeCells('A1:K1');
const t2=ws2.getCell('A1');
t2.value=_f.allTxns?'Transactions for All Listed Batches':`Transactions for Batch: ${_selBatch} (${_selItem})`;
t2.font={bold:true,size:14,color:{argb:XLSX_WHITE}};
t2.fill={type:'pattern',pattern:'solid',fgColor:{argb:XLSX_GREEN}};
ws2.getRow(1).height=22;
const hdr2=['#','Document','Doc Row','Date','Whse','Whse Name','G/L Acct/BP Name','Qty','Direction','Item No.','Batch'];
hdr2.forEach((h,i)=>{
const c=ws2.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');
});
ws2.getRow(3).height=20;
_txns.forEach((t,i)=>{
const row=ws2.getRow(4+i);
const vals=[t.no,`${t.docTypeLabel} ${t.docNum}`,t.docRow,t.date?new Date(t.date):null,t.warehouse,t.warehouseName,t.bpName,t.qty,t.direction,t.itemCode,t.batchNo];
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===3)cell.numFmt='dd-mmm-yyyy';
if(ci===7){cell.numFmt='#,##0.######';if(t.qty<0)cell.font={bold:true,color:{argb:XLSX_RED}};}
});
});
ws2.columns=[{width:6},{width:16},{width:10},{width:13},{width:8},{width:18},{width:28},{width:14},{width:10},{width:14},{width:16}];
}
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=`Batch_No_Transaction_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>