- Login page with animated floating stat cards - Executive dashboard with Chart.js KPIs and activity feed - Full asset registry (list, search, filter, bulk actions, QR) - Asset detail page with 6 tabs (financial, maintenance, history…) - 3-step asset creation wizard with category-specific fields - Inventory: stock overview, GRN, location tree, physical audit - Procurement: PR → approval → PO → GRN → asset lifecycle - Maintenance: Kanban board, PM schedule, AMC contracts - Reports: depreciation schedule, utilization, compliance gauge - User management: roles, permission matrix, session control - Settings: 8 config sections (org, depreciation, security, integrations) - Premium dark UI with CSS variables, Chart.js 4, toast system
276 lines
12 KiB
JavaScript
276 lines
12 KiB
JavaScript
// ================================================================
|
||
// AMS — Core Application Logic | app.js
|
||
// ================================================================
|
||
|
||
/* ── Active Nav ─────────────────────────────────────────────────── */
|
||
function setActiveNav() {
|
||
const page = window.location.pathname.split('/').pop() || 'dashboard.html';
|
||
document.querySelectorAll('.nav-item').forEach(el => {
|
||
const href = el.getAttribute('href') || '';
|
||
if (href && page === href) el.classList.add('active');
|
||
else el.classList.remove('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'); }
|
||
});
|
||
});
|
||
}
|
||
|
||
/* ── 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 ─────────────────────────────────────── */
|
||
function populateSidebarUser() {
|
||
if (!window.AMS) return;
|
||
const u = AMS.currentUser;
|
||
const nameEl = document.getElementById('sidebarUserName');
|
||
const roleEl = document.getElementById('sidebarUserRole');
|
||
const avEl = document.getElementById('sidebarUserAv');
|
||
if (nameEl) nameEl.textContent = u.name;
|
||
if (roleEl) roleEl.textContent = u.role;
|
||
if (avEl) avEl.textContent = u.avatar;
|
||
}
|
||
|
||
/* ── 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) {
|
||
// Deterministic pseudo-QR pattern based on seed string
|
||
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));
|
||
// Force finder pattern corners
|
||
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();
|
||
populateSidebarUser();
|
||
populateNotifBadge();
|
||
renderNotifPanel();
|
||
|
||
// 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 = ''; }
|
||
});
|
||
});
|
||
});
|