sale order
SAP-ERP Portal CI/CD / build (push) Successful in 3m57s

This commit is contained in:
John
2026-10-05 18:45:17 +05:30
parent e725a6571b
commit eead8f5ffd
129 changed files with 14252 additions and 452 deletions
+87 -8
View File
@@ -302,6 +302,8 @@ let _bomFetched=false;
let _saving=false, _created=null, _bomLoading=false;
let _list=[], _listLoading=false;
let _viewSearch='', _viewStatus='', _viewPage=0;
let _viewDateFrom='', _viewDateTo=''; // filters by the Work Order's Created date
let _sapBatchMatches=[], _sapBatchTimer=null; // cross-reference results from /sap/lookup/batch-in-sap — see scheduleSapBatchLookup()
const VF_PAGE_SIZE=24;
let _searchTimer={}, _ddInput=null;
@@ -1796,6 +1798,35 @@ async function startEdit(w){
// then shows up correctly instead of staying wrongly flattened forever.
// (A non-rejected edit, or one that was never flattened, keeps loading the
// saved rows as before — no need to touch what's already correct.)
// A rejected order's header (batch size/MFG/EXP/etc.) is a snapshot taken
// when it was first created from the Batch Intimation — if that Intimation
// was corrected afterwards (the whole point of sending a WO back), editing
// here must show the CURRENT intimation data, not the stale snapshot. Only
// for the rejected+resubmit path — a normal in-flight edit keeps its own
// already-correct saved values untouched.
if(_editingRejected&&_intimationId){
try{
const r=await api('GET',`/batch-intimations/${_intimationId}`);
const it=r.data;
const p=(it.products||[]).find(x=>String(x.itemCode)===String(w.productCode));
const b=p&&(p.batches||[]).find(x=>String(x.batchNo)===String(w.batchNumber));
if(b){
_srcBatchVolume=b.batchVolume!=null&&b.batchVolume!==''?String(b.batchVolume):'';
_srcSolutionVolumes=Array.isArray(b.solutionVolumes)?b.solutionVolumes.filter(sv=>(sv.name||'').trim()&&sv.volume!=null&&sv.volume!==''):[];
Object.assign(_wo,{
reference:it.docNo||w.reference||'',
productName:p.itemName||p.itemDesc||w.productName||'',
batchNumber:b.batchNo||w.batchNumber||'',
totalUnits:b.batchSize!=null?String(b.batchSize):(w.totalUnits||''),
batchSize:_srcBatchVolume?`${_srcBatchVolume} Ltrs`:(w.batchSize||''),
mfgDate:b.mfgDate||'',
expDate:b.expDate||'',
market:b.market||w.market||'',
});
toast('Header refreshed from latest '+(it.docNo||'Intimation')+' data');
}
}catch(_e){/* fall back silently to the saved snapshot below */}
}
if(_editingRejected&&w.componentsOnly&&_wo.productCode){
_forceComponentsOnly=false; // try for a real split now, not the old flat snapshot
_bomFetched=false;_raw=[rawBlank()];_pack=[packBlank()];
@@ -1875,6 +1906,13 @@ function filteredWOList(){
(w.productName||'').toUpperCase().includes(s)||
(w.batchNumber||'').toUpperCase().includes(s)||
(w.reference||'').toUpperCase().includes(s));
if(_viewDateFrom||_viewDateTo)list=list.filter(w=>{
if(!w.createdAt)return false;
const d=w.createdAt.slice(0,10);
if(_viewDateFrom&&d<_viewDateFrom)return false;
if(_viewDateTo&&d>_viewDateTo)return false;
return true;
});
return list;
}
@@ -1889,6 +1927,9 @@ function renderView(){
const chips=[['','All'],['IN_PROGRESS','Pending'],['APPROVED','Approved'],['REJECTED','Rejected']];
const bar=`<div class="vf-bar">
<div class="vf-search"><i data-lucide="search" style="width:14px;height:14px"></i><input id="vf-search" placeholder="Search WO No, product, batch, reference…" value="${esc(_viewSearch)}"/></div>
<input class="fi" id="vf-datefrom" type="date" value="${esc(_viewDateFrom)}" style="width:140px"/>
<span style="font-size:11px;color:var(--text3)">to</span>
<input class="fi" id="vf-dateto" type="date" value="${esc(_viewDateTo)}" style="width:140px"/>
<div class="vf-chips">${chips.map(([v,lbl])=>`<button class="vf-chip ${_viewStatus===v?'on':''}" data-vfstatus="${v}">${lbl}</button>`).join('')}</div>
<span class="vf-count">${list.length} of ${_list.length}</span>
</div>`;
@@ -1914,11 +1955,44 @@ function renderView(){
<span><i data-lucide="hash" style="width:12px;height:12px;flex-shrink:0"></i> Batch ${esc(w.batchNumber||'—')}</span>
<span><i data-lucide="link" style="width:12px;height:12px;flex-shrink:0"></i> Intimation ${esc(w.intimationDocNo||'—')}</span>
<span><i data-lucide="user" style="width:12px;height:12px;flex-shrink:0"></i> ${esc(w.createdByName||w.createdBy||'—')}</span>
${w.createdAt?`<span><i data-lucide="calendar" style="width:12px;height:12px;flex-shrink:0"></i> ${fmtDate(w.createdAt)}</span>`:''}
</div>
</div>`;
}).join('')}</div>`
: `<div class="card"><div class="empty-msg">No work orders match your search/filter.</div></div>`;
return bar+grid+pager;
const sapBanner=_sapBatchMatches.length?`<div style="margin:0 0 12px;padding:10px 12px;background:var(--wo-surface2,#f5f7fa);border:1px solid var(--wo-border,#dfe4ea);border-radius:10px">
<div style="font-size:11px;font-weight:700;color:var(--wo-text2);margin-bottom:6px"><i data-lucide="search-check" style="width:12px;height:12px;vertical-align:-2px"></i> Also found in SAP (not one of this portal's own Work Orders):</div>
${_sapBatchMatches.map(m=>m.source==='productionOrder'
?`<div style="font-size:12px;padding:3px 0"><a href="/production#order=${m.absEntry}&company=${encodeURIComponent(_co)}" style="color:#2563eb" target="_blank">Production Order ${esc(String(m.docNum))}</a> — ${esc(m.itemCode)} ${esc(m.itemName||'')} · Batch (SAP Comments) <span style="font-family:monospace">${esc(m.batch)}</span></div>`
:`<div style="font-size:12px;padding:3px 0">SAP Batch Master — ${esc(m.itemCode)} ${esc(m.itemName||'')} · <span style="font-family:monospace">${esc(m.batch)}</span></div>`
).join('')}
</div>`:'';
return bar+sapBanner+grid+pager;
}
// Same cross-reference lookup as Batch Issuance's own search box — see that
// file's identical function for the full rationale (SAP Production Orders
// created directly in SAP B1 store their batch number as free text in
// Comments, invisible to every local search otherwise).
function scheduleSapBatchLookup(){
clearTimeout(_sapBatchTimer);
const term=_viewSearch.trim();
if(term.length<4){
if(_sapBatchMatches.length){_sapBatchMatches=[];q('#body').innerHTML=renderView();wireView();if(window.lucide)lucide.createIcons();}
return;
}
_sapBatchTimer=setTimeout(async()=>{
if(_viewSearch.trim()!==term)return;
try{
const r=await api('GET',`/sap/lookup/batch-in-sap?batch=${encodeURIComponent(term)}&company=${encodeURIComponent(_co)}`);
if(_viewSearch.trim()!==term)return;
_sapBatchMatches=r.data||[];
}catch(_e){_sapBatchMatches=[];}
const pos=q('#vf-search')?.selectionStart;
q('#body').innerHTML=renderView();wireView();
const s2=q('#vf-search');if(s2&&pos!=null){s2.focus();s2.setSelectionRange(pos,pos);}
if(window.lucide)lucide.createIcons();
},450);
}
function wireView(){
@@ -1933,9 +2007,12 @@ function wireView(){
const s2=q('#vf-search');
if(s2){s2.focus();s2.setSelectionRange(pos,pos);}
if(window.lucide)lucide.createIcons();
scheduleSapBatchLookup();
};
}
document.querySelectorAll('[data-vfstatus]').forEach(b=>b.onclick=function(){_viewStatus=this.dataset.vfstatus;_viewPage=0;q('#body').innerHTML=renderView();wireView();if(window.lucide)lucide.createIcons();});
if(q('#vf-datefrom'))q('#vf-datefrom').onchange=function(){_viewDateFrom=this.value;_viewPage=0;q('#body').innerHTML=renderView();wireView();if(window.lucide)lucide.createIcons();};
if(q('#vf-dateto'))q('#vf-dateto').onchange=function(){_viewDateTo=this.value;_viewPage=0;q('#body').innerHTML=renderView();wireView();if(window.lucide)lucide.createIcons();};
if(q('#vf-prev'))q('#vf-prev').onclick=function(){_viewPage--;q('#body').innerHTML=renderView();wireView();if(window.lucide)lucide.createIcons();};
if(q('#vf-next'))q('#vf-next').onclick=function(){_viewPage++;q('#body').innerHTML=renderView();wireView();if(window.lucide)lucide.createIcons();};
}
@@ -2079,19 +2156,21 @@ async function openWODetail(id){
// permission (matches routes/workOrders.js's PUT /:id REJECTED branch).
const canEditHere=w.status==='REJECTED'?hasStepPerm(`work_order:${STEP_KEYS[0]}`,'edit'):(canAct&&curStepKey&&hasStepPerm(`work_order:${curStepKey}`,'edit'));
const canDeleteHere=hasStepPerm(`work_order:${curStepKey||'prepared_qa'}`,'delete');
// Admin-only override: recall a FULLY APPROVED order back to QA for
// editing (no approval step required — an admin action, not a normal
// in-flight rejection). Re-uses the REJECTED status, so once sent back it
// shows up with the existing "Edit & Resubmit" button, exactly like a
// Recall a FULLY APPROVED order back to QA for editing — admin/
// system_admin always may; a regular user may also be granted this via
// 'approve' on the dedicated work_order:send_to_qa step (Admin → Edit
// User → Approval Steps) — previously admin/system_admin only, no
// assignable step at all. Re-uses the REJECTED status, so once sent back
// it shows up with the existing "Edit & Resubmit" button, exactly like a
// normal rejection would.
const canSendToQa=w.status==='APPROVED'&&_user&&(_user.role==='admin'||_user.role==='system_admin');
const canSendToQa=w.status==='APPROVED'&&_user&&(_user.role==='admin'||_user.role==='system_admin'||hasStepPerm('work_order:send_to_qa','approve'));
const footerBtns=`
<button class="wc-btn" onclick="window.open('/work-order-print?id=${w.id}','_blank')"><i data-lucide="printer" style="width:13px;height:13px"></i> Print</button>
<button class="wc-btn" onclick="downloadWoPdf(${w.id},'${esc(w.woNo)}')"><i data-lucide="file-down" style="width:13px;height:13px"></i> Download PDF</button>
${CAN_DUPLICATE_WO?`<button class="wc-btn" data-dup="${w.id}" title="Create a new Work Order — same item selection, fresh batch, starts at Step 1"><i data-lucide="copy" style="width:13px;height:13px"></i> Duplicate</button>`:''}
${canApproveHere?`<button class="wc-btn" data-appr="${w.id}" style="border-color:#b3dab3;color:#16a34a"><i data-lucide="check" style="width:13px;height:13px"></i> Approve step</button>
<button class="wc-btn del" data-rej="${w.id}"><i data-lucide="x" style="width:13px;height:13px"></i> Reject</button>`:''}
${canSendToQa?`<button class="wc-btn" data-sendqa="${w.id}" style="border-color:#fbd38d;color:#b45309" title="Admin: recall this fully approved Work Order back to QA for editing"><i data-lucide="undo-2" style="width:13px;height:13px"></i> Send to QA</button>`:''}
${canSendToQa?`<button class="wc-btn" data-sendqa="${w.id}" style="border-color:#fbd38d;color:#b45309" title="Recall this fully approved Work Order back to QA for editing — restarts the full 5-step chain"><i data-lucide="undo-2" style="width:13px;height:13px"></i> Send to QA</button>`:''}
${canEditHere?`<button class="wc-btn" data-edit="${w.id}"><i data-lucide="pencil" style="width:13px;height:13px"></i> ${w.status==='REJECTED'?'Edit & Resubmit':'Edit'}</button>`:''}
${canDeleteHere?`<button class="wc-btn del" data-del="${w.id}"><i data-lucide="trash-2" style="width:13px;height:13px"></i> Delete</button>`:''}`;
@@ -2275,6 +2354,6 @@ function applyDuplicatePayloadIfAny(){
}
})();
</script>
<script src="/sidebar.js?v=25"></script>
<script src="/sidebar.js?v=31"></script>
</body>
</html>