recreate project

This commit is contained in:
root
2026-02-10 22:11:06 -05:00
commit 663c0cdbda
10149 changed files with 1379710 additions and 0 deletions
@@ -0,0 +1,27 @@
<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<div class="container mt-4">
<h3>Add Permission</h3>
<?php if (session()->getFlashdata('error')): ?>
<div class="alert alert-danger"><?= esc(session()->getFlashdata('error')) ?></div>
<?php endif; ?>
<form method="post">
<?= csrf_field() ?>
<div class="mb-3">
<label class="form-label">Permission Name</label>
<input type="text" name="name" class="form-control" required value="<?= old('name') ?>">
</div>
<div class="mb-3">
<label class="form-label">Description</label>
<textarea name="description" class="form-control"><?= old('description') ?></textarea>
</div>
<button type="submit" class="btn btn-success">Save</button>
<a href="<?= site_url('rolepermission/list_permissions') ?>" class="btn btn-secondary">Cancel</a>
</form>
<br>
</div>
<?= $this->endSection() ?>
+165
View File
@@ -0,0 +1,165 @@
<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<div class="container-fluid py-5">
<div class="row justify-content-center">
<div class="col-xl-6">
<?php if (session()->getFlashdata('errors')): ?>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<?php foreach (session()->getFlashdata('errors') as $error): ?>
<p class="mb-0"><?= esc($error) ?></p>
<?php endforeach; ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<?php if (session()->getFlashdata('error')): ?>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<p class="mb-0"><?= esc(session()->getFlashdata('error')) ?></p>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<?php if (session()->getFlashdata('success')): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
<p class="mb-0"><?= esc(session()->getFlashdata('success')) ?></p>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">add new role</h6>
</div>
<div class="card-body">
<form method="post" action="<?= site_url('/roles/store') ?>">
<?= csrf_field() ?>
<!-- Role Name -->
<div class="mb-3">
<label for="roleName" class="form-label">role name</label>
<input type="text"
class="form-control"
id="roleName"
name="name"
minlength="3"
maxlength="255"
required
value="<?= esc(old('name')) ?>">
<div class="form-text">stored lowercase automatically.</div>
</div>
<!-- Slug (auto from name, editable) -->
<div class="mb-3">
<label for="roleSlug" class="form-label">slug</label>
<input type="text"
class="form-control"
id="roleSlug"
name="slug"
maxlength="64"
value="<?= esc(old('slug')) ?>">
<div class="form-text">auto-generated from name (lowercase, az, 09, underscores).</div>
</div>
<!-- Dashboard Route -->
<div class="mb-3">
<label for="dashboardRoute" class="form-label">dashboard route</label>
<input type="text"
class="form-control"
id="dashboardRoute"
name="dashboard_route"
maxlength="255"
required
value="<?= esc(old('dashboard_route') ?? '/landing_page/guest_dashboard') ?>">
<div class="form-text">example: <code>/teacher_dashboard</code> or <code>administrator/administratordashboard</code> (stored lowercase).</div>
</div>
<!-- Description -->
<div class="mb-3">
<label for="roleDescription" class="form-label">description</label>
<textarea class="form-control"
id="roleDescription"
name="description"
rows="3"
maxlength="500"><?= esc(old('description')) ?></textarea>
</div>
<!-- Priority -->
<div class="mb-3">
<label for="rolePriority" class="form-label">priority</label>
<input type="number"
class="form-control"
id="rolePriority"
name="priority"
min="0"
step="1"
required
value="<?= esc(old('priority') ?? 100) ?>">
<div class="form-text">lower number = higher priority when user has multiple roles.</div>
</div>
<!-- Is Active -->
<div class="mb-3 form-check">
<input class="form-check-input"
type="checkbox"
value="1"
id="roleActive"
name="is_active"
<?= (old('is_active', '1') === '1') ? 'checked' : '' ?>>
<label class="form-check-label" for="roleActive">
active
</label>
<!-- Hidden fallback to send 0 if unchecked via JS; server still validates -->
<input type="hidden" name="is_active" value="<?= (old('is_active', '1') === '1') ? '1' : '0' ?>" id="isActiveHidden">
</div>
<div class="d-flex justify-content-between">
<a href="<?= site_url('rolepermission/roles') ?>" class="btn btn-secondary">cancel</a>
<button type="submit" class="btn btn-primary">save role</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<!-- Slug + lowercase helpers -->
<script>
(function () {
const nameEl = document.getElementById('roleName');
const slugEl = document.getElementById('roleSlug');
const routeEl = document.getElementById('dashboardRoute');
const descEl = document.getElementById('roleDescription');
const activeCb = document.getElementById('roleActive');
const activeHidden = document.getElementById('isActiveHidden');
function toSlug(str) {
return String(str || '')
.toLowerCase()
.replace(/[^a-z0-9]+/g, '_')
.replace(/^_+|_+$/g, '');
}
// Auto-generate slug from name if slug empty or was auto-generated
let userEditedSlug = false;
slugEl.addEventListener('input', () => { userEditedSlug = true; });
nameEl.addEventListener('input', () => {
if (!userEditedSlug || slugEl.value.trim() === '') {
slugEl.value = toSlug(nameEl.value);
}
});
// Force lowercase for route + description on blur
[routeEl, descEl].forEach(el => {
el && el.addEventListener('blur', () => { el.value = (el.value || '').toLowerCase(); });
});
// Keep hidden is_active in sync so we always submit 0/1
if (activeCb && activeHidden) {
activeCb.addEventListener('change', () => {
activeHidden.value = activeCb.checked ? '1' : '0';
});
}
})();
</script>
<?= $this->endSection() ?>
+377
View File
@@ -0,0 +1,377 @@
<?= $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 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);
const nameCell = document.createElement('td');
nameCell.textContent = `${user.firstname ?? ''} ${user.lastname ?? ''}`.trim();
row.appendChild(nameCell);
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() ?>
@@ -0,0 +1,27 @@
<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<div class="container mt-4">
<h3>Edit Permission</h3>
<?php if (session()->getFlashdata('error')): ?>
<div class="alert alert-danger"><?= esc(session()->getFlashdata('error')) ?></div>
<?php endif; ?>
<form method="post">
<?= csrf_field() ?>
<div class="mb-3">
<label class="form-label">Permission Name</label>
<input type="text" name="name" class="form-control" required value="<?= old('name', $permission['name']) ?>">
</div>
<div class="mb-3">
<label class="form-label">Description</label>
<textarea name="description" class="form-control"><?= old('description', $permission['description']) ?></textarea>
</div>
<button type="submit" class="btn btn-primary">Update</button>
<a href="<?= site_url('rolepermission/list_permissions') ?>" class="btn btn-secondary">Cancel</a>
</form>
<br>
</div>
<?= $this->endSection() ?>
+162
View File
@@ -0,0 +1,162 @@
<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<div class="container-fluid py-5">
<div class="row justify-content-center">
<div class="col-xl-6">
<?php if (session()->getFlashdata('errors')): ?>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<?php foreach (session()->getFlashdata('errors') as $error): ?>
<p class="mb-0"><?= esc($error) ?></p>
<?php endforeach; ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<?php if (session()->getFlashdata('error')): ?>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<p class="mb-0"><?= esc(session()->getFlashdata('error')) ?></p>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<?php if (session()->getFlashdata('success')): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
<p class="mb-0"><?= esc(session()->getFlashdata('success')) ?></p>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">edit role</h6>
</div>
<div class="card-body">
<form method="post" action="<?= site_url('roles/update/' . $role['id']) ?>">
<?= csrf_field() ?>
<!-- Role Name -->
<div class="mb-3">
<label for="roleName" class="form-label">role name</label>
<input type="text"
class="form-control"
id="roleName"
name="name"
minlength="3"
maxlength="255"
required
value="<?= esc(old('name', $role['name'])) ?>">
<div class="form-text">stored lowercase automatically.</div>
</div>
<!-- Slug -->
<div class="mb-3">
<label for="roleSlug" class="form-label">slug</label>
<input type="text"
class="form-control"
id="roleSlug"
name="slug"
maxlength="64"
value="<?= esc(old('slug', $role['slug'])) ?>">
<div class="form-text">lowercase, az, 09, underscores. auto-updated from name if left blank.</div>
</div>
<!-- Dashboard Route -->
<div class="mb-3">
<label for="dashboardRoute" class="form-label">dashboard route</label>
<input type="text"
class="form-control"
id="dashboardRoute"
name="dashboard_route"
maxlength="255"
required
value="<?= esc(old('dashboard_route', $role['dashboard_route'])) ?>">
<div class="form-text">example: <code>/teacher_dashboard</code> or <code>administrator/administratordashboard</code> (stored lowercase).</div>
</div>
<!-- Description -->
<div class="mb-3">
<label for="roleDescription" class="form-label">description</label>
<textarea class="form-control"
id="roleDescription"
name="description"
rows="3"
maxlength="500"><?= esc(old('description', $role['description'])) ?></textarea>
</div>
<!-- Priority -->
<div class="mb-3">
<label for="rolePriority" class="form-label">priority</label>
<input type="number"
class="form-control"
id="rolePriority"
name="priority"
min="0"
step="1"
required
value="<?= esc(old('priority', $role['priority'] ?? 100)) ?>">
<div class="form-text">lower number = higher priority when user has multiple roles.</div>
</div>
<!-- Is Active -->
<div class="mb-3 form-check">
<?php $isActiveOld = old('is_active', (string) ($role['is_active'] ?? '1')); ?>
<input class="form-check-input"
type="checkbox"
value="1"
id="roleActive"
<?= ($isActiveOld === '1') ? 'checked' : '' ?>>
<label class="form-check-label" for="roleActive">active</label>
<input type="hidden" name="is_active" id="isActiveHidden" value="<?= $isActiveOld === '1' ? '1' : '0' ?>">
</div>
<div class="d-flex justify-content-between">
<a href="<?= site_url('rolepermission/roles') ?>" class="btn btn-secondary">cancel</a>
<button type="submit" class="btn btn-primary">update role</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<script>
(function () {
const nameEl = document.getElementById('roleName');
const slugEl = document.getElementById('roleSlug');
const routeEl = document.getElementById('dashboardRoute');
const descEl = document.getElementById('roleDescription');
const activeCb = document.getElementById('roleActive');
const activeHidden = document.getElementById('isActiveHidden');
function toSlug(str) {
return String(str || '')
.toLowerCase()
.replace(/[^a-z0-9]+/g, '_')
.replace(/^_+|_+$/g, '');
}
// If slug is empty, keep auto-filling from name
let userEditedSlug = false;
slugEl.addEventListener('input', () => { userEditedSlug = slugEl.value.trim() !== ''; });
nameEl.addEventListener('input', () => {
if (!userEditedSlug || slugEl.value.trim() === '') {
slugEl.value = toSlug(nameEl.value);
}
});
// Force lowercase on blur for route/description
[routeEl, descEl].forEach(el => el && el.addEventListener('blur', () => {
el.value = (el.value || '').toLowerCase();
}));
// Keep hidden is_active synced
if (activeCb && activeHidden) {
activeCb.addEventListener('change', () => {
activeHidden.value = activeCb.checked ? '1' : '0';
});
}
})();
</script>
<?= $this->endSection() ?>
@@ -0,0 +1,189 @@
<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<div class="container-fluid">
<div class="wrapper">
<div class="content">
<h2 class="text-center mt-4 mb-3">Edit Role Permissions</h2>
<!-- Flash messages -->
<?php if (session()->getFlashdata('status')): ?>
<div class="alert alert-success"><?= session()->getFlashdata('status'); ?></div>
<?php elseif (session()->getFlashdata('error')): ?>
<div class="alert alert-danger"><?= session()->getFlashdata('error'); ?></div>
<?php endif; ?>
<!-- Top action bar -->
<div class="form-group d-flex justify-content-center flex-wrap gap-2 mb-3">
<button type="button" class="btn btn-secondary" onclick="selectAllPermissions()">Select All</button>
<button type="button" class="btn btn-secondary" onclick="deselectAllPermissions()">Deselect All</button>
<button form="permissionsForm" type="submit" class="btn btn-primary">Save Permissions</button>
<a href="<?= site_url('rolepermission/roles') ?>" class="btn btn-outline-secondary">Cancel</a>
</div>
<!-- Role Selection -->
<?php if (isset($roles)): ?>
<form id="roleSelectForm" class="mb-3" onsubmit="return false;">
<div class="form-group" style="max-width:480px;">
<label for="roleSelect" class="form-label">Select Role</label>
<select name="role_id" id="roleSelect" class="form-control" onchange="loadPermissions(this.value)">
<option value="">-- Select Role --</option>
<?php foreach ($roles as $role): ?>
<option value="<?= $role['id']; ?>" <?= isset($roleId) && (string)$roleId === (string)$role['id'] ? 'selected' : ''; ?>>
<?= esc($role['name']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
</form>
<?php else: ?>
<p>No roles available. Please add roles first.</p>
<?php endif; ?>
<!-- Permissions Form -->
<form id="permissionsForm" method="post" action="<?= site_url('rolepermission/edit_role_permissions/' . ($roleId ?? '')); ?>" class="mt-3" <?= empty($roleId) ? 'style="display:none;"' : '' ?>>
<?= csrf_field() ?>
<input type="hidden" name="role_id" id="roleId" value="<?= $roleId ?? ''; ?>">
<div class="table-responsive">
<table id="permissionsTableEl" class="table table-bordered table-striped align-middle w-100">
<thead class="table-light">
<tr>
<th style="min-width:260px;">Permission</th>
<th>Create</th>
<th>Read</th>
<th>Update</th>
<th>Delete</th>
</tr>
</thead>
<tbody id="permissionsTable">
<?php if (isset($permissions, $existingPermissions, $roleId)): ?>
<?php foreach ($permissions as $permission): ?>
<?php $existingPermission = $existingPermissions[$permission['id']] ?? null; ?>
<tr>
<td><?= esc($permission['name']); ?></td>
<td class="text-center">
<input type="checkbox" class="permission-checkbox"
name="permissions[<?= $permission['id'] ?>][create]" value="1"
<?= ($existingPermission['can_create'] ?? false) ? 'checked' : ''; ?>>
</td>
<td class="text-center">
<input type="checkbox" class="permission-checkbox"
name="permissions[<?= $permission['id'] ?>][read]" value="1"
<?= ($existingPermission['can_read'] ?? false) ? 'checked' : ''; ?>>
</td>
<td class="text-center">
<input type="checkbox" class="permission-checkbox"
name="permissions[<?= $permission['id'] ?>][update]" value="1"
<?= ($existingPermission['can_update'] ?? false) ? 'checked' : ''; ?>>
</td>
<td class="text-center">
<input type="checkbox" class="permission-checkbox"
name="permissions[<?= $permission['id'] ?>][delete]" value="1"
<?= ($existingPermission['can_delete'] ?? false) ? 'checked' : ''; ?>>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<!-- Bottom action bar -->
<div class="form-group d-flex justify-content-center flex-wrap gap-2 mt-3">
<button type="button" class="btn btn-secondary" onclick="selectAllPermissions()">Select All</button>
<button type="button" class="btn btn-secondary" onclick="deselectAllPermissions()">Deselect All</button>
<button type="submit" class="btn btn-primary">Save Permissions</button>
<a href="<?= site_url('rolepermission/roles') ?>" class="btn btn-outline-secondary">Cancel</a>
</div>
</form>
<br>
</div>
</div>
</div>
<?= $this->endSection() ?>
<?= $this->section('styles') ?>
<!-- DataTables (Bootstrap 5) CSS -->
<link rel="stylesheet" href="https://cdn.datatables.net/v/bs5/dt-2.1.8/r-3.0.3/datatables.min.css">
<?= $this->endSection() ?>
<?= $this->section('scripts') ?>
<!-- DataTables + deps (assumes jQuery already included by your layout) -->
<script src="https://cdn.datatables.net/v/bs5/dt-2.1.8/r-3.0.3/datatables.min.js"></script>
<script>
let dtInstance = null;
function initPermissionTable() {
// Destroy an existing instance safely
if (dtInstance) {
dtInstance.destroy();
dtInstance = null;
}
// Initialize DataTable
dtInstance = new DataTable('#permissionsTableEl', {
responsive: true,
paging: true,
searching: true,
info: true,
ordering: true, // keeps alphabetical sort by Permission column
order: [[0, 'asc']], // default sort by Permission name
pageLength: 100,
lengthMenu: [10, 25, 50, 100],
// Ensure checkboxes remain interactive
columnDefs: [
{ targets: [1,2,3,4], orderable: false, searchable: false, className: 'text-center' }
]
});
}
// Load permissions for the selected role via AJAX
function loadPermissions(roleId) {
if (roleId) {
// Update form action & hidden input
$('#permissionsForm').attr('action', '<?= site_url("rolepermission/edit_role_permissions"); ?>/' + roleId);
$('#roleId').val(roleId);
// Show form while loading (optional UX)
$('#permissionsForm').show();
$.get('<?= site_url("rolepermission/load_permissions"); ?>/' + roleId, function (data) {
// Replace tbody content with rows returned by server
$('#permissionsTable').html(data);
// Re-init DataTable after DOM update
initPermissionTable();
}).fail(function () {
alert('Error loading permissions. Please try again.');
});
} else {
// Hide form if no role selected
$('#permissionsForm').hide();
// If table exists, destroy DT to avoid errors
if (dtInstance) {
dtInstance.destroy();
dtInstance = null;
}
}
}
// Select/Deselect helpers
function selectAllPermissions() {
$('.permission-checkbox').prop('checked', true);
}
function deselectAllPermissions() {
$('.permission-checkbox').prop('checked', false);
}
// On page load: if role preselected, ensure DT is in place
$(document).ready(function () {
<?php if (isset($roleId) && !empty($roleId)): ?>
$('#permissionsForm').show();
initPermissionTable();
<?php else: ?>
$('#permissionsForm').hide();
<?php endif; ?>
});
</script>
<?= $this->endSection() ?>
+213
View File
@@ -0,0 +1,213 @@
<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<div class="container-fluid"
data-roles-endpoint="<?= esc($rolesEndpoint ?? site_url('api/rolepermission/roles')) ?>">
<div class="wrapper">
<div class="content">
<h2 class="text-center mt-4 mb-3">List of Roles</h2>
<?php if (session()->getFlashdata('success')): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
<?= session()->getFlashdata('success') ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<?php if (session()->getFlashdata('error')): ?>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<?= session()->getFlashdata('error') ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<div class="mt-3 d-flex justify-content-end">
<a href="<?= site_url('roles/add') ?>" class="btn btn-success">
<i class="fas fa-plus"></i> Add New Role
</a>
<a href="<?= site_url('rolepermission/assign_role') ?>" class="btn btn-info ms-2">
<i class="fas fa-user-tag"></i> Assign Role to User
</a>
</div>
<div id="rolesAlert" class="alert alert-danger d-none mt-3" role="alert"></div>
<div class="table-responsive mt-3">
<table id="rolesTable" class="display table table-bordered table-striped align-middle">
<thead class="table-dark">
<tr>
<th>Role ID</th>
<th>Role Name</th>
<th>Description</th>
<th>Number of Users</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="5" class="text-center text-muted">Loading roles...</td>
</tr>
</tbody>
</table>
</div>
</div>
<br>
</div>
</div>
<?= $this->endSection() ?>
<?= $this->section('scripts') ?>
<script>
document.addEventListener('DOMContentLoaded', () => {
const container = document.querySelector('[data-roles-endpoint]');
if (!container) {
return;
}
const endpoint = container.dataset.rolesEndpoint;
const tableBody = document.querySelector('#rolesTable tbody');
const alertBox = document.getElementById('rolesAlert');
const hasDataTables = () =>
!!(window.jQuery && window.jQuery.fn && window.jQuery.fn.DataTable);
const destroyDataTable = () => {
if (!hasDataTables()) return;
if (window.jQuery.fn.DataTable.isDataTable('#rolesTable')) {
window.jQuery('#rolesTable').DataTable().clear().destroy();
}
};
const initDataTable = () => {
if (!hasDataTables()) return;
window.jQuery('#rolesTable').DataTable({
pageLength: 10,
lengthMenu: [5, 10, 25, 50, 100],
ordering: true,
columnDefs: [
{ orderable: false, targets: [4] },
],
});
};
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 renderRoles = (roles) => {
if (!tableBody) return;
destroyDataTable();
tableBody.innerHTML = '';
if (!Array.isArray(roles) || roles.length === 0) {
const row = document.createElement('tr');
const cell = document.createElement('td');
cell.colSpan = 5;
cell.classList.add('text-center', 'text-muted');
cell.textContent = 'No roles found.';
row.appendChild(cell);
tableBody.appendChild(row);
return;
}
roles.forEach((role) => {
const row = document.createElement('tr');
const idCell = document.createElement('td');
idCell.textContent = role.id ?? '';
row.appendChild(idCell);
const nameCell = document.createElement('td');
nameCell.textContent = role.name ?? '';
row.appendChild(nameCell);
const descCell = document.createElement('td');
descCell.textContent = role.description ?? '';
row.appendChild(descCell);
const countCell = document.createElement('td');
countCell.textContent = Number.isFinite(role.user_count)
? role.user_count
: (role.user_count ?? 0);
row.appendChild(countCell);
const actionsCell = document.createElement('td');
actionsCell.innerHTML = `
<a href="<?= site_url('rolepermission/edit_role_permissions/') ?>${role.id}"
class="btn btn-sm btn-warning me-1">
<i class="fas fa-edit"></i> Update Role Permissions
</a>
<a href="<?= site_url('roles/edit/') ?>${role.id}"
class="btn btn-sm btn-primary me-1">
<i class="fas fa-cog"></i> Edit Role
</a>
<form action="<?= site_url('roles/delete/') ?>${role.id}" method="post" class="d-inline">
<?= csrf_field() ?>
<input type="hidden" name="_method" value="DELETE">
<button type="submit" class="btn btn-sm btn-danger"
onclick="return confirm('Are you sure you want to delete this role?')">
<i class="fas fa-trash"></i> Delete
</button>
</form>
`;
row.appendChild(actionsCell);
tableBody.appendChild(row);
});
initDataTable();
};
const fetchRoles = () => {
if (!endpoint) return;
tableBody.innerHTML = `
<tr>
<td colspan="5" class="text-center text-muted">Loading roles...</td>
</tr>
`;
fetch(endpoint, {
headers: { 'Accept': 'application/json' },
credentials: 'same-origin',
})
.then((response) => {
if (!response.ok) {
throw new Error(`Request failed with status ${response.status}`);
}
return response.json();
})
.then((payload) => {
clearError();
const roles = Array.isArray(payload?.roles) ? payload.roles : [];
roles.forEach((role) => {
const count = Number(role.user_count ?? role.userCount ?? 0);
role.user_count = Number.isFinite(count) ? count : 0;
});
renderRoles(roles);
})
.catch((error) => {
console.error(error);
showError('Unable to load roles.');
destroyDataTable();
tableBody.innerHTML = `
<tr>
<td colspan="5" class="text-center text-danger">Failed to load roles.</td>
</tr>
`;
});
};
fetchRoles();
});
</script>
<?= $this->endSection() ?>
+245
View File
@@ -0,0 +1,245 @@
<?php
// app/Views/rolepermission/permissionList.php
?>
<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<div class="container-fluid"
data-permissions-endpoint="<?= esc($permissionsEndpoint ?? site_url('api/rolepermission/permissions')) ?>">
<div class="wrapper">
<div class="content">
<h2 class="text-center mt-4 mb-3">List of Permissions</h2>
<!-- Flash Messages -->
<?php if (session()->getFlashdata('success')): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
<?= session()->getFlashdata('success') ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<?php if (session()->getFlashdata('error')): ?>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<?= session()->getFlashdata('error') ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<!-- Header actions -->
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<small class="text-muted">
<span id="permissionsTotal" class="text-muted"></span>
</small>
</div>
<div class="d-flex gap-2">
<a href="<?= site_url('rolepermission/add_permission') ?>" class="btn btn-success">
<i class="fas fa-plus"></i> Add New Permission
</a>
<a href="<?= site_url('rolepermission/roles') ?>" class="btn btn-secondary">
<i class="fas fa-list"></i> Back to Roles
</a>
</div>
</div>
<div class="table-responsive">
<table id="myTable" class="display table table-bordered table-striped align-middle">
<thead class="table-dark">
<tr>
<th style="width: 90px;">ID</th>
<th style="width: 280px;">Name</th>
<th>Description</th>
<th style="width: 180px;">Created</th>
<th style="width: 180px;">Updated</th>
<th style="width: 220px;">Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="6" class="text-center text-muted py-4">
Loading permissions…
</td>
</tr>
</tbody>
</table>
</div>
<div id="permissionsAlert" class="alert alert-danger d-none" role="alert"></div>
<br>
</div>
</div>
</div>
<?= $this->endSection() ?>
<?= $this->section('scripts') ?>
<!-- Make sure DataTables JS & CSS are included in your layout (management_layout) -->
<script>
document.addEventListener('DOMContentLoaded', () => {
const container = document.querySelector('[data-permissions-endpoint]');
if (!container) {
return;
}
const endpoint = container.dataset.permissionsEndpoint;
const tableBody = document.querySelector('#myTable tbody');
const totalLabel = document.getElementById('permissionsTotal');
const alertBox = document.getElementById('permissionsAlert');
const hasDataTables = () => (
window.jQuery && window.jQuery.fn && window.jQuery.fn.DataTable
);
const destroyDataTable = () => {
if (!hasDataTables()) return;
if (window.jQuery.fn.DataTable.isDataTable('#myTable')) {
window.jQuery('#myTable').DataTable().clear().destroy();
}
};
const initDataTable = () => {
if (!hasDataTables()) return;
window.jQuery('#myTable').DataTable({
pageLength: 10,
lengthMenu: [5, 10, 25, 50, 100],
ordering: true,
});
};
const setTotal = (count) => {
if (!totalLabel) return;
totalLabel.textContent = `Total: ${count}`;
};
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 formatDateTime = (value) => {
if (!value) return '—';
const date = new Date(value.replace(' ', 'T'));
if (Number.isNaN(date.valueOf())) {
return value;
}
const pad = (num) => String(num).padStart(2, '0');
const datePart = `${pad(date.getMonth() + 1)}-${pad(date.getDate())}-${date.getFullYear()}`;
const hasTime = /[T ]\d{2}:\d{2}/.test(String(value));
if (!hasTime) {
return datePart;
}
const timePart = `${pad(date.getHours())}:${pad(date.getMinutes())}`;
return `${datePart} ${timePart}`;
};
const renderPermissions = (permissions) => {
destroyDataTable();
tableBody.innerHTML = '';
if (!Array.isArray(permissions) || permissions.length === 0) {
const row = document.createElement('tr');
const cell = document.createElement('td');
cell.colSpan = 6;
cell.classList.add('text-center', 'text-muted', 'py-4');
cell.textContent = 'No permissions found.';
row.appendChild(cell);
tableBody.appendChild(row);
setTotal(0);
return;
}
permissions.forEach((permission) => {
const row = document.createElement('tr');
const idCell = document.createElement('td');
idCell.textContent = permission.id ?? '';
row.appendChild(idCell);
const nameCell = document.createElement('td');
nameCell.innerHTML = `<code>${permission.name ?? ''}</code>`;
row.appendChild(nameCell);
const descCell = document.createElement('td');
descCell.textContent = permission.description ?? '';
row.appendChild(descCell);
const createdCell = document.createElement('td');
createdCell.textContent = formatDateTime(permission.created_at ?? null);
row.appendChild(createdCell);
const updatedCell = document.createElement('td');
updatedCell.textContent = formatDateTime(permission.updated_at ?? null);
row.appendChild(updatedCell);
const actionsCell = document.createElement('td');
actionsCell.innerHTML = `
<a href="<?= site_url('rolepermission/edit_permission/') ?>${permission.id}"
class="btn btn-sm btn-warning me-1">
<i class="fas fa-edit"></i> Edit
</a>
<form action="<?= site_url('rolepermission/delete_permission/') ?>${permission.id}" method="post" class="d-inline">
<?= csrf_field() ?>
<button type="submit" class="btn btn-sm btn-danger"
onclick="return confirm('Delete permission \"${permission.name ?? ''}\"? This cannot be undone.');">
<i class="fas fa-trash-alt"></i> Delete
</button>
</form>
`;
row.appendChild(actionsCell);
tableBody.appendChild(row);
});
setTotal(permissions.length);
initDataTable();
};
const fetchPermissions = () => {
if (!endpoint) return;
tableBody.innerHTML = `
<tr>
<td colspan="6" class="text-center text-muted py-4">
Loading permissions…
</td>
</tr>
`;
fetch(endpoint, {
headers: { 'Accept': 'application/json' },
credentials: 'same-origin',
})
.then((response) => {
if (!response.ok) {
throw new Error(`Request failed with status ${response.status}`);
}
return response.json();
})
.then((payload) => {
clearError();
renderPermissions(Array.isArray(payload?.permissions) ? payload.permissions : []);
})
.catch((error) => {
console.error(error);
showError('Unable to load permissions.');
destroyDataTable();
tableBody.innerHTML = `
<tr>
<td colspan="6" class="text-center text-danger py-4">
Failed to load permissions.
</td>
</tr>
`;
});
};
fetchPermissions();
});
</script>
<?= $this->endSection() ?>