# Story 3.2: Time Slot Blocking ## Epic Reference **Epic 3:** Booking & Consultation System ## User Story As an **admin**, I want **to block specific dates or time ranges for personal events or holidays**, So that **clients cannot book during my unavailable times**. ## Story Context ### Existing System Integration - **Integrates with:** blocked_times table, availability calendar - **Technology:** Livewire Volt, Flux UI - **Follows pattern:** CRUD pattern with calendar integration - **Touch points:** Availability calculation service ## Acceptance Criteria ### Block Time Management - [x] Block entire days (all-day events) - [x] Block specific time ranges within a day - [x] Add reason/note for blocked time - [x] View list of all blocked times (upcoming and past) - [x] Edit blocked times - [x] Delete blocked times ### Creating Blocked Time - [x] Select date (date picker) - [x] Choose: All day OR specific time range - [x] If time range: start time and end time - [x] Optional reason/note field - [x] Confirmation on save ### Display & Integration - [ ] Blocked times show as unavailable in calendar (Story 3.3 dependency) - [ ] Visual distinction from "already booked" slots (Story 3.3 dependency) - [x] Future blocked times don't affect existing approved bookings - [x] Warning if blocking time with pending bookings ### List View - [x] Show all blocked times - [x] Sort by date (upcoming first) - [x] Filter: past/upcoming/all - [x] Quick actions: edit, delete - [x] Show reason if provided ### Quality Requirements - [x] Bilingual support - [x] Audit log for create/edit/delete - [x] Validation: end time after start time - [x] Tests for blocking logic ## Technical Notes ### Database Schema ```php // blocked_times table Schema::create('blocked_times', function (Blueprint $table) { $table->id(); $table->date('block_date'); $table->time('start_time')->nullable(); // null = all day $table->time('end_time')->nullable(); // null = all day $table->string('reason')->nullable(); $table->timestamps(); }); ``` ### Model ```php 'date', ]; public function isAllDay(): bool { return is_null($this->start_time) && is_null($this->end_time); } public function scopeUpcoming($query) { return $query->where('block_date', '>=', today()); } public function scopePast($query) { return $query->where('block_date', '<', today()); } public function scopeForDate($query, $date) { return $query->where('block_date', $date); } public function blocksSlot(string $time): bool { if ($this->isAllDay()) { return true; } $slotTime = Carbon::parse($time); $start = Carbon::parse($this->start_time); $end = Carbon::parse($this->end_time); return $slotTime->between($start, $end) || $slotTime->eq($start); } } ``` ### Volt Component for Create/Edit ```php exists) { $this->blockedTime = $blockedTime; $this->block_date = $blockedTime->block_date->format('Y-m-d'); $this->is_all_day = $blockedTime->isAllDay(); $this->start_time = $blockedTime->start_time ?? '09:00'; $this->end_time = $blockedTime->end_time ?? '17:00'; $this->reason = $blockedTime->reason ?? ''; } else { $this->block_date = today()->format('Y-m-d'); } } public function save(): void { $validated = $this->validate([ 'block_date' => ['required', 'date', 'after_or_equal:today'], 'is_all_day' => ['boolean'], 'start_time' => ['required_if:is_all_day,false'], 'end_time' => ['required_if:is_all_day,false', 'after:start_time'], 'reason' => ['nullable', 'string', 'max:255'], ]); $data = [ 'block_date' => $this->block_date, 'start_time' => $this->is_all_day ? null : $this->start_time, 'end_time' => $this->is_all_day ? null : $this->end_time, 'reason' => $this->reason ?: null, ]; if ($this->blockedTime) { $this->blockedTime->update($data); $action = 'update'; } else { $this->blockedTime = BlockedTime::create($data); $action = 'create'; } AdminLog::create([ 'admin_id' => auth()->id(), 'action_type' => $action, 'target_type' => 'blocked_time', 'target_id' => $this->blockedTime->id, 'new_values' => $data, 'ip_address' => request()->ip(), ]); session()->flash('success', __('messages.blocked_time_saved')); $this->redirect(route('admin.blocked-times.index')); } public function delete(): void { $this->blockedTime->delete(); AdminLog::create([ 'admin_id' => auth()->id(), 'action_type' => 'delete', 'target_type' => 'blocked_time', 'target_id' => $this->blockedTime->id, 'ip_address' => request()->ip(), ]); session()->flash('success', __('messages.blocked_time_deleted')); $this->redirect(route('admin.blocked-times.index')); } }; ``` ### Pending Booking Warning Check ```php // Add to Volt component - check for pending bookings before save public function checkPendingBookings(): array { $date = Carbon::parse($this->block_date); return Consultation::where('scheduled_date', $date->toDateString()) ->where('status', 'pending') ->when(!$this->is_all_day, function ($query) { $query->whereBetween('scheduled_time', [$this->start_time, $this->end_time]); }) ->with('user:id,full_name,company_name') ->get() ->toArray(); } ``` ### Integration with Availability Service ```php // In AvailabilityService public function getBlockedSlots(Carbon $date): array { $blockedTimes = BlockedTime::forDate($date)->get(); $blockedSlots = []; foreach ($blockedTimes as $blocked) { if ($blocked->isAllDay()) { // Return all possible slots as blocked $workingHour = WorkingHour::where('day_of_week', $date->dayOfWeek)->first(); return $workingHour ? $workingHour->getSlots(60) : []; } // Get slots that fall within blocked range $start = Carbon::parse($blocked->start_time); $end = Carbon::parse($blocked->end_time); $current = $start->copy(); while ($current->lt($end)) { $blockedSlots[] = $current->format('H:i'); $current->addMinutes(60); } } return array_unique($blockedSlots); } public function isDateFullyBlocked(Carbon $date): bool { return BlockedTime::forDate($date) ->where(function ($query) { $query->whereNull('start_time') ->whereNull('end_time'); }) ->exists(); } ``` ### List View Component ```blade
{{ __('admin.blocked_times') }} {{ __('admin.add_blocked_time') }}
@forelse($blockedTimes as $blocked)
{{ $blocked->block_date->format('d/m/Y') }}
@if($blocked->isAllDay()) {{ __('admin.all_day') }} @else {{ $blocked->start_time }} - {{ $blocked->end_time }} @endif
@if($blocked->reason)
{{ $blocked->reason }}
@endif
{{ __('common.edit') }} {{ __('common.delete') }}
@empty

{{ __('admin.no_blocked_times') }}

@endforelse
``` ## Assumptions - `AdminLog` model exists from Epic 1 with fields: `admin_id`, `action_type`, `target_type`, `target_id`, `new_values`, `ip_address` - Route naming convention: `admin.blocked-times.{index|create|edit}` - Admin middleware protecting all blocked-times routes - Flux UI modal component available for delete confirmation ## Required Translation Keys ```php // lang/en/*.php and lang/ar/*.php 'messages.blocked_time_saved' => 'Blocked time saved successfully', 'messages.blocked_time_deleted' => 'Blocked time deleted successfully', 'admin.blocked_times' => 'Blocked Times', 'admin.add_blocked_time' => 'Add Blocked Time', 'admin.all_day' => 'All Day', 'admin.closed' => 'Closed', 'admin.no_blocked_times' => 'No blocked times found', 'common.edit' => 'Edit', 'common.delete' => 'Delete', 'common.to' => 'to', ``` ## Test Scenarios ### Feature Tests (`tests/Feature/Admin/BlockedTimesTest.php`) **CRUD Operations:** - [x] Admin can create an all-day block - [x] Admin can create a time-range block (e.g., 09:00-12:00) - [x] Admin can add optional reason to blocked time - [x] Admin can edit an existing blocked time - [x] Admin can delete a blocked time - [x] Non-admin users cannot access blocked time routes **Validation:** - [x] Cannot create block with end_time before start_time - [x] Cannot create block for past dates (new blocks only) - [x] Can edit existing blocks for past dates (data integrity) - [x] Reason field respects 255 character max length **List View:** - [x] List displays all blocked times sorted by date (upcoming first) - [x] Filter by "upcoming" shows only future blocks - [x] Filter by "past" shows only past blocks - [x] Filter by "all" shows all blocks **Integration:** - [x] `blocksSlot()` returns true for times within blocked range - [x] `blocksSlot()` returns true for all times when all-day block - [x] `blocksSlot()` returns false for times outside blocked range - [ ] `isDateFullyBlocked()` correctly identifies all-day blocks (AvailabilityService - Story 3.3) - [ ] `getBlockedSlots()` returns correct slots for partial day blocks (AvailabilityService - Story 3.3) **Edge Cases:** - [x] Multiple blocks on same date handled correctly - [x] Block at end of working hours (edge of range) - [x] Warning displayed when blocking date with pending consultations ### Unit Tests (`tests/Unit/Models/BlockedTimeTest.php`) - [x] `isAllDay()` returns true when start_time and end_time are null - [x] `isAllDay()` returns false when times are set - [x] `scopeUpcoming()` filters correctly - [x] `scopePast()` filters correctly - [x] `scopeForDate()` filters by exact date ## Definition of Done - [x] Can create all-day blocks - [x] Can create time-range blocks - [x] Can add reason to blocked time - [x] List view shows all blocked times - [x] Can edit blocked times - [x] Can delete blocked times - [ ] Blocked times show as unavailable in calendar (Story 3.3 dependency) - [x] Existing bookings not affected - [x] Audit logging complete - [x] Bilingual support - [x] Tests pass - [x] Code formatted with Pint ## Dependencies - **Story 3.1:** Working hours configuration (`docs/stories/story-3.1-working-hours-configuration.md`) - Provides: `WorkingHour` model, `AvailabilityService` base - **Story 3.3:** Availability calendar (`docs/stories/story-3.3-availability-calendar-display.md`) - Consumes: blocked times data for calendar display - **Epic 1:** Core Foundation - Provides: `AdminLog` model for audit logging, admin authentication ## Risk Assessment - **Primary Risk:** Blocking times with pending bookings - **Mitigation:** Warning message, don't auto-cancel existing bookings - **Rollback:** Delete blocked time to restore availability ## Estimation **Complexity:** Medium **Estimated Effort:** 3-4 hours --- ## Dev Agent Record ### Status **Ready for Review** ### Agent Model Used Claude Opus 4.5 (claude-opus-4-5-20251101) ### File List **New Files:** - `resources/views/livewire/admin/settings/blocked-times.blade.php` - Volt component for blocked times CRUD - `tests/Unit/Models/BlockedTimeTest.php` - Unit tests for BlockedTime model - `tests/Feature/Admin/BlockedTimesTest.php` - Feature tests for blocked times CRUD - `lang/en/common.php` - Common translations (save, cancel, edit, delete, optional) - `lang/ar/common.php` - Arabic common translations **Modified Files:** - `app/Models/BlockedTime.php` - Added scopes (upcoming, past, forDate) and blocksSlot method - `database/factories/BlockedTimeFactory.php` - Enhanced with allDay, timeRange, upcoming, past, today, withReason states - `routes/web.php` - Added blocked-times route - `lang/en/admin.php` - Added blocked times translations - `lang/ar/admin.php` - Added Arabic blocked times translations - `lang/en/messages.php` - Added blocked_time_saved, blocked_time_deleted messages - `lang/ar/messages.php` - Added Arabic blocked time messages - `lang/en/validation.php` - Added block_date_future validation message - `lang/ar/validation.php` - Added Arabic block_date_future validation message - `lang/en/clients.php` - Added 'unknown' translation key - `lang/ar/clients.php` - Added Arabic 'unknown' translation key ### Change Log - Implemented full CRUD for blocked times with modal-based create/edit - Added all-day and time-range blocking support - Added filter for upcoming/past/all blocked times - Added pending booking warning when blocking dates with existing consultations - Added audit logging for create/update/delete operations - Added bilingual support (English/Arabic) - Created comprehensive unit and feature tests (46 tests, 106 assertions) ### Completion Notes - Calendar integration (showing blocked times as unavailable) is deferred to Story 3.3 - AvailabilityService integration (getBlockedSlots, isDateFullyBlocked) is deferred to Story 3.3 - All 303 tests in the full test suite pass - Code formatted with Pint --- ## QA Results ### Review Date: 2025-12-26 ### Reviewed By: Quinn (Test Architect) ### Code Quality Assessment **Overall: Excellent** - The implementation demonstrates high-quality code with clean architecture, proper separation of concerns, and comprehensive test coverage. The Volt component follows established patterns in the codebase, and the model implementation is clean with well-designed scopes. **Strengths:** - Clean class-based Volt component with proper state management - Well-structured modal flow for create/edit operations - Comprehensive pending booking warning system with reactive updates - Proper audit logging with old/new values capture - Factory states are comprehensive and follow Laravel conventions - `blocksSlot()` method correctly handles boundary conditions (inclusive start, exclusive end) **Code Quality Highlights:** - Model scopes (`upcoming`, `past`, `forDate`) are properly typed with `Builder` return types - Carbon usage is appropriate for date/time manipulation - Flux UI components are used consistently with the project patterns - Proper `wire:key` usage in loops for optimal Livewire rendering ### Refactoring Performed None required - code quality meets project standards. ### Compliance Check - Coding Standards: ✓ Code follows Laravel/Pint conventions - Project Structure: ✓ Files in correct locations following project patterns - Testing Strategy: ✓ Comprehensive unit and feature tests - All ACs Met: ✓ All acceptance criteria marked as complete (where applicable to this story) ### Improvements Checklist All items are addressed or appropriately deferred: - [x] CRUD operations fully implemented and tested - [x] Modal-based UI for create/edit with proper state management - [x] Pending booking warning with reactive updates - [x] Audit logging for all operations - [x] Bilingual support (EN/AR) - [x] Filter functionality (upcoming/past/all) - [x] Validation rules properly applied - [x] Delete confirmation modal - [ ] Calendar display integration (correctly deferred to Story 3.3) - [ ] AvailabilityService integration (correctly deferred to Story 3.3) ### Security Review **Status: PASS** - Route properly protected by `admin` middleware - Authorization tests verify non-admin access is forbidden - No direct user input used in queries without validation - Audit logging captures IP addresses for traceability ### Performance Considerations **Status: PASS** - Efficient queries using scopes - Eager loading used for `user` relation in pending bookings check - No N+1 query issues detected - List view uses simple pagination pattern (no performance concerns at expected scale) ### Files Modified During Review None - no files were modified during review. ### Gate Status Gate: **PASS** → docs/qa/gates/3.2-time-slot-blocking.yml ### Requirements Traceability | AC# | Acceptance Criteria | Test Coverage | Status | |-----|---------------------|---------------|--------| | 1 | Block entire days (all-day events) | `admin can create an all-day block` | ✓ | | 2 | Block specific time ranges | `admin can create a time-range block` | ✓ | | 3 | Add reason/note for blocked time | `admin can create block without reason`, `list shows reason if provided` | ✓ | | 4 | View list of all blocked times | `list displays all blocked times sorted by date` | ✓ | | 5 | Edit blocked times | `admin can edit an existing blocked time`, `admin can change block from all-day to time-range` | ✓ | | 6 | Delete blocked times | `admin can delete a blocked time` | ✓ | | 7 | Select date (date picker) | Component uses native date input | ✓ | | 8 | Choose all day or time range | `is_all_day` switch with conditional time fields | ✓ | | 9 | Time range selection | `start_time`, `end_time` inputs when not all-day | ✓ | | 10 | Optional reason field | Nullable validation, tested | ✓ | | 11 | Future blocks don't affect approved bookings | Only checks pending status | ✓ | | 12 | Warning for pending bookings | `warning displayed when blocking date with pending consultations` | ✓ | | 13 | Sort by date (upcoming first) | `filter by upcoming shows only future blocks` | ✓ | | 14 | Filter: past/upcoming/all | 3 filter tests | ✓ | | 15 | Quick actions: edit, delete | Buttons in list view | ✓ | | 16 | Bilingual support | EN/AR translation files | ✓ | | 17 | Audit logging | 3 audit log tests | ✓ | | 18 | Validation: end time after start | `cannot create block with end time before start time` | ✓ | | 19 | Non-admin access forbidden | `non-admin cannot access blocked times page` | ✓ | ### Recommended Status ✓ **Ready for Done** - All acceptance criteria are met, tests pass (46 tests, 106 assertions), code quality is high, and calendar integration items are correctly deferred to Story 3.3.