# Phase 6-11 — Facility, Space, Assignment & Attendance

## Goal

Model physical spaces with time-based dynamic layouts, universal assignment of people to responsibilities, and a single attendance engine that serves all subjects and sources.

---

# PART A: FACILITY MANAGEMENT

## Entity: `facilities`

| Column | Type | Rules |
|--------|------|-------|
| id | bigint PK | |
| uuid | uuid UNIQUE | |
| academy_id | FK organizations | |
| branch_id | FK branches | |
| name | string | "Football Field A", "Indoor Pool" |
| name_ar | string | |
| code | string(10) | Unique per academy |
| type | string(30) | CHECK: football_field, basketball_court, swimming_pool, gym_hall, martial_arts_hall, tennis_court, multi_purpose, track, other |
| ownership | string(20) | CHECK: owned, rented, shared, temporary |
| capacity | integer | Max people at once |
| area_sqm | decimal(8,2) | Nullable — physical area |
| address | text | Nullable — if offsite |
| latitude | decimal(10,7) | Nullable |
| longitude | decimal(10,7) | Nullable |
| surface_type | string(20) | Nullable — grass, artificial, wood, concrete, water, mat |
| is_indoor | boolean | Default false |
| has_lighting | boolean | Default true |
| has_changing_rooms | boolean | Default false |
| has_showers | boolean | Default false |
| rental_cost_per_hour | bigint | Nullable — piasters (for rented facilities) |
| rental_contact | string | Nullable |
| maintenance_notes | text | Nullable |
| photo_path | string | Nullable |
| status | string(20) | CHECK: active, maintenance, closed, reserved, unavailable |
| status_reason | text | Nullable |
| status_until | date | Nullable — when maintenance/closure ends |
| metadata | jsonb | Default '{}' |
| created_by | FK users | |
| created_at | timestamp | |
| updated_at | timestamp | |
| deleted_at | timestamp | |

## Entity: `facility_operating_hours`

| Column | Type | Rules |
|--------|------|-------|
| id | bigint PK | |
| facility_id | FK facilities | |
| day_of_week | integer | 0-6 |
| opens_at | time | |
| closes_at | time | |
| is_closed | boolean | Default false — if true, facility closed this day |
| notes | string | Nullable — "Maintenance morning only" |

## Facility Business Rules
- Operating hours are enforced: no reservations outside hours
- Maintenance windows: facility.status = 'maintenance' blocks ALL reservations for the period
- Shared facilities: multiple academies may reference the same physical space (future — for now, one academy owns it)
- Rented facilities: system tracks rental cost → feeds into financial module as an expense
- Facility closure: cancels all scheduled sessions + notifies affected groups

---

# PART B: SPACE MANAGEMENT

The most unique module. A facility is NOT permanently divided — layouts are TEMPORAL.

## Entity: `space_layouts`

A layout defines HOW a facility is divided during a specific time window.

| Column | Type | Rules |
|--------|------|-------|
| id | bigint PK | |
| uuid | uuid UNIQUE | |
| facility_id | FK facilities | |
| academy_id | FK organizations | |
| name | string | Nullable — "Tournament Layout", "Normal Training" |
| layout_type | string(20) | CHECK: grid, lanes, zones, custom |
| rows | integer | Nullable — for grid type |
| columns | integer | Nullable — for grid type |
| lane_count | integer | Nullable — for lanes type |
| zone_definitions | jsonb | Default '[]' — for zones/custom [{name, capacity, color}] |
| effective_date | date | When this layout applies |
| effective_day_of_week | integer | Nullable — if recurring (null = specific date only) |
| start_time | time | |
| end_time | time | |
| is_recurring | boolean | Default false — recurring = every week on this day/time |
| status | string(20) | CHECK: active, expired, template |
| created_by | FK users | |
| created_at | timestamp | |
| updated_at | timestamp | |

### Layout Types

**Grid** (football fields, gyms):
- Defined by rows × columns
- Each cell is one segment
- Visual: rectangular grid overlay

**Lanes** (swimming pools, tracks):
- Defined by lane_count
- Each lane is one segment
- Visual: parallel strips

**Zones** (martial arts, mixed-use):
- Defined by zone_definitions JSON
- Each zone has a name, capacity, optional color
- Visual: labeled areas (non-geometric)

**Custom**:
- Free-form defined by zone_definitions
- No geometric constraints

## Entity: `space_segments`

Individual bookable units within a layout.

| Column | Type | Rules |
|--------|------|-------|
| id | bigint PK | |
| space_layout_id | FK space_layouts | |
| identifier | string(20) | "R1C1", "Lane 3", "Tatami A" |
| label | string | Nullable — human-friendly name |
| label_ar | string | Nullable |
| row | integer | Nullable — grid position |
| column | integer | Nullable — grid position |
| lane_number | integer | Nullable |
| zone_name | string | Nullable |
| capacity | integer | Default 1 — how many people fit in this segment |
| is_available | boolean | Default true — permanently unavailable (broken equipment, etc.) |
| sort_order | integer | Default 0 |
| metadata | jsonb | Default '{}' |

## Entity: `space_reservations`

Booking a segment (or multiple) for a specific time.

| Column | Type | Rules |
|--------|------|-------|
| id | bigint PK | |
| uuid | uuid UNIQUE | |
| academy_id | FK organizations | |
| facility_id | FK facilities | |
| space_layout_id | FK space_layouts | |
| segments | jsonb | Array of segment_ids reserved |
| reserved_for_type | string | Polymorphic — TrainingGroup, Event, PrivateSession, Maintenance |
| reserved_for_id | bigint | |
| training_session_id | FK training_sessions | Nullable — link to specific session |
| date | date | |
| start_time | time | |
| end_time | time | |
| status | string(20) | CHECK: confirmed, tentative, cancelled |
| reserved_by | FK users | |
| notes | text | Nullable |
| created_at | timestamp | |
| updated_at | timestamp | |

### Space Reservation Business Rules
- **No double-booking:** Same segment + overlapping time = BLOCKED
- **Time overlap detection:** 5:00-6:00 and 5:30-6:30 share segments from the SAME layout (overlapping time means same grid)
- **Non-overlapping times get their own layout:** 5:00-6:00 and 7:00-8:00 can have different layouts entirely
- **Layout inheritance:** If no layout exists for a time window, admin creates one. Once created, it persists for that recurring slot.
- **Cancellation:** Frees segments for other groups. Triggers notification if waitlisted.
- **Maintenance block:** Segments can be individually blocked (equipment on segment, wet floor)
- **Visual booking:** UI shows grid/lanes/zones with occupied (red) and available (green) cells

### Collision Detection Service

```
SpaceCollisionService::check(facility, date, startTime, endTime, segmentIds): CollisionResult

1. Find all reservations for this facility on this date that overlap the time window
2. Collect all their reserved segment_ids
3. Check intersection with requested segmentIds
4. If intersection is empty → no collision, return OK
5. If intersection exists → return conflicting reservations with details

Time overlap logic:
- Two time ranges overlap if: start1 < end2 AND start2 < end1
```

### Scheduling Workflow (UI Flow)

```
1. Select Training Group
2. Select Date + Time
3. System shows available facilities (filtered by: open, correct type, has capacity)
4. Admin selects facility
5. System checks: does a layout exist for this facility at this time?
   - YES: load layout, show segments (occupied = red, available = green)
   - NO: admin creates a new layout (choose type, define dimensions)
6. Admin selects available segments (click, drag, row-select, column-select)
7. System validates: no collisions, within operating hours, segment is available
8. Reservation saved
9. Schedule entry created → sessions generated → reservations auto-created per session
```

---

# PART C: ASSIGNMENT ENGINE

## Goal

A universal system for assigning any person to any responsibility for any duration.

## Entity: `assignments`

| Column | Type | Rules |
|--------|------|-------|
| id | bigint PK | |
| uuid | uuid UNIQUE | |
| academy_id | FK organizations | |
| person_id | FK people | Who is assigned |
| role | string(30) | What role they're fulfilling |
| assignable_type | string | Polymorphic target — what they're assigned TO |
| assignable_id | bigint | |
| scope | string(20) | CHECK: full, partial, supervising, assisting, observing |
| branch_id | FK branches | Nullable |
| start_date | date | |
| end_date | date | Nullable — null = indefinite |
| start_time | time | Nullable — if time-specific (daily recurring) |
| end_time | time | Nullable |
| day_of_week | integer | Nullable — for recurring day-specific assignments |
| is_recurring | boolean | Default false |
| priority | integer | Default 0 — when multiple people assigned |
| notes | text | Nullable |
| status | string(20) | CHECK: active, suspended, completed, cancelled |
| assigned_by | FK users | |
| created_at | timestamp | |
| updated_at | timestamp | |

### Assignment Targets (assignable_type)

| Target | Example |
|--------|---------|
| `TrainingProgram` | Head coach for all groups of this program |
| `TrainingGroup` | Primary trainer for this specific group |
| `TrainingSession` | Substitute for one session only |
| `Facility` | Facility supervisor (oversees all groups there) |
| `SpaceSegment` | Area-specific responsibility |
| `Branch` | Floating staff available anywhere in branch |
| `Event` | Tournament coordinator |

### Assignment Roles

System roles (seeded):
- `head_trainer`, `assistant_trainer`, `substitute_trainer`
- `supervisor`, `coordinator`, `observer`
- `medical_staff`, `equipment_manager`, `safety_officer`
- `photographer`, `referee`, `volunteer`
- `cleaner`, `security`, `receptionist`

Custom roles: academies can define additional roles.

### Assignment Business Rules
- One person can have MULTIPLE simultaneous assignments (supervisor of court + trainer of group = both valid)
- Assignment conflicts: same person at different facilities at overlapping times = BLOCKED
- Trainer max hours: if trainer has max_daily_sessions=4 and already has 4 → warn/block
- Assignment inheritance: trainer assigned to GROUP → automatically expected at all sessions of that group
- Session-level override: group trainer is Mohamed, but session override = Ali → Ali attends, Mohamed does NOT
- Recurring assignments: "Every Monday 5-7PM, Ahmed supervises Football Court A" → generates attendance expectations weekly
- End date handling: assignment with end_date → auto-completes on that date

### Assignment → Attendance Connection

**Critical design decision:** Assignments AUTOMATICALLY generate attendance expectations.

When `assignment.status = active`:
- If target is `TrainingGroup`: person expected at every session of that group
- If target is `Facility` with time: person expected during those hours
- If target is `TrainingSession`: person expected at that one session
- If target is `Branch` without specific time: person expected during branch operating hours

The Attendance Engine reads assignments to know WHO should be WHERE and WHEN.

---

# PART D: ATTENDANCE ENGINE

## Goal

One attendance system for everyone. Subjects: players, trainers, staff, volunteers, visitors. Sources: sessions, shifts, events, meetings.

## Entity: `attendance_records`

| Column | Type | Rules |
|--------|------|-------|
| id | bigint PK | |
| uuid | uuid UNIQUE | |
| academy_id | FK organizations | |
| person_id | FK people | Who is this attendance for |
| subject_type | string(20) | CHECK: participant, trainer, employee, volunteer, visitor |
| attendable_type | string | Polymorphic — what they're attending |
| attendable_id | bigint | TrainingSession, Event, Shift, Meeting |
| assignment_id | FK assignments | Nullable — which assignment created this expectation |
| date | date | |
| expected_start | time | Nullable — when they should arrive |
| expected_end | time | Nullable — when they should leave |
| actual_check_in | time | Nullable |
| actual_check_out | time | Nullable |
| status | string(20) | CHECK: see below |
| marked_by | FK users | Nullable — who recorded this |
| marked_at | timestamp | Nullable — when recorded |
| verification_method | string(20) | Nullable — CHECK: manual, qr_code, biometric, self_checkin, system_auto |
| late_minutes | integer | Nullable — calculated if late |
| early_departure_minutes | integer | Nullable — calculated if left early |
| excuse_type | string(20) | Nullable — CHECK: medical, family, weather, transport, academic, other |
| excuse_document_path | string | Nullable — uploaded excuse |
| excuse_approved | boolean | Nullable — true/false, null=pending |
| excuse_approved_by | FK users | Nullable |
| notes | text | Nullable |
| metadata | jsonb | Default '{}' |
| created_at | timestamp | |
| updated_at | timestamp | |

### Attendance Statuses

| Status | Meaning | Created By |
|--------|---------|-----------|
| `expected` | Attendance expected, not yet taken | System (auto-generated) |
| `present` | Person was there on time | Trainer/Admin marks |
| `late` | Person arrived after grace period | System calculates from check_in vs expected_start |
| `excused` | Absent with valid excuse | Admin approves excuse |
| `absent` | Absent without excuse | Default after session ends if not marked present |
| `no_show` | Expected but never appeared, no excuse | After grace period + no contact |
| `left_early` | Was present but left before session ended | Trainer marks |
| `partial` | Present for part of the session | Trainer marks + time range |
| `cancelled` | Session was cancelled, attendance voided | System (when session cancelled) |
| `exempt` | Not expected to attend (medical, excused in advance) | Admin/Trainer pre-marks |

### Attendance Auto-Generation

**When a training session is created:**
1. Get all enrolled participants in the group → create `expected` records (subject_type=participant)
2. Get all assigned trainers for this session/group → create `expected` records (subject_type=trainer)
3. Get all facility supervisors for this facility+time → create `expected` records (subject_type=employee)

**When a shift is created (future module):**
1. Get all employees assigned to this shift → create `expected` records

This means: when a trainer opens the attendance sheet, all participants are ALREADY listed with status=`expected`. They just change each one to `present`/`absent`/etc.

### Attendance Business Rules

**Grace Period:**
- Configurable per academy (default 15 min)
- Arrive after grace period → status=`late`, late_minutes calculated
- Configurable: separate grace for trainers vs participants

**Auto-Absent:**
- Scheduled job: if session ended 2+ hours ago and attendance not taken → mark all remaining `expected` as `absent` + flag for admin review
- Configurable: some academies want manual-only (disable auto-absent)

**Excuse Workflow:**
1. Participant/Guardian submits excuse (before or after absence)
2. If submitted before session → status=`excused` (pre-approved types: medical with doc)
3. If submitted after → status remains `absent` until approved
4. Admin reviews: approve → status changes to `excused`, reject → remains `absent`
5. Configurable: max excuses per month before flagging

**Attendance Impact on Other Modules:**
| Impact | Trigger | Action |
|--------|---------|--------|
| Trainer Payment | Session completed + trainer=present | Include in payroll calculation |
| Trainer Payment | Session + trainer=absent | Exclude from payment (or deduct) |
| Program Completion | All sessions attended + min% reached | Eligible for graduation |
| Auto-Suspension | X consecutive absences without excuse | Participant status → suspended |
| Guardian Notification | Any absence | Send notification to primary guardian |
| Session Stats | End of session | Update actual_participants count |
| Invoice Adjustment | Frozen participant attends (shouldn't) | Flag for review |

**Bulk Attendance:**
- Trainer can mark "all present" then adjust individuals
- Trainer can mark "all absent" for cancelled sessions (system should handle this)
- Admin can mark attendance for multiple sessions at once (backfill)

### Attendance Reporting

Per-participant report:
- Total sessions expected
- Total present + late + excused + absent
- Attendance percentage = (present + late + excused) / (expected - cancelled - exempt) × 100
- Trend: last 4 weeks breakdown
- Flag: declining attendance triggers notification

Per-group report:
- Average attendance rate
- Best/worst attendance days
- No-show patterns (specific participants repeatedly missing)

Per-trainer report:
- Sessions conducted
- Punctuality (late arrivals)
- Sessions where they didn't take attendance (flagged)

---

## Scheduled Jobs (Facility + Attendance)

| Job | Frequency | Action |
|-----|-----------|--------|
| `AutoMarkAbsent` | Hourly | Sessions ended 2h+ ago with unmarked attendance → mark absent |
| `FacilityMaintenanceCheck` | Daily | Alert if maintenance.status_until has passed (should be back active) |
| `AttendanceAlerts` | Daily | Flag participants with 3+ consecutive absences → notify guardian + admin |
| `TrainerAttendanceReminder` | 30min after session start | If trainer hasn't checked in → notify admin |
| `ExcuseExpiryCheck` | Daily | Pending excuses older than 7 days → auto-reject + notify |

---

## Events Dispatched

| Event | Trigger | Consumers |
|-------|---------|-----------|
| `AttendanceMarked` | Any attendance status change | Stats recalculation, guardian notification |
| `ParticipantAbsent` | Participant marked absent | Guardian notification, consecutive count check |
| `TrainerAbsent` | Trainer absent from assigned session | Admin alert, substitute needed |
| `AttendanceThresholdBreached` | Participant below min attendance % | Auto-suspension check, notification |
| `SpaceReservationCreated` | New reservation confirmed | Calendar update |
| `SpaceCollisionDetected` | Conflict found during booking | Block booking, show conflict details |
| `FacilityStatusChanged` | Facility goes to maintenance/closed | Cancel affected sessions, notify groups |
| `AssignmentCreated` | New assignment | Attendance expectations generated |
| `AssignmentEnded` | Assignment reaches end_date | Stop generating attendance expectations |
