# Story 3.1: Working Hours Configuration ## Epic Reference **Epic 3:** Booking & Consultation System ## User Story As an **admin**, I want **to configure available working hours for each day of the week**, So that **clients can only book consultations during my available times**. ## Story Context ### Existing System Integration - **Integrates with:** working_hours table, availability calendar - **Technology:** Livewire Volt, Flux UI forms - **Follows pattern:** Admin settings pattern - **Touch points:** Booking availability calculation ## Acceptance Criteria ### Working Hours Management - [ ] Set available days (enable/disable each day of week) - [ ] Set start time for each enabled day - [ ] Set end time for each enabled day - [ ] Support different hours for different days - [ ] 15-minute buffer automatically applied between appointments - [ ] 12-hour time format display (AM/PM) ### Configuration Interface - [ ] Visual weekly schedule view - [ ] Toggle for each day (Sunday-Saturday) - [ ] Time pickers for start/end times - [ ] Preview of available slots per day - [ ] Save button with confirmation ### Behavior - [ ] Changes take effect immediately for new bookings - [ ] Existing approved bookings NOT affected by changes - [ ] Warning if changing hours that have pending bookings - [ ] Validation: end time must be after start time ### Quality Requirements - [ ] Bilingual labels and messages - [ ] Default working hours on initial setup - [ ] Audit log entry on changes - [ ] Tests for configuration logic ## Technical Notes ### Database Schema ```php // working_hours table Schema::create('working_hours', function (Blueprint $table) { $table->id(); $table->tinyInteger('day_of_week'); // 0=Sunday, 6=Saturday $table->time('start_time'); $table->time('end_time'); $table->boolean('is_active')->default(true); $table->timestamps(); }); ``` ### Model ```php 'boolean', ]; public static function getDayName(int $dayOfWeek, string $locale = null): string { $locale = $locale ?? app()->getLocale(); $days = [ 'en' => ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], 'ar' => ['الأحد', 'الإثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة', 'السبت'], ]; return $days[$locale][$dayOfWeek] ?? $days['en'][$dayOfWeek]; } public function getSlots(int $duration = 60): array { $slots = []; $start = Carbon::parse($this->start_time); $end = Carbon::parse($this->end_time); while ($start->copy()->addMinutes($duration)->lte($end)) { $slots[] = $start->format('H:i'); $start->addMinutes($duration); } return $slots; } } ``` ### AdminLog Schema Reference The `AdminLog` model is used for audit logging (defined in Epic 1). Schema: ```php // admin_logs table Schema::create('admin_logs', function (Blueprint $table) { $table->id(); $table->foreignId('admin_id')->constrained('users')->cascadeOnDelete(); $table->string('action_type'); // 'create', 'update', 'delete' $table->string('target_type'); // 'working_hours', 'user', 'consultation', etc. $table->unsignedBigInteger('target_id')->nullable(); $table->json('old_values')->nullable(); $table->json('new_values')->nullable(); $table->ipAddress('ip_address')->nullable(); $table->timestamps(); }); ``` ### Volt Component ```php first(); $this->schedule[$day] = [ 'is_active' => $workingHour?->is_active ?? false, 'start_time' => $workingHour?->start_time ?? '09:00', 'end_time' => $workingHour?->end_time ?? '17:00', ]; } } public function save(): void { // Validate end time is after start time for active days foreach ($this->schedule as $day => $config) { if ($config['is_active'] && $config['end_time'] <= $config['start_time']) { $this->addError("schedule.{$day}.end_time", __('validation.end_time_after_start')); return; } } // Check for pending bookings on days being modified (warning only) $warnings = $this->checkPendingBookings(); // Store old values for audit log $oldValues = WorkingHour::all()->keyBy('day_of_week')->toArray(); foreach ($this->schedule as $day => $config) { WorkingHour::updateOrCreate( ['day_of_week' => $day], [ 'is_active' => $config['is_active'], 'start_time' => $config['start_time'], 'end_time' => $config['end_time'], ] ); } // Log action with old and new values AdminLog::create([ 'admin_id' => auth()->id(), 'action_type' => 'update', 'target_type' => 'working_hours', 'old_values' => $oldValues, 'new_values' => $this->schedule, 'ip_address' => request()->ip(), ]); $message = __('messages.working_hours_saved'); if (!empty($warnings)) { $message .= ' ' . __('messages.pending_bookings_warning', ['count' => count($warnings)]); } session()->flash('success', $message); } /** * Check if there are pending bookings on days being disabled or with changed hours. * Returns array of affected booking info for warning display. */ private function checkPendingBookings(): array { // This will be fully implemented when Consultation model exists (Story 3.4+) // For now, return empty array - structure shown for developer guidance return []; // Future implementation: // return Consultation::where('status', 'pending') // ->whereIn('day_of_week', $affectedDays) // ->get() // ->toArray(); } }; ``` ### Blade Template ```blade
{{ __('admin.working_hours') }} @foreach(range(0, 6) as $day)
{{ \App\Models\WorkingHour::getDayName($day) }} @if($schedule[$day]['is_active']) {{ __('common.to') }} @else {{ __('admin.closed') }} @endif
@endforeach {{ __('common.save') }}
``` ### Slot Calculation Service ```php dayOfWeek; $workingHour = WorkingHour::where('day_of_week', $dayOfWeek) ->where('is_active', true) ->first(); if (!$workingHour) { return []; } // Get all slots for the day $slots = $workingHour->getSlots(60); // 1 hour slots (45min + 15min buffer) // Remove already booked slots $bookedSlots = Consultation::where('scheduled_date', $date->toDateString()) ->whereIn('status', ['pending', 'approved']) ->pluck('scheduled_time') ->map(fn($time) => Carbon::parse($time)->format('H:i')) ->toArray(); // Remove blocked times $blockedSlots = $this->getBlockedSlots($date); return array_diff($slots, $bookedSlots, $blockedSlots); } } ``` ## Testing Requirements ### Test File Location `tests/Feature/Admin/WorkingHoursTest.php` ### Factory Required Create `database/factories/WorkingHourFactory.php`: ```php $this->faker->numberBetween(0, 6), 'start_time' => '09:00', 'end_time' => '17:00', 'is_active' => true, ]; } public function inactive(): static { return $this->state(fn (array $attributes) => [ 'is_active' => false, ]); } } ``` ### Test Scenarios #### Unit Tests (WorkingHour Model) ```php test('getDayName returns correct English day names', function () { expect(WorkingHour::getDayName(0, 'en'))->toBe('Sunday'); expect(WorkingHour::getDayName(6, 'en'))->toBe('Saturday'); }); test('getDayName returns correct Arabic day names', function () { expect(WorkingHour::getDayName(0, 'ar'))->toBe('الأحد'); expect(WorkingHour::getDayName(4, 'ar'))->toBe('الخميس'); }); test('getSlots returns correct 1-hour slots', function () { $workingHour = WorkingHour::factory()->create([ 'start_time' => '09:00', 'end_time' => '12:00', 'is_active' => true, ]); $slots = $workingHour->getSlots(60); expect($slots)->toBe(['09:00', '10:00', '11:00']); }); test('getSlots returns empty array when duration exceeds available time', function () { $workingHour = WorkingHour::factory()->create([ 'start_time' => '09:00', 'end_time' => '09:30', 'is_active' => true, ]); expect($workingHour->getSlots(60))->toBe([]); }); ``` #### Feature Tests (Volt Component) ```php test('admin can view working hours configuration', function () { $admin = User::factory()->admin()->create(); Volt::test('admin.working-hours') ->actingAs($admin) ->assertSuccessful() ->assertSee(__('admin.working_hours')); }); test('admin can save working hours configuration', function () { $admin = User::factory()->admin()->create(); Volt::test('admin.working-hours') ->actingAs($admin) ->set('schedule.1.is_active', true) ->set('schedule.1.start_time', '09:00') ->set('schedule.1.end_time', '17:00') ->call('save') ->assertHasNoErrors(); expect(WorkingHour::where('day_of_week', 1)->first()) ->is_active->toBeTrue() ->start_time->toBe('09:00') ->end_time->toBe('17:00'); }); test('validation fails when end time is before start time', function () { $admin = User::factory()->admin()->create(); Volt::test('admin.working-hours') ->actingAs($admin) ->set('schedule.1.is_active', true) ->set('schedule.1.start_time', '17:00') ->set('schedule.1.end_time', '09:00') ->call('save') ->assertHasErrors(['schedule.1.end_time']); }); test('audit log is created when working hours are saved', function () { $admin = User::factory()->admin()->create(); Volt::test('admin.working-hours') ->actingAs($admin) ->set('schedule.0.is_active', true) ->call('save'); expect(AdminLog::where('target_type', 'working_hours')->count())->toBe(1); }); test('inactive days do not require time validation', function () { $admin = User::factory()->admin()->create(); Volt::test('admin.working-hours') ->actingAs($admin) ->set('schedule.1.is_active', false) ->set('schedule.1.start_time', '17:00') ->set('schedule.1.end_time', '09:00') ->call('save') ->assertHasNoErrors(); }); ``` ### Translation Keys Required Add to `lang/en/validation.php`: ```php 'end_time_after_start' => 'End time must be after start time.', ``` Add to `lang/ar/validation.php`: ```php 'end_time_after_start' => 'يجب أن يكون وقت الانتهاء بعد وقت البدء.', ``` Add to `lang/en/messages.php`: ```php 'working_hours_saved' => 'Working hours saved successfully.', 'pending_bookings_warning' => 'Note: :count pending booking(s) may be affected.', ``` Add to `lang/ar/messages.php`: ```php 'working_hours_saved' => 'تم حفظ ساعات العمل بنجاح.', 'pending_bookings_warning' => 'ملاحظة: قد يتأثر :count حجز(حجوزات) معلقة.', ``` ## Definition of Done - [ ] Can enable/disable each day of week - [ ] Can set start/end times per day - [ ] Changes save correctly to database - [ ] Existing bookings not affected - [ ] Preview shows available slots - [ ] 12-hour time format displayed - [ ] Audit log created on save - [ ] Bilingual support complete - [ ] Tests for configuration - [ ] Code formatted with Pint ## Dependencies - **Epic 1:** Database schema, admin authentication ## Risk Assessment - **Primary Risk:** Changing hours affects availability incorrectly - **Mitigation:** Clear separation between existing bookings and new availability - **Rollback:** Restore previous working hours from audit log ## Estimation **Complexity:** Medium **Estimated Effort:** 3-4 hours