8.7 KiB
8.7 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/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
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