add open position system
Deploy to Shared Hosting / Shared hosting deploy (push) Failing after 48s
Tests / PHPUnit (push) Successful in 1m29s

This commit is contained in:
root
2026-09-02 00:53:55 -04:00
parent 5b11e2d859
commit dbfd72c2f9
24 changed files with 1627 additions and 47 deletions
+30 -40
View File
@@ -329,45 +329,35 @@
<p class="mb-0">Review the available roles below and apply by creating an account.</p>
</div>
<div class="row g-4">
<div class="col-lg-4">
<article class="opening-card">
<h3>Volunteer Teacher</h3>
<div class="opening-meta">Instruction &middot; Chelmsford, MA &middot; Part-time volunteer</div>
<p>Lead Quran, Arabic or Islamic Studies lessons in a warm classroom environment.</p>
<ul class="opening-list">
<li>Prepare weekly lessons and classroom activities.</li>
<li>Guide students with patience and clear communication.</li>
<li>Partner with school administration and families when needed.</li>
</ul>
<a class="btn-brand-sm" href="<?= base_url('/register') ?>">Apply Now <i class="fa fa-arrow-right"></i></a>
</article>
</div>
<div class="col-lg-4">
<article class="opening-card">
<h3>Teacher Assistant</h3>
<div class="opening-meta">Classroom Support &middot; Chelmsford, MA &middot; Part-time volunteer</div>
<p>Support teachers and help students stay engaged throughout Sunday classes.</p>
<ul class="opening-list">
<li>Assist with activities, materials and classroom routines.</li>
<li>Provide individual support to students during lessons.</li>
<li>Help maintain a respectful and focused learning environment.</li>
</ul>
<a class="btn-brand-sm" href="<?= base_url('/register') ?>">Apply Now <i class="fa fa-arrow-right"></i></a>
</article>
</div>
<div class="col-lg-4">
<article class="opening-card">
<h3>Administrative Volunteer</h3>
<div class="opening-meta">Operations &middot; Chelmsford, MA &middot; Part-time volunteer</div>
<p>Help the school operate smoothly through weekly administrative and event support.</p>
<ul class="opening-list">
<li>Support communications, supplies and weekly coordination.</li>
<li>Help organize school events and program logistics.</li>
<li>Assist staff with structured tasks before or during Sunday school.</li>
</ul>
<a class="btn-brand-sm" href="<?= base_url('/register') ?>">Apply Now <i class="fa fa-arrow-right"></i></a>
</article>
</div>
<?php if (!empty($positions)): ?>
<?php foreach ($positions as $position): ?>
<div class="col-lg-4">
<article class="opening-card">
<h3><?= esc($position['title']) ?></h3>
<div class="opening-meta">
<?= esc($position['department'] ?? '') ?>
<?php if (!empty($position['location'])): ?> &middot; <?= esc($position['location']) ?><?php endif; ?>
<?php if (!empty($position['employment_type'])): ?> &middot; <?= esc($position['employment_type']) ?><?php endif; ?>
</div>
<?php if (!empty($position['requirements'])): ?>
<ul class="opening-list">
<?php foreach (array_slice(array_filter(preg_split('/\r\n|\r|\n/', (string) $position['requirements'])), 0, 3) as $requirement): ?>
<li><?= esc(preg_replace('/^\s*-\s*/', '', $requirement)) ?></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
<a class="btn-brand-sm" href="<?= site_url('careers/' . $position['position_id']) ?>">View Details <i class="fa fa-arrow-right"></i></a>
</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>
@@ -379,4 +369,4 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
</html>
@@ -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>
+115 -4
View File
@@ -198,6 +198,74 @@
background-color: var(--accent);
}
/* Open positions banner */
.careers-ticker {
display: block;
background-color: var(--primary-dark);
color: var(--paper);
text-decoration: none;
overflow: hidden;
border-top: 1px solid rgba(243, 239, 227, 0.14);
border-bottom: 1px solid rgba(243, 239, 227, 0.14);
}
.careers-ticker:hover,
.careers-ticker:focus {
color: var(--paper);
}
.ticker-track {
display: flex;
width: max-content;
animation: ticker-scroll 10s linear infinite;
}
.careers-ticker:hover .ticker-track,
.careers-ticker:focus .ticker-track,
.careers-ticker:focus-within .ticker-track {
animation-play-state: paused;
}
.ticker-group {
display: flex;
align-items: center;
min-width: max-content;
}
.ticker-label,
.ticker-item {
display: inline-flex;
align-items: center;
min-height: 54px;
white-space: nowrap;
}
.ticker-label {
padding: 0 1.5rem;
font-weight: 700;
color: var(--accent-soft);
}
.ticker-item {
gap: 0.5rem;
padding: 0 1.75rem;
border-left: 1px solid rgba(243, 239, 227, 0.16);
}
.ticker-title {
font-weight: 700;
}
.ticker-meta {
color: #DCE6DD;
font-size: 0.9rem;
}
@keyframes ticker-scroll {
from { transform: translateX(100vw); }
to { transform: translateX(-50%); }
}
@media (max-width: 992px) {
.carousel-item { height: 480px; }
.carousel-caption { max-width: 78%; padding: 1.5rem; bottom: 8%; }
@@ -209,6 +277,9 @@
.carousel-caption { left: 5%; right: 5%; max-width: none; padding: 1.1rem 1.25rem; bottom: 6%; }
.carousel-caption h1 { font-size: 1.2rem; }
.carousel-caption p { font-size: 0.85rem; }
.ticker-track { animation-duration: 10s; }
.ticker-label, .ticker-item { min-height: 48px; }
.ticker-item { padding: 0 1.25rem; }
}
/* Join line */
@@ -506,6 +577,46 @@
</div>
<!-- Carousel End -->
<?php
$tickerPositions = !empty($openPositions) && is_array($openPositions) ? $openPositions : [];
$tickerItems = [];
foreach ($tickerPositions as $position) {
$meta = array_filter([
$position['department'] ?? '',
$position['location'] ?? '',
$position['employment_type'] ?? '',
], static fn ($value) => trim((string) $value) !== '');
$tickerItems[] = [
'title' => (string) ($position['title'] ?? 'Open Position'),
'meta' => implode(' · ', $meta),
];
}
if ($tickerItems === []) {
$tickerItems[] = [
'title' => 'Volunteer opportunities',
'meta' => 'See current openings',
];
}
?>
<a class="careers-ticker" href="<?= site_url('careers') ?>" aria-label="View open volunteer positions">
<div class="ticker-track">
<?php for ($copy = 0; $copy < 2; $copy++): ?>
<div class="ticker-group" <?= $copy === 1 ? 'aria-hidden="true"' : '' ?>>
<span class="ticker-label">Now recruiting volunteers</span>
<?php foreach ($tickerItems as $item): ?>
<span class="ticker-item">
<span class="ticker-title"><?= esc($item['title']) ?></span>
<?php if ($item['meta'] !== ''): ?>
<span class="ticker-meta"><?= esc($item['meta']) ?></span>
<?php endif; ?>
<i class="fa fa-arrow-right" aria-hidden="true"></i>
</span>
<?php endforeach; ?>
</div>
<?php endfor; ?>
</div>
</a>
<!-- Join line -->
<div class="join-strip">
<a href="<?= base_url('account_creation_guide.pdf') ?>" target="_blank">
@@ -515,7 +626,7 @@
<!-- Statistics Start -->
<div class="stats-band section-tight">
<div class="container-narrow" data-dashboard-endpoint="/api/administrator/dashboard">
<div class="container-narrow" data-dashboard-endpoint="<?= site_url('api/administrator/dashboard') ?>">
<h2>Active Participants</h2>
<div class="stats-row">
<div class="stat-cell">
@@ -644,7 +755,7 @@
</script>
<script>
document.addEventListener('DOMContentLoaded', function () {
const container = document.querySelector('.stats-band[data-dashboard-endpoint]');
const container = document.querySelector('.stats-band [data-dashboard-endpoint]');
if (!container) return;
const endpoint = container.dataset.dashboardEndpoint;
const statElements = container.querySelectorAll('[data-stat]');
@@ -666,7 +777,7 @@
const counts = payload && typeof payload === 'object' && payload.counts ? payload.counts : {};
statElements.forEach(function (element) {
const key = element.dataset.stat;
const value = counts[key];
const value = Number(counts[key]);
element.textContent = Number.isFinite(value) ? numberFormatter.format(value) : '—';
});
})
@@ -685,4 +796,4 @@
</script>
</body>
</html>
</html>
+27
View File
@@ -0,0 +1,27 @@
<?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="requirements">Requirements</label>
<textarea id="requirements" name="requirements" class="form-control" rows="6"><?= esc(old('requirements', $item['requirements'] ?? '')) ?></textarea>
</div>
</div>
+62
View File
@@ -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() ?>
+27
View File
@@ -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() ?>
+38
View File
@@ -0,0 +1,38 @@
<?= $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>Open 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>Actions</th></tr></thead>
<tbody>
<?php foreach ($positions as $position): ?>
<tr>
<td><?= esc($position['title']) ?></td>
<td><?= esc($position['department'] ?? '') ?></td>
<td><?= esc($position['location'] ?? '') ?></td>
<td><?= esc($position['employment_type'] ?? '') ?></td>
<td><?= esc(ucfirst($position['status'])) ?></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 ($position['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() ?>
+47
View File
@@ -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">
<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() ?>
+53
View File
@@ -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() ?>
+187
View File
@@ -0,0 +1,187 @@
<?= $this->extend('layout/main_layout') ?>
<?= $this->section('styles') ?>
<style>
.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() ?>
+14
View File
@@ -0,0 +1,14 @@
<?= $this->extend('layout/main_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() ?>
+110
View File
@@ -0,0 +1,110 @@
<?= $this->extend('layout/main_layout') ?>
<?= $this->section('styles') ?>
<style>
.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'])): ?> &middot; <?= esc($position['location']) ?><?php endif; ?>
<?php if (!empty($position['employment_type'])): ?> &middot; <?= esc($position['employment_type']) ?><?php endif; ?>
</p>
<h2 class="h4 mt-4">Description</h2>
<div><?= $renderPostingText($position['description'] ?? '') ?></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() ?>
+4
View File
@@ -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>