344 lines
19 KiB
HTML
344 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>Inventory Transfer — 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;--green2:#059669;--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:900px;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(6,182,212,.14);color:var(--teal);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}
|
|
.fg{margin-bottom:10px}
|
|
.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(--teal)}
|
|
.fi-mono{font-family:var(--mono)}
|
|
select.fi{cursor:pointer}
|
|
.fi-row{display:grid;grid-template-columns:1fr 1fr;gap: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(--teal);color:#042b33}
|
|
.b-primary:disabled{opacity:.5;cursor:not-allowed}
|
|
.b-cancel{background:var(--surface2);color:var(--text2);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(--teal);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(6,182,212,.1)}
|
|
.sd-code{font-family:var(--mono);font-size:11px;color:var(--teal);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}
|
|
.line-card{background:var(--surface2);border:1px solid var(--border2);border-radius:9px;padding:10px 12px;margin-bottom:10px}
|
|
.line-top{display:flex;align-items:center;gap:8px;margin-bottom:8px}
|
|
.line-num{font-size:11px;font-weight:700;color:var(--text3)}
|
|
.line-remove{margin-left:auto;background:none;border:none;color:var(--red);cursor:pointer;font-size:14px;padding:2px 6px}
|
|
.line-grid{display:grid;grid-template-columns:2fr 1fr;gap:8px}
|
|
.line-item-code{font-family:var(--mono);font-weight:700;color:var(--teal);font-size:12.5px}
|
|
.line-item-name{font-size:11px;color:var(--text2);margin-top:1px}
|
|
.batch-sec{margin-top:10px;border-top:1px dashed var(--border2);padding-top:8px}
|
|
.batch-title{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.3px;color:var(--text3);margin-bottom:6px}
|
|
.batch-auto{color:var(--teal);font-weight:400;text-transform:none;letter-spacing:0}
|
|
.batch-row{display:flex;align-items:center;gap:8px;padding:4px 0;font-size:11.5px}
|
|
.batch-num{font-family:var(--mono);color:var(--text);min-width:150px}
|
|
.batch-avail{color:var(--text3);min-width:90px}
|
|
.batch-inp{width:90px;padding:5px 7px;border:1px solid var(--border2);border-radius:6px;background:var(--surface);color:var(--text);font-family:var(--mono);font-size:11.5px;text-align:right}
|
|
.batch-total-row{display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;padding:6px 8px;border-radius:6px}
|
|
.empty-msg{text-align:center;padding:26px;color:var(--text3);font-size:12px}
|
|
.sp{width:14px;height:14px;border:2px solid rgba(6,182,212,.25);border-top-color:var(--teal);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}
|
|
.success-box{text-align:center;padding:30px 20px}
|
|
.success-box .big{font-size:40px;color:var(--green);margin-bottom:10px}
|
|
</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 fmtQty(n){return(Math.round((parseFloat(n)||0)*1000)/1000).toString();}
|
|
|
|
async function api(method,p,body){
|
|
const headers={Authorization:'Bearer '+_tok};
|
|
const opts={method,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;
|
|
}
|
|
|
|
// Per-company SAP login (see [[per-company-sap-login]]) — each user can have
|
|
// a different SAP User/Password per company, embedded in the JWT. Checked
|
|
// client-side BEFORE attempting to post, same pattern as every other SAP-
|
|
// posting page (e.g. Receipt from Production), so switching Company here
|
|
// re-checks against the newly selected company every time.
|
|
function hasSapLogin(){try{const p=JSON.parse(atob(_tok.split('.')[1]));return !!(p&&p.sapLogins&&p.sapLogins[_co]&&p.sapLogins[_co].pwdEnc);}catch(_e){return false;}}
|
|
function requireSapLogin(){if(!hasSapLogin()){toast(`No SAP login set for company "${_co}" — open Profile → "My SAP Account" and add one before posting a transfer.`,'err');return false;}return true;}
|
|
|
|
let _co=window.__DEFAULT_COMPANY__||'';
|
|
let _companies=[];
|
|
let _wh=[];
|
|
let _fromWh='',_toWh='',_postDate='',_remarks='';
|
|
let _lines=[]; // [{itemCode,itemName,quantity,batches,batchLoading,selectedBatches}]
|
|
let _submitting=false;
|
|
let _success=null;
|
|
let _seriesInfo=null; // {series,seriesName} — admin-configured document series, resolved to its SAP name
|
|
|
|
function coOptions(sel){return _companies.map(c=>`<option value="${esc(c.value)}" ${sel===c.value?'selected':''}>${esc(c.label)}</option>`).join('');}
|
|
function whOptions(sel){return `<option value="">— select —</option>`+_wh.map(w=>`<option value="${esc(w.code)}" ${sel===w.code?'selected':''}>${esc(w.code)}${w.name?' - '+esc(w.name):''}</option>`).join('');}
|
|
|
|
async function loadWarehouses(){
|
|
try{const r=await api('GET',`/sap/lookup/warehouses?company=${encodeURIComponent(_co)}`);if(r.success&&r.data?.length)_wh=r.data;}catch(_e){}
|
|
}
|
|
async function loadSeriesInfo(){
|
|
try{const r=await api('GET',`/inventory-transfer/series-info?company=${encodeURIComponent(_co)}`);_seriesInfo=r.success?{series:r.series,seriesName:r.seriesName}:null;}
|
|
catch(_e){_seriesInfo=null;}
|
|
}
|
|
async function fetchItems(v){
|
|
try{const r=await api('GET',`/sap/lookup/items?q=${encodeURIComponent(v)}&company=${encodeURIComponent(_co)}`);if(r.success&&r.data?.length)return r.data;}catch(_e){}
|
|
return [];
|
|
}
|
|
|
|
function addLine(){
|
|
_lines.push({itemCode:'',itemName:'',quantity:0,batches:[],batchLoading:false,selectedBatches:[]});
|
|
render();
|
|
}
|
|
function removeLine(i){ _lines.splice(i,1); render(); }
|
|
|
|
// Same FEFO auto-select pattern as Issue for Production (public/issue-
|
|
// production.html's autoSelectBatches) — earliest-expiry batches first,
|
|
// greedily filled until the requested quantity is covered.
|
|
async function loadBatches(line){
|
|
if(!line.itemCode||!_fromWh){line.batches=[];line.selectedBatches=[];return;}
|
|
line.batchLoading=true;render();
|
|
try{
|
|
const r=await api('GET',`/sap/lookup/batches?item=${encodeURIComponent(line.itemCode)}&warehouse=${encodeURIComponent(_fromWh)}&company=${encodeURIComponent(_co)}`);
|
|
line.batches=r.data||[];
|
|
if(line.batches.length) autoSelectBatches(line);
|
|
}catch(_e){line.batches=[];}
|
|
line.batchLoading=false;render();
|
|
}
|
|
function autoSelectBatches(line){
|
|
let rem=parseFloat(line.quantity)||0;
|
|
line.selectedBatches=[];
|
|
for(const b of line.batches){
|
|
if(rem<=0)break;
|
|
const take=Math.min(b.Quantity,rem);
|
|
line.selectedBatches.push({BatchNumber:b.BatchNumber,Quantity:take});
|
|
rem-=take;
|
|
}
|
|
}
|
|
|
|
let _codeSearchTimer=null;
|
|
function wireItemSearch(i){
|
|
const inp=q(`#li-item-${i}`),dd=q(`#li-dd-${i}`);if(!inp||!dd)return;
|
|
inp.oninput=function(){
|
|
const l=_lines[i];
|
|
l.itemCode=this.value.toUpperCase();l.itemName='';l.batches=[];l.selectedBatches=[];
|
|
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(l.itemCode!==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.ItemCode)}</span><span class="sd-name">${esc(it.ItemName)}</span>`;
|
|
row.onclick=()=>{
|
|
l.itemCode=it.ItemCode;l.itemName=it.ItemName||'';
|
|
dd.style.display='none';render();loadBatches(l);
|
|
};
|
|
dd.appendChild(row);
|
|
});
|
|
},250);
|
|
};
|
|
}
|
|
document.addEventListener('click',e=>{
|
|
if(!e.target.closest('.search-wrap'))document.querySelectorAll('.search-dd').forEach(d=>d.style.display='none');
|
|
});
|
|
|
|
function lineBatchHtml(l,i){
|
|
if(l.batchLoading)return `<div class="batch-sec"><span class="sp"></span> Loading batches…</div>`;
|
|
if(!l.batches.length)return'';
|
|
const total=l.selectedBatches.reduce((s,b)=>s+(b.Quantity||0),0);
|
|
const ok=Math.abs(total-(parseFloat(l.quantity)||0))<=0.001;
|
|
return `<div class="batch-sec">
|
|
<div class="batch-title">Batches <span class="batch-auto">(auto-selected, FEFO)</span></div>
|
|
<div class="batch-total-row" style="border:1px solid ${ok?'rgba(13,187,138,.35)':'rgba(229,62,62,.4)'};background:${ok?'rgba(13,187,138,.08)':'rgba(229,62,62,.08)'}">
|
|
<span style="font-size:11px;font-weight:700;color:${ok?'var(--green)':'var(--red)'}">Total</span>
|
|
<span style="font-size:12px;font-weight:700;color:${ok?'var(--green)':'var(--red)'}">${fmtQty(total)}${ok?'':` / ${fmtQty(l.quantity)} required`}</span>
|
|
</div>
|
|
${l.batches.map((b,bi)=>{
|
|
const sel=l.selectedBatches.find(s=>s.BatchNumber===b.BatchNumber);
|
|
const qty=sel?sel.Quantity:0;
|
|
return `<div class="batch-row">
|
|
<span class="batch-num">${esc(b.BatchNumber)}</span>
|
|
<span class="batch-avail">Avail: ${fmtQty(b.Quantity)}</span>
|
|
<input class="batch-inp" data-li="${i}" data-bi="${bi}" data-bn="${esc(b.BatchNumber)}" type="number" min="0" max="${b.Quantity}" step="0.001" value="${qty}"/>
|
|
</div>`;
|
|
}).join('')}
|
|
</div>`;
|
|
}
|
|
|
|
function render(){
|
|
const app=q('#app');
|
|
if(_success){
|
|
app.innerHTML=`
|
|
<div class="page-head">
|
|
<div class="page-icon">🔁</div>
|
|
<div><div class="page-title">Inventory Transfer</div><div class="page-sub">Move stock between warehouses — same as SAP B1's own Inventory Transfer window</div></div>
|
|
</div>
|
|
<div class="card"><div class="card-bd success-box">
|
|
<div class="big">✓</div>
|
|
<div style="font-size:15px;font-weight:700;margin-bottom:6px">Inventory Transfer Posted</div>
|
|
<div style="font-size:12px;color:var(--text2);margin-bottom:18px">DocEntry <b>${esc(_success.docEntry)}</b> — DocNum <b>${esc(_success.docNum)}</b><br/>${esc(_fromWh)} → ${esc(_toWh)}</div>
|
|
<button class="btn b-primary" id="btn-new">+ New Inventory Transfer</button>
|
|
</div></div>`;
|
|
q('#btn-new').onclick=()=>{_success=null;_fromWh='';_toWh='';_postDate='';_remarks='';_lines=[];render();};
|
|
return;
|
|
}
|
|
const totalQty=_lines.reduce((s,l)=>s+(parseFloat(l.quantity)||0),0);
|
|
app.innerHTML=`
|
|
<div class="page-head">
|
|
<div class="page-icon">🔁</div>
|
|
<div><div class="page-title">Inventory Transfer</div><div class="page-sub">Move stock between warehouses — batches auto-selected (FEFO) once issued, same as Issue for Production</div></div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-hd"><span>Transfer Details</span>${_seriesInfo?.seriesName?`<span class="right">Series: <b style="color:var(--teal)">${esc(_seriesInfo.seriesName)}</b></span>`:''}</div>
|
|
<div class="card-bd">
|
|
<div class="fg"><label class="fl">Company</label><select class="fi" id="sel-co">${coOptions(_co)}</select></div>
|
|
<div class="fi-row">
|
|
<div class="fg"><label class="fl">From Warehouse *</label><select class="fi" id="sel-fromwh">${whOptions(_fromWh)}</select></div>
|
|
<div class="fg"><label class="fl">To Warehouse *</label><select class="fi" id="sel-towh">${whOptions(_toWh)}</select></div>
|
|
</div>
|
|
<div class="fi-row">
|
|
<div class="fg"><label class="fl">Posting Date</label><input class="fi" id="inp-postdate" type="date" value="${esc(_postDate)}"/></div>
|
|
<div class="fg"><label class="fl">Remarks</label><input class="fi" id="inp-remarks" value="${esc(_remarks)}" placeholder="Optional"/></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-hd"><span>Items</span><span class="right">${_lines.length} line(s)${totalQty?` · Total Qty ${fmtQty(totalQty)}`:''}</span></div>
|
|
<div class="card-bd">
|
|
${!_lines.length?`<div class="empty-msg">No items added yet.</div>`:
|
|
_lines.map((l,i)=>`
|
|
<div class="line-card">
|
|
<div class="line-top"><span class="line-num">#${i+1}</span><button type="button" class="line-remove" data-remove="${i}" title="Remove">✕</button></div>
|
|
<div class="line-grid">
|
|
<div class="search-wrap">
|
|
<label class="fl">Item Code</label>
|
|
<input class="fi fi-mono" id="li-item-${i}" value="${esc(l.itemCode)}" placeholder="Search item code / name…" autocomplete="off"/>
|
|
<div class="search-dd" id="li-dd-${i}"></div>
|
|
${l.itemName?`<div class="line-item-name">${esc(l.itemName)}</div>`:''}
|
|
</div>
|
|
<div><label class="fl">Quantity</label><input class="fi fi-mono" data-qty="${i}" type="number" min="0.001" step="0.001" value="${l.quantity||''}" style="text-align:right"/></div>
|
|
</div>
|
|
${lineBatchHtml(l,i)}
|
|
</div>`).join('')}
|
|
<button type="button" class="btn b-cancel" id="btn-add-line">+ Add Item</button>
|
|
</div>
|
|
</div>
|
|
<div class="card"><div class="card-bd" style="display:flex;gap:10px">
|
|
<button class="btn b-primary" id="btn-ok" style="flex:1" ${_submitting?'disabled':''}>${_submitting?'<span class="sp"></span> Posting…':'OK — Post Transfer'}</button>
|
|
<button class="btn b-cancel" id="btn-cancel" style="flex:1">Cancel</button>
|
|
</div></div>`;
|
|
wire();
|
|
}
|
|
|
|
function wire(){
|
|
if(_success)return;
|
|
q('#sel-co').onchange=async function(){_co=this.value;_wh=[];_fromWh='';_toWh='';_lines=[];await Promise.all([loadWarehouses(),loadSeriesInfo()]);render();};
|
|
q('#sel-fromwh').onchange=function(){_fromWh=this.value;_lines.forEach(l=>{l.batches=[];l.selectedBatches=[];if(l.itemCode)loadBatches(l);});render();};
|
|
q('#sel-towh').onchange=function(){_toWh=this.value;};
|
|
q('#inp-postdate').onchange=function(){_postDate=this.value;};
|
|
q('#inp-remarks').oninput=function(){_remarks=this.value;};
|
|
q('#btn-add-line').onclick=addLine;
|
|
q('#btn-cancel').onclick=()=>{_fromWh='';_toWh='';_postDate='';_remarks='';_lines=[];render();};
|
|
q('#btn-ok').onclick=doTransfer;
|
|
document.querySelectorAll('[data-remove]').forEach(btn=>{btn.onclick=()=>removeLine(parseInt(btn.dataset.remove));});
|
|
document.querySelectorAll('[data-qty]').forEach(inp=>{
|
|
inp.onchange=function(){
|
|
const i=parseInt(this.dataset.qty);
|
|
_lines[i].quantity=parseFloat(this.value)||0;
|
|
if(_lines[i].batches.length) autoSelectBatches(_lines[i]);
|
|
render();
|
|
};
|
|
});
|
|
document.querySelectorAll('.batch-inp').forEach(inp=>{
|
|
inp.onchange=function(){
|
|
const li=parseInt(this.dataset.li),bn=this.dataset.bn;
|
|
const qty=parseFloat(this.value)||0;
|
|
const l=_lines[li];
|
|
const existing=l.selectedBatches.find(b=>b.BatchNumber===bn);
|
|
if(existing)existing.Quantity=qty;
|
|
else if(qty>0)l.selectedBatches.push({BatchNumber:bn,Quantity:qty});
|
|
render();
|
|
};
|
|
});
|
|
_lines.forEach((_,i)=>wireItemSearch(i));
|
|
}
|
|
|
|
async function doTransfer(){
|
|
if(!_fromWh||!_toWh){toast('Select From and To Warehouse','err');return;}
|
|
if(_fromWh===_toWh){toast('From and To Warehouse must be different','err');return;}
|
|
const valid=_lines.filter(l=>l.itemCode&&(parseFloat(l.quantity)||0)>0);
|
|
if(!valid.length){toast('Add at least one item with a quantity','err');return;}
|
|
for(const l of valid){
|
|
if(l.batches.length){
|
|
const total=l.selectedBatches.reduce((s,b)=>s+(b.Quantity||0),0);
|
|
if(Math.abs(total-l.quantity)>0.001){toast(`${l.itemCode}: batch qty (${fmtQty(total)}) must equal quantity (${fmtQty(l.quantity)})`,'err');return;}
|
|
}
|
|
}
|
|
if(!requireSapLogin())return;
|
|
_submitting=true;render();
|
|
try{
|
|
const payload={
|
|
company:_co,fromWarehouse:_fromWh,toWarehouse:_toWh,
|
|
postingDate:_postDate||undefined,comments:_remarks||undefined,
|
|
lines:valid.map(l=>({itemCode:l.itemCode,quantity:l.quantity,batchNumbers:l.selectedBatches})),
|
|
};
|
|
const r=await api('POST','/inventory-transfer',payload);
|
|
_success={docEntry:r.data?.DocEntry||'',docNum:r.data?.DocNum||''};
|
|
toast('Inventory Transfer posted!','ok');
|
|
}catch(e){toast(e.message||'Failed','err');}
|
|
_submitting=false;render();
|
|
}
|
|
|
|
async function init(){
|
|
try{const list=await window.fetchCompanyList();if(list.length)_companies=list;}catch(_e){}
|
|
await Promise.all([loadWarehouses(),loadSeriesInfo()]);
|
|
render();
|
|
}
|
|
init();
|
|
</script>
|
|
<script src="/sidebar.js?v=31"></script>
|
|
</body>
|
|
</html>
|