Files
alrahma_sunday_school/app/Views/user/user_list.php
T
2026-02-10 22:11:06 -05:00

599 lines
25 KiB
PHP

<?= $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() ?>