generated sotries
This commit is contained in:
@@ -0,0 +1,261 @@
|
||||
# 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
|
||||
<?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
|
||||
<?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
|
||||
```blade
|
||||
<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
|
||||
<?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
|
||||
Reference in New Issue
Block a user