# Cycle 8: Facility & Space Management Gaps

## Goal
Build Space Layout Editor UI, Reservation booking UI, Segment management, per-day operating hours, and missing columns.

---

## Migration: `2024_01_01_000066_add_facility_operating_hours.php`

Currently facilities have simple `operating_start`/`operating_end` columns (single time range for all days). The doc specifies per-day operating hours:

```sql
CREATE TABLE facility_operating_hours (
    id              BIGSERIAL PRIMARY KEY,
    facility_id     BIGINT NOT NULL REFERENCES facilities(id) ON DELETE CASCADE,
    day_of_week     SMALLINT NOT NULL CHECK (day_of_week BETWEEN 0 AND 6),
    opens_at        TIME NOT NULL,
    closes_at       TIME NOT NULL,
    is_closed       BOOLEAN NOT NULL DEFAULT FALSE,
    notes           VARCHAR(200),
    created_at      TIMESTAMP NOT NULL DEFAULT NOW()
);

CREATE UNIQUE INDEX facility_operating_hours_unique
    ON facility_operating_hours(facility_id, day_of_week);
```

### Model: `app/Domain/Facility/Models/FacilityOperatingHours.php`
- No traits (child of facility, no tenant column)
- Relationships: facility() BelongsTo
- Scope: forDay($dayOfWeek), open()

### Facility Model — Add:
```php
public function operatingHours(): HasMany
{
    return $this->hasMany(FacilityOperatingHours::class);
}

public function isOpenOn(int $dayOfWeek, string $time): bool
{
    $hours = $this->operatingHours()->where('day_of_week', $dayOfWeek)->first();
    if (!$hours || $hours->is_closed) return false;
    return $time >= $hours->opens_at && $time <= $hours->closes_at;
}
```

---

## Migration: `2024_01_01_000067_add_missing_facility_columns.php`

### On `facilities`:
```php
$table->string('ownership', 20)->nullable();
$table->boolean('has_changing_rooms')->default(false);
$table->boolean('has_showers')->default(false);
$table->string('rental_contact', 100)->nullable();
$table->text('maintenance_notes')->nullable();
$table->text('status_reason')->nullable();
$table->date('status_until')->nullable();
```
CHECK: `ownership IN ('owned', 'rented', 'shared', 'temporary')`

---

## Space Layout Editor (Livewire + Alpine)

### Architecture
The layout editor is a visual tool that lets admins:
1. Select a facility
2. Choose layout type (grid/lanes/zones/custom)
3. Configure parameters (rows×cols for grid, lane_count for lanes, zone definitions)
4. See a real-time visual preview
5. Auto-generate segments on save

### Livewire: `app/Livewire/Facilities/SpaceLayoutEditor.php`

```php
class SpaceLayoutEditor extends Component
{
    public ?int $facilityId = null;
    public ?int $layoutId = null; // null = creating new
    public string $name_ar = '';
    public string $name = '';
    public string $layoutType = 'grid';
    public array $layoutConfig = [];
    public bool $isRecurring = true;
    public ?int $effectiveDayOfWeek = null;
    public ?string $effectiveDate = null;
    public string $startTime = '08:00';
    public string $endTime = '22:00';

    // Alpine communicates segment state back via wire:model
    public array $segments = []; // preview of generated segments

    public function mount(?int $facility = null, ?int $layout = null)
    {
        $this->authorize('facilities.manage_layouts');
        $this->facilityId = $facility;
        if ($layout) {
            $this->loadLayout($layout);
        }
    }

    public function updatedLayoutType(): void
    {
        // Reset config when type changes
        $this->layoutConfig = match ($this->layoutType) {
            'grid' => ['rows' => 2, 'columns' => 2],
            'lanes' => ['lane_count' => 4],
            'zones' => ['definitions' => []],
            'custom' => ['definitions' => []],
        };
        $this->regeneratePreview();
    }

    public function regeneratePreview(): void
    {
        // Generate segment preview based on type + config
        $this->segments = match ($this->layoutType) {
            'grid' => $this->generateGridPreview(),
            'lanes' => $this->generateLanesPreview(),
            'zones' => $this->layoutConfig['definitions'] ?? [],
            'custom' => $this->layoutConfig['definitions'] ?? [],
        };
    }

    public function save(): void
    {
        $this->validate([...]);
        $service = app(SpaceLayoutService::class);

        if ($this->layoutId) {
            $service->update(SpaceLayout::find($this->layoutId), [...], auth()->user());
        } else {
            $service->create([
                'facility_id' => $this->facilityId,
                'name_ar' => $this->name_ar,
                'layout_type' => $this->layoutType,
                'layout_config' => $this->layoutConfig,
                'is_recurring' => $this->isRecurring,
                'effective_day_of_week' => $this->effectiveDayOfWeek,
                'effective_date' => $this->effectiveDate,
                'start_time' => $this->startTime,
                'end_time' => $this->endTime,
            ], auth()->user());
        }

        session()->flash('success', __('تم حفظ التخطيط بنجاح'));
        $this->redirect(route('facilities.show', $this->facilityId));
    }

    private function generateGridPreview(): array
    {
        $segments = [];
        $rows = $this->layoutConfig['rows'] ?? 2;
        $cols = $this->layoutConfig['columns'] ?? 2;
        for ($r = 1; $r <= $rows; $r++) {
            for ($c = 1; $c <= $cols; $c++) {
                $segments[] = [
                    'code' => "R{$r}C{$c}",
                    'name_ar' => "صف {$r} عمود {$c}",
                    'row_index' => $r,
                    'col_index' => $c,
                    'is_available' => true,
                ];
            }
        }
        return $segments;
    }

    private function generateLanesPreview(): array
    {
        $segments = [];
        $count = $this->layoutConfig['lane_count'] ?? 4;
        for ($i = 1; $i <= $count; $i++) {
            $segments[] = [
                'code' => "L{$i}",
                'name_ar' => "حارة {$i}",
                'lane_number' => $i,
                'is_available' => true,
            ];
        }
        return $segments;
    }
}
```

### View: `resources/views/livewire/facilities/space-layout-editor.blade.php`
- Left panel: Layout configuration (type selector, parameters)
- Center: Visual grid/lane/zone preview (Alpine-rendered)
  - Grid: CSS Grid display, each cell clickable (toggle available)
  - Lanes: Horizontal strips
  - Zones: Named blocks with drag-resize (Alpine)
- Right panel: Segment details (name, capacity, availability)
- Bottom: Save button with loading state

---

## Space Reservation Booking UI

### Livewire: `app/Livewire/Facilities/ReservationCalendar.php`

```php
class ReservationCalendar extends Component
{
    public int $facilityId;
    public string $selectedDate;
    public array $reservations = [];
    public array $availableSegments = [];

    // Booking form state
    public bool $showBookingModal = false;
    public string $bookingStart = '';
    public string $bookingEnd = '';
    public array $selectedSegments = [];
    public string $bookingTitle = '';
    public string $bookingNotes = '';

    public function mount(int $facility)
    {
        $this->authorize('reservations.list');
        $this->facilityId = $facility;
        $this->selectedDate = today()->format('Y-m-d');
        $this->loadReservations();
    }

    public function updatedSelectedDate(): void
    {
        $this->loadReservations();
    }

    public function loadReservations(): void
    {
        $this->reservations = SpaceReservation::where('facility_id', $this->facilityId)
            ->where('reservation_date', $this->selectedDate)
            ->where('status', '!=', 'cancelled')
            ->with('reservable')
            ->get()
            ->toArray();

        // Resolve layout for this date
        $layout = app(SpaceLayoutService::class)->resolveLayout(
            $this->facilityId,
            Carbon::parse($this->selectedDate)
        );

        $this->availableSegments = $layout?->segments()
            ->where('is_available', true)
            ->get()
            ->toArray() ?? [];
    }

    public function checkAvailability(): array
    {
        $collisionService = app(SpaceCollisionService::class);
        return $collisionService->check(
            $this->facilityId,
            $this->selectedDate,
            $this->bookingStart,
            $this->bookingEnd,
            $this->selectedSegments
        );
    }

    public function book(): void
    {
        $this->validate([
            'bookingStart' => 'required',
            'bookingEnd' => 'required|after:bookingStart',
            'selectedSegments' => 'required|array|min:1',
        ]);

        $service = app(ReservationService::class);
        $service->reserve([
            'facility_id' => $this->facilityId,
            'reservation_date' => $this->selectedDate,
            'start_time' => $this->bookingStart,
            'end_time' => $this->bookingEnd,
            'segment_ids' => $this->selectedSegments,
            'title' => $this->bookingTitle,
            'notes' => $this->bookingNotes,
            'status' => 'confirmed',
        ], auth()->user());

        $this->showBookingModal = false;
        $this->loadReservations();
        session()->flash('success', __('تم الحجز بنجاح'));
    }
}
```

### View Features:
- Date picker (navigate days)
- Timeline view: horizontal time blocks (08:00-22:00) with colored blocks for reservations
- Each segment shown as a row in the timeline
- Click empty slot → opens booking modal
- Collision warnings shown in red
- Legend: confirmed (green), tentative (yellow), own booking (blue)

---

## Facility Show Page Enhancement

### Livewire: `app/Livewire/Facilities/FacilityShow.php`

Tabs:
1. **Info** — facility details, status, operating hours
2. **Layouts** — list of space layouts with "create new" button → SpaceLayoutEditor
3. **Reservations** — ReservationCalendar embedded
4. **Schedule** — show which groups/sessions use this facility

---

## Routes

```php
Route::middleware(['auth', 'permission:facilities.list'])->group(function () {
    Route::get('/facilities/{facility}', FacilityShow::class)->name('facilities.show');
    Route::get('/facilities/{facility}/layouts/create', SpaceLayoutEditor::class)->name('facilities.layouts.create');
    Route::get('/facilities/{facility}/layouts/{layout}/edit', SpaceLayoutEditor::class)->name('facilities.layouts.edit');
    Route::get('/facilities/{facility}/reservations', ReservationCalendar::class)->name('facilities.reservations');
});
```

---

## Permissions to Seed

```
facilities.manage_layouts
facilities.manage_hours
reservations.list, reservations.create, reservations.cancel, reservations.confirm
```

---

## Validation: Reservation Cannot Be Outside Operating Hours

Update `ReservationService::reserve()`:
```php
// After existing validations, add:
$facility = Facility::find($data['facility_id']);
$dayOfWeek = Carbon::parse($data['reservation_date'])->dayOfWeek;

$hours = $facility->operatingHours()->where('day_of_week', $dayOfWeek)->first();
if ($hours && $hours->is_closed) {
    throw new DomainException('المنشأة مغلقة في هذا اليوم');
}
if ($hours && ($data['start_time'] < $hours->opens_at || $data['end_time'] > $hours->closes_at)) {
    throw new DomainException('الحجز خارج أوقات العمل');
}
```

---

## Verification

- [ ] Operating hours: per-day config saves and loads correctly
- [ ] SpaceLayoutEditor: create grid layout → segments auto-generated
- [ ] SpaceLayoutEditor: create lanes → correct lane segments
- [ ] Reservation calendar: shows existing bookings as colored blocks
- [ ] Booking: select segments + time → collision check → confirm
- [ ] Collision detection prevents overlapping confirmed reservations
- [ ] Tentative reservations show warning but don't block
- [ ] Reservation rejected outside operating hours
- [ ] FacilityShow tabs work (info, layouts, reservations, schedule)
