527 lines
24 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ================================================================
// 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, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
// 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, '&quot;').replace(/</g, '\\u003C');
}
/* ── Active Nav ─────────────────────────────────────────────────── */
function setActiveNav() {
const page = window.location.pathname.split('/').pop() || 'dashboard.html';
const active = (window.NAV_ACTIVE_ALIAS && window.NAV_ACTIVE_ALIAS[page]) || page;
document.querySelectorAll('.nav-item').forEach(el => {
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 = `<div class="toast-icon">${icons[type]}</div>
<div class="toast-msg"><div class="toast-title">${title}</div>${text?`<div class="toast-text">${text}</div>`:''}</div>
<button onclick="this.parentElement.remove()" style="background:none;border:none;color:var(--text-muted);cursor:pointer;font-size:18px;padding:0 2px;margin-left:8px;line-height:1">×</button>`;
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 = `<i data-lucide="${dark ? 'sun' : 'moon'}"></i>`;
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 = `
<div class="modal" style="max-width:400px; background:var(--bg-elevated); border:1px solid var(--border-strong); border-radius:14px; box-shadow:var(--shadow-lg)">
<div class="modal-header" style="display:flex; justify-content:space-between; align-items:center; padding:16px; border-bottom:1px solid var(--border)">
<span class="modal-title" style="font-weight:700; color:var(--text-primary)">🔐 Switch User Role</span>
<button class="modal-close" onclick="closeModal('dynRoleModal')" style="background:none; border:none; color:var(--text-muted); cursor:pointer; font-size:16px">✕</button>
</div>
<div class="modal-body" style="padding:16px">
<div style="font-size:12.5px; color:var(--text-muted); margin-bottom:16px">
Select a simulated role to test the access control permissions matrix.
</div>
<div style="display:flex; flex-direction:column; gap:8px; max-height:300px; overflow-y:auto" id="dynRoleList">
</div>
</div>
<div class="modal-footer" style="padding:12px 16px; border-top:1px solid var(--border)">
<button class="btn btn-secondary w-full" onclick="closeModal('dynRoleModal')">Close</button>
</div>
</div>
`;
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 => `
<button class="btn btn-secondary" onclick="switchSimUser('${u.id}')" style="display:flex; align-items:center; justify-content:space-between; padding:10px 14px; text-align:left; width:100%; border:1px solid ${AMS.currentUser.id === u.id ? 'var(--primary)' : 'var(--border)'}; background:${AMS.currentUser.id === u.id ? 'var(--primary-glow)' : 'var(--bg-surface)'}">
<div style="display:flex; align-items:center; gap:10px">
<div style="width:28px; height:28px; border-radius:50%; background:${u.color}22; color:${u.color}; display:flex; align-items:center; justify-content:center; font-size:11px; font-weight:700">${u.av}</div>
<div>
<div style="font-weight:600; font-size:12.5px; color:var(--text-primary)">${u.name}</div>
<div style="font-size:10.5px; color:var(--text-muted)">${u.role} · ${u.dept}</div>
</div>
</div>
${AMS.currentUser.id === u.id ? '<span style="color:var(--primary-light); font-size:11.5px; font-weight:600">✓ Active</span>' : ''}
</button>
`).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 = `
<div style="height:100vh; display:flex; flex-direction:column; align-items:center; justify-content:center; background:#0f172a; color:#fff; font-family:'Inter', sans-serif; text-align:center; padding:20px">
<div style="font-size:72px; margin-bottom:20px">🚫</div>
<h1 style="font-size:28px; font-weight:800; margin-bottom:8px">Access Denied</h1>
<p style="color:#94a3b8; max-width:400px; margin-bottom:24px">You do not have the required permissions to access this module. Current Role: <strong>${AMS.currentUser.role}</strong></p>
<button class="btn btn-primary" onclick="window.location.href='dashboard.html'">Go to Dashboard</button>
</div>
`;
}
/* ── 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 => `
<div class="notif-item" onclick="markNotifRead('${n.id}')">
${n.read ? '' : '<div class="notif-dot"></div>'}
<div style="flex:1">
<div class="notif-text">${n.text}</div>
<div class="notif-time">${n.time}</div>
</div>
</div>`).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 = `<button class="page-btn" onclick="(${onChange})(${Math.max(1,current-1)})" ${current===1?'disabled':''}></button>`;
for (let i=1;i<=pages;i++) {
html += `<button class="page-btn${i===current?' active':''}" onclick="(${onChange})(${i})">${i}</button>`;
}
html += `<button class="page-btn" onclick="(${onChange})(${Math.min(pages,current+1)})" ${current===pages?'disabled':''}></button>`;
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<seed.length; i++) hash = (hash * 31 + seed.charCodeAt(i)) | 0;
const rand = (n) => { 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 = ''; }
});
});
});