first commit
SAP-ERP Portal CI/CD / build (push) Failing after 5m20s

This commit is contained in:
John
2026-09-23 17:31:02 +05:30
commit 69b4e68baf
51657 changed files with 3864077 additions and 0 deletions
+728
View File
@@ -0,0 +1,728 @@
<!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>Issue for 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&family=Bebas+Neue&display=swap" rel="stylesheet"/>
<style>
:root{--ink:#080e18;--ink2:#0d1626;--ink3:#111f35;--purple:#9f5afd;--purple2:#7c3aed;--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;--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}
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(--teal);color:#fff;font-family:var(--display);font-size:15px;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;font-family:var(--font)}.nav-link:hover{color:var(--text);background:rgba(255,255,255,.05)}.nav-link.active{color:var(--teal);background:rgba(6,182,212,.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:720px;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;color:var(--text);display:flex;align-items:center;gap:8px;border-bottom:1px solid var(--border)}
.card-hd .ico{font-size:15px}
.card-hd .right{margin-left:auto;font-size:11px;color:var(--text3);font-weight:400}
.card-bd{padding:12px 14px}
.fg{margin-bottom:10px}
.fl{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.4px;color:var(--text3);margin-bottom:4px;display:block}
.fi{width:100%;padding:8px 10px;border:1.5px solid var(--border2);border-radius:7px;font-size:13px;font-family:var(--font);background:var(--surface2);color:var(--text);outline:none;height:36px}.fi:focus{border-color:var(--teal);box-shadow:0 0 0 2px rgba(6,182,212,.15)}.fi::placeholder{color:var(--text3)}
select.fi{cursor:pointer;appearance:none;background:var(--surface2) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%233d5470'/%3E%3C/svg%3E") no-repeat right 10px center;padding-right:26px}
select.fi option{background:var(--surface2)}
.fi-row{display:grid;grid-template-columns:1fr 1fr;gap:8px}
@media(max-width:420px){.fi-row{grid-template-columns:1fr}}
.fi-mono{font-family:var(--mono);font-size:12px;text-transform:uppercase}
/* Order cards — square-tile grid, same pattern as production.html */
.po-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:10px}
.po-card{background:var(--surface2);border:1px solid var(--border);border-radius:12px;padding:10px;cursor:pointer;transition:all .15s;aspect-ratio:1/1;display:flex;flex-direction:column;overflow:hidden}
.po-card:hover{border-color:var(--teal);box-shadow:0 6px 18px rgba(6,182,212,.14);transform:translateY(-2px)}
.po-card.selected{border-color:var(--teal);background:rgba(6,182,212,.10);box-shadow:0 0 0 2px rgba(6,182,212,.25)}
.po-top{display:flex;align-items:flex-start;justify-content:space-between;gap:6px;flex-wrap:wrap}
.po-code{font-family:var(--mono);font-size:10px;color:var(--teal);font-weight:600}
.po-doc{font-family:var(--mono);font-size:10px;color:var(--text3)}
.po-icon{width:26px;height:26px;border-radius:7px;background:rgba(6,182,212,.12);color:var(--teal);display:flex;align-items:center;justify-content:center;margin:5px 0;font-size:13px}
.po-name{font-size:11px;font-weight:600;color:var(--text);line-height:1.25;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.po-meta{font-size:9.5px;color:var(--text3);display:flex;flex-direction:column;gap:2px;margin-top:auto;padding-top:5px;border-top:1px solid var(--border)}
.po-meta b{color:var(--text2)}
/* Detail / issue popup */
.modal-bg{position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:500;display:none;align-items:flex-start;justify-content:center;padding:12px;overflow-y:auto}
.modal-bg.show{display:flex}
@keyframes mIn{from{transform:translateY(16px);opacity:0}to{transform:translateY(0);opacity:1}}
.modal{background:var(--ink2);border:1px solid var(--border2);border-radius:var(--radius);width:100%;max-width:640px;margin:auto;box-shadow:0 24px 80px rgba(0,0,0,.6);animation:mIn .2s ease}
.modal-hd{padding:12px 14px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px;flex-wrap:wrap;justify-content:space-between}
.modal-close{width:28px;height:28px;border-radius:6px;border:1px solid var(--border);background:var(--surface);color:var(--text2);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:14px}.modal-close:hover{background:var(--red);color:#fff;border-color:var(--red)}
.modal-bd{padding:14px;max-height:75vh;overflow-y:auto}
/* Component cards */
.comp-card{background:var(--ink2);border:1px solid var(--border);border-radius:8px;padding:10px 12px;margin-bottom:8px;position:relative;transition:opacity .15s,border-color .15s}
.comp-card:hover{border-color:var(--border2)}
.comp-card.unselected{opacity:.5}
.comp-card.done{opacity:.65}
.comp-top-row{display:flex;align-items:center;gap:8px;margin-bottom:2px}
.comp-check{width:16px;height:16px;accent-color:var(--teal);cursor:pointer;flex-shrink:0}
.comp-actions{margin-left:auto;display:flex;align-items:center;gap:6px}
.comp-remove{background:none;border:1px solid var(--border2);color:var(--text3);font-size:9px;padding:3px 8px;border-radius:5px;cursor:pointer;font-weight:700;white-space:nowrap}
.comp-remove:hover{background:rgba(229,62,62,.12);border-color:var(--red);color:var(--red)}
.comp-num{font-family:var(--mono);font-size:10px;color:var(--text3);background:var(--surface3);padding:1px 6px;border-radius:4px}
.comp-badge-done{font-size:9px;font-weight:700;padding:1px 6px;border-radius:4px;background:rgba(13,187,138,.12);color:var(--green)}
/* Toolbar above the components list */
.comp-toolbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:10px;padding:8px 10px;background:var(--surface3);border:1px solid var(--border);border-radius:8px;font-size:11px;color:var(--text2)}
.comp-toolbar b{color:var(--text)}
.chip-btn{background:none;border:1px solid var(--border2);color:var(--teal);font-size:10px;font-weight:700;padding:4px 10px;border-radius:6px;cursor:pointer}
.chip-btn:hover{background:rgba(6,182,212,.1);border-color:var(--teal)}
.removed-strip{display:flex;align-items:center;gap:8px;padding:7px 10px;background:rgba(229,62,62,.06);border:1px solid rgba(229,62,62,.18);border-radius:7px;font-size:11px;color:var(--text2);margin-bottom:10px}
.removed-strip .chip-btn{color:var(--amber);border-color:rgba(245,166,35,.3)}
.removed-strip .chip-btn:hover{background:rgba(245,166,35,.12);border-color:var(--amber)}
.comp-type{font-size:9px;font-weight:700;text-transform:uppercase;padding:2px 6px;border-radius:3px;display:inline-block;margin-bottom:6px}
.comp-type.item{background:rgba(6,182,212,.12);color:var(--teal)}
.comp-type.resource{background:rgba(245,166,35,.12);color:var(--amber)}
.comp-item{font-family:var(--mono);font-size:12px;color:var(--teal);font-weight:600}
.comp-desc{font-size:12px;color:var(--text);margin:3px 0 8px;line-height:1.3}
.comp-grid{display:grid;grid-template-columns:1fr 1fr 1fr;gap:6px}
@media(max-width:420px){.comp-grid{grid-template-columns:1fr 1fr}}
.comp-field{display:flex;flex-direction:column;gap:2px}
.comp-fl{font-size:9px;font-weight:700;text-transform:uppercase;color:var(--text3);letter-spacing:.3px}
.comp-fv{font-size:12px;font-family:var(--mono);color:var(--text2)}
.comp-inp{width:100%;padding:4px 6px;border:1px solid var(--border2);border-radius:5px;font-size:12px;font-family:var(--mono);background:var(--surface2);color:var(--text);outline:none;height:28px}.comp-inp:focus{border-color:var(--teal)}
/* Batch section */
.batch-sec{margin-top:8px;padding:8px;background:var(--surface3);border-radius:6px;border:1px solid var(--border)}
.batch-title{font-size:9px;font-weight:700;text-transform:uppercase;color:var(--amber);margin-bottom:6px;display:flex;align-items:center;gap:4px}
.batch-row{display:flex;gap:6px;align-items:center;padding:4px 0;border-bottom:1px solid rgba(28,46,72,.3);font-size:11px;flex-wrap:wrap}
.batch-row:last-child{border-bottom:none}
.batch-num{font-family:var(--mono);color:var(--teal);font-size:11px;min-width:100px;flex:1}
.batch-avail{color:var(--text3);font-size:10px;min-width:60px}
.batch-inp{width:80px;padding:3px 5px;border:1px solid var(--border2);border-radius:4px;font-size:11px;font-family:var(--mono);background:var(--surface2);color:var(--text);outline:none;text-align:right}.batch-inp:focus{border-color:var(--teal)}
.batch-auto{font-size:9px;color:var(--green);font-weight:600}
.batch-loading{font-size:10px;color:var(--text3);padding:6px 0}
/* Buttons */
.btn-bar{display:flex;gap:8px;flex-wrap:wrap}
.btn{padding:10px 20px;border-radius:8px;border:none;font-family:var(--font);font-size:13px;font-weight:700;cursor:pointer;transition:all .18s;display:inline-flex;align-items:center;gap:6px;justify-content:center}.btn:disabled{opacity:.4;cursor:not-allowed}
.b-issue{background:var(--teal);color:#fff;flex:1}.b-issue:hover:not(:disabled){background:#0891b2;box-shadow:0 4px 16px rgba(6,182,212,.4)}
.b-cancel{background:var(--surface2);color:var(--text2);border:1px solid var(--border2)}.b-cancel:hover{border-color:var(--text2)}
.b-new{background:rgba(6,182,212,.1);color:var(--teal);border:1px solid rgba(6,182,212,.3);flex:1}.b-new:hover{background:rgba(6,182,212,.2)}
.alert{padding:10px 12px;border-radius:8px;font-size:12px;margin-bottom:12px;line-height:1.5}
.al-teal{background:rgba(6,182,212,.08);border:1px solid rgba(6,182,212,.2);color:#67e8f9}
.al-green{background:rgba(13,187,138,.08);border:1px solid rgba(13,187,138,.2);color:#6ee7b7}
.al-red{background:rgba(229,62,62,.08);border:1px solid rgba(229,62,62,.2);color:#fca5a5}
.al-amber{background:rgba(245,166,35,.08);border:1px solid rgba(245,166,35,.2);color:#fcd34d}
.sp{width:14px;height:14px;border:2px solid rgba(255,255,255,.25);border-top-color:#fff;border-radius:50%;animation:spin .6s linear infinite;display:inline-block}
@keyframes spin{to{transform:rotate(360deg)}}
.toast-c{position:fixed;top:56px;right:10px;z-index:9999;display:flex;flex-direction:column;gap:6px;pointer-events:none}
.toast{padding:9px 13px;border-radius:8px;font-size:12px;font-weight:600;box-shadow:var(--shadow);animation:sIn .22s ease;max-width:280px;pointer-events:auto}
.t-ok{background:var(--green);color:#fff}.t-err{background:var(--red);color:#fff}
@keyframes sIn{from{transform:translateX(110%);opacity:0}to{transform:translateX(0);opacity:1}}
.suc-box{text-align:center;padding:28px 16px}
.suc-icon{font-size:44px;margin-bottom:10px}
.suc-title{font-family:var(--display);font-size:26px;color:var(--green);margin-bottom:6px}
.suc-pill{display:inline-block;font-family:var(--mono);font-size:13px;padding:5px 16px;border-radius:6px;background:rgba(6,182,212,.12);color:var(--teal);border:1px solid rgba(6,182,212,.25);margin:8px 0 12px}
.empty-msg{text-align:center;padding:24px;color:var(--text3);font-size:12px}
.order-info{display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:12px;font-size:11px}
@media(max-width:420px){.order-info{grid-template-columns:1fr}}
.oi-item{display:flex;flex-direction:column;gap:1px}
.oi-l{font-size:9px;font-weight:700;text-transform:uppercase;color:var(--text3)}
.oi-v{font-family:var(--mono);font-size:11px;color:var(--text)}
</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="/bom" class="nav-link">BOM</a>
<a href="/production" class="nav-link">Production</a>
<a href="/issue-production" class="nav-link active">Issue</a>
<a href="/receipt-production" class="nav-link">Receipt</a>
<a href="/grpo" class="nav-link">GRPO</a><a href="/approvals" class="nav-link">Approvals</a>
</div>
<button class="btn-sm" id="btn-logout">Logout</button>
</nav>
<main id="app"></main>
<div class="modal-bg" id="iss-detail-modal"><div class="modal" id="iss-modal-inner"></div></div>
<script>
'use strict';
let _tok=null,_user=null,_wh=[],_varieties=[];
const BE='/api';
(function(){try{_tok=sessionStorage.getItem('portal_token');const u=sessionStorage.getItem('portal_user');if(u)_user=JSON.parse(u);}catch{}})();
if(!_tok)window.location.href='/';
// Fail fast (with a clear message) if the user hasn't set their own SAP
// login — the JWT carries a non-empty sapUser claim when they have.
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('Set your SAP User ID & Password first — open Profile → "My SAP Account".','err');return false;}return true;}
document.getElementById('btn-logout').onclick=()=>{sessionStorage.clear();document.cookie='portal_token=;path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT';document.cookie='portal_user=;path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT';;localStorage.removeItem('token');localStorage.removeItem('portal_user');window.location.href='/';};
// Mirrors middleware/auth.js's hasStepPerm — this page is normally only
// reached via production.html's already-gated button, but a direct/bookmarked
// URL should still be blocked here too (the server enforces this regardless).
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;
}
// Being assigned the step (any perm) is enough to perform the action.
function hasStepAssigned(fullKey){return ['view','add','edit','approve','delete'].some(p=>hasStepPerm(fullKey,p));}
const CAN_ISSUE_PO=hasStepAssigned('production_order:issuance');
// Consumable Orders use their OWN independent "Issue" approval step, not the
// general production_order:issuance step (and vice versa) — no creator
// bypass either, so this is a plain static permission check.
const CAN_ISSUE_CONSUMABLE=hasStepPerm('production_order:consumable_issue','approve');
const CAN_CREATE_CONSUMABLE=hasStepPerm('production_order:consumable_create','add');
let _co=window.__DEFAULT_COMPANY__;
let _companies=[{value:_co,label:_co}]; // live list — populated in init()
function coOptions(sel){return _companies.map(c=>`<option value="${esc(c.value)}" ${sel===c.value?'selected':''}>${esc(c.label)}</option>`).join('');}
let _branches=[];
let _selectedOrder=null;
let _lines=[];
let _removedLines=[]; // components taken out of THIS issue (not submitted); can be restored
let _hasHiddenPending=false; // a per-user item-group restriction hid a component that still genuinely needs issuing
let _issueGroups=null; // this user's allowed Item Groups for issuing (null = not yet loaded / no restriction)
let _loadingDetail=false;
let _submitting=false;
let _success=null;
let _readyOrders=[];
let _readyLoading=false;
let _modalOpen=false;
let _page=0,_pageSize=20,_total=null,_rawCount=0,_search=''; // ready-orders pagination + server search
// A user who can act on Consumable Orders but does NOT hold the general
// Issuance approval step must never see (or be able to submit for) a normal
// PWO here — force the list to Consumable-only for them, with no toggle
// (there's nothing else they're allowed to browse to anyway).
const CONSUMABLE_ONLY_USER=CAN_ISSUE_CONSUMABLE&&!CAN_ISSUE_PO;
let _viewOrderType=CONSUMABLE_ONLY_USER?'consumable':''; // '' = all, 'consumable' = Consumable Orders only, 'regular' = everything else
function esc(v){return(v||'').toString().replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');}
function q(s){return document.querySelector(s);}
function mk(c){const d=document.createElement('div');d.className=c;return d;}
async function api(m,p,b=null){
const h={'Content-Type':'application/json'};if(_tok)h['Authorization']='Bearer '+_tok;
const o={method:m.toUpperCase(),headers:h};if(b)o.body=JSON.stringify(b);
let r,d;try{r=await fetch(BE+p,o);d=await r.json();}catch(_e){throw new Error('Cannot reach server');}
if(r.status===401){window.location.href='/';throw new Error('Session expired');}
if(!r.ok)throw new Error(d?.message||'HTTP '+r.status);return d;
}
function toast(msg,type='ok'){const tc=document.getElementById('toast-c');const el=document.createElement('div');el.className=`toast t-${type}`;el.textContent=(type==='ok'?'OK ':'ERR ')+msg;tc.appendChild(el);setTimeout(()=>el.remove(),4500);}
// Whether a next page of Released orders exists — from the server total when
// known, else from whether a full RAW (pre-filter) page came back.
function pagerHasNext(){ if(_total!=null) return (_page+1)*_pageSize<_total; return _rawCount>=_pageSize; }
function renderPager(){
if(_readyLoading) return '';
const hasNext=pagerHasNext();
const totalPages=_total!=null?Math.max(1,Math.ceil(_total/_pageSize)):null;
const label=_total!=null?`Page ${_page+1} of ${totalPages} · ${_total} released`:`Page ${_page+1}`;
return `<div style="display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:12px">
<button class="btn" id="rp-prev" ${_page===0?'disabled':''} style="padding:6px 14px;font-size:11px;background:var(--surface2);color:var(--text2);border:1px solid var(--border2)${_page===0?';opacity:.45;cursor:not-allowed':''}">‹ Prev</button>
<span style="font-size:11px;color:var(--text3)">${label}</span>
<button class="btn" id="rp-next" ${hasNext?'':'disabled'} style="padding:6px 14px;font-size:11px;background:var(--surface2);color:var(--text2);border:1px solid var(--border2)${hasNext?'':';opacity:.45;cursor:not-allowed'}">Next ›</button>
</div>`;
}
// Released orders not yet fully issued — the default "what's ready to
// issue" list shown when the page isn't deep-linked to one specific order.
async function loadReadyOrders(resetPage){
if(resetPage)_page=0;
_readyLoading=true;render();
const searchQ=_search.trim()?`&search=${encodeURIComponent(_search.trim())}`:'';
try{
const r=await api('GET',`/sap/production-orders?status=Released&top=${_pageSize}&skip=${_page*_pageSize}&company=${encodeURIComponent(_co)}${searchQ}${_viewOrderType?'&orderType='+_viewOrderType:''}`);
_rawCount=(r.data||[]).length;
_readyOrders=(r.data||[]).filter(o=>o._issueStatus!=='full');
_total=(typeof r.total==='number')?r.total:null;
}catch(_e){_readyOrders=[];_rawCount=0;_total=null;}
_readyLoading=false;render();
}
async function loadWH(){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 loadVarieties(){try{const r=await api('GET',`/sap/lookup/costing-codes?dim=1&company=${encodeURIComponent(_co)}`);if(r.success&&r.data?.length)_varieties=r.data;}catch(_e){}}
async function loadBranches(){try{const r=await api('GET',`/sap/lookup/branches?company=${encodeURIComponent(_co)}`);if(r.success&&r.data?.length)_branches=r.data;}catch(_e){}}
async function selectOrder(absEntry){
_modalOpen=true;_loadingDetail=true;_selectedOrder=null;_lines=[];_removedLines=[];render();
try{
const r=await api('GET',`/sap/production-orders/${absEntry}?company=${encodeURIComponent(_co)}`);
const d=r.data||r;
_selectedOrder=d;
_lines=(d.ProductionOrderLines||[]).map(l=>({
lineNum:l.LineNumber,
itemCode:l.ItemNo||'',
itemName:l.ItemName||'',
itemType:l.ItemType||'pit_Item',
plannedQty:l.PlannedQuantity||0,
issuedQty:l.IssuedQuantity||0,
remainingQty:Math.max(0,(l.PlannedQuantity||0)-(l.IssuedQuantity||0)),
issueQty:Math.max(0,(l.PlannedQuantity||0)-(l.IssuedQuantity||0)),
selected:true, // include in this issue by default; user may uncheck or Remove
_savedQty:null, // remembers issueQty while unchecked, so re-checking restores it
warehouse:l.Warehouse||d.Warehouse||'',
costingCode:l.DistributionRule||'',
batches:[],
selectedBatches:[],
batchLoading:false,
}));
// Backflush-type lines are consumed by SAP itself, never manually issued
// here — hide them outright (not a permission thing, so it doesn't count
// toward _hasHiddenPending like the item-group filter below does).
_lines=_lines.filter(l=>{
const src=(d.ProductionOrderLines||[]).find(x=>x.LineNumber===l.lineNum);
return src?.ProductionOrderIssueType!=='im_Backflush';
});
// Per-user item-group restriction: hide components whose item group is NOT
// in this user's allowed list. Empty/no list = show everything (unchanged).
// Track whether anything genuinely still-outstanding got hidden this way
// — the "all fully issued" congratulatory message must NOT claim the
// order is done just because nothing left VISIBLE to this user needs
// issuing; that's a permission gap, not the order's real state.
_hasHiddenPending=false;
if(Array.isArray(_issueGroups)&&_issueGroups.length&&_lines.length){
try{
const codes=[...new Set(_lines.map(l=>l.itemCode).filter(Boolean))];
const gr=await api('GET',`/sap/lookup/item-groups-for?codes=${encodeURIComponent(codes.join(','))}&company=${encodeURIComponent(_co)}`);
const grpByCode=gr.data||{};const allow=new Set(_issueGroups.map(String));
const hidden=_lines.filter(l=>!allow.has(String(grpByCode[l.itemCode])));
_hasHiddenPending=hidden.some(l=>l.remainingQty>0.001);
_lines=_lines.filter(l=>allow.has(String(grpByCode[l.itemCode])));
}catch(_e){}
}
// A component replaced by an approved Substitution deviation, whose own
// remaining is already down to SAP's zero-floor (see [[production-
// deviation-workflow]]) — must never be issuable (it isn't actually
// available), and must not keep this order from ever reading as "fully
// issued" just because a negligible 0.0001 sits unissued forever.
try{
const devR=await api('GET',`/sap/deviations?sapAbsEntry=${absEntry}&company=${encodeURIComponent(_co)}`);
const supersededCodes=new Set((devR.data||[])
.filter(dv=>dv.type==='SUBSTITUTION'&&dv.qaStatus==='APPROVED'&&dv.itemCode)
.map(dv=>dv.itemCode.toUpperCase()));
if(supersededCodes.size){
_lines.forEach(l=>{
if(supersededCodes.has((l.itemCode||'').toUpperCase())&&l.remainingQty<=0.001){
l.superseded=true; l.selected=false; l.issueQty=0; l.remainingQty=0;
}
});
}
}catch(_e){}
// Load batches for all lines in parallel
await Promise.all(_lines.map(l=>loadBatches(l)));
}catch(e){toast(e.message,'err');}
_loadingDetail=false;render();
}
async function loadBatches(line){
line.batchLoading=true;
try{
const r=await api('GET',`/sap/lookup/batches?item=${encodeURIComponent(line.itemCode)}&warehouse=${encodeURIComponent(line.warehouse)}&company=${encodeURIComponent(_co)}`);
line.batches=r.data||[];
if(line.batches.length) autoSelectBatches(line);
}catch{}
line.batchLoading=false;
}
function autoSelectBatches(line){
let rem=line.issueQty;
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;
}
}
async function doIssue(){
if(!_selectedOrder||!_lines.length)return;
const issuable=_lines.filter(l=>l.selected&&l.issueQty>0);
if(!issuable.length){toast('No items selected to issue — check at least one component and enter a quantity','err');return;}
// Validate batch totals
for(const l of issuable){
if(l.batches.length){
const batchTotal=l.selectedBatches.reduce((s,b)=>s+(b.Quantity||0),0);
if(Math.abs(batchTotal-l.issueQty)>0.001){
toast(`${l.itemCode}: batch qty (${batchTotal}) must equal issue qty (${l.issueQty})`,'err');return;
}
}
}
if(!requireSapLogin())return;
_submitting=true;render();
const bplId=parseInt(q('#sel-branch')?.value)||2;
const postDate=q('#inp-postdate')?.value||'';
const payload={
company:_co,
branchId:bplId,
};
if(postDate){payload.DocDate=postDate;payload.DocDueDate=postDate;}
payload.DocumentLines=issuable.map(l=>{
const dl={BaseEntry:_selectedOrder.AbsoluteEntry,BaseLine:l.lineNum,BaseType:202,ItemCode:l.itemCode,Quantity:l.issueQty,WarehouseCode:l.warehouse};
if(l.costingCode) dl.CostingCode=l.costingCode;
if(l.selectedBatches.length){
dl.BatchNumbers=l.selectedBatches.filter(b=>b.Quantity>0).map(b=>({BatchNumber:b.BatchNumber,Quantity:b.Quantity}));
}
return dl;
});
try{
const r=await api('POST','/sap/issue-production',payload);
_success={docEntry:r.data?.DocEntry||'',docNum:r.data?.DocNum||''};
toast('Issue for Production posted!');
}catch(e){toast(e.message||'Failed','err');}
_submitting=false;render();
}
// ═══════════════════════════════════════
// RENDER
// ═══════════════════════════════════════
function render(){
try{ _doRender(); }catch(e){ console.error('Render error:',e); document.getElementById('app').innerHTML=`<div style="padding:20px;color:var(--red)">Render error: ${e.message}</div>`; }
}
function closeIssModal(){_modalOpen=false;_selectedOrder=null;_lines=[];_removedLines=[];_success=null;render();loadReadyOrders();}
function _doRender(){
const app=document.getElementById('app');app.innerHTML='';
if(!CAN_ISSUE_PO&&!CAN_ISSUE_CONSUMABLE){
app.innerHTML='<div class="card"><div class="card-bd" style="text-align:center;padding:30px;color:var(--text3)">You\'re not assigned the "Issuance" approval step, so you can\'t issue items for production. Ask an admin to grant it in User Management if you believe this is incorrect.</div></div>';
document.getElementById('iss-detail-modal')?.classList.remove('show');
return;
}
// Company
const c1=mk('card');
const brOpts=_branches.map(b=>`<option value="${b.BPLId}" ${b.BPLId===2?'selected':''}>${esc(b.BPLName)} (${b.BPLId})</option>`).join('')||'<option value="2">FACTORY (2)</option>';
c1.innerHTML=`<div class="card-hd"><span class="ico"></span> Issue for Production</div>
<div class="card-bd">
<div class="fi-row">
<div class="fg"><label class="fl">Company</label>
<select class="fi" id="sel-co">
${coOptions(_co)}
</select></div>
<div class="fg"><label class="fl">Branch</label><select class="fi" id="sel-branch">${brOpts}</select></div>
</div>
<div class="fg"><label class="fl">Posting Date</label><input class="fi" id="inp-postdate" type="date" value="${new Date().toISOString().slice(0,10)}"/></div>
</div>`;
app.appendChild(c1);
// Orders ready for issuance — square-tile grid, always visible (matches
// production.html's own list/picker), opens the detail popup on click.
const c2b=mk('card');
const gridBody=_readyLoading
?'<div class="empty-msg"><span class="sp" style="border-color:rgba(6,182,212,.25);border-top-color:var(--teal)"></span> Loading released orders...</div>'
:!_readyOrders.length?'<div class="empty-msg">No released orders are awaiting issuance right now.</div>'
:`<div class="po-grid">${_readyOrders.map(o=>`<div class="po-card${_selectedOrder&&_selectedOrder.AbsoluteEntry===o.AbsoluteEntry?' selected':''}" data-ready="${o.AbsoluteEntry}">
<div class="po-top"><span class="po-code">${esc(o.ItemNo)}</span>${o._issueStatus==='partial'?'<span style="font-size:8px;font-weight:700;padding:1px 5px;border-radius:3px;background:rgba(245,166,35,.12);color:var(--amber)">PARTIAL</span>':''}${o.IsConsumable?'<span style="font-size:8px;font-weight:700;padding:1px 5px;border-radius:3px;background:rgba(159,90,253,.14);color:var(--purple)">CONSUMABLE</span>':''}</div>
<div class="po-icon">📦</div>
<div class="po-name" title="${esc(o.ProductDescription||'')}">${esc(o.ProductDescription||'-')}</div>
<div class="po-meta"><span>#${o.DocumentNumber||o.AbsoluteEntry} · Qty <b>${o.PlannedQuantity}</b></span><span>WH <b>${esc(o.Warehouse||'-')}</b></span></div>
${(o.WoNo||o.BatchNumber)?`<div class="po-meta"><span>${o.WoNo?`WO <b>${esc(o.WoNo)}</b>`:''}${o.WoNo&&o.BatchNumber?' · ':''}${o.BatchNumber?`Batch <b>${esc(o.BatchNumber)}</b>`:''}</span></div>`:''}
</div>`).join('')}</div>`;
const searchRow=`<div style="display:flex;gap:6px;margin-bottom:10px">
<input class="fi fi-mono" id="rp-search" value="${esc(_search)}" placeholder="Search item code, description, document no, order no, WO no or batch no…" style="font-size:12px;flex:1"/>
<button class="btn" id="rp-load" style="padding:6px 16px;font-size:11px;background:var(--teal);color:#fff;white-space:nowrap">Search</button>
</div>`;
const typeRow=CONSUMABLE_ONLY_USER
?`<div style="font-size:11px;color:var(--purple);margin-bottom:10px">Showing Consumable Orders only — you're not assigned the general Issuance approval step.</div>`
:(CAN_CREATE_CONSUMABLE||CAN_ISSUE_CONSUMABLE)?`<div style="display:flex;gap:4px;margin-bottom:10px;flex-wrap:wrap;align-items:center">
<span style="font-size:10px;color:var(--text3);text-transform:uppercase;letter-spacing:.4px;margin-right:2px">Type</span>
${[['','All'],['regular','Regular PWO'],['consumable','Consumable']].map(([v,l])=>`<button class="btn" style="padding:5px 11px;font-size:11px;${_viewOrderType===v?'background:rgba(159,90,253,.15);color:var(--purple);border:1px solid rgba(159,90,253,.4)':'background:var(--surface2);color:var(--text3);border:1px solid var(--border2)'}" data-vot="${v}">${l}</button>`).join('')}
</div>`:'';
c2b.innerHTML=`<div class="card-hd"><span class="ico"></span> Orders Ready for Issuance<span class="right">${_readyOrders.length}</span></div><div class="card-bd">${searchRow}${typeRow}${gridBody}${renderPager()}</div>`;
app.appendChild(c2b);
renderModal();
// Wire (page-level controls)
setTimeout(()=>{
q('#sel-co')&&(q('#sel-co').onchange=async function(){_co=this.value;await Promise.all([loadWH(),loadBranches(),loadVarieties()]);loadReadyOrders(true);});
q('#rp-search')&&(q('#rp-search').oninput=function(){_search=this.value;});
q('#rp-search')&&(q('#rp-search').onkeydown=function(e){if(e.key==='Enter'){e.preventDefault();loadReadyOrders(true);}});
q('#rp-load')&&(q('#rp-load').onclick=()=>loadReadyOrders(true));
document.querySelectorAll('[data-vot]').forEach(b=>b.onclick=function(){_viewOrderType=this.dataset.vot;loadReadyOrders(true);});
q('#rp-prev')&&(q('#rp-prev').onclick=()=>{if(_page>0){_page--;loadReadyOrders();}});
q('#rp-next')&&(q('#rp-next').onclick=()=>{if(pagerHasNext()){_page++;loadReadyOrders();}});
document.querySelectorAll('[data-ready]').forEach(c=>{c.onclick=()=>selectOrder(parseInt(c.dataset.ready));});
},0);
}
// ═══════════════════════════════════════
// DETAIL / ISSUE POPUP
// ═══════════════════════════════════════
function renderModal(){
const bg=document.getElementById('iss-detail-modal');
const inner=document.getElementById('iss-modal-inner');
if(!_modalOpen){bg.classList.remove('show');return;}
bg.classList.add('show');
if(_success){
inner.innerHTML=`<div class="modal-hd"><span>Issue for Production — Posted</span><button class="modal-close" id="iss-modal-x">✕</button></div>
<div class="modal-bd"><div class="suc-box">
<div class="suc-icon">✓</div>
<div class="suc-title">Issue Posted!</div>
<p style="font-size:13px;color:var(--text3)">Components issued against Production Order #${esc(String(_selectedOrder?.DocumentNumber||''))}</p>
<div class="suc-pill">DocEntry #${esc(String(_success.docEntry))} | DocNum #${esc(String(_success.docNum))}</div>
<div class="alert al-green" style="max-width:400px;margin:10px auto;text-align:left">
Inventory General Exit created in SAP B1. The issued quantities are now deducted from stock.
</div>
<div class="btn-bar" style="justify-content:center;margin-top:12px">
<button class="btn b-new" id="btn-new">+ New Issue</button>
</div>
</div></div>`;
setTimeout(()=>{
q('#iss-modal-x').onclick=closeIssModal;
q('#btn-new').onclick=closeIssModal;
},0);
return;
}
if(_loadingDetail||!_selectedOrder){
inner.innerHTML=`<div class="modal-hd"><span>Loading Order…</span><button class="modal-close" id="iss-modal-x">✕</button></div>
<div class="modal-bd"><div class="empty-msg"><span class="sp" style="border-color:rgba(6,182,212,.25);border-top-color:var(--teal)"></span> Loading order details...</div></div>`;
setTimeout(()=>{q('#iss-modal-x').onclick=closeIssModal;},0);
return;
}
const d=_selectedOrder;
const linesHtml=_lines.map((l,i)=>{
const whOpts=_wh.map(w=>`<option value="${esc(w.code)}" ${l.warehouse===w.code?'selected':''}>${esc(w.code)}${w.name?' - '+esc(w.name):''}</option>`).join('');
const typeLabel=l.itemType==='pit_Resource'?'Resource':'Item';
const typeCls=l.itemType==='pit_Resource'?'resource':'item';
const isDone=l.remainingQty<=0;
const inactive=isDone||!l.selected; // fully issued, or user unchecked it
let batchHtml='';
if(!inactive&&l.batchLoading){
batchHtml='<div class="batch-loading"><span class="sp" style="width:10px;height:10px;border-color:rgba(245,166,35,.25);border-top-color:var(--amber)"></span> Loading batches...</div>';
}else if(!inactive&&l.batches.length){
batchHtml=`<div class="batch-sec"><div class="batch-title">Batches <span class="batch-auto">(auto-selected)</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: ${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>`;
}
return`<div class="comp-card${!l.selected&&!isDone?' unselected':''}${isDone?' done':''}">
<div class="comp-top-row">
${isDone?'':`<input type="checkbox" class="comp-check" data-i="${i}" ${l.selected?'checked':''} title="Include this item in this issue"/>`}
<span class="comp-num">#${i+1}</span>
<span class="comp-type ${typeCls}">${typeLabel}</span>
${l.superseded?'<span class="comp-badge-done" style="background:rgba(122,154,184,.15);color:var(--text2)" title="Replaced by a Substitution deviation — no longer available to issue">↦ Superseded</span>':(isDone?'<span class="comp-badge-done">✓ Fully Issued</span>':'')}
<div class="comp-actions">${isDone?'':`<button type="button" class="comp-remove" data-remove-i="${i}" title="Take this item out of this issue">✕ Remove</button>`}</div>
</div>
<div class="comp-item">${esc(l.itemCode)}</div>
<div class="comp-desc">${esc(l.itemName)}</div>
<div class="comp-grid">
<div class="comp-field"><span class="comp-fl">Planned</span><span class="comp-fv">${l.plannedQty}</span></div>
<div class="comp-field"><span class="comp-fl">Issued</span><span class="comp-fv">${l.issuedQty}</span></div>
<div class="comp-field"><span class="comp-fl">Remaining</span><span class="comp-fv" style="color:${l.remainingQty>0?'var(--amber)':'var(--green)'}">${l.remainingQty}</span></div>
<div class="comp-field"><span class="comp-fl">Issue Qty *</span><input class="comp-inp" data-i="${i}" data-f="issueQty" type="number" min="0" step="0.001" value="${l.issueQty}" ${inactive?'disabled':''}/></div>
<div class="comp-field" style="grid-column:span 2"><span class="comp-fl">Warehouse</span><select class="comp-inp" data-i="${i}" data-f="warehouse" style="padding:2px 4px" ${inactive?'disabled':''}>${whOpts}</select></div>
</div>
${batchHtml}
</div>`;
}).join('');
const pendingLines=_lines.filter(l=>l.remainingQty>0);
const selectedCount=pendingLines.filter(l=>l.selected).length;
const totalQty=_lines.filter(l=>l.selected).reduce((s,l)=>s+(l.issueQty||0),0);
const toolbar=pendingLines.length?`<div class="comp-toolbar">
<span><b>${selectedCount}</b> of <b>${pendingLines.length}</b> item${pendingLines.length===1?'':'s'} selected${totalQty>0?` · Qty to issue <b>${Math.round(totalQty*1000)/1000}</b>`:''}</span>
<button type="button" class="chip-btn" id="btn-select-all">Select all</button>
<button type="button" class="chip-btn" id="btn-select-none">Clear all</button>
</div>`:'';
const removedStrip=_removedLines.length?`<div class="removed-strip">
<span><b>${_removedLines.length}</b> item${_removedLines.length===1?'':'s'} removed from this issue</span>
<button type="button" class="chip-btn" id="btn-restore-removed" style="margin-left:auto">↩ Restore</button>
</div>`:'';
const allIssued=_lines.length>0&&_lines.every(l=>l.remainingQty<=0);
const hasQty=_lines.some(l=>l.selected&&l.issueQty>0);
// A per-user item-group restriction can hide a component that genuinely
// still needs issuing — in that case the order is NOT actually done, this
// user just has nothing left THEY'RE permitted to act on. Never claim
// "fully issued" when that's true — it's misleading and could make
// someone think the order needs no further attention from anyone.
const footer=allIssued
?(_hasHiddenPending
?`<div class="alert al-amber" style="margin:0">Nothing left for you to issue in your assigned item group(s) — but this order still has other components pending in a different item group. Someone with access to those groups still needs to issue them.</div>
<div class="btn-bar" style="margin-top:8px"><button class="btn b-cancel" id="btn-clear" style="flex:1">Select Another Order</button></div>`
:`<div class="alert al-green" style="margin:0">All components have been fully issued for this production order.</div>
<div class="btn-bar" style="margin-top:8px"><button class="btn b-cancel" id="btn-clear" style="flex:1">Select Another Order</button></div>`)
:`<div class="btn-bar" style="margin-top:8px">
<button class="btn b-issue" id="btn-issue" ${!hasQty||_submitting?'disabled':''}><span id="sp-issue" style="display:${_submitting?'inline-block':'none'}" class="sp"></span> ${d._localIsConsumable?'Issue Consumable':'Issue to Production'}</button>
<button class="btn b-cancel" id="btn-clear">Clear</button>
</div>`;
inner.innerHTML=`
<div class="modal-hd">
<span style="font-family:var(--mono);color:var(--teal)">${esc(d.ItemNo)}</span>
<span style="color:var(--text3);font-size:11px">Order #${d.DocumentNumber||d.AbsoluteEntry}</span>
<button class="modal-close" id="iss-modal-x" style="margin-left:auto">✕</button>
</div>
<div class="modal-bd">
<div class="order-info">
<div class="oi-item"><span class="oi-l">Product</span><span class="oi-v">${esc(d.ItemNo)}</span></div>
<div class="oi-item"><span class="oi-l">Description</span><span class="oi-v" style="font-family:var(--font)">${esc(d.ProductDescription||'-')}</span></div>
<div class="oi-item"><span class="oi-l">Planned Qty</span><span class="oi-v">${d.PlannedQuantity}</span></div>
<div class="oi-item"><span class="oi-l">Completed</span><span class="oi-v">${d.CompletedQuantity||0}</span></div>
<div class="oi-item"><span class="oi-l">Warehouse</span><span class="oi-v">${esc(d.Warehouse||'-')}</span></div>
<div class="oi-item"><span class="oi-l">Due Date</span><span class="oi-v">${d.DueDate?new Date(d.DueDate).toLocaleDateString('en-IN'):'-'}</span></div>
</div>
<div class="sec-title" style="font-size:10px;font-weight:800;text-transform:uppercase;color:var(--text3);margin:10px 0 8px">${d._localIsConsumable?'Issue Consumable Components':'Issue Components'} (${_lines.length})</div>
${removedStrip}
${toolbar}
${_lines.length?linesHtml:'<div class="empty-msg">No component lines</div>'}
${footer}
</div>`;
setTimeout(()=>{
q('#iss-modal-x').onclick=closeIssModal;
wireLineInputs();
wireBatchInputs();
wireSelectionControls();
q('#btn-issue')&&(q('#btn-issue').onclick=doIssue);
q('#btn-clear')&&(q('#btn-clear').onclick=closeIssModal);
},0);
}
// Checkbox (include/exclude), Remove button, and the select-all/clear-all/
// restore toolbar — lets the user issue only SOME of an order's components
// today and come back for the rest later, instead of an all-or-nothing post.
function wireSelectionControls(){
document.querySelectorAll('.comp-check[data-i]').forEach(el=>{
el.onchange=function(){
const i=parseInt(this.dataset.i);
const l=_lines[i];if(!l)return;
l.selected=this.checked;
if(l.selected){
l.issueQty=l._savedQty!=null?l._savedQty:l.remainingQty;
l._savedQty=null;
if(l.batches.length) autoSelectBatches(l);
}else{
l._savedQty=l.issueQty;
l.issueQty=0;
}
render();
};
});
document.querySelectorAll('[data-remove-i]').forEach(el=>{
el.onclick=function(){
const i=parseInt(this.dataset.removeI);
const removed=_lines.splice(i,1)[0];
if(removed)_removedLines.push(removed);
render();
};
});
q('#btn-select-all')&&(q('#btn-select-all').onclick=()=>{
_lines.forEach(l=>{ if(l.remainingQty>0&&!l.selected){ l.selected=true; l.issueQty=l._savedQty!=null?l._savedQty:l.remainingQty; l._savedQty=null; if(l.batches.length)autoSelectBatches(l); } });
render();
});
q('#btn-select-none')&&(q('#btn-select-none').onclick=()=>{
_lines.forEach(l=>{ if(l.remainingQty>0&&l.selected){ l._savedQty=l.issueQty; l.selected=false; l.issueQty=0; } });
render();
});
q('#btn-restore-removed')&&(q('#btn-restore-removed').onclick=()=>{
_lines.push(..._removedLines);
_removedLines=[];
render();
});
}
function wireLineInputs(){
document.querySelectorAll('.comp-inp[data-i]').forEach(el=>{
el.onchange=function(){
const i=parseInt(this.dataset.i),f=this.dataset.f;
if(f==='warehouse'){
_lines[i].warehouse=this.value;
// Reload batches for new warehouse
_lines[i].batches=[];_lines[i].selectedBatches=[];
loadBatches(_lines[i]).then(()=>render());
}else if(f==='issueQty'){
_lines[i].issueQty=parseFloat(this.value)||0;
if(_lines[i].batches.length) autoSelectBatches(_lines[i]);
render();
}else if(f==='costingCode'){
_lines[i].costingCode=this.value;return;
}
};
});
}
function wireBatchInputs(){
document.querySelectorAll('.batch-inp').forEach(el=>{
el.onchange=function(){
const li=parseInt(this.dataset.li);
const bn=this.dataset.bn;
const qty=parseFloat(this.value)||0;
const line=_lines[li];
if(!line)return;
const existing=line.selectedBatches.find(b=>b.BatchNumber===bn);
if(existing) existing.Quantity=qty;
else if(qty>0) line.selectedBatches.push({BatchNumber:bn,Quantity:qty});
// Remove zeros
line.selectedBatches=line.selectedBatches.filter(b=>b.Quantity>0);
};
});
}
async function init(){
try{
// Check URL hash for auto-load (e.g. #order=8312&company=TEST_OIL_15122025)
const hash=window.location.hash;
const orderMatch=hash.match(/order=(\d+)/);
const coMatch=hash.match(/company=([^&]+)/);
if(coMatch){_co=decodeURIComponent(coMatch[1]);}
try{const list=await window.fetchCompanyList();if(list.length)_companies=list;}catch(_e){}
// Load this user's allowed issue Item Groups (fresh from DB, so admin
// changes apply without a re-login). Empty/absent = no restriction.
try{const pr=await api('GET','/auth/profile');_issueGroups=Array.isArray(pr?.user?.issueItemGroups)?pr.user.issueItemGroups:[];}catch(_e){_issueGroups=[];}
await Promise.all([loadWH(),loadBranches(),loadVarieties()]);
render();
if(orderMatch){
const entry=parseInt(orderMatch[1]);
if(entry) await selectOrder(entry);
}else{
// No specific order in the URL — show what's ready for issuance by
// default instead of an empty page.
loadReadyOrders();
}
}catch(e){
console.error('Init error:',e);
render();
}
}
init();
// If this page is already open (e.g. the sidebar "Pending Actions" bell
// linked here for a different order while the user was still on this same
// page), only the URL's hash changes — browsers don't reload the page for
// a hash-only change, so init() would never re-run and the old/empty state
// would stick around until a manual refresh. Re-check the hash and open
// the newly-linked order whenever that happens.
window.addEventListener('hashchange',()=>{
const hash=window.location.hash;
const orderMatch=hash.match(/order=(\d+)/);
const coMatch=hash.match(/company=([^&]+)/);
if(coMatch)_co=decodeURIComponent(coMatch[1]);
if(orderMatch){
const entry=parseInt(orderMatch[1]);
if(entry)selectOrder(entry);
}
});
</script>
<script src="/sidebar.js?v=25"></script>
</body>
</html>