libra/docs/stories/story-3.5-admin-booking-rev...

29 KiB

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

  • View all pending booking requests
  • Display: client name, requested date/time, submission date
  • Show problem summary preview
  • Click to view full details
  • Sort by date (oldest first default)
  • Filter by date range

Booking Details View

  • Full client information
  • Complete problem summary
  • Client consultation history
  • Requested date and time

Approval Workflow

  • Set consultation type:
    • Free consultation
    • Paid consultation
  • If paid: set payment amount
  • If paid: add payment instructions (optional)
  • Approve button with confirmation
  • On approval:
    • Status changes to 'approved'
    • Client notified via email
    • .ics calendar file attached to email
    • Payment instructions included if paid

Rejection Workflow

  • Optional rejection reason field
  • Reject button with confirmation
  • On rejection:
    • Status changes to 'rejected'
    • Client notified via email with reason

Quick Actions

  • Quick approve (free) button on list
  • Quick reject button on list
  • Bulk actions (optional)

Quality Requirements

  • Audit log for all decisions
  • Bilingual notifications
  • 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

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

<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

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/:

// 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';
    }
}
// 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

// 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:
    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

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

  • Pending bookings list displays correctly
  • Can view booking details
  • Can approve as free consultation
  • Can approve as paid with amount
  • Can reject with optional reason
  • Approval sends email with .ics file
  • Rejection sends email with reason
  • Quick actions work from list
  • Audit log entries created
  • Bilingual support complete
  • Tests for approval/rejection
  • 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

  • Status guard prevents double-processing (already implemented)
  • Error handling for calendar generation (already implemented)
  • Null safety for user notifications (already implemented)
  • Audit logging with context (already implemented)
  • 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

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.