-
{{ __('booking.client_email') }}
-
-
- {{ $consultation->getClientEmail() }}
-
-
+
+
+ {{ \Carbon\Carbon::parse($booking->booking_time)->format('g:i A') }}
-
-
{{ __('booking.client_phone') }}
-
-
- {{ $consultation->getClientPhone() }}
-
-
+
- @unless($consultation->isGuest())
-
-
{{ __('admin.client_type') }}
-
{{ $consultation->user->user_type->label() }}
+
+
+ {{ __('admin.submitted') }}: {{ $booking->created_at->translatedFormat('d M Y') }}
- @endunless
-
+
+
+
+ {{ Str::limit($booking->problem_summary, 150) }}
+
```
-### Step 9: Update Booking Approval Logic
-Update the approve method in the booking review component to send guest emails:
+Also update the `quickApprove` and `quickReject` methods to handle guest bookings:
+
+```php
+public function quickApprove(int $id): void
+{
+ $consultation = Consultation::with('user')->findOrFail($id);
+
+ // ... existing validation and update logic ...
+
+ // Send appropriate notification/email based on guest/client
+ if ($consultation->isGuest()) {
+ Mail::to($consultation->guest_email)->queue(
+ new \App\Mail\GuestBookingApprovedMail(
+ $consultation,
+ app()->getLocale()
+ )
+ );
+ } elseif ($consultation->user) {
+ $consultation->user->notify(
+ new BookingApproved($consultation, $icsContent ?? '', null)
+ );
+ }
+
+ // ... rest of logging ...
+}
+
+public function quickReject(int $id): void
+{
+ $consultation = Consultation::with('user')->findOrFail($id);
+
+ // ... existing validation and update logic ...
+
+ // Send appropriate notification/email based on guest/client
+ if ($consultation->isGuest()) {
+ Mail::to($consultation->guest_email)->queue(
+ new \App\Mail\GuestBookingRejectedMail(
+ $consultation,
+ app()->getLocale(),
+ null
+ )
+ );
+ } elseif ($consultation->user) {
+ $consultation->user->notify(
+ new BookingRejected($consultation, null)
+ );
+ }
+
+ // ... rest of logging ...
+}
+```
+
+Add the required imports at the top:
+```php
+use App\Mail\GuestBookingApprovedMail;
+use App\Mail\GuestBookingRejectedMail;
+use Illuminate\Support\Facades\Mail;
+```
+
+### Step 10: Update Admin Booking Review Page
+Update `resources/views/livewire/admin/bookings/review.blade.php`.
+
+Replace the "Client Information" section (around line 188-218):
+
+```blade
+
+
+
+ {{ __('admin.client_information') }}
+ @if($consultation->isGuest())
+ {{ __('admin.guest') }}
+ @endif
+
+
+
+
+
{{ __('admin.client_name') }}
+
+ {{ $consultation->getClientName() }}
+
+
+
+
+ @unless($consultation->isGuest())
+
+
{{ __('admin.client_type') }}
+
+ {{ $consultation->user?->user_type?->label() ?? '-' }}
+
+
+ @endunless
+
+
+```
+
+Update the `approve()` method in the PHP section:
```php
public function approve(): void
{
- // ... existing approval logic ...
+ if ($this->consultation->status !== ConsultationStatus::Pending) {
+ session()->flash('error', __('admin.booking_already_processed'));
+ $this->showApproveModal = false;
+ return;
+ }
- // Send appropriate email based on guest/client
+ $this->validate([
+ 'consultationType' => ['required', 'in:free,paid'],
+ 'paymentAmount' => ['required_if:consultationType,paid', 'nullable', 'numeric', 'min:0'],
+ 'paymentInstructions' => ['nullable', 'string', 'max:1000'],
+ ]);
+
+ $oldStatus = $this->consultation->status->value;
+ $type = $this->consultationType === 'paid' ? ConsultationType::Paid : ConsultationType::Free;
+
+ $this->consultation->update([
+ 'status' => ConsultationStatus::Approved,
+ 'consultation_type' => $type,
+ 'payment_amount' => $type === ConsultationType::Paid ? $this->paymentAmount : null,
+ 'payment_status' => $type === ConsultationType::Paid ? PaymentStatus::Pending : PaymentStatus::NotApplicable,
+ ]);
+
+ // Generate calendar file
+ $icsContent = null;
+ try {
+ $calendarService = app(CalendarService::class);
+ $icsContent = $calendarService->generateIcs($this->consultation);
+ } catch (\Exception $e) {
+ Log::error('Failed to generate calendar file', [
+ 'consultation_id' => $this->consultation->id,
+ 'error' => $e->getMessage(),
+ ]);
+ }
+
+ // Send appropriate notification/email based on guest/client
if ($this->consultation->isGuest()) {
Mail::to($this->consultation->guest_email)->queue(
- new GuestBookingApprovedMail($this->consultation)
+ new GuestBookingApprovedMail(
+ $this->consultation,
+ app()->getLocale(),
+ $this->paymentInstructions ?: null
+ )
);
- } else {
- Mail::to($this->consultation->user)->queue(
- new BookingApprovedMail($this->consultation)
+ } elseif ($this->consultation->user) {
+ $this->consultation->user->notify(
+ new BookingApproved(
+ $this->consultation,
+ $icsContent ?? '',
+ $this->paymentInstructions ?: null
+ )
);
}
-}
+ // Log action
+ AdminLog::create([
+ 'admin_id' => auth()->id(),
+ 'action' => 'approve',
+ 'target_type' => 'consultation',
+ 'target_id' => $this->consultation->id,
+ 'old_values' => ['status' => $oldStatus],
+ 'new_values' => [
+ 'status' => ConsultationStatus::Approved->value,
+ 'consultation_type' => $type->value,
+ 'payment_amount' => $this->paymentAmount,
+ ],
+ 'ip_address' => request()->ip(),
+ 'created_at' => now(),
+ ]);
+
+ session()->flash('success', __('admin.booking_approved'));
+ $this->redirect(route('admin.bookings.pending'), navigate: true);
+}
+```
+
+Update the `reject()` method similarly:
+
+```php
public function reject(): void
{
- // ... existing rejection logic ...
+ if ($this->consultation->status !== ConsultationStatus::Pending) {
+ session()->flash('error', __('admin.booking_already_processed'));
+ $this->showRejectModal = false;
+ return;
+ }
- // Send appropriate email based on guest/client
+ $this->validate([
+ 'rejectionReason' => ['nullable', 'string', 'max:1000'],
+ ]);
+
+ $oldStatus = $this->consultation->status->value;
+
+ $this->consultation->update([
+ 'status' => ConsultationStatus::Rejected,
+ ]);
+
+ // Send appropriate notification/email based on guest/client
if ($this->consultation->isGuest()) {
Mail::to($this->consultation->guest_email)->queue(
- new GuestBookingRejectedMail($this->consultation, $this->rejectionReason)
+ new GuestBookingRejectedMail(
+ $this->consultation,
+ app()->getLocale(),
+ $this->rejectionReason ?: null
+ )
);
- } else {
- Mail::to($this->consultation->user)->queue(
- new BookingRejectedMail($this->consultation, $this->rejectionReason)
+ } elseif ($this->consultation->user) {
+ $this->consultation->user->notify(
+ new BookingRejected($this->consultation, $this->rejectionReason ?: null)
);
}
+
+ // Log action
+ AdminLog::create([
+ 'admin_id' => auth()->id(),
+ 'action' => 'reject',
+ 'target_type' => 'consultation',
+ 'target_id' => $this->consultation->id,
+ 'old_values' => ['status' => $oldStatus],
+ 'new_values' => [
+ 'status' => ConsultationStatus::Rejected->value,
+ 'reason' => $this->rejectionReason,
+ ],
+ 'ip_address' => request()->ip(),
+ 'created_at' => now(),
+ ]);
+
+ session()->flash('success', __('admin.booking_rejected'));
+ $this->redirect(route('admin.bookings.pending'), navigate: true);
}
```
-### Step 10: Add Translation Keys
-Add to `lang/en/emails.php`:
+Add required imports at the top of the PHP section:
```php
-'guest_booking_submitted_subject' => 'Booking Request Received - Libra Law Firm',
-'guest_booking_submitted_title' => 'Your Booking Request Has Been Received',
-'guest_booking_submitted_greeting' => 'Dear :name,',
-'guest_booking_submitted_body' => 'Thank you for your consultation request. We have received your booking and our team will review it shortly.',
-'guest_booking_submitted_next_steps' => 'You will receive another email once your booking has been reviewed. If approved, you will receive the consultation details and a calendar invitation.',
-'guest_booking_approved_subject' => 'Booking Confirmed - Libra Law Firm',
-'guest_booking_rejected_subject' => 'Booking Update - Libra Law Firm',
-'booking_type' => 'Booking Type',
-'guest_booking' => 'Guest (No Account)',
+use App\Mail\GuestBookingApprovedMail;
+use App\Mail\GuestBookingRejectedMail;
+use Illuminate\Support\Facades\Mail;
```
-Add to `lang/ar/emails.php`:
+Update the `with()` method to handle guest consultations (they have no history):
```php
-'guest_booking_submitted_subject' => 'تم استلام طلب الحجز - مكتب ليبرا للمحاماة',
-'guest_booking_submitted_title' => 'تم استلام طلب الحجز الخاص بك',
-'guest_booking_submitted_greeting' => 'عزيزي/عزيزتي :name،',
-'guest_booking_submitted_body' => 'شكراً لطلب الاستشارة. لقد تلقينا حجزك وسيقوم فريقنا بمراجعته قريباً.',
-'guest_booking_submitted_next_steps' => 'ستتلقى رسالة أخرى عند مراجعة حجزك. في حال الموافقة، ستتلقى تفاصيل الاستشارة ودعوة تقويم.',
-'guest_booking_approved_subject' => 'تأكيد الحجز - مكتب ليبرا للمحاماة',
-'guest_booking_rejected_subject' => 'تحديث الحجز - مكتب ليبرا للمحاماة',
-'booking_type' => 'نوع الحجز',
-'guest_booking' => 'زائر (بدون حساب)',
+public function with(): array
+{
+ // Guest bookings don't have consultation history
+ if ($this->consultation->isGuest()) {
+ return ['consultationHistory' => collect()];
+ }
+
+ return [
+ 'consultationHistory' => Consultation::query()
+ ->where('user_id', $this->consultation->user_id)
+ ->where('id', '!=', $this->consultation->id)
+ ->orderBy('booking_date', 'desc')
+ ->limit(5)
+ ->get(),
+ ];
+}
```
+Update the modal summary sections to use helper methods:
+
+In the Approve Modal (around line 314):
+```blade
+
+
{{ __('admin.client') }}: {{ $consultation->getClientName() }}
+
{{ __('admin.date') }}: {{ \Carbon\Carbon::parse($consultation->booking_date)->translatedFormat('l, d M Y') }}
+
{{ __('admin.time') }}: {{ \Carbon\Carbon::parse($consultation->booking_time)->format('g:i A') }}
+
+```
+
+In the Reject Modal (around line 369):
+```blade
+
+
{{ __('admin.client') }}: {{ $consultation->getClientName() }}
+
{{ __('admin.date') }}: {{ \Carbon\Carbon::parse($consultation->booking_date)->translatedFormat('l, d M Y') }}
+
{{ __('admin.time') }}: {{ \Carbon\Carbon::parse($consultation->booking_time)->format('g:i A') }}
+
+```
+
+### Step 11: Add Translation Keys
Add to `lang/en/admin.php`:
```php
'guest' => 'Guest',
@@ -420,7 +870,20 @@ Add to `lang/ar/admin.php`:
## Testing Requirements
### Email Tests
+Create `tests/Feature/GuestEmailNotificationTest.php`:
+
```php
+guest_email)->send(
- new GuestBookingSubmittedMail($consultation)
+ new GuestBookingSubmittedMail($consultation, 'en')
);
Mail::assertSent(GuestBookingSubmittedMail::class, function ($mail) use ($consultation) {
@@ -445,7 +908,7 @@ test('guest receives approval email with calendar attachment', function () {
]);
Mail::to($consultation->guest_email)->send(
- new GuestBookingApprovedMail($consultation)
+ new GuestBookingApprovedMail($consultation, 'en')
);
Mail::assertSent(GuestBookingApprovedMail::class, function ($mail) {
@@ -453,6 +916,22 @@ test('guest receives approval email with calendar attachment', function () {
});
});
+test('guest receives rejection email', function () {
+ Mail::fake();
+
+ $consultation = Consultation::factory()->guest()->create([
+ 'status' => ConsultationStatus::Rejected,
+ ]);
+
+ Mail::to($consultation->guest_email)->send(
+ new GuestBookingRejectedMail($consultation, 'en', 'Not available')
+ );
+
+ Mail::assertSent(GuestBookingRejectedMail::class, function ($mail) use ($consultation) {
+ return $mail->hasTo($consultation->guest_email);
+ });
+});
+
test('admin email shows guest indicator for guest bookings', function () {
Mail::fake();
@@ -466,74 +945,153 @@ test('admin email shows guest indicator for guest bookings', function () {
```
### Admin Interface Tests
+Create `tests/Feature/Admin/GuestBookingManagementTest.php`:
+
```php
+admin()->create();
$guestConsultation = Consultation::factory()->guest()->pending()->create();
+ $this->actingAs($admin);
+
Volt::test('admin.bookings.pending')
- ->actingAs($admin)
->assertSee($guestConsultation->guest_name)
->assertSee(__('admin.guest'));
});
-test('admin can view guest booking details', function () {
+test('admin can view guest booking details in review page', function () {
$admin = User::factory()->admin()->create();
- $consultation = Consultation::factory()->guest()->create([
+ $consultation = Consultation::factory()->guest()->pending()->create([
'guest_name' => 'Test Guest',
'guest_email' => 'test@example.com',
]);
+ $this->actingAs($admin);
+
Volt::test('admin.bookings.review', ['consultation' => $consultation])
- ->actingAs($admin)
->assertSee('Test Guest')
->assertSee('test@example.com')
->assertSee(__('admin.guest'));
});
-test('admin can approve guest booking', function () {
+test('admin can approve guest booking via modal', function () {
Mail::fake();
$admin = User::factory()->admin()->create();
$consultation = Consultation::factory()->guest()->pending()->create();
+ $this->actingAs($admin);
+
Volt::test('admin.bookings.review', ['consultation' => $consultation])
- ->actingAs($admin)
+ ->call('openApproveModal')
+ ->assertSet('showApproveModal', true)
+ ->set('consultationType', 'free')
->call('approve');
expect($consultation->fresh()->status)->toBe(ConsultationStatus::Approved);
Mail::assertQueued(GuestBookingApprovedMail::class);
});
-test('admin can reject guest booking', function () {
+test('admin can reject guest booking via modal', function () {
Mail::fake();
$admin = User::factory()->admin()->create();
$consultation = Consultation::factory()->guest()->pending()->create();
+ $this->actingAs($admin);
+
Volt::test('admin.bookings.review', ['consultation' => $consultation])
- ->actingAs($admin)
+ ->call('openRejectModal')
+ ->assertSet('showRejectModal', true)
->set('rejectionReason', 'Not available for this time')
->call('reject');
expect($consultation->fresh()->status)->toBe(ConsultationStatus::Rejected);
Mail::assertQueued(GuestBookingRejectedMail::class);
});
+
+test('admin can quick approve guest booking from pending list', function () {
+ Mail::fake();
+
+ $admin = User::factory()->admin()->create();
+ $consultation = Consultation::factory()->guest()->pending()->create();
+
+ $this->actingAs($admin);
+
+ Volt::test('admin.bookings.pending')
+ ->call('quickApprove', $consultation->id);
+
+ expect($consultation->fresh()->status)->toBe(ConsultationStatus::Approved);
+ Mail::assertQueued(GuestBookingApprovedMail::class);
+});
+
+test('admin can quick reject guest booking from pending list', function () {
+ Mail::fake();
+
+ $admin = User::factory()->admin()->create();
+ $consultation = Consultation::factory()->guest()->pending()->create();
+
+ $this->actingAs($admin);
+
+ Volt::test('admin.bookings.pending')
+ ->call('quickReject', $consultation->id);
+
+ expect($consultation->fresh()->status)->toBe(ConsultationStatus::Rejected);
+ Mail::assertQueued(GuestBookingRejectedMail::class);
+});
+
+test('guest booking review shows no consultation history', function () {
+ $admin = User::factory()->admin()->create();
+ $consultation = Consultation::factory()->guest()->pending()->create();
+
+ $this->actingAs($admin);
+
+ Volt::test('admin.bookings.review', ['consultation' => $consultation])
+ ->assertDontSee(__('admin.consultation_history'));
+});
+
+test('client booking still uses notification system', function () {
+ Notification::fake();
+
+ $admin = User::factory()->admin()->create();
+ $client = User::factory()->client()->create();
+ $consultation = Consultation::factory()->pending()->create(['user_id' => $client->id]);
+
+ $this->actingAs($admin);
+
+ Volt::test('admin.bookings.review', ['consultation' => $consultation])
+ ->call('openApproveModal')
+ ->set('consultationType', 'free')
+ ->call('approve');
+
+ Notification::assertSentTo($client, BookingApproved::class);
+});
```
## Dependencies
-- Story 11.1 (Database Schema & Model Updates)
-- Story 11.2 (Public Booking Form)
+- Story 11.1 (Database Schema & Model Updates) - provides `isGuest()`, `getClientName()`, `getClientEmail()`, `getClientPhone()` methods
+- Story 11.2 (Public Booking Form) - uses the guest email classes created in this story
## Definition of Done
-- [ ] Guest booking submitted email created and working
-- [ ] Guest booking approved email created with calendar attachment
-- [ ] Guest booking rejected email created
-- [ ] Admin new booking email updated for guests
-- [ ] Admin pending bookings shows guest indicator
-- [ ] Admin booking review shows guest contact info
-- [ ] Approve/reject sends correct email (guest vs client)
+- [ ] Guest booking submitted email created and working (en/ar templates)
+- [ ] Guest booking approved email created with calendar attachment (en/ar templates)
+- [ ] Guest booking rejected email created (en/ar templates)
+- [ ] Admin new booking email updated for guests (shows guest indicator)
+- [ ] Admin pending bookings shows guest badge and uses helper methods
+- [ ] Admin booking review shows guest contact info with mailto/tel links
+- [ ] Quick approve/reject sends correct email (guest Mail vs client Notification)
+- [ ] Modal approve/reject sends correct email (guest Mail vs client Notification)
- [ ] All translations in place (Arabic/English)
- [ ] All email tests pass
- [ ] All admin interface tests pass
-- [ ] Existing client booking emails unchanged
+- [ ] Existing client booking notifications unchanged