sackey e839d40a99
Some checks are pending
linter / quality (push) Waiting to run
tests / ci (push) Waiting to run
Initial commit
2025-07-30 17:15:50 +00:00

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(),
]);
}
}