fix positions
Deploy to Shared Hosting / Shared hosting deploy (push) Failing after 54s
Tests / PHPUnit (push) Successful in 1m22s

This commit is contained in:
root
2026-09-03 03:21:37 -04:00
parent 6936a822c8
commit c70f6bdc6e
12 changed files with 344 additions and 18 deletions
+13 -1
View File
@@ -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');
}
}
}
@@ -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.
}
}
+14 -1
View File
@@ -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
View File
@@ -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'])): ?> &middot; <?= esc($position['location']) ?><?php endif; ?>
<?php if (!empty($position['employment_type'])): ?> &middot; <?= 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>
+14 -4
View File
@@ -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']))) : '&mdash;' ?></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>
+5 -1
View File
@@ -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 -1
View File
@@ -1,4 +1,4 @@
<?= $this->extend('layout/main_layout') ?>
<?= $this->extend('layout/careers_layout') ?>
<?= $this->section('content') ?>
<div class="container py-5">
+6 -1
View File
@@ -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'])): ?> &middot; <?= esc($position['location']) ?><?php endif; ?>
<?php if (!empty($position['employment_type'])): ?> &middot; <?= 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>
+157
View File
@@ -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>