(function () { const body = document.body; const menuButton = document.getElementById('mobileMenuBtn'); const sidebarBackdrop = document.getElementById('sidebarBackdrop'); function initNotiflix() { if (!window.Notiflix) return; window.Notiflix.Notify.init({ width: '320px', position: 'right-top', distance: '16px', timeout: 2600, borderRadius: '8px', fontFamily: 'Inter, sans-serif', fontSize: '13px', messageMaxLength: 180, clickToClose: true, pauseOnHover: true, cssAnimationStyle: 'from-right', useIcon: true, zindex: 5000, success: { background: '#067647', textColor: '#ffffff' }, failure: { background: '#b42318', textColor: '#ffffff' }, warning: { background: '#b54708', textColor: '#ffffff' }, info: { background: '#3755c3', textColor: '#ffffff' } }); window.Notiflix.Confirm.init({ width: '360px', borderRadius: '8px', fontFamily: 'Inter, sans-serif', titleColor: '#111827', titleFontSize: '16px', messageColor: '#475569', messageFontSize: '13px', okButtonBackground: '#3755c3', okButtonColor: '#ffffff', cancelButtonBackground: '#e2e8f0', cancelButtonColor: '#334155', 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' }; } 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', 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, '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`); 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)); }); } initNotiflix(); initFileDropzones(); initRegistrationUniqueChecks(); initProfileForms(); initEditAppForms(); 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); }); })();