fix positions
This commit is contained in:
@@ -235,7 +235,7 @@ class JobPostingController extends BaseController
|
||||
public function positions()
|
||||
{
|
||||
return view('jobs/admin/positions', [
|
||||
'positions' => $this->positions->openPositions(),
|
||||
'positions' => $this->positions->adminPositions(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -263,6 +263,9 @@ class JobPostingController extends BaseController
|
||||
|
||||
$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.');
|
||||
@@ -287,11 +290,20 @@ class JobPostingController extends BaseController
|
||||
|
||||
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.');
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ class CreateJobPostings extends Migration
|
||||
'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],
|
||||
'created_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
'updated_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
]);
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ class JobPositionModel extends Model
|
||||
'requirements',
|
||||
'status',
|
||||
'posted_by',
|
||||
'posted_at',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
@@ -31,6 +32,18 @@ class JobPositionModel extends Model
|
||||
|
||||
public function openPositions(): array
|
||||
{
|
||||
return $this->where('status', 'open')->orderBy('created_at', 'DESC')->findAll();
|
||||
return $this
|
||||
->where('status', 'open')
|
||||
->orderBy('posted_at', 'DESC')
|
||||
->orderBy('created_at', 'DESC')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
public function adminPositions(): array
|
||||
{
|
||||
return $this
|
||||
->orderBy('updated_at', 'DESC')
|
||||
->orderBy('created_at', 'DESC')
|
||||
->findAll();
|
||||
}
|
||||
}
|
||||
|
||||
+41
-9
@@ -230,6 +230,28 @@
|
||||
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;
|
||||
@@ -269,9 +291,7 @@
|
||||
<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 Page</a>
|
||||
</div>
|
||||
<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>
|
||||
@@ -327,24 +347,36 @@
|
||||
<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 bellow are taking 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['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 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']) ?>">View Details <i class="fa fa-arrow-right"></i></a>
|
||||
</article>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
<div class="container-fluid mt-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h2>Open Positions</h2>
|
||||
<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>
|
||||
@@ -13,18 +13,28 @@
|
||||
<?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>
|
||||
<thead><tr><th>Title</th><th>Department</th><th>Location</th><th>Type</th><th>Status</th><th>Post Date</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><?= esc(ucfirst($position['status'])) ?></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>
|
||||
<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'): ?>
|
||||
<?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>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
<?= $this->extend('layout/main_layout') ?>
|
||||
<?= $this->extend('layout/careers_layout') ?>
|
||||
<?= $this->section('styles') ?>
|
||||
<style>
|
||||
body {
|
||||
background-color: var(--sage);
|
||||
}
|
||||
|
||||
.job-apply-page {
|
||||
max-width: 920px;
|
||||
margin: 0 auto;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?= $this->extend('layout/main_layout') ?>
|
||||
<?= $this->extend('layout/careers_layout') ?>
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<div class="container py-5">
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
<?= $this->extend('layout/main_layout') ?>
|
||||
<?= $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 {
|
||||
@@ -89,6 +93,7 @@ $renderPostingText = static function (?string $text): string {
|
||||
<?= 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; ?>
|
||||
</p>
|
||||
<h2 class="h4 mt-4">Description</h2>
|
||||
<div><?= $renderPostingText($position['description'] ?? '') ?></div>
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
background: #fff;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.volunteer-opening-card + .volunteer-opening-card {
|
||||
@@ -26,6 +27,28 @@
|
||||
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() ?>
|
||||
|
||||
@@ -239,8 +262,13 @@
|
||||
$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>
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user