400 lines
16 KiB
PHP
400 lines
16 KiB
PHP
<?= $this->extend('layout/management_layout') ?>
|
|
<?= $this->section('content') ?>
|
|
|
|
<?php
|
|
helper('url');
|
|
$assignRoleEndpoint = $assignRoleEndpoint ?? site_url('api/rolepermission/users');
|
|
$rolesEndpoint = $rolesEndpoint ?? site_url('api/rolepermission/roles');
|
|
?>
|
|
|
|
<div class="container-fluid"
|
|
data-users-endpoint="<?= esc($assignRoleEndpoint) ?>"
|
|
data-roles-endpoint="<?= esc($rolesEndpoint) ?>">
|
|
<div class="wrapper">
|
|
<div class="content"></div>
|
|
<h2 class="text-center mt-4 mb-3">Assign Role to Users</h2>
|
|
<?= $this->include('partials/academic_filter') ?>
|
|
|
|
<?php if (session()->getFlashdata('success')): ?>
|
|
<div class="alert alert-success"><?= esc(session()->getFlashdata('success')); ?></div>
|
|
<?php elseif (session()->getFlashdata('error')): ?>
|
|
<div class="alert alert-danger"><?= esc(session()->getFlashdata('error')); ?></div>
|
|
<?php endif; ?>
|
|
|
|
<div id="assignRoleAlert" class="alert alert-danger d-none" role="alert"></div>
|
|
|
|
<div class="table-responsive">
|
|
<table id="usersTable" class="display table table-bordered table-striped align-middle">
|
|
<thead class="table-dark">
|
|
<tr>
|
|
<th>User ID</th>
|
|
<th>Account Number</th>
|
|
<th>Name</th>
|
|
<th>Email</th>
|
|
<th>Current Roles</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td colspan="6" class="text-center text-muted">Loading users…</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal fade" id="assignRoleModal" tabindex="-1" aria-labelledby="assignRoleModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<form method="post" action="<?= site_url('rolepermission/save_role'); ?>" id="assignRoleForm">
|
|
<?= csrf_field(); ?>
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="assignRoleModalLabel">Assign Roles</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<input type="hidden" name="user_id" id="modal_user_id">
|
|
<div class="mb-3">
|
|
<label class="form-label">User</label>
|
|
<div id="modal_user_info" class="fw-semibold"></div>
|
|
</div>
|
|
<div class="form-group" id="modal_roles_container">
|
|
<label class="form-label">Select Roles</label>
|
|
<div id="modal_roles_list" class="d-flex flex-column gap-1"></div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" class="btn btn-primary">Save Roles</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<?= $this->endSection() ?>
|
|
|
|
<?= $this->section('scripts') ?>
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
// --- FixedHeader helpers ---
|
|
function getFixedHeaderOffset() {
|
|
let total = 0;
|
|
const stack = [];
|
|
const header = document.querySelector('header.navbar.sticky-top, header.navbar.fixed-top');
|
|
if (header) stack.push(header);
|
|
const mgmt = document.getElementById('navbarManagement');
|
|
if (mgmt && (mgmt.classList.contains('sticky-top') || mgmt.classList.contains('fixed-top'))) stack.push(mgmt);
|
|
document.querySelectorAll('.navbar.sticky-top, .navbar.fixed-top').forEach(el => { if (!stack.includes(el)) stack.push(el); });
|
|
stack.forEach(el => { const h = el.offsetHeight || el.getBoundingClientRect().height || 0; total += Math.max(0, Math.round(h)); });
|
|
return total;
|
|
}
|
|
|
|
function loadScript(src, id) {
|
|
return new Promise((resolve, reject) => {
|
|
if (id && document.getElementById(id)) return resolve();
|
|
const s = document.createElement('script');
|
|
if (id) s.id = id;
|
|
s.src = src;
|
|
s.onload = resolve;
|
|
s.onerror = reject;
|
|
document.head.appendChild(s);
|
|
});
|
|
}
|
|
function loadCss(href, id) {
|
|
return new Promise((resolve) => {
|
|
if (id && document.getElementById(id)) return resolve();
|
|
const l = document.createElement('link');
|
|
if (id) l.id = id;
|
|
l.rel = 'stylesheet';
|
|
l.href = href;
|
|
l.onload = resolve;
|
|
document.head.appendChild(l);
|
|
});
|
|
}
|
|
|
|
function ensureFixedHeaderAssets() {
|
|
const hasFH = !!(window.jQuery && window.jQuery.fn && window.jQuery.fn.dataTable && window.jQuery.fn.dataTable.FixedHeader);
|
|
if (hasFH) return Promise.resolve();
|
|
return Promise.all([
|
|
loadScript('https://cdn.jsdelivr.net/npm/datatables.net-fixedheader@3.4.0/js/dataTables.fixedHeader.min.js', 'dt-fixedheader'),
|
|
loadCss('https://cdn.jsdelivr.net/npm/datatables.net-fixedheader-bs5@3.4.0/css/fixedHeader.bootstrap5.min.css', 'dt-fixedheader-css')
|
|
]).catch(() => {});
|
|
}
|
|
|
|
// Start loading FixedHeader assets early
|
|
ensureFixedHeaderAssets();
|
|
const container = document.querySelector('[data-users-endpoint]');
|
|
if (!container) {
|
|
return;
|
|
}
|
|
|
|
const usersEndpoint = container.dataset.usersEndpoint;
|
|
const rolesEndpoint = container.dataset.rolesEndpoint;
|
|
const alertBox = document.getElementById('assignRoleAlert');
|
|
const tableBody = document.querySelector('#usersTable tbody');
|
|
const modalElement = document.getElementById('assignRoleModal');
|
|
const modalUserInfo = document.getElementById('modal_user_info');
|
|
const modalUserId = document.getElementById('modal_user_id');
|
|
const modalRolesList = document.getElementById('modal_roles_list');
|
|
const modalInstance = modalElement ? new bootstrap.Modal(modalElement) : null;
|
|
|
|
let users = [];
|
|
let usersById = new Map();
|
|
let roles = [];
|
|
|
|
const hasDataTables = () =>
|
|
!!(window.jQuery && window.jQuery.fn && window.jQuery.fn.DataTable);
|
|
|
|
const destroyDataTable = () => {
|
|
if (!hasDataTables()) return;
|
|
if (window.jQuery.fn.DataTable.isDataTable('#usersTable')) {
|
|
window.jQuery('#usersTable').DataTable().clear().destroy();
|
|
}
|
|
};
|
|
|
|
const initDataTable = () => {
|
|
if (!hasDataTables()) return;
|
|
const opts = {
|
|
pageLength: 100,
|
|
lengthMenu: [5, 10, 25, 50, 100],
|
|
columnDefs: [
|
|
{ orderable: false, targets: [5] },
|
|
],
|
|
};
|
|
|
|
if (window.jQuery.fn.dataTable && window.jQuery.fn.dataTable.FixedHeader) {
|
|
opts.fixedHeader = { header: true, headerOffset: getFixedHeaderOffset() };
|
|
}
|
|
|
|
const dt = window.jQuery('#usersTable').DataTable(opts);
|
|
|
|
// If plugin finished loading slightly after init, attach FixedHeader instance
|
|
if (!opts.fixedHeader) {
|
|
ensureFixedHeaderAssets().then(() => {
|
|
if (window.jQuery.fn.dataTable && window.jQuery.fn.dataTable.FixedHeader) {
|
|
try { new window.jQuery.fn.dataTable.FixedHeader(dt, { header: true, headerOffset: getFixedHeaderOffset() }); } catch (_) {}
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
const showError = (message) => {
|
|
if (!alertBox) return;
|
|
alertBox.textContent = message;
|
|
alertBox.classList.remove('d-none');
|
|
};
|
|
|
|
const clearError = () => {
|
|
if (!alertBox) return;
|
|
alertBox.textContent = '';
|
|
alertBox.classList.add('d-none');
|
|
};
|
|
|
|
const buildRolesMarkup = (selectedIds = []) => {
|
|
modalRolesList.innerHTML = '';
|
|
const selectedSet = new Set(selectedIds.map((id) => Number(id)));
|
|
|
|
if (!Array.isArray(roles) || roles.length === 0) {
|
|
modalRolesList.innerHTML = '<div class="text-muted">No roles available.</div>';
|
|
return;
|
|
}
|
|
|
|
roles.forEach((role) => {
|
|
const checkboxId = `roleCheck_${role.id}`;
|
|
const wrapper = document.createElement('div');
|
|
wrapper.classList.add('form-check');
|
|
|
|
const input = document.createElement('input');
|
|
input.type = 'checkbox';
|
|
input.className = 'form-check-input';
|
|
input.name = 'role_ids[]';
|
|
input.value = role.id;
|
|
input.id = checkboxId;
|
|
if (selectedSet.has(Number(role.id))) {
|
|
input.checked = true;
|
|
}
|
|
|
|
const label = document.createElement('label');
|
|
label.className = 'form-check-label';
|
|
label.htmlFor = checkboxId;
|
|
label.textContent = role.name ?? '';
|
|
|
|
wrapper.appendChild(input);
|
|
wrapper.appendChild(label);
|
|
modalRolesList.appendChild(wrapper);
|
|
});
|
|
};
|
|
|
|
const openModalForUser = (userId) => {
|
|
const user = usersById.get(userId);
|
|
if (!user || !modalInstance) return;
|
|
|
|
modalUserId.value = user.id ?? '';
|
|
const fullName = `${user.firstname ?? ''} ${user.lastname ?? ''}`.trim();
|
|
modalUserInfo.textContent = `${fullName} (${user.email ?? ''})`;
|
|
|
|
buildRolesMarkup([]);
|
|
const selectedRoleIds = [];
|
|
if (Array.isArray(user.roles) && user.roles.length > 0) {
|
|
const lookup = new Map(roles.map((role) => [role.name, role.id]));
|
|
user.roles.forEach((roleName) => {
|
|
if (lookup.has(roleName)) {
|
|
selectedRoleIds.push(lookup.get(roleName));
|
|
}
|
|
});
|
|
}
|
|
buildRolesMarkup(selectedRoleIds);
|
|
modalInstance.show();
|
|
};
|
|
|
|
const renderNameCell = (user) => {
|
|
const td = document.createElement('td');
|
|
const fullName = `${user?.firstname ?? ''} ${user?.lastname ?? ''}`.trim();
|
|
const label = fullName !== '' ? fullName : '—';
|
|
const roleList = Array.isArray(user?.roles)
|
|
? user.roles.map((role) => (role || '').toString().toLowerCase())
|
|
: [];
|
|
const isParent = roleList.includes('parent');
|
|
const uid = Number(user?.id || 0);
|
|
|
|
if (isParent && uid > 0) {
|
|
const link = document.createElement('a');
|
|
link.href = '#';
|
|
link.className = 'text-decoration-none';
|
|
link.setAttribute('data-family-guardian-id', String(uid));
|
|
link.textContent = label;
|
|
td.appendChild(link);
|
|
return td;
|
|
}
|
|
|
|
td.textContent = label;
|
|
return td;
|
|
};
|
|
|
|
const renderTable = () => {
|
|
if (!tableBody) return;
|
|
|
|
destroyDataTable();
|
|
tableBody.innerHTML = '';
|
|
|
|
if (!Array.isArray(users) || users.length === 0) {
|
|
const row = document.createElement('tr');
|
|
const cell = document.createElement('td');
|
|
cell.colSpan = 6;
|
|
cell.classList.add('text-center', 'text-muted');
|
|
cell.textContent = 'No users found.';
|
|
row.appendChild(cell);
|
|
tableBody.appendChild(row);
|
|
return;
|
|
}
|
|
|
|
users.forEach((user) => {
|
|
usersById.set(user.id, user);
|
|
|
|
const row = document.createElement('tr');
|
|
|
|
const userIdCell = document.createElement('td');
|
|
userIdCell.textContent = user.id ?? '';
|
|
row.appendChild(userIdCell);
|
|
|
|
const accountCell = document.createElement('td');
|
|
accountCell.textContent = user.account_id ?? '';
|
|
row.appendChild(accountCell);
|
|
|
|
row.appendChild(renderNameCell(user));
|
|
|
|
const emailCell = document.createElement('td');
|
|
emailCell.textContent = user.email ?? '';
|
|
row.appendChild(emailCell);
|
|
|
|
const rolesCell = document.createElement('td');
|
|
if (Array.isArray(user.roles) && user.roles.length > 0) {
|
|
rolesCell.innerHTML = user.roles
|
|
.map((role) => `<span class="badge bg-primary me-1 mb-1">${role}</span>`)
|
|
.join('');
|
|
} else {
|
|
rolesCell.innerHTML = '<span class="text-muted">No role assigned</span>';
|
|
}
|
|
row.appendChild(rolesCell);
|
|
|
|
const actionsCell = document.createElement('td');
|
|
const button = document.createElement('button');
|
|
button.type = 'button';
|
|
button.className = 'btn btn-primary btn-sm';
|
|
button.textContent = 'Assign / Update Roles';
|
|
button.addEventListener('click', () => openModalForUser(user.id));
|
|
actionsCell.appendChild(button);
|
|
row.appendChild(actionsCell);
|
|
|
|
tableBody.appendChild(row);
|
|
});
|
|
|
|
initDataTable();
|
|
};
|
|
|
|
const fetchRoles = () => {
|
|
if (!rolesEndpoint) return Promise.resolve();
|
|
return fetch(rolesEndpoint, {
|
|
headers: { 'Accept': 'application/json' },
|
|
credentials: 'same-origin',
|
|
})
|
|
.then((response) => {
|
|
if (!response.ok) {
|
|
throw new Error(`Roles request failed with status ${response.status}`);
|
|
}
|
|
return response.json();
|
|
})
|
|
.then((payload) => {
|
|
roles = Array.isArray(payload?.roles) ? payload.roles : [];
|
|
});
|
|
};
|
|
|
|
const fetchUsers = () => {
|
|
if (!usersEndpoint) return;
|
|
|
|
tableBody.innerHTML = `
|
|
<tr>
|
|
<td colspan="6" class="text-center text-muted">Loading users…</td>
|
|
</tr>
|
|
`;
|
|
|
|
return fetch(usersEndpoint, {
|
|
headers: { 'Accept': 'application/json' },
|
|
credentials: 'same-origin',
|
|
})
|
|
.then((response) => {
|
|
if (!response.ok) {
|
|
throw new Error(`Users request failed with status ${response.status}`);
|
|
}
|
|
return response.json();
|
|
})
|
|
.then((payload) => {
|
|
users = Array.isArray(payload?.users) ? payload.users : [];
|
|
clearError();
|
|
renderTable();
|
|
})
|
|
.catch((error) => {
|
|
console.error(error);
|
|
showError('Unable to load users.');
|
|
destroyDataTable();
|
|
tableBody.innerHTML = `
|
|
<tr>
|
|
<td colspan="6" class="text-center text-danger">Failed to load users.</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
};
|
|
|
|
fetchRoles()
|
|
.then(fetchUsers)
|
|
.catch((error) => {
|
|
console.error(error);
|
|
showError('Unable to load roles.');
|
|
});
|
|
});
|
|
</script>
|
|
<?= $this->endSection() ?>
|