// sidebar.js — Shared sidebar navigation for all portal pages (function(){ // Favicon (company logo) — set here once rather than in every page's //
, since this script is already loaded on every page including the // login screen. Runs before the auth check below so it applies even when // logged out. Removes any pre-existing icon links first so this always wins. (function setFavicon(){ document.querySelectorAll('link[rel~="icon"]').forEach(el=>el.remove()); const link=document.createElement('link'); link.rel='icon';link.type='image/png';link.href='/logo.png'; document.head.appendChild(link); })(); // Session-only auth: login lives ONLY in sessionStorage (see auth-guard.js), // never localStorage, so a new tab intentionally needs its own login. const tok=sessionStorage.getItem('portal_token'); let userStr=null;try{userStr=sessionStorage.getItem('portal_user');}catch(_e){} if(!tok||!userStr) return; // sidebar.js renders straight into document.body (prepend below), completely // independent of whatever #root shows — so on pages like index.html (which // don't load auth-guard.js and branch login-vs-dashboard themselves), a // stale-but-still-present EXPIRED token used to render a fully functional // sidebar right alongside the login screen. Token presence alone isn't // enough; check the JWT's own exp claim before rendering anything. function jwtExpired(t){ try{ const payload=JSON.parse(atob(t.split('.')[1].replace(/-/g,'+').replace(/_/g,'/'))); return !payload.exp || (Date.now()>=payload.exp*1000); }catch(_e){ return true; } } if(jwtExpired(tok)){ try{sessionStorage.clear();document.cookie='portal_token=;path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT';document.cookie='portal_user=;path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT';;}catch(_e){} return; } // Refresh cached user (modules AND approval-step grants) from the server so // admin-side access changes apply without forcing a logout — reload once if // anything access-relevant actually changed. // // approvalSteps/modules/role are baked into the JWT ITSELF at login, and // every server-side permission check (requireApprovalStep/requireWorkflowPerm // in middleware/auth.js) decodes that token directly — no DB lookup. So it's // not enough to update the cached portal_user for the UI; the bearer token // used on every /api call must also be replaced, via POST /api/auth/refresh, // or a newly granted Approval Step keeps failing server-side ("You are not // assigned...") even after the sidebar/UI already shows it as granted. (function refreshUserModules(){ // Never auto-refresh while impersonating — /auth/refresh would mint a // plain token for the TARGET user without impersonatedBy, silently // ending the impersonation (banner gone) while still leaving the // session logged in AS that user instead of bouncing back to the real // one. Impersonation sessions are short-lived by design; "Return to my // account" is the only way out, not a background refresh. let _cu=null; try{_cu=JSON.parse(userStr);}catch(_e){} if(_cu&&_cu.impersonatedBy) return; fetch('/api/auth/profile',{headers:{Authorization:'Bearer '+tok}}) .then(r=>r.json()) .then(d=>{ if(!d||!d.success||!d.user) return; const fresh=d.user; const cached=JSON.parse(userStr); const same=JSON.stringify(fresh.modules||null)===JSON.stringify(cached.modules||null) && JSON.stringify(fresh.approvalSteps||null)===JSON.stringify(cached.approvalSteps||null) && JSON.stringify(fresh.allowedCompanies||null)===JSON.stringify(cached.allowedCompanies||null) && fresh.role===cached.role && fresh.active!==false && !!fresh.woVerifyOverride===!!cached.woVerifyOverride; if(fresh.active===false){ try{sessionStorage.clear();document.cookie='portal_token=;path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT';document.cookie='portal_user=;path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT';;}catch(_e){} location.replace('/'); return; } if(!same){ // Mint a fresh token (carries the new approvalSteps/modules/role) // BEFORE reloading, so the reloaded page's very first API call // already authorizes correctly. fetch('/api/auth/refresh',{method:'POST',headers:{Authorization:'Bearer '+tok}}) .then(r=>r.json()) .then(rd=>{ const merged=Object.assign({},cached,{modules:fresh.modules,approvalSteps:fresh.approvalSteps,allowedCompanies:fresh.allowedCompanies,role:fresh.role,fullName:fresh.fullName,name:fresh.fullName||cached.name,woVerifyOverride:fresh.woVerifyOverride}); try{ if(rd&&rd.success&&rd.token) sessionStorage.setItem('portal_token',rd.token); sessionStorage.setItem('portal_user',JSON.stringify(merged)); document.cookie='portal_user='+encodeURIComponent(JSON.stringify(merged))+';path=/;SameSite=Lax'; if(rd&&rd.success&&rd.token) document.cookie='portal_token='+encodeURIComponent(rd.token)+';path=/;SameSite=Lax'; }catch(_e){} if(!sessionStorage.getItem('portal_modules_reloaded')){ sessionStorage.setItem('portal_modules_reloaded','1'); location.reload(); } }) .catch(()=>{}); } else { sessionStorage.removeItem('portal_modules_reloaded'); } }) .catch(()=>{}); })(); // Ensure lucide is loaded before building sidebar function withLucide(cb){ if(window.lucide&&lucide.icons&&lucide.createElement){cb();return;} var s=document.createElement('script'); s.src='/lucide.min.js?v=399'; s.onload=cb; s.onerror=cb; // build even if lucide fails (icons will be blank) document.head.appendChild(s); } function sbIco(name,size=15){ if(window.lucide&&lucide.icons&&lucide.createElement){ var p=name.replace(/(^[a-z]|-[a-z0-9])/g,function(m){return m.replace('-','').toUpperCase();}); var d=lucide.icons[p]; if(d){try{ var svg=lucide.createElement([d[0],Object.assign({},d[1],{width:size,height:size}),d[2]]); svg.style.flexShrink='0'; return svg.outerHTML; }catch(e){}} } return''; } withLucide(buildSidebar); function buildSidebar(){ // Read the role up-front (the shared `user` variable is only declared further // down — referencing it inside the LINKS initializer would TDZ-crash). let _role=null;try{_role=(JSON.parse(sessionStorage.getItem('portal_user')||'null')||{}).role;}catch(_e){} const LINKS=[ {href:'/',lucide:'layout-dashboard',label:'Home',module:'home'}, // Board Dashboard link is for NON-board-member users granted the module — // board members skip it (they're auto-redirected there on login anyway). ...(_role!=='board_member'?[{href:'/board-dashboard',lucide:'trending-up',label:'Board Dashboard',module:'board-dashboard'}]:[]), // Inventory {type:'group',label:'Inventory',lucide:'boxes'}, {href:'/bom', lucide:'list-tree', label:'BOM', module:'bom', sub:true}, {href:'/items', lucide:'package-plus', label:'Create Item', module:'items', sub:true,exact:true}, {href:'/items?list=1', lucide:'list', label:'View Items', module:'items', sub:true,exact:true}, // Production {type:'group',label:'Production',lucide:'factory'}, {href:'/production-dashboard',lucide:'gauge', label:'Dashboard', module:'production-dashboard', sub:true}, {href:'/production', lucide:'file-plus-2', label:'Production Order', module:'production-create', sub:true}, {href:'/issue-production', lucide:'package-open', label:'Issue', module:'production-issue', sub:true}, {href:'/receipt-production',lucide:'package-check', label:'Receipt', module:'production-receipt', sub:true}, {href:'/verify-work-order', lucide:'clipboard-check', label:'Verify Work Order',module:'work-order-verify', sub:true}, {href:'/verify-production', lucide:'clipboard-check', label:'Verify', module:'production-verify', sub:true}, {href:'/receipts-history', lucide:'history', label:'Receipts History', module:'production-receipt', sub:true}, {href:'/close-production', lucide:'circle-check-big',label:'Close', module:'production-close', sub:true}, {href:'/requirements', lucide:'clipboard-list', label:'Requirements', module:'production-requirements', sub:true}, {href:'/batch-issuance', lucide:'layers', label:'Batch Issuance', module:'production-batch-issuance', sub:true}, {href:'/work-order', lucide:'file-cog', label:'Work Order', module:'production-work-order', sub:true}, {href:'/man-power', lucide:'users', label:'Man Power', module:'man-power', sub:true}, {href:'/oee', lucide:'activity', label:'OEE', module:'oee', sub:true}, {href:'/ppc-report', lucide:'table-2', label:'PPC Report', module:'ppc-report', sub:true}, {href:'/pwo-source-audit', lucide:'shield-check', label:'PWO Source Report',module:'pwo-source-audit', sub:true}, {href:'/rejection-register',lucide:'clipboard-x', label:'Rejection Register',module:'rejection-register', sub:true}, {href:'/rejection-analytics',lucide:'bar-chart-3', label:'Rejection Analytics',module:'rejection-analytics', sub:true}, {href:'/production-planning',lucide:'calendar-clock', label:'Production Planning',module:'production-planning', sub:true}, // Purchase {type:'group',label:'Purchase',lucide:'shopping-cart'}, {href:'/purchase-request', lucide:'file-text', label:'Purchase Request', module:'purchase-request', sub:true}, {href:'/purchase-quotation',lucide:'file-pen', label:'Quotation', module:'purchase-quotation', sub:true}, {href:'/purchase-order', lucide:'shopping-bag', label:'Purchase Order', module:'purchase-order', sub:true}, {href:'/grpo', lucide:'truck', label:'Goods Receipt PO', module:'purchase-grpo', sub:true}, // Approval {type:'group',label:'Approvals',lucide:'badge-check'}, {href:'/approvals', lucide:'stamp', label:'External Approval',module:'approvals', sub:true}, {href:'/sap-approvals', lucide:'shield-check', label:'SAP Approval', module:'sap-approvals', sub:true}, // Forms {type:'group',label:'Forms',lucide:'clipboard-pen'}, {href:'/vendor-register', lucide:'building-2', label:'Vendor', module:'vendors', sub:true}, {href:'/register', lucide:'user-plus', label:'Customer', module:'customers', sub:true}, // Documents {href:'/documents', lucide:'folder-open', label:'Documents', module:'documents'}, // Projects {type:'group',label:'Projects',lucide:'briefcase'}, {href:'/project-form?new=1', lucide:'folder-plus', label:'New Project', module:'projects-new', sub:true,exact:true}, {href:'/project-form?list=1',lucide:'folders', label:'View Projects', module:'projects-view', sub:true,exact:true}, {href:'/project-approvals', lucide:'clipboard-check',label:'Project Approvals',module:'projects-approvals', sub:true,exact:true}, // Finance {type:'group',label:'Finance',lucide:'landmark'}, {href:'/costing-pl', lucide:'calendar-range',label:'Monthly Accounts', module:'finance-monthly', sub:true,exact:true}, {href:'/costing-comparison', lucide:'trending-up', label:'P&L Comparison', module:'finance-comparison', sub:true,exact:true}, {href:'/cost-sheet', lucide:'file-spreadsheet',label:'Cost Sheet', module:'finance-costsheet', sub:true,exact:true}, {href:'/balance-sheet', lucide:'scale', label:'Balance Sheet', module:'finance-balancesheet',sub:true,exact:true}, {href:'/cash-flow', lucide:'banknote', label:'Cash Flow', module:'finance-cashflow', sub:true,exact:true}, {href:'/salary', lucide:'hand-coins', label:'Salary', module:'salary', sub:true,exact:true}, // GSTR {type:'group',label:'GST Reports',lucide:'receipt'}, {href:'/gstr1', lucide:'file-output', label:'GSTR-1', module:'gstr1', sub:true}, {href:'/gstr2', lucide:'file-input', label:'GSTR-2 Purchase', module:'gstr2', sub:true}, {href:'/itc04', lucide:'repeat', label:'ITC-04 Job Work', module:'itc04', sub:true}, // Masters {type:'group',label:'Masters',lucide:'database'}, {href:'/business-master', lucide:'building', label:'Business Master', module:'business-master', sub:true}, // Reports {type:'group',label:'Reports',lucide:'pie-chart'}, {href:'/reports', lucide:'bar-chart-3', label:'Reports', module:'reports', sub:true}, {href:'/general-ledger', lucide:'book-open', label:'General Ledger', module:'general-ledger', sub:true}, // Admin — the '/admin' link itself moved to the fixed bottom cluster // (near Profile/Guide, always visible without scrolling — see sb-bottom // below); these two stay in the regular scrollable list. {href:'/item-group-classification', lucide:'layers-3', label:'Item Group Rules', module:'item-group-classification', adminOnly:true}, {href:'/audit-logs', lucide:'scroll-text', label:'Audit Logs', module:'audit'}, {href:'/mail-logs', lucide:'mail', label:'Mail Log', module:'mail_log'}, ]; const path=window.location.pathname; const fullPath=window.location.pathname+window.location.search; let user=null; try{const u=sessionStorage.getItem('portal_user');if(u)user=JSON.parse(u);}catch(_e){} const isAdmin=user?.role==='admin'||user?.role==='sap_adder'; // full module bypass (SAP Adder kept its original admin-like visibility) const isSubAdmin=user?.role==='system_admin'; // System Admin: sees ONLY granted modules (but may open admin-only pages he's been granted) const userModules=user?.modules||null; const themeLink=document.createElement('link');themeLink.rel='stylesheet';themeLink.href='/theme.css?v=3';document.head.appendChild(themeLink); const oldNav=document.querySelector('nav'); if(oldNav)oldNav.remove(); const style=document.createElement('style'); style.textContent=` /* ── Sidebar shell ── */ .sb{ position:fixed;left:0;top:0;bottom:0; background:#ffffff; border-right:1px solid #eef1f5; box-shadow:1px 0 0 rgba(15,23,42,.02),2px 0 12px rgba(15,23,42,.03); z-index:400;display:flex;flex-direction:column; overflow-x:hidden;overflow-y:auto; font-feature-settings:"tnum"; } .sb::-webkit-scrollbar{width:0} /* ── Logo ── */ .sb-logo{ padding:18px 16px 16px; border-bottom:1px solid #f1f4f8; flex-shrink:0;display:flex;align-items:center;gap:11px } .sb-logo-mark{ width:40px;height:40px; display:flex;align-items:center;justify-content:center; flex-shrink:0; } .sb-logo-mark img,.sb-logo-mark svg{width:100%;height:100%;object-fit:contain;display:block} .sb-logo-text{font-family:'Space Grotesk',sans-serif;font-size:13.5px;font-weight:700;color:#0f172a;letter-spacing:.1px;line-height:1.25} .sb-logo-text small{display:block;font-size:10px;font-weight:500;color:#94a3b8;letter-spacing:.3px;margin-top:2px;text-transform:uppercase} /* ── Pending-actions bell ── */ .sb-bell-row{padding:8px 10px 0} .sb-bell{ width:100%;display:flex;align-items:center;gap:9px; padding:8px 10px;border-radius:9px;border:1px solid #eef1f5;background:#f8fafc; color:#475569;font-size:12.5px;font-weight:600;font-family:'Space Grotesk',sans-serif; cursor:pointer;transition:all .15s;position:relative; } .sb-bell:hover{background:#eff6ff;border-color:#bfdbfe;color:#1d4ed8} .sb-bell svg{flex-shrink:0;color:#64748b} .sb-bell:hover svg{color:#2563eb} .sb-bell.has-pending svg{color:#d97706} .sb-bell-badge{ margin-left:auto;background:#dc2626;color:#fff;font-size:10px;font-weight:800; border-radius:10px;padding:1px 7px;line-height:1.5;flex-shrink:0; } .sb-bell-panel{ display:none;position:fixed;background:#fff;border:1px solid #e2e8f0;border-radius:12px; box-shadow:0 16px 40px rgba(15,23,42,.16);z-index:900;width:320px;max-height:420px; overflow-y:auto;font-family:'Space Grotesk',sans-serif; } .sb-bell-panel.show{display:block} .sb-bp-hd{padding:12px 16px;border-bottom:1px solid #f1f4f8;font-size:12px;font-weight:700;color:#0f172a;position:sticky;top:0;background:#fff} .sb-bp-empty{padding:28px 16px;text-align:center;font-size:12px;color:#94a3b8} .sb-bp-item{display:block;padding:10px 16px;border-bottom:1px solid #f8fafc;text-decoration:none;transition:background .12s} .sb-bp-item:hover{background:#f8fafc} .sb-bp-item:last-child{border-bottom:none} .sb-bp-mod{font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:#2563eb;margin-bottom:2px} .sb-bp-title{font-size:12.5px;font-weight:700;color:#0f172a} .sb-bp-detail{font-size:11px;color:#64748b;margin-top:2px;line-height:1.4} /* ── Nav links ── */ .sb-links{flex:1;overflow-y:auto;overflow-x:hidden;padding:10px 10px} .sb-links::-webkit-scrollbar{width:0} .sb-link{ display:flex;align-items:center;gap:9px; padding:8px 10px; color:#64748b; text-decoration:none;font-size:13px;font-weight:500; white-space:nowrap; border-radius:8px; font-family:'Space Grotesk',sans-serif; margin:1.5px 0; transition:background .15s,color .15s; } .sb-link:hover{color:#0f172a;background:#f8fafc} .sb-link.active{color:#1d4ed8;background:#eff6ff;font-weight:600} .sb-link svg{color:#94a3b8;flex-shrink:0;transition:color .15s} .sb-link:hover svg{color:#334155} .sb-link.active svg{color:#2563eb} .sb-link .sb-lbl{font-size:13px} /* ── Section divider ── */ .sb-divider{height:1px;background:#f1f4f8;margin:8px 10px} /* ── Group headers ── */ .sb-group{ display:flex;align-items:center;gap:8px; padding:7px 10px; color:#94a3af; font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:1px; white-space:nowrap;margin-top:14px; font-family:'Space Grotesk',sans-serif; cursor:pointer;user-select:none; border-radius:8px; transition:all .15s; } .sb-group:hover{color:#475569;background:#f8fafc} .sb-group.open{color:#1e293b} .sb-group svg{flex-shrink:0;opacity:.85;color:#94a3b8} .sb-chev{ margin-left:auto;font-size:11px;line-height:1; transition:transform .2s ease;display:inline-block; color:#cbd5e1; } .sb-group.open .sb-chev{transform:rotate(90deg);color:#2563eb} /* ── Collapsible sub-group ── */ .sb-sub-group{overflow:hidden;max-height:0;transition:max-height .28s cubic-bezier(.4,0,.2,1)} .sb-sub-group.open{max-height:600px} .sb-sub{padding-left:24px!important;font-size:12.5px!important} .sb-sub svg{color:#94a3b8!important} .sb-sub.active svg{color:#2563eb!important} .sb-sub .sb-lbl{font-size:12px!important} /* ── Bottom section ── */ .sb-bottom{ padding:12px 10px; border-top:1px solid #f1f4f8; flex-shrink:0; } .sb-user{ display:flex;align-items:center;gap:10px; padding:9px 10px; background:#f8fafc; border:1px solid #eef1f5; border-radius:11px; cursor:pointer; transition:all .15s; } .sb-user svg{color:#cbd5e1;flex-shrink:0;transition:transform .15s} .sb-user:hover svg{transform:translateX(2px);color:#94a3b8} .sb-user:hover{background:#eff6ff;border-color:#bfdbfe} .sb-avatar{ width:31px;height:31px;border-radius:9px; background:linear-gradient(135deg,#2563eb 0%,#4338ca 100%); box-shadow:0 2px 6px rgba(37,99,235,.22); display:flex;align-items:center;justify-content:center; font-size:12px;font-weight:700;color:#fff;flex-shrink:0; } .sb-user-info{min-width:0;flex:1} .sb-role{display:block;font-size:9.5px;font-weight:700;color:#94a3b8;text-transform:uppercase;letter-spacing:.6px} .sb-name{display:block;font-size:12.5px;font-weight:600;color:#0f172a;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:1px} .sb-logout{ width:100%;padding:7px 10px;border-radius:8px;margin-top:4px; font-size:11.5px;font-weight:600; border:none; color:#94a3b8;background:transparent; cursor:pointer;font-family:'Space Grotesk',sans-serif; transition:all .15s; text-align:left;display:flex;align-items:center;gap:7px; } .sb-logout:hover{background:#fef2f2;color:#dc2626} .sb-logout svg{flex-shrink:0} .sb-help{ width:100%;padding:7px 10px;border-radius:8px;margin-top:6px; font-size:11.5px;font-weight:600; border:none;text-decoration:none; color:#94a3b8;background:transparent; cursor:pointer;font-family:'Space Grotesk',sans-serif; transition:all .15s; text-align:left;display:flex;align-items:center;gap:7px; } .sb-help:hover{background:#eff6ff;color:#2563eb} .sb-help svg{flex-shrink:0} .sb-overlay{position:fixed;inset:0;background:rgba(0,0,0,.4);z-index:399;display:none;backdrop-filter:blur(2px)} .sb-overlay.show{display:block} /* ── Toggle button (desktop) ── */ .sb-toggle-btn{ background:none;border:none;cursor:pointer; color:#9ca3af;font-size:16px;line-height:1; padding:4px 5px;border-radius:6px;margin-left:auto;flex-shrink:0; transition:color .15s,background .15s; } .sb-toggle-btn:hover{color:#374151;background:#f3f4f6} @media(min-width:769px){ .sb{width:210px;transition:width .25s ease;overflow:hidden} .sb .sb-lbl{display:inline} .sb .sb-user,.sb .sb-logout,.sb .sb-help{display:flex} .sb-logout{display:block!important} body{margin-left:210px!important;transition:margin-left .25s ease} .mob-bar{display:none!important} .sb-overlay{display:none!important} /* Collapsed = icon-only rail: links stay visible & clickable as icons, only the text labels/chevrons hide. Hover tooltips come from the title attributes set at render time. */ body.sb-hidden .sb{width:56px} body.sb-hidden{margin-left:56px!important} body.sb-hidden .sb-lbl, body.sb-hidden .sb-chev, body.sb-hidden .sb-logo-text, body.sb-hidden .sb-logo-mark, body.sb-hidden .sb-bell-panel, body.sb-hidden .sb-user-info, body.sb-hidden .sb-user>svg{display:none!important} body.sb-hidden .sb-logo{justify-content:center;padding:12px 0} body.sb-hidden .sb-toggle-btn{color:#6b7280;margin:0;font-size:18px;padding:6px;width:auto} body.sb-hidden .sb-bell-row{display:flex;justify-content:center} body.sb-hidden .sb-bell{justify-content:center;padding:8px} body.sb-hidden .sb-links{padding:10px 8px} body.sb-hidden .sb-link{justify-content:center;padding:9px 0;gap:0} body.sb-hidden .sb-sub{padding-left:0!important} body.sb-hidden .sb-group{justify-content:center;padding:8px 0;gap:0;margin-top:10px} body.sb-hidden .sb-user{justify-content:center;padding:6px 0} body.sb-hidden .sb-logout{display:flex!important;justify-content:center} body.sb-hidden .sb-help{display:flex!important;justify-content:center} } @media(max-width:768px){ .sb{width:260px;transform:translateX(-100%);transition:transform .25s ease} .sb.open{transform:translateX(0)} .sb .sb-lbl{display:inline} body{margin-left:0!important;padding-bottom:56px!important} .mob-bar{position:fixed;bottom:0;left:0;right:0;height:56px;background:#fff;border-top:1px solid #e5e7eb;z-index:300;display:flex;align-items:center;justify-content:space-around;padding:0 4px;box-shadow:0 -2px 8px rgba(0,0,0,.04)} .mob-bar a{display:flex;flex-direction:column;align-items:center;gap:2px;color:#6b7280;text-decoration:none;font-size:9px;font-weight:600;font-family:'Space Grotesk',sans-serif;padding:6px 4px;border-radius:8px;transition:all .12s;flex:1;text-align:center} .mob-bar a .mb-ico{display:flex;align-items:center;justify-content:center} .mob-bar a .mb-ico svg{color:#6b7280} .mob-bar a.active{color:#2563eb;background:#eff6ff} .mob-bar a.active .mb-ico svg{color:#2563eb} .mob-bar .mb-more{color:#6b7280;cursor:pointer;border:none;background:none;font-family:'Space Grotesk',sans-serif;font-size:9px;font-weight:600;display:flex;flex-direction:column;align-items:center;gap:2px;padding:6px 4px;flex:1} .mob-bar .mb-more .mb-ico{display:flex;align-items:center;justify-content:center} .mob-bar .mb-more .mb-ico svg{color:#6b7280} } /* ── Impersonation banner + picker modal ── */ .sb-imp-banner{position:fixed;top:0;left:0;right:0;height:34px;z-index:600;background:#7c3aed;color:#fff;display:flex;align-items:center;justify-content:center;gap:10px;font-family:'Space Grotesk',sans-serif;font-size:12px;font-weight:600;padding:0 12px;box-shadow:0 2px 8px rgba(124,58,237,.3)} .sb-imp-banner b{font-weight:800} .sb-imp-banner button{background:#fff;color:#7c3aed;border:none;border-radius:6px;padding:4px 12px;font-size:11px;font-weight:700;cursor:pointer;font-family:inherit} .sb-imp-banner button:hover{background:#f3e8ff} body.sb-impersonating{padding-top:34px} .sb-imp-modal-ov{display:none;position:fixed;inset:0;z-index:9998;background:rgba(15,23,42,.5);align-items:flex-start;justify-content:center;padding:60px 12px} .sb-imp-modal-ov.show{display:flex} .sb-imp-modal{width:100%;max-width:400px;background:#fff;border-radius:12px;box-shadow:0 20px 60px rgba(0,0,0,.25);overflow:hidden;font-family:'Space Grotesk',sans-serif} .sb-imp-modal-hd{padding:14px 16px;border-bottom:1px solid #eef1f5;font-size:14px;font-weight:700;color:#1f2937;display:flex;align-items:center;gap:10px} .sb-imp-modal-hd button{margin-left:auto;width:26px;height:26px;border-radius:7px;border:1px solid #eef1f5;background:none;color:#6b7280;cursor:pointer} .sb-imp-modal-bd{padding:16px} .sb-imp-modal-bd label{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.4px;color:#94a3b8;margin-bottom:5px;display:block} .sb-imp-modal-bd input{width:100%;padding:8px 10px;border:1.5px solid #e2e8f0;border-radius:8px;font-size:13px;font-family:inherit;outline:none;box-sizing:border-box} .sb-imp-modal-bd input:focus{border-color:#7c3aed} .sb-imp-modal-ft{display:flex;gap:8px;padding:12px 16px;border-top:1px solid #eef1f5;background:#f8fafc} .sb-imp-modal-ft button{flex:1;padding:9px;border-radius:8px;border:none;font-size:12.5px;font-weight:700;cursor:pointer;font-family:inherit} .sb-imp-cancel{background:#eef1f5;color:#475569} .sb-imp-confirm{background:#7c3aed;color:#fff} .sb-imp-confirm:disabled{opacity:.5;cursor:not-allowed} `; document.head.appendChild(style); // ── Helpers ────────────────────────────────────────────────────────────────── // Backward-compat: a user granted the OLD broad key (e.g. 'production', // before it was split into per-page sub-modules) still gets every new // sub-key under it, so nobody silently loses access when this ships. const LEGACY_BROAD_MODULES={production:'production-',purchase:'purchase-',costing:'finance-',projects:'projects-'}; const hasModule=(m)=>{ if(isAdmin||m==='home')return true; if(!userModules)return false; // no modules explicitly granted → no access (was: unchecked = full access) if(userModules.includes(m))return true; for(const broad in LEGACY_BROAD_MODULES){ if(userModules.includes(broad)&&m.indexOf(LEGACY_BROAD_MODULES[broad])===0)return true; } return false; }; // Mirrors the same exception in auth-guard.js: a pure Store reviewer for // Pre-PWO Store Review has no reason to hold the broad 'production-create' // module (that would ALSO grant full Production Order creation) — holding // either Pre-PWO approval step (any perm) is enough to show the sidebar // link too, otherwise auth-guard.js lets them into the page but they'd // have no way to find it. const hasPrePwoStepAccess=(()=>{ if(isAdmin)return true; const steps=Array.isArray(user?.approvalSteps)?user.approvalSteps:[]; const keys=['production_order:prepwo_share','production_order:prepwo_review']; return steps.some(s=>{ if(typeof s==='string')return keys.includes(s); return s&&keys.includes(s.step)&&Array.isArray(s.perms)&&s.perms.length>0; }); })(); // Same idea for OEE: holding 'oee:entry' (any perm) is enough to see the // sidebar link even without the broader 'oee' module. const hasOeeStepAccess=(()=>{ if(isAdmin)return true; const steps=Array.isArray(user?.approvalSteps)?user.approvalSteps:[]; return steps.some(s=>{ if(typeof s==='string')return s==='oee:entry'; return s&&s.step==='oee:entry'&&Array.isArray(s.perms)&&s.perms.length>0; }); })(); const canSeeLink=(l)=>hasModule(l.module) ||(l.href==='/production'&&hasPrePwoStepAccess) ||(l.href==='/oee'&&hasOeeStepAccess); // Prefix match must land on a real path boundary (path is exactly l.href, // or continues with '/') — plain startsWith() would wrongly match e.g. // '/production-dashboard' against the shorter '/production', marking both // "Production Order" (/production) and "Dashboard" (/production-dashboard) // active at once. `path` is window.location.pathname (no query string). function isActive(l){ if(l.exact) return fullPath===l.href; if(path===l.href) return true; if(l.href==='/') return false; return path.startsWith(l.href) && path[l.href.length]==='/'; } // ── Filter links: groups only if at least one child is visible ──────────────── const filteredLinks=[]; for(let i=0;i