# 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 - [ ] Block entire days (all-day events) - [ ] Block specific time ranges within a day - [ ] Add reason/note for blocked time - [ ] View list of all blocked times (upcoming and past) - [ ] Edit blocked times - [ ] Delete blocked times ### Creating Blocked Time - [ ] Select date (date picker) - [ ] Choose: All day OR specific time range - [ ] If time range: start time and end time - [ ] Optional reason/note field - [ ] Confirmation on save ### Display & Integration - [ ] Blocked times show as unavailable in calendar - [ ] Visual distinction from "already booked" slots - [ ] Future blocked times don't affect existing approved bookings - [ ] Warning if blocking time with pending bookings ### List View - [ ] Show all blocked times - [ ] Sort by date (upcoming first) - [ ] Filter: past/upcoming/all - [ ] Quick actions: edit, delete - [ ] Show reason if provided ### Quality Requirements - [ ] Bilingual support - [ ] Audit log for create/edit/delete - [ ] Validation: end time after start time - [ ] 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')); } }; ``` ### 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
``` ## Definition of Done - [ ] Can create all-day blocks - [ ] Can create time-range blocks - [ ] Can add reason to blocked time - [ ] List view shows all blocked times - [ ] Can edit blocked times - [ ] Can delete blocked times - [ ] Blocked times show as unavailable in calendar - [ ] Existing bookings not affected - [ ] Audit logging complete - [ ] Bilingual support - [ ] Tests pass - [ ] Code formatted with Pint ## Dependencies - **Story 3.1:** Working hours configuration - **Story 3.3:** Availability calendar (consumes blocked times) ## 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