410 lines
15 KiB
Markdown
410 lines
15 KiB
Markdown
# Story 6.6: Data Export - Timeline Reports
|
|
|
|
## 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
|
|
- [ ] `test_admin_can_access_timeline_export_page` - Page loads with filter controls
|
|
- [ ] `test_admin_can_export_all_timelines_csv` - CSV downloads with all timelines
|
|
- [ ] `test_admin_can_export_all_timelines_pdf` - PDF downloads with branding
|
|
- [ ] `test_admin_can_filter_by_client` - Only selected client's timelines exported
|
|
- [ ] `test_admin_can_filter_by_status_active` - Only active timelines exported
|
|
- [ ] `test_admin_can_filter_by_status_archived` - Only archived timelines exported
|
|
- [ ] `test_admin_can_filter_by_date_range` - Timelines within range exported
|
|
- [ ] `test_include_updates_toggle_adds_content_to_pdf` - Update text appears in PDF
|
|
- [ ] `test_csv_headers_match_admin_language` - AR/EN headers based on locale
|
|
|
|
### Validation Tests
|
|
- [ ] `test_date_from_cannot_be_after_date_to` - Validation error shown
|
|
- [ ] `test_client_filter_only_shows_individual_and_company_users` - Admin users excluded
|
|
|
|
### Edge Case Tests
|
|
- [ ] `test_export_empty_results_returns_valid_csv` - Empty CSV with headers
|
|
- [ ] `test_export_empty_results_returns_valid_pdf` - PDF with "no records" message
|
|
- [ ] `test_timeline_without_updates_shows_zero_count` - updates_count = 0
|
|
- [ ] `test_timeline_without_reference_shows_dash` - case_reference displays "-"
|
|
- [ ] `test_pdf_renders_arabic_content_correctly` - Arabic text not garbled
|
|
|
|
### Authorization Tests
|
|
- [ ] `test_non_admin_cannot_access_timeline_export` - 403 or redirect
|
|
- [ ] `test_guest_redirected_to_login` - Redirect to login page
|
|
|
|
## Definition of Done
|
|
|
|
- [ ] Volt component created at `resources/views/livewire/pages/admin/exports/timelines.blade.php`
|
|
- [ ] PDF template created at `resources/views/exports/timelines-pdf.blade.php`
|
|
- [ ] Route registered in admin routes
|
|
- [ ] Navigation link added to admin dashboard exports section
|
|
- [ ] All filters work (client, status, date range)
|
|
- [ ] CSV export generates valid file with correct data
|
|
- [ ] PDF export generates with Libra branding (navy/gold)
|
|
- [ ] Include updates toggle works for PDF
|
|
- [ ] Empty results handled gracefully
|
|
- [ ] Bilingual support (AR/EN headers and labels)
|
|
- [ ] All translation keys added
|
|
- [ ] All tests pass
|
|
- [ ] 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
|