Skip to content

Latest commit

 

History

History
662 lines (443 loc) · 28.6 KB

File metadata and controls

662 lines (443 loc) · 28.6 KB

feature-x.md — Employee Management System with RBAC

Source plan: EMS RBAC Plan. This file is the executable, per-step breakdown. Each step is scoped to < 200 LOC of net new code. Implementation order matters: every step lists the prior steps it depends on under Blockers.


0. Conventions

  • Repo layout (npm workspaces):
    /
    ├── package.json            # workspace root
    ├── feature-x.md
    ├── README.md
    ├── docker-compose.yml      # added in Step 25
    ├── backend/                # Node.js + Express + Mongoose
    └── frontend/               # Next.js (App Router)
    
  • Languages: TypeScript on both sides (strict: true).
  • Branching: one feature branch per phase (e.g. phase-2-auth), one commit per step.
  • LOC counting rule: net new lines of source code (.ts, .tsx, .js). Generated files (lockfiles, next-env.d.ts, shadcn primitives) and config JSON do not count toward the 200-LOC budget.
  • Roles (single source of truth, frozen in Step 5):
    • ADMIN — full power.
    • HR — manage employees (cannot escalate roles), payroll, onboarding, leave.
    • MANAGER — view team, approve team leaves, conduct team reviews.
    • EMPLOYEE — self-service only.
  • Env vars (consolidated):
    • backend/.env: PORT, MONGO_URI, JWT_ACCESS_SECRET, JWT_REFRESH_SECRET, ACCESS_TOKEN_TTL=15m, REFRESH_TOKEN_TTL=7d, CORS_ORIGIN, NODE_ENV.
    • frontend/.env.local: NEXT_PUBLIC_API_URL.

Phase 1 — Foundation

Step 1 — Monorepo scaffolding

Goal: Establish the workspace so subsequent steps can install into backend/ and frontend/ independently.

Files to create / change:

  • package.json (root) — "workspaces": ["backend", "frontend"], root scripts (dev, lint, test).
  • .gitignore — node_modules, .env, .next, dist, coverage.
  • .editorconfig — 2-space indent, LF, final newline.
  • README.md — quickstart (clone, npm install, npm run dev).
  • .nvmrc — pin Node 20 LTS.

Blockers: none.

Tests:

  • npm install at root completes with no errors.
  • npm run -w backend --silent and npm run -w frontend --silent both list no scripts (placeholder dirs OK).

Acceptance: Workspace bootstraps cleanly, root node_modules exists, no app code yet.

LOC budget: ~40.


Step 2 — Backend skeleton

Goal: Bootable Express server with MongoDB connection and a health endpoint.

Files:

  • backend/package.json — deps: express, mongoose, cors, morgan, dotenv. Dev: typescript, tsx, @types/*, eslint.
  • backend/tsconfig.json — strict, target ES2022, outDir: dist.
  • backend/src/server.ts — boots app on PORT.
  • backend/src/app.ts — Express instance, JSON/CORS/morgan, mounts /api/health.
  • backend/src/config/db.tsconnectDb() with retry log.
  • backend/src/config/env.ts — typed env loader (throws on missing).
  • backend/.env.example — all keys with placeholder values.

Blockers: Step 1.

Tests:

  • npm run -w backend dev starts and logs Mongo connected against a local MongoDB.
  • curl localhost:4000/api/health{ "ok": true }.

Acceptance: Server hot-reloads via tsx watch, crashes loudly if envs are missing.

LOC budget: ~120.


Step 3 — Core models + seed

Goal: Define User and Employee schemas; seed an admin so we can log in once auth lands.

Files:

  • backend/src/models/User.tsemail (unique), passwordHash, role enum, employeeId ref, timestamps; pre-save bcrypt hook in Step 4 (placeholder here is just the schema).
  • backend/src/models/Employee.ts — name, designation, department, managerId (self-ref), dateOfJoining, status (active|onboarding|terminated), contact, address (subdoc).
  • backend/src/types/roles.tsexport const ROLES = ['ADMIN','HR','MANAGER','EMPLOYEE'] as const; export type Role = typeof ROLES[number];
  • backend/src/scripts/seed.ts — idempotent: creates default admin from env, plus 3 demo employees (HR/Manager/Employee).
  • backend/package.json — add "seed": "tsx src/scripts/seed.ts".

Blockers: Step 2.

Tests:

  • npm run -w backend seed twice — second run is a no-op (no duplicates).
  • Mongo shell: db.users.findOne({role:'ADMIN'}) returns the seed admin.

Acceptance: Models compile under strict TS; seed produces a known fixture set used by later test plans.

LOC budget: ~160.


Phase 2 — Auth & RBAC

Step 4 — Auth endpoints (JWT + refresh)

Goal: /auth/register (admin-gated, deferred enforcement to Step 5), /auth/login, /auth/refresh, /auth/logout, /auth/me.

Files:

  • backend/src/controllers/authController.ts — login (bcrypt compare), issue access token (15 m) + refresh token (7 d) as httpOnly Secure SameSite=Lax cookie.
  • backend/src/routes/auth.ts — wires controller, mounted at /api/auth in app.ts.
  • backend/src/utils/jwt.tssignAccess, signRefresh, verify* helpers.
  • backend/src/utils/password.tshash(), compare() thin wrappers around bcrypt.
  • backend/src/models/User.ts — finalize pre-save bcrypt hook + comparePassword() instance method.
  • backend/src/middleware/cookie.ts — sets/clears refresh cookie consistently.

Blockers: Step 3.

Tests (manual + supertest skeleton in Step 25):

  • POST /api/auth/login with seed admin → 200, body has accessToken, Set-Cookie: refresh=...; HttpOnly.
  • POST /api/auth/refresh with cookie → new access token; without cookie → 401.
  • Wrong password → 401, generic error message (no user enumeration).

Acceptance: Tokens are signed with separate secrets; refresh rotation invalidates old refresh on next refresh (jti tracked on user doc).

LOC budget: ~180.


Step 5 — RBAC middleware + permission map

Goal: Centralized authorization. Every protected route uses one of three guards.

Files:

  • backend/src/middleware/auth.tsrequireAuth (verifies access token, attaches req.user).
  • backend/src/middleware/rbac.tsrequireRole(...roles), requireSelfOrRole(getOwnerId, ...roles).
  • backend/src/permissions.ts — declarative matrix:
    export const can = {
      employee: { create:['ADMIN'], readAny:['ADMIN','HR','MANAGER'], updateAny:['ADMIN','HR'], delete:['ADMIN'] },
      leave:    { approve:['HR','MANAGER'], applySelf:['*'] },
      payroll:  { generate:['ADMIN','HR'], readAny:['ADMIN','HR'] },
      review:   { conductTeam:['MANAGER','HR','ADMIN'], finalize:['HR','ADMIN'] },
    } as const;
  • backend/src/routes/auth.ts — protect /auth/register with requireRole('ADMIN') retroactively.

Blockers: Step 4.

Tests:

  • Seed admin can hit /api/auth/register; non-admin gets 403.
  • Missing/expired token → 401 with WWW-Authenticate: Bearer header.
  • Unit test the permission map covers every role × every resource action.

Acceptance: No route after this step uses inline role checks — only the guards from this file.

LOC budget: ~140.


Step 6 — Frontend scaffold (Next.js + Tailwind + shadcn/ui)

Goal: Empty Next.js app boots, talks to backend through a typed client, has a base layout and theme.

Files:

  • frontend/package.json — Next 14+ App Router, tailwindcss, class-variance-authority, clsx, lucide-react, @tanstack/react-query, axios, sonner, zod, react-hook-form.
  • frontend/tailwind.config.ts, postcss.config.js, app/globals.css — shadcn defaults.
  • frontend/components.json — shadcn config (NOT counted toward LOC).
  • frontend/src/app/layout.tsx — root layout, fonts, <ThemeProvider>, <QueryProvider>, <Toaster>.
  • frontend/src/app/page.tsx — landing redirects to /login or /dashboard based on auth.
  • frontend/src/lib/api.ts — axios instance, request interceptor injects access token, response interceptor calls /auth/refresh on 401 once.
  • frontend/src/lib/query-provider.tsx — TanStack Query client.
  • frontend/src/lib/theme-provider.tsx — next-themes.

Blockers: Step 2 (API URL exists), Step 5 nice-to-have for refresh-on-401 to actually work end-to-end.

Tests:

  • npm run -w frontend dev opens localhost:3000 with the landing redirect.
  • DevTools network tab: a fetch to ${NEXT_PUBLIC_API_URL}/health succeeds (CORS clean).

Acceptance: shadcn Button renders correctly with theme; dark mode toggle wires up but is hidden until Step 24.

LOC budget: ~150 (shadcn primitives generated by CLI do not count).


Step 7 — Auth UI + protected routes

Goal: Login + register pages, persistent auth state, route protection.

Files:

  • frontend/src/app/(auth)/login/page.tsx — react-hook-form + zod, submits to /api/auth/login, stores access token in memory.
  • frontend/src/app/(auth)/register/page.tsx — admin-only flow (gated client-side, enforced server-side).
  • frontend/src/lib/auth-context.tsxuseAuth(): user, accessToken, login(), logout(). Bootstraps via /auth/me on mount.
  • frontend/src/middleware.ts — Next middleware: any path under /dashboard without a refresh cookie redirects to /login.
  • frontend/src/components/ui/* — shadcn input, label, button, form, card (generated).

Blockers: Steps 4, 5, 6.

Tests:

  • Bad credentials → inline error, no token stored.
  • Successful login → redirect to /dashboard, /auth/me returns user.
  • Hard refresh keeps user logged in (refresh cookie + silent refresh on mount).
  • /dashboard without cookie → redirected to /login?next=/dashboard.

Acceptance: Access token never persisted to localStorage. Logout clears cookie via /auth/logout and resets context.

LOC budget: ~190.


Step 8 — Role-aware dashboard shell

Goal: Single dashboard layout that exposes different navigation per role.

Files:

  • frontend/src/app/(dashboard)/layout.tsx — sidebar + topbar + <main>{children}</main>.
  • frontend/src/app/(dashboard)/page.tsx — role-aware home: ADMIN → KPIs, HR → pending approvals, MANAGER → team, EMPLOYEE → my snapshot.
  • frontend/src/components/Sidebar.tsx — items declared with roles: Role[]; filters via useAuth().
  • frontend/src/components/RoleGate.tsx<RoleGate allow={['ADMIN','HR']}>...children...</RoleGate>; renders null otherwise.
  • frontend/src/lib/permissions.ts — mirror of backend permissions.ts (manually kept in sync; called out as a tech-debt item to codegen later).

Blockers: Step 7.

Tests:

  • Log in as each seeded role; sidebar items differ as expected.
  • Manually visiting a hidden URL still works only if backend permits — verified by hitting an HR-only API as an EMPLOYEE and seeing 403.

Acceptance: No screen is visible to a role unless backend would also authorize it.

LOC budget: ~170.


Phase 3 — Employee Management

Step 9 — Employee CRUD API

Goal: REST endpoints scoped by role.

Files:

  • backend/src/routes/employees.tsGET /, GET /:id, POST /, PATCH /:id, DELETE /:id (soft delete).
  • backend/src/controllers/employeeController.ts — pagination (?page&limit), search (?q), filter by department/status.
  • backend/src/services/employeeService.ts — business logic kept out of controller; applies role-scoped filters (EMPLOYEE → only self; MANAGER → only team).
  • backend/src/validators/employee.ts — zod schemas for create/update.

Blockers: Steps 3, 5.

Tests (supertest in Step 25, manual now):

  • ADMIN can list all, HR cannot promote anyone to ADMIN (validator strips/rejects role escalation), EMPLOYEE GET /api/employees/:otherId → 403.
  • Soft delete sets status: 'terminated' and excludes from default list; ?includeTerminated=true requires ADMIN.

Acceptance: Every action goes through requireAuth + a permission guard. No raw find() without a role-scoped filter.

LOC budget: ~190.


Step 10 — Employee list UI

Goal: Browsable, filterable employee directory for ADMIN/HR/MANAGER.

Files:

  • frontend/src/app/(dashboard)/employees/page.tsx — server component shell + client <EmployeeTable>.
  • frontend/src/components/EmployeeTable.tsx — shadcn <Table>, debounced search, pagination, role-aware action menu.
  • frontend/src/hooks/useEmployees.ts — TanStack Query hook (useQuery + keepPreviousData).
  • frontend/src/types/employee.ts — TS type mirroring API response.

Blockers: Steps 8, 9.

Tests:

  • Search "ali" filters in <300 ms perceived latency (debounce 250 ms).
  • EMPLOYEE who reaches this URL by accident sees an empty state ("You don't have access") because backend returns 403 — handled gracefully.
  • Pagination math: 25 employees, 10 per page → 3 pages.

Acceptance: No flashes of unauthorized content; loading state uses skeleton rows.

LOC budget: ~180.


Step 11 — Employee detail / self profile

Goal: Detail page with field-level edit permissions and a /profile self-service page.

Files:

  • frontend/src/app/(dashboard)/employees/[id]/page.tsx — header card + tabs (Profile / Leaves / Payslips / Attendance — tabs lazy-load in later phases).
  • frontend/src/app/(dashboard)/profile/page.tsx — same component reused with id = me.
  • frontend/src/components/EmployeeForm.tsx — react-hook-form, disables fields the role can't edit (e.g., EMPLOYEE can edit phone/address only).
  • frontend/src/lib/fieldPermissions.ts — central map: { field: roles[] }.

Blockers: Step 10.

Tests:

  • Log in as EMPLOYEE → /profile shows phone editable, designation read-only.
  • HR editing another employee can change designation but cannot change role (UI not rendered, server enforces).
  • Optimistic update on save; on server 4xx the form re-syncs.

Acceptance: Field permissions defined in one place; both UI and a server-side test exist for at least 2 sensitive fields.

LOC budget: ~190.


Phase 4 — Leave Management

Step 12 — Leave API

Goal: Apply, list, approve, reject; balances per leave type.

Files:

  • backend/src/models/Leave.tsemployeeId, type (CL/SL/PL), from, to, days (computed), status (pending|approved|rejected|cancelled), reason, decidedBy, decisionNote.
  • backend/src/models/LeaveBalance.tsemployeeId, year, balances: { CL:n, SL:n, PL:n }.
  • backend/src/routes/leaves.tsPOST /, GET /me, GET /pending (HR/Manager), POST /:id/approve, POST /:id/reject, POST /:id/cancel.
  • backend/src/services/leaveService.ts — business day calculator (excludes weekends; holidays out of scope), balance debit on approve, refund on cancel.

Blockers: Steps 5, 9.

Tests:

  • Apply 3-day leave → balance unchanged until approval; on approve → balance decreases by 3.
  • MANAGER can approve only own team (verified via Employee.managerId).
  • Overlapping pending leave → 409 Conflict.

Acceptance: All state transitions are enforced server-side; client cannot fabricate status.

LOC budget: ~195.


Step 13 — Apply / my leaves UI

Goal: Employee can apply and see their leaves.

Files:

  • frontend/src/app/(dashboard)/leaves/apply/page.tsx<LeaveForm> + balance summary card.
  • frontend/src/app/(dashboard)/leaves/page.tsx — list of own leaves, filter by status.
  • frontend/src/components/LeaveForm.tsx — date range picker (shadcn calendar), reason textarea, type select.
  • frontend/src/hooks/useLeaves.tsuseMyLeaves, useApplyLeave.

Blockers: Steps 8, 12.

Tests:

  • Apply with to < from → client-side validation error.
  • After successful apply, list page reflects new pending leave (cache invalidation).
  • Cancelling a pending leave returns balance (visible after refetch).

Acceptance: Empty states for "no leaves yet"; toasts on success/error.

LOC budget: ~170.


Step 14 — Approval queue UI

Goal: HR/Manager queue for pending approvals with inline actions.

Files:

  • frontend/src/app/(dashboard)/leaves/approvals/page.tsx — table of pending leaves, inline Approve / Reject buttons.
  • frontend/src/components/LeaveDecisionDialog.tsx — modal asks for decisionNote, posts to backend.
  • frontend/src/hooks/useLeaves.ts — extend with usePendingLeaves, useDecideLeave.

Blockers: Steps 8, 12, 13.

Tests:

  • MANAGER queue lists only direct reports.
  • HR queue lists everyone's pending (verified by counts).
  • After Approve, row disappears, my-leaves view as that employee shows status=approved.

Acceptance: Optimistic removal from queue; rollback on server error.

LOC budget: ~150.


Phase 5 — Salary Slips

Step 15 — Payroll API + PDF

Goal: Define salary structure, generate monthly payslip records, stream PDF on demand.

Files:

  • backend/src/models/SalaryStructure.tsemployeeId, effectiveFrom, basic, hra, allowances[], deductions[].
  • backend/src/models/Payslip.tsemployeeId, month (YYYY-MM), gross, deductions, net, lines[], generated pdfPath (or stream-only).
  • backend/src/routes/payroll.tsPOST /run (ADMIN/HR; idempotent per employee/month), GET /me?month=, GET /:id/pdf (streams).
  • backend/src/services/payslipPdf.tspdfkit writer: header, employee block, earnings table, deductions table, net, signature line.
  • backend/package.json — add pdfkit, @types/pdfkit.

Blockers: Steps 5, 9.

Tests:

  • Running for the same month twice produces no duplicate rows (upsert on (employeeId, month)).
  • EMPLOYEE can GET /me?month=2026-04 only for self.
  • GET /:id/pdf streams application/pdf with Content-Disposition: attachment.

Acceptance: Net = gross − deductions, asserted in service unit test.

LOC budget: ~195.


Step 16 — Payroll UI

Goal: Admin run + employee view/download.

Files:

  • frontend/src/app/(dashboard)/payroll/page.tsx — month picker, "Run payroll" button (ADMIN/HR only via RoleGate), result table.
  • frontend/src/app/(dashboard)/my-payslips/page.tsx — list of own payslips with Download button.
  • frontend/src/components/PayslipPreview.tsx — embeds the streamed PDF in <iframe> for quick view.
  • frontend/src/hooks/usePayroll.tsuseRunPayroll, useMyPayslips, useDownloadPayslip.

Blockers: Steps 8, 15.

Tests:

  • Non-privileged user cannot see Run button (RoleGate) and gets 403 if calling API directly.
  • Download triggers browser save dialog with correct filename payslip-2026-04-<empCode>.pdf.

Acceptance: Loading + empty + error states all rendered; never blank screen.

LOC budget: ~160.


Phase 6 — Attendance

Step 17 — Attendance API

Goal: Daily check-in/out, monthly summary, regularization request.

Files:

  • backend/src/models/Attendance.tsemployeeId, date (YYYY-MM-DD), checkIn, checkOut, status (present|absent|half-day|leave), regularization?: { requestedAt, reason, decidedBy?, status }.
  • backend/src/routes/attendance.tsPOST /check-in, POST /check-out, GET /me?month=, GET /summary?month= (HR/MANAGER), POST /:id/regularize, POST /:id/regularize/decide.
  • backend/src/services/attendanceService.ts — derives status from times; integrates with leave (an approved leave on that date sets status: leave).

Blockers: Steps 5, 12.

Tests:

  • Two check-ins on the same day → second is rejected (409).
  • Check-out without check-in → 400.
  • Approved leave on Friday auto-fills attendance row as leave.

Acceptance: Date math is timezone-stable (server stores UTC, derives local day from configured TZ env, default Asia/Kolkata; document this).

LOC budget: ~190.


Step 18 — Attendance UI

Goal: Dashboard widget + monthly calendar.

Files:

  • frontend/src/components/AttendanceWidget.tsx — shows today's status, primary action button (Check in / Check out / Done).
  • frontend/src/app/(dashboard)/attendance/page.tsx — month grid (shadcn or custom div grid), color-coded cells, click to view detail and request regularization.
  • frontend/src/hooks/useAttendance.tsuseToday, useMonth, mutations.

Blockers: Steps 8, 17.

Tests:

  • Widget transitions Check in → Check out → Done within one session without reload.
  • Calendar correctly merges leave days from leave system.
  • Regularization request shows "pending decision" badge.

Acceptance: Widget is the only place mutations happen on the dashboard home; calendar is read+request only.

LOC budget: ~180.


Phase 7 — Onboarding Workflow

Step 19 — Onboarding API

Goal: Templates of tasks, auto-assigned on hire; employee marks complete.

Files:

  • backend/src/models/OnboardingTemplate.tsname, tasks: [{ title, description, dueDays, assigneeRole?: 'EMPLOYEE'|'HR' }].
  • backend/src/models/OnboardingTask.ts — instance per employee: templateRef, employeeId, title, dueDate, status (pending|done|skipped), completedAt.
  • backend/src/routes/onboarding.ts — template CRUD (ADMIN/HR), POST /assign/:employeeId (auto-fires when employee is created with status: onboarding), GET /me, POST /tasks/:id/complete.
  • backend/src/services/onboardingService.ts — assign-on-hire helper, called from employeeService.createEmployee.

Blockers: Steps 5, 9.

Tests:

  • Creating an employee with status: onboarding auto-creates tasks from the default template.
  • Employee can only complete own tasks; HR can complete any.
  • Skipping a task is a separate action (audit-friendly).

Acceptance: At least one default template seeded in Step 3 update (acceptable retroactive change to seed script).

LOC budget: ~180.


Step 20 — Onboarding UI

Goal: Employee checklist + admin template editor.

Files:

  • frontend/src/app/(dashboard)/onboarding/page.tsx — for EMPLOYEE: checklist with progress bar; for HR/ADMIN: list of onboarding employees with progress.
  • frontend/src/app/(dashboard)/onboarding/templates/page.tsx — ADMIN/HR template editor (CRUD + reorder tasks).
  • frontend/src/components/OnboardingChecklist.tsx — checklist with optimistic toggle.
  • frontend/src/hooks/useOnboarding.ts.

Blockers: Steps 8, 19.

Tests:

  • Toggling a task immediately moves the progress bar; API failure rolls back.
  • Template editor preserves task order on save (uses array indices).
  • An employee with all tasks done: HR sees a "Complete onboarding" CTA which flips employee status to active.

Acceptance: No template can be deleted while in use (server-side guard verified by UI error toast).

LOC budget: ~190.


Phase 8 — Performance Reviews

Step 21 — Reviews API

Goal: Per-cycle reviews with a state machine: draft → self_submitted → manager_submitted → finalized.

Files:

  • backend/src/models/ReviewCycle.tsname, period (e.g. H1 2026), openFrom, openTo, status.
  • backend/src/models/Review.tscycleId, employeeId, managerId, selfRatings, selfComments, managerRatings, managerComments, finalRating, status.
  • backend/src/routes/reviews.ts — cycle CRUD (HR/ADMIN), GET /me?cycleId=, POST /me/submit-self, GET /team?cycleId= (MANAGER), POST /:id/submit-manager, POST /:id/finalize (HR/ADMIN).
  • backend/src/services/reviewService.ts — state-machine guard: each transition validates current status.

Blockers: Steps 5, 9.

Tests:

  • A MANAGER cannot submit until employee submits self.
  • HR cannot finalize before manager submits.
  • Re-finalizing a finalized review returns 409.

Acceptance: Every state transition recorded with decidedBy + timestamp on the review doc.

LOC budget: ~195.


Step 22 — Reviews UI

Goal: Self review form, manager view, finalization for HR.

Files:

  • frontend/src/app/(dashboard)/reviews/page.tsx — role-aware: EMPLOYEE sees self review for active cycle; MANAGER sees team list; HR sees all + finalize.
  • frontend/src/components/ReviewForm.tsx — same form rendered with different mode prop (self|manager|finalize).
  • frontend/src/hooks/useReviews.ts.

Blockers: Steps 8, 21.

Tests:

  • Submitted self review becomes read-only for the employee.
  • Manager view hides ratings until self is submitted (server returns 403 + UI shows waiting state).
  • Finalize action shows confirmation dialog; success shows a final summary card.

Acceptance: One single <ReviewForm> for all three modes — no duplicated rating UI.

LOC budget: ~190.


Phase 9 — Polish & Hardening

Step 23 — Backend hardening + audit log

Goal: Production-grade baseline: validation, rate limit, security headers, audit trail.

Files:

  • backend/src/middleware/validate.ts — generic zod request validator.
  • backend/src/validators/*.ts — fill in any remaining (leave.ts, payroll.ts, attendance.ts, review.ts).
  • backend/src/middleware/rateLimit.tsexpress-rate-limit instance (tighter on /auth/*).
  • backend/src/middleware/audit.ts — wraps mutating routes; logs actor, action, targetType, targetId, before, after (diff) into AuditLog.
  • backend/src/models/AuditLog.ts.
  • backend/src/app.ts — wire helmet(), rateLimit, audit.

Blockers: All earlier backend steps.

Tests:

  • /api/auth/login blocks after 10 attempts/min from same IP.
  • An HR editing an employee creates one AuditLog row with non-empty before/after diff.
  • Helmet sets X-Frame-Options, Strict-Transport-Security (in prod).

Acceptance: A privileged read-only route GET /api/audit?targetId= exists for ADMIN.

LOC budget: ~180.


Step 24 — Frontend polish

Goal: Toasts, skeletons, error boundary, dark mode, empty states.

Files:

  • frontend/src/app/error.tsx, frontend/src/app/not-found.tsx.
  • frontend/src/components/EmptyState.tsx, Skeletons.tsx.
  • frontend/src/components/ThemeToggle.tsx — surface toggle in topbar.
  • Sweep existing pages (employees/leaves/payroll/attendance/onboarding/reviews) to use <Skeletons> and <EmptyState>. Each touch is 5–15 LOC.

Blockers: All earlier frontend steps.

Tests:

  • Throw an error in a page locally → error.tsx renders with retry.
  • Toggle dark mode → persists across reload (next-themes).
  • All pages have a non-blank state for loading, empty, and error.

Acceptance: Lighthouse Accessibility ≥ 95 on /dashboard and /login.

LOC budget: ~190 (the per-page sweep is the bulk; if it exceeds budget, split into 24a / 24b).


Step 25 — Tests + Docker + Deploy README

Goal: Repeatable test suites and a one-command local stack.

Files:

  • backend/jest.config.js, backend/src/__tests__/auth.spec.ts, backend/src/__tests__/rbac.spec.ts, backend/src/__tests__/leaves.spec.ts, backend/src/__tests__/payroll.spec.ts.
    • Use mongodb-memory-server for isolated DB.
    • Cover the role × action matrix from Step 5 with table-driven tests.
  • frontend/playwright.config.ts, frontend/e2e/auth.spec.ts, frontend/e2e/rbac.spec.ts — login flow + 1 unauthorized-access flow per role.
  • docker-compose.ymlmongo, backend, frontend services with health checks.
  • backend/Dockerfile, frontend/Dockerfile.
  • README.md (update) — quickstart, env vars, docker compose up, default seeded credentials, screenshots placeholder.

Blockers: All earlier steps.

Tests:

  • npm test -w backend passes locally (CI optional).
  • npm run e2e -w frontend passes against the docker-compose stack.
  • docker compose up brings the app up and /api/health is reachable from the frontend container.

Acceptance: Fresh clone → cp backend/.env.example backend/.env && docker compose up && npm run -w backend seed → can log in as admin/hr/employee from the UI.

LOC budget: ~190 (excluding generated Playwright config and Dockerfile boilerplate).


Risk Register

Risk Mitigation Step
Permission drift between FE and BE Single source permissions.ts on backend; FE imports a hand-mirrored copy with a TODO to codegen 5, 8
PDF generation memory pressure pdfkit streams; no buffering whole doc 15
Token theft via XSS Access token in memory only; refresh in httpOnly cookie 4, 7
Refresh-cookie CSRF SameSite=Lax + only /auth/refresh reads it; consider double-submit token if SameSite=None ever needed 4
Timezone bugs in attendance UTC storage + explicit TZ config; tests exercise day boundaries 17
Role escalation via PATCH Validators strip role from non-ADMIN payloads 5, 9

Done Criteria for the Whole Feature

  1. All 25 steps merged; each commit independently boots/runs.
  2. Seed admin can perform every action defined in the permission matrix.
  3. A seeded EMPLOYEE can complete a full month: onboarding → check-in/out daily → apply leave → view payslip → submit self review.
  4. npm test (backend) and npm run e2e (frontend) both green.
  5. docker compose up from a fresh clone serves the app on localhost:3000.