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
+108 -18
View File
@@ -192,8 +192,16 @@ let _editId=null;
let _saving=false, _created=null;
let _list=[], _listLoading=false, _expanded=null;
let _linkedWo={}; // { [intimationId]: [{id,woNo,productCode,batchNumber}] } — Delete stays fully blocked once ANY Work Order exists; Edit now locks only the SPECIFIC (productCode,batchNumber) rows that already have one — see startEdit()
let _linkedWoAll={}; // same shape, but REJECTED Work Orders included — used only to keep an
// already-referenced row's Batch No. (and the row itself) from being
// changed/removed even while the rest of that row stays editable for a
// rejected-and-resubmitting Work Order — see _editLockedSet/_batchNoLockedSet
let _batchNoLockedSet=new Set(); // `${itemCode}|${batchNo}` keys whose Batch No. field (and row-remove button) must stay locked this edit session, even for rows the fuller _editLockedSet leaves otherwise editable
let _editLockedSet=new Set(); // `${itemCode}|${batchNo}` keys locked for the CURRENTLY open edit session — those rows are read-only (already used by a generated Work Order); everything else in the document stays freely editable
let _viewSearch='', _viewStatus='';
let _viewDateFrom='', _viewDateTo=''; // filters by the Intimation's own Date field
let _viewPage=0; const VF_PAGE_SIZE=20;
let _sapBatchMatches=[], _sapBatchQuery='', _sapBatchTimer=null; // cross-reference results from /sap/lookup/batch-in-sap — see scheduleSapBatchLookup()
let _batchManagedItems=new Set(); // item codes SAP itself tracks by batch (OITM.ManBtchNum='Y') — Batch No./MFG/EXP become required per item automatically
let _biAllowExceedPending=false; // Admin → System Settings — when true, Total Units may exceed the requirement's pending qty
// SFG (Semi-Finished Good) workflow — Admin → System Settings → Batch
@@ -689,15 +697,21 @@ function renderCreate(){
// editable, and new ones can always be added.
const rowLocked=_editLockedSet.has(`${p.itemCode}|${b.batchNo}`);
const dis=rowLocked?'disabled':'';
// Batch No. itself (and this row's remove button) stays locked even for
// a row a rejected Work Order references — otherwise fixing the other
// fields could also let the Batch No. drift, silently breaking that
// rejected order's (productCode,batchNumber) link back to this row.
const noLocked=rowLocked||_batchNoLockedSet.has(`${p.itemCode}|${b.batchNo}`);
const noDis=noLocked?'disabled':'';
return `
<tr${rowLocked?' title="Already used by a generated Work Order — locked"':''}>
<td><input class="fi" ${dis} data-p="${pi}" data-b="${bi}" data-f="batchNo" placeholder="${req?'Batch No. *':'Batch No.'}" value="${esc(b.batchNo)}" style="${req?'border-color:var(--amber)':''}"/></td>
<tr${rowLocked?' title="Already used by a generated Work Order — locked"':(noLocked?' title="Batch No. is used by a rejected Work Order — number is locked, other fields can still be corrected"':'')}>
<td><input class="fi" ${noDis} data-p="${pi}" data-b="${bi}" data-f="batchNo" placeholder="${req?'Batch No. *':'Batch No.'}" value="${esc(b.batchNo)}" style="${req?'border-color:var(--amber)':''}"/></td>
<td>${dateCell(pi,bi,'mfgDate',b.mfgDate,'req',rowDateFmt(pi,bi,'req',b),rowLocked)}</td>
<td>${dateCell(pi,bi,'expDate',b.expDate,'req',rowDateFmt(pi,bi,'req',b),rowLocked)}</td>
<td><input class="fi" ${dis} data-p="${pi}" data-b="${bi}" data-f="market" placeholder="Market" value="${esc(b.market)}"/></td>
<td><input class="fi" ${dis} type="number" min="0" step="any" data-p="${pi}" data-b="${bi}" data-f="batchSize" placeholder="0" value="${esc(b.batchSize)}"/></td>
${batchVolumeCell('req',pi,bi,b,dis)}
<td class="td-act">${rowLocked?`<i data-lucide="lock" style="width:14px;height:14px;color:var(--text3)" title="Locked — Work Order already generated"></i>`:`<button class="b-del" data-rmb="${pi}:${bi}" title="Remove batch"><i data-lucide="trash-2" style="width:14px;height:14px"></i></button>`}</td>
<td class="td-act">${noLocked?`<i data-lucide="lock" style="width:14px;height:14px;color:var(--text3)" title="${rowLocked?'Locked — Work Order already generated':'Batch No. locked — a rejected Work Order references it'}"></i>`:`<button class="b-del" data-rmb="${pi}:${bi}" title="Remove batch"><i data-lucide="trash-2" style="width:14px;height:14px"></i></button>`}</td>
</tr>`;}).join('');
return `
<div class="prod-block">
@@ -785,17 +799,23 @@ function renderSfgCreate(){
// editable, and new ones can always be added.
const rowLocked=_editLockedSet.has(`${p.itemCode}|${b.batchNo}`);
const dis=rowLocked?'disabled':'';
// Batch No. itself (and this row's remove button) stays locked even for
// a row a rejected Work Order references — otherwise fixing the other
// fields could also let the Batch No. drift, silently breaking that
// rejected order's (productCode,batchNumber) link back to this row.
const noLocked=rowLocked||_batchNoLockedSet.has(`${p.itemCode}|${b.batchNo}`);
const noDis=noLocked?'disabled':'';
return `
<tr${rowLocked?' title="Already used by a generated Work Order — locked"':''}>
<td><input class="fi" ${dis} data-p="${pi}" data-b="${bi}" data-f="batchNo" placeholder="${req?'Batch No. *':'Batch No.'}" value="${esc(b.batchNo)}" style="${req?'border-color:var(--amber)':''}"/></td>
<tr${rowLocked?' title="Already used by a generated Work Order — locked"':(noLocked?' title="Batch No. is used by a rejected Work Order — number is locked, other fields can still be corrected"':'')}>
<td><input class="fi" ${noDis} data-p="${pi}" data-b="${bi}" data-f="batchNo" placeholder="${req?'Batch No. *':'Batch No.'}" value="${esc(b.batchNo)}" style="${req?'border-color:var(--amber)':''}"/></td>
<td>${dateCell(pi,bi,'mfgDate',b.mfgDate,'sfg',rowDateFmt(pi,bi,'sfg',b),rowLocked)}</td>
<td>${dateCell(pi,bi,'expDate',b.expDate,'sfg',rowDateFmt(pi,bi,'sfg',b),rowLocked)}</td>
<td><input class="fi" ${dis} data-p="${pi}" data-b="${bi}" data-f="market" placeholder="Market" value="${esc(b.market)}"/></td>
<td><input class="fi" ${dis} type="number" min="0" step="any" data-p="${pi}" data-b="${bi}" data-f="batchSize" placeholder="0" value="${esc(b.batchSize)}"/></td>
${batchVolumeCell('sfg',pi,bi,b,dis)}
<td class="td-act">${rowLocked?`<i data-lucide="lock" style="width:14px;height:14px;color:var(--text3)" title="Locked — Work Order already generated"></i>`:`<button class="b-del" data-rmb="${pi}:${bi}" title="Remove batch"><i data-lucide="trash-2" style="width:14px;height:14px"></i></button>`}</td>
<td class="td-act">${noLocked?`<i data-lucide="lock" style="width:14px;height:14px;color:var(--text3)" title="${rowLocked?'Locked — Work Order already generated':'Batch No. locked — a rejected Work Order references it'}"></i>`:`<button class="b-del" data-rmb="${pi}:${bi}" title="Remove batch"><i data-lucide="trash-2" style="width:14px;height:14px"></i></button>`}</td>
</tr>`;}).join('');
const prodLocked=p.batches.some(b=>_editLockedSet.has(`${p.itemCode}|${b.batchNo}`));
const prodLocked=p.batches.some(b=>_editLockedSet.has(`${p.itemCode}|${b.batchNo}`)||_batchNoLockedSet.has(`${p.itemCode}|${b.batchNo}`));
return `
<div class="prod-block">
<div class="prod-hd">
@@ -840,7 +860,7 @@ function renderSfgCreate(){
// Patches only #body (not a full render()) so an in-progress search/edit in
// this tab is never disrupted — same principle used throughout this app.
function renderSfgOnly(){const b=q('#body');if(b){b.innerHTML=renderSfgCreate();wireSfgCreate();if(window.lucide)lucide.createIcons();}}
function resetSfg(){_sfgProducts=[];_sfgDate=today();_sfgRemarks='';_sfgSearchVal='';_editLockedSet=new Set();}
function resetSfg(){_sfgProducts=[];_sfgDate=today();_sfgRemarks='';_sfgSearchVal='';_editLockedSet=new Set();_batchNoLockedSet=new Set();}
async function fetchSfgItems(v){try{const r=await api('GET',`/sap/lookup/items?q=${encodeURIComponent(v)}&company=${encodeURIComponent(_co)}`);if(r.success&&r.data?.length)return r.data;}catch{}return[];}
async function loadSfgBatchManagedItems(){
@@ -1030,7 +1050,7 @@ async function submit(){
}catch(e){_saving=false;render();toast(e.message,'err');}
}
function newDoc(){_editId=null;_created=null;_reqId='';_refNo='';_products=[];_date=today();_remarks='';_batchManagedItems=new Set();_editLockedSet=new Set();_tab='create';render();loadReqs();}
function newDoc(){_editId=null;_created=null;_reqId='';_refNo='';_products=[];_date=today();_remarks='';_batchManagedItems=new Set();_editLockedSet=new Set();_batchNoLockedSet=new Set();_tab='create';render();loadReqs();}
async function startEdit(bi){
// Rows whose (itemCode,batchNo) already has a generated Work Order —
@@ -1040,6 +1060,8 @@ async function startEdit(bi){
// added. Same key format captureStatus() already uses.
const linked=_linkedWo[String(bi.id)]||[];
_editLockedSet=new Set(linked.map(l=>`${l.productCode||''}|${l.batchNumber||''}`));
const linkedAll=_linkedWoAll[String(bi.id)]||[];
_batchNoLockedSet=new Set(linkedAll.map(l=>`${l.productCode||''}|${l.batchNumber||''}`));
// A "Without Intimation" (SFG) record has no refNo/requirement at all by
// design — editing it must stay on that same no-requirement path, not the
// regular Requirement-based edit form (whose submit() requires a refNo and
@@ -1109,7 +1131,7 @@ function renderSuccess(app){
async function loadList(){
_listLoading=true;render();
try{const r=await api('GET',`/batch-intimations?company=${encodeURIComponent(_co)}`);_list=r.data||[];}catch(e){toast(e.message,'err');_list=[];}
try{const r=await api('GET','/batch-intimations/linked-work-orders/all');_linkedWo=r.data||{};}catch(_e){_linkedWo={};}
try{const r=await api('GET','/batch-intimations/linked-work-orders/all');_linkedWo=r.data||{};_linkedWoAll=r.allData||{};}catch(_e){_linkedWo={};_linkedWoAll={};}
_listLoading=false;render();
}
function lockedInfo(id){const l=_linkedWo[String(id)];return (l&&l.length)?l:null;}
@@ -1139,23 +1161,45 @@ function filteredBiList(){
(bi.docNo||'').toUpperCase().includes(s)||
(bi.refNo||'').toUpperCase().includes(s)||
(bi.createdByName||bi.createdBy||'').toUpperCase().includes(s)||
(bi.products||[]).some(p=>(p.itemCode||'').toUpperCase().includes(s)||(p.itemName||'').toUpperCase().includes(s)));
(bi.products||[]).some(p=>(p.itemCode||'').toUpperCase().includes(s)||(p.itemName||'').toUpperCase().includes(s)||
(p.batches||[]).some(b=>(b.batchNo||'').toUpperCase().includes(s))));
if(_viewDateFrom||_viewDateTo)list=list.filter(bi=>{
if(!bi.date)return false;
const d=bi.date.slice(0,10);
if(_viewDateFrom&&d<_viewDateFrom)return false;
if(_viewDateTo&&d>_viewDateTo)return false;
return true;
});
return list;
}
function renderView(){
if(_listLoading)return `<div class="card"><div class="empty-msg"><span class="sp" style="border-color:rgba(37,99,235,.25);border-top-color:#2563eb"></span> Loading…</div></div>`;
if(!_list.length)return `<div class="card"><div class="empty-msg">No intimations yet. Create one from the “New Intimation” tab.</div></div>`;
const list=filteredBiList();
const allList=filteredBiList();
const totalPages=Math.max(1,Math.ceil(allList.length/VF_PAGE_SIZE));
if(_viewPage>=totalPages)_viewPage=totalPages-1;
if(_viewPage<0)_viewPage=0;
const list=allList.slice(_viewPage*VF_PAGE_SIZE,(_viewPage+1)*VF_PAGE_SIZE);
const statuses=['SENT TO QA','PARTIALLY CAPTURED','FULLY CAPTURED'];
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 doc no, ref no, item code/name…" 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">
<button class="vf-chip ${!_viewStatus?'on':''}" data-vfstatus="">All</button>
${statuses.map(st=>`<button class="vf-chip ${_viewStatus===st?'on':''}" data-vfstatus="${esc(st)}">${esc(st)}</button>`).join('')}
</div>
<span class="vf-count">${list.length} of ${_list.length}</span>
<span class="vf-count">${allList.length} of ${_list.length}</span>
</div>`;
const sapBanner=_sapBatchMatches.length?`<div style="margin-bottom:12px;padding:10px 12px;background:var(--surface2);border:1px solid var(--border,#dfe4ea);border-radius:10px">
<div style="font-size:11px;font-weight:700;color:var(--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 Intimations/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:var(--blue)" target="_blank">Production Order ${esc(String(m.docNum))}</a> — ${esc(m.itemCode)} ${esc(m.itemName||'')} · Batch (SAP Comments) <span style="font-family:var(--mono)">${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:var(--mono)">${esc(m.batch)}</span></div>`
).join('')}
</div>`:'';
const grid=list.length
? `<div class="bi-grid">${list.map(bi=>{
const batchCount=(bi.products||[]).reduce((n,p)=>n+((p.batches||[]).length),0);
@@ -1177,9 +1221,40 @@ function renderView(){
<span>${esc(bi.createdByName||bi.createdBy||'')}${bi.updatedAt?' · edited':''}</span>
</div>
</div>`;
}).join('')}</div>`
}).join('')}</div>
<div style="display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:12px">
<button class="btn b-cancel" id="vf-prev" ${_viewPage===0?'disabled':''} style="padding:6px 14px;font-size:11px">‹ Prev</button>
<span style="font-size:11px;color:var(--text3)">Page ${_viewPage+1} of ${totalPages} · ${allList.length} intimation${allList.length===1?'':'s'}</span>
<button class="btn b-cancel" id="vf-next" ${_viewPage>=totalPages-1?'disabled':''} style="padding:6px 14px;font-size:11px">Next ›</button>
</div>`
: `<div class="card"><div class="empty-msg">No intimations match your search/filter.</div></div>`;
return bar+grid;
return bar+sapBanner+grid;
}
// Looks up the current search term against SAP itself (OBTN batch master +
// Production Order Comments — see /sap/lookup/batch-in-sap) once it's long
// enough to be a real batch number, not just item-code-length noise. Debounced
// so it doesn't fire on every keystroke; only re-renders the SAP banner, not
// the whole list, to avoid stealing focus from the search input mid-type.
function scheduleSapBatchLookup(){
clearTimeout(_sapBatchTimer);
const term=_viewSearch.trim();
if(term.length<4){
if(_sapBatchMatches.length){_sapBatchMatches=[];_sapBatchQuery='';q('#body').innerHTML=renderView();wireView();if(window.lucide)lucide.createIcons();}
return;
}
_sapBatchTimer=setTimeout(async()=>{
if(_viewSearch.trim()!==term)return; // stale — a newer keystroke already superseded this
try{
const r=await api('GET',`/sap/lookup/batch-in-sap?batch=${encodeURIComponent(term)}&company=${encodeURIComponent(_co)}`);
if(_viewSearch.trim()!==term)return; // still stale by the time the response lands
_sapBatchMatches=r.data||[];_sapBatchQuery=term;
}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 closeBiDetail(){document.getElementById('bi-detail-modal').classList.remove('show');}
@@ -1228,8 +1303,15 @@ function openBiDetail(id){
if(window.lucide)lucide.createIcons();
document.getElementById('bi-modal-x').onclick=closeBiDetail;
if(CAN_CREATE_WO&&!fullyCaptured)document.getElementById('bi-modal-wo').onclick=()=>{window.location.href='/work-order?from='+bi.id;};
if(!locked){
// Edit's own enable/disable condition is fullyCaptured (a partial lock
// still opens Edit, just with the locked rows read-only inside it — see
// startEdit()) — this must match, not `locked`, which is also true for a
// PARTIAL lock and was silently leaving Edit's click handler unwired
// while the button still rendered enabled (looked clickable, did nothing).
if(!fullyCaptured){
document.getElementById('bi-modal-edit').onclick=()=>{closeBiDetail();startEdit(bi);};
}
if(!locked){
document.getElementById('bi-modal-del').onclick=()=>{closeBiDetail();deleteDoc(bi.id,bi.docNo);};
}
}
@@ -1240,15 +1322,23 @@ function wireView(){
if(s){
s.oninput=function(){
_viewSearch=this.value;
_viewPage=0;
const pos=this.selectionStart;
q('#body').innerHTML=renderView();
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;q('#body').innerHTML=renderView();wireView();if(window.lucide)lucide.createIcons();});
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();});
const df=q('#vf-datefrom'),dt=q('#vf-dateto');
if(df)df.onchange=function(){_viewDateFrom=this.value;_viewPage=0;q('#body').innerHTML=renderView();wireView();if(window.lucide)lucide.createIcons();};
if(dt)dt.onchange=function(){_viewDateTo=this.value;_viewPage=0;q('#body').innerHTML=renderView();wireView();if(window.lucide)lucide.createIcons();};
const vp=q('#vf-prev'),vn=q('#vf-next');
if(vp)vp.onclick=function(){_viewPage--;q('#body').innerHTML=renderView();wireView();if(window.lucide)lucide.createIcons();};
if(vn)vn.onclick=function(){_viewPage++;q('#body').innerHTML=renderView();wireView();if(window.lucide)lucide.createIcons();};
}
// ─────────── INIT ───────────
@@ -1291,6 +1381,6 @@ api('GET','/config').then(cfg=>{
}
}).catch(()=>{});
</script>
<script src="/sidebar.js?v=25"></script>
<script src="/sidebar.js?v=31"></script>
</body>
</html>