recreate project
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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() ?>
|
||||
@@ -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() ?>
|
||||
@@ -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; ?>
|
||||
@@ -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() ?>
|
||||
@@ -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">2–30 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">2–30 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">2–30 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">2–15 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">2–30 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">2–30 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() ?>
|
||||
@@ -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() ?>
|
||||
@@ -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() ?>
|
||||
@@ -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() ?>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user