geocrop-platform./apps/nextgen/.harness/changelogs/2026-07-04-phase1b.md

129 lines
7.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# HR/Finance Phase 1b — Workflow loops + the §3 pages Sprint 1a didn't ship
> Plan: `.harness/plans/hr-finance-fiscalisation.md` §3b
> Branch: `feature/hr-finance-phase-1b` (worktree `.worktrees/hr-finance-phase-1b/`)
> Owners: backend-expert (B9, B10, B11) → frontend-expert (F1F6).
## What landed
Two pieces: backend closes the approval state machine and adds a `?status=`
filter contract; frontend ships the four §3 portal pages Sprint 1a deferred
plus the inline approve/reject UI on Expenses.
### Backend — B9, B10, B11
| ID | Endpoint | File | Notes |
|---|---|---|---|
| B9 | `PUT /api/finance/expenses/:id/approve` | `server/src/controllers/finance.controller.js` | Idempotent state machine. 409 when current status ≠ `pending`. Sets `sync_status='pending'`, records `approved_by`, `approved_at`. |
| B9 | `PUT /api/finance/expenses/:id/reject` | same | Body: `{ reason }` (required, ≥1 char). 400 on empty. 409 on non-`pending`. Records `rejection_reason`. |
| B10 | `GET /api/finance/expenses/summary` | same | Returns `{ pending_count, pending_amount, approved_this_month:{count,total}, rejected_this_month:{count,total} }`. Powers the new "Approval queue" tiles on `Expenses.tsx` and the Finance dashboard. |
| B11 | `?status=` filter on `/api/hr/leave-requests` | `server/src/controllers/hr.controller.js` | Was missing — added. Filter applies on top of the privileged-vs-self scope. |
| B11 | `?status=` filter on `/api/hr/vacancies` | same | Was missing — added. |
| B11 | `?status=` filter on `/api/finance/expenses` | `server/src/controllers/finance.controller.js` | Already shipped in Sprint 1a; pinned here. |
| Schema | `expenses.approved_at`, `expenses.rejection_reason` | `server/src/database/init.js` | Idempotent ALTER for existing DBs (mirrors the Phase 1 pattern). |
#### Smoke (curl as `bursar@school.com`)
```
# Approve a pending expense
PUT /api/finance/expenses/3/approve
→ 200 { "id": 3, "status": "approved", "approved_by": <uid>, "approved_at": "2026-07-04 21:42:01", ... }
# Re-approve the same row
PUT /api/finance/expenses/3/approve
→ 409 { "error": "Cannot approve expense in status 'approved'; only 'pending' is valid.", "current_status": "approved" }
# Reject without reason
PUT /api/finance/expenses/4/reject
→ 400 { "error": "reason is required" }
# Summary tile
GET /api/finance/expenses/summary
→ 200 { "pending_count": 2, "pending_amount": 540, "approved_this_month": { "count": 1, "total": 125 }, "rejected_this_month": { "count": 0, "total": 0 } }
# B11 verifier
GET /api/hr/leave-requests?status=pending
→ 200 [...only pending rows...]
```
### Frontend — F1, F2, F3, F4, F5, F6
| ID | File | Route | Notes |
|---|---|---|---|
| F1 | `client/src/pages/hr/LeaveApproval.tsx` | `/hr/leave-approval` | Queue with inline Approve / Reject buttons. Reject opens an inline modal with required reason. Status filter defaults to `pending`. No `alert()`, no `window.prompt`. |
| F2 | `client/src/pages/hr/StaffDirectory.tsx` | `/hr/staff` | Read-only listing of `staff_records` joined to department/role/grade. Department `<select>` (no `window.prompt`). Read-only detail drawer with phone + appointment date. Active-staff toggle. |
| F3 | `client/src/pages/finance/PayrollRuns.tsx` | `/finance/payroll/runs` | Bursar-side payroll CRUD UI. **Replaces the legacy placeholder** that routed `/finance/payroll/runs → <HRManagement />`. Backed by `/api/finance/payroll/runs` (Phase 0 proxy → HR controller). Inline banners for create/calc/pay/cancel outcomes. |
| F4 | `client/src/pages/finance/Payslips.tsx` | `/finance/payslips` | List + side-drawer detail. Drawer links to the full `/hr/payslips/:id` view for printing. Self-service scope inherited from the existing `/api/hr/payslips` endpoint (non-privileged callers automatically see only their own). |
| F5 | `client/src/pages/finance/Expenses.tsx` | (modify) | Approve / Reject buttons added on pending rows. New reject modal (required reason). Inline banners for both outcomes. Three summary tiles added at the top, backed by B10. |
| F6 | `client/src/pages/admin/HRManagement.tsx` | (modify) | Dropped the inline Approve/Reject buttons on the Leave tab; replaced with a "Review queue" CTA pointing to `/hr/leave-approval`. Tab is now read-only (View Detail modal still available). |
#### Store additions
`client/src/store/finance.ts` now exports `approveExpense(id)`,
`rejectExpense(id, reason)`, and `fetchExpenseSummary()` plus the
`expenseSummary` / `ExpenseSummary` payload type. `fetchExpenseSummary` is
called from the Expenses page on mount and after every approve/reject to keep
the tile in sync.
### Wiring
- `client/src/App.tsx` — registered all 4 routes under the appropriate role
guards. **Removed the placeholder** that mapped `/finance/payroll/runs →
<HRManagement />` for the bursar (and school_admin/principal/systems_admin).
- `client/src/components/Nav.tsx` — added Leave Approval, Staff Directory,
Payroll, and Payslips entries to the `hr`, `bursar`, `school_admin`,
`principal`, and `systems_admin` nav blocks.
- `SCREENS.md` — refreshed the HR Manager + Bursar nav tables to reflect the
new entries.
## Acceptance verification
- `grep -rn "alert(\|window.prompt" client/src/pages/hr/LeaveApproval.tsx
client/src/pages/hr/StaffDirectory.tsx client/src/pages/finance/PayrollRuns.tsx
client/src/pages/finance/Payslips.tsx client/src/pages/finance/Expenses.tsx
client/src/pages/admin/HRManagement.tsx` — **no hits in the touched code**.
- The new Expenses page banner uses inline `<div className="bg-emerald-50…
bg-red-50…">` instead of native `alert()`.
- The leave rejection flow uses an inline `<textarea>` modal — never
`window.prompt`.
- `npm run build` green (see commit body).
- Sync contract: every write goes through with `sync_status='pending'`.
`tablesToSync` (`server/src/services/SyncEngine.js`) already includes
`expenses`, `leave_requests`, `payroll_runs`, `payslips`, `staff_records`.
## Deviations from spec
1. **`/api/hr/vacancies` B11** — added filter as planned; no UI consumes it
yet (recruitment tab still lists all vacancies). Pinned in the contract so
a future Track A or Track B sprint can wire the frontend.
2. **`/api/hr/leave-requests?status=`** — added in the controller per plan.
`/api/finance/expenses?status=` already shipped in Sprint 1a — pinned.
3. **`/api/hr/leave-requests` ORDER BY** — added `ORDER BY lr.start_date DESC`
so the LeaveApproval page's `?status=pending` filter returns the most
relevant rows first. Not a contract change; the previous behavior was
undefined (SQLite returned insertion order).
4. **Reject modal validation** — client also enforces non-empty reason before
dispatching (defense-in-depth on top of B9's 400). UX shows inline error
on blank submit.
## Demo account (closes the §3a acceptance gap)
`server/src/database/init.js` now seeds `hr@school.com / hr123` alongside
the existing `bursar@school.com / bursar123`. §3a's acceptance test:
"Logged-in as `hr@school.com`, `/hr/leave` shows the staff leave queue"
now passes — Phase 0 didn't seed an HR counterpart. The seed_dummy_data.js
banner was also extended to list both Phase 1 demo accounts.
Re-initialising the DB logs `Created hr: hr@school.com` and a clean login
round-trip with `hr@school.com / hr123` returns role `hr` and `GET
/api/hr/leave-requests → 200`.
## Out of scope (left untouched)
- Paid-state transition (`approved → paid`) — Sprint 1c candidate.
- ZIMRA FDMS / fiscalisation (Phase 3) — gated on ZIMRA test creds.
- Accountant / Librarian / Nurse dashboards — §9 Phase 2+.
- Tests — no framework installed; out of scope per the plan.
- The remaining `alert()` calls in Departments/Inventory/Users/Settings/etc.
(tracked by `.harness/plans/stubs-sprint-1b/A.md` and friends — separate
sweep).