Handover workspace

ERS, Todo, OfferReview, and Docu in one view

Imported from live server docs, code structure, and deployment notes.

Apr 3, 2026, 12:38 PM

ERS

Employee Renewal System (ERS) — v0.1 Spec (Code-Ready)

**Version:** 0.1.0 **Stack:** Postgres + Next.js (App Router) + VPS (Hostinger) + local filesystem uploads **Auth:** Email + Password (no 2FA) **Notifications:** In-app + Email **Audit Log:** Included **Admin Settings:** Included **UI Footer:** Show app version on every page

docs/specs/ERS-v0.1-spec.md

Updated Feb 20, 2026, 7:24 AM

Employee Renewal System (ERS) — v0.1 Spec (Code-Ready)

Version: 0.1.0
Stack: Postgres + Next.js (App Router) + VPS (Hostinger) + local filesystem uploads
Auth: Email + Password (no 2FA)
Notifications: In-app + Email
Audit Log: Included
Admin Settings: Included
UI Footer: Show app version on every page


1) Goal

Internal system to manage employee contract renewals:

  • HR uploads renewal packet (offer + performance snapshot)
  • Manager submits renewal justification when requested
  • CEO reviews, compares compensation vs peers (same grade), then approves/rejects
  • CEO can digitally sign the offer letter by placing a signature onto the PDF

Primary outcomes:

  • Track renewals, salary, grade
  • Compare salary vs peers at same grade
  • Capture justification and decision
  • Full audit trail

2) Roles & Permissions (RBAC)

Roles

  • CEO (Admin): full access; final decision; signature management
  • HR: create & manage renewal cases, upload docs, request manager justification, email notifications
  • Manager: respond to assigned justification requests; view assigned packets

Permission Rules (hard)

  • HR & Manager cannot edit a case after CEO decision (locked_at set).
  • After CEO signs the offer (status=SIGNED), case is closed for all roles (read-only).
  • CEO decision requires:
    • Latest OFFER_LETTER exists
    • PerformanceSnapshot exists
    • ManagerJustification exists (v0 hard rule)

3) Key Entities (Data Model)

3.1 User

  • id (uuid)
  • email (unique)
  • name
  • role (enum: CEO | HR | MANAGER)
  • password_hash
  • is_active (bool default true)
  • created_at, updated_at

3.2 Employee

  • id (uuid)
  • employee_code (unique, optional)
  • full_name
  • email (unique, optional)
  • department
  • role_title
  • grade (free text e.g. "G5")
  • manager_user_id (fk users.id)
  • employment_type (enum: PERMANENT | CONTRACT)
  • status (enum: ACTIVE | INACTIVE)
  • created_at, updated_at

3.3 RenewalCase

  • id (uuid)
  • employee_id (fk)
  • contract_start_date, contract_end_date
  • renewal_start_date, renewal_end_date
  • current_monthly_salary (numeric)
  • proposed_monthly_salary (numeric)
  • currency (default "MYR")
  • proposed_grade (string nullable) // free text; if null use employee.grade
  • status (enum):
    • DRAFT
    • PENDING_MANAGER
    • READY_FOR_CEO
    • APPROVED
    • REJECTED
    • SIGNED
  • justification_request_sent_at (timestamptz nullable)
  • manager_justification_due_at (timestamptz nullable)

CEO decision:

  • ceo_decision (enum: NONE | APPROVE | REJECT)
  • ceo_decision_at (timestamptz nullable)
  • ceo_decision_notes (text nullable)

Locking:

  • locked_at (timestamptz nullable)

Meta:

  • created_by_user_id (fk users.id)
  • created_at, updated_at

3.4 PerformanceSnapshot (per RenewalCase)

  • id (uuid)
  • renewal_case_id (fk)
  • review_period (string e.g. "2025 H2")
  • overall_score (numeric nullable)
  • score_breakdown (jsonb nullable)
  • hr_notes (text nullable)
  • manager_comments (text nullable)
  • created_at, updated_at

3.5 ManagerJustification (one per RenewalCase)

  • id (uuid)
  • renewal_case_id (fk unique)
  • manager_user_id (fk users.id nullable for guest submissions)
  • recommendation (enum: RENEW | DO_NOT_RENEW | RENEW_WITH_CONDITIONS)
  • justification (text)
  • conditions (text nullable)
  • submitted_at (timestamptz nullable)
  • submitted_by_name (text nullable)
  • submitted_by_email (text nullable)
  • last_updated_at (timestamptz)

3.5b GuestJustificationToken (one-time external link)

  • id (uuid)
  • renewal_case_id (fk)
  • token_hash (unique)
  • manager_name (nullable)
  • manager_email
  • created_by_user_id (fk users.id)
  • expires_at
  • used_at (nullable)
  • revoked_at (nullable)
  • created_at

3.6 Document (versioned)

  • id (uuid)
  • renewal_case_id (fk)
  • doc_type (enum: OFFER_LETTER | SIGNED_OFFER | PERFORMANCE_REPORT | OTHER)
  • file_name
  • mime_type
  • file_size
  • storage_key (string) // relative path in VPS filesystem
  • uploaded_by_user_id (fk users.id)
  • uploaded_at (timestamptz)
  • version (int default 1)
  • is_latest (bool default true)

Versioning rule:

  • Uploading same (renewal_case_id + doc_type) again:
    • set previous is_latest=false
    • increment version

3.7 EmployeeCompensation (for peer comparison)

We start empty and fill over time from approved renewals.

  • id (uuid)
  • employee_id (fk)
  • effective_date (date)
  • monthly_salary (numeric)
  • grade (string)
  • source (enum: RENEWAL_APPROVED | MANUAL)
  • renewal_case_id (fk nullable)
  • created_at

Creation rule:

  • When CEO approves a renewal case:
    • insert EmployeeCompensation using proposed salary + effective_date=renewal_start_date
    • grade = proposed_grade ?? employee.grade

3.8 InAppNotification

  • id (uuid)
  • user_id (fk)
  • type (enum: JUSTIFICATION_REQUESTED | CASE_READY_FOR_CEO | CEO_DECIDED | CASE_SIGNED)
  • title (string)
  • body (text)
  • link_path (string) // e.g. /manager/requests/{id}
  • is_read (bool default false)
  • created_at

3.9 EmailOutbox (durable outbound queue)

  • id (uuid)
  • to_email
  • subject
  • body_html
  • body_text
  • status (enum: PENDING | SENT | FAILED)
  • provider_message_id (string nullable)
  • error_message (text nullable)
  • attempts (int default 0)
  • last_attempt_at (timestamptz nullable)
  • created_at

3.10 AuditLog (required)

  • id (uuid)
  • actor_user_id (fk)
  • entity_type (enum: RENEWAL_CASE | DOCUMENT | JUSTIFICATION | SETTINGS | EMPLOYEE | USER | SIGNATURE | NOTIFICATION | EMAIL)
  • entity_id (uuid)
  • action (string)
  • before (jsonb nullable)
  • after (jsonb nullable)
  • created_at (timestamptz)

Minimum audited actions:

  • CREATE_CASE, UPDATE_CASE, REQUEST_JUSTIFICATION, SUBMIT_JUSTIFICATION
  • UPLOAD_DOC
  • CEO_APPROVE, CEO_REJECT
  • SIGNATURE_UPLOAD, PDF_SIGNED
  • SETTINGS_UPDATE

3.11 AdminSettings (keyed settings)

  • id (uuid)
  • key (string unique)
  • value_json (jsonb)
  • updated_by_user_id (fk users.id)
  • updated_at

Must include keys (v0):

  • app_version: { "value": "0.1.0" }
  • company_name
  • upload_max_mb
  • email_from
  • smtp (optional to store; can be env-only)
  • manager_justification_default_due_days

4) Digital Signature (PDF signing) — v0 Implementation Plan

4.1 Signature Asset

Store CEO signature as PNG with transparency.

Recommended:

  • user_signature table:
    • id (uuid)
    • user_id (fk users.id unique)
    • file_name, mime_type, file_size, storage_key
    • created_at, updated_at

4.2 Signing UX

  • CEO opens renewal case > Offer Letter preview
  • Click “Sign”
  • PDF preview with draggable signature overlay
  • CEO clicks “Apply Signature”
  • Frontend sends { pageNumber, x, y, width, height } in PDF points

Coordinate conversion:

  • Convert DOM px coords to PDF points
  • PDF origin is bottom-left; DOM origin is top-left

4.3 Backend stamping

Use pdf-lib server-side:

  • Load OFFER_LETTER
  • Embed signature PNG
  • Draw on given page coords
  • Save new PDF => SIGNED_OFFER

Rule:

  • Must APPROVE before SIGN
    • approve => status=APPROVED
    • sign => create SIGNED_OFFER + status=SIGNED

5) Core Workflows

5.1 HR creates renewal

  • Create RenewalCase (DRAFT)
  • Upload OFFER_LETTER + PERFORMANCE_REPORT (optional)
  • Upsert PerformanceSnapshot
  • Request Manager Justification => status=PENDING_MANAGER + notify manager (in-app + email)

5.2 Manager justification

  • Manager opens “My Requests”
  • Submits recommendation + justification
  • Alternative: HR sends one-time guest link; external manager can submit without account
  • HR marks READY_FOR_CEO => notify CEO (in-app + email)

5.3 CEO decision

  • Approve => status=APPROVED + locked_at + insert EmployeeCompensation + notify HR/manager
  • Reject => status=REJECTED + locked_at + notify HR/manager

5.4 CEO signs PDF (after approval)

  • Stamp one or more signatures across pages => SIGNED_OFFER + status=SIGNED
  • On successful sign, case is closed (no further edits)
  • Notify HR users (in-app + email via outbox)

6) UI Pages

  • /login
  • /hr/renewals, /hr/renewals/new, /hr/renewals/[id]
  • /manager/requests, /manager/requests/[id]
  • /ceo/renewals, /ceo/renewals/[id], /ceo/signature
  • /guest/justification/[token] (public one-time manager link)
  • /admin/settings, /admin/audit, /admin/users
  • /notifications

Footer on every page:

  • ERS v{app_version}

7) API Routes (high-level)

Auth:

  • POST /api/auth/login, POST /api/auth/logout, GET /api/auth/me

Employees:

  • GET/POST /api/employees
  • GET/PATCH /api/employees/:id

Renewals:

  • GET/POST /api/renewals
  • GET/PATCH /api/renewals/:id
  • POST /api/renewals/:id/request-justification
  • POST /api/renewals/:id/mark-ready
  • POST /api/renewals/:id/approve
  • POST /api/renewals/:id/reject

Documents:

  • POST /api/renewals/:id/documents
  • GET /api/documents/:docId/download

Performance snapshot:

  • GET/POST /api/renewals/:id/performance-snapshot

Justification:

  • GET /api/renewals/:id/justification
  • POST/PATCH /api/renewals/:id/justification
  • GET/POST /api/guest/justification/:token

Signing:

  • POST/GET /api/ceo/signature
  • POST /api/renewals/:id/sign-pdf

Notifications:

  • GET /api/notifications
  • POST /api/notifications/:id/read

Admin:

  • GET/PATCH /api/admin/settings
  • GET /api/admin/audit
  • GET/POST /api/admin/users
  • PATCH /api/admin/users/:id

Internal:

  • POST /api/internal/email/dispatch (secret-protected)

8) Storage Plan (VPS filesystem)

Base: /var/app/ers_uploads/

  • documents: documents/{renewal_case_id}/{doc_type}/v{version}/{filename}
  • signatures: signatures/{user_id}/{filename}

Store relative path in DB storage_key.


9) Definition of Done (v0)

  • HR can create packet end-to-end
  • Manager can submit justification
  • CEO can view peer stats, approve/reject, and sign PDF
  • In-app + email notifications work via outbox + dispatch endpoint
  • Audit log populated
  • Footer shows version everywhere