7.1 KiB
7.1 KiB
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
// 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
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class WorkingHour extends Model
{
protected $fillable = [
'day_of_week',
'start_time',
'end_time',
'is_active',
];
protected $casts = [
'is_active' => '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;
}
}
Volt Component
<?php
use App\Models\WorkingHour;
use Livewire\Volt\Component;
new class extends Component {
public array $schedule = [];
public function mount(): void
{
// Initialize with existing or default schedule
for ($day = 0; $day <= 6; $day++) {
$workingHour = WorkingHour::where('day_of_week', $day)->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
{
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
AdminLog::create([
'admin_id' => auth()->id(),
'action_type' => 'update',
'target_type' => 'working_hours',
'new_values' => $this->schedule,
'ip_address' => request()->ip(),
]);
session()->flash('success', __('messages.working_hours_saved'));
}
};
Blade Template
<div>
<flux:heading>{{ __('admin.working_hours') }}</flux:heading>
@foreach(range(0, 6) as $day)
<div class="flex items-center gap-4 py-3 border-b">
<flux:switch
wire:model.live="schedule.{{ $day }}.is_active"
/>
<span class="w-24">
{{ \App\Models\WorkingHour::getDayName($day) }}
</span>
@if($schedule[$day]['is_active'])
<flux:input
type="time"
wire:model="schedule.{{ $day }}.start_time"
/>
<span>{{ __('common.to') }}</span>
<flux:input
type="time"
wire:model="schedule.{{ $day }}.end_time"
/>
@else
<span class="text-charcoal/50">{{ __('admin.closed') }}</span>
@endif
</div>
@endforeach
<flux:button wire:click="save" class="mt-4">
{{ __('common.save') }}
</flux:button>
</div>
Slot Calculation Service
<?php
namespace App\Services;
class AvailabilityService
{
public function getAvailableSlots(Carbon $date): array
{
$dayOfWeek = $date->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);
}
}
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