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
bookedDatesarray 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 widgetresources/views/livewire/client/booking.blade.php- Add status banner (from Story 7.5)resources/lang/en/booking.php- Add translation keysresources/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 structurecanBookTodayis true when no booking exists for todaycanBookTodayis false when pending booking exists for todaycanBookTodayis false when approved booking exists for today- Cancelled bookings do not affect
canBookToday bookedDatesonly 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
bookedDatesprop 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 bannerresources/views/livewire/availability-calendar.blade.php- Added bookedDates prop and user_booked status handlinglang/en/booking.php- Added booking status translation keyslang/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 -
bookedDatesprop 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:
- ✓ Dashboard widget showing booking status - Present in
dashboard.blade.php(lines 196-241) - ✓ Booking page status banner - Implemented in
book.blade.php(lines 184-225) - ✓ "You can book today" message - Translation key
booking.can_book_today - ✓ "Already booked today" message - Translation key
booking.already_booked_today - ✓ "Pending request for [date]" message - Translation key
booking.pending_for_date - ✓
bookedDatesarray passed to calendar - Line 231 in book.blade.php - ✓ Calendar marks user's booked dates as unavailable -
user_bookedstatus in calendar component - ✓ Visual indicator differentiates user's bookings - Sky-blue styling distinct from gray "unavailable"
- ✓ Clear 1-per-day limit messaging -
booking.limit_messagetranslation - ✓ Bilingual messages (AR/EN) - Both lang files updated
- ✓ Multiple pending requests handling - Shows count and list up to 3
- ✓ Cancelled bookings don't block - Query filters by Pending/Approved only
- ✓ 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.phpfail due to date-sensitive logic (tests 58-65, 77-86, 129-140 useCarbon::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: PASS → docs/qa/gates/7.6-booking-limit-indicator.yml
Recommended Status
✓ 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