# Disciplines Module — Architecture Map

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

---

## 1. Purpose & Responsibilities

The Disciplines module manages **sport type definitions** (e.g., football, swimming, tennis) and provides:
- Sport discipline CRUD (create, list, show, edit, toggle active)
- Category-based classification (individual, team, racket, leisure, emerging)
- Configuration via JSON (medical cert requirements, guardian consent, player limits)
- Sports dashboard (aggregated KPIs across the sports subsystem)
- Training group waitlist management (queue players for full groups)

It serves as the **taxonomy layer** for the sports subsystem — every academy, training group, and coach is ultimately linked to a discipline.

It does **NOT** directly manage:
- Academies (Academies module)
- Training groups or sessions (SportsActivity module)
- Players or enrollments (PlayerAffairs module)
- Coaches (Coaches module)
- Subscriptions/billing (ActivitySubscriptions module)

**Classification:** Fully independent module with its own controllers, routes, views, services, and models. NOT a wrapper or redirect.

---

## 2. Directory & File Structure

```
app/Modules/Disciplines/
├── bootstrap.php                          # Permission registration (4 permissions)
├── Routes.php                             # Web routes (15 routes)
├── Controllers/
│   ├── DisciplineController.php          # Discipline CRUD + toggle
│   ├── SportsDashboardController.php     # KPI dashboard (queries many tables)
│   └── WaitlistController.php            # Group waitlist management
├── Models/
│   ├── SportDiscipline.php               # Main model (soft delete, author tracking)
│   └── GroupWaitlist.php                 # Waitlist model (no soft delete)
├── Services/
│   ├── DisciplineService.php             # Query helpers (getActive, grouped by category)
│   └── WaitlistService.php              # Waitlist lifecycle (add, offer, accept, expire, remove)
└── Views/
    ├── index.php                         # Paginated discipline list with category tabs
    ├── create.php                        # Create form
    ├── edit.php                          # Edit form
    ├── show.php                          # Discipline detail page
    ├── sports_dashboard.php             # KPI dashboard view
    ├── waitlist.php                      # Waitlist index
    └── waitlist_add.php                 # Add player to waitlist form
```

---

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

### 3.1 `sport_disciplines` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| code | varchar(50) | NO | UNI | Auto-generated from name if empty |
| name_ar | varchar(200) | NO | | Arabic name (required) |
| name_en | varchar(200) | YES | | English name |
| category | varchar(50) | NO | MUL | individual/team/racket/leisure/emerging |
| icon | varchar(100) | YES | | Icon identifier (default: 'activity') |
| description_ar | text | YES | | Arabic description |
| config_json | json | YES | | requires_medical_cert, requires_guardian_consent_under, max_players_per_session, age_groups, skill_levels |
| sort_order | int unsigned | NO | | Default: 0 |
| is_active | tinyint(1) | NO | MUL | Default: 1 |
| is_archived | tinyint(1) | NO | | Soft delete flag |
| archived_at | timestamp | YES | | |
| archived_by | bigint unsigned | 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 |

### 3.2 `group_waitlist` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | int unsigned | NO | PRI | auto_increment |
| group_id | int unsigned | NO | MUL | FK to training_groups |
| player_id | int unsigned | NO | MUL | FK to players |
| priority | int unsigned | NO | | Queue position (default: 0) |
| status | enum | NO | | 'waiting','offered','accepted','expired','cancelled' |
| requested_at | datetime | NO | | When added to waitlist |
| offered_at | datetime | YES | | When spot was offered |
| expires_at | datetime | YES | | 48-hour offer expiry |
| accepted_at | datetime | YES | | When accepted |
| notes | text | YES | | |
| created_at | datetime | NO | | |
| updated_at | datetime | NO | | |

### 3.3 `training_groups` Table (referenced, owned by SportsActivity)

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| code | varchar(50) | NO | UNI | |
| name_ar | varchar(255) | NO | | |
| name_en | varchar(255) | YES | | |
| academy_id | bigint unsigned | NO | MUL | FK to academies |
| level_id | bigint unsigned | NO | MUL | FK to academy_levels |
| coach_id | bigint unsigned | YES | MUL | FK to coaches |
| facility_id | bigint unsigned | YES | MUL | FK to facilities |
| group_type | varchar(20) | NO | | Default: 'group' |
| min_capacity | int unsigned | NO | | Default: 1 |
| max_capacity | int unsigned | NO | | Default: 20 |
| current_count | int unsigned | NO | | Default: 0 |
| age_from | int unsigned | YES | | |
| age_to | int unsigned | YES | | |
| gender_restriction | varchar(10) | YES | | |
| day_of_week | tinyint unsigned | YES | | |
| start_time | time | YES | | |
| end_time | time | YES | | |
| pricing_tier | varchar(20) | NO | | Default: 'standard' |
| is_full | tinyint(1) | NO | MUL | Default: 0 |
| is_active | tinyint(1) | NO | | Default: 1 |
| is_archived | tinyint(1) | NO | | |
| archived_at | timestamp | YES | | |
| created_at | timestamp | NO | | |
| updated_at | timestamp | NO | | |
| created_by | bigint unsigned | YES | | |
| updated_by | bigint unsigned | YES | | |

---

## 4. Routes

| Method | Path | Handler | Middleware | Permission |
|--------|------|---------|------------|------------|
| GET | /sports-dashboard | SportsDashboardController@index | auth | discipline.view |
| GET | /disciplines | DisciplineController@index | auth | discipline.view |
| GET | /disciplines/create | DisciplineController@create | auth | discipline.manage |
| POST | /disciplines | DisciplineController@store | auth, csrf | discipline.manage |
| GET | /disciplines/{id} | DisciplineController@show | auth | discipline.view |
| GET | /disciplines/{id}/edit | DisciplineController@edit | auth | discipline.manage |
| POST | /disciplines/{id} | DisciplineController@update | auth, csrf | discipline.manage |
| POST | /disciplines/{id}/toggle | DisciplineController@toggle | auth, csrf | discipline.manage |
| GET | /waitlist | WaitlistController@index | auth | waitlist.view |
| GET | /waitlist/add | WaitlistController@addPlayer | auth | waitlist.manage |
| POST | /waitlist | WaitlistController@store | auth, csrf | waitlist.manage |
| POST | /waitlist/{id}/offer | WaitlistController@offerSpot | auth, csrf | waitlist.manage |
| POST | /waitlist/{id}/accept | WaitlistController@acceptOffer | auth, csrf | waitlist.manage |
| POST | /waitlist/{id}/remove | WaitlistController@remove | auth, csrf | waitlist.manage |

---

## 5. Events Dispatched

| Event | Payload | Dispatched From |
|-------|---------|-----------------|
| `waitlist.spot_offered` | `{waitlist_id, group_id, player_id, expires_at}` | WaitlistService::offerSpot |
| `waitlist.spot_accepted` | `{waitlist_id, group_id, player_id}` | WaitlistService::acceptOffer |

---

## 6. Events Consumed

None registered in bootstrap.php. This module does not listen to any events.

---

## 7. Cross-Module Dependencies

### 7.1 Disciplines module IMPORTS FROM:

None — the DisciplineController and models are self-contained. The WaitlistController queries `training_groups` via raw SQL but does not import any external module classes.

### 7.2 Other modules that IMPORT FROM Disciplines:

| Module | What it imports |
|--------|----------------|
| Academies | `SportDiscipline::allActive()`, `SportDiscipline::find()` (academy-to-discipline link) |
| Facilities | `SportDiscipline::allActive()` (facility-to-discipline link) |
| Coaches | `coach.getDisciplines()`, `CoachService::syncDisciplines()` |
| ActivitySubscriptions | `SportDiscipline::allActive()` (filtering, enrollment wizard) |
| SportsDashboard | Queries `sport_disciplines` table for counts |

### 7.3 Data Dependencies (SQL queries in SportsDashboardController):

| Table | Owner Module | Usage |
|-------|--------------|-------|
| players | PlayerAffairs | Player stats (total, active, suspended) |
| academy_enrollments | Academies | Active enrollment count |
| activity_subscriptions | ActivitySubscriptions | Monthly subscription stats |
| reservations | Reservations | Facility utilization |
| coaches | Coaches | Coach count |
| training_sessions | SportsActivity | Weekly session stats |
| academy_contracts | AcademyContracts | Contract status overview |
| training_groups | SportsActivity | Group capacity stats |
| pool_bookings | Facilities/Reservations | Pool utilization today |

---

## 8. Permissions

| Key | Arabic | English |
|-----|--------|---------|
| discipline.view | عرض الألعاب | View Disciplines |
| discipline.manage | إدارة الألعاب | Manage Disciplines |
| waitlist.view | عرض قائمة الانتظار | View Waitlist |
| waitlist.manage | إدارة قائمة الانتظار | Manage Waitlist |

---

## 9. Discipline Categories

| Code | Arabic Label | Color |
|------|-------------|-------|
| individual | الفردية | #0D7377 |
| team | الجماعية | #0284C7 |
| racket | الراكيت | #7C3AED |
| leisure | الترفيهية | #D97706 |
| emerging | الناشئة | #059669 |

---

## 10. Waitlist Lifecycle

```
[Player requests spot] → status: 'waiting' (priority auto-assigned)
                              ↓
[Staff offers spot]    → status: 'offered' (48-hour expiry timer set)
                              ↓
[Player accepts]       → status: 'accepted'
    OR
[Timer expires]        → status: 'expired' (via processExpiredOffers cron)
    OR
[Staff removes]        → DELETE from table (hard delete)
```

**Key behaviors:**
- Priority is auto-incremented (MAX + 1 for the group)
- Duplicate check prevents same player being added twice to same group while waiting/offered
- Offer expires after 48 hours
- Acceptance does NOT auto-enroll the player (separate enrollment step needed)
- Remove is a hard delete, not soft delete

---

## 11. Sports Dashboard KPIs

The SportsDashboardController aggregates data from across the sports subsystem:

| KPI | Source Table | Metric |
|-----|-------------|--------|
| Player counts | players | total, active, suspended, revoked, inactive |
| Active enrollments | academy_enrollments | COUNT where status='active' |
| Monthly subscriptions | activity_subscriptions | paid/pending/overdue counts + collected/outstanding amounts |
| Collection rate | activity_subscriptions | paid / total * 100 |
| Facility reservations | reservations | total + revenue this month |
| Recent players | players | Last 10 registered |
| Unpaid subscriptions | activity_subscriptions + players | Top 15 pending/overdue |
| Expiring medical certs | players | Next 30 days |
| Academy count | academies | Active count |
| Discipline count | sport_disciplines | Active count |
| Coach stats | coaches | Total + active |
| Training sessions | training_sessions | This week: total/completed/cancelled/scheduled |
| Contract stats | academy_contracts | Total/active/expiring soon (30 days) |
| Group capacity | training_groups | Total/full/total players |
| Pool utilization | pool_bookings | Today: total/confirmed/in_progress |

---

## 12. High-Risk Areas

### 12.1 Sports Dashboard — Heavy Query Load
- The `SportsDashboardController::index()` method executes **12+ separate SQL queries**
- No caching layer — every page load hits the DB 12+ times
- If any referenced table doesn't exist (e.g., pool_bookings, training_sessions), the query will fail
- This is the most query-intensive single endpoint in the sports subsystem

### 12.2 Waitlist — Hard Delete
- `removeFromWaitlist()` performs a hard DELETE, not soft delete
- No audit trail for removed entries
- If accidentally removed, data is unrecoverable

### 12.3 Waitlist Expiry — Requires Cron
- `WaitlistService::processExpiredOffers()` must be called by a cron job
- If cron doesn't run, offers stay in 'offered' status indefinitely
- The `acceptOffer()` method does check expiry and transitions to 'expired' — but only when someone tries to accept

### 12.4 Waitlist Search Uses Legacy Join to `members`
- `GroupWaitlist::search()` joins `members` table via `m.id = gw.player_id`
- But `player_id` is FK to `players` table, not `members`
- This could cause incorrect results (joining to wrong table)
- The column referenced is `m.name_ar` and `m.membership_number` — these exist on `members` but the FK points to `players`

### 12.5 No Sidebar Menu Registration
- bootstrap.php only registers permissions, not a MenuRegistry entry
- Comment in Facilities bootstrap says "sidebar menu owned by Disciplines bootstrap.php" — but Disciplines doesn't register it either
- Navigation likely handled by a parent module (SportsActivity or SportsDashboard)

---

## 13. Known Patterns & Gotchas

1. **config_json is flexible**: Stores medical cert requirement, guardian consent age, max players — no schema enforcement
2. **SportsDashboard is a cross-cutting view**: Belongs to Disciplines module but queries data from 10+ other modules
3. **Waitlist is for training_groups, not academies**: Despite living in the Disciplines module, waitlist is specifically for `training_groups`
4. **Discipline has no direct FK from other tables**: Other entities (academies, coaches) have `discipline_id` FK pointing TO this table
5. **Model extracts age_groups and skill_levels from config**: `getAgeGroups()` and `getSkillLevels()` parse config_json — these are used by other modules for grouping logic
6. **The `/sports-dashboard` route uses `discipline.view` permission** — somewhat misleading since it shows data far beyond disciplines
