# Users Module — Architecture Map

> **Last updated:** 2026-06-10
> **Status:** Living document — incrementally updated as new information is discovered

---

## 1. Purpose & Responsibilities

The Users module is the **employee account management** module of the ERP:
- Create employee accounts (username, password, profile info)
- Edit employee details and reset passwords
- Assign/remove roles to employees (time-bounded with expiration)
- Assign employees to branches (multi-branch support)
- Manage direct permissions (grant/deny overrides beyond role-based)
- View employee activity (login history)
- Permission audit trail logging
- Preview effective permissions for role combinations

It does **NOT** manage:
- Role definitions or role-level permissions (Roles module)
- Authentication/login flow (Auth module)
- HR employee profiles (HR module — separate entity)
- Session management (Auth module)

---

## 2. Directory & File Structure

```
app/Modules/Users/
├── bootstrap.php                          # Empty — defers to Roles/bootstrap.php
├── Routes.php                             # Web routes (10 routes)
├── Controllers/
│   ├── UserController.php                # Employee CRUD + activity log
│   └── UserPermissionController.php      # Direct permissions + role management + preview
├── Models/
│   └── Employee.php                      # Core Employee model (auth, permissions, roles)
├── Services/
│   └── PermissionAuditService.php        # Audit logging for permission changes
└── Views/
    ├── index.php                         # Employee list with search/filter
    ├── create.php                        # New employee form (username, password, roles, branch)
    ├── edit.php                          # Edit employee form
    ├── show.php                          # Employee profile (roles, active sessions)
    ├── activity.php                      # Login attempt history
    └── permissions.php                   # Granular permission management UI
```

---

## 3. Database Schema (Production — Source of Truth)

### 3.1 `employees` Table (6 rows)

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| username | varchar(50) | NO | UNI | Login identifier |
| password_hash | varchar(255) | NO | | bcrypt hash |
| full_name_ar | varchar(200) | NO | | Arabic display name |
| full_name_en | varchar(200) | YES | | English name |
| email | varchar(255) | YES | | |
| phone | varchar(20) | YES | | |
| branch_id | bigint unsigned | YES | MUL | Primary branch FK |
| is_active | tinyint(1) | NO | MUL | Default: 1 |
| force_password_change | tinyint(1) | NO | | Default: 0 |
| password_changed_at | timestamp | YES | | |
| last_login_at | timestamp | YES | | |
| last_login_ip | varchar(45) | YES | | |
| failed_login_count | int unsigned | NO | | Default: 0 |
| locked_until | timestamp | YES | | |
| created_at | timestamp | NO | | |
| updated_at | timestamp | NO | | |
| created_by | bigint unsigned | YES | | FK to employees |
| updated_by | bigint unsigned | YES | | FK to employees |
| is_archived | tinyint(1) | NO | MUL | Soft delete flag |
| archived_at | timestamp | YES | | |
| archived_by | bigint unsigned | YES | | |

### 3.2 `employee_roles` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| employee_id | bigint unsigned | NO | MUL | FK to employees |
| role_id | bigint unsigned | NO | MUL | FK to roles |
| assigned_at | timestamp | NO | | When role was assigned |
| assigned_by | bigint unsigned | YES | | FK to employees (who assigned) |
| expires_at | timestamp | YES | MUL | Time-bounded role (null = permanent) |
| is_active | tinyint(1) | NO | | Default: 1 |

### 3.3 `employee_permissions` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| employee_id | bigint unsigned | NO | MUL | FK to employees |
| permission_key | varchar(100) | NO | MUL | Permission identifier |
| is_denied | tinyint(1) | NO | | 0=grant, 1=deny (overrides role) |
| granted_at | timestamp | NO | | |
| granted_by | bigint unsigned | YES | | FK to employees |
| notes | varchar(500) | YES | | Reason for override |

### 3.4 `employee_branches` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| employee_id | bigint unsigned | NO | MUL | FK to employees |
| branch_id | bigint unsigned | NO | MUL | FK to branches |
| is_primary | tinyint(1) | NO | | Default: 0 |
| assigned_at | timestamp | NO | | |
| assigned_by | bigint unsigned | YES | | FK to employees |

### 3.5 `permission_audit_log` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| employee_id | bigint unsigned | NO | MUL | Target employee |
| action | enum | NO | | role_assigned/role_removed/perm_granted/perm_denied/perm_removed/branch_added/branch_removed |
| target_type | varchar(50) | NO | | 'role', 'permission', or 'branch' |
| target_value | varchar(200) | NO | | Role code or permission key |
| performed_by | bigint unsigned | NO | | FK to employees (who made change) |
| performed_at | timestamp | NO | MUL | |
| notes | text | YES | | |

---

## 4. Permission Resolution Algorithm

The Employee model resolves permissions in this order:

```
1. Collect role permissions:
   - Get all active, non-expired roles for employee
   - Get all permission_keys from role_permissions for those roles
   - Recursively inherit parent role permissions (with cycle protection)

2. Apply direct overrides:
   - employee_permissions WHERE is_denied = 0 → ADD to permissions
   - employee_permissions WHERE is_denied = 1 → REMOVE from permissions

3. Wildcard check:
   - If '*' is in permissions → hasPermission() returns true for everything
   - UNLESS the specific key is in direct denials

Final: permissions = (role_perms + parent_perms + direct_grants) - direct_denials
```

### Caching
- `cachedPermissions`, `cachedDenials`, `cachedRoles` — per-request in-memory cache
- Cleared by `syncDirectPermissions()`

---

## 5. Employee Creation Flow

```
POST /users (store)
├── Validate: username (unique), password (min 8), full_name_ar, branch_id
├── Hash password (bcrypt, cost from config)
├── Create Employee record (is_active=1, force_password_change=1)
├── Assign roles (employee_roles with optional expires_at)
├── Assign branches (employee_branches, first = primary)
├── Insert into password_history
└── Redirect with success
```

---

## 6. Permission Management Flow

```
GET /users/{id}/permissions (edit form)
├── Load all permissions from PermissionRegistry (grouped by module)
├── Load all active roles
├── Load employee's current roles
├── Build role-to-permission map (which role provides which permission)
├── Load direct grants/denials
└── Render checkbox grid: role checkboxes + per-permission state (inherit/grant/deny)

POST /users/{id}/permissions (update)
├── Delete all employee_roles → re-insert selected roles (full sync)
├── Parse perm_state[key] = 'grant'|'deny'|'' for each permission
├── employee.syncDirectPermissions(grants, denials)
├── Audit log: role_assigned, role_removed, perm_granted, perm_denied
└── Redirect with success
```

---

## 7. Events Dispatched

The Users module dispatches `employee.created` via the Model event system (Employee model has `$dispatchEvents = true`):

| Event | Payload | Dispatched From |
|-------|---------|-----------------|
| `employee.created` | `{id, username, ...fillable}` | Employee::create() (Model layer auto-dispatch) |

---

## 8. Events Consumed

| Event | Listener Module | Action |
|-------|----------------|--------|
| `employee.created` | HR | Logs that HR profile is needed |

---

## 9. Cross-Module Dependencies

### 9.1 Users module IMPORTS FROM:

| Module | What it uses |
|--------|-------------|
| Roles | Role::allActive() for forms, Role model |
| Auth | ActiveSession::getActiveForEmployee() in show page |
| Core | PermissionRegistry, Pagination |

### 9.2 Other modules that IMPORT FROM Users:

| Module | What it imports |
|--------|----------------|
| Auth | Employee model (findByUsername, password verification, login recording) |
| HR | Employee reference via employee_id FK in hr_employee_profiles |
| All Modules | Employee model implicitly (currentEmployee() from App singleton) |

---

## 10. Routes

| Method | Path | Handler | Middleware | Permission |
|--------|------|---------|------------|------------|
| GET | /users | UserController@index | auth | user.view |
| GET | /users/create | UserController@create | auth | user.create |
| POST | /users | UserController@store | auth, csrf | user.create |
| GET | /users/{id} | UserController@show | auth | user.view |
| GET | /users/{id}/edit | UserController@edit | auth | user.edit |
| POST | /users/{id} | UserController@update | auth, csrf | user.edit |
| GET | /users/{id}/activity | UserController@activity | auth | user.view |
| GET | /users/{id}/permissions | UserPermissionController@edit | auth | user.manage_permissions |
| POST | /users/{id}/permissions | UserPermissionController@update | auth, csrf | user.manage_permissions |
| GET | /users/preview-permissions | UserPermissionController@preview | auth | user.manage_permissions |

---

## 11. Permissions

Registered in `Roles/bootstrap.php` (shared group):

| Key | Description |
|-----|-------------|
| user.view | View employee list and profiles |
| user.create | Create new employee accounts |
| user.edit | Edit employee details |
| user.deactivate | Deactivate employee accounts |
| user.assign_role | Assign roles to employees |
| user.manage_permissions | Manage direct permission grants/denials |

---

## 12. Sidebar Menu

Registered in `Roles/bootstrap.php` under group `users_roles`:
- Parent: "المستخدمون والصلاحيات" (Users & Roles) → /users
- Children:
  - "الموظفون" (Employees) → /users (user.view)
  - "إضافة موظف" (Add Employee) → /users/create (user.create)
  - "الأدوار" (Roles) → /roles (role.view)

---

## 13. High-Risk Areas

### 13.1 Role Sync (DELETE + INSERT on every save)
- Both UserController::update() and UserPermissionController::update() delete ALL employee_roles then re-insert
- NOT wrapped in a transaction — partial failure leaves employee with no roles
- Two forms can save roles (edit page AND permissions page) — last write wins

### 13.2 Super Admin Check
- Super admin is determined by: employee has role with `role_code = 'super_admin'`
- Which grants `*` wildcard permission
- If super_admin role is deactivated (is_active=0), all super admins lose access simultaneously

### 13.3 Password Reset by Admin
- Admin can set a new password via the edit form (no confirmation of admin's own password)
- New password is set with `force_password_change = 1`
- No notification is sent to the affected employee

### 13.4 Role Expiration
- `expires_at` in employee_roles is checked at permission resolution time (`er.expires_at IS NULL OR er.expires_at > NOW()`)
- Expired roles are not cleaned up — they remain with is_active=1 but are ignored
- No cron job or notification for expiring roles

### 13.5 Audit Service Failure Tolerance
- PermissionAuditService wraps all DB operations in try/catch
- Audit failures are silently swallowed — permission changes succeed but may not be logged

---

## 14. Known Patterns & Gotchas

1. **Employee != HR Profile**: The `employees` table is the auth/system user. The HR module has a separate `hr_employee_profiles` table linked via `employee_id` FK
2. **branch_id on employees**: This is the PRIMARY branch for simple lookups. Multi-branch access uses `employee_branches` table
3. **No employee archival endpoint**: There's no route to archive employees — only `is_active` toggle via edit
4. **Permission preview is AJAX**: `GET /users/preview-permissions?roles[]=1&roles[]=2` returns JSON of aggregated permissions for selected roles
5. **Direct permissions override roles**: A direct `is_denied=1` entry will block a permission even if all roles grant it
6. **Employee model is used by Auth module**: The `Employee` model lives in Users module but is the primary model for authentication
7. **Index page JOIN**: Employee list joins `branches` table for display; filter by active/inactive status
