reviewed epic 3
This commit is contained in:
@@ -66,10 +66,14 @@ Schema::create('working_hours', function (Blueprint $table) {
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class WorkingHour extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'day_of_week',
|
||||
'start_time',
|
||||
@@ -107,10 +111,28 @@ class WorkingHour extends Model
|
||||
}
|
||||
```
|
||||
|
||||
### 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
|
||||
<?php
|
||||
|
||||
use App\Models\AdminLog;
|
||||
use App\Models\WorkingHour;
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
@@ -133,6 +155,20 @@ new class extends Component {
|
||||
|
||||
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],
|
||||
@@ -144,16 +180,39 @@ new class extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
// Log action
|
||||
// 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(),
|
||||
]);
|
||||
|
||||
session()->flash('success', __('messages.working_hours_saved'));
|
||||
$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();
|
||||
}
|
||||
};
|
||||
```
|
||||
@@ -201,6 +260,10 @@ new class extends Component {
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Consultation;
|
||||
use App\Models\WorkingHour;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class AvailabilityService
|
||||
{
|
||||
public function getAvailableSlots(Carbon $date): array
|
||||
@@ -232,6 +295,168 @@ class AvailabilityService
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Requirements
|
||||
|
||||
### Test File Location
|
||||
`tests/Feature/Admin/WorkingHoursTest.php`
|
||||
|
||||
### Factory Required
|
||||
Create `database/factories/WorkingHourFactory.php`:
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\WorkingHour;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
class WorkingHourFactory extends Factory
|
||||
{
|
||||
protected $model = WorkingHour::class;
|
||||
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'day_of_week' => $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
|
||||
|
||||
Reference in New Issue
Block a user