15 KiB
Story 4.1: Timeline Creation
Epic Reference
Epic 4: Case Timeline System
User Story
As an admin, I want to create case timelines for clients, So that I can track and communicate progress on their legal matters.
Story Context
Existing System Integration
- Integrates with: timelines table, timeline_updates table, users table, admin_logs table
- Technology: Livewire Volt, Flux UI
- Follows pattern: Admin CRUD pattern (see existing admin components)
- Touch points: User relationship, client dashboard, audit logging
Reference Documents
- Epic:
docs/epics/epic-4-case-timeline.md - Database Schema:
docs/stories/story-1.1-project-setup-database-schema.md#database-schema-reference - User Model: Requires users with
user_typeof 'individual' or 'company' (from Epic 2)
Acceptance Criteria
Timeline Creation Form
- Select client (search by name/email) - only users with user_type 'individual' or 'company'
- Case name/title (required)
- Case reference number (optional, unique if provided)
- Initial notes (optional)
Behavior
- Timeline assigned to selected client
- Creation date automatically recorded
- Status defaults to 'active'
- Can create multiple timelines per client
- Confirmation message on successful creation
- Timeline immediately visible to client
Validation
- Case name required
- Case reference unique if provided
- Client must exist
Quality Requirements
- Audit log entry created via AdminLog model
- Bilingual labels and messages
- Tests for creation flow
Technical Notes
File Structure
Routes:
GET /admin/timelines/create -> admin.timelines.create
POST handled by Livewire component
Files to Create:
resources/views/livewire/pages/admin/timelines/create.blade.php (Volt component)
Models Required (from Story 1.1):
app/Models/Timeline.php
app/Models/TimelineUpdate.php
app/Models/AdminLog.php
Database Schema
// timelines table (from Story 1.1)
Schema::create('timelines', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('case_name');
$table->string('case_reference')->nullable()->unique();
$table->enum('status', ['active', 'archived'])->default('active');
$table->timestamps();
});
// timeline_updates table (from Story 1.1)
Schema::create('timeline_updates', function (Blueprint $table) {
$table->id();
$table->foreignId('timeline_id')->constrained()->cascadeOnDelete();
$table->foreignId('admin_id')->constrained('users')->cascadeOnDelete();
$table->text('update_text');
$table->timestamps();
});
Required Translation Keys
// resources/lang/en/messages.php
'timeline_created' => 'Timeline created successfully.',
// resources/lang/ar/messages.php
'timeline_created' => 'تم إنشاء الجدول الزمني بنجاح.',
// resources/lang/en/labels.php
'case_name' => 'Case Name',
'case_reference' => 'Case Reference',
'initial_notes' => 'Initial Notes',
'select_client' => 'Select Client',
'search_client' => 'Search by name or email...',
'create_timeline' => 'Create Timeline',
// resources/lang/ar/labels.php
'case_name' => 'اسم القضية',
'case_reference' => 'رقم مرجع القضية',
'initial_notes' => 'ملاحظات أولية',
'select_client' => 'اختر العميل',
'search_client' => 'البحث بالاسم أو البريد الإلكتروني...',
'create_timeline' => 'إنشاء جدول زمني',
Volt Component
<?php
use App\Models\AdminLog;
use App\Models\Timeline;
use App\Models\User;
use Livewire\Volt\Component;
new class extends Component {
public string $search = '';
public ?int $selectedUserId = null;
public ?User $selectedUser = null;
public string $caseName = '';
public string $caseReference = '';
public string $initialNotes = '';
public function updatedSearch(): void
{
// Reset selection when search changes
if ($this->selectedUser && ! str_contains(strtolower($this->selectedUser->name), strtolower($this->search))) {
$this->selectedUserId = null;
$this->selectedUser = null;
}
}
public function getClientsProperty()
{
if (strlen($this->search) < 2) {
return collect();
}
return User::query()
->whereIn('user_type', ['individual', 'company'])
->where('status', 'active')
->where(function ($query) {
$query->where('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->name;
}
public function create(): void
{
$this->validate([
'selectedUserId' => ['required', 'exists:users,id'],
'caseName' => ['required', 'string', 'max:255'],
'caseReference' => ['nullable', 'string', 'max:50', 'unique:timelines,case_reference'],
]);
$timeline = Timeline::create([
'user_id' => $this->selectedUserId,
'case_name' => $this->caseName,
'case_reference' => $this->caseReference ?: null,
'status' => 'active',
]);
// Add initial notes as first update if provided
if ($this->initialNotes) {
$timeline->updates()->create([
'admin_id' => auth()->id(),
'update_text' => $this->initialNotes,
]);
}
AdminLog::create([
'admin_id' => auth()->id(),
'action_type' => 'create',
'target_type' => 'timeline',
'target_id' => $timeline->id,
'new_values' => $timeline->toArray(),
'ip_address' => request()->ip(),
]);
session()->flash('success', __('messages.timeline_created'));
$this->redirect(route('admin.timelines.show', $timeline));
}
}; ?>
<div>
{{-- Component template here using Flux UI --}}
</div>
Test Scenarios
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/createtest_admin_can_search_clients_by_name- Search returns matching userstest_admin_can_search_clients_by_email- Search returns matching userstest_admin_can_create_timeline_with_required_fields- Timeline created with case_name onlytest_admin_can_create_timeline_with_case_reference- Timeline created with optional referencetest_initial_notes_creates_first_timeline_update- TimelineUpdate record createdtest_audit_log_created_on_timeline_creation- AdminLog entry exists
Validation Tests
test_case_name_is_required- Validation error without case_nametest_case_reference_must_be_unique- Validation error on duplicate referencetest_case_reference_allows_multiple_nulls- Multiple timelines without reference allowedtest_client_selection_is_required- Validation error without selecting clienttest_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 userstest_guest_cannot_access_timeline_creation- Redirect to login
Edge Case Tests
test_search_only_returns_individual_and_company_users- Admin users not in resultstest_search_only_returns_active_users- Deactivated users not in resultstest_can_create_multiple_timelines_for_same_client- No unique constraint on user_id
Definition of Done
- Volt component created at
resources/views/livewire/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) - Requires future story for client timeline view
- Audit log created
- All translation keys added (AR/EN)
- All tests pass
- Code formatted with Pint
Dependencies
- Story 1.1: Database schema (timelines, timeline_updates, admin_logs tables and models)
- Story 1.2: Authentication & role system (admin role check)
- Story 2.1/2.2: User accounts exist to assign timelines to
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 (
actionvs story spec'saction_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:
- Volt component created with proper class-based pattern
- Client search implemented with debounce for performance
- Validation rules properly defined
- AdminLog integration working correctly
- Translation files complete for both languages
- All 22 tests passing
- 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 creationlang/en/timelines.php- English translation keys for timelineslang/ar/timelines.php- Arabic translation keys for timelinestests/Feature/Admin/TimelineCreationTest.php- 22 Pest tests for timeline creation
Modified:
routes/web.php- Added admin.timelines.create routelang/en/messages.php- Added timeline_created messagelang/ar/messages.php- Added timeline_created message (Arabic)
Change Log
- Created Volt component with client search, case name/reference inputs, initial notes
- Implemented searchable client dropdown (individual/company users only, active only)
- Added validation rules (case name required, case reference unique)
- Created audit logging on timeline creation
- Initial notes saved as first TimelineUpdate if provided
- Added bilingual translation support (EN/AR)
- Registered route at
/admin/timelines/create - Created 22 comprehensive Pest tests covering all scenarios
Completion Notes
- Component path adjusted from
pages/admin/timelines/toadmin/timelines/to match existing codebase structure - Redirect after creation goes to
admin.dashboard(notadmin.timelines.show) since show route is in a future story - Used existing User model scopes (
clients(),active()) instead of raw queries - AdminLog uses
actionfield (notaction_typeas in story spec) to match existing model - One DoD item remains unchecked: "Client can view timeline immediately" requires client dashboard timeline view (future story)