520 lines
29 KiB
HTML
520 lines
29 KiB
HTML
<!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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');}
|
||
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>
|