@@ -0,0 +1,234 @@
|
||||
<!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>Deviations — 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;--extra:#ec4899;--extrasevere:#ea580c;--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:1500px;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(234,88,12,.14);color:var(--extrasevere);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)}
|
||||
select.fi{cursor:pointer}
|
||||
.filters{display:flex;gap:8px;flex-wrap:wrap;align-items:flex-end}
|
||||
.filters>div{min-width:150px}
|
||||
.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-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 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:70vh;overflow-y:auto;border-radius:8px;border:1px solid var(--border2)}
|
||||
.pill{font-size:8.5px;font-weight:700;padding:2px 8px;border-radius:999px;white-space:nowrap;display:inline-block}
|
||||
.link{color:var(--blue);text-decoration:none;cursor:pointer;font-weight:600}
|
||||
.link:hover{text-decoration:underline}
|
||||
</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 fmtDT(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()} ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`;}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Same labels/colors as public/production.html's own deviation cards —
|
||||
// kept in sync so a type/status looks identical wherever it's shown.
|
||||
function devTypeLabel(t){return t==='SHORTAGE'?'Shortage':t==='SUBSTITUTION'?'Substitution':t==='DAMAGE'?'Damage/Loss':t==='ADDITION'?'Addition':t;}
|
||||
function devTypeColor(t){return t==='SHORTAGE'?'var(--amber)':t==='SUBSTITUTION'?'var(--extra)':t==='ADDITION'?'var(--green)':'var(--extrasevere)';}
|
||||
function qaBadge(status){
|
||||
if(status==='N_A')return '<span style="color:var(--text3)">—</span>';
|
||||
if(status==='PENDING')return '<span class="pill" style="background:rgba(245,166,35,.15);color:var(--amber)">PENDING</span>';
|
||||
if(status==='APPROVED')return '<span class="pill" style="background:rgba(13,187,138,.15);color:var(--green)">APPROVED</span>';
|
||||
if(status==='REJECTED')return '<span class="pill" style="background:rgba(229,62,62,.15);color:var(--red)">REJECTED</span>';
|
||||
return esc(status);
|
||||
}
|
||||
|
||||
let _co=window.__DEFAULT_COMPANY__||'';
|
||||
let _companies=[];
|
||||
let _rows=[];
|
||||
let _loading=false,_loaded=false;
|
||||
const _f={type:'',qaStatus:'',search:'',dateFrom:'',dateTo:''};
|
||||
let _page=0;const PAGE_SIZE=20;
|
||||
|
||||
function coOptions(sel){return _companies.map(c=>`<option value="${esc(c.value)}" ${sel===c.value?'selected':''}>${esc(c.label)}</option>`).join('');}
|
||||
|
||||
async function loadCompanies(){
|
||||
try{const list=await window.fetchCompanyList();if(list.length)_companies=list;}catch(_e){}
|
||||
}
|
||||
|
||||
async function loadDeviations(){
|
||||
_loading=true;_loaded=true;_page=0;render();
|
||||
try{
|
||||
const r=await api('/sap/deviations/report?company='+encodeURIComponent(_co));
|
||||
_rows=r.data||[];
|
||||
}catch(e){toast(e.message,'err');_rows=[];}
|
||||
_loading=false;render();
|
||||
}
|
||||
|
||||
function filteredRows(){
|
||||
return _rows.filter(r=>{
|
||||
if(_f.type&&r.type!==_f.type)return false;
|
||||
if(_f.qaStatus&&r.qaStatus!==_f.qaStatus)return false;
|
||||
if(_f.dateFrom||_f.dateTo){
|
||||
const d=(r.raisedAt||'').slice(0,10);
|
||||
if(!d)return false;
|
||||
if(_f.dateFrom&&d<_f.dateFrom)return false;
|
||||
if(_f.dateTo&&d>_f.dateTo)return false;
|
||||
}
|
||||
if(_f.search){
|
||||
const s=_f.search.trim().toUpperCase();
|
||||
const hay=[r.sapDocNum,r.itemCode,r.itemName,r.newItemCode,r.newItemName,r.raisedName,r.raisedBy,r.reason].map(x=>(x||'').toUpperCase()).join(' ');
|
||||
if(!hay.includes(s))return false;
|
||||
}
|
||||
return true;
|
||||
}).sort((a,b)=>new Date(b.raisedAt||0)-new Date(a.raisedAt||0));
|
||||
}
|
||||
|
||||
function render(){
|
||||
const app=q('#app');
|
||||
const allList=filteredRows();
|
||||
const totalPages=Math.max(1,Math.ceil(allList.length/PAGE_SIZE));
|
||||
if(_page>=totalPages)_page=totalPages-1;
|
||||
if(_page<0)_page=0;
|
||||
const list=allList.slice(_page*PAGE_SIZE,(_page+1)*PAGE_SIZE);
|
||||
app.innerHTML=`
|
||||
<div class="page-head">
|
||||
<div class="page-icon">⚠️</div>
|
||||
<div><div class="page-title">Deviations</div><div class="page-sub">Every Shortage / Substitution / Damage / Addition raised across all Production Orders, in one place</div></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-hd"><span>Filters</span></div>
|
||||
<div class="card-bd">
|
||||
<div class="filters">
|
||||
<div style="min-width:180px"><label class="fl">Company</label><select class="fi" id="sel-co">${coOptions(_co)}</select></div>
|
||||
<div><label class="fl">Type</label><select class="fi" id="f-type">
|
||||
<option value="">All Types</option>
|
||||
<option value="SHORTAGE" ${_f.type==='SHORTAGE'?'selected':''}>Shortage</option>
|
||||
<option value="SUBSTITUTION" ${_f.type==='SUBSTITUTION'?'selected':''}>Substitution</option>
|
||||
<option value="DAMAGE" ${_f.type==='DAMAGE'?'selected':''}>Damage/Loss</option>
|
||||
<option value="ADDITION" ${_f.type==='ADDITION'?'selected':''}>Addition</option>
|
||||
</select></div>
|
||||
<div><label class="fl">QA Status</label><select class="fi" id="f-qa">
|
||||
<option value="">All</option>
|
||||
<option value="PENDING" ${_f.qaStatus==='PENDING'?'selected':''}>Pending</option>
|
||||
<option value="APPROVED" ${_f.qaStatus==='APPROVED'?'selected':''}>Approved</option>
|
||||
<option value="REJECTED" ${_f.qaStatus==='REJECTED'?'selected':''}>Rejected</option>
|
||||
<option value="N_A" ${_f.qaStatus==='N_A'?'selected':''}>Not Required</option>
|
||||
</select></div>
|
||||
<div><label class="fl">Raised Date</label><div style="display:flex;gap:6px;align-items:center">
|
||||
<input class="fi" id="f-datefrom" type="date" value="${esc(_f.dateFrom)}"/><span style="font-size:11px;color:var(--text3)">to</span><input class="fi" id="f-dateto" type="date" value="${esc(_f.dateTo)}"/>
|
||||
</div></div>
|
||||
<div style="flex:1;min-width:220px"><label class="fl">Search</label><input class="fi" id="f-search" placeholder="PWO#, Item, Raised By, Reason…" value="${esc(_f.search)}"/></div>
|
||||
<div><button class="btn b-primary" id="btn-run" ${_loading?'disabled':''}>${_loading?'Loading…':'Refresh'}</button></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-hd"><span>Deviated Production Orders</span><span class="right">${_loaded&&!_loading?`<span class="count">${allList.length} deviation(s)</span>`:''}</span></div>
|
||||
<div class="card-bd">
|
||||
${_loading?`<div class="empty-msg"><span class="sp"></span> Loading…</div>`
|
||||
:!_loaded?`<div class="empty-msg">Loading…</div>`
|
||||
:!allList.length?`<div class="empty-msg">No deviations found.</div>`
|
||||
:`<div class="tbl-wrap"><table class="lst"><thead><tr>
|
||||
<th class="num">#</th><th>PWO #</th><th>Item</th><th>Type</th><th class="num">Qty</th><th>Warehouse</th>
|
||||
<th>Reason</th><th>Raised By</th><th>Raised At</th><th>QA Status</th><th>Reversed</th><th></th>
|
||||
</tr></thead><tbody>${list.map((r,i)=>`
|
||||
<tr>
|
||||
<td class="num">${_page*PAGE_SIZE+i+1}</td>
|
||||
<td class="mono">${esc(r.sapDocNum||'—')}</td>
|
||||
<td>${r.type==='SUBSTITUTION'||r.type==='ADDITION'?`${esc(r.itemCode||'')}${r.newItemCode?` → <b>${esc(r.newItemCode)}</b>`:''}`:esc(r.itemCode||'')}<div style="font-size:10px;color:var(--text3)">${esc(r.itemName||r.newItemName||'')}</div></td>
|
||||
<td><span class="pill" style="background:rgba(255,255,255,.06);color:${devTypeColor(r.type)}">${devTypeLabel(r.type)}</span></td>
|
||||
<td class="num">${num3(r.quantity)}</td>
|
||||
<td>${esc(r.destWarehouse||r.warehouse||'')}</td>
|
||||
<td style="white-space:normal;max-width:220px">${esc(r.reason||'')}</td>
|
||||
<td>${esc(r.raisedName||r.raisedBy||'')}</td>
|
||||
<td>${fmtDT(r.raisedAt)}</td>
|
||||
<td>${qaBadge(r.qaStatus)}</td>
|
||||
<td>${r.reversed?'<span style="color:var(--red)">Yes</span>':'—'}</td>
|
||||
<td>${r.sapAbsEntry?`<a class="link" href="/production?open=${r.sapAbsEntry}">Open PWO ›</a>`:''}</td>
|
||||
</tr>
|
||||
`).join('')}</tbody></table></div>
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:12px">
|
||||
<button class="btn b-cancel" id="dev-prev" ${_page===0?'disabled':''} style="padding:6px 14px;font-size:11px">‹ Prev</button>
|
||||
<span style="font-size:11px;color:var(--text3)">Page ${_page+1} of ${totalPages} · ${allList.length} deviation${allList.length===1?'':'s'}</span>
|
||||
<button class="btn b-cancel" id="dev-next" ${_page>=totalPages-1?'disabled':''} style="padding:6px 14px;font-size:11px">Next ›</button>
|
||||
</div>`}
|
||||
</div>
|
||||
</div>`;
|
||||
wire();
|
||||
}
|
||||
|
||||
function wire(){
|
||||
q('#sel-co').onchange=async function(){_co=this.value;_loaded=false;_rows=[];_page=0;render();await loadDeviations();};
|
||||
q('#f-type').onchange=function(){_f.type=this.value;_page=0;render();};
|
||||
q('#f-qa').onchange=function(){_f.qaStatus=this.value;_page=0;render();};
|
||||
q('#f-datefrom').onchange=function(){_f.dateFrom=this.value;_page=0;render();};
|
||||
q('#f-dateto').onchange=function(){_f.dateTo=this.value;_page=0;render();};
|
||||
q('#f-search').oninput=function(){
|
||||
_f.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){}}
|
||||
};
|
||||
q('#btn-run').onclick=loadDeviations;
|
||||
q('#dev-prev')&&(q('#dev-prev').onclick=()=>{_page--;render();});
|
||||
q('#dev-next')&&(q('#dev-next').onclick=()=>{_page++;render();});
|
||||
}
|
||||
|
||||
async function init(){
|
||||
await loadCompanies();
|
||||
render();
|
||||
await loadDeviations();
|
||||
}
|
||||
init();
|
||||
</script>
|
||||
<script src="/sidebar.js?v=31"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user