599 lines
25 KiB
PHP
Executable File
599 lines
25 KiB
PHP
Executable File
<?= $this->extend('layout/management_layout') ?>
|
|
<?= $this->section('content') ?>
|
|
|
|
<?php
|
|
helper('url');
|
|
$userListEndpoint = $userListEndpoint ?? site_url('api/users');
|
|
?>
|
|
|
|
<div class="container-fluid" data-users-endpoint="<?= esc($userListEndpoint) ?>">
|
|
<div class="wrapper">
|
|
<div class="content"></div>
|
|
|
|
<h2 class="text-center mt-4 mb-3">User List</h2>
|
|
<?= $this->include('partials/academic_filter') ?>
|
|
|
|
<?php if (session()->getFlashdata('success')): ?>
|
|
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
|
<?= esc(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">
|
|
<?= esc(session()->getFlashdata('error')) ?>
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<div id="userListAlert" class="alert alert-danger d-none" role="alert"></div>
|
|
|
|
<div class="table-responsive">
|
|
<table id="myTable" class="display table table-striped table-bordered align-middle" style="width:100%">
|
|
<thead class="table-dark">
|
|
<tr>
|
|
<th>ID</th>
|
|
<th>First Name</th>
|
|
<th>Last Name</th>
|
|
<th>Email</th>
|
|
<th>Phone</th>
|
|
<th>User Type</th>
|
|
<th>Roles</th>
|
|
<th style="width:140px;">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="userTableBody">
|
|
<tr>
|
|
<td colspan="7" class="text-center text-muted">Loading users…</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Edit User Modal -->
|
|
<div class="modal fade" id="editUserModal" tabindex="-1" aria-labelledby="editUserModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable">
|
|
<form class="modal-content" method="post" action="<?= site_url('user/update') ?>" id="editUserForm">
|
|
<?= csrf_field() ?>
|
|
<input type="hidden" name="id" value="">
|
|
|
|
<div class="modal-header bg-primary text-white">
|
|
<h5 class="modal-title" id="editUserModalLabel">Edit User</h5>
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
|
|
<div class="modal-body">
|
|
<div class="row g-3">
|
|
<!-- Column A -->
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Account ID</label>
|
|
<input type="text" name="account_id" class="form-control" value="">
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">First Name</label>
|
|
<input type="text" name="firstname" class="form-control" required value="">
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Last Name</label>
|
|
<input type="text" name="lastname" class="form-control" required value="">
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Gender</label>
|
|
<select name="gender" class="form-select">
|
|
<option value="">—</option>
|
|
<option value="male">Male</option>
|
|
<option value="female">Female</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Phone</label>
|
|
<input type="text" name="cellphone" class="form-control" value="">
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Email</label>
|
|
<input type="email" name="email" class="form-control" required value="">
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Address Street</label>
|
|
<input type="text" name="address_street" class="form-control" value="">
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Apt</label>
|
|
<input type="text" name="apt" class="form-control" value="">
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">City</label>
|
|
<input type="text" name="city" class="form-control" value="">
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">State</label>
|
|
<input type="text" name="state" maxlength="2" class="form-control" value="">
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">ZIP</label>
|
|
<input type="text" name="zip" class="form-control" value="">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Column B -->
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label d-block">Accept School Policy</label>
|
|
<input type="hidden" name="accept_school_policy" value="0">
|
|
<div class="form-check form-switch">
|
|
<input class="form-check-input" type="checkbox" name="accept_school_policy" value="1">
|
|
<label class="form-check-label">Accepted</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">User Type</label>
|
|
<input type="text" name="user_type" class="form-control" value="">
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">RFID Tag</label>
|
|
<input type="text" name="rfid_tag" class="form-control" value="">
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">School ID</label>
|
|
<input type="text" name="school_id" class="form-control" value="">
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Failed Attempts</label>
|
|
<input type="number" name="failed_attempts" class="form-control" value="">
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Last Failed At</label>
|
|
<input type="datetime-local" name="last_failed_at" class="form-control" value="">
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Semester</label>
|
|
<input type="text" name="semester" class="form-control" value="">
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">School Year</label>
|
|
<input type="text" name="school_year" class="form-control" value="">
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Status</label>
|
|
<input type="text" name="status" class="form-control" value="">
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label d-block">Is Suspended</label>
|
|
<input type="hidden" name="is_suspended" value="0">
|
|
<div class="form-check form-switch">
|
|
<input class="form-check-input" type="checkbox" name="is_suspended" value="1">
|
|
<label class="form-check-label">Suspended</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label d-block">Is Verified</label>
|
|
<input type="hidden" name="is_verified" value="0">
|
|
<div class="form-check form-switch">
|
|
<input class="form-check-input" type="checkbox" name="is_verified" value="1">
|
|
<label class="form-check-label">Verified</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Token</label>
|
|
<input type="text" name="token" class="form-control" value="">
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Updated At</label>
|
|
<input type="datetime-local" name="updated_at" class="form-control" value="">
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Created At</label>
|
|
<input type="datetime-local" name="created_at" class="form-control" value="">
|
|
</div>
|
|
</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 Changes</button>
|
|
</div>
|
|
</form>
|
|
</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 early
|
|
ensureFixedHeaderAssets();
|
|
const container = document.querySelector('[data-users-endpoint]');
|
|
if (!container) {
|
|
return;
|
|
}
|
|
|
|
const endpoint = container.dataset.usersEndpoint;
|
|
const FAMILY_URL = <?= json_encode(site_url('family')) ?>;
|
|
const alertBox = document.getElementById('userListAlert');
|
|
const tableBody = document.getElementById('userTableBody');
|
|
const dataTableSelector = '#myTable';
|
|
const editModalEl = document.getElementById('editUserModal');
|
|
const editForm = document.getElementById('editUserForm');
|
|
const editModalTitle = document.getElementById('editUserModalLabel');
|
|
const modalInstance = editModalEl && window.bootstrap
|
|
? new window.bootstrap.Modal(editModalEl)
|
|
: null;
|
|
|
|
let users = [];
|
|
|
|
const showError = (message) => {
|
|
if (!alertBox) {
|
|
return;
|
|
}
|
|
alertBox.textContent = message;
|
|
alertBox.classList.remove('d-none');
|
|
};
|
|
|
|
const clearError = () => {
|
|
if (!alertBox) {
|
|
return;
|
|
}
|
|
alertBox.classList.add('d-none');
|
|
alertBox.textContent = '';
|
|
};
|
|
|
|
const formatDateTimeLocal = (value) => {
|
|
if (!value) {
|
|
return '';
|
|
}
|
|
const normalized = String(value).trim().replace(' ', 'T');
|
|
const date = new Date(normalized);
|
|
if (Number.isNaN(date.valueOf())) {
|
|
const match = normalized.match(/^(\d{4}-\d{2}-\d{2})[T ](\d{2}:\d{2})/);
|
|
return match ? `${match[1]}T${match[2]}` : '';
|
|
}
|
|
const pad = (num) => String(num).padStart(2, '0');
|
|
return [
|
|
date.getFullYear(),
|
|
pad(date.getMonth() + 1),
|
|
pad(date.getDate())
|
|
].join('-') + 'T' + [pad(date.getHours()), pad(date.getMinutes())].join(':');
|
|
};
|
|
|
|
const destroyDataTable = () => {
|
|
if (!window.jQuery || !window.jQuery.fn || !window.jQuery.fn.DataTable) {
|
|
return;
|
|
}
|
|
if (window.jQuery.fn.DataTable.isDataTable(dataTableSelector)) {
|
|
window.jQuery(dataTableSelector).DataTable().clear().destroy();
|
|
}
|
|
};
|
|
|
|
const initDataTable = () => {
|
|
if (!window.jQuery || !window.jQuery.fn || !window.jQuery.fn.DataTable) {
|
|
return;
|
|
}
|
|
const opts = {
|
|
pageLength: 100,
|
|
responsive: true,
|
|
order: [[0, 'asc']],
|
|
columnDefs: [
|
|
{
|
|
orderable: false,
|
|
targets: [7],
|
|
},
|
|
],
|
|
};
|
|
|
|
if (window.jQuery.fn.dataTable && window.jQuery.fn.dataTable.FixedHeader) {
|
|
opts.fixedHeader = { header: true, headerOffset: getFixedHeaderOffset() };
|
|
}
|
|
|
|
const dt = window.jQuery(dataTableSelector).DataTable(opts);
|
|
|
|
// Attach FixedHeader if it loads slightly later
|
|
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 dash = (v) => {
|
|
const s = (v ?? '').toString().trim();
|
|
return s === '' ? '—' : s;
|
|
};
|
|
|
|
const createCell = (text) => {
|
|
const td = document.createElement('td');
|
|
td.textContent = dash(text);
|
|
return td;
|
|
};
|
|
|
|
const createNameCell = (user, roles, which) => {
|
|
const td = document.createElement('td');
|
|
const label = dash(user?.[which]);
|
|
const uid = parseInt(user?.id || 0, 10);
|
|
const roleList = Array.isArray(roles) ? roles.map(r => (r||'').toString().toLowerCase()) : [];
|
|
const isParent = roleList.includes('parent');
|
|
if (uid > 0 && isParent) {
|
|
const a = document.createElement('a');
|
|
a.href = FAMILY_URL + '?guardian_id=' + encodeURIComponent(uid);
|
|
a.className = 'text-decoration-none';
|
|
a.setAttribute('data-family-guardian-id', String(uid));
|
|
a.textContent = label;
|
|
td.appendChild(a);
|
|
} else {
|
|
td.textContent = label;
|
|
}
|
|
return td;
|
|
};
|
|
|
|
const createEmailCell = (email) => {
|
|
const td = document.createElement('td');
|
|
const e = (email ?? '').toString().trim();
|
|
if (e) {
|
|
const a = document.createElement('a');
|
|
a.href = `mailto:${e}`;
|
|
a.textContent = e;
|
|
td.appendChild(a);
|
|
} else {
|
|
td.textContent = '—';
|
|
}
|
|
return td;
|
|
};
|
|
|
|
const createPhoneCell = (phone) => {
|
|
const td = document.createElement('td');
|
|
const p = (phone ?? '').toString().trim();
|
|
if (p) {
|
|
const a = document.createElement('a');
|
|
a.href = `tel:${p}`;
|
|
a.textContent = p;
|
|
td.appendChild(a);
|
|
} else {
|
|
td.textContent = '—';
|
|
}
|
|
return td;
|
|
};
|
|
|
|
const createActionsCell = (entry) => {
|
|
const td = document.createElement('td');
|
|
const button = document.createElement('button');
|
|
button.type = 'button';
|
|
button.className = 'btn btn-sm btn-primary';
|
|
button.innerHTML = '<i class="fas fa-edit"></i> Edit';
|
|
button.addEventListener('click', () => openEditModal(entry));
|
|
td.appendChild(button);
|
|
return td;
|
|
};
|
|
|
|
const renderTable = () => {
|
|
if (!tableBody) {
|
|
return;
|
|
}
|
|
|
|
destroyDataTable();
|
|
tableBody.innerHTML = '';
|
|
|
|
if (users.length === 0) {
|
|
const row = document.createElement('tr');
|
|
const cell = document.createElement('td');
|
|
cell.colSpan = 7;
|
|
cell.classList.add('text-center', 'text-muted');
|
|
cell.textContent = 'No users found.';
|
|
row.appendChild(cell);
|
|
tableBody.appendChild(row);
|
|
initDataTable();
|
|
return;
|
|
}
|
|
|
|
users.forEach((entry) => {
|
|
const { user, roles } = entry;
|
|
const row = document.createElement('tr');
|
|
// ID: prefer school_id, else fallback to numeric user id
|
|
const idDisplay = (user.school_id && String(user.school_id).trim() !== '')
|
|
? user.school_id
|
|
: (user.id ?? '');
|
|
row.appendChild(createCell(idDisplay));
|
|
// First/Last name: clickable for parents to open Family Card
|
|
row.appendChild(createNameCell(user, roles, 'firstname'));
|
|
row.appendChild(createNameCell(user, roles, 'lastname'));
|
|
// Email / Phone with links and fallback dash
|
|
row.appendChild(createEmailCell(user.email));
|
|
row.appendChild(createPhoneCell(user.cellphone));
|
|
// User type (from users table)
|
|
row.appendChild(createCell(user.user_type ?? ''));
|
|
// Roles
|
|
row.appendChild(createCell((roles && roles.length) ? roles.join(', ') : 'No Role Assigned'));
|
|
// Actions
|
|
row.appendChild(createActionsCell(entry));
|
|
tableBody.appendChild(row);
|
|
});
|
|
|
|
initDataTable();
|
|
};
|
|
|
|
const setInputValue = (name, value) => {
|
|
if (!editForm) {
|
|
return;
|
|
}
|
|
const field = editForm.elements[name];
|
|
if (!field) {
|
|
return;
|
|
}
|
|
|
|
const isCollection = typeof field === 'object'
|
|
&& field !== null
|
|
&& typeof field.length === 'number'
|
|
&& typeof field.tagName === 'undefined';
|
|
|
|
if (isCollection) {
|
|
const checkbox = editForm.querySelector(`input[name="${name}"][type="checkbox"]`);
|
|
if (checkbox) {
|
|
checkbox.checked = value === true || value === '1' || value === 1;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (field.getAttribute('type') === 'checkbox') {
|
|
field.checked = value === true || value === '1' || value === 1;
|
|
return;
|
|
}
|
|
|
|
field.value = value ?? '';
|
|
};
|
|
|
|
const openEditModal = (entry) => {
|
|
if (!editForm) {
|
|
return;
|
|
}
|
|
|
|
const { user } = entry;
|
|
|
|
setInputValue('id', user.id ?? '');
|
|
setInputValue('account_id', user.account_id ?? '');
|
|
setInputValue('firstname', user.firstname ?? '');
|
|
setInputValue('lastname', user.lastname ?? '');
|
|
setInputValue('gender', (user.gender ?? '').toString().toLowerCase());
|
|
setInputValue('cellphone', user.cellphone ?? '');
|
|
setInputValue('email', user.email ?? '');
|
|
setInputValue('address_street', user.address_street ?? '');
|
|
setInputValue('apt', user.apt ?? '');
|
|
setInputValue('city', user.city ?? '');
|
|
setInputValue('state', user.state ?? '');
|
|
setInputValue('zip', user.zip ?? '');
|
|
setInputValue('accept_school_policy', user.accept_school_policy ?? 0);
|
|
setInputValue('user_type', user.user_type ?? '');
|
|
setInputValue('rfid_tag', user.rfid_tag ?? '');
|
|
setInputValue('school_id', user.school_id ?? '');
|
|
setInputValue('failed_attempts', user.failed_attempts ?? '');
|
|
setInputValue('last_failed_at', formatDateTimeLocal(user.last_failed_at ?? ''));
|
|
setInputValue('semester', user.semester ?? '');
|
|
setInputValue('school_year', user.school_year ?? '');
|
|
setInputValue('status', user.status ?? '');
|
|
setInputValue('is_suspended', user.is_suspended ?? 0);
|
|
setInputValue('is_verified', user.is_verified ?? 0);
|
|
setInputValue('token', user.token ?? '');
|
|
setInputValue('updated_at', formatDateTimeLocal(user.updated_at ?? ''));
|
|
setInputValue('created_at', formatDateTimeLocal(user.created_at ?? ''));
|
|
|
|
if (editModalTitle) {
|
|
const fullName = [user.firstname ?? '', user.lastname ?? ''].filter(Boolean).join(' ').trim();
|
|
editModalTitle.textContent = fullName !== '' ? `Edit User — ${fullName}` : 'Edit User';
|
|
}
|
|
|
|
if (modalInstance) {
|
|
modalInstance.show();
|
|
}
|
|
};
|
|
|
|
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 list = Array.isArray(payload?.users) ? payload.users : [];
|
|
users = list.map((entry) => ({
|
|
user: entry?.user ?? {},
|
|
roles: Array.isArray(entry?.roles) ? entry.roles : [],
|
|
role_ids: Array.isArray(entry?.role_ids) ? entry.role_ids : [],
|
|
}));
|
|
renderTable();
|
|
})
|
|
.catch((error) => {
|
|
showError(error.message || 'Unable to load users.');
|
|
if (tableBody) {
|
|
tableBody.innerHTML = '';
|
|
const row = document.createElement('tr');
|
|
const cell = document.createElement('td');
|
|
cell.colSpan = 7;
|
|
cell.classList.add('text-center', 'text-danger');
|
|
cell.textContent = 'Failed to load users.';
|
|
row.appendChild(cell);
|
|
tableBody.appendChild(row);
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
<?= $this->endSection() ?>
|