libra/docs/stories/story-4.1-timeline-creation.md

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_type of '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/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

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

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

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

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