*/ private const DAY_KEYS = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']; /** * @return list */ public function availableSlots(QrCode $qrCode, string $date, int $serviceIndex): array { $content = $qrCode->content(); $services = $content['services'] ?? []; $service = $services[$serviceIndex] ?? null; if (! is_array($service)) { return []; } $duration = max(15, (int) ($service['duration_minutes'] ?? 30)); $day = Carbon::parse($date)->startOfDay(); if ($day->isPast() && ! $day->isToday()) { return []; } $windows = $this->windowsForDay($content, $day); if ($windows === []) { return []; } $booked = QrBooking::query() ->where('qr_code_id', $qrCode->id) ->whereIn('status', [QrBooking::STATUS_PENDING, QrBooking::STATUS_CONFIRMED]) ->whereDate('starts_at', $day->toDateString()) ->get(['starts_at', 'ends_at']); $slots = []; foreach ($windows as $window) { $cursor = $day->copy()->setTimeFromTimeString($window['start']); $windowEnd = $day->copy()->setTimeFromTimeString($window['end']); while ($cursor->copy()->addMinutes($duration)->lte($windowEnd)) { $slotEnd = $cursor->copy()->addMinutes($duration); if ($day->isToday() && $cursor->lte(now())) { $cursor->addMinutes($duration); continue; } if (! $this->overlapsBooked($cursor, $slotEnd, $booked)) { $slots[] = [ 'starts_at' => $cursor->toIso8601String(), 'ends_at' => $slotEnd->toIso8601String(), 'label' => $cursor->format('g:i A'), ]; } $cursor->addMinutes($duration); } } return $slots; } /** @return list */ private function windowsForDay(array $content, Carbon $day): array { $key = self::DAY_KEYS[$day->dayOfWeek]; $availability = $content['availability'] ?? []; $windows = $availability[$key] ?? []; if (! is_array($windows) || $windows === []) { $opens = (string) ($content['opens_at'] ?? '09:00'); $closes = (string) ($content['closes_at'] ?? '17:00'); $activeDays = $content['active_days'] ?? ['mon', 'tue', 'wed', 'thu', 'fri']; if (! in_array($key, (array) $activeDays, true)) { return []; } return [['start' => $opens, 'end' => $closes]]; } $normalized = []; foreach ($windows as $window) { if (! is_array($window)) { continue; } $start = trim((string) ($window['start'] ?? '')); $end = trim((string) ($window['end'] ?? '')); if ($start !== '' && $end !== '') { $normalized[] = ['start' => $start, 'end' => $end]; } } return $normalized; } /** @param Collection $booked */ private function overlapsBooked(Carbon $start, Carbon $end, Collection $booked): bool { foreach ($booked as $booking) { if ($start < $booking->ends_at && $end > $booking->starts_at) { return true; } } return false; } }