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

13 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