libra/docs/stories/story-6.4-data-export-user-...

18 KiB

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

// 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):

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:

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:

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

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

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

Dev Agent Record

Status

Ready for Review

Agent Model Used

Claude Opus 4.5

File List

Created:

  • resources/views/livewire/admin/users/export-users.blade.php - Livewire Volt component with export functionality
  • resources/views/pdf/users-export.blade.php - PDF template with Libra branding
  • lang/en/export.php - English translation file
  • lang/ar/export.php - Arabic translation file
  • tests/Feature/Admin/UserExportTest.php - Feature tests (20 tests)

Modified:

  • routes/web.php - Added export route
  • composer.json - Added league/csv and barryvdh/laravel-dompdf packages

Change Log

  1. Installed required packages: league/csv and barryvdh/laravel-dompdf
  2. Created bilingual translation files with all required export keys
  3. Created Livewire Volt component with:
    • Filter form (user type, status, date range)
    • CSV export with UTF-8 BOM for Arabic Excel support
    • PDF export with Libra branding and professional styling
    • Live preview count
    • Loading states on buttons
    • Empty dataset handling with notifications
  4. Created PDF template with:
    • Libra branding header (Navy #0A1F44, Gold #D4AF37)
    • Generation timestamp footer
    • Page numbers
    • RTL support for Arabic locale
  5. Added route: admin/users/export -> admin.users.export
  6. Created comprehensive feature tests (20 tests, all passing)
  7. Formatted code with Pint

Completion Notes

  • All 20 feature tests pass
  • CSV exports with UTF-8 BOM for proper Arabic display in Excel
  • PDF includes all branding requirements (colors, logo text, timestamps)
  • Component excludes admin users from export (only clients)
  • Uses cursor() for memory-efficient CSV export with large datasets
  • Dispatches notifications for empty datasets instead of generating empty files
  • Manual testing required for visual verification of PDF branding and Arabic rendering

QA Results

Review Date: 2025-12-27

Reviewed By: Quinn (Test Architect)

Code Quality Assessment

Overall: EXCELLENT - The implementation is well-structured, follows Laravel/Livewire best practices, and demonstrates solid software engineering principles. Code is clean, readable, and maintainable.

Strengths:

  • Clean separation of concerns with private helper methods (getFilteredUsers, getActiveFilters)
  • Proper use of class-based Volt component pattern matching project conventions
  • Memory-efficient CSV export using cursor() for large datasets
  • Consistent use of Enums (UserType, UserStatus) instead of magic strings
  • Bilingual support with proper locale handling
  • UTF-8 BOM for Excel Arabic compatibility
  • Professional PDF template with proper branding colors (#0A1F44 Navy, #D4AF37 Gold)
  • RTL support in PDF for Arabic locale

Minor Observations:

  • PDF uses $user->user_type->value comparison while Blade uses Enum directly in places - consistent but slightly different patterns (not an issue, just observation)
  • Large dataset warning (500+) dispatched but no hard limit implemented - acceptable per story scope

Refactoring Performed

No refactoring performed - code quality meets standards.

Compliance Check

  • Coding Standards: ✓ Class-based Volt, Flux UI components, Model::query() pattern, proper naming
  • Project Structure: ✓ Files in correct locations per story specification
  • Testing Strategy: ✓ Pest tests with Volt::test(), factory states used
  • All ACs Met: ✓ All 13 acceptance criteria implemented and verified

Improvements Checklist

All items verified - no required changes:

  • Proper admin middleware protection (admin middleware in routes)
  • Input validation via Livewire property types and Eloquent query building
  • Empty dataset handling (notification instead of empty file)
  • Large dataset handling (cursor() for CSV, warning for PDF 500+)
  • UTF-8 BOM for Arabic Excel support
  • Bilingual column headers based on admin preference
  • Loading states on export buttons (wire:loading directives)
  • Admin users excluded from export results
  • All 20 tests passing

Security Review

Status: PASS

  • Access control: Route protected by auth, active, and admin middleware stack
  • Authorization test coverage: Tests verify non-admin and unauthenticated access is blocked
  • Data exposure: Export correctly excludes admin users, only exports client data
  • No SQL injection risk: Uses Eloquent query builder with proper parameter binding
  • No XSS risk: PDF template uses Blade escaping

Performance Considerations

Status: PASS

  • CSV export uses cursor() for memory-efficient streaming with large datasets
  • PDF warns users about large exports (500+)
  • No N+1 query issues (no relationships loaded)
  • Query uses proper indexes (user_type, status, created_at)
  • Streaming response prevents memory exhaustion

Files Modified During Review

Navigation added to make export accessible from UI:

  • resources/views/livewire/admin/widgets/quick-actions.blade.php - Added "Export Users" button
  • resources/views/livewire/admin/clients/individual/index.blade.php - Added "Export Users" button
  • resources/views/livewire/admin/clients/company/index.blade.php - Added "Export Users" button
  • lang/en/widgets.php - Added export_users translation key
  • lang/ar/widgets.php - Added export_users translation key

Reason: UI requirement "Export section accessible from Admin User Management page" was not met - no navigation existed to reach the export page.

Gate Status

Gate: PASS → docs/qa/gates/6.4-data-export-user-lists.yml

✓ Ready for Done

Implementation is complete, well-tested (20 tests, 26 assertions), follows all coding standards, and meets all acceptance criteria. Manual testing for visual verification of PDF branding and Arabic rendering remains per story specification.