selectedDate = $date ?: now()->addDay()->format('Y-m-d'); $this->selectedTechnician = $technicianId ?: ''; $this->serviceDuration = $duration; $this->technicians = Technician::where('status', 'active')->orderBy('first_name')->get(); $this->generateTimeSlots(); $this->loadBookedSlots(); $this->calculateAvailableSlots(); } public function updatedSelectedDate() { $this->selectedSlot = ''; $this->generateTimeSlots(); $this->loadBookedSlots(); $this->calculateAvailableSlots(); $this->dispatch('date-changed', date: $this->selectedDate); } public function updatedSelectedTechnician() { $this->selectedSlot = ''; $this->loadBookedSlots(); $this->calculateAvailableSlots(); $this->dispatch('technician-changed', technicianId: $this->selectedTechnician); } public function updatedServiceDuration() { $this->selectedSlot = ''; $this->calculateAvailableSlots(); } public function selectSlot($time) { $this->selectedSlot = $time; $this->dispatch('slot-selected', [ 'date' => $this->selectedDate, 'time' => $time, 'technician_id' => $this->selectedTechnician, 'duration' => $this->serviceDuration ]); } public function clearSelection() { $this->selectedSlot = ''; $this->dispatch('slot-cleared'); } private function generateTimeSlots() { $this->timeSlots = []; $date = Carbon::parse($this->selectedDate); // Don't show slots for past dates if ($date->isPast() && !$date->isToday()) { return; } $start = $date->copy()->setTimeFromTimeString($this->businessStart); $end = $date->copy()->setTimeFromTimeString($this->businessEnd); $lunchStart = $date->copy()->setTimeFromTimeString($this->lunchStart); $lunchEnd = $date->copy()->setTimeFromTimeString($this->lunchEnd); $current = $start->copy(); while ($current < $end) { $timeString = $current->format('H:i'); // Skip lunch time if ($current >= $lunchStart && $current < $lunchEnd) { $current->addMinutes($this->slotInterval); continue; } // Skip past times for today if ($date->isToday() && $current <= now()) { $current->addMinutes($this->slotInterval); continue; } $this->timeSlots[] = [ 'time' => $timeString, 'label' => $current->format('g:i A'), 'datetime' => $current->copy(), ]; $current->addMinutes($this->slotInterval); } } private function loadBookedSlots() { $query = Appointment::whereDate('scheduled_datetime', $this->selectedDate) ->whereNotIn('status', ['cancelled', 'no_show']); if ($this->selectedTechnician) { $query->where('assigned_technician_id', $this->selectedTechnician); } $appointments = $query->get(); $this->bookedSlots = []; foreach ($appointments as $appointment) { $startTime = Carbon::parse($appointment->scheduled_datetime); $endTime = $startTime->copy()->addMinutes($appointment->estimated_duration_minutes); // Mark all slots that overlap with this appointment as booked $current = $startTime->copy(); while ($current < $endTime) { $this->bookedSlots[] = [ 'time' => $current->format('H:i'), 'appointment_id' => $appointment->id, 'customer_name' => $appointment->customer->first_name . ' ' . $appointment->customer->last_name, 'service' => $appointment->service_requested, 'status' => $appointment->status, ]; $current->addMinutes($this->slotInterval); } } } private function calculateAvailableSlots() { $this->availableSlots = []; $bookedTimes = collect($this->bookedSlots)->pluck('time')->toArray(); foreach ($this->timeSlots as $slot) { $isAvailable = true; $slotStart = Carbon::parse($this->selectedDate . ' ' . $slot['time']); $slotEnd = $slotStart->copy()->addMinutes($this->serviceDuration); // Check if this slot and required duration would conflict with any booked slot $checkTime = $slotStart->copy(); while ($checkTime < $slotEnd) { if (in_array($checkTime->format('H:i'), $bookedTimes)) { $isAvailable = false; break; } $checkTime->addMinutes($this->slotInterval); } // Check if slot extends beyond business hours $businessEnd = Carbon::parse($this->selectedDate . ' ' . $this->businessEnd); if ($slotEnd > $businessEnd) { $isAvailable = false; } // Check if slot conflicts with lunch time $lunchStart = Carbon::parse($this->selectedDate . ' ' . $this->lunchStart); $lunchEnd = Carbon::parse($this->selectedDate . ' ' . $this->lunchEnd); if ($slotStart < $lunchEnd && $slotEnd > $lunchStart) { $isAvailable = false; } if ($isAvailable) { $this->availableSlots[] = $slot['time']; } } } public function getSlotStatus($time) { $bookedSlot = collect($this->bookedSlots)->firstWhere('time', $time); if ($bookedSlot) { return [ 'status' => 'booked', 'data' => $bookedSlot ]; } if (in_array($time, $this->availableSlots)) { return [ 'status' => 'available', 'data' => null ]; } return [ 'status' => 'unavailable', 'data' => null ]; } public function getAvailableSlotsForApi() { return collect($this->timeSlots) ->filter(function ($slot) { return in_array($slot['time'], $this->availableSlots); }) ->values() ->toArray(); } public function getBookedSlotsInfo() { return collect($this->bookedSlots) ->groupBy('time') ->map(function ($slots) { return $slots->first(); }) ->values() ->toArray(); } public function isSlotSelected($time) { return $this->selectedSlot === $time; } public function getNextAvailableDate() { $date = Carbon::parse($this->selectedDate); $maxDays = 30; // Look ahead 30 days for ($i = 1; $i <= $maxDays; $i++) { $checkDate = $date->copy()->addDays($i); // Skip weekends (assuming business doesn't operate on weekends) if ($checkDate->isWeekend()) { continue; } // Generate slots for this date $tempDate = $this->selectedDate; $this->selectedDate = $checkDate->format('Y-m-d'); $this->generateTimeSlots(); $this->loadBookedSlots(); $this->calculateAvailableSlots(); if (!empty($this->availableSlots)) { $nextDate = $this->selectedDate; $this->selectedDate = $tempDate; // Restore original date $this->generateTimeSlots(); $this->loadBookedSlots(); $this->calculateAvailableSlots(); return $nextDate; } $this->selectedDate = $tempDate; // Restore original date } return null; } public function render() { return view('livewire.appointments.time-slots', [ 'nextAvailableDate' => $this->getNextAvailableDate(), ]); } }