527 lines
24 KiB
JavaScript
527 lines
24 KiB
JavaScript
// ================================================================
|
||
// 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, '"')
|
||
.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(/</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 = ''; }
|
||
});
|
||
});
|
||
});
|