= esc($position['title']) ?>
- --
-
-
- = esc(preg_replace('/^\s*-\s*/', '', $requirement)) ?> - -
= esc($descriptionPreview) ?>
View Detailsdiff --git a/app/Controllers/View/JobPostingController.php b/app/Controllers/View/JobPostingController.php index 142ec64..e60cc8b 100644 --- a/app/Controllers/View/JobPostingController.php +++ b/app/Controllers/View/JobPostingController.php @@ -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.'); } diff --git a/app/Database/Migrations/2026-09-01-000100_CreateJobPostings.php b/app/Database/Migrations/2026-09-01-000100_CreateJobPostings.php index 0006a78..c7b3cdf 100644 --- a/app/Database/Migrations/2026-09-01-000100_CreateJobPostings.php +++ b/app/Database/Migrations/2026-09-01-000100_CreateJobPostings.php @@ -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], ]); diff --git a/app/Database/Migrations/2026-09-03-000120_AddPostedAtToJobPositions.php b/app/Database/Migrations/2026-09-03-000120_AddPostedAtToJobPositions.php new file mode 100644 index 0000000..269fe0f --- /dev/null +++ b/app/Database/Migrations/2026-09-03-000120_AddPostedAtToJobPositions.php @@ -0,0 +1,38 @@ +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'); + } + } +} diff --git a/app/Database/Migrations/2026-09-03-000130_BackfillPostedAtForClosedFilledJobPositions.php b/app/Database/Migrations/2026-09-03-000130_BackfillPostedAtForClosedFilledJobPositions.php new file mode 100644 index 0000000..13d1f9f --- /dev/null +++ b/app/Database/Migrations/2026-09-03-000130_BackfillPostedAtForClosedFilledJobPositions.php @@ -0,0 +1,26 @@ +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. + } +} diff --git a/app/Models/JobPositionModel.php b/app/Models/JobPositionModel.php index ca671d5..1ac4640 100644 --- a/app/Models/JobPositionModel.php +++ b/app/Models/JobPositionModel.php @@ -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(); } } diff --git a/app/Views/careers.php b/app/Views/careers.php index f8236bc..417a2b9 100644 --- a/app/Views/careers.php +++ b/app/Views/careers.php @@ -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 @@