# Story 11.3: Guest Email Notifications & Admin Integration
## Epic Reference
**Epic 11:** Guest Booking
## Story Context
This story completes the guest booking workflow by implementing email notifications for guests and updating the admin interface to properly display and manage guest bookings alongside client bookings.
## User Story
As a **guest** who submitted a booking,
I want **to receive email confirmations about my booking status**,
So that **I know my request was received and can track its progress**.
As an **admin**,
I want **to see guest contact information when reviewing bookings**,
So that **I can contact them and manage their appointments**.
## Acceptance Criteria
### Guest Email Notifications
- [ ] Guest receives confirmation email when booking submitted
- [ ] Guest receives approval email when booking approved (with date/time details)
- [ ] Guest receives rejection email when booking rejected
- [ ] All emails use existing email template/branding
- [ ] Emails sent to guest_email address
- [ ] Bilingual support based on site locale at submission time
### Admin Pending Bookings View
- [ ] Guest bookings appear in pending list alongside client bookings
- [ ] Guest bookings show "Guest" badge/indicator
- [ ] Guest name, email, phone displayed in list
- [ ] Click through to booking review shows full guest details
### Admin Booking Review Page
- [ ] Guest contact info displayed prominently
- [ ] Guest name shown instead of user name
- [ ] Guest email shown with mailto link
- [ ] Guest phone shown with tel link
- [ ] Approve/reject workflow works for guest bookings
- [ ] Email notifications sent to guest on status change
### Existing Admin Email
- [ ] `NewBookingAdminEmail` updated to handle guest bookings
- [ ] Admin email shows guest contact info for guest bookings
- [ ] Admin email shows client info for client bookings
## Technical Context
### Existing Email Architecture
The current codebase uses two approaches for sending emails:
1. **Notifications** (`app/Notifications/`) - Used for client bookings via `$user->notify()`
2. **Mailables** (`app/Mail/`) - Used for direct email sending via `Mail::to()`
For **guest bookings**, we use Mailables (not Notifications) because guests don't have User accounts.
### Existing Admin Email Structure
The `NewBookingAdminEmail` uses locale-specific markdown templates:
- `resources/views/emails/admin/new-booking/en.blade.php`
- `resources/views/emails/admin/new-booking/ar.blade.php`
### Existing Admin Components
- `pending.blade.php` uses `$booking` variable (not `$consultation`)
- `review.blade.php` uses modal-based approve/reject flow (`openApproveModal()` -> `approve()`)
- Client notifications use `BookingApproved` and `BookingRejected` Notifications
## Implementation Steps
### Step 1: Create Guest Booking Submitted Email
Create `app/Mail/GuestBookingSubmittedMail.php`:
```php
locale === 'ar'
? 'تم استلام طلب الحجز - مكتب ليبرا للمحاماة'
: 'Booking Request Received - Libra Law Firm',
);
}
public function content(): Content
{
return new Content(
markdown: 'emails.guest.booking-submitted.' . $this->locale,
with: [
'consultation' => $this->consultation,
'guestName' => $this->consultation->guest_name,
'formattedDate' => $this->getFormattedDate(),
'formattedTime' => $this->getFormattedTime(),
],
);
}
private function getFormattedDate(): string
{
$date = $this->consultation->booking_date;
return $this->locale === 'ar'
? $date->format('d/m/Y')
: $date->format('m/d/Y');
}
private function getFormattedTime(): string
{
return Carbon::parse($this->consultation->booking_time)->format('h:i A');
}
}
```
### Step 2: Create Guest Booking Submitted Email Templates
Create `resources/views/emails/guest/booking-submitted/en.blade.php`:
```blade
# Your Booking Request Has Been Received
Dear {{ $guestName }},
Thank you for your consultation request. We have received your booking and our team will review it shortly.
**Requested Date:** {{ $formattedDate }}
**Requested Time:** {{ $formattedTime }}
**Duration:** 45 minutes
You will receive another email once your booking has been reviewed. If approved, you will receive the consultation details and a calendar invitation.
Thanks,
{{ config('app.name') }}
```
Create `resources/views/emails/guest/booking-submitted/ar.blade.php`:
```blade
# تم استلام طلب الحجز الخاص بك
عزيزي/عزيزتي {{ $guestName }}،
شكراً لطلب الاستشارة. لقد تلقينا حجزك وسيقوم فريقنا بمراجعته قريباً.
**التاريخ المطلوب:** {{ $formattedDate }}
**الوقت المطلوب:** {{ $formattedTime }}
**المدة:** 45 دقيقة
ستتلقى رسالة أخرى عند مراجعة حجزك. في حال الموافقة، ستتلقى تفاصيل الاستشارة ودعوة تقويم.
شكراً،
{{ config('app.name') }}
```
### Step 3: Create Guest Booking Approved Email
Create `app/Mail/GuestBookingApprovedMail.php`:
```php
locale === 'ar'
? 'تأكيد الحجز - مكتب ليبرا للمحاماة'
: 'Booking Confirmed - Libra Law Firm',
);
}
public function content(): Content
{
return new Content(
markdown: 'emails.guest.booking-approved.' . $this->locale,
with: [
'consultation' => $this->consultation,
'guestName' => $this->consultation->guest_name,
'formattedDate' => $this->getFormattedDate(),
'formattedTime' => $this->getFormattedTime(),
'isPaid' => $this->consultation->consultation_type?->value === 'paid',
'paymentAmount' => $this->consultation->payment_amount,
'paymentInstructions' => $this->paymentInstructions,
],
);
}
public function attachments(): array
{
try {
$calendarService = app(CalendarService::class);
$icsContent = $calendarService->generateIcs($this->consultation);
return [
Attachment::fromData(fn () => $icsContent, 'consultation.ics')
->withMime('text/calendar'),
];
} catch (\Exception $e) {
Log::error('Failed to generate calendar attachment for guest email', [
'consultation_id' => $this->consultation->id,
'error' => $e->getMessage(),
]);
return [];
}
}
private function getFormattedDate(): string
{
$date = $this->consultation->booking_date;
return $this->locale === 'ar'
? $date->format('d/m/Y')
: $date->format('m/d/Y');
}
private function getFormattedTime(): string
{
return Carbon::parse($this->consultation->booking_time)->format('h:i A');
}
}
```
### Step 4: Create Guest Booking Approved Email Templates
Create `resources/views/emails/guest/booking-approved/en.blade.php`:
```blade
# Your Booking Has Been Confirmed
Dear {{ $guestName }},
Great news! Your consultation request has been approved.
**Date:** {{ $formattedDate }}
**Time:** {{ $formattedTime }}
**Duration:** 45 minutes
**Type:** {{ $isPaid ? 'Paid Consultation' : 'Free Consultation' }}
@if($isPaid && $paymentAmount)
**Amount Due:** {{ number_format($paymentAmount, 2) }} ILS
@if($paymentInstructions)
**Payment Instructions:**
{{ $paymentInstructions }}
@endif
@endif
A calendar invitation (.ics file) is attached to this email. You can add it to your calendar application.
We look forward to meeting with you.
Thanks,
{{ config('app.name') }}
```
Create `resources/views/emails/guest/booking-approved/ar.blade.php`:
```blade
# تم تأكيد حجزك
عزيزي/عزيزتي {{ $guestName }}،
أخبار سارة! تمت الموافقة على طلب الاستشارة الخاص بك.
**التاريخ:** {{ $formattedDate }}
**الوقت:** {{ $formattedTime }}
**المدة:** 45 دقيقة
**النوع:** {{ $isPaid ? 'استشارة مدفوعة' : 'استشارة مجانية' }}
@if($isPaid && $paymentAmount)
**المبلغ المستحق:** {{ number_format($paymentAmount, 2) }} شيكل
@if($paymentInstructions)
**تعليمات الدفع:**
{{ $paymentInstructions }}
@endif
@endif
مرفق بهذه الرسالة ملف دعوة تقويم (.ics). يمكنك إضافته إلى تطبيق التقويم الخاص بك.
نتطلع للقائك.
شكراً،
{{ config('app.name') }}
```
### Step 5: Create Guest Booking Rejected Email
Create `app/Mail/GuestBookingRejectedMail.php`:
```php
locale === 'ar'
? 'تحديث الحجز - مكتب ليبرا للمحاماة'
: 'Booking Update - Libra Law Firm',
);
}
public function content(): Content
{
return new Content(
markdown: 'emails.guest.booking-rejected.' . $this->locale,
with: [
'consultation' => $this->consultation,
'guestName' => $this->consultation->guest_name,
'formattedDate' => $this->getFormattedDate(),
'formattedTime' => $this->getFormattedTime(),
'reason' => $this->reason,
'hasReason' => !empty($this->reason),
],
);
}
private function getFormattedDate(): string
{
$date = $this->consultation->booking_date;
return $this->locale === 'ar'
? $date->format('d/m/Y')
: $date->format('m/d/Y');
}
private function getFormattedTime(): string
{
return Carbon::parse($this->consultation->booking_time)->format('h:i A');
}
}
```
### Step 6: Create Guest Booking Rejected Email Templates
Create `resources/views/emails/guest/booking-rejected/en.blade.php`:
```blade
# Booking Update
Dear {{ $guestName }},
We regret to inform you that we are unable to accommodate your consultation request for the following time:
**Requested Date:** {{ $formattedDate }}
**Requested Time:** {{ $formattedTime }}
@if($hasReason)
**Reason:** {{ $reason }}
@endif
We apologize for any inconvenience. Please feel free to submit a new booking request for a different time slot.
Thanks,
{{ config('app.name') }}
```
Create `resources/views/emails/guest/booking-rejected/ar.blade.php`:
```blade
# تحديث الحجز
عزيزي/عزيزتي {{ $guestName }}،
نأسف لإبلاغك بأننا غير قادرين على استيعاب طلب الاستشارة الخاص بك في الوقت التالي:
**التاريخ المطلوب:** {{ $formattedDate }}
**الوقت المطلوب:** {{ $formattedTime }}
@if($hasReason)
**السبب:** {{ $reason }}
@endif
نعتذر عن أي إزعاج. لا تتردد في تقديم طلب حجز جديد لفترة زمنية مختلفة.
شكراً،
{{ config('app.name') }}
```
### Step 7: Update NewBookingAdminEmail
Update `app/Mail/NewBookingAdminEmail.php` to handle guest bookings.
Modify the `content()` method to use the Consultation model's helper methods:
```php
public function content(): Content
{
$admin = $this->getAdminUser();
$locale = $admin?->preferred_language ?? 'en';
return new Content(
markdown: 'emails.admin.new-booking.'.$locale,
with: [
'consultation' => $this->consultation,
'client' => $this->consultation->user,
'clientName' => $this->consultation->getClientName(),
'clientEmail' => $this->consultation->getClientEmail(),
'clientPhone' => $this->consultation->getClientPhone(),
'isGuest' => $this->consultation->isGuest(),
'formattedDate' => $this->getFormattedDate($locale),
'formattedTime' => $this->getFormattedTime(),
'reviewUrl' => $this->getReviewUrl(),
],
);
}
```
### Step 8: Update Admin New Booking Email Templates
Update `resources/views/emails/admin/new-booking/en.blade.php` to show guest indicator:
Add after the title section:
```blade
@if($isGuest)
**Booking Type:** Guest (No Account)
@endif
**Client Name:** {{ $clientName }}
**Client Email:** {{ $clientEmail }}
**Client Phone:** {{ $clientPhone ?? 'Not provided' }}
```
Update `resources/views/emails/admin/new-booking/ar.blade.php` similarly:
```blade
@if($isGuest)
**نوع الحجز:** زائر (بدون حساب)
@endif
**اسم العميل:** {{ $clientName }}
**بريد العميل:** {{ $clientEmail }}
**هاتف العميل:** {{ $clientPhone ?? 'غير متوفر' }}
```
### Step 9: Update Admin Pending Bookings List
Update `resources/views/livewire/admin/bookings/pending.blade.php`.
In the booking info section (around line 194), replace the client name display:
```blade
@if($booking->isGuest())
{{ __('admin.guest') }}
@endif
{{ $booking->getClientName() }}
{{ $booking->status->label() }}
{{ \Carbon\Carbon::parse($booking->booking_date)->translatedFormat('l, d M Y') }}
{{ \Carbon\Carbon::parse($booking->booking_time)->format('g:i A') }}
{{ __('admin.submitted') }}: {{ $booking->created_at->translatedFormat('d M Y') }}
{{ Str::limit($booking->problem_summary, 150) }}
```
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
{
if ($this->consultation->status !== ConsultationStatus::Pending) {
session()->flash('error', __('admin.booking_already_processed'));
$this->showApproveModal = false;
return;
}
$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,
app()->getLocale(),
$this->paymentInstructions ?: null
)
);
} 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
{
if ($this->consultation->status !== ConsultationStatus::Pending) {
session()->flash('error', __('admin.booking_already_processed'));
$this->showRejectModal = false;
return;
}
$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,
app()->getLocale(),
$this->rejectionReason ?: null
)
);
} 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);
}
```
Add required imports at the top of the PHP section:
```php
use App\Mail\GuestBookingApprovedMail;
use App\Mail\GuestBookingRejectedMail;
use Illuminate\Support\Facades\Mail;
```
Update the `with()` method to handle guest consultations (they have no history):
```php
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',
'client_information' => 'Client Information',
'client_type' => 'Client Type',
```
Add to `lang/ar/admin.php`:
```php
'guest' => 'زائر',
'client_information' => 'معلومات العميل',
'client_type' => 'نوع العميل',
```
## Testing Requirements
### Email Tests
Create `tests/Feature/GuestEmailNotificationTest.php`:
```php
guest()->create([
'status' => ConsultationStatus::Pending,
]);
Mail::to($consultation->guest_email)->send(
new GuestBookingSubmittedMail($consultation, 'en')
);
Mail::assertSent(GuestBookingSubmittedMail::class, function ($mail) use ($consultation) {
return $mail->hasTo($consultation->guest_email);
});
});
test('guest receives approval email with calendar attachment', function () {
Mail::fake();
$consultation = Consultation::factory()->guest()->create([
'status' => ConsultationStatus::Approved,
]);
Mail::to($consultation->guest_email)->send(
new GuestBookingApprovedMail($consultation, 'en')
);
Mail::assertSent(GuestBookingApprovedMail::class, function ($mail) {
return count($mail->attachments()) === 1;
});
});
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();
$consultation = Consultation::factory()->guest()->create();
$admin = User::factory()->admin()->create();
Mail::to($admin)->send(new NewBookingAdminEmail($consultation));
Mail::assertSent(NewBookingAdminEmail::class);
});
```
### 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')
->assertSee($guestConsultation->guest_name)
->assertSee(__('admin.guest'));
});
test('admin can view guest booking details in review page', function () {
$admin = User::factory()->admin()->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])
->assertSee('Test Guest')
->assertSee('test@example.com')
->assertSee(__('admin.guest'));
});
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])
->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 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])
->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) - 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 (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 notifications unchanged