519 lines
16 KiB
PHP
519 lines
16 KiB
PHP
<?php
|
|
|
|
use App\Enums\ConsultationStatus;
|
|
use App\Models\AdminLog;
|
|
use App\Models\BlockedTime;
|
|
use App\Models\Consultation;
|
|
use App\Models\User;
|
|
use Livewire\Volt\Volt;
|
|
|
|
beforeEach(function () {
|
|
$this->admin = User::factory()->admin()->create();
|
|
});
|
|
|
|
// ===========================================
|
|
// Access Tests
|
|
// ===========================================
|
|
|
|
test('admin can access blocked times page', function () {
|
|
$this->actingAs($this->admin)
|
|
->get(route('admin.settings.blocked-times'))
|
|
->assertOk();
|
|
});
|
|
|
|
test('non-admin cannot access blocked times page', function () {
|
|
$client = User::factory()->individual()->create();
|
|
|
|
$this->actingAs($client)
|
|
->get(route('admin.settings.blocked-times'))
|
|
->assertForbidden();
|
|
});
|
|
|
|
test('unauthenticated user cannot access blocked times page', function () {
|
|
$this->get(route('admin.settings.blocked-times'))
|
|
->assertRedirect(route('login'));
|
|
});
|
|
|
|
// ===========================================
|
|
// Create All-Day Block Tests
|
|
// ===========================================
|
|
|
|
test('admin can create an all-day block', function () {
|
|
$this->actingAs($this->admin);
|
|
|
|
$blockDate = today()->addDays(5)->format('Y-m-d');
|
|
|
|
$initialCount = BlockedTime::count();
|
|
|
|
Volt::test('admin.settings.blocked-times')
|
|
->call('openCreateModal')
|
|
->set('block_date', $blockDate)
|
|
->set('is_all_day', true)
|
|
->set('reason', 'Holiday')
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
expect(BlockedTime::count())->toBe($initialCount + 1);
|
|
|
|
$blocked = BlockedTime::latest()->first();
|
|
|
|
expect($blocked)->not->toBeNull()
|
|
->and($blocked->isAllDay())->toBeTrue()
|
|
->and($blocked->reason)->toBe('Holiday');
|
|
});
|
|
|
|
// ===========================================
|
|
// Create Time-Range Block Tests
|
|
// ===========================================
|
|
|
|
test('admin can create a time-range block', function () {
|
|
$this->actingAs($this->admin);
|
|
|
|
$initialCount = BlockedTime::count();
|
|
|
|
Volt::test('admin.settings.blocked-times')
|
|
->call('openCreateModal')
|
|
->set('block_date', today()->addDays(3)->format('Y-m-d'))
|
|
->set('is_all_day', false)
|
|
->set('start_time', '09:00')
|
|
->set('end_time', '12:00')
|
|
->set('reason', 'Morning meeting')
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
expect(BlockedTime::count())->toBe($initialCount + 1);
|
|
|
|
$blocked = BlockedTime::latest()->first();
|
|
|
|
expect($blocked)->not->toBeNull()
|
|
->and($blocked->isAllDay())->toBeFalse()
|
|
->and($blocked->start_time)->toContain('09:00')
|
|
->and($blocked->end_time)->toContain('12:00')
|
|
->and($blocked->reason)->toBe('Morning meeting');
|
|
});
|
|
|
|
test('admin can create block without reason', function () {
|
|
$this->actingAs($this->admin);
|
|
|
|
$initialCount = BlockedTime::count();
|
|
|
|
Volt::test('admin.settings.blocked-times')
|
|
->call('openCreateModal')
|
|
->set('block_date', today()->addDays(2)->format('Y-m-d'))
|
|
->set('is_all_day', true)
|
|
->set('reason', '')
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
expect(BlockedTime::count())->toBe($initialCount + 1);
|
|
|
|
$blocked = BlockedTime::latest()->first();
|
|
|
|
expect($blocked)->not->toBeNull()
|
|
->and($blocked->reason)->toBeNull();
|
|
});
|
|
|
|
// ===========================================
|
|
// Edit Blocked Time Tests
|
|
// ===========================================
|
|
|
|
test('admin can edit an existing blocked time', function () {
|
|
$blocked = BlockedTime::factory()->allDay()->create([
|
|
'block_date' => today()->addDays(5),
|
|
'reason' => 'Original reason',
|
|
]);
|
|
|
|
$this->actingAs($this->admin);
|
|
|
|
Volt::test('admin.settings.blocked-times')
|
|
->call('openEditModal', $blocked->id)
|
|
->set('reason', 'Updated reason')
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
$blocked->refresh();
|
|
|
|
expect($blocked->reason)->toBe('Updated reason');
|
|
});
|
|
|
|
test('admin can change block from all-day to time-range', function () {
|
|
$blocked = BlockedTime::factory()->allDay()->create([
|
|
'block_date' => today()->addDays(5),
|
|
]);
|
|
|
|
$this->actingAs($this->admin);
|
|
|
|
Volt::test('admin.settings.blocked-times')
|
|
->call('openEditModal', $blocked->id)
|
|
->set('is_all_day', false)
|
|
->set('start_time', '14:00')
|
|
->set('end_time', '18:00')
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
$blocked->refresh();
|
|
|
|
expect($blocked->isAllDay())->toBeFalse()
|
|
->and($blocked->start_time)->toContain('14:00')
|
|
->and($blocked->end_time)->toContain('18:00');
|
|
});
|
|
|
|
test('admin can edit past blocked time date', function () {
|
|
$blocked = BlockedTime::factory()->past()->create();
|
|
|
|
$this->actingAs($this->admin);
|
|
|
|
$component = Volt::test('admin.settings.blocked-times')
|
|
->call('openEditModal', $blocked->id)
|
|
->set('reason', 'Updated past block')
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
$blocked->refresh();
|
|
|
|
expect($blocked->reason)->toBe('Updated past block');
|
|
});
|
|
|
|
// ===========================================
|
|
// Delete Blocked Time Tests
|
|
// ===========================================
|
|
|
|
test('admin can delete a blocked time', function () {
|
|
$blocked = BlockedTime::factory()->create();
|
|
$blockedId = $blocked->id;
|
|
|
|
$this->actingAs($this->admin);
|
|
|
|
Volt::test('admin.settings.blocked-times')
|
|
->call('confirmDelete', $blockedId)
|
|
->call('delete')
|
|
->assertHasNoErrors();
|
|
|
|
expect(BlockedTime::find($blockedId))->toBeNull();
|
|
});
|
|
|
|
// ===========================================
|
|
// Validation Tests
|
|
// ===========================================
|
|
|
|
test('cannot create block with end time before start time', function () {
|
|
$this->actingAs($this->admin);
|
|
|
|
Volt::test('admin.settings.blocked-times')
|
|
->call('openCreateModal')
|
|
->set('block_date', today()->addDays(5)->format('Y-m-d'))
|
|
->set('is_all_day', false)
|
|
->set('start_time', '14:00')
|
|
->set('end_time', '10:00')
|
|
->call('save')
|
|
->assertHasErrors(['end_time']);
|
|
});
|
|
|
|
test('cannot create block for past dates', function () {
|
|
$this->actingAs($this->admin);
|
|
|
|
Volt::test('admin.settings.blocked-times')
|
|
->call('openCreateModal')
|
|
->set('block_date', today()->subDays(5)->format('Y-m-d'))
|
|
->set('is_all_day', true)
|
|
->call('save')
|
|
->assertHasErrors(['block_date']);
|
|
});
|
|
|
|
test('can create block for today', function () {
|
|
$this->actingAs($this->admin);
|
|
|
|
Volt::test('admin.settings.blocked-times')
|
|
->call('openCreateModal')
|
|
->set('block_date', today()->format('Y-m-d'))
|
|
->set('is_all_day', true)
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
expect(BlockedTime::where('block_date', today())->exists())->toBeTrue();
|
|
});
|
|
|
|
test('reason field respects 255 character max length', function () {
|
|
$this->actingAs($this->admin);
|
|
|
|
$longReason = str_repeat('a', 256);
|
|
|
|
Volt::test('admin.settings.blocked-times')
|
|
->call('openCreateModal')
|
|
->set('block_date', today()->addDays(5)->format('Y-m-d'))
|
|
->set('is_all_day', true)
|
|
->set('reason', $longReason)
|
|
->call('save')
|
|
->assertHasErrors(['reason']);
|
|
});
|
|
|
|
// ===========================================
|
|
// List View Tests
|
|
// ===========================================
|
|
|
|
test('list displays all blocked times sorted by date', function () {
|
|
BlockedTime::factory()->create(['block_date' => today()->addDays(10)]);
|
|
BlockedTime::factory()->create(['block_date' => today()->addDays(2)]);
|
|
BlockedTime::factory()->create(['block_date' => today()->addDays(5)]);
|
|
|
|
$this->actingAs($this->admin);
|
|
|
|
$component = Volt::test('admin.settings.blocked-times')
|
|
->set('filter', 'all');
|
|
|
|
$blockedTimes = $component->viewData('blockedTimes');
|
|
|
|
expect($blockedTimes)->toHaveCount(3);
|
|
});
|
|
|
|
test('filter by upcoming shows only future blocks', function () {
|
|
BlockedTime::factory()->upcoming()->create();
|
|
BlockedTime::factory()->today()->create();
|
|
BlockedTime::factory()->past()->create();
|
|
|
|
$this->actingAs($this->admin);
|
|
|
|
$component = Volt::test('admin.settings.blocked-times')
|
|
->set('filter', 'upcoming');
|
|
|
|
$blockedTimes = $component->viewData('blockedTimes');
|
|
|
|
expect($blockedTimes)->toHaveCount(2)
|
|
->and($blockedTimes->every(fn ($b) => $b->block_date->gte(today())))->toBeTrue();
|
|
});
|
|
|
|
test('filter by past shows only past blocks', function () {
|
|
BlockedTime::factory()->upcoming()->create();
|
|
BlockedTime::factory()->today()->create();
|
|
BlockedTime::factory()->past()->create();
|
|
|
|
$this->actingAs($this->admin);
|
|
|
|
$component = Volt::test('admin.settings.blocked-times')
|
|
->set('filter', 'past');
|
|
|
|
$blockedTimes = $component->viewData('blockedTimes');
|
|
|
|
expect($blockedTimes)->toHaveCount(1)
|
|
->and($blockedTimes->every(fn ($b) => $b->block_date->lt(today())))->toBeTrue();
|
|
});
|
|
|
|
test('filter by all shows all blocks', function () {
|
|
BlockedTime::factory()->upcoming()->create();
|
|
BlockedTime::factory()->today()->create();
|
|
BlockedTime::factory()->past()->create();
|
|
|
|
$this->actingAs($this->admin);
|
|
|
|
$component = Volt::test('admin.settings.blocked-times')
|
|
->set('filter', 'all');
|
|
|
|
$blockedTimes = $component->viewData('blockedTimes');
|
|
|
|
expect($blockedTimes)->toHaveCount(3);
|
|
});
|
|
|
|
test('list shows reason if provided', function () {
|
|
BlockedTime::factory()->create([
|
|
'block_date' => today()->addDays(5),
|
|
'reason' => 'Personal vacation',
|
|
]);
|
|
|
|
$this->actingAs($this->admin);
|
|
|
|
Volt::test('admin.settings.blocked-times')
|
|
->assertSee('Personal vacation');
|
|
});
|
|
|
|
// ===========================================
|
|
// Audit Log Tests
|
|
// ===========================================
|
|
|
|
test('audit log is created when blocked time is created', function () {
|
|
$this->actingAs($this->admin);
|
|
|
|
Volt::test('admin.settings.blocked-times')
|
|
->call('openCreateModal')
|
|
->set('block_date', today()->addDays(5)->format('Y-m-d'))
|
|
->set('is_all_day', true)
|
|
->set('reason', 'Test')
|
|
->call('save');
|
|
|
|
$log = AdminLog::where('target_type', 'blocked_time')->first();
|
|
|
|
expect($log)->not->toBeNull()
|
|
->and($log->admin_id)->toBe($this->admin->id)
|
|
->and($log->action)->toBe('create');
|
|
});
|
|
|
|
test('audit log is created when blocked time is updated', function () {
|
|
$blocked = BlockedTime::factory()->create([
|
|
'block_date' => today()->addDays(5),
|
|
]);
|
|
|
|
$this->actingAs($this->admin);
|
|
|
|
Volt::test('admin.settings.blocked-times')
|
|
->call('openEditModal', $blocked->id)
|
|
->set('reason', 'Updated')
|
|
->call('save');
|
|
|
|
$log = AdminLog::where('target_type', 'blocked_time')
|
|
->where('action', 'update')
|
|
->first();
|
|
|
|
expect($log)->not->toBeNull()
|
|
->and($log->target_id)->toBe($blocked->id);
|
|
});
|
|
|
|
test('audit log is created when blocked time is deleted', function () {
|
|
$blocked = BlockedTime::factory()->create();
|
|
$blockedId = $blocked->id;
|
|
|
|
$this->actingAs($this->admin);
|
|
|
|
Volt::test('admin.settings.blocked-times')
|
|
->call('confirmDelete', $blockedId)
|
|
->call('delete');
|
|
|
|
$log = AdminLog::where('target_type', 'blocked_time')
|
|
->where('action', 'delete')
|
|
->first();
|
|
|
|
expect($log)->not->toBeNull()
|
|
->and($log->target_id)->toBe($blockedId);
|
|
});
|
|
|
|
// ===========================================
|
|
// Pending Booking Warning Tests
|
|
// ===========================================
|
|
|
|
test('warning displayed when blocking date with pending consultations', function () {
|
|
$client = User::factory()->individual()->create();
|
|
$blockDate = today()->addDays(5);
|
|
|
|
Consultation::factory()->create([
|
|
'user_id' => $client->id,
|
|
'booking_date' => $blockDate,
|
|
'booking_time' => '10:00:00',
|
|
'status' => ConsultationStatus::Pending,
|
|
]);
|
|
|
|
$this->actingAs($this->admin);
|
|
|
|
$component = Volt::test('admin.settings.blocked-times')
|
|
->call('openCreateModal')
|
|
->set('block_date', $blockDate->format('Y-m-d'))
|
|
->set('is_all_day', true)
|
|
->call('checkPendingBookings');
|
|
|
|
expect($component->get('pendingBookings'))->toHaveCount(1)
|
|
->and($component->get('pendingBookings')[0]['client'])->toBe($client->full_name);
|
|
});
|
|
|
|
test('warning shows pending bookings within time range', function () {
|
|
$client = User::factory()->individual()->create();
|
|
$blockDate = today()->addDays(5);
|
|
|
|
// Consultation within blocked range (10:00 is between 09:00 and 12:00)
|
|
Consultation::factory()->create([
|
|
'user_id' => $client->id,
|
|
'booking_date' => $blockDate,
|
|
'booking_time' => '10:00:00',
|
|
'status' => ConsultationStatus::Pending,
|
|
]);
|
|
|
|
// Consultation outside blocked range (15:00 is not between 09:00 and 12:00)
|
|
Consultation::factory()->create([
|
|
'user_id' => $client->id,
|
|
'booking_date' => $blockDate,
|
|
'booking_time' => '15:00:00',
|
|
'status' => ConsultationStatus::Pending,
|
|
]);
|
|
|
|
$this->actingAs($this->admin);
|
|
|
|
$component = Volt::test('admin.settings.blocked-times')
|
|
->call('openCreateModal')
|
|
->set('block_date', $blockDate->format('Y-m-d'))
|
|
->set('is_all_day', false)
|
|
->set('start_time', '09:00')
|
|
->set('end_time', '12:00')
|
|
->call('checkPendingBookings');
|
|
|
|
expect($component->get('pendingBookings'))->toHaveCount(1);
|
|
});
|
|
|
|
// ===========================================
|
|
// Modal Tests
|
|
// ===========================================
|
|
|
|
test('create modal opens with default values', function () {
|
|
$this->actingAs($this->admin);
|
|
|
|
$component = Volt::test('admin.settings.blocked-times')
|
|
->call('openCreateModal');
|
|
|
|
expect($component->get('showModal'))->toBeTrue()
|
|
->and($component->get('editingId'))->toBeNull()
|
|
->and($component->get('is_all_day'))->toBeTrue()
|
|
->and($component->get('block_date'))->toBe(today()->format('Y-m-d'));
|
|
});
|
|
|
|
test('edit modal opens with existing values', function () {
|
|
$blocked = BlockedTime::factory()->timeRange('10:00', '14:00')->create([
|
|
'block_date' => today()->addDays(5),
|
|
'reason' => 'Test reason',
|
|
]);
|
|
|
|
$this->actingAs($this->admin);
|
|
|
|
$component = Volt::test('admin.settings.blocked-times')
|
|
->call('openEditModal', $blocked->id);
|
|
|
|
expect($component->get('showModal'))->toBeTrue()
|
|
->and($component->get('editingId'))->toBe($blocked->id)
|
|
->and($component->get('is_all_day'))->toBeFalse()
|
|
->and($component->get('start_time'))->toBe('10:00')
|
|
->and($component->get('end_time'))->toBe('14:00')
|
|
->and($component->get('reason'))->toBe('Test reason');
|
|
});
|
|
|
|
test('modal closes and resets on close', function () {
|
|
$blocked = BlockedTime::factory()->create();
|
|
|
|
$this->actingAs($this->admin);
|
|
|
|
$component = Volt::test('admin.settings.blocked-times')
|
|
->call('openEditModal', $blocked->id)
|
|
->call('closeModal');
|
|
|
|
expect($component->get('showModal'))->toBeFalse()
|
|
->and($component->get('editingId'))->toBeNull();
|
|
});
|
|
|
|
// ===========================================
|
|
// Multiple Blocks on Same Date Tests
|
|
// ===========================================
|
|
|
|
test('multiple time-range blocks on same date handled correctly', function () {
|
|
$blockDate = today()->addDays(5);
|
|
|
|
BlockedTime::factory()->timeRange('09:00', '12:00')->create([
|
|
'block_date' => $blockDate,
|
|
]);
|
|
|
|
$this->actingAs($this->admin);
|
|
|
|
Volt::test('admin.settings.blocked-times')
|
|
->call('openCreateModal')
|
|
->set('block_date', $blockDate->format('Y-m-d'))
|
|
->set('is_all_day', false)
|
|
->set('start_time', '14:00')
|
|
->set('end_time', '17:00')
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
expect(BlockedTime::where('block_date', $blockDate)->count())->toBe(2);
|
|
});
|