285 lines
9.0 KiB
PHP
285 lines
9.0 KiB
PHP
<?php
|
|
|
|
namespace App\Livewire\Appointments;
|
|
|
|
use Livewire\Component;
|
|
use App\Models\Appointment;
|
|
use App\Models\Technician;
|
|
use Carbon\Carbon;
|
|
|
|
class TimeSlots extends Component
|
|
{
|
|
public $selectedDate;
|
|
public $selectedTechnician = '';
|
|
public $serviceDuration = 60; // minutes
|
|
public $availableSlots = [];
|
|
public $bookedSlots = [];
|
|
public $selectedSlot = '';
|
|
public $timeSlots = [];
|
|
public $technicians = [];
|
|
|
|
// Business hours configuration
|
|
public $businessStart = '08:00';
|
|
public $businessEnd = '18:00';
|
|
public $slotInterval = 30; // minutes
|
|
public $lunchStart = '12:00';
|
|
public $lunchEnd = '13:00';
|
|
|
|
public function mount($date = null, $technicianId = null, $duration = 60)
|
|
{
|
|
$this->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(),
|
|
]);
|
|
}
|
|
}
|