/** Roles & Users admin page — role hierarchy, permission matrix, user CRUD. */ let allRoles = []; let allUsers = []; let userPage = 0; const USERS_PER_PAGE = 10; function roleModuleActionLabel(action) { return { canView: 'View', canCreate: 'Create', canEdit: 'Edit', canDelete: 'Delete' }[action] || action; } // ---------------------------------------------------------------- Roles --- async function loadRoles() { const el = document.getElementById('role-list'); try { const data = await ddaApi('GET', '/api/dda/rbac/roles'); allRoles = data.roles || []; renderRoleList(); populateUserRoleSelect(); } catch (err) { el.innerHTML = `

Could not load roles: ${escapeHtml(err.message || 'error')}

`; } } function renderRoleList() { const el = document.getElementById('role-list'); if (!allRoles.length) { el.innerHTML = '

No roles yet.

'; return; } el.innerHTML = allRoles.map((r) => `
${escapeHtml(r.name)}${r.isSystem ? 'Built-in' : ''} ${r.isActive === false ? 'Inactive' : 'Active'}
${escapeHtml(r.description || '')}
`).join(''); el.querySelectorAll('[data-perm-role]').forEach((btn) => { btn.addEventListener('click', () => openPermMatrix(Number(btn.dataset.permRole))); }); el.querySelectorAll('[data-edit-role]').forEach((btn) => { btn.addEventListener('click', () => openRoleModal(Number(btn.dataset.editRole))); }); } function openRoleModal(roleId) { const modal = document.getElementById('role-modal'); const title = document.getElementById('role-modal-title'); const role = allRoles.find((r) => r.id === roleId); document.getElementById('role-id').value = role ? role.id : ''; document.getElementById('role-name').value = role ? role.name : ''; document.getElementById('role-description').value = role ? role.description || '' : ''; document.getElementById('role-rank').value = role ? role.rank : 0; document.getElementById('role-active').checked = role ? role.isActive !== false : true; title.textContent = role ? 'Edit Role' : 'New Role'; modal.classList.remove('hidden'); } document.getElementById('btn-new-role')?.addEventListener('click', () => openRoleModal(null)); document.getElementById('role-modal-close')?.addEventListener('click', () => document.getElementById('role-modal').classList.add('hidden')); document.getElementById('role-modal')?.addEventListener('click', (e) => { if (e.target.id === 'role-modal') document.getElementById('role-modal').classList.add('hidden'); }); document.getElementById('role-form')?.addEventListener('submit', async (e) => { e.preventDefault(); hideDdaError(); const id = document.getElementById('role-id').value; const body = { name: document.getElementById('role-name').value.trim(), description: document.getElementById('role-description').value.trim(), rank: Number(document.getElementById('role-rank').value) || 0, isActive: document.getElementById('role-active').checked, }; try { if (id) { await ddaApi('PUT', `/api/dda/rbac/roles/${id}`, { body: JSON.stringify(body) }); } else { await ddaApi('POST', '/api/dda/rbac/roles', { body: JSON.stringify(body) }); } document.getElementById('role-modal').classList.add('hidden'); showDdaSuccess('Role saved.'); await loadRoles(); } catch (err) { showDdaError(err.message); } }); // ---------------------------------------------------- Permission matrix --- async function openPermMatrix(roleId) { const modal = document.getElementById('perm-modal'); const wrap = document.getElementById('perm-matrix-wrap'); const title = document.getElementById('perm-modal-title'); wrap.innerHTML = '

Loading…

'; modal.classList.remove('hidden'); modal.dataset.roleId = roleId; try { const data = await ddaApi('GET', `/api/dda/rbac/roles/${roleId}/permissions`); title.textContent = `Permission Matrix — ${data.role.name}`; const actions = ['canView', 'canCreate', 'canEdit', 'canDelete']; wrap.innerHTML = ` ${actions.map((a) => ``).join('')} ${data.permissions.map((p) => ` ${actions.map((a) => ``).join('')} `).join('')}
Module${roleModuleActionLabel(a)}
${escapeHtml(p.moduleName)}
`; } catch (err) { wrap.innerHTML = `

Could not load permissions: ${escapeHtml(err.message || 'error')}

`; } } document.getElementById('perm-modal-close')?.addEventListener('click', () => document.getElementById('perm-modal').classList.add('hidden')); document.getElementById('perm-modal')?.addEventListener('click', (e) => { if (e.target.id === 'perm-modal') document.getElementById('perm-modal').classList.add('hidden'); }); function setAllPermCheckboxes(checked) { document.querySelectorAll('#perm-matrix-wrap input[type="checkbox"]').forEach((cb) => { cb.checked = checked; }); } document.getElementById('btn-grant-all')?.addEventListener('click', () => setAllPermCheckboxes(true)); document.getElementById('btn-revoke-all')?.addEventListener('click', () => setAllPermCheckboxes(false)); document.getElementById('btn-save-perms')?.addEventListener('click', async () => { const modal = document.getElementById('perm-modal'); const roleId = modal.dataset.roleId; const rows = modal.querySelectorAll('.dda-perm-matrix tbody tr'); const permissions = [...rows].map((row) => { const get = (a) => row.querySelector(`input[data-action="${a}"]`)?.checked || false; return { moduleId: Number(row.dataset.moduleId), canView: get('canView'), canCreate: get('canCreate'), canEdit: get('canEdit'), canDelete: get('canDelete'), }; }); try { await ddaApi('PUT', `/api/dda/rbac/roles/${roleId}/permissions`, { body: JSON.stringify({ permissions }) }); showDdaSuccess('Permissions saved.'); modal.classList.add('hidden'); } catch (err) { showDdaError(err.message); } }); // ---------------------------------------------------------------- Users --- function populateUserRoleSelect() { const sel = document.getElementById('user-role-select'); if (!sel) return; sel.innerHTML = allRoles.map((r) => ``).join(''); } async function loadUsers() { const el = document.getElementById('user-table-wrap'); try { const data = await ddaApi('GET', '/api/dda/rbac/users'); allUsers = data.users || []; userPage = 0; applyUserFilter(); } catch (err) { el.innerHTML = `

Could not load users: ${escapeHtml(err.message || 'error')}

`; } } function applyUserFilter() { const q = (document.getElementById('user-filter')?.value || '').trim().toLowerCase(); const rows = q ? allUsers.filter((u) => u.email.toLowerCase().includes(q) || (u.fullName || '').toLowerCase().includes(q) || (u.phone || '').includes(q)) : allUsers; renderUserTable(rows); } function renderUserTable(rows) { const el = document.getElementById('user-table-wrap'); if (!allUsers.length) { el.innerHTML = '

No users yet.

'; document.getElementById('user-pagination')?.replaceChildren(); return; } if (!rows.length) { el.innerHTML = '

No users match your search.

'; document.getElementById('user-pagination')?.replaceChildren(); return; } const totalPages = Math.max(1, Math.ceil(rows.length / USERS_PER_PAGE)); userPage = Math.max(0, Math.min(userPage, totalPages - 1)); const start = userPage * USERS_PER_PAGE; const pageRows = rows.slice(start, start + USERS_PER_PAGE); el.innerHTML = ` ${pageRows.map((u) => ` `).join('')}
NameEmailMobileRoleActions
${escapeHtml(u.fullName || '—')} ${escapeHtml(u.email)} ${escapeHtml(u.phone || '—')} ${escapeHtml(u.role || '—')}
`; el.querySelectorAll('[data-edit-user]').forEach((btn) => { btn.addEventListener('click', () => openUserModal(Number(btn.dataset.editUser))); }); el.querySelectorAll('[data-del-user]').forEach((btn) => { btn.addEventListener('click', () => deleteUser(Number(btn.dataset.delUser))); }); if (typeof renderPaginationControls === 'function') { renderPaginationControls(document.getElementById('user-pagination'), userPage, totalPages, (p) => { userPage = p; renderUserTable(rows); }); } } function openUserModal(userId) { const modal = document.getElementById('user-modal'); const title = document.getElementById('user-modal-title'); const countrySelect = document.getElementById('user-country-code'); if (typeof window.populateCountrySelect === 'function') { window.populateCountrySelect(countrySelect, '+91'); } const user = allUsers.find((u) => u.id === userId); document.getElementById('user-id').value = user ? user.id : ''; document.getElementById('user-email').value = user ? user.email : ''; document.getElementById('user-email').disabled = !!user; document.getElementById('user-full-name').value = user ? user.fullName || '' : ''; const parts = typeof window.splitPhoneE164 === 'function' ? window.splitPhoneE164(user ? user.phone || '' : '') : { dial: '+91', local: (user && user.phone) || '' }; if (countrySelect) countrySelect.value = parts.dial; document.getElementById('user-phone').value = parts.local; document.getElementById('user-role-select').value = user ? user.roleId : (allRoles[0]?.id || ''); document.getElementById('user-password').value = ''; document.getElementById('user-password').required = !user; document.getElementById('user-password-hint').textContent = user ? '(leave blank to keep current password)' : '(min 8 characters)'; title.textContent = user ? 'Edit User' : 'New User'; modal.classList.remove('hidden'); } document.getElementById('btn-new-user')?.addEventListener('click', () => openUserModal(null)); document.getElementById('user-modal-close')?.addEventListener('click', () => document.getElementById('user-modal').classList.add('hidden')); document.getElementById('user-modal')?.addEventListener('click', (e) => { if (e.target.id === 'user-modal') document.getElementById('user-modal').classList.add('hidden'); }); document.getElementById('user-filter')?.addEventListener('input', applyUserFilter); document.getElementById('user-form')?.addEventListener('submit', async (e) => { e.preventDefault(); hideDdaError(); const id = document.getElementById('user-id').value; const password = document.getElementById('user-password').value; try { const countryCode = document.getElementById('user-country-code')?.value || '+91'; const phone = document.getElementById('user-phone').value.replace(/\D/g, ''); if (id) { const body = { fullName: document.getElementById('user-full-name').value.trim(), phone, countryCode, roleId: Number(document.getElementById('user-role-select').value), }; if (password) body.password = password; await ddaApi('PUT', `/api/dda/rbac/users/${id}`, { body: JSON.stringify(body) }); } else { const body = { email: document.getElementById('user-email').value.trim(), fullName: document.getElementById('user-full-name').value.trim(), phone, countryCode, roleId: Number(document.getElementById('user-role-select').value), password, }; await ddaApi('POST', '/api/dda/rbac/users', { body: JSON.stringify(body) }); } document.getElementById('user-modal').classList.add('hidden'); showDdaSuccess('User saved.'); await loadUsers(); } catch (err) { showDdaError(err.message); } }); async function deleteUser(userId) { if (!confirm('Delete this user? This cannot be undone.')) return; try { await ddaApi('DELETE', `/api/dda/rbac/users/${userId}`); showDdaSuccess('User deleted.'); await loadUsers(); } catch (err) { showDdaError(err.message); } } document.addEventListener('DOMContentLoaded', () => { loadRoles(); loadUsers(); });