+87
-8
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user