libra/docs/stories/story-6.5-data-export-consu...

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 user relationship 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's scheduled_date, scheduled_time
  • Translation files are in lang/ directory (not resources/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 CSV
  • resources/views/livewire/admin/consultations/export-consultations.blade.php:76 - Large export warning implementation
  • resources/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 rejected status to filter tests (minor - currently relies on factory states)

Security Review

Status: PASS

  • Route protected by auth, active, and admin middleware
  • 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

✓ 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.