# Cycle 7: Training & Attendance Gaps

## Goal
Align status values, add excuse management, build missing UI (sessions, schedules, holidays, waitlists), add FK constraints, wire events.

---

## Part A: Status Alignment

### Problem
Multiple entities have status values that differ between code and docs. Since the code is already built and data may exist, we align BY ADDING missing statuses rather than removing existing ones.

### Migration: `2024_01_01_000063_align_training_statuses.php`

#### Participants — Add `on_leave` and `archived`:
```php
DB::statement("ALTER TABLE participants DROP CONSTRAINT IF EXISTS participants_status_check");
DB::statement("ALTER TABLE participants ADD CONSTRAINT participants_status_check
    CHECK (status IN ('registered', 'active', 'frozen', 'suspended', 'inactive', 'on_leave', 'graduated', 'transferred', 'withdrawn', 'blacklisted', 'archived'))");
```

#### ParticipantStatus Enum — Add cases:
```php
case OnLeave = 'on_leave';
case Archived = 'archived';
```

#### ParticipantService VALID_TRANSITIONS — Add:
```php
'active' => [...existing..., 'on_leave'],
'on_leave' => ['active', 'withdrawn', 'inactive'],
'inactive' => [...existing..., 'archived'],
'archived' => [], // terminal
```

---

#### Enrollments — Add `frozen`, `withdrawn`, `transferred`:
```php
DB::statement("ALTER TABLE enrollments DROP CONSTRAINT IF EXISTS enrollments_status_check");
DB::statement("ALTER TABLE enrollments ADD CONSTRAINT enrollments_status_check
    CHECK (status IN ('pending', 'active', 'completed', 'cancelled', 'expired', 'waitlisted', 'frozen', 'withdrawn', 'transferred'))");
```

#### EnrollmentStatus Enum — Add cases:
```php
case Frozen = 'frozen';
case Withdrawn = 'withdrawn';
case Transferred = 'transferred';
```

---

#### Training Groups — Add `on_hold`:
Already exists in code. Align GroupStatus enum label() and color() methods.

---

#### Training Sessions — Add `no_show`:
```php
DB::statement("ALTER TABLE training_sessions DROP CONSTRAINT IF EXISTS training_sessions_status_check");
DB::statement("ALTER TABLE training_sessions ADD CONSTRAINT training_sessions_status_check
    CHECK (status IN ('scheduled', 'in_progress', 'completed', 'cancelled', 'rescheduled', 'no_show'))");
```

#### SessionStatus Enum — Add:
```php
case NoShow = 'no_show';
```

---

## Part B: Excuse Management Subsystem

### Migration: `2024_01_01_000064_add_excuse_columns_to_attendance.php`

```php
Schema::table('attendance_records', function (Blueprint $table) {
    $table->string('excuse_type', 20)->nullable()->after('notes');
    $table->text('excuse_reason')->nullable();
    $table->string('excuse_document_path', 500)->nullable();
    $table->boolean('excuse_approved')->nullable();  // null = pending, true = approved, false = rejected
    $table->foreignId('excuse_approved_by')->nullable()->constrained('users');
    $table->timestamp('excuse_submitted_at')->nullable();
    $table->timestamp('excuse_decided_at')->nullable();
    $table->string('excuse_rejection_reason')->nullable();
});

DB::statement("ALTER TABLE attendance_records ADD CONSTRAINT attendance_excuse_type_check
    CHECK (excuse_type IN ('medical', 'family', 'weather', 'transport', 'academic', 'travel', 'other'))");
```

### ExcuseService: `app/Domain/Attendance/Services/ExcuseService.php`

```php
class ExcuseService
{
    public function submit(
        AttendanceRecord $record,
        string $excuseType,
        string $reason,
        ?string $documentPath,
        User $actor
    ): AttendanceRecord {
        // Validate: record.status must be 'absent' or 'no_show' or 'late'
        // Validate: excuse not already submitted (excuse_submitted_at is null)
        // Validate: within excuse window (system_setting: 'excuse_window_days', default 7)
        return DB::transaction(function () use ($record, $excuseType, $reason, $documentPath, $actor) {
            $record->update([
                'excuse_type' => $excuseType,
                'excuse_reason' => $reason,
                'excuse_document_path' => $documentPath,
                'excuse_submitted_at' => now(),
                'status' => 'excused', // temporarily mark as excused pending approval
            ]);
            // Dispatch ExcuseSubmitted event
            return $record;
        });
    }

    public function approve(AttendanceRecord $record, User $actor): AttendanceRecord
    {
        return DB::transaction(function () use ($record, $actor) {
            $record->update([
                'excuse_approved' => true,
                'excuse_approved_by' => $actor->id,
                'excuse_decided_at' => now(),
                'status' => 'excused', // confirmed excused
            ]);
            // Dispatch ExcuseApproved event
            return $record;
        });
    }

    public function reject(AttendanceRecord $record, string $reason, User $actor): AttendanceRecord
    {
        return DB::transaction(function () use ($record, $reason, $actor) {
            $record->update([
                'excuse_approved' => false,
                'excuse_approved_by' => $actor->id,
                'excuse_decided_at' => now(),
                'excuse_rejection_reason' => $reason,
                'status' => 'absent', // revert to absent
            ]);
            // Dispatch ExcuseRejected event
            return $record;
        });
    }

    public function getPendingExcuses(int $academyId): Collection
    {
        return AttendanceRecord::where('academy_id', $academyId)
            ->whereNotNull('excuse_submitted_at')
            ->whereNull('excuse_approved')
            ->with(['subject', 'session.group'])
            ->orderBy('excuse_submitted_at')
            ->get();
    }
}
```

---

## Part C: Training Sessions UI

### Livewire: `app/Livewire/Sessions/SessionList.php`
```php
class SessionList extends Component
{
    use WithPagination;

    #[Url] public string $search = '';
    #[Url] public string $status = '';
    #[Url] public string $groupId = '';
    #[Url] public string $dateFrom = '';
    #[Url] public string $dateTo = '';

    public function render()
    {
        $this->authorize('sessions.list');

        $sessions = TrainingSession::query()
            ->with(['group', 'trainer'])
            ->when($this->search, fn ($q) => $q->whereHas('group', fn ($g) =>
                $g->where('name_ar', 'like', "%{$this->search}%")))
            ->when($this->status, fn ($q) => $q->where('status', $this->status))
            ->when($this->groupId, fn ($q) => $q->where('training_group_id', $this->groupId))
            ->when($this->dateFrom, fn ($q) => $q->where('session_date', '>=', $this->dateFrom))
            ->when($this->dateTo, fn ($q) => $q->where('session_date', '<=', $this->dateTo))
            ->orderByDesc('session_date')
            ->orderBy('start_time')
            ->paginate(20);

        return view('livewire.sessions.session-list', compact('sessions'));
    }

    // Actions: cancel, reschedule, startSession, completeSession
}
```

### Livewire: `app/Livewire/Sessions/SessionShow.php`
- Session details (group, trainer, date, time, status)
- Attendance summary (present/absent/late counts)
- Link to TakeAttendance
- Actions: cancel, reschedule, substitute trainer

---

## Part D: Training Schedules UI

### Livewire: `app/Livewire/Schedules/ScheduleManager.php`
- Shown within Group Show page (inline)
- List all schedules for a group
- Add/edit/delete schedules
- Fields: day_of_week, start_time, end_time, facility, trainer, assistant_trainer
- Effective date range (effective_from, effective_until)
- Toggle active/inactive

---

## Part E: Holiday CRUD

### Livewire: `app/Livewire/Holidays/HolidayList.php`
- List with year filter, affects filter
- Inline create/edit form (modal or inline row)
- Fields: name_ar, name, date, is_recurring, affects, branch_id (optional)
- Delete with confirmation

### Routes:
```php
Route::get('/holidays', HolidayList::class)->name('holidays.index')->middleware('permission:holidays.list');
```

---

## Part F: Waitlist Management UI

### Livewire: `app/Livewire/Waitlists/WaitlistManager.php`
- Shown per group (filter by group_id)
- Shows: position, participant name, requested_at, status (waiting/notified/expired)
- Actions: notify (send offer), promote (enroll directly), remove
- Auto-calculated: spots available = group.max_capacity - group.current_count

### Add to Group Show page as a tab alongside schedules and enrollments.

---

## Part G: Add Missing FK Constraints

### Migration: `2024_01_01_000065_add_deferred_fk_constraints.php`

```php
// training_schedules.facility_id → facilities
Schema::table('training_schedules', function (Blueprint $table) {
    // Only add if not already constrained
    $table->foreign('facility_id')->references('id')->on('facilities')->nullOnDelete();
});

// training_sessions.facility_id → facilities (if column exists as unsignedBigInteger)
Schema::table('training_sessions', function (Blueprint $table) {
    $table->foreign('facility_id')->references('id')->on('facilities')->nullOnDelete();
});
```

---

## Part H: Session Events

Events to add (dispatched from SessionGeneratorService and new session actions):

| Event | When |
|-------|------|
| `SessionCreated` | After generate() creates a session |
| `SessionCancelled` | After cancelSession() |
| `SessionCompleted` | After completeSession() (new method) |
| `SessionStarted` | After startSession() (new method) |

### New SessionGeneratorService Methods:

```php
public function startSession(TrainingSession $session, User $actor): TrainingSession
{
    // Validate: status must be 'scheduled' AND session_date = today AND start_time <= now
    // Transition: scheduled → in_progress
    // Dispatch SessionStarted event
}

public function completeSession(TrainingSession $session, User $actor): TrainingSession
{
    // Validate: status must be 'in_progress'
    // Transition: in_progress → completed
    // Update actual metrics (actual_end_time, etc.)
    // Dispatch SessionCompleted event
}
```

---

## Routes to Add

```php
Route::middleware(['auth', 'permission:sessions.list'])->group(function () {
    Route::get('/sessions', SessionList::class)->name('sessions.index');
    Route::get('/sessions/{session}', SessionShow::class)->name('sessions.show');
});

Route::get('/holidays', HolidayList::class)->name('holidays.index')
    ->middleware(['auth', 'permission:settings.manage']);
```

---

## Sidebar Addition

Under "التدريب" (Training):
- الحصص (Sessions) — sessions.index
- الإجازات (Holidays) — holidays.index

Under each Group Show:
- Tab: الجداول (Schedules)
- Tab: قائمة الانتظار (Waitlist)

---

## Permissions to Seed

```
sessions.list, sessions.show, sessions.cancel, sessions.reschedule, sessions.start, sessions.complete
holidays.list, holidays.create, holidays.update, holidays.delete
excuses.list, excuses.approve, excuses.reject
waitlists.manage, waitlists.promote
schedules.manage
```

---

## Verification

- [ ] New statuses accepted by CHECK constraints
- [ ] ParticipantService allows `active → on_leave → active` transition
- [ ] Excuse submitted → appears in pending list → approve/reject works
- [ ] Session list shows all sessions with correct filters
- [ ] Schedule manager CRUD within group page works
- [ ] Holiday CRUD works, session generator skips holidays
- [ ] Waitlist shows correct position, promote enrolls participant
- [ ] FK constraints prevent orphaned records
- [ ] Session start/complete transitions work
