842 lines
29 KiB
Markdown
842 lines
29 KiB
Markdown
# Story 3.5: Admin Booking Review & Approval
|
|
|
|
## Epic Reference
|
|
**Epic 3:** Booking & Consultation System
|
|
|
|
## User Story
|
|
As an **admin**,
|
|
I want **to review, categorize, and approve or reject booking requests**,
|
|
So that **I can manage my consultation schedule and set appropriate consultation types**.
|
|
|
|
## Story Context
|
|
|
|
### Existing System Integration
|
|
- **Integrates with:** consultations table, notifications, .ics generation
|
|
- **Technology:** Livewire Volt, Flux UI
|
|
- **Follows pattern:** Admin action workflow
|
|
- **Touch points:** Client notifications, calendar file
|
|
|
|
## Acceptance Criteria
|
|
|
|
### Pending Bookings List
|
|
- [x] View all pending booking requests
|
|
- [x] Display: client name, requested date/time, submission date
|
|
- [x] Show problem summary preview
|
|
- [x] Click to view full details
|
|
- [x] Sort by date (oldest first default)
|
|
- [x] Filter by date range
|
|
|
|
### Booking Details View
|
|
- [x] Full client information
|
|
- [x] Complete problem summary
|
|
- [x] Client consultation history
|
|
- [x] Requested date and time
|
|
|
|
### Approval Workflow
|
|
- [x] Set consultation type:
|
|
- Free consultation
|
|
- Paid consultation
|
|
- [x] If paid: set payment amount
|
|
- [x] If paid: add payment instructions (optional)
|
|
- [x] Approve button with confirmation
|
|
- [x] On approval:
|
|
- Status changes to 'approved'
|
|
- Client notified via email
|
|
- .ics calendar file attached to email
|
|
- Payment instructions included if paid
|
|
|
|
### Rejection Workflow
|
|
- [x] Optional rejection reason field
|
|
- [x] Reject button with confirmation
|
|
- [x] On rejection:
|
|
- Status changes to 'rejected'
|
|
- Client notified via email with reason
|
|
|
|
### Quick Actions
|
|
- [x] Quick approve (free) button on list
|
|
- [x] Quick reject button on list
|
|
- [ ] Bulk actions (optional)
|
|
|
|
### Quality Requirements
|
|
- [x] Audit log for all decisions
|
|
- [x] Bilingual notifications
|
|
- [x] Tests for approval/rejection flow
|
|
|
|
## Technical Notes
|
|
|
|
### Consultation Status Flow
|
|
```
|
|
pending -> approved (admin approves)
|
|
pending -> rejected (admin rejects)
|
|
approved -> completed (after consultation)
|
|
approved -> no_show (client didn't attend)
|
|
approved -> cancelled (admin cancels)
|
|
```
|
|
|
|
### Volt Component for Review
|
|
```php
|
|
<?php
|
|
|
|
use App\Models\Consultation;
|
|
use App\Notifications\BookingApproved;
|
|
use App\Notifications\BookingRejected;
|
|
use App\Services\CalendarService;
|
|
use Livewire\Volt\Component;
|
|
|
|
new class extends Component {
|
|
public Consultation $consultation;
|
|
|
|
public string $consultationType = 'free';
|
|
public ?float $paymentAmount = null;
|
|
public string $paymentInstructions = '';
|
|
public string $rejectionReason = '';
|
|
|
|
public bool $showApproveModal = false;
|
|
public bool $showRejectModal = false;
|
|
|
|
public function mount(Consultation $consultation): void
|
|
{
|
|
$this->consultation = $consultation;
|
|
}
|
|
|
|
public function approve(): void
|
|
{
|
|
$this->validate([
|
|
'consultationType' => ['required', 'in:free,paid'],
|
|
'paymentAmount' => ['required_if:consultationType,paid', 'nullable', 'numeric', 'min:0'],
|
|
'paymentInstructions' => ['nullable', 'string', 'max:1000'],
|
|
]);
|
|
|
|
$this->consultation->update([
|
|
'status' => 'approved',
|
|
'type' => $this->consultationType,
|
|
'payment_amount' => $this->consultationType === 'paid' ? $this->paymentAmount : null,
|
|
'payment_status' => $this->consultationType === 'paid' ? 'pending' : 'not_applicable',
|
|
]);
|
|
|
|
// Generate calendar file
|
|
$calendarService = app(CalendarService::class);
|
|
$icsContent = $calendarService->generateIcs($this->consultation);
|
|
|
|
// Send notification with .ics attachment
|
|
$this->consultation->user->notify(
|
|
new BookingApproved(
|
|
$this->consultation,
|
|
$icsContent,
|
|
$this->paymentInstructions
|
|
)
|
|
);
|
|
|
|
// Log action
|
|
AdminLog::create([
|
|
'admin_id' => auth()->id(),
|
|
'action_type' => 'approve',
|
|
'target_type' => 'consultation',
|
|
'target_id' => $this->consultation->id,
|
|
'old_values' => ['status' => 'pending'],
|
|
'new_values' => [
|
|
'status' => 'approved',
|
|
'type' => $this->consultationType,
|
|
'payment_amount' => $this->paymentAmount,
|
|
],
|
|
'ip_address' => request()->ip(),
|
|
]);
|
|
|
|
session()->flash('success', __('messages.booking_approved'));
|
|
$this->redirect(route('admin.bookings.pending'));
|
|
}
|
|
|
|
public function reject(): void
|
|
{
|
|
$this->validate([
|
|
'rejectionReason' => ['nullable', 'string', 'max:1000'],
|
|
]);
|
|
|
|
$this->consultation->update([
|
|
'status' => 'rejected',
|
|
]);
|
|
|
|
// Send rejection notification
|
|
$this->consultation->user->notify(
|
|
new BookingRejected($this->consultation, $this->rejectionReason)
|
|
);
|
|
|
|
// Log action
|
|
AdminLog::create([
|
|
'admin_id' => auth()->id(),
|
|
'action_type' => 'reject',
|
|
'target_type' => 'consultation',
|
|
'target_id' => $this->consultation->id,
|
|
'old_values' => ['status' => 'pending'],
|
|
'new_values' => [
|
|
'status' => 'rejected',
|
|
'reason' => $this->rejectionReason,
|
|
],
|
|
'ip_address' => request()->ip(),
|
|
]);
|
|
|
|
session()->flash('success', __('messages.booking_rejected'));
|
|
$this->redirect(route('admin.bookings.pending'));
|
|
}
|
|
};
|
|
```
|
|
|
|
### Blade Template for Approval Modal
|
|
```blade
|
|
<flux:modal wire:model="showApproveModal">
|
|
<flux:heading>{{ __('admin.approve_booking') }}</flux:heading>
|
|
|
|
<div class="space-y-4 mt-4">
|
|
<!-- Client Info -->
|
|
<div class="bg-cream p-3 rounded-lg">
|
|
<p><strong>{{ __('admin.client') }}:</strong> {{ $consultation->user->name }}</p>
|
|
<p><strong>{{ __('admin.date') }}:</strong>
|
|
{{ $consultation->scheduled_date->translatedFormat('l, d M Y') }}</p>
|
|
<p><strong>{{ __('admin.time') }}:</strong>
|
|
{{ Carbon::parse($consultation->scheduled_time)->format('g:i A') }}</p>
|
|
</div>
|
|
|
|
<!-- Consultation Type -->
|
|
<flux:field>
|
|
<flux:label>{{ __('admin.consultation_type') }}</flux:label>
|
|
<flux:radio.group wire:model.live="consultationType">
|
|
<flux:radio value="free" label="{{ __('admin.free_consultation') }}" />
|
|
<flux:radio value="paid" label="{{ __('admin.paid_consultation') }}" />
|
|
</flux:radio.group>
|
|
</flux:field>
|
|
|
|
<!-- Payment Amount (if paid) -->
|
|
@if($consultationType === 'paid')
|
|
<flux:field>
|
|
<flux:label>{{ __('admin.payment_amount') }} *</flux:label>
|
|
<flux:input
|
|
type="number"
|
|
wire:model="paymentAmount"
|
|
step="0.01"
|
|
min="0"
|
|
/>
|
|
<flux:error name="paymentAmount" />
|
|
</flux:field>
|
|
|
|
<flux:field>
|
|
<flux:label>{{ __('admin.payment_instructions') }}</flux:label>
|
|
<flux:textarea
|
|
wire:model="paymentInstructions"
|
|
rows="3"
|
|
placeholder="{{ __('admin.payment_instructions_placeholder') }}"
|
|
/>
|
|
</flux:field>
|
|
@endif
|
|
</div>
|
|
|
|
<div class="flex gap-3 mt-6">
|
|
<flux:button wire:click="$set('showApproveModal', false)">
|
|
{{ __('common.cancel') }}
|
|
</flux:button>
|
|
<flux:button variant="primary" wire:click="approve">
|
|
{{ __('admin.approve') }}
|
|
</flux:button>
|
|
</div>
|
|
</flux:modal>
|
|
```
|
|
|
|
### Pending Bookings List Component
|
|
```php
|
|
<?php
|
|
|
|
use App\Models\Consultation;
|
|
use Livewire\Volt\Component;
|
|
use Livewire\WithPagination;
|
|
|
|
new class extends Component {
|
|
use WithPagination;
|
|
|
|
public string $dateFrom = '';
|
|
public string $dateTo = '';
|
|
|
|
public function with(): array
|
|
{
|
|
return [
|
|
'bookings' => Consultation::where('status', 'pending')
|
|
->when($this->dateFrom, fn($q) => $q->where('scheduled_date', '>=', $this->dateFrom))
|
|
->when($this->dateTo, fn($q) => $q->where('scheduled_date', '<=', $this->dateTo))
|
|
->with('user')
|
|
->orderBy('scheduled_date')
|
|
->orderBy('scheduled_time')
|
|
->paginate(15),
|
|
];
|
|
}
|
|
|
|
public function quickApprove(int $id): void
|
|
{
|
|
$consultation = Consultation::findOrFail($id);
|
|
$consultation->update([
|
|
'status' => 'approved',
|
|
'type' => 'free',
|
|
'payment_status' => 'not_applicable',
|
|
]);
|
|
|
|
// Generate and send notification with .ics
|
|
// ...
|
|
|
|
session()->flash('success', __('messages.booking_approved'));
|
|
}
|
|
|
|
public function quickReject(int $id): void
|
|
{
|
|
$consultation = Consultation::findOrFail($id);
|
|
$consultation->update(['status' => 'rejected']);
|
|
|
|
// Send rejection notification
|
|
// ...
|
|
|
|
session()->flash('success', __('messages.booking_rejected'));
|
|
}
|
|
};
|
|
```
|
|
|
|
### Notification Classes
|
|
Create these notification classes in `app/Notifications/`:
|
|
|
|
```php
|
|
// app/Notifications/BookingApproved.php
|
|
namespace App\Notifications;
|
|
|
|
use App\Models\Consultation;
|
|
use Illuminate\Bus\Queueable;
|
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
use Illuminate\Notifications\Messages\MailMessage;
|
|
use Illuminate\Notifications\Notification;
|
|
|
|
class BookingApproved extends Notification implements ShouldQueue
|
|
{
|
|
use Queueable;
|
|
|
|
public function __construct(
|
|
public Consultation $consultation,
|
|
public string $icsContent,
|
|
public ?string $paymentInstructions = null
|
|
) {}
|
|
|
|
public function via(object $notifiable): array
|
|
{
|
|
return ['mail'];
|
|
}
|
|
|
|
public function toMail(object $notifiable): MailMessage
|
|
{
|
|
$locale = $notifiable->preferred_language ?? 'ar';
|
|
|
|
return (new MailMessage)
|
|
->subject($this->getSubject($locale))
|
|
->markdown('emails.booking.approved', [
|
|
'consultation' => $this->consultation,
|
|
'paymentInstructions' => $this->paymentInstructions,
|
|
'locale' => $locale,
|
|
])
|
|
->attachData(
|
|
$this->icsContent,
|
|
'consultation.ics',
|
|
['mime' => 'text/calendar']
|
|
);
|
|
}
|
|
|
|
private function getSubject(string $locale): string
|
|
{
|
|
return $locale === 'ar'
|
|
? 'تمت الموافقة على حجز استشارتك'
|
|
: 'Your Consultation Booking Approved';
|
|
}
|
|
}
|
|
```
|
|
|
|
```php
|
|
// app/Notifications/BookingRejected.php
|
|
namespace App\Notifications;
|
|
|
|
use App\Models\Consultation;
|
|
use Illuminate\Bus\Queueable;
|
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
use Illuminate\Notifications\Messages\MailMessage;
|
|
use Illuminate\Notifications\Notification;
|
|
|
|
class BookingRejected extends Notification implements ShouldQueue
|
|
{
|
|
use Queueable;
|
|
|
|
public function __construct(
|
|
public Consultation $consultation,
|
|
public ?string $rejectionReason = null
|
|
) {}
|
|
|
|
public function via(object $notifiable): array
|
|
{
|
|
return ['mail'];
|
|
}
|
|
|
|
public function toMail(object $notifiable): MailMessage
|
|
{
|
|
$locale = $notifiable->preferred_language ?? 'ar';
|
|
|
|
return (new MailMessage)
|
|
->subject($this->getSubject($locale))
|
|
->markdown('emails.booking.rejected', [
|
|
'consultation' => $this->consultation,
|
|
'rejectionReason' => $this->rejectionReason,
|
|
'locale' => $locale,
|
|
]);
|
|
}
|
|
|
|
private function getSubject(string $locale): string
|
|
{
|
|
return $locale === 'ar'
|
|
? 'بخصوص طلب الاستشارة الخاص بك'
|
|
: 'Regarding Your Consultation Request';
|
|
}
|
|
}
|
|
```
|
|
|
|
### Error Handling
|
|
```php
|
|
// In the approve() method, wrap calendar generation with error handling
|
|
public function approve(): void
|
|
{
|
|
// ... validation ...
|
|
|
|
try {
|
|
$calendarService = app(CalendarService::class);
|
|
$icsContent = $calendarService->generateIcs($this->consultation);
|
|
} catch (\Exception $e) {
|
|
// Log error but don't block approval
|
|
Log::error('Failed to generate calendar file', [
|
|
'consultation_id' => $this->consultation->id,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
$icsContent = null;
|
|
}
|
|
|
|
// Update consultation status regardless
|
|
$this->consultation->update([
|
|
'status' => 'approved',
|
|
'type' => $this->consultationType,
|
|
'payment_amount' => $this->consultationType === 'paid' ? $this->paymentAmount : null,
|
|
'payment_status' => $this->consultationType === 'paid' ? 'pending' : 'not_applicable',
|
|
]);
|
|
|
|
// Send notification (with or without .ics)
|
|
$this->consultation->user->notify(
|
|
new BookingApproved(
|
|
$this->consultation,
|
|
$icsContent ?? '',
|
|
$this->paymentInstructions
|
|
)
|
|
);
|
|
|
|
// ... audit log and redirect ...
|
|
}
|
|
```
|
|
|
|
### Edge Cases
|
|
- **Already approved booking:** The approve/reject buttons should only appear for `status = 'pending'`. Add guard clause:
|
|
```php
|
|
if ($this->consultation->status !== 'pending') {
|
|
session()->flash('error', __('messages.booking_already_processed'));
|
|
return;
|
|
}
|
|
```
|
|
- **Concurrent approval:** Use database transaction with locking to prevent race conditions
|
|
- **Missing user:** Check `$this->consultation->user` exists before sending notification
|
|
|
|
### Testing Examples
|
|
|
|
```php
|
|
use App\Models\Consultation;
|
|
use App\Models\User;
|
|
use App\Notifications\BookingApproved;
|
|
use App\Notifications\BookingRejected;
|
|
use App\Services\CalendarService;
|
|
use Illuminate\Support\Facades\Notification;
|
|
use Livewire\Volt\Volt;
|
|
|
|
// Test: Admin can view pending bookings list
|
|
it('displays pending bookings list for admin', function () {
|
|
$admin = User::factory()->admin()->create();
|
|
$consultations = Consultation::factory()->count(3)->pending()->create();
|
|
|
|
Volt::test('admin.bookings.pending-list')
|
|
->actingAs($admin)
|
|
->assertSee($consultations->first()->user->name)
|
|
->assertStatus(200);
|
|
});
|
|
|
|
// Test: Admin can approve booking as free consultation
|
|
it('can approve booking as free consultation', function () {
|
|
Notification::fake();
|
|
|
|
$admin = User::factory()->admin()->create();
|
|
$consultation = Consultation::factory()->pending()->create();
|
|
|
|
Volt::test('admin.bookings.review', ['consultation' => $consultation])
|
|
->actingAs($admin)
|
|
->set('consultationType', 'free')
|
|
->call('approve')
|
|
->assertHasNoErrors()
|
|
->assertRedirect(route('admin.bookings.pending'));
|
|
|
|
expect($consultation->fresh())
|
|
->status->toBe('approved')
|
|
->type->toBe('free')
|
|
->payment_status->toBe('not_applicable');
|
|
|
|
Notification::assertSentTo($consultation->user, BookingApproved::class);
|
|
});
|
|
|
|
// Test: Admin can approve booking as paid consultation with amount
|
|
it('can approve booking as paid consultation with amount', function () {
|
|
Notification::fake();
|
|
|
|
$admin = User::factory()->admin()->create();
|
|
$consultation = Consultation::factory()->pending()->create();
|
|
|
|
Volt::test('admin.bookings.review', ['consultation' => $consultation])
|
|
->actingAs($admin)
|
|
->set('consultationType', 'paid')
|
|
->set('paymentAmount', 150.00)
|
|
->set('paymentInstructions', 'Bank transfer to account XYZ')
|
|
->call('approve')
|
|
->assertHasNoErrors();
|
|
|
|
expect($consultation->fresh())
|
|
->status->toBe('approved')
|
|
->type->toBe('paid')
|
|
->payment_amount->toBe(150.00)
|
|
->payment_status->toBe('pending');
|
|
});
|
|
|
|
// Test: Paid consultation requires payment amount
|
|
it('requires payment amount for paid consultation', function () {
|
|
$admin = User::factory()->admin()->create();
|
|
$consultation = Consultation::factory()->pending()->create();
|
|
|
|
Volt::test('admin.bookings.review', ['consultation' => $consultation])
|
|
->actingAs($admin)
|
|
->set('consultationType', 'paid')
|
|
->set('paymentAmount', null)
|
|
->call('approve')
|
|
->assertHasErrors(['paymentAmount']);
|
|
});
|
|
|
|
// Test: Admin can reject booking with reason
|
|
it('can reject booking with optional reason', function () {
|
|
Notification::fake();
|
|
|
|
$admin = User::factory()->admin()->create();
|
|
$consultation = Consultation::factory()->pending()->create();
|
|
|
|
Volt::test('admin.bookings.review', ['consultation' => $consultation])
|
|
->actingAs($admin)
|
|
->set('rejectionReason', 'Schedule conflict')
|
|
->call('reject')
|
|
->assertHasNoErrors()
|
|
->assertRedirect(route('admin.bookings.pending'));
|
|
|
|
expect($consultation->fresh())->status->toBe('rejected');
|
|
|
|
Notification::assertSentTo($consultation->user, BookingRejected::class);
|
|
});
|
|
|
|
// Test: Quick approve from list
|
|
it('can quick approve booking from list', function () {
|
|
Notification::fake();
|
|
|
|
$admin = User::factory()->admin()->create();
|
|
$consultation = Consultation::factory()->pending()->create();
|
|
|
|
Volt::test('admin.bookings.pending-list')
|
|
->actingAs($admin)
|
|
->call('quickApprove', $consultation->id)
|
|
->assertHasNoErrors();
|
|
|
|
expect($consultation->fresh())
|
|
->status->toBe('approved')
|
|
->type->toBe('free');
|
|
});
|
|
|
|
// Test: Quick reject from list
|
|
it('can quick reject booking from list', function () {
|
|
Notification::fake();
|
|
|
|
$admin = User::factory()->admin()->create();
|
|
$consultation = Consultation::factory()->pending()->create();
|
|
|
|
Volt::test('admin.bookings.pending-list')
|
|
->actingAs($admin)
|
|
->call('quickReject', $consultation->id)
|
|
->assertHasNoErrors();
|
|
|
|
expect($consultation->fresh())->status->toBe('rejected');
|
|
});
|
|
|
|
// Test: Audit log entry created on approval
|
|
it('creates audit log entry on approval', function () {
|
|
Notification::fake();
|
|
|
|
$admin = User::factory()->admin()->create();
|
|
$consultation = Consultation::factory()->pending()->create();
|
|
|
|
Volt::test('admin.bookings.review', ['consultation' => $consultation])
|
|
->actingAs($admin)
|
|
->set('consultationType', 'free')
|
|
->call('approve');
|
|
|
|
$this->assertDatabaseHas('admin_logs', [
|
|
'admin_id' => $admin->id,
|
|
'action_type' => 'approve',
|
|
'target_type' => 'consultation',
|
|
'target_id' => $consultation->id,
|
|
]);
|
|
});
|
|
|
|
// Test: Cannot approve already processed booking
|
|
it('cannot approve already approved booking', function () {
|
|
$admin = User::factory()->admin()->create();
|
|
$consultation = Consultation::factory()->approved()->create();
|
|
|
|
Volt::test('admin.bookings.review', ['consultation' => $consultation])
|
|
->actingAs($admin)
|
|
->call('approve')
|
|
->assertHasErrors();
|
|
});
|
|
|
|
// Test: Filter bookings by date range
|
|
it('can filter pending bookings by date range', function () {
|
|
$admin = User::factory()->admin()->create();
|
|
|
|
$oldBooking = Consultation::factory()->pending()->create([
|
|
'scheduled_date' => now()->subDays(10),
|
|
]);
|
|
$newBooking = Consultation::factory()->pending()->create([
|
|
'scheduled_date' => now()->addDays(5),
|
|
]);
|
|
|
|
Volt::test('admin.bookings.pending-list')
|
|
->actingAs($admin)
|
|
->set('dateFrom', now()->toDateString())
|
|
->assertSee($newBooking->user->name)
|
|
->assertDontSee($oldBooking->user->name);
|
|
});
|
|
|
|
// Test: Bilingual notification (Arabic)
|
|
it('sends approval notification in client preferred language', function () {
|
|
Notification::fake();
|
|
|
|
$admin = User::factory()->admin()->create();
|
|
$arabicUser = User::factory()->create(['preferred_language' => 'ar']);
|
|
$consultation = Consultation::factory()->pending()->create(['user_id' => $arabicUser->id]);
|
|
|
|
Volt::test('admin.bookings.review', ['consultation' => $consultation])
|
|
->actingAs($admin)
|
|
->set('consultationType', 'free')
|
|
->call('approve');
|
|
|
|
Notification::assertSentTo($arabicUser, BookingApproved::class, function ($notification) {
|
|
return $notification->consultation->user->preferred_language === 'ar';
|
|
});
|
|
});
|
|
```
|
|
|
|
## Definition of Done
|
|
|
|
- [x] Pending bookings list displays correctly
|
|
- [x] Can view booking details
|
|
- [x] Can approve as free consultation
|
|
- [x] Can approve as paid with amount
|
|
- [x] Can reject with optional reason
|
|
- [x] Approval sends email with .ics file
|
|
- [x] Rejection sends email with reason
|
|
- [x] Quick actions work from list
|
|
- [x] Audit log entries created
|
|
- [x] Bilingual support complete
|
|
- [x] Tests for approval/rejection
|
|
- [x] Code formatted with Pint
|
|
|
|
## Dependencies
|
|
|
|
- **Story 3.4:** `docs/stories/story-3.4-booking-request-submission.md` - Creates pending bookings to review
|
|
- **Story 3.6:** `docs/stories/story-3.6-calendar-file-generation.md` - CalendarService for .ics generation
|
|
- **Epic 8:** `docs/epics/epic-8-email-notifications.md#story-84-booking-approved-email` - BookingApproved notification
|
|
- **Epic 8:** `docs/epics/epic-8-email-notifications.md#story-85-booking-rejected-email` - BookingRejected notification
|
|
|
|
## Risk Assessment
|
|
|
|
- **Primary Risk:** Approving wrong booking
|
|
- **Mitigation:** Confirmation dialog, clear booking details display
|
|
- **Rollback:** Admin can cancel approved booking
|
|
|
|
## Estimation
|
|
|
|
**Complexity:** Medium
|
|
**Estimated Effort:** 4-5 hours
|
|
|
|
---
|
|
|
|
## Dev Agent Record
|
|
|
|
### Status
|
|
Ready for Review
|
|
|
|
### Agent Model Used
|
|
Claude Opus 4.5
|
|
|
|
### File List
|
|
|
|
**New Files:**
|
|
- `resources/views/livewire/admin/bookings/pending.blade.php` - Pending bookings list component
|
|
- `resources/views/livewire/admin/bookings/review.blade.php` - Booking review/approval component
|
|
- `app/Services/CalendarService.php` - .ics calendar file generation service
|
|
- `app/Notifications/BookingApproved.php` - Booking approval notification
|
|
- `app/Notifications/BookingRejected.php` - Booking rejection notification
|
|
- `resources/views/emails/booking-approved.blade.php` - Approval email template
|
|
- `resources/views/emails/booking-rejected.blade.php` - Rejection email template
|
|
- `tests/Feature/Admin/BookingReviewApprovalTest.php` - Test suite for approval/rejection
|
|
|
|
**Modified Files:**
|
|
- `routes/web.php` - Added admin booking routes
|
|
- `resources/views/components/layouts/app/sidebar.blade.php` - Added navigation link
|
|
- `lang/en/navigation.php` - Added booking navigation translations
|
|
- `lang/ar/navigation.php` - Added booking navigation translations (Arabic)
|
|
- `lang/en/admin.php` - Added booking management translations
|
|
- `lang/ar/admin.php` - Added booking management translations (Arabic)
|
|
- `lang/en/emails.php` - Added approval/rejection email translations
|
|
- `lang/ar/emails.php` - Added approval/rejection email translations (Arabic)
|
|
- `lang/en/common.php` - Added common translations (clear, unknown, currency)
|
|
- `lang/ar/common.php` - Added common translations (Arabic)
|
|
- `composer.json` - Added spatie/icalendar-generator dependency
|
|
|
|
### Change Log
|
|
- Installed `spatie/icalendar-generator` package for .ics file generation
|
|
- Created pending bookings list with filtering by date range
|
|
- Created booking review page with full client details and consultation history
|
|
- Implemented approval workflow with free/paid consultation types
|
|
- Implemented rejection workflow with optional reason
|
|
- Added quick approve/reject actions from list view
|
|
- Created CalendarService for generating .ics calendar files
|
|
- Created BookingApproved notification with .ics attachment
|
|
- Created BookingRejected notification
|
|
- Added bilingual email templates (Arabic/English)
|
|
- Added comprehensive admin and navigation translations
|
|
- Added audit logging for all approval/rejection actions
|
|
- Created 21 tests covering all workflows
|
|
|
|
### Completion Notes
|
|
- All acceptance criteria completed except "Bulk actions" which was marked as optional
|
|
- Full test coverage with 21 passing tests
|
|
- All 312 project tests pass (748 assertions)
|
|
- Code formatted with Pint
|
|
|
|
---
|
|
|
|
## QA Results
|
|
|
|
### Review Date: 2025-12-26
|
|
|
|
### Reviewed By: Quinn (Test Architect)
|
|
|
|
### Code Quality Assessment
|
|
|
|
**Overall Assessment: HIGH QUALITY**
|
|
|
|
This implementation demonstrates excellent adherence to Laravel and project conventions. The code is well-structured, follows the Volt class-based component pattern, and implements all critical acceptance criteria. Key strengths include:
|
|
|
|
1. **Proper Status Guards**: Both components correctly check `status !== ConsultationStatus::Pending` before processing, preventing double-processing of bookings
|
|
2. **Error Handling**: Calendar generation failures are caught and logged but don't block the approval flow
|
|
3. **User Existence Checks**: Notifications only send if `$consultation->user` exists
|
|
4. **Comprehensive Audit Logging**: All actions are logged with old/new values and IP address
|
|
5. **Clean Separation of Concerns**: CalendarService handles ICS generation, Notifications handle email delivery
|
|
|
|
### Refactoring Performed
|
|
|
|
None required. The code quality is excellent and follows all project patterns.
|
|
|
|
### Compliance Check
|
|
|
|
- Coding Standards: ✓ Code formatted with Pint, follows Laravel 12 conventions
|
|
- Project Structure: ✓ Volt components in correct location, follows existing patterns
|
|
- Testing Strategy: ✓ 21 comprehensive tests covering all workflows
|
|
- All ACs Met: ✓ All acceptance criteria implemented (bulk actions marked optional in story)
|
|
|
|
### Requirements Traceability
|
|
|
|
| AC | Description | Test Coverage |
|
|
|----|-------------|---------------|
|
|
| 1 | View all pending booking requests | `pending bookings list displays pending consultations` |
|
|
| 2 | Display client name, date/time, submission date | `pending.blade.php` displays all fields |
|
|
| 3 | Show problem summary preview | `Str::limit($booking->problem_summary, 150)` |
|
|
| 4 | Click to view full details | `review` route and component |
|
|
| 5 | Sort by date (oldest first) | `orderBy('booking_date')->orderBy('booking_time')` |
|
|
| 6 | Filter by date range | `admin can filter bookings by date range` |
|
|
| 7 | Full client information | `review.blade.php` client information section |
|
|
| 8 | Complete problem summary | `whitespace-pre-wrap` display in review |
|
|
| 9 | Client consultation history | `booking details view shows client consultation history` |
|
|
| 10 | Set consultation type (free/paid) | `admin can approve booking as free/paid` |
|
|
| 11 | Payment amount for paid | `paid consultation requires payment amount` |
|
|
| 12 | Payment instructions | `paymentInstructions` field in modal |
|
|
| 13 | Approve with confirmation | `showApproveModal` with confirm |
|
|
| 14 | Status changes to approved | Tests verify `status->toBe(ConsultationStatus::Approved)` |
|
|
| 15 | Client notified via email | `Notification::assertSentTo` tests |
|
|
| 16 | .ics calendar attached | `CalendarService::generateIcs` + `attachData` |
|
|
| 17 | Payment instructions in email | Included in `BookingApproved` notification |
|
|
| 18 | Rejection reason field | `rejectionReason` input in modal |
|
|
| 19 | Reject with confirmation | `showRejectModal` with confirm |
|
|
| 20 | Rejection notification | `BookingRejected` notification sent |
|
|
| 21 | Quick approve button | `quickApprove` method tested |
|
|
| 22 | Quick reject button | `quickReject` method tested |
|
|
| 23 | Audit log for decisions | `audit log entry created on approval/rejection` tests |
|
|
| 24 | Bilingual notifications | `notification sent in client preferred language` tests |
|
|
|
|
### Improvements Checklist
|
|
|
|
- [x] Status guard prevents double-processing (already implemented)
|
|
- [x] Error handling for calendar generation (already implemented)
|
|
- [x] Null safety for user notifications (already implemented)
|
|
- [x] Audit logging with context (already implemented)
|
|
- [x] Bilingual support for emails (already implemented)
|
|
- [ ] Consider adding database transaction with locking for concurrent approval protection (future enhancement)
|
|
- [ ] Bulk actions (marked as optional in story - not implemented)
|
|
|
|
### Security Review
|
|
|
|
**Status: PASS**
|
|
|
|
1. **Authorization**: Routes are protected by `admin` middleware (verified in routes/web.php)
|
|
2. **Access Control Tests**: Tests verify guests and clients cannot access admin booking pages
|
|
3. **Input Validation**: Proper validation rules for consultation type and payment amount
|
|
4. **Audit Trail**: All admin actions logged with admin_id, action, target, and IP address
|
|
5. **Status Guards**: Cannot re-process already processed bookings
|
|
|
|
### Performance Considerations
|
|
|
|
**Status: PASS**
|
|
|
|
1. **Eager Loading**: `with('user')` used consistently to prevent N+1 queries
|
|
2. **Pagination**: List uses pagination (15 per page)
|
|
3. **Limited History**: Consultation history limited to 5 records
|
|
4. **Selective Fields**: User relation loads only needed fields: `id,full_name,email,phone,user_type`
|
|
5. **Queued Notifications**: Both `BookingApproved` and `BookingRejected` implement `ShouldQueue`
|
|
|
|
### Files Modified During Review
|
|
|
|
None - no modifications required.
|
|
|
|
### Gate Status
|
|
|
|
Gate: **PASS** → docs/qa/gates/3.5-admin-booking-review-approval.yml
|
|
|
|
### Recommended Status
|
|
|
|
✓ **Ready for Done**
|
|
|
|
The implementation is complete, well-tested, and follows all project conventions. All 21 tests pass with 47 assertions. The code demonstrates excellent quality with proper error handling, security measures, and bilingual support.
|
|
|
|
**Note to Story Owner**: Consider implementing bulk actions in a future story if the admin frequently needs to process multiple bookings at once.
|