+101
-10
@@ -176,11 +176,21 @@ let _userNames=null; // lazy-loaded [{username,fullName}], only fetched if CAN
|
||||
let _co=window.__DEFAULT_COMPANY__;let _companies=[{value:_co,label:_co}];
|
||||
let _rows=[],_loading=false,_hasSig=false,_curPO=null;
|
||||
let _viewSearch='',_viewStatus='',_viewPage=0; // _viewStatus: '' | 'ISSUED' | 'RECEIVED' | 'VERIFIED' — every row here is an approved Work Order already, so the filter is about which sign-off is still outstanding, not WO approval status
|
||||
let _viewDateFrom='',_viewDateTo=''; // filters by the Work Order's Created date
|
||||
let _sapBatchMatches=[], _sapBatchTimer=null; // cross-reference results from /sap/lookup/batch-in-sap — same rationale as work-order.html's identical function
|
||||
const VF_PAGE_SIZE=24;
|
||||
// This user's allowed Item Groups for issuing (Admin → user → Issue Items).
|
||||
// Empty = no restriction. Same rule the real SAP issuance enforces.
|
||||
let _issueGroups=[];
|
||||
let _balanceIds=[]; // Admin → System Settings — allowed Weighing Balance IDs; empty = free text
|
||||
// Admin → System Settings → "Raw Material Qty Issued Source". Only under
|
||||
// 'issue_for_production' (the default) does Qty Issued get set BEFORE this
|
||||
// row's own "Mark Issued" stamp (derived from the real SAP posting) — so
|
||||
// only then does it make sense to require that qty first. Under
|
||||
// 'mark_issued', THIS stamp is what sets Qty Issued to the full required
|
||||
// amount in the first place, so gating the button on qty already being
|
||||
// there would make it permanently unreachable.
|
||||
let _rawQtyIssuedSource='issue_for_production';
|
||||
|
||||
function esc(v){return(v==null?'':String(v)).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');}
|
||||
function q(s){return document.querySelector(s);}
|
||||
@@ -299,7 +309,7 @@ const ROW_STAMP_PREREQ={received:'issued',verified:'received'};
|
||||
// A row's Issued/Received/Verified cell: signer+date if already stamped, a
|
||||
// one-click "Mark …" button if this user may stamp it AND its prerequisite
|
||||
// stamp already exists, a "waiting on …" note if not, else a dash.
|
||||
function stampCell(row,canMark,which,label){
|
||||
function stampCell(row,canMark,which,label,blockedMsg){
|
||||
const sig=rowSig(row,which);
|
||||
if(sig){
|
||||
// Already signed — normally final. A woVerifyOverride/admin user who
|
||||
@@ -312,16 +322,18 @@ function stampCell(row,canMark,which,label){
|
||||
return issSigDisp(sig);
|
||||
}
|
||||
const prereq=ROW_STAMP_PREREQ[which];
|
||||
// A woVerifyOverride/admin user may sign a stage out of the normal
|
||||
// Issued→Received→Verified order (server allows it — see canOverrideStamp
|
||||
// in routes/workOrders.js) — everyone else still sees "Waiting on …" and
|
||||
// can't act until the prior stage is done.
|
||||
if(prereq&&!row[prereq+'At']&&!(canMark&&CAN_VERIFY_OVERRIDE))return `<span style="color:var(--text3);font-size:9.5px">Waiting on ${prereq}</span>`;
|
||||
// The NEXT-action button only ever appears once the prior stage is
|
||||
// genuinely done — no exception, for anyone, including a woVerifyOverride/
|
||||
// admin user. Override is only ever for RE-SIGNING an already-completed
|
||||
// stamp (see the `sig` branch above — correcting who signed/when), never
|
||||
// for skipping straight past a stage that hasn't happened yet.
|
||||
if(prereq&&!row[prereq+'At'])return `<span style="color:var(--text3);font-size:9.5px">Waiting on ${prereq}</span>`;
|
||||
if(canMark){
|
||||
if(CAN_VERIFY_OVERRIDE)
|
||||
return `<button class="btn-mini" onclick="openVerifyOverride(${row.__woId},'${row.__section}',${row.__idx},'${which}')">${esc(label)}</button>`;
|
||||
return `<button class="btn-mini" onclick="markRow(${row.__woId},'${row.__section}',${row.__idx},'${which}',this)">${esc(label)}</button>`;
|
||||
}
|
||||
if(blockedMsg)return `<span style="color:var(--text3);font-size:9.5px">${esc(blockedMsg)}</span>`;
|
||||
return '<span style="color:var(--text3)">—</span>';
|
||||
}
|
||||
// Weighing Balance ID / AR No. — editable inline by whoever holds 'edit' on
|
||||
@@ -352,8 +364,42 @@ function issCells(row,iss,groupMap){
|
||||
// and workOrderPdf.js already do.
|
||||
const qtyVal=row.__section==='raw'?row.qtyIssued:(iss.qtyByItem&&iss.qtyByItem[(row.itemCode||'').trim()]);
|
||||
const qty=`<td class="wo-td">${num3wo(qtyVal)}</td>`;
|
||||
const canIssue=CAN_MARK_ISSUED&&itemGroupAllowed(row.itemCode,groupMap);
|
||||
return qty+`<td class="wo-td">${stampCell(row,canIssue,'issued','Mark Issued')}</td><td class="wo-td">${stampCell(row,CAN_MARK_RECEIVED,'received','Mark Received')}</td><td class="wo-td">${stampCell(row,CAN_MARK_VERIFIED,'verified','Mark Verified')}</td>`;
|
||||
// "Mark Issued" only appears once this row's FULL required quantity has
|
||||
// actually been issued (in SAP for Packing/live off the PO; persisted on
|
||||
// the row itself for Raw Material — see the comment above) — not merely
|
||||
// whenever some partial quantity has gone out. A 0.001 tolerance absorbs
|
||||
// float rounding from the prorated Raw Material figure.
|
||||
const reqQty=parseFloat(row.__section==='raw'?row.qtyReq:row.qtyReqUnits)||0;
|
||||
// The 'mark_issued' bypass ONLY applies to Raw Material — its displayed
|
||||
// Qty Issued is the persisted, prorated value this exact stamp writes
|
||||
// under that mode (see the server route), so checking it first would be
|
||||
// circular. Packing Material's displayed Qty Issued always comes straight
|
||||
// off the live SAP posting (qtyByItem above) no matter which mode is set —
|
||||
// it's never written by this stamp — so it must always be checked for
|
||||
// real, or a Work Order with no Production Order/issuance yet (qtyVal
|
||||
// empty) would wrongly show "Mark Issued" as available.
|
||||
// Even under the 'mark_issued' bypass, nothing is issuable before a
|
||||
// Production Order actually exists for this Work Order — there's no
|
||||
// production event at all yet for the raw-material stamp to be recording.
|
||||
// This is what actually closes the reported bug: a fresh WO with "No
|
||||
// Production Order created yet" was still showing every raw-material row
|
||||
// as markable, because the bypass alone had no precondition of its own.
|
||||
const rawBypass=row.__section==='raw'&&_rawQtyIssuedSource==='mark_issued'&&iss.hasPO;
|
||||
const fullyIssued=rawBypass||(iss.hasPO&&(reqQty<=0||(parseFloat(qtyVal)||0)+0.001>=reqQty));
|
||||
const permittedToIssue=CAN_MARK_ISSUED&&itemGroupAllowed(row.itemCode,groupMap);
|
||||
const canIssue=permittedToIssue&&fullyIssued;
|
||||
const issuedBlockedMsg=(permittedToIssue&&!fullyIssued)?(iss.hasPO?'Awaiting full qty issued':'No Production Order yet'):undefined;
|
||||
// Received/Verified must also require a Production Order to exist — not
|
||||
// just because Issued isn't stamped yet (the ordinary prereq check below
|
||||
// already covers that for everyone), but specifically because a
|
||||
// woVerifyOverride/admin user is otherwise allowed to sign OUT of the
|
||||
// normal order to catch up genuine paperwork, and that bypass alone has no
|
||||
// "did production even start" precondition of its own. Without gating it
|
||||
// here too, that same user could stamp Received/Verified on a Work Order
|
||||
// with no Production Order at all — nothing to actually be catching up on.
|
||||
const canReceive=CAN_MARK_RECEIVED&&iss.hasPO;
|
||||
const canVerify=CAN_MARK_VERIFIED&&iss.hasPO;
|
||||
return qty+`<td class="wo-td">${stampCell(row,canIssue,'issued','Mark Issued',issuedBlockedMsg)}</td><td class="wo-td">${stampCell(row,canReceive,'received','Mark Received')}</td><td class="wo-td">${stampCell(row,canVerify,'verified','Mark Verified')}</td>`;
|
||||
}
|
||||
|
||||
function buildWoMaterials(w,iss,groupMap){
|
||||
@@ -493,6 +539,13 @@ function filteredRows(){
|
||||
(w.itemName||'').toUpperCase().includes(s)||
|
||||
(w.batchNumber||'').toUpperCase().includes(s)||
|
||||
(w.intimationDocNo||'').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;
|
||||
}
|
||||
function woCard(w){
|
||||
@@ -513,6 +566,7 @@ function woCard(w){
|
||||
<span><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0"><line x1="4" y1="9" x2="20" y2="9"/><line x1="4" y1="15" x2="20" y2="15"/><line x1="10" y1="3" x2="8" y2="21"/><line x1="16" y1="3" x2="14" y2="21"/></svg> Batch ${esc(w.batchNumber||'—')}</span>
|
||||
<span><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg> Intimation ${esc(w.intimationDocNo||'—')}</span>
|
||||
<span><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg> ${esc(w.createdByName||w.createdBy||'—')}</span>
|
||||
${w.createdAt?`<span>${new Date(w.createdAt).toLocaleDateString('en-IN',{day:'2-digit',month:'short',year:'2-digit'})}</span>`:''}
|
||||
<span style="color:var(--text3)">${esc(poChip)}</span>
|
||||
</div>
|
||||
</div>`;
|
||||
@@ -540,9 +594,19 @@ function render(){
|
||||
!_rows.length?`<div class="empty-msg">No approved work orders yet.</div>`:`
|
||||
<div class="vf-bar">
|
||||
<div class="vf-search"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="position:absolute;left:11px;top:50%;transform:translateY(-50%);color:var(--text3)"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg><input id="vf-search" placeholder="Search WO No, product, batch, intimation…" value="${esc(_viewSearch)}" style="padding-left:32px"/></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}" title="${v?'Still pending '+lbl:'All approved Work Orders'}">${lbl}</button>`).join('')}</div>
|
||||
<span class="vf-count">${list.length} of ${_rows.length}</span>
|
||||
</div>
|
||||
${_sapBatchMatches.length?`<div style="margin:0 0 12px;padding:10px 12px;background:var(--surface2,#f5f7fa);border:1px solid var(--border,#dfe4ea);border-radius:10px">
|
||||
<div style="font-size:11px;font-weight:700;color:var(--text2);margin-bottom:6px">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>`:''}
|
||||
${pageList.length?`<div class="wo-grid">${pageList.map(woCard).join('')}</div>`:'<div class="empty-msg">No work orders match this search/filter.</div>'}
|
||||
${list.length>VF_PAGE_SIZE?`<div class="vf-pager">
|
||||
<button id="vf-prev" ${_viewPage===0?'disabled':''}>← Prev</button>
|
||||
@@ -556,8 +620,11 @@ function render(){
|
||||
q('#vf-search')&&(q('#vf-search').oninput=function(){
|
||||
const pos=this.selectionStart;_viewSearch=this.value;_viewPage=0;render();
|
||||
const el=q('#vf-search');if(el){el.focus();try{el.setSelectionRange(pos,pos);}catch(_e){}}
|
||||
scheduleSapBatchLookup();
|
||||
});
|
||||
document.querySelectorAll('[data-vfstatus]').forEach(b=>b.onclick=function(){_viewStatus=this.dataset.vfstatus;_viewPage=0;render();});
|
||||
q('#vf-datefrom')&&(q('#vf-datefrom').onchange=function(){_viewDateFrom=this.value;_viewPage=0;render();});
|
||||
q('#vf-dateto')&&(q('#vf-dateto').onchange=function(){_viewDateTo=this.value;_viewPage=0;render();});
|
||||
if(q('#vf-prev'))q('#vf-prev').onclick=function(){_viewPage--;render();};
|
||||
if(q('#vf-next'))q('#vf-next').onclick=function(){_viewPage++;render();};
|
||||
document.querySelectorAll('[data-open]').forEach(el=>el.onclick=function(){
|
||||
@@ -565,15 +632,39 @@ function render(){
|
||||
});
|
||||
}
|
||||
|
||||
// Same cross-reference lookup as Batch Issuance/Work Order's own search
|
||||
// boxes — see batch-issuance.html'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).
|
||||
function scheduleSapBatchLookup(){
|
||||
clearTimeout(_sapBatchTimer);
|
||||
const term=_viewSearch.trim();
|
||||
if(term.length<4){
|
||||
if(_sapBatchMatches.length){_sapBatchMatches=[];render();}
|
||||
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;
|
||||
render();
|
||||
const s2=q('#vf-search');if(s2&&pos!=null){s2.focus();try{s2.setSelectionRange(pos,pos);}catch(_e){}}
|
||||
},450);
|
||||
}
|
||||
|
||||
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){}
|
||||
try{const pr=await api('GET','/auth/profile');_issueGroups=Array.isArray(pr?.user?.issueItemGroups)?pr.user.issueItemGroups.map(String):[];}catch(_e){}
|
||||
try{const cfg=await api('GET','/config');if(Array.isArray(cfg?.woWeighingBalanceIds))_balanceIds=cfg.woWeighingBalanceIds;}catch(_e){}
|
||||
try{const cfg=await api('GET','/config');if(Array.isArray(cfg?.woWeighingBalanceIds))_balanceIds=cfg.woWeighingBalanceIds;if(cfg?.woRawQtyIssuedSource)_rawQtyIssuedSource=cfg.woRawQtyIssuedSource;}catch(_e){}
|
||||
if(CAN_VIEW)load();else render();
|
||||
}
|
||||
init();
|
||||
</script>
|
||||
<script src="/sidebar.js?v=25"></script>
|
||||
<script src="/sidebar.js?v=31"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user