Files
John eead8f5ffd
SAP-ERP Portal CI/CD / build (push) Successful in 3m57s
sale order
2026-10-05 18:45:17 +05:30

235 lines
14 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');}
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>