1064 lines
64 KiB
HTML
1064 lines
64 KiB
HTML
<!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,maximum-scale=1"/>
|
||
<title>Requirements Management — SAP B1</title>
|
||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700;800&display=swap" rel="stylesheet"/>
|
||
<script src="/lucide.min.js?v=399"></script>
|
||
<style>
|
||
:root{
|
||
--blue:#2563eb;--blue2:#1d4ed8;--blue-light:#eff6ff;
|
||
--text:#111827;--text2:#374151;--text3:#6b7280;--text4:#9ca3af;
|
||
--bg:#f9fafb;--surface:#fff;--surface2:#f9fafb;--border:#e5e7eb;--border2:#d1d5db;
|
||
--font:'Space Grotesk',sans-serif;--mono:'JetBrains Mono',ui-monospace,monospace;
|
||
--red:#dc2626;--green:#16a34a;--amber:#d97706;--radius:10px;
|
||
--shadow:0 1px 3px rgba(0,0,0,.06),0 1px 2px rgba(0,0,0,.04);
|
||
}
|
||
*{box-sizing:border-box;margin:0;padding:0}
|
||
body{font-family:var(--font);background:var(--bg);color:var(--text);min-height:100vh;-webkit-font-smoothing:antialiased}
|
||
main{padding:24px;max-width:1100px;margin:0 auto}
|
||
|
||
.page-head{margin-bottom:18px}
|
||
.page-title{font-size:22px;font-weight:800;color:var(--text);display:flex;align-items:center;gap:10px}
|
||
.page-sub{font-size:13px;color:var(--text3);margin-top:3px}
|
||
|
||
.tabs{display:inline-flex;gap:4px;background:var(--surface3,#f3f4f6);padding:4px;border-radius:10px;margin-bottom:16px}
|
||
.tab{padding:8px 18px;border-radius:8px;font-size:13px;font-weight:700;color:var(--text3);cursor:pointer;border:none;background:none;font-family:var(--font);transition:all .15s}
|
||
.tab.on{background:var(--surface);color:var(--blue);box-shadow:var(--shadow)}
|
||
|
||
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);box-shadow:var(--shadow);margin-bottom:16px;overflow:visible}
|
||
.card-hd{padding:14px 18px;border-bottom:1px solid var(--border);font-size:14px;font-weight:700;background:var(--surface2);border-radius:10px 10px 0 0;display:flex;align-items:center;gap:8px}
|
||
.card-bd{padding:18px}
|
||
|
||
.ref-pill{font-family:var(--mono);font-size:14px;font-weight:700;color:var(--blue);background:var(--blue-light);border:1px solid #bfdbfe;padding:6px 14px;border-radius:8px;display:inline-block}
|
||
|
||
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-bottom:18px}
|
||
.fg{display:flex;flex-direction:column;gap:6px}
|
||
.fl{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:var(--text3)}
|
||
.fi{width:100%;padding:9px 12px;border:1.5px solid var(--border);border-radius:8px;font-size:13px;font-family:var(--font);background:var(--surface);color:var(--text);outline:none;height:38px}
|
||
.fi:focus{border-color:var(--blue);box-shadow:0 0 0 3px rgba(37,99,235,.12)}
|
||
.fi::placeholder{color:var(--text4)}
|
||
|
||
.tbl-wrap{overflow-x:auto;border:1px solid var(--border);border-radius:10px}
|
||
table.req-tbl{width:100%;border-collapse:collapse;font-size:13px;min-width:720px}
|
||
.req-tbl th{background:#eef0f3;color:var(--text3);font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.4px;text-align:left;padding:9px 10px;border-bottom:1px solid var(--border)}
|
||
.req-tbl td{padding:6px 8px;border-bottom:1px solid var(--border);vertical-align:middle}
|
||
.req-tbl tr:last-child td{border-bottom:none}
|
||
.td-sr{width:52px;text-align:center;font-weight:700;color:var(--text3)}
|
||
.td-qty{width:120px}
|
||
.td-act{width:44px;text-align:center}
|
||
.search-wrap{position:relative}
|
||
/* Floating item-search dropdown — attached to <body> so no container can clip it */
|
||
.search-dd{position:fixed;background:var(--surface);border:1px solid var(--blue);border-radius:8px;box-shadow:0 8px 28px rgba(0,0,0,.18);z-index:9000;max-height:260px;overflow-y:auto;display:none}
|
||
.sd-row{padding:8px 10px;cursor:pointer;border-bottom:1px solid #eef0f3;display:flex;flex-direction:column;gap:2px}
|
||
.sd-row:hover{background:var(--blue-light)}
|
||
.sd-code{font-family:var(--mono);font-size:11px;font-weight:700;color:var(--blue)}
|
||
.sd-name{font-size:12px;color:var(--text2)}
|
||
.sd-msg{padding:12px;text-align:center;color:var(--text3);font-size:12px}
|
||
.b-del{background:rgba(220,38,38,.06);color:var(--red);border:1px solid rgba(220,38,38,.2);border-radius:7px;width:30px;height:30px;cursor:pointer;display:inline-flex;align-items:center;justify-content:center}
|
||
.b-del:hover{background:rgba(220,38,38,.14)}
|
||
|
||
.btn{padding:10px 20px;border-radius:9px;border:none;font-family:var(--font);font-size:13px;font-weight:700;cursor:pointer;transition:all .15s;display:inline-flex;align-items:center;gap:7px;justify-content:center}
|
||
.btn:disabled{opacity:.5;cursor:not-allowed}
|
||
.b-add{background:var(--blue-light);color:var(--blue);border:1px solid #bfdbfe;margin-top:12px}
|
||
.b-add:hover{background:#dbeafe}
|
||
.b-primary{background:var(--blue);color:#fff;box-shadow:var(--shadow)}
|
||
.b-primary:hover:not(:disabled){background:var(--blue2)}
|
||
.b-cancel{background:var(--surface);color:var(--text2);border:1px solid var(--border)}
|
||
.b-cancel:hover{background:var(--surface2)}
|
||
.footer-row{display:flex;gap:10px;justify-content:flex-end;margin-top:20px;padding-top:16px;border-top:1px solid var(--border)}
|
||
|
||
.sp{width:14px;height:14px;border:2px solid rgba(255,255,255,.35);border-top-color:#fff;border-radius:50%;animation:spin .6s linear infinite;display:inline-block}
|
||
@keyframes spin{to{transform:rotate(360deg)}}
|
||
|
||
/* View list — search & filter bar */
|
||
.vf-bar{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin-bottom:16px}
|
||
.vf-search{position:relative;flex:1;min-width:220px}
|
||
.vf-search input{width:100%;padding:9px 12px 9px 34px;border:1.5px solid var(--border);border-radius:9px;font-size:13px;font-family:var(--font);background:var(--surface);color:var(--text);outline:none}
|
||
.vf-search input:focus{border-color:var(--blue);box-shadow:0 0 0 3px rgba(37,99,235,.12)}
|
||
.vf-search i{position:absolute;left:11px;top:50%;transform:translateY(-50%);color:var(--text4);pointer-events:none}
|
||
.vf-chips{display:flex;gap:6px;flex-wrap:wrap}
|
||
.vf-chip{padding:8px 14px;border-radius:20px;border:1.5px solid var(--border);background:var(--surface);color:var(--text3);font-size:12px;font-weight:700;font-family:var(--font);cursor:pointer;transition:all .12s;white-space:nowrap}
|
||
.vf-chip:hover{border-color:var(--blue);color:var(--blue2)}
|
||
.vf-chip.on{background:var(--blue);border-color:var(--blue);color:#fff}
|
||
.vf-count{font-size:12px;color:var(--text3);white-space:nowrap}
|
||
|
||
/* View list — square-tile grid, same pattern as work-order.html */
|
||
.req-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:14px}
|
||
.req-card{background:var(--surface);border:1px solid var(--border);border-radius:12px;box-shadow:var(--shadow);padding:14px 16px;cursor:pointer;transition:all .15s;aspect-ratio:1/1;display:flex;flex-direction:column;overflow:hidden}
|
||
.req-card:hover{border-color:var(--blue);box-shadow:0 6px 20px rgba(37,99,235,.14);transform:translateY(-2px)}
|
||
.rc-top{display:flex;align-items:flex-start;justify-content:space-between;gap:8px;flex-wrap:wrap}
|
||
.rc-ref{font-family:var(--mono);font-size:13.5px;font-weight:700;color:var(--blue)}
|
||
.status-badge{font-size:9.5px;font-weight:700;padding:3px 9px;border-radius:20px;text-transform:uppercase;letter-spacing:.3px;white-space:nowrap}
|
||
.sb-open{background:#eff6ff;color:#2563eb;border:1px solid #bfdbfe}
|
||
.sb-closed{background:#e6f5e6;color:#16a34a;border:1px solid #b3dab3}
|
||
.rc-icon{width:36px;height:36px;border-radius:9px;background:var(--blue-light);color:var(--blue2);display:flex;align-items:center;justify-content:center;margin:8px 0}
|
||
.rc-actions{display:inline-flex;gap:6px}
|
||
.rc-btn{display:inline-flex;align-items:center;gap:4px;font-family:var(--font);font-size:11px;font-weight:700;padding:6px 12px;border-radius:7px;cursor:pointer;background:var(--surface);border:1px solid var(--border);color:var(--text2)}
|
||
.rc-btn:hover{border-color:var(--blue);color:var(--blue);background:var(--blue-light)}
|
||
.rc-btn.del:hover{border-color:var(--red);color:var(--red);background:rgba(220,38,38,.06)}
|
||
.rc-meta{font-size:11px;color:var(--text3);margin-top:auto;padding-top:8px;border-top:1px solid var(--border);display:flex;flex-direction:column;gap:3px}
|
||
.rc-by{font-size:10.5px;color:var(--text4);margin-top:2px}
|
||
.empty-msg{text-align:center;padding:40px;color:var(--text3);font-size:13px}
|
||
|
||
/* Detail popup */
|
||
.req-modal-bg{position:fixed;inset:0;background:rgba(15,23,42,.55);z-index:500;display:none;align-items:flex-start;justify-content:center;padding:28px 20px;overflow-y:auto}
|
||
.req-modal-bg.show{display:flex}
|
||
.req-modal{background:var(--surface);border-radius:16px;width:100%;max-width:900px;margin:auto;box-shadow:0 30px 70px rgba(15,23,42,.3);max-height:92vh;display:flex;flex-direction:column;animation:mIn .18s ease}
|
||
@keyframes mIn{from{transform:translateY(16px);opacity:0}to{transform:translateY(0);opacity:1}}
|
||
.req-modal-hd{padding:16px 22px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px;flex-shrink:0;background:var(--surface2);border-radius:16px 16px 0 0}
|
||
.req-modal-close{margin-left:auto;width:32px;height:32px;border-radius:9px;border:1px solid var(--border);background:var(--surface);color:var(--text3);cursor:pointer;display:flex;align-items:center;justify-content:center}
|
||
.req-modal-close:hover{background:var(--red);color:#fff;border-color:var(--red)}
|
||
.req-modal-bd{padding:20px 22px;overflow-y:auto}
|
||
.req-modal-ft{padding:14px 22px;border-top:1px solid var(--border);display:flex;gap:8px;flex-wrap:wrap;flex-shrink:0;background:var(--surface2);border-radius:0 0 16px 16px}
|
||
@media(max-width:768px){.req-modal{max-height:100vh;border-radius:0}.req-modal-bg{padding:0}}
|
||
|
||
/* Success */
|
||
.suc-box{text-align:center;padding:36px 20px}
|
||
.suc-icon{width:56px;height:56px;border-radius:50%;background:#e6f5e6;color:#16a34a;display:inline-flex;align-items:center;justify-content:center;margin-bottom:14px}
|
||
.suc-title{font-size:19px;font-weight:800;color:#16a34a;margin-bottom:6px}
|
||
.suc-sub{font-size:13px;color:var(--text3);margin-bottom:16px}
|
||
|
||
/* Toast */
|
||
.toast-c{position:fixed;bottom:24px;right:24px;z-index:9999;display:flex;flex-direction:column;gap:8px}
|
||
.toast{padding:11px 16px;border-radius:9px;font-size:12px;font-weight:600;box-shadow:0 4px 16px rgba(0,0,0,.18);color:#fff;max-width:320px}
|
||
.t-ok{background:#16a34a}.t-err{background:#dc2626}
|
||
|
||
@media(max-width:768px){main{padding:14px 14px 80px}.grid2{grid-template-columns:1fr}}
|
||
|
||
/* Calculator tab */
|
||
.grid4{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:8px}
|
||
@media(max-width:860px){.grid4{grid-template-columns:1fr 1fr}}
|
||
.calc-note{font-size:11.5px;color:var(--text3);background:var(--surface2);border:1px solid var(--border);border-radius:8px;padding:8px 11px;margin-top:4px;margin-bottom:16px;line-height:1.5}
|
||
table.calc-tbl{width:100%;border-collapse:collapse;font-size:12px;min-width:1180px}
|
||
.calc-tbl th{background:#eef0f3;color:var(--text3);font-size:9.5px;font-weight:700;text-transform:uppercase;letter-spacing:.3px;text-align:right;padding:8px 8px;border-bottom:1px solid var(--border);white-space:nowrap}
|
||
.calc-tbl th.l{text-align:left}
|
||
.calc-tbl td{padding:5px 7px;border-bottom:1px solid var(--border);vertical-align:middle;text-align:right;font-family:var(--mono);font-size:11.5px}
|
||
.calc-tbl td.l{text-align:left;font-family:var(--font)}
|
||
.calc-tbl tr:last-child td{border-bottom:none}
|
||
.calc-tbl .req-pos{color:var(--blue);font-weight:700}
|
||
.calc-tbl .req-neg{color:var(--text4)}
|
||
.calc-chk{width:15px;height:15px;cursor:pointer}
|
||
.calc-inp-qty{width:90px;padding:4px 6px;border:1px solid var(--border);border-radius:5px;font-family:var(--mono);font-size:11.5px;text-align:right}
|
||
.btn-mini2{padding:4px 9px;border-radius:6px;border:1px solid var(--border);background:var(--surface);color:var(--blue);font-size:10.5px;font-weight:700;cursor:pointer}
|
||
.btn-mini2:hover{background:var(--blue-light)}
|
||
.btn-mini2:disabled{opacity:.5;cursor:not-allowed}
|
||
</style>
|
||
<link rel="stylesheet" href="/theme.css?v=3"/>
|
||
</head>
|
||
<body>
|
||
<nav></nav>
|
||
<main id="app"></main>
|
||
<div class="toast-c" id="toast-c"></div>
|
||
<div class="req-modal-bg" id="req-detail-modal"><div class="req-modal" id="req-modal-inner"></div></div>
|
||
<div class="req-modal-bg" id="grp-modal"><div class="req-modal" id="grp-modal-inner" style="max-width:640px"></div></div>
|
||
|
||
<script>
|
||
'use strict';
|
||
let _tok=null,_user=null;
|
||
const BACKEND='/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='/';
|
||
|
||
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;
|
||
}
|
||
function hasStepAssigned(fullKey){return ['view','add','edit','approve','delete'].some(p=>hasStepPerm(fullKey,p));}
|
||
function hasWorkflowPerm(workflow,perm){
|
||
if(_user&&_user.role==='admin')return true;
|
||
const steps=Array.isArray(_user&&_user.approvalSteps)?_user.approvalSteps:[];
|
||
return steps.some(s=>{
|
||
if(typeof s==='string')return s.startsWith(workflow+':');
|
||
return s&&s.step&&s.step.startsWith(workflow+':')&&Array.isArray(s.perms)&&s.perms.includes(perm);
|
||
});
|
||
}
|
||
// Any assignment at all on ANY "requirement:*" step (manage OR
|
||
// production_review OR store_review, with any perm) is enough to see the
|
||
// page — e.g. a Store user who only has 'add' on requirement:store_review
|
||
// (no 'view' checked) still needs to reach the list to do their review.
|
||
// Individual actions (New/Edit/Delete/Share/Review) stay gated on their own
|
||
// specific perm below.
|
||
function hasWorkflowAssigned(workflow){
|
||
if(_user&&_user.role==='admin')return true;
|
||
const steps=Array.isArray(_user&&_user.approvalSteps)?_user.approvalSteps:[];
|
||
return steps.some(s=>{
|
||
if(typeof s==='string')return s.startsWith(workflow+':');
|
||
return s&&s.step&&s.step.startsWith(workflow+':')&&Array.isArray(s.perms)&&s.perms.length>0;
|
||
});
|
||
}
|
||
const CAN_VIEW_REQ=hasWorkflowAssigned('requirement');
|
||
const CAN_ADD_REQ=hasStepPerm('requirement:manage','add');
|
||
const CAN_EDIT_REQ=hasStepPerm('requirement:manage','edit');
|
||
const CAN_DELETE_REQ=hasStepPerm('requirement:manage','delete');
|
||
const CAN_SHARE_REQ=hasStepPerm('requirement:production_review','add');
|
||
const CAN_STORE_REVIEW_REQ=hasStepPerm('requirement:store_review','add');
|
||
|
||
let _reqReviewEnabled=false, _reqReviewHardGate=false; // Admin → System Settings — "Requirement — Store Review Workflow"
|
||
let _storeReviewRows=null, _storeReviewFor=null, _storeReviewRemarks=''; // active Store-review edit session
|
||
|
||
let _co=window.__DEFAULT_COMPANY__;
|
||
let _tab=CAN_ADD_REQ?'create':'view';
|
||
let _periodFrom='', _periodTo='';
|
||
let _rows=[blankRow()];
|
||
let _remarks='';
|
||
let _nextRef='…';
|
||
let _editId=null; // null = create mode; otherwise the requirement id being edited
|
||
let _saving=false;
|
||
let _created=null;
|
||
let _list=[], _listLoading=false, _expanded=null;
|
||
let _viewSearch='', _viewStatus='';
|
||
let _searchTimer={};
|
||
|
||
// ── Requirement Calculator state ──
|
||
let _calcRows=[]; // {itemCode,itemName,itemDesc,saleQtyA,saleQtyB,avgSale,stock,quarantine,totalFG,stockInHandMonths,requirement,selected,loading,calculated}
|
||
let _calcPAFrom='',_calcPATo='',_calcPBFrom='',_calcPBTo='';
|
||
let _calcMonths=10;
|
||
let _calcStockWhs='',_calcQuarantineWhs='';
|
||
let _warehouses=[];
|
||
let _calcInited=false;
|
||
let _calcAllRunning=false;
|
||
let _calcPAEnabled=true,_calcPBEnabled=true; // Admin → System Settings — at least one always stays true
|
||
let _calcPALabel='Period A',_calcPBLabel='Period B'; // Admin → System Settings — display names for the two periods
|
||
|
||
// ── Item Groups (self-service presets, shared company-wide) ──
|
||
let _calcGroups=[]; // [{id,name,items:[{itemCode,itemName}],createdBy,createdByName}]
|
||
let _calcGroupPick=''; // currently selected group id in the picker
|
||
let _grpEditId=null; // null = creating a new group; otherwise editing this group's id
|
||
let _grpName='';
|
||
let _grpItems=[]; // [{itemCode,itemName}] being built in the modal
|
||
let _grpSearchTimer=null;
|
||
|
||
function blankRow(){return{itemCode:'',itemName:'',itemDesc:'',requiredQty:''};}
|
||
function esc(v){return(v==null?'':v).toString().replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');}
|
||
function q(s){return document.querySelector(s);}
|
||
function fmtDate(d){if(!d)return'—';try{return new Date(d).toLocaleDateString('en-IN',{day:'2-digit',month:'short',year:'numeric'});}catch{return d;}}
|
||
// YYYY-MM-DD one day after the given date — used as the "To" picker's min so it
|
||
// can only be later than "From".
|
||
function nextDay(ymd){const[y,m,d]=ymd.split('-').map(Number);return new Date(Date.UTC(y,m-1,d+1)).toISOString().slice(0,10);}
|
||
|
||
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(BACKEND+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=q('#toast-c');const el=document.createElement('div');el.className=`toast t-${type}`;el.textContent=msg;tc.appendChild(el);setTimeout(()=>el.remove(),4000);}
|
||
async function fetchItems(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 loadNextRef(){try{const r=await api('GET',`/requirements/next-ref?company=${encodeURIComponent(_co)}`);if(r.success)_nextRef=r.refNo;}catch{_nextRef='REQ-…';}render();}
|
||
async function loadReviewConfig(){
|
||
try{const cfg=await api('GET','/config');
|
||
if(cfg){
|
||
_reqReviewEnabled=!!cfg.requirementStoreReviewEnabled;
|
||
_reqReviewHardGate=!!cfg.requirementStoreReviewHardGate;
|
||
}
|
||
}catch{}
|
||
if(_tab==='view')render();
|
||
}
|
||
|
||
// ─────────── RENDER ───────────
|
||
function render(){
|
||
const app=q('#app');
|
||
if(_created){renderSuccess(app);lucide.createIcons();return;}
|
||
if(!CAN_VIEW_REQ){
|
||
app.innerHTML=`
|
||
<div class="page-head">
|
||
<div class="page-title"><i data-lucide="clipboard-list"></i> Requirements Management</div>
|
||
</div>
|
||
<div class="card"><div class="card-bd" style="text-align:center;padding:30px;color:var(--text3)">You are not assigned any "Requirements" permission. Ask an admin to grant it (User Management → Approval Steps → Requirements).</div></div>`;
|
||
lucide.createIcons();
|
||
return;
|
||
}
|
||
app.innerHTML=`
|
||
<div class="page-head">
|
||
<div class="page-title"><i data-lucide="clipboard-list"></i> Requirements Management</div>
|
||
<div class="page-sub">Logistics — raise material requirements against a period</div>
|
||
</div>
|
||
<div class="tabs">
|
||
${CAN_ADD_REQ?`<button class="tab ${_tab==='create'?'on':''}" data-t="create">+ New Requirement</button>`:''}
|
||
<button class="tab ${_tab==='view'?'on':''}" data-t="view">View Requirements</button>
|
||
${CAN_ADD_REQ?`<button class="tab ${_tab==='calc'?'on':''}" data-t="calc">📊 Calculator</button>`:''}
|
||
</div>
|
||
<div id="body"></div>`;
|
||
q('#body').innerHTML = _tab==='create' ? renderCreate() : _tab==='calc' ? renderCalc() : renderView();
|
||
document.querySelectorAll('.tab').forEach(b=>b.onclick=()=>{
|
||
const t=b.dataset.t;
|
||
if(t==='view'){_tab='view';loadList();}
|
||
else if(t==='calc'){_tab='calc';initCalc();}
|
||
else{newRequirement();}
|
||
});
|
||
if(_tab==='create')wireCreate(); else if(_tab==='calc')wireCalc(); else wireView();
|
||
lucide.createIcons();
|
||
}
|
||
|
||
function renderCreate(){
|
||
const rowsHtml=_rows.map((r,i)=>`
|
||
<tr>
|
||
<td class="td-sr">${i+1}</td>
|
||
<td>
|
||
<div class="search-wrap">
|
||
<input class="fi" data-ri="${i}" placeholder="Search item code / name…" value="${esc(r.itemCode||'')}" autocomplete="off"/>
|
||
</div>
|
||
</td>
|
||
<td><input class="fi" data-desc="${i}" placeholder="Description" value="${esc(r.itemDesc)}"/></td>
|
||
<td class="td-qty"><input class="fi" data-qty="${i}" type="number" min="0" step="any" placeholder="0" value="${esc(r.requiredQty)}"/></td>
|
||
<td class="td-act"><button class="b-del" data-rm="${i}" title="Remove"><i data-lucide="trash-2" style="width:14px;height:14px"></i></button></td>
|
||
</tr>`).join('');
|
||
|
||
return `
|
||
<div class="card">
|
||
<div class="card-hd"><i data-lucide="${_editId?'pencil':'file-plus'}" style="width:16px;height:16px"></i> ${_editId?'Edit Requirement':'New Requirement'} <span style="margin-left:auto" class="ref-pill">${esc(_nextRef)}</span></div>
|
||
<div class="card-bd">
|
||
<div class="grid2">
|
||
<div class="fg"><label class="fl">Requirement Period — From</label><input class="fi" type="date" id="inp-from" value="${esc(_periodFrom)}"/></div>
|
||
<div class="fg"><label class="fl">Requirement Period — To</label><input class="fi" type="date" id="inp-to" value="${esc(_periodTo)}" min="${_periodFrom?nextDay(_periodFrom):''}"/></div>
|
||
</div>
|
||
|
||
<div class="tbl-wrap">
|
||
<table class="req-tbl">
|
||
<thead><tr>
|
||
<th class="td-sr">Sr.</th>
|
||
<th>Item Name</th>
|
||
<th>Item Description</th>
|
||
<th class="td-qty">Required Qty</th>
|
||
<th class="td-act"></th>
|
||
</tr></thead>
|
||
<tbody>${rowsHtml}</tbody>
|
||
</table>
|
||
</div>
|
||
<button class="btn b-add" id="btn-add"><i data-lucide="plus" style="width:15px;height:15px"></i> Add Row</button>
|
||
|
||
<div class="fg" style="margin-top:18px"><label class="fl">Remarks (optional)</label><input class="fi" id="inp-remarks" placeholder="Notes…" value="${esc(_remarks)}"/></div>
|
||
|
||
<div class="footer-row">
|
||
<button class="btn b-cancel" id="btn-reset">${_editId?'Cancel':'Reset'}</button>
|
||
<button class="btn b-primary" id="btn-submit" ${_saving?'disabled':''}>${_saving?'<span class="sp"></span> Saving…':(_editId?'Update Requirement':'Generate Requirement')}</button>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
function wireCreate(){
|
||
q('#inp-from').onchange=function(){
|
||
_periodFrom=this.value;
|
||
const toEl=q('#inp-to');
|
||
if(toEl){toEl.min=_periodFrom?nextDay(_periodFrom):'';
|
||
// alert + clear a now-invalid To right away (not only on save)
|
||
if(_periodTo&&new Date(_periodTo)<=new Date(_periodFrom)){
|
||
toast('"To" date must be later than "From" date','err');
|
||
_periodTo='';toEl.value='';
|
||
}}
|
||
};
|
||
q('#inp-to').onchange=function(){
|
||
if(_periodFrom&&this.value&&new Date(this.value)<=new Date(_periodFrom)){
|
||
toast('"To" date must be later than "From" date','err');
|
||
_periodTo='';this.value='';return;
|
||
}
|
||
_periodTo=this.value;
|
||
};
|
||
q('#inp-remarks').oninput=function(){_remarks=this.value;};
|
||
q('#btn-add').onclick=()=>{_rows.push(blankRow());render();};
|
||
q('#btn-reset').onclick=()=>{if(_editId){_editId=null;_tab='view';loadList();}else{_periodFrom='';_periodTo='';_remarks='';_rows=[blankRow()];render();}};
|
||
q('#btn-submit').onclick=submit;
|
||
|
||
document.querySelectorAll('[data-desc]').forEach(el=>el.oninput=function(){_rows[+this.dataset.desc].itemDesc=this.value;});
|
||
document.querySelectorAll('[data-qty]').forEach(el=>el.oninput=function(){_rows[+this.dataset.qty].requiredQty=this.value;});
|
||
document.querySelectorAll('[data-rm]').forEach(b=>b.onclick=function(){const i=+this.dataset.rm;_rows.splice(i,1);if(!_rows.length)_rows=[blankRow()];render();});
|
||
|
||
// Per-row item search — uses a single floating dropdown attached to <body>
|
||
document.querySelectorAll('[data-ri]').forEach(inp=>{
|
||
const i=+inp.dataset.ri;
|
||
inp.oninput=function(){
|
||
const v=this.value.trim();_rows[i].itemName='';_rows[i].itemCode='';
|
||
if(v.length<2){hideDD();return;}
|
||
clearTimeout(_searchTimer[i]);
|
||
_searchTimer[i]=setTimeout(async()=>{
|
||
showDD(inp,'<div class="sd-msg"><span class="sp" style="border-color:rgba(37,99,235,.25);border-top-color:#2563eb"></span></div>');
|
||
const items=await fetchItems(v);
|
||
if(!items.length){showDD(inp,'<div class="sd-msg">No items found</div>');return;}
|
||
const dd=getDD();dd.innerHTML='';
|
||
items.forEach(it=>{
|
||
const row=document.createElement('div');row.className='sd-row';
|
||
row.innerHTML=`<span class="sd-code">${esc(it.ItemCode)}</span><span class="sd-name">${esc(it.ItemName)}</span>`;
|
||
row.onmousedown=e=>{e.preventDefault();selectItem(i,it);hideDD();};
|
||
dd.appendChild(row);
|
||
});
|
||
positionDD(inp);
|
||
},250);
|
||
};
|
||
inp.onblur=()=>setTimeout(hideDD,150);
|
||
});
|
||
}
|
||
|
||
// ── Floating dropdown helpers ──
|
||
function getDD(){
|
||
let dd=document.getElementById('float-dd');
|
||
if(!dd){dd=document.createElement('div');dd.id='float-dd';dd.className='search-dd';document.body.appendChild(dd);}
|
||
return dd;
|
||
}
|
||
function positionDD(inp){
|
||
const dd=getDD(),r=inp.getBoundingClientRect();
|
||
// <body> may have a zoom (theme.css sets zoom:1.10); fixed coords inside a
|
||
// zoomed element are scaled, so divide the viewport rect back by the zoom.
|
||
const z=parseFloat(getComputedStyle(document.body).zoom)||1;
|
||
dd.style.left=(r.left/z)+'px';dd.style.top=((r.bottom+3)/z)+'px';dd.style.width=(r.width/z)+'px';
|
||
}
|
||
function showDD(inp,html){const dd=getDD();dd.innerHTML=html;dd.style.display='block';positionDD(inp);_ddInput=inp;}
|
||
function hideDD(){const dd=document.getElementById('float-dd');if(dd)dd.style.display='none';_ddInput=null;}
|
||
let _ddInput=null;
|
||
window.addEventListener('scroll',()=>{if(_ddInput)positionDD(_ddInput);},true);
|
||
window.addEventListener('resize',hideDD);
|
||
|
||
function selectItem(i,it){
|
||
_rows[i].itemCode=it.ItemCode;
|
||
_rows[i].itemName=it.ItemName;
|
||
// Always overwrite — picking a DIFFERENT item must replace whatever
|
||
// description was there before (a leftover/stale description from a prior
|
||
// selection previously blocked this, since it only filled when empty).
|
||
_rows[i].itemDesc=it.ItemName;
|
||
// Update inputs in place (avoid full re-render / focus loss)
|
||
const nameInp=document.querySelector(`[data-ri="${i}"]`);if(nameInp)nameInp.value=it.ItemCode;
|
||
const descInp=document.querySelector(`[data-desc="${i}"]`);if(descInp)descInp.value=it.ItemName;
|
||
}
|
||
|
||
async function submit(){
|
||
if(_editId&&!CAN_EDIT_REQ){toast('You are not permitted to edit requirements','err');return;}
|
||
if(!_editId&&!CAN_ADD_REQ){toast('You are not permitted to create requirements','err');return;}
|
||
if(!_periodFrom||!_periodTo){toast('Select requirement period (from & to)','err');return;}
|
||
if(new Date(_periodTo)<=new Date(_periodFrom)){toast('"To" date must be later than "From" date','err');return;}
|
||
const lines=_rows.filter(r=>r.itemCode).map(r=>({itemCode:r.itemCode,itemName:r.itemName,itemDesc:r.itemDesc,requiredQty:Number(r.requiredQty)||0}));
|
||
if(!lines.length){toast('Add at least one item (search & select)','err');return;}
|
||
if(lines.some(l=>!(l.requiredQty>0))){toast('Enter a required quantity for every item','err');return;}
|
||
_saving=true;render();
|
||
try{
|
||
if(_editId){
|
||
const r=await api('PUT','/requirements/'+_editId,{periodFrom:_periodFrom,periodTo:_periodTo,lines,remarks:_remarks});
|
||
_saving=false;_editId=null;toast('Requirement '+(r.data?.refNo||'')+' updated');_tab='view';loadList();
|
||
}else{
|
||
const r=await api('POST','/requirements',{periodFrom:_periodFrom,periodTo:_periodTo,lines,remarks:_remarks,company:_co});
|
||
_created=r.data;_saving=false;render();
|
||
}
|
||
}catch(e){_saving=false;render();toast(e.message,'err');}
|
||
}
|
||
|
||
function newRequirement(){_editId=null;_created=null;_periodFrom='';_periodTo='';_remarks='';_rows=[blankRow()];_tab='create';loadNextRef();}
|
||
|
||
function startEdit(r){
|
||
_editId=r.id;_created=null;_tab='create';
|
||
_nextRef=r.refNo;
|
||
_periodFrom=r.periodFrom||'';_periodTo=r.periodTo||'';_remarks=r.remarks||'';
|
||
_rows=(r.lines&&r.lines.length)?r.lines.map(l=>({itemCode:l.itemCode||'',itemName:l.itemName||'',itemDesc:l.itemDesc||'',requiredQty:l.requiredQty!=null?l.requiredQty:''})):[blankRow()];
|
||
render();window.scrollTo(0,0);
|
||
}
|
||
|
||
async function deleteReq(id,ref){
|
||
if(!CAN_DELETE_REQ){toast('You are not permitted to delete requirements','err');return;}
|
||
if(!confirm('Delete requirement '+ref+'?\n\nIt will be removed from the list (soft delete).'))return;
|
||
try{await api('DELETE','/requirements/'+id);toast('Requirement '+ref+' deleted');loadList();}
|
||
catch(e){toast(e.message,'err');}
|
||
}
|
||
|
||
function renderSuccess(app){
|
||
app.innerHTML=`
|
||
<div class="card" style="max-width:520px;margin:40px auto">
|
||
<div class="suc-box">
|
||
<div class="suc-icon"><i data-lucide="check" style="width:30px;height:30px"></i></div>
|
||
<div class="suc-title">Requirement Generated</div>
|
||
<div class="suc-sub">Your requirement has been created successfully.</div>
|
||
<div class="ref-pill" style="font-size:16px;padding:8px 18px">${esc(_created.refNo)}</div>
|
||
<div class="footer-row" style="justify-content:center;border:none;margin-top:24px">
|
||
<button class="btn b-cancel" id="btn-view">View All</button>
|
||
<button class="btn b-primary" id="btn-another">+ New Requirement</button>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
q('#btn-another').onclick=()=>{_created=null;_periodFrom='';_periodTo='';_remarks='';_rows=[blankRow()];loadNextRef();};
|
||
q('#btn-view').onclick=()=>{_created=null;_tab='view';loadList();};
|
||
}
|
||
|
||
// ─────────── CALCULATOR ───────────
|
||
// A worksheet (per the reference sheet): historical Sale Qty over two
|
||
// periods, live SAP Stock + Quarantine Stock, and a suggested production
|
||
// Requirement per item — which the user can then send straight into the
|
||
// "New Requirement" tab to actually save it.
|
||
function blankCalcRow(){return{itemCode:'',itemName:'',itemDesc:'',saleQtyA:null,saleQtyB:null,avgSale:null,stock:null,quarantine:null,totalFG:null,stockInHandMonths:null,requirement:null,selected:true,loading:false,calculated:false};}
|
||
function n3(v){if(v==null||isNaN(v))return '—';return(Math.round(v*1000)/1000).toLocaleString('en-IN');}
|
||
|
||
async function loadWarehouses(){
|
||
if(_warehouses.length)return;
|
||
try{const r=await api('GET',`/sap/lookup/warehouses?company=${encodeURIComponent(_co)}`);_warehouses=r.data||[];}catch(_e){_warehouses=[];}
|
||
}
|
||
async function loadCalcConfig(){
|
||
try{const cfg=await api('GET','/config');
|
||
if(cfg){
|
||
if(typeof cfg.reqCalcPeriodAEnabled==='boolean')_calcPAEnabled=cfg.reqCalcPeriodAEnabled;
|
||
if(typeof cfg.reqCalcPeriodBEnabled==='boolean')_calcPBEnabled=cfg.reqCalcPeriodBEnabled;
|
||
if(cfg.reqCalcPeriodALabel)_calcPALabel=cfg.reqCalcPeriodALabel;
|
||
if(cfg.reqCalcPeriodBLabel)_calcPBLabel=cfg.reqCalcPeriodBLabel;
|
||
}
|
||
}catch(_e){}
|
||
}
|
||
async function loadCalcGroups(){
|
||
try{const r=await api('GET',`/requirement-calc/groups?company=${encodeURIComponent(_co)}`);_calcGroups=r.data||[];}catch(_e){_calcGroups=[];}
|
||
}
|
||
async function initCalc(){
|
||
await Promise.all([loadWarehouses(),loadCalcConfig(),loadCalcGroups()]);
|
||
if(!_calcInited){_calcRows=[blankCalcRow()];_calcInited=true;}
|
||
render();
|
||
}
|
||
|
||
const CALC_STOCK_WHS=[{code:'1A',name:'FG DOMESTIC'},{code:'1B',name:'FG EXPORT'}];
|
||
const CALC_QTN_WHS=[{code:'16',name:'QUARANTINE'}];
|
||
function renderCalc(){
|
||
const whOptsFrom=(list,sel)=>'<option value="">— select —</option>'+list.map(w=>`<option value="${esc(w.code)}" ${sel===w.code?'selected':''}>${esc(w.code)} — ${esc(w.name||'')}</option>`).join('');
|
||
const rowsHtml=_calcRows.map((r,i)=>{
|
||
const reqCls=r.requirement>0?'req-pos':'req-neg';
|
||
return `<tr>
|
||
<td class="l" style="width:26px;text-align:center"><input type="checkbox" class="calc-chk" data-csel="${i}" ${r.selected?'checked':''}/></td>
|
||
<td class="l" style="width:40px;text-align:center">${i+1}</td>
|
||
<td class="l" style="min-width:160px"><div class="search-wrap"><input class="fi" style="height:32px" data-cri="${i}" placeholder="Search item code / name…" value="${esc(r.itemCode||'')}" autocomplete="off"/></div></td>
|
||
<td class="l" style="min-width:200px">${esc(r.itemDesc||r.itemName||'')}</td>
|
||
<td>${r.loading?'…':n3(r.saleQtyA)}</td>
|
||
<td>${r.loading?'…':n3(r.saleQtyB)}</td>
|
||
<td>${r.loading?'…':n3(r.avgSale)}</td>
|
||
<td>${r.loading?'…':n3(r.stock)}</td>
|
||
<td>${r.loading?'…':n3(r.quarantine)}</td>
|
||
<td>${r.loading?'…':n3(r.totalFG)}</td>
|
||
<td>${r.loading?'…':n3(r.stockInHandMonths)}</td>
|
||
<td class="${r.calculated?reqCls:''}">${r.loading?'<span class="sp" style="border-color:rgba(37,99,235,.25);border-top-color:#2563eb;width:11px;height:11px"></span>':n3(r.requirement)}</td>
|
||
<td style="text-align:center"><button class="btn-mini2" data-ccalc="${i}" ${!r.itemCode||r.loading?'disabled':''} title="Calculate this row">↻</button></td>
|
||
<td class="td-act"><button class="b-del" data-crm="${i}" title="Remove"><i data-lucide="trash-2" style="width:13px;height:13px"></i></button></td>
|
||
</tr>`;
|
||
}).join('');
|
||
const anyCalculated=_calcRows.some(r=>r.calculated);
|
||
const selCount=_calcRows.filter(r=>r.selected&&r.calculated).length;
|
||
return `
|
||
<div class="card">
|
||
<div class="card-hd"><i data-lucide="calculator" style="width:16px;height:16px"></i> Requirement Calculator</div>
|
||
<div class="card-bd">
|
||
<div class="calc-note">${_calcPAEnabled&&_calcPBEnabled
|
||
?`Enter the two comparison periods and the warehouses that represent your normal Stock and Quarantine Stock, add items, then click <b>Calculate All</b>. Average Sale/Month = (${esc(_calcPALabel)} qty + ${esc(_calcPBLabel)} qty) ÷ Months; Requirement = Average Sale/Month × 2 − Total FG.`
|
||
:`Only ${esc(_calcPAEnabled?_calcPALabel:_calcPBLabel)} is enabled (Admin → System Settings). Enter that period and the warehouses that represent your normal Stock and Quarantine Stock, add items, then click <b>Calculate All</b>. Average Sale/Month = ${esc(_calcPAEnabled?_calcPALabel:_calcPBLabel)} qty ÷ the number of months that period spans; Requirement = Average Sale/Month × 2 − Total FG.`}</div>
|
||
<div class="grid4">
|
||
${_calcPAEnabled?`
|
||
<div class="fg"><label class="fl">${esc(_calcPALabel)} — From</label><input class="fi" type="date" id="calc-pa-from" value="${esc(_calcPAFrom)}"/></div>
|
||
<div class="fg"><label class="fl">${esc(_calcPALabel)} — To</label><input class="fi" type="date" id="calc-pa-to" value="${esc(_calcPATo)}"/></div>`:''}
|
||
${_calcPBEnabled?`
|
||
<div class="fg"><label class="fl">${esc(_calcPBLabel)} — From</label><input class="fi" type="date" id="calc-pb-from" value="${esc(_calcPBFrom)}"/></div>
|
||
<div class="fg"><label class="fl">${esc(_calcPBLabel)} — To</label><input class="fi" type="date" id="calc-pb-to" value="${esc(_calcPBTo)}"/></div>`:''}
|
||
</div>
|
||
<div class="grid4">
|
||
<div class="fg"><label class="fl">Stock Warehouse</label><select class="fi" id="calc-wh-stock">${whOptsFrom(CALC_STOCK_WHS,_calcStockWhs)}</select></div>
|
||
<div class="fg"><label class="fl">Quarantine Warehouse</label><select class="fi" id="calc-wh-qtn">${whOptsFrom(CALC_QTN_WHS,_calcQuarantineWhs)}</select></div>
|
||
${(_calcPAEnabled&&_calcPBEnabled)?`<div class="fg"><label class="fl">Months (divisor)</label><input class="fi" type="number" min="1" step="1" id="calc-months" value="${esc(_calcMonths)}"/></div>`:''}
|
||
<div class="fg" style="justify-content:flex-end;display:flex;align-items:flex-end"><button class="btn b-primary" id="btn-calc-all" style="width:100%;height:38px" ${_calcAllRunning?'disabled':''}>${_calcAllRunning?'<span class="sp"></span> Calculating…':'⚡ Calculate All'}</button></div>
|
||
</div>
|
||
|
||
<div class="grid4" style="align-items:flex-end">
|
||
<div class="fg" style="grid-column:span 2">
|
||
<label class="fl">Item Group — fetch all items from a saved group</label>
|
||
<select class="fi" id="calc-grp-pick">
|
||
<option value="">— select a group —</option>
|
||
${_calcGroups.map(g=>`<option value="${g.id}" ${_calcGroupPick===String(g.id)?'selected':''}>${esc(g.name)} (${g.items.length} item${g.items.length===1?'':'s'})</option>`).join('')}
|
||
</select>
|
||
</div>
|
||
<div class="fg"><button class="btn b-cancel" id="btn-grp-fetch" style="width:100%;height:38px" ${_calcGroupPick?'':'disabled'}>Fetch Group Items</button></div>
|
||
<div class="fg"><button class="btn b-cancel" id="btn-grp-manage" style="width:100%;height:38px"><i data-lucide="settings" style="width:14px;height:14px"></i> Manage Groups</button></div>
|
||
</div>
|
||
|
||
<div class="tbl-wrap">
|
||
<table class="calc-tbl">
|
||
<thead><tr>
|
||
<th></th><th>Sr.</th><th class="l">Item No.</th><th class="l">Item Description</th>
|
||
<th>Sale Qty<br/>(${esc(_calcPALabel)})</th><th>Sale Qty<br/>(${esc(_calcPBLabel)})</th><th>Avg Sale<br/>/Month</th>
|
||
<th>Stock</th><th>Quarantine<br/>Stock</th><th>Total FG</th><th>Stock in Hand<br/>(Months)</th>
|
||
<th>Requirement</th><th></th><th></th>
|
||
</tr></thead>
|
||
<tbody>${rowsHtml}</tbody>
|
||
</table>
|
||
</div>
|
||
<button class="btn b-add" id="btn-calc-add"><i data-lucide="plus" style="width:15px;height:15px"></i> Add Row</button>
|
||
|
||
<div class="footer-row">
|
||
<span style="margin-right:auto;align-self:center;font-size:12px;color:var(--text3)">${anyCalculated?`${selCount} of ${_calcRows.filter(r=>r.calculated).length} calculated row(s) selected`:'Calculate at least one row to continue'}</span>
|
||
${CAN_ADD_REQ?`<button class="btn b-primary" id="btn-calc-send" ${selCount?'':'disabled'}>Send Selected to New Requirement →</button>`:''}
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
async function calcRow(i){
|
||
const r=_calcRows[i];
|
||
if(!r||!r.itemCode)return;
|
||
if(_calcPAEnabled&&(!_calcPAFrom||!_calcPATo)){toast(`Set the ${_calcPALabel} date range first`,'err');return;}
|
||
if(_calcPBEnabled&&(!_calcPBFrom||!_calcPBTo)){toast(`Set the ${_calcPBLabel} date range first`,'err');return;}
|
||
r.loading=true;render();
|
||
try{
|
||
const qs=new URLSearchParams({
|
||
itemCode:r.itemCode,periodAFrom:_calcPAFrom,periodATo:_calcPATo,periodBFrom:_calcPBFrom,periodBTo:_calcPBTo,
|
||
months:String(_calcMonths||10),
|
||
});
|
||
if(_calcStockWhs)qs.set('stockWhs',_calcStockWhs);
|
||
if(_calcQuarantineWhs)qs.set('quarantineWhs',_calcQuarantineWhs);
|
||
const d=await api('GET',`/requirement-calc/line?${qs.toString()}`);
|
||
const v=d.data||{};
|
||
Object.assign(r,{saleQtyA:v.saleQtyPeriodA,saleQtyB:v.saleQtyPeriodB,avgSale:v.avgSalePerMonth,stock:v.stock,quarantine:v.quarantineStock,totalFG:v.totalFG,stockInHandMonths:v.stockInHandMonths,requirement:v.requirement,calculated:true});
|
||
}catch(e){toast(`${r.itemCode}: ${e.message}`,'err');}
|
||
r.loading=false;render();
|
||
}
|
||
async function calcAllRows(){
|
||
if(_calcPAEnabled&&(!_calcPAFrom||!_calcPATo)){toast(`Set the ${_calcPALabel} date range first`,'err');return;}
|
||
if(_calcPBEnabled&&(!_calcPBFrom||!_calcPBTo)){toast(`Set the ${_calcPBLabel} date range first`,'err');return;}
|
||
const idxs=_calcRows.map((r,i)=>r.itemCode?i:-1).filter(i=>i>=0);
|
||
if(!idxs.length){toast('Add at least one item first','err');return;}
|
||
_calcAllRunning=true;render();
|
||
await Promise.all(idxs.map(i=>calcRow(i)));
|
||
_calcAllRunning=false;render();
|
||
}
|
||
|
||
function sendCalcToCreate(){
|
||
if(!CAN_ADD_REQ){toast('You are not permitted to create requirements','err');return;}
|
||
const chosen=_calcRows.filter(r=>r.selected&&r.calculated&&r.itemCode);
|
||
if(!chosen.length){toast('Select at least one calculated row first','err');return;}
|
||
_rows=chosen.map(r=>({itemCode:r.itemCode,itemName:r.itemName,itemDesc:r.itemDesc||r.itemName,requiredQty:Math.max(0,Math.round((r.requirement||0)*1000)/1000)}));
|
||
_periodFrom=_calcPBFrom||'';_periodTo=_calcPBTo||'';
|
||
_editId=null;_created=null;_tab='create';
|
||
loadNextRef();
|
||
toast(`${chosen.length} item(s) sent to New Requirement — review quantities and period, then Generate.`);
|
||
}
|
||
|
||
function wireCalc(){
|
||
if(q('#calc-pa-from'))q('#calc-pa-from').onchange=function(){_calcPAFrom=this.value;};
|
||
if(q('#calc-pa-to'))q('#calc-pa-to').onchange=function(){_calcPATo=this.value;};
|
||
if(q('#calc-pb-from'))q('#calc-pb-from').onchange=function(){_calcPBFrom=this.value;};
|
||
if(q('#calc-pb-to'))q('#calc-pb-to').onchange=function(){_calcPBTo=this.value;};
|
||
q('#calc-wh-stock').onchange=function(){_calcStockWhs=this.value;};
|
||
q('#calc-wh-qtn').onchange=function(){_calcQuarantineWhs=this.value;};
|
||
if(q('#calc-months'))q('#calc-months').onchange=function(){_calcMonths=parseFloat(this.value)||10;};
|
||
q('#btn-calc-all').onclick=calcAllRows;
|
||
q('#btn-calc-add').onclick=()=>{_calcRows.push(blankCalcRow());render();};
|
||
if(q('#btn-calc-send'))q('#btn-calc-send').onclick=sendCalcToCreate;
|
||
|
||
q('#calc-grp-pick').onchange=function(){_calcGroupPick=this.value;render();};
|
||
q('#btn-grp-fetch').onclick=fetchGroupIntoRows;
|
||
q('#btn-grp-manage').onclick=()=>openGroupModal();
|
||
|
||
document.querySelectorAll('[data-csel]').forEach(el=>el.onchange=function(){_calcRows[+this.dataset.csel].selected=this.checked;render();});
|
||
document.querySelectorAll('[data-ccalc]').forEach(el=>el.onclick=function(){calcRow(+this.dataset.ccalc);});
|
||
document.querySelectorAll('[data-crm]').forEach(el=>el.onclick=function(){const i=+this.dataset.crm;_calcRows.splice(i,1);if(!_calcRows.length)_calcRows=[blankCalcRow()];render();});
|
||
|
||
// Per-row item search — reuses the same floating dropdown as the Create tab.
|
||
document.querySelectorAll('[data-cri]').forEach(inp=>{
|
||
const i=+inp.dataset.cri;
|
||
inp.oninput=function(){
|
||
const v=this.value.trim();_calcRows[i].itemName='';_calcRows[i].itemCode='';_calcRows[i].calculated=false;
|
||
if(v.length<2){hideDD();return;}
|
||
clearTimeout(_searchTimer['c'+i]);
|
||
_searchTimer['c'+i]=setTimeout(async()=>{
|
||
showDD(inp,'<div class="sd-msg"><span class="sp" style="border-color:rgba(37,99,235,.25);border-top-color:#2563eb"></span></div>');
|
||
const items=await fetchItems(v);
|
||
if(!items.length){showDD(inp,'<div class="sd-msg">No items found</div>');return;}
|
||
const dd=getDD();dd.innerHTML='';
|
||
items.forEach(it=>{
|
||
const row=document.createElement('div');row.className='sd-row';
|
||
row.innerHTML=`<span class="sd-code">${esc(it.ItemCode)}</span><span class="sd-name">${esc(it.ItemName)}</span>`;
|
||
row.onmousedown=e=>{e.preventDefault();_calcRows[i].itemCode=it.ItemCode;_calcRows[i].itemName=it.ItemName;_calcRows[i].itemDesc=it.ItemName;hideDD();render();};
|
||
dd.appendChild(row);
|
||
});
|
||
positionDD(inp);
|
||
},250);
|
||
};
|
||
inp.onblur=()=>setTimeout(hideDD,150);
|
||
});
|
||
}
|
||
|
||
// ── Item Group management modal ──
|
||
function openGroupModal(editId){
|
||
_grpEditId=editId||null;
|
||
if(editId){const g=_calcGroups.find(x=>x.id===editId);_grpName=g?g.name:'';_grpItems=g?g.items.map(it=>({...it})):[];}
|
||
else{_grpName='';_grpItems=[];}
|
||
renderGroupModal();
|
||
document.getElementById('grp-modal').classList.add('show');
|
||
}
|
||
function closeGroupModal(){document.getElementById('grp-modal').classList.remove('show');}
|
||
|
||
function renderGroupModal(){
|
||
const inner=document.getElementById('grp-modal-inner');
|
||
const canEditThis=g=>_user&&(_user.role==='admin'||g.createdBy===_user.username);
|
||
const listHtml=_calcGroups.length?_calcGroups.map(g=>`
|
||
<div style="display:flex;align-items:center;gap:8px;padding:8px 0;border-bottom:1px solid var(--border)">
|
||
<div style="flex:1"><b>${esc(g.name)}</b> <span style="color:var(--text3);font-size:11px">— ${g.items.length} item(s), by ${esc(g.createdByName||g.createdBy)}</span></div>
|
||
${canEditThis(g)?`<button class="btn-mini2" data-grp-edit="${g.id}">Edit</button><button class="btn-mini2" data-grp-del="${g.id}" style="color:var(--red)">Delete</button>`:''}
|
||
</div>`).join(''):`<div style="color:var(--text3);font-size:12px;padding:8px 0">No groups saved yet.</div>`;
|
||
const itemsHtml=_grpItems.map((it,i)=>`
|
||
<span style="display:inline-flex;align-items:center;gap:6px;background:var(--surface2);border:1px solid var(--border);border-radius:14px;padding:4px 10px;margin:3px;font-size:11.5px">
|
||
${esc(it.itemCode)} — ${esc(it.itemName||'')}
|
||
<button data-grp-item-rm="${i}" style="border:none;background:none;color:var(--red);cursor:pointer;font-weight:700">×</button>
|
||
</span>`).join('');
|
||
inner.innerHTML=`
|
||
<div class="req-modal-hd"><i data-lucide="layers" style="width:16px;height:16px"></i> Item Groups
|
||
<button class="req-modal-close" id="grp-modal-x"><i data-lucide="x" style="width:15px;height:15px"></i></button>
|
||
</div>
|
||
<div class="req-modal-bd">
|
||
<div style="margin-bottom:14px">${listHtml}</div>
|
||
<div class="fg"><label class="fl">${_grpEditId?'Editing Group':'New Group'} — Name</label><input class="fi" id="grp-name-inp" value="${esc(_grpName)}" placeholder="e.g. All Blood Bag Items"/></div>
|
||
<div class="fg" style="margin-top:10px"><label class="fl">Add Items</label><div class="search-wrap"><input class="fi" id="grp-item-search" placeholder="Search item code / name…" autocomplete="off"/></div></div>
|
||
<div style="margin-top:8px">${itemsHtml||'<span style="color:var(--text3);font-size:12px">No items added yet.</span>'}</div>
|
||
</div>
|
||
<div class="req-modal-ft">
|
||
<button class="btn b-cancel" id="grp-cancel-btn">${_grpEditId?'Cancel Edit':'Close'}</button>
|
||
<button class="btn b-primary" id="grp-save-btn">${_grpEditId?'Save Changes':'Create Group'}</button>
|
||
</div>`;
|
||
if(window.lucide)lucide.createIcons();
|
||
wireGroupModal();
|
||
}
|
||
|
||
function wireGroupModal(){
|
||
document.getElementById('grp-modal-x').onclick=closeGroupModal;
|
||
document.getElementById('grp-cancel-btn').onclick=()=>{if(_grpEditId){_grpEditId=null;_grpName='';_grpItems=[];renderGroupModal();}else closeGroupModal();};
|
||
document.getElementById('grp-save-btn').onclick=saveGroup;
|
||
document.getElementById('grp-name-inp').oninput=function(){_grpName=this.value;};
|
||
|
||
document.querySelectorAll('[data-grp-edit]').forEach(el=>el.onclick=function(){openGroupModal(+this.dataset.grpEdit);});
|
||
document.querySelectorAll('[data-grp-del]').forEach(el=>el.onclick=function(){deleteGroupConfirm(+this.dataset.grpDel);});
|
||
document.querySelectorAll('[data-grp-item-rm]').forEach(el=>el.onclick=function(){_grpItems.splice(+this.dataset.grpItemRm,1);renderGroupModal();});
|
||
|
||
const inp=document.getElementById('grp-item-search');
|
||
inp.oninput=function(){
|
||
const v=this.value.trim();
|
||
if(v.length<2){hideDD();return;}
|
||
clearTimeout(_grpSearchTimer);
|
||
_grpSearchTimer=setTimeout(async()=>{
|
||
showDD(inp,'<div class="sd-msg"><span class="sp" style="border-color:rgba(37,99,235,.25);border-top-color:#2563eb"></span></div>');
|
||
const items=await fetchItems(v);
|
||
if(!items.length){showDD(inp,'<div class="sd-msg">No items found</div>');return;}
|
||
const dd=getDD();dd.innerHTML='';
|
||
items.forEach(it=>{
|
||
const row=document.createElement('div');row.className='sd-row';
|
||
row.innerHTML=`<span class="sd-code">${esc(it.ItemCode)}</span><span class="sd-name">${esc(it.ItemName)}</span>`;
|
||
row.onmousedown=e=>{
|
||
e.preventDefault();
|
||
if(!_grpItems.some(x=>x.itemCode===it.ItemCode))_grpItems.push({itemCode:it.ItemCode,itemName:it.ItemName});
|
||
inp.value='';hideDD();renderGroupModal();
|
||
};
|
||
dd.appendChild(row);
|
||
});
|
||
positionDD(inp);
|
||
},250);
|
||
};
|
||
inp.onblur=()=>setTimeout(hideDD,150);
|
||
}
|
||
|
||
async function saveGroup(){
|
||
if(!_grpName.trim()){toast('Enter a group name','err');return;}
|
||
if(!_grpItems.length){toast('Add at least one item','err');return;}
|
||
try{
|
||
if(_grpEditId){
|
||
await api('PUT',`/requirement-calc/groups/${_grpEditId}`,{name:_grpName.trim(),items:_grpItems});
|
||
toast('Group updated');
|
||
}else{
|
||
await api('POST','/requirement-calc/groups',{name:_grpName.trim(),items:_grpItems,company:_co});
|
||
toast('Group created');
|
||
}
|
||
_grpEditId=null;_grpName='';_grpItems=[];
|
||
await loadCalcGroups();
|
||
renderGroupModal();
|
||
}catch(e){toast(e.message,'err');}
|
||
}
|
||
|
||
async function deleteGroupConfirm(id){
|
||
const g=_calcGroups.find(x=>x.id===id);
|
||
if(!g)return;
|
||
if(!confirm(`Delete group "${g.name}"? This cannot be undone.`))return;
|
||
try{
|
||
await api('DELETE',`/requirement-calc/groups/${id}`);
|
||
toast('Group deleted');
|
||
if(_calcGroupPick===String(id))_calcGroupPick='';
|
||
await loadCalcGroups();
|
||
renderGroupModal();
|
||
render();
|
||
}catch(e){toast(e.message,'err');}
|
||
}
|
||
|
||
function fetchGroupIntoRows(){
|
||
const g=_calcGroups.find(x=>String(x.id)===_calcGroupPick);
|
||
if(!g)return;
|
||
const existing=new Set(_calcRows.filter(r=>r.itemCode).map(r=>r.itemCode));
|
||
let added=0;
|
||
g.items.forEach(it=>{
|
||
if(existing.has(it.itemCode))return;
|
||
existing.add(it.itemCode);
|
||
const row=blankCalcRow();
|
||
row.itemCode=it.itemCode;row.itemName=it.itemName;row.itemDesc=it.itemName;
|
||
const blankIdx=_calcRows.findIndex(r=>!r.itemCode);
|
||
if(blankIdx>=0)_calcRows[blankIdx]=row;else _calcRows.push(row);
|
||
added++;
|
||
});
|
||
if(!added){toast('All items from that group are already in the worksheet','err');return;}
|
||
toast(`${added} item(s) added from "${g.name}"`);
|
||
render();
|
||
}
|
||
|
||
// ─────────── VIEW ───────────
|
||
async function loadList(){_listLoading=true;render();try{const r=await api('GET',`/requirements?company=${encodeURIComponent(_co)}`);_list=r.data||[];}catch(e){toast(e.message,'err');_list=[];}_listLoading=false;render();}
|
||
|
||
function reqBadge(st){
|
||
return st==='FULLY_INTIMATED'
|
||
? `<span class="status-badge sb-closed">Fully Intimated</span>`
|
||
: st==='PARTIAL'
|
||
? `<span class="status-badge" style="background:#fff7ed;color:#c2410c;border:1px solid #fed7aa">Partial</span>`
|
||
: `<span class="status-badge sb-open">Open</span>`;
|
||
}
|
||
|
||
function reviewBadge(r){
|
||
if(!_reqReviewEnabled)return '';
|
||
const stage=r.reviewStage||0;
|
||
if(stage===1)return `<span class="status-badge" style="background:#eff6ff;color:#1d4ed8;border:1px solid #bfdbfe">⇄ With Store</span>`;
|
||
if(stage===2)return `<span class="status-badge" style="background:#f0fdf4;color:#15803d;border:1px solid #bbf7d0">✓ Store Reviewed</span>`;
|
||
return '';
|
||
}
|
||
|
||
function filteredReqList(){
|
||
let list=_list;
|
||
if(_viewStatus)list=list.filter(r=>(r.intimationStatus||'OPEN')===_viewStatus);
|
||
const s=_viewSearch.trim().toUpperCase();
|
||
if(s)list=list.filter(r=>
|
||
(r.refNo||'').toUpperCase().includes(s)||
|
||
(r.createdByName||r.createdBy||'').toUpperCase().includes(s)||
|
||
(r.remarks||'').toUpperCase().includes(s)||
|
||
(r.lines||[]).some(l=>(l.itemCode||'').toUpperCase().includes(s)||(l.itemName||'').toUpperCase().includes(s)));
|
||
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 requirements yet. Create one from the “New Requirement” tab.</div></div>`;
|
||
const list=filteredReqList();
|
||
const chips=[['','All'],['OPEN','Open'],['PARTIAL','Partial'],['FULLY_INTIMATED','Fully Intimated']];
|
||
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 ref no, item code/name, remarks…" value="${esc(_viewSearch)}"/></div>
|
||
<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>`;
|
||
const grid=list.length
|
||
? `<div class="req-grid">${list.map(r=>{
|
||
const st=r.intimationStatus||'OPEN';
|
||
return `
|
||
<div class="req-card" data-id="${r.id}">
|
||
<div class="rc-top">
|
||
<span class="rc-ref">${esc(r.refNo)}</span>
|
||
${reqBadge(st)}
|
||
${reviewBadge(r)}
|
||
</div>
|
||
<div class="rc-icon"><i data-lucide="clipboard-list" style="width:18px;height:18px"></i></div>
|
||
<div style="font-size:12px;color:var(--text2);font-weight:600">${(r.lines||[]).length} item(s)</div>
|
||
<div class="rc-by">${esc(r.createdByName||r.createdBy||'')}</div>
|
||
<div class="rc-meta">
|
||
<span><i data-lucide="calendar" style="width:12px;height:12px;vertical-align:-2px"></i> ${fmtDate(r.periodFrom)} → ${fmtDate(r.periodTo)}</span>
|
||
<span><i data-lucide="clock" style="width:12px;height:12px;vertical-align:-2px"></i> ${fmtDate(r.createdAt)}</span>
|
||
</div>
|
||
</div>`;
|
||
}).join('')}</div>`
|
||
: `<div class="card"><div class="empty-msg">No requirements match your search/filter.</div></div>`;
|
||
return bar+grid;
|
||
}
|
||
|
||
function closeReqDetail(){document.getElementById('req-detail-modal').classList.remove('show');}
|
||
function openReqDetail(id){
|
||
const r=_list.find(x=>x.id===id);
|
||
if(!r)return;
|
||
const st=r.intimationStatus||'OPEN';
|
||
const lines=(r.lines||[]).map(l=>{
|
||
const done=l.intimatedQty!=null?l.intimatedQty:0;
|
||
const pend=l.pendingQty!=null?l.pendingQty:l.requiredQty;
|
||
const pc=Number(pend)>0?'color:#c2410c;font-weight:700':'color:#16a34a;font-weight:700';
|
||
return `<tr><td class="td-sr">${l.srNo}</td><td style="font-family:var(--mono);font-size:12px;color:var(--blue)">${esc(l.itemCode)}</td><td>${esc(l.itemName)}</td><td>${esc(l.itemDesc)}</td><td class="td-qty">${esc(l.requiredQty)}</td><td class="td-qty">${esc(done)}</td><td class="td-qty" style="${pc}">${esc(pend)}</td></tr>`;
|
||
}).join('');
|
||
const actions=r.hasIntimations
|
||
? `<button class="rc-btn" disabled title="Locked — batch intimation(s) exist against this requirement. Editing and deleting are disabled." style="opacity:.5;cursor:not-allowed"><i data-lucide="lock" style="width:13px;height:13px"></i> Locked</button>`
|
||
: `${CAN_EDIT_REQ?`<button class="rc-btn" data-edit="${r.id}"><i data-lucide="pencil" style="width:13px;height:13px"></i> Edit</button>`:''}
|
||
${CAN_DELETE_REQ?`<button class="rc-btn del" data-del="${r.id}"><i data-lucide="trash-2" style="width:13px;height:13px"></i> Delete</button>`:''}`;
|
||
const stage=r.reviewStage||0;
|
||
let reviewActions='';
|
||
if(_reqReviewEnabled){
|
||
if(stage===0&&CAN_SHARE_REQ)
|
||
reviewActions=`<button class="rc-btn" id="req-share-btn" style="background:#eff6ff;color:#1d4ed8;border-color:#bfdbfe"><i data-lucide="send" style="width:13px;height:13px"></i> Share with Store</button>`;
|
||
else if(stage===1&&CAN_STORE_REVIEW_REQ)
|
||
reviewActions=`<button class="rc-btn" id="req-storereview-btn" style="background:#eff6ff;color:#1d4ed8;border-color:#bfdbfe"><i data-lucide="edit-3" style="width:13px;height:13px"></i> Store Review</button>`;
|
||
}
|
||
const reviewSection = (_reqReviewEnabled && stage>0) ? renderReviewSection(r) : '';
|
||
const inner=document.getElementById('req-modal-inner');
|
||
inner.innerHTML=`
|
||
<div class="req-modal-hd">
|
||
<span class="rc-ref">${esc(r.refNo)}</span>
|
||
${reqBadge(st)}
|
||
<span style="font-size:11px;color:var(--text4)">${esc(r.createdByName||r.createdBy||'')}${r.updatedAt?' · edited':''}</span>
|
||
<button class="req-modal-close" id="req-modal-x"><i data-lucide="x" style="width:15px;height:15px"></i></button>
|
||
</div>
|
||
<div class="req-modal-bd">
|
||
<div class="rc-meta" style="margin-top:0;border-top:none;padding-top:0">
|
||
<span><i data-lucide="calendar" style="width:13px;height:13px;vertical-align:-2px"></i> ${fmtDate(r.periodFrom)} → ${fmtDate(r.periodTo)}</span>
|
||
<span><i data-lucide="package" style="width:13px;height:13px;vertical-align:-2px"></i> ${(r.lines||[]).length} item(s)</span>
|
||
${st!=='OPEN'?`<span><i data-lucide="layers" style="width:13px;height:13px;vertical-align:-2px"></i> ${st==='FULLY_INTIMATED'?'fully':'partly'} intimated</span>`:''}
|
||
</div>
|
||
<div class="tbl-wrap" style="margin-top:14px"><table class="req-tbl">
|
||
<thead><tr><th class="td-sr">Sr.</th><th>Code</th><th>Item Name</th><th>Description</th><th class="td-qty">Req. Qty</th><th class="td-qty">Intimated</th><th class="td-qty">Pending</th></tr></thead>
|
||
<tbody>${lines}</tbody>
|
||
</table></div>
|
||
${r.remarks?`<div style="margin-top:10px;font-size:12px;color:var(--text3)"><strong>Remarks:</strong> ${esc(r.remarks)}</div>`:''}
|
||
${reviewSection}
|
||
</div>
|
||
<div class="req-modal-ft">${reviewActions}${actions}</div>`;
|
||
document.getElementById('req-detail-modal').classList.add('show');
|
||
if(window.lucide)lucide.createIcons();
|
||
document.getElementById('req-modal-x').onclick=closeReqDetail;
|
||
const editBtn=inner.querySelector('[data-edit]');if(editBtn)editBtn.onclick=()=>{closeReqDetail();startEdit(r);};
|
||
const delBtn=inner.querySelector('[data-del]');if(delBtn)delBtn.onclick=()=>{closeReqDetail();deleteReq(r.id,r.refNo);};
|
||
const shareBtn=inner.querySelector('#req-share-btn');if(shareBtn)shareBtn.onclick=()=>shareWithStore(r);
|
||
const srBtn=inner.querySelector('#req-storereview-btn');if(srBtn)srBtn.onclick=()=>openStoreReview(r);
|
||
}
|
||
|
||
// ── Store Review workflow: share / before-after / editable review screen ──
|
||
function renderReviewSection(r){
|
||
const stage=r.reviewStage||0;
|
||
let html=`<div style="margin-top:18px;border-top:1px dashed var(--border);padding-top:14px">
|
||
<div style="font-size:12px;font-weight:700;color:var(--text2);margin-bottom:8px">
|
||
<i data-lucide="repeat" style="width:13px;height:13px;vertical-align:-2px"></i> Store Review
|
||
${stage===1?'<span style="color:#1d4ed8">— shared, awaiting Store</span>':'<span style="color:#15803d">— reviewed by Store</span>'}
|
||
</div>
|
||
<div style="font-size:11px;color:var(--text4)">Shared by ${esc(r.sharedByName||r.sharedBy||'—')} ${r.sharedAt?'· '+fmtDate(r.sharedAt):''}</div>`;
|
||
if(stage===2){
|
||
html+=`<div style="font-size:11px;color:var(--text4);margin-top:2px">Reviewed by ${esc(r.reviewedByName||r.reviewedBy||'—')} ${r.reviewedAt?'· '+fmtDate(r.reviewedAt):''}</div>`;
|
||
if(r.reviewRemarks)html+=`<div style="margin-top:6px;font-size:12px;color:var(--text3)"><strong>Store remarks:</strong> ${esc(r.reviewRemarks)}</div>`;
|
||
const before=r.originalLines||[];
|
||
const beforeMap={};before.forEach(l=>beforeMap[l.itemCode]=l);
|
||
const after=r.lines||[];
|
||
// Only rows Store actually touched: new items, or an existing item whose
|
||
// qty changed. Untouched lines are omitted — this is a change log, not a
|
||
// full re-listing of the requirement's items.
|
||
const rows=after.filter(l=>{
|
||
const b=beforeMap[l.itemCode];
|
||
return !b||Number(b.requiredQty)!==Number(l.requiredQty);
|
||
}).map(l=>{
|
||
const b=beforeMap[l.itemCode];
|
||
return `<tr>
|
||
<td style="font-family:var(--mono);font-size:12px;color:var(--blue)">${esc(l.itemCode)}</td>
|
||
<td>${esc(l.itemName)}</td>
|
||
<td class="td-qty">${b?esc(b.requiredQty):'<span style="color:#16a34a;font-weight:700">new</span>'}</td>
|
||
<td class="td-qty" style="color:#c2410c;font-weight:700">${esc(l.requiredQty)}</td>
|
||
</tr>`;
|
||
}).join('');
|
||
const removed=before.filter(b=>!after.some(a=>a.itemCode===b.itemCode));
|
||
const removedRows=removed.map(b=>`<tr><td style="font-family:var(--mono);font-size:12px;color:var(--blue)">${esc(b.itemCode)}</td><td>${esc(b.itemName)}</td><td class="td-qty">${esc(b.requiredQty)}</td><td class="td-qty" style="color:#dc2626;text-decoration:line-through">removed</td></tr>`).join('');
|
||
html+=(rows+removedRows)
|
||
? `<div class="tbl-wrap" style="margin-top:10px"><table class="req-tbl">
|
||
<thead><tr><th>Code</th><th>Item Name</th><th class="td-qty">Before</th><th class="td-qty">After</th></tr></thead>
|
||
<tbody>${rows}${removedRows}</tbody>
|
||
</table></div>`
|
||
: `<div style="margin-top:10px;font-size:12px;color:var(--text3)">No changes — Store reverted without modifying any item or quantity.</div>`;
|
||
}
|
||
html+='</div>';
|
||
return html;
|
||
}
|
||
|
||
async function shareWithStore(r){
|
||
try{
|
||
const res=await api('POST',`/requirements/${r.id}/share-with-store`,{});
|
||
toast('Shared with Store');
|
||
closeReqDetail();
|
||
await loadList();
|
||
}catch(e){toast(e.message,'err');}
|
||
}
|
||
|
||
function openStoreReview(r){
|
||
_storeReviewFor=r;
|
||
_storeReviewRows=(r.lines||[]).map(l=>({itemCode:l.itemCode||'',itemName:l.itemName||'',itemDesc:l.itemDesc||'',requiredQty:l.requiredQty!=null?l.requiredQty:''}));
|
||
_storeReviewRemarks='';
|
||
renderStoreReviewModal();
|
||
}
|
||
|
||
function renderStoreReviewModal(){
|
||
const r=_storeReviewFor;
|
||
const rowsHtml=_storeReviewRows.map((row,i)=>`
|
||
<tr>
|
||
<td class="td-sr">${i+1}</td>
|
||
<td style="font-family:var(--mono);font-size:12px;color:var(--blue)">${esc(row.itemCode)}</td>
|
||
<td>${esc(row.itemName)}</td>
|
||
<td class="td-qty"><input class="fi" data-srqty="${i}" type="number" min="0" step="any" value="${esc(row.requiredQty)}"/></td>
|
||
<td class="td-act"><button class="b-del" data-srrm="${i}" title="Remove"><i data-lucide="trash-2" style="width:14px;height:14px"></i></button></td>
|
||
</tr>`).join('');
|
||
const inner=document.getElementById('req-modal-inner');
|
||
inner.innerHTML=`
|
||
<div class="req-modal-hd">
|
||
<span class="rc-ref">${esc(r.refNo)}</span>
|
||
<span style="font-size:12px;font-weight:700;color:#1d4ed8">Store Review</span>
|
||
<button class="req-modal-close" id="req-modal-x"><i data-lucide="x" style="width:15px;height:15px"></i></button>
|
||
</div>
|
||
<div class="req-modal-bd">
|
||
<div style="font-size:12px;color:var(--text3);margin-bottom:10px">Cross-check against SAP's MRP Wizard output, adjust quantities / remove items as needed, then revert to Production. This is a single round trip.</div>
|
||
<div class="tbl-wrap"><table class="req-tbl">
|
||
<thead><tr><th class="td-sr">Sr.</th><th>Code</th><th>Item Name</th><th class="td-qty">Required Qty</th><th class="td-act"></th></tr></thead>
|
||
<tbody>${rowsHtml}</tbody>
|
||
</table></div>
|
||
<div class="fg" style="margin-top:14px"><label class="fl">Remarks (optional)</label><input class="fi" id="sr-remarks" placeholder="Notes for Production…" value="${esc(_storeReviewRemarks)}"/></div>
|
||
</div>
|
||
<div class="req-modal-ft">
|
||
<button class="rc-btn" id="sr-cancel">Cancel</button>
|
||
<button class="rc-btn" id="sr-submit" style="background:#eff6ff;color:#1d4ed8;border-color:#bfdbfe"><i data-lucide="check" style="width:13px;height:13px"></i> Revert to Production</button>
|
||
</div>`;
|
||
document.getElementById('req-detail-modal').classList.add('show');
|
||
if(window.lucide)lucide.createIcons();
|
||
document.getElementById('req-modal-x').onclick=closeReqDetail;
|
||
document.getElementById('sr-cancel').onclick=closeReqDetail;
|
||
document.getElementById('sr-remarks').oninput=function(){_storeReviewRemarks=this.value;};
|
||
inner.querySelectorAll('[data-srqty]').forEach(el=>el.oninput=function(){_storeReviewRows[+this.dataset.srqty].requiredQty=this.value;});
|
||
inner.querySelectorAll('[data-srrm]').forEach(b=>b.onclick=function(){_storeReviewRows.splice(+this.dataset.srrm,1);renderStoreReviewModal();});
|
||
document.getElementById('sr-submit').onclick=submitStoreReview;
|
||
}
|
||
|
||
async function submitStoreReview(){
|
||
const lines=_storeReviewRows.filter(row=>row.itemCode).map(row=>({itemCode:row.itemCode,itemName:row.itemName,itemDesc:row.itemDesc,requiredQty:Number(row.requiredQty)||0}));
|
||
if(!lines.length){toast('At least one item is required','err');return;}
|
||
try{
|
||
await api('POST',`/requirements/${_storeReviewFor.id}/revert-to-production`,{lines,remarks:_storeReviewRemarks});
|
||
toast('Reverted to Production');
|
||
closeReqDetail();
|
||
_storeReviewFor=null;_storeReviewRows=null;
|
||
await loadList();
|
||
}catch(e){toast(e.message,'err');}
|
||
}
|
||
|
||
function wireView(){
|
||
document.querySelectorAll('.req-card').forEach(c=>c.onclick=()=>openReqDetail(+c.dataset.id));
|
||
const s=q('#vf-search');
|
||
if(s){
|
||
s.oninput=function(){
|
||
_viewSearch=this.value;
|
||
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();
|
||
};
|
||
}
|
||
document.querySelectorAll('[data-vfstatus]').forEach(b=>b.onclick=function(){_viewStatus=this.dataset.vfstatus;q('#body').innerHTML=renderView();wireView();if(window.lucide)lucide.createIcons();});
|
||
}
|
||
|
||
// ─────────── INIT ───────────
|
||
if(_tab==='view')loadList(); else loadNextRef();
|
||
loadReviewConfig();
|
||
</script>
|
||
<script src="/sidebar.js?v=31"></script>
|
||
</body>
|
||
</html>
|