328 lines
19 KiB
HTML
328 lines
19 KiB
HTML
<!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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');}
|
||
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=31"></script>
|
||
</body>
|
||
</html>
|