# TrainerPortal Module — Architecture Map

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

---

## 1. Purpose & Responsibilities

The TrainerPortal module is a **mobile-first REST API** for coaches (trainers) to self-service their schedule, profile, and leave management. It provides:
- Coach authentication via phone + password (token-based, not session)
- Coach profile and academy assignment viewing
- Weekly schedule / session list / group roster access
- Day-off request submission and tracking
- Replacement volunteering (cover another coach's day off)
- Player notification dispatch on day-off and replacement events

It does **NOT** directly manage:
- Coach CRUD (Coaches module handles that)
- Training session creation/modification (SportsActivity/Scheduling modules)
- Attendance recording (SportsActivity module)
- Admin approval of day-off requests (managed externally)
- Player management (PlayerAffairs module)

---

## 2. Directory & File Structure

```
app/Modules/TrainerPortal/
├── bootstrap.php                          # Event listeners (day_off → player notifications, replacement → player notifications)
├── ApiRoutes.php                          # REST API routes (15 endpoints)
├── Controllers/
│   └── Api/
│       ├── CoachAuthController.php       # Login/logout (token management)
│       ├── CoachProfileController.php    # Profile + academy info
│       ├── CoachScheduleController.php   # Weekly schedule, sessions, groups, group players
│       ├── DayOffController.php          # Request day off, view my requests
│       └── ReplacementController.php     # View available replacements, volunteer
└── Services/
    ├── CoachDayOffService.php            # Day-off request logic, conflict check, event dispatch
    └── ReplacementService.php            # Replacement volunteering, session reassignment, conflict check
```

**Note:** No Views directory — this is a pure API module (no web UI).

---

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

### 3.1 `coach_tokens` Table

| Column | Type | Nullable | Key | Default | Notes |
|--------|------|----------|-----|---------|-------|
| id | int unsigned | NO | PRI | auto_increment | |
| coach_id | int unsigned | NO | MUL | | FK to coaches |
| token | varchar(64) | NO | UNI | | 64-char hex token |
| name | varchar(100) | NO | | 'portal' | Token purpose |
| last_used_at | datetime | YES | | NULL | |
| expires_at | datetime | YES | MUL | NULL | Token TTL |
| is_revoked | tinyint(1) | NO | | 0 | Revocation flag |
| created_at | datetime | NO | | CURRENT_TIMESTAMP | |

### 3.2 `coach_day_off_requests` Table

| Column | Type | Nullable | Key | Default | Notes |
|--------|------|----------|-----|---------|-------|
| id | int unsigned | NO | PRI | auto_increment | |
| coach_id | int unsigned | NO | MUL | | FK to coaches |
| request_date | date | NO | | | The day off being requested |
| reason_ar | text | YES | | NULL | Arabic reason |
| status | varchar(20) | NO | MUL | 'pending' | pending/covered/rejected |
| replacement_coach_id | int unsigned | YES | MUL | NULL | FK to coaches (volunteer) |
| replacement_volunteered_at | datetime | YES | | NULL | When volunteer stepped up |
| affected_sessions_json | json | YES | | NULL | Snapshot of sessions on that date |
| reviewed_by | int unsigned | YES | | NULL | Admin who reviewed |
| reviewed_at | datetime | YES | | NULL | |
| admin_notes | text | YES | | NULL | |
| created_at | datetime | NO | | CURRENT_TIMESTAMP | |
| updated_at | datetime | YES | | NULL | |

### 3.3 Related Tables (owned by other modules, queried by TrainerPortal)

| Table | Owner Module | Relationship |
|-------|--------------|--------------|
| coaches | Coaches | Main entity (auth, profile) |
| coach_disciplines | Coaches | Coach sport disciplines |
| sport_disciplines | SportsActivity | Discipline definitions |
| coach_academy_assignments | Coaches | Academy role assignments |
| academies | Academies | Academy info |
| academy_contracts | Academies | Active contracts |
| training_sessions | SportsActivity | Coach schedule |
| training_groups | SportsActivity | Coach groups |
| group_memberships | SportsActivity | Players in groups |
| players | PlayerAffairs | Player info in groups |
| facilities | Facilities | Session location |
| academy_levels | Academies | Group level info |

---

## 4. API Routes

| Method | Path | Handler | Middleware | Purpose |
|--------|------|---------|-----------|---------|
| POST | /api/v1/coach/auth/login | CoachAuthController@login | cors | Coach login (phone + password) |
| POST | /api/v1/coach/auth/logout | CoachAuthController@logout | cors, coach_auth | Revoke token |
| GET | /api/v1/coach/profile | CoachProfileController@profile | cors, coach_auth | Coach profile + disciplines |
| GET | /api/v1/coach/academy | CoachProfileController@academy | cors, coach_auth | Academy assignments + contracts |
| GET | /api/v1/coach/schedule | CoachScheduleController@weeklySchedule | cors, coach_auth | Weekly session schedule |
| GET | /api/v1/coach/sessions | CoachScheduleController@sessions | cors, coach_auth | Sessions in date range |
| GET | /api/v1/coach/groups | CoachScheduleController@groups | cors, coach_auth | Active groups |
| GET | /api/v1/coach/groups/{id}/players | CoachScheduleController@groupPlayers | cors, coach_auth | Players in a specific group |
| POST | /api/v1/coach/day-off | DayOffController@request | cors, coach_auth | Request a day off |
| GET | /api/v1/coach/day-off | DayOffController@myRequests | cors, coach_auth | List my day-off requests |
| GET | /api/v1/coach/day-off/{id} | DayOffController@requestDetails | cors, coach_auth | Single request details |
| GET | /api/v1/coach/replacements | ReplacementController@available | cors, coach_auth | Available replacement opportunities |
| POST | /api/v1/coach/replacements/{id}/volunteer | ReplacementController@volunteer | cors, coach_auth | Volunteer as replacement |

**Authentication:** `coach_auth` middleware validates bearer token against `coach_tokens` table (non-revoked, non-expired).

---

## 5. Events Dispatched

| Event | Payload | Dispatched From |
|-------|---------|-----------------|
| `coach.day_off_requested` | `{coach_id, coach_name, request_id, request_date, affected_sessions}` | CoachDayOffService::requestDayOff |
| `coach.replacement_confirmed` | `{original_coach_id, original_coach_name, replacement_coach_id, replacement_name, request_id, request_date}` | ReplacementService::volunteer |

---

## 6. Events Consumed (in bootstrap.php)

| Event | Source | Action |
|-------|--------|--------|
| `coach.day_off_requested` | Self (TrainerPortal) | Notify players in coach's groups about cancelled training via PlayerNotificationService |
| `coach.replacement_confirmed` | Self (TrainerPortal) | Notify players about replacement coach via PlayerNotificationService |

**Note:** These events are also consumed by:
- **Notifications module** — `NotificationTriggerService::processEvent()` for both events
- **Scheduling module** — References `coach_day_off` as a scheduling rule type

---

## 7. Cross-Module Dependencies

### 7.1 TrainerPortal IMPORTS FROM:

| Module | What it uses |
|--------|-------------|
| PlayerApi | `PlayerNotificationService::notify()` — sends notifications to affected players |

### 7.2 Other modules that reference TrainerPortal data:

| Module | What it references |
|--------|-------------------|
| Notifications | Listens to `coach.day_off_requested` and `coach.replacement_confirmed` events |
| Scheduling | References `coach_day_off` as a rule type; handles scheduling conflicts |

---

## 8. Permissions

The TrainerPortal has **no ERP permissions** registered. All access control is via:
- `coach_auth` middleware (bearer token validation)
- Coach ownership checks (coach can only see their own data)
- Group ownership check (groupPlayers verifies `coach_id` matches)

---

## 9. Core Business Flows

### 9.1 Coach Authentication

```
1. POST /api/v1/coach/auth/login
   - Validate: phone + password required
   - Look up coach by phone (non-archived)
   - Verify password_hash
   - Generate 64-char hex token (random_bytes(32))
   - Store in coach_tokens with TTL (default 2160 hours = 90 days)
   - Update coaches.last_login_at
   - Return: token, expires_at, coach info

2. POST /api/v1/coach/auth/logout
   - Set is_revoked = 1 on the current token
```

### 9.2 Day-Off Request

```
1. POST /api/v1/coach/day-off
   - Validate: date required, reason optional
   - Check no existing pending/covered request for same date
   - Snapshot affected sessions (scheduled sessions on that date)
   - Insert coach_day_off_requests (status=pending)
   - Dispatch: coach.day_off_requested
   - → bootstrap listener notifies all players in coach's groups

2. Return: request_id, affected_sessions count
```

### 9.3 Replacement Volunteering

```
1. GET /api/v1/coach/replacements — list pending requests (not own, not covered, future dates)
2. POST /api/v1/coach/replacements/{id}/volunteer
   - Verify request is pending and uncovered
   - Verify volunteer is not the requesting coach
   - Check volunteer has no schedule conflict on that date
   - Update request: replacement_coach_id, status='covered'
   - Reassign all affected training_sessions to volunteer coach
   - Dispatch: coach.replacement_confirmed
   - → bootstrap listener notifies all players about replacement
```

---

## 10. High-Risk Areas

### 10.1 Session Reassignment on Volunteer (CRITICAL)
- `ReplacementService::volunteer()` directly updates `training_sessions.coach_id` for all affected sessions
- No rollback mechanism — if volunteer cancels, sessions remain reassigned
- Affected sessions are determined from a JSON snapshot taken at request time (may be stale)
- If sessions were modified after the day-off request, the snapshot references stale IDs

### 10.2 Token Security
- Token is 64 hex chars (256-bit entropy) — strong
- TTL defaults to 2160 hours (90 days) — long-lived tokens
- No token rotation or refresh mechanism
- `last_used_at` is never updated in current code (column exists but unused)
- Tokens survive password changes (no "revoke all on password change" logic)

### 10.3 Day-Off Conflict Detection
- Only checks for non-rejected existing requests on same date
- Does NOT check if the date is a non-working day for the coach
- Does NOT validate against academy/club calendar or holidays

### 10.4 Player Notification Volume
- On day-off request, ALL players across ALL of the coach's active groups get notified
- On replacement confirmed, same volume
- No deduplication — a player in multiple groups of the same coach gets multiple notifications

---

## 11. Known Patterns & Gotchas

1. **Pure API module** — no web views, no sidebar menu, no ERP permissions
2. **coach_auth middleware** — separate from the `auth` middleware used by web modules; validates bearer tokens
3. **CORS middleware** — all endpoints include `cors` for cross-origin mobile app access
4. **Self-consuming events** — bootstrap.php dispatches events from services AND listens to them in the same module
5. **LAST_INSERT_ID() pattern** — `CoachDayOffService` uses a separate query for last insert ID instead of `$db->insert()` return value
6. **No pagination** — schedule, sessions, groups, and day-off lists have no pagination (may become problematic at scale)
7. **Config dependency** — token TTL from `config('api.token.coach_ttl_hours')` defaults to 2160 if not set
8. **Ownership enforcement** — `groupPlayers` checks group belongs to the coach; schedule endpoints filter by coach_id
