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
+27
View File
@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html>
<head>
<title>Create User</title>
</head>
<body>
<h1>Create User</h1>
<form method="post" action="/user/store">
<?= csrf_field(); ?>
<label>Password:</label><input type="password" name="password" required><br>
<label>First Name:</label><input type="text" name="firstname" required><br>
<label>Last Name:</label><input type="text" name="lastname" required><br>
<label>Cell Phone Number:</label><input type="text" name="cellphone" required><br>
<label>Email:</label><input type="email" name="email" required><br>
<label>Address Street:</label><input type="text" name="address_street" required><br>
<label>City:</label><input type="text" name="city" required><br>
<label>State:</label><input type="text" name="state" required><br>
<label>Zip:</label><input type="text" name="zip" required><br>
<label>Role:</label><input type="text" name="role" required><br>
<label>Accept School Policy:</label><input type="checkbox" name="accept_school_policy" value="1"><br>
<input type="submit" value="Create">
</form>
</body>
</html>
+30
View File
@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html>
<head>
<title>Edit User</title>
</head>
<body>
<h1>Edit User</h1>
<form method="post" action="/user/update/<?= $user['id'] ?>">
<?= csrf_field(); ?>
<label>Password:</label><input type="password" name="password" required><br>
<label>First Name:</label><input type="text" name="firstname" value="<?= $user['firstname'] ?>" required><br>
<label>Last Name:</label><input type="text" name="lastname" value="<?= $user['lastname'] ?>" required><br>
<label>Cell Phone Number:</label><input type="text" name="cellphone" value="<?= $user['cellphone'] ?>"
required><br>
<label>Email:</label><input type="email" name="email" value="<?= $user['email'] ?>" required><br>
<label>Address Street:</label><input type="text" name="address_street" value="<?= $user['address_street'] ?>"
required><br>
<label>City:</label><input type="text" name="city" value="<?= $user['city'] ?>" required><br>
<label>State:</label><input type="text" name="state" value="<?= $user['state'] ?>" required><br>
<label>Zip:</label><input type="text" name="zip" value="<?= $user['zip'] ?>" required><br>
<label>Role:</label><input type="text" name="role" value="<?= $user['role'] ?>" required><br>
<label>Accept School Policy:</label><input type="checkbox" name="accept_school_policy" value="1"
<?= $user['accept_school_policy'] ? 'checked' : '' ?>><br>
<input type="submit" value="Update">
</form>
</body>
</html>
+55
View File
@@ -0,0 +1,55 @@
<?= $this->extend('layout/register_layout') ?>
<?= $this->section('content') ?>
<div class="registration-form container mt-5 mb-5 text-center">
<?php if (session()->getFlashdata('error')): ?>
<div class="alert alert-danger text-center">
<?= esc(session()->getFlashdata('error')) ?>
</div>
<?php endif; ?>
<!-- Form -->
<form method="post" action="/processForgotPassword" class="text-start" style="max-width: 600px; margin: 0 auto;">
<?= csrf_field() ?>
<div class="text-center mb-4">
<a href="<?= base_url('/') ?>">
<img src="<?= base_url('assets/images/alrahma_logo.png') ?>" alt=""
style="width: 180px; height: 120px;">
</a>
</div>
<h3 class="text-center text-success" style="font-family: Arial, sans-serif;">Reset Your Password</h3>
<div class="centered-paragraph mb-4 text-center">
<p>Please enter the email address you used to register with us.</p>
<p>We'll send you a link to reset your password.</p>
</div>
<div class="form-group mb-3">
<label for="email"></label>
<input type="email"
class="form-control item"
id="email"
name="email"
maxlength="50"
placeholder="Email Address"
value="<?= old('email') ?>"
required>
</div>
<div class="d-grid mb-3">
<button type="submit" class="btn btn-success item">Reset Password</button>
</div>
<div class="text-center">
<p class="mb-0">Don't have an account?</p>
<a href="/register" class="d-inline-block mt-1">Register now</a>
</div>
</form>
</div>
<!-- ✅ Include modal view -->
<?= view('user/password_reset_confirmation') ?>
<?= view('/errors/custom/blocked') ?>
<?= $this->endSection() ?>
+128
View File
@@ -0,0 +1,128 @@
<?php
use CodeIgniter\Exceptions\PageNotFoundException;
if (!function_exists('base_url')) {
throw new PageNotFoundException('base_url function not found');
}
$session = session();
if ($session->get('isLoggedIn')) {
header('Location: ' . base_url('/dashboard'));
exit();
}
?>
<?= $this->extend('layout/register_layout') ?>
<?= $this->section('content') ?>
<div class="registration-form container mt-5 mb-5">
<form id="loginForm" method="post" action="<?= base_url('/user/login') ?>">
<?= csrf_field() ?>
<div class="text-center mb-4">
<a href="<?= base_url('/') ?>">
<img src="<?= base_url('assets/images/alrahma_logo.png') ?>" alt=""
style="width: 180px; height: 120px;">
</a>
</div>
<h3 class="text-center text-success" style="font-family: Arial, sans-serif;">Login to Your Account</h3>
<br>
<!-- Email Field -->
<div class="form-group mb-3">
<label for="email" class="form-label"></label>
<input type="email" class="form-control item" id="email" name="email"
placeholder="Enter your email" maxlength="50" value="<?= old('email') ?>" required>
<div id="email-error" class="text-danger small mt-1"></div>
</div>
<!-- Password Field -->
<div class="form-group position-relative mb-4">
<label for="password" class="form-label"></label>
<input type="password"
class="form-control item pe-5"
id="password"
name="password"
placeholder="Enter your password"
maxlength="30"
required>
<span class="position-absolute top-50 end-0 translate-middle-y me-3"
onclick="togglePasswordVisibility('password', this)"
style="cursor: pointer;">
<i class="fa-solid fa-eye"></i>
</span>
<div id="password-error" class="text-danger small mt-1"></div>
</div>
<!-- Submit Button -->
<div class="mb-3 d-grid">
<button type="submit" class="btn btn-success item">Login</button>
</div>
<!-- Flash Error Message -->
<?php if ($session->getFlashdata('error')): ?>
<div class="alert alert-danger mt-3 text-center" role="alert">
<?= esc($session->getFlashdata('error')) ?>
</div>
<?php endif; ?>
<!-- Forgot Password -->
<div class="forgot-password text-center mt-3">
<a href="<?= base_url('user/forgot_password') ?>">Forgot Password?</a>
</div>
</form>
</div>
<?= $this->endSection() ?>
<?= $this->section('scripts') ?>
<script>
function togglePasswordVisibility(inputId, iconElement) {
const input = document.getElementById(inputId);
const icon = iconElement.querySelector('i');
if (input.type === 'password') {
input.type = 'text';
icon.classList.remove('fa-eye');
icon.classList.add('fa-eye-slash');
} else {
input.type = 'password';
icon.classList.remove('fa-eye-slash');
icon.classList.add('fa-eye');
}
}
document.getElementById('loginForm').addEventListener('submit', function(event) {
let isValid = true;
const email = document.getElementById('email').value.trim();
const password = document.getElementById('password').value.trim();
const emailError = document.getElementById('email-error');
const passwordError = document.getElementById('password-error');
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!email) {
emailError.textContent = 'Email is required.';
isValid = false;
} else if (!emailRegex.test(email)) {
emailError.textContent = 'Please enter a valid email address.';
isValid = false;
} else {
emailError.textContent = '';
}
if (!password) {
passwordError.textContent = 'Password is required.';
isValid = false;
} else {
passwordError.textContent = '';
}
if (!isValid) {
event.preventDefault();
}
});
</script>
<?= $this->endSection() ?>
+272
View File
@@ -0,0 +1,272 @@
<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<?php
helper('url');
$endpoint = $loginActivityEndpoint ?? site_url('api/login-activity');
$defaultPerPage = isset($defaultPerPage) && (int)$defaultPerPage > 0 ? (int)$defaultPerPage : 25;
?>
<div class="container-fluid"
data-login-activity-endpoint="<?= esc($endpoint) ?>"
data-default-per-page="<?= esc($defaultPerPage) ?>">
<div class="wrapper">
<div class="content"></div>
<h2 class="text-center mt-4 mb-3">Login Activity</h2>
<?= $this->include('partials/academic_filter') ?>
<div id="loginActivityAlert" class="alert alert-danger d-none" role="alert"></div>
<div class="d-flex justify-content-between align-items-center flex-wrap gap-3 mb-3">
<div>
<label for="perPageSelect" class="form-label mb-0 me-2">Rows per page:</label>
<select class="form-select d-inline-block w-auto" id="perPageSelect">
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
<div class="d-flex align-items-center gap-2">
<button type="button" class="btn btn-outline-secondary" id="prevPageBtn">Previous</button>
<span id="paginationInfo" class="text-muted small">Page 1 of 1</span>
<button type="button" class="btn btn-outline-secondary" id="nextPageBtn">Next</button>
</div>
</div>
<div class="table-responsive">
<table id="loginActivityTable" class="table table-striped table-bordered align-middle mb-0">
<thead class="table-dark">
<tr>
<th>User ID</th>
<th>Email</th>
<th>Login Time</th>
<th>Logout Time</th>
<th>IP Address</th>
<th>User Agent</th>
</tr>
</thead>
<tbody id="loginActivityBody">
<tr>
<td colspan="6" class="text-center text-muted">Loading login activity…</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<?= $this->endSection() ?>
<?= $this->section('scripts') ?>
<script>
document.addEventListener('DOMContentLoaded', () => {
const container = document.querySelector('[data-login-activity-endpoint]');
if (!container) {
return;
}
const endpoint = container.dataset.loginActivityEndpoint;
const alertBox = document.getElementById('loginActivityAlert');
const tableBody = document.getElementById('loginActivityBody');
const perPageSelect = document.getElementById('perPageSelect');
const paginationInfo = document.getElementById('paginationInfo');
const prevBtn = document.getElementById('prevPageBtn');
const nextBtn = document.getElementById('nextPageBtn');
const tableSelector = '#loginActivityTable';
let currentPage = 1;
let perPage = parseInt(container.dataset.defaultPerPage, 10) || 25;
let pageCount = 1;
const setPerPageSelect = () => {
const options = Array.from(perPageSelect.options);
const found = options.some((opt) => parseInt(opt.value, 10) === perPage);
if (!found) {
const option = document.createElement('option');
option.value = String(perPage);
option.textContent = perPage;
perPageSelect.appendChild(option);
}
perPageSelect.value = String(perPage);
};
setPerPageSelect();
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 destroyDataTable = () => {
if (!window.jQuery || !window.jQuery.fn || !window.jQuery.fn.DataTable) {
return;
}
if (window.jQuery.fn.DataTable.isDataTable(tableSelector)) {
window.jQuery(tableSelector).DataTable().clear().destroy();
}
};
const initDataTable = () => {
if (!window.jQuery || !window.jQuery.fn || !window.jQuery.fn.DataTable) {
return;
}
window.jQuery(tableSelector).DataTable({
paging: false,
searching: false,
info: false,
order: [[2, 'desc']],
});
};
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 renderTable = (activities) => {
destroyDataTable();
tableBody.innerHTML = '';
if (!Array.isArray(activities) || activities.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 login activities found.';
row.appendChild(cell);
tableBody.appendChild(row);
initDataTable();
return;
}
activities.forEach((activity) => {
const row = document.createElement('tr');
const cells = [
activity.user_id ?? '—',
activity.email ?? '—',
formatDateTime(activity.login_time ?? null),
formatDateTime(activity.logout_time ?? null),
activity.ip_address ?? '—',
activity.user_agent ?? '—',
];
cells.forEach((text) => {
const cell = document.createElement('td');
cell.textContent = text;
row.appendChild(cell);
});
tableBody.appendChild(row);
});
initDataTable();
};
const updatePaginationControls = () => {
paginationInfo.textContent = `Page ${currentPage} of ${pageCount}`;
prevBtn.disabled = currentPage <= 1;
nextBtn.disabled = currentPage >= pageCount;
};
const buildUrl = () => {
try {
const url = new URL(endpoint, window.location.origin);
url.searchParams.set('page', currentPage);
url.searchParams.set('per_page', perPage);
return url.toString();
} catch (error) {
const separator = endpoint.includes('?') ? '&' : '?';
return `${endpoint}${separator}page=${encodeURIComponent(currentPage)}&per_page=${encodeURIComponent(perPage)}`;
}
};
const fetchActivities = () => {
clearError();
const url = buildUrl();
return fetch(url, {
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) => {
const activities = Array.isArray(payload?.activities) ? payload.activities : [];
const pagination = payload?.pagination ?? {};
currentPage = Number.isFinite(pagination.currentPage) ? pagination.currentPage : currentPage;
pageCount = Number.isFinite(pagination.pageCount) ? pagination.pageCount : pageCount;
renderTable(activities);
updatePaginationControls();
})
.catch((error) => {
showError(error.message || 'Unable to load login activity.');
destroyDataTable();
tableBody.innerHTML = '';
const row = document.createElement('tr');
const cell = document.createElement('td');
cell.colSpan = 6;
cell.classList.add('text-center', 'text-danger');
cell.textContent = 'Failed to load login activity.';
row.appendChild(cell);
tableBody.appendChild(row);
updatePaginationControls();
});
};
perPageSelect.addEventListener('change', () => {
perPage = parseInt(perPageSelect.value, 10) || 25;
currentPage = 1;
fetchActivities();
});
prevBtn.addEventListener('click', () => {
if (currentPage > 1) {
currentPage -= 1;
fetchActivities();
}
});
nextBtn.addEventListener('click', () => {
if (currentPage < pageCount) {
currentPage += 1;
fetchActivities();
}
});
fetchActivities();
});
</script>
<?= $this->endSection() ?>
@@ -0,0 +1,27 @@
<!-- app/Views/user/password_reset_confirmation.php -->
<div class="modal fade" id="emailConfirmModal" tabindex="-1" aria-labelledby="emailConfirmLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content rounded-4 shadow border-0" style="max-width: 600px; margin: auto;">
<div class="modal-body text-center">
<img src="<?= base_url('assets/images/alrahma_logo.png') ?>" alt="Logo" style="width: 180px; height: 120px;">
<h5 class="modal-title text-success" id="emailConfirmLabel">Check Your Email</h5>
<p class="lead mt-3">
A link to reset the password has been sent to this email:<br>
<strong><?= esc(session()->getFlashdata('email')) ?></strong>
</p>
<p>Please check your inbox and follow the instructions to reset the password.</p>
<a href="<?= base_url('login') ?>" class="btn btn-success mt-3">Return to Login</a>
</div>
</div>
</div>
</div>
<?php if (session()->getFlashdata('show_modal')): ?>
<script>
window.addEventListener('DOMContentLoaded', function() {
const modal = new bootstrap.Modal(document.getElementById('emailConfirmModal'));
modal.show();
});
</script>
<?php endif; ?>
+43
View File
@@ -0,0 +1,43 @@
<?= $this->extend('layout/register_layout') ?>
<?= $this->section('content') ?>
<!-- Triggered Modal on Load -->
<div class="registration-form container mt-5 mb-5 text-center">
</div>
<!-- Hidden trigger for modal -->
<div class="modal fade" id="successModal" tabindex="-1" aria-labelledby="successModalLabel" aria-hidden="true">
<div class="text-center mb-4">
<a href="<?= base_url('/') ?>">
<img src="<?= base_url('assets/images/alrahma_logo.png') ?>" alt="" style="width: 180px; height: 120px;">
</a>
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-success shadow">
<div class="modal-header bg-success text-white">
<h5 class="text-center modal-title" id="successModalLabel"></h5>
</div>
<div class="modal-body">
<h4 class="mb-3">Password Set Successfully!</h4>
<p class="mb-0">You will be redirected to the login page shortly.</p>
</div>
</div>
</div>
</div>
</div>
<?= $this->endSection() ?>
<?= $this->section('scripts') ?>
<script>
$(document).ready(function() {
// Show modal on load
const modal = new bootstrap.Modal(document.getElementById('successModal'));
modal.show();
// Redirect after 2 seconds
setTimeout(() => {
window.location.href = "<?= base_url('login') ?>";
}, 5000);
});
</script>
<?= $this->endSection() ?>
+570
View File
@@ -0,0 +1,570 @@
<?= $this->extend('layout/register_layout') ?>
<?= $this->section('content') ?>
<div class="registration-form container mt-5 mb-5">
<?php $errors = session()->getFlashdata('errors') ?? []; ?>
<?php $captcha_error = session()->getFlashdata('captcha_error') ?? ''; ?>
<form action="<?= base_url('/register') ?>" method="post" id="registrationForm">
<?= csrf_field() ?>
<?php if (session()->getFlashdata('error')): ?>
<div class="alert alert-danger" role="alert">
<?= session()->getFlashdata('error') ?>
</div>
<?php endif; ?>
<?php if (session()->getFlashdata('top_error')): ?>
<div class="alert alert-danger text-center">
<?= esc(session()->getFlashdata('top_error')) ?>
</div>
<?php endif; ?>
<div class="text-center mb-4">
<a href="<?= base_url('/') ?>">
<img src="<?= base_url('assets/images/alrahma_logo.png') ?>" alt=""
style="width: 180px; height: 120px;">
</a>
</div>
<h3 class="text-center text-success" style="font-family: Arial, sans-serif;">Registration Form</h3>
<div class="alert alert-info" role="alert">
- This account is for parents/guardians to manage students. If you already registered in a prior year, please reuse your existing account.<br>
- If you cannot access your old email, reset your password.
</div>
<p class="text-center text-secondary mt-3 mb-2">
(All fields with * are required)
</p>
<br>
<div class="mb-3">
<input type="text" class="form-control item" id="firstname" name="firstname" placeholder="First Name*"
maxlength="50" required value="<?= old('firstname') ?>">
<div class="form-text text-muted d-none" id="firstname-hint">230 characters. Letters, spaces, and hyphens only.</div>
<?php if (isset($errors['firstname'])): ?>
<div class="text-danger"><?= $errors['firstname'] ?></div>
<?php endif; ?>
</div>
<div class="mb-3">
<input type="text" class="form-control item" id="lastname" name="lastname" placeholder="Last Name*"
maxlength="50" required value="<?= old('lastname') ?>">
<div class="form-text text-muted d-none" id="lastname-hint">230 characters. Letters, spaces, and hyphens only.</div>
<?php if (isset($errors['lastname'])): ?>
<div class="text-danger"><?= $errors['lastname'] ?></div>
<?php endif; ?>
</div>
<div class="mb-3">
<select id="gender" name="gender" class="form-select item" required>
<option value="" disabled selected>Select Gender*</option>
<option value="Male" <?= old('gender') == 'Male' ? 'selected' : '' ?>>Male</option>
<option value="Female" <?= old('gender') == 'Female' ? 'selected' : '' ?>>Female</option>
</select>
<?php if (isset($errors['gender'])): ?>
<div class="text-danger"><?= $errors['gender'] ?></div>
<?php endif; ?>
</div>
<div class="mb-3">
<input type="tel"
class="form-control item"
id="cellphone"
name="cellphone"
placeholder="Phone - Cell*"
maxlength="12"
required
title="Enter a valid 10-digit phone number (e.g., 123-456-7890)"
value="<?= old('cellphone') ?>">
<div class="form-text text-muted d-none" id="cellphone-hint">10-digit US phone number required.</div>
<?php if (isset($errors['cellphone'])): ?>
<div class="text-danger"><?= $errors['cellphone'] ?></div>
<?php endif; ?>
<div id="cellphone-error" class="text-danger"></div>
</div>
<div class="mb-3">
<input type="email" class="form-control item" id="email" name="email" placeholder="Email*"
maxlength="50" required value="<?= old('email') ?>">
<div class="form-text text-muted d-none" id="email-hint">Valid format (e.g. user@example.com), max 50 characters.</div>
<?php if (isset($errors['email'])): ?>
<div class="text-danger"><?= $errors['email'] ?></div>
<?php endif; ?>
<div id="email-error" class="text-danger"></div>
</div>
<div class="mb-3 position-relative">
<input type="email" class="form-control item" id="confirm_email" name="confirm_email"
placeholder="Confirm Email*" maxlength="50" required
value="<?= old('confirm_email') ?>"
onpaste="return false;" oncopy="return false;" oncut="return false;"
title="Copy/paste is disabled for accuracy">
<div class="form-text text-muted d-none" id="confirm_email-hint">Must match the email field. Copy/paste disabled.</div>
<?php if (isset($errors['confirm_email'])): ?>
<div class="text-danger"><?= $errors['confirm_email'] ?></div>
<?php endif; ?>
<div id="confirm-email-error" class="text-danger"></div>
<div class="invalid-feedback d-none" id="copy-warning-email">
Copy/paste is disabled for this field.
</div>
</div>
<div class="mb-3">
<input type="text" class="form-control item" id="address_street" name="address_street"
placeholder="Address - Street*" maxlength="50" required value="<?= old('address_street') ?>">
<div class="form-text text-muted d-none" id="address_street-hint">230 characters. Letters, digits, spaces, hyphens allowed.</div>
</div>
<div class="mb-3">
<input type="text" class="form-control item" id="apt" name="apt" placeholder="Apt" maxlength="15"
value="<?= old('apt') ?>">
<div class="form-text text-muted d-none" id="apt-hint">Optional. Max 15 characters.</div>
<div id="apt-error" class="text-danger small"></div>
</div>
<div class="mb-3">
<input type="text" class="form-control item" id="city" name="city" placeholder="City*" maxlength="15"
required value="<?= old('city') ?>">
<div class="form-text text-muted d-none" id="city-hint">215 characters. Letters and spaces allowed.</div>
<?php if (isset($errors['city'])): ?>
<div class="text-danger"><?= $errors['city'] ?></div>
<?php endif; ?>
</div>
<div class="mb-3">
<select id="state" name="state" class="form-select item" required>
<option value="" disabled selected>Select State*</option>
<?php
$states = [
'CT' => 'Connecticut',
'ME' => 'Maine',
'MA' => 'Massachusetts',
'NH' => 'New Hampshire',
'NY' => 'New York',
'RI' => 'Rhode Island',
'VT' => 'Vermont'
];
foreach ($states as $abbr => $name): ?>
<option value="<?= $abbr ?>" <?= old('state') == $abbr ? 'selected' : '' ?>><?= $name ?></option>
<?php endforeach; ?>
</select>
<?php if (isset($errors['state'])): ?>
<div class="text-danger"><?= $errors['state'] ?></div>
<?php endif; ?>
</div>
<div class="mb-3">
<input type="text" class="form-control item" id="zip" name="zip" placeholder="Zip*" maxlength="5"
pattern="\d{5}" title="Please enter a 5-digit zip code" inputmode="numeric" required
value="<?= old('zip') ?>">
<div class="form-text text-muted d-none" id="zip-hint">5-digit US ZIP code only.</div>
<?php if (isset($errors['zip'])): ?>
<div class="text-danger"><?= $errors['zip'] ?></div>
<?php endif; ?>
<div id="zip-error" class="text-danger"></div>
</div>
<!-- Parent/Guardian Checkbox -->
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" value="1" id="is_parent" name="is_parent" <?= old('is_parent') ? 'checked' : '' ?>>
<label class="form-check-label" for="is_parent">
Check this box if you are a Parent/Guardian
</label>
</div>
<!-- Second Parent Info Section -->
<div id="second-parent-section" style="display: none; border: 1px solid #ccc; padding: 15px; margin-top: 10px; border-radius: 5px;">
<h6>Second Parent/Guardian Information</h6>
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" value="1"
id="no_second_parent_info" name="no_second_parent_info"
<?= old('no_second_parent_info') ? 'checked' : '' ?>>
<label class="form-check-label" for="no_second_parent_info">
Check this box if no second parent info is available
</label>
</div>
<div class="row" id="second-parent-fields">
<!-- First Name -->
<div class="mb-3">
<input type="text" class="form-control item" id="second_firstname" name="second_firstname"
placeholder="First Name*" maxlength="50" value="<?= old('second_firstname') ?>">
<div class="form-text text-muted d-none" id="second_firstname-hint">230 characters. Letters, spaces, and hyphens only.</div>
<?php if (isset($errors['second_firstname'])): ?>
<div class="text-danger"><?= $errors['second_firstname'] ?></div>
<?php endif; ?>
</div>
<!-- Last Name -->
<div class="mb-3">
<input type="text" class="form-control item" id="second_lastname" name="second_lastname"
placeholder="Last Name*" maxlength="50" value="<?= old('second_lastname') ?>">
<div class="form-text text-muted d-none" id="second_lastname-hint">230 characters. Letters, spaces, and hyphens only.</div>
<?php if (isset($errors['second_lastname'])): ?>
<div class="text-danger"><?= $errors['second_lastname'] ?></div>
<?php endif; ?>
</div>
<div class="mb-3">
<select id="second_gender" name="second_gender" class="form-select item">
<option value="" disabled selected>Select Gender*</option>
<option value="Male" <?= old('second_gender') == 'Male' ? 'selected' : '' ?>>Male</option>
<option value="Female" <?= old('second_gender') == 'Female' ? 'selected' : '' ?>>Female</option>
</select>
<?php if (isset($errors['second_gender'])): ?>
<div class="text-danger"><?= $errors['second_gender'] ?></div>
<?php endif; ?>
</div>
<div class="mb-3">
<input type="email" class="form-control item" id="second_email" name="second_email" placeholder="Email*"
maxlength="50" value="<?= old('second_email') ?>">
<div class="form-text text-muted d-none" id="second_email-hint">Valid format (e.g. user@example.com), max 50 characters.</div>
<?php if (isset($errors['second_email'])): ?>
<div class="text-danger"><?= $errors['second_email'] ?></div>
<?php endif; ?>
<div id="second_email-error" class="text-danger"></div>
</div>
<div class="mb-3">
<input type="text"
class="form-control item"
id="second_cellphone"
name="second_cellphone"
placeholder="Phone - Cell*"
maxlength="12"
title="Enter a valid 10-digit phone number (e.g., 123-456-7890"
value="<?= old('second_cellphone') ?>">
<div class="form-text text-muted d-none" id="second_cellphone-hint">10-digit US phone number required.</div>
<?php if (isset($errors['second_cellphone'])): ?>
<div class="text-danger"><?= $errors['second_cellphone'] ?></div>
<?php endif; ?>
<div id="second_cellphone-error" class="text-danger"></div>
</div>
</div>
</div>
<br>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" value="1" id="accept_school_policy"
name="accept_school_policy" required <?= old('accept_school_policy') ? 'checked' : '' ?>>
<label class="form-check-label mb-3 policy-label" for="accept_school_policy">
I have read and I accept all school policies
<!-- Use a button, not an <a>; keep it OUTSIDE the label -->
<button type="button" class="btn btn-link p-0 small text-success" id="openPolicyLink">
(Read school policies here)
</button>
</label>
</div>
<div class="mb-3">
<div class="text-center border p-2 bg-light item">
<?= strip_tags($captchaQuestion) ?>
</div>
</div>
<div class="mb-3 position-relative">
<input type="text"
class="form-control item <?= isset($errors['captcha']) ? 'is-invalid' : '' ?>"
id="captcha"
name="captcha"
placeholder="Enter the text above*"
maxlength="8"
autocomplete="off"
required
onpaste="return false;" oncopy="return false;" oncut="return false;"
title="Copy/paste is disabled for accuracy">
<!-- JS-based warning -->
<div id="captcha-warning" class="invalid-feedback d-none">
⚠️ Copying and pasting is disabled. Please manually type the answer.
</div>
<!-- Server-side error -->
<?php if (isset($errors['captcha'])): ?>
<div class="invalid-feedback d-block"><?= esc($errors['captcha']) ?></div>
<?php endif; ?>
</div>
<div class="mb-3 d-grid">
<button type="submit" class="btn btn-success item">Create Account</button>
<a href="/" class="btn btn-secondary item">Back to Home</a>
</div>
</form>
<!-- Policy Modal -->
<div class="modal fade" id="policyModal" tabindex="-1" aria-labelledby="policyModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title text-success" id="policyModalLabel">School Policies</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<iframe src="<?= base_url('policy/school_policy') ?>" width="100%" height="400" frameborder="0"></iframe>
</div>
</div>
</div>
</div>
</div>
<?= $this->endSection() ?>
<?= $this->section('scripts') ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
// ---- Elements ----
const form = document.getElementById('registrationForm');
const isParentCheckbox = document.getElementById('is_parent');
const secondParentSection = document.getElementById('second-parent-section');
const noSecondParentCheckbox = document.getElementById('no_second_parent_info');
const secondParentFieldsWrapper = document.getElementById('second-parent-fields');
// Policy button: open modal via JS only (no data attributes, no href)
const policyBtn = document.getElementById('openPolicyLink');
if (policyBtn) {
['click', 'mousedown', 'mouseup'].forEach(evt =>
policyBtn.addEventListener(evt, ev => {
ev.preventDefault();
ev.stopPropagation();
})
);
policyBtn.addEventListener('click', function() {
const modalEl = document.getElementById('policyModal');
if (modalEl && typeof bootstrap !== 'undefined' && bootstrap.Modal) {
new bootstrap.Modal(modalEl).show();
}
});
}
// Second-parent inputs we consider "required" when applicable
const secondRequiredIds = ['second_firstname', 'second_lastname', 'second_email', 'second_cellphone'];
const secondRequiredEls = secondRequiredIds.map(id => document.getElementById(id)).filter(Boolean);
// Phone inputs (yours + second parent)
const phoneInputs = document.querySelectorAll('input[type="tel"], #cellphone, #second_cellphone');
// ---- Helpers ----
function setSecondParentFieldsRequired(enable) {
secondRequiredEls.forEach(el => {
if (!el) return;
if (enable) {
el.setAttribute('required', 'required');
el.classList.add('second-required');
} else {
el.removeAttribute('required');
el.classList.remove('second-required');
el.classList.remove('is-invalid');
}
});
}
function enableDisableSecondParentFields(disable) {
const inputs = secondParentFieldsWrapper ? secondParentFieldsWrapper.querySelectorAll('input, select') : [];
inputs.forEach(el => {
el.disabled = disable;
if (disable) el.classList.remove('is-invalid');
});
}
function clearSecondParentFields() {
if (!secondParentFieldsWrapper) return;
secondParentFieldsWrapper.querySelectorAll('input').forEach(el => {
el.value = '';
});
// Clear inline error text if present
const errIds = ['second_cellphone-error', 'second_email-error'];
errIds.forEach(id => {
const e = document.getElementById(id);
if (e) e.textContent = '';
});
}
function showHideSecondParentSection(show) {
if (!secondParentSection) return;
secondParentSection.style.display = show ? 'block' : 'none';
}
// ---- Behaviors per your rules ----
// 1) Default: DO NOT auto-check "no_second_parent_info". Keep it unchecked.
if (noSecondParentCheckbox) noSecondParentCheckbox.checked = false;
// 2) Toggle logic when main "is_parent" changes
function onIsParentChanged() {
if (isParentCheckbox.checked) {
// Parent -> show section. Now whether we require fields depends on the "no info" switch.
showHideSecondParentSection(true);
if (noSecondParentCheckbox && noSecondParentCheckbox.checked) {
// No info: ignore second parent data
setSecondParentFieldsRequired(false);
enableDisableSecondParentFields(true);
clearSecondParentFields();
if (secondParentFieldsWrapper) secondParentFieldsWrapper.style.display = 'none';
} else {
// Info required
setSecondParentFieldsRequired(true);
enableDisableSecondParentFields(false);
if (secondParentFieldsWrapper) secondParentFieldsWrapper.style.display = 'block';
}
} else {
// Not a parent -> ignore second parent data completely
setSecondParentFieldsRequired(false);
enableDisableSecondParentFields(true);
clearSecondParentFields();
showHideSecondParentSection(false);
// IMPORTANT: do NOT auto-toggle the "no_second_parent_info" checkbox here.
}
}
// 3) Toggle logic when "no_second_parent_info" changes
function onNoSecondParentChanged() {
// If not parent, ignore entirely
if (!isParentCheckbox.checked) {
setSecondParentFieldsRequired(false);
enableDisableSecondParentFields(true);
clearSecondParentFields();
showHideSecondParentSection(false);
return;
}
// Parent is checked:
if (noSecondParentCheckbox.checked) {
// No info -> ignore second parent data
setSecondParentFieldsRequired(false);
enableDisableSecondParentFields(true);
clearSecondParentFields();
if (secondParentFieldsWrapper) secondParentFieldsWrapper.style.display = 'none';
} else {
// Require info
setSecondParentFieldsRequired(true);
enableDisableSecondParentFields(false);
if (secondParentFieldsWrapper) secondParentFieldsWrapper.style.display = 'block';
}
}
// ---- Phone formatting / validation ----
phoneInputs.forEach(input => {
input.addEventListener('input', function(e) {
const raw = e.target.value.replace(/\D/g, '').slice(0, 10);
let formatted = raw;
if (raw.length > 3) formatted = raw.slice(0, 3) + '-' + raw.slice(3);
if (raw.length > 6) formatted = formatted.slice(0, 7) + '-' + raw.slice(6);
e.target.value = formatted;
const errorElement = document.getElementById(input.id + '-error');
if (raw.length === 10 || input.disabled || !input.hasAttribute('required')) {
input.classList.remove('is-invalid');
if (errorElement) errorElement.textContent = '';
} else {
input.classList.add('is-invalid');
if (errorElement) errorElement.textContent = 'Please enter a valid 10-digit phone number.';
}
});
input.addEventListener('blur', function(e) {
const raw = e.target.value.replace(/\D/g, '');
const errorElement = document.getElementById(input.id + '-error');
if (raw.length !== 10 && !input.disabled && input.hasAttribute('required')) {
input.classList.add('is-invalid');
if (errorElement) errorElement.textContent = 'Please enter a valid 10-digit phone number.';
} else {
input.classList.remove('is-invalid');
if (errorElement) errorElement.textContent = '';
}
});
});
// ---- Event wiring ----
if (isParentCheckbox) isParentCheckbox.addEventListener('change', onIsParentChanged);
if (noSecondParentCheckbox) noSecondParentCheckbox.addEventListener('change', onNoSecondParentChanged);
// ---- Initial state ----
// Start with section hidden and inputs disabled, not required.
showHideSecondParentSection(false);
setSecondParentFieldsRequired(false);
enableDisableSecondParentFields(true);
// ---- Final pre-submit guard ----
if (form) {
form.addEventListener('submit', function(e) {
let ok = true;
// Rule 1: First, check Parent/Guardian
if (!isParentCheckbox.checked) {
// Not a parent -> ignore second parent data
setSecondParentFieldsRequired(false);
enableDisableSecondParentFields(true);
clearSecondParentFields();
showHideSecondParentSection(false);
} else {
// Is a parent
if (noSecondParentCheckbox && noSecondParentCheckbox.checked) {
// No second parent info -> ignore second parent data
setSecondParentFieldsRequired(false);
enableDisableSecondParentFields(true);
clearSecondParentFields();
} else {
// Require second parent info
setSecondParentFieldsRequired(true);
enableDisableSecondParentFields(false);
// Validate second required fields
for (const el of secondRequiredEls) {
if (!el) continue;
const val = (el.value || '').trim();
if (!val) {
el.classList.add('is-invalid');
ok = false;
} else {
el.classList.remove('is-invalid');
}
if (el.id === 'second_cellphone') {
const raw = val.replace(/\D/g, '');
if (raw.length !== 10) {
el.classList.add('is-invalid');
const err = document.getElementById('second_cellphone-error');
if (err) err.textContent = 'Please enter a valid 10-digit phone number.';
ok = false;
}
}
if (el.id === 'second_email') {
const emailOk = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val);
if (!emailOk) {
el.classList.add('is-invalid');
const err = document.getElementById('second_email-error');
if (err) err.textContent = 'Please enter a valid email address.';
ok = false;
}
}
}
}
}
// Validate all required phone inputs (primary + any enabled second)
document.querySelectorAll('input[type="tel"]').forEach(input => {
const raw = (input.value || '').replace(/\D/g, '');
if (input.hasAttribute('required') && !input.disabled && raw.length !== 10) {
input.classList.add('is-invalid');
const err = document.getElementById(input.id + '-error');
if (err) err.textContent = 'Please enter a valid 10-digit phone number.';
ok = false;
}
});
if (!ok) {
e.preventDefault();
const firstError = document.querySelector('.is-invalid');
if (firstError) firstError.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
});
}
});
</script>
<?= $this->endSection() ?>
+114
View File
@@ -0,0 +1,114 @@
<?= $this->extend('layout/register_layout') ?>
<?= $this->section('content') ?>
<div class="registration-form container mt-5 mb-5 text-center">
<?php if (session()->has('error')): ?>
<p style="color: red;"><?= session('error') ?></p>
<?php endif; ?>
<form action="<?= site_url('user/processResetPassword'); ?>" method="post" onsubmit="return validatePassword()">
<?= csrf_field() ?>
<div class="text-center mb-4">
<a href="<?= base_url('/') ?>">
<img src="<?= base_url('assets/images/alrahma_logo.png') ?>" alt="" style="width: 180px; height: 120px;"></a>
</div>
<h3 class="text-center text-success" style="font-family: Arial, sans-serif;">Reset Your Password</h3>
<br>
<input type="hidden" name="token" value="<?= esc($token); ?>">
<!-- Password Field -->
<div class="form-group position-relative mb-4 text-start">
<label for="password"></label>
<input type="password"
class="form-control item pe-5"
id="password"
name="password"
placeholder="Enter new password"
maxlength="30"
required>
<span class="position-absolute top-50 end-0 translate-middle-y me-3"
onclick="togglePasswordVisibility('password', this)"
style="cursor: pointer;">
<i class="fa-solid fa-eye"></i>
</span>
<small id="passwordHelp" class="text-danger d-none">
Password must be at least 8 characters, include uppercase, lowercase, number, and special character.
</small>
</div>
<!-- Confirm Password Field -->
<div class="form-group position-relative mb-4 text-start">
<label for="password_confirm"></label>
<input type="password"
class="form-control item pe-5"
id="password_confirm"
name="pass_confirm"
placeholder="Confirm new password"
maxlength="30"
required>
<span class="position-absolute top-50 end-0 translate-middle-y me-3"
onclick="togglePasswordVisibility('password_confirm', this)"
style="cursor: pointer;">
<i class="fa-solid fa-eye"></i>
</span>
<small id="confirmPasswordHelp" class="text-danger d-none">
Passwords do not match.
</small>
</div>
<div class="mb-3 d-grid">
<button type="submit" class="btn btn-success item">Reset Password</button>
</div>
</form>
</div>
<?= $this->endSection() ?>
<?= $this->section('scripts') ?>
<script>
function togglePasswordVisibility(inputId, iconElement) {
const input = document.getElementById(inputId);
const icon = iconElement.querySelector('i');
if (input.type === 'password') {
input.type = 'text';
icon.classList.remove('fa-eye');
icon.classList.add('fa-eye-slash');
} else {
input.type = 'password';
icon.classList.remove('fa-eye-slash');
icon.classList.add('fa-eye');
}
}
function validatePassword() {
const password = document.getElementById('password').value;
const passwordConfirm = document.getElementById('password_confirm').value;
const passwordHelp = document.getElementById('passwordHelp');
const confirmPasswordHelp = document.getElementById('confirmPasswordHelp');
const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@\-=\+*#$%&!?])[A-Za-z\d@\-=\+*#$%&!?]{8,}$/;
let valid = true;
// Validate password format
if (!passwordRegex.test(password)) {
passwordHelp.classList.remove('d-none');
valid = false;
} else {
passwordHelp.classList.add('d-none');
}
// Validate matching confirm password
if (password !== passwordConfirm) {
confirmPasswordHelp.classList.remove('d-none');
valid = false;
} else {
confirmPasswordHelp.classList.add('d-none');
}
return valid;
}
</script>
<?= $this->endSection() ?>
+151
View File
@@ -0,0 +1,151 @@
<?= $this->extend('layout/register_layout') ?>
<?= $this->section('content') ?>
<div class="registration-form container mt-5 mb-5">
<form method="post" action="<?= base_url('/user/save_password') ?>" onsubmit="return validatePassword()" autocomplete="off">
<?= csrf_field(); ?>
<div class="text-center mb-4">
<a href="<?= base_url('/') ?>">
<img src="<?= base_url('assets/images/alrahma_logo.png') ?>" alt="" style="width: 180px; height: 120px;">
</a>
</div>
<h3 class="text-center text-success" style="font-family: Arial, sans-serif;">Create Your Password</h3>
<br>
<input type="hidden" name="user_id" value="<?= esc($userId); ?>" required>
<input type="hidden" name="token" value="<?= esc($token); ?>" required>
<!-- New Password -->
<div class="mb-3">
<div class="input-with-icon">
<input type="password"
class="form-control item"
id="password"
name="password"
placeholder="Enter new password"
maxlength="40"
required
autocomplete="new-password"
oncopy="return false"
oncut="return false"
onpaste="return false">
<span class="toggle-password" onclick="togglePassword('password', this)">
<i class="fa-solid fa-eye"></i>
</span>
</div>
<small id="passwordHelp" class="text-danger d-none">
Password must be at least 8 characters long, contain a number, an uppercase letter, a lowercase letter, and one special character: @, -, =, +, *, #, $, %, &, !
</small>
<small id="passwordCopyWarning" class="text-muted d-none">
Copy and paste are disabled for security reasons.
</small>
</div>
<!-- Confirm Password -->
<div class="mb-3">
<div class="input-with-icon">
<input type="password"
class="form-control item"
id="password_confirm"
name="password_confirm"
placeholder="Confirm new password"
maxlength="40"
required
autocomplete="new-password"
oncopy="return false"
oncut="return false"
onpaste="return false">
<span class="toggle-password" onclick="togglePassword('password_confirm', this)">
<i class="fa-solid fa-eye"></i>
</span>
</div>
<small id="confirmPasswordHelp" class="text-danger d-none">
Passwords do not match.
</small>
<small id="confirmCopyWarning" class="text-muted d-none">
Copy and paste are disabled for security reasons.
</small>
</div>
<!-- Submit -->
<div class="mb-3 d-grid">
<button type="submit" class="btn btn-success item">Save Password</button>
</div>
</form>
</div>
<?= $this->endSection() ?>
<?= $this->section('scripts') ?>
<script>
document.addEventListener('DOMContentLoaded', () => {
const showWarning = (inputId, warningId) => {
const input = document.getElementById(inputId);
const warning = document.getElementById(warningId);
['copy', 'paste', 'cut'].forEach(eventName => {
input.addEventListener(eventName, (e) => {
e.preventDefault();
warning.classList.remove('d-none');
// Clear existing timeout if still active
if (warning.timeout) clearTimeout(warning.timeout);
// Hide again after 4 seconds
warning.timeout = setTimeout(() => {
warning.classList.add('d-none');
}, 4000);
});
});
};
showWarning('password', 'passwordCopyWarning');
showWarning('password_confirm', 'confirmCopyWarning');
});
function togglePassword(fieldId, iconContainer) {
const input = document.getElementById(fieldId);
const icon = iconContainer.querySelector('i');
if (input.type === 'password') {
input.type = 'text';
icon.classList.remove('fa-eye');
icon.classList.add('fa-eye-slash');
} else {
input.type = 'password';
icon.classList.remove('fa-eye-slash');
icon.classList.add('fa-eye');
}
}
function validatePassword() {
const password = document.getElementById('password').value;
const passwordConfirm = document.getElementById('password_confirm').value;
const passwordHelp = document.getElementById('passwordHelp');
const confirmPasswordHelp = document.getElementById('confirmPasswordHelp');
const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@\-=\+*#$%&!?])[A-Za-z\d@\-=\+*#$%&!?]{8,}$/;
let valid = true;
if (!passwordRegex.test(password)) {
passwordHelp.classList.remove('d-none');
valid = false;
} else {
passwordHelp.classList.add('d-none');
}
if (password !== passwordConfirm) {
confirmPasswordHelp.classList.remove('d-none');
valid = false;
} else {
confirmPasswordHelp.classList.add('d-none');
}
return valid;
}
</script>
<?= $this->endSection() ?>
+598
View File
@@ -0,0 +1,598 @@
<?= $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() ?>
+44
View File
@@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Welcome Back!</title>
<link rel="stylesheet" href="/css/styles.css"> <!-- Adjust the path accordingly -->
<link href="<?= base_url('assets/images/favicon.ico') ?>" rel="icon">
</head>
<body>
<div class="container">
<div class="left-side">
<img src="<?= base_url('assets/images/logo.png') ?>" alt="School Logo" class="logo"> <!-- Adjust the path accordingly -->
<div>Al Rahma Sunday School</div>
</div>
<div class="right-side">
<div class="welcome-back-container">
<h2>Welcome back!</h2>
<p>As user have assigned multiple roles in the Al Rahma Sunday School's portal, please select a role you
want to login with:</p>
<form method="post" action="/user/select_role">
<?= csrf_field(); ?>
<div class="form-group">
<label for="role">Role:</label>
<select id="role" name="role">
<option value="parent">Parent / Guardian</option>
<option value="teacher">Teacher</option>
<option value="admin">Admin</option>
<option value="admin">Guest</option>
<option value="administrator">Administrator</option>
</select>
</div>
<div class="form-actions">
<button type="submit">Login</button>
</div>
</form>
</div>
</div>
</div>
</body>
</html>