1. Intro & Legend
Panduan ni jelaskan apa user setiap access level nampak bila guna THR — landing page, sidebar menu, feature yang dia boleh buat atau tak boleh buat, dan flow penggunaan harian. Source of truth: scan terus dari /home/lanccc/projects/THR/src/.
TL;DR — cuma 4 level ada gating sebenar dalam code
Walaupun accessLevelUtils.js define 9 nama (0-8), kod cuma check pada level 0, 5, 7, 8. Level 1, 2, 3, 4, 6 wujud sebagai label HR tapi tak ada perbezaan behavior — mereka fall back ke L0 (Employee). Level 70 & 80 reserved tapi bukan dalam enum.
Colour legend
Population snapshot
| Level | Name | Headcount | Gated? | Theme colour in UI |
|---|---|---|---|---|
| 0 | Employee | 584 (95%) | Yes (default) | Green |
| 1 | Senior Employee | 0 | No | Green (inherits L0) |
| 2 | Supervisor / Team Lead | 1 | No | Green (inherits L0) |
| 3 | Assistant Manager | 0 | No | Green (inherits L0) |
| 4 | Manager | 0 | No | Green (inherits L0) |
| 5 | Senior Manager | 0 | Yes | Blue |
| 6 | Director | 0 | No | Blue (inherits L5) |
| 7 | HR Admin | 22 | Yes (org-scoped) | Orange |
| 8 | Super Admin | 5 | Yes (system-wide) | Red |
| 70 | Reserved | 0 | Not in enum | Gray |
| 80 | Reserved | 0 | Not in enum | Gray |
Note pasal Level 2
Memory record sebut "Level 2 Team Lead (1)" dalam DB tapi kod gating guna access_level >= 5 untuk team features. Jadi orang yang ada L2 tu tak boleh approve leave walaupun namanya "Team Lead" — dia dapat view sama macam Employee biasa.
2. Access Matrix
Bird's-eye view — 9 level aktif (row 70/80 skip sebab reserved) lawan feature major. Klik row untuk jump ke journey card yang berkenaan.
| Feature / Page | L0 | L1 | L2 | L3 | L4 | L5 | L6 | L7 | L8 |
|---|---|---|---|---|---|---|---|---|---|
| Self-service (Personal) | |||||||||
| Dashboard (personal stats) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| My Profile (edit) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Clock in / out (attendance) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Apply leave | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Submit claim + upload receipt | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| View own payslip | ◐ | ◐ | ◐ | ◐ | ◐ | ◐ | ◐ | ◐ | ◐ |
| Read memo / announcement | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Messaging / internal chat | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Documents (view org docs) | ◐ | ◐ | ◐ | ◐ | ◐ | ◐ | ◐ | ✓ | ✓ |
| Team management | |||||||||
| View "My Team" list | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ | ✓ | ✓ | ✓ |
| Approve / reject team leave | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ | ✓ | ✓ | ✓ |
| Team reports & analytics | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ | ✓ | ✓ | ✓ |
| Team attendance overview | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ | ✓ | ✓ | ✓ |
| HR tools (org-scoped) | |||||||||
| Employee directory (browse) | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ◐ | ✓ |
| Organizations (manage) | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ◐ | ✓ |
| Leave balance admin | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ | ✓ |
| Attendance admin (override) | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ | ✓ |
| Payroll management | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ | ✓ |
| Change request approval | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ◐ | ✓ |
| Broadcast memo / announcement | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ◐ | ✓ |
| System admin | |||||||||
| Admin Settings (system config) | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ |
| Org-admin assignments | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ |
| User / auth management | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ |
| Activity logs / audit trail | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ |
| System health checkup | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ |
| Access-level switcher (impersonate) | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ |
| Sofia AI assistant (if enabled) | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ | ✓ |
Org scoping untuk L7 (HR Admin)
HR Admin bukan dapat akses seluruh data — dia hanya nampak employee & applications dari organizations yang dia di-assign dalam thr_admin_org_assignments.admin_user_id. Contoh: HR TCSB cuma boleh approve leave employee TCSB. Kalau multi-org (e.g., TCSB + TASB), nampak gabungan kedua-duanya. Full list = hanya L8.
Persona · Level 0 · Employee
Employee (Self-Service)
Level default untuk semua employee biasa. Fokus self-service — apply leave, clock in, submit claim, baca memo. Tak boleh tengok orang lain punya data, tak boleh approve apa-apa.
Dashboard mockup
Good morning, Ahmad 👋
L0 dashboard · personal widgets only · green accents
Signature flows
thr_attendance
→ UI: badge turns green "● Working"thr_leave_applications (status=pending)
→ Notification fires to L5 manager OR L7 HR Admin
→ Employee sees "Pending approval" chipthr_claims (status=pending)
→ L7 HR Admin notificationWhat L0 can vs cannot do
Persona · Level 1 · Senior Employee
Senior Employee (Unused)
No behavioural difference from L0
Wujud dalam accessLevelUtils.js:6 sebagai label "Senior Employee" tapi tiada satu pun komponen yang check access_level === 1 atau access_level >= 1. Kalau user di-assign L1, dia lihat interface identical dengan L0 Employee. Essentially a tier for HR record-keeping — untuk sign seniority dalam HR record tanpa beri power extra.
Kalau nak activate: perlu tambah gating seperti if (level >= 1) show seniorEmployeeBadge atau if (level >= 1) allow priority leave booking.
Persona · Level 2 · Supervisor
Supervisor / Team Lead (Unused gating)
⚠ Trap: Level ≠ Power
Walaupun DB ada 1 orang dengan L2 (and label dia "Team Lead"), semua gating code guna access_level >= 5 untuk team features. Jadi orang ni buka THR, dia tak nampak Leave Approvals, Team Reports, atau My Team — dia fall-through ke Employee view.
Expected behaviour (ikut nama "Supervisor") ialah boleh approve leave, tapi actually tidak. Ini kemungkinan tech debt atau mis-configuration — perlu promote ke L5 kalau nak dia functional sebagai supervisor.
Persona · Level 3 · Assistant Manager
Assistant Manager (Unused)
Sama macam L1/L2 — tiada gating code. Behaviour = Employee. Notifikasi ke manager mungkin trigger pada access_level >= 3 (NotificationBell.jsx:171) tapi tu routing logic, bukan feature gate.
Persona · Level 4 · Manager
Manager (Unused)
Tier label "Manager" tetapi behavior-wise sama dengan L0. Untuk dapat power approve, perlu upgrade ke L5.
Persona · Level 5 · Senior Manager
Senior Manager (Team Lead tier)
First level dengan team powers. Boleh approve leave, tengok team reports, buka attendance overview untuk team. Tak ada akses ke HR tools macam employee directory atau payroll — itu L7.
Dashboard mockup
Pending Leave Approvals (3)
L5 dashboard · approval queue + team stats · blue accents
Signature flows
LeaveApprovals.jsx:486 — "your team's leave requests"
→ Check leave_type, dates, balance remaining
→ Click Approve (or Reject with reason)
→ thr_leave_applications.status = 'approved'
→ Trigger: notify employee + deduct from thr_leave_balances
→ Audit trail row insertedWhat L5 adds on top of L0
Persona · Level 6 · Director
Director (Inherits L5)
Wujud sebagai label dalam accessLevelUtils.js:11 tapi tak ada gating unik. Kerana semua manager-tier gates guna >= 5, L6 dapat exactly same view macam L5 Senior Manager. Tiada feature tambahan khas untuk Director level.
Persona · Level 7 · HR Admin
HR Admin (Organization-scoped)
Role utama untuk jalankan HR operations — manage employees, approve change requests, configure leave balances, run payroll (Phase 2). Bukan global access: dia hanya nampak data dari org(s) yang dia di-assign dalam thr_admin_org_assignments. Kalau assign pada 2 org, dia nampak gabungan dua.
Dashboard mockup
TCSB · Todak Culture 149 employees
L7 dashboard · org-scoped (TCSB only) · orange accents · HR queues
Signature flows
thr_change_requests (status=pending, org_id=X)
→ L7 sees filtered list: hrOrganizationService.getChangeRequestScope()
→ Query: WHERE organization_id IN (assignedOrgs)
→ Review: old value vs proposed value diff
→ Approve → thr_employees updated + audit trail
→ Reject → notify requester with reasonthr_leave_entitlements updated
→ Cron recalculates thr_leave_balances for all active employees in orgthr_memos insert
→ Notification fan-out to employees in target orgs
→ Note: L7 limited to own orgs. For all-hands memo → L8 only.What L7 adds on top of L5
/admin (system config)Admin-Org assignment mechanism
L7 users di-assign ke orgs oleh L8 via /admin/org-assignments. Row dalam thr_admin_org_assignments: { admin_user_id, organization_id, role, created_at }. Scope query:
// hrOrganizationService.js
const scope = {
canViewAll: false,
organizationIds: [...user's assigned orgs]
};
query.in('organization_id', scope.organizationIds);
Dec 2025 bug: column admin_user_id pernah tersalah set sebagai employee_id → 509 HR admin dapat scope salah. Pastikan join pakai admin_user_id.
Persona · Level 8 · Super Admin
Super Admin (System-wide)
Role paling tinggi — full system access. Manage users, levels, system config, view semua activity logs, impersonate level lain untuk test, run system health check. Tiada org scoping — dia nampak semua 24 orgs.
Dashboard mockup
System Overview
L8 dashboard · system-wide · red accents · health monitoring + audit
Signature flows
thr_admin_org_assignments
→ Next login, employee sees new org's data in HR dashboardWhat L8 exclusively controls
Power without RLS
L8 sekarang ni effectively god mode — tapi tanpa Postgres RLS. Semua gating app-level. Maksudnya kalau ada bug UI atau direct API call dari browser devtools, user boleh bypass access_level check. Critical tables yang RLS disabled: thr_employees, thr_organizations, thr_attendance, thr_leave_*, thr_notifications, thr_activity_logs, thr_audit_trail, thr_admin_org_assignments. Rujuk RLS section dalam main docs.
Persona · Level 70 · Reserved
Reserved (Not in enum)
Level ini wujud sebagai nilai numerik yang sah dalam column access_level (constraint: integer) tapi tidak didefinisikan dalam accessLevelUtils.js (yang senaraikan label sampai 8 sahaja). Kalau ada row dengan access_level = 70, UI akan tunjuk "Unknown Level" atau fallback ke L8 behavior (sebab >= 8 gating triggers).
Kemungkinan intent: untuk special accounts macam service account, audit user, atau super-super-admin (tertiary tier). Tapi takde code yang handle ini secara explicit sekarang.
Persona · Level 80 · Reserved
Reserved (Not in enum)
Sama dengan L70 — valid numeric value tapi tiada label atau gating khusus. Kemungkinan untuk root-level system account (e.g., automated provisioning user) yang perlu bypass semua checks. Untuk aktivasi, perlu:
- Tambah label dalam
accessLevelUtils.js - Tambah case dalam
getAccessLevelTheme() - Tambah section dalam sidebar
getMenuStructure() - Decide behavior (super-override vs restricted service account)
4. Cross-Role Signature Flows
Flow yang merentas level — tunjuk bagaimana interaksi antara persona.
Flow A · Leave approval end-to-end
Flow B · New employee onboarding
Flow C · Change request escalation
Flow D · System admin task (L8 only)
5. Gaps & Ambiguities
1 · 7 of 11 access levels have no distinct behavior
L1, L2, L3, L4, L6, L70, L80 — defined or permissible but no code branches on them. Effectively 7 tiers are decorative. If you need graduated permissions (e.g., "can view team but not approve"), current code can't express it without new gating.
2 · Theme switching is hard-coded
getAccessLevelTheme() hardcodes ranges 0-4 green, 5-6 blue, 7 orange, 8 red. Kalau tambah L9 atau activate L70, theme default ke green — dia tak stand out.
3 · Phase 2 features cluttering nav
Items macam /reports, /team, /myClaims, /myDocuments, /myPayslips semua disabled (phaseDisabled: true) tapi tetap render dalam sidebar. User boleh klik tapi dapat "Phase 2" overlay. Adakah better kalau sembunyi terus sehingga siap?
4 · No JWT claim for access_level
Access level fetched dari thr_employees setiap page load. Takde JWT claim, so kalau access_level change mid-session, user masih dapat cached level sampai next refresh. Potentially security lag.
5 · Impersonation audit gap?
AccessLevelSwitcher membolehkan L8 impersonate level lain. Perlu check: adakah action yang dibuat semasa impersonate direkod dengan actual_user_id=L8 dan effective_level=N, atau seolah-olah dilakukan oleh L8 biasa? Kalau tak direkod, audit trail boleh dipertikaikan.
Recommendations
| Action | Benefit | Effort |
|---|---|---|
| Collapse L1-L4 → single "Employee" tier OR activate gates | Remove confusion | S |
Replace >= 5 with named roles (ROLE_TEAM_LEAD, ROLE_HR_ADMIN) | Self-documenting code | M |
| Enable Postgres RLS matching app-layer scope | Defense-in-depth | L |
| Add JWT claim for access_level + tenant | Proper auth, real-time revoke | M |
| Hide Phase 2 items from nav completely | Cleaner UX | S |
| Audit impersonation actions explicitly | Compliance | S |
How to get real screenshots to replace mockups
Mockups ni replicate MUI theme tapi bukan real screenshots. Kalau nak real:
- Install
playwright+ chromium on lan-claude - Enable
demo_modelocalStorage flag (bypass auth) - Script: loop setiap
access_level, navigate to key routes,page.screenshot() - Save to
/home/lanccc/thr-docs/screenshots/and swap.mockupsections dengan<img>
Kalau ko confirm, aku boleh setup script ni.