Files
sap-erp/public/verify-production.html
T
John 69b4e68baf
SAP-ERP Portal CI/CD / build (push) Failing after 5m20s
first commit
2026-09-23 17:31:02 +05:30

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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');}
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 &amp; 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>