where('businessId', $business->id) ->where('isActive', true) ->where('allowOnlineBooking', true) ->whereIn('id', $serviceIds) ->get(); if ($services->count() !== count(array_unique($serviceIds))) { return collect(); } $totalMinutes = $services->sum('durationMinutes') + $services->sum('bufferBeforeMinutes') + $services->sum('bufferAfterMinutes'); $rule = $business->bookingRule; $interval = max(5, (int) ($rule?->slotIntervalMinutes ?? 30)); $businessTimezone = $business->timezone ?: 'UTC'; $localDate = $date->setTimezone($businessTimezone)->startOfDay(); $weekday = strtoupper($localDate->format('l')); $businessHours = $business->hours()->where('dayOfWeek', $weekday)->first(); if (! $businessHours || $businessHours->isClosed || $businessHours->opensAtMinutes === null || $businessHours->closesAtMinutes === null) { return collect(); } $staffQuery = StaffProfile::query() ->where('businessId', $business->id) ->where('status', 'ACTIVE') ->where('isBookableOnline', true) ->when($staffId, fn ($query) => $query->whereKey($staffId)) ->whereHas('services', fn ($query) => $query->whereIn('Service.id', $serviceIds)->groupBy('StaffProfile.id')->havingRaw('COUNT(DISTINCT Service.id) = ?', [count(array_unique($serviceIds))])); return $staffQuery->with(['user', 'workingHours', 'breaks.days', 'timeOff'])->get()->flatMap(function (StaffProfile $staff) use ($business, $localDate, $weekday, $totalMinutes, $interval, $businessHours): Collection { $hours = $staff->workingHours->firstWhere('dayOfWeek', $weekday); if (! $hours || $hours->isOff || $hours->startMinutes === null || $hours->endMinutes === null) { return collect(); } $startMinute = max($hours->startMinutes, $businessHours->opensAtMinutes); $endMinute = min($hours->endMinutes, $businessHours->closesAtMinutes); $slots = collect(); for ($minute = $startMinute; $minute + $totalMinutes <= $endMinute; $minute += $interval) { $start = $localDate->addMinutes($minute); $end = $start->addMinutes($totalMinutes); $blocked = $staff->breaks->contains(fn ($break) => $this->overlapsRecurringBreak($break, $weekday, $minute, $minute + $totalMinutes)) || $staff->timeOff->contains(fn ($timeOff) => $this->overlapsTimeOff($timeOff, $localDate, $minute, $minute + $totalMinutes)); if ($blocked || $start->lessThan(CarbonImmutable::now($business->timezone)->addMinutes((int) ($business->bookingRule?->minNoticeMinutes ?? 120)))) { continue; } $hasConflict = $staff->appointments() ->whereNotIn('status', [AppointmentStatus::Cancelled->value, AppointmentStatus::NoShow->value]) ->where('startAt', '<', $end->utc()) ->where('endAt', '>', $start->utc()) ->exists(); if (! $hasConflict) { $slots->push([ 'staffId' => $staff->id, 'staffName' => $staff->user?->name ?: 'Available barber', 'startAt' => $start->toIso8601String(), 'endAt' => $end->toIso8601String(), 'label' => $start->format('g:i A'), ]); } } return $slots; })->sortBy('startAt')->values(); } private function overlapsRecurringBreak(object $break, string $weekday, int $start, int $end): bool { $applies = $break->days->contains('dayOfWeek', $weekday); return $applies && $break->startMinutes < $end && $break->endMinutes > $start; } private function overlapsTimeOff(object $timeOff, CarbonImmutable $date, int $start, int $end): bool { if ($date->lessThan($timeOff->startDate->startOfDay()) || $date->greaterThan($timeOff->endDate->endOfDay())) { return false; } return $timeOff->startMinutes === null || ($timeOff->startMinutes < $end && $timeOff->endMinutes > $start); } }