# FacilityGrids Module — Architecture Map

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

---

## 1. Purpose & Responsibilities

The FacilityGrids module is the **unified facility scheduling and grid management system**. It manages:
- Facility grid definitions (pool lanes, court sections, generic zones)
- Zone-based scheduling (which coach/academy/activity occupies which zones at what times)
- Trainee assignment to zones (linking players to physical spaces)
- Monthly planning (draft/active/archived plan lifecycle with cloning)
- Pool hour-by-hour planning (granular snapshot per hour per day)
- Facility attendance tracking (coaches and trainees, present/absent/late/excused)
- Pool financial dashboard (revenue from reservations + free time entries)
- Conflict detection (prevents overlapping schedules on same zones)
- Legacy redirect handling (old `/mirror/*` and `/pool/*/grid` paths)

It is the **successor** to both the legacy mirror system (FacilityDashboards) and pool-specific grid management (PoolManagement).

It does **NOT** directly manage:
- Facility CRUD (owned by Facilities module)
- Reservations/bookings (owned by Reservations module)
- Player registration (owned by PlayerAffairs)
- Coach registration (owned by Coaches module)
- Academy registration (owned by SportsActivity module)
- Payments (Payments/Cashier modules)

---

## 2. Directory & File Structure

```
app/Modules/FacilityGrids/
├── bootstrap.php                              # Permission registration (view + manage)
├── Routes.php                                 # 38 routes (CRUD, schedules, trainees, plans, attendance, financial)
├── Controllers/
│   ├── FacilityGridController.php            # Grid CRUD + toggle active + API state
│   ├── GridDashboardController.php           # Overview dashboard (stats, occupancy, top coaches)
│   ├── ZoneScheduleController.php            # Schedule CRUD with conflict detection
│   ├── ZoneTraineeController.php             # Assign/remove/move/clear trainees in zones
│   ├── MonthlyPlanController.php             # Monthly plan lifecycle (create, clone, activate, archive, diff)
│   ├── PoolHourPlanController.php            # Hour-by-hour pool grid snapshots
│   ├── FacilityAttendanceController.php      # Attendance recording and reporting
│   ├── PoolFinancialDashboardController.php  # Revenue dashboard per grid
│   └── RedirectController.php                # Legacy URL redirects (/mirror/*, /pool/*/grid)
├── Models/
│   ├── FacilityGrid.php                      # Grid model (allActive, getZones, getGridTypes)
│   ├── FacilityMonthlyPlan.php               # Monthly plan model (clonePlan, getActive)
│   └── FacilityZoneSchedule.php              # Schedule model (getActiveForGrid, getDayNames, getActivityTypes)
├── Services/
│   ├── GridStateService.php                  # Real-time grid state (zone occupancy, schedule resolution)
│   ├── FacilityAttendanceService.php         # Attendance batch recording, reporting, payment reminders
│   ├── PoolFinancialService.php              # Financial dashboard data aggregation
│   └── PoolHourPlanService.php               # Hour plan CRUD (save/copy snapshots)
└── Views/
    ├── index.php                              # Grid list
    ├── create.php                             # New grid form
    ├── edit.php                               # Edit grid form
    ├── show.php                               # Single grid view (interactive)
    ├── grid_interactive.php                   # Interactive grid display
    ├── dashboard.php                          # Grid overview dashboard
    ├── schedules.php                          # Schedule management
    ├── monthly_plans.php                      # Monthly plan list
    ├── monthly_plan_create.php                # Create/clone plan
    ├── monthly_plan_show.php                  # Plan detail with schedules
    ├── pool_hour_plans.php                    # Hour plan calendar
    ├── pool_hour_plan_edit.php                # Edit hour plan for a specific day
    ├── facility_attendance.php                # Attendance form
    └── pool_financial_dashboard.php           # Financial dashboard
```

---

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

### 3.1 `facility_grids` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| facility_id | bigint unsigned | NO | MUL | FK to facilities |
| name_ar | varchar(300) | NO | | Arabic name |
| name_en | varchar(300) | YES | | English name |
| grid_type | varchar(20) | NO | MUL | 'pool', 'court', 'generic' (default: generic) |
| rows_count | int unsigned | NO | | Default: 4 |
| cols_count | int unsigned | NO | | Default: 6 |
| row_label_ar | varchar(50) | YES | | Default: 'حارة' |
| col_label_ar | varchar(50) | YES | | Default: 'قسم' |
| physical_length | decimal(5,2) | YES | | Meters |
| physical_width | decimal(5,2) | YES | | Meters |
| config_json | json | YES | | Additional configuration |
| is_active | tinyint(1) | NO | | Default: 1 |
| 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 `facility_grid_zones` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| grid_id | bigint unsigned | NO | MUL | FK to facility_grids |
| row_index | int unsigned | NO | | 0-based row position |
| col_index | int unsigned | NO | | 0-based col position |
| label | varchar(100) | YES | | Custom zone label |
| zone_type | varchar(20) | YES | | Optional type classification |
| width_meters | decimal(4,2) | YES | | Physical width |
| max_occupants | int unsigned | NO | | Default: 5 |
| is_active | tinyint(1) | NO | | Default: 1 |
| sort_order | int unsigned | NO | | Default: 0 |
| created_at | timestamp | NO | | |
| updated_at | timestamp | NO | | |

### 3.3 `facility_zone_schedules` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| grid_id | bigint unsigned | NO | MUL | FK to facility_grids |
| plan_month | varchar(7) | YES | MUL | e.g. '2026-06' |
| plan_id | bigint unsigned | YES | MUL | FK to facility_monthly_plans |
| schedule_name | varchar(255) | NO | | Display name |
| zone_selection_type | varchar(20) | NO | | 'cells', 'row', 'column', 'all' |
| zone_selection_json | json | NO | | Array of {row, col} or legacy format |
| day_of_week | tinyint unsigned | NO | MUL | 0=Sunday, 6=Saturday |
| start_time | time | NO | | |
| end_time | time | NO | | |
| activity_type | varchar(30) | YES | | swimming_lessons, academy_session, etc. |
| coach_id | bigint unsigned | YES | | FK to coaches |
| coach_name | varchar(200) | YES | | Denormalized |
| academy_id | bigint unsigned | YES | | FK to academies |
| academy_name | varchar(200) | YES | | Denormalized |
| age_group | varchar(50) | YES | | e.g. '6-10' |
| gender | varchar(10) | YES | | Default: 'mixed' |
| max_occupants | int unsigned | NO | | Default: 8 |
| color | varchar(20) | YES | | Default: '#3B82F6' |
| effective_from | date | NO | | Schedule start date |
| effective_to | date | YES | | NULL = indefinite |
| is_active | tinyint(1) | NO | MUL | Soft-delete via 0 |
| notes | text | 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.4 `facility_zone_trainees` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| zone_id | bigint unsigned | NO | MUL | FK to facility_grid_zones |
| schedule_id | bigint unsigned | YES | MUL | FK to facility_zone_schedules |
| player_id | bigint unsigned | YES | MUL | FK to players |
| trainee_name | varchar(300) | YES | | Manual name if no player_id |
| assigned_at | timestamp | NO | | |
| assigned_by | bigint unsigned | YES | | FK to employees |

### 3.5 `facility_monthly_plans` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| grid_id | bigint unsigned | NO | MUL | FK to facility_grids |
| plan_month | varchar(7) | NO | | e.g. '2026-06' |
| plan_name_ar | varchar(200) | NO | | |
| plan_name_en | varchar(200) | YES | | |
| status | varchar(20) | NO | MUL | 'draft', 'active', 'archived' |
| working_days_json | json | NO | | Array of day numbers [1,2,3,4,5,6] |
| day_hours_start | time | NO | | Default: 06:00 |
| day_hours_end | time | NO | | Default: 14:00 |
| night_hours_start | time | NO | | Default: 14:00 |
| night_hours_end | time | NO | | Default: 22:00 |
| cloned_from_id | bigint unsigned | YES | | FK to self (for diff comparison) |
| total_sessions | int unsigned | NO | | Default: 0 |
| total_coaches | int unsigned | NO | | Default: 0 |
| total_trainees | int unsigned | NO | | Default: 0 |
| notes | text | YES | | |
| activated_at | datetime | YES | | |
| activated_by | bigint unsigned | YES | | FK to employees |
| created_at | timestamp | NO | | |
| updated_at | timestamp | NO | | |
| created_by | bigint unsigned | YES | | |
| updated_by | bigint unsigned | YES | | |

### 3.6 `pool_hour_plans` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| monthly_plan_id | int unsigned | NO | MUL | FK to facility_monthly_plans |
| plan_date | date | NO | MUL | Specific date |
| hour_slot | time | NO | | e.g. '08:00:00' |
| period | varchar(10) | NO | | 'day' or 'night' |
| grid_snapshot_json | json | NO | | Full grid state at that hour |
| notes | text | YES | | |
| created_at | datetime | NO | | |
| updated_at | datetime | YES | | |

### 3.7 `facility_attendance` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| grid_id | int unsigned | NO | MUL | FK to facility_grids |
| schedule_id | int unsigned | YES | | FK to facility_zone_schedules |
| monthly_plan_id | int unsigned | YES | MUL | FK to facility_monthly_plans |
| attendance_date | date | NO | | |
| hour_slot | time | YES | | Optional hourly granularity |
| entity_type | varchar(20) | NO | MUL | 'coach' or 'trainee' |
| entity_id | int unsigned | NO | | FK to coaches or players |
| entity_name_cache | varchar(200) | YES | | Denormalized name |
| status | varchar(20) | NO | | Default: 'present' (present/absent/late/excused) |
| check_in_time | time | YES | | Actual check-in time |
| notes | text | YES | | |
| recorded_by | int unsigned | YES | | FK to employees |
| created_at | datetime | NO | | |

### 3.8 Related Tables (owned by other modules, queried by FacilityGrids)

| Table | Owner Module | Relationship |
|-------|--------------|--------------|
| facilities | Facilities | facility_id FK in facility_grids |
| coaches | Coaches | coach_id FK in schedules, attendance |
| academies | SportsActivity | academy_id FK in schedules |
| players | PlayerAffairs | player_id FK in trainees, attendance |
| reservations | Reservations | Used by PoolFinancialService |
| free_time_entries | PoolManagement? | Used by PoolFinancialService |

---

## 4. Routes (38 total)

### Grid CRUD & Dashboard
| Method | Path | Handler | Permission |
|--------|------|---------|------------|
| GET | `/facility-grids/dashboard` | GridDashboardController@index | facility_grid.view |
| GET | `/facility-grids` | FacilityGridController@index | facility_grid.view |
| GET | `/facility-grids/create` | FacilityGridController@create | facility_grid.manage |
| POST | `/facility-grids` | FacilityGridController@store | facility_grid.manage |
| GET | `/facility-grids/{id}` | FacilityGridController@show | facility_grid.view |
| GET | `/facility-grids/{id}/edit` | FacilityGridController@edit | facility_grid.manage |
| POST | `/facility-grids/{id}/update` | FacilityGridController@update | facility_grid.manage |
| POST | `/facility-grids/{id}/toggle` | FacilityGridController@toggle | facility_grid.manage |
| GET | `/api/facility-grids/{id}/state` | FacilityGridController@apiState | facility_grid.view |

### Zone Schedules
| Method | Path | Handler | Permission |
|--------|------|---------|------------|
| GET | `/facility-grids/{gridId}/schedules` | ZoneScheduleController@index | facility_grid.manage |
| POST | `/facility-grids/{gridId}/schedules` | ZoneScheduleController@store | facility_grid.manage |
| POST | `/facility-grids/{gridId}/schedules/{id}` | ZoneScheduleController@update | facility_grid.manage |
| POST | `/facility-grids/{gridId}/schedules/{id}/delete` | ZoneScheduleController@destroy | facility_grid.manage |
| GET | `/facility-grids/{gridId}/schedules/conflicts` | ZoneScheduleController@checkConflicts | facility_grid.view |

### Zone Trainees
| Method | Path | Handler | Permission |
|--------|------|---------|------------|
| POST | `/facility-grids/{gridId}/trainees` | ZoneTraineeController@assign | facility_grid.manage |
| POST | `/facility-grids/{gridId}/trainees/{id}/remove` | ZoneTraineeController@remove | facility_grid.manage |
| POST | `/facility-grids/{gridId}/trainees/{id}/move` | ZoneTraineeController@move | facility_grid.manage |
| POST | `/facility-grids/{gridId}/zones/{zoneId}/clear` | ZoneTraineeController@clearZone | facility_grid.manage |

### Monthly Plans
| Method | Path | Handler | Permission |
|--------|------|---------|------------|
| GET | `/facility-grids/{gridId}/plans` | MonthlyPlanController@index | facility_grid.view |
| GET | `/facility-grids/{gridId}/plans/create` | MonthlyPlanController@create | facility_grid.manage |
| POST | `/facility-grids/{gridId}/plans` | MonthlyPlanController@store | facility_grid.manage |
| GET | `/facility-grids/{gridId}/plans/{planId}` | MonthlyPlanController@show | facility_grid.view |
| POST | `/facility-grids/{gridId}/plans/{planId}/activate` | MonthlyPlanController@activate | facility_grid.manage |
| POST | `/facility-grids/{gridId}/plans/{planId}/archive` | MonthlyPlanController@archive | facility_grid.manage |
| GET | `/facility-grids/{gridId}/plans/{planId}/diff` | MonthlyPlanController@diff | facility_grid.view |

### Pool Hour Plans
| Method | Path | Handler | Permission |
|--------|------|---------|------------|
| GET | `/facility-grids/{gridId}/plans/{planId}/hours` | PoolHourPlanController@index | facility_grid.view |
| GET | `/facility-grids/{gridId}/plans/{planId}/hours/{date}` | PoolHourPlanController@dayView | facility_grid.view |
| GET | `/facility-grids/{gridId}/plans/{planId}/hours/{date}/{hour}` | PoolHourPlanController@showHour | facility_grid.view |
| POST | `/facility-grids/{gridId}/plans/{planId}/hours/{date}/{hour}` | PoolHourPlanController@storeHour | facility_grid.manage |
| POST | `/facility-grids/{gridId}/plans/{planId}/hours/{date}/{hour}/copy` | PoolHourPlanController@copyHour | facility_grid.manage |

### Facility Attendance
| Method | Path | Handler | Permission |
|--------|------|---------|------------|
| GET | `/facility-grids/{gridId}/attendance` | FacilityAttendanceController@index | facility_grid.manage |
| POST | `/facility-grids/{gridId}/attendance` | FacilityAttendanceController@store | facility_grid.manage |
| GET | `/facility-grids/{gridId}/attendance/report` | FacilityAttendanceController@report | facility_grid.view |
| POST | `/facility-grids/{gridId}/attendance/remind` | FacilityAttendanceController@remind | facility_grid.manage |

### Pool Financial Dashboard
| Method | Path | Handler | Permission |
|--------|------|---------|------------|
| GET | `/facility-grids/{gridId}/financial-dashboard` | PoolFinancialDashboardController@dashboard | facility_grid.view |
| GET | `/facility-grids/{gridId}/financial-dashboard/export` | PoolFinancialDashboardController@export | facility_grid.view |

### Legacy Redirects
| Method | Path | Redirects To |
|--------|------|--------------|
| GET | `/mirror` | `/facility-grids` |
| GET | `/mirror/{id}` | `/facility-grids/{id}` |
| GET | `/pool/{id}/grid` | `/facility-grids/{id}` |
| GET | `/pool/{id}/schedules` | `/facility-grids/{id}/schedules` |

---

## 5. Events Dispatched

| Event | Payload | Dispatched From |
|-------|---------|-----------------|
| `facility.attendance_absent` | `{entity_type, entity_id, grid_id, date}` | FacilityAttendanceService::recordBatch |
| `facility.payment_reminder` | `{player_id, player_name, phone, grid_id}` | FacilityAttendanceService::sendPaymentReminders |

---

## 6. Events Consumed

None registered in bootstrap.php. This module dispatches events for other modules to consume but does not listen to external events.

---

## 7. Cross-Module Dependencies

### 7.1 FacilityGrids IMPORTS FROM (reads/validates):

| Module/Table | What it uses |
|-------------|-------------|
| Facilities | `facilities` table (lookup for grid creation, name display) |
| Coaches | `coaches` table (validate is_active, get name, join for display) |
| SportsActivity | `academies` table (validate is_active, get name, join for display) |
| PlayerAffairs | `players` table (validate card_status + medical_status, get name) |
| Reservations | `reservations` table (PoolFinancialService revenue queries) |
| PoolManagement? | `free_time_entries` table (PoolFinancialService revenue queries) |

### 7.2 Other modules that IMPORT FROM FacilityGrids:

| Module | What it imports |
|--------|----------------|
| PlaygroundAdmin | `FacilityAttendanceService` (used directly) |
| PlaygroundAdmin | Queries `facility_attendance` table directly |
| SportsDashboard | Queries `facility_zone_schedules` table for coach session counts |
| PoolManagement | Routes comment references migration to FacilityGrids |

---

## 8. Permissions

| Key | Description |
|-----|-------------|
| facility_grid.view | View grids, schedules, plans, dashboards, reports |
| facility_grid.manage | Create/edit/delete grids, schedules, trainees, plans, attendance |

---

## 9. Core Business Flows

### 9.1 Grid Lifecycle

```
Create Grid → Auto-generate zones (rows x cols) → Add Schedules → Assign Trainees
                                                                        ↓
Edit Grid (resize) → Regenerate zones (add new, deactivate overflow)
                                                                        ↓
Toggle Grid → is_active = 0 (soft deactivation)
```

### 9.2 Monthly Plan Lifecycle

```
Create Plan (draft) → Add Schedules (linked via plan_id)
         ↓
Clone from Previous Plan → All schedules duplicated with new effective dates
         ↓
Activate Plan → Previous active plan archived, this becomes 'active'
         ↓
Archive Plan → status = 'archived'
```

### 9.3 Schedule Conflict Detection

Before creating/updating a schedule:
1. Resolve positions from `zone_selection_type` + `zone_selection_json`
2. Query existing schedules for same grid + day + overlapping time + overlapping effective dates
3. For each existing schedule, resolve its positions
4. Check for physical position overlap (same row:col)
5. If overlap found → reject with conflict details

### 9.4 Trainee Assignment Validation

Before assigning a trainee (player) to a zone:
1. Verify player exists in `players` table
2. Check `card_status = 'active'` (player card must be active)
3. Check `medical_status IN ('fit', 'conditional')` (must be medically cleared)
4. Check zone capacity (`max_occupants` not exceeded)

---

## 10. Zone Selection System

Schedules specify which zones they occupy via `zone_selection_type` + `zone_selection_json`:

| Type | JSON Format | Effect |
|------|-------------|--------|
| `cells` | `[{row:0, col:1}, {row:0, col:2}]` | Specific individual cells |
| `row` | `[{row:0, col:0}]` or `{row: N}` | Entire row(s) across all columns |
| `column` | `[{row:0, col:2}]` or `{col: N}` | Entire column(s) across all rows |
| `all` | Any | All zones in the grid |

Default `max_occupants` by selection type: row=8, column=6, cells=5.

---

## 11. High-Risk Areas

### 11.1 Conflict Detection Logic (CRITICAL)
- `GridStateService::checkConflicts()` is the guard against double-booking zones
- Must handle both new and legacy JSON formats for zone selection
- Time overlap uses `start_time < end_time AND end_time > start_time` (correct open interval)
- Date overlap uses `effective_from <= effective_to_of_new AND effective_to >= effective_from_of_new`
- If this logic has bugs: two activities scheduled in same physical space at same time

### 11.2 Zone Regeneration on Grid Resize
- `regenerateZones()` adds new zones but deactivates overflow zones (row >= new_rows OR col >= new_cols)
- Uses OR logic: `row_index >= ? OR col_index >= ?` — this is correct for shrinking
- Does NOT delete zones, only sets `is_active = 0`
- Trainees on deactivated zones remain in DB (orphaned references)

### 11.3 Plan Activation Cascade
- Activating a plan automatically archives ALL other active plans for the same grid
- Uses a bulk UPDATE then individual plan update — not wrapped in transaction
- If interrupted between the two queries: could have zero active plans

### 11.4 Player Validation at Assignment Time Only
- Player `card_status` and `medical_status` are checked only when assigning
- If a player's status changes AFTER assignment, they remain in the zone
- No periodic reconciliation or event-driven removal

### 11.5 Denormalized Names
- `coach_name` and `academy_name` stored in schedules alongside IDs
- If coach/academy name changes, schedule shows old name until manually updated
- `entity_name_cache` in attendance is set at recording time, not refreshed

---

## 12. Known Patterns & Gotchas

1. **No soft delete on most tables**: Uses `is_active = 0` flag instead of `is_archived`
2. **Zone selection JSON has two formats**: Legacy `{row: N}` and new `[{row: 0, col: 1}]` — code handles both
3. **AJAX-heavy**: Most schedule/trainee operations return JSON; only index/show pages return HTML
4. **Pool-specific features**: Hour plans and financial dashboard are pool-oriented but work on any grid type
5. **Cloned plans copy all schedules**: With new `effective_from`/`effective_to` set to new month boundaries
6. **RedirectController maintains backward compatibility**: Old mirror and pool URLs still work via 301 redirects
7. **No authentication on API state endpoint**: Uses `['auth']` middleware but no CSRF — designed for AJAX polling
8. **Attendance has no undo**: Records are upserted (updated if same entity+date+hour exists)
