323 lines
24 KiB
HTML
323 lines
24 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>Verify Production — 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&display=swap" rel="stylesheet"/>
|
|
<style>
|
|
:root{--ink:#080e18;--ink2:#0d1626;--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;--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}
|
|
nav{position:sticky;top:0;z-index:200;background:rgba(8,14,24,.92);backdrop-filter:blur(20px);border-bottom:1px solid var(--border);padding:0 12px;height:50px;display:flex;align-items:center;justify-content:space-between;gap:6px}
|
|
.nav-logo{background:var(--blue);color:#fff;font-family:var(--mono);font-weight:700;font-size:14px;letter-spacing:2px;padding:3px 8px;border-radius:5px}
|
|
.nav-links{display:flex;gap:2px;overflow-x:auto;flex:1;justify-content:center}
|
|
.nav-link{padding:4px 8px;border-radius:5px;font-size:10px;font-weight:600;color:var(--text2);text-decoration:none;white-space:nowrap}.nav-link.active{color:var(--blue);background:rgba(26,111,255,.1)}
|
|
.btn-sm{padding:4px 8px;border-radius:5px;font-size:10px;font-weight:700;border:1px solid var(--border2);color:var(--text3);background:none;cursor:pointer;font-family:var(--font)}.btn-sm:hover{border-color:var(--red);color:var(--red)}
|
|
main{padding:12px;max-width:1200px;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}
|
|
.btn{padding:8px 15px;border-radius:8px;border:none;font-family:var(--font);font-size:12px;font-weight:700;cursor:pointer}.btn:disabled{opacity:.45;cursor:not-allowed}
|
|
.b-primary{background:var(--blue);color:#fff}.b-verify{background:var(--green);color:#04231a}.b-verify:hover:not(:disabled){filter:brightness(1.08)}
|
|
.hint{font-size:11px;color:var(--amber);background:rgba(245,166,35,.08);border:1px solid rgba(245,166,35,.22);border-radius:8px;padding:8px 11px;margin-bottom:12px}
|
|
.po{display:flex;gap:14px;align-items:flex-start;padding:12px 14px;background:var(--surface2);border:1px solid var(--border);border-radius:12px;margin-bottom:8px}
|
|
.po-main{flex:1;min-width:0}
|
|
.po-t{font-size:14px;font-weight:700;color:var(--text)}
|
|
.po-sub{font-size:11px;color:var(--text3);margin-top:3px;display:flex;gap:8px;flex-wrap:wrap}
|
|
.po-sub b{color:var(--text2);font-family:var(--mono)}
|
|
.po-right{display:flex;flex-direction:column;gap:6px;align-items:flex-end;min-width:230px}
|
|
.po-right .fi{height:32px}
|
|
.chips{display:flex;gap:5px;flex-wrap:wrap;margin-top:6px}
|
|
.chip{font-size:9px;font-weight:700;color:var(--text2);background:var(--surface3);border:1px solid var(--border2);border-radius:6px;padding:2px 8px;text-transform:uppercase;letter-spacing:.3px}
|
|
.pill{font-size:9px;font-weight:800;padding:2px 8px;border-radius:20px;text-transform:uppercase}
|
|
.p-ok{background:rgba(13,187,138,.14);color:#0aa457}.p-pend{background:rgba(245,166,35,.14);color:#c78a1a}
|
|
.vinfo{font-size:11px;color:var(--green);text-align:right}
|
|
.vinfo b{color:var(--text2)}
|
|
.empty-msg{text-align:center;padding:24px;color:var(--text3);font-size:12px}
|
|
.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)}}
|
|
.tabs{display:flex;gap:6px;margin-bottom:12px}
|
|
.tab{padding:7px 14px;border-radius:8px;font-size:12px;font-weight:700;border:1px solid var(--border2);background:var(--surface2);color:var(--text2);cursor:pointer}
|
|
.tab.on{background:var(--blue);color:#fff;border-color:var(--blue)}
|
|
.toast-c{position:fixed;top:56px;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-ok{background:var(--green);color:#fff}.t-err{background:var(--red);color:#fff}
|
|
.po{cursor:pointer;transition:border-color .15s}
|
|
.po:hover{border-color:var(--blue)}
|
|
.modal-ov{display:none;position:fixed;inset:0;z-index:500;background:rgba(4,8,14,.7);backdrop-filter:blur(3px);align-items:flex-start;justify-content:center;padding:26px 12px;overflow:auto}
|
|
.modal-ov.show{display:flex}
|
|
.modal-w{width:100%;max-width:900px;background:var(--surface);border:1px solid var(--border2);border-radius:14px;box-shadow:var(--shadow);overflow:hidden}
|
|
.modal-hd2{display:flex;align-items:center;gap:10px;padding:13px 16px;background:var(--surface3);border-bottom:1px solid var(--border)}
|
|
.modal-hd2-t{font-size:14px;font-weight:700}
|
|
.modal-x{margin-left:auto;width:28px;height:28px;border-radius:8px;border:1px solid var(--border2);background:none;color:var(--text2);cursor:pointer}
|
|
.modal-x:hover{border-color:var(--red);color:var(--red)}
|
|
.modal-bd2{padding:14px 16px;max-height:calc(100vh - 170px);overflow:auto}
|
|
.dt-grid{display:grid;grid-template-columns:1fr 1fr;gap:2px 18px;font-size:12px;margin-bottom:14px}
|
|
.dt-row{display:flex;justify-content:space-between;gap:10px;padding:5px 0;border-bottom:1px solid var(--border)}
|
|
.dt-l{color:var(--text3);font-size:10.5px;text-transform:uppercase;letter-spacing:.3px}
|
|
.dt-v{color:var(--text);font-weight:600;text-align:right}
|
|
.sec-t{font-size:10px;font-weight:700;text-transform:uppercase;color:var(--text3);margin:12px 0 8px}
|
|
.modal-ft{display:flex;gap:8px;padding:13px 16px;border-top:1px solid var(--border);background:var(--surface3);align-items:center}
|
|
</style>
|
|
<link rel="stylesheet" href="/theme.css?v=3"/>
|
|
</head>
|
|
<body>
|
|
<div class="toast-c" id="toast-c"></div>
|
|
<nav>
|
|
<span class="nav-logo">SAP</span>
|
|
<div class="nav-links"><a href="/" class="nav-link">Home</a><a href="/verify-production" class="nav-link active">Verify Production</a></div>
|
|
<button class="btn-sm" id="btn-logout">Logout</button>
|
|
</nav>
|
|
<main id="app"></main>
|
|
<div class="modal-ov" id="detail-modal"><div class="modal-w" id="detail-modal-inner"></div></div>
|
|
|
|
<script>
|
|
'use strict';
|
|
let _tok=null,_user=null;const BE='/api';
|
|
try{_tok=sessionStorage.getItem('portal_token');const u=sessionStorage.getItem('portal_user');if(u)_user=JSON.parse(u);}catch(_e){}
|
|
if(!_tok)location.href='/';
|
|
document.getElementById('btn-logout').onclick=()=>{sessionStorage.clear();location.href='/';};
|
|
|
|
function hasStepPerm(fullKey,perm){
|
|
if(_user&&_user.role==='admin')return true;
|
|
const steps=Array.isArray(_user&&_user.approvalSteps)?_user.approvalSteps:[];
|
|
for(const s of steps){ if(typeof s==='string'){if(s===fullKey)return true;} else if(s&&s.step===fullKey){return Array.isArray(s.perms)&&s.perms.includes(perm);} }
|
|
return false;
|
|
}
|
|
const CAN_VIEW=hasStepPerm('production_order:verify','view');
|
|
const CAN_VERIFY=hasStepPerm('production_order:verify','approve');
|
|
|
|
let _co=window.__DEFAULT_COMPANY__;let _companies=[{value:_co,label:_co}];
|
|
let _rows=[],_loading=false,_tab='pending',_hasSig=false;
|
|
|
|
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,t='ok'){const c=q('#toast-c');const e=document.createElement('div');e.className='toast t-'+t;e.textContent=m;c.appendChild(e);setTimeout(()=>e.remove(),4200);}
|
|
function fmt(iso){if(!iso)return '—';const d=new Date(iso);if(isNaN(d))return '—';const p=n=>String(n).padStart(2,'0');return `${p(d.getDate())}-${['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'][d.getMonth()]}-${d.getFullYear()} ${p(d.getHours())}:${p(d.getMinutes())}`;}
|
|
function coOptions(sel){return _companies.map(c=>`<option value="${esc(c.value)}" ${sel===c.value?'selected':''}>${esc(c.label)}</option>`).join('');}
|
|
|
|
async function api(m,p,b){
|
|
const h={'Content-Type':'application/json'};if(_tok)h.Authorization='Bearer '+_tok;
|
|
const o={method:m,headers:h};if(b)o.body=JSON.stringify(b);
|
|
const r=await fetch(BE+p,o);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;
|
|
}
|
|
|
|
async function load(){
|
|
_loading=true;render();
|
|
try{const r=await api('GET',`/production-orders/for-verification?company=${encodeURIComponent(_co)}`);_rows=r.data||[];}
|
|
catch(e){toast(e.message,'err');_rows=[];}
|
|
_loading=false;render();
|
|
}
|
|
async function doVerify(id,fromModal){
|
|
const rem=(q(fromModal?'#md-rem':'#rem-'+id)||{}).value||'';
|
|
const btn=q(fromModal?'#md-vb':'#vb-'+id);if(btn){btn.disabled=true;btn.innerHTML='<span class="sp"></span>';}
|
|
try{
|
|
await api('POST',`/production-orders/${id}/verify`,{company:_co,remarks:rem});
|
|
toast('Production order verified');
|
|
closeDetail();
|
|
load();
|
|
}catch(e){toast(e.message,'err');if(btn){btn.disabled=false;btn.textContent='✔ Verify';}}
|
|
}
|
|
|
|
// ── Full production order detail (same QA comparison as the Production page) ──
|
|
async function fetchWoApprovedMap(p){
|
|
if(!p.workOrderId)return null;
|
|
try{
|
|
const r=await api('GET',`/work-orders/${p.workOrderId}`);
|
|
const w=r.data||r;const map={};
|
|
(w.rawMaterials||[]).forEach(m=>{const c=(m.itemCode||'').toUpperCase();if(c)map[c]=parseFloat(m.qtyReq)||0;});
|
|
(w.packingMaterials||[]).forEach(m=>{const c=(m.itemCode||'').toUpperCase();if(c)map[c]=parseFloat(m.qtyReqUnits)||0;});
|
|
return map;
|
|
}catch(_e){return null;}
|
|
}
|
|
function buildQaComparison(lines,woApproved){
|
|
const EPS=0.001,r3=n=>Math.round((Number(n)||0)*1000)/1000;
|
|
const items=(lines||[]).filter(l=>l.ItemType!=='pit_Resource');
|
|
const totalIssued=items.reduce((s,l)=>s+(Number(l.IssuedQuantity)||0),0);
|
|
const issuedPhase=totalIssued>EPS;
|
|
let issueMismatch=0,woMismatch=0,matchCount=0;
|
|
const rows=items.map((l,idx)=>{
|
|
const planned=Number(l.PlannedQuantity)||0,issued=Number(l.IssuedQuantity)||0;
|
|
const code=(l.ItemNo||'').toUpperCase();
|
|
const approved=woApproved&&woApproved[code]!=null?Number(woApproved[code]):null;
|
|
const iDiff=issued-planned;
|
|
const issuedBad=issuedPhase&&Math.abs(iDiff)>EPS;if(issuedBad)issueMismatch++;
|
|
const woBad=approved!=null&&Math.abs(planned-approved)>EPS;if(woBad)woMismatch++;
|
|
if(issuedPhase&&!issuedBad&&!woBad)matchCount++;
|
|
const zebra=idx%2?'background:rgba(255,255,255,.015)':'';
|
|
const rowBg=issuedBad?'background:rgba(229,62,62,.07)':(woBad?'background:rgba(245,166,35,.07)':zebra);
|
|
const accent=issuedBad?'inset 3px 0 0 var(--red)':(woBad?'inset 3px 0 0 var(--amber)':'inset 3px 0 0 transparent');
|
|
const issuedColor=!issuedPhase?'var(--text2)':(Math.abs(iDiff)<=EPS?'var(--green)':(iDiff<0?'var(--amber)':'var(--red)'));
|
|
const woCell=approved==null?'<span style="color:var(--text3)">—</span>'
|
|
:(woBad?`<span style="display:inline-block;background:var(--amber);color:#1a1200;font-weight:700;padding:1px 8px;border-radius:5px">${r3(approved)} <span style="color:var(--red)">⚠</span></span>`:`<span style="color:var(--text2)">${r3(approved)}</span>`);
|
|
let pill;
|
|
if(!issuedPhase) pill='<span style="font-size:8.5px;font-weight:700;padding:1px 7px;border-radius:999px;background:rgba(122,154,184,.12);color:var(--text2)">PENDING</span>';
|
|
else if(issuedBad) pill=`<span style="font-size:8.5px;font-weight:700;padding:1px 7px;border-radius:999px;background:rgba(229,62,62,.13);color:#c81e1e">${iDiff<0?'SHORT':'OVER'} ${(iDiff>0?'+':'')+r3(iDiff)}</span>`;
|
|
else if(woBad) pill='<span style="font-size:8.5px;font-weight:700;padding:1px 7px;border-radius:999px;background:rgba(245,166,35,.18);color:#9a6700">WO DIFF</span>';
|
|
else pill='<span style="font-size:8.5px;font-weight:700;padding:1px 7px;border-radius:999px;background:rgba(13,187,138,.16);color:#0a8f57">MATCH</span>';
|
|
const td='padding:8px 10px;border-bottom:1px solid var(--border)';
|
|
return `<tr style="${rowBg}">
|
|
<td style="${td};box-shadow:${accent}"><div style="font-family:var(--mono);color:var(--blue);font-size:11px;font-weight:600">${esc(l.ItemNo)}</div><div style="font-size:10px;color:var(--text3);max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(l.ItemName||'')}</div></td>
|
|
<td style="${td};text-align:right;font-family:var(--mono);color:var(--text2)">${r3(planned)}</td>
|
|
<td style="${td};text-align:right;font-family:var(--mono)">${woCell}</td>
|
|
<td style="${td};text-align:right;font-family:var(--mono);font-weight:700;color:${issuedColor}">${r3(issued)}</td>
|
|
<td style="${td};text-align:right">${pill}</td>
|
|
</tr>`;
|
|
}).join('');
|
|
const chip=(txt,color,bg)=>`<span style="display:inline-flex;align-items:center;gap:4px;font-size:9.5px;font-weight:700;padding:3px 9px;border-radius:999px;background:${bg};color:${color}">${txt}</span>`;
|
|
const chips=[chip(`${items.length} MATERIALS`,'var(--text2)','rgba(122,154,184,.12)')];
|
|
if(!issuedPhase) chips.push(chip('AWAITING ISSUANCE','#1d4ed8','rgba(26,111,255,.12)'));
|
|
else{ chips.push(chip(`✓ ${matchCount} MATCH`,'#0a8f57','rgba(13,187,138,.16)'));
|
|
if(issueMismatch) chips.push(chip(`✗ ${issueMismatch} ISSUE DIFF`,'#c81e1e','rgba(229,62,62,.13)')); }
|
|
if(woMismatch) chips.push(chip(`⚠ ${woMismatch} WO DIFF`,'#9a6700','rgba(245,166,35,.18)'));
|
|
const statusLine=!issuedPhase
|
|
? 'Issued quantities populate after Issue for Production.'
|
|
: (issueMismatch>0
|
|
? `<b style="color:#c81e1e">${issueMismatch}</b> component(s) issued differently from plan${woMismatch?`, <b style="color:#9a6700">${woMismatch}</b> differ from the Work Order`:''} — review the highlighted rows.`
|
|
: (woMismatch>0
|
|
? `Issued matches plan; <b style="color:#9a6700">${woMismatch}</b> component(s) differ from the approved Work Order (amber ⚠).`
|
|
: 'Issued matches the approved BOM'+(woApproved?' and Work Order':'')+' on every component.'));
|
|
const barColor=!issuedPhase?'var(--blue)':(issueMismatch>0?'var(--red)':(woMismatch>0?'var(--amber)':'var(--green)'));
|
|
const header=`<div style="display:flex;align-items:flex-start;gap:10px;padding:11px 12px;border-bottom:1px solid var(--border);background:var(--surface3)">
|
|
<div style="width:3px;align-self:stretch;border-radius:3px;background:${barColor};flex-shrink:0"></div>
|
|
<div style="flex:1;min-width:0">
|
|
<div style="display:flex;flex-wrap:wrap;gap:5px;margin-bottom:6px">${chips.join('')}</div>
|
|
<div style="font-size:11px;color:var(--text2);line-height:1.45">${statusLine}</div>
|
|
${woApproved?'':'<div style="font-size:10px;color:var(--text3);margin-top:4px">Work-Order comparison unavailable (no linked Work Order).</div>'}
|
|
</div>
|
|
</div>`;
|
|
const th='padding:7px 10px;color:var(--text3);font-size:8.5px;text-transform:uppercase;letter-spacing:.6px;font-weight:700;border-bottom:1px solid var(--border2)';
|
|
const table=items.length?`<div style="overflow-x:auto"><table style="width:100%;border-collapse:collapse;font-size:11px">
|
|
<thead><tr style="background:var(--surface2)">
|
|
<th style="${th};text-align:left">Item</th><th style="${th};text-align:right">BOM Planned</th><th style="${th};text-align:right">WO Approved</th><th style="${th};text-align:right">Issued</th><th style="${th};text-align:right">Status</th>
|
|
</tr></thead><tbody>${rows}</tbody></table></div>`
|
|
:'<div style="padding:16px;color:var(--text3);font-size:12px;text-align:center">No material components on this order.</div>';
|
|
const legend=`<div style="display:flex;gap:14px;flex-wrap:wrap;padding:8px 12px;border-top:1px solid var(--border);font-size:9px;color:var(--text3)">
|
|
<span><b style="color:#0a8f57">Match</b> issued = plan</span>
|
|
<span><b style="color:#c81e1e">Short/Over</b> issued ≠ plan</span>
|
|
<span><b style="color:#9a6700">WO Diff ⚠</b> plan ≠ Work Order</span>
|
|
</div>`;
|
|
return `<div style="border:1px solid var(--border2);border-radius:12px;overflow:hidden;background:var(--ink2)">${header}${table}${legend}</div>`;
|
|
}
|
|
|
|
function closeDetail(){q('#detail-modal').classList.remove('show');}
|
|
async function openDetail(p){
|
|
const modal=q('#detail-modal'),inner=q('#detail-modal-inner');
|
|
modal.classList.add('show');
|
|
inner.innerHTML=`<div class="modal-hd2"><div class="modal-hd2-t">Loading…</div><button class="modal-x" onclick="closeDetail()">✕</button></div><div class="modal-bd2" style="text-align:center;padding:40px"><span class="sp"></span></div>`;
|
|
try{
|
|
const verified=!!p.verifiedAt;
|
|
let sap=null,compHtml='';
|
|
if(p.sapAbsEntry){
|
|
try{
|
|
const r=await api('GET',`/sap/production-orders/${p.sapAbsEntry}?company=${encodeURIComponent(_co)}`);
|
|
sap=r.data||r;
|
|
const lines=sap.ProductionOrderLines||[];
|
|
const woApproved=await fetchWoApprovedMap(p);
|
|
compHtml=buildQaComparison(lines,woApproved);
|
|
}catch(e){compHtml=`<div style="color:var(--text3);font-size:12px;padding:10px">Could not load SAP line detail: ${esc(e.message)}</div>`;}
|
|
}
|
|
const dt=(l,v)=>`<div class="dt-row"><span class="dt-l">${esc(l)}</span><span class="dt-v">${v==null||v===''?'—':esc(String(v))}</span></div>`;
|
|
const header=sap||{};
|
|
inner.innerHTML=`
|
|
<div class="modal-hd2">
|
|
<div class="modal-hd2-t">${esc(p.itemCode||'')} <span style="color:var(--text3);font-weight:400">— ${esc(p.itemName||header.ProductDescription||'')}</span></div>
|
|
<button class="modal-x" onclick="closeDetail()">✕</button>
|
|
</div>
|
|
<div class="modal-bd2">
|
|
<div class="dt-grid">
|
|
${dt('SAP Doc #',p.sapDocNum||header.DocumentNumber)}
|
|
${dt('AbsEntry',p.sapAbsEntry)}
|
|
${dt('Work Order',p.woNo||(p.workOrderId?('#'+p.workOrderId):'—'))}
|
|
${dt('Batch Number',p.batchNumber)}
|
|
${dt('Planned Qty',p.plannedQty||header.PlannedQuantity)}
|
|
${dt('Completed Qty',header.CompletedQuantity)}
|
|
${dt('Warehouse',p.warehouse||header.Warehouse)}
|
|
${dt('FG Warehouse',p.fgWarehouse)}
|
|
${dt('MFG Date',p.mfgDate)}
|
|
${dt('EXP Date',p.expDate)}
|
|
${dt('Stage',p.currentStep||('#'+p.stage))}
|
|
${dt('Status',p.status)}
|
|
${dt('Created By',p.createdByName||p.createdBy)}
|
|
${dt('Created At',fmt(p.createdAt))}
|
|
</div>
|
|
<div class="sec-t">Material Issuance — BOM vs Work Order vs Issued</div>
|
|
${compHtml||'<div style="color:var(--text3);font-size:12px">Not linked to a SAP production order.</div>'}
|
|
${p.remarks?`<div class="sec-t">Remarks</div><div style="font-size:12px;color:var(--text2)">${esc(p.remarks)}</div>`:''}
|
|
${verified?`<div class="sec-t">Verification</div><div style="font-size:12px;color:var(--green)">Verified by <b>${esc(p.verifiedByName||p.verifiedBy)}</b> on ${esc(fmt(p.verifiedAt))}${p.verifiedRemarks?` — “${esc(p.verifiedRemarks)}”`:''}</div>`:''}
|
|
</div>
|
|
${!verified&&CAN_VERIFY?`<div class="modal-ft">
|
|
<input class="fi" id="md-rem" placeholder="Remarks (optional)" style="flex:1"/>
|
|
<button class="btn b-verify" id="md-vb" onclick="doVerify(${p.id},true)">✔ Verify This Production Order</button>
|
|
</div>`:''}
|
|
`;
|
|
}catch(e){inner.innerHTML=`<div class="modal-hd2"><div class="modal-hd2-t">Error</div><button class="modal-x" onclick="closeDetail()">✕</button></div><div class="modal-bd2" style="color:var(--red)">${esc(e.message)}</div>`;}
|
|
}
|
|
window.closeDetail=closeDetail;
|
|
|
|
function poCard(p){
|
|
const verified=!!p.verifiedAt;
|
|
const title=`${p.sapDocNum?('Doc #'+esc(p.sapDocNum)):('PO #'+p.id)}`;
|
|
const sub=`<span>${esc(p.itemCode||'')} ${esc(p.itemName||'')}</span>${p.plannedQty?`<span>·</span><span>Qty <b>${esc(p.plannedQty)}</b></span>`:''}${p.woNo?`<span>·</span><span>WO <b>${esc(p.woNo)}</b></span>`:''}${p.batchNumber?`<span>·</span><span>Batch <b>${esc(p.batchNumber)}</b></span>`:''}`;
|
|
const chips=`<span class="chip">Stage: ${esc(p.currentStep||('#'+p.stage))}</span><span class="chip">${esc(p.status||'')}</span>`;
|
|
const right=verified
|
|
? `<span class="pill p-ok">Verified</span><div class="vinfo">by <b>${esc(p.verifiedByName||p.verifiedBy)}</b><br/>${esc(fmt(p.verifiedAt))}</div>${p.verifiedRemarks?`<div style="font-size:10.5px;color:var(--text3)">“${esc(p.verifiedRemarks)}”</div>`:''}`
|
|
: (CAN_VERIFY
|
|
? `<button class="btn b-verify" data-view="${p.id}">View & Verify</button>`
|
|
: `<button class="btn b-primary" data-view="${p.id}">View Details</button><span class="pill p-pend">Pending</span>`);
|
|
return `<div class="po" data-open="${p.id}">
|
|
<div class="po-main"><div class="po-t">${title}</div><div class="po-sub">${sub}</div><div class="chips">${chips}</div></div>
|
|
<div class="po-right">${right}</div>
|
|
</div>`;
|
|
}
|
|
|
|
function render(){
|
|
const app=document.getElementById('app');
|
|
if(!CAN_VIEW){app.innerHTML='<div class="card"><div class="card-bd empty-msg">You are not assigned the "Verify" permission. Ask an admin to grant Approval Steps → Production Order → Verify.</div></div>';return;}
|
|
const pending=_rows.filter(p=>!p.verifiedAt);
|
|
const done=_rows.filter(p=>p.verifiedAt);
|
|
const list=_tab==='pending'?pending:done;
|
|
app.innerHTML=`<div class="card">
|
|
<div class="card-hd"><span>✔ Verify Production Orders</span><span class="right">${_loading?'':`${pending.length} pending`}</span></div>
|
|
<div class="card-bd">
|
|
${CAN_VERIFY&&!_hasSig?`<div class="hint">You don't have a signature image on file — upload one under <b>Profile → My Signature</b> so it prints on the Work Order's "Verified By" column.</div>`:''}
|
|
<div style="display:flex;gap:8px;align-items:flex-end;margin-bottom:12px;flex-wrap:wrap">
|
|
<div style="min-width:180px"><label class="fl">Company</label><select class="fi" id="sel-co">${coOptions(_co)}</select></div>
|
|
<button class="btn b-primary" id="btn-reload" style="height:34px">Reload</button>
|
|
</div>
|
|
<div class="tabs">
|
|
<button class="tab ${_tab==='pending'?'on':''}" data-tab="pending">Pending (${pending.length})</button>
|
|
<button class="tab ${_tab==='done'?'on':''}" data-tab="done">Verified (${done.length})</button>
|
|
</div>
|
|
${_loading?`<div class="empty-msg"><span class="sp"></span> Loading…</div>`:
|
|
!list.length?`<div class="empty-msg">${_tab==='pending'?'No issued production orders awaiting verification.':'Nothing verified yet.'}</div>`:
|
|
list.map(poCard).join('')}
|
|
</div></div>`;
|
|
q('#sel-co')&&(q('#sel-co').onchange=function(){_co=this.value;load();});
|
|
q('#btn-reload')&&(q('#btn-reload').onclick=load);
|
|
document.querySelectorAll('.tab').forEach(b=>b.onclick=function(){_tab=this.dataset.tab;render();});
|
|
document.querySelectorAll('[data-open]').forEach(el=>el.onclick=function(){
|
|
const p=_rows.find(x=>x.id===parseInt(this.dataset.open));if(p)openDetail(p);
|
|
});
|
|
}
|
|
window.doVerify=doVerify;
|
|
|
|
async function init(){
|
|
try{const list=await window.fetchCompanyList();if(list.length)_companies=list;}catch(_e){}
|
|
try{const r=await api('GET','/auth/signature');_hasSig=!!(r&&r.signature);}catch(_e){}
|
|
if(CAN_VIEW)load();else render();
|
|
}
|
|
init();
|
|
</script>
|
|
<script src="/sidebar.js?v=25"></script>
|
|
</body>
|
|
</html>
|