# Story 6.6: Data Export - Timeline Reports

> **Note:** The color values in this story were implemented with the original Navy+Gold palette.
> These colors were updated in Epic 10 (Brand Color Refresh) to the new Charcoal+Warm Gray palette.
> See `docs/brand.md` for current color specifications.

## Epic Reference
**Epic 6:** Admin Dashboard

## User Story
As an **admin**,
I want **to export timeline and case data**,
So that **I can maintain records and generate case reports**.

## Story Context

### UI Location
This export feature is part of the Admin Dashboard exports section, accessible via the admin navigation. The timeline export page provides filter controls and export buttons for both CSV and PDF formats.

### Existing System Integration
- **Follows pattern:** Story 6.4 (User Lists Export) and Story 6.5 (Consultation Export) - same UI layout, filter approach, and export mechanisms
- **Integrates with:** Timeline model, TimelineUpdate model, User model
- **Technology:** Livewire Volt, Flux UI, league/csv, barryvdh/laravel-dompdf
- **Touch points:** Admin dashboard navigation, timeline management section

### Reference Documents
- **Epic:** `docs/epics/epic-6-admin-dashboard.md#story-66-data-export---timeline-reports`
- **Export Pattern Reference:** `docs/stories/story-6.4-data-export-user-lists.md` - establishes CSV/PDF export patterns
- **Similar Implementation:** `docs/stories/story-6.5-data-export-consultation-records.md` - query and filter patterns
- **Timeline System:** `docs/epics/epic-4-case-timeline.md` - timeline model and relationships
- **Timeline Schema:** `docs/stories/story-4.1-timeline-creation.md#database-schema` - database structure
- **PRD Export Requirements:** `docs/prd.md#117-export-functionality` - business requirements

## Acceptance Criteria

### Export Options
- [ ] Export all timelines (across all clients)
- [ ] Export timelines for specific client (client selector/search)

### Filters
- [ ] Status filter (active/archived/all)
- [ ] Date range filter (created_at)
- [ ] Client filter (search by name/email)

### Export Includes
- [ ] Case name and reference
- [ ] Client name
- [ ] Status
- [ ] Created date
- [ ] Number of updates
- [ ] Last update date

### Formats
- [ ] CSV format with bilingual headers
- [ ] PDF format with Libra branding

### Optional Features
- [ ] Include update content toggle (full content vs summary only)
- [ ] When enabled, PDF includes all update entries per timeline

### UI Requirements
- [ ] Filter controls match Story 6.4/6.5 layout
- [ ] Export buttons clearly visible
- [ ] Loading state during export generation
- [ ] Success/error feedback messages

## Technical Notes

### File Structure
```
Routes:
  GET  /admin/exports/timelines -> admin.exports.timelines (Volt page)

Files to Create:
  resources/views/livewire/pages/admin/exports/timelines.blade.php (Volt component)
  resources/views/exports/timelines-pdf.blade.php (PDF template)

Models Required (from Epic 4):
  app/Models/Timeline.php
  app/Models/TimelineUpdate.php
```

### Database Schema Reference
```php
// timelines table (from Story 4.1)
// Fields: id, user_id, case_name, case_reference, status, created_at, updated_at

// timeline_updates table (from Story 4.1)
// Fields: id, timeline_id, admin_id, update_text, created_at, updated_at
```

### CSV Export Implementation
```php
use League\Csv\Writer;

public function exportCsv(): StreamedResponse
{
    return response()->streamDownload(function () {
        $csv = Writer::createFromString();
        $csv->insertOne([
            __('export.case_name'),
            __('export.case_reference'),
            __('export.client_name'),
            __('export.status'),
            __('export.created_date'),
            __('export.updates_count'),
            __('export.last_update'),
        ]);

        $this->getFilteredTimelines()
            ->cursor()
            ->each(fn($timeline) => $csv->insertOne([
                $timeline->case_name,
                $timeline->case_reference ?? '-',
                $timeline->user->name,
                __('status.' . $timeline->status),
                $timeline->created_at->format('Y-m-d'),
                $timeline->updates_count,
                $timeline->updates_max_created_at
                    ? Carbon::parse($timeline->updates_max_created_at)->format('Y-m-d H:i')
                    : '-',
            ]));

        echo $csv->toString();
    }, 'timelines-export-' . now()->format('Y-m-d') . '.csv');
}

private function getFilteredTimelines()
{
    return Timeline::query()
        ->with('user')
        ->withCount('updates')
        ->withMax('updates', 'created_at')
        ->when($this->clientId, fn($q) => $q->where('user_id', $this->clientId))
        ->when($this->status && $this->status !== 'all', fn($q) => $q->where('status', $this->status))
        ->when($this->dateFrom, fn($q) => $q->where('created_at', '>=', $this->dateFrom))
        ->when($this->dateTo, fn($q) => $q->where('created_at', '<=', $this->dateTo))
        ->orderBy('created_at', 'desc');
}
```

### PDF Export Implementation
```php
use Barryvdh\DomPDF\Facade\Pdf;

public function exportPdf(): Response
{
    $timelines = $this->getFilteredTimelines()
        ->when($this->includeUpdates, fn($q) => $q->with('updates'))
        ->get();

    $pdf = Pdf::loadView('exports.timelines-pdf', [
        'timelines' => $timelines,
        'includeUpdates' => $this->includeUpdates,
        'generatedAt' => now(),
        'filters' => [
            'status' => $this->status,
            'dateFrom' => $this->dateFrom,
            'dateTo' => $this->dateTo,
            'client' => $this->clientId ? User::find($this->clientId)?->name : null,
        ],
    ]);

    return $pdf->download('timelines-report-' . now()->format('Y-m-d') . '.pdf');
}
```

### Volt Component Structure
```php
<?php

use App\Models\Timeline;
use App\Models\User;
use Barryvdh\DomPDF\Facade\Pdf;
use League\Csv\Writer;
use Livewire\Volt\Component;
use Symfony\Component\HttpFoundation\StreamedResponse;

new class extends Component {
    public string $clientSearch = '';
    public ?int $clientId = null;
    public string $status = 'all';
    public ?string $dateFrom = null;
    public ?string $dateTo = null;
    public bool $includeUpdates = false;

    public function getClientsProperty()
    {
        if (strlen($this->clientSearch) < 2) {
            return collect();
        }

        return User::query()
            ->whereIn('user_type', ['individual', 'company'])
            ->where(fn($q) => $q
                ->where('name', 'like', "%{$this->clientSearch}%")
                ->orWhere('email', 'like', "%{$this->clientSearch}%"))
            ->limit(10)
            ->get();
    }

    public function selectClient(int $id): void
    {
        $this->clientId = $id;
        $this->clientSearch = User::find($id)?->name ?? '';
    }

    public function clearClient(): void
    {
        $this->clientId = null;
        $this->clientSearch = '';
    }

    public function exportCsv(): StreamedResponse { /* see above */ }
    public function exportPdf(): Response { /* see above */ }
}; ?>

<div>
    {{-- Filter controls and export buttons using Flux UI --}}
    {{-- Follow layout pattern from Story 6.4/6.5 --}}
</div>
```

### PDF Template Structure
```blade
{{-- resources/views/exports/timelines-pdf.blade.php --}}
<!DOCTYPE html>
<html lang="{{ app()->getLocale() }}" dir="{{ app()->getLocale() === 'ar' ? 'rtl' : 'ltr' }}">
<head>
    <meta charset="UTF-8">
    <style>
        /* Libra branding: Navy (#0A1F44) and Gold (#D4AF37) */
        body { font-family: DejaVu Sans, sans-serif; }
        .header { background: #0A1F44; color: #D4AF37; padding: 20px; }
        .logo { /* Libra logo styling */ }
        table { width: 100%; border-collapse: collapse; }
        th { background: #0A1F44; color: white; padding: 8px; }
        td { border: 1px solid #ddd; padding: 8px; }
        .update-entry { background: #f9f9f9; margin: 5px 0; padding: 10px; }
        .footer { text-align: center; font-size: 10px; color: #666; }
    </style>
</head>
<body>
    <div class="header">
        <img src="{{ public_path('images/logo.png') }}" class="logo">
        <h1>{{ __('export.timeline_report') }}</h1>
        <p>{{ __('export.generated_at') }}: {{ $generatedAt->format('Y-m-d H:i') }}</p>
    </div>

    @if($filters['client'] || $filters['status'] !== 'all' || $filters['dateFrom'])
    <div class="filters">
        <h3>{{ __('export.applied_filters') }}</h3>
        <!-- Display active filters -->
    </div>
    @endif

    <table>
        <thead>
            <tr>
                <th>{{ __('export.case_name') }}</th>
                <th>{{ __('export.case_reference') }}</th>
                <th>{{ __('export.client_name') }}</th>
                <th>{{ __('export.status') }}</th>
                <th>{{ __('export.created_date') }}</th>
                <th>{{ __('export.updates_count') }}</th>
                <th>{{ __('export.last_update') }}</th>
            </tr>
        </thead>
        <tbody>
            @forelse($timelines as $timeline)
            <tr>
                <td>{{ $timeline->case_name }}</td>
                <td>{{ $timeline->case_reference ?? '-' }}</td>
                <td>{{ $timeline->user->name }}</td>
                <td>{{ __('status.' . $timeline->status) }}</td>
                <td>{{ $timeline->created_at->format('Y-m-d') }}</td>
                <td>{{ $timeline->updates_count }}</td>
                <td>{{ $timeline->updates_max_created_at ? Carbon::parse($timeline->updates_max_created_at)->format('Y-m-d H:i') : '-' }}</td>
            </tr>
            @if($includeUpdates && $timeline->updates->count())
            <tr>
                <td colspan="7">
                    <strong>{{ __('export.updates') }}:</strong>
                    @foreach($timeline->updates as $update)
                    <div class="update-entry">
                        <small>{{ $update->created_at->format('Y-m-d H:i') }}</small>
                        <p>{{ Str::limit($update->update_text, 500) }}</p>
                    </div>
                    @endforeach
                </td>
            </tr>
            @endif
            @empty
            <tr>
                <td colspan="7">{{ __('export.no_records') }}</td>
            </tr>
            @endforelse
        </tbody>
    </table>

    <div class="footer">
        <p>{{ __('export.libra_footer') }} | {{ config('app.url') }}</p>
    </div>
</body>
</html>
```

### Required Translation Keys
```php
// resources/lang/en/export.php
'timeline_report' => 'Timeline Report',
'case_name' => 'Case Name',
'case_reference' => 'Case Reference',
'client_name' => 'Client Name',
'status' => 'Status',
'created_date' => 'Created Date',
'updates_count' => 'Updates',
'last_update' => 'Last Update',
'updates' => 'Updates',
'generated_at' => 'Generated At',
'applied_filters' => 'Applied Filters',
'no_records' => 'No records found',
'libra_footer' => 'Libra Law Firm',
'export_timelines' => 'Export Timelines',
'include_updates' => 'Include Update Content',
'all_clients' => 'All Clients',
'select_client' => 'Select Client',

// resources/lang/ar/export.php
'timeline_report' => 'تقرير الجدول الزمني',
'case_name' => 'اسم القضية',
'case_reference' => 'رقم المرجع',
'client_name' => 'اسم العميل',
'status' => 'الحالة',
'created_date' => 'تاريخ الإنشاء',
'updates_count' => 'التحديثات',
'last_update' => 'آخر تحديث',
'updates' => 'التحديثات',
'generated_at' => 'تاريخ الإنشاء',
'applied_filters' => 'الفلاتر المطبقة',
'no_records' => 'لا توجد سجلات',
'libra_footer' => 'مكتب ليبرا للمحاماة',
'export_timelines' => 'تصدير الجداول الزمنية',
'include_updates' => 'تضمين محتوى التحديثات',
'all_clients' => 'جميع العملاء',
'select_client' => 'اختر العميل',
```

### Edge Cases & Error Handling
- **Empty results:** Generate valid file with headers only, show info message
- **Large datasets:** Use `cursor()` for memory-efficient iteration in CSV
- **PDF memory limits:** When `includeUpdates` is true and data is large, consider:
  - Limiting to first 100 timelines with warning message
  - Truncating update text to 500 characters
- **Arabic content in PDF:** Use DejaVu Sans font which supports Arabic characters
- **Date range validation:** Ensure dateFrom <= dateTo

## Test Scenarios

All tests should use Pest and be placed in `tests/Feature/Admin/TimelineExportTest.php`.

### Happy Path Tests
- [x] `test_admin_can_access_timeline_export_page` - Page loads with filter controls
- [x] `test_admin_can_export_all_timelines_csv` - CSV downloads with all timelines
- [x] `test_admin_can_export_all_timelines_pdf` - PDF downloads with branding
- [x] `test_admin_can_filter_by_client` - Only selected client's timelines exported
- [x] `test_admin_can_filter_by_status_active` - Only active timelines exported
- [x] `test_admin_can_filter_by_status_archived` - Only archived timelines exported
- [x] `test_admin_can_filter_by_date_range` - Timelines within range exported
- [x] `test_include_updates_toggle_adds_content_to_pdf` - Update text appears in PDF
- [x] `test_csv_headers_match_admin_language` - AR/EN headers based on locale

### Validation Tests
- [x] `test_date_from_cannot_be_after_date_to` - Validation error shown (handled by filter logic)
- [x] `test_client_filter_only_shows_individual_and_company_users` - Admin users excluded

### Edge Case Tests
- [x] `test_export_empty_results_returns_valid_csv` - Empty CSV with headers (dispatches notification)
- [x] `test_export_empty_results_returns_valid_pdf` - PDF with "no records" message
- [x] `test_timeline_without_updates_shows_zero_count` - updates_count = 0
- [x] `test_timeline_without_reference_shows_dash` - case_reference displays "-"
- [x] `test_pdf_renders_arabic_content_correctly` - Arabic text not garbled (uses DejaVu Sans)

### Authorization Tests
- [x] `test_non_admin_cannot_access_timeline_export` - 403 or redirect
- [x] `test_guest_redirected_to_login` - Redirect to login page

## Definition of Done

- [x] Volt component created at `resources/views/livewire/admin/timelines/export-timelines.blade.php` (adjusted path for consistency)
- [x] PDF template created at `resources/views/pdf/timelines-export.blade.php` (adjusted path for consistency)
- [x] Route registered in admin routes
- [x] Navigation link added to admin timelines index page
- [x] All filters work (client, status, date range)
- [x] CSV export generates valid file with correct data
- [x] PDF export generates with Libra branding (navy/gold)
- [x] Include updates toggle works for PDF
- [x] Empty results handled gracefully
- [x] Bilingual support (AR/EN headers and labels)
- [x] All translation keys added
- [x] All tests pass (26 tests)
- [x] Code formatted with Pint

## Dependencies

- **Story 6.4:** Data Export - User Lists (establishes export patterns, packages already installed)
- **Story 6.5:** Data Export - Consultation Records (similar implementation pattern)
- **Story 4.1:** Timeline Creation (Timeline model, database schema)
- **Story 4.2:** Timeline Updates Management (TimelineUpdate model)
- **Story 1.3:** Bilingual Infrastructure (translation system)

## Estimation
**Complexity:** Medium | **Effort:** 3 hours

---

## Dev Agent Record

### Status
**Ready for Review**

### Agent Model Used
Claude Opus 4.5

### File List

**Created:**
- `resources/views/livewire/admin/timelines/export-timelines.blade.php` - Volt component for timeline export
- `resources/views/pdf/timelines-export.blade.php` - PDF template with Libra branding
- `tests/Feature/Admin/TimelineExportTest.php` - 26 feature tests

**Modified:**
- `routes/web.php` - Added export route at `admin/timelines/export`
- `lang/en/export.php` - Added timeline export translation keys
- `lang/ar/export.php` - Added Arabic timeline export translations
- `resources/views/livewire/admin/timelines/index.blade.php` - Added export button

### Completion Notes
- All 26 tests pass (CSV export, PDF export, filtering, client search, authorization)
- Follows established patterns from Story 6.4 and 6.5
- Component location differs slightly from story spec (placed alongside other timeline components for consistency)
- Export button added to timelines index page header
- Include updates toggle limits to 10 updates per timeline in PDF to prevent memory issues
- Memory issue during full test suite is pre-existing (occurs in users-export.blade.php, not this implementation)

### Change Log
| Date | Change | By |
|------|--------|-----|
| 2025-12-27 | Initial implementation complete | Claude Opus 4.5 |

## QA Results

### Review Date: 2025-12-27

### Reviewed By: Quinn (Test Architect)

### Code Quality Assessment

**Overall Rating: Excellent**

The implementation demonstrates strong adherence to established patterns from Story 6.4 and 6.5. The code is well-structured, follows Laravel/Livewire best practices, and properly implements all acceptance criteria.

**Strengths:**
- Consistent with established export component patterns (users, consultations)
- Proper use of Volt class-based component architecture
- Memory-efficient CSV export using `cursor()` for large datasets
- Appropriate eager loading with selective columns (`with('user:id,full_name')`)
- Proper handling of Timeline status enum throughout
- Good RTL/LTR support for Arabic content in PDF
- DejaVu Sans font for proper Arabic character rendering
- UTF-8 BOM included for Excel Arabic compatibility
- Well-implemented client search with dropdown functionality
- Appropriate memory safeguards (10 updates limit per timeline in PDF, >500 warning)

### Requirements Traceability

| AC | Test Coverage | Given-When-Then |
|----|---------------|-----------------|
| Export all timelines | ✓ `admin_can_export_all_timelines_csv`, `admin_can_export_all_timelines_pdf` | Given admin on export page, When no filters set, Then all timelines exported |
| Export for specific client | ✓ `admin_can_filter_by_client` | Given admin selects client, When export triggered, Then only that client's timelines included |
| Status filter | ✓ `admin_can_filter_by_status_active`, `admin_can_filter_by_status_archived` | Given admin selects status, When export triggered, Then only matching status exported |
| Date range filter | ✓ `admin_can_filter_by_date_range` | Given admin sets date range, When export triggered, Then only timelines in range exported |
| Client filter search | ✓ `client_search_returns_matching_clients`, `client_search_excludes_admin_users`, `client_search_requires_minimum_2_characters` | Given admin types in search, When >= 2 chars entered, Then matching clients shown excluding admins |
| Export includes all data fields | ✓ `timeline_without_updates_shows_zero_count`, `timeline_without_reference_shows_dash` | Given timeline with various data states, When exported, Then all fields correctly formatted |
| CSV with bilingual headers | ✓ `export_uses_admin_preferred_language_for_headers`, `export_uses_English_when_admin_prefers_English` | Given admin preference set, When export generated, Then headers match language |
| PDF with Libra branding | ✓ `admin_can_export_all_timelines_pdf` | Given admin exports PDF, When file generated, Then Navy/Gold branding present |
| Include updates toggle | ✓ `include_updates_toggle_adds_content_to_pdf` | Given includeUpdates enabled, When PDF exported, Then update entries included |
| UI filter controls | ✓ `preview_count_updates_when_filters_change`, `clear_filters_resets_all_filter_values` | Given filters applied, When count displayed, Then matches filtered records |
| Loading states | ✓ Implemented with `wire:loading` | Given export in progress, When button clicked, Then loading state shown |
| Empty results handling | ✓ `CSV_export_dispatches_notification_when_no_timelines_match`, `PDF_export_dispatches_notification_when_no_timelines_match` | Given no matching results, When export attempted, Then notification dispatched |
| Authorization | ✓ `non_admin_cannot_access_timeline_export`, `unauthenticated_user_cannot_access_timeline_export_page` | Given non-admin/guest, When accessing page, Then 403/redirect |

### Refactoring Performed

None required - implementation follows established patterns and best practices.

### Compliance Check

- Coding Standards: ✓ Pint passes with no issues
- Project Structure: ✓ Component placed in `admin/timelines/` alongside related components
- Testing Strategy: ✓ 26 comprehensive Pest tests covering all scenarios
- All ACs Met: ✓ All acceptance criteria have corresponding test coverage

### Improvements Checklist

- [x] All acceptance criteria implemented
- [x] CSV export with cursor for memory efficiency
- [x] PDF export with memory limits (10 updates per timeline, 500 record warning)
- [x] Bilingual support (AR/EN headers and labels)
- [x] RTL/LTR direction handling in PDF
- [x] Export button added to timelines index page
- [x] Route properly registered
- [x] All translations added
- [x] Authorization tests included
- [ ] Consider adding test for combined filter edge cases (all filters at once with specific values)
- [ ] Consider adding test to verify CSV content structure (beyond just download success)

### Security Review

**Status: PASS**

- ✓ Admin middleware properly applied via route group
- ✓ No SQL injection vulnerabilities (using Eloquent query builder with proper parameter binding)
- ✓ Client search excludes admin users (prevents leaking admin data)
- ✓ No sensitive data exposed in exports beyond authorized scope
- ✓ CSRF protection via Livewire's built-in mechanisms

### Performance Considerations

**Status: PASS**

- ✓ CSV uses `cursor()` for memory-efficient large dataset handling
- ✓ PDF limits updates to 10 per timeline to prevent memory exhaustion
- ✓ Warning shown when exporting >500 records
- ✓ Proper eager loading with selective columns (`user:id,full_name`)
- ✓ Preview count uses COUNT query, not collection count
- ✓ Client search limited to 10 results

### Files Modified During Review

None - implementation is complete and correct as delivered.

### Gate Status

Gate: **PASS** → `docs/qa/gates/6.6-data-export-timeline-reports.yml`

### Recommended Status

✓ **Ready for Done** - All acceptance criteria met with comprehensive test coverage. Implementation follows established patterns and passes all quality checks.
