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

520 lines
29 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>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>