(function () { const body = document.body; const menuButton = document.getElementById('mobileMenuBtn'); const sidebarBackdrop = document.getElementById('sidebarBackdrop'); const FLUENT_ICON_FILES = Object.freeze({ add: 'add_20_regular.svg', add_box: 'box_24_regular.svg', apps: 'apps_24_regular.svg', archive: 'archive_24_regular.svg', attach_file: 'attach_20_regular.svg', campaign: 'megaphone_20_regular.svg', check_circle: 'checkmark_circle_20_filled.svg', close: 'dismiss_20_regular.svg', dashboard: 'grid_24_regular.svg', database_off: 'database_warning_20_regular.svg', delete: 'delete_20_regular.svg', deployed_code: 'cube_24_regular.svg', description: 'document_24_regular.svg', download: 'arrow_download_20_regular.svg', draft: 'document_24_regular.svg', edit: 'edit_20_regular.svg', error: 'error_circle_20_filled.svg', forward_to_inbox: 'mail_arrow_forward_20_regular.svg', group: 'people_24_regular.svg', inventory_2: 'box_24_regular.svg', library_books: 'library_24_regular.svg', link_off: 'link_dismiss_20_regular.svg', login: 'arrow_enter_20_regular.svg', logout: 'sign_out_20_regular.svg', mark_email_unread: 'mail_unread_24_regular.svg', memory: 'memory_16_regular.svg', menu: 'navigation_20_regular.svg', notifications: 'alert_20_regular.svg', notifications_none: 'alert_off_20_regular.svg', note_add: 'document_add_24_regular.svg', person_add: 'person_add_20_regular.svg', precision_manufacturing: 'bot_24_filled.svg', save: 'save_20_regular.svg', search_off: 'search_24_regular.svg', send: 'send_20_regular.svg', stars: 'star_20_filled.svg', swap_horiz: 'arrow_swap_20_regular.svg', sync: 'arrow_sync_20_regular.svg', upgrade: 'arrow_up_20_regular.svg', upload: 'arrow_upload_20_regular.svg', upload_file: 'document_arrow_up_24_regular.svg', visibility: 'eye_20_regular.svg', warning: 'warning_20_filled.svg' }); function applyFluentIcon(element, iconName) { if (!element) return; const fileName = FLUENT_ICON_FILES[iconName] || 'question_20_regular.svg'; element.classList.remove('material-symbols-outlined'); element.classList.add('fluent-icon'); element.style.setProperty('--fluent-icon-url', `url("/vendor/fluent-icons/${fileName}")`); element.setAttribute('aria-hidden', 'true'); element.textContent = ''; } function hydrateFluentIcons(root) { root.querySelectorAll('.material-symbols-outlined').forEach((element) => { applyFluentIcon(element, element.textContent.trim()); }); } function initNotiflix() { if (!window.Notiflix) return; window.Notiflix.Notify.init({ width: '320px', position: 'right-top', distance: '16px', timeout: 2600, borderRadius: '4px', fontFamily: 'Montserrat Variable, Montserrat, sans-serif', fontSize: '13px', messageMaxLength: 180, clickToClose: true, pauseOnHover: true, cssAnimationStyle: 'from-right', useIcon: true, zindex: 5000, success: { background: '#2e7d32', textColor: '#ffffff' }, failure: { background: '#c62828', textColor: '#ffffff' }, warning: { background: '#9a5b00', textColor: '#ffffff' }, info: { background: '#223771', textColor: '#ffffff' } }); window.Notiflix.Confirm.init({ width: '360px', borderRadius: '4px', fontFamily: 'Montserrat Variable, Montserrat, sans-serif', titleColor: '#161824', titleFontSize: '16px', messageColor: '#565b70', messageFontSize: '13px', okButtonBackground: '#223771', okButtonColor: '#ffffff', cancelButtonBackground: '#eeedf6', cancelButtonColor: '#3c4054', backOverlayColor: 'rgba(15, 23, 42, 0.42)', zindex: 5001, cssAnimationStyle: 'zoom' }); } function notify(type, message) { if (!message) return; if (!window.Notiflix) { console.info(message); return; } const Notify = window.Notiflix.Notify; if (type === 'success') { Notify.success(message); return; } if (type === 'failure') { Notify.failure(message); return; } if (type === 'warning') { Notify.warning(message); return; } Notify.info(message); } function confirmAction(message, onConfirm) { if (!window.Notiflix) { if (window.confirm(message || 'Xác nhận thao tác?') && typeof onConfirm === 'function') { onConfirm(); } return; } window.Notiflix.Confirm.show( 'Xác nhận thao tác', message, 'Xác nhận', 'Hủy', () => { if (typeof onConfirm === 'function') { onConfirm(); return; } notify('success', 'Đã xác nhận thao tác'); }, () => { notify('info', 'Đã hủy thao tác'); } ); } function setMobileNav(open) { body.classList.toggle('mobile-nav-open', open); if (menuButton) { menuButton.setAttribute('aria-expanded', open ? 'true' : 'false'); } } function openModal(id) { const modal = document.getElementById(id); if (!modal) return; modal.classList.add('open'); const focusTarget = modal.querySelector('input, select, textarea, button'); if (focusTarget) { window.setTimeout(() => focusTarget.focus(), 60); } } function closeModal(modal) { if (!modal) return; modal.classList.remove('open'); } function applyTableFilters(tableId) { const table = document.getElementById(tableId); if (!table) return; const searchInput = document.querySelector(`[data-table-search="${tableId}"]`); const query = searchInput ? searchInput.value.trim().toLowerCase() : ''; const selects = document.querySelectorAll(`[data-filter-select][data-filter-table="${tableId}"]`); table.querySelectorAll('tbody tr').forEach((row) => { const matchesSearch = !query || (row.dataset.search || '').includes(query); let matchesSelects = true; selects.forEach((select) => { const column = select.dataset.filterColumn; const value = select.value; if (value && row.dataset[column] !== value) { matchesSelects = false; } }); row.hidden = !(matchesSearch && matchesSelects); }); } function formatFileSize(bytes) { if (!Number.isFinite(bytes) || bytes <= 0) return '0 B'; const units = ['B', 'KB', 'MB', 'GB']; const index = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1); const value = bytes / Math.pow(1024, index); return `${value.toFixed(value >= 10 || index === 0 ? 0 : 1)} ${units[index]}`; } function isAllowedPackageFile(file) { if (!file) return false; const name = file.name.toLowerCase(); const allowedExtensions = ['.deb', '.tar', '.tar.gz', '.tgz', '.zip', '.gz']; return allowedExtensions.some((extension) => name.endsWith(extension)); } function renderSelectedFile(zone, file) { const preview = zone.querySelector('[data-file-preview]'); const fileName = zone.querySelector('[data-file-name]'); const fileMeta = zone.querySelector('[data-file-meta]'); if (!preview || !fileName || !fileMeta) return; if (!file) { preview.hidden = true; fileName.textContent = 'Chưa chọn file'; fileMeta.textContent = ''; zone.classList.remove('has-file'); return; } fileName.textContent = file.name; fileMeta.textContent = `${formatFileSize(file.size)} • ${file.type || 'package file'}`; preview.hidden = false; zone.classList.add('has-file'); notify('success', `Đã chọn file: ${file.name}`); } function setInputFiles(input, files) { try { input.files = files; } catch (error) { console.info('Browser does not allow assigning dropped files to input.files.', error); } } function handlePackageFiles(zone, files) { const input = zone.querySelector('[data-file-input]'); const file = files && files[0]; if (!input || !file) return; if (!isAllowedPackageFile(file)) { notify('warning', 'File chưa đúng định dạng. Vui lòng chọn .deb, .tar, .tgz, .zip hoặc .gz.'); return; } setInputFiles(input, files); renderSelectedFile(zone, file); } function initFileDropzones() { document.querySelectorAll('[data-file-dropzone]').forEach((zone) => { const input = zone.querySelector('[data-file-input]'); const browseButton = zone.querySelector('[data-file-browse]'); const clearButton = zone.querySelector('[data-file-clear]'); if (!input) return; if (browseButton) { browseButton.addEventListener('click', () => input.click()); } if (clearButton) { clearButton.addEventListener('click', () => { input.value = ''; renderSelectedFile(zone, null); notify('info', 'Đã bỏ file đã chọn'); }); } input.addEventListener('change', () => { handlePackageFiles(zone, input.files); }); ['dragenter', 'dragover'].forEach((eventName) => { zone.addEventListener(eventName, (event) => { event.preventDefault(); event.stopPropagation(); zone.classList.add('dragover'); }); }); ['dragleave', 'drop'].forEach((eventName) => { zone.addEventListener(eventName, (event) => { event.preventDefault(); event.stopPropagation(); zone.classList.remove('dragover'); }); }); zone.addEventListener('drop', (event) => { handlePackageFiles(zone, event.dataTransfer.files); }); }); } function updateRegisterSubmit(form) { const submitButton = form.querySelector('[data-register-submit]'); if (!submitButton) return; const uniqueInputs = form.querySelectorAll('[data-unique-check]'); const isBlocked = Array.from(uniqueInputs).some((input) => ( input.dataset.uniqueStatus === 'error' || input.dataset.uniqueStatus === 'checking' )); submitButton.disabled = isBlocked; } function setUniqueState(input, state, message) { const field = input.closest('.form-field'); const form = input.closest('[data-register-form]'); const feedback = form ? form.querySelector(`[data-unique-feedback="${input.dataset.uniqueCheck}"]`) : null; input.dataset.uniqueStatus = state; if (field) { field.classList.toggle('has-error', state === 'error'); field.classList.toggle('has-success', state === 'success'); } if (feedback) { feedback.textContent = message || ''; feedback.style.display = message ? 'block' : ''; } input.setCustomValidity(state === 'error' ? message : ''); if (form) { updateRegisterSubmit(form); } } function checkUniqueInput(input) { const field = input.dataset.uniqueCheck; const value = input.value.trim(); if (!field || !value) { setUniqueState(input, 'idle', ''); return; } if (field === 'email' && !input.validity.valid) { setUniqueState(input, 'idle', ''); return; } const requestId = String(Date.now()); input.dataset.uniqueRequestId = requestId; setUniqueState(input, 'checking', 'Đang kiểm tra...'); fetch(`/register/check?field=${encodeURIComponent(field)}&value=${encodeURIComponent(value)}`, { headers: { Accept: 'application/json' } }) .then((response) => response.ok ? response.json() : Promise.reject(new Error('Cannot check field'))) .then((data) => { if (input.dataset.uniqueRequestId !== requestId) return; setUniqueState( input, data.available ? 'success' : 'error', data.message || (data.available ? 'Có thể sử dụng.' : 'Đã tồn tại.') ); }) .catch((error) => { console.info('Cannot check registration field:', error); if (input.dataset.uniqueRequestId === requestId) { setUniqueState(input, 'idle', ''); } }); } function initRegistrationUniqueChecks() { document.querySelectorAll('[data-register-form]').forEach((form) => { const timers = new Map(); form.querySelectorAll('[data-unique-check]').forEach((input) => { input.addEventListener('input', () => { window.clearTimeout(timers.get(input)); setUniqueState(input, 'idle', ''); timers.set(input, window.setTimeout(() => checkUniqueInput(input), 450)); }); input.addEventListener('blur', () => { window.clearTimeout(timers.get(input)); checkUniqueInput(input); }); if (input.value.trim()) { checkUniqueInput(input); } }); form.addEventListener('submit', (event) => { const blockedInput = form.querySelector('[data-unique-status="error"], [data-unique-status="checking"]'); if (!blockedInput) return; event.preventDefault(); notify('warning', blockedInput.dataset.uniqueStatus === 'checking' ? 'Đang kiểm tra username/email, vui lòng chờ một chút.' : 'Vui lòng đổi username/email đang bị trùng.'); blockedInput.focus(); }); }); } function getUserDataFromRow(row) { return { id: row.dataset.userId || '', name: row.dataset.userName || '', username: row.dataset.userUsername || '', email: row.dataset.userEmail || '', fullName: row.dataset.userFullName || '', role: row.dataset.userRole || 'User', status: row.dataset.userStatus || '', isActive: row.dataset.userActive === 'true', createdAt: row.dataset.userCreatedAt || '', updatedAt: row.dataset.userUpdatedAt || '', packageCount: row.dataset.userPackageCount || '0', applicationCount: row.dataset.userApplicationCount || '0', documentCount: row.dataset.userDocumentCount || '0' }; } function parseJsonAttribute(value, fallback) { try { return JSON.parse(value || ''); } catch (error) { return fallback; } } function getAppDataFromTrigger(trigger) { return { id: trigger.dataset.appId || '', code: trigger.dataset.appCode || '', name: trigger.dataset.appName || '', version: trigger.dataset.appVersion || '', status: trigger.dataset.appStatus || 'Draft', openUrl: trigger.dataset.appOpenUrl || '', notes: trigger.dataset.appNotes || '', packages: parseJsonAttribute(trigger.dataset.appPackages, []) }; } function findEditAppVersionSelect(form, packageId) { return Array.from(form.querySelectorAll('[data-edit-app-version]')) .find((select) => select.dataset.editAppVersion === packageId); } function setEditAppPackageEnabled(form, checkbox) { const select = findEditAppVersionSelect(form, checkbox.dataset.editAppPackage); if (select) { select.disabled = !checkbox.checked; } } function setEditAppField(form, field, value) { const input = form.querySelector(`[data-edit-app-field="${field}"]`); if (input) { input.value = value || ''; } } function openAppEdit(trigger) { const app = getAppDataFromTrigger(trigger); const form = document.getElementById('editAppForm'); if (!form || !app.id) return; form.action = `/applications/${encodeURIComponent(app.id)}/edit`; setEditAppField(form, 'appCode', app.code); setEditAppField(form, 'appName', app.name); setEditAppField(form, 'appVersion', app.version); setEditAppField(form, 'status', app.status); setEditAppField(form, 'openUrl', app.openUrl); setEditAppField(form, 'notes', app.notes); form.querySelectorAll('[data-edit-app-package]').forEach((checkbox) => { checkbox.checked = false; setEditAppPackageEnabled(form, checkbox); }); app.packages.forEach((packageItem) => { const packageId = packageItem.packageId || ''; const checkbox = Array.from(form.querySelectorAll('[data-edit-app-package]')) .find((input) => input.dataset.editAppPackage === packageId); const select = findEditAppVersionSelect(form, packageId); if (checkbox) { checkbox.checked = true; } if (select) { select.disabled = false; select.value = packageItem.selectedVersionId || ''; } }); openModal('editAppModal'); } function openPackageUpdate(packageId) { const modalId = 'updatePackageModal'; const modal = document.getElementById(modalId); const packageSelect = modal ? modal.querySelector('select[name="packageId"]') : null; if (packageSelect && packageId) { packageSelect.value = packageId; } openModal(modalId); } function setText(selector, value) { const element = document.querySelector(selector); if (element) { element.textContent = value || ''; } } function openUserDetail(row) { const user = getUserDataFromRow(row); setText('[data-user-detail="name"]', user.name); setText('[data-user-detail="username"]', user.username); setText('[data-user-detail="email"]', user.email); setText('[data-user-detail="role"]', user.role); setText('[data-user-detail="status"]', user.status); setText('[data-user-detail="createdAt"]', user.createdAt); setText('[data-user-detail="updatedAt"]', user.updatedAt || 'Chưa cập nhật'); setText( '[data-user-detail="ownedData"]', `${user.packageCount} packages, ${user.applicationCount} apps, ${user.documentCount} tài liệu` ); openModal('userDetailModal'); } function openUserEdit(row) { const user = getUserDataFromRow(row); const form = document.getElementById('editUserForm'); if (!form) return; form.action = `/users/${encodeURIComponent(user.id)}/edit`; form.querySelector('[data-edit-user-field="username"]').value = user.username; form.querySelector('[data-edit-user-field="fullName"]').value = user.fullName; form.querySelector('[data-edit-user-field="email"]').value = user.email; form.querySelector('[data-edit-user-field="role"]').value = user.role; form.querySelector('[data-edit-user-field="isActive"]').checked = user.isActive; form.querySelector('[data-edit-user-field="newPassword"]').value = ''; form.querySelector('[data-edit-user-field="confirmPassword"]').value = ''; openModal('editUserModal'); } function validateProfileForm(form, shouldNotify) { const email = form.querySelector('[data-profile-email]'); const confirmEmail = form.querySelector('[data-profile-confirm-email]'); const newPassword = form.querySelector('[data-profile-new-password]'); const confirmPassword = form.querySelector('[data-profile-confirm-password]'); const emailFeedback = form.querySelector('[data-profile-feedback="email"]'); const passwordFeedback = form.querySelector('[data-profile-feedback="password"]'); const emailMismatch = Boolean(email && confirmEmail && email.value.trim().toLowerCase() !== confirmEmail.value.trim().toLowerCase()); const passwordMismatch = Boolean(newPassword && confirmPassword && (newPassword.value || confirmPassword.value) && newPassword.value !== confirmPassword.value); if (confirmEmail) { confirmEmail.setCustomValidity(emailMismatch ? 'Confirm email mới chưa khớp.' : ''); const field = confirmEmail.closest('.form-field'); if (field) { field.classList.toggle('has-error', emailMismatch); } } if (emailFeedback) { emailFeedback.textContent = emailMismatch ? 'Confirm email mới chưa khớp.' : ''; emailFeedback.style.display = emailMismatch ? 'block' : ''; } if (confirmPassword) { confirmPassword.setCustomValidity(passwordMismatch ? 'Xác nhận mật khẩu mới chưa khớp.' : ''); const field = confirmPassword.closest('.form-field'); if (field) { field.classList.toggle('has-error', passwordMismatch); } } if (passwordFeedback) { passwordFeedback.textContent = passwordMismatch ? 'Xác nhận mật khẩu mới chưa khớp.' : ''; passwordFeedback.style.display = passwordMismatch ? 'block' : ''; } if (shouldNotify && emailMismatch) { notify('warning', 'Confirm email mới chưa khớp.'); } else if (shouldNotify && passwordMismatch) { notify('warning', 'Xác nhận mật khẩu mới chưa khớp.'); } return !emailMismatch && !passwordMismatch; } function initProfileForms() { document.querySelectorAll('[data-profile-form]').forEach((form) => { form.querySelectorAll('input').forEach((input) => { input.addEventListener('input', () => validateProfileForm(form, false)); }); form.addEventListener('submit', (event) => { if (!validateProfileForm(form, true)) { event.preventDefault(); } }); }); } function initEditAppForms() { document.querySelectorAll('#editAppForm [data-edit-app-package]').forEach((checkbox) => { const form = checkbox.closest('form'); setEditAppPackageEnabled(form, checkbox); checkbox.addEventListener('change', () => setEditAppPackageEnabled(form, checkbox)); }); } function formatNotificationTime(value) { const date = new Date(value); if (Number.isNaN(date.getTime())) return ''; const elapsedSeconds = Math.max(0, Math.floor((Date.now() - date.getTime()) / 1000)); if (elapsedSeconds < 60) return 'Vừa xong'; if (elapsedSeconds < 3600) return `${Math.floor(elapsedSeconds / 60)} phút trước`; if (elapsedSeconds < 86400) return `${Math.floor(elapsedSeconds / 3600)} giờ trước`; if (elapsedSeconds < 604800) return `${Math.floor(elapsedSeconds / 86400)} ngày trước`; return new Intl.DateTimeFormat('vi-VN', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }).format(date); } function getNotificationIcon(notification) { if (notification.isSystem) return 'campaign'; if (notification.severity === 'success') return 'check_circle'; if (notification.severity === 'warning') return 'warning'; if (notification.severity === 'error') return 'error'; if (notification.entityType === 'package') return 'inventory_2'; if (notification.entityType === 'application') return 'apps'; return 'notifications'; } async function notificationRequest(url, options = {}) { const response = await fetch(url, { cache: 'no-store', credentials: 'same-origin', ...options, headers: { Accept: 'application/json', ...(options.body ? { 'Content-Type': 'application/json' } : {}), ...(options.headers || {}) } }); const data = await response.json().catch(() => ({})); if (!response.ok) { throw new Error(data.error || 'Không thể xử lý thông báo.'); } return data; } function initNotificationCenter() { const center = document.querySelector('[data-notification-center]'); if (!center) return; const toggle = center.querySelector('[data-notification-toggle]'); const panel = center.querySelector('[data-notification-panel]'); const badge = center.querySelector('[data-notification-badge]'); const summary = center.querySelector('[data-notification-summary]'); const list = center.querySelector('[data-notification-list]'); const empty = center.querySelector('[data-notification-empty]'); const readAllButton = center.querySelector('[data-notification-read-all]'); const filterButtons = center.querySelectorAll('[data-notification-filter]'); const knownNotificationIds = new Set(); let currentFilter = 'all'; let initialized = false; let loading = false; let reloadPending = false; function setPanelOpen(open) { panel.hidden = !open; toggle.setAttribute('aria-expanded', open ? 'true' : 'false'); } function setEmptyState(title, message, visible) { const titleElement = empty.querySelector('strong'); const messageElement = empty.querySelector('span:last-child'); if (titleElement) titleElement.textContent = title; if (messageElement) messageElement.textContent = message; empty.hidden = !visible; } function updateUnreadCount(count) { const unreadCount = Math.max(0, Number(count) || 0); badge.textContent = unreadCount > 99 ? '99+' : String(unreadCount); badge.hidden = unreadCount === 0; summary.textContent = unreadCount > 0 ? `${unreadCount} thông báo chưa đọc` : 'Không có thông báo chưa đọc'; readAllButton.disabled = unreadCount === 0; } function renderNotifications(notifications) { list.replaceChildren(); setEmptyState( currentFilter === 'unread' ? 'Không có thông báo chưa đọc' : 'Chưa có thông báo', 'Các cập nhật quan trọng sẽ xuất hiện tại đây.', notifications.length === 0 ); notifications.forEach((notification) => { const item = document.createElement('button'); item.type = 'button'; item.className = `notification-item tone-${notification.severity || 'info'}${notification.isRead ? '' : ' unread'}`; item.dataset.notificationId = notification.id; const icon = document.createElement('span'); icon.className = 'notification-item-icon'; const iconGlyph = document.createElement('span'); applyFluentIcon(iconGlyph, getNotificationIcon(notification)); icon.appendChild(iconGlyph); const copy = document.createElement('span'); copy.className = 'notification-item-copy'; const title = document.createElement('strong'); title.textContent = notification.title; const message = document.createElement('span'); message.textContent = notification.message; const time = document.createElement('time'); time.dateTime = notification.createdAt || ''; time.textContent = formatNotificationTime(notification.createdAt); copy.append(title, message, time); const unreadDot = document.createElement('span'); unreadDot.className = 'notification-unread-dot'; unreadDot.setAttribute('aria-label', notification.isRead ? 'Đã đọc' : 'Chưa đọc'); item.append(icon, copy, unreadDot); item.addEventListener('click', async () => { try { if (!notification.isRead) { await notificationRequest(`/api/notifications/${encodeURIComponent(notification.id)}/read`, { method: 'POST' }); notification.isRead = true; item.classList.remove('unread'); await loadNotifications(false); } } catch (error) { notify('warning', error.message); } if ( notification.actionUrl && notification.actionUrl.startsWith('/') && !notification.actionUrl.startsWith('//') ) { window.location.assign(notification.actionUrl); } }); list.appendChild(item); }); } async function loadNotifications(announceNew = true) { if (loading) { reloadPending = true; return; } loading = true; try { const data = await notificationRequest( `/api/notifications?limit=20&unreadOnly=${currentFilter === 'unread' ? 'true' : 'false'}` ); if (!data.available) { badge.hidden = true; summary.textContent = 'Chưa kích hoạt trong database'; readAllButton.disabled = true; list.replaceChildren(); setEmptyState( 'Thông báo chưa khả dụng', 'Admin cần chạy migration 04_notifications.sql.', true ); return; } const notifications = Array.isArray(data.notifications) ? data.notifications : []; if (initialized && announceNew) { const importantNotification = notifications.find((notification) => ( !knownNotificationIds.has(notification.id) && !notification.isRead && ['warning', 'error'].includes(notification.severity) )); if (importantNotification) { notify( importantNotification.severity === 'error' ? 'failure' : 'warning', importantNotification.title ); } } notifications.forEach((notification) => knownNotificationIds.add(notification.id)); initialized = true; updateUnreadCount(data.unreadCount); renderNotifications(notifications); } catch (error) { summary.textContent = 'Không thể tải thông báo'; setEmptyState('Không thể tải thông báo', 'Vui lòng thử lại sau.', true); } finally { loading = false; if (reloadPending) { reloadPending = false; loadNotifications(false); } } } async function refreshUnreadCount() { try { const data = await notificationRequest('/api/notifications?countOnly=true'); if (!data.available) { badge.hidden = true; summary.textContent = 'Chưa kích hoạt trong database'; readAllButton.disabled = true; return; } updateUnreadCount(data.unreadCount); } catch (error) { console.info('Cannot refresh notification count:', error); } } toggle.addEventListener('click', () => { const open = panel.hidden; setPanelOpen(open); if (open) loadNotifications(false); }); readAllButton.addEventListener('click', async () => { readAllButton.disabled = true; try { await notificationRequest('/api/notifications/read-all', { method: 'POST' }); await loadNotifications(false); } catch (error) { notify('failure', error.message); } }); filterButtons.forEach((button) => { button.addEventListener('click', () => { currentFilter = button.dataset.notificationFilter; filterButtons.forEach((candidate) => { const active = candidate === button; candidate.classList.toggle('active', active); candidate.setAttribute('aria-selected', active ? 'true' : 'false'); }); loadNotifications(false); }); }); document.addEventListener('click', (event) => { if (!panel.hidden && !center.contains(event.target)) { setPanelOpen(false); } }); document.addEventListener('keydown', (event) => { if (event.key === 'Escape' && !panel.hidden) { setPanelOpen(false); toggle.focus(); } }); document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') { if (panel.hidden) refreshUnreadCount(); else loadNotifications(true); } }); document.addEventListener('notifications:refresh', () => { if (panel.hidden) refreshUnreadCount(); else loadNotifications(false); }); const broadcastButton = center.querySelector('[data-modal-open="systemNotificationModal"]'); if (broadcastButton) { broadcastButton.addEventListener('click', () => setPanelOpen(false)); } window.setInterval(() => { if (document.visibilityState !== 'visible') return; if (panel.hidden) refreshUnreadCount(); else loadNotifications(true); }, 30000); refreshUnreadCount(); } function initSystemNotificationForm() { const form = document.querySelector('[data-system-notification-form]'); if (!form) return; form.addEventListener('submit', (event) => { event.preventDefault(); if (!form.reportValidity()) return; confirmAction('Gửi thông báo này tới tất cả tài khoản đang hoạt động?', async () => { const submitButton = form.querySelector('[data-system-notification-submit]'); const formData = new FormData(form); submitButton.disabled = true; try { const result = await notificationRequest('/api/notifications/broadcasts', { method: 'POST', body: JSON.stringify({ title: formData.get('title'), severity: formData.get('severity'), message: formData.get('message'), actionUrl: formData.get('actionUrl') }) }); closeModal(form.closest('.modal-backdrop')); form.reset(); notify('success', `Đã gửi thông báo tới ${result.recipientCount || 0} người dùng.`); document.dispatchEvent(new CustomEvent('notifications:refresh')); } catch (error) { notify('failure', error.message); } finally { submitButton.disabled = false; } }); }); } hydrateFluentIcons(document); initNotiflix(); initFileDropzones(); initRegistrationUniqueChecks(); initProfileForms(); initEditAppForms(); initNotificationCenter(); initSystemNotificationForm(); if (body.dataset.notice) { notify(body.dataset.noticeType || 'info', body.dataset.notice); const url = new URL(window.location.href); if (url.searchParams.has('notice') || url.searchParams.has('noticeType')) { url.searchParams.delete('notice'); url.searchParams.delete('noticeType'); window.history.replaceState({}, document.title, `${url.pathname}${url.search}${url.hash}`); } delete body.dataset.notice; delete body.dataset.noticeType; } if (menuButton) { menuButton.addEventListener('click', () => { setMobileNav(!body.classList.contains('mobile-nav-open')); }); } if (sidebarBackdrop) { sidebarBackdrop.addEventListener('click', () => setMobileNav(false)); } document.querySelectorAll('[data-table-search]').forEach((input) => { input.addEventListener('input', () => applyTableFilters(input.dataset.tableSearch)); }); document.querySelectorAll('[data-filter-select]').forEach((select) => { select.addEventListener('change', () => applyTableFilters(select.dataset.filterTable)); }); document.addEventListener('click', (event) => { const appEditButton = event.target.closest('[data-app-edit]'); if (appEditButton) { openAppEdit(appEditButton); return; } const packageUpdateButton = event.target.closest('[data-package-update]'); if (packageUpdateButton) { openPackageUpdate(packageUpdateButton.dataset.packageUpdate); return; } const refreshButton = event.target.closest('[data-refresh-page]'); if (refreshButton) { window.location.reload(); return; } const userViewButton = event.target.closest('[data-user-view]'); if (userViewButton) { const row = userViewButton.closest('tr'); if (row) { openUserDetail(row); } return; } const userEditButton = event.target.closest('[data-user-edit]'); if (userEditButton) { const row = userEditButton.closest('tr'); if (row) { openUserEdit(row); } return; } const modalOpenButton = event.target.closest('[data-modal-open]'); if (modalOpenButton) { openModal(modalOpenButton.dataset.modalOpen); return; } const modalCloseButton = event.target.closest('[data-modal-close]'); if (modalCloseButton) { closeModal(modalCloseButton.closest('.modal-backdrop')); } const modalBackdrop = event.target.classList.contains('modal-backdrop') ? event.target : null; if (modalBackdrop) { closeModal(modalBackdrop); } const toastButton = event.target.closest('[data-toast]'); if (toastButton) { notify(toastButton.dataset.toastType || 'info', toastButton.dataset.toast); } const confirmButton = event.target.closest('[data-confirm]'); if (confirmButton) { confirmAction(confirmButton.dataset.confirm); } }); document.addEventListener('submit', (event) => { const form = event.target.closest('form[data-confirm-submit]'); if (!form || form.dataset.confirmed === 'true') return; event.preventDefault(); confirmAction(form.dataset.confirmSubmit, () => { form.dataset.confirmed = 'true'; form.submit(); }); }); document.addEventListener('keydown', (event) => { if (event.key !== 'Escape') return; const openModalNode = document.querySelector('.modal-backdrop.open'); if (openModalNode) { closeModal(openModalNode); return; } setMobileNav(false); }); })();