libra/docs/stories/story-7.6-booking-limit-ind...

13 KiB

Story 7.6: Booking Limit Indicator

Epic Reference

Epic 7: Client Dashboard

Dependencies

  • Story 7.5: New Booking Interface (provides booking page where indicator displays)
  • Story 3.3: Availability Calendar (calendar component to integrate with)

User Story

As a client, I want to see my booking status and limits clearly, So that I understand when I can book consultations.

Acceptance Criteria

Display Locations

  • Dashboard widget showing booking status
  • Booking page status banner

Status Messages

  • "You can book a consultation today" (when no booking exists for today)
  • "You already have a booking for today" (when pending/approved booking exists for today)
  • "You have a pending request for [date]" (shows first pending request date)

Calendar Integration

  • Pass bookedDates array to availability calendar component
  • Calendar marks user's booked dates as unavailable (distinct styling)
  • Visual indicator differentiates "user already booked" from "no slots available"

Information

  • Clear messaging about 1-per-day limit
  • Bilingual messages (Arabic/English)

Edge Cases

  • Handle multiple pending requests (show count or list)
  • Handle cancelled bookings (should not block new booking)
  • Loading state while fetching booking status

Technical Notes

Files to Create

  • resources/views/livewire/client/booking-status.blade.php - Reusable status component

Files to Modify

  • resources/views/livewire/client/dashboard.blade.php - Add booking status widget
  • resources/views/livewire/client/booking.blade.php - Add status banner (from Story 7.5)
  • resources/lang/en/booking.php - Add translation keys
  • resources/lang/ar/booking.php - Add translation keys

Component Implementation

<?php

use Livewire\Volt\Component;

new class extends Component {
    public function getBookingStatus(): array
    {
        $user = auth()->user();

        $todayBooking = $user->consultations()
            ->whereDate('scheduled_date', today())
            ->whereIn('status', ['pending', 'approved'])
            ->first();

        $pendingRequests = $user->consultations()
            ->where('status', 'pending')
            ->where('scheduled_date', '>=', today())
            ->orderBy('scheduled_date')
            ->get();

        $upcomingApproved = $user->consultations()
            ->where('status', 'approved')
            ->where('scheduled_date', '>=', today())
            ->get();

        return [
            'canBookToday' => is_null($todayBooking),
            'todayBooking' => $todayBooking,
            'pendingRequests' => $pendingRequests,
            'upcomingApproved' => $upcomingApproved,
            'bookedDates' => $user->consultations()
                ->whereIn('status', ['pending', 'approved'])
                ->where('scheduled_date', '>=', today())
                ->pluck('scheduled_date')
                ->map(fn($d) => $d->format('Y-m-d'))
                ->toArray(),
        ];
    }

    public function with(): array
    {
        return $this->getBookingStatus();
    }
}; ?>

<div>
    <!-- Template below -->
</div>

Template

<div class="bg-cream rounded-lg p-4">
    {{-- Loading State --}}
    <div wire:loading class="animate-pulse">
        <div class="h-5 bg-charcoal/20 rounded w-3/4"></div>
    </div>

    <div wire:loading.remove>
        @if($canBookToday)
            <div class="flex items-center gap-2 text-success">
                <flux:icon name="check-circle" class="w-5 h-5" />
                <span>{{ __('booking.can_book_today') }}</span>
            </div>
        @else
            <div class="flex items-center gap-2 text-warning">
                <flux:icon name="exclamation-circle" class="w-5 h-5" />
                <span>{{ __('booking.already_booked_today') }}</span>
            </div>
        @endif

        @if($pendingRequests->isNotEmpty())
            <div class="mt-2 text-sm text-charcoal/70">
                @if($pendingRequests->count() === 1)
                    <p>{{ __('booking.pending_for_date', ['date' => $pendingRequests->first()->scheduled_date->format('d/m/Y')]) }}</p>
                @else
                    <p>{{ __('booking.pending_count', ['count' => $pendingRequests->count()]) }}</p>
                    <ul class="mt-1 list-disc list-inside">
                        @foreach($pendingRequests->take(3) as $request)
                            <li>{{ $request->scheduled_date->format('d/m/Y') }}</li>
                        @endforeach
                    </ul>
                @endif
            </div>
        @endif

        <p class="mt-2 text-sm text-charcoal/70">
            {{ __('booking.limit_message') }}
        </p>
    </div>
</div>

Translation Keys

// resources/lang/en/booking.php
return [
    'can_book_today' => 'You can book a consultation today',
    'already_booked_today' => 'You already have a booking for today',
    'pending_for_date' => 'You have a pending request for :date',
    'pending_count' => 'You have :count pending requests',
    'limit_message' => 'Note: You can book a maximum of 1 consultation per day.',
];

// resources/lang/ar/booking.php
return [
    'can_book_today' => 'يمكنك حجز استشارة اليوم',
    'already_booked_today' => 'لديك حجز بالفعل لهذا اليوم',
    'pending_for_date' => 'لديك طلب معلق بتاريخ :date',
    'pending_count' => 'لديك :count طلبات معلقة',
    'limit_message' => 'ملاحظة: يمكنك حجز استشارة واحدة كحد أقصى في اليوم.',
];

Calendar Integration

Pass bookedDates to the availability calendar from Story 3.3:

{{-- In booking page --}}
<livewire:availability-calendar :disabled-dates="$bookedDates" />

The calendar should style user's booked dates differently (e.g., with a badge or distinct color) from dates with no available slots.

Test Scenarios

Unit Tests

  • getBookingStatus() returns correct structure
  • canBookToday is true when no booking exists for today
  • canBookToday is false when pending booking exists for today
  • canBookToday is false when approved booking exists for today
  • Cancelled bookings do not affect canBookToday
  • bookedDates only includes pending and approved bookings

Feature Tests

  • Status displays "can book today" message for new users
  • Status displays "already booked" when user has today's booking
  • Status displays pending request date correctly
  • Multiple pending requests display count and dates
  • Component renders on dashboard page
  • Component renders on booking page
  • Messages display correctly in Arabic locale
  • Messages display correctly in English locale

Browser Tests (optional)

  • Calendar visually shows user's booked dates as unavailable
  • Status updates after successful booking submission

References

  • PRD Section 5.8: Client Dashboard - Booking limit status requirement
  • PRD Section 5.4: "Maximum 1 consultation per client per day"
  • Epic 7 Success Criteria: "Booking limit enforcement (1 per day)"

Definition of Done

  • Booking status component created
  • Status displays on dashboard
  • Status displays on booking page
  • Calendar highlights user's booked dates
  • Messages are accurate for all states
  • Bilingual support (AR/EN)
  • Loading state implemented
  • Edge cases handled (multiple pending, cancelled)
  • Unit tests pass
  • Feature tests pass
  • Code formatted with Pint

Estimation

Complexity: Low | Effort: 2-3 hours


Dev Agent Record

Agent Model Used

Claude Opus 4.5 (claude-opus-4-5-20251101)

Completion Notes

  • Dashboard widget already existed from Story 7.2, no changes needed
  • Booking status banner integrated directly into booking page component instead of separate component
  • Calendar component updated to accept bookedDates prop and display user's booked dates with distinct sky-blue styling
  • User booked dates shown in legend only when the user has booked dates
  • All 23 new test assertions pass (16 booking status indicator tests + 7 calendar booked dates tests)

File List

Modified:

  • resources/views/livewire/client/consultations/book.blade.php - Added getBookingStatus method and status banner
  • resources/views/livewire/availability-calendar.blade.php - Added bookedDates prop and user_booked status handling
  • lang/en/booking.php - Added booking status translation keys
  • lang/ar/booking.php - Added booking status translation keys (Arabic)
  • tests/Feature/Livewire/AvailabilityCalendarTest.php - Added user booked dates tests

Created:

  • tests/Feature/Client/BookingStatusIndicatorTest.php - Comprehensive booking status indicator tests

Change Log

Date Change Reason
2025-12-29 Added booking status banner to booking page Display booking limit status to users
2025-12-29 Added bookedDates integration to calendar Visual indicator for user's booked dates
2025-12-29 Added bilingual translation keys Support AR/EN booking status messages
2025-12-29 Created comprehensive test suite Verify all acceptance criteria

Status

Ready for Review

QA Results

Review Date: 2025-12-29

Reviewed By: Quinn (Test Architect)

Code Quality Assessment

Overall: Excellent - The implementation is clean, well-structured, and follows Laravel/Livewire best practices. The booking status indicator is elegantly integrated into the existing booking page component rather than creating unnecessary separate components. Code is readable, properly typed, and follows the project's Volt single-file component pattern.

Highlights:

  • Clean separation of concerns with getBookingStatus() method
  • Proper use of Eloquent for querying user's bookings
  • Correct handling of status enums (ConsultationStatus::Pending, ::Approved)
  • Smart calendar integration - bookedDates prop allows calendar to mark user's booked dates with distinct styling
  • Proper dark mode support throughout the UI

Refactoring Performed

None required - the implementation quality is high.

Compliance Check

  • Coding Standards: ✓ Pint passes with no issues
  • Project Structure: ✓ Volt single-file component pattern followed correctly
  • Testing Strategy: ✓ Comprehensive test coverage with 16 booking status tests + 7 calendar integration tests
  • All ACs Met: ✓ All acceptance criteria are fully implemented

AC Verification:

  1. ✓ Dashboard widget showing booking status - Present in dashboard.blade.php (lines 196-241)
  2. ✓ Booking page status banner - Implemented in book.blade.php (lines 184-225)
  3. ✓ "You can book today" message - Translation key booking.can_book_today
  4. ✓ "Already booked today" message - Translation key booking.already_booked_today
  5. ✓ "Pending request for [date]" message - Translation key booking.pending_for_date
  6. bookedDates array passed to calendar - Line 231 in book.blade.php
  7. ✓ Calendar marks user's booked dates as unavailable - user_booked status in calendar component
  8. ✓ Visual indicator differentiates user's bookings - Sky-blue styling distinct from gray "unavailable"
  9. ✓ Clear 1-per-day limit messaging - booking.limit_message translation
  10. ✓ Bilingual messages (AR/EN) - Both lang files updated
  11. ✓ Multiple pending requests handling - Shows count and list up to 3
  12. ✓ Cancelled bookings don't block - Query filters by Pending/Approved only
  13. ✓ Loading state - Present via Livewire's wire:loading directives

Improvements Checklist

  • All acceptance criteria implemented
  • Tests pass for all new functionality (16/16 booking status tests)
  • Bilingual support complete
  • Calendar integration working correctly
  • Pre-existing test flakiness - 3 tests in AvailabilityCalendarTest.php fail due to date-sensitive logic (tests 58-65, 77-86, 129-140 use Carbon::now()->next(Carbon::SUNDAY) which resolves to January 2026 when run in late December 2025, but calendar defaults to current month). This is a pre-existing issue from Story 3.3, not introduced by Story 7.6.

Security Review

Status: PASS

  • User data isolation verified - getBookingStatus() queries only the authenticated user's bookings
  • Test confirms other users' bookings are not visible (booking status does not include other users bookings)
  • No SQL injection risks - uses Eloquent ORM with parameterized queries
  • No XSS risks - all output properly escaped via Blade templating

Performance Considerations

Status: PASS

  • getBookingStatus() executes 3 queries (today booking, pending requests, booked dates) - acceptable for the use case
  • Queries are properly scoped with date filters and status filters
  • No N+1 query issues detected
  • Calendar component efficiently passes booked dates as a simple array

Files Modified During Review

None - no refactoring was required.

Gate Status

Gate: PASSdocs/qa/gates/7.6-booking-limit-indicator.yml

Ready for Done

Notes:

  • All 16 new booking status indicator tests pass
  • All 7 new calendar booked dates tests pass
  • The 3 failing tests are pre-existing flaky tests from Story 3.3 (date-sensitive logic that breaks at month boundaries). These should be addressed in a separate technical debt story, not blocking Story 7.6 which did not introduce them.
  • Code is clean, well-tested, and follows all project conventions