252 lines
8.8 KiB
PHP
252 lines
8.8 KiB
PHP
<?php
|
|
|
|
use App\Services\AvailabilityService;
|
|
use Carbon\Carbon;
|
|
use Livewire\Volt\Component;
|
|
|
|
new class extends Component
|
|
{
|
|
public int $year;
|
|
|
|
public int $month;
|
|
|
|
public ?string $selectedDate = null;
|
|
|
|
public array $monthAvailability = [];
|
|
|
|
public array $availableSlots = [];
|
|
|
|
public array $bookedDates = [];
|
|
|
|
public function mount(array $bookedDates = []): void
|
|
{
|
|
$this->bookedDates = $bookedDates;
|
|
$this->year = now()->year;
|
|
$this->month = now()->month;
|
|
$this->loadMonthAvailability();
|
|
}
|
|
|
|
public function loadMonthAvailability(): void
|
|
{
|
|
$service = app(AvailabilityService::class);
|
|
$this->monthAvailability = $service->getMonthAvailability($this->year, $this->month);
|
|
}
|
|
|
|
public function previousMonth(): void
|
|
{
|
|
$date = Carbon::create($this->year, $this->month, 1)->subMonth();
|
|
|
|
// Prevent navigating to past months
|
|
if ($date->lt(now()->startOfMonth())) {
|
|
return;
|
|
}
|
|
|
|
$this->year = $date->year;
|
|
$this->month = $date->month;
|
|
$this->selectedDate = null;
|
|
$this->availableSlots = [];
|
|
$this->loadMonthAvailability();
|
|
}
|
|
|
|
public function nextMonth(): void
|
|
{
|
|
$date = Carbon::create($this->year, $this->month, 1)->addMonth();
|
|
$this->year = $date->year;
|
|
$this->month = $date->month;
|
|
$this->selectedDate = null;
|
|
$this->availableSlots = [];
|
|
$this->loadMonthAvailability();
|
|
}
|
|
|
|
public function selectDate(string $date): void
|
|
{
|
|
// Prevent selecting user's already booked dates
|
|
if (in_array($date, $this->bookedDates)) {
|
|
return;
|
|
}
|
|
|
|
$status = $this->monthAvailability[$date] ?? 'unavailable';
|
|
|
|
if (in_array($status, ['available', 'partial'])) {
|
|
$this->selectedDate = $date;
|
|
$this->loadAvailableSlots();
|
|
}
|
|
}
|
|
|
|
public function loadAvailableSlots(): void
|
|
{
|
|
if (! $this->selectedDate) {
|
|
$this->availableSlots = [];
|
|
|
|
return;
|
|
}
|
|
|
|
$service = app(AvailabilityService::class);
|
|
$this->availableSlots = $service->getAvailableSlots(
|
|
Carbon::parse($this->selectedDate)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get the day headers with locale-aware ordering.
|
|
*
|
|
* @return array<string>
|
|
*/
|
|
private function getDayHeaders(): array
|
|
{
|
|
$days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
|
|
// Arabic calendar traditionally starts on Saturday
|
|
if (app()->getLocale() === 'ar') {
|
|
array_unshift($days, array_pop($days));
|
|
}
|
|
|
|
return $days;
|
|
}
|
|
|
|
/**
|
|
* Build calendar days array for the current month.
|
|
*
|
|
* @return array<int, array{date: string, day: int, status: string}|null>
|
|
*/
|
|
private function buildCalendarDays(): array
|
|
{
|
|
$firstDay = Carbon::create($this->year, $this->month, 1);
|
|
$lastDay = $firstDay->copy()->endOfMonth();
|
|
|
|
// Calculate start padding based on locale
|
|
$startPadding = $firstDay->dayOfWeek;
|
|
if (app()->getLocale() === 'ar') {
|
|
// Adjust for Saturday start
|
|
$startPadding = ($firstDay->dayOfWeek + 1) % 7;
|
|
}
|
|
|
|
$days = array_fill(0, $startPadding, null);
|
|
|
|
// Fill month days
|
|
$current = $firstDay->copy();
|
|
while ($current->lte($lastDay)) {
|
|
$dateStr = $current->format('Y-m-d');
|
|
|
|
// Check if user has already booked this date
|
|
$status = in_array($dateStr, $this->bookedDates)
|
|
? 'user_booked'
|
|
: ($this->monthAvailability[$dateStr] ?? 'unavailable');
|
|
|
|
$days[] = [
|
|
'date' => $dateStr,
|
|
'day' => $current->day,
|
|
'status' => $status,
|
|
];
|
|
$current->addDay();
|
|
}
|
|
|
|
return $days;
|
|
}
|
|
|
|
public function with(): array
|
|
{
|
|
return [
|
|
'monthName' => Carbon::create($this->year, $this->month, 1)->translatedFormat('F Y'),
|
|
'calendarDays' => $this->buildCalendarDays(),
|
|
'dayHeaders' => $this->getDayHeaders(),
|
|
'bookedDates' => $this->bookedDates,
|
|
];
|
|
}
|
|
}; ?>
|
|
|
|
<div>
|
|
<!-- Calendar Header -->
|
|
<div class="flex items-center justify-between mb-4">
|
|
<flux:button size="sm" wire:click="previousMonth" variant="ghost">
|
|
<flux:icon name="chevron-{{ app()->getLocale() === 'ar' ? 'right' : 'left' }}" class="size-5" />
|
|
</flux:button>
|
|
|
|
<flux:heading size="lg" class="text-body">{{ $monthName }}</flux:heading>
|
|
|
|
<flux:button size="sm" wire:click="nextMonth" variant="ghost">
|
|
<flux:icon name="chevron-{{ app()->getLocale() === 'ar' ? 'left' : 'right' }}" class="size-5" />
|
|
</flux:button>
|
|
</div>
|
|
|
|
<!-- Day Headers -->
|
|
<div class="grid grid-cols-7 gap-1 mb-2" dir="{{ app()->getLocale() === 'ar' ? 'rtl' : 'ltr' }}">
|
|
@foreach($dayHeaders as $day)
|
|
<div class="text-center text-sm font-semibold text-zinc-600 dark:text-zinc-400">
|
|
{{ __("calendar.{$day}") }}
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
|
|
<!-- Calendar Grid -->
|
|
<div class="calendar-grid" dir="{{ app()->getLocale() === 'ar' ? 'rtl' : 'ltr' }}">
|
|
@foreach($calendarDays as $dayData)
|
|
@if($dayData === null)
|
|
<div class="calendar-cell"></div>
|
|
@else
|
|
<button
|
|
wire:click="selectDate('{{ $dayData['date'] }}')"
|
|
@class([
|
|
'calendar-cell rounded-lg text-center transition-colors font-medium min-h-[44px] text-white',
|
|
'bg-[#4A5D23]/70 hover:bg-[#4A5D23]/85 dark:bg-[#4A5D23]/60 dark:hover:bg-[#4A5D23]/75' => $dayData['status'] === 'available',
|
|
'bg-[#D4A84B]/70 hover:bg-[#D4A84B]/85 dark:bg-[#D4A84B]/60 dark:hover:bg-[#D4A84B]/75' => $dayData['status'] === 'partial',
|
|
'bg-sky-400/50 cursor-not-allowed dark:bg-sky-500/40' => $dayData['status'] === 'user_booked',
|
|
'bg-zinc-300/50 !text-zinc-400 cursor-not-allowed dark:bg-zinc-700/50 dark:!text-zinc-500' => in_array($dayData['status'], ['past', 'closed', 'blocked', 'full']),
|
|
'ring-2 ring-[#A68966] dark:ring-[#C4A882]' => $selectedDate === $dayData['date'],
|
|
])
|
|
@disabled(in_array($dayData['status'], ['past', 'closed', 'blocked', 'full', 'user_booked']))
|
|
>
|
|
{{ $dayData['day'] }}
|
|
</button>
|
|
@endif
|
|
@endforeach
|
|
</div>
|
|
|
|
<!-- Time Slots -->
|
|
@if($selectedDate)
|
|
<div class="mt-6">
|
|
<flux:heading size="sm" class="mb-3 text-body">
|
|
{{ __('booking.available_times') }} -
|
|
{{ \Carbon\Carbon::parse($selectedDate)->translatedFormat('d M Y') }}
|
|
</flux:heading>
|
|
|
|
@if(count($availableSlots) > 0)
|
|
<div class="time-slots-grid">
|
|
@foreach($availableSlots as $slot)
|
|
<button
|
|
wire:click="$parent.selectSlot('{{ $selectedDate }}', '{{ $slot }}')"
|
|
class="time-slot-btn rounded-lg border border-amber-500 text-amber-600 hover:bg-amber-500 hover:text-white transition-colors dark:border-amber-400 dark:text-amber-400 dark:hover:bg-amber-500 dark:hover:text-white"
|
|
>
|
|
{{ \Carbon\Carbon::parse($slot)->format('g:i A') }}
|
|
</button>
|
|
@endforeach
|
|
</div>
|
|
@else
|
|
<p class="text-zinc-500 dark:text-zinc-400">{{ __('booking.no_slots_available') }}</p>
|
|
@endif
|
|
</div>
|
|
@endif
|
|
|
|
<!-- Legend -->
|
|
<div class="flex flex-wrap gap-4 mt-6 text-sm">
|
|
<div class="flex items-center gap-2">
|
|
<div class="w-4 h-4 rounded bg-[#4A5D23]/70 dark:bg-[#4A5D23]/60"></div>
|
|
<span class="text-zinc-600 dark:text-zinc-400">{{ __('booking.available') }}</span>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<div class="w-4 h-4 rounded bg-[#D4A84B]/70 dark:bg-[#D4A84B]/60"></div>
|
|
<span class="text-zinc-600 dark:text-zinc-400">{{ __('booking.partial') }}</span>
|
|
</div>
|
|
@if(count($bookedDates) > 0)
|
|
<div class="flex items-center gap-2">
|
|
<div class="w-4 h-4 rounded bg-sky-400/50 dark:bg-sky-500/40"></div>
|
|
<span class="text-zinc-600 dark:text-zinc-400">{{ __('booking.user_booked') }}</span>
|
|
</div>
|
|
@endif
|
|
<div class="flex items-center gap-2">
|
|
<div class="w-4 h-4 rounded bg-zinc-300/50 dark:bg-zinc-700/50"></div>
|
|
<span class="text-zinc-600 dark:text-zinc-400">{{ __('booking.unavailable') }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|