# RULE FILE 05 — Facility, Assignment & Attendance Rules

---

## Facility Rules

### Status Transitions

```
active       → maintenance (scheduled or emergency)
active       → closed (long-term shutdown)
active       → reserved (exclusive booking)
active       → unavailable (external factor)
maintenance  → active (maintenance complete)
closed       → active (reopened)
reserved     → active (reservation period ended)
unavailable  → active (issue resolved)
```

### Facility Closure Impact

When facility `status` changes to anything other than `active`:
1. Find all sessions in this facility during the closure period
2. Cancel those sessions (status='cancelled', cancellation_reason='facility_closure')
3. Cancel corresponding space reservations
4. Notify affected groups' trainers and enrolled participants' guardians
5. Mark affected attendance records as 'cancelled'
6. Log the event in audit trail

### Operating Hours Enforcement

- No reservation can START before `opens_at` for that day
- No reservation can END after `closes_at` for that day
- If `is_closed = true` for that day → block ALL reservations
- Operating hours are per-facility, per-day-of-week

---

## Space Layout Rules

### Layout Types — EXACT Behavior

| Type | Definition Columns | Segment Generation |
|------|-------------------|-------------------|
| `grid` | `rows`, `columns` | Auto: rows × columns segments, identified as R{r}C{c} |
| `lanes` | `lane_count` | Auto: N segments, identified as Lane 1, Lane 2, ... |
| `zones` | `zone_definitions` (jsonb) | From JSON: each zone object = one segment |
| `custom` | `zone_definitions` (jsonb) | From JSON: free-form, no geometric meaning |

### Layout Temporal Rules

1. A facility can have DIFFERENT layouts at different times on the same day
2. Example: 8AM-12PM = 3×2 grid (kids groups, small areas), 2PM-8PM = 2×1 grid (adult, larger areas)
3. Layouts are identified by: `facility_id + effective_day_of_week + start_time + end_time`
4. No two layouts for the same facility can have overlapping time windows on the same day
5. `is_recurring = true`: applies every week on that day/time
6. `is_recurring = false` + `effective_date`: applies to one specific date only (overrides recurring)
7. A specific-date layout OVERRIDES the recurring layout for that time window

### Segment Availability Rules

- `is_available = false` on a segment: permanently blocked (broken equipment, structural issue)
- This is different from a reservation — a reserved segment is temporarily occupied
- Unavailable segments show as GREY in UI (not green/red)
- Cannot reserve an unavailable segment — service throws `SegmentUnavailableException`

---

## Space Reservation & Collision Detection

### Collision Detection — THE EXACT ALGORITHM

```php
SpaceCollisionService::check(
    int $facilityId,
    string $date,
    string $startTime,
    string $endTime,
    array $segmentIds,
    ?int $excludeReservationId = null // for editing existing reservation
): CollisionResult

// Algorithm:
// 1. Find all CONFIRMED reservations for this facility on this date
//    WHERE status = 'confirmed'
//    AND reservation_id != $excludeReservationId (if editing)
//
// 2. Filter to those that OVERLAP in time:
//    WHERE start_time < $endTime AND end_time > $startTime
//    (standard interval overlap formula)
//
// 3. For each overlapping reservation, decode its segments JSON array
//
// 4. Check intersection: requested_segments ∩ occupied_segments
//
// 5. If intersection is EMPTY → return CollisionResult(hasCollision: false)
//
// 6. If intersection is NOT EMPTY → return CollisionResult(
//      hasCollision: true,
//      conflictingSegments: [segment_ids],
//      conflictingReservations: [{reservation_id, reserved_for_type, reserved_for_id, time_range}]
//    )
```

### Reservation Status

```
CHECK (status IN ('confirmed', 'tentative', 'cancelled'))
```

- `confirmed`: segment is locked, collision detection blocks others
- `tentative`: segment is soft-held, collision detection WARNS but doesn't block
- `cancelled`: ignored by collision detection entirely

### Reservation Rules

1. Reservations reference segments from a SPECIFIC layout (`space_layout_id`)
2. The layout must be valid for the reservation's date + time (temporal match)
3. `segments` column stores an array of segment IDs: `[1, 3, 5]`
4. When a group schedule is created: auto-create reservations for all generated sessions
5. Cancelling a reservation frees segments immediately
6. Reservation `reserved_for_type` values: `'TrainingGroup', 'Event', 'PrivateSession', 'Maintenance', 'External'`

---

## Assignment Engine

### The Universal Assignment — One Table Rules Them All

```
assignments: id, uuid, academy_id, person_id, role, 
assignable_type, assignable_id, scope,
branch_id (nullable), start_date, end_date (nullable),
start_time (nullable), end_time (nullable), day_of_week (nullable),
is_recurring, priority, notes, status,
assigned_by, created_at, updated_at
```

### Assignment Target Types (assignable_type)

| Target Model | What It Means |
|-------------|---------------|
| `TrainingProgram` | Person oversees all groups of this program |
| `TrainingGroup` | Person is assigned to this specific group (e.g., primary trainer) |
| `TrainingSession` | One-time session assignment (substitute) |
| `Facility` | Person supervises this facility during assigned hours |
| `Branch` | Floating staff, available anywhere in this branch |
| `Event` | Event-specific role |

### Assignment Roles — Seeded Values

```
CHECK (role IN (
    'head_trainer', 'assistant_trainer', 'substitute_trainer',
    'supervisor', 'coordinator', 'observer',
    'medical_staff', 'equipment_manager', 'safety_officer',
    'photographer', 'referee', 'volunteer',
    'cleaner', 'security', 'receptionist'
))
```

### Assignment Scope

```
CHECK (scope IN ('full', 'partial', 'supervising', 'assisting', 'observing'))
```

- `full`: fully responsible (primary trainer)
- `partial`: handles part of the session (e.g., warm-up only)
- `supervising`: oversight, not direct involvement
- `assisting`: helper role under primary
- `observing`: present but not participating (evaluator, trainee)

### Assignment Conflict Detection

```php
AssignmentConflictService::check(int $personId, string $date, string $startTime, string $endTime): ?Conflict

// A conflict exists if:
// 1. Person has ANOTHER active assignment
// 2. That targets a DIFFERENT location (different facility or branch)
// 3. With OVERLAPPING time on the same date
//
// Same-location double assignment is ALLOWED (person can be trainer + supervisor at same place)
// Different-location overlap is BLOCKED (can't be in two places at once)
```

### Assignment → Attendance Generation Rules

| Assignment Type | Attendance Generated When |
|----------------|--------------------------|
| Group assignment | For every session of that group |
| Session assignment | For that one session only |
| Facility assignment (recurring) | For every day matching day_of_week + time window |
| Program assignment | For every session of every group in that program |
| Branch assignment (no time) | Daily during branch operating hours |

### Assignment Status

```
CHECK (status IN ('active', 'suspended', 'completed', 'cancelled'))
```

- `active`: currently in effect, generating attendance expectations
- `suspended`: temporarily paused (trainer on leave)
- `completed`: end_date reached, no longer active
- `cancelled`: manually terminated early

### Assignment Override Rules

1. Session-level assignment OVERRIDES group-level for that specific session
2. Example: Group trainer = Mohamed (assignment to group). Session 15 has substitute = Ali (assignment to session). For session 15: Ali is expected, Mohamed is NOT.
3. This means: when generating attendance for session 15, check session-level assignments FIRST. If found, use those. If not, fall back to group-level assignments.

---

## Attendance Engine

### Status Values — THE COMPLETE SET

```
CHECK (status IN ('expected', 'present', 'late', 'excused', 'absent', 'no_show', 'left_early', 'partial', 'cancelled', 'exempt'))
```

### Status Semantics — No Ambiguity

| Status | Who Sets It | When |
|--------|------------|------|
| `expected` | System | Auto-generated before session starts |
| `present` | Trainer/Admin | Person arrived on time |
| `late` | System calculates | `actual_check_in > expected_start + grace_period` |
| `excused` | Admin approves | Excuse submitted and approved |
| `absent` | System or Trainer | Session ended, person never appeared, no excuse |
| `no_show` | System | Expected, never appeared, no contact, past grace period |
| `left_early` | Trainer | Person was present but left before session ended |
| `partial` | Trainer | Attended part of session (with time range) |
| `cancelled` | System | Session itself was cancelled |
| `exempt` | Admin/System | Person not expected (frozen, approved leave, not enrolled yet) |

### Attendance Auto-Generation Service

```php
AttendanceGenerationService::generateForSession(TrainingSession $session): void

// 1. Get all ACTIVE enrollments for this session's training_group
// 2. For each enrolled participant: create attendance_record(status='expected', subject_type='participant')
// 3. Get all ACTIVE assignments targeting this session OR this group (with override logic)
// 4. For each assigned person: create attendance_record(status='expected', subject_type=role-based)
// 5. Does NOT generate for: frozen participants, suspended with exempt config, cancelled enrollments
```

### Grace Period Rules

1. Academy setting: `attendance_grace_period_minutes` (default: 15)
2. Separate setting: `trainer_grace_period_minutes` (default: 10)
3. If `actual_check_in` is provided:
   - If `actual_check_in <= expected_start + grace_period` → status = `present`
   - If `actual_check_in > expected_start + grace_period` → status = `late`, `late_minutes = actual_check_in - expected_start`
4. Grace period applies PER SUBJECT TYPE (trainers have tighter window than kids)

### Auto-Absent Job

```php
// Runs hourly via scheduler
AutoMarkAbsentJob::handle()

// 1. Find all attendance records WHERE:
//    - status = 'expected'
//    - The session's date + end_time is MORE than 2 hours ago
//    - attendance has NOT been taken (no trainer marked anything)
// 2. Mark all as status = 'absent'
// 3. Flag the session for admin review (attendance never taken = trainer forgot?)
// 4. Dispatch AbsentMarkedEvent for each (triggers guardian notifications)
```

### Attendance Percentage Calculation

```
attendance_rate = (present + late + partial) / (expected - cancelled - exempt) × 100

- present: full attendance
- late: still attended (counts positively)
- partial: still attended (counts positively)
- excused: does NOT count toward attendance % (neither positive nor negative)
- absent: counts AGAINST
- no_show: counts AGAINST
- left_early: counts as partial (positive)
- cancelled: excluded from calculation
- exempt: excluded from calculation
```

### Attendance Impact Matrix

| Condition | Threshold | Action |
|-----------|-----------|--------|
| Consecutive absences (no excuse) | 3 → warn, 5 → suspend | Auto-notify guardian. At threshold → ParticipantService::changeStatus('suspended') |
| Attendance % below minimum | < 75% (configurable) | Notify guardian + admin. Flag participant. |
| Trainer absent from assigned session | 1 occurrence | Alert admin immediately. Assign substitute. |
| Trainer attendance not taken | 30 min after session start | Send reminder to trainer. |
| Trainer attendance NEVER taken | 2 hours after session end | Auto-mark absent. Escalate to admin. |

### Excuse Workflow — EXACT STEPS

```
1. Guardian/Participant submits excuse (via app or admin enters on their behalf)
   → Creates excuse record: type, document_path (optional), date, session_id

2. Excuse types: 'medical', 'family', 'weather', 'transport', 'academic', 'other'
   → 'medical' with attached document = auto-approved (configurable)
   → All others = pending admin review

3. Admin reviews:
   → Approve: attendance.status changes from 'absent' to 'excused'
   → Reject: attendance.status remains 'absent', guardian notified of rejection

4. Excuse window: configurable (default: 7 days after absence)
   → After window: excuse cannot be submitted, absence is final

5. Excuse limit: configurable per academy (default: 5 per month)
   → At limit: admin can still approve (override), but gets warning
```

### Verification Methods

```
CHECK (verification_method IN ('manual', 'qr_code', 'biometric', 'self_checkin', 'system_auto'))
```

- `manual`: trainer marks on screen (default, always available)
- `qr_code`: participant scans QR at facility (future feature, interface defined)
- `biometric`: fingerprint/face (future, interface defined)
- `self_checkin`: participant confirms own attendance (for adult programs, if enabled)
- `system_auto`: system auto-marked (e.g., auto-absent job)

---

## Cross-Module Integration: Attendance Talks To Everything

| Module | Integration Point | Data Flow |
|--------|------------------|-----------|
| Financial | Trainer payroll | Only sessions where trainer.status = 'present' count toward pay |
| Financial | Session-based billing | If paying per-session, only 'present' sessions are billable |
| Participant | Auto-suspension | Consecutive absences trigger status change |
| Participant | Graduation eligibility | Min attendance % required for graduation |
| Training | Session completion | Session marked 'completed' only when attendance is taken |
| Notifications | Guardian alerts | Every 'absent' triggers guardian notification |
| Notifications | Trainer reminders | Unsubmitted attendance triggers reminder |
| Reporting | Attendance reports | Aggregated by participant, group, trainer, branch |
| Evaluations | Participation grade | Attendance % may factor into evaluation |
