Files
alrahma_sunday_school/docs/Job_postings_feature_plan.md
root 8d644a2c85
Deploy to Shared Hosting / Shared hosting deploy (push) Failing after 47s
Tests / PHPUnit (push) Successful in 1m21s
organize doc files and remove unecessary files
2026-09-02 23:24:43 -04:00

10 KiB

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

-- 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)

-- 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

-- 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

-- 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)

-- 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?