16 KiB
Story 6.5: Data Export - Consultation Records
Epic Reference
Epic 6: Admin Dashboard
Dependencies
- Story 6.4: Uses identical export patterns (CSV streaming, PDF generation, bilingual headers)
- Epic 3: Consultation model with
userrelationship and booking statuses
User Story
As an admin, I want to export consultation/booking data in CSV and PDF formats, So that I can generate reports for accounting, analyze consultation patterns, and maintain offline records of client interactions.
Acceptance Criteria
Export Options
- Export all consultations
- Export filtered subset based on criteria below
Filters
- Date range (booking_date between start/end)
- Consultation type (free/paid)
- Status (pending/approved/completed/no-show/cancelled/rejected)
- Payment status (pending/received/na)
Export Includes
- Client name (from user relationship)
- Date and time (booking_date, booking_time)
- Consultation type (free/paid)
- Status
- Payment status
- Problem summary (truncated in PDF if > 500 chars)
Formats
- CSV format with streaming download
- PDF format with professional layout and Libra branding
Bilingual Support
- Column headers based on admin's preferred language
- Use translation keys from
lang/{locale}/export.php
Technical Notes
Implementation Pattern
Follow the export pattern established in Story 6.4. Reuse any base export functionality created there.
Files to Create/Modify
app/Http/Controllers/Admin/ConsultationExportController.php # New controller
resources/views/exports/consultations.blade.php # PDF template
resources/lang/en/export.php # Add consultation keys
resources/lang/ar/export.php # Add consultation keys
routes/web.php # Add export routes
Routes
// In admin routes group
Route::prefix('exports')->name('admin.exports.')->group(function () {
Route::get('consultations/csv', [ConsultationExportController::class, 'csv'])->name('consultations.csv');
Route::get('consultations/pdf', [ConsultationExportController::class, 'pdf'])->name('consultations.pdf');
});
Controller Implementation
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Consultation;
use Barryvdh\DomPDF\Facade\Pdf;
use Illuminate\Http\Request;
use League\Csv\Writer;
use Symfony\Component\HttpFoundation\StreamedResponse;
class ConsultationExportController extends Controller
{
public function csv(Request $request): StreamedResponse
{
$consultations = $this->getFilteredConsultations($request);
return response()->streamDownload(function () use ($consultations) {
$csv = Writer::createFromString();
$csv->insertOne([
__('export.client_name'),
__('export.date'),
__('export.time'),
__('export.consultation_type'),
__('export.status'),
__('export.payment_status'),
__('export.problem_summary'),
]);
$consultations->cursor()->each(fn ($consultation) => $csv->insertOne([
$consultation->user->full_name,
$consultation->scheduled_date->format('Y-m-d'),
$consultation->scheduled_time->format('H:i'),
__('consultations.type.'.$consultation->consultation_type),
__('consultations.status.'.$consultation->status),
__('consultations.payment.'.$consultation->payment_status),
$consultation->problem_summary,
]));
echo $csv->toString();
}, 'consultations-export-'.now()->format('Y-m-d').'.csv');
}
public function pdf(Request $request)
{
$consultations = $this->getFilteredConsultations($request)->get();
$pdf = Pdf::loadView('exports.consultations', [
'consultations' => $consultations,
'generatedAt' => now(),
'filters' => $request->only(['date_from', 'date_to', 'type', 'status', 'payment_status']),
]);
return $pdf->download('consultations-export-'.now()->format('Y-m-d').'.pdf');
}
private function getFilteredConsultations(Request $request)
{
return Consultation::query()
->with('user')
->when($request->date_from, fn ($q) => $q->where('scheduled_date', '>=', $request->date_from))
->when($request->date_to, fn ($q) => $q->where('scheduled_date', '<=', $request->date_to))
->when($request->type, fn ($q) => $q->where('consultation_type', $request->type))
->when($request->status, fn ($q) => $q->where('status', $request->status))
->when($request->payment_status, fn ($q) => $q->where('payment_status', $request->payment_status))
->orderBy('scheduled_date', 'desc');
}
}
PDF Template Structure
The PDF template (resources/views/exports/consultations.blade.php) should include:
- Libra logo and branding header (navy blue #0A1F44, gold #D4AF37)
- Report title with generation timestamp
- Applied filters summary
- Data table with alternating row colors
- Problem summary truncated to 500 chars with "..." if longer
- Footer with page numbers
Translation Keys
Add to resources/lang/en/export.php:
'client_name' => 'Client Name',
'date' => 'Date',
'time' => 'Time',
'consultation_type' => 'Type',
'status' => 'Status',
'payment_status' => 'Payment Status',
'problem_summary' => 'Problem Summary',
Add to resources/lang/ar/export.php:
'client_name' => 'اسم العميل',
'date' => 'التاريخ',
'time' => 'الوقت',
'consultation_type' => 'النوع',
'status' => 'الحالة',
'payment_status' => 'حالة الدفع',
'problem_summary' => 'ملخص المشكلة',
Edge Cases
- Empty results: Return empty CSV with headers only, or PDF with "No consultations found" message
- Large datasets: Use cursor() for CSV streaming; for PDF, consider chunking or limiting to 500 records with warning
- Large problem summaries: Truncate to 500 characters in PDF table cells with "..." indicator
Testing Requirements
Feature Tests
Create tests/Feature/Admin/ConsultationExportTest.php:
<?php
use App\Models\Consultation;
use App\Models\User;
test('admin can export consultations as csv', function () {
$admin = User::factory()->admin()->create();
Consultation::factory()->count(5)->create();
$response = $this->actingAs($admin)
->get(route('admin.exports.consultations.csv'));
$response->assertOk();
$response->assertHeader('content-type', 'text/csv; charset=UTF-8');
});
test('admin can export consultations as pdf', function () {
$admin = User::factory()->admin()->create();
Consultation::factory()->count(5)->create();
$response = $this->actingAs($admin)
->get(route('admin.exports.consultations.pdf'));
$response->assertOk();
$response->assertHeader('content-type', 'application/pdf');
});
test('consultation export filters by date range', function () {
$admin = User::factory()->admin()->create();
Consultation::factory()->create(['scheduled_date' => now()->subDays(10)]);
Consultation::factory()->create(['scheduled_date' => now()]);
$response = $this->actingAs($admin)
->get(route('admin.exports.consultations.csv', [
'date_from' => now()->subDays(5)->format('Y-m-d'),
]));
$response->assertOk();
// CSV should contain only 1 consultation (today's)
});
test('consultation export filters by type', function () {
$admin = User::factory()->admin()->create();
Consultation::factory()->create(['consultation_type' => 'free']);
Consultation::factory()->create(['consultation_type' => 'paid']);
$response = $this->actingAs($admin)
->get(route('admin.exports.consultations.csv', ['type' => 'paid']));
$response->assertOk();
});
test('consultation export filters by status', function () {
$admin = User::factory()->admin()->create();
Consultation::factory()->create(['status' => 'completed']);
Consultation::factory()->create(['status' => 'no-show']);
$response = $this->actingAs($admin)
->get(route('admin.exports.consultations.csv', ['status' => 'completed']));
$response->assertOk();
});
test('consultation export handles empty results', function () {
$admin = User::factory()->admin()->create();
$response = $this->actingAs($admin)
->get(route('admin.exports.consultations.csv'));
$response->assertOk();
// Should return CSV with headers only
});
test('guests cannot access consultation exports', function () {
$response = $this->get(route('admin.exports.consultations.csv'));
$response->assertRedirect(route('login'));
});
Test Scenarios Checklist
- CSV export with no filters returns all consultations
- CSV export with date range filter works correctly
- CSV export with type filter (free/paid) works correctly
- CSV export with status filter works correctly
- CSV export with payment status filter works correctly
- CSV export with combined filters works correctly
- PDF export generates valid PDF with branding
- PDF export includes applied filters summary
- Empty export returns appropriate response (not error)
- Large dataset (500+ records) exports within reasonable time
- Bilingual headers render correctly based on admin locale
- Unauthenticated users are redirected to login
Definition of Done
- All filters work correctly (date range, type, status, payment)
- CSV export streams correctly with proper headers
- PDF export generates with Libra branding (logo, colors)
- Large problem summaries truncated properly in PDF
- Bilingual column headers work based on admin language
- Empty results handled gracefully
- All feature tests pass
- Code formatted with Pint
Estimation
Complexity: Medium | Effort: 3 hours
References
- PRD Section 11.2: Export Functionality requirements
- Story 6.4: User export implementation (follow same patterns)
- Epic 3: Consultation model structure and statuses
Dev Agent Record
Status
Ready for Review
Agent Model Used
Claude Opus 4.5
File List
| File | Action |
|---|---|
lang/en/export.php |
Modified - Added consultation export translation keys |
lang/ar/export.php |
Modified - Added consultation export translation keys |
resources/views/livewire/admin/consultations/export-consultations.blade.php |
Created - Volt component for consultation export |
resources/views/pdf/consultations-export.blade.php |
Created - PDF template for consultation export |
routes/web.php |
Modified - Added consultation export route |
tests/Feature/Admin/ConsultationExportTest.php |
Created - Feature tests (24 tests) |
Change Log
- Added consultation export translation keys to English and Arabic translation files
- Created Volt component following Story 6.4 pattern (not controller-based as suggested in Technical Notes)
- Created PDF template with Libra branding, bilingual support, and problem summary truncation
- Added route inside consultations group to avoid conflict with
{consultation}dynamic route - Created comprehensive test suite covering all acceptance criteria
Completion Notes
- Implementation follows Story 6.4 Volt component pattern rather than controller-based approach in Technical Notes
- Used actual database field names (
booking_date,booking_time) instead of story'sscheduled_date,scheduled_time - Translation files are in
lang/directory (notresources/lang/) - All 24 feature tests pass
- Full admin test suite passes (memory exhaustion occurs in unrelated tests due to pre-existing dompdf memory issues)
Debug Log References
N/A - No debugging issues encountered
QA Results
Review Date: 2025-12-27
Reviewed By: Quinn (Test Architect)
Code Quality Assessment
Overall: EXCELLENT implementation that closely follows Story 6.4 patterns. The implementation correctly deviated from the Technical Notes by using a Volt component instead of a controller-based approach, maintaining architectural consistency across the export features.
Strengths:
- Clean, maintainable Volt component following established project patterns
- Proper use of enums (ConsultationStatus, ConsultationType, PaymentStatus)
- Streaming CSV download with UTF-8 BOM for Excel Arabic support
- PDF template with proper Libra branding (navy #0A1F44, gold #D4AF37)
- Comprehensive bilingual support with explicit locale parameter
- Edge case handling: empty results with notification, large dataset warning (>500 records)
- Problem summary truncation at 500 chars in PDF
- Proper eager loading with
with('user:id,full_name')to prevent N+1
Code Quality Highlights:
resources/views/livewire/admin/consultations/export-consultations.blade.php:48- Efficient cursor-based streaming for CSVresources/views/livewire/admin/consultations/export-consultations.blade.php:76- Large export warning implementationresources/views/pdf/consultations-export.blade.php:307-311- Proper problem summary truncation with Str::limit
Refactoring Performed
None required. Implementation quality is high and follows established patterns.
Compliance Check
- Coding Standards: ✓ Pint passes, proper type hints, descriptive method names
- Project Structure: ✓ Volt component pattern matches Story 6.4, proper file locations
- Testing Strategy: ✓ 24 Pest tests with comprehensive coverage
- All ACs Met: ✓ All acceptance criteria verified (see traceability below)
Requirements Traceability
| AC# | Acceptance Criteria | Test Coverage |
|---|---|---|
| 1 | Export all consultations | admin can export all consultations as CSV |
| 2 | Export filtered subset | Multiple filter tests (type, status, payment, date range, combined) |
| 3 | Date range filter (booking_date) | admin can export consultations filtered by date range |
| 4 | Consultation type filter (free/paid) | filtered by free type, filtered by paid type |
| 5 | Status filter (6 states) | Tests for pending, approved, completed, cancelled, no-show |
| 6 | Payment status filter (pending/received/na) | payment status pending, payment status received |
| 7 | Client name export | Verified via user relationship eager loading |
| 8 | Date/time export | Verified in component using booking_date, booking_time |
| 9 | Type/Status/Payment export | Verified with translation keys |
| 10 | Problem summary (truncated) | PDF template line 307-311 uses Str::limit(500) |
| 11 | CSV streaming | exportCsv() uses streamDownload with cursor() |
| 12 | PDF professional layout | Template has Libra branding, page numbers, filters summary |
| 13 | Bilingual headers | Locale parameter passed to all __() calls |
| 14 | Empty results handling | CSV/PDF export dispatches notification when no consultations match |
| 15 | Access control | non-admin cannot access, unauthenticated user tests |
Improvements Checklist
- All acceptance criteria implemented and tested
- Follows Story 6.4 established patterns
- Bilingual support with proper locale handling
- Empty results handled gracefully with notification dispatch
- Large dataset warning at 500+ records
- Problem summary truncation in PDF
- All 24 tests passing
- Consider adding
rejectedstatus to filter tests (minor - currently relies on factory states)
Security Review
Status: PASS
- Route protected by
auth,active, andadminmiddleware - No raw SQL queries - uses Eloquent with proper parameterized filters
- No user input directly rendered in PDF (all values escaped via Blade)
- Export only accessible to authenticated admin users
Performance Considerations
Status: PASS
- CSV uses
cursor()for memory-efficient streaming - PDF has 500 record soft limit with warning notification
- Eager loading prevents N+1 queries:
with('user:id,full_name') - Selective column loading for user relationship
Files Modified During Review
None - no refactoring required.
Gate Status
Gate: PASS → docs/qa/gates/6.5-data-export-consultation-records.yml
Recommended Status
✓ Ready for Done
All acceptance criteria met, comprehensive test coverage (24 tests), proper error handling, bilingual support verified, and implementation follows established patterns from Story 6.4.