restaurant_id)->first(); if ($restaurant) { return $restaurant->timezone; } return "UTC"; } /** TESTED * Check if some date is bookable * * @param $timezone * * @return bool */ public function isBookable($timezone) { if (!$this->active) return false; $thisClass = get_class($this); $now = evavel_date_now()->setTimeZone($timezone)->format('Y-m-d'); if ($thisClass == Shift::class) { // Check last day is greater than today if ($this->end_date >= $now) { return true; } } else if ($thisClass == Event::class) { // Get included dates // Check some date is > now $dates = $this->include_dates; if (is_array($dates)){ foreach($dates as $date) { if ($date >= $now) { return true; } } } } return false; } /** TESTED * Check date is available * * @param Carbon|String $day * * @return bool */ public function isDateBookable($day) { $thisClass = get_class($this); if ($thisClass == Shift::class) { return $this->isDateBookableForShift($day); } else if ($thisClass == Event::class) { return $this->isDateBookableForEvent($day); } return false; } /** TESTED * Check specific date * * @param Carbon|String $day * * @return bool */ public function isDateBookableForShift($day) { // Get day of the week if (is_string($day)) { $day_string = $day; $day = evavel_date_createFromFormat('Y-m-d', $day); } else { $day_string = $day->toDateString(); } $dayOfWeek = $day->dayOfWeek; // Check the range if ($day_string < $this->start_date || $day_string > $this->end_date) { return false; } // Check exclude dates if (is_array($this->exclude_dates) && in_array($day_string, $this->exclude_dates)) { return false; } // Check include dates if (is_array($this->include_dates) && in_array($day_string, $this->include_dates)) { return true; } // Check week days $shifts_days_of_week = $this->days_of_week; $weekdays = ['sun', 'mon','tue','wed','thu','fri','sat','sun']; if ( $shifts_days_of_week[ $weekdays[$dayOfWeek] ] === false ) { return false; } return true; } /** TESTED * Check specific date * * @param Carbon|String $day * * @return bool */ public function isDateBookableForEvent($day) { if (is_string($day)) { $day_string = $day; } else { $day_string = $day->toDateString(); } if (is_array($this->include_dates) && in_array($day_string, $this->include_dates)) { return true; } return false; } /** TESTED * Get list of slots defined as available to book * Does not take into account open/close rules * * @return int[]|string[] */ public function timeSlotsBookable($date = null) { $all_slots = $this->mapBlockSlots($date); $slots = array_filter($all_slots, function($slot) { return $slot === true; }); return array_keys($slots); } /** TESTED * Simple list of slots given interval * @param $interval * * @return array */ protected function listSlots($interval, $first_seating, $last_seating) { $slots = []; //$first_seating = $this->first_seating; //$last_seating = $this->last_seating; for($time = $first_seating; $time <= $last_seating; $time += $interval) { $slots[$time] = true; } return $slots; } /** TESTED * Get all slots in array. * Keys are times, value is true(available), false(blocked) * ex: [ 30600 => true, 37800 => false ] * * @return array */ public function mapBlockSlots($date = null) { $first_seating = $this->getFirstSeatingForDate($date); $last_seating = $this->getLastSeatingForDate($date); $block_slots = $this->getBlockSlotsForDate($date); $slots = $this->listSlots($this->interval, $first_seating, $last_seating); // Remove block slots foreach ($block_slots as $b_slot) { $slots[$b_slot['time']] = !$b_slot['block']; } //ray('MAP BLOC SLOTS'); //ray($slots); return $slots; } /** TESTED * Check number of guests are allowed * @param $guests * * @return bool */ public function isNumberOfCoversAllowed($guests) { if ($guests < $this->min_covers_reservation) return false; if ($guests > $this->max_covers_reservation) return false; return true; } /** TESTED * Check if specific time is bookable * Does not mean it is available (depends on date and open/close rules) * @param $time * * @return bool */ public function isTimeSlotBookable($time, $date) { $first_seating = $this->getFirstSeatingForDate($date); $last_seating = $this->getLastSeatingForDate($date); if ($time < $first_seating) return false; if ($time > $last_seating) return false; // Check it is not blocked $slots = $this->mapBlockSlots($date); if (isset($slots[$time]) && $slots[$time] === true) { return true; } return false; } /** TESTED * Check if date and time is bookable based on the Open/Close rules * @param $date_string * @param $time * * @return bool */ public function isDateTimeBookable($date_string, $time) { //return true; $list = $this->openCloseWindowForSlots($date_string); //ray('>>>>>>>>> isDateTimeBookable ' . $date_string. ' ' .$time); //ray($list); //return true; return (isset($list[$time]) && $list[$time]['available']); } /** TESTED * Check if has some slot available using the Open/Close rules * @param $booking_date * @param $timezone * * @return bool */ // Este metodo es lento, no optimizado public function hasSomeSlotBookable($booking_date = null, $timezone = null) { $slots = $this->openCloseWindowForSlots($booking_date, $timezone, true); return count($slots) > 0; } /** * Obtener mapa de slots bloqueados optimizado */ private function getBlockedSlotsMap($date) { static $blocked_cache = []; $cache_key = $this->id . '_blocked_' . $date; if (isset($blocked_cache[$cache_key])) { return $blocked_cache[$cache_key]; } $blocked_slots = []; $first_seating = $this->getFirstSeatingForDate($date); $last_seating = $this->getLastSeatingForDate($date); // Inicializar todos como disponibles for ($time = $first_seating; $time <= $last_seating; $time += $this->interval) { $blocked_slots[$time] = true; } // Aplicar bloqueos $block_slots = $this->getBlockSlotsForDate($date); foreach ($block_slots as $block_slot) { if (isset($blocked_slots[$block_slot['time']])) { $blocked_slots[$block_slot['time']] = !$block_slot['block']; } } $blocked_cache[$cache_key] = $blocked_slots; return $blocked_slots; } /* TESTED * From specific booking date return list of Carbon dates for every slot * [ 52200 => [ available => true, date => Carbon, start => Carbon, end => Carbon ], ... ] * * @param $booking_date * @param $timezone * * @return array */ public function openCloseWindowForSlots($booking_date = null, $timezone = null, $filter_only_available = true) { // CACHE DE PETICION // // isDateTimeBookable() reconstruye este mapa entero solo para leer una // franja, y se llama una vez por franja desde isAvailable() y desde // getAllBookableSlots(). Con 49 franjas eso son 49 reconstrucciones de // un mapa de 49 franjas por cada consulta de disponibilidad. // // Se puede memorizar sin riesgo porque el resultado NO depende de las // reservas: solo de la configuracion del turno (open/close), las franjas // bloqueadas, los dias cerrados, la fecha y la hora actual. Crear una // reserva a mitad de peticion no puede invalidarlo. // // La hora actual queda congelada en la primera llamada de la peticion. // Es lo deseable: antes, una peticion larga que cruzara el instante de // apertura o cierre de una franja podia evaluarla de forma distinta en // dos comprobaciones de la misma respuesta. static $window_cache = []; // Tope de entradas. Sin el, recorridos largos (availableDates() puede // iterar cientos de dias segun el end_date del turno) dejarian crecer // la cache hasta tocar el memory_limit. Al pasarse se vacia entera: // esos recorridos visitan cada fecha una sola vez, asi que no se pierde // nada reutilizable. Cada entrada ocupa del orden de 65 KB. $cache_max_entries = 100; if (!$timezone) { $timezone = $this->getTimezone(); } if ($booking_date == null){ $booking_date = evavel_now_timezone($timezone)->format("Y-m-d"); } // get_class() hace falta: el trait lo usa la clase padre Scheduler, asi // que Shift y Event comparten esta misma variable estatica $cache_key = implode('|', [ get_class($this), $this->id, $booking_date, $timezone, $filter_only_available ? '1' : '0', ]); if (array_key_exists($cache_key, $window_cache)) { return $window_cache[$cache_key]; } if (count($window_cache) >= $cache_max_entries) { $window_cache = []; } // Be sure date is bookable if (!$this->isDateBookable($booking_date)) return $window_cache[$cache_key] = []; $now = evavel_now_timezone($timezone); $slots = $this->mapBlockSlots($booking_date); $result = []; foreach($slots as $time => $bookable) { if ($bookable === false) continue; $open_date = $this->openDate($booking_date, $time, $timezone); $close_date = $this->closeDate($booking_date, $time, $timezone); $booking_date_time = evavel_carbon($booking_date, $time, $timezone); $available = true; $message = ''; if ($open_date > $close_date) { $available = false; $message = __eva('Open date > Close date'); } else if ($now < $open_date) { $available = false; $message = __eva('Not open yet'); } else if ($now > $close_date) { $available = false; $message = __eva('Already closed'); } else if ($now > $booking_date_time) { $available = false; $message = __eva('Date past'); } // Only time slots that are available if (!$filter_only_available || ($filter_only_available && $available)) { $result[$time] = [ 'available' => $available, 'message' => $message, 'date' => $booking_date_time, 'start' => $open_date, 'end' => $close_date ]; } } return $window_cache[$cache_key] = $result; } /** TESTED * Calculate open date * @param $time_slot * @param $timezone * * @return Carbon|false|null */ protected function openDate($date_string, $time_slot, $timezone) { switch ($this->open_reservation_mode) { case 'open_all_time': return evavel_carbon_2000(); break; case 'open_hours_before': return evavel_carbon($date_string, $time_slot, $timezone)->addSeconds(- $this->open_hours_before); break; case 'open_same_day_at_time': return evavel_carbon($date_string, $this->open_same_day_time, $timezone); break; case 'open_days_before_at_time': return evavel_carbon($date_string, $this->open_several_days_time, $timezone)->addDays(- $this->open_several_days_count); break; default: return null; } } /** TESTED * Calculate close date * @param $time_slot * @param $timezone * * @return Carbon|false|null */ protected function closeDate($date_string, $time_slot, $timezone) { switch ($this->close_reservation_mode) { case 'until_last_minute': return evavel_carbon($date_string, $time_slot, $timezone); break; case 'until_hours': return evavel_carbon($date_string, $time_slot, $timezone)->addSeconds(- $this->until_hours_period); break; case 'until_same_day': return evavel_carbon($date_string, $this->until_same_day_time, $timezone); break; case 'until_previous_day': return evavel_carbon($date_string, $this->until_previous_day_time, $timezone)->addDays(- $this->until_previous_day_count); break; default: return null; } } /** TESTED * Get the key for interval * @param $start_seconds * @param $end_seconds * * @return string */ protected function intervalKey($start_seconds, $end_seconds) { return $start_seconds.'-'.$end_seconds; // For debugging //return evavel_seconds_to_Hmi($start_seconds).'-'.evavel_seconds_to_Hmi($end_seconds); } /** TESTED * Get intervals every 15 minutes with number of covers * Get current bookings occupied * Son covers calculados para saber si hay mas sitio * @param $date_string * * @return array */ public function mapTimeSlotCovers($date_string, $include_selected = true, $exclude_booking_id = null) { $interval = $this->stepQuarterOfMinute; $slots_covers = $this->prepareArraySlots($date_string); $list_statuses = BookingStatus::occupied(); if (!$include_selected) { $list_statuses = BookingStatus::occupied_not_selected(); } // Bookings with this shift/event and date // Son asientos ocupados en total, no importa el estado $query = Booking::where('shift_event_id', $this->id) ->where('date', $date_string) ->whereIn('status', $list_statuses); if ($exclude_booking_id) { $query->where('id', '!=', $exclude_booking_id); } $bookings = $query->get(); foreach ($bookings as $booking) { $start_time = $booking->time; $end_time = $start_time + $booking->duration; for ($time = $start_time; $time < $end_time; $time += $interval) { $key = $this->intervalKey($time, $time+$interval); if (isset($slots_covers[$key])) { $slots_covers[$key] += $booking->party; } } } return $slots_covers; } protected function prepareArraySlots($date) { // Every 15 minutes to take into account duration that can be set with 15 min precision (ex: 1h 45min) $interval = $this->stepQuarterOfMinute; //$slots = $this->listSlots($interval); $first_seating = $this->getFirstSeatingForDate($date); $last_seating = $this->getLastSeatingForDate($date); $slots = $this->listSlots($interval, $first_seating, $last_seating); $slots_covers = []; $count = 0; foreach($slots as $time => $bookable) { $count++; // SKip last interval. If shift is from 17:00 to 19:00 we don't need interval 19:00-19:15 if ($count < count($slots)) { $key = $this->intervalKey($time, $time+$interval); $slots_covers[$key] = 0; } } return $slots_covers; } /** * Map time slot bookings count (number of reservations, not covers) * Similar to mapTimeSlotCovers but counts 1 per booking instead of party size */ public function mapTimeSlotBookings($date_string, $include_selected = true, $exclude_booking_id = null) { $interval = $this->stepQuarterOfMinute; $slots_bookings = $this->prepareArraySlots($date_string); //ray('Incluye status selected: '. $include_selected ? 'TRUE':'false'); //$backtrace = debug_backtrace(); // $backtrace[0] = la función actual // $backtrace[1] = quien la llamó //if (isset($backtrace[1])) { // ray($backtrace); //} $list_statuses = BookingStatus::occupied(); if (!$include_selected) { $list_statuses = BookingStatus::occupied_not_selected(); } $query = Booking::where('shift_event_id', $this->id) ->where('date', $date_string) ->whereIn('status', $list_statuses); if ($exclude_booking_id) { $query->where('id', '!=', $exclude_booking_id); } $bookings = $query->get(); foreach ($bookings as $booking) { $start_time = $booking->time; $end_time = $start_time + $booking->duration; for ($time = $start_time; $time < $end_time; $time += $interval) { $key = $this->intervalKey($time, $time+$interval); if (isset($slots_bookings[$key])) { $slots_bookings[$key] += 1; } } } return $slots_bookings; } public function mapTimeSlotCovers_WithNewCovers($date_string) { $interval = $this->stepQuarterOfMinute; $slots_covers = $this->mapTimeSlotCovers($date_string, false); foreach($slots_covers as $key => $value) { $slots_covers[$key] = [ 'total' => $value, 'new' => 0 ]; } $list_statuses = BookingStatus::occupied_not_selected(); $bookings = Booking::where('shift_event_id', $this->id) ->where('date', $date_string) ->whereIn('status', $list_statuses) ->get(); foreach ($bookings as $booking) { $time = $booking->time; $key = $this->intervalKey($time, $time+$interval); if (isset($slots_covers[$key])) { $slots_covers[$key]['new'] += $booking->party; } } return $slots_covers; } /** TESTED * Get duration in seconds depending on the number of covers * @param $num_guests * * @return int */ public function getDuration($num_guests) { $mode = $this->duration_mode; switch ($this->duration_mode) { case 'time': return $this->duration_time; break; case 'covers': $duration = 0; foreach ($this->duration_covers as $d_cover){ if ($num_guests >= $d_cover['label']) { $duration = $d_cover['value']; } } return $duration; break; default: return 7200; } } /** TESTED * Check a specific slot, specific date, if it is closed * @param $date_string * @param $time * * @return bool */ public function isSlotClosed($date_string, $time) { $closed_slots = ClosedSlot::where(evavel_tenant_field(), $this->restaurant_id)->first(); if (!$closed_slots) return false; $data = $closed_slots->{$date_string}; if (!$data) return false; foreach($data as $item){ if (isset($item['id']) && $item['id'] == $this->id){ if (in_array($time, $item['slots'])){ return true; } } } return false; } /** TESTED * Check if booking is allowed using all the parameters and rules * @param $date_string * @param $guests * @param $time * * @return bool */ public function isAvailable($date_string, $time, $guests, $exclude_booking_id = null, $return_schedule_combination = false) { //ray('isAvailable exclude_booking_id:' . $exclude_booking_id); // Check date is bookable and rules open/close if (!$this->isDateTimeBookable($date_string, $time)) { //ray('NO DISPONIBLE ' . $date_string . ' ' . $time); return false; } //ray('>>>>> isAvailable ' . $date_string.' ' . $time. ' ' .$guests. ' - ' . evavel_seconds_to_Hm($time)); // IMPORTANTE ++++++++++++++++++++++++++++++++++++++++++++++++++++++ $requireSingleTablesToBeOnlineForGroups = false; // Comprobar si cuando usa mesas esta activado la opcion de limitar por covers $tables_limit_covers = $this->tables_limit_covers; //ray('Tables limit covers: ' . $this->tables_limit_covers); // Comprobar si esta activado la opcion de limitar por numero de reservas $tables_limit_bookings = $this->tables_limit_bookings; // Based on availability type the check is different //ray($this->availability_type); switch ($this->availability_type) { case self::AVAILABILITY_ALL_TABLES: if (method_exists($this, 'isAvailableBy_SearchTables')){ if ($tables_limit_covers && !$this->isAvailableByVolumeSlots($date_string, $time, $guests, $exclude_booking_id) ) { return null; } if ($tables_limit_bookings && !$this->isAvailableByVolumeSlotBookings($date_string, $time, $guests, $exclude_booking_id) ) { //ray('RESERVAS LIMITADAS para '.$date_string.' ' . $time. ' ' .$guests. ' - ' . evavel_seconds_to_Hm($time),); return null; } $tables = $this->isAvailableBy_SearchTables($date_string, $time, $guests, $requireSingleTablesToBeOnlineForGroups, null, $exclude_booking_id); return $tables; } return $this->isAvailableByTables_MaxCovers($date_string, $time, $guests, false); break; case self::AVAILABILITY_SPECIFIC_TABLES: if (method_exists($this, 'isAvailableBy_SearchTables')){ if ($tables_limit_covers && !$this->isAvailableByVolumeSlots($date_string, $time, $guests, $exclude_booking_id) ) { return null; } if ($tables_limit_bookings && !$this->isAvailableByVolumeSlotBookings($date_string, $time, $guests, $exclude_booking_id) ) { return null; } $tables = $this->isAvailableBy_SearchTables($date_string, $time, $guests, $requireSingleTablesToBeOnlineForGroups, null, $exclude_booking_id); return $tables; } $tables = $this->isAvailableBySpecificTables_MaxCovers($date_string, $time, $guests); return $tables; break; case self::AVAILABILITY_TABLES_SCHEDULE: // Usar el nuevo método para tables_schedule if (method_exists($this, 'isAvailableByTablesSchedule')) { // Comprobar limitacion de volumen de covers if ($tables_limit_covers && !$this->isAvailableByVolumeSlots($date_string, $time, $guests, $exclude_booking_id) ) { return null; } // Comprobar limitacion de numero de reservas if ($tables_limit_bookings && !$this->isAvailableByVolumeSlotBookings($date_string, $time, $guests, $exclude_booking_id) ) { return null; } // Solicita $tables = $this->isAvailableByTablesSchedule($date_string, $time, $guests, $exclude_booking_id, true); //ray('tables_schedule: ', $tables); // Es un grupo? if (isset($tables['mode']) && $tables['mode'] == 'group') { if ($return_schedule_combination){ return $tables; } return $tables['tables']; } // Es solo un array con mesa return $tables; } return false; break; case self::AVAILABILITY_TABLES_LAYOUTS: $tables = $this->isAvailableByTablesFromLayouts($date_string, $time, $guests, $exclude_booking_id); return $tables; break; case self::AVAILABILITY_VOLUME_TOTAL: return $this->isAvailableByVolumeTotal($date_string, $time, $guests, $exclude_booking_id); break; case self::AVAILABILITY_VOLUME_SLOTS: // Aqui no se aplica /*if ($tables_limit_bookings && !$this->isAvailableByVolumeSlotBookings($date_string, $time, $guests) ) { return false; }*/ return $this->isAvailableByVolumeSlots($date_string, $time, $guests, $exclude_booking_id); break; default: return false; } } /** 2025-02-10 * Get all available tables and combinations for a specific date, time and number of guests * Returns an array with two keys: 'single_tables' and 'combinations' * * @param string $date_string Date in Y-m-d format * @param int $time Time in seconds from start of day * @param int $guests Number of guests * @param bool $singleTablesRequireToBeOnline Whether single tables need to be online for combinations * @param int|null $areaIdToFilter Optional area ID to filter results * * @return array|false Returns false if no tables available, otherwise returns array with available tables */ public function getAllAvailableTables( $date_string, $time, $guests, $singleTablesRequireToBeOnline = false, $areaIdToFilter = null ) { // Check if we should use table availability if ( ! in_array( $this->availability_type, [ self::AVAILABILITY_ALL_TABLES, self::AVAILABILITY_SPECIFIC_TABLES ] ) ) { return false; } // Check date is bookable and rules open/close if ( ! $this->isDateTimeBookable( $date_string, $time ) ) { return false; } $result = [ 'single_tables' => [], 'combinations' => [] ]; // Get single tables $check_number_of_guests = true; $listTablesFree = $this->getListTablesFree( $date_string, $time, $guests, false, $check_number_of_guests ); if ( $areaIdToFilter ) { $listTablesFree = $this->filterListByArea( $listTablesFree, $areaIdToFilter ); } // Add all suitable single tables foreach ( $listTablesFree as $table ) { if ( $guests >= $table['min_seats'] && $guests <= $table['max_seats'] ) { $result['single_tables'][] = $table; } } // Get combinations based on single tables requirement if ( $singleTablesRequireToBeOnline ) { $listGroupsFree = $this->getListTablesGroupsFree_WithAllTablesOnline( $date_string, $time, $guests ); } else { $listGroupsFree = $this->getListTablesGroupsFree( $date_string, $time, $guests ); } if ( $areaIdToFilter ) { $listGroupsFree = $this->filterListByArea( $listGroupsFree, $areaIdToFilter ); } // Add all suitable combinations with their component tables foreach ( $listGroupsFree as $group ) { if ( $guests >= $group['min_seats'] && $guests <= $group['max_seats'] ) { $group['tables'] = $this->getGroupTables( $group, $date_string ); $result['combinations'][] = $group; } } // Sort single tables by seats (ascending) usort( $result['single_tables'], function ( $a, $b ) { return $a['max_seats'] - $b['max_seats']; } ); // Sort combinations by total seats (ascending) usort( $result['combinations'], function ( $a, $b ) { return $a['max_seats'] - $b['max_seats']; } ); return ! empty( $result['single_tables'] ) || ! empty( $result['combinations'] ) ? $result : false; } /** TESTED * Check if booking is allowed based on all the tables online * Calculate the total covers for all tables and current bookings * @param $date_string * @param $time * @param $guests * * @return bool */ protected function isAvailableByTables_MaxCovers($date_string, $time, $guests) { $max_online_covers = intval( $this->compute('tables_max_online_covers') ); $total_covers = $this->bookingsTotalCovers($date_string); //ray($total_covers.' / '.$max_online_covers); return $max_online_covers >= ($total_covers + $guests); } /** TESTED * Check if booking is allowed based on specific tables selected * Calculate the total covers for tables and current bookings * @param $date_string * @param $time * @param $guests * * @return bool */ protected function isAvailableBySpecificTables_MaxCovers($date_string, $time, $guests) { $max_covers = $this->maxCoversForEspecificTables(); $total_covers = $this->bookingsTotalCovers($date_string); return $max_covers >= ($total_covers + $guests); } /** TESTED * Check if booking is allowed based on max covers * Compare with the max cover allowed and the current bookings * @param $date_string * @param $time * @param $guests * * @return bool */ protected function isAvailableByVolumeTotal($date_string, $time, $guests, $exclude_booking_id = null) { $total_covers = $this->bookingsTotalCovers($date_string, $exclude_booking_id); return ($total_covers + $guests) <= $this->availability_total; } /** TESTED * Check if booking is allowed based on max covers allowed per slot * @param $date_string * @param $time * @param $guests * * @return bool */ protected function isAvailableByVolumeSlots($date_string, $time, $guests, $exclude_booking_id = null) { $result = false; $duration = $this->getDuration($guests); $start_time = $time; $end_time = $time + $duration; // COMPROBAR ASIENTOS TOTALES DEL INTERVALO ============================================================= // Occupancy of covers for each slot $mapCovers = $this->mapTimeSlotCovers($date_string, true, $exclude_booking_id); //ray('COVERS EXISTENTES:'. $this->name . '-' . $date_string.' - '.$time.' - '.$guests, $mapCovers); // Extract the intervals needed for the booking with the covers used per interval $current_covers = []; for ($seconds = $start_time; $seconds < $end_time; $seconds += $this->stepQuarterOfMinute) { $key = $this->intervalKey($seconds, $seconds + $this->stepQuarterOfMinute); if (isset($mapCovers[$key])) { $current_covers[$key] = $mapCovers[$key]; } // ERROR. some interval not defined so not available else { //ray('ERROR: interval not defined: ' . $key); //return false; } } //ray('COVERS EXTRACTED:' , $current_covers); /* I have all the intervals with covers used [ "54000-54900" => 6 "54900-55800" => 6 "55800-56700" => 6 "56700-57600" => 6 "57600-58500" => 2 "58500-59400" => 2 "59400-60300" => 0 ] */ // I need to compare with the setting Covers per slot $list_allowed_covers = $this->getCoversAvailability(); //ray('LIST ALLOWED COVERS: ' , $list_allowed_covers); //ray($current_covers); // Compare max allowed with (current covers + needed covers) // key: '36000-36900' , '36900-37800', etc $count_success = 0; foreach ($current_covers as $key => $current_num) { if (isset($list_allowed_covers[$key])) { $max_covers = $list_allowed_covers[$key]; if ($max_covers >= ($current_num + $guests)){ $count_success ++; } } } // If all intervals are ok then booking is ok if ($count_success > 0 && $count_success == count($current_covers)) { $result = true; } else { // No cumple la primera condicion, ya puede volver return false; } // COMPROBAR ASIENTOS NUEVOS DEL INTERVALO ============================================================= // Comprobar nuevos seats permitidos por slot solo si esta activado $check_new_covers = $this->availability_slots_limit_new_covers; if ($check_new_covers != 1) { return $result; } // Estos son los asientos nuevos maximos, me los da cada 15 minutos, aunque en realidad los intervalos sean de mas tiempo $list_max_new_covers = $this->getCoversAvailability('new_covers'); // Lo maximos a coger son los del intervalo $time-$time+900 $max_new_covers_allowed = $list_max_new_covers[$time.'-'.($time+$this->stepQuarterOfMinute)]; // Ese maximo valor hay que compararlo con las reservas nuevas recibidas durante el intervalo completo // time-time+interval // Contador acumulado de recibidas: las FINISHED siguen contando $query = Booking::where('restaurant_id', $this->restaurant_id) ->where('shift_event_id', $this->id) ->where('date', $date_string) ->whereIn('status', BookingStatus::occupied_or_finished()) ->where('time', '>=', $time) ->where('time', '<', $time + $this->interval); if ($exclude_booking_id) { $query->where('id', '!=', $exclude_booking_id); } $bookings = $query->get(); // Obtener los asientos de esas reservas $new_seats_booked = 0; foreach($bookings as $booking) { $new_seats_booked += $booking->party; } //ray('Date: ' . $date_string.' : ' .$time/3600 . '-------------------------------------------'); //ray('New seats booked: ' . $new_seats_booked); //ray('Guests: ' . $guests); //ray('Max new allowed: ' . $max_new_covers_allowed); $result_2 = ($new_seats_booked + $guests) <= $max_new_covers_allowed; // Combinacion de ambas cosas //ray('RESULT : '. (intval($time/3600).':'.($time - 3600*intval($time/3600) )/60) .' = '.$result.' + '.$result_2); return $result && $result_2; } /** * Check if a booking is available based on bookings-per-slot limits * Similar to isAvailableByVolumeSlots but counts number of reservations instead of covers */ protected function isAvailableByVolumeSlotBookings($date_string, $time, $guests, $exclude_booking_id = null) { //ray('Comprobando limitacion de bookings a slot: '.$date_string.' - '.$time.' - '.$guests.' - '); $result = false; $duration = $this->getDuration($guests); $start_time = $time; $end_time = $time + $duration; // COMPROBAR RESERVAS TOTALES DEL INTERVALO ============================================================= $mapBookings = $this->mapTimeSlotBookings($date_string, true, $exclude_booking_id); //ray('BOOKINGS EXISTENTES : '.$this->name.'-'.$date_string.' - '.$time.' - '.$guests.' - ', $mapBookings); // Extract the intervals needed for the booking $current_bookings = []; for ($seconds = $start_time; $seconds < $end_time; $seconds += $this->stepQuarterOfMinute) { $key = $this->intervalKey($seconds, $seconds + $this->stepQuarterOfMinute); if (isset($mapBookings[$key])) { $current_bookings[$key] = $mapBookings[$key]; } else { //return false; } } // Compare with the setting Bookings per slot $list_allowed_bookings = $this->getBookingsAvailability(); //ray('LIST ALLOWED BOOKINGS: ' , $list_allowed_bookings); $count_success = 0; foreach ($current_bookings as $key => $current_num) { if (isset($list_allowed_bookings[$key])) { $max_bookings = $list_allowed_bookings[$key]; // +1 porque es una nueva reserva (no sumamos guests sino 1) if ($max_bookings >= ($current_num + 1)){ $count_success ++; } } } if ($count_success > 0 && $count_success == count($current_bookings)) { $result = true; } else { return false; } // COMPROBAR RESERVAS NUEVAS DEL INTERVALO ============================================================= $check_new_bookings = $this->availability_slots_limit_new_bookings; if ($check_new_bookings != 1) { return $result; } $list_max_new_bookings = $this->getBookingsAvailability('new_bookings'); $max_new_bookings_allowed = $list_max_new_bookings[$time.'-'.($time+$this->stepQuarterOfMinute)]; // Contar el numero de reservas (no covers) en el intervalo // Contador acumulado de recibidas: las FINISHED siguen contando $query = Booking::where('restaurant_id', $this->restaurant_id) ->where('shift_event_id', $this->id) ->where('date', $date_string) ->whereIn('status', BookingStatus::occupied_or_finished()) ->where('time', '>=', $time) ->where('time', '<', $time + $this->interval); if ($exclude_booking_id) { $query->where('id', '!=', $exclude_booking_id); } $bookings = $query->get(); // Contar reservas, no asientos $new_bookings_count = count($bookings); $result_2 = ($new_bookings_count + 1) <= $max_new_bookings_allowed; return $result && $result_2; } /** TESTED * Get Total covers in bookings for date with this shift * @param $date_string * * @return int */ protected function bookingsTotalCovers($date_string, $exclude_booking_id = null) { // Tope acumulado del dia/turno: las FINISHED siguen contando $query = Booking::where('shift_event_id', $this->id) ->where('date', $date_string) ->whereIn('status', BookingStatus::occupied_or_finished()); if ($exclude_booking_id) { $query->where('id', '!=', $exclude_booking_id); } $bookings = $query->get(); $total_covers = 0; foreach($bookings as $booking){ $total_covers += $booking->party; } return $total_covers; } /** TESTED * Transform number of covers per interval in a list of intervals of 900 seconds * so can be compared with the mapTimeSlotsCovers * @return array */ protected function getCoversAvailability( $variable = 'covers' ) { $list = []; $items = $this->availability_slots; $interval = $this->interval; for($i = 0; $i < count($items); $i++) { $time_start = $items[$i]['time']; $time_end = $time_start + $interval; $covers = $items[$i][$variable]; for ($seconds = $time_start; $seconds < $time_end; $seconds += $this->stepQuarterOfMinute){ $key = $this->intervalKey($seconds, $seconds+$this->stepQuarterOfMinute); $list[$key] = $covers; } } return $list; } /** * Similar to getCoversAvailability but reads from availability_slots_bookings * Returns max bookings allowed per 15-min interval */ protected function getBookingsAvailability( $variable = 'bookings' ) { $list = []; $items = $this->availability_slots_bookings; //ray($items); $interval = $this->interval; if (!is_array($items)) { return $list; } for($i = 0; $i < count($items); $i++) { $time_start = $items[$i]['time']; $time_end = $time_start + $interval; $bookings = $items[$i][$variable]; for ($seconds = $time_start; $seconds < $time_end; $seconds += $this->stepQuarterOfMinute){ $key = $this->intervalKey($seconds, $seconds+$this->stepQuarterOfMinute); $list[$key] = $bookings; } } //ray($list); return $list; } // Return slots available for a specific date public function getSlotsAvailable($guests, $date, $time, $numberOfSlotsToGet = 4) { /* PROCESS Check the $date,$guests,$time is allowed by this shift 1. For that date calculate all slots for each shift 2. Filter shift rule -> blocked slots 3. Filter shift rule duration (depends on guests) . Filter based on shift Open rule . Filter based on shift Close rule . Filter shift rule Covers . Based on bookings, and shift rules remove slots not available 8. Return some slots above and below the selected time I can use filters to remove slots with every filter */ // Full range of slots $slots = []; for ($seconds = $this->first_seating; $seconds < $this->last_seating; $seconds += $this->interval){ if ($this->isAvailable($date, $seconds, $guests)) { $slots[] = $seconds; } } $filters = [ //new FilterByBlockSlots, // NOT needed new FilterByDuration, new FilterByNearestSlots ]; foreach($filters as $filter) { $slots = $filter->apply($this, $slots, $guests, $date, $time, $numberOfSlotsToGet); } return $slots; } // ESTE ES LA FUNCION PARA AVERIGUAR LOS SLOTS DISPONIBLES /** TESTED * Get all slots available based only on date and guests * @param $guests * @param $date * * @return array */ public function getAllSlotsAvailable($guests, $date, $exclude_booking_id = null) { //ray('getAllSlotsAvailable for '.$guests.' guests and date ' . $date); // Esto parece que no se ejecuta nunca porque solo lo llama calculateSlotsForOneService // para eso ya esta calculateSlotsForOneServiceWithTablesSchedule // Para tables_schedule, usar un método especializado /*if ($this->availability_type === self::AVAILABILITY_TABLES_SCHEDULE) { if (method_exists($this, 'getAllSlotsForTablesWithSchedule')) { $tables_slots = $this->getAllSlotsForTablesWithSchedule($date, $guests, $exclude_booking_id); ray($tables_slots); return $tables_slots; } return []; }*/ $first_seating = $this->getFirstSeatingForDate($date); $last_seating = $this->getLastSeatingForDate($date); $block_slots = $this->getBlockSlotsForDate($date); //ray($block_slots); // Full range of slots $slots = []; for ($seconds = $first_seating; $seconds <= $last_seating; $seconds += $this->interval) { //ray('Comprobando availability para '.$date.' '.$seconds.' '.$guests); if ($this->isAvailable($date, $seconds, $guests, $exclude_booking_id)) { $slots[] = $seconds; } } //ray($slots); $filters = [ new FilterByDuration, new FilterByMinMaxGuests, new FilterByClosedSlots ]; foreach($filters as $filter) { $slots = $filter->apply($this, $slots, $guests, $date, 0); } return $slots; } // All slots bookable for specific date public function getAllBookableSlots($date) { $first_seating = $this->getFirstSeatingForDate($date); $last_seating = $this->getLastSeatingForDate($date); $slots = []; for ($seconds = $first_seating; $seconds <= $last_seating; $seconds += $this->interval) { if ($this->isDateTimeBookable($date, $seconds)){ $slots[] = $seconds; } } return $slots; } public function availableDates($timezone = 'America/New_York', $days_in_advance = 15) { $days_available = []; for ($i = 0; $i <= $days_in_advance; $i++) { $date = evavel_date_now()->setTimezone($timezone)->addDays($i); $date_string = $date->format('Y-m-d'); if ($this->hasSomeSlotBookable($date_string, $timezone)){ $days_available[] = $date->format('Y-m-d'); } } // I have to remove closedDays $closedDays = ClosedDay::where(ClosedDay::$pivot_tenant_field, $this->restaurant_id)->first(); if ($closedDays) { $days_available = $closedDays->removeDatesFromSimpleArray($days_available); } return $days_available; } protected function maxCoversForEspecificTables() { $tables_included = $this->list_of_tables; if (is_array($tables_included)) { $tables = Table::where('restaurant_id', $this->restaurant_id)->get(); $max_covers = 0; foreach($tables as $table){ if (in_array($table->id, $tables_included)){ $max_covers += intval($table->max_seats); } } return $max_covers; } return 0; } public function totalCovers() { switch ($this->availability_type) { case 'tables': return intval( $this->compute('tables_max_total_covers') ); break; case 'specific_tables': return $this->maxCoversForEspecificTables(); break; case 'volume_total': return $this->availability_total; break; case 'volume_slots': $slots = $this->availability_slots; if (is_array($slots)){ $count = 0; foreach ($slots as $slot){ $count += intval($slot['covers']); } return $count; } return 0; break; default: return 0; } } /** * Count number of tables based on avaiability type * @return int|string */ public function totalTables() { switch ($this->availability_type) { case 'tables': $tables = Table::where('restaurant_id', $this->restaurant_id)->get(); return count($tables); break; case 'specific_tables': $tables_included = $this->list_of_tables; if (is_array($tables_included)){ return count($tables_included); } return 0; break; case 'volume_total': return '-'; break; case 'volume_slots': return '-'; break; default: return 0; } } public function getBookingStatusForNewReservation($date, $time, $party, $email, $exclude_booking_id = null) { //ray('getBookingStatusForNewReservation: ' . $exclude_booking_id); //$exclude_booking_id = null; // Si ha puesto confirmed para todos if ($this->booking_status == 'confirmed') { return BookingStatus::BOOKED; } // Si se ha puesto pending para todos if ($this->booking_status == 'pending') { return BookingStatus::PENDING; } // Confirmed then pending with rules $status = BookingStatus::BOOKED; $rules = [ 'rulePendingAfterXConfirmed', 'rulePendingMoreThanXSeats', 'rulePendingForSpecificSlots', 'rulePendingForCustomers', 'rulePendingForSpecificDays', 'rulePendingForXhoursBeforeTimeSlot' ]; // If some rule is pending then return pending foreach($rules as $rule) { $status = $this->{$rule}($date, $time, $party, $status, $email, $exclude_booking_id); if ($status == BookingStatus::PENDING) return $status; } return $status; } public function rulePendingAfterXConfirmed($date, $time, $party, $status, $email, $exclude_booking_id = null) { //ray('rulePendingAfterXConfirmed: ' . $exclude_booking_id); // Check if rule is enabled if ($this->rule_bookings_enable !== true) { return $status; } // Get all bookings confirmed or reconfirmed $query = Booking::where(evavel_tenant_field(), $this->restaurant_id) ->where('shift_event_id', $this->id) ->where('date', $date) ->whereIn('status', [BookingStatus::BOOKED, BookingStatus::CONFIRMED]); if ($exclude_booking_id) { $query->where('id', '!=', $exclude_booking_id); } $bookings = $query->get(); $count = 0; foreach($bookings as $booking){ $count += intval($booking->party); } $max = intval($this->status_confirmed_covers); //ray('MAX:'.$max.' COUNT:'.$count); if ($max > 0) { if ( ($count + $party) > $max) { return BookingStatus::PENDING; } } return $status; } public function rulePendingMoreThanXSeats($date, $time, $party, $status, $email, $exclude_booking_id = null) { // Check if rule is enabled if ($this->rule_seats_enable !== true) { return $status; } $seats = intval($this->status_seats_pending); if ($seats > 0){ if (intval($party) >= $seats) { return BookingStatus::PENDING; } } return $status; } public function rulePendingForSpecificSlots($date, $time, $party, $status, $email, $exclude_booking_id = null) { // Check if rule is enabled if ( $this->rule_slots_enable !== true ) { return $status; } $list = $this->status_per_slot; if (!is_array($list)) return $status; foreach($list as $item) { if ($item['time'] == $time) { return $item['pending'] == true ? BookingStatus::PENDING : $status; } } return $status; } public function rulePendingForCustomers($date, $time, $party, $status, $email, $exclude_booking_id = null) { // Check tags selected and then customers with those tags // If customer email fits email then put as pending // Check if rule is enabled if ( $this->rule_customers_enable !== true ) { return $status; } $tags_id = $this->status_tags_customers; if (!$tags_id || empty($tags_id)) { return $status; } // Hay que buscar el usuario dentro del restaurante actual $customer = Customer::where('restaurant_id', $this->restaurant_id) ->where('email', $email) ->first(); $customer_tags_id = $customer->tags->pluck('id'); if (!$customer_tags_id || empty($customer_tags_id)) { return $status; } foreach($tags_id as $tag_id) { if (in_array($tag_id, $customer_tags_id)) { $status = $this->rule_customers_status; if ($status == BookingStatus::DENIED) { return BookingStatus::DENIED; } return BookingStatus::PENDING; } } return $status; } public function rulePendingForSpecificDays($date, $time, $party, $status, $email, $exclude_booking_id = null) { // Check if rule is enabled if ($this->rule_exclude_days_enable !== true) { return $status; } $days = $this->status_exclude_days; if (!$days || empty($days) || !is_array($days)) { return $status; } if (in_array($date, $days)) { return BookingStatus::PENDING; } return $status; } public function rulePendingForXhoursBeforeTimeSlot($date, $time, $party, $status, $email, $exclude_booking_id = null) { // Check if rule is enabled if ($this->rule_hours_before_enable !== true) { return $status; } $hours_before = intval($this->rule_hours_before); if ($hours_before <= 0) { return $status; } // Get current time in restaurant's timezone $now = evavel_now_timezone($this->restaurant->timezone); // Get booking datetime using the provided date and seconds since midnight $booking_datetime = evavel_carbon($date, $time, $this->restaurant->timezone); // Calculate difference in hours $diff_in_seconds = $booking_datetime->timestamp - $now->timestamp; $diff_in_hours = $diff_in_seconds / 3600; // If booking is within the hours_before window, mark as pending if ($diff_in_hours <= $hours_before) { return BookingStatus::PENDING; } return $status; } function applyRulePendingForSpecificTables($booking) { // If already pending then skip if ($booking->status == BookingStatus::PENDING) return; // It no tables selected then no need to check $tables_list = $booking->tablesList; if (!is_array($tables_list)) return; // Check if rule is enabled if ($this->rule_exclude_tables_enable !== true) return; $tables_exclude = $this->status_exclude_tables; if (!$tables_exclude || empty($tables_exclude) || !is_array($tables_exclude)) return; // Check if tables is included $is_included = false; foreach($tables_list as $table_id){ if (in_array($table_id, $tables_exclude)){ $is_included = true; } } if ($is_included){ $booking->status = BookingStatus::PENDING; } } public function showServiceDurationInWidget($widget_id) { // Default widget setting $widget_form = \Alexr\Settings\WidgetForm::where('id', $widget_id)->first(); if (!$widget_form) return false; $show_services_duration = isset($widget_form->form_config['show_services_duration']) ? $widget_form->form_config['show_services_duration'] : 'yes'; $show_services_duration = $show_services_duration == 'no' ? false : true; // Custom shift setting $widget_custom = $this->widget_custom; if (!$widget_custom) return $show_services_duration; // Check is enabled overwrite general if (!isset($widget_custom['overwrite_general'])) return null; $overrite_general = $widget_custom['overwrite_general']; if ($overrite_general != '1' && $overrite_general != 1 && $overrite_general != 'true' && $overrite_general !== true) { return $show_services_duration; } if (!isset($widget_custom['show_duration'])) return $show_services_duration; return $widget_custom['show_duration'] == 'yes'; } //====== FIRST SEATING , LAST SEATING , BLOCK SLOTS ================================================================== // Nuevos metodos para poder sobre escribir first_seating, last_seating, block_slots por cada dia de la semana public function getFirstSeatingForDate($date = null) { if ($date == null) return $this->first_seating; $dayOfWeek = strtolower(date('D', strtotime($date))); $day_active = "working_hours_$dayOfWeek"; if ($this->{$day_active}) { $key = "first_seating_$dayOfWeek"; $value = $this->{$key}; if ($value != null) { //ray('First seating custom ' . $value); return $value; } } //ray('First seating general ' . $this->first_seating); return $this->first_seating; } public function getLastSeatingForDate($date = null) { if ($date == null) return $this->last_seating; $dayOfWeek = strtolower(date('D', strtotime($date))); $day_active = "working_hours_$dayOfWeek"; if ($this->{$day_active}) { $key = "last_seating_$dayOfWeek"; $value = $this->{$key}; if ($value != null) { //ray('Last seating custom ' . $value); return $value; } } //ray('Last seating general ' . $this->last_seating); return $this->last_seating; } // Devuelve un listado // [ ["time" => 50400, "block" => false], ["time" => 52200, "block" => false] ] public function getBlockSlotsForDate($date = null) { if ($date == null) return $this->block_slots; $dayOfWeek = strtolower(date('D', strtotime($date))); $day_active = "working_hours_$dayOfWeek"; if ($this->{$day_active}) { $key = "block_slots_$dayOfWeek"; $list_times = $this->{$key}; $first_seating = intval($this->getFirstSeatingForDate($date)); $last_seating = intval($this->getLastSeatingForDate($date)); $interval = intval($this->interval); // Prepara un listado de slots con el formato apropiado $slots = []; for ($i = $first_seating; $i <= $last_seating; $i += $interval) { $slot = [ 'time' => $i, 'block' => false ]; if (is_array($list_times) && in_array($i, $list_times)){ $slot['block'] = true; } $slots[] = $slot; } //ray('SLOTS CUSTOM', $slots); return $slots; } //ray('SLOTS GENERAL', $this->block_slots); return $this->block_slots; } //====================================================================================== // Next funcions are OLD stuff // @TODO (OLD) refactor with the new functions public function calculateMaxDaysInAdvance() { // If not active do not take into account if (!$this->isBookable()) return -1; $maxDaysInAdvance = 0; // Check last day of the period $today = evavel_date_now(); $endDate = evavel_date_createFromFormat('Y-m-d H:i:s', $this->end_date.' 23:59:59'); $diffDays = $endDate->diffInDays(evavel_date_now()); $isFuture = $today <= $endDate; if($isFuture) { $maxDaysInAdvance = $diffDays; } // Check the opening setting $open_reservation_mode = $this->open_reservation_mode; switch ($open_reservation_mode) { case 'open_all_time': // Do nothing break; case 'open_hours_before': $seconds = $this->open_hours_before; $days = intval($seconds/86400); if ($days < $maxDaysInAdvance) { $maxDaysInAdvance = $days; } break; case 'open_same_day_at_time': $maxDaysInAdvance = 0; break; case 'open_days_before_at_time': if ($this->open_several_days_count < $maxDaysInAdvance){ $maxDaysInAdvance = $this->open_several_days_count; } break; default: break; } return $maxDaysInAdvance; } /** * Calculate covers available when selecting areas in availability_type = volume_total * @param $date_string * @param $time * @param $guests * * @param int|null $exclude_booking_id reserva que no se cuenta (al editarla desde el dashboard) * * @return array */ public function getMaxCoversPerArea($date_string, $time, $guests, $exclude_booking_id = null) { // Get areas // MAX: Get max values set in the shift when availability type // OCCUPIED: Get bookings and count seats for each area when status is confirmed $setting = $this->getSettingsMaxCoversPerArea(); $areas = Area::where('restaurant_id', $this->restaurant_id)->where('bookable_online',1)->get(); $list = []; $required_time_start = $time; $required_time_end = $required_time_start + $this->getDuration($guests); foreach($areas as $area) { // Get seats used for this area selected $query = Booking::where('restaurant_id', $this->restaurant_id) ->where('shift_event_id', $this->id) ->where('date', $date_string) ->where('area_selected_id', $area->id) ->whereIn('status', BookingStatus::occupied()); if ($exclude_booking_id) { $query->where('id', '!=', $exclude_booking_id); } $bookings = $query->get(); $occupied = 0; foreach($bookings as $booking){ $booking_start_time = $booking->time; $booking_end_time = $booking->time + $booking->duration; if ( alexr_is_range_overlapping($required_time_start, $required_time_end, $booking_start_time, $booking_end_time) ) { $occupied += intval($booking->party); } } $max_covers = isset($setting['area-'.$area->id]) ? $setting['area-'.$area->id] : 0; $list[] = [ 'area_id' => $area->id, 'max_covers' => $max_covers, // Viene de los settings 'occupied' => $occupied ]; } return $list; /* [ ['area_id' => 6, 'max_covers' => 50, 'occupied' => 48], ['area_id' => 13, 'max_covers' => 20, 'occupied' => 19], ] */ } /** * Modo de seleccion de area de este servicio. * * Hay dos comportamientos distintos y conviene no mezclarlos: * * 'covers' -> volume_total: el area tiene un AFORO propio configurado en * covers_can_select_area_maxperarea. Es el caso de "Terraza 36, * Bodega 40". * 'tables' -> servicios por mesas: el area no tiene aforo, solo restringe * de que area se asignan las mesas. * * @return string|false 'covers' | 'tables' | false si no se puede elegir area */ public function areaSelectionMode() { if ($this->availability_type == self::AVAILABILITY_VOLUME_TOTAL) { $mode = $this->covers_can_select_area; return in_array($mode, ['yes_area', 'yes_panorama_area']) ? 'covers' : false; } if (in_array($this->availability_type, [ self::AVAILABILITY_ALL_TABLES, self::AVAILABILITY_SPECIFIC_TABLES, self::AVAILABILITY_TABLES_SCHEDULE, ])) { $mode = $this->can_select_area; return ($mode && $mode != 'no') ? 'tables' : false; } return false; } /** * Aforo de un area concreta para una fecha y hora. * * OJO con el aforo 0: getMaxCoversPerArea() devuelve 0 para las areas que no * tienen aforo configurado, y eso significa "sin plazas", no "sin limite". * Es la misma lectura que hace el widget, que bloquea la seleccion cuando * los comensales superan las plazas libres. * * @param string $date_string * @param int $time * @param int $guests * @param int $area_id * @param int|null $exclude_booking_id reserva que no se cuenta (al editarla) * * @return array|null ['max' =>, 'occupied' =>, 'free' =>] o null si el area * no es reservable online en este restaurante */ public function getAreaCoversAvailability($date_string, $time, $guests, $area_id, $exclude_booking_id = null) { $list = $this->getMaxCoversPerArea($date_string, $time, $guests, $exclude_booking_id); foreach ($list as $item) { if ($item['area_id'] != $area_id) continue; $max = intval($item['max_covers']); $occupied = intval($item['occupied']); return [ 'max' => $max, 'occupied' => $occupied, 'free' => max(0, $max - $occupied), ]; } return null; } /** * Caben $guests comensales en esta area? * * Esta es la comprobacion que hasta ahora solo existia en el JavaScript del * widget (selectArea()), nunca en el servidor. Al centralizarla aqui la * pueden usar tanto el widget como la Bot API y no se pueden separar. * * @return bool */ public function isAreaAvailableForCovers($date_string, $time, $guests, $area_id) { $info = $this->getAreaCoversAvailability($date_string, $time, $guests, $area_id); // Area desconocida o no reservable online if ($info === null) return false; return $guests <= $info['free']; } /** * Aforo por area para VARIOS slots de un mismo dia. * * getMaxCoversPerArea() lanza una consulta por area, asi que llamarla dentro * de un bucle de slots dispara las consultas (49 slots x 2 areas = 98). * Aqui se cargan las reservas del dia una sola vez y se cruzan en memoria. * * @param string $date_string * @param array $slots lista de horas en segundos * @param int $guests * * @return array [ slot => [ ['area_id','area_name','max','occupied','free'], ... ] ] */ public function getAreasAvailabilityForSlots($date_string, array $slots, $guests) { $setting = $this->getSettingsMaxCoversPerArea(); $areas = Area::where('restaurant_id', $this->restaurant_id) ->where('bookable_online', 1) ->get(); // Una sola consulta para todo el dia $bookings = Booking::where('restaurant_id', $this->restaurant_id) ->where('shift_event_id', $this->id) ->where('date', $date_string) ->whereIn('status', BookingStatus::occupied()) ->get(); $duration = $this->getDuration($guests); $result = []; foreach ($slots as $time) { $required_time_start = intval($time); $required_time_end = $required_time_start + $duration; $list = []; foreach ($areas as $area) { $occupied = 0; foreach ($bookings as $booking) { if ($booking->area_selected_id != $area->id) continue; if (alexr_is_range_overlapping( $required_time_start, $required_time_end, $booking->time, $booking->time + $booking->duration) ) { $occupied += intval($booking->party); } } $max = isset($setting['area-'.$area->id]) ? intval($setting['area-'.$area->id]) : 0; $list[] = [ 'area_id' => intval($area->id), 'area_name' => $area->name, 'max' => $max, 'occupied' => $occupied, 'free' => max(0, $max - $occupied), ]; } $result[intval($time)] = $list; } return $result; } /** * Use when availability_type = volume_total * Get settings for max covers per area * @return int[] */ public function getSettingsMaxCoversPerArea() { // area_id:max , area_id:max /* [ "area" => 6, "value" => "45" ], [ "area" => 13, "value" => "12" ] */ $values = $this->covers_can_select_area_maxperarea; // 6:30,13:20 if (is_array($values)) { $list = []; foreach($values as $arr) { $list['area-'.$arr['area']] = intval($arr['value']); } return $list; /* [ 'area-6' => 50, 'area-13' => 45, ]*/ } return null; } public function isBlockingTables() { if ($this->skip_block_tables == 1) { return false; } return true; } }