// ================================================================ // AMS — Core Application Logic | app.js // ================================================================ /* ── HTML escaping (XSS-safe output) ───────────────────────────── */ function escapeHtml(value) { if (value === null || value === undefined) return ''; return String(value) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } // Escape for use inside a single-quoted JS string in an inline handler. function escapeJs(value) { if (value === null || value === undefined) return ''; return String(value).replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/"/g, '"').replace(/ { const href = el.getAttribute('href') || ''; el.classList.toggle('active', !!href && href === active); }); } /* ── Modals ─────────────────────────────────────────────────────── */ function openModal(id) { const el = document.getElementById(id); if (el) { el.classList.add('open'); document.body.style.overflow = 'hidden'; } } function closeModal(id) { const el = document.getElementById(id); if (el) { el.classList.remove('open'); document.body.style.overflow = ''; } } /* ── Tabs ─────────────────────────────────────────────────────── */ function initTabs(container) { const root = container || document; root.querySelectorAll('.tab-btn').forEach(btn => { btn.addEventListener('click', () => { const tabsEl = btn.closest('.tabs'); const tabGroup = tabsEl ? tabsEl.dataset.group : null; const targetId = btn.dataset.tab; tabsEl.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); const scope = btn.closest('.tab-container') || btn.closest('.card') || btn.closest('.content') || document; scope.querySelectorAll('.tab-content').forEach(tc => { if (tabGroup && tc.dataset.group && tc.dataset.group !== tabGroup) return; tc.classList.toggle('active', tc.id === targetId); }); }); }); } /* ── Toast ─────────────────────────────────────────────────────── */ function showToast(title, text='', type='success', dur=4200) { const icons = { success:'✅', error:'❌', warning:'⚠️', info:'ℹ️' }; const c = document.getElementById('toastContainer'); if (!c) return; const t = document.createElement('div'); t.className = `toast ${type}`; t.innerHTML = `
${icons[type]}
${title}
${text?`
${text}
`:''}
`; c.appendChild(t); setTimeout(() => { t.style.opacity='0'; t.style.transform='translateX(30px)'; t.style.transition='all .3s ease'; setTimeout(()=>t.remove(),350); }, dur); } /* ── Table Search ─────────────────────────────────────────────── */ function initSearch(inputId, tableId) { const inp = document.getElementById(inputId); const tbl = document.getElementById(tableId); if (!inp || !tbl) return; inp.addEventListener('input', () => { const q = inp.value.toLowerCase().trim(); let vis = 0; tbl.querySelectorAll('tbody tr').forEach(r => { const show = !q || r.textContent.toLowerCase().includes(q); r.style.display = show ? '' : 'none'; if (show) vis++; }); const cnt = document.getElementById(tableId + '-count'); if (cnt) cnt.textContent = vis; }); } /* ── Select Filter ─────────────────────────────────────────────── */ function initFilter(selId, tableId, colIdx) { const sel = document.getElementById(selId); const tbl = document.getElementById(tableId); if (!sel || !tbl) return; sel.addEventListener('change', () => { const v = sel.value.toLowerCase(); tbl.querySelectorAll('tbody tr').forEach(r => { if (!v) { r.style.display = ''; return; } const cell = r.cells[colIdx]; r.style.display = (cell && cell.textContent.toLowerCase().includes(v)) ? '' : 'none'; }); }); } /* ── Bulk Checkboxes ─────────────────────────────────────────── */ function initCheckboxes(tableId, bulkBarId) { const tbl = document.getElementById(tableId); const bar = document.getElementById(bulkBarId); if (!tbl) return; const all = tbl.querySelector('.select-all'); const rows = () => [...tbl.querySelectorAll('.row-check')]; const updateBar = () => { if (!bar) return; const n = rows().filter(c => c.checked).length; bar.style.display = n > 0 ? 'flex' : 'none'; const cnt = bar.querySelector('.bulk-count'); if (cnt) cnt.textContent = `${n} item${n!==1?'s':''} selected`; }; if (all) { all.addEventListener('change', () => { rows().forEach(c => { c.checked = all.checked; c.closest('tr').classList.toggle('row-selected', all.checked); }); updateBar(); }); } rows().forEach(c => { c.addEventListener('change', () => { c.closest('tr').classList.toggle('row-selected', c.checked); if (all) all.checked = rows().every(r => r.checked); updateBar(); }); }); } /* ── Confirm Dialog ─────────────────────────────────────────── */ function confirmAction(title, msg, cb, type='danger') { const m = document.getElementById('confirmModal'); if (!m) { if (confirm(msg) && cb) cb(); return; } m.querySelector('.confirm-title').textContent = title; m.querySelector('.confirm-message').textContent = msg; const btn = m.querySelector('.confirm-ok'); btn.onclick = () => { closeModal('confirmModal'); if (cb) cb(); }; openModal('confirmModal'); } /* ── Dropdowns ─────────────────────────────────────────────── */ function initDropdowns() { document.querySelectorAll('[data-dropdown]').forEach(trigger => { trigger.addEventListener('click', e => { e.stopPropagation(); const menuId = trigger.dataset.dropdown; const menu = document.getElementById(menuId); if (!menu) return; document.querySelectorAll('.dropdown-menu.open').forEach(m => { if (m.id !== menuId) m.classList.remove('open'); }); menu.classList.toggle('open'); }); }); document.addEventListener('click', () => { document.querySelectorAll('.dropdown-menu.open').forEach(m => m.classList.remove('open')); }); } /* ── Tree ─────────────────────────────────────────────────── */ function initTrees() { document.querySelectorAll('.tree-toggle').forEach(t => { t.addEventListener('click', () => { const ch = t.closest('.tree-node').querySelector('.tree-children'); const ar = t.querySelector('.tree-arrow'); if (ch) { ch.classList.toggle('open'); if (ar) ar.classList.toggle('open'); } }); }); } /* ── Theme Toggle (light / dark) ─────────────────────────────── */ function getTheme() { return document.documentElement.getAttribute('data-theme') || 'light'; } function setTheme(theme) { document.documentElement.setAttribute('data-theme', theme); localStorage.setItem('AMS_THEME', theme); updateThemeUI(); if (window.lucide) lucide.createIcons(); } function toggleTheme() { setTheme(getTheme() === 'dark' ? 'light' : 'dark'); } function updateThemeUI() { const dark = getTheme() === 'dark'; const btn = document.getElementById('themeToggle'); if (btn) { btn.innerHTML = ``; btn.title = dark ? 'Switch to light theme' : 'Switch to dark theme'; } // Keep Settings → Appearance theme cards in sync if present document.querySelectorAll('[data-theme-opt]').forEach(opt => { opt.classList.toggle('selected', opt.getAttribute('data-theme-opt') === getTheme()); }); } function injectThemeToggle() { const actions = document.querySelector('.topbar-actions'); if (!actions || document.getElementById('themeToggle')) return; const btn = document.createElement('button'); btn.className = 'icon-btn'; btn.id = 'themeToggle'; btn.setAttribute('onclick', 'toggleTheme()'); actions.insertBefore(btn, actions.firstChild); updateThemeUI(); } /* ── Notif Panel ─────────────────────────────────────────────── */ function initNotifPanel() { const bell = document.getElementById('notifBell'); const panel = document.getElementById('notifPanel'); if (bell && panel) { bell.addEventListener('click', e => { e.stopPropagation(); panel.classList.toggle('open'); }); document.addEventListener('click', e => { if (!panel.contains(e.target) && e.target !== bell) panel.classList.remove('open'); }); } } /* ── Sidebar User Info ─────────────────────────────────────── */ /* ── Sidebar User Info ─────────────────────────────────────── */ const Permissions = { 'Super Admin': { viewAllAssets: true, createEditAssets: true, deleteDispose: true, assignAssets: true, transferAssets: true, viewFinancials: true, approvePR: true, raiseTickets: true, manageUsers: true, systemSettings: true }, 'Asset Manager': { viewAllAssets: true, createEditAssets: true, deleteDispose: true, assignAssets: true, transferAssets: true, viewFinancials: true, approvePR: true, raiseTickets: true, manageUsers: false, systemSettings: false }, 'IT Head': { viewAllAssets: true, createEditAssets: true, deleteDispose: false, assignAssets: true, transferAssets: true, viewFinancials: false, approvePR: false, raiseTickets: true, manageUsers: true, systemSettings: false }, 'Asset Coordinator': { viewAllAssets: true, createEditAssets: true, deleteDispose: false, assignAssets: true, transferAssets: true, viewFinancials: false, approvePR: false, raiseTickets: true, manageUsers: false, systemSettings: false }, 'Department Head': { viewAllAssets: true, createEditAssets: false, deleteDispose: false, assignAssets: true, transferAssets: true, viewFinancials: false, approvePR: true, raiseTickets: true, manageUsers: false, systemSettings: false }, 'Finance Head': { viewAllAssets: true, createEditAssets: false, deleteDispose: false, assignAssets: false, transferAssets: false, viewFinancials: true, approvePR: true, raiseTickets: true, manageUsers: false, systemSettings: false }, 'Employee': { viewAllAssets: false, createEditAssets: false, deleteDispose: false, assignAssets: false, transferAssets: false, viewFinancials: false, approvePR: false, raiseTickets: true, manageUsers: false, systemSettings: false } }; function checkPermission(perm) { if (!window.AMS || !window.AMS.currentUser) return false; const role = window.AMS.currentUser.role; const rolePerms = Permissions[role] || Permissions['Employee']; return !!rolePerms[perm]; } function populateSidebarUser() { if (!window.AMS) return; const u = AMS.currentUser; // Hook the click listener on sidebar footer or user card to open switcher const card = document.querySelector('.user-card, .sidebar-footer'); if (card) { card.style.cursor = 'pointer'; card.title = 'Click to switch simulated role'; card.addEventListener('click', (e) => { e.preventDefault(); openRoleSwitcherModal(); }); } const nameEls = document.querySelectorAll('.user-name, #sidebarUserName'); const roleEls = document.querySelectorAll('.user-role, #sidebarUserRole'); const avEls = document.querySelectorAll('.user-av, #sidebarUserAv'); nameEls.forEach(el => el.textContent = u.name); roleEls.forEach(el => el.textContent = u.role); avEls.forEach(el => el.textContent = u.avatar); } function openRoleSwitcherModal() { let modal = document.getElementById('dynRoleModal'); if (!modal) { modal = document.createElement('div'); modal.id = 'dynRoleModal'; modal.className = 'modal-overlay'; modal.style.zIndex = '9999'; modal.innerHTML = ` `; document.body.appendChild(modal); modal.addEventListener('click', e => { if (e.target === modal) closeModal('dynRoleModal'); }); } const list = document.getElementById('dynRoleList'); list.innerHTML = AMS.users.map(u => ` `).join(''); openModal('dynRoleModal'); } function switchSimUser(userId) { const u = AMS.users.find(x => x.id === userId); if (u) { AMS.currentUser = u; AMS.save(); closeModal('dynRoleModal'); showToast('Role Switched', `Logged in as ${u.name} (${u.role})`, 'success'); setTimeout(() => { const page = window.location.pathname.split('/').pop() || 'dashboard.html'; if (u.role === 'Employee' && ['settings.html', 'users.html', 'asset-create.html'].some(p => page.includes(p))) { window.location.href = 'dashboard.html'; } else { window.location.reload(); } }, 800); } } function applyRolePermissions() { if (!window.AMS || !window.AMS.currentUser) return; const u = AMS.currentUser; const page = window.location.pathname.split('/').pop() || 'dashboard.html'; // Page access control if (page.includes('settings.html') && !checkPermission('systemSettings')) { showAccessDenied(); return; } if (page.includes('users.html') && !checkPermission('manageUsers')) { showAccessDenied(); return; } if (page.includes('asset-create.html') && !checkPermission('createEditAssets')) { showAccessDenied(); return; } // Hide sidebar sections/links document.querySelectorAll('.sidebar-nav .nav-item').forEach(item => { const href = item.getAttribute('href') || ''; if (href.includes('settings.html') && !checkPermission('systemSettings')) { item.style.display = 'none'; } if (href.includes('users.html') && !checkPermission('manageUsers')) { item.style.display = 'none'; } if (href.includes('asset-create.html') && !checkPermission('createEditAssets')) { item.style.display = 'none'; } if (u.role === 'Employee') { if (href.includes('reports.html')) { item.style.display = 'none'; } } }); // Disable / Hide edit operations if (!checkPermission('createEditAssets')) { document.querySelectorAll('.btn-primary[href="asset-create.html"], a[href="asset-create.html"], button[onclick*="openModal(\'editModal\')"], button[onclick*="saveEdit"]').forEach(el => { el.style.display = 'none'; }); } if (!checkPermission('assignAssets')) { document.querySelectorAll('button[onclick*="openAssignModal"], button[onclick*="openModal(\'assignModal\')"], button[title="Assign"]').forEach(el => { el.style.display = 'none'; }); } if (!checkPermission('transferAssets')) { document.querySelectorAll('button[onclick*="openTransferModal"], button[onclick*="openModal(\'transferModal\')"], button[title="Transfer"]').forEach(el => { el.style.display = 'none'; }); } if (!checkPermission('deleteDispose')) { document.querySelectorAll('button[onclick*="confirmDispose"], button[onclick*="bulkAction(\'dispose\')"], button[title="Dispose"]').forEach(el => { el.style.display = 'none'; }); } } function showAccessDenied() { document.body.innerHTML = `
🚫

Access Denied

You do not have the required permissions to access this module. Current Role: ${AMS.currentUser.role}

`; } /* ── Topbar Notifications Badge ─────────────────────────────── */ function populateNotifBadge() { if (!window.AMS) return; const unread = AMS.notifications.filter(n => !n.read).length; const dot = document.querySelector('#notifBell .badge-dot'); if (dot) dot.style.display = unread > 0 ? '' : 'none'; } /* ── Render Notifications Panel ─────────────────────────────── */ function renderNotifPanel() { if (!window.AMS) return; const list = document.getElementById('notifList'); if (!list) return; list.innerHTML = AMS.notifications.map(n => `
${n.read ? '' : '
'}
${n.text}
${n.time}
`).join(''); } function markNotifRead(id) { if (window.AMS) { const n = AMS.notifications.find(x => x.id === id); if (n) n.read = true; populateNotifBadge(); renderNotifPanel(); } } /* ── Pagination ─────────────────────────────────────────────── */ function renderPagination(containerId, total, perPage=10, current=1, onChange) { const c = document.getElementById(containerId); if (!c) return; const pages = Math.ceil(total / perPage); let html = ``; for (let i=1;i<=pages;i++) { html += ``; } html += ``; c.innerHTML = html; } /* ── Download CSV ─────────────────────────────────────────── */ function downloadCSV(data, filename='export.csv') { if (!data.length) return; const headers = Object.keys(data[0]).join(','); const rows = data.map(r => Object.values(r).map(v => `"${String(v).replace(/"/g,'""')}"`).join(',')); const csv = [headers, ...rows].join('\n'); const a = document.createElement('a'); a.href = URL.createObjectURL(new Blob([csv],{type:'text/csv'})); a.download = filename; a.click(); } /* ── Generate QR SVG Pattern ───────────────────────────────── */ function generateQRPattern(seed) { let hash = 0; for (let i=0; i { hash = (hash * 1103515245 + 12345) | 0; return Math.abs(hash) % n; }; const cells = []; for (let i=0;i<49;i++) cells.push(rand(2)); const corners = [0,1,2,3,4,5,6,7,14,21,28,35,42,43,44,45,46,47,48]; corners.forEach(i => cells[i] = i%7<1 || i%7>5 || Math.floor(i/7)<1 || Math.floor(i/7)>5 ? 1 : 0); return cells; } /* ── DOMContentLoaded Init ─────────────────────────────────── */ document.addEventListener('DOMContentLoaded', () => { setActiveNav(); initTabs(); initDropdowns(); initTrees(); initNotifPanel(); injectThemeToggle(); populateSidebarUser(); populateNotifBadge(); renderNotifPanel(); applyRolePermissions(); if (window.lucide) { lucide.createIcons(); } // Auto close modal on overlay click document.querySelectorAll('.modal-overlay').forEach(ov => { ov.addEventListener('click', e => { if (e.target === ov) { ov.classList.remove('open'); document.body.style.overflow = ''; } }); }); // Auto close modal on .modal-close document.querySelectorAll('.modal-close').forEach(btn => { btn.addEventListener('click', () => { const ov = btn.closest('.modal-overlay'); if (ov) { ov.classList.remove('open'); document.body.style.overflow = ''; } }); }); });