13 KiB
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/csvandbarryvdh/laravel-dompdfmust 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- containsuser_type,national_id,company_cert_numberfields
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_atfield (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 (
namefor individual,company_namefor company) - Email (
email) - Phone (
phone) - User type (
user_type: individual/company) - National ID / Company registration (
national_idfor individual,company_cert_numberfor company) - Status (
status: active/deactivated) - Created date (
created_atformatted 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_languagesetting - 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