reviewed epic 5 & 6 stories
This commit is contained in:
@@ -3,57 +3,297 @@
|
||||
## 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**,
|
||||
So that **I can analyze and report on consultation history**.
|
||||
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
|
||||
- [ ] Date range (scheduled_date between start/end)
|
||||
- [ ] Consultation type (free/paid)
|
||||
- [ ] Status (approved/completed/no-show/cancelled)
|
||||
- [ ] Payment status
|
||||
- [ ] Status (pending/approved/completed/no-show/cancelled)
|
||||
- [ ] Payment status (pending/received)
|
||||
|
||||
### Export Includes
|
||||
- [ ] Client name
|
||||
- [ ] Date and time
|
||||
- [ ] Consultation type
|
||||
- [ ] Client name (from user relationship)
|
||||
- [ ] Date and time (scheduled_date, scheduled_time)
|
||||
- [ ] Consultation type (free/paid)
|
||||
- [ ] Status
|
||||
- [ ] Payment status
|
||||
- [ ] Problem summary
|
||||
- [ ] Problem summary (truncated in PDF if > 500 chars)
|
||||
|
||||
### Formats
|
||||
- [ ] CSV format
|
||||
- [ ] PDF format with professional layout and branding
|
||||
- [ ] 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 `resources/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
|
||||
|
||||
```php
|
||||
public function exportConsultationsPdf(Request $request)
|
||||
// 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
|
||||
<?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
|
||||
{
|
||||
$consultations = Consultation::query()
|
||||
->with('user')
|
||||
->when($request->date_from, fn($q) => $q->where('scheduled_date', '>=', $request->date_from))
|
||||
->when($request->status, fn($q) => $q->where('status', $request->status))
|
||||
->get();
|
||||
public function csv(Request $request): StreamedResponse
|
||||
{
|
||||
$consultations = $this->getFilteredConsultations($request);
|
||||
|
||||
$pdf = Pdf::loadView('exports.consultations', compact('consultations'));
|
||||
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'),
|
||||
]);
|
||||
|
||||
return $pdf->download('consultations-export.pdf');
|
||||
$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`:
|
||||
```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`:
|
||||
```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
|
||||
<?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
|
||||
- [ ] CSV export accurate
|
||||
- [ ] PDF professionally formatted
|
||||
- [ ] Large summaries handled in PDF
|
||||
- [ ] Tests pass
|
||||
- [ ] 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
|
||||
|
||||
Reference in New Issue
Block a user