Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2d5b151234 | |||
| ede7fd947a | |||
| 485341875a | |||
| 2ad7dc1170 | |||
| 9e2f858343 | |||
| c70f6bdc6e | |||
| 6936a822c8 | |||
| 228824182a | |||
| 1d964c79de | |||
| 02fd6e4863 | |||
| 46769e8b27 | |||
| 88772c3ea0 | |||
| 8d644a2c85 | |||
| e81a1832ad | |||
| dbfd72c2f9 | |||
| 5b11e2d859 | |||
| 6ae90d757b | |||
| 849a4579e9 | |||
| 8d83cf84ab | |||
| e1fa1ded64 | |||
| 2eae9819fe | |||
| cf5314eaf6 | |||
| b689570052 | |||
| 332b0d1007 | |||
| 140be9922d | |||
| 361d0c0d3a | |||
| 0f8ad86b4f |
@@ -55,6 +55,7 @@ Thumbs.db
|
||||
/build/
|
||||
/build.tar.gz
|
||||
/builds
|
||||
/_chunks/
|
||||
/phpunit.xml.cache
|
||||
/.phpunit.result.cache
|
||||
/writable/reports/*
|
||||
|
||||
Binary file not shown.
@@ -11,7 +11,7 @@ class DeleteInactiveUsers extends BaseCommand
|
||||
{
|
||||
protected $group = 'Maintenance';
|
||||
protected $name = 'users:delete-inactive-users';
|
||||
protected $description = 'Delete users that are inactive and created more than 15 minutes ago, along with their entries in the parents table and user_roles table if applicable.';
|
||||
protected $description = 'Delete unverified inactive registrations created more than 15 minutes ago, along with their entries in the parents table and user_roles table if applicable.';
|
||||
|
||||
public function run(array $params)
|
||||
{
|
||||
@@ -24,11 +24,12 @@ class DeleteInactiveUsers extends BaseCommand
|
||||
log_message('debug', 'Cutoff time for deletion: ' . $cutoffTime);
|
||||
|
||||
// ─────────────────────────────────────────────────────
|
||||
// 1 Fetch inactive users older than 15 min
|
||||
// 1 Fetch unfinished registrations older than 15 min
|
||||
// ─────────────────────────────────────────────────────
|
||||
$users = $db->table('users')
|
||||
->select('id, firstname, lastname, email, created_at')
|
||||
->where('status', 'Inactive')
|
||||
->where('is_verified', 0)
|
||||
->where('created_at <', $cutoffTime)
|
||||
->get()
|
||||
->getResultArray();
|
||||
@@ -102,4 +103,4 @@ class DeleteInactiveUsers extends BaseCommand
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
+31
-5
@@ -222,6 +222,30 @@ $routes->post('/user/store', 'View\UserController::store');
|
||||
$routes->get('/thankyou', 'View\UserController::thankyou'); // Thank you page route
|
||||
$routes->get('/', 'View\UserController::home'); // Home page route
|
||||
$routes->get('/about', 'View\UserController::about'); // About page route
|
||||
$routes->get('/careers', 'View\JobPostingController::publicIndex'); // Careers page route
|
||||
$routes->get('/careers/application-received', 'View\JobPostingController::applicationReceived');
|
||||
$routes->get('/careers/(:segment)/details', 'View\JobPostingController::trackDetailsClick/$1');
|
||||
$routes->get('/careers/(:segment)', 'View\JobPostingController::show/$1');
|
||||
$routes->get('/careers/(:segment)/apply', 'View\JobPostingController::apply/$1');
|
||||
$routes->post('/careers/(:segment)/apply', 'View\JobPostingController::submitApplication/$1');
|
||||
$routes->group('administrator/job-postings', ['filter' => 'auth:administrator|administrative staff|principal'], static function ($routes) {
|
||||
$routes->get('', 'View\JobPostingController::positions');
|
||||
$routes->get('templates', 'View\JobPostingController::templates');
|
||||
$routes->get('templates/new', 'View\JobPostingController::newTemplate');
|
||||
$routes->post('templates', 'View\JobPostingController::createTemplate');
|
||||
$routes->get('templates/(:segment)/edit', 'View\JobPostingController::editTemplate/$1');
|
||||
$routes->post('templates/(:segment)', 'View\JobPostingController::updateTemplate/$1');
|
||||
$routes->post('templates/(:segment)/archive', 'View\JobPostingController::archiveTemplate/$1');
|
||||
$routes->post('templates/versions/(:segment)/restore', 'View\JobPostingController::restoreTemplateVersion/$1');
|
||||
$routes->get('positions', 'View\JobPostingController::positions');
|
||||
$routes->get('positions/new', 'View\JobPostingController::newPosition');
|
||||
$routes->post('positions', 'View\JobPostingController::createPosition');
|
||||
$routes->get('positions/(:segment)/edit', 'View\JobPostingController::editPosition/$1');
|
||||
$routes->post('positions/(:segment)', 'View\JobPostingController::updatePosition/$1');
|
||||
$routes->get('applications', 'View\JobPostingController::applications');
|
||||
$routes->post('applications/(:segment)', 'View\JobPostingController::updateApplication/$1');
|
||||
$routes->get('applications/(:segment)/resume', 'View\JobPostingController::resume/$1');
|
||||
});
|
||||
$routes->get('/classes', 'View\UserController::classes'); // Classes page route
|
||||
$routes->get('/contact', 'View\UserController::contact'); // Contact Us page route
|
||||
$routes->post('/user/login', 'AuthController::login');
|
||||
@@ -917,11 +941,12 @@ $routes->post('/parent/edit_emergency_contact/(:num)', 'View\ParentController::e
|
||||
|
||||
|
||||
/*management navigation bar*/
|
||||
$routes->get('nav-builder', 'View\NavBuilderController::index', ['filter' => 'auth']);
|
||||
$routes->get('api/nav-builder', 'View\NavBuilderController::data', ['filter' => 'auth']);
|
||||
$routes->post('nav-builder/save', 'View\NavBuilderController::save', ['filter' => 'auth']);
|
||||
$routes->get('nav-builder/delete/(:num)', 'View\NavBuilderController::delete/$1', ['filter' => 'auth']);
|
||||
$routes->post('nav-builder/reorder', 'View\NavBuilderController::reorder', ['filter' => 'auth']);
|
||||
$routes->get('nav-builder', 'View\NavBuilderController::index', ['filter' => 'auth:administrator']);
|
||||
$routes->get('api/nav-builder', 'View\NavBuilderController::data', ['filter' => 'auth:administrator']);
|
||||
$routes->post('nav-builder/save', 'View\NavBuilderController::save', ['filter' => 'auth:administrator']);
|
||||
$routes->get('nav-builder/delete/(:num)', 'View\NavBuilderController::delete/$1', ['filter' => 'auth:administrator']);
|
||||
$routes->post('nav-builder/reorder', 'View\NavBuilderController::reorder', ['filter' => 'auth:administrator']);
|
||||
$routes->post('nav-builder/role-access', 'View\NavBuilderController::roleAccess', ['filter' => 'auth:administrator']);
|
||||
|
||||
|
||||
// Teacher book distribution is the only inventory path teachers may access directly.
|
||||
@@ -1267,6 +1292,7 @@ $routes->get('/landing_page/admin_dashboard', 'View\LandingPageController::admin
|
||||
$routes->get('/teacher_dashboard', 'View\LandingPageController::teacher', ['filter' => 'auth:teacher_dashboard,read']);
|
||||
$routes->get('/landing_page/student_dashboard', 'View\LandingPageController::student', ['filter' => 'auth:student_dashboard,read']);
|
||||
$routes->get('/parent_dashboard', 'View\LandingPageController::parentDashboard', ['filter' => 'auth:parent_dashboard,read']);
|
||||
$routes->post('/parent_dashboard/job-openings-popup', 'View\LandingPageController::hideJobOpeningsPopup', ['filter' => 'auth:parent_dashboard,read']);
|
||||
$routes->get('/landing_page/guest_dashboard', 'View\LandingPageController::guest', ['filter' => 'auth:guest_dashboard,read']);
|
||||
$routes->get('/dashboard', 'View\LandingPageController::index');
|
||||
$routes->get('/access_denied', 'ErrorController::accessDenied');
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Controllers;
|
||||
use App\Models\LoginActivityModel;
|
||||
use App\Models\UserModel;
|
||||
use App\Models\UserRoleModel;
|
||||
use App\Models\UserAccessProfileModel;
|
||||
use CodeIgniter\Events\Events;
|
||||
use App\Models\IpAttemptModel;
|
||||
use App\Models\PasswordResetModel;
|
||||
@@ -212,6 +213,7 @@ class AuthController extends BaseController
|
||||
|
||||
// Fetch roles
|
||||
$roleNames = $this->getUserRoleNames((int) $user['id']);
|
||||
$accessProfile = $this->accessProfileForUser((int) $user['id'], $roleNames);
|
||||
|
||||
// Build roles map (object with keys per example)
|
||||
$rolesMap = [];
|
||||
@@ -248,6 +250,10 @@ class AuthController extends BaseController
|
||||
'id' => (int) $user['id'],
|
||||
'name' => $payload['name'],
|
||||
'roles' => (object) $rolesMap,
|
||||
'primary_category' => $accessProfile['primary_category'],
|
||||
'is_admin' => $accessProfile['is_admin'],
|
||||
'is_teacher' => $accessProfile['is_teacher'],
|
||||
'is_parent' => $accessProfile['is_parent'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
@@ -274,6 +280,10 @@ class AuthController extends BaseController
|
||||
'type' => session()->get('user_type'),
|
||||
'roles' => $roles,
|
||||
'role' => $activeRole,
|
||||
'primary_category' => session()->get('primary_category'),
|
||||
'is_admin' => (bool) session()->get('is_admin'),
|
||||
'is_teacher' => (bool) session()->get('is_teacher'),
|
||||
'is_parent' => (bool) session()->get('is_parent'),
|
||||
],
|
||||
]);
|
||||
}
|
||||
@@ -371,6 +381,7 @@ class AuthController extends BaseController
|
||||
'iat' => $now,
|
||||
'exp' => $exp,
|
||||
];
|
||||
$accessProfile = $this->accessProfileForUser((int) $userId, $payload['roles']);
|
||||
|
||||
$secret = require_env('JWT_SECRET');
|
||||
$token = jwt_encode($payload, $secret, 'HS256');
|
||||
@@ -384,6 +395,10 @@ class AuthController extends BaseController
|
||||
'name' => $payload['name'],
|
||||
'email' => $userData['email'],
|
||||
'roles' => $payload['roles'],
|
||||
'primary_category' => $accessProfile['primary_category'],
|
||||
'is_admin' => $accessProfile['is_admin'],
|
||||
'is_teacher' => $accessProfile['is_teacher'],
|
||||
'is_parent' => $accessProfile['is_parent'],
|
||||
],
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
@@ -477,11 +492,17 @@ class AuthController extends BaseController
|
||||
protected function getUserRoleNames(int $userId): array
|
||||
{
|
||||
$userRoleModel = new UserRoleModel();
|
||||
$rolesRows = $userRoleModel->select('roles.name')
|
||||
$db = \Config\Database::connect();
|
||||
$builder = $userRoleModel->select('roles.name')
|
||||
->join('roles', 'roles.id = user_roles.role_id')
|
||||
->where('user_roles.user_id', $userId)
|
||||
->get()
|
||||
->getResultArray();
|
||||
->where('COALESCE(roles.is_active, 1) = 1', null, false);
|
||||
|
||||
if ($db->fieldExists('deleted_at', 'user_roles')) {
|
||||
$builder->where('user_roles.deleted_at', null);
|
||||
}
|
||||
|
||||
$rolesRows = $builder->get()->getResultArray();
|
||||
|
||||
return array_column($rolesRows, 'name');
|
||||
}
|
||||
@@ -565,11 +586,17 @@ class AuthController extends BaseController
|
||||
private function loginUser($user, ?string $redirectTo = null)
|
||||
{
|
||||
$userRoleModel = new UserRoleModel();
|
||||
$roles = $userRoleModel->select('roles.name')
|
||||
$db = \Config\Database::connect();
|
||||
$rolesBuilder = $userRoleModel->select('roles.name')
|
||||
->join('roles', 'roles.id = user_roles.role_id')
|
||||
->where('user_roles.user_id', $user['id'])
|
||||
->get()
|
||||
->getResultArray();
|
||||
->where('COALESCE(roles.is_active, 1) = 1', null, false);
|
||||
|
||||
if ($db->fieldExists('deleted_at', 'user_roles')) {
|
||||
$rolesBuilder->where('user_roles.deleted_at', null);
|
||||
}
|
||||
|
||||
$roles = $rolesBuilder->get()->getResultArray();
|
||||
|
||||
if (empty($roles)) {
|
||||
log_message('error', 'No roles found for user ID: ' . $user['id']);
|
||||
@@ -577,6 +604,7 @@ class AuthController extends BaseController
|
||||
}
|
||||
|
||||
$roleNames = array_column($roles, 'name');
|
||||
$accessProfile = $this->accessProfileForUser((int) $user['id'], $roleNames);
|
||||
|
||||
session()->regenerate(true);
|
||||
session()->set([
|
||||
@@ -588,6 +616,10 @@ class AuthController extends BaseController
|
||||
'login_time' => time(),
|
||||
'last_activity' => time(),
|
||||
'roles' => $roleNames,
|
||||
'primary_category' => $accessProfile['primary_category'],
|
||||
'is_admin' => $accessProfile['is_admin'],
|
||||
'is_teacher' => $accessProfile['is_teacher'],
|
||||
'is_parent' => $accessProfile['is_parent'],
|
||||
'semester' => $this->semester,
|
||||
'school_year' => $this->schoolYear,
|
||||
]);
|
||||
@@ -612,6 +644,32 @@ class AuthController extends BaseController
|
||||
|
||||
}
|
||||
|
||||
private function accessProfileForUser(int $userId, array $roleNames): array
|
||||
{
|
||||
try {
|
||||
$profile = model(UserAccessProfileModel::class)->getForUser($userId);
|
||||
if ($profile !== null) {
|
||||
return [
|
||||
'primary_category' => (string) ($profile['primary_category'] ?? UserAccessProfileModel::CATEGORY_GUEST),
|
||||
'is_admin' => (bool) ($profile['is_admin'] ?? false),
|
||||
'is_teacher' => (bool) ($profile['is_teacher'] ?? false),
|
||||
'is_parent' => (bool) ($profile['is_parent'] ?? false),
|
||||
];
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
log_message('warning', 'Unable to load user access profile: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
$flags = UserAccessProfileModel::flagsForRoles($roleNames);
|
||||
|
||||
return [
|
||||
'primary_category' => UserAccessProfileModel::primaryCategory($flags),
|
||||
'is_admin' => (bool) $flags['is_admin'],
|
||||
'is_teacher' => (bool) $flags['is_teacher'],
|
||||
'is_parent' => (bool) $flags['is_parent'],
|
||||
];
|
||||
}
|
||||
|
||||
private function applyStylePreferences(int $userId): void
|
||||
{
|
||||
if (!$userId) {
|
||||
|
||||
@@ -9,6 +9,7 @@ use App\Models\StudentModel;
|
||||
use App\Models\TeacherModel;
|
||||
use App\Models\ClassSectionModel;
|
||||
use App\Models\ConfigurationModel;
|
||||
use App\Support\Enrollment\EnrollmentEligibility;
|
||||
use Config\Database;
|
||||
|
||||
class AssignmentController extends BaseController
|
||||
@@ -177,11 +178,13 @@ class AssignmentController extends BaseController
|
||||
continue;
|
||||
}
|
||||
|
||||
$calculatedAge = EnrollmentEligibility::ageOnSeptemberFirst($student['dob'] ?? null, $year);
|
||||
|
||||
$students[] = [
|
||||
'id' => (int)$student['id'],
|
||||
'firstname' => esc($student['firstname']),
|
||||
'lastname' => esc($student['lastname']),
|
||||
'age' => esc($student['age']),
|
||||
'age' => esc((string)($calculatedAge ?? ($student['age'] ?? ''))),
|
||||
'gender' => esc($student['gender']),
|
||||
'registration_grade' => esc($student['registration_grade']),
|
||||
'photo_consent' => esc($student['photo_consent'] ? 'Yes' : 'No'),
|
||||
|
||||
@@ -122,6 +122,10 @@ class BadgesController extends PrintablesBaseController
|
||||
$roleResolved = $postedRole !== null ? $postedRole : $resolveRole($info);
|
||||
$roleResolved = $formatRole((string)$roleResolved);
|
||||
$info['role_resolved'] = $roleResolved;
|
||||
if ($postedRole !== null && trim((string)$postedRole) !== '') {
|
||||
$info['roles_raw'] = $roleResolved;
|
||||
$info['role_name_raw'] = $roleResolved;
|
||||
}
|
||||
|
||||
// Prefer posted class name if provided (keeps what the user saw)
|
||||
if (!empty($classesMap[$userId])) {
|
||||
|
||||
@@ -522,7 +522,7 @@ class FilesController extends Controller
|
||||
$roles[] = $activeRole;
|
||||
}
|
||||
|
||||
foreach (['administrator', 'administrative staff', 'principal', 'teacher', 'teacher_assistant'] as $role) {
|
||||
foreach (['administrator', 'administrative staff', 'principal', 'teacher', 'teacher_assistant', 'head fa', 'head of fa', 'head_of_fa', 'financial_contributor'] as $role) {
|
||||
if (in_array($role, $roles, true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2293,6 +2293,10 @@ private function getGradeLevel($grade): array
|
||||
'administrative staff',
|
||||
'principal',
|
||||
'admin',
|
||||
'head fa',
|
||||
'head of fa',
|
||||
'head_of_fa',
|
||||
'financial_contributor',
|
||||
]);
|
||||
|
||||
if ($userId <= 0 || (! $isStaff && $userId !== $parentId)) {
|
||||
|
||||
@@ -0,0 +1,534 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\View;
|
||||
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\ApplicationModel;
|
||||
use App\Models\JobPositionModel;
|
||||
use App\Models\JobTemplateModel;
|
||||
use App\Models\JobTemplateVersionModel;
|
||||
use App\Services\EmailService;
|
||||
use App\Services\PhoneFormatterService;
|
||||
|
||||
class JobPostingController extends BaseController
|
||||
{
|
||||
private const ADMIN_FILTER_ROUTE = 'administrator/job-postings';
|
||||
private array $positionStatuses = ['draft', 'open', 'closed', 'filled'];
|
||||
private array $applicationStatuses = ['new', 'reviewed', 'contacted', 'rejected', 'hired'];
|
||||
|
||||
protected JobTemplateModel $templates;
|
||||
protected JobTemplateVersionModel $templateVersions;
|
||||
protected JobPositionModel $positions;
|
||||
protected ApplicationModel $applications;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
helper(['form', 'url', 'text']);
|
||||
$this->templates = new JobTemplateModel();
|
||||
$this->templateVersions = new JobTemplateVersionModel();
|
||||
$this->positions = new JobPositionModel();
|
||||
$this->applications = new ApplicationModel();
|
||||
}
|
||||
|
||||
public function publicIndex()
|
||||
{
|
||||
return view('careers', [
|
||||
'positions' => $this->positions->openPositions(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(string $positionId)
|
||||
{
|
||||
$position = $this->positions->where('position_id', $positionId)->where('status', 'open')->first();
|
||||
if (!$position) {
|
||||
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound('Position not found');
|
||||
}
|
||||
|
||||
return view('jobs/show', ['position' => $position]);
|
||||
}
|
||||
|
||||
public function trackDetailsClick(string $positionId)
|
||||
{
|
||||
$position = $this->positions->where('position_id', $positionId)->where('status', 'open')->first();
|
||||
if (!$position) {
|
||||
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound('Position not found');
|
||||
}
|
||||
|
||||
$this->positions->recordDetailsClick($positionId);
|
||||
|
||||
return redirect()->to(site_url('careers/' . rawurlencode($positionId)));
|
||||
}
|
||||
|
||||
public function apply(string $positionId)
|
||||
{
|
||||
$position = $this->positions->where('position_id', $positionId)->where('status', 'open')->first();
|
||||
if (!$position) {
|
||||
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound('Position not found');
|
||||
}
|
||||
|
||||
return view('jobs/apply', ['position' => $position]);
|
||||
}
|
||||
|
||||
public function submitApplication(string $positionId)
|
||||
{
|
||||
$position = $this->positions->where('position_id', $positionId)->where('status', 'open')->first();
|
||||
if (!$position) {
|
||||
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound('Position not found');
|
||||
}
|
||||
|
||||
$rules = [
|
||||
'first_name' => 'required|regex_match[/^[a-zA-Z\s-]+$/]|min_length[2]|max_length[30]',
|
||||
'last_name' => 'required|regex_match[/^[a-zA-Z\s-]+$/]|min_length[2]|max_length[30]',
|
||||
'email' => 'required|valid_email|max_length[50]',
|
||||
'phone' => 'required|regex_match[/^[\d\s\-\(\)\.]+$/]|min_length[10]|max_length[20]',
|
||||
'resume' => 'uploaded[resume]|max_size[resume,5120]|ext_in[resume,pdf,doc,docx]',
|
||||
];
|
||||
$messages = [
|
||||
'first_name' => [
|
||||
'regex_match' => 'First name may only contain letters, spaces, and dashes.',
|
||||
'min_length' => 'First name must be at least 2 characters.',
|
||||
'max_length' => 'First name must be 30 characters or fewer.',
|
||||
],
|
||||
'last_name' => [
|
||||
'regex_match' => 'Last name may only contain letters, spaces, and dashes.',
|
||||
'min_length' => 'Last name must be at least 2 characters.',
|
||||
'max_length' => 'Last name must be 30 characters or fewer.',
|
||||
],
|
||||
'email' => [
|
||||
'valid_email' => 'Please enter a valid email address.',
|
||||
'max_length' => 'Email must be 50 characters or fewer.',
|
||||
],
|
||||
'phone' => [
|
||||
'regex_match' => 'Please enter a valid 10-digit phone number, for example 123-456-7890.',
|
||||
'min_length' => 'Please enter a valid 10-digit phone number, for example 123-456-7890.',
|
||||
'max_length' => 'Please enter a valid 10-digit phone number, for example 123-456-7890.',
|
||||
],
|
||||
];
|
||||
|
||||
if (!$this->validate($rules, $messages)) {
|
||||
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
|
||||
}
|
||||
|
||||
$formattedPhone = (new PhoneFormatterService())->formatPhoneNumber((string) $this->request->getPost('phone'));
|
||||
if ($formattedPhone === null) {
|
||||
return redirect()->back()->withInput()->with('errors', [
|
||||
'phone' => 'Please enter a valid 10-digit phone number, for example 123-456-7890.',
|
||||
]);
|
||||
}
|
||||
|
||||
$file = $this->request->getFile('resume');
|
||||
$uploadDir = WRITEPATH . 'uploads/job_applications';
|
||||
if (!is_dir($uploadDir)) {
|
||||
mkdir($uploadDir, 0755, true);
|
||||
}
|
||||
|
||||
$newName = $file->getRandomName();
|
||||
$file->move($uploadDir, $newName);
|
||||
$relativePath = 'job_applications/' . $newName;
|
||||
|
||||
$application = [
|
||||
'application_id' => $this->newUuid(),
|
||||
'position_id' => $position['position_id'],
|
||||
'first_name' => trim((string) $this->request->getPost('first_name')),
|
||||
'last_name' => trim((string) $this->request->getPost('last_name')),
|
||||
'email' => trim((string) $this->request->getPost('email')),
|
||||
'phone' => $formattedPhone,
|
||||
'resume_file_url' => $relativePath,
|
||||
'status' => 'new',
|
||||
'submitted_at' => utc_now(),
|
||||
];
|
||||
|
||||
if (!$this->applications->insert($application)) {
|
||||
return redirect()->back()->withInput()->with('error', 'Unable to submit application.');
|
||||
}
|
||||
|
||||
$this->sendApplicationEmails($application, $position);
|
||||
|
||||
return redirect()->to(site_url('careers/application-received'))->with('success', 'Application submitted.');
|
||||
}
|
||||
|
||||
public function applicationReceived()
|
||||
{
|
||||
return view('jobs/received');
|
||||
}
|
||||
|
||||
public function templates()
|
||||
{
|
||||
return view('jobs/admin/templates', [
|
||||
'templates' => $this->templates->orderBy('updated_at', 'DESC')->findAll(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function newTemplate()
|
||||
{
|
||||
return view('jobs/admin/template_form', ['template' => null, 'versions' => []]);
|
||||
}
|
||||
|
||||
public function createTemplate()
|
||||
{
|
||||
$payload = $this->templatePayload();
|
||||
if ($payload === null) {
|
||||
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
|
||||
}
|
||||
|
||||
$payload['template_id'] = $this->newUuid();
|
||||
$payload['version'] = 1;
|
||||
$payload['is_active'] = 1;
|
||||
$payload['created_by'] = $this->currentUserId();
|
||||
|
||||
if (!$this->templates->insert($payload)) {
|
||||
return redirect()->back()->withInput()->with('error', 'Unable to create template.');
|
||||
}
|
||||
|
||||
$this->snapshotTemplate($payload);
|
||||
return redirect()->to(site_url(self::ADMIN_FILTER_ROUTE . '/templates'))->with('success', 'Template created.');
|
||||
}
|
||||
|
||||
public function editTemplate(string $templateId)
|
||||
{
|
||||
$template = $this->templates->find($templateId);
|
||||
if (!$template) {
|
||||
return redirect()->to(site_url(self::ADMIN_FILTER_ROUTE . '/templates'))->with('error', 'Template not found.');
|
||||
}
|
||||
|
||||
return view('jobs/admin/template_form', [
|
||||
'template' => $template,
|
||||
'versions' => $this->templateVersions->where('template_id', $templateId)->orderBy('version', 'DESC')->findAll(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function updateTemplate(string $templateId)
|
||||
{
|
||||
$template = $this->templates->find($templateId);
|
||||
if (!$template) {
|
||||
return redirect()->to(site_url(self::ADMIN_FILTER_ROUTE . '/templates'))->with('error', 'Template not found.');
|
||||
}
|
||||
|
||||
$payload = $this->templatePayload();
|
||||
if ($payload === null) {
|
||||
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
|
||||
}
|
||||
|
||||
if ($this->request->getPost('save_mode') === 'new_version') {
|
||||
$payload['version'] = ((int) ($template['version'] ?? 1)) + 1;
|
||||
} else {
|
||||
$payload['version'] = (int) ($template['version'] ?? 1);
|
||||
}
|
||||
|
||||
if (!$this->templates->update($templateId, $payload)) {
|
||||
return redirect()->back()->withInput()->with('error', 'Unable to update template.');
|
||||
}
|
||||
|
||||
$payload['template_id'] = $templateId;
|
||||
$this->snapshotTemplate($payload);
|
||||
return redirect()->to(site_url(self::ADMIN_FILTER_ROUTE . '/templates'))->with('success', 'Template updated.');
|
||||
}
|
||||
|
||||
public function archiveTemplate(string $templateId)
|
||||
{
|
||||
$this->templates->update($templateId, ['is_active' => 0]);
|
||||
return redirect()->to(site_url(self::ADMIN_FILTER_ROUTE . '/templates'))->with('success', 'Template archived.');
|
||||
}
|
||||
|
||||
public function restoreTemplateVersion(string $versionId)
|
||||
{
|
||||
$version = $this->templateVersions->find($versionId);
|
||||
if (!$version) {
|
||||
return redirect()->back()->with('error', 'Template version not found.');
|
||||
}
|
||||
|
||||
$payload = $this->templateVersionPayload($version);
|
||||
$payload['version'] = (int) $version['version'];
|
||||
$this->templates->update($version['template_id'], $payload);
|
||||
|
||||
return redirect()->to(site_url(self::ADMIN_FILTER_ROUTE . '/templates/' . $version['template_id'] . '/edit'))->with('success', 'Template version restored.');
|
||||
}
|
||||
|
||||
public function positions()
|
||||
{
|
||||
return view('jobs/admin/positions', [
|
||||
'positions' => $this->positions->adminPositions(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function newPosition()
|
||||
{
|
||||
$template = null;
|
||||
$templateId = (string) $this->request->getGet('template_id');
|
||||
if ($templateId !== '') {
|
||||
$template = $this->templates->find($templateId);
|
||||
}
|
||||
|
||||
return view('jobs/admin/position_form', [
|
||||
'position' => $template ? $this->templateVersionPayload($template) + ['template_id' => $templateId, 'status' => 'draft'] : null,
|
||||
'statuses' => $this->positionStatuses,
|
||||
'templates' => $this->templates->where('is_active', 1)->orderBy('title', 'ASC')->findAll(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function createPosition()
|
||||
{
|
||||
$payload = $this->positionPayload();
|
||||
if ($payload === null) {
|
||||
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
|
||||
}
|
||||
|
||||
$payload['position_id'] = $this->newUuid();
|
||||
$payload['posted_by'] = $this->currentUserId();
|
||||
if ($payload['status'] === 'open') {
|
||||
$payload['posted_at'] = utc_now();
|
||||
}
|
||||
|
||||
if (!$this->positions->insert($payload)) {
|
||||
return redirect()->back()->withInput()->with('error', 'Unable to create position.');
|
||||
}
|
||||
|
||||
return redirect()->to(site_url(self::ADMIN_FILTER_ROUTE . '/positions'))->with('success', 'Position created.');
|
||||
}
|
||||
|
||||
public function editPosition(string $positionId)
|
||||
{
|
||||
$position = $this->positions->find($positionId);
|
||||
if (!$position) {
|
||||
return redirect()->to(site_url(self::ADMIN_FILTER_ROUTE . '/positions'))->with('error', 'Position not found.');
|
||||
}
|
||||
|
||||
return view('jobs/admin/position_form', [
|
||||
'position' => $position,
|
||||
'statuses' => $this->positionStatuses,
|
||||
'templates' => $this->templates->where('is_active', 1)->orderBy('title', 'ASC')->findAll(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function updatePosition(string $positionId)
|
||||
{
|
||||
$position = $this->positions->find($positionId);
|
||||
if (!$position) {
|
||||
return redirect()->to(site_url(self::ADMIN_FILTER_ROUTE . '/positions'))->with('error', 'Position not found.');
|
||||
}
|
||||
|
||||
$payload = $this->positionPayload();
|
||||
if ($payload === null) {
|
||||
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
|
||||
}
|
||||
|
||||
if ($payload['status'] === 'open' && (($position['status'] ?? '') !== 'open' || empty($position['posted_at']))) {
|
||||
$payload['posted_at'] = utc_now();
|
||||
}
|
||||
|
||||
if (!$this->positions->update($positionId, $payload)) {
|
||||
return redirect()->back()->withInput()->with('error', 'Unable to update position.');
|
||||
}
|
||||
|
||||
return redirect()->to(site_url(self::ADMIN_FILTER_ROUTE . '/positions'))->with('success', 'Position updated.');
|
||||
}
|
||||
|
||||
public function applications()
|
||||
{
|
||||
$builder = $this->applications
|
||||
->select('applications.*, job_positions.title AS position_title, job_positions.department')
|
||||
->join('job_positions', 'job_positions.position_id = applications.position_id', 'left');
|
||||
|
||||
$status = (string) $this->request->getGet('status');
|
||||
if (in_array($status, $this->applicationStatuses, true)) {
|
||||
$builder->where('applications.status', $status);
|
||||
}
|
||||
|
||||
$positionId = (string) $this->request->getGet('position_id');
|
||||
if ($positionId !== '') {
|
||||
$builder->where('applications.position_id', $positionId);
|
||||
}
|
||||
|
||||
return view('jobs/admin/applications', [
|
||||
'applications' => $builder->orderBy('submitted_at', 'DESC')->findAll(),
|
||||
'positions' => $this->positions->orderBy('title', 'ASC')->findAll(),
|
||||
'statuses' => $this->applicationStatuses,
|
||||
'selectedStatus' => $status,
|
||||
'selectedPosition' => $positionId,
|
||||
]);
|
||||
}
|
||||
|
||||
public function updateApplication(string $applicationId)
|
||||
{
|
||||
$status = (string) $this->request->getPost('status');
|
||||
if (!in_array($status, $this->applicationStatuses, true)) {
|
||||
return redirect()->back()->with('error', 'Invalid application status.');
|
||||
}
|
||||
|
||||
if (!$this->applications->update($applicationId, [
|
||||
'status' => $status,
|
||||
'admin_notes' => (string) $this->request->getPost('admin_notes'),
|
||||
])) {
|
||||
return redirect()->back()->with('error', 'Unable to update application.');
|
||||
}
|
||||
|
||||
$application = $this->applications
|
||||
->select('applications.*, job_positions.title AS position_title')
|
||||
->join('job_positions', 'job_positions.position_id = applications.position_id', 'left')
|
||||
->find($applicationId);
|
||||
|
||||
if ($application) {
|
||||
$this->sendApplicationStatusEmail($application);
|
||||
}
|
||||
|
||||
return redirect()->back()->with('success', 'Application updated.');
|
||||
}
|
||||
|
||||
public function resume(string $applicationId)
|
||||
{
|
||||
$application = $this->applications->find($applicationId);
|
||||
if (!$application) {
|
||||
return redirect()->back()->with('error', 'Application not found.');
|
||||
}
|
||||
|
||||
$path = WRITEPATH . 'uploads/' . ltrim((string) $application['resume_file_url'], '/');
|
||||
if (!is_file($path)) {
|
||||
return redirect()->back()->with('error', 'Resume file not found.');
|
||||
}
|
||||
|
||||
return $this->response->download($path, null);
|
||||
}
|
||||
|
||||
private function templatePayload(): ?array
|
||||
{
|
||||
if (!$this->validate(['title' => 'required|max_length[255]'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->sharedPostingPayload();
|
||||
}
|
||||
|
||||
private function positionPayload(): ?array
|
||||
{
|
||||
if (!$this->validate([
|
||||
'title' => 'required|max_length[255]',
|
||||
'status' => 'required|in_list[draft,open,closed,filled]',
|
||||
])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$payload = $this->sharedPostingPayload();
|
||||
$payload['template_id'] = $this->request->getPost('template_id') ?: null;
|
||||
$payload['status'] = (string) $this->request->getPost('status');
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
private function sharedPostingPayload(): array
|
||||
{
|
||||
return [
|
||||
'title' => trim((string) $this->request->getPost('title')),
|
||||
'description' => trim((string) $this->request->getPost('description')),
|
||||
'department' => trim((string) $this->request->getPost('department')),
|
||||
'location' => trim((string) $this->request->getPost('location')),
|
||||
'employment_type' => trim((string) $this->request->getPost('employment_type')),
|
||||
'responsibilities' => trim((string) $this->request->getPost('responsibilities')),
|
||||
'requirements' => trim((string) $this->request->getPost('requirements')),
|
||||
];
|
||||
}
|
||||
|
||||
private function snapshotTemplate(array $template): void
|
||||
{
|
||||
$this->templateVersions
|
||||
->where('template_id', $template['template_id'])
|
||||
->where('version', (int) ($template['version'] ?? 1))
|
||||
->delete();
|
||||
|
||||
$snapshot = $this->templateVersionPayload($template);
|
||||
$snapshot['version_id'] = $this->newUuid();
|
||||
$snapshot['template_id'] = $template['template_id'];
|
||||
$snapshot['version'] = (int) ($template['version'] ?? 1);
|
||||
$snapshot['saved_by'] = $this->currentUserId();
|
||||
$snapshot['saved_at'] = utc_now();
|
||||
|
||||
$this->templateVersions->insert($snapshot);
|
||||
}
|
||||
|
||||
private function templateVersionPayload(array $data): array
|
||||
{
|
||||
return [
|
||||
'title' => (string) ($data['title'] ?? ''),
|
||||
'description' => (string) ($data['description'] ?? ''),
|
||||
'department' => (string) ($data['department'] ?? ''),
|
||||
'location' => (string) ($data['location'] ?? ''),
|
||||
'employment_type' => (string) ($data['employment_type'] ?? ''),
|
||||
'responsibilities' => (string) ($data['responsibilities'] ?? ''),
|
||||
'requirements' => (string) ($data['requirements'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
private function sendApplicationEmails(array $application, array $position): void
|
||||
{
|
||||
$fullName = trim($application['first_name'] . ' ' . $application['last_name']);
|
||||
$body = view('emails/job_application_confirmation', [
|
||||
'name' => $fullName,
|
||||
'position' => $position,
|
||||
]);
|
||||
|
||||
try {
|
||||
(new EmailService())->send($application['email'], 'Application received: ' . $position['title'], $body, 'general');
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'Job application confirmation email failed: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
$adminRecipients = array_unique(array_filter([
|
||||
trim((string) env('JOBS_ADMIN_EMAIL', '')),
|
||||
trim((string) env('PRINCIPAL_EMAIL', '')),
|
||||
], static fn (string $email): bool => $email !== '' && filter_var($email, FILTER_VALIDATE_EMAIL)));
|
||||
|
||||
if ($adminRecipients !== []) {
|
||||
$adminBody = '<p>New application received for <strong>' . esc($position['title']) . '</strong>.</p>'
|
||||
. '<p>Applicant: ' . esc($fullName) . '<br>Email: ' . esc($application['email']) . '<br>Phone: ' . esc($application['phone']) . '</p>';
|
||||
|
||||
$emailService = new EmailService();
|
||||
try {
|
||||
foreach ($adminRecipients as $adminEmail) {
|
||||
$emailService->send($adminEmail, 'New job application: ' . $position['title'], $adminBody, 'general');
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'Job application admin email failed: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function sendApplicationStatusEmail(array $application): void
|
||||
{
|
||||
$email = trim((string) ($application['email'] ?? ''));
|
||||
if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$status = (string) ($application['status'] ?? '');
|
||||
$statusLabel = ucfirst(str_replace('_', ' ', $status));
|
||||
$positionTitle = (string) ($application['position_title'] ?? 'the volunteer position');
|
||||
$fullName = trim((string) (($application['first_name'] ?? '') . ' ' . ($application['last_name'] ?? '')));
|
||||
|
||||
$body = view('emails/job_application_status_update', [
|
||||
'name' => $fullName !== '' ? $fullName : 'Applicant',
|
||||
'positionTitle' => $positionTitle,
|
||||
'statusLabel' => $statusLabel,
|
||||
'adminNotes' => trim((string) ($application['admin_notes'] ?? '')),
|
||||
]);
|
||||
|
||||
try {
|
||||
(new EmailService())->send($email, 'Application status update: ' . $positionTitle, $body, 'general');
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'Job application status email failed: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function currentUserId(): ?int
|
||||
{
|
||||
$userId = (int) session()->get('user_id');
|
||||
return $userId > 0 ? $userId : null;
|
||||
}
|
||||
|
||||
private function newUuid(): string
|
||||
{
|
||||
$bytes = random_bytes(16);
|
||||
$bytes[6] = chr((ord($bytes[6]) & 0x0f) | 0x40);
|
||||
$bytes[8] = chr((ord($bytes[8]) & 0x3f) | 0x80);
|
||||
|
||||
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($bytes), 4));
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,8 @@ use App\Models\ScoreCommentModel;
|
||||
use App\Models\AttendanceRecordModel;
|
||||
use App\Models\AttendanceDayModel;
|
||||
use App\Models\CalendarModel;
|
||||
use App\Models\JobPositionModel;
|
||||
use App\Models\PreferencesModel;
|
||||
use \Config\Database;
|
||||
use DateTimeImmutable;
|
||||
use DateTimeZone;
|
||||
@@ -774,39 +776,6 @@ class LandingPageController extends BaseController
|
||||
}
|
||||
|
||||
|
||||
// Fetch Notifications (only active, non-expired, non-deleted)
|
||||
$notifications = $this->db->table('notifications')
|
||||
->select([
|
||||
'notifications.id',
|
||||
'notifications.title',
|
||||
'notifications.message',
|
||||
'notifications.target_group',
|
||||
'notifications.created_at',
|
||||
'notifications.expires_at',
|
||||
'user_notifications.user_id',
|
||||
"CASE
|
||||
WHEN user_notifications.user_id IS NOT NULL THEN 'personal'
|
||||
ELSE 'broadcast'
|
||||
END as notification_type"
|
||||
])
|
||||
->join(
|
||||
'user_notifications',
|
||||
'user_notifications.notification_id = notifications.id AND user_notifications.user_id = ' . (int) $parentId,
|
||||
'left'
|
||||
)
|
||||
->groupStart()
|
||||
->where('notifications.target_group', 'parent')
|
||||
->orWhere('user_notifications.user_id', $parentId)
|
||||
->groupEnd()
|
||||
->where('notifications.deleted_at IS NULL') // Exclude soft-deleted notifications
|
||||
->groupStart()
|
||||
->where('notifications.expires_at IS NULL')
|
||||
->orWhere('notifications.expires_at > NOW()') // Exclude expired
|
||||
->groupEnd()
|
||||
->orderBy('notifications.created_at', 'DESC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
// Fetch Student Information (no filtering needed by school year or semester)
|
||||
|
||||
$students = $this->db->table('students')
|
||||
@@ -822,6 +791,8 @@ class LandingPageController extends BaseController
|
||||
}
|
||||
unset($student);
|
||||
|
||||
$notifications = $this->parentDashboardNotifications((int) $parentId, (int) session()->get('user_id'));
|
||||
|
||||
|
||||
// Fetch Attendance Records (filtered by most recent school year and semester)
|
||||
$attendanceData = $this->db->table('attendance_data')
|
||||
@@ -867,6 +838,17 @@ class LandingPageController extends BaseController
|
||||
->get()
|
||||
->getRowArray();
|
||||
$paymentBalance = (float) ($paymentRow['account_balance'] ?? 0);
|
||||
$openPositions = [];
|
||||
|
||||
if (! $this->parentHidesJobOpeningsPopup($parentId)) {
|
||||
try {
|
||||
if ($this->db->tableExists('job_positions')) {
|
||||
$openPositions = (new JobPositionModel())->openPositions();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'Unable to load parent dashboard job positions: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Pass data to the view, including the deadlines
|
||||
return view('/landing_page/parent_dashboard', [
|
||||
@@ -878,9 +860,259 @@ class LandingPageController extends BaseController
|
||||
'lastDayOfRegistration' => $this->lastDayOfRegistration, // Add the enrollment deadline to the view
|
||||
'withdrawalDeadline' => $this->refundDeadline, // Add the refund deadline to the view
|
||||
'paymentBalance' => $paymentBalance, // 🔹 New
|
||||
'openPositions' => $openPositions,
|
||||
]);
|
||||
}
|
||||
|
||||
public function hideJobOpeningsPopup()
|
||||
{
|
||||
$userId = (int) (session()->get('user_id') ?? 0);
|
||||
if ($userId <= 0) {
|
||||
return $this->response->setStatusCode(401)->setJSON([
|
||||
'ok' => false,
|
||||
'error' => 'Please log in to update this setting.',
|
||||
'csrf_token' => csrf_token(),
|
||||
'csrf_hash' => csrf_hash(),
|
||||
]);
|
||||
}
|
||||
|
||||
if (
|
||||
! $this->db->tableExists('user_preferences')
|
||||
|| ! $this->db->fieldExists('hide_job_openings_popup', 'user_preferences')
|
||||
) {
|
||||
return $this->response->setStatusCode(500)->setJSON([
|
||||
'ok' => false,
|
||||
'error' => 'Preference storage is not ready.',
|
||||
'csrf_token' => csrf_token(),
|
||||
'csrf_hash' => csrf_hash(),
|
||||
]);
|
||||
}
|
||||
|
||||
$hide = (string) $this->request->getPost('hide_job_openings_popup') === '1' ? 1 : 0;
|
||||
$preferencesModel = new PreferencesModel();
|
||||
$existing = $preferencesModel->where('user_id', $userId)->first();
|
||||
|
||||
if ($existing) {
|
||||
$preferencesModel->update((int) $existing['id'], [
|
||||
'hide_job_openings_popup' => $hide,
|
||||
]);
|
||||
} else {
|
||||
$preferencesModel->insert([
|
||||
'user_id' => $userId,
|
||||
'hide_job_openings_popup' => $hide,
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->response->setJSON([
|
||||
'ok' => true,
|
||||
'hide_job_openings_popup' => $hide,
|
||||
'csrf_token' => csrf_token(),
|
||||
'csrf_hash' => csrf_hash(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function parentHidesJobOpeningsPopup(int $parentId): bool
|
||||
{
|
||||
if ($parentId <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
if (
|
||||
! $this->db->tableExists('user_preferences')
|
||||
|| ! $this->db->fieldExists('hide_job_openings_popup', 'user_preferences')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$preferences = (new PreferencesModel())->where('user_id', $parentId)->first();
|
||||
|
||||
return ! empty($preferences['hide_job_openings_popup']);
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'Unable to load parent job openings popup preference: ' . $e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function parentDashboardNotifications(int $parentId, int $userId): array
|
||||
{
|
||||
$notifications = array_merge(
|
||||
$this->parentAttendanceNotifications($parentId),
|
||||
$this->activeParentBroadcastNotifications($parentId, $userId)
|
||||
);
|
||||
|
||||
usort($notifications, static function (array $a, array $b): int {
|
||||
return strtotime((string) ($b['created_at'] ?? '')) <=> strtotime((string) ($a['created_at'] ?? ''));
|
||||
});
|
||||
|
||||
return array_slice($notifications, 0, 25);
|
||||
}
|
||||
|
||||
private function parentAttendanceNotifications(int $parentId): array
|
||||
{
|
||||
if (
|
||||
$parentId <= 0
|
||||
|| ! $this->db->tableExists('parent_notifications')
|
||||
|| ! $this->db->tableExists('students')
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$rows = $this->db->table('parent_notifications pn')
|
||||
->select([
|
||||
'pn.id',
|
||||
'pn.student_id',
|
||||
'pn.code',
|
||||
'pn.incident_date',
|
||||
'pn.channel',
|
||||
'pn.subject',
|
||||
'pn.status',
|
||||
'pn.response',
|
||||
'pn.semester',
|
||||
'pn.school_year',
|
||||
'pn.created_at',
|
||||
'pn.updated_at',
|
||||
'students.firstname',
|
||||
'students.lastname',
|
||||
])
|
||||
->join('students', 'students.id = pn.student_id')
|
||||
->where('students.parent_id', $parentId)
|
||||
->where('pn.school_year', (string) $this->schoolYear)
|
||||
->where('pn.semester', (string) $this->semester)
|
||||
->orderBy('COALESCE(pn.updated_at, pn.created_at)', 'DESC', false)
|
||||
->orderBy('pn.id', 'DESC')
|
||||
->limit(100)
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
$notifications = [];
|
||||
$seen = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$key = implode('|', [
|
||||
(string) ($row['student_id'] ?? ''),
|
||||
(string) ($row['code'] ?? ''),
|
||||
(string) ($row['incident_date'] ?? ''),
|
||||
(string) ($row['subject'] ?? ''),
|
||||
]);
|
||||
|
||||
if (isset($seen[$key])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$seen[$key] = true;
|
||||
$studentName = trim((string) ($row['firstname'] ?? '') . ' ' . (string) ($row['lastname'] ?? ''));
|
||||
$code = strtoupper((string) ($row['code'] ?? ''));
|
||||
$incidentDate = (string) ($row['incident_date'] ?? '');
|
||||
$subject = trim((string) ($row['subject'] ?? ''));
|
||||
|
||||
$title = $subject !== '' ? $subject : $this->parentNotificationCodeLabel($code);
|
||||
if ($studentName !== '') {
|
||||
$title .= ' - ' . $studentName;
|
||||
}
|
||||
|
||||
$messageParts = [];
|
||||
if ($incidentDate !== '') {
|
||||
$messageParts[] = 'Incident date: ' . $incidentDate;
|
||||
}
|
||||
if (!empty($row['status'])) {
|
||||
$messageParts[] = 'Status: ' . ucfirst((string) $row['status']);
|
||||
}
|
||||
|
||||
$notifications[] = [
|
||||
'id' => $row['id'] ?? null,
|
||||
'title' => $title,
|
||||
'message' => implode(' | ', $messageParts),
|
||||
'created_at' => $row['updated_at'] ?: ($row['created_at'] ?? null),
|
||||
'notification_type' => $this->parentNotificationCodeLabel($code),
|
||||
'status' => $row['status'] ?? null,
|
||||
'code' => $code,
|
||||
'incident_date' => $incidentDate,
|
||||
'student_name' => $studentName,
|
||||
];
|
||||
|
||||
if (count($notifications) >= 25) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $notifications;
|
||||
}
|
||||
|
||||
private function activeParentBroadcastNotifications(int $parentId, int $userId): array
|
||||
{
|
||||
if ($parentId <= 0 || ! $this->db->tableExists('notifications')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$builder = $this->db->table('notifications')
|
||||
->select([
|
||||
'notifications.id',
|
||||
'notifications.title',
|
||||
'notifications.message',
|
||||
'notifications.target_group',
|
||||
'notifications.created_at',
|
||||
'notifications.expires_at',
|
||||
"CASE
|
||||
WHEN user_notifications.user_id IS NOT NULL THEN 'personal'
|
||||
ELSE 'broadcast'
|
||||
END as notification_type",
|
||||
])
|
||||
->join(
|
||||
'user_notifications',
|
||||
'user_notifications.notification_id = notifications.id AND user_notifications.user_id IN (' . implode(',', array_unique([$parentId, $userId])) . ')',
|
||||
'left'
|
||||
)
|
||||
->groupStart()
|
||||
->whereIn('notifications.target_group', ['parent', 'everyone'])
|
||||
->orWhere('user_notifications.user_id IS NOT NULL')
|
||||
->groupEnd()
|
||||
->where('notifications.deleted_at IS NULL')
|
||||
->groupStart()
|
||||
->where('notifications.scheduled_at IS NULL')
|
||||
->orWhere('notifications.scheduled_at <=', utc_now())
|
||||
->groupEnd()
|
||||
->groupStart()
|
||||
->where('notifications.expires_at IS NULL')
|
||||
->orWhere('notifications.expires_at >', utc_now())
|
||||
->groupEnd();
|
||||
|
||||
if ($this->db->fieldExists('school_year', 'notifications')) {
|
||||
$builder->where('notifications.school_year', (string) $this->schoolYear);
|
||||
}
|
||||
|
||||
if ($this->db->fieldExists('semester', 'notifications')) {
|
||||
$builder->groupStart()
|
||||
->where('notifications.semester', null)
|
||||
->orWhere('notifications.semester', '')
|
||||
->orWhere('notifications.semester', (string) $this->semester)
|
||||
->groupEnd();
|
||||
}
|
||||
|
||||
return $builder
|
||||
->groupBy('notifications.id')
|
||||
->orderBy('notifications.created_at', 'DESC')
|
||||
->limit(25)
|
||||
->get()
|
||||
->getResultArray();
|
||||
}
|
||||
|
||||
private function parentNotificationCodeLabel(string $code): string
|
||||
{
|
||||
return match ($code) {
|
||||
'ABS_1' => 'Unreported absence',
|
||||
'ABS_2' => 'Repeated absences',
|
||||
'ABS_3' => 'Attendance warning',
|
||||
'ABS_4' => 'Attendance review',
|
||||
'LATE_2' => 'Repeated lateness',
|
||||
'LATE_3' => 'Lateness warning',
|
||||
'LATE_4' => 'Lateness review',
|
||||
'MIX_L2A1' => 'Attendance warning',
|
||||
default => 'Parent notice',
|
||||
};
|
||||
}
|
||||
|
||||
public function guest()
|
||||
{
|
||||
return view('/landing_page/guest_dashboard');
|
||||
|
||||
@@ -22,33 +22,14 @@ class NavBuilderController extends BaseController
|
||||
|
||||
protected function ensureAdmin(): void
|
||||
{
|
||||
$sessionRole = session()->get('role'); // could be a string or array in your app
|
||||
$roleNames = is_array($sessionRole) ? $sessionRole : [$sessionRole];
|
||||
$roleNames = array_values(array_filter(array_map('strval', $roleNames)));
|
||||
$session = session();
|
||||
$roles = array_filter(array_merge(
|
||||
(array) $session->get('roles'),
|
||||
(array) $session->get('role')
|
||||
));
|
||||
|
||||
if (empty($roleNames)) {
|
||||
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
// Map role names -> ids
|
||||
$roleIdRows = $db->table('roles')->select('id')->whereIn('name', $roleNames)->get()->getResultArray();
|
||||
$roleIds = array_map('intval', array_column($roleIdRows, 'id'));
|
||||
if (empty($roleIds)) {
|
||||
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
|
||||
// Is this route allowed for any of the user's roles?
|
||||
$allowed = $db->table('role_nav_items AS rni')
|
||||
->select('1')
|
||||
->join('nav_items AS ni', 'ni.id = rni.nav_item_id')
|
||||
->where('ni.url', 'nav-builder') // IMPORTANT: your current route path
|
||||
->whereIn('rni.role_id', $roleIds)
|
||||
->get(1)->getFirstRow();
|
||||
|
||||
if (!$allowed) {
|
||||
// You can show a nicer "Access Denied" view if you prefer
|
||||
$normalizedRoles = array_map(static fn ($role) => strtolower(trim((string) $role)), $roles);
|
||||
if (!in_array('administrator', $normalizedRoles, true)) {
|
||||
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
}
|
||||
@@ -106,6 +87,15 @@ public function save()
|
||||
if ($menuParentId !== null) {
|
||||
$parent = $this->items->select('id')->where('id', $menuParentId)->first();
|
||||
if (!$parent) {
|
||||
if ($this->wantsJson()) {
|
||||
return $this->response
|
||||
->setStatusCode(422)
|
||||
->setJSON([
|
||||
'ok' => false,
|
||||
'message' => 'Selected parent does not exist.',
|
||||
'csrf' => $this->csrfPayload(),
|
||||
]);
|
||||
}
|
||||
return redirect()->back()->with('error', 'Selected parent does not exist.')->withInput();
|
||||
}
|
||||
}
|
||||
@@ -139,9 +129,33 @@ public function save()
|
||||
}
|
||||
|
||||
$this->service->clearCache();
|
||||
if ($this->wantsJson()) {
|
||||
return $this->response->setJSON([
|
||||
'ok' => true,
|
||||
'message' => 'Menu saved.',
|
||||
'id' => $id,
|
||||
'csrf' => $this->csrfPayload(),
|
||||
'payload' => $this->buildNavPayload(),
|
||||
]);
|
||||
}
|
||||
|
||||
return redirect()->back()->with('success', 'Menu saved.');
|
||||
}
|
||||
|
||||
private function wantsJson(): bool
|
||||
{
|
||||
return $this->request->isAJAX()
|
||||
|| str_contains(strtolower($this->request->getHeaderLine('Accept')), 'application/json');
|
||||
}
|
||||
|
||||
private function csrfPayload(): array
|
||||
{
|
||||
return [
|
||||
'name' => csrf_token(),
|
||||
'hash' => csrf_hash(),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
public function delete($id)
|
||||
{
|
||||
@@ -158,12 +172,182 @@ public function save()
|
||||
{
|
||||
$this->ensureAdmin();
|
||||
|
||||
$structure = $this->request->getPost('structure') ?? [];
|
||||
if (is_array($structure) && !empty($structure)) {
|
||||
$updates = $this->normalizeStructureUpdates($structure);
|
||||
foreach ($updates as $row) {
|
||||
$this->items->update($row['id'], [
|
||||
'menu_parent_id' => $row['menu_parent_id'],
|
||||
'sort_order' => $row['sort_order'],
|
||||
]);
|
||||
}
|
||||
|
||||
$this->service->clearCache();
|
||||
return $this->response->setJSON(['ok' => true, 'csrf' => $this->csrfPayload()]);
|
||||
}
|
||||
|
||||
// Backward-compatible payload used by the previous builder.
|
||||
$orders = $this->request->getPost('orders') ?? [];
|
||||
foreach ($orders as $id => $order) {
|
||||
$this->items->update((int) $id, ['sort_order' => (int) $order]);
|
||||
}
|
||||
|
||||
$this->service->clearCache();
|
||||
return $this->response->setJSON(['ok' => true]);
|
||||
return $this->response->setJSON(['ok' => true, 'csrf' => $this->csrfPayload()]);
|
||||
}
|
||||
|
||||
public function roleAccess()
|
||||
{
|
||||
$this->ensureAdmin();
|
||||
|
||||
$action = strtolower(trim((string) $this->request->getPost('action')));
|
||||
$navItemId = (int) $this->request->getPost('nav_item_id');
|
||||
$sourceRoleId = (int) $this->request->getPost('source_role_id');
|
||||
$targetRoleId = (int) $this->request->getPost('target_role_id');
|
||||
|
||||
if ($navItemId <= 0 || !$this->items->select('id')->find($navItemId)) {
|
||||
return $this->response
|
||||
->setStatusCode(422)
|
||||
->setJSON(['ok' => false, 'message' => 'Selected page does not exist.', 'csrf' => $this->csrfPayload()]);
|
||||
}
|
||||
|
||||
if ($action === 'remove') {
|
||||
if (!$this->roleExists($sourceRoleId)) {
|
||||
return $this->response
|
||||
->setStatusCode(422)
|
||||
->setJSON(['ok' => false, 'message' => 'Selected role does not exist.', 'csrf' => $this->csrfPayload()]);
|
||||
}
|
||||
|
||||
$this->maps
|
||||
->where('nav_item_id', $navItemId)
|
||||
->where('role_id', $sourceRoleId)
|
||||
->delete();
|
||||
} elseif ($action === 'move') {
|
||||
if (!$this->roleExists($sourceRoleId) || !$this->roleExists($targetRoleId)) {
|
||||
return $this->response
|
||||
->setStatusCode(422)
|
||||
->setJSON(['ok' => false, 'message' => 'Selected role does not exist.', 'csrf' => $this->csrfPayload()]);
|
||||
}
|
||||
|
||||
if ($sourceRoleId !== $targetRoleId) {
|
||||
$this->maps
|
||||
->where('nav_item_id', $navItemId)
|
||||
->where('role_id', $sourceRoleId)
|
||||
->delete();
|
||||
|
||||
$exists = $this->maps
|
||||
->where('nav_item_id', $navItemId)
|
||||
->where('role_id', $targetRoleId)
|
||||
->first();
|
||||
|
||||
if (!$exists) {
|
||||
$this->maps->insert([
|
||||
'role_id' => $targetRoleId,
|
||||
'nav_item_id' => $navItemId,
|
||||
]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return $this->response
|
||||
->setStatusCode(422)
|
||||
->setJSON(['ok' => false, 'message' => 'Unsupported role access action.', 'csrf' => $this->csrfPayload()]);
|
||||
}
|
||||
|
||||
$this->service->clearCache();
|
||||
|
||||
return $this->response->setJSON([
|
||||
'ok' => true,
|
||||
'message' => 'Role access updated.',
|
||||
'csrf' => $this->csrfPayload(),
|
||||
'payload' => $this->buildNavPayload(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function roleExists(int $roleId): bool
|
||||
{
|
||||
if ($roleId <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$db = \Config\Database::connect();
|
||||
return (bool) $db->table('roles')
|
||||
->select('id')
|
||||
->where('id', $roleId)
|
||||
->get(1)
|
||||
->getFirstRow();
|
||||
}
|
||||
|
||||
private function normalizeStructureUpdates(array $structure): array
|
||||
{
|
||||
$existingRows = $this->items->select('id, menu_parent_id')->findAll();
|
||||
$existingIds = array_map('intval', array_column($existingRows, 'id'));
|
||||
$existingIdLookup = array_fill_keys($existingIds, true);
|
||||
|
||||
$updates = [];
|
||||
foreach ($structure as $row) {
|
||||
if (!is_array($row)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$id = (int) ($row['id'] ?? 0);
|
||||
if ($id <= 0 || !isset($existingIdLookup[$id])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$parentId = $row['parent_id'] ?? null;
|
||||
$parentId = ($parentId === '' || $parentId === null) ? null : (int) $parentId;
|
||||
if ($parentId !== null && (!isset($existingIdLookup[$parentId]) || $parentId === $id)) {
|
||||
$parentId = null;
|
||||
}
|
||||
|
||||
$updates[] = [
|
||||
'id' => $id,
|
||||
'menu_parent_id' => $parentId,
|
||||
'sort_order' => max(0, (int) ($row['sort_order'] ?? 0)),
|
||||
];
|
||||
}
|
||||
|
||||
$updatesById = [];
|
||||
foreach ($updates as $row) {
|
||||
$updatesById[$row['id']] = $row;
|
||||
}
|
||||
|
||||
foreach ($updatesById as $id => &$row) {
|
||||
if ($row['menu_parent_id'] !== null && $this->wouldCreateCycle($id, $row['menu_parent_id'], $updatesById, $existingRows)) {
|
||||
$row['menu_parent_id'] = null;
|
||||
}
|
||||
}
|
||||
unset($row);
|
||||
|
||||
return array_values($updatesById);
|
||||
}
|
||||
|
||||
private function wouldCreateCycle(int $id, int $parentId, array $updatesById, array $existingRows): bool
|
||||
{
|
||||
$parentById = [];
|
||||
foreach ($existingRows as $row) {
|
||||
$parentById[(int) ($row['id'] ?? 0)] = isset($row['menu_parent_id']) && (int) $row['menu_parent_id'] !== 0
|
||||
? (int) $row['menu_parent_id']
|
||||
: null;
|
||||
}
|
||||
foreach ($updatesById as $row) {
|
||||
$parentById[$row['id']] = $row['menu_parent_id'];
|
||||
}
|
||||
|
||||
$seen = [];
|
||||
$current = $parentId;
|
||||
while ($current !== null) {
|
||||
if ($current === $id) {
|
||||
return true;
|
||||
}
|
||||
if (isset($seen[$current])) {
|
||||
return true;
|
||||
}
|
||||
$seen[$current] = true;
|
||||
$current = $parentById[$current] ?? null;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function distinctRoles(): array
|
||||
@@ -256,9 +440,59 @@ public function save()
|
||||
'items' => $flattened,
|
||||
'roles' => $roles,
|
||||
'parentOptions' => $parentOptions,
|
||||
'routeOptions' => $this->routeOptions(),
|
||||
];
|
||||
}
|
||||
|
||||
private function routeOptions(): array
|
||||
{
|
||||
$routes = service('routes');
|
||||
$getRoutes = $routes->getRoutes('GET', false);
|
||||
if (empty($getRoutes)) {
|
||||
$routes = $routes->loadRoutes();
|
||||
$getRoutes = $routes->getRoutes('GET', false);
|
||||
}
|
||||
$options = [];
|
||||
|
||||
foreach (array_keys($getRoutes) as $route) {
|
||||
$route = trim((string) $route, '/');
|
||||
if ($route === '' || $this->shouldHideRouteOption($route)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$options[] = [
|
||||
'value' => $route,
|
||||
'label' => $this->routeLabel($route),
|
||||
'needs_params' => str_contains($route, '(') || str_contains($route, '[') || str_contains($route, '{'),
|
||||
];
|
||||
}
|
||||
|
||||
usort($options, static function ($a, $b) {
|
||||
return strnatcasecmp($a['label'] ?? '', $b['label'] ?? '');
|
||||
});
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
private function shouldHideRouteOption(string $route): bool
|
||||
{
|
||||
if (str_starts_with($route, 'api/') || str_starts_with($route, 'docs/') || str_starts_with($route, 'debugbar/')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (bool) preg_match('#(^|/)(csrf-token|file|attachment|download|delete)(/|$)#i', $route);
|
||||
}
|
||||
|
||||
private function routeLabel(string $route): string
|
||||
{
|
||||
$label = preg_replace('#\(\?:[^)]+\)|\(\[\^/\]\+\)|\(\[0-9\]\+\)|\(:[a-z_]+\)|\{[^}]+\}#i', '{value}', $route) ?? $route;
|
||||
$label = str_replace(['_', '-'], ' ', $label);
|
||||
$label = preg_replace('#/+#', ' / ', $label) ?? $label;
|
||||
$label = preg_replace('/\s+/', ' ', $label) ?? $label;
|
||||
|
||||
return ucwords(trim($label));
|
||||
}
|
||||
|
||||
private function flattenTreeForResponse(array $nodes, array $roleAssignments, ?string $parentLabel = null, int $depth = 0, array &$rows = []): array
|
||||
{
|
||||
foreach ($nodes as $node) {
|
||||
@@ -300,4 +534,28 @@ public function save()
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
private function sortTreeByOrder(array &$nodes): void
|
||||
{
|
||||
usort($nodes, function ($a, $b) {
|
||||
$order = ((int) ($a['sort_order'] ?? 0)) <=> ((int) ($b['sort_order'] ?? 0));
|
||||
if ($order !== 0) {
|
||||
return $order;
|
||||
}
|
||||
|
||||
$label = strnatcasecmp($this->labelKey($a['label'] ?? ''), $this->labelKey($b['label'] ?? ''));
|
||||
if ($label !== 0) {
|
||||
return $label;
|
||||
}
|
||||
|
||||
return ((int) ($a['id'] ?? 0)) <=> ((int) ($b['id'] ?? 0));
|
||||
});
|
||||
|
||||
foreach ($nodes as &$node) {
|
||||
if (!empty($node['children'])) {
|
||||
$this->sortTreeByOrder($node['children']);
|
||||
}
|
||||
}
|
||||
unset($node);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -442,7 +442,7 @@ class ParentAttendanceReportController extends BaseController
|
||||
}
|
||||
|
||||
// ✅ Build formatted success message
|
||||
$msg = '✅ <strong>Submission received successfully for ' . count($successNames) . ' item' . (count($successNames) > 1 ? 's' : '') . '.</strong><br>';
|
||||
$msg = '✅ <strong>Submission received successfully for ' . count($successNames) . ' request' . (count($successNames) > 1 ? 's' : '') . '.</strong><br>';
|
||||
$msg .= '<ul style="margin-top:5px;">';
|
||||
foreach ($successNames as $s) {
|
||||
$dateLabel = $s['date_label'] ?? ($s['date'] ?? '');
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1685,7 +1685,7 @@ class PaymentController extends ResourceController
|
||||
$roles[] = $activeRole;
|
||||
}
|
||||
|
||||
$staffRoles = ['administrator', 'administrative staff', 'principal', 'teacher', 'teacher_assistant'];
|
||||
$staffRoles = ['administrator', 'administrative staff', 'principal', 'teacher', 'teacher_assistant', 'head fa', 'head of fa', 'head_of_fa', 'financial_contributor'];
|
||||
foreach ($staffRoles as $role) {
|
||||
if (in_array($role, $roles, true)) {
|
||||
return true;
|
||||
|
||||
@@ -1414,7 +1414,7 @@ class RefundController extends BaseController
|
||||
$roles[] = $activeRole;
|
||||
}
|
||||
|
||||
foreach (['administrator', 'administrative staff', 'principal', 'teacher', 'teacher_assistant'] as $role) {
|
||||
foreach (['administrator', 'administrative staff', 'principal', 'teacher', 'teacher_assistant', 'head fa', 'head of fa', 'head_of_fa', 'financial_contributor'] as $role) {
|
||||
if (in_array($role, $roles, true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ use App\Models\UserRoleModel;
|
||||
use App\Models\PasswordResetRequestModel;
|
||||
use App\Models\RolePermissionModel;
|
||||
use App\Models\IpAttemptModel;
|
||||
use App\Models\JobPositionModel;
|
||||
use CodeIgniter\Controller;
|
||||
use App\Controllers\View\EmailController;
|
||||
use App\Models\LoginActivityModel; // Make sure this import is present
|
||||
@@ -22,7 +23,7 @@ require_once APPPATH . 'Helpers/pbkdf2_helper.php';
|
||||
|
||||
class UserController extends BaseController
|
||||
{
|
||||
private const ACTIVATION_TTL_HOURS = 48;
|
||||
private const ACTIVATION_TTL_MINUTES = 15;
|
||||
protected $userModel;
|
||||
protected $roleModel;
|
||||
protected $userRoleModel;
|
||||
@@ -108,7 +109,18 @@ class UserController extends BaseController
|
||||
// Method to show the home page
|
||||
public function home()
|
||||
{
|
||||
return view('/index');
|
||||
$openPositions = [];
|
||||
try {
|
||||
if ($this->db->tableExists('job_positions')) {
|
||||
$openPositions = (new JobPositionModel())->openPositions();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'Unable to load home page job positions: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
return view('/index', [
|
||||
'openPositions' => $openPositions,
|
||||
]);
|
||||
}
|
||||
|
||||
// Method to show the about page
|
||||
@@ -117,6 +129,12 @@ class UserController extends BaseController
|
||||
return view('/about');
|
||||
}
|
||||
|
||||
// Method to show the careers page
|
||||
public function careers()
|
||||
{
|
||||
return view('/careers');
|
||||
}
|
||||
|
||||
// Method to show the classes page
|
||||
public function classes()
|
||||
{
|
||||
@@ -654,30 +672,7 @@ class UserController extends BaseController
|
||||
|
||||
public function confirm($token)
|
||||
{
|
||||
log_message('info', 'Processing email confirmation.');
|
||||
|
||||
$tokenHash = $this->hashToken($token);
|
||||
$user = $this->userModel
|
||||
->groupStart()
|
||||
->where('token', $tokenHash)
|
||||
->orWhere('token', $token)
|
||||
->groupEnd()
|
||||
->where('created_at >=', Time::now()->subHours(self::ACTIVATION_TTL_HOURS)->toDateTimeString())
|
||||
->first();
|
||||
|
||||
if (!$user || $user['is_verified'] == 1) {
|
||||
return redirect()->to('/invalid_token');
|
||||
}
|
||||
|
||||
|
||||
// Mark the user as verified and generate an account ID
|
||||
$account_id = 'ACC' . str_pad($user['id'], 8, '0', STR_PAD_LEFT); // Example: ACC00000001
|
||||
$this->userModel->update($user['id'], ['is_verified' => 1, 'token' => null, 'account_id' => $account_id]);
|
||||
|
||||
log_message('info', 'User verified and account ID generated: ' . $account_id);
|
||||
|
||||
// Redirect to the set password page
|
||||
return redirect()->to('/set_password/' . $user['id']);
|
||||
return $this->setPassword($token);
|
||||
}
|
||||
|
||||
public function setPassword($token)
|
||||
@@ -690,7 +685,7 @@ class UserController extends BaseController
|
||||
->where('token', $tokenHash)
|
||||
->orWhere('token', $token)
|
||||
->groupEnd()
|
||||
->where('created_at >=', Time::now()->subHours(self::ACTIVATION_TTL_HOURS)->toDateTimeString())
|
||||
->where('created_at >=', Time::now()->subMinutes(self::ACTIVATION_TTL_MINUTES)->toDateTimeString())
|
||||
->first();
|
||||
|
||||
if (!$user || $user['is_verified'] == 1) {
|
||||
@@ -747,7 +742,7 @@ class UserController extends BaseController
|
||||
->where('token', $tokenHash)
|
||||
->orWhere('token', $token)
|
||||
->groupEnd()
|
||||
->where('created_at >=', Time::now()->subHours(self::ACTIVATION_TTL_HOURS)->toDateTimeString())
|
||||
->where('created_at >=', Time::now()->subMinutes(self::ACTIVATION_TTL_MINUTES)->toDateTimeString())
|
||||
->first();
|
||||
|
||||
log_message('debug', "Attempting to set password for user $userId");
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
final class CreateUserAccessProfiles extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (! $this->db->tableExists('user_access_profiles')) {
|
||||
$this->forge->addField([
|
||||
'id' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'unsigned' => true,
|
||||
'auto_increment' => true,
|
||||
],
|
||||
'user_id' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'unsigned' => true,
|
||||
],
|
||||
'primary_category' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 20,
|
||||
'default' => 'guest',
|
||||
],
|
||||
'is_admin' => [
|
||||
'type' => 'TINYINT',
|
||||
'constraint' => 1,
|
||||
'default' => 0,
|
||||
],
|
||||
'is_teacher' => [
|
||||
'type' => 'TINYINT',
|
||||
'constraint' => 1,
|
||||
'default' => 0,
|
||||
],
|
||||
'is_parent' => [
|
||||
'type' => 'TINYINT',
|
||||
'constraint' => 1,
|
||||
'default' => 0,
|
||||
],
|
||||
'role_names' => [
|
||||
'type' => 'TEXT',
|
||||
'null' => true,
|
||||
],
|
||||
'created_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
'updated_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
]);
|
||||
$this->forge->addKey('id', true);
|
||||
$this->forge->addKey('user_id', false, true);
|
||||
$this->forge->addKey('primary_category');
|
||||
$this->forge->addKey('is_admin');
|
||||
$this->forge->addKey('is_teacher');
|
||||
$this->forge->addKey('is_parent');
|
||||
$this->forge->createTable('user_access_profiles', true);
|
||||
}
|
||||
|
||||
$this->backfillProfiles();
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$this->forge->dropTable('user_access_profiles', true);
|
||||
}
|
||||
|
||||
private function backfillProfiles(): void
|
||||
{
|
||||
if (
|
||||
! $this->db->tableExists('users')
|
||||
|| ! $this->db->tableExists('roles')
|
||||
|| ! $this->db->tableExists('user_roles')
|
||||
|| ! $this->db->tableExists('user_access_profiles')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$deletedFilter = $this->db->fieldExists('deleted_at', 'user_roles')
|
||||
? 'AND ur.deleted_at IS NULL'
|
||||
: '';
|
||||
|
||||
$sql = "
|
||||
INSERT INTO user_access_profiles (
|
||||
user_id,
|
||||
primary_category,
|
||||
is_admin,
|
||||
is_teacher,
|
||||
is_parent,
|
||||
role_names,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
u.id AS user_id,
|
||||
CASE
|
||||
WHEN MAX(CASE WHEN LOWER(REPLACE(REPLACE(COALESCE(r.slug, r.name), ' ', '_'), '-', '_')) NOT IN ('guest', 'parent', 'student', 'teacher', 'teacher_assistant', 'assistant_teacher', 'ta') THEN 1 ELSE 0 END) = 1 THEN 'admin'
|
||||
WHEN MAX(CASE WHEN LOWER(REPLACE(REPLACE(COALESCE(r.slug, r.name), ' ', '_'), '-', '_')) IN ('teacher', 'teacher_assistant', 'assistant_teacher', 'ta') THEN 1 ELSE 0 END) = 1 THEN 'teacher'
|
||||
WHEN MAX(CASE WHEN LOWER(REPLACE(REPLACE(COALESCE(r.slug, r.name), ' ', '_'), '-', '_')) = 'parent' THEN 1 ELSE 0 END) = 1 THEN 'parent'
|
||||
ELSE 'guest'
|
||||
END AS primary_category,
|
||||
MAX(CASE WHEN LOWER(REPLACE(REPLACE(COALESCE(r.slug, r.name), ' ', '_'), '-', '_')) NOT IN ('guest', 'parent', 'student', 'teacher', 'teacher_assistant', 'assistant_teacher', 'ta') THEN 1 ELSE 0 END) AS is_admin,
|
||||
MAX(CASE WHEN LOWER(REPLACE(REPLACE(COALESCE(r.slug, r.name), ' ', '_'), '-', '_')) IN ('teacher', 'teacher_assistant', 'assistant_teacher', 'ta') THEN 1 ELSE 0 END) AS is_teacher,
|
||||
MAX(CASE WHEN LOWER(REPLACE(REPLACE(COALESCE(r.slug, r.name), ' ', '_'), '-', '_')) = 'parent' THEN 1 ELSE 0 END) AS is_parent,
|
||||
GROUP_CONCAT(DISTINCT r.name ORDER BY COALESCE(r.priority, 999), r.name SEPARATOR ', ') AS role_names,
|
||||
? AS created_at,
|
||||
? AS updated_at
|
||||
FROM users u
|
||||
LEFT JOIN user_roles ur ON ur.user_id = u.id {$deletedFilter}
|
||||
LEFT JOIN roles r ON r.id = ur.role_id AND COALESCE(r.is_active, 1) = 1
|
||||
GROUP BY u.id
|
||||
ON DUPLICATE KEY UPDATE
|
||||
primary_category = VALUES(primary_category),
|
||||
is_admin = VALUES(is_admin),
|
||||
is_teacher = VALUES(is_teacher),
|
||||
is_parent = VALUES(is_parent),
|
||||
role_names = VALUES(role_names),
|
||||
updated_at = VALUES(updated_at)
|
||||
";
|
||||
|
||||
$this->db->query($sql, [$now, $now]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class CreateJobPostings extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
if (!$this->db->tableExists('job_templates')) {
|
||||
$this->forge->addField([
|
||||
'template_id' => ['type' => 'VARCHAR', 'constraint' => 36],
|
||||
'title' => ['type' => 'VARCHAR', 'constraint' => 255],
|
||||
'description' => ['type' => 'TEXT', 'null' => true],
|
||||
'department' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'location' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'employment_type' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true],
|
||||
'responsibilities' => ['type' => 'TEXT', 'null' => true],
|
||||
'requirements' => ['type' => 'TEXT', 'null' => true],
|
||||
'version' => ['type' => 'INT', 'constraint' => 11, 'default' => 1],
|
||||
'is_active' => ['type' => 'TINYINT', 'constraint' => 1, 'default' => 1],
|
||||
'created_by' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'null' => true],
|
||||
'created_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
'updated_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
]);
|
||||
$this->forge->addKey('template_id', true);
|
||||
$this->forge->addKey('is_active', false, false, 'idx_job_templates_active');
|
||||
$this->forge->createTable('job_templates', true);
|
||||
}
|
||||
|
||||
if (!$this->db->tableExists('job_template_versions')) {
|
||||
$this->forge->addField([
|
||||
'version_id' => ['type' => 'VARCHAR', 'constraint' => 36],
|
||||
'template_id' => ['type' => 'VARCHAR', 'constraint' => 36],
|
||||
'version' => ['type' => 'INT', 'constraint' => 11],
|
||||
'title' => ['type' => 'VARCHAR', 'constraint' => 255],
|
||||
'description' => ['type' => 'TEXT', 'null' => true],
|
||||
'department' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'location' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'employment_type' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true],
|
||||
'responsibilities' => ['type' => 'TEXT', 'null' => true],
|
||||
'requirements' => ['type' => 'TEXT', 'null' => true],
|
||||
'saved_by' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'null' => true],
|
||||
'saved_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
]);
|
||||
$this->forge->addKey('version_id', true);
|
||||
$this->forge->addKey('template_id', false, false, 'idx_template_versions_template_id');
|
||||
$this->forge->createTable('job_template_versions', true);
|
||||
}
|
||||
|
||||
if (!$this->db->tableExists('job_positions')) {
|
||||
$this->forge->addField([
|
||||
'position_id' => ['type' => 'VARCHAR', 'constraint' => 36],
|
||||
'template_id' => ['type' => 'VARCHAR', 'constraint' => 36, 'null' => true],
|
||||
'title' => ['type' => 'VARCHAR', 'constraint' => 255],
|
||||
'description' => ['type' => 'TEXT', 'null' => true],
|
||||
'department' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'location' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'employment_type' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true],
|
||||
'responsibilities' => ['type' => 'TEXT', 'null' => true],
|
||||
'requirements' => ['type' => 'TEXT', 'null' => true],
|
||||
'status' => ['type' => 'VARCHAR', 'constraint' => 20, 'default' => 'draft'],
|
||||
'posted_by' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'null' => true],
|
||||
'posted_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
'details_click_count' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'default' => 0],
|
||||
'created_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
'updated_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
]);
|
||||
$this->forge->addKey('position_id', true);
|
||||
$this->forge->addKey('status', false, false, 'idx_job_positions_status');
|
||||
$this->forge->createTable('job_positions', true);
|
||||
}
|
||||
|
||||
if (!$this->db->tableExists('applications')) {
|
||||
$this->forge->addField([
|
||||
'application_id' => ['type' => 'VARCHAR', 'constraint' => 36],
|
||||
'position_id' => ['type' => 'VARCHAR', 'constraint' => 36],
|
||||
'first_name' => ['type' => 'VARCHAR', 'constraint' => 100],
|
||||
'last_name' => ['type' => 'VARCHAR', 'constraint' => 100],
|
||||
'email' => ['type' => 'VARCHAR', 'constraint' => 255],
|
||||
'phone' => ['type' => 'VARCHAR', 'constraint' => 30],
|
||||
'resume_file_url' => ['type' => 'TEXT'],
|
||||
'status' => ['type' => 'VARCHAR', 'constraint' => 20, 'default' => 'new'],
|
||||
'admin_notes' => ['type' => 'TEXT', 'null' => true],
|
||||
'submitted_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
]);
|
||||
$this->forge->addKey('application_id', true);
|
||||
$this->forge->addKey('position_id', false, false, 'idx_applications_position_id');
|
||||
$this->forge->addKey('status', false, false, 'idx_applications_status');
|
||||
$this->forge->addKey('email', false, false, 'idx_applications_email');
|
||||
$this->forge->createTable('applications', true);
|
||||
}
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
$this->forge->dropTable('applications', true);
|
||||
$this->forge->dropTable('job_positions', true);
|
||||
$this->forge->dropTable('job_template_versions', true);
|
||||
$this->forge->dropTable('job_templates', true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class SeedVolunteerJobPostings extends Migration
|
||||
{
|
||||
private array $postings = [
|
||||
[
|
||||
'template_id' => 'f9df54cc-0173-4c5d-a3b6-d7ef64821e01',
|
||||
'position_id' => 'cc23227b-82b1-4bcb-8976-fcd21f0a1e01',
|
||||
'version_id' => '6a408f24-88db-4c0e-b7f2-26cc2da51e01',
|
||||
'title' => 'Grade 6 Teacher Volunteer',
|
||||
'description' => "Al Rahma Sunday School is looking for a dedicated Grade 6 Teacher Volunteer to teach and mentor students in a positive Islamic learning environment. The teacher will help students strengthen their understanding of Islamic studies, Quran, Arabic, manners, and character development.",
|
||||
'responsibilities' => "- Teach Grade 6 Sunday school lessons based on the school curriculum\n- Prepare weekly lesson plans and classroom activities\n- Help students understand Islamic values and apply them in daily life\n- Manage classroom behavior in a respectful and positive way\n- Encourage student participation, discussion, and teamwork\n- Track attendance and student progress\n- Communicate with school administration and parents when needed\n- Support school events, exams, and activities when requested",
|
||||
'requirements' => "- Strong commitment to Islamic education and community service\n- Comfortable teaching and guiding middle school students\n- Patient, responsible, organized, and dependable\n- Good communication skills\n- Prior teaching, tutoring, or youth mentoring experience preferred\n- Must be available during Sunday school hours",
|
||||
],
|
||||
[
|
||||
'template_id' => 'f9df54cc-0173-4c5d-a3b6-d7ef64821e02',
|
||||
'position_id' => 'cc23227b-82b1-4bcb-8976-fcd21f0a1e02',
|
||||
'version_id' => '6a408f24-88db-4c0e-b7f2-26cc2da51e02',
|
||||
'title' => 'Grade 8 Teacher Volunteer',
|
||||
'description' => "Al Rahma Sunday School is seeking a Grade 8 Teacher Volunteer to support students as they continue developing their Islamic knowledge, personal responsibility, and connection to the Muslim community.\n\nThe Grade 8 teacher will lead classroom lessons, encourage meaningful discussion, and help students build confidence in their faith and character.",
|
||||
'responsibilities' => "- Teach Grade 8 Sunday school curriculum\n- Prepare engaging lessons, discussions, and activities\n- Help students understand Islamic teachings in age-appropriate ways\n- Encourage respectful dialogue, critical thinking, and positive behavior\n- Support students with Quran, Islamic studies, Arabic, and character development\n- Maintain classroom order and a safe learning environment\n- Monitor attendance and student participation\n- Communicate with administration and parents as needed\n- Assist with school programs, projects, or events when requested",
|
||||
'requirements' => "- Commitment to Islamic values and youth education\n- Ability to connect with middle school students\n- Responsible, patient, respectful, and organized\n- Strong communication and classroom management skills\n- Teaching, tutoring, halaqa, or mentoring experience preferred\n- Must be dependable and available on Sundays",
|
||||
],
|
||||
[
|
||||
'template_id' => 'f9df54cc-0173-4c5d-a3b6-d7ef64821e03',
|
||||
'position_id' => 'cc23227b-82b1-4bcb-8976-fcd21f0a1e03',
|
||||
'version_id' => '6a408f24-88db-4c0e-b7f2-26cc2da51e03',
|
||||
'title' => 'Youth Teacher / Mentor Volunteer',
|
||||
'description' => "Al Rahma Sunday School is looking for a Youth Teacher / Mentor Volunteer to work with students ages 16-17. This role focuses on teaching, mentoring, and guiding older teens as they strengthen their Islamic identity, personal responsibility, leadership skills, and connection to the Muslim community.\n\nThe Youth Teacher will lead age-appropriate lessons and discussions that connect Islamic values to real-life topics students face at this stage of life.",
|
||||
'responsibilities' => "- Teach and mentor students ages 16-17\n- Lead discussions on Islamic identity, character, Quran, Seerah, manners, leadership, and real-life challenges\n- Create a respectful classroom environment where students feel comfortable asking questions\n- Encourage critical thinking, responsibility, positive decision-making, and community involvement\n- Prepare weekly lessons, activities, or discussion topics\n- Support students in developing confidence in their faith and values\n- Maintain appropriate teacher-student boundaries and a safe learning environment\n- Track attendance and student participation\n- Communicate with Sunday school administration and parents when needed\n- Support youth projects, events, service activities, or school programs when requested",
|
||||
'requirements' => "- Strong commitment to Islamic education and youth development\n- Ability to relate respectfully and effectively with older teens\n- Comfortable leading meaningful discussions with students ages 16-17\n- Patient, dependable, trustworthy, and organized\n- Good communication, leadership, and classroom management skills\n- Prior experience in teaching, mentoring, halaqas, youth programs, or community service preferred\n- Must be available during Sunday school hours",
|
||||
],
|
||||
];
|
||||
|
||||
public function up()
|
||||
{
|
||||
if (!$this->db->tableExists('job_templates') || !$this->db->tableExists('job_positions')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$now = gmdate('Y-m-d H:i:s');
|
||||
|
||||
foreach ($this->postings as $posting) {
|
||||
$shared = [
|
||||
'title' => $posting['title'],
|
||||
'description' => $posting['description'],
|
||||
'department' => 'Sunday School',
|
||||
'location' => 'Al Rahma Sunday School',
|
||||
'employment_type' => 'Volunteer',
|
||||
'responsibilities' => $posting['responsibilities'],
|
||||
'requirements' => $posting['requirements'],
|
||||
];
|
||||
|
||||
$template = $this->db->table('job_templates')
|
||||
->where('template_id', $posting['template_id'])
|
||||
->orWhere('title', $posting['title'])
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
$templateId = (string) ($template['template_id'] ?? $posting['template_id']);
|
||||
|
||||
if (!$template) {
|
||||
$this->db->table('job_templates')->insert($shared + [
|
||||
'template_id' => $templateId,
|
||||
'version' => 1,
|
||||
'is_active' => 1,
|
||||
'created_by' => null,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
}
|
||||
|
||||
if ($this->db->tableExists('job_template_versions')) {
|
||||
$versionExists = $this->db->table('job_template_versions')
|
||||
->where('version_id', $posting['version_id'])
|
||||
->countAllResults() > 0;
|
||||
|
||||
if (!$versionExists) {
|
||||
$this->db->table('job_template_versions')->insert($shared + [
|
||||
'version_id' => $posting['version_id'],
|
||||
'template_id' => $templateId,
|
||||
'version' => 1,
|
||||
'saved_by' => null,
|
||||
'saved_at' => $now,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$positionExists = $this->db->table('job_positions')
|
||||
->where('position_id', $posting['position_id'])
|
||||
->orWhere('title', $posting['title'])
|
||||
->countAllResults() > 0;
|
||||
|
||||
if (!$positionExists) {
|
||||
$this->db->table('job_positions')->insert($shared + [
|
||||
'position_id' => $posting['position_id'],
|
||||
'template_id' => $templateId,
|
||||
'status' => 'draft',
|
||||
'posted_by' => null,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
foreach ($this->postings as $posting) {
|
||||
if ($this->db->tableExists('job_positions')) {
|
||||
$this->db->table('job_positions')->where('position_id', $posting['position_id'])->delete();
|
||||
}
|
||||
|
||||
if ($this->db->tableExists('job_template_versions')) {
|
||||
$this->db->table('job_template_versions')->where('version_id', $posting['version_id'])->delete();
|
||||
}
|
||||
|
||||
if ($this->db->tableExists('job_templates')) {
|
||||
$this->db->table('job_templates')->where('template_id', $posting['template_id'])->delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class AddHideJobOpeningsPopupToPreferences extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
if (! $this->db->tableExists('user_preferences')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $this->db->fieldExists('hide_job_openings_popup', 'user_preferences')) {
|
||||
$definition = [
|
||||
'type' => 'TINYINT',
|
||||
'constraint' => 1,
|
||||
'default' => 0,
|
||||
'null' => false,
|
||||
];
|
||||
|
||||
if ($this->db->fieldExists('menu_custom_mode', 'user_preferences')) {
|
||||
$definition['after'] = 'menu_custom_mode';
|
||||
}
|
||||
|
||||
$this->forge->addColumn('user_preferences', [
|
||||
'hide_job_openings_popup' => $definition,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
if (
|
||||
$this->db->tableExists('user_preferences')
|
||||
&& $this->db->fieldExists('hide_job_openings_popup', 'user_preferences')
|
||||
) {
|
||||
$this->forge->dropColumn('user_preferences', 'hide_job_openings_popup');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class AddResponsibilitiesToJobPostings extends Migration
|
||||
{
|
||||
private array $tables = [
|
||||
'job_templates',
|
||||
'job_template_versions',
|
||||
'job_positions',
|
||||
];
|
||||
|
||||
public function up()
|
||||
{
|
||||
foreach ($this->tables as $table) {
|
||||
if (!$this->db->tableExists($table) || $this->db->fieldExists('responsibilities', $table)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->forge->addColumn($table, [
|
||||
'responsibilities' => [
|
||||
'type' => 'TEXT',
|
||||
'null' => true,
|
||||
'after' => 'employment_type',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
$this->backfillResponsibilities();
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
foreach ($this->tables as $table) {
|
||||
if ($this->db->tableExists($table) && $this->db->fieldExists('responsibilities', $table)) {
|
||||
$this->forge->dropColumn($table, 'responsibilities');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function backfillResponsibilities(): void
|
||||
{
|
||||
$primaryKeys = [
|
||||
'job_templates' => 'template_id',
|
||||
'job_template_versions' => 'version_id',
|
||||
'job_positions' => 'position_id',
|
||||
];
|
||||
|
||||
foreach ($primaryKeys as $table => $primaryKey) {
|
||||
if (!$this->db->tableExists($table) || !$this->db->fieldExists('responsibilities', $table)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$rows = $this->db->table($table)
|
||||
->select($primaryKey . ', description, responsibilities')
|
||||
->groupStart()
|
||||
->where('responsibilities', null)
|
||||
->orWhere('responsibilities', '')
|
||||
->groupEnd()
|
||||
->like('description', 'Responsibilities:')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$description = (string) ($row['description'] ?? '');
|
||||
$parts = preg_split('/\R\RResponsibilities:\R/', $description, 2);
|
||||
|
||||
if (!is_array($parts) || count($parts) !== 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->db->table($table)
|
||||
->where($primaryKey, $row[$primaryKey])
|
||||
->update([
|
||||
'description' => trim($parts[0]),
|
||||
'responsibilities' => trim($parts[1]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class BackfillJobPostingResponsibilities extends Migration
|
||||
{
|
||||
private array $primaryKeys = [
|
||||
'job_templates' => 'template_id',
|
||||
'job_template_versions' => 'version_id',
|
||||
'job_positions' => 'position_id',
|
||||
];
|
||||
|
||||
public function up()
|
||||
{
|
||||
foreach ($this->primaryKeys as $table => $primaryKey) {
|
||||
if (!$this->db->tableExists($table) || !$this->db->fieldExists('responsibilities', $table)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$rows = $this->db->table($table)
|
||||
->select($primaryKey . ', description, responsibilities')
|
||||
->groupStart()
|
||||
->where('responsibilities', null)
|
||||
->orWhere('responsibilities', '')
|
||||
->groupEnd()
|
||||
->like('description', 'Responsibilities:')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$parts = preg_split('/\R\RResponsibilities:\R/', (string) ($row['description'] ?? ''), 2);
|
||||
|
||||
if (!is_array($parts) || count($parts) !== 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->db->table($table)
|
||||
->where($primaryKey, $row[$primaryKey])
|
||||
->update([
|
||||
'description' => trim($parts[0]),
|
||||
'responsibilities' => trim($parts[1]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
// Data-only migration; do not merge responsibilities back into descriptions on rollback.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class AddPostedAtToJobPositions extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
if (!$this->db->tableExists('job_positions')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->db->fieldExists('posted_at', 'job_positions')) {
|
||||
$this->forge->addColumn('job_positions', [
|
||||
'posted_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
'after' => 'posted_by',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
$this->db->table('job_positions')
|
||||
->whereIn('status', ['open', 'closed', 'filled'])
|
||||
->where('posted_at', null)
|
||||
->set('posted_at', 'COALESCE(created_at, updated_at)', false)
|
||||
->update();
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
if ($this->db->tableExists('job_positions') && $this->db->fieldExists('posted_at', 'job_positions')) {
|
||||
$this->forge->dropColumn('job_positions', 'posted_at');
|
||||
}
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class BackfillPostedAtForClosedFilledJobPositions extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
if (!$this->db->tableExists('job_positions') || !$this->db->fieldExists('posted_at', 'job_positions')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->db->table('job_positions')
|
||||
->whereIn('status', ['closed', 'filled'])
|
||||
->where('posted_at', null)
|
||||
->set('posted_at', 'COALESCE(created_at, updated_at)', false)
|
||||
->update();
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
// Data-only fallback for legacy rows.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class AddDetailsClickCountToJobPositions extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
if (!$this->db->tableExists('job_positions') || $this->db->fieldExists('details_click_count', 'job_positions')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->forge->addColumn('job_positions', [
|
||||
'details_click_count' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'unsigned' => true,
|
||||
'default' => 0,
|
||||
'after' => 'posted_at',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
if ($this->db->tableExists('job_positions') && $this->db->fieldExists('details_click_count', 'job_positions')) {
|
||||
$this->forge->dropColumn('job_positions', 'details_click_count');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class GrantRequestedRolePageAccess extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$this->grantNavAccess();
|
||||
$this->grantNamedPermissions();
|
||||
cache()->clean();
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
}
|
||||
|
||||
private function grantNavAccess(): void
|
||||
{
|
||||
if (! $this->db->tableExists('nav_items') || ! $this->db->tableExists('role_nav_items')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->grantNavItemsToRoles(
|
||||
['head of csm', 'head_of_department_communication', 'csm_contributor'],
|
||||
[
|
||||
['label' => 'Communication'],
|
||||
['url' => 'admin/enrollment/new-students'],
|
||||
['url' => 'whatsapp/'],
|
||||
['url' => 'admin/print-requests'],
|
||||
['url' => '/administrator/absence'],
|
||||
]
|
||||
);
|
||||
|
||||
$financialNavItems = array_merge(
|
||||
[['label' => 'Financial']],
|
||||
$this->findChildNavSpecsForParentLabel('Financial'),
|
||||
[
|
||||
['label' => 'Event Management'],
|
||||
['url' => 'administrator/events'],
|
||||
['url' => 'admin/print-requests'],
|
||||
['url' => '/administrator/absence'],
|
||||
]
|
||||
);
|
||||
|
||||
$this->grantNavItemsToRoles(
|
||||
['head fa', 'head of fa', 'head_of_fa', 'head of department (finance)', 'head of department finance'],
|
||||
array_merge($financialNavItems, [
|
||||
['url' => 'admin/enrollment/new-students'],
|
||||
])
|
||||
);
|
||||
|
||||
$this->grantNavItemsToRoles(
|
||||
['financial_contributor'],
|
||||
[
|
||||
['label' => 'Financial'],
|
||||
['url' => 'payment/manual_pay'],
|
||||
['url' => '/payment/manual'],
|
||||
['url' => 'admin/print-requests'],
|
||||
['url' => '/administrator/absence'],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
private function grantNamedPermissions(): void
|
||||
{
|
||||
if (
|
||||
! $this->db->tableExists('roles')
|
||||
|| ! $this->db->tableExists('permissions')
|
||||
|| ! $this->db->tableExists('role_permissions')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->grantPermissionsToRoles(
|
||||
['head of csm', 'head_of_department_communication', 'csm_contributor'],
|
||||
[
|
||||
'view_new_students' => ['read' => true],
|
||||
]
|
||||
);
|
||||
|
||||
$this->grantPermissionsToRoles(
|
||||
['head fa', 'head of fa', 'head_of_fa', 'head of department (finance)', 'head of department finance'],
|
||||
[
|
||||
'view_new_students' => ['read' => true],
|
||||
'view_invoice' => ['read' => true],
|
||||
'view_payment' => ['read' => true],
|
||||
'view_financial_reports' => ['create' => true, 'read' => true, 'update' => true],
|
||||
'create_invoice' => ['create' => true, 'read' => true],
|
||||
'update_invoice' => ['read' => true, 'update' => true],
|
||||
'create_payment' => ['create' => true, 'read' => true],
|
||||
'update_payment' => ['read' => true, 'update' => true],
|
||||
'oversee_financial_aid' => ['create' => true, 'read' => true, 'update' => true],
|
||||
]
|
||||
);
|
||||
|
||||
$this->grantPermissionsToRoles(
|
||||
['financial_contributor'],
|
||||
[
|
||||
'view_invoice' => ['read' => true],
|
||||
'view_payment' => ['read' => true],
|
||||
'create_payment' => ['create' => true, 'read' => true],
|
||||
'update_payment' => ['read' => true, 'update' => true],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $roleKeys
|
||||
* @param list<array{label?: string, url?: string}> $navSpecs
|
||||
*/
|
||||
private function grantNavItemsToRoles(array $roleKeys, array $navSpecs): void
|
||||
{
|
||||
$navIds = $this->resolveNavItemIds($navSpecs);
|
||||
if ($navIds === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$now = date('Y-m-d H:i:s');
|
||||
|
||||
if ($this->db->fieldExists('role_id', 'role_nav_items') && $this->db->tableExists('roles')) {
|
||||
foreach ($this->resolveRoleIds($roleKeys) as $roleId) {
|
||||
foreach ($navIds as $navId) {
|
||||
$exists = $this->db->table('role_nav_items')
|
||||
->where('role_id', $roleId)
|
||||
->where('nav_item_id', $navId)
|
||||
->countAllResults() > 0;
|
||||
|
||||
if (! $exists) {
|
||||
$this->db->table('role_nav_items')->insert([
|
||||
'role_id' => $roleId,
|
||||
'nav_item_id' => $navId,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->db->fieldExists('role', 'role_nav_items')) {
|
||||
foreach ($roleKeys as $role) {
|
||||
foreach ($navIds as $navId) {
|
||||
$exists = $this->db->table('role_nav_items')
|
||||
->where('LOWER(role)', strtolower($role))
|
||||
->where('nav_item_id', $navId)
|
||||
->countAllResults() > 0;
|
||||
|
||||
if (! $exists) {
|
||||
$insert = [
|
||||
'role' => strtolower($role),
|
||||
'nav_item_id' => $navId,
|
||||
'created_at' => $now,
|
||||
];
|
||||
if ($this->db->fieldExists('updated_at', 'role_nav_items')) {
|
||||
$insert['updated_at'] = $now;
|
||||
}
|
||||
$this->db->table('role_nav_items')->insert($insert);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $roleKeys
|
||||
* @param array<string, array{create?: bool, read?: bool, update?: bool, delete?: bool}> $permissions
|
||||
*/
|
||||
private function grantPermissionsToRoles(array $roleKeys, array $permissions): void
|
||||
{
|
||||
$roleIds = $this->resolveRoleIds($roleKeys);
|
||||
if ($roleIds === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$now = date('Y-m-d H:i:s');
|
||||
|
||||
foreach ($permissions as $permissionName => $flags) {
|
||||
$permissionId = $this->resolvePermissionId($permissionName);
|
||||
if ($permissionId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($roleIds as $roleId) {
|
||||
$existing = $this->db->table('role_permissions')
|
||||
->where('role_id', $roleId)
|
||||
->where('permission_id', $permissionId)
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
$grant = [
|
||||
'can_create' => ! empty($flags['create']) ? 1 : 0,
|
||||
'can_read' => ! empty($flags['read']) ? 1 : 0,
|
||||
'can_update' => ! empty($flags['update']) ? 1 : 0,
|
||||
'can_delete' => ! empty($flags['delete']) ? 1 : 0,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
|
||||
if ($this->db->fieldExists('can_manage', 'role_permissions')) {
|
||||
$grant['can_manage'] = 0;
|
||||
}
|
||||
|
||||
if ($existing === null) {
|
||||
$grant['role_id'] = $roleId;
|
||||
$grant['permission_id'] = $permissionId;
|
||||
$grant['created_at'] = $now;
|
||||
$this->db->table('role_permissions')->insert($grant);
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->db->table('role_permissions')
|
||||
->where('id', (int) $existing['id'])
|
||||
->update([
|
||||
'can_create' => max((int) ($existing['can_create'] ?? 0), $grant['can_create']),
|
||||
'can_read' => max((int) ($existing['can_read'] ?? 0), $grant['can_read']),
|
||||
'can_update' => max((int) ($existing['can_update'] ?? 0), $grant['can_update']),
|
||||
'can_delete' => max((int) ($existing['can_delete'] ?? 0), $grant['can_delete']),
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $roleKeys
|
||||
* @return list<int>
|
||||
*/
|
||||
private function resolveRoleIds(array $roleKeys): array
|
||||
{
|
||||
if (! $this->db->tableExists('roles')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$normalized = array_values(array_unique(array_map('strtolower', $roleKeys)));
|
||||
if ($normalized === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$builder = $this->db->table('roles')->select('id');
|
||||
$builder->groupStart()->whereIn('LOWER(name)', $normalized);
|
||||
if ($this->db->fieldExists('slug', 'roles')) {
|
||||
$builder->orWhereIn('LOWER(slug)', $normalized);
|
||||
}
|
||||
$rows = $builder->groupEnd()->get()->getResultArray();
|
||||
|
||||
return array_values(array_unique(array_map(static fn (array $row): int => (int) $row['id'], $rows)));
|
||||
}
|
||||
|
||||
private function resolvePermissionId(string $permissionName): int
|
||||
{
|
||||
$permission = $this->db->table('permissions')
|
||||
->select('id')
|
||||
->where('LOWER(name)', strtolower($permissionName))
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
if ($permission !== null) {
|
||||
return (int) $permission['id'];
|
||||
}
|
||||
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$insert = [
|
||||
'name' => $permissionName,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
if ($this->db->fieldExists('description', 'permissions')) {
|
||||
$insert['description'] = 'Seeded route permission.';
|
||||
}
|
||||
|
||||
$this->db->table('permissions')->insert($insert);
|
||||
|
||||
return (int) $this->db->insertID();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array{label?: string, url?: string}> $navSpecs
|
||||
* @return list<int>
|
||||
*/
|
||||
private function resolveNavItemIds(array $navSpecs): array
|
||||
{
|
||||
$ids = [];
|
||||
foreach ($navSpecs as $spec) {
|
||||
if (isset($spec['url'])) {
|
||||
$url = $this->normalizePath($spec['url']);
|
||||
$rows = $this->db->table('nav_items')
|
||||
->select('id, url')
|
||||
->where('url IS NOT NULL', null, false)
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
foreach ($rows as $row) {
|
||||
if ($this->normalizePath((string) ($row['url'] ?? '')) === $url) {
|
||||
$ids[] = (int) $row['id'];
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
} elseif (isset($spec['label'])) {
|
||||
$builder = $this->db->table('nav_items')->select('id');
|
||||
$builder->where('LOWER(label)', strtolower($spec['label']));
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($builder->get()->getResultArray() as $row) {
|
||||
$ids[] = (int) $row['id'];
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_unique(array_filter($ids)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{url: string}>
|
||||
*/
|
||||
private function findChildNavSpecsForParentLabel(string $parentLabel): array
|
||||
{
|
||||
$parentColumn = $this->parentColumn();
|
||||
if ($parentColumn === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$parents = $this->db->table('nav_items')
|
||||
->select('id')
|
||||
->where('LOWER(label)', strtolower($parentLabel))
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
$parentIds = array_values(array_filter(array_map(static fn (array $row): int => (int) $row['id'], $parents)));
|
||||
if ($parentIds === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$children = $this->db->table('nav_items')
|
||||
->select('url')
|
||||
->whereIn($parentColumn, $parentIds)
|
||||
->where('url IS NOT NULL', null, false)
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
return array_values(array_filter(array_map(
|
||||
static fn (array $row): array => ['url' => (string) $row['url']],
|
||||
$children
|
||||
), static fn (array $spec): bool => trim($spec['url']) !== ''));
|
||||
}
|
||||
|
||||
private function parentColumn(): ?string
|
||||
{
|
||||
if ($this->db->fieldExists('menu_parent_id', 'nav_items')) {
|
||||
return 'menu_parent_id';
|
||||
}
|
||||
|
||||
if ($this->db->fieldExists('parent_id', 'nav_items')) {
|
||||
return 'parent_id';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function normalizePath(string $path): string
|
||||
{
|
||||
return trim(preg_replace('#/+#', '/', $path), '/');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class BackfillHeadFaNewStudentsAccess extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$roleIds = $this->resolveRoleIds([
|
||||
'head fa',
|
||||
'head of fa',
|
||||
'head_of_fa',
|
||||
'head of department (finance)',
|
||||
'head of department finance',
|
||||
]);
|
||||
|
||||
if ($roleIds === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->grantPermission($roleIds, 'view_new_students', ['read' => true]);
|
||||
$this->grantNavUrl($roleIds, 'admin/enrollment/new-students');
|
||||
cache()->clean();
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $roleKeys
|
||||
* @return list<int>
|
||||
*/
|
||||
private function resolveRoleIds(array $roleKeys): array
|
||||
{
|
||||
if (! $this->db->tableExists('roles')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$normalized = array_values(array_unique(array_map('strtolower', $roleKeys)));
|
||||
$builder = $this->db->table('roles')->select('id');
|
||||
$builder->groupStart()->whereIn('LOWER(name)', $normalized);
|
||||
if ($this->db->fieldExists('slug', 'roles')) {
|
||||
$builder->orWhereIn('LOWER(slug)', $normalized);
|
||||
}
|
||||
$rows = $builder->groupEnd()->get()->getResultArray();
|
||||
|
||||
return array_values(array_unique(array_map(static fn (array $row): int => (int) $row['id'], $rows)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<int> $roleIds
|
||||
* @param array{create?: bool, read?: bool, update?: bool, delete?: bool} $flags
|
||||
*/
|
||||
private function grantPermission(array $roleIds, string $permissionName, array $flags): void
|
||||
{
|
||||
if (! $this->db->tableExists('permissions') || ! $this->db->tableExists('role_permissions')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$permissionId = $this->resolvePermissionId($permissionName);
|
||||
$now = date('Y-m-d H:i:s');
|
||||
|
||||
foreach ($roleIds as $roleId) {
|
||||
$existing = $this->db->table('role_permissions')
|
||||
->where('role_id', $roleId)
|
||||
->where('permission_id', $permissionId)
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
$grant = [
|
||||
'can_create' => ! empty($flags['create']) ? 1 : 0,
|
||||
'can_read' => ! empty($flags['read']) ? 1 : 0,
|
||||
'can_update' => ! empty($flags['update']) ? 1 : 0,
|
||||
'can_delete' => ! empty($flags['delete']) ? 1 : 0,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
|
||||
if ($existing === null) {
|
||||
$grant['role_id'] = $roleId;
|
||||
$grant['permission_id'] = $permissionId;
|
||||
$grant['created_at'] = $now;
|
||||
if ($this->db->fieldExists('can_manage', 'role_permissions')) {
|
||||
$grant['can_manage'] = 0;
|
||||
}
|
||||
$this->db->table('role_permissions')->insert($grant);
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->db->table('role_permissions')
|
||||
->where('id', (int) $existing['id'])
|
||||
->update([
|
||||
'can_create' => max((int) ($existing['can_create'] ?? 0), $grant['can_create']),
|
||||
'can_read' => max((int) ($existing['can_read'] ?? 0), $grant['can_read']),
|
||||
'can_update' => max((int) ($existing['can_update'] ?? 0), $grant['can_update']),
|
||||
'can_delete' => max((int) ($existing['can_delete'] ?? 0), $grant['can_delete']),
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function resolvePermissionId(string $permissionName): int
|
||||
{
|
||||
$permission = $this->db->table('permissions')
|
||||
->select('id')
|
||||
->where('LOWER(name)', strtolower($permissionName))
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
if ($permission !== null) {
|
||||
return (int) $permission['id'];
|
||||
}
|
||||
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$insert = [
|
||||
'name' => $permissionName,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
if ($this->db->fieldExists('description', 'permissions')) {
|
||||
$insert['description'] = 'Seeded route permission.';
|
||||
}
|
||||
|
||||
$this->db->table('permissions')->insert($insert);
|
||||
|
||||
return (int) $this->db->insertID();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<int> $roleIds
|
||||
*/
|
||||
private function grantNavUrl(array $roleIds, string $url): void
|
||||
{
|
||||
if (
|
||||
! $this->db->tableExists('nav_items')
|
||||
|| ! $this->db->tableExists('role_nav_items')
|
||||
|| ! $this->db->fieldExists('role_id', 'role_nav_items')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$navIds = $this->resolveNavItemIdsByUrl($url);
|
||||
if ($navIds === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$now = date('Y-m-d H:i:s');
|
||||
foreach ($roleIds as $roleId) {
|
||||
foreach ($navIds as $navId) {
|
||||
$exists = $this->db->table('role_nav_items')
|
||||
->where('role_id', $roleId)
|
||||
->where('nav_item_id', $navId)
|
||||
->countAllResults() > 0;
|
||||
|
||||
if (! $exists) {
|
||||
$this->db->table('role_nav_items')->insert([
|
||||
'role_id' => $roleId,
|
||||
'nav_item_id' => $navId,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<int>
|
||||
*/
|
||||
private function resolveNavItemIdsByUrl(string $url): array
|
||||
{
|
||||
$targetUrl = $this->normalizePath($url);
|
||||
$rows = $this->db->table('nav_items')
|
||||
->select('id, url')
|
||||
->where('url IS NOT NULL', null, false)
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
$ids = [];
|
||||
foreach ($rows as $row) {
|
||||
if ($this->normalizePath((string) ($row['url'] ?? '')) === $targetUrl) {
|
||||
$ids[] = (int) $row['id'];
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_unique(array_filter($ids)));
|
||||
}
|
||||
|
||||
private function normalizePath(string $path): string
|
||||
{
|
||||
return trim(preg_replace('#/+#', '/', $path), '/');
|
||||
}
|
||||
}
|
||||
@@ -81,6 +81,18 @@ class AuthFilter implements FilterInterface
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->isAllowedByGrantedNavItem($request, $roleIds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->isAllowedFamilyCardRequest($request, $roleIds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->isAllowedPrintRequestSupportRequest($request, $roleIds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return $this->deny($request, "You don't have permission to use this feature.");
|
||||
}
|
||||
|
||||
@@ -271,4 +283,128 @@ class AuthFilter implements FilterInterface
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function isAllowedByGrantedNavItem(RequestInterface $request, array $roleIds): bool
|
||||
{
|
||||
if (
|
||||
empty($roleIds)
|
||||
|| ! $this->db->tableExists('role_nav_items')
|
||||
|| ! $this->db->tableExists('nav_items')
|
||||
|| ! $this->db->fieldExists('role_id', 'role_nav_items')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$path = $this->normalizePath($request->getUri()->getPath());
|
||||
if ($path === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$rows = $this->db->table('role_nav_items rni')
|
||||
->select('ni.url')
|
||||
->join('nav_items ni', 'ni.id = rni.nav_item_id')
|
||||
->whereIn('rni.role_id', $roleIds)
|
||||
->where('ni.url IS NOT NULL', null, false)
|
||||
->where('ni.is_enabled', 1)
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$url = $this->normalizePath((string) ($row['url'] ?? ''));
|
||||
if ($url === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($path === $url || str_starts_with($path . '/', rtrim($url, '/') . '/')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (str_starts_with($url, 'admin/')) {
|
||||
$withoutAdminPrefix = substr($url, 6);
|
||||
if ($path === $withoutAdminPrefix || str_starts_with($path . '/', rtrim($withoutAdminPrefix, '/') . '/')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function isAllowedFamilyCardRequest(RequestInterface $request, array $roleIds): bool
|
||||
{
|
||||
if ($this->normalizePath($request->getUri()->getPath()) !== 'family/card') {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (['view_new_students', 'view_financial_reports'] as $permissionName) {
|
||||
if ($this->userHasNamedPermission($roleIds, $permissionName, 'read')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function isAllowedPrintRequestSupportRequest(RequestInterface $request, array $roleIds): bool
|
||||
{
|
||||
$path = $this->normalizePath($request->getUri()->getPath());
|
||||
$printRequestPaths = [
|
||||
'print-requests/update/',
|
||||
'print-requests/delete/',
|
||||
'print-requests/file/',
|
||||
'uploads/print_requests/',
|
||||
];
|
||||
|
||||
foreach ($printRequestPaths as $prefix) {
|
||||
if (str_starts_with($path, $prefix)) {
|
||||
return $this->hasGrantedNavUrl($roleIds, 'admin/print-requests');
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function hasGrantedNavUrl(array $roleIds, string $url): bool
|
||||
{
|
||||
if (
|
||||
empty($roleIds)
|
||||
|| ! $this->db->tableExists('role_nav_items')
|
||||
|| ! $this->db->tableExists('nav_items')
|
||||
|| ! $this->db->fieldExists('role_id', 'role_nav_items')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$targetUrl = $this->normalizePath($url);
|
||||
$rows = $this->db->table('role_nav_items rni')
|
||||
->select('ni.url')
|
||||
->join('nav_items ni', 'ni.id = rni.nav_item_id')
|
||||
->whereIn('rni.role_id', $roleIds)
|
||||
->where('ni.url IS NOT NULL', null, false)
|
||||
->where('ni.is_enabled', 1)
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
foreach ($rows as $row) {
|
||||
if ($this->normalizePath((string) ($row['url'] ?? '')) === $targetUrl) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function normalizePath(string $path): string
|
||||
{
|
||||
$path = trim(preg_replace('#/+#', '/', $path), '/');
|
||||
if ($path === 'index.php') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (str_starts_with($path, 'index.php/')) {
|
||||
return substr($path, 10);
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,6 @@ class CleanupScheduler implements FilterInterface
|
||||
{
|
||||
// Call the cleanup controller method
|
||||
\CodeIgniter\CLI\CLI::init();
|
||||
command('cleanup:unverified_users');
|
||||
command('users:delete-inactive-users');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ final class SchoolYearWritableFilter implements FilterInterface
|
||||
'api/register',
|
||||
'user/select_role',
|
||||
'set-role',
|
||||
'parent_dashboard/job-openings-popup',
|
||||
'processForgotPassword',
|
||||
'user/forgot_password',
|
||||
'user/processResetPassword',
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class ApplicationModel extends Model
|
||||
{
|
||||
protected $table = 'applications';
|
||||
protected $primaryKey = 'application_id';
|
||||
protected $useAutoIncrement = false;
|
||||
protected $returnType = 'array';
|
||||
protected $allowedFields = [
|
||||
'application_id',
|
||||
'position_id',
|
||||
'first_name',
|
||||
'last_name',
|
||||
'email',
|
||||
'phone',
|
||||
'resume_file_url',
|
||||
'status',
|
||||
'admin_notes',
|
||||
'submitted_at',
|
||||
];
|
||||
protected $useTimestamps = false;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class JobPositionModel extends Model
|
||||
{
|
||||
protected $table = 'job_positions';
|
||||
protected $primaryKey = 'position_id';
|
||||
protected $useAutoIncrement = false;
|
||||
protected $returnType = 'array';
|
||||
protected $allowedFields = [
|
||||
'position_id',
|
||||
'template_id',
|
||||
'title',
|
||||
'description',
|
||||
'department',
|
||||
'location',
|
||||
'employment_type',
|
||||
'responsibilities',
|
||||
'requirements',
|
||||
'status',
|
||||
'posted_by',
|
||||
'posted_at',
|
||||
'details_click_count',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
|
||||
public function openPositions(): array
|
||||
{
|
||||
return $this
|
||||
->where('status', 'open')
|
||||
->orderBy('posted_at', 'DESC')
|
||||
->orderBy('created_at', 'DESC')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
public function recordDetailsClick(string $positionId): bool
|
||||
{
|
||||
return $this->builder()
|
||||
->set('details_click_count', 'COALESCE(details_click_count, 0) + 1', false)
|
||||
->where('position_id', $positionId)
|
||||
->where('status', 'open')
|
||||
->update();
|
||||
}
|
||||
|
||||
public function adminPositions(): array
|
||||
{
|
||||
return $this
|
||||
->orderBy('updated_at', 'DESC')
|
||||
->orderBy('created_at', 'DESC')
|
||||
->findAll();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class JobTemplateModel extends Model
|
||||
{
|
||||
protected $table = 'job_templates';
|
||||
protected $primaryKey = 'template_id';
|
||||
protected $useAutoIncrement = false;
|
||||
protected $returnType = 'array';
|
||||
protected $allowedFields = [
|
||||
'template_id',
|
||||
'title',
|
||||
'description',
|
||||
'department',
|
||||
'location',
|
||||
'employment_type',
|
||||
'responsibilities',
|
||||
'requirements',
|
||||
'version',
|
||||
'is_active',
|
||||
'created_by',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class JobTemplateVersionModel extends Model
|
||||
{
|
||||
protected $table = 'job_template_versions';
|
||||
protected $primaryKey = 'version_id';
|
||||
protected $useAutoIncrement = false;
|
||||
protected $returnType = 'array';
|
||||
protected $allowedFields = [
|
||||
'version_id',
|
||||
'template_id',
|
||||
'version',
|
||||
'title',
|
||||
'description',
|
||||
'department',
|
||||
'location',
|
||||
'employment_type',
|
||||
'responsibilities',
|
||||
'requirements',
|
||||
'saved_by',
|
||||
'saved_at',
|
||||
];
|
||||
protected $useTimestamps = false;
|
||||
}
|
||||
@@ -22,6 +22,7 @@ class PreferencesModel extends Model
|
||||
'menu_custom_bg', // Custom menu background color
|
||||
'menu_custom_text', // Custom menu text color
|
||||
'menu_custom_mode', // Custom menu mode: light|dark
|
||||
'hide_job_openings_popup', // Parent dismissed volunteer openings popup
|
||||
'created_at', // Timestamp of when the record was created
|
||||
'updated_at' // Timestamp of when the record was last updated
|
||||
];
|
||||
|
||||
@@ -63,8 +63,16 @@ class RoleModel extends Model
|
||||
$names = array_values(array_filter(array_map('strval', $names)));
|
||||
if (empty($names)) return [];
|
||||
|
||||
// collation is usually case-insensitive; if not, add LOWER() both sides
|
||||
$ids = $this->select('id')->whereIn('name', $names)->findColumn('id');
|
||||
$lower = array_values(array_unique(array_map('strtolower', $names)));
|
||||
$builder = $this->select('id')
|
||||
->groupStart()
|
||||
->whereIn('LOWER(name)', $lower);
|
||||
|
||||
if ($this->db->fieldExists('slug', $this->table)) {
|
||||
$builder->orWhereIn('LOWER(slug)', $lower);
|
||||
}
|
||||
|
||||
$ids = $builder->groupEnd()->findColumn('id');
|
||||
return array_map('intval', $ids ?? []);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -492,18 +492,6 @@ class StudentModel extends Model
|
||||
(
|
||||
student_class.student_id IS NOT NULL
|
||||
OR enrollments.student_id IS NOT NULL
|
||||
OR (
|
||||
NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM student_class sc_history
|
||||
WHERE sc_history.student_id = students.id
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM enrollments e_history
|
||||
WHERE e_history.student_id = students.id
|
||||
)
|
||||
)
|
||||
)
|
||||
";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class UserAccessProfileModel extends Model
|
||||
{
|
||||
public const CATEGORY_ADMIN = 'admin';
|
||||
public const CATEGORY_TEACHER = 'teacher';
|
||||
public const CATEGORY_PARENT = 'parent';
|
||||
public const CATEGORY_GUEST = 'guest';
|
||||
|
||||
private const TEACHER_ROLE_TOKENS = ['teacher', 'teacher_assistant', 'teacher assistant', 'assistant_teacher', 'ta'];
|
||||
private const NON_ADMIN_ROLE_TOKENS = ['guest', 'parent', 'student', 'teacher', 'teacher_assistant', 'teacher assistant', 'assistant_teacher', 'ta'];
|
||||
|
||||
protected $table = 'user_access_profiles';
|
||||
protected $primaryKey = 'id';
|
||||
protected $returnType = 'array';
|
||||
protected $allowedFields = [
|
||||
'user_id',
|
||||
'primary_category',
|
||||
'is_admin',
|
||||
'is_teacher',
|
||||
'is_parent',
|
||||
'role_names',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
|
||||
public function syncUser(int $userId): ?array
|
||||
{
|
||||
if ($userId <= 0 || ! $this->db->tableExists($this->table)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$roles = $this->rolesForUser($userId);
|
||||
$flags = self::flagsForRoles($roles);
|
||||
$now = utc_now();
|
||||
|
||||
$data = [
|
||||
'user_id' => $userId,
|
||||
'primary_category' => self::primaryCategory($flags),
|
||||
'is_admin' => $flags['is_admin'] ? 1 : 0,
|
||||
'is_teacher' => $flags['is_teacher'] ? 1 : 0,
|
||||
'is_parent' => $flags['is_parent'] ? 1 : 0,
|
||||
'role_names' => implode(', ', array_values(array_unique(array_map(
|
||||
static fn (array $role): string => (string) ($role['name'] ?? ''),
|
||||
$roles
|
||||
)))),
|
||||
'updated_at' => $now,
|
||||
];
|
||||
|
||||
$existing = $this->where('user_id', $userId)->first();
|
||||
if ($existing) {
|
||||
$this->update((int) $existing['id'], $data);
|
||||
return $this->find((int) $existing['id']);
|
||||
}
|
||||
|
||||
$data['created_at'] = $now;
|
||||
$id = $this->insert($data);
|
||||
|
||||
return $id ? $this->find((int) $id) : null;
|
||||
}
|
||||
|
||||
public function syncAll(): void
|
||||
{
|
||||
if (! $this->db->tableExists('users') || ! $this->db->tableExists($this->table)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$rows = $this->db->table('users')->select('id')->get()->getResultArray();
|
||||
foreach ($rows as $row) {
|
||||
$this->syncUser((int) ($row['id'] ?? 0));
|
||||
}
|
||||
}
|
||||
|
||||
public function getForUser(int $userId): ?array
|
||||
{
|
||||
if ($userId <= 0 || ! $this->db->tableExists($this->table)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$profile = $this->where('user_id', $userId)->first();
|
||||
|
||||
return $profile ?: $this->syncUser($userId);
|
||||
}
|
||||
|
||||
public function getUsersByCategory(string $category): array
|
||||
{
|
||||
$field = match (self::normalizeRoleToken($category)) {
|
||||
self::CATEGORY_ADMIN => 'is_admin',
|
||||
self::CATEGORY_TEACHER => 'is_teacher',
|
||||
self::CATEGORY_PARENT => 'is_parent',
|
||||
default => null,
|
||||
};
|
||||
|
||||
if ($field === null || ! $this->db->tableExists($this->table)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->select('users.*, user_access_profiles.primary_category, user_access_profiles.role_names')
|
||||
->join('users', 'users.id = user_access_profiles.user_id', 'inner')
|
||||
->where($field, 1)
|
||||
->orderBy('users.lastname', 'ASC')
|
||||
->orderBy('users.firstname', 'ASC')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
public static function flagsForRoles(array $roles): array
|
||||
{
|
||||
$tokens = [];
|
||||
foreach ($roles as $role) {
|
||||
if (is_array($role)) {
|
||||
$tokens[] = self::normalizeRoleToken((string) ($role['slug'] ?? ''));
|
||||
$tokens[] = self::normalizeRoleToken((string) ($role['name'] ?? ''));
|
||||
continue;
|
||||
}
|
||||
|
||||
$tokens[] = self::normalizeRoleToken((string) $role);
|
||||
}
|
||||
|
||||
$tokens = array_values(array_unique(array_filter($tokens)));
|
||||
$isParent = in_array('parent', $tokens, true);
|
||||
$isTeacher = count(array_intersect($tokens, array_map([self::class, 'normalizeRoleToken'], self::TEACHER_ROLE_TOKENS))) > 0;
|
||||
$isAdmin = false;
|
||||
|
||||
foreach ($tokens as $token) {
|
||||
if (! in_array($token, array_map([self::class, 'normalizeRoleToken'], self::NON_ADMIN_ROLE_TOKENS), true)) {
|
||||
$isAdmin = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'is_admin' => $isAdmin,
|
||||
'is_teacher' => $isTeacher,
|
||||
'is_parent' => $isParent,
|
||||
];
|
||||
}
|
||||
|
||||
public static function primaryCategory(array $flags): string
|
||||
{
|
||||
if (! empty($flags['is_admin'])) {
|
||||
return self::CATEGORY_ADMIN;
|
||||
}
|
||||
|
||||
if (! empty($flags['is_teacher'])) {
|
||||
return self::CATEGORY_TEACHER;
|
||||
}
|
||||
|
||||
if (! empty($flags['is_parent'])) {
|
||||
return self::CATEGORY_PARENT;
|
||||
}
|
||||
|
||||
return self::CATEGORY_GUEST;
|
||||
}
|
||||
|
||||
private function rolesForUser(int $userId): array
|
||||
{
|
||||
if (! $this->db->tableExists('user_roles') || ! $this->db->tableExists('roles')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$builder = $this->db->table('user_roles ur')
|
||||
->select('r.name, r.slug')
|
||||
->join('roles r', 'r.id = ur.role_id', 'inner')
|
||||
->where('ur.user_id', $userId)
|
||||
->where('COALESCE(r.is_active, 1) = 1', null, false);
|
||||
|
||||
if ($this->db->fieldExists('deleted_at', 'user_roles')) {
|
||||
$builder->where('ur.deleted_at', null);
|
||||
}
|
||||
|
||||
return $builder->get()->getResultArray();
|
||||
}
|
||||
|
||||
private static function normalizeRoleToken(string $value): string
|
||||
{
|
||||
$value = strtolower(trim($value));
|
||||
return str_replace([' ', '-'], '_', $value);
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,36 @@ class UserModel extends Model
|
||||
protected $useTimestamps = true; // Enable automatic timestamps
|
||||
protected $createdField = 'created_at'; // Define the field name for the created timestamp
|
||||
protected $updatedField = 'updated_at'; // Define the field name for the updated timestamp
|
||||
protected $afterInsert = ['syncAccessProfileAfterInsert'];
|
||||
protected $afterDelete = ['deleteAccessProfileAfterDelete'];
|
||||
|
||||
protected function syncAccessProfileAfterInsert(array $data): array
|
||||
{
|
||||
try {
|
||||
$userId = (int) ($data['id'] ?? 0);
|
||||
if ($userId > 0) {
|
||||
model(UserAccessProfileModel::class)->syncUser($userId);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'UserModel access profile sync failed: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function deleteAccessProfileAfterDelete(array $data): array
|
||||
{
|
||||
try {
|
||||
$ids = array_filter(array_map('intval', (array) ($data['id'] ?? [])));
|
||||
if ($ids !== [] && $this->db->tableExists('user_access_profiles')) {
|
||||
$this->db->table('user_access_profiles')->whereIn('user_id', $ids)->delete();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'UserModel access profile cleanup failed: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
// Existing methods remain unchanged
|
||||
|
||||
@@ -55,12 +85,12 @@ class UserModel extends Model
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unverified users created more than 2 minutes ago.
|
||||
* Get unverified users created more than 15 minutes ago.
|
||||
*/
|
||||
public function getUnverifiedUsers()
|
||||
{
|
||||
return $this->where('is_verified', 0)
|
||||
->where('created_at <', date('Y-m-d H:i:s', time() - 120))
|
||||
->where('created_at <', date('Y-m-d H:i:s', time() - 15 * 60))
|
||||
->findAll();
|
||||
}
|
||||
|
||||
|
||||
@@ -33,9 +33,10 @@ class UserRoleModel extends Model
|
||||
$userId = (int) ($data['data']['user_id'] ?? 0);
|
||||
if ($userId > 0) {
|
||||
service('staffDirectorySync')->syncUser($userId);
|
||||
model(UserAccessProfileModel::class)->syncUser($userId);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'UserRoleModel staff directory sync failed: ' . $e->getMessage());
|
||||
log_message('error', 'UserRoleModel role-derived sync failed: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
return $data;
|
||||
@@ -45,8 +46,9 @@ class UserRoleModel extends Model
|
||||
{
|
||||
try {
|
||||
service('staffDirectorySync')->syncAll();
|
||||
model(UserAccessProfileModel::class)->syncAll();
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'UserRoleModel staff directory delete sync failed: ' . $e->getMessage());
|
||||
log_message('error', 'UserRoleModel role-derived delete sync failed: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
return $data;
|
||||
|
||||
@@ -825,16 +825,12 @@ final class EnrollmentTransitionService
|
||||
private function firstEnrollmentPlacement(array $student, string $targetSchoolYear): array
|
||||
{
|
||||
$grade = $this->classBaseName((string) ($student['registration_grade'] ?? ''));
|
||||
$targetClass = $grade !== '' ? $this->classByName($grade, $targetSchoolYear) : null;
|
||||
$targetSection = is_array($targetClass)
|
||||
? $this->baseSectionForClass((int) ($targetClass['id'] ?? 0), $targetSchoolYear)
|
||||
: null;
|
||||
|
||||
return [
|
||||
'assigned_grade_id' => $targetClass['id'] ?? null,
|
||||
'assigned_grade_name' => $targetClass['class_name'] ?? ($grade !== '' ? $grade : null),
|
||||
'assigned_class_section_id' => $targetSection['class_section_id'] ?? null,
|
||||
'placement_status' => $targetSection === null ? 'manual_class_required' : 'same_class_assigned',
|
||||
'assigned_grade_id' => null,
|
||||
'assigned_grade_name' => $grade !== '' ? $grade : null,
|
||||
'assigned_class_section_id' => null,
|
||||
'placement_status' => 'manual_class_required',
|
||||
'flags' => [],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Parents;
|
||||
|
||||
use App\Controllers\View\EmailController;
|
||||
use App\Models\AuthorizedUserModel;
|
||||
use App\Models\UserModel;
|
||||
use App\Services\SchoolIdService;
|
||||
use CodeIgniter\Database\BaseConnection;
|
||||
use Exception;
|
||||
|
||||
class ParentAccountService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly BaseConnection $db,
|
||||
private readonly UserModel $userModel,
|
||||
private readonly AuthorizedUserModel $authorizedUsersModel,
|
||||
) {
|
||||
}
|
||||
|
||||
public function canAccessUserRecord(int $requestedUserId, int $sessionUserId, array $sessionRoles): bool
|
||||
{
|
||||
if ($sessionUserId <= 0 || $requestedUserId <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($sessionUserId === $requestedUserId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$roles = array_map(
|
||||
static fn ($role): string => strtolower(trim((string) $role)),
|
||||
array_filter($sessionRoles)
|
||||
);
|
||||
|
||||
return (bool) array_intersect($roles, ['administrator', 'administrative staff', 'principal', 'admin']);
|
||||
}
|
||||
|
||||
public function isEmailUnique(string $email): bool
|
||||
{
|
||||
foreach (['users' => 'email', 'emergency_contacts' => 'email'] as $table => $column) {
|
||||
if ($this->db->table($table)->where($column, $email)->countAllResults() > 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function administratorParentList(): array
|
||||
{
|
||||
return $this->userModel->where('role', 'parent')->findAll();
|
||||
}
|
||||
|
||||
public function parentById(int $id): ?array
|
||||
{
|
||||
$parent = $this->userModel->find($id);
|
||||
|
||||
return is_array($parent) ? $parent : null;
|
||||
}
|
||||
|
||||
public function createAdministratorParent(array $post): bool|int|string
|
||||
{
|
||||
return $this->userModel->insert([
|
||||
'firstname' => $post['firstname'] ?? null,
|
||||
'lastname' => $post['lastname'] ?? null,
|
||||
'email' => strtolower((string) ($post['email'] ?? '')),
|
||||
'password' => password_hash((string) ($post['password'] ?? ''), PASSWORD_DEFAULT),
|
||||
'role' => 'parent',
|
||||
]);
|
||||
}
|
||||
|
||||
public function updateAdministratorParent(int $id, array $post): bool
|
||||
{
|
||||
$data = [
|
||||
'firstname' => $post['firstname'] ?? null,
|
||||
'lastname' => $post['lastname'] ?? null,
|
||||
'email' => strtolower((string) ($post['email'] ?? '')),
|
||||
];
|
||||
|
||||
if (! empty($post['password'])) {
|
||||
$data['password'] = password_hash((string) $post['password'], PASSWORD_DEFAULT);
|
||||
}
|
||||
|
||||
return (bool) $this->userModel->update($id, $data);
|
||||
}
|
||||
|
||||
public function deleteParent(int $id): bool
|
||||
{
|
||||
return (bool) $this->userModel->delete($id);
|
||||
}
|
||||
|
||||
public function createRelatedUser(array $userData, string $relationToStudent, string $semester, string $schoolYear): int|false
|
||||
{
|
||||
$schoolIdService = new SchoolIdService();
|
||||
$token = bin2hex(random_bytes(48));
|
||||
$tokenHash = hash('sha256', $token);
|
||||
$userType = in_array(strtolower($relationToStudent), ['wife', 'husband'], true) ? 'Secondary' : 'Tertiary';
|
||||
|
||||
$validation = \Config\Services::validation();
|
||||
$validation->setRules([
|
||||
'firstname' => [
|
||||
'label' => 'First Name',
|
||||
'rules' => 'required|min_length[2]|max_length[100]|regex_match[/^[A-Za-z\s\-]+$/]',
|
||||
'errors' => ['regex_match' => 'First name may only contain letters, spaces, and dashes.'],
|
||||
],
|
||||
'lastname' => [
|
||||
'label' => 'Last Name',
|
||||
'rules' => 'required|min_length[2]|max_length[100]|regex_match[/^[A-Za-z\s\-]+$/]',
|
||||
'errors' => ['regex_match' => 'Last name may only contain letters, spaces, and dashes.'],
|
||||
],
|
||||
'email' => [
|
||||
'label' => 'Email Address',
|
||||
'rules' => 'required|valid_email|max_length[150]|is_unique[users.email]',
|
||||
'errors' => ['is_unique' => 'This email is already registered.'],
|
||||
],
|
||||
'cellphone' => [
|
||||
'label' => 'Cell Phone',
|
||||
'rules' => 'required|regex_match[/^\d{10}$/]',
|
||||
'errors' => ['regex_match' => 'Phone number must be exactly 10 digits.'],
|
||||
],
|
||||
'gender' => 'required|in_list[Male,Female]',
|
||||
'city' => 'required|max_length[100]',
|
||||
'state' => 'required|max_length[100]',
|
||||
'zip' => 'required|regex_match[/^\d{5}$/]',
|
||||
]);
|
||||
|
||||
if (! $validation->run($userData)) {
|
||||
log_message('error', 'User creation failed due to invalid data: ' . json_encode($validation->getErrors()));
|
||||
return false;
|
||||
}
|
||||
|
||||
$userEntry = [
|
||||
'firstname' => ucfirst(strtolower($userData['firstname'])),
|
||||
'lastname' => ucfirst(strtolower($userData['lastname'])),
|
||||
'gender' => $userData['gender'],
|
||||
'cellphone' => $userData['cellphone'],
|
||||
'email' => strtolower($userData['email']),
|
||||
'address_street' => $userData['address_street'] ?? '',
|
||||
'apt' => $userData['apt'] ?? null,
|
||||
'city' => ucfirst(strtolower($userData['city'])),
|
||||
'state' => strtoupper($userData['state']),
|
||||
'zip' => $userData['zip'],
|
||||
'accept_school_policy' => $userData['accept_school_policy'] ?? 0,
|
||||
'token' => $tokenHash,
|
||||
'is_verified' => 0,
|
||||
'status' => 'Inactive',
|
||||
'user_type' => $userType,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
'school_id' => $schoolIdService->generateUserSchoolId(),
|
||||
];
|
||||
|
||||
try {
|
||||
if (! $this->userModel->insert($userEntry)) {
|
||||
log_message('error', 'Failed to insert user: ' . print_r($this->userModel->errors(), true));
|
||||
return false;
|
||||
}
|
||||
|
||||
$userId = (int) $this->userModel->getInsertID();
|
||||
$this->sendActivationEmail((string) $userData['email'], $token);
|
||||
log_message('info', "User with ID $userId created successfully and activation email sent.");
|
||||
|
||||
return $userId;
|
||||
} catch (Exception $e) {
|
||||
log_message('error', 'Exception during user creation: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function updateAuthorizedUsers(int $userId, array $data): void
|
||||
{
|
||||
$validation = \Config\Services::validation();
|
||||
$validation->setRules([
|
||||
'email' => [
|
||||
'label' => 'Email',
|
||||
'rules' => 'required|valid_email|max_length[150]',
|
||||
'errors' => [
|
||||
'required' => 'Email is required.',
|
||||
'valid_email' => 'Please provide a valid email address.',
|
||||
'max_length' => 'Email must be less than 150 characters.',
|
||||
],
|
||||
],
|
||||
'name' => [
|
||||
'label' => 'Name',
|
||||
'rules' => 'required|min_length[3]|max_length[100]|regex_match[/^[A-Za-z\s\-]+$/]',
|
||||
'errors' => [
|
||||
'required' => 'Name is required.',
|
||||
'regex_match' => 'Name can only contain letters, spaces, and dashes.',
|
||||
'min_length' => 'Name must be at least 3 characters long.',
|
||||
'max_length' => 'Name must be less than 100 characters.',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
if (! $validation->run($data)) {
|
||||
log_message('error', 'Invalid authorized user data: ' . json_encode($validation->getErrors()));
|
||||
return;
|
||||
}
|
||||
|
||||
$existingAuthorizedUser = $this->authorizedUsersModel
|
||||
->where('user_id', $userId)
|
||||
->where('email', $data['email'])
|
||||
->first();
|
||||
|
||||
if ($existingAuthorizedUser) {
|
||||
$this->authorizedUsersModel->update($existingAuthorizedUser['id'], $data);
|
||||
return;
|
||||
}
|
||||
|
||||
$data['user_id'] = $userId;
|
||||
$data['status'] = 'Pending';
|
||||
$this->authorizedUsersModel->insert($data);
|
||||
}
|
||||
|
||||
private function sendActivationEmail(string $email, string $token): void
|
||||
{
|
||||
$emailController = new EmailController();
|
||||
$subject = 'Activate Your Account';
|
||||
$activationLink = site_url('/user/confirm/' . $token);
|
||||
$message = "Please click the following link to confirm your email and set your password: $activationLink";
|
||||
|
||||
if ($emailController->sendEmail($email, $subject, $message)) {
|
||||
log_message('info', 'Activation email sent successfully to ' . $email);
|
||||
} else {
|
||||
log_message('error', 'Failed to send activation email to ' . $email);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Parents;
|
||||
|
||||
use CodeIgniter\Database\BaseConnection;
|
||||
|
||||
class ParentAttendanceService
|
||||
{
|
||||
public function __construct(private readonly BaseConnection $db)
|
||||
{
|
||||
}
|
||||
|
||||
public function attendanceForParent(int $parentId, string $schoolYear): array
|
||||
{
|
||||
if ($parentId <= 0 || $schoolYear === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->db->table('attendance_data')
|
||||
->select('students.firstname, students.lastname, attendance_data.date, attendance_data.status, attendance_data.reason')
|
||||
->join('students', 'students.id = attendance_data.student_id')
|
||||
->where('attendance_data.school_year', $schoolYear)
|
||||
->where('students.parent_id', $parentId)
|
||||
->get()
|
||||
->getResultArray();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Parents;
|
||||
|
||||
use App\Controllers\View\InvoiceController;
|
||||
use App\Models\EnrollmentModel;
|
||||
use App\Models\EventChargesModel;
|
||||
use App\Models\EventModel;
|
||||
|
||||
class ParentEventParticipationService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EventChargesModel $chargesModel,
|
||||
private readonly EventModel $eventModel,
|
||||
private readonly EnrollmentModel $enrollmentModel,
|
||||
private readonly InvoiceController $invoiceController,
|
||||
) {
|
||||
}
|
||||
|
||||
public function pageData(int $parentId, string $schoolYear, string $semester): array
|
||||
{
|
||||
$activeEvents = $this->eventModel->getActiveEvents($schoolYear, $semester);
|
||||
$chargesList = $this->chargesModel->getChargesWithEventInfo($parentId, $schoolYear, $semester);
|
||||
|
||||
$charges = [];
|
||||
$externalParticipantsByEvent = [];
|
||||
foreach ($chargesList as $charge) {
|
||||
$studentId = $charge['student_id'] ?? null;
|
||||
$eventId = (int) ($charge['event_id'] ?? 0);
|
||||
|
||||
if (! empty($studentId)) {
|
||||
$charges[$studentId . ':' . $eventId] = [
|
||||
'participation' => $charge['participation'],
|
||||
'date' => $charge['updated_at'] ?? $charge['created_at'],
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
$externalName = trim((string) ($charge['external_firstname'] ?? '') . ' ' . (string) ($charge['external_lastname'] ?? ''));
|
||||
if ($eventId > 0 && $externalName !== '') {
|
||||
$externalParticipantsByEvent[$eventId][] = [
|
||||
'name' => $externalName,
|
||||
'note' => (string) ($charge['external_note'] ?? ''),
|
||||
'participation' => (string) ($charge['participation'] ?? ''),
|
||||
'event_paid' => ! empty($charge['event_paid']),
|
||||
'charged' => (float) ($charge['charged'] ?? ($charge['event_amount'] ?? 0)),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'activeEvents' => $activeEvents,
|
||||
'charges' => $charges,
|
||||
'externalParticipantsByEvent' => $externalParticipantsByEvent,
|
||||
'yourStudents' => $this->enrollmentModel->getEnrolledStudents($parentId, $schoolYear),
|
||||
'activeEventCount' => is_array($activeEvents) ? count($activeEvents) : 0,
|
||||
];
|
||||
}
|
||||
|
||||
public function updateParticipation(array $participations, int $parentId, string $schoolYear, string $semester): void
|
||||
{
|
||||
foreach ($participations as $key => $value) {
|
||||
[$studentId, $eventId] = explode(':', (string) $key);
|
||||
|
||||
$existing = $this->chargesModel->where([
|
||||
'parent_id' => $parentId,
|
||||
'student_id' => $studentId,
|
||||
'event_id' => $eventId,
|
||||
])->first();
|
||||
|
||||
if ($value === 'no') {
|
||||
if ($existing) {
|
||||
$this->chargesModel->delete($existing['id']);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($existing) {
|
||||
$this->chargesModel->update($existing['id'], ['participation' => $value]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$event = $this->eventModel->getEvent($eventId, $schoolYear);
|
||||
$this->chargesModel->insert([
|
||||
'parent_id' => $parentId,
|
||||
'student_id' => $studentId,
|
||||
'event_id' => $eventId,
|
||||
'participation' => $value,
|
||||
'charged' => $event['amount'],
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
'updated_by' => $parentId,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->invoiceController->generateInvoice($parentId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Parents;
|
||||
|
||||
use CodeIgniter\Database\BaseConnection;
|
||||
|
||||
class ParentPaymentService
|
||||
{
|
||||
public function __construct(private readonly BaseConnection $db)
|
||||
{
|
||||
}
|
||||
|
||||
public function invoicesForParent(int $parentId, bool $includeRegisteredKids = false): array
|
||||
{
|
||||
if ($parentId <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$select = $includeRegisteredKids ? 'invoices.*, registeredKids' : '*';
|
||||
|
||||
return $this->db->table('invoices')
|
||||
->select($select)
|
||||
->where('parent_id', $parentId)
|
||||
->get()
|
||||
->getResultArray();
|
||||
}
|
||||
|
||||
public function markTuitionPaidForParent(int $parentId): void
|
||||
{
|
||||
if ($parentId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->db->table('students')
|
||||
->where('parent_id', $parentId)
|
||||
->update(['tuition_paid' => 1]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Parents;
|
||||
|
||||
use App\Models\StudentModel;
|
||||
use App\Models\UserModel;
|
||||
use App\Services\EmailService;
|
||||
use Throwable;
|
||||
|
||||
class ParentRegistrationNotificationService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly UserModel $userModel,
|
||||
private readonly StudentModel $studentModel,
|
||||
private readonly EmailService $emailService,
|
||||
private readonly string $adminEmail = 'registration@alrahmaisgl.org',
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<int> $studentIds
|
||||
*/
|
||||
public function sendAdminNewStudentEmails(array $studentIds, int $parentId): void
|
||||
{
|
||||
$parent = $this->userModel->find($parentId);
|
||||
if (! is_array($parent)) {
|
||||
log_message('warning', 'Unable to send admin student registration email: parent not found for ID ' . $parentId);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (array_values(array_unique(array_filter(array_map('intval', $studentIds)))) as $studentId) {
|
||||
$student = $this->studentModel->find($studentId);
|
||||
if (! is_array($student)) {
|
||||
log_message('warning', 'Unable to send admin student registration email: student not found for ID ' . $studentId);
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->sendAdminNewStudentEmail($student, $parent, $parentId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $student
|
||||
* @param array<string, mixed> $parent
|
||||
*/
|
||||
private function sendAdminNewStudentEmail(array $student, array $parent, int $parentId): void
|
||||
{
|
||||
$studentId = (int) ($student['id'] ?? 0);
|
||||
$studentFullName = trim((string) ($student['firstname'] ?? '') . ' ' . (string) ($student['lastname'] ?? ''))
|
||||
?: 'Student ID ' . $studentId;
|
||||
|
||||
$payload = $student;
|
||||
$payload['parents'] = [
|
||||
'user_id' => $parentId,
|
||||
'firstname' => (string) ($parent['firstname'] ?? ''),
|
||||
'lastname' => (string) ($parent['lastname'] ?? ''),
|
||||
'email' => (string) ($parent['email'] ?? ''),
|
||||
];
|
||||
|
||||
$adminMessage = view('emails/admin_student_registered', ['student' => $payload], ['saveData' => true]);
|
||||
|
||||
try {
|
||||
$sent = $this->emailService->send(
|
||||
$this->adminEmail,
|
||||
'New Student Registered: ' . $studentFullName,
|
||||
$adminMessage,
|
||||
'notifications'
|
||||
);
|
||||
|
||||
if (! $sent) {
|
||||
log_message('error', 'Admin student registration email failed for student ID ' . $studentId);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
log_message('error', 'Admin student registration email failed for student ID ' . $studentId . ': ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,724 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Parents;
|
||||
|
||||
use App\Models\EmergencyContactModel;
|
||||
use App\Models\EnrollmentModel;
|
||||
use App\Models\StudentAllergyModel;
|
||||
use App\Models\StudentMedicalConditionModel;
|
||||
use App\Models\StudentModel;
|
||||
use App\Models\UserModel;
|
||||
use App\Services\PhoneFormatterService;
|
||||
use App\Services\SchoolIdService;
|
||||
use CodeIgniter\Database\BaseConnection;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
use DateTime;
|
||||
use DateTimeImmutable;
|
||||
use DateTimeZone;
|
||||
use InvalidArgumentException;
|
||||
use Throwable;
|
||||
|
||||
class ParentRegistrationService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly BaseConnection $db,
|
||||
private readonly UserModel $userModel,
|
||||
private readonly StudentModel $studentModel,
|
||||
private readonly EnrollmentModel $enrollmentModel,
|
||||
private readonly EmergencyContactModel $emergencyContactModel,
|
||||
private readonly StudentMedicalConditionModel $medicalConditionModel,
|
||||
private readonly StudentAllergyModel $allergyModel,
|
||||
) {
|
||||
}
|
||||
|
||||
public function registrationData(
|
||||
int $parentId,
|
||||
string $selectedSchoolYear,
|
||||
bool $isEditable,
|
||||
int $maxChilds,
|
||||
int $maxEmergency
|
||||
): array {
|
||||
$enrollments = $this->getEnrollmentsByParent($parentId, $selectedSchoolYear);
|
||||
$enrollmentMap = [];
|
||||
foreach ($enrollments as $enroll) {
|
||||
$enrollmentMap[$enroll['student_id']] = $enroll;
|
||||
}
|
||||
|
||||
$user = $this->userModel->find($parentId);
|
||||
if (! $user || ($user['user_type'] ?? '') !== 'primary') {
|
||||
throw new \RuntimeException('Only primary parents are allowed to register children.');
|
||||
}
|
||||
|
||||
$kids = $this->studentModel->where('parent_id', $parentId)->findAll();
|
||||
foreach ($kids as &$kid) {
|
||||
$studentId = (int) ($kid['id'] ?? 0);
|
||||
$kid['allergies'] = $this->allergyModel->where('student_id', $studentId)->findColumn('allergy') ?? [];
|
||||
$kid['medical_conditions'] = $this->medicalConditionModel->where('student_id', $studentId)->findColumn('condition_name') ?? [];
|
||||
$kid['enrollment'] = isset($enrollmentMap[$studentId]['id']) && ! empty($enrollmentMap[$studentId]['id']) ? 1 : 0;
|
||||
}
|
||||
unset($kid);
|
||||
|
||||
$this->ensureStudentYearStatusRows($kids, $selectedSchoolYear);
|
||||
service('studentYearStatus')->attachToStudents($kids, $selectedSchoolYear);
|
||||
|
||||
foreach ($kids as &$kid) {
|
||||
$kid['can_delete'] = $this->canParentDeleteStudent($kid, $parentId);
|
||||
}
|
||||
unset($kid);
|
||||
|
||||
return [
|
||||
'existingKids' => $kids,
|
||||
'emergencies' => $this->emergencyContactModel->where('parent_id', $parentId)->findAll(),
|
||||
'parent' => $user,
|
||||
'maxChilds' => $maxChilds,
|
||||
'maxEmergency' => $maxEmergency,
|
||||
'enrollments' => $enrollments,
|
||||
'selectedYear' => $selectedSchoolYear,
|
||||
'isEditable' => $isEditable,
|
||||
];
|
||||
}
|
||||
|
||||
public function validateRegistrationSubmission(array $post, array $registrationData): array
|
||||
{
|
||||
$existingKids = $registrationData['existingKids'] ?? [];
|
||||
$existingECs = $registrationData['emergencies'] ?? [];
|
||||
$maxChilds = (int) ($registrationData['maxChilds'] ?? 0);
|
||||
$maxEmergency = (int) ($registrationData['maxEmergency'] ?? 0);
|
||||
|
||||
$incomingFirstNames = (array) ($post['studentFirstName'] ?? []);
|
||||
$incomingLastNames = (array) ($post['studentLastName'] ?? []);
|
||||
$incomingDOBs = (array) ($post['dob'] ?? []);
|
||||
$newStudentCount = count(array_filter($incomingFirstNames));
|
||||
|
||||
foreach ($incomingFirstNames as $i => $firstName) {
|
||||
$lastName = trim($incomingLastNames[$i] ?? '');
|
||||
$dob = trim($incomingDOBs[$i] ?? '');
|
||||
if (empty($firstName) || empty($lastName) || empty($dob)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($existingKids as $kid) {
|
||||
if (
|
||||
strtolower($kid['firstname']) === strtolower($firstName)
|
||||
&& strtolower($kid['lastname']) === strtolower($lastName)
|
||||
&& $kid['dob'] === $dob
|
||||
) {
|
||||
return ['ok' => false, 'error' => "Duplicate student detected: {$firstName} {$lastName} with DOB {$dob} already exists."];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$seenStudents = [];
|
||||
foreach ($incomingFirstNames as $i => $firstName) {
|
||||
$lastName = trim($incomingLastNames[$i] ?? '');
|
||||
$dob = trim($incomingDOBs[$i] ?? '');
|
||||
if (empty($firstName) || empty($lastName) || empty($dob)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$key = strtolower($firstName . '|' . $lastName . '|' . $dob);
|
||||
if (isset($seenStudents[$key])) {
|
||||
return ['ok' => false, 'error' => "Duplicate student entry in the form: {$firstName} {$lastName} with DOB {$dob}."];
|
||||
}
|
||||
$seenStudents[$key] = true;
|
||||
}
|
||||
|
||||
$incomingECFirst = (array) ($post['emergency_firstname'] ?? []);
|
||||
$incomingECLast = (array) ($post['emergency_lastname'] ?? []);
|
||||
$incomingECPhones = (array) ($post['emergency_phone'] ?? []);
|
||||
$incomingECEmails = (array) ($post['emergency_email'] ?? []);
|
||||
$newECCount = count(array_filter($incomingECFirst));
|
||||
|
||||
foreach ($incomingECFirst as $i => $first) {
|
||||
$last = trim($incomingECLast[$i] ?? '');
|
||||
$phone = preg_replace('/\D/', '', $incomingECPhones[$i] ?? '');
|
||||
$email = strtolower(trim($incomingECEmails[$i] ?? ''));
|
||||
if (empty($first) || empty($last)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($existingECs as $contact) {
|
||||
$existingPhone = preg_replace('/\D/', '', $contact['cellphone']);
|
||||
$existingEmail = strtolower($contact['email']);
|
||||
if (
|
||||
strtolower($contact['emergency_contact_name']) === strtolower(trim($first . ' ' . $last))
|
||||
|| ($phone && $phone === $existingPhone)
|
||||
|| ($email && $email === $existingEmail)
|
||||
) {
|
||||
return ['ok' => false, 'error' => "Duplicate emergency contact: {$first} {$last} already exists."];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$seenContacts = [];
|
||||
foreach ($incomingECFirst as $i => $first) {
|
||||
$last = trim($incomingECLast[$i] ?? '');
|
||||
$phone = preg_replace('/\D/', '', $incomingECPhones[$i] ?? '');
|
||||
$email = strtolower(trim($incomingECEmails[$i] ?? ''));
|
||||
if (empty($first) || empty($last)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$key = strtolower($first . '|' . $last . '|' . $phone . '|' . $email);
|
||||
if (isset($seenContacts[$key])) {
|
||||
return ['ok' => false, 'error' => "Duplicate emergency contact entry in the form: {$first} {$last}."];
|
||||
}
|
||||
$seenContacts[$key] = true;
|
||||
}
|
||||
|
||||
$existingKidsCount = count($existingKids);
|
||||
$existingECCount = count($existingECs);
|
||||
if (($existingKidsCount + $newStudentCount) > $maxChilds) {
|
||||
return ['ok' => false, 'error' => "Student limit exceeded. You have $existingKidsCount and tried to add $newStudentCount (limit: $maxChilds)."];
|
||||
}
|
||||
|
||||
if (($existingECCount + $newECCount) > $maxEmergency) {
|
||||
return ['ok' => false, 'error' => "Emergency contact limit exceeded. You have $existingECCount and tried to add $newECCount (limit: $maxEmergency)."];
|
||||
}
|
||||
|
||||
return ['ok' => true];
|
||||
}
|
||||
|
||||
public function saveStudentAtIndex(
|
||||
int $idx,
|
||||
array $post,
|
||||
int $parentId,
|
||||
string $schoolYear,
|
||||
SchoolIdService $schoolIdService,
|
||||
?bool $isNew = null,
|
||||
?int $studentId = null,
|
||||
?string $schoolStartDate = null,
|
||||
?string $ageDateReference = null
|
||||
): array {
|
||||
$firstName = $post['studentFirstName'][$idx] ?? null;
|
||||
$lastName = $post['studentLastName'][$idx] ?? null;
|
||||
$dob = $post['dob'][$idx] ?? null;
|
||||
$gender = $post['gender'][$idx] ?? null;
|
||||
$grade = $post['registration_grade'][$idx] ?? null;
|
||||
$conditions = $post['medical_conditions'][$idx] ?? [];
|
||||
$allergies = $post['allergies'][$idx] ?? [];
|
||||
$photoRaw = $post['photo_consent'][$idx] ?? '';
|
||||
|
||||
if (! $firstName || ! $lastName || ! $dob || ! $gender || ! $grade) {
|
||||
return ['ok' => false, 'empty' => true];
|
||||
}
|
||||
|
||||
$firstName = $this->normalizeStudentName((string) $firstName);
|
||||
$lastName = $this->normalizeStudentName((string) $lastName);
|
||||
$this->validateNames($firstName);
|
||||
$this->validateNames($lastName);
|
||||
|
||||
$dobObj = new DateTime((string) $dob);
|
||||
$schoolYearAgeDeadline = $this->schoolYearAgeDeadline($schoolYear, $schoolStartDate);
|
||||
$age = $this->calculateAgeAsOfSchoolYearStartYear((string) $dob, $schoolYear);
|
||||
$validation = $this->validateDobAge(
|
||||
(string) $dob,
|
||||
$this->registrationMinimumAgeDeadline($schoolYear, $ageDateReference),
|
||||
5,
|
||||
18,
|
||||
$schoolYearAgeDeadline
|
||||
);
|
||||
|
||||
if (! $validation['isValid']) {
|
||||
$displayDeadline = (new DateTime($schoolYearAgeDeadline))->format('m-d-Y');
|
||||
return [
|
||||
'ok' => false,
|
||||
'error' => "Student '{$firstName} {$lastName}' {$validation['message']}. General age is calculated as of {$displayDeadline}.",
|
||||
];
|
||||
}
|
||||
|
||||
$studentData = [
|
||||
'firstname' => $firstName,
|
||||
'lastname' => $lastName,
|
||||
'age' => $age,
|
||||
'dob' => $dobObj->format('Y-m-d'),
|
||||
'gender' => $gender,
|
||||
'registration_grade' => $grade,
|
||||
'photo_consent' => strtolower((string) $photoRaw) === 'yes' ? 1 : 0,
|
||||
'parent_id' => $parentId,
|
||||
'year_of_registration' => date('Y'),
|
||||
];
|
||||
|
||||
if ($this->db->fieldExists('school_year', 'students')) {
|
||||
$studentData['school_year'] = $schoolYear;
|
||||
}
|
||||
|
||||
if ($isNew !== null) {
|
||||
$studentData['is_new'] = $isNew ? 1 : 0;
|
||||
}
|
||||
|
||||
$existingBuilder = $this->studentModel
|
||||
->where('parent_id', $parentId)
|
||||
->where('dob', $dobObj->format('Y-m-d'))
|
||||
->where('firstname', $firstName)
|
||||
->where('lastname', $lastName);
|
||||
|
||||
if ($this->db->fieldExists('school_year', 'students')) {
|
||||
$existingBuilder->where('school_year', $schoolYear);
|
||||
}
|
||||
|
||||
if (! $studentId && $existingBuilder->first()) {
|
||||
return ['ok' => false, 'error' => "Student '{$firstName} {$lastName}' with the same birthdate is already registered for $schoolYear."];
|
||||
}
|
||||
|
||||
if ($studentId) {
|
||||
$existing = $this->studentModel->find($studentId);
|
||||
if (! is_array($existing) || (int) ($existing['parent_id'] ?? 0) !== $parentId) {
|
||||
return ['ok' => false, 'error' => 'Student record was not found for this parent account.'];
|
||||
}
|
||||
|
||||
$this->auditParentStudentFieldChanges($existing, $studentData, $parentId, 'parent_student_edit');
|
||||
$this->studentModel->update($studentId, $studentData);
|
||||
|
||||
if ($this->parentEditAffectsEligibility($existing, $studentData)) {
|
||||
$this->recheckEligibilityAfterParentEdit($studentId, $parentId, $schoolYear);
|
||||
}
|
||||
} else {
|
||||
$studentData['registration_date'] = utc_now();
|
||||
$studentData['tuition_paid'] = 0;
|
||||
$studentData['school_id'] = $schoolIdService->generateStudentSchoolId();
|
||||
|
||||
try {
|
||||
$studentId = (int) $this->studentModel->insert($studentData, true);
|
||||
} catch (DatabaseException $e) {
|
||||
if (strpos($e->getMessage(), '1062') !== false) {
|
||||
return ['ok' => false, 'error' => "Student '{$firstName} {$lastName}' with the same birthdate is already registered for $schoolYear."];
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
if ($isNew !== null && $studentId > 0) {
|
||||
$studentYearStatus = service('studentYearStatus');
|
||||
$statusSaved = $studentYearStatus->upsert($studentId, $schoolYear, $isNew);
|
||||
if (! $statusSaved || ! $studentYearStatus->hasStatus($studentId, $schoolYear)) {
|
||||
throw new \RuntimeException('Student year status could not be saved for student ID ' . $studentId . ' and school year ' . $schoolYear . '.');
|
||||
}
|
||||
}
|
||||
|
||||
$this->medicalConditionModel->where('student_id', $studentId)->delete();
|
||||
foreach ((array) $conditions as $condition) {
|
||||
$condition = trim((string) $condition);
|
||||
if ($condition !== '') {
|
||||
$this->medicalConditionModel->insert(['student_id' => $studentId, 'condition_name' => $condition]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->allergyModel->where('student_id', $studentId)->delete();
|
||||
foreach ((array) $allergies as $allergy) {
|
||||
$allergy = trim((string) $allergy);
|
||||
if ($allergy !== '') {
|
||||
$this->allergyModel->insert(['student_id' => $studentId, 'allergy' => $allergy]);
|
||||
}
|
||||
}
|
||||
|
||||
return ['ok' => true, 'student_id' => $studentId];
|
||||
}
|
||||
|
||||
public function saveEmergencyContact(int $parentId, array $post, ?array $single = null, ?int $id = null): array
|
||||
{
|
||||
$phoneFormatter = new PhoneFormatterService();
|
||||
|
||||
if ($single !== null) {
|
||||
$firstName = $this->formatName($single['first_name'] ?? '');
|
||||
$lastName = $this->formatName($single['last_name'] ?? '');
|
||||
$relation = trim($single['relation'] ?? '');
|
||||
$phone = $phoneFormatter->formatPhoneNumber($single['cellphone'] ?? '');
|
||||
$email = strtolower(trim($single['email'] ?? ''));
|
||||
|
||||
if ($firstName === '' && $lastName === '' && $phone === '(000)-000-0000' && $email === '' && $relation === '') {
|
||||
return ['ok' => true, 'empty' => true];
|
||||
}
|
||||
|
||||
$this->validateNames($firstName);
|
||||
$this->validateNames($lastName);
|
||||
|
||||
if ($email && ! filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
throw new \Exception('Invalid email format for emergency contact.');
|
||||
}
|
||||
|
||||
$data = [
|
||||
'parent_id' => $parentId,
|
||||
'emergency_contact_name' => $firstName . ' ' . $lastName,
|
||||
'cellphone' => $phone,
|
||||
'email' => $email,
|
||||
'relation' => $relation,
|
||||
'updated_at' => utc_now(),
|
||||
];
|
||||
|
||||
$duplicateBuilder = $this->emergencyContactModel
|
||||
->where('parent_id', $parentId)
|
||||
->where('emergency_contact_name', $data['emergency_contact_name'])
|
||||
->where('cellphone', $phone)
|
||||
->where('email', $email)
|
||||
->where('relation', $relation);
|
||||
|
||||
if ($id !== null) {
|
||||
$duplicateBuilder->where('id !=', $id);
|
||||
}
|
||||
|
||||
if ($duplicateBuilder->first()) {
|
||||
return ['ok' => false, 'error' => $id !== null
|
||||
? 'Another emergency contact with the same information already exists.'
|
||||
: 'This emergency contact is already registered.'];
|
||||
}
|
||||
|
||||
if ($id !== null) {
|
||||
$this->emergencyContactModel->update($id, $data);
|
||||
} else {
|
||||
$this->emergencyContactModel->insert($data);
|
||||
}
|
||||
|
||||
return ['ok' => true];
|
||||
}
|
||||
|
||||
$firstNames = (array) ($post['emergency_firstname'] ?? []);
|
||||
$lastNames = (array) ($post['emergency_lastname'] ?? []);
|
||||
$relations = (array) ($post['emergency_relation'] ?? []);
|
||||
$phones = (array) ($post['emergency_phone'] ?? []);
|
||||
$emails = (array) ($post['emergency_email'] ?? []);
|
||||
|
||||
foreach ($firstNames as $idx => $first) {
|
||||
$firstName = $this->formatName($first ?? '');
|
||||
$lastName = $this->formatName($lastNames[$idx] ?? '');
|
||||
$relation = trim($relations[$idx] ?? '');
|
||||
$phone = $phoneFormatter->formatPhoneNumber($phones[$idx] ?? '');
|
||||
$email = strtolower(trim($emails[$idx] ?? ''));
|
||||
|
||||
if ($firstName === '' && $lastName === '' && $phone === '(000)-000-0000' && $email === '' && $relation === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($phone === '(000)-000-0000') {
|
||||
throw new \Exception('Invalid phone number.');
|
||||
}
|
||||
if ($email && ! filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
throw new \Exception('Invalid email format for emergency contact.');
|
||||
}
|
||||
|
||||
$fullName = $firstName . ' ' . $lastName;
|
||||
$exists = $this->emergencyContactModel->where([
|
||||
'parent_id' => $parentId,
|
||||
'emergency_contact_name' => $fullName,
|
||||
'cellphone' => $phone,
|
||||
'email' => $email,
|
||||
'relation' => $relation,
|
||||
])->first();
|
||||
|
||||
if (! $exists) {
|
||||
$this->emergencyContactModel->insert([
|
||||
'parent_id' => $parentId,
|
||||
'emergency_contact_name' => $fullName,
|
||||
'cellphone' => $phone,
|
||||
'email' => $email,
|
||||
'relation' => $relation,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return ['ok' => true];
|
||||
}
|
||||
|
||||
public function canParentDeleteStudent(array $student, int $parentId): bool
|
||||
{
|
||||
$studentId = (int) ($student['id'] ?? 0);
|
||||
if ($studentId <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$statusYear = trim((string) ($student['school_year'] ?? ''));
|
||||
if ($statusYear === '') {
|
||||
$statusYear = (string) (service('studentYearStatus')->activeSchoolYear() ?? '');
|
||||
}
|
||||
|
||||
$isNew = $statusYear !== ''
|
||||
? service('studentYearStatus')->isNew($studentId, $statusYear)
|
||||
: ((string) ($student['is_new'] ?? '1') === '1');
|
||||
|
||||
return $isNew
|
||||
&& ! $this->studentHasEnrollmentHistory($studentId, $parentId)
|
||||
&& ! $this->studentHasClassAssignmentHistory($studentId);
|
||||
}
|
||||
|
||||
public function validateDobAge(
|
||||
string $dob,
|
||||
string $registrationAgeDeadline,
|
||||
int $minAge = 5,
|
||||
int $maxAge = 18,
|
||||
?string $schoolYearAgeDeadline = null
|
||||
): array {
|
||||
$response = ['isValid' => false, 'message' => '', 'age' => null];
|
||||
$tz = new DateTimeZone('UTC');
|
||||
$dob = trim($dob);
|
||||
if ($dob === '') {
|
||||
$response['message'] = 'Date of birth is required';
|
||||
return $response;
|
||||
}
|
||||
|
||||
$birthDate = DateTimeImmutable::createFromFormat('!Y-m-d', $dob, $tz);
|
||||
$errs = DateTimeImmutable::getLastErrors();
|
||||
if ($birthDate === false || (is_array($errs) && (($errs['warning_count'] ?? 0) > 0 || ($errs['error_count'] ?? 0) > 0))) {
|
||||
$response['message'] = 'Invalid date format (Use YYYY-MM-DD)';
|
||||
return $response;
|
||||
}
|
||||
|
||||
try {
|
||||
$minimumAgeDeadline = new DateTimeImmutable($registrationAgeDeadline, $tz);
|
||||
} catch (Throwable $e) {
|
||||
$minimumAgeDeadline = new DateTimeImmutable('now', $tz);
|
||||
}
|
||||
|
||||
try {
|
||||
$ageDeadline = new DateTimeImmutable($schoolYearAgeDeadline ?: $registrationAgeDeadline, $tz);
|
||||
} catch (Throwable $e) {
|
||||
$ageDeadline = $minimumAgeDeadline;
|
||||
}
|
||||
|
||||
$minimumAgeDeadline = $minimumAgeDeadline->setTime(23, 59, 59);
|
||||
$ageDeadline = $ageDeadline->setTime(23, 59, 59);
|
||||
$ageAtDeadline = $birthDate->diff($ageDeadline)->y;
|
||||
$ageAtMinimumAgeDeadline = $birthDate->diff($minimumAgeDeadline)->y;
|
||||
$response['age'] = $ageAtDeadline;
|
||||
|
||||
$minBirthDate = $ageDeadline->modify('-' . ($maxAge + 1) . ' years')->modify('+1 day')->setTime(0, 0, 0);
|
||||
$maxBirthDate = $minimumAgeDeadline->modify("-{$minAge} years")->setTime(23, 59, 59);
|
||||
$response['isValid'] = ($birthDate >= $minBirthDate) && ($birthDate <= $maxBirthDate);
|
||||
|
||||
if (! $response['isValid']) {
|
||||
$response['message'] = sprintf(
|
||||
'Must be at least %d years old by %s and no older than %d by %s. Current registration age would be: %d',
|
||||
$minAge,
|
||||
$minimumAgeDeadline->format('m-d-Y'),
|
||||
$maxAge,
|
||||
$ageDeadline->format('m-d-Y'),
|
||||
$ageAtMinimumAgeDeadline
|
||||
);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function schoolYearAgeDeadline(string $schoolYear, ?string $schoolStartDate = null): string
|
||||
{
|
||||
if (preg_match('/^(\d{4})/', trim($schoolYear), $matches)) {
|
||||
return $matches[1] . '-09-01';
|
||||
}
|
||||
|
||||
if (! empty($schoolStartDate) && strtotime($schoolStartDate)) {
|
||||
return (new DateTimeImmutable($schoolStartDate))->format('Y-m-d');
|
||||
}
|
||||
|
||||
return date('Y') . '-09-01';
|
||||
}
|
||||
|
||||
public function registrationMinimumAgeDeadline(string $schoolYear, ?string $ageDateReference = null): string
|
||||
{
|
||||
$configured = trim((string) $ageDateReference);
|
||||
if ($configured !== '' && strtotime($configured)) {
|
||||
return (new DateTimeImmutable($configured))->format('Y-m-d');
|
||||
}
|
||||
|
||||
if (preg_match('/^(\d{4})-(\d{4})$/', trim($schoolYear), $matches)) {
|
||||
return $matches[2] . '-12-31';
|
||||
}
|
||||
|
||||
return date('Y') . '-12-31';
|
||||
}
|
||||
|
||||
public function formatName(string $name): string
|
||||
{
|
||||
$name = trim($name);
|
||||
$name = strtolower($name);
|
||||
$name = ucwords($name, ' ');
|
||||
|
||||
return implode('-', array_map('ucfirst', explode('-', $name)));
|
||||
}
|
||||
|
||||
public function validateNames(string $name): void
|
||||
{
|
||||
if (! preg_match('/^[A-Za-z\s\-]{2,30}$/', $name)) {
|
||||
throw new InvalidArgumentException('Invalid name format: Only letters, spaces, or dashes (2-30 chars) allowed.');
|
||||
}
|
||||
}
|
||||
|
||||
private function getEnrollmentsByParent(int $parentId, string $schoolYear): array
|
||||
{
|
||||
return $this->enrollmentModel
|
||||
->where('parent_id', $parentId)
|
||||
->where('school_year', $schoolYear)
|
||||
->orderBy('enrollment_date', 'DESC')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
private function ensureStudentYearStatusRows(array $students, string $schoolYear): void
|
||||
{
|
||||
$schoolYear = trim($schoolYear);
|
||||
if ($students === [] || ! preg_match('/^\d{4}-\d{4}$/', $schoolYear)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$studentYearStatus = service('studentYearStatus');
|
||||
foreach ($students as $student) {
|
||||
$studentId = (int) ($student['id'] ?? $student['student_id'] ?? 0);
|
||||
if ($studentId <= 0 || $studentYearStatus->hasStatus($studentId, $schoolYear)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$isNew = (int) ($student['is_new'] ?? 1) === 1;
|
||||
if (! $studentYearStatus->upsert($studentId, $schoolYear, $isNew)) {
|
||||
log_message('error', 'Unable to repair student_year_status for student_id={studentId}, school_year={schoolYear}', [
|
||||
'studentId' => $studentId,
|
||||
'schoolYear' => $schoolYear,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function calculateAgeAsOfSchoolYearStartYear(?string $dob, string $schoolYear): ?int
|
||||
{
|
||||
$dob = trim((string) $dob);
|
||||
$schoolYear = trim($schoolYear);
|
||||
|
||||
if ($dob === '' || ! preg_match('/^(\d{4})/', $schoolYear, $matches)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$timezone = new DateTimeZone((string) (config('School')->attendance['timezone'] ?? user_timezone()));
|
||||
$birthDate = DateTimeImmutable::createFromFormat('!Y-m-d', $dob, $timezone);
|
||||
$errors = DateTimeImmutable::getLastErrors();
|
||||
$hasParseErrors = is_array($errors)
|
||||
&& (($errors['warning_count'] ?? 0) > 0 || ($errors['error_count'] ?? 0) > 0);
|
||||
|
||||
if ($birthDate === false || $hasParseErrors) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$schoolYearStartYearCutoff = new DateTimeImmutable($matches[1] . '-09-01', $timezone);
|
||||
if ($birthDate > $schoolYearStartYearCutoff) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $birthDate->diff($schoolYearStartYearCutoff)->y;
|
||||
} catch (Throwable $e) {
|
||||
log_message('warning', 'Unable to calculate school-year age from DOB: {message}', [
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private function normalizeStudentName(string $name): string
|
||||
{
|
||||
$name = trim(preg_replace('/\s+/', ' ', $name) ?? '');
|
||||
|
||||
return mb_convert_case($name, MB_CASE_TITLE, 'UTF-8');
|
||||
}
|
||||
|
||||
private function auditParentStudentFieldChanges(array $original, array $updated, int $parentId, string $source): void
|
||||
{
|
||||
if (! $this->db->tableExists('enrollment_transition_audits')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$studentId = (int) ($original['id'] ?? 0);
|
||||
if ($studentId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$changes = [];
|
||||
foreach (['firstname', 'lastname', 'dob'] as $field) {
|
||||
$oldValue = trim((string) ($original[$field] ?? ''));
|
||||
$newValue = trim((string) ($updated[$field] ?? ''));
|
||||
if ($oldValue !== $newValue) {
|
||||
$changes[$field] = [
|
||||
'old_value' => $oldValue,
|
||||
'new_value' => $newValue,
|
||||
'changed' => true,
|
||||
'changed_by' => $parentId,
|
||||
'changed_at' => date('Y-m-d H:i:s'),
|
||||
'source' => $source,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if ($changes === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->db->table('enrollment_transition_audits')->insert([
|
||||
'student_id' => $studentId,
|
||||
'school_year' => (string) ($updated['school_year'] ?? $original['school_year'] ?? date('Y')),
|
||||
'source_school_year' => null,
|
||||
'action' => 'parent_student_field_edit',
|
||||
'performed_by' => $parentId,
|
||||
'original_values_json' => json_encode($original, JSON_UNESCAPED_SLASHES),
|
||||
'new_values_json' => json_encode(['changes' => $changes, 'updated' => $updated], JSON_UNESCAPED_SLASHES),
|
||||
'reason' => $source,
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
}
|
||||
|
||||
private function parentEditAffectsEligibility(array $original, array $updated): bool
|
||||
{
|
||||
return trim((string) ($original['dob'] ?? '')) !== trim((string) ($updated['dob'] ?? ''))
|
||||
|| trim((string) ($original['lastname'] ?? '')) !== trim((string) ($updated['lastname'] ?? ''));
|
||||
}
|
||||
|
||||
private function recheckEligibilityAfterParentEdit(int $studentId, int $parentId, string $targetSchoolYear): void
|
||||
{
|
||||
$previousSchoolYear = $this->previousSchoolYearName($targetSchoolYear);
|
||||
if ($previousSchoolYear === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$evaluation = service('enrollmentTransition')->evaluateForParent(
|
||||
$parentId,
|
||||
$studentId,
|
||||
$previousSchoolYear,
|
||||
$targetSchoolYear,
|
||||
'parent'
|
||||
);
|
||||
|
||||
if (($evaluation['can_enroll'] ?? false) === true || ($evaluation['parent_enrollment_allowed'] ?? false) === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
$message = (string) ($evaluation['primary_parent_message'] ?? 'Updated student information affects enrollment eligibility.');
|
||||
service('enrollmentTransition')->logEnrollmentBlock($evaluation, 'parent_student_edit', $parentId, $parentId);
|
||||
session()->setFlashdata('warning', $message);
|
||||
}
|
||||
|
||||
private function previousSchoolYearName(string $schoolYear): ?string
|
||||
{
|
||||
if (! preg_match('/^(\d{4})-(\d{4})$/', trim($schoolYear), $matches)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ((int) $matches[1] - 1) . '-' . ((int) $matches[2] - 1);
|
||||
}
|
||||
|
||||
private function studentHasEnrollmentHistory(int $studentId, int $parentId): bool
|
||||
{
|
||||
if (! $this->db->tableExists('enrollments')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->db->table('enrollments')
|
||||
->where('student_id', $studentId)
|
||||
->where('parent_id', $parentId)
|
||||
->countAllResults() > 0;
|
||||
}
|
||||
|
||||
private function studentHasClassAssignmentHistory(int $studentId): bool
|
||||
{
|
||||
if (! $this->db->tableExists('student_class')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->db->table('student_class')
|
||||
->where('student_id', $studentId)
|
||||
->countAllResults() > 0;
|
||||
}
|
||||
}
|
||||
@@ -1,239 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Al Rahma Sunday School</title>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||
<meta content="" name="keywords">
|
||||
<meta content="" name="description">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link href="<?= base_url('assets/images/favicon.ico') ?>" rel="icon">
|
||||
|
||||
<!-- Google Web Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Heebo:wght@400;500;600&family=Inter:wght@600&family=Lobster+Two:wght@700&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Icon Font Stylesheet -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.10.0/css/all.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.4.1/font/bootstrap-icons.css" rel="stylesheet">
|
||||
|
||||
<!-- Libraries Stylesheet -->
|
||||
<link href="<?= base_url('lib/animate/animate.min.css') ?>" rel="stylesheet">
|
||||
<link href="<?= base_url('lib/owlcarousel/assets/owl.carousel.min.css') ?>" rel="stylesheet">
|
||||
|
||||
<!-- Customized Bootstrap Stylesheet -->
|
||||
<link href="<?= base_url('css/bootstrap.min.css') ?>" rel="stylesheet">
|
||||
|
||||
<!-- Template Stylesheet -->
|
||||
<link href="<?= base_url('css/style.css') ?>" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container-xxl bg-white p-0">
|
||||
<!-- Spinner Start -->
|
||||
<div id="spinner" class="show bg-white position-fixed translate-middle w-100 vh-100 top-50 start-50 d-flex align-items-center justify-content-center">
|
||||
<div class="spinner-border text-primary" style="width: 3rem; height: 3rem;" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Spinner End -->
|
||||
|
||||
<!-- Navbar Start -->
|
||||
<nav class="navbar navbar-expand-lg bg-white navbar-light sticky-top px-4 px-lg-5 py-lg-0">
|
||||
<a href="<?= base_url('/') ?>" class="navbar-brand d-flex align-items-center">
|
||||
<img src="<?= base_url('images/logo.png') ?>" alt="Al Rahma Sunday School" style="height: 40px; width: 40px; border-radius: 50%; object-fit: contain; background-color: #fff;">
|
||||
<h1 class="m-0 ms-2 green-title">Al Rahma Sunday School</h1>
|
||||
</a>
|
||||
<button type="button" class="navbar-toggler" data-bs-toggle="collapse" data-bs-target="#navbarCollapse">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarCollapse">
|
||||
<div class="navbar-nav mx-auto">
|
||||
<a href="<?= base_url('/') ?>" class="nav-item nav-link">Home</a>
|
||||
<a href="<?= base_url('/about') ?>" class="nav-item nav-link">About Us</a>
|
||||
<a href="<?= base_url('/classes') ?>" class="nav-item nav-link">Classes</a>
|
||||
<a href="<?= base_url('/contact') ?>" class="nav-item nav-link">Contact Us</a>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<a href="/user/login" class="btn btn-primary rounded-pill px-3">Login<i class="fa fa-arrow-right ms-3"></i></a>
|
||||
<a href="/register" class="btn btn-primary rounded-pill px-3 me-2">Register<i class="fa fa-arrow-right ms-3"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
<!-- Navbar End -->
|
||||
|
||||
<!-- Page Header Start -->
|
||||
<!-- Page Header Start -->
|
||||
<div class="container-xxl py-5 page-header position-relative mb-5">
|
||||
<div class="container py-5">
|
||||
<h1 class="display-2 text-white animated slideInDown mb-4">About Us</h1>
|
||||
<nav aria-label="breadcrumb animated slideInDown">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item text-green"><a href="/">Home</a></li>
|
||||
<li class="breadcrumb-item text-green"><a href="#">Pages</a></li>
|
||||
<li class="breadcrumb-item text-green active" aria-current="page">About Us</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Page Header End -->
|
||||
|
||||
<!-- Page Header End -->
|
||||
|
||||
<!-- About Start -->
|
||||
<div class="container-xxl py-5">
|
||||
<div class="container">
|
||||
<div class="row g-5 align-items-center">
|
||||
<div class="col-lg-6 wow fadeInUp" data-wow-delay="0.1s">
|
||||
<h1 class="mb-4">Learn More About Our Work And Our Cultural Activities</h1>
|
||||
<p>Discover the impactful work we do and immerse yourself in our vibrant cultural activities.
|
||||
</p>
|
||||
<p class="mb-4">Our programs are designed to enrich the community, fostering a deep appreciation for diverse traditions and values.
|
||||
Join us to experience firsthand the creativity and passion that drive our initiatives.
|
||||
</p>
|
||||
<div class="row g-4 align-items-center">
|
||||
<div class="col-sm-6">
|
||||
<a class="btn btn-primary rounded-pill py-3 px-5" href="#">Read More</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 about-img wow fadeInUp" data-wow-delay="0.5s">
|
||||
<div class="row">
|
||||
<div class="col-12 text-center">
|
||||
<img class="img-fluid w-75 rounded-circle bg-light p-3" src="<?= base_url('images/about-1.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-6 text-start" style="margin-top: -150px;">
|
||||
<img class="img-fluid w-100 rounded-circle bg-light p-3" src="<?= base_url('images/about-2.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-6 text-end" style="margin-top: -150px;">
|
||||
<img class="img-fluid w-100 rounded-circle bg-light p-3" src="<?= base_url('images/about-3.jpg') ?>" alt="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- About End -->
|
||||
|
||||
<!-- Call To Action Start -->
|
||||
<div class="container-xxl py-5">
|
||||
<div class="container">
|
||||
<div class="bg-light rounded">
|
||||
<div class="row g-0">
|
||||
<div class="col-lg-6 wow fadeIn" data-wow-delay="0.1s" style="min-height: 400px;">
|
||||
<div class="position-relative h-100">
|
||||
<img class="position-absolute w-100 h-100 rounded" src="<?= base_url('images/call-to-action.jpg') ?>" style="object-fit: cover;">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 wow fadeIn" data-wow-delay="0.5s">
|
||||
<div class="h-100 d-flex flex-column justify-content-center p-5">
|
||||
<h1 class="mb-4">Become A Teacher or Admin</h1>
|
||||
<p class="mb-4">Becoming a teacher or admin at our Sunday school is a rewarding opportunity to make a meaningful impact on young lives. As a teacher, you will inspire and guide children on their spiritual journey, fostering their growth and understanding of faith. As an admin, you will play a crucial role in supporting the school's operations and ensuring a smooth and effective learning environment. Join our dedicated team and contribute to a nurturing environment that shapes the future of our community.</p>
|
||||
<a class="btn btn-primary py-3 px-5" href="#">Get Started Now<i class="fa fa-arrow-right ms-2"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Call To Action End -->
|
||||
|
||||
<!-- Footer Start -->
|
||||
<div class="container-fluid bg-dark text-white-50 footer pt-5 mt-5 wow fadeIn" data-wow-delay="0.1s">
|
||||
<div class="container py-5">
|
||||
<div class="row g-5">
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<h3 class="text-white mb-4">Get In Touch</h3>
|
||||
<p class="mb-2"><i class="fa fa-map-marker-alt me-3"></i>5 Courthouse Lane, Chelmsford, MA 01824</p>
|
||||
<p class="mb-2"><i class="fa fa-phone-alt me-3"></i>+1 978-364-0219</p>
|
||||
<p class="mb-2"><i class="fa fa-envelope me-3"></i>alrahma.isgl@gmail.com</p>
|
||||
<div class="d-flex pt-2">
|
||||
<a class="btn btn-outline-light btn-social" href=""><i class="fab fa-twitter"></i></a>
|
||||
<a class="btn btn-outline-light btn-social" href=""><i class="fab fa-facebook-f"></i></a>
|
||||
<a class="btn btn-outline-light btn-social" href=""><i class="fab fa-youtube"></i></a>
|
||||
<a class="btn btn-outline-light btn-social" href=""><i class="fab fa-linkedin-in"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<h3 class="text-white mb-4">Quick Links</h3>
|
||||
<a class="btn btn-link text-white-50" href="<?= base_url('/about') ?>">About Us</a>
|
||||
<a class="btn btn-link text-white-50" href="<?= base_url('/contact') ?>">Contact Us</a>
|
||||
<a class="btn btn-link text-white-50" href="<?= base_url('/services') ?>">Our Services</a>
|
||||
<a class="btn btn-link text-white-50" href="<?= base_url('/privacy') ?>">Privacy Policy</a>
|
||||
<a class="btn btn-link text-white-50" href="<?= base_url('/terms') ?>">Terms & Condition</a>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<h3 class="text-white mb-4">Photo Gallery</h3>
|
||||
<div class="row g-2 pt-2">
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-1.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-2.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-3.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-4.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-5.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-6.jpg') ?>" alt="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<h3 class="text-white mb-4">Newsletter</h3>
|
||||
<p>Our newsletter is your gateway to staying informed and connected with our Sunday school community. </p>
|
||||
<div class="position-relative mx-auto" style="max-width: 400px;">
|
||||
<input class="form-control bg-transparent w-100 py-3 ps-4 pe-5" type="text" placeholder="Your email">
|
||||
<button type="button" class="btn btn-primary py-2 position-absolute top-0 end-0 mt-2 me-2">SignUp</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="copyright">
|
||||
<div class="row">
|
||||
<div class="col-md-6 text-center text-md-start mb-3 mb-md-0">
|
||||
© <a class="border-bottom" href="#">Al Rahma Sunday School by ISGL</a>, All Right Reserved.
|
||||
Designed By <a class="border-bottom" href="https://htmlcodex.com">HTML Codex</a>
|
||||
</div>
|
||||
<div class="col-md-6 text-center text-md-end">
|
||||
<div class="footer-menu">
|
||||
<a href="#">Home</a>
|
||||
<a href="#">Cookies</a>
|
||||
<a href="#">Help</a>
|
||||
<a href="#">FQAs</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Footer End -->
|
||||
|
||||
<!-- Back to Top -->
|
||||
<a href="#" class="btn btn-lg btn-primary btn-lg-square back-to-top"><i class="bi bi-arrow-up"></i></a>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript Libraries -->
|
||||
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="<?= base_url('lib/wow/wow.min.js') ?>"></script>
|
||||
<script src="<?= base_url('lib/easing/easing.min.js') ?>"></script>
|
||||
<script src="<?= base_url('lib/waypoints/waypoints.min.js') ?>"></script>
|
||||
<script src="<?= base_url('lib/owlcarousel/owl.carousel.min.js') ?>"></script>
|
||||
|
||||
<!-- Template Javascript -->
|
||||
<script src="<?= base_url('js/main.js') ?>"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,270 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Al Rahma Sunday School</title>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||
<meta content="" name="keywords">
|
||||
<meta content="" name="description">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link href="<?= base_url('assets/images/favicon.ico') ?>" rel="icon">
|
||||
|
||||
<!-- Google Web Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Heebo:wght@400;500;600&family=Inter:wght@600&family=Lobster+Two:wght@700&display=swap"
|
||||
rel="stylesheet">
|
||||
|
||||
<!-- Icon Font Stylesheet -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.10.0/css/all.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.4.1/font/bootstrap-icons.css" rel="stylesheet">
|
||||
|
||||
<!-- Libraries Stylesheet -->
|
||||
<link href="lib/animate/animate.min.css" rel="stylesheet">
|
||||
<link href="lib/owlcarousel/assets/owl.carousel.min.css" rel="stylesheet">
|
||||
|
||||
<!-- Customized Bootstrap Stylesheet -->
|
||||
<link href="css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
<!-- Template Stylesheet -->
|
||||
<link href="assets/css/style.css" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container-xxl bg-white p-0">
|
||||
<!-- Spinner Start -->
|
||||
<div id="spinner"
|
||||
class="show bg-white position-fixed translate-middle w-100 vh-100 top-50 start-50 d-flex align-items-center justify-content-center">
|
||||
<div class="spinner-border text-primary" style="width: 3rem; height: 3rem;" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Spinner End -->
|
||||
|
||||
|
||||
<!-- Navbar Start -->
|
||||
<nav class="navbar navbar-expand-lg bg-white navbar-light sticky-top px-4 px-lg-5 py-lg-0">
|
||||
<a href="<?= base_url('/') ?>" class="navbar-brand d-flex align-items-center">
|
||||
<img src="<?= base_url('images/logo.png') ?>" alt="Al Rahma Sunday School" style="height: 40px; width: 40px; border-radius: 50%; object-fit: contain; background-color: #fff;">
|
||||
<h1 class="m-0 ms-2 green-title">Al Rahma Sunday School</h1>
|
||||
</a>
|
||||
<button type="button" class="navbar-toggler" data-bs-toggle="collapse" data-bs-target="#navbarCollapse">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarCollapse">
|
||||
<div class="navbar-nav mx-auto">
|
||||
<a href="<?= base_url('/') ?>" class="nav-item nav-link">Home</a>
|
||||
<a href="<?= base_url('/about') ?>" class="nav-item nav-link">About Us</a>
|
||||
<a href="<?= base_url('/classes') ?>" class="nav-item nav-link">Classes</a>
|
||||
<div class="nav-item dropdown">
|
||||
<a href="#" class="nav-link dropdown-toggle active" data-bs-toggle="dropdown">Pages</a>
|
||||
<div class="dropdown-menu rounded-0 rounded-bottom border-0 shadow-sm m-0">
|
||||
<a href="<?= base_url('/facility') ?>" class="dropdown-item">School Facilities</a>
|
||||
<a href="team.html" class="dropdown-item">Popular Teachers</a>
|
||||
<a href="<?= base_url('/call-to-action') ?>" class="dropdown-item">Become A Teacher or Admins</a>
|
||||
<a href="<?= base_url('/appointment') ?>" class="dropdown-item active">Make Appointment</a>
|
||||
<a href="<?= base_url('/testimonial') ?>" class="dropdown-item">Testimonial</a>
|
||||
<a href="<?= base_url('/notFound') ?>" class="dropdown-item">notFound Error</a>
|
||||
</div>
|
||||
</div>
|
||||
<a href="<?= base_url('/contact') ?>" class="nav-item nav-link">Contact Us</a>
|
||||
</div>
|
||||
<a href="/register" class="btn btn-primary rounded-pill px-3 d-none d-lg-block">Register
|
||||
<i class="fa fa-arrow-right ms-3"></i></a>
|
||||
<a href="/login" class="btn btn-primary rounded-pill px-3 d-none d-lg-block">Login<i
|
||||
class="fa fa-arrow-right ms-3"></i></a>
|
||||
|
||||
</div>
|
||||
</nav>
|
||||
<!-- Navbar End -->
|
||||
|
||||
|
||||
<!-- Page Header End -->
|
||||
<div class="container-xxl py-5 page-header position-relative mb-5">
|
||||
<div class="container py-5">
|
||||
<h1 class="display-2 text-white animated slideInDown mb-4">Appointment</h1>
|
||||
<nav aria-label="breadcrumb animated slideInDown">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="/">Home</a></li>
|
||||
<li class="breadcrumb-item"><a href="#">Pages</a></li>
|
||||
<li class="breadcrumb-item text-white active" aria-current="page">Appointment</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Page Header End -->
|
||||
|
||||
|
||||
<!-- Appointment Start -->
|
||||
<div class="container-xxl py-5">
|
||||
<div class="container">
|
||||
<div class="bg-light rounded">
|
||||
<div class="row g-0">
|
||||
<div class="col-lg-6 wow fadeIn" data-wow-delay="0.1s">
|
||||
<div class="h-100 d-flex flex-column justify-content-center p-5">
|
||||
<h1 class="mb-4">Make Appointment</h1>
|
||||
<form>
|
||||
<div class="row g-3">
|
||||
<div class="col-sm-6">
|
||||
<div class="form-floating">
|
||||
<input type="text" class="form-control border-0" id="gname"
|
||||
placeholder="Gurdian Name">
|
||||
<label for="gname">Gurdian Name</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="form-floating">
|
||||
<input type="email" class="form-control border-0" id="gmail"
|
||||
placeholder="Gurdian Email">
|
||||
<label for="gmail">Gurdian Email</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="form-floating">
|
||||
<input type="text" class="form-control border-0" id="cname"
|
||||
placeholder="Child Name">
|
||||
<label for="cname">Child Name</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="form-floating">
|
||||
<input type="text" class="form-control border-0" id="cage"
|
||||
placeholder="Child Age">
|
||||
<label for="cage">Child Age</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-floating">
|
||||
<textarea class="form-control border-0"
|
||||
placeholder="Leave a message here" id="message"
|
||||
style="height: 100px"></textarea>
|
||||
<label for="message">Message</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button class="btn btn-primary w-100 py-3" type="submit">Submit</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 wow fadeIn" data-wow-delay="0.5s" style="min-height: 400px;">
|
||||
<div class="position-relative h-100">
|
||||
<img class="position-absolute w-100 h-100 rounded" src="images/appointment.jpg"
|
||||
style="object-fit: cover;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Appointment End -->
|
||||
|
||||
|
||||
<!-- Footer Start -->
|
||||
<div class="container-fluid bg-dark text-white-50 footer pt-5 mt-5 wow fadeIn" data-wow-delay="0.1s">
|
||||
<div class="container py-5">
|
||||
<div class="row g-5">
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<h3 class="text-white mb-4">Get In Touch</h3>
|
||||
<p class="mb-2"><i class="fa fa-map-marker-alt me-3"></i>5 Courthouse Lane, Chelmsford, MA 01824
|
||||
</p>
|
||||
<p class="mb-2"><i class="fa fa-phone-alt me-3"></i>+1 978-364-0219
|
||||
0</p>
|
||||
<p class="mb-2"><i class="fa fa-envelope me-3"></i>alrahma.isgl@gmail.com
|
||||
</p>
|
||||
<div class="d-flex pt-2">
|
||||
<a class="btn btn-outline-light btn-social" href=""><i class="fab fa-twitter"></i></a>
|
||||
<a class="btn btn-outline-light btn-social" href=""><i class="fab fa-facebook-f"></i></a>
|
||||
<a class="btn btn-outline-light btn-social" href=""><i class="fab fa-youtube"></i></a>
|
||||
<a class="btn btn-outline-light btn-social" href=""><i class="fab fa-linkedin-in"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<h3 class="text-white mb-4">Quick Links</h3>
|
||||
<a class="btn btn-link text-white-50" href="">About Us</a>
|
||||
<a class="btn btn-link text-white-50" href="">Contact Us</a>
|
||||
<a class="btn btn-link text-white-50" href="">Our Services</a>
|
||||
<a class="btn btn-link text-white-50" href="">Privacy Policy</a>
|
||||
<a class="btn btn-link text-white-50" href="">Terms & Condition</a>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<h3 class="text-white mb-4">Photo Gallery</h3>
|
||||
<div class="row g-2 pt-2">
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-1.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-2.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-3.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-4.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-5.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-6.jpg') ?>" alt="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<h3 class="text-white mb-4">Newsletter</h3>
|
||||
<p>Our newsletter is your gateway to staying informed and connected with our Sunday school community. </p>
|
||||
<div class="position-relative mx-auto" style="max-width: 400px;">
|
||||
<input class="form-control bg-transparent w-100 py-3 ps-4 pe-5" type="text"
|
||||
placeholder="Your email">
|
||||
<button type="button"
|
||||
class="btn btn-primary py-2 position-absolute top-0 end-0 mt-2 me-2">SignUp</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="copyright">
|
||||
<div class="row">
|
||||
<div class="col-md-6 text-center text-md-start mb-3 mb-md-0">
|
||||
© <a class="border-bottom" href="#">Al Rahma Sunday School by ISGL</a>, All Right
|
||||
Reserved.
|
||||
|
||||
<!--/*** This template is free as long as you keep the footer author’s credit link/attribution link/backlink. If you'd like to use the template without the footer author’s credit link/attribution link/backlink, you can purchase the Credit Removal License from "https://htmlcodex.com/credit-removal". Thank you for your support. ***/-->
|
||||
Designed By <a class="border-bottom" href="https://htmlcodex.com">HTML Codex</a>
|
||||
</div>
|
||||
<div class="col-md-6 text-center text-md-end">
|
||||
<div class="footer-menu">
|
||||
<a href="">Home</a>
|
||||
<a href="">Cookies</a>
|
||||
<a href="">Help</a>
|
||||
<a href="">FQAs</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Footer End -->
|
||||
|
||||
|
||||
<!-- Back to Top -->
|
||||
<a href="#" class="btn btn-lg btn-primary btn-lg-square back-to-top"><i class="bi bi-arrow-up"></i></a>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript Libraries -->
|
||||
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="lib/wow/wow.min.js"></script>
|
||||
<script src="lib/easing/easing.min.js"></script>
|
||||
<script src="lib/waypoints/waypoints.min.js"></script>
|
||||
<script src="lib/owlcarousel/owl.carousel.min.js"></script>
|
||||
|
||||
<!-- Template Javascript -->
|
||||
<script src="js/main.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,230 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Al Rahma Sunday School</title>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||
<meta content="" name="keywords">
|
||||
<meta content="" name="description">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link href="<?= base_url('assets/images/favicon.ico') ?>" rel="icon">
|
||||
|
||||
<!-- Google Web Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Heebo:wght@400;500;600&family=Inter:wght@600&family=Lobster+Two:wght@700&display=swap"
|
||||
rel="stylesheet">
|
||||
|
||||
<!-- Icon Font Stylesheet -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.10.0/css/all.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.4.1/font/bootstrap-icons.css" rel="stylesheet">
|
||||
|
||||
<!-- Libraries Stylesheet -->
|
||||
<link href="lib/animate/animate.min.css" rel="stylesheet">
|
||||
<link href="lib/owlcarousel/assets/owl.carousel.min.css" rel="stylesheet">
|
||||
|
||||
<!-- Customized Bootstrap Stylesheet -->
|
||||
<link href="css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
<!-- Template Stylesheet -->
|
||||
<link href="assets/css/style.css" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container-xxl bg-white p-0">
|
||||
<!-- Spinner Start -->
|
||||
<div id="spinner"
|
||||
class="show bg-white position-fixed translate-middle w-100 vh-100 top-50 start-50 d-flex align-items-center justify-content-center">
|
||||
<div class="spinner-border text-primary" style="width: 3rem; height: 3rem;" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Spinner End -->
|
||||
|
||||
|
||||
<!-- Navbar Start -->
|
||||
<nav class="navbar navbar-expand-lg bg-white navbar-light sticky-top px-4 px-lg-5 py-lg-0">
|
||||
<a href="<?= base_url('/') ?>" class="navbar-brand d-flex align-items-center">
|
||||
<img src="<?= base_url('images/logo.png') ?>" alt="Al Rahma Sunday School" style="height: 40px; width: 40px; border-radius: 50%; object-fit: contain; background-color: #fff;">
|
||||
<h1 class="m-0 ms-2 green-title">Al Rahma Sunday School</h1>
|
||||
</a>
|
||||
<button type="button" class="navbar-toggler" data-bs-toggle="collapse" data-bs-target="#navbarCollapse">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarCollapse">
|
||||
<div class="navbar-nav mx-auto">
|
||||
<a href="<?= base_url('/') ?>" class="nav-item nav-link">Home</a>
|
||||
<a href="<?= base_url('/about') ?>" class="nav-item nav-link">About Us</a>
|
||||
<a href="<?= base_url('/classes') ?>" class="nav-item nav-link">Classes</a>
|
||||
<div class="nav-item dropdown">
|
||||
<a href="#" class="nav-link dropdown-toggle active" data-bs-toggle="dropdown">Pages</a>
|
||||
<div class="dropdown-menu rounded-0 rounded-bottom border-0 shadow-sm m-0">
|
||||
<a href="<?= base_url('/facility') ?>" class="dropdown-item">School Facilities</a>
|
||||
<a href="team.html" class="dropdown-item">Popular Teachers</a>
|
||||
<a href="call-to-action.html" class="dropdown-item active">Become A Teacher or Admins</a>
|
||||
<a href="appointment.html" class="dropdown-item">Make Appointment</a>
|
||||
<a href="<?= base_url('/testimonial') ?>" class="dropdown-item">Testimonial</a>
|
||||
<a href="<?= base_url('/notFound') ?>" class="dropdown-item">404 Error</a>
|
||||
</div>
|
||||
</div>
|
||||
<a href="<?= base_url('/contact') ?>" class="nav-item nav-link">Contact Us</a>
|
||||
</div>
|
||||
<a href="/register" class="btn btn-primary rounded-pill px-3 d-none d-lg-block">Register
|
||||
<i class="fa fa-arrow-right ms-3"></i></a>
|
||||
<a href="/login" class="btn btn-primary rounded-pill px-3 d-none d-lg-block">Login<i
|
||||
class="fa fa-arrow-right ms-3"></i></a>
|
||||
|
||||
</div>
|
||||
</nav>
|
||||
<!-- Navbar End -->
|
||||
|
||||
|
||||
<!-- Page Header End -->
|
||||
<div class="container-xxl py-5 page-header position-relative mb-5">
|
||||
<div class="container py-5">
|
||||
<h1 class="display-2 text-white animated slideInDown mb-4">Become A Teacher or Admins</h1>
|
||||
<nav aria-label="breadcrumb animated slideInDown">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="/">Home</a></li>
|
||||
<li class="breadcrumb-item"><a href="#">Pages</a></li>
|
||||
<li class="breadcrumb-item text-white active" aria-current="page">Become A Teacher or Admins</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Page Header End -->
|
||||
|
||||
|
||||
<!-- Call To Action Start -->
|
||||
<div class="container-xxl py-5">
|
||||
<div class="container">
|
||||
<div class="bg-light rounded">
|
||||
<div class="row g-0">
|
||||
<div class="col-lg-6 wow fadeIn" data-wow-delay="0.1s" style="min-height: 400px;">
|
||||
<div class="position-relative h-100">
|
||||
<img class="position-absolute w-100 h-100 rounded" src="images/call-to-action.jpg"
|
||||
style="object-fit: cover;">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 wow fadeIn" data-wow-delay="0.5s">
|
||||
<div class="h-100 d-flex flex-column justify-content-center p-5">
|
||||
<h1 class="mb-4">Become A Teacher or Admin</h1>
|
||||
<p class="mb-4">Becoming a teacher or admin at our Sunday school is a rewarding opportunity to make a meaningful impact on young lives. As a teacher, you will inspire and guide children on their spiritual journey, fostering their growth and understanding of faith. As an admin, you will play a crucial role in supporting the school's operations and ensuring a smooth and effective learning environment. Join our dedicated team and contribute to a nurturing environment that shapes the future of our community.</p>
|
||||
<a class="btn btn-primary py-3 px-5" href="/register">Get Started Now<i
|
||||
class="fa fa-arrow-right ms-2"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Call To Action End -->
|
||||
|
||||
|
||||
<!-- Footer Start -->
|
||||
<div class="container-fluid bg-dark text-white-50 footer pt-5 mt-5 wow fadeIn" data-wow-delay="0.1s">
|
||||
<div class="container py-5">
|
||||
<div class="row g-5">
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<h3 class="text-white mb-4">Get In Touch</h3>
|
||||
<p class="mb-2"><i class="fa fa-map-marker-alt me-3"></i>5 Courthouse Lane, Chelmsford, MA 01824
|
||||
</p>
|
||||
<p class="mb-2"><i class="fa fa-phone-alt me-3"></i>+1 978-364-0219
|
||||
0</p>
|
||||
<p class="mb-2"><i class="fa fa-envelope me-3"></i>alrahma.isgl@gmail.com
|
||||
</p>
|
||||
<div class="d-flex pt-2">
|
||||
<a class="btn btn-outline-light btn-social" href=""><i class="fab fa-twitter"></i></a>
|
||||
<a class="btn btn-outline-light btn-social" href=""><i class="fab fa-facebook-f"></i></a>
|
||||
<a class="btn btn-outline-light btn-social" href=""><i class="fab fa-youtube"></i></a>
|
||||
<a class="btn btn-outline-light btn-social" href=""><i class="fab fa-linkedin-in"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<h3 class="text-white mb-4">Quick Links</h3>
|
||||
<a class="btn btn-link text-white-50" href="">About Us</a>
|
||||
<a class="btn btn-link text-white-50" href="">Contact Us</a>
|
||||
<a class="btn btn-link text-white-50" href="">Our Services</a>
|
||||
<a class="btn btn-link text-white-50" href="">Privacy Policy</a>
|
||||
<a class="btn btn-link text-white-50" href="">Terms & Condition</a>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<h3 class="text-white mb-4">Photo Gallery</h3>
|
||||
<div class="row g-2 pt-2">
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-1.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-2.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-3.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-4.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-5.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-6.jpg') ?>" alt="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<h3 class="text-white mb-4">Newsletter</h3>
|
||||
<p>Our newsletter is your gateway to staying informed and connected with our Sunday school community. </p>
|
||||
<div class="position-relative mx-auto" style="max-width: 400px;">
|
||||
<input class="form-control bg-transparent w-100 py-3 ps-4 pe-5" type="text"
|
||||
placeholder="Your email">
|
||||
<button type="button"
|
||||
class="btn btn-primary py-2 position-absolute top-0 end-0 mt-2 me-2">SignUp</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="copyright">
|
||||
<div class="row">
|
||||
<div class="col-md-6 text-center text-md-start mb-3 mb-md-0">
|
||||
© <a class="border-bottom" href="#">Al Rahma Sunday School by ISGL</a>, All Right
|
||||
Reserved.
|
||||
|
||||
<!--/*** This template is free as long as you keep the footer author’s credit link/attribution link/backlink. If you'd like to use the template without the footer author’s credit link/attribution link/backlink, you can purchase the Credit Removal License from "https://htmlcodex.com/credit-removal". Thank you for your support. ***/-->
|
||||
Designed By <a class="border-bottom" href="https://htmlcodex.com">HTML Codex</a>
|
||||
</div>
|
||||
<div class="col-md-6 text-center text-md-end">
|
||||
<div class="footer-menu">
|
||||
<a href="">Home</a>
|
||||
<a href="">Cookies</a>
|
||||
<a href="">Help</a>
|
||||
<a href="">FQAs</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Footer End -->
|
||||
|
||||
|
||||
<!-- Back to Top -->
|
||||
<a href="#" class="btn btn-lg btn-primary btn-lg-square back-to-top"><i class="bi bi-arrow-up"></i></a>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript Libraries -->
|
||||
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="lib/wow/wow.min.js"></script>
|
||||
<script src="lib/easing/easing.min.js"></script>
|
||||
<script src="lib/waypoints/waypoints.min.js"></script>
|
||||
<script src="lib/owlcarousel/owl.carousel.min.js"></script>
|
||||
|
||||
<!-- Template Javascript -->
|
||||
<script src="js/main.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,415 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Careers | Al Rahma Sunday School</title>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||
<meta content="Volunteer careers and openings at Al Rahma Sunday School" name="description">
|
||||
|
||||
<link href="<?= base_url('assets/images/favicon.ico') ?>" rel="icon">
|
||||
<link href="<?= base_url('assets/boot_css/bootstrap.min.css') ?>" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Amiri:wght@400;700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.4.1/font/bootstrap-icons.css" rel="stylesheet">
|
||||
<link href="<?= base_url('css/style.css') ?>" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--ink: #16262B;
|
||||
--ink-soft: #3E4E51;
|
||||
--paper: #F3EFE3;
|
||||
--paper-deep: #E9E3D2;
|
||||
--sage: #E7EEE6;
|
||||
--primary: #0B5D52;
|
||||
--primary-dark: #073F38;
|
||||
--accent: #C6963C;
|
||||
--accent-soft: #EFE1BC;
|
||||
--line: rgba(22, 38, 43, 0.12);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
color: var(--ink);
|
||||
background-color: var(--paper);
|
||||
overflow-x: hidden;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: 'Amiri', serif;
|
||||
font-weight: 700;
|
||||
color: var(--ink);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
|
||||
.rule {
|
||||
width: 56px;
|
||||
height: 3px;
|
||||
background-color: var(--accent);
|
||||
border: none;
|
||||
margin: 0 0 1.25rem;
|
||||
}
|
||||
|
||||
.btn-brand {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
background-color: var(--primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
padding: 0.85rem 1.75rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
text-decoration: none;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-brand:hover {
|
||||
background-color: var(--primary-dark);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-brand-sm {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
background-color: var(--primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
padding: 0.6rem 1.25rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-brand-sm:hover {
|
||||
background-color: var(--primary-dark);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-brand-outline {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
border: 1.5px solid var(--ink);
|
||||
color: var(--ink);
|
||||
border-radius: 999px;
|
||||
padding: 0.5rem 1.25rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-brand-outline:hover {
|
||||
background-color: var(--ink);
|
||||
color: var(--paper);
|
||||
}
|
||||
|
||||
.btn-brand-danger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
border: 1.5px solid #a33;
|
||||
color: #a33;
|
||||
border-radius: 999px;
|
||||
padding: 0.5rem 1.25rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-brand-danger:hover {
|
||||
background-color: #a33;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Navbar */
|
||||
.navbar {
|
||||
background-color: var(--paper) !important;
|
||||
border-bottom: 1px solid var(--line);
|
||||
padding-top: 0.6rem;
|
||||
padding-bottom: 0.6rem;
|
||||
}
|
||||
|
||||
.navbar .nav-link {
|
||||
color: var(--ink-soft);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.navbar .nav-link:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.navbar-text {
|
||||
color: var(--ink-soft);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Hero */
|
||||
.careers-hero {
|
||||
position: relative;
|
||||
background: linear-gradient(rgba(7, 63, 56, .88), rgba(7, 63, 56, .88)), url("<?= base_url('images/call-to-action.jpg') ?>") center center / cover no-repeat;
|
||||
min-height: 320px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--paper);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.careers-hero::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0.14;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='84' height='84' viewBox='0 0 84 84'%3E%3Cg fill='none' stroke='%23C6963C' stroke-width='1'%3E%3Cpath d='M42 2 L62 22 L42 42 L22 22 Z'/%3E%3Cpath d='M0 42 L20 22 L40 42 L20 62 Z'/%3E%3Cpath d='M42 42 L62 22 L84 42 L62 62 Z'/%3E%3Cpath d='M42 42 L62 62 L42 84 L22 62 Z'/%3E%3C/g%3E%3C/svg%3E");
|
||||
background-size: 84px 84px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.careers-hero .container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.careers-hero h1 {
|
||||
color: var(--paper);
|
||||
font-size: 2.6rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.careers-hero p.lead {
|
||||
color: #DCE6DD;
|
||||
font-size: 1.1rem;
|
||||
max-width: 56ch;
|
||||
}
|
||||
|
||||
/* Culture strip */
|
||||
.culture-item {
|
||||
background: #ffffff;
|
||||
border-top: 3px solid var(--accent);
|
||||
padding: 1.75rem 1.5rem;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.culture-item h3 {
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
|
||||
.culture-item p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Openings */
|
||||
.openings-section {
|
||||
background-color: var(--sage);
|
||||
}
|
||||
|
||||
.openings-intro h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.opening-card {
|
||||
border: 1px solid var(--line);
|
||||
background: #ffffff;
|
||||
height: 100%;
|
||||
padding: 1.75rem 1.75rem 3rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.opening-new-badge {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
border-radius: 0 0 0 8px;
|
||||
background: var(--accent-soft);
|
||||
color: var(--primary-dark);
|
||||
border: 1px solid rgba(198, 150, 60, 0.45);
|
||||
padding: 0.25rem 0.65rem;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.opening-new-badge i {
|
||||
color: var(--accent);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.opening-card h3 {
|
||||
color: var(--ink);
|
||||
font-size: 1.3rem;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.opening-meta {
|
||||
color: var(--primary);
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
letter-spacing: 0.01em;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.opening-clicks {
|
||||
position: absolute;
|
||||
right: 1.25rem;
|
||||
bottom: 1rem;
|
||||
color: var(--ink-soft);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.opening-list {
|
||||
padding-left: 1.1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
|
||||
.opening-list li {
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.opening-list li::marker {
|
||||
color: var(--accent);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-light sticky-top px-4 px-lg-5">
|
||||
<a href="<?= base_url('/') ?>" class="navbar-brand d-flex align-items-center">
|
||||
<img src="<?= base_url('images/logo.png') ?>" alt="Al Rahma Sunday School" style="height: 56px; width: 56px; border-radius: 50%; object-fit: contain; background-color: #fff;">
|
||||
</a>
|
||||
<button type="button" class="navbar-toggler" data-bs-toggle="collapse" data-bs-target="#navbarCollapse">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarCollapse">
|
||||
<div class="navbar-nav mx-auto"></div>
|
||||
<div class="d-flex align-items-center">
|
||||
<?php if (session()->get('is_logged_in')): ?>
|
||||
<span class="navbar-text me-3">Welcome, <?= esc(session()->get('user_name')) ?></span>
|
||||
<a href="<?= base_url('/dashboard') ?>" class="btn-brand-outline me-2">Dashboard <i class="fa fa-tachometer-alt"></i></a>
|
||||
<a href="<?= base_url('/logout') ?>" class="btn-brand-danger">Logout <i class="fa fa-sign-out-alt"></i></a>
|
||||
<?php else: ?>
|
||||
<a href="<?= base_url('/login') ?>" class="btn-brand-outline me-2">Login <i class="fa fa-arrow-right"></i></a>
|
||||
<a href="<?= base_url('/register') ?>" class="btn-brand-sm">Register <i class="fa fa-arrow-right"></i></a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<header class="careers-hero">
|
||||
<div class="container py-5">
|
||||
<div class="col-lg-8">
|
||||
<h1>Open Positions</h1>
|
||||
<p class="lead mb-4">Serve with Al Rahma Sunday School and help students grow in Quran, Arabic, Islamic Studies and character.</p>
|
||||
<a class="btn-brand" href="#openings">View Openings <i class="fa fa-arrow-down"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section class="container-xxl py-5">
|
||||
<div class="container">
|
||||
<div class="row g-4">
|
||||
<div class="col-md-4">
|
||||
<div class="culture-item">
|
||||
<h3>Faith-Centered Work</h3>
|
||||
<p>Support a school community focused on Islamic learning, strong character and service.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="culture-item">
|
||||
<h3>Supportive Team</h3>
|
||||
<p>Collaborate with teachers, assistants and administrators committed to student success.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="culture-item">
|
||||
<h3>Sunday Schedule</h3>
|
||||
<p>Volunteer in a structured weekend program serving families in the Greater Lowell community.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="openings" class="openings-section py-5">
|
||||
<div class="container">
|
||||
<div class="text-center openings-intro mb-5">
|
||||
<hr class="rule mx-auto">
|
||||
<h2>Current Open Positions</h2>
|
||||
<p class="mb-0">Review the available roles below and apply by creating an account.</p>
|
||||
<p class="mb-0 fw-bold">All positions are non-paid and take place at ISGL: 5 Courthouse Lane, Chelmsford, MA 01824</p>
|
||||
</div>
|
||||
<div class="row g-4">
|
||||
<?php if (!empty($positions)): ?>
|
||||
<?php foreach ($positions as $position): ?>
|
||||
<?php
|
||||
$postedAt = !empty($position['posted_at']) ? strtotime((string) $position['posted_at']) : false;
|
||||
$isNewPosition = $postedAt !== false && $postedAt >= strtotime('-14 days');
|
||||
?>
|
||||
<div class="col-lg-4">
|
||||
<article class="opening-card">
|
||||
<?php if ($isNewPosition): ?>
|
||||
<span class="opening-new-badge"><i class="fa fa-star" aria-hidden="true"></i> New</span>
|
||||
<?php endif; ?>
|
||||
<h3><?= esc($position['title']) ?></h3>
|
||||
<div class="opening-meta">
|
||||
<?= esc($position['department'] ?? '') ?>
|
||||
<?php if (!empty($position['location'])): ?> · <?= esc($position['location']) ?><?php endif; ?>
|
||||
<?php if (!empty($position['employment_type'])): ?> · <?= esc($position['employment_type']) ?><?php endif; ?>
|
||||
<?php if (!empty($position['posted_at'])): ?><br><?= esc(date('M j, Y', strtotime((string) $position['posted_at']))) ?><?php endif; ?>
|
||||
</div>
|
||||
<?php if (!empty($position['description'])): ?>
|
||||
<?php
|
||||
$descriptionPreview = trim(preg_replace('/\s+/', ' ', (string) $position['description']));
|
||||
$descriptionWords = preg_split('/\s+/', $descriptionPreview) ?: [];
|
||||
if (count($descriptionWords) > 32) {
|
||||
$descriptionPreview = implode(' ', array_slice($descriptionWords, 0, 32)) . '...';
|
||||
}
|
||||
?>
|
||||
<p class="mb-4"><?= esc($descriptionPreview) ?></p>
|
||||
<?php endif; ?>
|
||||
<a class="btn-brand-sm" href="<?= site_url('careers/' . $position['position_id'] . '/details') ?>">View Details <i class="fa fa-arrow-right"></i></a>
|
||||
<div class="opening-clicks"><?= esc(number_format((int) ($position['details_click_count'] ?? 0))) ?> views</div>
|
||||
</article>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<div class="col-lg-8 mx-auto">
|
||||
<article class="opening-card text-center">
|
||||
<h3>No open positions right now</h3>
|
||||
<p class="mb-0">Please check back for future opportunities to serve with Al Rahma Sunday School.</p>
|
||||
</article>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<?php include(__DIR__ . '/partials/footer.php'); ?>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,231 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Al Rahma Sunday School</title>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||
<meta content="" name="keywords">
|
||||
<meta content="" name="description">
|
||||
|
||||
|
||||
<!-- Favicon -->
|
||||
<link href="<?= base_url('assets/images/favicon.ico') ?>" rel="icon">
|
||||
|
||||
<!-- Google Web Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Heebo:wght@400;500;600&family=Inter:wght@600&family=Lobster+Two:wght@700&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Icon Font Stylesheet -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.10.0/css/all.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.4.1/font/bootstrap-icons.css" rel="stylesheet">
|
||||
|
||||
<!-- Libraries Stylesheet -->
|
||||
<link href="<?= base_url('lib/animate/animate.min.css') ?>" rel="stylesheet">
|
||||
<link href="<?= base_url('lib/owlcarousel/assets/owl.carousel.min.css') ?>" rel="stylesheet">
|
||||
|
||||
<!-- Customized Bootstrap Stylesheet -->
|
||||
<link href="<?= base_url('css/bootstrap.min.css') ?>" rel="stylesheet">
|
||||
|
||||
<!-- Template Stylesheet -->
|
||||
<link href="<?= base_url('css/style.css') ?>" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container-xxl bg-white p-0">
|
||||
<!-- Spinner Start -->
|
||||
<div id="spinner" class="show bg-white position-fixed translate-middle w-100 vh-100 top-50 start-50 d-flex align-items-center justify-content-center">
|
||||
<div class="spinner-border text-primary" style="width: 3rem; height: 3rem;" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Spinner End -->
|
||||
|
||||
<!-- Navbar Start -->
|
||||
<nav class="navbar navbar-expand-lg bg-white navbar-light sticky-top px-4 px-lg-5 py-lg-0">
|
||||
<a href="<?= base_url('/') ?>" class="navbar-brand d-flex align-items-center">
|
||||
<img src="<?= base_url('images/logo.png') ?>" alt="Al Rahma Sunday School" style="height: 40px; width: 40px; border-radius: 50%; object-fit: contain; background-color: #fff;">
|
||||
<h1 class="m-0 ms-2 green-title">Al Rahma Sunday School</h1>
|
||||
</a>
|
||||
<button type="button" class="navbar-toggler" data-bs-toggle="collapse" data-bs-target="#navbarCollapse">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarCollapse">
|
||||
<div class="navbar-nav mx-auto">
|
||||
<a href="<?= base_url('/') ?>" class="nav-item nav-link">Home</a>
|
||||
<a href="<?= base_url('/about') ?>" class="nav-item nav-link">About Us</a>
|
||||
<a href="<?= base_url('/classes') ?>" class="nav-item nav-link">Classes</a>
|
||||
<a href="<?= base_url('/contact') ?>" class="nav-item nav-link">Contact Us</a>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<a href="/user/login" class="btn btn-primary rounded-pill px-3">Login<i class="fa fa-arrow-right ms-3"></i></a>
|
||||
<a href="/register" class="btn btn-primary rounded-pill px-3 me-2">Register<i class="fa fa-arrow-right ms-3"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
<!-- Navbar End -->
|
||||
|
||||
<!-- Page Header End -->
|
||||
<div class="container-xxl py-5 page-header position-relative mb-5">
|
||||
<div class="container py-5">
|
||||
<h1 class="display-2 text-white animated slideInDown mb-4">Classes</h1>
|
||||
<nav aria-label="breadcrumb animated slideInDown">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item text-green"><a href="/">Home</a></li>
|
||||
<li class="breadcrumb-item text-green"><a href="#">Pages</a></li>
|
||||
<li class="breadcrumb-item text-green active" aria-current="page">Classes</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Page Header End -->
|
||||
|
||||
<!-- Classes Start -->
|
||||
<div class="container-xxl py-5">
|
||||
<div class="container">
|
||||
<div class="text-center mx-auto mb-5 wow fadeInUp" data-wow-delay="0.1s" style="max-width: 600px;">
|
||||
<h1 class="mb-3">School Classes</h1>
|
||||
<p>Our school offers classes for students from grades 1 to 10, providing a comprehensive and engaging curriculum that fosters academic and personal growth. Additionally, our youth program offers enriching activities and mentorship for older students, helping them develop leadership skills and a sense of community. Together, these programs ensure a well-rounded education and support for every stage of your child's development.</p>
|
||||
</div>
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-4 col-md-6 wow fadeInUp" data-wow-delay="0.1s">
|
||||
<div class="classes-item">
|
||||
<div class="bg-light rounded-circle w-75 mx-auto p-3">
|
||||
<img class="img-fluid rounded-circle" src="<?= base_url('assets/images/classes-1.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="bg-light rounded p-4 pt-5 mt-n5">
|
||||
<a class="d-block text-center h3 mt-3 mb-4" href="">Quran Learning</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4 col-md-6 wow fadeInUp" data-wow-delay="0.3s">
|
||||
<div class="classes-item">
|
||||
<div class="bg-light rounded-circle w-75 mx-auto p-3">
|
||||
<img class="img-fluid rounded-circle" src="<?= base_url('assets/images/classes-2.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="bg-light rounded p-4 pt-5 mt-n5">
|
||||
<a class="d-block text-center h3 mt-3 mb-4" href="">Islamic Studies</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4 col-md-6 wow fadeInUp" data-wow-delay="0.5s">
|
||||
<div class="classes-item">
|
||||
<div class="bg-light rounded-circle w-75 mx-auto p-3">
|
||||
<img class="img-fluid rounded-circle" src="<?= base_url('assets/images/classes-3.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="bg-light rounded p-4 pt-5 mt-n5">
|
||||
<a class="d-block text-center h3 mt-3 mb-4" href="">Arabic Learning</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Classes End -->
|
||||
|
||||
<!-- Footer Start -->
|
||||
<div class="container-fluid bg-dark text-white-50 footer pt-5 mt-5 wow fadeIn" data-wow-delay="0.1s">
|
||||
<div class="container py-5">
|
||||
<div class="row g-5">
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<h3 class="text-white mb-4">Get In Touch</h3>
|
||||
<p class="mb-2"><i class="fa fa-map-marker-alt me-3"></i>5 Courthouse Lane, Chelmsford, MA 01824
|
||||
</p>
|
||||
<p class="mb-2"><i class="fa fa-phone-alt me-3"></i>+1 978-364-0219
|
||||
</p>
|
||||
<p class="mb-2"><i class="fa fa-envelope me-3"></i>alrahma.isgl@gmail.com
|
||||
</p>
|
||||
<div class="d-flex pt-2">
|
||||
<a class="btn btn-outline-light btn-social" href=""><i class="fab fa-twitter"></i></a>
|
||||
<a class="btn btn-outline-light btn-social" href=""><i class="fab fa-facebook-f"></i></a>
|
||||
<a class="btn btn-outline-light btn-social" href=""><i class="fab fa-youtube"></i></a>
|
||||
<a class="btn btn-outline-light btn-social" href=""><i class="fab fa-linkedin-in"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<h3 class="text-white mb-4">Quick Links</h3>
|
||||
<a class="btn btn-link text-white-50" href="<?= base_url('about.html') ?>">About Us</a>
|
||||
|
||||
<a class="btn btn-link text-white-50" href="<?= base_url('contact.html') ?>">Classes</a>
|
||||
<a class="btn btn-link text-white-50" href="<?= base_url('services.html') ?>">Our Services</a>
|
||||
<a class="btn btn-link text-white-50" href="<?= base_url('privacy.html') ?>">Privacy Policy</a>
|
||||
<a class="btn btn-link text-white-50" href="<?= base_url('terms.html') ?>">Terms & Condition</a>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<h3 class="text-white mb-4">Photo Gallery</h3>
|
||||
<div class="row g-2 pt-2">
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-1.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-2.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-3.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-4.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-5.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-6.jpg') ?>" alt="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<h3 class="text-white mb-4">Newsletter</h3>
|
||||
<p>Our newsletter is your gateway to staying informed and connected with our Sunday school community. </p>
|
||||
<div class="position-relative mx-auto" style="max-width: 400px;">
|
||||
<input class="form-control bg-transparent w-100 py-3 ps-4 pe-5" type="text" placeholder="Your email">
|
||||
<button type="button" class="btn btn-primary py-2 position-absolute top-0 end-0 mt-2 me-2">SignUp</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="copyright">
|
||||
<div class="row">
|
||||
<div class="col-md-6 text-center text-md-start mb-3 mb-md-0">
|
||||
© <a class="border-bottom" href="#">Al Rahma Sunday School by ISGL</a>, All Right Reserved.
|
||||
|
||||
<!--/*** This template is free as long as you keep the footer author’s credit link/attribution link/backlink. If you'd like to use the template without the footer author’s credit link/attribution link/backlink, you can purchase the Credit Removal License from "https://htmlcodex.com/credit-removal". Thank you for your support. ***/-->
|
||||
Designed By <a class="border-bottom" href="https://htmlcodex.com">HTML Codex</a>
|
||||
</div>
|
||||
<div class="col-md-6 text-center text-md-end">
|
||||
<div class="footer-menu">
|
||||
<a href="<?= base_url('/') ?>" class="nav-item nav-link">Home</a>
|
||||
<a href="#">Cookies</a>
|
||||
<a href="#">Help</a>
|
||||
<a href="#">FQAs</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Footer End -->
|
||||
|
||||
<!-- Back to Top -->
|
||||
<a href="#" class="btn btn-lg btn-primary btn-lg-square back-to-top"><i class="bi bi-arrow-up"></i></a>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript Libraries -->
|
||||
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="<?= base_url('lib/wow/wow.min.js') ?>"></script>
|
||||
<script src="<?= base_url('lib/easing/easing.min.js') ?>"></script>
|
||||
<script src="<?= base_url('lib/waypoints/waypoints.min.js') ?>"></script>
|
||||
<script src="<?= base_url('lib/owlcarousel/owl.carousel.min.js') ?>"></script>
|
||||
|
||||
<!-- Template Javascript -->
|
||||
<script src="<?= base_url('js/main.js') ?>"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,283 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Al Rahma Sunday School</title>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||
<meta content="" name="keywords">
|
||||
<meta content="" name="description">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link href="<?= base_url('assets/images/favicon.ico') ?>" rel="icon">
|
||||
|
||||
<!-- Google Web Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Heebo:wght@400;500;600&family=Inter:wght@600&family=Lobster+Two:wght@700&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Icon Font Stylesheet -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.10.0/css/all.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.4.1/font/bootstrap-icons.css" rel="stylesheet">
|
||||
|
||||
<!-- Libraries Stylesheet -->
|
||||
<link href="<?= base_url('lib/animate/animate.min.css') ?>" rel="stylesheet">
|
||||
<link href="<?= base_url('lib/owlcarousel/assets/owl.carousel.min.css') ?>" rel="stylesheet">
|
||||
|
||||
<!-- Customized Bootstrap Stylesheet -->
|
||||
<link href="<?= base_url('css/bootstrap.min.css') ?>" rel="stylesheet">
|
||||
|
||||
<!-- Template Stylesheet -->
|
||||
<link href="<?= base_url('css/style.css') ?>" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container-xxl bg-white p-0">
|
||||
<!-- Spinner Start -->
|
||||
<div id="spinner" class="show bg-white position-fixed translate-middle w-100 vh-100 top-50 start-50 d-flex align-items-center justify-content-center">
|
||||
<div class="spinner-border text-primary" style="width: 3rem; height: 3rem;" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Spinner End -->
|
||||
|
||||
<!-- Navbar Start -->
|
||||
<nav class="navbar navbar-expand-lg bg-white navbar-light sticky-top px-4 px-lg-5 py-lg-0">
|
||||
<a href="<?= base_url('/') ?>" class="navbar-brand d-flex align-items-center">
|
||||
<img src="<?= base_url('images/logo.png') ?>" alt="Al Rahma Sunday School" style="height: 40px; width: 40px; border-radius: 50%; object-fit: contain; background-color: #fff;">
|
||||
<h1 class="m-0 ms-2 green-title">Al Rahma Sunday School</h1>
|
||||
</a>
|
||||
<button type="button" class="navbar-toggler" data-bs-toggle="collapse" data-bs-target="#navbarCollapse">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarCollapse">
|
||||
<div class="navbar-nav mx-auto">
|
||||
<a href="<?= base_url('/') ?>" class="nav-item nav-link">Home</a>
|
||||
<a href="<?= base_url('/about') ?>" class="nav-item nav-link">About Us</a>
|
||||
<a href="<?= base_url('/classes') ?>" class="nav-item nav-link">Classes</a>
|
||||
<a href="<?= base_url('/contact') ?>" class="nav-item nav-link">Contact Us</a>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<a href="/user/login" class="btn btn-primary rounded-pill px-3">Login<i class="fa fa-arrow-right ms-3"></i></a>
|
||||
<a href="/register" class="btn btn-primary rounded-pill px-3 me-2">Register<i class="fa fa-arrow-right ms-3"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
<!-- Navbar End -->
|
||||
|
||||
<!-- Page Header Start -->
|
||||
<div class="container-xxl py-5 page-header position-relative mb-5">
|
||||
<div class="container py-5">
|
||||
<h1 class="display-2 text-white animated slideInDown mb-4">Contact Us</h1>
|
||||
<nav aria-label="breadcrumb animated slideInDown">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item text-green"><a href="/">Home</a></li>
|
||||
<li class="breadcrumb-item text-green"><a href="#">Pages</a></li>
|
||||
<li class="breadcrumb-item text-green active" aria-current="page">Contact Us</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Page Header End -->
|
||||
|
||||
<!-- Contact Start -->
|
||||
<div class="container-xxl py-5">
|
||||
<div class="container">
|
||||
<div class="text-center mx-auto mb-5 wow fadeInUp" data-wow-delay="0.1s" style="max-width: 600px;">
|
||||
<h1 class="mb-3">Get In Touch</h1>
|
||||
<p>Get in touch with us today to learn more about our programs and how you can get involved.</p>
|
||||
</div>
|
||||
<div class="row g-4 mb-5">
|
||||
<div class="col-md-6 col-lg-4 text-center wow fadeInUp" data-wow-delay="0.1s">
|
||||
<div class="bg-light rounded-circle d-inline-flex align-items-center justify-content-center mb-4"
|
||||
style="width: 75px; height: 75px;">
|
||||
<i class="fa fa-map-marker-alt fa-2x text-primary"></i>
|
||||
</div>
|
||||
<h6>5 Courthouse Lane, Chelmsford, MA 01824</h6>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg-4 text-center wow fadeInUp" data-wow-delay="0.3s">
|
||||
<div class="bg-light rounded-circle d-inline-flex align-items-center justify-content-center mb-4"
|
||||
style="width: 75px; height: 75px;">
|
||||
<i class="fa fa-envelope-open fa-2x text-primary"></i>
|
||||
</div>
|
||||
<h6>alrahma.isgl@gmail.com</h6>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg-4 text-center wow fadeInUp" data-wow-delay="0.5s">
|
||||
<div class="bg-light rounded-circle d-inline-flex align-items-center justify-content-center mb-4"
|
||||
style="width: 75px; height: 75px;">
|
||||
<i class="fa fa-phone-alt fa-2x text-primary"></i>
|
||||
</div>
|
||||
<h6>+1 978-364-0219</h6>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-light rounded">
|
||||
<div class="row g-0">
|
||||
<div class="col-lg-6 wow fadeIn" data-wow-delay="0.1s">
|
||||
<div class="h-100 d-flex flex-column justify-content-center p-5">
|
||||
<form id="contactForm">
|
||||
<div class="row g-3">
|
||||
<div class="col-sm-6">
|
||||
<div class="form-floating">
|
||||
<input type="text" class="form-control border-0" id="name" name="name" placeholder="Your Name">
|
||||
<label for="name">Your Name</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="form-floating">
|
||||
<input type="email" class="form-control border-0" id="email" name="email" placeholder="Your Email">
|
||||
<label for="email">Your Email</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-floating">
|
||||
<input type="text" class="form-control border-0" id="subject" name="subject" placeholder="Subject">
|
||||
<label for="subject">Subject</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-floating">
|
||||
<textarea class="form-control border-0" placeholder="Leave a message here" id="message" name="message" style="height: 100px"></textarea>
|
||||
<label for="message">Message</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button class="btn btn-primary w-100 py-3" type="submit">Send Message</button>
|
||||
</div>
|
||||
<div id="formResponse" class="mt-3"></div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 wow fadeIn" data-wow-delay="0.5s" style="min-height: 400px;">
|
||||
<div class="position-relative h-100">
|
||||
<iframe class="position-relative rounded w-100 h-100"
|
||||
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d2949.902891223497!2d-71.3606721845427!3d42.59682637917086!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x89e3a489bb344a85%3A0xeaba1b29330726cb!2s5%20Courthouse%20Ln%2C%20Chelmsford%2C%20MA%2001824%2C%20USA!5e0!3m2!1sen!2sbd!4v1627993075161!5m2!1sen!2sbd"
|
||||
frameborder="0" style="min-height: 400px; border:0;" allowfullscreen=""
|
||||
aria-hidden="false" tabindex="0"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Contact End -->
|
||||
|
||||
|
||||
<!-- Footer Start -->
|
||||
<div class="container-fluid bg-dark text-white-50 footer pt-5 mt-5 wow fadeIn" data-wow-delay="0.1s">
|
||||
<div class="container py-5">
|
||||
<div class="row g-5">
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<h3 class="text-white mb-4">Get In Touch</h3>
|
||||
<p class="mb-2"><i class="fa fa-map-marker-alt me-3"></i>5 Courthouse Lane, Chelmsford, MA 01824
|
||||
</p>
|
||||
<p class="mb-2"><i class="fa fa-phone-alt me-3"></i>+1 978-364-0219
|
||||
</p>
|
||||
<p class="mb-2"><i class="fa fa-envelope me-3"></i>alrahma.isgl@gmail.com
|
||||
</p>
|
||||
<div class="d-flex pt-2">
|
||||
<a class="btn btn-outline-light btn-social" href=""><i class="fab fa-twitter"></i></a>
|
||||
<a class="btn btn-outline-light btn-social" href=""><i class="fab fa-facebook-f"></i></a>
|
||||
<a class="btn btn-outline-light btn-social" href=""><i class="fab fa-youtube"></i></a>
|
||||
<a class="btn btn-outline-light btn-social" href=""><i class="fab fa-linkedin-in"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<h3 class="text-white mb-4">Quick Links</h3>
|
||||
<a class="btn btn-link text-white-50" href="<?= base_url('/about') ?>">About Us</a>
|
||||
<a class="btn btn-link text-white-50" href="<?= base_url('/contact') ?>">Contact Us</a>
|
||||
<a class="btn btn-link text-white-50" href="<?= base_url('/services') ?>">Our Services</a>
|
||||
<a class="btn btn-link text-white-50" href="<?= base_url('/privacy') ?>">Privacy Policy</a>
|
||||
<a class="btn btn-link text-white-50" href="<?= base_url('/terms') ?>">Terms & Condition</a>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<h3 class="text-white mb-4">Photo Gallery</h3>
|
||||
<div class="row g-2 pt-2">
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-1.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-2.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-3.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-4.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-5.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-6.jpg') ?>" alt="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<h3 class="text-white mb-4">Newsletter</h3>
|
||||
<p>Our newsletter is your gateway to staying informed and connected with our Sunday school community. </p>
|
||||
<div class="position-relative mx-auto" style="max-width: 400px;">
|
||||
<input class="form-control bg-transparent w-100 py-3 ps-4 pe-5" type="text" placeholder="Your email">
|
||||
<button type="button" class="btn btn-primary py-2 position-absolute top-0 end-0 mt-2 me-2">SignUp</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="copyright">
|
||||
<div class="row">
|
||||
<div class="col-md-6 text-center text-md-start mb-3 mb-md-0">
|
||||
© <a class="border-bottom" href="#">Al Rahma Sunday School by ISGL</a>, All Right Reserved.
|
||||
Designed By <a class="border-bottom" href="https://htmlcodex.com">HTML Codex</a>
|
||||
</div>
|
||||
<div class="col-md-6 text-center text-md-end">
|
||||
<div class="footer-menu">
|
||||
<a href="#">Home</a>
|
||||
<a href="#">Cookies</a>
|
||||
<a href="#">Help</a>
|
||||
<a href="#">FQAs</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Footer End -->
|
||||
|
||||
<!-- Back to Top -->
|
||||
<a href="#" class="btn btn-lg btn-primary btn-lg-square back-to-top"><i class="bi bi-arrow-up"></i></a>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript Libraries -->
|
||||
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="<?= base_url('lib/wow/wow.min.js') ?>"></script>
|
||||
<script src="<?= base_url('lib/easing/easing.min.js') ?>"></script>
|
||||
<script src="<?= base_url('lib/waypoints/waypoints.min.js') ?>"></script>
|
||||
<script src="<?= base_url('lib/owlcarousel/owl.carousel.min.js') ?>"></script>
|
||||
|
||||
<!-- Template Javascript -->
|
||||
<script src="<?= base_url('js/main.js') ?>"></script>
|
||||
<script>
|
||||
document.getElementById('contactForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
let formData = new FormData(this);
|
||||
|
||||
fetch('contact_process.php', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
document.getElementById('formResponse').innerHTML = data;
|
||||
document.getElementById('contactForm').reset();
|
||||
})
|
||||
.catch(error => console.error('Error:', error));
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,37 +0,0 @@
|
||||
<?php
|
||||
if ($_SERVER["REQUEST_METHOD"] == "POST") {
|
||||
$name = strip_tags(trim($_POST["name"]));
|
||||
$email = filter_var(trim($_POST["email"]), FILTER_SANITIZE_EMAIL);
|
||||
$subject = strip_tags(trim($_POST["subject"]));
|
||||
$message = trim($_POST["message"]);
|
||||
|
||||
// Check that data was sent to the mailer.
|
||||
if (empty($name) || empty($subject) || empty($message) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
echo "Oops! There was a problem with your submission. Please complete the form and try again.";
|
||||
exit;
|
||||
}
|
||||
|
||||
// Set the recipient email address.
|
||||
$recipient = "alrahma.isgl@gmail.com";
|
||||
|
||||
// Set the email subject.
|
||||
$email_subject = "New contact from $name: $subject";
|
||||
|
||||
// Build the email content.
|
||||
$email_content = "Name: $name\n";
|
||||
$email_content .= "Email: $email\n\n";
|
||||
$email_content .= "Message:\n$message\n";
|
||||
|
||||
// Build the email headers.
|
||||
$email_headers = "From: $name <$email>";
|
||||
|
||||
// Send the email.
|
||||
if (mail($recipient, $email_subject, $email_content, $email_headers)) {
|
||||
echo "Thank you! Your message has been sent.";
|
||||
} else {
|
||||
echo "Oops! Something went wrong and we couldn't send your message.";
|
||||
}
|
||||
} else {
|
||||
echo "There was a problem with your submission, please try again.";
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,7 @@
|
||||
<p>Assalamu alaikum <?= esc($name) ?>,</p>
|
||||
|
||||
<p>Thank you for applying for the <?= esc($position['title']) ?> position at Al Rahma Sunday School.</p>
|
||||
|
||||
<p>We received your application and will review it with the hiring team. If your background matches the role, we will follow up with next steps.</p>
|
||||
|
||||
<p>Al Rahma Sunday School</p>
|
||||
@@ -0,0 +1,22 @@
|
||||
<p>Assalamu alaikum <?= esc($name) ?>,</p>
|
||||
|
||||
<?php
|
||||
$statusMessages = [
|
||||
'New' => 'Thank you for applying for ' . esc($positionTitle) . '. We have received your application and it is now in our review queue.',
|
||||
'Reviewed' => 'Thank you for applying for ' . esc($positionTitle) . '. Our team has reviewed your application and will contact you if additional information or next steps are needed.',
|
||||
'Contacted' => 'Thank you for applying for ' . esc($positionTitle) . '. Our team has moved your application forward and has contacted you, or will contact you shortly, about next steps.',
|
||||
'Rejected' => 'Thank you for your interest in serving as ' . esc($positionTitle) . '. After reviewing the current needs of the program, we are not moving forward with your application for this role at this time. We sincerely appreciate your willingness to support Al Rahma Sunday School and encourage you to consider future volunteer opportunities.',
|
||||
'Hired' => 'Congratulations. We are pleased to move forward with your application for ' . esc($positionTitle) . '. Our team will follow up with next steps.',
|
||||
];
|
||||
$message = $statusMessages[$statusLabel] ?? 'Thank you for applying for ' . esc($positionTitle) . '. We are writing to share an update about your application.';
|
||||
?>
|
||||
|
||||
<p><?= $message ?></p>
|
||||
|
||||
<?php if (!empty($adminNotes)): ?>
|
||||
<p><strong>Message from Al Rahma Sunday School:</strong><br><?= nl2br(esc($adminNotes)) ?></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<p>Thank you for your interest in serving with Al Rahma Sunday School.</p>
|
||||
|
||||
<p>Al Rahma Sunday School</p>
|
||||
@@ -35,16 +35,17 @@
|
||||
<table id="enrollmentTable" class="table table-bordered table-striped mt-4 align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Registration Date</th>
|
||||
<th>Parent/Guardian</th>
|
||||
<th>Register Date</th>
|
||||
<th>Parent</th>
|
||||
<th>Student Name</th>
|
||||
<th>School ID</th>
|
||||
<th>Age</th>
|
||||
<th>New Student</th>
|
||||
<th>Current Class</th>
|
||||
<th>Actual Status</th>
|
||||
<th>Update Enrollment Status</th>
|
||||
<th>Assign Class</th>
|
||||
<th>Age</th>
|
||||
<th>New Student</th>
|
||||
<th>Registered Class</th>
|
||||
<th>Current Class</th>
|
||||
<th>Actual Status</th>
|
||||
<th>Update Status</th>
|
||||
<th>Assign Class</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -100,11 +101,14 @@
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
|
||||
<!-- Class -->
|
||||
<td><?= esc($student['class_section'] ?? 'Class not Assigned') ?></td>
|
||||
<!-- Registered Class -->
|
||||
<td><?= esc(trim((string)($student['registration_grade'] ?? '')) !== '' ? (string)$student['registration_grade'] : '-') ?></td>
|
||||
|
||||
<!-- Enrollment Status -->
|
||||
<td>
|
||||
<!-- Class -->
|
||||
<td><?= esc($student['class_section'] ?? 'Class not Assigned') ?></td>
|
||||
|
||||
<!-- Enrollment Status -->
|
||||
<td>
|
||||
<?php
|
||||
$status = $student['enrollment_status'] ?? 'not enrolled';
|
||||
switch ($status) {
|
||||
@@ -185,11 +189,11 @@
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="10">No students available.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="11">No students available.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -383,10 +387,10 @@
|
||||
dom: "<'row mb-2'<'col-sm-6'l><'col-sm-6'f>>" + "t" +
|
||||
"<'row mt-2'<'col-sm-5'i><'col-sm-7'p>>",
|
||||
|
||||
// Disable sort/search on interactive columns (status select, assign select)
|
||||
columnDefs: [
|
||||
{ targets: [8, 9], orderable: false, searchable: false },
|
||||
{ targets: [0, 1, 2, 3, 4, 5, 6, 7], render: function(data, type) {
|
||||
// Disable sort/search on interactive columns (status select, assign select)
|
||||
columnDefs: [
|
||||
{ targets: [9, 10], orderable: false, searchable: false },
|
||||
{ targets: [0, 1, 2, 3, 4, 5, 6, 7, 8], render: function(data, type) {
|
||||
if (type === 'filter' || type === 'sort' || type === 'type') {
|
||||
return stripHtml(data);
|
||||
}
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Al Rahma Sunday School</title>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||
<meta content="" name="keywords">
|
||||
<meta content="" name="description">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link href="<?= base_url('assets/images/favicon.ico') ?>" rel="icon">
|
||||
|
||||
<!-- Google Web Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Heebo:wght@400;500;600&family=Inter:wght@600&family=Lobster+Two:wght@700&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Icon Font Stylesheet -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.10.0/css/all.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.4.1/font/bootstrap-icons.css" rel="stylesheet">
|
||||
|
||||
<!-- Libraries Stylesheet -->
|
||||
<link href="<?= base_url('public/lib/animate/animate.min.css'); ?>" rel="stylesheet">
|
||||
<link href="<?= base_url('public/lib/owlcarousel/assets/owl.carousel.min.css'); ?>" rel="stylesheet">
|
||||
|
||||
<!-- Customized Bootstrap Stylesheet -->
|
||||
<link href="<?= base_url('public/css/bootstrap.min.css'); ?>" rel="stylesheet">
|
||||
|
||||
<!-- Template Stylesheet -->
|
||||
<link href="<?= base_url('public/assets/css/style.css'); ?>" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container-xxl bg-white p-0">
|
||||
<!-- Spinner Start -->
|
||||
<div id="spinner" class="show bg-white position-fixed translate-middle w-100 vh-100 top-50 start-50 d-flex align-items-center justify-content-center">
|
||||
<div class="spinner-border text-primary" style="width: 3rem; height: 3rem;" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Spinner End -->
|
||||
|
||||
<!-- Navbar Start -->
|
||||
<nav class="navbar navbar-expand-lg bg-white navbar-light sticky-top px-4 px-lg-5 py-lg-0">
|
||||
<a href="<?= base_url('/') ?>" class="navbar-brand d-flex align-items-center">
|
||||
<img src="<?= base_url('images/logo.png') ?>" alt="Al Rahma Sunday School" style="height: 40px; width: 40px; border-radius: 50%; object-fit: contain; background-color: #fff;">
|
||||
<h1 class="m-0 ms-2 green-title">Al Rahma Sunday School</h1>
|
||||
</a>
|
||||
<button type="button" class="navbar-toggler" data-bs-toggle="collapse" data-bs-target="#navbarCollapse">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarCollapse">
|
||||
<div class="navbar-nav mx-auto">
|
||||
<a href="<?= base_url('/') ?>" class="nav-item nav-link active">Home</a>
|
||||
<a href="<?= base_url('/about') ?>" class="nav-item nav-link">About Us</a>
|
||||
<a href="<?= base_url('/classes') ?>" class="nav-item nav-link">Classes</a>
|
||||
<a href="<?= base_url('/contact') ?>" class="nav-item nav-link">Contact Us</a>
|
||||
</div>
|
||||
<a href="<?= base_url('/register') ?>" class="btn btn-primary rounded-pill px-3 d-none d-lg-block">Register
|
||||
<i class="fa fa-arrow-right ms-3"></i></a>
|
||||
<a href="<?= base_url('/user/login') ?>" class="btn btn-primary rounded-pill px-3 d-none d-lg-block">Login<i class="fa fa-arrow-right ms-3"></i></a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Navbar End -->
|
||||
|
||||
<!-- Your page content here -->
|
||||
|
||||
<!-- Footer -->
|
||||
<footer>
|
||||
<!-- Your footer content -->
|
||||
</footer>
|
||||
|
||||
<!-- Back to Top -->
|
||||
<a href="#" class="btn btn-lg btn-primary btn-lg-square back-to-top"><i class="bi bi-arrow-up"></i></a>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript Libraries -->
|
||||
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="<?= base_url('public/lib/wow/wow.min.js'); ?>"></script>
|
||||
<script src="<?= base_url('public/lib/easing/easing.min.js'); ?>"></script>
|
||||
<script src="<?= base_url('public/lib/waypoints/waypoints.min.js'); ?>"></script>
|
||||
<script src="<?= base_url('public/lib/owlcarousel/owl.carousel.min.js'); ?>"></script>
|
||||
|
||||
<!-- Template Javascript -->
|
||||
<script src="<?= base_url('public/js/main.js'); ?>"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,90 +0,0 @@
|
||||
<!-- footer.php -->
|
||||
<div class="container-fluid bg-dark text-white-50 footer pt-5 mt-5 wow fadeIn" data-wow-delay="0.1s">
|
||||
<div class="container py-5">
|
||||
<div class="row g-5">
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<h3 class="text-white mb-4">Get In Touch</h3>
|
||||
<p class="mb-2"><i class="fa fa-map-marker-alt me-3"></i>5 Courthouse Lane, Chelmsford, MA 01824</p>
|
||||
<p class="mb-2"><i class="fa fa-phone-alt me-3"></i>+1 978-364-0219</p>
|
||||
<p class="mb-2"><i class="fa fa-envelope me-3"></i>alrahma.isgl@gmail.com</p>
|
||||
<div class="d-flex pt-2">
|
||||
<a class="btn btn-outline-light btn-social" href=""><i class="fab fa-twitter"></i></a>
|
||||
<a class="btn btn-outline-light btn-social" href=""><i class="fab fa-facebook-f"></i></a>
|
||||
<a class="btn btn-outline-light btn-social" href=""><i class="fab fa-youtube"></i></a>
|
||||
<a class="btn btn-outline-light btn-social" href=""><i class="fab fa-linkedin-in"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<h3 class="text-white mb-4">Quick Links</h3>
|
||||
<a class="btn btn-link text-white-50" href="about.php">About Us</a>
|
||||
<a class="btn btn-link text-white-50" href="contact.php">Contact Us</a>
|
||||
<a class="btn btn-link text-white-50" href="services.php">Our Services</a>
|
||||
<a class="btn btn-link text-white-50" href="privacy.php">Privacy Policy</a>
|
||||
<a class="btn btn-link text-white-50" href="terms.php">Terms & Condition</a>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<h3 class="text-white mb-4">Photo Gallery</h3>
|
||||
<div class="row g-2 pt-2">
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-1.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-2.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-3.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-4.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-5.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-6.jpg') ?>" alt="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<h3 class="text-white mb-4">Newsletter</h3>
|
||||
<p>Discover the latest updates and exciting events happening at our school in this month's newsletter. From academic achievements to upcoming activities, stay informed and engaged with our vibrant school community.</p>
|
||||
<div class="position-relative mx-auto" style="max-width: 400px;">
|
||||
<input class="form-control bg-transparent w-100 py-3 ps-4 pe-5" type="text" placeholder="Your email">
|
||||
<button type="button" class="btn btn-primary py-2 position-absolute top-0 end-0 mt-2 me-2">SignUp</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="copyright">
|
||||
<div class="row">
|
||||
<div class="col-md-6 text-center text-md-start mb-3 mb-md-0">
|
||||
© <a class="border-bottom" href="#">Al Rahma Sunday School by ISGL</a>, All Right Reserved.
|
||||
<!--/*** This template is free as long as you keep the footer author’s credit link/attribution link/backlink. If you'd like to use the template without the footer author’s credit link/attribution link/backlink, you can purchase the Credit Removal License from "https://htmlcodex.com/credit-removal". Thank you for your support. ***/-->
|
||||
Designed By <a class="border-bottom" href="https://htmlcodex.com">HTML Codex</a>
|
||||
</div>
|
||||
<div class="col-md-6 text-center text-md-end">
|
||||
<div class="footer-menu">
|
||||
<a href="index.php">Home</a>
|
||||
<a href="">Cookies</a>
|
||||
<a href="">Help</a>
|
||||
<a href="">FQAs</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Back to Top -->
|
||||
<a href="#" class="btn btn-lg btn-primary btn-lg-square back-to-top"><i class="bi bi-arrow-up"></i></a>
|
||||
<!-- JavaScript Libraries -->
|
||||
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="lib/wow/wow.min.js"></script>
|
||||
<script src="lib/easing/easing.min.js"></script>
|
||||
<script src="lib/waypoints/waypoints.min.js"></script>
|
||||
<script src="lib/owlcarousel/owl.carousel.min.js"></script>
|
||||
<!-- Template Javascript -->
|
||||
<script src="js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,26 +0,0 @@
|
||||
<!-- header.php -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Al Rahma Sunday School</title>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||
<meta content="" name="keywords">
|
||||
<meta content="" name="description">
|
||||
<!-- Favicon -->
|
||||
<link href="<?= base_url('assets/images/favicon.ico') ?>" rel="icon">
|
||||
<!-- Google Web Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Heebo:wght@400;500;600&family=Inter:wght@600&family=Lobster+Two:wght@700&display=swap" rel="stylesheet">
|
||||
<!-- Icon Font Stylesheet -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.10.0/css/all.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.4.1/font/bootstrap-icons.css" rel="stylesheet">
|
||||
<!-- Libraries Stylesheet -->
|
||||
<link href="lib/animate/animate.min.css" rel="stylesheet">
|
||||
<link href="lib/owlcarousel/assets/owl.carousel.min.css" rel="stylesheet">
|
||||
<!-- Customized Bootstrap Stylesheet -->
|
||||
<link href="css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Template Stylesheet -->
|
||||
<link href="assets/css/style.css" rel="stylesheet">
|
||||
</head>
|
||||
@@ -1,51 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>School</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">
|
||||
Al Rahma Sunday School
|
||||
<!-- Add the logo image below the text -->
|
||||
<img src="<?= base_url('assets/images/logo.png') ?>" alt="School Logo" class="logo"> <!-- Adjust the path accordingly -->
|
||||
</div>
|
||||
<div class="right-side">
|
||||
<div class="login-container">
|
||||
<h2>Login to your account</h2>
|
||||
<div class="social-login">
|
||||
<a href="#" class="facebook">f</a>
|
||||
<a href="#" class="google">G+</a>
|
||||
<a href="#" class="linkedin">in</a>
|
||||
</div>
|
||||
<p>____________________ OR ____________________</p>
|
||||
<form method="post" action="/user/login">
|
||||
<?= csrf_field(); ?>
|
||||
<div class="form-group">
|
||||
<label for="email">Email:</label>
|
||||
<input type="email" id="email" name="email" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Password:</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit">Login</button>
|
||||
<div class="form-links">
|
||||
<a href="/register">Register</a>
|
||||
<a href="<?= site_url('user/forgot_password') ?>">Forgot Password</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
+633
-629
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,31 @@
|
||||
<?php $item = $item ?? []; ?>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="title">Title</label>
|
||||
<input id="title" name="title" class="form-control" value="<?= esc(old('title', $item['title'] ?? '')) ?>" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="department">Department</label>
|
||||
<input id="department" name="department" class="form-control" value="<?= esc(old('department', $item['department'] ?? '')) ?>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="location">Location</label>
|
||||
<input id="location" name="location" class="form-control" value="<?= esc(old('location', $item['location'] ?? '')) ?>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="employment_type">Employment Type</label>
|
||||
<input id="employment_type" name="employment_type" class="form-control" value="<?= esc(old('employment_type', $item['employment_type'] ?? '')) ?>">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="description">Description</label>
|
||||
<textarea id="description" name="description" class="form-control" rows="6"><?= esc(old('description', $item['description'] ?? '')) ?></textarea>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="responsibilities">Responsibilities</label>
|
||||
<textarea id="responsibilities" name="responsibilities" class="form-control" rows="6"><?= esc(old('responsibilities', $item['responsibilities'] ?? '')) ?></textarea>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="requirements">Requirements</label>
|
||||
<textarea id="requirements" name="requirements" class="form-control" rows="6"><?= esc(old('requirements', $item['requirements'] ?? '')) ?></textarea>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,62 @@
|
||||
<?= $this->extend('layout/management_layout') ?>
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<div class="container-fluid mt-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h2>Job Applications</h2>
|
||||
<a href="<?= site_url('administrator/job-postings/positions') ?>" class="btn btn-outline-secondary">Positions</a>
|
||||
</div>
|
||||
<?php if (session('success')): ?><div class="alert alert-success"><?= esc(session('success')) ?></div><?php endif; ?>
|
||||
<?php if (session('error')): ?><div class="alert alert-danger"><?= esc(session('error')) ?></div><?php endif; ?>
|
||||
<form method="get" class="row g-2 mb-4">
|
||||
<div class="col-md-4">
|
||||
<select name="position_id" class="form-select">
|
||||
<option value="">All positions</option>
|
||||
<?php foreach ($positions as $position): ?>
|
||||
<option value="<?= esc($position['position_id']) ?>" <?= $selectedPosition === $position['position_id'] ? 'selected' : '' ?>><?= esc($position['title']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select name="status" class="form-select">
|
||||
<option value="">All statuses</option>
|
||||
<?php foreach ($statuses as $status): ?>
|
||||
<option value="<?= esc($status) ?>" <?= $selectedStatus === $status ? 'selected' : '' ?>><?= esc(ucfirst($status)) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-auto"><button class="btn btn-outline-primary" type="submit">Filter</button></div>
|
||||
</form>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-bordered align-middle no-mgmt-sticky">
|
||||
<thead><tr><th>Applicant</th><th>Position</th><th>Position ID</th><th>Email</th><th>Phone</th><th>Submitted</th><th>Status / Notes</th><th>Resume</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($applications as $application): ?>
|
||||
<tr>
|
||||
<td><?= esc(trim($application['first_name'] . ' ' . $application['last_name'])) ?></td>
|
||||
<td><?= esc($application['position_title'] ?? '') ?></td>
|
||||
<td><?= esc(substr((string) ($application['position_id'] ?? ''), 0, 8)) ?></td>
|
||||
<td><a href="mailto:<?= esc($application['email']) ?>"><?= esc($application['email']) ?></a></td>
|
||||
<td><?= esc($application['phone']) ?></td>
|
||||
<td><?= esc($application['submitted_at']) ?></td>
|
||||
<td style="min-width: 280px;">
|
||||
<form action="<?= site_url('administrator/job-postings/applications/' . $application['application_id']) ?>" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<select name="status" class="form-select form-select-sm mb-2">
|
||||
<?php foreach ($statuses as $status): ?>
|
||||
<option value="<?= esc($status) ?>" <?= $application['status'] === $status ? 'selected' : '' ?>><?= esc(ucfirst($status)) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<textarea name="admin_notes" class="form-control form-control-sm mb-2" rows="2"><?= esc($application['admin_notes'] ?? '') ?></textarea>
|
||||
<button class="btn btn-sm btn-primary" type="submit">Save</button>
|
||||
</form>
|
||||
</td>
|
||||
<td><a class="btn btn-sm btn-outline-secondary" href="<?= site_url('administrator/job-postings/applications/' . $application['application_id'] . '/resume') ?>">Download</a></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
@@ -0,0 +1,27 @@
|
||||
<?= $this->extend('layout/management_layout') ?>
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<?php $isEdit = !empty($position['position_id']); ?>
|
||||
<div class="container mt-4">
|
||||
<h2><?= $isEdit ? 'Edit Job Position' : 'New Job Position' ?></h2>
|
||||
<?php if (session('errors')): ?>
|
||||
<div class="alert alert-danger"><?php foreach ((array) session('errors') as $error): ?><div><?= esc($error) ?></div><?php endforeach; ?></div>
|
||||
<?php endif; ?>
|
||||
<form action="<?= $isEdit ? site_url('administrator/job-postings/positions/' . $position['position_id']) : site_url('administrator/job-postings/positions') ?>" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="template_id" value="<?= esc(old('template_id', $position['template_id'] ?? '')) ?>">
|
||||
<?= view('jobs/admin/_posting_fields', ['item' => $position ?? []]) ?>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="status">Status</label>
|
||||
<select id="status" name="status" class="form-select">
|
||||
<?php foreach ($statuses as $status): ?>
|
||||
<option value="<?= esc($status) ?>" <?= old('status', $position['status'] ?? 'draft') === $status ? 'selected' : '' ?>><?= esc(ucfirst($status)) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Save Position</button>
|
||||
<a href="<?= site_url('administrator/job-postings/positions') ?>" class="btn btn-outline-secondary">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
@@ -0,0 +1,49 @@
|
||||
<?= $this->extend('layout/management_layout') ?>
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<div class="container-fluid mt-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h2>Job Positions</h2>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="<?= site_url('administrator/job-postings/applications') ?>" class="btn btn-outline-secondary">Applications</a>
|
||||
<a href="<?= site_url('administrator/job-postings/templates') ?>" class="btn btn-primary">New Position</a>
|
||||
</div>
|
||||
</div>
|
||||
<?php if (session('success')): ?><div class="alert alert-success"><?= esc(session('success')) ?></div><?php endif; ?>
|
||||
<?php if (session('error')): ?><div class="alert alert-danger"><?= esc(session('error')) ?></div><?php endif; ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-bordered no-mgmt-sticky">
|
||||
<thead><tr><th>Title</th><th>Department</th><th>Location</th><th>Type</th><th>Status</th><th>Post Date</th><th>Detail Clicks</th><th>Actions</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($positions as $position): ?>
|
||||
<?php
|
||||
$status = strtolower((string) ($position['status'] ?? 'draft'));
|
||||
$statusBadgeClass = match ($status) {
|
||||
'open' => 'bg-success',
|
||||
'filled' => 'bg-primary',
|
||||
'closed' => 'bg-secondary',
|
||||
default => 'bg-warning text-dark',
|
||||
};
|
||||
?>
|
||||
<tr>
|
||||
<td><?= esc($position['title']) ?></td>
|
||||
<td><?= esc($position['department'] ?? '') ?></td>
|
||||
<td><?= esc($position['location'] ?? '') ?></td>
|
||||
<td><?= esc($position['employment_type'] ?? '') ?></td>
|
||||
<td><span class="badge <?= esc($statusBadgeClass) ?>"><?= esc(ucfirst($status)) ?></span></td>
|
||||
<td><?= !empty($position['posted_at']) ? esc(date('M j, Y', strtotime((string) $position['posted_at']))) : '—' ?></td>
|
||||
<td><?= esc(number_format((int) ($position['details_click_count'] ?? 0))) ?></td>
|
||||
<td>
|
||||
<a class="btn btn-sm btn-outline-primary" href="<?= site_url('administrator/job-postings/positions/' . $position['position_id'] . '/edit') ?>">Edit</a>
|
||||
<?php if ($status === 'open'): ?>
|
||||
<a class="btn btn-sm btn-outline-secondary" href="<?= site_url('careers/' . $position['position_id']) ?>">Public View</a>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
@@ -0,0 +1,47 @@
|
||||
<?= $this->extend('layout/management_layout') ?>
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<?php $isEdit = !empty($template); ?>
|
||||
<div class="container mt-4">
|
||||
<h2><?= $isEdit ? 'Edit Job Template' : 'New Job Template' ?></h2>
|
||||
<?php if (session('errors')): ?>
|
||||
<div class="alert alert-danger"><?php foreach ((array) session('errors') as $error): ?><div><?= esc($error) ?></div><?php endforeach; ?></div>
|
||||
<?php endif; ?>
|
||||
<form action="<?= $isEdit ? site_url('administrator/job-postings/templates/' . $template['template_id']) : site_url('administrator/job-postings/templates') ?>" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<?= view('jobs/admin/_posting_fields', ['item' => $template ?? []]) ?>
|
||||
<?php if ($isEdit): ?>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="save_mode">Save Mode</label>
|
||||
<select id="save_mode" name="save_mode" class="form-select">
|
||||
<option value="overwrite">Overwrite current version</option>
|
||||
<option value="new_version">Save as new version</option>
|
||||
</select>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<button type="submit" class="btn btn-primary">Save Template</button>
|
||||
<a href="<?= site_url('administrator/job-postings/templates') ?>" class="btn btn-outline-secondary">Cancel</a>
|
||||
</form>
|
||||
<?php if ($isEdit && !empty($versions)): ?>
|
||||
<h3 class="h5 mt-5">Version History</h3>
|
||||
<table class="table table-sm table-bordered no-mgmt-sticky" data-no-mgmt-sticky>
|
||||
<thead><tr><th>Version</th><th>Saved At</th><th>Actions</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($versions as $version): ?>
|
||||
<tr>
|
||||
<td><?= esc($version['version']) ?></td>
|
||||
<td><?= esc($version['saved_at']) ?></td>
|
||||
<td>
|
||||
<form action="<?= site_url('administrator/job-postings/templates/versions/' . $version['version_id'] . '/restore') ?>" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<button class="btn btn-sm btn-outline-primary" type="submit">Restore</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
@@ -0,0 +1,53 @@
|
||||
<?= $this->extend('layout/management_layout') ?>
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<div class="container-fluid mt-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h2>Job Templates</h2>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="<?= site_url('administrator/job-postings/positions') ?>" class="btn btn-outline-secondary">Open Positions</a>
|
||||
<a href="<?= site_url('administrator/job-postings/templates/new') ?>" class="btn btn-primary">New Template</a>
|
||||
</div>
|
||||
</div>
|
||||
<?php if (session('success')): ?><div class="alert alert-success"><?= esc(session('success')) ?></div><?php endif; ?>
|
||||
<?php if (session('error')): ?><div class="alert alert-danger"><?= esc(session('error')) ?></div><?php endif; ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-bordered no-mgmt-sticky">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Department</th>
|
||||
<th>Location</th>
|
||||
<th>Type</th>
|
||||
<th>Version</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($templates as $template): ?>
|
||||
<tr>
|
||||
<td><?= esc($template['title']) ?></td>
|
||||
<td><?= esc($template['department'] ?? '') ?></td>
|
||||
<td><?= esc($template['location'] ?? '') ?></td>
|
||||
<td><?= esc($template['employment_type'] ?? '') ?></td>
|
||||
<td><?= esc($template['version']) ?></td>
|
||||
<td><?= !empty($template['is_active']) ? 'Active' : 'Archived' ?></td>
|
||||
<td class="d-flex gap-2">
|
||||
<a class="btn btn-sm btn-outline-primary" href="<?= site_url('administrator/job-postings/templates/' . $template['template_id'] . '/edit') ?>">Edit</a>
|
||||
<a class="btn btn-sm btn-outline-success" href="<?= site_url('administrator/job-postings/positions/new?template_id=' . $template['template_id']) ?>">Create Position</a>
|
||||
<?php if (!empty($template['is_active'])): ?>
|
||||
<form action="<?= site_url('administrator/job-postings/templates/' . $template['template_id'] . '/archive') ?>" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<button class="btn btn-sm btn-outline-danger" type="submit">Archive</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
@@ -0,0 +1,191 @@
|
||||
<?= $this->extend('layout/careers_layout') ?>
|
||||
<?= $this->section('styles') ?>
|
||||
<style>
|
||||
body {
|
||||
background-color: var(--sage);
|
||||
}
|
||||
|
||||
.job-apply-page {
|
||||
max-width: 920px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.job-apply-page h1 {
|
||||
font-family: "Inter", sans-serif;
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
margin-bottom: 2rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.job-apply-page .form-label {
|
||||
font-family: "Heebo", sans-serif;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.job-apply-page .btn-primary {
|
||||
background-color: #198754;
|
||||
border-color: #198754;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.job-apply-page .btn-primary:hover {
|
||||
background-color: #146c43;
|
||||
border-color: #146c43;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.job-apply-page .field-error {
|
||||
min-height: 1.25rem;
|
||||
}
|
||||
</style>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<div class="container py-5">
|
||||
<a href="<?= site_url('careers/' . $position['position_id']) ?>" class="btn btn-outline-secondary btn-sm mb-4">Back to position</a>
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8 job-apply-page">
|
||||
<h1>Apply: <?= esc($position['title']) ?></h1>
|
||||
<?php if (session('error')): ?><div class="alert alert-danger"><?= esc(session('error')) ?></div><?php endif; ?>
|
||||
<?php if (session('errors')): ?>
|
||||
<div class="alert alert-danger">
|
||||
<?php foreach ((array) session('errors') as $error): ?>
|
||||
<div><?= esc($error) ?></div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<p class="text-secondary mb-3">(All fields with * are required)</p>
|
||||
<form action="<?= site_url('careers/' . $position['position_id'] . '/apply') ?>" method="post" enctype="multipart/form-data" class="mt-4">
|
||||
<?= csrf_field() ?>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="first_name">First Name*</label>
|
||||
<input id="first_name" name="first_name" class="form-control" maxlength="30" pattern="[A-Za-z\s-]{2,30}" placeholder="First Name*" title="2-30 characters. Letters, spaces, and dashes only." value="<?= esc(old('first_name')) ?>" required>
|
||||
<div class="form-text text-muted">2-30 characters. Letters, spaces, and dashes only.</div>
|
||||
<div id="first_name-error" class="text-danger small field-error"></div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="last_name">Last Name*</label>
|
||||
<input id="last_name" name="last_name" class="form-control" maxlength="30" pattern="[A-Za-z\s-]{2,30}" placeholder="Last Name*" title="2-30 characters. Letters, spaces, and dashes only." value="<?= esc(old('last_name')) ?>" required>
|
||||
<div class="form-text text-muted">2-30 characters. Letters, spaces, and dashes only.</div>
|
||||
<div id="last_name-error" class="text-danger small field-error"></div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="email">Email*</label>
|
||||
<input id="email" type="email" name="email" class="form-control" maxlength="50" placeholder="Email*" value="<?= esc(old('email')) ?>" required>
|
||||
<div class="form-text text-muted">Valid email format, maximum 50 characters.</div>
|
||||
<div id="email-error" class="text-danger small field-error"></div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="phone">Phone*</label>
|
||||
<input id="phone" type="tel" name="phone" class="form-control" minlength="10" maxlength="20" inputmode="tel" pattern="[\d\s\-\(\)\.]+" placeholder="Phone*" title="Enter a valid 10-digit phone number, for example 123-456-7890" value="<?= esc(old('phone')) ?>" required>
|
||||
<div class="form-text text-muted">Enter a 10-digit phone number, for example 123-456-7890.</div>
|
||||
<div id="phone-error" class="text-danger small field-error"></div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="resume">Resume*</label>
|
||||
<input id="resume" type="file" name="resume" class="form-control" accept=".pdf,.doc,.docx" required>
|
||||
<div class="form-text">PDF, DOC, or DOCX. Maximum size 5 MB.</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary mt-4">Submit Application</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const form = document.querySelector('.job-apply-page form');
|
||||
const fields = {
|
||||
first_name: {
|
||||
element: document.getElementById('first_name'),
|
||||
validate: value => /^[A-Za-z\s-]{2,30}$/.test(value),
|
||||
message: 'First name must be 2-30 characters and use only letters, spaces, and dashes.'
|
||||
},
|
||||
last_name: {
|
||||
element: document.getElementById('last_name'),
|
||||
validate: value => /^[A-Za-z\s-]{2,30}$/.test(value),
|
||||
message: 'Last name must be 2-30 characters and use only letters, spaces, and dashes.'
|
||||
},
|
||||
email: {
|
||||
element: document.getElementById('email'),
|
||||
validate: value => value.length <= 50 && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
|
||||
message: 'Please enter a valid email address, maximum 50 characters.'
|
||||
},
|
||||
phone: {
|
||||
element: document.getElementById('phone'),
|
||||
validate: value => value.replace(/\D/g, '').length === 10,
|
||||
message: 'Please enter a valid 10-digit phone number.'
|
||||
}
|
||||
};
|
||||
|
||||
function setFieldState(input, valid, message) {
|
||||
const error = document.getElementById(input.id + '-error');
|
||||
const hasValue = input.value.trim() !== '';
|
||||
|
||||
input.classList.toggle('is-valid', valid && hasValue);
|
||||
input.classList.toggle('is-invalid', !valid && hasValue);
|
||||
|
||||
if (error) {
|
||||
error.textContent = !valid && hasValue ? message : '';
|
||||
}
|
||||
}
|
||||
|
||||
function formatPhone(input) {
|
||||
const digits = input.value.replace(/\D/g, '').slice(0, 10);
|
||||
let formatted = digits;
|
||||
if (digits.length > 3) {
|
||||
formatted = digits.slice(0, 3) + '-' + digits.slice(3);
|
||||
}
|
||||
if (digits.length > 6) {
|
||||
formatted = digits.slice(0, 3) + '-' + digits.slice(3, 6) + '-' + digits.slice(6);
|
||||
}
|
||||
input.value = formatted;
|
||||
}
|
||||
|
||||
function validateField(name) {
|
||||
const config = fields[name];
|
||||
if (!config || !config.element) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (name === 'phone') {
|
||||
formatPhone(config.element);
|
||||
}
|
||||
|
||||
const value = config.element.value.trim();
|
||||
const valid = config.validate(value);
|
||||
setFieldState(config.element, valid, config.message);
|
||||
return valid;
|
||||
}
|
||||
|
||||
Object.keys(fields).forEach(name => {
|
||||
const input = fields[name].element;
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
|
||||
input.addEventListener('input', () => validateField(name));
|
||||
input.addEventListener('blur', () => validateField(name));
|
||||
});
|
||||
|
||||
if (form) {
|
||||
form.addEventListener('submit', function (event) {
|
||||
const ok = Object.keys(fields).every(validateField);
|
||||
if (!ok) {
|
||||
event.preventDefault();
|
||||
const firstInvalid = form.querySelector('.is-invalid');
|
||||
if (firstInvalid) {
|
||||
firstInvalid.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?= $this->extend('layout/careers_layout') ?>
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<div class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-7">
|
||||
<div class="alert alert-success">Thank you. Your application has been received.</div>
|
||||
<p>We will review your submission and follow up if there is a match for the role.</p>
|
||||
<a href="<?= site_url('careers') ?>" class="btn btn-primary">Return to Openings</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
@@ -0,0 +1,118 @@
|
||||
<?= $this->extend('layout/careers_layout') ?>
|
||||
<?= $this->section('styles') ?>
|
||||
<style>
|
||||
body {
|
||||
background-color: var(--sage);
|
||||
}
|
||||
|
||||
.job-posting-copy,
|
||||
.job-posting-copy p,
|
||||
.job-posting-copy li {
|
||||
font-family: "Heebo", sans-serif;
|
||||
font-size: 1rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.job-posting-copy h2,
|
||||
.job-posting-copy h3 {
|
||||
font-family: "Inter", sans-serif;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<?php
|
||||
$renderPostingText = static function (?string $text): string {
|
||||
$lines = preg_split('/\r\n|\r|\n/', trim((string) $text));
|
||||
$html = '';
|
||||
$paragraph = [];
|
||||
$list = [];
|
||||
|
||||
$flushParagraph = static function () use (&$html, &$paragraph): void {
|
||||
if ($paragraph === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$html .= '<p>' . esc(implode(' ', $paragraph)) . '</p>';
|
||||
$paragraph = [];
|
||||
};
|
||||
|
||||
$flushList = static function () use (&$html, &$list): void {
|
||||
if ($list === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$html .= '<ul>';
|
||||
foreach ($list as $item) {
|
||||
$html .= '<li>' . esc($item) . '</li>';
|
||||
}
|
||||
$html .= '</ul>';
|
||||
$list = [];
|
||||
};
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$line = trim((string) $line);
|
||||
if ($line === '') {
|
||||
$flushParagraph();
|
||||
$flushList();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (str_ends_with($line, ':')) {
|
||||
$flushParagraph();
|
||||
$flushList();
|
||||
$html .= '<h3 class="h4 mt-4">' . esc(rtrim($line, ':')) . '</h3>';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (str_starts_with($line, '- ')) {
|
||||
$flushParagraph();
|
||||
$list[] = substr($line, 2);
|
||||
continue;
|
||||
}
|
||||
|
||||
$flushList();
|
||||
$paragraph[] = $line;
|
||||
}
|
||||
|
||||
$flushParagraph();
|
||||
$flushList();
|
||||
|
||||
return $html;
|
||||
};
|
||||
?>
|
||||
|
||||
<div class="container py-5">
|
||||
<a href="<?= site_url('careers') ?>" class="btn btn-outline-secondary btn-sm mb-4">Back to openings</a>
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-8 job-posting-copy">
|
||||
<h1><?= esc($position['title']) ?></h1>
|
||||
<p class="text-muted">
|
||||
<?= esc($position['department'] ?? '') ?>
|
||||
<?php if (!empty($position['location'])): ?> · <?= esc($position['location']) ?><?php endif; ?>
|
||||
<?php if (!empty($position['employment_type'])): ?> · <?= esc($position['employment_type']) ?><?php endif; ?>
|
||||
<?php if (!empty($position['posted_at'])): ?><br><?= esc(date('M j, Y', strtotime((string) $position['posted_at']))) ?><?php endif; ?>
|
||||
<br><?= esc(number_format((int) ($position['details_click_count'] ?? 0))) ?> views
|
||||
</p>
|
||||
<h2 class="h4 mt-4">Description</h2>
|
||||
<div><?= $renderPostingText($position['description'] ?? '') ?></div>
|
||||
<h2 class="h4 mt-4">Responsibilities</h2>
|
||||
<div><?= $renderPostingText($position['responsibilities'] ?? '') ?></div>
|
||||
<h2 class="h4 mt-4">Requirements</h2>
|
||||
<div><?= $renderPostingText($position['requirements'] ?? '') ?></div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h2 class="h5">Apply for this position</h2>
|
||||
<p class="text-muted">Submit your contact information and resume for review.</p>
|
||||
<a href="<?= site_url('careers/' . $position['position_id'] . '/apply') ?>" class="btn btn-primary w-100">Apply Now</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
@@ -1,5 +1,59 @@
|
||||
<?= $this->extend('layout/main_layout') ?>
|
||||
|
||||
<?= $this->section('styles') ?>
|
||||
<style>
|
||||
.volunteer-openings-modal .modal-header {
|
||||
background: linear-gradient(135deg, #0f766e, #2563eb);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.volunteer-openings-modal .btn-close {
|
||||
filter: invert(1) grayscale(100%) brightness(200%);
|
||||
}
|
||||
|
||||
.volunteer-opening-card {
|
||||
border: 1px solid rgba(15, 23, 42, 0.12);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
background: #fff;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.volunteer-opening-card + .volunteer-opening-card {
|
||||
margin-top: 0.85rem;
|
||||
}
|
||||
|
||||
.volunteer-opening-meta {
|
||||
color: #64748b;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.volunteer-opening-new-badge {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
border-radius: 0 8px 0 8px;
|
||||
background: #fef3c7;
|
||||
color: #14532d;
|
||||
border: 1px solid #facc15;
|
||||
padding: 0.2rem 0.55rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.volunteer-opening-new-badge i {
|
||||
color: #ca8a04;
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
</style>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
<?php $openPositions = $openPositions ?? []; ?>
|
||||
|
||||
<!-- Circle Styles -->
|
||||
|
||||
@@ -40,14 +94,18 @@
|
||||
<ul class="mb-0 ps-3">
|
||||
<?php foreach ($notifications as $n): ?>
|
||||
<li class="mb-2">
|
||||
<strong><?= esc($n['title']) ?></strong><br>
|
||||
<strong><?= esc($n['title'] ?? 'Notification') ?></strong><br>
|
||||
<small class="text-muted">
|
||||
<?= isset($n['created_at']) && strtotime($n['created_at'])
|
||||
? esc(local_datetime($n['created_at'], 'm-d-Y H:i'))
|
||||
: 'No date' ?>
|
||||
</small>
|
||||
<?php if (!empty($n['message'] ?? '')): ?>
|
||||
<br>
|
||||
<span><?= esc($n['message']) ?></span>
|
||||
<?php endif; ?>
|
||||
<br>
|
||||
<span class="badge bg-light text-dark"><?= ucfirst($n['notification_type'] ?? 'broadcast') ?></span>
|
||||
<span class="badge bg-light text-dark"><?= esc(ucfirst((string) ($n['notification_type'] ?? 'notice'))) ?></span>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
@@ -178,4 +236,156 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($openPositions)): ?>
|
||||
<div class="modal fade volunteer-openings-modal" id="volunteerOpeningsModal" tabindex="-1" aria-labelledby="volunteerOpeningsModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<div>
|
||||
<h5 class="modal-title mb-1" id="volunteerOpeningsModalLabel">Volunteer openings</h5>
|
||||
<div class="small opacity-75">Al Rahma Sunday School is looking for help from our community.</div>
|
||||
</div>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="mb-3">
|
||||
Please review the current openings below. If you or someone you know can help, submit an application or share the opportunity.
|
||||
</p>
|
||||
|
||||
<?php foreach ($openPositions as $position): ?>
|
||||
<?php
|
||||
$positionId = (string) ($position['position_id'] ?? '');
|
||||
$detailsUrl = site_url('careers/' . rawurlencode($positionId) . '/details');
|
||||
$applyUrl = site_url('careers/' . rawurlencode($positionId) . '/apply');
|
||||
$meta = array_filter([
|
||||
$position['department'] ?? '',
|
||||
$position['location'] ?? '',
|
||||
$position['employment_type'] ?? '',
|
||||
]);
|
||||
$postedAt = !empty($position['posted_at']) ? strtotime((string) $position['posted_at']) : false;
|
||||
$isNewPosition = $postedAt !== false && $postedAt >= strtotime('-14 days');
|
||||
?>
|
||||
<div class="volunteer-opening-card">
|
||||
<?php if ($isNewPosition): ?>
|
||||
<span class="volunteer-opening-new-badge"><i class="fa fa-star" aria-hidden="true"></i> New</span>
|
||||
<?php endif; ?>
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between gap-3">
|
||||
<div>
|
||||
<h6 class="mb-1"><?= esc($position['title'] ?? 'Volunteer position') ?></h6>
|
||||
<?php if (!empty($meta)): ?>
|
||||
<div class="volunteer-opening-meta mb-2"><?= esc(implode(' | ', $meta)) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($position['description'])): ?>
|
||||
<div class="small text-muted"><?= esc($position['description']) ?></div>
|
||||
<?php endif; ?>
|
||||
<div class="small text-muted fw-semibold mt-2"><?= esc(number_format((int) ($position['details_click_count'] ?? 0))) ?> views</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-start gap-2 flex-shrink-0">
|
||||
<a class="btn btn-outline-secondary btn-sm" href="<?= esc($detailsUrl) ?>">Details</a>
|
||||
<a class="btn btn-primary btn-sm" href="<?= esc($applyUrl) ?>">Apply</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="form-check me-auto text-start">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
value="1"
|
||||
id="hideJobOpeningsPopup"
|
||||
data-save-url="<?= esc(site_url('parent_dashboard/job-openings-popup')) ?>"
|
||||
data-csrf-name="<?= esc(csrf_token()) ?>"
|
||||
data-csrf-value="<?= esc(csrf_hash()) ?>">
|
||||
<label class="form-check-label" for="hideJobOpeningsPopup">
|
||||
Do not show again
|
||||
</label>
|
||||
<div class="small text-muted d-none" id="hideJobOpeningsPopupStatus"></div>
|
||||
</div>
|
||||
<a class="btn btn-outline-secondary" href="<?= site_url('careers') ?>">View all openings</a>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?php if (!empty($openPositions)): ?>
|
||||
<?= $this->section('scripts') ?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const modalEl = document.getElementById('volunteerOpeningsModal');
|
||||
if (!modalEl || !window.bootstrap) {
|
||||
return;
|
||||
}
|
||||
|
||||
bootstrap.Modal.getOrCreateInstance(modalEl).show();
|
||||
|
||||
const checkbox = document.getElementById('hideJobOpeningsPopup');
|
||||
const statusEl = document.getElementById('hideJobOpeningsPopupStatus');
|
||||
if (!checkbox) {
|
||||
return;
|
||||
}
|
||||
|
||||
checkbox.addEventListener('change', async function () {
|
||||
if (!checkbox.checked) {
|
||||
return;
|
||||
}
|
||||
|
||||
checkbox.disabled = true;
|
||||
if (statusEl) {
|
||||
statusEl.classList.remove('d-none', 'text-danger');
|
||||
statusEl.classList.add('text-muted');
|
||||
statusEl.textContent = 'Saving preference...';
|
||||
}
|
||||
|
||||
const body = new FormData();
|
||||
let csrfName = checkbox.dataset.csrfName || '';
|
||||
let csrfValue = checkbox.dataset.csrfValue || '';
|
||||
body.append('hide_job_openings_popup', '1');
|
||||
if (csrfName && csrfValue) {
|
||||
body.append(csrfName, csrfValue);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(checkbox.dataset.saveUrl || '', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
...(csrfValue ? { 'X-CSRF-TOKEN': csrfValue } : {})
|
||||
},
|
||||
body
|
||||
});
|
||||
const data = await response.json().catch(function () {
|
||||
return {};
|
||||
});
|
||||
|
||||
if (data.csrf_token && data.csrf_hash) {
|
||||
checkbox.dataset.csrfName = data.csrf_token;
|
||||
checkbox.dataset.csrfValue = data.csrf_hash;
|
||||
}
|
||||
|
||||
if (!response.ok || !data.ok) {
|
||||
throw new Error(data.error || 'Unable to save preference.');
|
||||
}
|
||||
|
||||
if (statusEl) {
|
||||
statusEl.textContent = 'Saved.';
|
||||
}
|
||||
} catch (error) {
|
||||
checkbox.checked = false;
|
||||
checkbox.disabled = false;
|
||||
if (statusEl) {
|
||||
statusEl.classList.remove('d-none', 'text-muted');
|
||||
statusEl.classList.add('text-danger');
|
||||
statusEl.textContent = error.message || 'Unable to save preference.';
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
<?php endif; ?>
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title><?= esc($title ?? 'Careers | Al Rahma Sunday School') ?></title>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||
<meta content="Volunteer careers and openings at Al Rahma Sunday School" name="description">
|
||||
|
||||
<link href="<?= base_url('assets/images/favicon.ico') ?>" rel="icon">
|
||||
<link href="<?= base_url('assets/boot_css/bootstrap.min.css') ?>" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Amiri:wght@400;700&family=Heebo:wght@400;500;600&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.4.1/font/bootstrap-icons.css" rel="stylesheet">
|
||||
<link href="<?= base_url('css/style.css') ?>" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--ink: #16262B;
|
||||
--ink-soft: #3E4E51;
|
||||
--paper: #F3EFE3;
|
||||
--paper-deep: #E9E3D2;
|
||||
--sage: #E7EEE6;
|
||||
--primary: #0B5D52;
|
||||
--primary-dark: #073F38;
|
||||
--accent: #C6963C;
|
||||
--accent-soft: #EFE1BC;
|
||||
--line: rgba(22, 38, 43, 0.12);
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
color: var(--ink);
|
||||
background-color: var(--paper);
|
||||
overflow-x: hidden;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: 'Amiri', serif;
|
||||
font-weight: 700;
|
||||
color: var(--ink);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
p { color: var(--ink-soft); }
|
||||
|
||||
.navbar {
|
||||
background-color: var(--paper) !important;
|
||||
border-bottom: 1px solid var(--line);
|
||||
padding-top: 0.6rem;
|
||||
padding-bottom: 0.6rem;
|
||||
}
|
||||
|
||||
.navbar .nav-link {
|
||||
color: var(--ink-soft);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.navbar .nav-link:hover { color: var(--primary); }
|
||||
|
||||
.navbar-text {
|
||||
color: var(--ink-soft);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.btn-brand-sm,
|
||||
.btn-brand-outline,
|
||||
.btn-brand-danger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-brand-sm {
|
||||
background-color: var(--primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
padding: 0.6rem 1.25rem;
|
||||
}
|
||||
|
||||
.btn-brand-sm:hover {
|
||||
background-color: var(--primary-dark);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-brand-outline {
|
||||
border: 1.5px solid var(--ink);
|
||||
color: var(--ink);
|
||||
border-radius: 999px;
|
||||
padding: 0.5rem 1.25rem;
|
||||
}
|
||||
|
||||
.btn-brand-outline:hover {
|
||||
background-color: var(--ink);
|
||||
color: var(--paper);
|
||||
}
|
||||
|
||||
.btn-brand-danger {
|
||||
border: 1.5px solid #a33;
|
||||
color: #a33;
|
||||
border-radius: 999px;
|
||||
padding: 0.5rem 1.25rem;
|
||||
}
|
||||
|
||||
.btn-brand-danger:hover {
|
||||
background-color: #a33;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
<?= $this->renderSection('styles') ?>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-light sticky-top px-4 px-lg-5">
|
||||
<a href="<?= base_url('/') ?>" class="navbar-brand d-flex align-items-center">
|
||||
<img src="<?= base_url('images/logo.png') ?>" alt="Al Rahma Sunday School" style="height: 56px; width: 56px; border-radius: 50%; object-fit: contain; background-color: #fff;">
|
||||
</a>
|
||||
<button type="button" class="navbar-toggler" data-bs-toggle="collapse" data-bs-target="#navbarCollapse">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarCollapse">
|
||||
<div class="navbar-nav mx-auto"></div>
|
||||
<div class="d-flex align-items-center">
|
||||
<?php if (session()->get('is_logged_in')): ?>
|
||||
<span class="navbar-text me-3">Welcome, <?= esc(session()->get('user_name')) ?></span>
|
||||
<a href="<?= base_url('/dashboard') ?>" class="btn-brand-outline me-2">Dashboard <i class="fa fa-tachometer-alt"></i></a>
|
||||
<a href="<?= base_url('/logout') ?>" class="btn-brand-danger">Logout <i class="fa fa-sign-out-alt"></i></a>
|
||||
<?php else: ?>
|
||||
<a href="<?= base_url('/login') ?>" class="btn-brand-outline me-2">Login <i class="fa fa-arrow-right"></i></a>
|
||||
<a href="<?= base_url('/register') ?>" class="btn-brand-sm">Register <i class="fa fa-arrow-right"></i></a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
<?= $this->renderSection('content') ?>
|
||||
</main>
|
||||
|
||||
<?php include(__DIR__ . '/../partials/footer.php'); ?>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<?= $this->renderSection('scripts') ?>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -245,6 +245,13 @@
|
||||
background-color: var(--mgmt-thead-bg) !important;
|
||||
box-shadow: 0 1px 0 rgba(0,0,0,0.06);
|
||||
}
|
||||
.content table.no-mgmt-sticky > thead > tr > th,
|
||||
.content table[data-no-mgmt-sticky] > thead > tr > th {
|
||||
position: static !important;
|
||||
top: auto !important;
|
||||
z-index: auto !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
/* Explicitly reset any sticky styles on FullCalendar tables */
|
||||
.content .fc table thead th { position: static !important; top: auto !important; z-index: auto !important; box-shadow: none !important; }
|
||||
/* Month-grid uses container-local stickiness */
|
||||
@@ -483,6 +490,8 @@
|
||||
// Make header sticky by default (opt-out available)
|
||||
if (!(t.classList.contains('no-mgmt-sticky') || t.hasAttribute('data-no-mgmt-sticky'))) {
|
||||
t.classList.add('mgmt-sticky');
|
||||
} else {
|
||||
t.classList.remove('mgmt-sticky');
|
||||
}
|
||||
|
||||
// Avoid double headers when DataTables FixedHeader is active; attach FH if DT is present
|
||||
|
||||
+1129
-347
File diff suppressed because it is too large
Load Diff
@@ -1,21 +0,0 @@
|
||||
<!-- navbar.php -->
|
||||
<nav class="navbar navbar-expand-lg bg-white navbar-light sticky-top px-4 px-lg-5 py-lg-0">
|
||||
<a href="<?= base_url('/') ?>" class="navbar-brand d-flex align-items-center">
|
||||
<img src="<?= base_url('images/logo.png') ?>" alt="Al Rahma Sunday School" class="school-logo-circle" style="height: 40px; width: 40px; object-fit: contain; border-radius: 50%; background-color: #fff;">
|
||||
<h1 class="m-0 ms-2 green-title">Al Rahma Sunday School</h1>
|
||||
</a>
|
||||
<button type="button" class="navbar-toggler" data-bs-toggle="collapse" data-bs-target="#navbarCollapse">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarCollapse">
|
||||
<div class="navbar-nav mx-auto">
|
||||
<a href="<?= base_url('/') ?>" class="nav-item nav-link active">Home</a>
|
||||
<a href="<?= base_url('/about') ?>" class="nav-item nav-link">About Us</a>
|
||||
<a href="<?= base_url('/classes') ?>" class="nav-item nav-link">Classes</a>
|
||||
<a href="<?= base_url('/contact') ?>" class="nav-item nav-link">Contact Us</a>
|
||||
</div>
|
||||
<a href="<?= base_url('/register') ?>" class="btn btn-primary rounded-pill px-3 d-none d-lg-block">Register
|
||||
<i class="fa fa-arrow-right ms-3"></i></a>
|
||||
<a href="<?= base_url('/user/login') ?>" class="btn btn-primary rounded-pill px-3 d-none d-lg-block">Login<i class="fa fa-arrow-right ms-3"></i></a>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -38,6 +38,7 @@
|
||||
<tr>
|
||||
<th>Submitted</th>
|
||||
<th>Status</th>
|
||||
<th>Decision</th>
|
||||
<th>Requested</th>
|
||||
<th>Approved amount</th>
|
||||
<th>Note</th>
|
||||
@@ -45,9 +46,25 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($requests as $row): ?>
|
||||
<?php
|
||||
$status = (string) ($row['status'] ?? '');
|
||||
$decisionLabel = 'Pending review';
|
||||
$decisionClass = 'bg-secondary';
|
||||
if ($status === 'approved') {
|
||||
$decisionLabel = 'Approved';
|
||||
$decisionClass = 'bg-success';
|
||||
} elseif ($status === 'denied') {
|
||||
$decisionLabel = 'Denied';
|
||||
$decisionClass = 'bg-danger';
|
||||
} elseif ($status === 'under_review') {
|
||||
$decisionLabel = 'Under review';
|
||||
$decisionClass = 'bg-info text-dark';
|
||||
}
|
||||
?>
|
||||
<tr>
|
||||
<td><?= esc($row['created_at'] ?? '') ?></td>
|
||||
<td><?= esc($row['status'] ?? '') ?></td>
|
||||
<td><span class="badge <?= esc($decisionClass) ?>"><?= esc($decisionLabel) ?></span></td>
|
||||
<td><?= $row['requested_amount'] !== null && $row['requested_amount'] !== '' ? '$' . number_format((float) $row['requested_amount'], 2) : 'Not specified' ?></td>
|
||||
<td><?= $row['admin_amount'] !== null && $row['admin_amount'] !== '' ? '$' . number_format((float) $row['admin_amount'], 2) : '—' ?></td>
|
||||
<td><?= esc($row['admin_note'] ?? '') ?></td>
|
||||
|
||||
@@ -423,6 +423,6 @@ $closeAtFmt12 = $closeAt->format('m-d-Y g:i A T'); // e.g., 10-01-2025 12:00 AM
|
||||
<script type="module" src="/assets/js/name_validation.js"></script>
|
||||
<script type="module" src="/assets/js/age_validation.js"></script>
|
||||
<script type="module" src="/assets/js/phone_validation.js"></script>
|
||||
<script type="module" src="/assets/js/validate_student.js"></script>
|
||||
<script type="module" src="/assets/js/validate_student.js?v=<?= esc((string) filemtime(FCPATH . 'assets/js/validate_student.js')) ?>"></script>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
@@ -1,26 +1,8 @@
|
||||
<?php
|
||||
$userRole = session()->get('role'); // assuming you store it as 'role'
|
||||
$quickTourUrl = base_url('/help_center'); // default
|
||||
|
||||
switch ($userRole) {
|
||||
case 'parent':
|
||||
$quickTourUrl = base_url('/parent');
|
||||
break;
|
||||
case 'teacher':
|
||||
$quickTourUrl = base_url('/teacher');
|
||||
break;
|
||||
case 'teacher_assistant':
|
||||
$quickTourUrl = base_url('/teacher');
|
||||
break;
|
||||
}
|
||||
?>
|
||||
<link rel="stylesheet" href="<?= base_url('assets/css/landing_page.css') ?>">
|
||||
|
||||
<footer class="footer mt-auto custom-footer text-white py-4">
|
||||
<div class="container">
|
||||
<div class="row align-items-start justify-content-between">
|
||||
<!-- Contact Info -->
|
||||
<div class="col-md-6 col-12 mb-3 mb-md-0 text-md-start">
|
||||
<div class="col-md-6 col-12 mb-3 mb-md-0 text-center text-md-start">
|
||||
<ul class="list-unstyled mb-0 info-list">
|
||||
<li class="info-title"></li>
|
||||
<li><i class="fa fa-map-marker-alt me-2"></i>5 Courthouse Lane, Chelmsford, MA 01824</li>
|
||||
@@ -48,47 +30,50 @@ switch ($userRole) {
|
||||
<li>
|
||||
<a href="/account_creation_guide.pdf" target="_blank" rel="noopener noreferrer"
|
||||
class="pdf-link" data-filename="account_creation_guide.pdf">
|
||||
<i class="fas fa-file-pdf"></i>How To Create An Account
|
||||
<i class="fas fa-file-pdf me-1"></i>How To Create An Account
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<p class="text-center text-white">© 2026 Al Rahma Sunday School by ISGL. All Rights Reserved.</p>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<p class="rights-line">© 2026 Al Rahma Sunday School by ISGL. All Rights Reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Check if PDF files exist and add error handling
|
||||
document.querySelectorAll('.pdf-link').forEach(link => {
|
||||
const pdfUrl = link.getAttribute('href');
|
||||
<style>
|
||||
.custom-footer {
|
||||
background-color: var(--ink, #16262B);
|
||||
color: #CFC9B7;
|
||||
}
|
||||
|
||||
// Test if the PDF exists
|
||||
fetch(pdfUrl, {
|
||||
method: 'HEAD'
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
// File doesn't exist or is inaccessible
|
||||
link.style.opacity = '0.7';
|
||||
link.title = 'File might be temporarily unavailable';
|
||||
console.warn('PDF might be missing:', pdfUrl);
|
||||
.custom-footer .info-list li {
|
||||
margin-bottom: 0.5rem;
|
||||
color: #CFC9B7;
|
||||
}
|
||||
|
||||
// Modify click behavior to show helpful message
|
||||
link.addEventListener('click', function(e) {
|
||||
if (!confirm('The PDF file might be temporarily unavailable. Try to open it anyway?')) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error checking PDF:', pdfUrl, error);
|
||||
link.style.opacity = '0.7';
|
||||
link.title = 'File check failed';
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
.custom-footer .info-list i {
|
||||
color: var(--accent, #C6963C);
|
||||
}
|
||||
|
||||
.custom-footer .pdf-link {
|
||||
color: #CFC9B7;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.custom-footer .pdf-link:hover {
|
||||
color: var(--accent, #C6963C);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.custom-footer .rights-line {
|
||||
text-align: center;
|
||||
color: #7FBFA0;
|
||||
margin: 1.5rem auto 0;
|
||||
max-width: none;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -58,6 +58,7 @@ $role = strtolower(session()->get('role') ?? 'guest');
|
||||
</a>
|
||||
<div class="dropdown-menu" aria-labelledby="parentsManagementDropdown">
|
||||
<a class="dropdown-item" href="/staff/index">Staff Profile</a>
|
||||
<a class="dropdown-item" href="<?= site_url('administrator/job-postings/positions') ?>">Volunteer Positions</a>
|
||||
<a class="dropdown-item" href="/administrator/teacher_class_assignment">Teacher Class Assignment</a>
|
||||
</div>
|
||||
</li>
|
||||
@@ -190,6 +191,9 @@ $role = strtolower(session()->get('role') ?? 'guest');
|
||||
</a>
|
||||
<div class="dropdown-menu" aria-labelledby="parentsManagementDropdown">
|
||||
<a class="dropdown-item" href="/staff/index">Staff Profile</a>
|
||||
<?php if ($role === 'principal'): ?>
|
||||
<a class="dropdown-item" href="<?= site_url('administrator/job-postings/positions') ?>">Volunteer Positions</a>
|
||||
<?php endif; ?>
|
||||
<a class="dropdown-item" href="/administrator/teacher_class_assignment">Teacher Class Assignment</a>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
<div class="student-form border rounded p-3 mb-4 bg-light position-relative">
|
||||
<?php
|
||||
$publicGradeOptions = [
|
||||
'NA' => 'Not enrolled in public school',
|
||||
'KG' => 'Pre-K / Kindergarten',
|
||||
'1' => 'Grade 1',
|
||||
'2' => 'Grade 2',
|
||||
'3' => 'Grade 3',
|
||||
'4' => 'Grade 4',
|
||||
'5' => 'Grade 5',
|
||||
'6' => 'Grade 6',
|
||||
'7' => 'Grade 7',
|
||||
'8' => 'Grade 8',
|
||||
'9' => 'Grade 9',
|
||||
'10' => 'Grade 10',
|
||||
'11' => 'Grade 11',
|
||||
'12' => 'Grade 12',
|
||||
];
|
||||
?>
|
||||
<button type="button" class="btn-close position-absolute end-0 top-0 m-2 js-remove-student" aria-label="Close"></button>
|
||||
<h5 class="mb-3 text-primary">Student Information</h5>
|
||||
<div class="col-md-12 mb-3">
|
||||
<label class="form-label fw-bold mb-2 d-block">
|
||||
Was your child enrolled in Al Rahma Sunday School last year? <span class="text-danger">*</span>
|
||||
</label>
|
||||
|
||||
<div class="d-flex align-items-center flex-wrap gap-3">
|
||||
<div class="form-check ps-3">
|
||||
<input class="form-check-input" type="radio" name="last_year_0" value="yes" data-base-name="last_year" required autocomplete="off">
|
||||
<label class="form-check-label">Yes</label>
|
||||
</div>
|
||||
<div class="form-check ps-3">
|
||||
<input class="form-check-input" type="radio" name="last_year_0" value="no" data-base-name="last_year" required autocomplete="off">
|
||||
<label class="form-check-label">No</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="last_year_0" value="no" data-last-year-default>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">First Name <span class="text-danger">*</span></label>
|
||||
@@ -50,12 +53,14 @@
|
||||
<small class="text-danger age-error d-none">Student must be between 5 and 18 years old.</small>
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<label class="form-label grade-label">Last Year Grade <span class="text-danger">*</span></label>
|
||||
<label class="form-label grade-label">Public School Last Year Grade <span class="text-danger">*</span></label>
|
||||
<select class="form-select dynamic-grade grade-select"
|
||||
name="registration_grade[]"
|
||||
required data-base-name="registration_grade"
|
||||
disabled>
|
||||
required data-base-name="registration_grade">
|
||||
<option value="">Select Grade</option>
|
||||
<?php foreach ($publicGradeOptions as $value => $label): ?>
|
||||
<option value="<?= esc($value) ?>"><?= esc($label) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -231,46 +236,6 @@
|
||||
value: "12",
|
||||
text: "Grade 12"
|
||||
}
|
||||
],
|
||||
alrahma: [{
|
||||
value: "KG",
|
||||
text: "Pre-K / Kindergarten"
|
||||
},
|
||||
{
|
||||
value: "1",
|
||||
text: "Grade 1"
|
||||
}, {
|
||||
value: "2",
|
||||
text: "Grade 2"
|
||||
},
|
||||
{
|
||||
value: "3",
|
||||
text: "Grade 3"
|
||||
}, {
|
||||
value: "4",
|
||||
text: "Grade 4"
|
||||
},
|
||||
{
|
||||
value: "5",
|
||||
text: "Grade 5"
|
||||
}, {
|
||||
value: "6",
|
||||
text: "Grade 6"
|
||||
},
|
||||
{
|
||||
value: "7",
|
||||
text: "Grade 7"
|
||||
}, {
|
||||
value: "8",
|
||||
text: "Grade 8"
|
||||
},
|
||||
{
|
||||
value: "9",
|
||||
text: "Grade 9"
|
||||
}, {
|
||||
value: "10",
|
||||
text: "Youth"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
@@ -278,8 +243,9 @@
|
||||
if (!gradeSelect || !gradeLabel) return;
|
||||
gradeSelect.innerHTML = '<option value="">Select Grade</option>';
|
||||
gradeSelect.value = '';
|
||||
gradeSelect.disabled = true; // keep disabled until a radio is chosen
|
||||
gradeLabel.innerHTML = 'Last Year Grade <span class="text-danger">*</span>';
|
||||
gradeSelect.disabled = false;
|
||||
gradeLabel.innerHTML = 'Public School Last Year Grade <span class="text-danger">*</span>';
|
||||
populateSelect(gradeSelect, gradeOptions.public);
|
||||
}
|
||||
|
||||
function populateSelect(gradeSelect, options) {
|
||||
@@ -292,50 +258,22 @@
|
||||
});
|
||||
}
|
||||
|
||||
function updateGradeOptions(studentForm, type) {
|
||||
function updateGradeOptions(studentForm) {
|
||||
const gradeSelect = studentForm.querySelector('.grade-select');
|
||||
const gradeLabel = studentForm.querySelector('.grade-label');
|
||||
if (!gradeSelect || !gradeLabel) return;
|
||||
|
||||
gradeSelect.disabled = false; // enable now that a choice was made
|
||||
gradeSelect.disabled = false;
|
||||
gradeSelect.value = ''; // clear any stale selection
|
||||
|
||||
if (type === 'yes') {
|
||||
gradeLabel.innerHTML = 'Al Rahma School Last Year Grade <span class="text-danger">*</span>';
|
||||
populateSelect(gradeSelect, gradeOptions.alrahma);
|
||||
} else if (type === 'no') {
|
||||
gradeLabel.innerHTML = 'Public School Last Year Grade <span class="text-danger">*</span>';
|
||||
populateSelect(gradeSelect, gradeOptions.public);
|
||||
} else {
|
||||
resetGradeSelect(gradeSelect, gradeLabel);
|
||||
}
|
||||
gradeLabel.innerHTML = 'Public School Last Year Grade <span class="text-danger">*</span>';
|
||||
populateSelect(gradeSelect, gradeOptions.public);
|
||||
}
|
||||
|
||||
// Event: when a radio is chosen, update this form's select
|
||||
document.addEventListener('change', function(e) {
|
||||
if (e.target.matches('input[type="radio"][data-base-name="last_year"]')) {
|
||||
const form = e.target.closest('.student-form');
|
||||
if (form && e.target.checked) {
|
||||
updateGradeOptions(form, e.target.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// On load: enforce disabled unless a radio is already checked (server repopulation)
|
||||
// On load: ensure grade dropdown is populated.
|
||||
(function initStudentForms() {
|
||||
const forms = document.querySelectorAll('.student-form');
|
||||
forms.forEach(form => {
|
||||
const gradeSelect = form.querySelector('.grade-select');
|
||||
const gradeLabel = form.querySelector('.grade-label');
|
||||
|
||||
// Start disabled (HTML already has disabled, but enforce from JS too)
|
||||
resetGradeSelect(gradeSelect, gradeLabel);
|
||||
|
||||
// If a radio is already checked (e.g., after validation error), enable/populate accordingly
|
||||
const checked = form.querySelector('input[type="radio"][data-base-name="last_year"]:checked');
|
||||
if (checked) {
|
||||
updateGradeOptions(form, checked.value);
|
||||
}
|
||||
updateGradeOptions(form);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
<?php
|
||||
/** @var array<int,array> $payments */
|
||||
?>
|
||||
|
||||
<?= $this->extend('layout/management_layout') ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
<div class="container-fluid py-3">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<h1 class="h4 m-0">Payments</h1>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Invoice #</th>
|
||||
<th class="text-end">Paid Amount</th>
|
||||
<th class="text-end">Invoice Balance</th>
|
||||
<th>Method</th>
|
||||
<th>Payment Status</th>
|
||||
<th>Invoice Status</th>
|
||||
<th>School Year</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($payments)): ?>
|
||||
<tr>
|
||||
<td colspan="8" class="text-center text-muted py-4">No payment</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($payments as $p): ?>
|
||||
<tr>
|
||||
<td><?= esc(!empty($p['payment_date']) ? local_date($p['payment_date'], 'm-d-Y') : '') ?></td>
|
||||
<td><?= esc($p['invoice_number'] ?? ('#' . (int)($p['invoice_id'] ?? 0))) ?></td>
|
||||
<td class="text-end">$<?= number_format((float)($p['paid_amount'] ?? 0), 2) ?></td>
|
||||
<td class="text-end">$<?= number_format((float)($p['invoice_current_balance'] ?? 0), 2) ?></td>
|
||||
<td><?= esc($p['payment_method'] ?? '') ?></td>
|
||||
<td><?= esc($p['payment_status'] ?? '') ?></td>
|
||||
<td><?= esc($p['invoice_status'] ?? '') ?></td>
|
||||
<td><?= esc($p['school_year'] ?? '') ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?= $this->endSection() ?>
|
||||
@@ -28,7 +28,7 @@ return [
|
||||
[
|
||||
'title' => 'School Calendar (2026-2027)',
|
||||
'body' => '<table border="1" cellspacing="0" cellpadding="8" style="border-collapse: collapse; text-align: left; width: 100%;"><thead><tr><th>Date</th><th>Event</th></tr></thead><tbody><tr><td>09/13/2026</td><td>Make-up Exams / Staff Orientation</td></tr><tr><td>09/20/2026</td><td>First Day of School</td></tr><tr><td>10/05/2026</td><td>Last Day of Registration / Enrollment</td></tr><tr><td>11/29/2026</td><td>November Break</td></tr><tr><td>12/20/2026</td><td>Winter Break 1</td></tr><tr><td>12/27/2026</td><td>Winter Break 2</td></tr><tr><td>01/17/2027</td><td>Midterm Exam</td></tr><tr><td>02/21/2027</td><td>February Break</td></tr><tr><td>02/28/2027</td><td>Ramadan Last 10 Break</td></tr><tr><td>03/07/2027</td><td>Eid Al-Fitr Break</td></tr><tr><td>04/25/2027</td><td>April Break</td></tr><tr><td>05/16/2027</td><td>Eid Al-Adha Break</td></tr><tr><td>05/23/2027</td><td>Final Exam</td></tr><tr><td>05/30/2027</td><td>May Break</td></tr><tr><td>06/06/2027</td><td>Ceremony Day / Last Day of School</td></tr></tbody></table>'
|
||||
. $p('Note: The school will stay committed to this calendar so that parents and students can plan their year accordingly. In rare circumstances, the schedule may change due to unforeseen events, in which case parents will be notified immediately. If school is canceled due to inclement weather or any other reason, Al Rahma School will send a notification email to parents to inform them of the date and the reason for cancelation.'),
|
||||
. $p('Note: The school will stay committed to this calendar so that parents and students can plan their year accordingly. In rare circumstances, the schedule may change due to unforeseen events, in which case parents will be notified immediately. If school is canceled due to inclement weather or any other reason, Al Rahma School will send a notification email to parents to inform them of the date and the reason for cancellation.'),
|
||||
],
|
||||
[
|
||||
'title' => 'Registration',
|
||||
@@ -67,7 +67,7 @@ return [
|
||||
'title' => 'Withdrawals/Refunds',
|
||||
'body' => $p(
|
||||
'Parents can formally initiate a withdrawal request from school through their parent portal. Withdrawal requests are only available after enrollment is complete and must be completed online. The administration reviews the withdrawal request soon after and the affected students are removed from their assigned classes after the withdrawal is approved. The students get to keep all books they received previously from the school, and a new online invoice is generated.',
|
||||
'Refunds are pro-rated. All weeks preceding the formal withdrawal request count as attended by the student. The refund covers all school weeks after the request but subtracts any book costs incurred. Any balance left off will be settled promptly; the parent will be asked to pay the remaining balance if they end up owing money, or the school will issue a check with the final refund amount if the school ends up owing money. No refund will be issued to parents whose kids(s) has/have been expelled from the school, refunds are for voluntary withdrawals only.',
|
||||
'Refunds are pro-rated. All weeks preceding the formal withdrawal request count as attended by the student. The refund covers all school weeks after the request but subtracts any book costs incurred. Any remaining balance will be settled promptly; the parent will be asked to pay the remaining balance if they end up owing money, or the school will issue a check with the final refund amount if the school ends up owing money. No refund will be issued to parents whose kid(s) has/have been expelled from the school, refunds are for voluntary withdrawals only.',
|
||||
'Note that initiating a withdrawal is a serious decision and non-reversible. Once the withdrawal is complete, re-enrollment for the current year will not be possible. Future enrollments in subsequent years might be denied as well depending on the case.'
|
||||
),
|
||||
],
|
||||
|
||||
@@ -7,10 +7,17 @@ $formatRole = function (?string $role): string {
|
||||
$role = str_replace(['-', '_'], ' ', $role);
|
||||
$role = preg_replace('/\s+/', ' ', trim($role));
|
||||
if ($role === '') return '';
|
||||
$special = [
|
||||
'of' => 'of',
|
||||
'head' => 'Head',
|
||||
];
|
||||
$out = [];
|
||||
foreach (explode(' ', $role) as $w) {
|
||||
if ($w === '') continue;
|
||||
if (preg_match('/^[A-Za-z]{1,3}$/', $w)) {
|
||||
$lower = strtolower($w);
|
||||
if (isset($special[$lower])) {
|
||||
$out[] = $special[$lower];
|
||||
} elseif (preg_match('/^[A-Za-z]{1,3}$/', $w)) {
|
||||
$out[] = strtoupper($w); // TA, PTA, HR, KG...
|
||||
} else {
|
||||
$out[] = ucfirst(strtolower($w)); // Teacher, Assistant, Admin...
|
||||
@@ -59,14 +66,23 @@ $formatRole = function (?string $role): string {
|
||||
<?php $order = 1; ?>
|
||||
<?php foreach ($users as $user): ?>
|
||||
<?php
|
||||
// Pick a representative role (first of CSV or role_name/active_role)
|
||||
$roleRaw = $user['active_role'] ?? $user['role_name'] ?? ($user['roles'] ?? '');
|
||||
if (strpos((string)$roleRaw, ',') !== false) {
|
||||
$parts = array_filter(array_map('trim', explode(',', (string)$roleRaw)));
|
||||
$roleRaw = $parts[0] ?? '';
|
||||
$rolesSource = $user['roles_raw'] ?? ($user['roles'] ?? ($user['role_name_raw'] ?? ($user['role_name'] ?? '')));
|
||||
$roleOptions = [];
|
||||
foreach (array_filter(array_map('trim', explode(',', (string)$rolesSource)), 'strlen') as $roleOptionRaw) {
|
||||
$roleOptionLabel = $formatRole($roleOptionRaw);
|
||||
if ($roleOptionLabel !== '' && !in_array($roleOptionLabel, $roleOptions, true)) {
|
||||
$roleOptions[] = $roleOptionLabel;
|
||||
}
|
||||
}
|
||||
//$roleLabel = $roleRaw !== '' ? $formatRole($roleRaw) : '-';
|
||||
$roleLabel = $user['role_name'] ?? ($user['roles'] ?? '-'); // both are pre-formatted by $formatRole
|
||||
|
||||
if (empty($roleOptions)) {
|
||||
$fallbackRole = $formatRole((string)($user['role_name_raw'] ?? ($user['role_name'] ?? '')));
|
||||
if ($fallbackRole !== '') {
|
||||
$roleOptions[] = $fallbackRole;
|
||||
}
|
||||
}
|
||||
|
||||
$roleLabel = $roleOptions[0] ?? '-';
|
||||
|
||||
// Normalize user id key for checkbox
|
||||
$uid = $user['user_id'] ?? $user['id'] ?? ($user['users.id'] ?? null);
|
||||
@@ -81,7 +97,17 @@ $formatRole = function (?string $role): string {
|
||||
<td style="text-align: center;"><?= esc($order++) ?></td>
|
||||
<td><?= esc($user['firstname'] ?? '') ?></td>
|
||||
<td><?= esc($user['lastname'] ?? '') ?></td>
|
||||
<td><?= esc($roleLabel) ?></td>
|
||||
<td>
|
||||
<?php if (count($roleOptions) > 1): ?>
|
||||
<select class="form-select form-select-sm role-select" aria-label="Badge role for <?= esc(trim(($user['firstname'] ?? '') . ' ' . ($user['lastname'] ?? ''))) ?>">
|
||||
<?php foreach ($roleOptions as $roleOption): ?>
|
||||
<option value="<?= esc($roleOption) ?>"><?= esc($roleOption) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php else: ?>
|
||||
<?= esc($roleLabel) ?>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= $className !== '' ? esc($className) : '-' ?></td>
|
||||
<td><span class="badge bg-secondary prints-badge" data-user-id="<?= esc($uid ?? '') ?>">—</span></td>
|
||||
<td>
|
||||
@@ -235,11 +261,20 @@ $formatRole = function (?string $role): string {
|
||||
// Delegate checkbox change handling once at the table level (works across redraws)
|
||||
document.getElementById('staffTable').addEventListener('change', function (e) {
|
||||
const target = e.target;
|
||||
if (!target || !target.classList.contains('user-checkbox')) return;
|
||||
if (!target) return;
|
||||
const tr = target.closest('tr[data-user-id]');
|
||||
const userId = tr ? tr.getAttribute('data-user-id') : null;
|
||||
if (!userId) return;
|
||||
if (target.checked) selected.add(userId); else selected.delete(userId);
|
||||
|
||||
if (target.classList.contains('role-select')) {
|
||||
rowMeta[userId] = rowMeta[userId] || { role: '', className: '' };
|
||||
rowMeta[userId].role = target.value || '';
|
||||
} else if (target.classList.contains('user-checkbox')) {
|
||||
if (target.checked) selected.add(userId); else selected.delete(userId);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
syncHiddenInputs();
|
||||
syncPageCheckboxes();
|
||||
// If user interacts, cancel any pending auto-clear to avoid wiping new selections
|
||||
@@ -341,4 +376,4 @@ $formatRole = function (?string $role): string {
|
||||
window.addEventListener('focus', fetchPrintStatus);
|
||||
});
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
<!-- spinner.php -->
|
||||
<div id="spinner" class="show bg-white position-fixed translate-middle w-100 vh-100 top-50 start-50 d-flex align-items-center justify-content-center">
|
||||
<div class="spinner-border text-primary" style="width: 3rem; height: 3rem;" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,78 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Support</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Favicon -->
|
||||
<link href="<?= base_url('assets/images/favicon.ico') ?>" rel="icon">
|
||||
<style>
|
||||
.dashboard-title {
|
||||
border-bottom: 4px solid #007bff;
|
||||
padding-bottom: 10px;
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.pt-3.pb-2.mb-3 {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<?php include(__DIR__ . '/partials/header.php'); ?>
|
||||
<?php include(__DIR__ . '/partials/navbar.php'); ?>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<main class="col-12 px-md-4">
|
||||
<div class="pt-3 pb-2 mb-3 dashboard-title">
|
||||
<h1 class="h2">Support</h1>
|
||||
<h5 class="h5 mt-2">Submit your support request</h5>
|
||||
</div>
|
||||
|
||||
<?php if (session()->getFlashdata('success')): ?>
|
||||
<div class="alert alert-success">
|
||||
<?= session()->getFlashdata('success') ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (isset($validation)): ?>
|
||||
<div class="alert alert-danger">
|
||||
<?= $validation->listErrors() ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php helper(['form']); // Load form helper
|
||||
?>
|
||||
|
||||
<form action="<?= base_url('/support/submit') ?>" method="post">
|
||||
<div class="form-group">
|
||||
<label for="subject">Subject</label>
|
||||
<input type="text" class="form-control" id="subject" name="subject"
|
||||
value="<?= set_value('subject') ?>" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="message">Message</label>
|
||||
<textarea class="form-control" id="message" name="message" rows="5"
|
||||
required><?= set_value('message') ?></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary mt-3">Submit</button>
|
||||
</form>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php include(__DIR__ . '/partials/footer.php'); ?>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,60 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>My Support Requests</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link href="<?= base_url('assets/images/favicon.ico') ?>" rel="icon">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<?php include(__DIR__ . '/partials/header.php'); ?>
|
||||
<?php include(__DIR__ . '/partials/navbar.php'); ?>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<main class="col-md-9 ml-sm-auto col-lg-10 px-md-4">
|
||||
<div class="pt-3 pb-2 mb-3" style="margin-left: -2in;">
|
||||
<h1 class="h2">My Support Requests</h1>
|
||||
<h5 class="h5 mt-2">View your submitted support requests</h5>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Subject</th>
|
||||
<th>Message</th>
|
||||
<th>Status</th>
|
||||
<th>Created At</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (!empty($requests) && is_array($requests)) : ?>
|
||||
<?php foreach ($requests as $request) : ?>
|
||||
<tr>
|
||||
<td><?= esc($request['subject']); ?></td>
|
||||
<td><?= esc($request['message']); ?></td>
|
||||
<td><?= esc($request['status']); ?></td>
|
||||
<td><?= esc(!empty($request['created_at']) ? local_datetime($request['created_at'], 'm-d-Y H:i') : '') ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else : ?>
|
||||
<tr>
|
||||
<td colspan="4">No support requests found.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php include(__DIR__ . '/partials/footer.php'); ?>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,34 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>API Docs — Swagger UI</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css" />
|
||||
<style>
|
||||
body { margin: 0; background: #f7f7f7; }
|
||||
.topbar { display:none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="swagger-ui"></div>
|
||||
|
||||
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
const specs = <?php echo json_encode($specs ?? [], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>;
|
||||
window.ui = SwaggerUIBundle({
|
||||
dom_id: '#swagger-ui',
|
||||
urls: specs,
|
||||
urlsPrimaryName: (specs[0] && specs[0].name) || undefined,
|
||||
deepLinking: true,
|
||||
docExpansion: 'list',
|
||||
filter: true,
|
||||
layout: 'BaseLayout',
|
||||
presets: [SwaggerUIBundle.presets.apis],
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,203 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Al Rahma Sunday School</title>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||
<meta content="" name="keywords">
|
||||
<meta content="" name="description">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link href="<?= base_url('assets/images/favicon.ico') ?>" rel="icon">
|
||||
|
||||
<!-- Google Web Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Heebo:wght@400;500;600&family=Inter:wght@600&family=Lobster+Two:wght@700&display=swap"
|
||||
rel="stylesheet">
|
||||
|
||||
<!-- Icon Font Stylesheet -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.10.0/css/all.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.4.1/font/bootstrap-icons.css" rel="stylesheet">
|
||||
|
||||
<!-- Libraries Stylesheet -->
|
||||
<link href="lib/animate/animate.min.css" rel="stylesheet">
|
||||
<link href="lib/owlcarousel/assets/owl.carousel.min.css" rel="stylesheet">
|
||||
|
||||
<!-- Customized Bootstrap Stylesheet -->
|
||||
<link href="css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
<!-- Template Stylesheet -->
|
||||
<link href="assets/css/style.css" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container-xxl bg-white p-0">
|
||||
<!-- Spinner Start -->
|
||||
<div id="spinner"
|
||||
class="show bg-white position-fixed translate-middle w-100 vh-100 top-50 start-50 d-flex align-items-center justify-content-center">
|
||||
<div class="spinner-border text-primary" style="width: 3rem; height: 3rem;" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Spinner End -->
|
||||
|
||||
|
||||
<!-- Navbar Start -->
|
||||
<nav class="navbar navbar-expand-lg bg-white navbar-light sticky-top px-4 px-lg-5 py-lg-0">
|
||||
<a href="<?= base_url('/') ?>" class="navbar-brand d-flex align-items-center">
|
||||
<img src="<?= base_url('images/logo.png') ?>" alt="Al Rahma Sunday School" style="height: 40px; width: 40px; border-radius: 50%; object-fit: contain; background-color: #fff;">
|
||||
<h1 class="m-0 ms-2 green-title">Al Rahma Sunday School</h1>
|
||||
</a>
|
||||
<button type="button" class="navbar-toggler" data-bs-toggle="collapse" data-bs-target="#navbarCollapse">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarCollapse">
|
||||
<div class="navbar-nav mx-auto">
|
||||
<a href="<?= base_url('/') ?>" class="nav-item nav-link">Home</a>
|
||||
<a href="<?= base_url('/about') ?>" class="nav-item nav-link">About Us</a>
|
||||
<a href="<?= base_url('/classes') ?>" class="nav-item nav-link">Classes</a>
|
||||
<div class="nav-item dropdown">
|
||||
<a href="#" class="nav-link dropdown-toggle active" data-bs-toggle="dropdown">Pages</a>
|
||||
<div class="dropdown-menu rounded-0 rounded-bottom border-0 shadow-sm m-0">
|
||||
<a href="<?= base_url('/facility') ?>" class="dropdown-item">School Facilities</a>
|
||||
<a href="team.html" class="dropdown-item active">Popular Teachers</a>
|
||||
<a href="<?= base_url('/call-to-action') ?>" class="dropdown-item">Become A Teacher or Admins</a>
|
||||
<a href="appointment.html" class="dropdown-item">Make Appointment</a>
|
||||
<a href="<?= base_url('/testimonial') ?>" class="dropdown-item">Testimonial</a>
|
||||
<a href="<?= base_url('/notFound') ?>" class="dropdown-item">notFound Error</a>
|
||||
</div>
|
||||
</div>
|
||||
<a href="<?= base_url('/contact') ?>" class="nav-item nav-link">Contact Us</a>
|
||||
</div>
|
||||
<a href="/register" class="btn btn-primary rounded-pill px-3 d-none d-lg-block">Register
|
||||
<i class="fa fa-arrow-right ms-3"></i></a>
|
||||
<a href="/login" class="btn btn-primary rounded-pill px-3 d-none d-lg-block">Login<i
|
||||
class="fa fa-arrow-right ms-3"></i></a>
|
||||
|
||||
</div>
|
||||
</nav>
|
||||
<!-- Navbar End -->
|
||||
|
||||
|
||||
<!-- Page Header End -->
|
||||
<div class="container-xxl py-5 page-header position-relative mb-5">
|
||||
<div class="container py-5">
|
||||
<h1 class="display-2 text-white animated slideInDown mb-4">Teachers</h1>
|
||||
<nav aria-label="breadcrumb animated slideInDown">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="/">Home</a></li>
|
||||
<li class="breadcrumb-item"><a href="#">Pages</a></li>
|
||||
<li class="breadcrumb-item text-white active" aria-current="page">Teachers</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Page Header End -->
|
||||
|
||||
<!-- Footer Start -->
|
||||
<div class="container-fluid bg-dark text-white-50 footer pt-5 mt-5 wow fadeIn" data-wow-delay="0.1s">
|
||||
<div class="container py-5">
|
||||
<div class="row g-5">
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<h3 class="text-white mb-4">Get In Touch</h3>
|
||||
<p class="mb-2"><i class="fa fa-map-marker-alt me-3"></i>5 Courthouse Lane, Chelmsford, MA 01824
|
||||
</p>
|
||||
<p class="mb-2"><i class="fa fa-phone-alt me-3"></i>+1 978-364-0219
|
||||
0</p>
|
||||
<p class="mb-2"><i class="fa fa-envelope me-3"></i>alrahma.isgl@gmail.com
|
||||
</p>
|
||||
<div class="d-flex pt-2">
|
||||
<a class="btn btn-outline-light btn-social" href=""><i class="fab fa-twitter"></i></a>
|
||||
<a class="btn btn-outline-light btn-social" href=""><i class="fab fa-facebook-f"></i></a>
|
||||
<a class="btn btn-outline-light btn-social" href=""><i class="fab fa-youtube"></i></a>
|
||||
<a class="btn btn-outline-light btn-social" href=""><i class="fab fa-linkedin-in"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<h3 class="text-white mb-4">Quick Links</h3>
|
||||
<a class="btn btn-link text-white-50" href="">About Us</a>
|
||||
<a class="btn btn-link text-white-50" href="">Contact Us</a>
|
||||
<a class="btn btn-link text-white-50" href="">Our Services</a>
|
||||
<a class="btn btn-link text-white-50" href="">Privacy Policy</a>
|
||||
<a class="btn btn-link text-white-50" href="">Terms & Condition</a>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<h3 class="text-white mb-4">Photo Gallery</h3>
|
||||
<div class="row g-2 pt-2">
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-1.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-2.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-3.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-4.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-5.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-6.jpg') ?>" alt="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<h3 class="text-white mb-4">Newsletter</h3>
|
||||
<p>Our newsletter is your gateway to staying informed and connected with our Sunday school community. </p>
|
||||
<div class="position-relative mx-auto" style="max-width: 400px;">
|
||||
<input class="form-control bg-transparent w-100 py-3 ps-4 pe-5" type="text"
|
||||
placeholder="Your email">
|
||||
<button type="button"
|
||||
class="btn btn-primary py-2 position-absolute top-0 end-0 mt-2 me-2">SignUp</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="copyright">
|
||||
<div class="row">
|
||||
<div class="col-md-6 text-center text-md-start mb-3 mb-md-0">
|
||||
© <a class="border-bottom" href="#">Al Rahma Sunday School by ISGL</a>, All Right
|
||||
Reserved.
|
||||
|
||||
<!--/*** This template is free as long as you keep the footer author’s credit link/attribution link/backlink. If you'd like to use the template without the footer author’s credit link/attribution link/backlink, you can purchase the Credit Removal License from "https://htmlcodex.com/credit-removal". Thank you for your support. ***/-->
|
||||
Designed By <a class="border-bottom" href="https://htmlcodex.com">HTML Codex</a>
|
||||
</div>
|
||||
<div class="col-md-6 text-center text-md-end">
|
||||
<div class="footer-menu">
|
||||
<a href="">Home</a>
|
||||
<a href="">Cookies</a>
|
||||
<a href="">Help</a>
|
||||
<a href="">FQAs</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Footer End -->
|
||||
|
||||
|
||||
<!-- Back to Top -->
|
||||
<a href="#" class="btn btn-lg btn-primary btn-lg-square back-to-top"><i class="bi bi-arrow-up"></i></a>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript Libraries -->
|
||||
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="lib/wow/wow.min.js"></script>
|
||||
<script src="lib/easing/easing.min.js"></script>
|
||||
<script src="lib/waypoints/waypoints.min.js"></script>
|
||||
<script src="lib/owlcarousel/owl.carousel.min.js"></script>
|
||||
|
||||
<!-- Template Javascript -->
|
||||
<script src="js/main.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,255 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Al Rahma Sunday School</title>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||
<meta content="" name="keywords">
|
||||
<meta content="" name="description">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link href="<?= base_url('assets/images/favicon.ico') ?>" rel="icon">
|
||||
|
||||
<!-- Google Web Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Heebo:wght@400;500;600&family=Inter:wght@600&family=Lobster+Two:wght@700&display=swap"
|
||||
rel="stylesheet">
|
||||
|
||||
<!-- Icon Font Stylesheet -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.10.0/css/all.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.4.1/font/bootstrap-icons.css" rel="stylesheet">
|
||||
|
||||
<!-- Libraries Stylesheet -->
|
||||
<link href="lib/animate/animate.min.css" rel="stylesheet">
|
||||
<link href="lib/owlcarousel/assets/owl.carousel.min.css" rel="stylesheet">
|
||||
|
||||
<!-- Customized Bootstrap Stylesheet -->
|
||||
<link href="css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
<!-- Template Stylesheet -->
|
||||
<link href="assets/css/style.css" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container-xxl bg-white p-0">
|
||||
<!-- Spinner Start -->
|
||||
<div id="spinner"
|
||||
class="show bg-white position-fixed translate-middle w-100 vh-100 top-50 start-50 d-flex align-items-center justify-content-center">
|
||||
<div class="spinner-border text-primary" style="width: 3rem; height: 3rem;" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Spinner End -->
|
||||
|
||||
|
||||
<!-- Navbar Start -->
|
||||
<nav class="navbar navbar-expand-lg bg-white navbar-light sticky-top px-4 px-lg-5 py-lg-0">
|
||||
<a href="<?= base_url('/') ?>" class="navbar-brand d-flex align-items-center">
|
||||
<img src="<?= base_url('images/logo.png') ?>" alt="Al Rahma Sunday School" style="height: 40px; width: 40px; border-radius: 50%; object-fit: contain; background-color: #fff;">
|
||||
<h1 class="m-0 ms-2 green-title">Al Rahma Sunday School</h1>
|
||||
</a>
|
||||
<button type="button" class="navbar-toggler" data-bs-toggle="collapse" data-bs-target="#navbarCollapse">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarCollapse">
|
||||
<div class="navbar-nav mx-auto">
|
||||
<a href="<?= base_url('/') ?>" class="nav-item nav-link">Home</a>
|
||||
<a href="<?= base_url('/about') ?>" class="nav-item nav-link">About Us</a>
|
||||
<a href="<?= base_url('/classes') ?>" class="nav-item nav-link">Classes</a>
|
||||
<div class="nav-item dropdown">
|
||||
<a href="#" class="nav-link dropdown-toggle active" data-bs-toggle="dropdown">Pages</a>
|
||||
<div class="dropdown-menu rounded-0 rounded-bottom border-0 shadow-sm m-0">
|
||||
<a href="<?= base_url('/facility') ?>" class="dropdown-item">School Facilities</a>
|
||||
<a href="team.html" class="dropdown-item">Popular Teachers</a>
|
||||
<a href="<?= base_url('/call-to-action') ?>" class="dropdown-item">Become A Teacher or Admins</a>
|
||||
<a href="appointment.html" class="dropdown-item">Make Appointment</a>
|
||||
<a href="testimonial.html" class="dropdown-item active">Testimonial</a>
|
||||
<a href="<?= base_url('/notFound') ?>" class="dropdown-item">404 Error</a>
|
||||
</div>
|
||||
</div>
|
||||
<a href="<?= base_url('/contact') ?>" class="nav-item nav-link">Contact Us</a>
|
||||
</div>
|
||||
<a href="/register" class="btn btn-primary rounded-pill px-3 d-none d-lg-block">Register
|
||||
<i class="fa fa-arrow-right ms-3"></i></a>
|
||||
<a href="/login" class="btn btn-primary rounded-pill px-3 d-none d-lg-block">Login<i
|
||||
class="fa fa-arrow-right ms-3"></i></a>
|
||||
|
||||
</div>
|
||||
</nav>
|
||||
<!-- Navbar End -->
|
||||
<!-- Page Header End -->
|
||||
<div class="container-xxl py-5 page-header position-relative mb-5">
|
||||
<div class="container py-5">
|
||||
<h1 class="display-2 text-white animated slideInDown mb-4">Testimonial</h1>
|
||||
<nav aria-label="breadcrumb animated slideInDown">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="/">Home</a></li>
|
||||
<li class="breadcrumb-item"><a href="#">Pages</a></li>
|
||||
<li class="breadcrumb-item text-white active" aria-current="page">Testimonial</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Page Header End -->
|
||||
|
||||
<!-- Testimonial Start -->
|
||||
<div class="container-xxl py-5">
|
||||
<div class="container">
|
||||
<div class="text-center mx-auto mb-5 wow fadeInUp" data-wow-delay="0.1s" style="max-width: 600px;">
|
||||
<h1 class="mb-3">Parents/Guardians Say!</h1>
|
||||
<p>Parents say our Sunday school is exceptional, praising the engaging curriculum and caring staff.
|
||||
They appreciate the
|
||||
positive impact on their children's spiritual growth and the sense of community we foster. Join
|
||||
us and see why parents
|
||||
highly recommend our program.</p>
|
||||
</div>
|
||||
<div class="owl-carousel testimonial-carousel wow fadeInUp" data-wow-delay="0.1s">
|
||||
<div class="testimonial-item bg-light rounded p-5">
|
||||
<p class="fs-5">The teachers at this school are incredibly dedicated and supportive, always going the extra mile to help students succeed. The curriculum is comprehensive and engaging, preparing students well for future challenges. Additionally, the school's facilities are top-notch, providing a safe and conducive environment for learning.</p>
|
||||
<div class="d-flex align-items-center bg-white me-n5" style="border-radius: 50px 0 0 50px;">
|
||||
<img class="img-fluid flex-shrink-0 rounded-circle" src="images/testimonial-1.jpg"
|
||||
style="width: 90px; height: 90px;">
|
||||
<div class="ps-3">
|
||||
<h3 class="mb-1">Parent Name</h3>
|
||||
<!--span>Profession</span-->
|
||||
</div>
|
||||
<i class="fa fa-quote-right fa-3x text-primary ms-auto d-none d-sm-flex"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="testimonial-item bg-light rounded p-5">
|
||||
<p class="fs-5">The teachers at this school are incredibly supportive and always go the extra mile to ensure students understand the material.
|
||||
The school facilities are well-maintained, providing a safe and conducive environment for learning.</p>
|
||||
<div class="d-flex align-items-center bg-white me-n5" style="border-radius: 50px 0 0 50px;">
|
||||
<img class="img-fluid flex-shrink-0 rounded-circle" src="images/testimonial-2.jpg"
|
||||
style="width: 90px; height: 90px;">
|
||||
<div class="ps-3">
|
||||
<h3 class="mb-1">Parent Name</h3>
|
||||
<!--span>Profession</span-->
|
||||
</div>
|
||||
<i class="fa fa-quote-right fa-3x text-primary ms-auto d-none d-sm-flex"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="testimonial-item bg-light rounded p-5">
|
||||
<p class="fs-5">The school has a fantastic learning environment with dedicated teachers who genuinely care about students' success. The facilities are well-maintained and provide a safe space for both academic and extracurricular activities. Overall, it's a great place for children to grow and develop their skills.</p>
|
||||
<div class="d-flex align-items-center bg-white me-n5" style="border-radius: 50px 0 0 50px;">
|
||||
<img class="img-fluid flex-shrink-0 rounded-circle" src="images/testimonial-3.jpg"
|
||||
style="width: 90px; height: 90px;">
|
||||
<div class="ps-3">
|
||||
<h3 class="mb-1">Parent Name</h3>
|
||||
<!--span>Profession</span-->
|
||||
</div>
|
||||
<i class="fa fa-quote-right fa-3x text-primary ms-auto d-none d-sm-flex"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Testimonial End -->
|
||||
|
||||
<!-- Footer Start -->
|
||||
<div class="container-fluid bg-dark text-white-50 footer pt-5 mt-5 wow fadeIn" data-wow-delay="0.1s">
|
||||
<div class="container py-5">
|
||||
<div class="row g-5">
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<h3 class="text-white mb-4">Get In Touch</h3>
|
||||
<p class="mb-2"><i class="fa fa-map-marker-alt me-3"></i>5 Courthouse Lane, Chelmsford, MA 01824
|
||||
</p>
|
||||
<p class="mb-2"><i class="fa fa-phone-alt me-3"></i>+1 978-364-0219
|
||||
0</p>
|
||||
<p class="mb-2"><i class="fa fa-envelope me-3"></i>alrahma.isgl@gmail.com
|
||||
</p>
|
||||
<div class="d-flex pt-2">
|
||||
<a class="btn btn-outline-light btn-social" href=""><i class="fab fa-twitter"></i></a>
|
||||
<a class="btn btn-outline-light btn-social" href=""><i class="fab fa-facebook-f"></i></a>
|
||||
<a class="btn btn-outline-light btn-social" href=""><i class="fab fa-youtube"></i></a>
|
||||
<a class="btn btn-outline-light btn-social" href=""><i class="fab fa-linkedin-in"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<h3 class="text-white mb-4">Quick Links</h3>
|
||||
<a class="btn btn-link text-white-50" href="">About Us</a>
|
||||
<a class="btn btn-link text-white-50" href="">Contact Us</a>
|
||||
<a class="btn btn-link text-white-50" href="">Our Services</a>
|
||||
<a class="btn btn-link text-white-50" href="">Privacy Policy</a>
|
||||
<a class="btn btn-link text-white-50" href="">Terms & Condition</a>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<h3 class="text-white mb-4">Photo Gallery</h3>
|
||||
<div class="row g-2 pt-2">
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-1.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-2.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-3.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-4.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-5.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-6.jpg') ?>" alt="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<h3 class="text-white mb-4">Newsletter</h3>
|
||||
<p>Our newsletter is your gateway to staying informed and connected with our Sunday school community. </p>
|
||||
<div class="position-relative mx-auto" style="max-width: 400px;">
|
||||
<input class="form-control bg-transparent w-100 py-3 ps-4 pe-5" type="text"
|
||||
placeholder="Your email">
|
||||
<button type="button"
|
||||
class="btn btn-primary py-2 position-absolute top-0 end-0 mt-2 me-2">SignUp</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="copyright">
|
||||
<div class="row">
|
||||
<div class="col-md-6 text-center text-md-start mb-3 mb-md-0">
|
||||
© <a class="border-bottom" href="#">Al Rahma Sunday School by ISGL</a>, All Right
|
||||
Reserved.
|
||||
|
||||
<!--/*** This template is free as long as you keep the footer author’s credit link/attribution link/backlink. If you'd like to use the template without the footer author’s credit link/attribution link/backlink, you can purchase the Credit Removal License from "https://htmlcodex.com/credit-removal". Thank you for your support. ***/-->
|
||||
Designed By <a class="border-bottom" href="https://htmlcodex.com">HTML Codex</a>
|
||||
</div>
|
||||
<div class="col-md-6 text-center text-md-end">
|
||||
<div class="footer-menu">
|
||||
<a href="">Home</a>
|
||||
<a href="">Cookies</a>
|
||||
<a href="">Help</a>
|
||||
<a href="">FQAs</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Footer End -->
|
||||
|
||||
|
||||
<!-- Back to Top -->
|
||||
<a href="#" class="btn btn-lg btn-primary btn-lg-square back-to-top"><i class="bi bi-arrow-up"></i></a>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript Libraries -->
|
||||
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="lib/wow/wow.min.js"></script>
|
||||
<script src="lib/easing/easing.min.js"></script>
|
||||
<script src="lib/waypoints/waypoints.min.js"></script>
|
||||
<script src="lib/owlcarousel/owl.carousel.min.js"></script>
|
||||
|
||||
<!-- Template Javascript -->
|
||||
<script src="js/main.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,296 @@
|
||||
# Job Postings Feature — Implementation Plan
|
||||
|
||||
## 1. Overview
|
||||
|
||||
This feature has two sides:
|
||||
|
||||
- **Admin side** — create/manage job position templates, publish open positions, review applicant submissions.
|
||||
- **Client side** — browse open positions, view details, and submit an application (with resume upload) that triggers a confirmation email.
|
||||
|
||||
---
|
||||
|
||||
## 2. Data Model
|
||||
|
||||
### JobTemplate
|
||||
| Field | Type | Notes |
|
||||
|---|---|---|
|
||||
| `template_id` | UUID | Primary key |
|
||||
| `title` | string | |
|
||||
| `description` | text | |
|
||||
| `department` | string | |
|
||||
| `location` | string | |
|
||||
| `employment_type` | string | e.g. full-time, part-time, contract |
|
||||
| `requirements` | text | |
|
||||
| `salary_range` | string | optional |
|
||||
| `version` | int | increments on each edit |
|
||||
| `is_active` | bool | soft-archive instead of delete |
|
||||
| `created_by` | user ref | |
|
||||
| `created_at` / `updated_at` | timestamp | |
|
||||
|
||||
### JobPosition
|
||||
| Field | Type | Notes |
|
||||
|---|---|---|
|
||||
| `position_id` | UUID | **Unique ID per job, required** |
|
||||
| `template_id` | UUID (nullable) | set if created from a template |
|
||||
| `title`, `description`, `department`, `location`, `employment_type`, `requirements`, `salary_range` | — | copied from template at creation time (not a live reference) |
|
||||
| `status` | enum | draft / open / closed / filled |
|
||||
| `created_at` / `updated_at` | timestamp | |
|
||||
| `posted_by` | user ref | |
|
||||
|
||||
### Application
|
||||
| Field | Type | Notes |
|
||||
|---|---|---|
|
||||
| `application_id` | UUID | |
|
||||
| `position_id` | FK | which job they applied to |
|
||||
| `first_name`, `last_name` | string | required |
|
||||
| `email` | string | required, validated |
|
||||
| `phone` | string | required |
|
||||
| `resume_file_url` | string | pointer to stored file |
|
||||
| `submitted_at` | timestamp | |
|
||||
| `status` | enum | new / reviewed / contacted / rejected / hired |
|
||||
| `admin_notes` | text | for admin follow-up tracking |
|
||||
|
||||
---
|
||||
|
||||
## 3. Admin Side
|
||||
|
||||
### 3.1 Create Open Position
|
||||
- Form fields: title, description, department, location, employment type, requirements, salary range.
|
||||
- Option to start **blank** or **from a template**.
|
||||
- On save: generate unique `position_id`, default status `draft` → publish sets `open`.
|
||||
|
||||
### 3.2 Job Templates (CRUD)
|
||||
- List / create / edit / archive templates.
|
||||
- **Editing behavior**: support both
|
||||
- **Overwrite** current version, or
|
||||
- **Save as new version** (keeps history)
|
||||
- Version history viewable/revertible.
|
||||
- Archiving a template never affects positions already created from it (fields are copied, not linked live).
|
||||
|
||||
### 3.3 Create Position from Template
|
||||
- Admin selects a template → fields pre-fill a new position form → admin edits as needed → save generates a new `position_id`.
|
||||
|
||||
### 3.4 Review Applications
|
||||
- Dashboard listing all submissions, filterable by position, status, or date.
|
||||
- Detail view: applicant info, resume preview/download, position applied to.
|
||||
- Status + notes fields so admin can track follow-up (contacted, rejected, hired).
|
||||
- Nice-to-have: CSV export, direct email link to applicant.
|
||||
|
||||
---
|
||||
|
||||
## 4. Client Side
|
||||
|
||||
### 4.1 Open Positions Listing
|
||||
- Public page listing all positions where `status = open`.
|
||||
- Card/list view: title, department, location, short summary.
|
||||
|
||||
### 4.2 Position Detail Page
|
||||
- Full description, requirements, etc.
|
||||
- "Apply" call-to-action.
|
||||
|
||||
### 4.3 Application Form
|
||||
- Fields: First Name, Last Name, Email, Phone, Resume upload (PDF/DOC).
|
||||
- Client-side validation: required fields, email format, file type/size limits.
|
||||
- Submits to create an `Application` tied to `position_id`.
|
||||
|
||||
### 4.4 Confirmation Email
|
||||
- Sent automatically to the applicant's email on successful submission.
|
||||
- References the position title and sets expectation of review/follow-up.
|
||||
- Optional: parallel notification email to admin/HR inbox.
|
||||
|
||||
---
|
||||
|
||||
## 5. API Endpoints (suggested)
|
||||
|
||||
**Admin**
|
||||
```
|
||||
POST /admin/templates
|
||||
PUT /admin/templates/:id
|
||||
GET /admin/templates
|
||||
|
||||
POST /admin/positions (optional template_id)
|
||||
PUT /admin/positions/:id
|
||||
GET /admin/positions
|
||||
|
||||
GET /admin/applications (filter by position, status)
|
||||
PATCH /admin/applications/:id (update status/notes)
|
||||
```
|
||||
|
||||
**Client (public)**
|
||||
```
|
||||
GET /positions (open only)
|
||||
GET /positions/:id
|
||||
POST /positions/:id/apply (multipart form, includes resume)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Infrastructure Notes
|
||||
|
||||
- **File storage**: resumes go to object storage (S3 or equivalent); store the file URL/key on the Application record, not the binary in the database.
|
||||
- **Email**: use a transactional email provider (SendGrid, SES, Postmark) for confirmation + admin notification emails, driven by templates.
|
||||
- **Auth**: admin routes require authenticated/role-gated access; client routes remain public.
|
||||
|
||||
---
|
||||
|
||||
## 7. Database Migrations
|
||||
|
||||
Assumes PostgreSQL syntax (adjust types for MySQL/SQLite as needed). Each migration is additive and ordered so foreign keys resolve correctly.
|
||||
|
||||
### Migration 001 — create `job_templates`
|
||||
```sql
|
||||
-- up
|
||||
CREATE TABLE job_templates (
|
||||
template_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
title VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
department VARCHAR(255),
|
||||
location VARCHAR(255),
|
||||
employment_type VARCHAR(50),
|
||||
requirements TEXT,
|
||||
salary_range VARCHAR(100),
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_by UUID REFERENCES users(id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_job_templates_active ON job_templates (is_active);
|
||||
|
||||
-- down
|
||||
DROP TABLE IF EXISTS job_templates;
|
||||
```
|
||||
|
||||
### Migration 002 — create `job_template_versions` (version history)
|
||||
```sql
|
||||
-- up
|
||||
CREATE TABLE job_template_versions (
|
||||
version_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
template_id UUID NOT NULL REFERENCES job_templates(template_id) ON DELETE CASCADE,
|
||||
version INT NOT NULL,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
department VARCHAR(255),
|
||||
location VARCHAR(255),
|
||||
employment_type VARCHAR(50),
|
||||
requirements TEXT,
|
||||
salary_range VARCHAR(100),
|
||||
saved_by UUID REFERENCES users(id),
|
||||
saved_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_template_versions_template_id ON job_template_versions (template_id);
|
||||
|
||||
-- down
|
||||
DROP TABLE IF EXISTS job_template_versions;
|
||||
```
|
||||
|
||||
### Migration 003 — create `job_positions`
|
||||
```sql
|
||||
-- up
|
||||
CREATE TYPE position_status AS ENUM ('draft', 'open', 'closed', 'filled');
|
||||
|
||||
CREATE TABLE job_positions (
|
||||
position_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
template_id UUID REFERENCES job_templates(template_id) ON DELETE SET NULL,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
department VARCHAR(255),
|
||||
location VARCHAR(255),
|
||||
employment_type VARCHAR(50),
|
||||
requirements TEXT,
|
||||
salary_range VARCHAR(100),
|
||||
status position_status NOT NULL DEFAULT 'draft',
|
||||
posted_by UUID REFERENCES users(id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_job_positions_status ON job_positions (status);
|
||||
|
||||
-- down
|
||||
DROP TABLE IF EXISTS job_positions;
|
||||
DROP TYPE IF EXISTS position_status;
|
||||
```
|
||||
|
||||
### Migration 004 — create `applications`
|
||||
```sql
|
||||
-- up
|
||||
CREATE TYPE application_status AS ENUM ('new', 'reviewed', 'contacted', 'rejected', 'hired');
|
||||
|
||||
CREATE TABLE applications (
|
||||
application_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
position_id UUID NOT NULL REFERENCES job_positions(position_id) ON DELETE CASCADE,
|
||||
first_name VARCHAR(100) NOT NULL,
|
||||
last_name VARCHAR(100) NOT NULL,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
phone VARCHAR(30) NOT NULL,
|
||||
resume_file_url TEXT NOT NULL,
|
||||
status application_status NOT NULL DEFAULT 'new',
|
||||
admin_notes TEXT,
|
||||
submitted_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_applications_position_id ON applications (position_id);
|
||||
CREATE INDEX idx_applications_status ON applications (status);
|
||||
CREATE INDEX idx_applications_email ON applications (email);
|
||||
|
||||
-- down
|
||||
DROP TABLE IF EXISTS applications;
|
||||
DROP TYPE IF EXISTS application_status;
|
||||
```
|
||||
|
||||
### Migration 005 — updated_at auto-touch triggers (optional, Postgres)
|
||||
```sql
|
||||
-- up
|
||||
CREATE OR REPLACE FUNCTION set_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = now();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER trg_job_templates_updated_at
|
||||
BEFORE UPDATE ON job_templates
|
||||
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
CREATE TRIGGER trg_job_positions_updated_at
|
||||
BEFORE UPDATE ON job_positions
|
||||
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
-- down
|
||||
DROP TRIGGER IF EXISTS trg_job_templates_updated_at ON job_templates;
|
||||
DROP TRIGGER IF EXISTS trg_job_positions_updated_at ON job_positions;
|
||||
DROP FUNCTION IF EXISTS set_updated_at();
|
||||
```
|
||||
|
||||
### Migration order & notes
|
||||
1. `job_templates` → 2. `job_template_versions` → 3. `job_positions` → 4. `applications` → 5. triggers.
|
||||
2. `users` table is assumed to already exist (for `created_by` / `posted_by`); drop those FK constraints if no auth/user table exists yet.
|
||||
3. Run each migration's `up` in order; `down` scripts reverse in the opposite order for rollback.
|
||||
4. If using a migration tool (Knex, Prisma, Sequelize, Alembic, Rails ActiveRecord, etc.), split each numbered migration above into that tool's file format/naming convention — the SQL logic stays the same.
|
||||
|
||||
---
|
||||
|
||||
## 8. Build Phases
|
||||
|
||||
| Phase | Scope |
|
||||
|---|---|
|
||||
| 1 | Data models + admin CRUD for Positions (no templates yet) |
|
||||
| 2 | Template CRUD + versioning + "create position from template" |
|
||||
| 3 | Client-facing listing + detail pages |
|
||||
| 4 | Application form + file upload + submission handling |
|
||||
| 5 | Confirmation email + admin notification email |
|
||||
| 6 | Admin review dashboard (applications list, status, notes) |
|
||||
| 7 | Polish: validation, admin auth/permissions, testing |
|
||||
|
||||
---
|
||||
|
||||
## 9. Open Questions
|
||||
|
||||
- How is admin access authenticated/restricted (login system, roles)?
|
||||
- Should duplicate applications (same email + position) be blocked or allowed?
|
||||
- What resume file types/size limits are acceptable?
|
||||
- Should closed positions remain visible (marked "closed") or disappear from the client list entirely?
|
||||
@@ -0,0 +1,52 @@
|
||||
# Scope Discipline Rules
|
||||
|
||||
These rules override any general instinct to "improve while I'm in there." Follow them on every task, no exceptions.
|
||||
|
||||
## Before touching any code
|
||||
|
||||
1. Restate the task in one sentence: what behavior must change, and what the expected outcome is.
|
||||
2. Identify the smallest set of files/functions responsible for that behavior. This is your **allowed scope**. Everything else is **protected**.
|
||||
3. Read the relevant code before editing it. Do not edit based on assumptions about how it probably works.
|
||||
|
||||
## The hard rule: ask before expanding scope
|
||||
|
||||
If, while working, you find that:
|
||||
- a file outside your allowed scope needs to change,
|
||||
- a dependency needs to be added/updated,
|
||||
- a test needs modification,
|
||||
- an unrelated bug is blocking you,
|
||||
- or a "cleaner" implementation would touch more than the minimum,
|
||||
|
||||
**stop and ask me before making that change.** Explain:
|
||||
- what you were trying to do,
|
||||
- why the fix requires going outside the original scope,
|
||||
- exactly what you want to change and where.
|
||||
|
||||
Wait for my answer. Do not proceed on your own judgment, even if you're confident it's correct or trivial.
|
||||
|
||||
This applies even to small things (renaming a variable for clarity, fixing a typo in an unrelated comment, reformatting a block you had to scroll past). If it's not required to satisfy the request, ask first.
|
||||
|
||||
## While editing
|
||||
|
||||
- Make the fewest-line, fewest-file change that correctly satisfies the request.
|
||||
- Preserve existing naming, structure, patterns, and formatting. Match the codebase's existing style, don't impose your own.
|
||||
- Never run project-wide formatters/linters-with-autofix/import-organizers as a side effect of a small change.
|
||||
- Never touch tests except to add new ones that validate the requested behavior — and only after confirming that's in scope.
|
||||
- Treat any uncommitted/staged changes already in the working tree as off-limits. Don't revert, reset, or absorb them into your edit.
|
||||
|
||||
## Before reporting done
|
||||
|
||||
Review your own diff, file by file, line by line. For anything you can't justify with "this was required by the explicit request," revert it.
|
||||
|
||||
Then report:
|
||||
- **Files changed** — list, with a one-line reason each tied directly to the request.
|
||||
- **Scope confirmation** — explicitly state: "No files, dependencies, tests, or config outside this list were modified."
|
||||
- **Anything you noticed but didn't touch** — unrelated bugs, tech debt, cleanup opportunities. Mention them, don't fix them.
|
||||
|
||||
## If the task genuinely can't be done without expanding scope
|
||||
|
||||
Say so plainly, explain what would need to change and why, and wait for confirmation. Don't silently do the bigger version, and don't pretend a partial/incorrect fix is complete.
|
||||
|
||||
---
|
||||
|
||||
**Default when uncertain: don't make the change, ask instead.**
|
||||
@@ -0,0 +1,64 @@
|
||||
# Careers Section (Home Page) — Implementation Plan
|
||||
|
||||
## 1. Goals & Scope
|
||||
- Attract candidates and showcase company culture directly from the home page
|
||||
- Careers lives as a **section on the home page**, not a standalone `/careers` page
|
||||
- Section should link out to a full job detail view or an ATS for the actual application step
|
||||
- Decide: in-house application handling vs. linking out to an ATS (Greenhouse, Lever, Workable, etc.)
|
||||
|
||||
## 2. Content Structure (Home Page Section)
|
||||
- **Section heading/hero** — tagline + short pitch on why to work here
|
||||
- **Culture/values snippet** — a few photos or highlights, benefits teaser (health, remote work, PTO, equity, etc.)
|
||||
- **Open positions preview** — short list (e.g., top 3–5) with title, department, location, type
|
||||
- **"View all openings" CTA** — links to full listing (could be a modal, an anchor-expanded list, or an external ATS board)
|
||||
- **Application CTA** — "Apply Now" per role, linking to a form, email, or ATS
|
||||
- **Optional extras** — employee testimonials, office photos/video, perks grid
|
||||
|
||||
## 3. Data Model
|
||||
Fields per job posting:
|
||||
```
|
||||
title
|
||||
department
|
||||
location
|
||||
employment_type
|
||||
description
|
||||
requirements
|
||||
salary_range (optional)
|
||||
posted_date
|
||||
status (open/closed)
|
||||
apply_link
|
||||
```
|
||||
Store in a CMS (Sanity, Contentful, Strapi) or a simple JSON/database table if no CMS exists.
|
||||
|
||||
## 4. Technical Approach
|
||||
- **Static list** — hardcode JSON if roles change rarely
|
||||
- **CMS-driven** — non-technical team can add/remove postings without code
|
||||
- **ATS integration** — pull live listings via API (Greenhouse/Lever both offer public job board APIs) for least maintenance
|
||||
- **Application handling** — form → email service (e.g., SendGrid) or direct link to ATS application page
|
||||
|
||||
## 5. Pages / Routes
|
||||
- `/` (home page) — Careers section embedded, e.g. `#careers` anchor for nav linking
|
||||
- `/careers/[job-slug]` — individual job detail page (linked from the home section)
|
||||
- `/careers/apply/[job-slug]` — application form (optional)
|
||||
- Consider adding "Careers" to the main nav, scrolling/linking to the `#careers` section
|
||||
|
||||
## 6. Design Considerations
|
||||
- Keep the section concise — it's part of the home page, not the main focus, so limit to a handful of featured/open roles
|
||||
- Mobile-responsive job cards, and ensure the section doesn't overload home page load time
|
||||
- Clear, prominent "View openings" / "Apply" CTAs
|
||||
- SEO: `JobPosting` schema.org structured data on the linked job detail pages so roles surface in Google Jobs search
|
||||
- Make sure the section is reachable via nav (e.g., "Careers" nav item scrolls to the section or links to `/#careers`)
|
||||
|
||||
## 7. Build Order
|
||||
1. Design mockups/wireframe for the home page section (placement, hero, mini job list)
|
||||
2. Set up data source (CMS or ATS API) for job postings
|
||||
3. Build the home page Careers section + "view all/apply" links
|
||||
4. Build job detail page(s) linked from the section
|
||||
5. Wire up application flow
|
||||
6. Add SEO schema markup on job detail pages
|
||||
7. Test on mobile; submit a test application
|
||||
8. Launch and monitor applications
|
||||
|
||||
## Open Questions
|
||||
- What is the site built with (React/Next.js, WordPress, Webflow, plain HTML)?
|
||||
- Use an existing ATS (Greenhouse/Lever) or handle applications via email/form?
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user