first commit
SAP-ERP Portal CI/CD / build (push) Failing after 5m20s

This commit is contained in:
John
2026-09-23 17:31:02 +05:30
commit 69b4e68baf
51657 changed files with 3864077 additions and 0 deletions
+327
View File
@@ -0,0 +1,327 @@
<!DOCTYPE html>
<html lang="en">
<head>
<script src="/auth-guard.js?v=4"></script>
<script src="/company-list.js"></script>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0"/>
<title>PPC 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:1600px;margin:0 auto}
.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)}
select.fi{cursor:pointer}
.filters{display:flex;gap:8px;flex-wrap:wrap;align-items:flex-end}
.filters>div{min-width:130px}
.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)}
.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 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 tfoot td{padding:8px 10px;font-weight:700;color:var(--text);background:var(--surface3);border-top:2px solid var(--border2)}
.lst tfoot td:first-child{text-align:right}
.lst tfoot tr td{position:sticky;bottom:var(--sticky-bottom,0)}
.lst tfoot tr.stock-1a-row td{color:var(--teal)}
.lst tfoot tr.stock-1b-row td{color:var(--amber)}
.lst tfoot tr.balance-1a-row td,.lst tfoot tr.balance-1b-row td{color:var(--purple)}
.b-green{background:var(--green);color:#fff}
.lst tbody tr:hover td{background:rgba(26,111,255,.05)}
.mono{font-family:var(--mono)}
.pill{font-size:9px;font-weight:800;padding:2px 7px;border-radius:20px;text-transform:uppercase;letter-spacing:.3px;white-space:nowrap}
.p-trade{background:rgba(13,187,138,.14);color:#0aa457}
.p-institute{background:rgba(159,90,253,.16);color:#b98bff}
.p-domestic{background:rgba(6,182,212,.14);color:var(--teal)}
.p-export{background:rgba(245,166,35,.14);color:var(--amber)}
.p-ref{background:rgba(26,111,255,.14);color:#5b9bff;font-family:var(--mono)}
.p-ref-ex{background:rgba(245,166,35,.14);color:#c78a1a;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}
.tbl-wrap{overflow-x:auto;max-height:70vh;overflow-y:auto;border-radius:8px;border:1px solid var(--border2)}
.item-hd{font-size:9px}
.item-hd .code{display:block;font-family:var(--mono);font-weight:700;color:var(--text)}
.item-hd .name{display:block;font-weight:400;text-transform:none;color:var(--text3);max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
</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='/';
const MON=['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
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){const tc=q('#toast-c');const e=document.createElement('div');e.className='toast t-err';e.textContent=m;tc.appendChild(e);setTimeout(()=>e.remove(),4500);}
function fmtD(iso){if(!iso)return '—';const d=new Date(iso);if(isNaN(d))return '—';return `${String(d.getDate()).padStart(2,'0')}-${MON[d.getMonth()]}-${d.getFullYear()}`;}
function today(){return new Date().toISOString().slice(0,10);}
async function api(p){
const r=await fetch(BE+p,{headers:{Authorization:'Bearer '+_tok}});
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 _orderTypes=[]; // admin-managed (System Settings → PPC Report → Order Types) — SAP's U_SalesType is free text, so this isn't a fixed enum
// From/To start empty — leave both blank and "Run Report" shows every open
// (pending) order regardless of date; fill them in to narrow by DocDate.
const _f={from:'',to:'',itemGroup:'',salesType:'',customerType:''};
let _loading=false,_ran=false;
let _itemCodes=[],_rows=[];
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 loadOrderTypes(){
try{const r=await api('/ppc/order-types');_orderTypes=r.data||[];}
catch(_e){_orderTypes=[];}
}
async function loadItemGroups(){
try{const r=await api('/ppc/item-groups?company='+encodeURIComponent(_co));_itemGroups=r.data||[];}
catch(_e){_itemGroups=[];}
}
async function runReport(){
if(_f.to&&_f.to>today()){toast('"To" date cannot be in the future');return;}
if(_f.from&&_f.to&&_f.from>_f.to){toast('"From" date cannot be after "To" date');return;}
_loading=true;_ran=true;render();
const qs=new URLSearchParams();
qs.set('company',_co);
if(_f.from)qs.set('from',_f.from);
if(_f.to)qs.set('to',_f.to);
if(_f.itemGroup)qs.set('itemGroup',_f.itemGroup);
if(_f.salesType)qs.set('salesType',_f.salesType);
if(_f.customerType)qs.set('customerType',_f.customerType);
try{
const r=await api('/ppc/invoice-report?'+qs.toString());
_itemCodes=(r.data&&r.data.itemCodes)||[];
_rows=(r.data&&r.data.rows)||[];
}catch(e){toast(e.message);_itemCodes=[];_rows=[];}
_loading=false;render();
}
function typePill(t){
if(!t)return '—';
const cls=/trade/i.test(t)?'p-trade':/institute/i.test(t)?'p-institute':'';
return `<span class="pill ${cls}">${esc(t)}</span>`;
}
function custTypePill(t){
// Blank means neither Web SO No field is set AND U_CustomerType isn't
// either — i.e. an order entered directly in SAP, not through the portal/
// website. Still a real, included row (see visibleRows()/server-side
// logic — nothing here ever drops it) — just can't be classified
// Domestic/Export, so it's labeled as such instead of a bare dash.
if(!t)return `<span class="pill" style="background:rgba(122,154,184,.14);color:var(--text2)">Direct (SAP)</span>`;
const cls=t==='DOMESTIC'?'p-domestic':t==='EXPORT'?'p-export':'';
return `<span class="pill ${cls}">${esc(t==='DOMESTIC'?'Domestic':t==='EXPORT'?'Export':t)}</span>`;
}
function refBadge(v,cls){
if(v==null||v==='')return '—';
return `<span class="pill ${cls}">${esc(v)}</span>`;
}
// Live client-side search — no re-fetch, just narrows what's already loaded.
// Customer search filters rows by name; Product search filters which item
// columns show AND drops any row left with nothing in those columns.
const _q={customer:'',product:''};
function visibleItemCodes(){
const p=_q.product.trim().toLowerCase();
if(!p)return _itemCodes;
return _itemCodes.filter(it=>it.code.toLowerCase().includes(p)||it.name.toLowerCase().includes(p));
}
function visibleRows(cols){
const c=_q.customer.trim().toLowerCase();
let rows=_rows;
if(c)rows=rows.filter(r=>(r.cardName||'').toLowerCase().includes(c));
if(_q.product.trim())rows=rows.filter(r=>cols.some(it=>r.qtyByItem[it.code]));
return rows;
}
// Per-item column sums across the given (already search-filtered) rows —
// shared by the on-screen totals row and the Excel export.
function columnTotals(cols,rows){
return cols.map(it=>rows.reduce((sum,r)=>sum+(Number(r.qtyByItem[it.code])||0),0));
}
function renderTable(){
if(_loading)return `<div class="empty-msg"><span class="sp"></span> Loading…</div>`;
if(!_ran)return `<div class="empty-msg">Pick your filters and click "Run Report".</div>`;
if(!_rows.length)return `<div class="empty-msg">No pending orders match these filters.</div>`;
const cols=visibleItemCodes();
const rows=visibleRows(cols);
if(!rows.length)return `<div class="empty-msg">No rows match your search.</div>`;
const itemThs=cols.map(it=>`<th class="num item-hd" title="${esc(it.code)} — ${esc(it.name)}"><span class="code">${esc(it.code)}</span><span class="name">(${esc(it.name)})</span></th>`).join('');
const rowsHtml=rows.map((r,i)=>{
const cells=cols.map(it=>{
const v=r.qtyByItem[it.code];
return `<td class="num">${v?esc(v):''}</td>`;
}).join('');
return `<tr>
<td>${i+1}</td>
<td>${esc(r.cardName)}</td>
<td>${esc(r.city)}</td>
<td>${typePill(r.orderType)}</td>
<td>${custTypePill(r.customerType)}</td>
<td>${fmtD(r.dispatchDate)}</td>
<td>${refBadge(r.salesOrderNo,'p-ref')}</td>
<td>${refBadge(r.webSoNo,'p-ref')}</td>
<td>${refBadge(r.webSoNoEx,'p-ref-ex')}</td>
${cells}
</tr>`;
}).join('');
const totals=columnTotals(cols,rows);
// Show only the stock row matching the active Customer Type filter — both
// when no filter is set (nothing to narrow to).
const showDomestic=_f.customerType!=='EXPORT';
const showExport=_f.customerType!=='DOMESTIC';
// Balance = Current Stock − Total Pending Qty, per item — what's left (or
// short) after covering every still-open order shown. Positive = enough
// stock on hand; negative = short by that much, needs producing.
const balance1A=cols.map((it,i)=>(it.stock1A||0)-totals[i]);
const balance1B=cols.map((it,i)=>(it.stock1B||0)-totals[i]);
// Warehouse-wise breakdown, shown as a native hover tooltip on the Current
// Stock cells — full per-warehouse detail without cluttering the table
// with a row per warehouse (see earlier decision: just 1A/1B as rows).
const whTitles=cols.map(it=>(it.byWarehouse&&it.byWarehouse.length)
?it.byWarehouse.map(w=>`${w.code} (${w.name}): ${w.qty}`).join('\n')
:'No stock in any warehouse');
const footRows=[
{cls:'totals-row', label:'Total Pending Qty', show:true, vals:totals},
{cls:'stock-1a-row', label:'Current Stock (Domestic)', show:showDomestic, vals:cols.map(it=>it.stock1A||0), titles:whTitles},
{cls:'stock-1b-row', label:'Current Stock (Export)', show:showExport, vals:cols.map(it=>it.stock1B||0), titles:whTitles},
{cls:'balance-1a-row', label:'Balance (Domestic)', show:showDomestic, vals:balance1A},
{cls:'balance-1b-row', label:'Balance (Export)', show:showExport, vals:balance1B},
].filter(r=>r.show);
const footHtml=footRows.map((r,i)=>{
const bottom=(footRows.length-1-i)*33;
const isBalance=r.cls.startsWith('balance-');
const cells=r.vals.map((v,j)=>{
const t=r.titles?` title="${esc(r.titles[j])}"`:'';
const style=isBalance&&v<0?' style="color:var(--red)"':'';
return `<td class="num"${style}${t}>${esc(v)}</td>`;
}).join('');
return `<tr class="${r.cls}" style="--sticky-bottom:${bottom}px"><td colspan="9">${esc(r.label)}</td>${cells}</tr>`;
}).join('');
return `<div class="tbl-wrap"><table class="lst">
<thead><tr><th>Sr.No</th><th>Name of Customer</th><th>City</th><th>Order Type</th><th>Customer Type</th><th>Committed Dispatch Date</th><th>Sale Order No</th><th>Web SO No (Domestic)</th><th>Web SO No (Export)</th>${itemThs}</tr></thead>
<tbody>${rowsHtml}</tbody>
<tfoot>${footHtml}</tfoot>
</table></div>`;
}
function exportExcel(){
const cols=visibleItemCodes();
const rows=visibleRows(cols);
if(!rows.length){toast('Nothing to export');return;}
const csvCell=v=>{const s=String(v==null?'':v);return /[",\r\n]/.test(s)?'"'+s.replace(/"/g,'""')+'"':s;};
const header=['Sr.No','Name of Customer','City','Order Type','Customer Type','Committed Dispatch Date','Sale Order No','Web SO No (Domestic)','Web SO No (Export)',...cols.map(it=>`${it.code} (${it.name})`)];
const lines=[header.map(csvCell).join(',')];
rows.forEach((r,i)=>{
lines.push([i+1,r.cardName,r.city,r.orderType,r.customerType||'',fmtD(r.dispatchDate),r.salesOrderNo||'',r.webSoNo||'',r.webSoNoEx||'',...cols.map(it=>r.qtyByItem[it.code]||0)].map(csvCell).join(','));
});
const totals=columnTotals(cols,rows);
lines.push(['','','Total Pending Qty','','','','','','',...totals].map(csvCell).join(','));
if(_f.customerType!=='EXPORT')lines.push(['','','Current Stock (Domestic)','','','','','','',...cols.map(it=>it.stock1A||0)].map(csvCell).join(','));
if(_f.customerType!=='DOMESTIC')lines.push(['','','Current Stock (Export)','','','','','','',...cols.map(it=>it.stock1B||0)].map(csvCell).join(','));
if(_f.customerType!=='EXPORT')lines.push(['','','Balance (Domestic)','','','','','','',...cols.map((it,i)=>(it.stock1A||0)-totals[i])].map(csvCell).join(','));
if(_f.customerType!=='DOMESTIC')lines.push(['','','Balance (Export)','','','','','','',...cols.map((it,i)=>(it.stock1B||0)-totals[i])].map(csvCell).join(','));
const blob=new Blob([''+lines.join('\r\n')],{type:'text/csv;charset=utf-8'});
const a=document.createElement('a');
a.href=URL.createObjectURL(blob);
a.download=`PPC_Report_${_f.from||'all'}_to_${_f.to||'all'}.csv`;
a.click();
URL.revokeObjectURL(a.href);
}
function resultCountText(){
const cols=visibleItemCodes(),rows=visibleRows(cols);
return `${rows.length.toLocaleString()} order${rows.length===1?'':'s'} · ${cols.length} item${cols.length===1?'':'s'}`;
}
// Search inputs only ever patch #results + the count badge directly — never
// a full render() — so typing never rebuilds (and loses focus on) the very
// input the user is typing into. Same principle used throughout this app's
// other live-search fields.
function refreshResults(){
const res=q('#results');if(res)res.innerHTML=renderTable();
const cnt=q('#result-count');if(cnt)cnt.textContent=_ran&&!_loading?resultCountText():'';
}
function render(){
const app=q('#app');
app.innerHTML=`
<div class="card"><div class="card-hd"><span>📋 PPC Report</span><span class="right" id="result-count">${_ran&&!_loading?resultCountText():''}</span></div>
<div class="card-bd">
<div class="filters">
${_companies.length>1?`<div><label class="fl">Company</label><select class="fi" id="f-co">${coOptions(_co)}</select></div>`:''}
<div><label class="fl">From</label><input class="fi" type="date" id="f-from" value="${_f.from}" max="${_f.to||today()}" title="Leave blank (with To) to show every open order regardless of date"/></div>
<div><label class="fl">To</label><input class="fi" type="date" id="f-to" value="${_f.to}" min="${_f.from||''}" max="${today()}" title="Leave blank (with From) to show every open order regardless of date"/></div>
<div><label class="fl">Item Group</label><select class="fi" id="f-grp">${grpOptions(_f.itemGroup)}</select></div>
<div><label class="fl">Order Type</label><select class="fi" id="f-order">
<option value="">All</option>
${_orderTypes.map(t=>`<option value="${esc(t)}" ${_f.salesType===t?'selected':''}>${esc(t)}</option>`).join('')}
</select></div>
<div><label class="fl">Customer Type</label><select class="fi" id="f-cust">
<option value="">All</option>
<option value="DOMESTIC" ${_f.customerType==='DOMESTIC'?'selected':''}>Domestic</option>
<option value="EXPORT" ${_f.customerType==='EXPORT'?'selected':''}>Export</option>
</select></div>
<button class="btn b-primary" id="f-go" ${_loading?'disabled':''}>${_loading?'<span class="sp"></span> Running…':'Run Report'}</button>
</div>
<div class="filters" style="margin-top:8px">
<div style="min-width:220px;flex:1"><label class="fl">Search Customer</label><input class="fi" id="q-customer" placeholder="Customer name…" value="${esc(_q.customer)}"/></div>
<div style="min-width:220px;flex:1"><label class="fl">Search Product</label><input class="fi" id="q-product" placeholder="Item code or name…" value="${esc(_q.product)}"/></div>
<button class="btn b-green" id="f-export" ${_ran&&_rows.length?'':'disabled'}>⬇ Download Excel</button>
</div>
<div style="margin-top:12px" id="results">${renderTable()}</div>
</div></div>`;
wire();
}
function wire(){
if(q('#f-co'))q('#f-co').onchange=async function(){_co=this.value;await loadItemGroups();render();};
q('#f-from').onchange=function(){_f.from=this.value;render();};
q('#f-to').onchange=function(){_f.to=this.value;render();};
q('#f-grp').onchange=function(){_f.itemGroup=this.value;};
q('#f-order').onchange=function(){_f.salesType=this.value;};
q('#f-cust').onchange=function(){_f.customerType=this.value;};
q('#f-go').onclick=runReport;
q('#q-customer').oninput=function(){_q.customer=this.value;refreshResults();};
q('#q-product').oninput=function(){_q.product=this.value;refreshResults();};
if(q('#f-export'))q('#f-export').onclick=exportExcel;
}
(async function init(){
try{const list=await window.fetchCompanyList();if(list.length){_companies=list;if(!_co)_co=list[0].value;}}catch(_e){}
await Promise.all([loadItemGroups(),loadOrderTypes()]);
render();
})();
</script>
<script src="/sidebar.js?v=25"></script>
</body>
</html>