# Coaches Module — Architecture Map

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

---

## 1. Purpose & Responsibilities

The Coaches module is a **comprehensive coach lifecycle management system**. It manages:
- Coach registration with personal data, NID parsing, photo upload
- Sport discipline assignment (many-to-many)
- Academy assignment and role management (head_coach, coach, assistant)
- Weekly availability scheduling with date-bounded effectiveness
- Schedule conflict detection against academy_schedules
- Coach utilization reporting
- Multi-dimensional rating system (punctuality, communication, technique, motivation)
- Monthly performance metrics calculation and ranking
- Payment calculation by 4 models (per_session, per_player, monthly_fixed, hybrid)
- Payment approval workflow

It does **NOT** directly manage:
- Training sessions (Sessions module)
- Academy definitions (Academies module)
- Player enrollment (PlayerAffairs / SportsActivity modules)
- Actual money disbursement (generates payment records, not treasury requests)

---

## 2. Directory & File Structure

```
app/Modules/Coaches/
├── bootstrap.php                          # Permission registration (5 permissions)
├── Routes.php                             # Web routes (18 routes)
├── Controllers/
│   ├── CoachController.php               # Main CRUD + assign/unassign/toggle/availability (10 endpoints)
│   ├── CoachRatingController.php         # Rating submission, flagging, admin response (5 endpoints)
│   └── CoachPerformanceController.php    # Dashboard, ranking, detail, calculate (4 endpoints)
├── Models/
│   ├── Coach.php                         # Main model (soft delete, author tracking)
│   ├── CoachRating.php                   # Rating model (no soft delete)
│   └── CoachPerformanceMetric.php        # Metrics model (no soft delete, upsert pattern)
├── Services/
│   ├── CoachService.php                  # CRUD helpers, discipline sync, academy assign/unassign, availability
│   ├── CoachSchedulingService.php        # Availability check, conflict detection, utilization
│   ├── CoachRatingService.php            # Rating submission, duplicate check, flagging, response
│   ├── CoachPerformanceService.php       # Metric calculation, ranking, summary, batch cron
│   └── CoachPaymentService.php           # Payment calculation, monthly generation, approval
└── Views/
    ├── index.php                         # Coach list with search/filters
    ├── create.php                        # Coach registration form
    ├── edit.php                          # Coach edit form
    ├── show.php                          # Coach profile (assignments, availability, utilization)
    ├── ratings.php                       # Ratings list with averages
    ├── rate_coach.php                    # Rating submission form
    ├── performance_dashboard.php         # Monthly performance overview
    ├── performance_detail.php            # Single coach performance trends
    └── performance_ranking.php           # Coach ranking by composite score
```

---

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

### 3.1 `coaches` Table (1 row as of 2026-06-10)

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| code | varchar(30) | NO | UNI | Auto-generated if empty (C_NAME_XXXX) |
| full_name_ar | varchar(300) | NO | | Arabic name (3+ chars required) |
| full_name_en | varchar(300) | YES | | English name |
| national_id | varchar(20) | YES | MUL | Egyptian 14-digit NID |
| phone | varchar(30) | YES | | |
| password_hash | varchar(255) | YES | | For TrainerPortal auth |
| otp_code | varchar(6) | YES | | OTP for mobile auth |
| otp_expires_at | datetime | YES | | |
| last_login_at | datetime | YES | | |
| email | varchar(200) | YES | | |
| date_of_birth | date | YES | | Auto-parsed from NID |
| gender | varchar(10) | YES | | Auto-parsed from NID |
| photo_path | varchar(500) | YES | | Uploaded photo |
| bio_ar | text | YES | | Arabic biography |
| certifications_json | json | YES | | Array of certification strings |
| employment_type | varchar(20) | NO | | 'staff', 'contract', 'freelance' |
| coach_type | varchar(20) | NO | MUL | 'independent', 'academy', 'club_employee' |
| employee_id | bigint unsigned | YES | | FK to employees (if staff) |
| max_players_default | int unsigned | NO | | Default: 20 |
| hourly_rate | decimal(15,2) | YES | | EGP/hour |
| session_rate | decimal(15,2) | YES | | EGP/session |
| monthly_rate | decimal(15,2) | YES | | EGP/month (for fixed/hybrid) |
| payment_model | varchar(20) | NO | | 'per_session', 'per_player', 'monthly_fixed', 'hybrid' |
| is_active | tinyint(1) | NO | MUL | Default: 1 |
| is_archived | tinyint(1) | NO | | Soft delete, default 0 |
| archived_at | timestamp | YES | | |
| archived_by | bigint unsigned | YES | | |
| created_at | timestamp | NO | | |
| updated_at | timestamp | NO | | On update cascade |
| created_by | bigint unsigned | YES | | FK to employees |
| updated_by | bigint unsigned | YES | | FK to employees |
| branch_id | bigint unsigned | YES | | FK to branches |

### 3.2 `coach_disciplines` Table (1 row)

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| coach_id | bigint unsigned | NO | MUL | FK to coaches |
| discipline_id | bigint unsigned | NO | MUL | FK to sport_disciplines |
| specialization_level | varchar(30) | NO | | Default: 'general' |
| is_primary | tinyint(1) | NO | | Default: 0 |
| created_at | timestamp | NO | | |

### 3.3 `coach_academy_assignments` Table (0 rows)

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| coach_id | bigint unsigned | NO | MUL | FK to coaches |
| academy_id | bigint unsigned | NO | MUL | FK to academies |
| level_id | bigint unsigned | YES | MUL | FK to academy_levels |
| role | varchar(30) | NO | | 'head_coach', 'coach', 'assistant' |
| max_players | int unsigned | YES | | Override per-assignment |
| assigned_from | date | NO | | Start of assignment |
| assigned_to | date | YES | | End (NULL = ongoing) |
| is_active | tinyint(1) | NO | | Default: 1 |
| created_at | timestamp | NO | | |
| updated_at | timestamp | NO | | |

### 3.4 `coach_availability` Table (0 rows)

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| coach_id | bigint unsigned | NO | MUL | FK to coaches |
| day_of_week | tinyint unsigned | NO | | 0=Sunday through 6=Saturday |
| start_time | time | NO | | |
| end_time | time | NO | | |
| is_available | tinyint(1) | NO | | Default: 1 |
| effective_from | date | YES | | NULL = always valid |
| effective_to | date | YES | | NULL = no end |
| created_at | timestamp | NO | | |

### 3.5 `coach_ratings` Table (0 rows)

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| coach_id | bigint unsigned | NO | MUL | FK to coaches |
| player_id | bigint unsigned | YES | MUL | FK to players |
| member_id | bigint unsigned | YES | | FK to members (optional) |
| session_id | bigint unsigned | YES | | FK to training_sessions |
| rating | tinyint unsigned | NO | MUL | Overall score (1-5, computed avg) |
| punctuality_score | tinyint unsigned | YES | | 1-5 |
| communication_score | tinyint unsigned | YES | | 1-5 |
| technique_score | tinyint unsigned | YES | | 1-5 |
| motivation_score | tinyint unsigned | YES | | 1-5 |
| feedback_ar | text | YES | | Free-text feedback in Arabic |
| is_anonymous | tinyint(1) | NO | | Default: 0 |
| status | varchar(20) | NO | | 'published', 'flagged', 'hidden' |
| admin_response_ar | text | YES | | Admin response to feedback |
| created_at | timestamp | NO | | |

### 3.6 `coach_performance_metrics` Table (0 rows)

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| coach_id | bigint unsigned | NO | MUL | FK to coaches |
| period_month | varchar(7) | NO | MUL | 'YYYY-MM' format |
| total_sessions_assigned | int unsigned | NO | | Default: 0 |
| total_sessions_conducted | int unsigned | NO | | Default: 0 |
| total_sessions_cancelled | int unsigned | NO | | Default: 0 |
| avg_attendance_rate | decimal(5,2) | YES | | % of registered players attending |
| avg_rating | decimal(3,2) | YES | | 1.00-5.00 |
| total_ratings_count | int unsigned | NO | | Default: 0 |
| total_players_trained | int unsigned | NO | | Unique players that month |
| player_retention_rate | decimal(5,2) | YES | | % of last month's players still active |
| punctuality_rate | decimal(5,2) | YES | | % of sessions conducted vs assigned |
| revenue_generated | decimal(12,2) | NO | | From coach_payments, default 0.00 |
| calculated_at | datetime | NO | | Last calculation timestamp |
| created_at | timestamp | NO | | |

### 3.7 `coach_payments` Table (0 rows)

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| coach_id | bigint unsigned | NO | MUL | FK to coaches |
| payment_period | varchar(7) | NO | MUL | 'YYYY-MM' format |
| payment_model | varchar(30) | NO | | Snapshot of model used |
| sessions_conducted | int unsigned | NO | | Default: 0 |
| total_players_served | int unsigned | NO | | Default: 0 |
| base_amount | decimal(10,2) | NO | | Default: 0.00 |
| bonus | decimal(10,2) | NO | | Default: 0.00 |
| deductions | decimal(10,2) | NO | | Default: 0.00 |
| net_amount | decimal(10,2) | NO | | base + bonus - deductions |
| calculation_json | json | YES | | Full calculation breakdown |
| status | varchar(20) | NO | | 'draft', 'approved', etc. |
| approved_by | bigint unsigned | YES | | FK to employees |
| approved_at | timestamp | YES | | |
| paid_at | timestamp | YES | | |
| notes | text | YES | | |
| created_at | timestamp | NO | | |

---

## 4. Routes

### 4.1 Coach CRUD & Management (10 routes)

| Method | Path | Handler | Permission |
|--------|------|---------|------------|
| GET | /coaches | CoachController@index | coach.view |
| GET | /coaches/create | CoachController@create | coach.manage |
| POST | /coaches | CoachController@store | coach.manage |
| GET | /coaches/{id} | CoachController@show | coach.view |
| GET | /coaches/{id}/edit | CoachController@edit | coach.manage |
| POST | /coaches/{id} | CoachController@update | coach.manage |
| POST | /coaches/{id}/toggle | CoachController@toggle | coach.manage |
| POST | /coaches/{id}/assign | CoachController@assign | coach.manage |
| POST | /coaches/{id}/unassign | CoachController@unassign | coach.manage |
| POST | /coaches/{id}/availability | CoachController@saveAvailability | coach.manage |

### 4.2 Coach Ratings (5 routes)

| Method | Path | Handler | Permission |
|--------|------|---------|------------|
| GET | /coach-ratings | CoachRatingController@index | coach.rate |
| GET | /coach-ratings/create | CoachRatingController@create | coach.rate |
| POST | /coach-ratings | CoachRatingController@store | coach.rate |
| POST | /coach-ratings/{id}/flag | CoachRatingController@flagRating | coach.manage_ratings |
| POST | /coach-ratings/{id}/respond | CoachRatingController@respond | coach.manage_ratings |

### 4.3 Coach Performance (4 routes)

| Method | Path | Handler | Permission |
|--------|------|---------|------------|
| GET | /coach-performance | CoachPerformanceController@dashboard | coach.view_performance |
| GET | /coach-performance/ranking | CoachPerformanceController@ranking | coach.view_performance |
| GET | /coach-performance/{id} | CoachPerformanceController@detail | coach.view_performance |
| POST | /coach-performance/calculate | CoachPerformanceController@calculate | coach.view_performance |

---

## 5. Events Dispatched

| Event | Payload | Dispatched From |
|-------|---------|-----------------|
| `coach.rated` | `{rating_id, coach_id, player_id, rating}` | CoachRatingService::submitRating |
| `coach.payment.approved` | `{payment_id, coach_id, period, net_amount, approved_by}` | CoachPaymentService::approvePayment |

---

## 6. Events Consumed

| Event | Listener Module | Action |
|-------|----------------|--------|
| `coach.payment_processed` | Notifications | Triggers notification to coach |

---

## 7. Cross-Module Dependencies

### 7.1 Coaches module IMPORTS FROM:

| Module | What it uses |
|--------|-------------|
| Members | NationalIdParser (for NID auto-parsing DOB/gender) |

### 7.2 Other modules that REFERENCE Coaches:

| Module | What it references |
|--------|-------------------|
| Academies | AcademyController, AcademySchedule model (coach_id in schedules) |
| AcademyContracts | SettlementService (coach payment settlements) |
| ActivitySubscriptions | EnrollWizardController (coach selection in enrollment) |
| Disciplines | SportsDashboardController (coach stats per discipline) |
| FacilityDashboards | MirrorDisplayController/Service (coach on current schedule) |
| FacilityGrids | Multiple controllers/views (coach assignment in grids) |
| Notifications | bootstrap.php (listens to coach.payment_processed) |
| PlayerAffairs | EvaluationController, FitnessController (coach conducts evaluations) |
| PlayerApi | EnrollmentController, SportBrowseController (coach info in APIs) |
| PlaygroundAdmin | Schedule/Attendance controllers (coach on playground) |
| PoolManagement | PoolScheduleController (coach assignment to pool lanes) |
| Reports | CoachUtilizationReportService (reporting on coach workload) |
| Reservations | FacilityConflictDetector (checks coach schedule conflicts) |
| Scheduling | ConflictDetectionService, ScheduleChangeService (coach schedule management) |
| Sessions | SessionController, SessionAttendance, SessionFeedback (coach runs sessions) |
| SportsActivity | CoachController, multiple services/views (full coach management in SA context) |
| SportsDashboard | DashboardMetricsService (coach performance stats) |
| TrainerPortal | API controllers (CoachAuth, CoachProfile, CoachSchedule, DayOff, Replacement) |
| Training | SessionFeedbackService (coach feedback) |
| TrainingGroups | TrainingGroupController/Model (coach assigned to groups) |

---

## 8. Permissions

| Key | Description |
|-----|-------------|
| coach.view | View coaches list and profiles |
| coach.manage | Create, edit, toggle, assign/unassign, set availability |
| coach.rate | Submit ratings, view ratings list |
| coach.manage_ratings | Flag ratings, respond to ratings (admin) |
| coach.view_performance | View performance dashboard, ranking, details, trigger calculation |

---

## 9. Payment Models

### 9.1 Per Session
```
net = sessions_conducted * session_rate
```

### 9.2 Per Player
```
net = total_players_attended * (hourly_rate OR session_rate)
```

### 9.3 Monthly Fixed
```
net = monthly_rate (regardless of sessions)
```

### 9.4 Hybrid
```
base = monthly_rate
bonus = max(0, sessions_conducted - 20) * session_rate
net = base + bonus
```

### 9.5 Payment Lifecycle
1. `generateMonthlyPayments(period)` — batch creates `draft` records for all active coaches
2. `approvePayment(id, approvedBy)` — moves to `approved`, dispatches event
3. Manual: paid_at set when disbursed (no automated disbursement)

---

## 10. Performance Composite Score

Weighted formula (0-100 scale):
```
score = (avg_rating / 5 * 100) * 0.40
      + avg_attendance_rate * 0.25
      + (sessions_conducted / sessions_assigned * 100) * 0.20
      + player_retention_rate * 0.15
```

Performance levels:
| Score | Level | Arabic Label |
|-------|-------|-------------|
| 85+ | excellent | ممتاز |
| 70-84 | good | جيد |
| 55-69 | average | متوسط |
| 40-54 | below_average | أقل من المتوسط |
| <40 | poor | ضعيف |

---

## 11. Background Processes (Cron)

**CoachPerformanceService::calculateAllCoachesMetrics(yearMonth)**:
- Iterates all active coaches
- Calculates monthly metrics for each (sessions, attendance, ratings, retention, revenue)
- Upsert pattern: deletes existing metric row if present, then inserts fresh
- Logs count of successful calculations

**CoachPaymentService::generateMonthlyPayments(period)**:
- Iterates all active coaches
- Skips if payment already exists for the period
- Calculates payment based on coach's payment_model
- Inserts record with status='draft'

---

## 12. High-Risk Areas

### 12.1 Rating Duplicate Prevention
- `hasRated()` checks per session (if session_id provided) or 24-hour window (if no session)
- Race condition possible: two simultaneous requests could pass the check
- No database-level unique constraint to prevent duplicates

### 12.2 Availability Replace-All Pattern
- `setAvailability()` DELETEs all existing availability then INSERTs new slots
- If called during a concurrent read, availability could appear empty momentarily
- No transaction wrapping

### 12.3 Discipline Sync Replace-All Pattern
- `syncDisciplines()` DELETEs all existing disciplines then INSERTs new ones
- Same transactional concern as availability

### 12.4 Payment Calculation Data Dependencies
- Depends on `training_sessions` table (from Sessions module)
- Depends on `session_attendance` table (from Sessions module)
- If sessions are retroactively modified after payment generation, payments become stale

### 12.5 Code Uniqueness Collision
- Auto-generated code: `C_NAME_XXXX` (XXXX = time() % 10000)
- On collision, appends random hex — but the fallback only runs on create, not update
- Update path checks uniqueness but uses QueryBuilder `whereRaw` which may not be tested

### 12.6 TrainerPortal Authentication Columns
- `password_hash`, `otp_code`, `otp_expires_at`, `last_login_at` exist on coaches table
- The Coaches module does NOT manage these — TrainerPortal module handles mobile auth
- If coach is deactivated (is_active=0), TrainerPortal should check this, but it's cross-module

---

## 13. Known Patterns & Gotchas

1. **NID auto-parsing**: When a valid 14-digit NID is provided, date_of_birth and gender are automatically overwritten from NID parsing (even if manually entered values differ)
2. **Coach types determine assignment UI**: If coach_type='academy', the form shows academy/level selection; otherwise it's hidden
3. **No menu registration in bootstrap**: The Coaches module does NOT register sidebar menus — it's likely registered elsewhere or accessed via another module's navigation
4. **Photo upload is self-contained**: Handles upload directly in controller (not via shared PhotoUploadService like Members)
5. **Performance metrics are month-scoped**: Cannot drill into per-session or per-day granularity from the performance dashboard
6. **Ratings are player-submitted**: Ratings come from players (player_id), not from management — separate from performance metrics
7. **Composite score sorting**: Performance ranking sorts by composite score, but the initial query sorts by avg_rating — the re-sort happens in PHP
8. **Upsert for metrics**: `calculateForMonth()` checks for existing row and updates or inserts — not a true MySQL ON DUPLICATE KEY pattern
9. **Coach search**: Supports filtering by full_name_ar, full_name_en, code, phone (LIKE search), plus employment_type and payment_model filters
10. **Assignment unassign = soft-close**: Unassigning sets `is_active=0` and `assigned_to=today`, does not delete the record
