# Sports Activity System — Alignment Plan

## Honest Assessment

The current implementation has the **infrastructure** (tables, models, services, routes) but the **behavior** described in the requirements is NOT met. Here's the brutal truth:

| Requirement | Current Reality |
|-------------|----------------|
| Mirror filtered/categorized by sport | Flat grid of all facilities. No grouping, no filter, no discipline tabs. You click one-by-one. |
| Organization booking with separate pricing | Only `member` / `guest`. No organization concept anywhere in booking flow. |
| Wizard: pick sport → pick program → auto-assign group | Wizard makes user manually browse ALL groups (flat list), pick one themselves. No sport→program flow. |
| Auto-create group when full | Hard error "المجموعة ممتلئة". Suggests waitlist. Dead end. |
| Coach force-enroll past capacity | Hard block at `max_capacity`. No override. No bypass. |
| Coach transfer to full group | `GroupTransferService` hard blocks at line 40 if target full. No force flag. |
| Pool: open-access ticket zones | Pool grid only supports cell-level reserved bookings. No concept of "open zone pay-a-ticket-and-swim". |
| Mirror shows WHO booked + type clearly | Shows `booker_name` for hourly and `group_name` for training. But no organization, no color distinction by booker type. |
| Expected Capacity per facility unit | Does NOT exist. `max_capacity` on `sa_facility_units` is for concurrent booking slots, NOT for "how many people use this facility per booking". |
| Passes system for bookings | Does NOT exist. `sa_booking_participants` has individual names but no concept of anonymous numbered passes. |
| Participant registration (full/partial/passes) | Only basic `participant_name` + `player_id`. No national_id on participants. No pass-code system. No gate integration for bookings. |
| Booking = capacity for group of people | System treats `participant_count` as just a number for pricing. No enforcement at gates. No pass issuance. |

---

## What's Actually Working Correctly

1. **Facility model has `discipline_id`** — the link exists in DB, just unused in mirror UI.
2. **Mirror timeline renders correctly** per single facility — shows units as rows, time as columns, color-coded cells.
3. **Pool grid** works for cell-level assignment (training, blocked, maintenance, hourly).
4. **Booking service** correctly stores `booker_name`, `participant_count`, creates bookings.
5. **`sa_booking_participants` table** exists with: `booking_id`, `player_id`, `participant_name`, `is_member`, `price_charged`, `checked_in_at`, `checked_out_at`.
6. **Enrollment + payments** work for normal (non-full) groups.
7. **Registration wizard** works end-to-end — but with wrong UX flow.
8. **Group schedule + capacity tracking** — correctly maintained.
9. **`sa_bookings.booker_type`** is VARCHAR(20), already supports arbitrary values — no ENUM constraint.

---

## GAP 1: Mirror — No Filtering / No Discipline Categorization

### What the requirement says:
> المراية ما هي الا visualization للمرفق الواحد ولازم يكونو categorized or filtered بالرياضة الواحدة
> لو انا بحجز لحد bowling او باليستيشن مش لازم اخش عليهم مرفق مرفق عشان اشوف انهي حارة فاضية

### What currently exists:
- `mirror/index.php` — flat card grid, `foreach ($facilities as $f)`, no grouping.
- No discipline name shown on cards.
- No tabs, no filters, no search.
- `Facility::getActive()` returns all facilities unsorted by discipline.
- `MirrorController::index()` just passes raw facility list.

### What needs to happen:

**`MirrorController.php`** — `index()`:
- Load all disciplines that have active facilities.
- Group facilities by discipline.
- Pass both to view.

**`MirrorController.php`** — new `byDiscipline(Request, string $disciplineId)`:
- Load ALL facilities where `discipline_id = $id`.
- Build combined state for all of them in one page.
- Users see all bowling lanes / all PS courts / all football pitches at once.

**`MirrorController.php`** — new `byDisciplineDate(Request, string $disciplineId, string $date)`:
- Same but for specific date.

**`MirrorStateService.php`** — new `getMultiFacilityState(int $disciplineId, string $date)`:
- Loop each facility of that discipline.
- Call existing `getFacilityState()` per facility.
- Return array of all results.

**`Views/mirror/index.php`** — complete rewrite:
- Discipline tabs/filter chips at top.
- Facilities grouped under discipline headings.
- Click discipline → goes to `/sa/mirror/discipline/{id}` showing all facilities' timelines.
- Click single facility → existing behavior.

**`Views/mirror/discipline_view.php`** — NEW:
- Shows multiple facility timelines stacked vertically.
- Each facility has its own grid (rows = units, cols = time slots).
- Color-coded consistently.
- Date picker applies to all.

**`Routes.php`** — add:
```
['GET', '/sa/mirror/discipline/{id:\d+}', '...MirrorController@byDiscipline', ['auth'], 'sa.mirror.view']
['GET', '/sa/mirror/discipline/{id:\d+}/{date}', '...MirrorController@byDisciplineDate', ['auth'], 'sa.mirror.view']
```

**Cascading**:
- `Facility::getActive()` should also load `discipline_name` via join — or add `getActiveGroupedByDiscipline()`.
- Booking wizard could also benefit from discipline filter (but not required now).

---

## GAP 2: Organization Booker Type

### What the requirement says:
> سعر للساعة على حسب الوقت او اللي بيحجزها عضو او غير عضو او مؤسسة

### What currently exists:
- `SaConstants`: `BOOKER_MEMBER = 'member'`, `BOOKER_GUEST = 'guest'`. That's it.
- `BookingService::createHourlyBooking()` accepts `booker_type` but only member/guest logic.
- `PricingCalculatorService::calculate()` has `$isMember` boolean — binary, no organization path.
- `BookingWizardController` validates `booker_type` but no org option in UI.
- `sa_bookings.booker_type` is VARCHAR(20) — can store 'organization' without migration.

### What needs to happen:

**Migration** (for org-specific fields only):
```sql
ALTER TABLE sa_bookings ADD COLUMN organization_name VARCHAR(255) NULL AFTER booker_name;
ALTER TABLE sa_bookings ADD COLUMN organization_contact VARCHAR(100) NULL AFTER organization_name;
ALTER TABLE sa_bookings ADD COLUMN booker_national_id VARCHAR(14) NULL AFTER organization_contact;
-- booker_national_id may already exist, check first
```

**`SaConstants.php`**:
- Add `BOOKER_ORGANIZATION = 'organization'`

**`PricingCalculatorService.php`**:
- Change from `bool $isMember` to `string $bookerType`.
- Add pricing rule lookup for `organization` type.
- Fallback: if no org pricing rule exists, use guest pricing.

**`BookingService.php`**:
- Accept + store `organization_name`, `organization_contact`.

**`BookingWizardController.php`**:
- Add `organization` option to booker_type select.
- Show/hide org name + contact fields based on selection.
- Validation: if org type, require org name.

**`Views/booking-wizard/` (or `Views/bookings/wizard.php`)**:
- Add radio/select for "مؤسسة" as third booker type option.
- Conditional fields: org name, contact person.

**`sa_pricing_rules`** or **`sa_time_brackets`**:
- Verify schema supports a `target_type` or similar for organization pricing.
- If not: add organization rate columns to `sa_time_brackets`.

**Cascading**:
- Mirror view already shows `booker_name` — for org bookings, show `organization_name` instead.
- `MirrorStateService` — include `organization_name` in booking data passed to view.
- Reports that filter by booker_type will auto-include new type.

---

## GAP 3: Registration Wizard — Wrong Flow

### What the requirement says:
> بدخل اللاعب وبياناته برفق صورته وشهادته الطبية ان وجدت واختار الرياضة ثم اختار البرنامج التدريبي المسعر واحدفه على الخزنة اوتوماتيكي

### What currently exists:
- 5-step wizard: **استمارة** (form fee) → **الصورة** (photo) → **النشاط** (activity/group) → **الدفع** (pay) → **الاستلام** (card)
- Step 3 shows a flat list of ALL active groups across all disciplines.
- User must scroll through groups, manually pick one.
- No "choose sport first" step.
- No "choose program" step (program is just a label on the group card).
- Group list shows: group name, discipline name, program name, price, capacity bar.
- System does NOT auto-pick a group. It's 100% manual.

### What needs to happen:

**New wizard flow**:
| Step | Label | What happens |
|------|-------|-------------|
| 1 | بيانات اللاعب | Player data + photo + medical cert upload — all in ONE step |
| 2 | الرياضة | Pick discipline (sport) — big clickable tiles |
| 3 | البرنامج | Pick program (shows price for member/non-member). System auto-assigns group. |
| 4 | الدفع | Total fees calculated → send to cashier |
| 5 | الاستلام | Print form + generate card |

**`RegistrationWizardController.php`**:
- `wizardStep()` — restructure: load disciplines for step 2, programs for step 3.
- New endpoint: `getPrograms(Request, string $disciplineId): Response` — returns programs for selected discipline as JSON.
- `selectActivity()` — accept `program_id` instead of `group_id`. Internally resolve group via auto-assign.
- `determineStep()` — rewrite logic:
  - No photo + no medical → step 1
  - No discipline selected → step 2
  - No program selected → step 3
  - Not paid → step 4
  - Paid → step 5

**`RegistrationWizardService.php`**:
- `selectGroup()` → refactor to `selectProgram(int $registrationId, int $programId, int $months, bool $hasSibling)`:
  - Call `EnrollmentService::findOrCreateAvailableGroup($programId)` (from GAP 4).
  - Use returned group for enrollment.
  - Rest of logic (fee calc, discount, etc.) stays same.
- `startRegistration()` — accept photo + medical doc in same call (or keep separate endpoint but UI presents as one step).

**`sa_registrations` table**:
- May need `discipline_id` column if not present (check — might already have via `group_id` → `program` → `discipline`).

**`Views/registration/wizard.php`** — major rewrite:
- Step 1: merge current step 1 (form fee) + step 2 (photo) into single panel. Player fills data, captures photo, uploads medical cert — one "next" button.
- Step 2: NEW — show discipline cards (football, swimming, basketball, etc.) as big clickable tiles.
- Step 3: NEW — show programs under selected discipline with prices. No groups visible to user.
- Step 4: show fee breakdown + "إرسال للخزنة" button. Group is auto-assigned in background.
- Step 5: print + card (stays same).

**`Routes.php`** — add:
```
['GET', '/api/sa/programs-by-discipline/{id:\d+}', '...RegistrationWizardController@getPrograms', ['auth'], 'sa.registration.manage']
```

**Cascading**:
- If form fee is still required, include it in step 4 total (not separate step).
- Medical cert upload: add to step 1. Creates `sa_player_documents` record.
- Old in-progress registrations (old flow) — `determineStep()` must handle gracefully.

---

## GAP 4: Auto-Group Creation When Full

### What the requirement says:
> السيستم بيدخله في اخر مجموعة غير مكتملة للبرنامج دا ولو المجموعة المتاحة طلعت مكتملة السيستم اوتوماتيك يفتح مجموعة جديدة ويدخل اللاعب دا فيها

### What currently exists:
- `EnrollmentService::enroll()` line 70: `if (current_count >= max_capacity) → return error "المجموعة ممتلئة"`.
- `RegistrationWizardService::selectGroup()` line 190: same check — `return error "المجموعة ممتلئة"`.
- No method to find "last non-full group for program".
- No method to create a new group from a program template.

### What needs to happen:

**New method `EnrollmentService::findOrCreateAvailableGroup(int $programId): array`**:
1. Find last active non-full group for this program.
2. If found → return it.
3. If not found → clone from last group of same program (even if full):
   - Copy: `name_ar` (increment number), `program_id`, `facility_unit_id`, `coach_id`, fees, age limits, `max_capacity`, `branch_id`.
   - Set: `current_count = 0`, `is_full = 0`, `status = 'active'`.
   - Duplicate all `sa_group_schedule` entries from source group.
4. Return new group.

**Group naming**: "كرة قدم - مجموعة 3" → "كرة قدم - مجموعة 4". If no number, append " 2".

**Modify `RegistrationWizardService`**:
- New `selectProgram()` method — accepts `program_id`, auto-resolves group.

**Cascading**:
- `sa_group_schedule` entries duplicated → mirror shows training automatically.
- `SubscriptionGeneratorService` picks up new groups automatically.
- New group has `current_count = 0` — ready for enrollment.

---

## GAP 5: Coach Force-Enroll / Overload Group

### What the requirement says:
> من حق المدرب يدويا يرحل اي لاعب من مجموعة لمجموعة اخرى حتى لو هو فوق العدد المتاح (he can overload a group)

### What currently exists:
- `EnrollmentService::enroll()` line 70: hard block.
- `GroupTransferService::transfer()` line 40: `if (current_count >= max_capacity) → return error`.
- No permission for override. No UI button for force-add.

### What needs to happen:

**`EnrollmentService.php`** — new method `forceEnroll(int $groupId, int $playerId): array`:
- Same as `enroll()` but SKIP the capacity check.
- After enrollment, set `is_full = 1` if count >= max (informational only).

**`GroupTransferService.php`** — add `bool $force = false` parameter:
- When `$force = true` → skip capacity check at line 40.

**`GroupController.php`** — new `forceEnroll()` action + modify `transferPlayer()` to pass force flag when user has permission.

**`Routes.php`** — `['POST', '/sa/groups/{id:\d+}/force-enroll', ...]`

**`bootstrap.php`** — register `sa.group.force_enroll` permission.

**`Views/groups/show.php`** — "إضافة لاعب (تجاوز السعة)" button (shown only to authorized users).

**Cascading**:
- `current_count` can exceed `max_capacity`. `is_full = 1` stays set so normal path blocks.
- Add `force_enrolled` flag to `sa_group_players` for audit trail.

---

## GAP 6: Pool Open-Access Ticket Mode

### What the requirement says:
> ممكن نحجز حارات او cells ونحدد ايامه ومواعيده ون mark المساحة دي انها متاحة للحجز الحر... اي حد ييجي يدفع تيكت دخول

### What currently exists:
- Pool grid supports: training, blocked, maintenance, hourly — via `assignment_type`.
- `ServiceDeskController` has basic "تذكرة نشاط" — carnet-based, NOT pool-zone-aware.
- No concept of "open-access zone during time X".
- No occupancy tracking for open zones.
- Can't mark a zone as "members only" or "everyone".

### What needs to happen:

**Migration**:
```sql
ALTER TABLE sa_pool_zone_bookings ADD COLUMN access_mode ENUM('reserved','open_access') NOT NULL DEFAULT 'reserved' AFTER assignment_type;
ALTER TABLE sa_pool_zone_bookings ADD COLUMN access_restriction ENUM('all','members_only') NULL AFTER access_mode;
ALTER TABLE sa_pool_zone_bookings ADD COLUMN ticket_price_member DECIMAL(10,2) NULL;
ALTER TABLE sa_pool_zone_bookings ADD COLUMN ticket_price_nonmember DECIMAL(10,2) NULL;
ALTER TABLE sa_pool_zone_bookings ADD COLUMN max_occupancy INT UNSIGNED NULL;
ALTER TABLE sa_pool_zone_bookings ADD COLUMN current_occupancy INT UNSIGNED NOT NULL DEFAULT 0;

CREATE TABLE sa_pool_tickets (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    facility_id INT UNSIGNED NOT NULL,
    zone_booking_id BIGINT UNSIGNED NULL,
    booking_date DATE NOT NULL,
    start_time TIME NOT NULL,
    end_time TIME NOT NULL,
    member_id INT UNSIGNED NULL,
    player_id INT UNSIGNED NULL,
    guest_name VARCHAR(200) NULL,
    guest_national_id VARCHAR(14) NULL,
    is_member TINYINT(1) NOT NULL DEFAULT 0,
    amount_paid DECIMAL(10,2) NOT NULL DEFAULT 0.00,
    payment_status ENUM('unpaid','paid','refunded') NOT NULL DEFAULT 'unpaid',
    payment_request_id INT UNSIGNED NULL,
    entry_time DATETIME NULL,
    exit_time DATETIME NULL,
    status ENUM('issued','active','completed','cancelled') NOT NULL DEFAULT 'issued',
    issued_by INT UNSIGNED NULL,
    branch_id INT UNSIGNED NULL,
    created_at DATETIME NOT NULL,
    updated_at DATETIME NOT NULL,
    INDEX idx_pt_facility_date (facility_id, booking_date),
    INDEX idx_pt_zone (zone_booking_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```

**New `Services/PoolTicketService.php`**:
- `issueTicket()` — find open-access zone, check restriction + occupancy, create ticket, payment request, increment occupancy.
- `cancelTicket()` — decrement occupancy.
- `recordEntry()` / `recordExit()`.

**New `Controllers/PoolTicketController.php`** — list, issue, cancel.

**`PoolGridService.php`** — handle `access_mode` in state + `bulkAssign()`.

**`Views/pool-grid/`** — different color + occupancy display for open_access cells.

**Cascading**:
- `MirrorStateService` — open_access zones show as "دخول حر" not "محجوز".
- `PoolGridTemplateService` — templates need `access_mode` support.
- Pool financial dashboard includes ticket revenue.

---

## GAP 7: Expected Capacity + Passes System (NEW)

### What the requirement says:
> أي مرفق داخل النادي في النهاية بيكون محجوز... الحجز في الحقيقة لا يخص شخصًا واحدًا فقط، بل يخص عددًا معينًا من الأشخاص المسموح لهم بالدخول
> 
> لو شخص قام بحجز ملعب كرة خماسي لمدة ساعة، فمن الطبيعي إن الاستخدام الفعلي سيكون لـ 10 أفراد
> 
> Expected Capacity = العدد الطبيعي أو المتوقع للأشخاص الذين يستخدمون هذا المرفق في الحجز الواحد

### What currently exists:
- `sa_facility_units.max_capacity` — this is for **concurrent booking slots** (shared mode), NOT "how many people enter per booking".
- `sa_bookings.participant_count` — stored as a number, used only for pricing calculation.
- `sa_booking_participants` — can store names/player_ids, has `checked_in_at`/`checked_out_at`. But:
  - No national_id field on participants.
  - No "pass number" or anonymous pass concept.
  - No gate integration (gate system is for player cards only via `sa_gate_access_log`).
  - No enforcement at entry — just passive data.
- No concept of "expected capacity" on the facility unit.

### What needs to happen:

#### 7A: Expected Capacity on Facility Units

**Migration**:
```sql
ALTER TABLE sa_facility_units ADD COLUMN expected_capacity INT UNSIGNED NOT NULL DEFAULT 1 
    COMMENT 'Expected number of people per booking (e.g., 5-a-side pitch = 10)' AFTER max_capacity;
```

**`FacilityUnitController`** (or wherever units are managed):
- Add `expected_capacity` to create/edit forms.
- Display prominently in unit settings.

**`BookingService::createHourlyBooking()`**:
- When `participant_count` is not provided, default to `expected_capacity` from the facility unit.
- When `participant_count` significantly differs from `expected_capacity`, flag a warning (not a hard block).

**`BookingWizardController`**:
- Pre-fill participant count with `expected_capacity`.
- Show label: "العدد المتوقع: 10 أشخاص".

**`Views/booking-wizard/`**:
- Auto-fill participant count field.
- Visual indicator of expected vs entered.

#### 7B: Participant Registration — 3 Modes

The system must support 3 modes for tracking who is associated with a booking:

**Mode 1: Full Registration**
- Enter names + national IDs → creates `sa_booking_participants` with `player_id` linked.
- Each person is verifiable at the gate.
- Already partially supported — `sa_booking_participants` has `player_id`.

**Mode 2: Partial Registration (Names Only)**
- Enter names without full data.
- Already partially supported — `sa_booking_participants.participant_name`.
- Need: make this the default entry mode. Currently only used when booking is created via `BookingService` with explicit participants array.

**Mode 3: Passes (Anonymous)**
- Generate N passes linked to the booking.
- No personal data required.
- Gate staff count entries against pass limit.

**Migration for passes**:
```sql
CREATE TABLE sa_booking_passes (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    booking_id BIGINT UNSIGNED NOT NULL,
    pass_number VARCHAR(30) NOT NULL COMMENT 'e.g., BK-00123-P01',
    status ENUM('issued','used','expired','cancelled') NOT NULL DEFAULT 'issued',
    used_at DATETIME NULL,
    used_by_name VARCHAR(200) NULL COMMENT 'optional name if collected at gate',
    gate_employee_id BIGINT UNSIGNED NULL,
    created_at DATETIME NOT NULL,
    updated_at DATETIME NOT NULL,
    INDEX idx_bp_booking (booking_id),
    UNIQUE KEY uq_bp_number (pass_number),
    CONSTRAINT fk_bp_booking FOREIGN KEY (booking_id) REFERENCES sa_bookings(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- Track which mode a booking uses
ALTER TABLE sa_bookings ADD COLUMN participant_mode ENUM('full','partial','passes') NOT NULL DEFAULT 'passes' AFTER participant_count;
ALTER TABLE sa_bookings ADD COLUMN passes_total INT UNSIGNED NOT NULL DEFAULT 0 AFTER participant_mode;
ALTER TABLE sa_bookings ADD COLUMN passes_used INT UNSIGNED NOT NULL DEFAULT 0 AFTER passes_total;
```

**`SaConstants.php`**:
```php
const PARTICIPANT_MODE_FULL = 'full';
const PARTICIPANT_MODE_PARTIAL = 'partial';
const PARTICIPANT_MODE_PASSES = 'passes';
const PASS_ISSUED = 'issued';
const PASS_USED = 'used';
const PASS_EXPIRED = 'expired';
const PASS_CANCELLED = 'cancelled';
```

**New `Services/BookingPassService.php`**:
- `generatePasses(int $bookingId, int $count): array` — creates N pass records with sequential numbers.
- `usePass(string $passNumber, ?string $name, int $employeeId): array` — marks pass as used, increments `passes_used`.
- `getBookingPasses(int $bookingId): array` — list all passes for a booking.
- `validatePass(string $passNumber): array` — check if valid and unused.

**`BookingService::createHourlyBooking()`**:
- After creating booking, based on `participant_mode`:
  - `passes` → auto-call `BookingPassService::generatePasses()` with count = `participant_count` (defaulting to `expected_capacity`).
  - `full` / `partial` → create `sa_booking_participants` entries (existing logic).

**`BookingWizardController`**:
- After booking creation, prompt:
  - "هل تريد تسجيل بيانات المشاركين أم إصدار Passes؟"
  - Option 1: Enter names/IDs (full/partial mode).
  - Option 2: Issue passes (default — system auto-generates based on expected_capacity).

**New `Controllers/BookingPassController.php`**:
- `passes(Request, string $bookingId)` — view/print passes for a booking.
- `usePass(Request)` — gate employee scans/enters pass number → marks used.
- `printPasses(Request, string $bookingId)` — generate printable pass cards.

**Gate Integration**:
- `GateAccessService` (or equivalent) — when pass number is scanned:
  - Validate against `sa_booking_passes`.
  - Check booking date/time matches current time (within tolerance).
  - Mark pass as used.
  - Log entry.
- If participants registered (mode = full): validate against `sa_booking_participants` by player card or national ID.
- Mirror/dashboard: show "X/Y passes used" in real-time.

**`Views/bookings/show.php`**:
- Show participant mode.
- If passes: show pass list with status (issued/used), print button.
- If full/partial: show participant names.

**`Views/mirror/facility_view.php`**:
- Show pass usage in booking cell tooltip: "محجوز لـ [Name] — 7/10 دخلوا".

**Cascading**:
- Every booking type (individual, organization) uses this system.
- Pool tickets (GAP 6) are a separate concept — they're for open-access zones, not reserved bookings.
- Booking wizard must ask for mode AFTER creating the booking or as part of creation.
- `participant_count` field now has real meaning — it's the number of passes/people allowed.
- `expected_capacity` pre-fills it, but user can override.
- Reports: show pass utilization (how many passes issued vs used).

#### 7C: Expected Capacity ≠ Training Group Capacity

Per the requirements:
> في حالة البرامج التدريبية والمجموعات: الـ Expected Capacity لا يكون شرطًا ثابتًا

This means:
- `expected_capacity` on facility units is ONLY used for **free/hourly/org bookings**.
- Training group bookings do NOT auto-generate passes. The group has its own roster.
- The group's `max_capacity` on `sa_groups` is the player enrollment limit (separate concept).
- Multiple groups can share a facility at once (overlapping) — each group's attendees are tracked via `sa_attendance`, not via passes.

No additional code change needed here — just ensure `BookingService` only auto-generates passes for `booking_type = 'hourly'`, not `'training'`.

---

## GAP 8: Mirror Booking Labels + Type Colors

### What the requirement says:
> أي حجز لازم يظهر مع توضيح: الوقت المحجوز، الجهة أو الشخص الحاجز، نوع الحجز

### What currently exists:
- `facility_view.php` line 67: training shows `group_name`.
- `facility_view.php` line 70: booked shows `booker_name`.
- One color per status type. No distinction between member/guest/org bookings.
- Legend only shows: متاح, جزئي, ممتلئ, تدريب, محجوز, مغلق.

### What needs to happen:

**`Views/mirror/facility_view.php`**:
- For `booked` status cells:
  - If `booker_type = 'organization'` → show `organization_name` + orange background.
  - If `booker_type = 'member'` → show `booker_name` + green background.
  - If `booker_type = 'guest'` → show `booker_name` + yellow background.
- Add pass usage indicator: "7/10" in corner of cell.
- Legend update: show all actual booking types with colors.

**`MirrorStateService.php`**:
- Pass `booker_type`, `organization_name`, `passes_used`, `passes_total` in cell booking data.

**Color scheme**:
- Training → blue (#DBEAFE)
- Hourly member → green (#D1FAE5)
- Hourly guest → yellow (#FEF9C3)
- Hourly org → orange (#FFEDD5)
- Blocked/Maintenance → gray (#E5E7EB)
- Open access (pool) → cyan (#CFFAFE)

**Cascading**: Mostly view changes. Depends on GAP 2 (org type) and GAP 7 (passes) being done first.

---

## Execution Order

```
PHASE 1 — Foundation (independent, can be parallel):
  ├── 1A: GAP 2 — Organization booker type
  │         (migration + constants + pricing + booking service)
  ├── 1B: GAP 5 — Force-enroll capability
  │         (service + permission + route)
  ├── 1C: GAP 4 — Auto-group creation
  │         (new EnrollmentService method)
  └── 1D: GAP 7A — Expected Capacity on facility units
            (migration + UI field + booking wizard pre-fill)

PHASE 2 — Major features (some depend on Phase 1):
  ├── 2A: GAP 7B — Passes system
  │         (migration + BookingPassService + controller + gate integration)
  │         depends on: 1D (expected_capacity for default pass count)
  └── 2B: GAP 6 — Pool open-access tickets
            (migration + PoolTicketService + controller)
            (independent of Phase 1)

PHASE 3 — UI Overhaul (depends on Phase 1+2):
  ├── 3A: GAP 1 — Mirror discipline filter + multi-facility view
  │         depends on: 1A (org labels), 2A (pass counts in cells)
  ├── 3B: GAP 8 — Mirror labels + colors
  │         depends on: 1A (org type), 2A (passes display)
  └── 3C: GAP 3 — Registration wizard new flow
            depends on: 1C (auto-group), 1B (coach can still override after)
```

---

## Dependency Graph

```
GAP 7A (Expected Capacity) ──→ GAP 7B (Passes System) ──┐
                                                          ├──→ GAP 8 (Mirror Labels)
GAP 2 (Org Booker) ──────────────────────────────────────┘         │
                                                                    ↓
GAP 4 (Auto-Group) ────→ GAP 3 (Wizard Simplify)          GAP 1 (Mirror Filter)

GAP 5 (Force Enroll) ──── standalone

GAP 6 (Pool Tickets) ──── standalone
```

---

## Complete File List

### New Files to Create

| File | Purpose |
|------|---------|
| `database/migrations/Phase_70_XXX_add_expected_capacity.php` | expected_capacity on sa_facility_units |
| `database/migrations/Phase_70_XXX_add_booking_passes.php` | sa_booking_passes table + participant_mode/passes columns on sa_bookings |
| `database/migrations/Phase_70_XXX_add_organization_booker.php` | Org columns on sa_bookings |
| `database/migrations/Phase_70_XXX_pool_open_access.php` | access_mode + sa_pool_tickets table |
| `app/Modules/SportsActivity/Services/BookingPassService.php` | Pass generation, validation, usage |
| `app/Modules/SportsActivity/Services/PoolTicketService.php` | Pool ticket issuance |
| `app/Modules/SportsActivity/Controllers/BookingPassController.php` | Pass management endpoints |
| `app/Modules/SportsActivity/Controllers/PoolTicketController.php` | Pool ticket endpoints |
| `app/Modules/SportsActivity/Views/mirror/discipline_view.php` | Multi-facility discipline view |
| `app/Modules/SportsActivity/Views/bookings/passes.php` | Booking passes view/print |
| `app/Modules/SportsActivity/Views/pool-tickets/index.php` | Pool tickets list |
| `app/Modules/SportsActivity/Views/pool-tickets/issue.php` | Issue ticket form |

### Existing Files to Modify

| File | Gaps |
|------|------|
| `SaConstants.php` | 2, 5, 6, 7 |
| `EnrollmentService.php` | 4, 5 |
| `GroupTransferService.php` | 5 |
| `GroupController.php` | 5 |
| `BookingService.php` | 2, 7 |
| `PricingCalculatorService.php` | 2 |
| `BookingWizardController.php` | 2, 7 |
| `MirrorController.php` | 1 |
| `MirrorStateService.php` | 1, 7, 8 |
| `PoolGridService.php` | 6 |
| `PoolGridTemplateService.php` | 6 |
| `RegistrationWizardController.php` | 3, 4 |
| `RegistrationWizardService.php` | 3, 4 |
| `GateAccessService.php` | 7 |
| `ServiceDeskController.php` | 6 |
| `FacilityUnitController.php` (or FacilityController) | 7A |
| `bootstrap.php` | 5, 6 |
| `Routes.php` | 1, 2, 5, 6, 7 |
| `Views/mirror/index.php` | 1 |
| `Views/mirror/facility_view.php` | 8 |
| `Views/registration/wizard.php` | 3 |
| `Views/groups/show.php` | 5 |
| `Views/bookings/wizard.php` | 2, 7 |
| `Views/bookings/show.php` | 7 |
| `Views/pool-grid/*.php` | 6 |
| `Views/facilities/` (unit forms) | 7A |

---

## Risk / Edge Cases

1. **Auto-group has no schedule**: If source group has no `sa_group_schedule` entries, new group won't have any either. Warn coach.

2. **Force-enroll audit**: Add `force_enrolled = 1` flag on `sa_group_players` for tracking.

3. **Pool ticket occupancy drift**: Cron job to reconcile `current_occupancy` against active tickets daily.

4. **Old wizard registrations**: Existing in-progress registrations use old flow. `determineStep()` must handle NULL `discipline_id` gracefully.

5. **Multi-facility mirror performance**: Lazy-load per facility via AJAX or limit per page.

6. **Pass expiry**: Passes should auto-expire when booking time window ends. Cron or check-on-scan.

7. **Expected capacity override**: User can book with MORE or FEWER than expected capacity. System warns but doesn't block. Only generates passes equal to actual `participant_count` entered (which defaults to `expected_capacity`).

8. **Gate without passes**: If booking uses `participant_mode = 'full'` (names registered), gate validates by player card/NID against `sa_booking_participants`. No passes needed.

9. **Organization + passes**: An org booking for a 5-a-side pitch still gets 10 passes. The org contact is the booker, but the 10 people entering are different. System must support this.

10. **Mixed mode**: A booking starts with passes, but later some participants' names are collected at the gate. Consider allowing pass → name upgrade (optional, not blocking).
