From e669e97ca1268bee64ba32f7d7034351e5ac67dc Mon Sep 17 00:00:00 2001 From: Naser Mansour Date: Sat, 27 Dec 2025 00:17:37 +0200 Subject: [PATCH] cmplete story 4.1 with qa tests --- docs/qa/gates/4.1-timeline-creation.yml | 53 +++ docs/stories/story-4.1-timeline-creation.md | 193 ++++++++-- lang/ar/messages.php | 3 + lang/ar/timelines.php | 33 ++ lang/en/messages.php | 3 + lang/en/timelines.php | 33 ++ .../livewire/admin/timelines/create.blade.php | 223 +++++++++++ routes/web.php | 5 + tests/Feature/Admin/TimelineCreationTest.php | 359 ++++++++++++++++++ 9 files changed, 876 insertions(+), 29 deletions(-) create mode 100644 docs/qa/gates/4.1-timeline-creation.yml create mode 100644 lang/ar/timelines.php create mode 100644 lang/en/timelines.php create mode 100644 resources/views/livewire/admin/timelines/create.blade.php create mode 100644 tests/Feature/Admin/TimelineCreationTest.php diff --git a/docs/qa/gates/4.1-timeline-creation.yml b/docs/qa/gates/4.1-timeline-creation.yml new file mode 100644 index 0000000..ee5d7c3 --- /dev/null +++ b/docs/qa/gates/4.1-timeline-creation.yml @@ -0,0 +1,53 @@ +# Quality Gate: Story 4.1 - Timeline Creation +# Generated by Quinn (Test Architect) on 2025-12-27 + +schema: 1 +story: "4.1" +story_title: "Timeline Creation" +gate: PASS +status_reason: "All acceptance criteria met with comprehensive test coverage (22 tests). Clean implementation following established patterns. No security or performance concerns." +reviewer: "Quinn (Test Architect)" +updated: "2025-12-27T00:00:00Z" + +waiver: { active: false } + +top_issues: [] + +risk_summary: + totals: { critical: 0, high: 0, medium: 0, low: 0 } + recommendations: + must_fix: [] + monitor: [] + +quality_score: 100 +expires: "2026-01-10T00:00:00Z" + +evidence: + tests_reviewed: 22 + tests_passing: 22 + risks_identified: 0 + trace: + ac_covered: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14] + ac_gaps: [] + +nfr_validation: + security: + status: PASS + notes: "Admin middleware protects route. Validation on all inputs. Parameterized queries via Eloquent. CSRF handled by Livewire." + performance: + status: PASS + notes: "Search limited to 10 results. 300ms debounce on input. 2-char minimum threshold." + reliability: + status: PASS + notes: "Proper error handling via Livewire validation. Flash messages for user feedback." + maintainability: + status: PASS + notes: "Clean class-based Volt component. Follows existing admin CRUD patterns. Well-structured translations." + +recommendations: + immediate: [] + future: + - action: "Consider adding loading state indicator during form submission" + refs: ["resources/views/livewire/admin/timelines/create.blade.php"] + - action: "Consider pagination if client list could exceed 100+ records" + refs: ["resources/views/livewire/admin/timelines/create.blade.php:42-57"] diff --git a/docs/stories/story-4.1-timeline-creation.md b/docs/stories/story-4.1-timeline-creation.md index 8d827df..a1f6d21 100644 --- a/docs/stories/story-4.1-timeline-creation.md +++ b/docs/stories/story-4.1-timeline-creation.md @@ -208,44 +208,44 @@ new class extends Component { All tests should use Pest and be placed in `tests/Feature/Admin/TimelineCreationTest.php`. ### Happy Path Tests -- [ ] `test_admin_can_view_timeline_creation_form` - Admin can access /admin/timelines/create -- [ ] `test_admin_can_search_clients_by_name` - Search returns matching users -- [ ] `test_admin_can_search_clients_by_email` - Search returns matching users -- [ ] `test_admin_can_create_timeline_with_required_fields` - Timeline created with case_name only -- [ ] `test_admin_can_create_timeline_with_case_reference` - Timeline created with optional reference -- [ ] `test_initial_notes_creates_first_timeline_update` - TimelineUpdate record created -- [ ] `test_audit_log_created_on_timeline_creation` - AdminLog entry exists +- [x] `test_admin_can_view_timeline_creation_form` - Admin can access /admin/timelines/create +- [x] `test_admin_can_search_clients_by_name` - Search returns matching users +- [x] `test_admin_can_search_clients_by_email` - Search returns matching users +- [x] `test_admin_can_create_timeline_with_required_fields` - Timeline created with case_name only +- [x] `test_admin_can_create_timeline_with_case_reference` - Timeline created with optional reference +- [x] `test_initial_notes_creates_first_timeline_update` - TimelineUpdate record created +- [x] `test_audit_log_created_on_timeline_creation` - AdminLog entry exists ### Validation Tests -- [ ] `test_case_name_is_required` - Validation error without case_name -- [ ] `test_case_reference_must_be_unique` - Validation error on duplicate reference -- [ ] `test_case_reference_allows_multiple_nulls` - Multiple timelines without reference allowed -- [ ] `test_client_selection_is_required` - Validation error without selecting client -- [ ] `test_selected_client_must_exist` - Validation error for non-existent user_id +- [x] `test_case_name_is_required` - Validation error without case_name +- [x] `test_case_reference_must_be_unique` - Validation error on duplicate reference +- [x] `test_case_reference_allows_multiple_nulls` - Multiple timelines without reference allowed +- [x] `test_client_selection_is_required` - Validation error without selecting client +- [x] `test_selected_client_must_exist` - Validation error for non-existent user_id ### Authorization Tests -- [ ] `test_non_admin_cannot_access_timeline_creation` - Redirect or 403 for non-admin users -- [ ] `test_guest_cannot_access_timeline_creation` - Redirect to login +- [x] `test_non_admin_cannot_access_timeline_creation` - Redirect or 403 for non-admin users +- [x] `test_guest_cannot_access_timeline_creation` - Redirect to login ### Edge Case Tests -- [ ] `test_search_only_returns_individual_and_company_users` - Admin users not in results -- [ ] `test_search_only_returns_active_users` - Deactivated users not in results -- [ ] `test_can_create_multiple_timelines_for_same_client` - No unique constraint on user_id +- [x] `test_search_only_returns_individual_and_company_users` - Admin users not in results +- [x] `test_search_only_returns_active_users` - Deactivated users not in results +- [x] `test_can_create_multiple_timelines_for_same_client` - No unique constraint on user_id ## Definition of Done -- [ ] Volt component created at `resources/views/livewire/pages/admin/timelines/create.blade.php` -- [ ] Route registered for admin timeline creation -- [ ] Can search and select client (individual/company only) -- [ ] Can enter case name and reference -- [ ] Timeline created with correct data -- [ ] Initial notes saved as first update -- [ ] Unique reference validation works -- [ ] Client can view timeline immediately (verified via client dashboard) -- [ ] Audit log created -- [ ] All translation keys added (AR/EN) -- [ ] All tests pass -- [ ] Code formatted with Pint +- [x] Volt component created at `resources/views/livewire/admin/timelines/create.blade.php` +- [x] Route registered for admin timeline creation +- [x] Can search and select client (individual/company only) +- [x] Can enter case name and reference +- [x] Timeline created with correct data +- [x] Initial notes saved as first update +- [x] Unique reference validation works +- [ ] Client can view timeline immediately (verified via client dashboard) - *Requires future story for client timeline view* +- [x] Audit log created +- [x] All translation keys added (AR/EN) +- [x] All tests pass +- [x] Code formatted with Pint ## Dependencies @@ -256,3 +256,138 @@ All tests should use Pest and be placed in `tests/Feature/Admin/TimelineCreation ## Estimation **Complexity:** Low-Medium + +--- + +## QA Results + +### Review Date: 2025-12-27 + +### Reviewed By: Quinn (Test Architect) + +### Code Quality Assessment + +**Overall: Excellent implementation.** The timeline creation feature is well-structured, follows established codebase patterns, and demonstrates strong adherence to Laravel/Livewire best practices. The code is clean, maintainable, and properly tested. + +**Strengths:** +- Class-based Volt component pattern matches existing admin components exactly +- Proper use of User model scopes (`clients()`, `active()`) instead of raw queries +- Correct implementation of AdminLog with existing field names (`action` vs story spec's `action_type`) +- Flux UI components used consistently throughout +- Bilingual support complete with both EN/AR translation files +- Comprehensive test coverage (22 tests) covering happy paths, validation, authorization, and edge cases + +### Refactoring Performed + +None required. The implementation is clean and follows established patterns. + +### Compliance Check + +- Coding Standards: ✓ Follows class-based Volt pattern, uses Flux UI, proper validation +- Project Structure: ✓ Files placed in correct locations matching existing structure +- Testing Strategy: ✓ Pest tests with Volt::test() pattern, comprehensive coverage +- All ACs Met: ✓ All acceptance criteria fulfilled (see traceability below) + +### Requirements Traceability (Given-When-Then) + +| AC# | Acceptance Criteria | Test Coverage | Status | +|-----|---------------------|---------------|--------| +| 1 | Select client (search by name/email) - only individual/company users | `test_admin_can_search_clients_by_name`, `test_admin_can_search_clients_by_email`, `test_search_only_returns_individual_and_company_users` | ✓ | +| 2 | Case name/title (required) | `test_case_name_is_required`, `test_admin_can_create_timeline_with_required_fields` | ✓ | +| 3 | Case reference (optional, unique if provided) | `test_admin_can_create_timeline_with_case_reference`, `test_case_reference_must_be_unique`, `test_case_reference_allows_multiple_nulls` | ✓ | +| 4 | Initial notes (optional) | `test_initial_notes_creates_first_timeline_update`, `test_timeline_without_initial_notes_has_no_updates` | ✓ | +| 5 | Timeline assigned to selected client | `test_admin_can_create_timeline_with_required_fields` (verifies user_id) | ✓ | +| 6 | Creation date automatically recorded | Timestamps handled by Eloquent | ✓ | +| 7 | Status defaults to 'active' | `test_timeline_status_defaults_to_active` | ✓ | +| 8 | Can create multiple timelines per client | `test_can_create_multiple_timelines_for_same_client` | ✓ | +| 9 | Confirmation message on success | Component uses `session()->flash('success', ...)` | ✓ | +| 10 | Client must exist validation | `test_selected_client_must_exist` | ✓ | +| 11 | Audit log entry created | `test_audit_log_created_on_timeline_creation` | ✓ | +| 12 | Bilingual labels/messages | EN/AR translation files complete | ✓ | +| 13 | Authorization tests | `test_non_admin_cannot_access_timeline_creation`, `test_guest_cannot_access_timeline_creation` | ✓ | +| 14 | Search only returns active users | `test_search_only_returns_active_users` | ✓ | + +### Improvements Checklist + +All items completed by the developer: + +- [x] Volt component created with proper class-based pattern +- [x] Client search implemented with debounce for performance +- [x] Validation rules properly defined +- [x] AdminLog integration working correctly +- [x] Translation files complete for both languages +- [x] All 22 tests passing +- [x] Code formatted with Pint + +### Security Review + +**Status: PASS** + +- Authorization: Admin middleware properly protects the route (`routes/web.php:83-85`) +- Validation: All user inputs validated before database operations +- SQL Injection: Uses Eloquent query builder with parameterized queries +- CSRF: Handled by Livewire automatically +- Mass Assignment: Only fillable attributes used + +### Performance Considerations + +**Status: PASS** + +- Client search uses `limit(10)` to prevent excessive results +- Debounce (300ms) on search input reduces server requests +- Minimum 2-character search threshold prevents over-querying +- Eager loading not needed (no relationship hydration in search) + +### Files Modified During Review + +None. No refactoring was necessary. + +### Gate Status + +Gate: **PASS** → docs/qa/gates/4.1-timeline-creation.yml + +### Recommended Status + +✓ **Ready for Done** + +All acceptance criteria have been met, tests pass, code follows established patterns, and no security or performance concerns were identified. + +--- + +## Dev Agent Record + +### Status +**Ready for Review** + +### Agent Model Used +Claude Opus 4.5 (claude-opus-4-5-20251101) + +### File List + +**Created:** +- `resources/views/livewire/admin/timelines/create.blade.php` - Volt component for timeline creation +- `lang/en/timelines.php` - English translation keys for timelines +- `lang/ar/timelines.php` - Arabic translation keys for timelines +- `tests/Feature/Admin/TimelineCreationTest.php` - 22 Pest tests for timeline creation + +**Modified:** +- `routes/web.php` - Added admin.timelines.create route +- `lang/en/messages.php` - Added timeline_created message +- `lang/ar/messages.php` - Added timeline_created message (Arabic) + +### Change Log +1. Created Volt component with client search, case name/reference inputs, initial notes +2. Implemented searchable client dropdown (individual/company users only, active only) +3. Added validation rules (case name required, case reference unique) +4. Created audit logging on timeline creation +5. Initial notes saved as first TimelineUpdate if provided +6. Added bilingual translation support (EN/AR) +7. Registered route at `/admin/timelines/create` +8. Created 22 comprehensive Pest tests covering all scenarios + +### Completion Notes +- Component path adjusted from `pages/admin/timelines/` to `admin/timelines/` to match existing codebase structure +- Redirect after creation goes to `admin.dashboard` (not `admin.timelines.show`) since show route is in a future story +- Used existing User model scopes (`clients()`, `active()`) instead of raw queries +- AdminLog uses `action` field (not `action_type` as in story spec) to match existing model +- One DoD item remains unchecked: "Client can view timeline immediately" requires client dashboard timeline view (future story) diff --git a/lang/ar/messages.php b/lang/ar/messages.php index 788e898..fc8e079 100644 --- a/lang/ar/messages.php +++ b/lang/ar/messages.php @@ -24,4 +24,7 @@ return [ 'not_paid_consultation' => 'هذه ليست استشارة مدفوعة.', 'payment_already_received' => 'تم تحديد الدفع كمستلم مسبقاً.', 'client_account_not_found' => 'حساب العميل غير موجود.', + + // Timeline Management + 'timeline_created' => 'تم إنشاء الجدول الزمني بنجاح.', ]; diff --git a/lang/ar/timelines.php b/lang/ar/timelines.php new file mode 100644 index 0000000..53dd5b5 --- /dev/null +++ b/lang/ar/timelines.php @@ -0,0 +1,33 @@ + 'الجداول الزمنية', + 'create_timeline' => 'إنشاء جدول زمني', + 'back_to_timelines' => 'العودة إلى الجداول الزمنية', + + // Form labels + 'case_name' => 'اسم القضية', + 'case_reference' => 'رقم مرجع القضية', + 'initial_notes' => 'ملاحظات أولية', + 'select_client' => 'اختر العميل', + 'search_client' => 'البحث بالاسم أو البريد الإلكتروني...', + 'selected_client' => 'العميل المحدد', + + // Form actions + 'create' => 'إنشاء', + 'cancel' => 'إلغاء', + + // Placeholders + 'case_name_placeholder' => 'أدخل اسم القضية', + 'case_reference_placeholder' => 'أدخل رقم مرجع القضية (اختياري)', + 'initial_notes_placeholder' => 'أضف ملاحظات أولية لهذا الجدول الزمني (اختياري)', + + // Validation messages + 'case_reference_exists' => 'رقم مرجع القضية هذا مستخدم بالفعل.', + 'client_required' => 'يرجى اختيار عميل.', + + // Search + 'no_clients_found' => 'لم يتم العثور على عملاء مطابقين لبحثك.', + 'type_to_search' => 'اكتب حرفين على الأقل للبحث...', +]; diff --git a/lang/en/messages.php b/lang/en/messages.php index ebfca3a..3a1ef09 100644 --- a/lang/en/messages.php +++ b/lang/en/messages.php @@ -24,4 +24,7 @@ return [ 'not_paid_consultation' => 'This is not a paid consultation.', 'payment_already_received' => 'Payment has already been marked as received.', 'client_account_not_found' => 'Client account not found.', + + // Timeline Management + 'timeline_created' => 'Timeline created successfully.', ]; diff --git a/lang/en/timelines.php b/lang/en/timelines.php new file mode 100644 index 0000000..487d346 --- /dev/null +++ b/lang/en/timelines.php @@ -0,0 +1,33 @@ + 'Timelines', + 'create_timeline' => 'Create Timeline', + 'back_to_timelines' => 'Back to Timelines', + + // Form labels + 'case_name' => 'Case Name', + 'case_reference' => 'Case Reference', + 'initial_notes' => 'Initial Notes', + 'select_client' => 'Select Client', + 'search_client' => 'Search by name or email...', + 'selected_client' => 'Selected Client', + + // Form actions + 'create' => 'Create', + 'cancel' => 'Cancel', + + // Placeholders + 'case_name_placeholder' => 'Enter case name', + 'case_reference_placeholder' => 'Enter case reference (optional)', + 'initial_notes_placeholder' => 'Add initial notes for this timeline (optional)', + + // Validation messages + 'case_reference_exists' => 'This case reference is already in use.', + 'client_required' => 'Please select a client.', + + // Search + 'no_clients_found' => 'No clients found matching your search.', + 'type_to_search' => 'Type at least 2 characters to search...', +]; diff --git a/resources/views/livewire/admin/timelines/create.blade.php b/resources/views/livewire/admin/timelines/create.blade.php new file mode 100644 index 0000000..16672e8 --- /dev/null +++ b/resources/views/livewire/admin/timelines/create.blade.php @@ -0,0 +1,223 @@ + ['required', 'exists:users,id'], + 'caseName' => ['required', 'string', 'max:255'], + 'caseReference' => ['nullable', 'string', 'max:50', 'unique:timelines,case_reference'], + ]; + } + + public function messages(): array + { + return [ + 'selectedUserId.required' => __('timelines.client_required'), + 'caseReference.unique' => __('timelines.case_reference_exists'), + ]; + } + + public function updatedSearch(): void + { + if ($this->selectedUser && ! str_contains(strtolower($this->selectedUser->full_name), strtolower($this->search))) { + $this->selectedUserId = null; + $this->selectedUser = null; + } + } + + public function getClientsProperty() + { + if (strlen($this->search) < 2) { + return collect(); + } + + return User::query() + ->clients() + ->active() + ->where(function ($query) { + $query->where('full_name', 'like', "%{$this->search}%") + ->orWhere('email', 'like', "%{$this->search}%"); + }) + ->limit(10) + ->get(); + } + + public function selectUser(int $userId): void + { + $this->selectedUserId = $userId; + $this->selectedUser = User::find($userId); + $this->search = $this->selectedUser->full_name; + } + + public function clearSelection(): void + { + $this->selectedUserId = null; + $this->selectedUser = null; + $this->search = ''; + } + + public function create(): void + { + $this->validate(); + + $timeline = Timeline::create([ + 'user_id' => $this->selectedUserId, + 'case_name' => $this->caseName, + 'case_reference' => $this->caseReference ?: null, + 'status' => TimelineStatus::Active, + ]); + + if ($this->initialNotes) { + $timeline->updates()->create([ + 'admin_id' => auth()->id(), + 'update_text' => $this->initialNotes, + ]); + } + + AdminLog::create([ + 'admin_id' => auth()->id(), + 'action' => 'create', + 'target_type' => 'timeline', + 'target_id' => $timeline->id, + 'new_values' => $timeline->toArray(), + 'ip_address' => request()->ip(), + 'created_at' => now(), + ]); + + session()->flash('success', __('messages.timeline_created')); + + $this->redirect(route('admin.dashboard'), navigate: true); + } +}; ?> + +
+
+ + {{ __('timelines.back_to_timelines') }} + +
+ +
+ {{ __('timelines.create_timeline') }} +
+ +
+
+ {{-- Client Selection --}} + + {{ __('timelines.select_client') }} * + + @if($selectedUser) +
+ +
+
{{ $selectedUser->full_name }}
+
{{ $selectedUser->email }}
+
+ +
+ @else +
+ + + @if(strlen($search) >= 2) +
+ @forelse($this->clients as $client) + + @empty +
+ {{ __('timelines.no_clients_found') }} +
+ @endforelse +
+ @elseif(strlen($search) > 0 && strlen($search) < 2) +
+
+ {{ __('timelines.type_to_search') }} +
+
+ @endif +
+ @endif + + +
+ +
+ {{-- Case Name --}} + + {{ __('timelines.case_name') }} * + + + + + {{-- Case Reference --}} + + {{ __('timelines.case_reference') }} ({{ __('common.optional') }}) + + + +
+ + {{-- Initial Notes --}} + + {{ __('timelines.initial_notes') }} ({{ __('common.optional') }}) + + + + +
+ + {{ __('timelines.cancel') }} + + + {{ __('timelines.create') }} + +
+
+
+
diff --git a/routes/web.php b/routes/web.php index 3fd64f1..59ef365 100644 --- a/routes/web.php +++ b/routes/web.php @@ -79,6 +79,11 @@ Route::middleware(['auth', 'active'])->group(function () { Volt::route('/clients/{user}/consultations', 'admin.clients.consultation-history') ->name('admin.clients.consultation-history'); + // Timelines Management + Route::prefix('timelines')->name('admin.timelines.')->group(function () { + Volt::route('/create', 'admin.timelines.create')->name('create'); + }); + // Admin Settings Route::prefix('settings')->name('admin.settings.')->group(function () { Volt::route('/working-hours', 'admin.settings.working-hours')->name('working-hours'); diff --git a/tests/Feature/Admin/TimelineCreationTest.php b/tests/Feature/Admin/TimelineCreationTest.php new file mode 100644 index 0000000..509ecd2 --- /dev/null +++ b/tests/Feature/Admin/TimelineCreationTest.php @@ -0,0 +1,359 @@ +admin = User::factory()->admin()->create(); +}); + +// =========================================== +// Happy Path Tests +// =========================================== + +test('admin can view timeline creation form', function () { + $this->actingAs($this->admin) + ->get(route('admin.timelines.create')) + ->assertOk(); +}); + +test('admin can search clients by name', function () { + $client = User::factory()->individual()->create(['full_name' => 'John Doe']); + User::factory()->individual()->create(['full_name' => 'Jane Smith']); + + $this->actingAs($this->admin); + + $component = Volt::test('admin.timelines.create') + ->set('search', 'John'); + + expect($component->get('clients'))->toHaveCount(1); + expect($component->get('clients')->first()->full_name)->toBe('John Doe'); +}); + +test('admin can search clients by email', function () { + $client = User::factory()->individual()->create([ + 'full_name' => 'John Doe', + 'email' => 'john@example.com', + ]); + User::factory()->individual()->create([ + 'full_name' => 'Jane Smith', + 'email' => 'jane@example.com', + ]); + + $this->actingAs($this->admin); + + $component = Volt::test('admin.timelines.create') + ->set('search', 'john@'); + + expect($component->get('clients'))->toHaveCount(1); + expect($component->get('clients')->first()->email)->toBe('john@example.com'); +}); + +test('admin can create timeline with required fields', function () { + $client = User::factory()->individual()->create(); + + $this->actingAs($this->admin); + + Volt::test('admin.timelines.create') + ->set('selectedUserId', $client->id) + ->call('selectUser', $client->id) + ->set('caseName', 'Test Case') + ->call('create') + ->assertHasNoErrors() + ->assertRedirect(route('admin.dashboard')); + + expect(Timeline::where('case_name', 'Test Case')->exists())->toBeTrue(); + + $timeline = Timeline::where('case_name', 'Test Case')->first(); + expect($timeline->user_id)->toBe($client->id); + expect($timeline->status)->toBe(TimelineStatus::Active); + expect($timeline->case_reference)->toBeNull(); +}); + +test('admin can create timeline with case reference', function () { + $client = User::factory()->individual()->create(); + + $this->actingAs($this->admin); + + Volt::test('admin.timelines.create') + ->set('selectedUserId', $client->id) + ->call('selectUser', $client->id) + ->set('caseName', 'Test Case') + ->set('caseReference', 'REF-001') + ->call('create') + ->assertHasNoErrors(); + + $timeline = Timeline::where('case_name', 'Test Case')->first(); + expect($timeline->case_reference)->toBe('REF-001'); +}); + +test('initial notes creates first timeline update', function () { + $client = User::factory()->individual()->create(); + + $this->actingAs($this->admin); + + Volt::test('admin.timelines.create') + ->set('selectedUserId', $client->id) + ->call('selectUser', $client->id) + ->set('caseName', 'Test Case') + ->set('initialNotes', 'Initial case notes here') + ->call('create') + ->assertHasNoErrors(); + + $timeline = Timeline::where('case_name', 'Test Case')->first(); + expect($timeline->updates)->toHaveCount(1); + + $update = $timeline->updates->first(); + expect($update->update_text)->toBe('Initial case notes here'); + expect($update->admin_id)->toBe($this->admin->id); +}); + +test('audit log created on timeline creation', function () { + $client = User::factory()->individual()->create(); + + $this->actingAs($this->admin); + + Volt::test('admin.timelines.create') + ->set('selectedUserId', $client->id) + ->call('selectUser', $client->id) + ->set('caseName', 'Test Case') + ->call('create') + ->assertHasNoErrors(); + + expect(AdminLog::where('action', 'create') + ->where('target_type', 'timeline') + ->where('admin_id', $this->admin->id) + ->exists())->toBeTrue(); +}); + +// =========================================== +// Validation Tests +// =========================================== + +test('case name is required', function () { + $client = User::factory()->individual()->create(); + + $this->actingAs($this->admin); + + Volt::test('admin.timelines.create') + ->set('selectedUserId', $client->id) + ->call('selectUser', $client->id) + ->set('caseName', '') + ->call('create') + ->assertHasErrors(['caseName' => 'required']); +}); + +test('case reference must be unique', function () { + $client = User::factory()->individual()->create(); + Timeline::factory()->create(['case_reference' => 'REF-001']); + + $this->actingAs($this->admin); + + Volt::test('admin.timelines.create') + ->set('selectedUserId', $client->id) + ->call('selectUser', $client->id) + ->set('caseName', 'Test Case') + ->set('caseReference', 'REF-001') + ->call('create') + ->assertHasErrors(['caseReference' => 'unique']); +}); + +test('case reference allows multiple nulls', function () { + $client = User::factory()->individual()->create(); + + // Create a timeline without case_reference + Timeline::factory()->create([ + 'user_id' => $client->id, + 'case_reference' => null, + ]); + + $this->actingAs($this->admin); + + // Creating another timeline without case_reference should succeed + Volt::test('admin.timelines.create') + ->set('selectedUserId', $client->id) + ->call('selectUser', $client->id) + ->set('caseName', 'Test Case') + ->set('caseReference', '') + ->call('create') + ->assertHasNoErrors(); + + expect(Timeline::where('user_id', $client->id)->whereNull('case_reference')->count())->toBe(2); +}); + +test('client selection is required', function () { + $this->actingAs($this->admin); + + Volt::test('admin.timelines.create') + ->set('caseName', 'Test Case') + ->call('create') + ->assertHasErrors(['selectedUserId' => 'required']); +}); + +test('selected client must exist', function () { + $this->actingAs($this->admin); + + Volt::test('admin.timelines.create') + ->set('selectedUserId', 99999) + ->set('caseName', 'Test Case') + ->call('create') + ->assertHasErrors(['selectedUserId' => 'exists']); +}); + +// =========================================== +// Authorization Tests +// =========================================== + +test('non-admin cannot access timeline creation', function () { + $client = User::factory()->individual()->create(); + + $this->actingAs($client) + ->get(route('admin.timelines.create')) + ->assertForbidden(); +}); + +test('guest cannot access timeline creation', function () { + $this->get(route('admin.timelines.create')) + ->assertRedirect(route('login')); +}); + +// =========================================== +// Edge Case Tests +// =========================================== + +test('search only returns individual and company users', function () { + $individualClient = User::factory()->individual()->create(['full_name' => 'Individual Client']); + $companyClient = User::factory()->company()->create(['full_name' => 'Company Client']); + $adminUser = User::factory()->admin()->create(['full_name' => 'Admin User']); + + $this->actingAs($this->admin); + + $component = Volt::test('admin.timelines.create') + ->set('search', 'Client'); + + $clientNames = $component->get('clients')->pluck('full_name')->toArray(); + + expect($clientNames)->toContain('Individual Client'); + expect($clientNames)->toContain('Company Client'); + expect($clientNames)->not->toContain('Admin User'); +}); + +test('search only returns active users', function () { + $activeClient = User::factory()->individual()->create(['full_name' => 'Active Client']); + $deactivatedClient = User::factory()->individual()->deactivated()->create(['full_name' => 'Deactivated Client']); + + $this->actingAs($this->admin); + + $component = Volt::test('admin.timelines.create') + ->set('search', 'Client'); + + $clientNames = $component->get('clients')->pluck('full_name')->toArray(); + + expect($clientNames)->toContain('Active Client'); + expect($clientNames)->not->toContain('Deactivated Client'); +}); + +test('can create multiple timelines for same client', function () { + $client = User::factory()->individual()->create(); + + $this->actingAs($this->admin); + + // Create first timeline + Volt::test('admin.timelines.create') + ->set('selectedUserId', $client->id) + ->call('selectUser', $client->id) + ->set('caseName', 'First Case') + ->call('create') + ->assertHasNoErrors(); + + // Create second timeline for same client + Volt::test('admin.timelines.create') + ->set('selectedUserId', $client->id) + ->call('selectUser', $client->id) + ->set('caseName', 'Second Case') + ->call('create') + ->assertHasNoErrors(); + + expect(Timeline::where('user_id', $client->id)->count())->toBe(2); +}); + +// =========================================== +// Additional Tests +// =========================================== + +test('search requires at least 2 characters', function () { + User::factory()->individual()->create(['full_name' => 'John Doe']); + + $this->actingAs($this->admin); + + $component = Volt::test('admin.timelines.create') + ->set('search', 'J'); + + expect($component->get('clients'))->toHaveCount(0); + + $component->set('search', 'Jo'); + expect($component->get('clients'))->toHaveCount(1); +}); + +test('selecting user sets search to user name', function () { + $client = User::factory()->individual()->create(['full_name' => 'John Doe']); + + $this->actingAs($this->admin); + + $component = Volt::test('admin.timelines.create') + ->set('search', 'John') + ->call('selectUser', $client->id); + + expect($component->get('selectedUserId'))->toBe($client->id); + expect($component->get('search'))->toBe('John Doe'); +}); + +test('clearing selection resets user and search', function () { + $client = User::factory()->individual()->create(['full_name' => 'John Doe']); + + $this->actingAs($this->admin); + + $component = Volt::test('admin.timelines.create') + ->call('selectUser', $client->id) + ->call('clearSelection'); + + expect($component->get('selectedUserId'))->toBeNull(); + expect($component->get('selectedUser'))->toBeNull(); + expect($component->get('search'))->toBe(''); +}); + +test('timeline without initial notes has no updates', function () { + $client = User::factory()->individual()->create(); + + $this->actingAs($this->admin); + + Volt::test('admin.timelines.create') + ->set('selectedUserId', $client->id) + ->call('selectUser', $client->id) + ->set('caseName', 'Test Case') + ->set('initialNotes', '') + ->call('create') + ->assertHasNoErrors(); + + $timeline = Timeline::where('case_name', 'Test Case')->first(); + expect($timeline->updates)->toHaveCount(0); +}); + +test('timeline status defaults to active', function () { + $client = User::factory()->individual()->create(); + + $this->actingAs($this->admin); + + Volt::test('admin.timelines.create') + ->set('selectedUserId', $client->id) + ->call('selectUser', $client->id) + ->set('caseName', 'Test Case') + ->call('create') + ->assertHasNoErrors(); + + $timeline = Timeline::where('case_name', 'Test Case')->first(); + expect($timeline->status)->toBe(TimelineStatus::Active); +});