@@ -0,0 +1,417 @@
|
||||
<!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 Posting List — 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:1600px;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}
|
||||
.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 tr.subtotal td{background:var(--surface3);font-weight:700;color:var(--text2)}
|
||||
.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)}
|
||||
.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}
|
||||
</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||v==='')return'';const n=Number(v);if(isNaN(n))return'';return n.toLocaleString('en-IN',{maximumFractionDigits:6});}
|
||||
function fmtD(v){if(!v)return'—';const d=new Date(v);if(isNaN(d))return'—';return `${String(d.getDate()).padStart(2,'0')}/${String(d.getMonth()+1).padStart(2,'0')}/${d.getFullYear()}`;}
|
||||
function today(){return new Date().toISOString().slice(0,10);}
|
||||
|
||||
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();
|
||||
const _f={itemFrom:'',itemTo:'',itemGroup:'',dateFrom:today(),dateTo:today(),hideNoQtyChange:false,subDaily:true,subMonthly:true,subYearly:true};
|
||||
let _loading=false,_ran=false,_rows=[],_search='';
|
||||
let _page=0;
|
||||
const PAGE_SIZE=50;
|
||||
|
||||
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 wireItemSearch(inputId,ddId,field){
|
||||
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 fetchItems(v);
|
||||
if(_f[field]!==this.value.toUpperCase())return;
|
||||
if(!items.length){dd.innerHTML='<div class="sd-msg">No items</div>';return;}
|
||||
dd.innerHTML='';
|
||||
items.forEach(it=>{
|
||||
const row=document.createElement('div');row.className='sd-row';
|
||||
row.innerHTML=`<span class="sd-code">${esc(it.ItemCode)}</span><span class="sd-name">${esc(it.ItemName)}</span>`;
|
||||
row.onclick=()=>{_f[field]=it.ItemCode;inp.value=it.ItemCode;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(){
|
||||
if(!_f.dateFrom||!_f.dateTo){toast('Date From/To are required');return;}
|
||||
if(_f.dateFrom>_f.dateTo){toast('"Date From" cannot be after "Date To"');return;}
|
||||
_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.itemGroup)qs.set('itemGroup',_f.itemGroup);
|
||||
qs.set('dateFrom',_f.dateFrom);qs.set('dateTo',_f.dateTo);
|
||||
if(_f.hideNoQtyChange)qs.set('hideNoQtyChange','1');
|
||||
if(_selectedWh.size)qs.set('warehouses',[..._selectedWh].join(','));
|
||||
try{
|
||||
const r=await api('/inventory-posting-list?'+qs.toString());
|
||||
_rows=r.data||[];
|
||||
}catch(e){toast(e.message);_rows=[];}
|
||||
_loading=false;render();
|
||||
}
|
||||
|
||||
function filteredRows(){
|
||||
if(!_search.trim())return _rows;
|
||||
const s=_search.trim().toUpperCase();
|
||||
return _rows.filter(r=>(r.itemCode||'').toUpperCase().includes(s)||(r.itemName||'').toUpperCase().includes(s));
|
||||
}
|
||||
|
||||
// Inserts a subtotal row (Rec./Iss. Qty sums) whenever the item OR the
|
||||
// selected bucket(s) change — Daily/Monthly/Yearly toggles, matching SAP's
|
||||
// own "Display Subtotal" checkboxes. Rows are already server-sorted by
|
||||
// item then date, so a simple single pass is enough.
|
||||
function bucketKey(row,gran){
|
||||
const d=new Date(row.docDate);
|
||||
if(gran==='d')return row.itemCode+'|'+d.toISOString().slice(0,10);
|
||||
if(gran==='m')return row.itemCode+'|'+d.getFullYear()+'-'+String(d.getMonth()+1).padStart(2,'0');
|
||||
return row.itemCode+'|'+d.getFullYear();
|
||||
}
|
||||
function buildDisplayRows(list){
|
||||
const grans=[];
|
||||
if(_f.subDaily)grans.push('d');
|
||||
if(_f.subMonthly)grans.push('m');
|
||||
if(_f.subYearly)grans.push('y');
|
||||
if(!grans.length)return list.map(r=>({row:r}));
|
||||
const out=[];
|
||||
let prevKeys=null;
|
||||
let acc=null;
|
||||
const flush=()=>{
|
||||
if(!acc)return;
|
||||
const labels={d:'Daily Subtotal',m:'Monthly Subtotal',y:'Yearly Subtotal'};
|
||||
grans.forEach(g=>{
|
||||
out.push({subtotal:true,label:labels[g],inQty:acc[g].inQty,outQty:acc[g].outQty});
|
||||
});
|
||||
acc=null;
|
||||
};
|
||||
list.forEach(r=>{
|
||||
const keys=grans.map(g=>bucketKey(r,g));
|
||||
if(prevKeys&&keys.join('§')!==prevKeys.join('§')){flush();}
|
||||
if(!acc)acc=Object.fromEntries(grans.map(g=>[g,{inQty:0,outQty:0}]));
|
||||
grans.forEach(g=>{acc[g].inQty+=r.inQty;acc[g].outQty+=r.outQty;});
|
||||
out.push({row:r});
|
||||
prevKeys=keys;
|
||||
});
|
||||
flush();
|
||||
return out;
|
||||
}
|
||||
|
||||
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);
|
||||
const displayRows=buildDisplayRows(pageList);
|
||||
app.innerHTML=`
|
||||
<div class="page-head">
|
||||
<div class="page-icon">📒</div>
|
||||
<div><div class="page-title">Inventory Posting List</div><div class="page-sub">Every stock-moving transaction per item, with a running Balance — same as SAP B1's own Inventory Posting List 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>
|
||||
<div><label class="fl">Item Group</label><select class="fi" id="f-itemgroup">${grpOptions(_f.itemGroup)}</select></div>
|
||||
<div><label class="fl">Date</label><div class="range-pair"><input class="fi" id="f-datefrom" type="date" value="${esc(_f.dateFrom)}"/><span>to</span><input class="fi" id="f-dateto" type="date" value="${esc(_f.dateTo)}"/></div></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-hidenoqty" ${_f.hideNoQtyChange?'checked':''}/> Hide Trans. without Qty Change</label>
|
||||
<div style="display:flex;gap:16px;margin-top:4px">
|
||||
<span class="fl" style="margin:0;align-self:center">Display Subtotal:</span>
|
||||
<label class="chk-row" style="padding:4px 0"><input type="checkbox" id="f-subdaily" ${_f.subDaily?'checked':''}/> Daily</label>
|
||||
<label class="chk-row" style="padding:4px 0"><input type="checkbox" id="f-submonthly" ${_f.subMonthly?'checked':''}/> Monthly</label>
|
||||
<label class="chk-row" style="padding:4px 0"><input type="checkbox" id="f-subyearly" ${_f.subYearly?'checked':''}/> Yearly</label>
|
||||
</div>
|
||||
<div style="margin-top:10px">
|
||||
<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} transaction(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 transactions found for these filters.</div>`
|
||||
:`<div class="tbl-wrap"><table class="lst"><thead><tr>
|
||||
<th class="num">#</th><th>Posting Date</th><th>Item No.</th><th>Item Description</th><th>Document</th><th>Whse</th>
|
||||
<th class="num">Doc.Row</th><th>BP Name</th><th class="num">Rec. Qty</th><th class="num">Iss. Qty</th>
|
||||
<th class="num">Price</th><th class="num">Balance</th><th>Remarks</th><th>Ref. 2</th>
|
||||
</tr></thead><tbody>${displayRows.map((d,i)=>{
|
||||
if(d.subtotal)return `<tr class="subtotal"><td colspan="8">${esc(d.label)}</td><td class="num">${num3(d.inQty)}</td><td class="num">${num3(d.outQty)}</td><td colspan="4"></td></tr>`;
|
||||
const r=d.row;
|
||||
return `<tr>
|
||||
<td class="num">${_page*PAGE_SIZE+i+1}</td>
|
||||
<td>${fmtD(r.docDate)}</td>
|
||||
<td class="mono">${esc(r.itemCode)}</td>
|
||||
<td>${esc(r.itemName)}</td>
|
||||
<td>${esc(r.docLabel)}${r.docRef?' '+esc(r.docRef):''}</td>
|
||||
<td>${esc(r.warehouse)}</td>
|
||||
<td class="num">${r.docRow}</td>
|
||||
<td>${esc(r.bpName)}</td>
|
||||
<td class="num">${r.inQty?num3(r.inQty):''}</td>
|
||||
<td class="num">${r.outQty?num3(r.outQty):''}</td>
|
||||
<td class="num">${num3(r.price)}</td>
|
||||
<td class="num">${num3(r.balance)}</td>
|
||||
<td>${esc(r.remarks)}</td>
|
||||
<td>${esc(r.ref2)}</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();};
|
||||
wireItemSearch('f-itemfrom','dd-itemfrom','itemFrom');
|
||||
wireItemSearch('f-itemto','dd-itemto','itemTo');
|
||||
q('#f-itemgroup').onchange=function(){_f.itemGroup=this.value;};
|
||||
q('#f-datefrom').onchange=function(){_f.dateFrom=this.value;};
|
||||
q('#f-dateto').onchange=function(){_f.dateTo=this.value;};
|
||||
q('#f-hidenoqty').onchange=function(){_f.hideNoQtyChange=this.checked;};
|
||||
q('#f-subdaily').onchange=function(){_f.subDaily=this.checked;if(_ran)render();};
|
||||
q('#f-submonthly').onchange=function(){_f.subMonthly=this.checked;if(_ran)render();};
|
||||
q('#f-subyearly').onchange=function(){_f.subYearly=this.checked;if(_ran)render();};
|
||||
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('.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 other
|
||||
// reports in this app (bordered header banner + criteria summary, bordered/
|
||||
// zebra-striped data grid, frozen header row). Exports exactly what's
|
||||
// currently filtered on screen (search box), not just the current page.
|
||||
const XLSX_BLUE='FF1A6FFF', XLSX_INK='FF1E2A44', XLSX_WHITE='FFFFFFFF', XLSX_ZEBRA='FFEFF4FF';
|
||||
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 Posting List',{views:[{state:'frozen',ySplit:6}]});
|
||||
|
||||
ws.mergeCells('A1:N1');
|
||||
const title=ws.getCell('A1');
|
||||
title.value='Inventory Posting List — 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,'Date:',`${_f.dateFrom} to ${_f.dateTo}`],
|
||||
['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=['#','Posting Date','Item No.','Item Description','Document','Whse','Doc.Row','BP Name','Rec. Qty','Iss. Qty','Price','Balance','Remarks','Ref. 2'];
|
||||
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>=8&&i<=11))?'right':'center'};
|
||||
cell.border=xlsxBorder('FF0F172A');
|
||||
});
|
||||
hdrRow.height=20;
|
||||
|
||||
rows.forEach((r,i)=>{
|
||||
const row=ws.getRow(hdrRowNum+1+i);
|
||||
const vals=[i+1,fmtD(r.docDate),r.itemCode,r.itemName,`${r.docLabel}${r.docRef?' '+r.docRef:''}`,r.warehouse,r.docRow,r.bpName,r.inQty||'',r.outQty||'',r.price,r.balance,r.remarks,r.ref2];
|
||||
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>=8&&ci<=11)){cell.numFmt=ci===0?'0':'#,##0.######';cell.alignment={horizontal:'right'};}
|
||||
});
|
||||
});
|
||||
|
||||
ws.columns=[{width:6},{width:12},{width:16},{width:30},{width:20},{width:8},{width:9},{width:22},{width:12},{width:12},{width:12},{width:12},{width:16},{width:14}];
|
||||
|
||||
const buf=await wb.xlsx.writeBuffer();
|
||||
const blob=new Blob([buf],{type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'});
|
||||
const a=document.createElement('a');
|
||||
a.href=URL.createObjectURL(blob);
|
||||
a.download=`Inventory_Posting_List_${_f.dateFrom}_to_${_f.dateTo}.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>
|
||||
Reference in New Issue
Block a user