# Story 6.4: Data Export - User Lists ## Epic Reference **Epic 6:** Admin Dashboard ## User Story As an **admin**, I want **to export user data in CSV and PDF formats**, So that **I can generate reports and maintain offline records**. ## Dependencies - **Epic 2 Complete:** User Management system with User model and data - **Story 6.1:** Dashboard Overview (provides the admin dashboard layout where export UI will be accessible) - **Packages Required:** `league/csv` and `barryvdh/laravel-dompdf` must be installed ## References - PRD Section 11.2: Export Functionality - defines exportable data and formats - PRD Section 5.3: User Management System - defines user fields (individual vs company) - `docs/epics/epic-6-admin-dashboard.md#story-6.4` - epic-level acceptance criteria - User model: `app/Models/User.php` - contains `user_type`, `national_id`, `company_cert_number` fields ## Acceptance Criteria ### Export Options - [ ] Export all users (both individual and company clients) - [ ] Export individual clients only (`user_type = 'individual'`) - [ ] Export company clients only (`user_type = 'company'`) ### Filters - [ ] Date range filter on `created_at` field (start date, end date) - [ ] Status filter: active, deactivated, or all - [ ] Filters combine with export type (e.g., "active individual clients created in 2024") ### CSV Export Includes - [ ] Name (`name` for individual, `company_name` for company) - [ ] Email (`email`) - [ ] Phone (`phone`) - [ ] User type (`user_type`: individual/company) - [ ] National ID / Company registration (`national_id` for individual, `company_cert_number` for company) - [ ] Status (`status`: active/deactivated) - [ ] Created date (`created_at` formatted per locale) - [ ] UTF-8 BOM for proper Arabic character display in Excel ### PDF Export Includes - [ ] Same data fields as CSV in tabular format - [ ] Libra branding header (logo, firm name) - [ ] Generation timestamp in footer - [ ] Page numbers if multiple pages - [ ] Professional formatting with brand colors (Navy #0A1F44, Gold #D4AF37) ### Bilingual Support - [ ] Column headers based on admin's `preferred_language` setting - [ ] Date formatting per locale (DD/MM/YYYY for Arabic, MM/DD/YYYY for English) - [ ] PDF title and footer text bilingual ### UI Requirements - [ ] Export section accessible from Admin User Management page - [ ] Filter form with: user type dropdown, status dropdown, date range picker - [ ] "Export CSV" and "Export PDF" buttons - [ ] Loading indicator during export generation - [ ] Success toast on download start - [ ] Error toast if export fails ## Technical Implementation ### Files to Create/Modify **Livewire Component:** ``` resources/views/livewire/admin/users/export-users.blade.php ``` **PDF Template:** ``` resources/views/pdf/users-export.blade.php ``` **Translation File (add export keys):** ``` resources/lang/ar/export.php resources/lang/en/export.php ``` ### Routes Export actions will be methods on the Livewire component - no separate routes needed. The component handles streaming responses. ### Key User Model Fields ```php // From User model (app/Models/User.php) $user->name // Individual's full name $user->company_name // Company name (if company type) $user->email $user->phone $user->user_type // 'individual' or 'company' $user->national_id // Individual's national ID $user->company_cert_number // Company registration number $user->status // 'active' or 'deactivated' $user->preferred_language // 'ar' or 'en' $user->created_at ``` ### Implementation Pattern **CSV Export (Streamed Response):** ```php use League\Csv\Writer; use Symfony\Component\HttpFoundation\StreamedResponse; public function exportCsv(): StreamedResponse { $users = $this->getFilteredUsers(); $locale = auth()->user()->preferred_language ?? 'ar'; return response()->streamDownload(function () use ($users, $locale) { $csv = Writer::createFromString(); // UTF-8 BOM for Excel Arabic support echo "\xEF\xBB\xBF"; // Headers based on admin language $csv->insertOne([ __('export.name', [], $locale), __('export.email', [], $locale), __('export.phone', [], $locale), __('export.user_type', [], $locale), __('export.id_number', [], $locale), __('export.status', [], $locale), __('export.created_at', [], $locale), ]); $users->cursor()->each(function ($user) use ($csv, $locale) { $csv->insertOne([ $user->user_type === 'company' ? $user->company_name : $user->name, $user->email, $user->phone, __('export.type_' . $user->user_type, [], $locale), $user->user_type === 'company' ? $user->company_cert_number : $user->national_id, __('export.status_' . $user->status, [], $locale), $user->created_at->format($locale === 'ar' ? 'd/m/Y' : 'm/d/Y'), ]); }); echo $csv->toString(); }, 'users-export-' . now()->format('Y-m-d') . '.csv', [ 'Content-Type' => 'text/csv; charset=UTF-8', ]); } ``` **PDF Export:** ```php use Barryvdh\DomPDF\Facade\Pdf; public function exportPdf() { $users = $this->getFilteredUsers()->get(); $locale = auth()->user()->preferred_language ?? 'ar'; $pdf = Pdf::loadView('pdf.users-export', [ 'users' => $users, 'locale' => $locale, 'generatedAt' => now(), 'filters' => $this->getActiveFilters(), ]); // RTL support for Arabic if ($locale === 'ar') { $pdf->setOption('isHtml5ParserEnabled', true); $pdf->setOption('defaultFont', 'DejaVu Sans'); } return response()->streamDownload( fn () => print($pdf->output()), 'users-export-' . now()->format('Y-m-d') . '.pdf' ); } ``` **Filter Query Builder:** ```php private function getFilteredUsers() { return User::query() ->when($this->userType !== 'all', fn ($q) => $q->where('user_type', $this->userType)) ->when($this->status !== 'all', fn ($q) => $q->where('status', $this->status)) ->when($this->dateFrom, fn ($q) => $q->whereDate('created_at', '>=', $this->dateFrom)) ->when($this->dateTo, fn ($q) => $q->whereDate('created_at', '<=', $this->dateTo)) ->whereIn('user_type', ['individual', 'company']) // Exclude admin ->orderBy('created_at', 'desc'); } ``` ### Translation Keys Required ```php // resources/lang/en/export.php return [ 'name' => 'Name', 'email' => 'Email', 'phone' => 'Phone', 'user_type' => 'User Type', 'id_number' => 'ID Number', 'status' => 'Status', 'created_at' => 'Created Date', 'type_individual' => 'Individual', 'type_company' => 'Company', 'status_active' => 'Active', 'status_deactivated' => 'Deactivated', 'users_export_title' => 'Users Export', 'generated_at' => 'Generated at', 'page' => 'Page', ]; // resources/lang/ar/export.php return [ 'name' => 'الاسم', 'email' => 'البريد الإلكتروني', 'phone' => 'الهاتف', 'user_type' => 'نوع المستخدم', 'id_number' => 'رقم الهوية', 'status' => 'الحالة', 'created_at' => 'تاريخ الإنشاء', 'type_individual' => 'فرد', 'type_company' => 'شركة', 'status_active' => 'نشط', 'status_deactivated' => 'معطل', 'users_export_title' => 'تصدير المستخدمين', 'generated_at' => 'تم الإنشاء في', 'page' => 'صفحة', ]; ``` ## Edge Cases & Error Handling ### Empty Dataset - If no users match filters, show info message: "No users match the selected filters" - Do not generate empty export file ### Large Datasets (1000+ users) - Use `cursor()` for CSV to avoid memory issues - For PDF with 500+ users, show warning: "Large export may take a moment" - Consider chunking PDF generation or limiting to 500 users with message to narrow filters ### Export Failures - Catch exceptions and show error toast: "Export failed. Please try again." - Log error details for debugging ### Concurrent Requests - Disable export buttons while export is in progress (wire:loading) ### Arabic Content - CSV: Include UTF-8 BOM (`\xEF\xBB\xBF`) for Excel compatibility - PDF: Use font that supports Arabic (DejaVu Sans or Cairo) ## Testing Requirements ### Feature Tests ```php // tests/Feature/Admin/UserExportTest.php test('admin can export all users as CSV', function () { $admin = User::factory()->admin()->create(); User::factory()->count(5)->individual()->create(); User::factory()->count(3)->company()->create(); Volt::test('admin.users.export-users') ->actingAs($admin) ->set('userType', 'all') ->call('exportCsv') ->assertFileDownloaded(); }); test('admin can export filtered users by type', function () { $admin = User::factory()->admin()->create(); User::factory()->count(5)->individual()->create(); User::factory()->count(3)->company()->create(); // Test individual filter Volt::test('admin.users.export-users') ->actingAs($admin) ->set('userType', 'individual') ->call('exportCsv') ->assertFileDownloaded(); }); test('admin can export users filtered by status', function () { $admin = User::factory()->admin()->create(); User::factory()->count(3)->individual()->active()->create(); User::factory()->count(2)->individual()->deactivated()->create(); Volt::test('admin.users.export-users') ->actingAs($admin) ->set('status', 'active') ->call('exportCsv') ->assertFileDownloaded(); }); test('admin can export users filtered by date range', function () { $admin = User::factory()->admin()->create(); User::factory()->individual()->create(['created_at' => now()->subDays(10)]); User::factory()->individual()->create(['created_at' => now()->subDays(5)]); User::factory()->individual()->create(['created_at' => now()]); Volt::test('admin.users.export-users') ->actingAs($admin) ->set('dateFrom', now()->subDays(7)->format('Y-m-d')) ->set('dateTo', now()->format('Y-m-d')) ->call('exportCsv') ->assertFileDownloaded(); }); test('admin can export users as PDF', function () { $admin = User::factory()->admin()->create(); User::factory()->count(5)->individual()->create(); Volt::test('admin.users.export-users') ->actingAs($admin) ->call('exportPdf') ->assertFileDownloaded(); }); test('export shows message when no users match filters', function () { $admin = User::factory()->admin()->create(); Volt::test('admin.users.export-users') ->actingAs($admin) ->set('userType', 'individual') ->set('status', 'active') ->call('exportCsv') ->assertHasNoErrors() ->assertDispatched('notify'); // Empty dataset notification }); test('export headers use admin preferred language', function () { $admin = User::factory()->admin()->create(['preferred_language' => 'ar']); User::factory()->individual()->create(); // This would need custom assertion to check CSV content // Verify Arabic headers are used }); test('non-admin cannot access export', function () { $client = User::factory()->individual()->create(); Volt::test('admin.users.export-users') ->actingAs($client) ->assertForbidden(); }); ``` ### Manual Testing Checklist - [ ] CSV opens correctly in Excel with Arabic characters displaying properly - [ ] PDF renders with Libra branding (logo, colors) - [ ] PDF Arabic content is readable (correct font, RTL if needed) - [ ] Date range picker works correctly - [ ] Combined filters produce correct results - [ ] Large export (500+ users) completes without timeout - [ ] Export buttons disabled during generation (loading state) ## Definition of Done - [ ] Livewire component created with filter form and export buttons - [ ] CSV export works with all filter combinations - [ ] PDF export renders with Libra branding header and footer - [ ] Translation files created for both Arabic and English - [ ] UTF-8 BOM included in CSV for Arabic Excel compatibility - [ ] Large datasets handled efficiently using cursor/chunking - [ ] Empty dataset shows appropriate message (no empty file generated) - [ ] Error handling with user-friendly toast messages - [ ] Loading states on export buttons - [ ] All feature tests pass - [ ] Manual testing checklist completed - [ ] Code formatted with Pint ## Estimation **Complexity:** Medium | **Effort:** 4-5 hours ## Out of Scope - Background job processing for very large exports (defer to future enhancement) - Email delivery of export files - Scheduled/automated exports