isDateTimeBookable($date_string, $time)) { return false; } // 1. Buscar mesas disponibles con custom schedule - OK funciona bien $available_tables = []; if (isset($this->list_of_tables_with_schedule['tableSchedules']) && is_array($this->list_of_tables_with_schedule['tableSchedules'])) { // Obtener listado ordenado por areas y sidebar ++++++ $tables_ordered = $this->tablesOnlineOrderedByAreaAndSidebar(); //ray('TABLES ORDERED', $tables_ordered); $list_tables_schedule = $this->list_of_tables_with_schedule['tableSchedules']; foreach($tables_ordered as $table) { if (isset($list_tables_schedule[$table['id']])) { $table_available = $this->isTableWithScheduleAvailable( $table['id'], $date_string, $time, $guests, $exclude_booking_id ); if ($table_available) { $available_tables[] = $table_available; } } } } //ray('SINGLE TABLES AVAILABLE:', $available_tables); // 2. Buscar combinaciones con custom schedule $available_groups = []; if (empty($available_tables) && isset($this->list_of_tables_with_schedule['groupSchedules']) && is_array($this->list_of_tables_with_schedule['groupSchedules'])) { $groups_ordered = $this->groupsOnlineOrderedByAreaAndSidebar(); //ray('GROPUS ORDERED', $groups_ordered); $list_groups_schedule = $this->list_of_tables_with_schedule['groupSchedules']; //ray('LIST GROUP SCHEDULE', $list_groups_schedule); foreach($groups_ordered as $group) { if (isset($list_groups_schedule[$group['id']])) { $group_available = $this->isGroupWidthScheduleAvailable( $group['id'], $date_string, $time, $guests, $exclude_booking_id ); if ($group_available) { $available_groups[] = $group_available; } } } } //ray('GROUP TABLES AVAILABLE:', $available_groups); // NO ha encontrado nada if (empty($available_tables) && empty($available_groups)) { return false; } // Mesa sencilla, coger la mas optimizada - OK funciona bien if (is_array($available_tables) && !empty($available_tables) >= 1) { if (count($available_tables) == 1) { return $available_tables; } $best_table = $this->selectBestSingleTable($available_tables, $guests); return [$best_table]; } // Grupo, obtener el mejor grupo // hau que devolver el listado de mesas del grupo, no el grupo if (is_array($available_groups) && !empty($available_groups)) { if (count($available_groups) == 1) { $best_group = $available_groups[0]; } else { $best_group = $this->selectBestGroupTables($available_groups, $guests); } //ray('BEST GROUP', $best_group); if ($return_schedule_combination){ return $best_group; } return $best_group['tables']; } return $available_tables; } // OK, revisada /** * Obtiene todos los slots disponibles para las mesas con tables_schedule * Incluye tanto mesas con custom schedule como default schedule * * @param string $date_string Fecha en formato Y-m-d * @param int $guests Número de invitados * @param int|null $exclude_booking_id ID de reserva a excluir * @return array Lista de slots (horas) disponibles */ public function getAllSlotsForTablesWithSchedule($date_string, $guests, $exclude_booking_id = null) { $available_slots = []; $dayOfWeek = strtolower(date('D', strtotime($date_string))); // Obtener la duración estándar de la reserva basada en el número de invitados $standard_duration = $this->getDuration($guests); // Obtener el rango horario del turno $first_seating = $this->getFirstSeatingForDate($date_string); $last_seating = $this->getLastSeatingForDate($date_string); // 1. Procesar mesas con custom schedule //ray($this->list_of_tables_with_schedule['tableSchedules']); if (isset($this->list_of_tables_with_schedule['tableSchedules']) && is_array($this->list_of_tables_with_schedule['tableSchedules'])) { foreach ($this->list_of_tables_with_schedule['tableSchedules'] as $tableId => $weekSchedule) { // Verificar si la mesa existe y si tiene configuración para este día if (!isset($weekSchedule[$dayOfWeek]) || !$weekSchedule[$dayOfWeek]['active']) { continue; } $table = \Alexr\Models\Table::find($tableId); if (!$table || $guests < $table->min_seats || $guests > $table->max_seats || !$table->isBookableOnline() ) { continue; // Mesa no adecuada para el número de invitados } $dayConfig = $weekSchedule[$dayOfWeek]; // Si es tipo custom, procesar sus slots específicos if ($dayConfig['type'] === 'custom' && isset($dayConfig['schedule'])) { foreach ($dayConfig['schedule'] as $schedule) { foreach ($schedule['reserve'] as $slot) { // Calcular duración específica para este slot en el custom schedule $custom_duration = $schedule['to'] - $slot; // Verificar que el slot esté dentro del horario del schedule if ($slot < $schedule['from'] || $slot >= $schedule['to']) { continue; } // Verificar si la mesa está disponible para este slot con su duración específica if ($this->isTableFreeForTimeRange( $tableId, $date_string, $slot, $slot + $custom_duration, $exclude_booking_id )) { // Añadir el slot a la lista si no está ya if (!in_array($slot, $available_slots)) { $available_slots[] = $slot; } } } } } // He quitado el tipo default, es un follon } } // 2. Procesar grupos de mesas con custom schedule if (isset($this->list_of_tables_with_schedule['groupSchedules']) && is_array($this->list_of_tables_with_schedule['groupSchedules'])) { foreach ($this->list_of_tables_with_schedule['groupSchedules'] as $groupId => $weekSchedule) { // Verificar si el grupo tiene configuración para este día if (!isset($weekSchedule[$dayOfWeek]) || !$weekSchedule[$dayOfWeek]['active']) { continue; } $group = \Alexr\Models\Combination::find($groupId); //ray($group->toArray()); if (!$group || $guests < $group->min_seats || $guests > $group->max_seats || !$group->isBookableOnline()) { //ray('Grupo no adecuado:'. $group->id); continue; // Grupo no adecuado para el número de invitados } $dayConfig = $weekSchedule[$dayOfWeek]; // Si es tipo custom, procesar sus slots específicos if ($dayConfig['type'] === 'custom' && isset($dayConfig['schedule'])) { foreach ($dayConfig['schedule'] as $schedule) { foreach ($schedule['reserve'] as $slot) { // Calcular duración específica para este slot en el custom schedule $custom_duration = $schedule['to'] - $slot; // Verificar que el slot esté dentro del horario del schedule if ($slot < $schedule['from'] || $slot >= $schedule['to']) { continue; } // Verificar si todas las mesas del grupo están disponibles para este slot if ($this->isGroupFreeForTimeRange( $groupId, $date_string, $slot, $slot + $custom_duration, $exclude_booking_id )) { // Añadir el slot a la lista si no está ya if (!in_array($slot, $available_slots)) { $available_slots[] = $slot; } } } } } } } // Ordenar los slots sort($available_slots); //ray($available_slots); return $available_slots; } /** * Determina si una mesa con custom o default schedule está disponible para un día, hora y número de invitados * Considera correctamente la duración de la reserva * * @param int $tableId ID de la mesa * @param string $date_string Fecha en formato Y-m-d * @param int $time Hora en segundos desde medianoche * @param int $guests Número de invitados * @param int|null $exclude_booking_id ID de reserva a excluir (para modificaciones) * @return bool|array False si no está disponible, o array con datos de la mesa si está disponible */ public function isTableWithScheduleAvailable($tableId, $date_string, $time, $guests, $exclude_booking_id = null) { // Obtener la mesa por ID $table = Table::where('id', $tableId)->first(); if (!$table) { return false; } //ray('Table I '.$table->name); // Comprobar si el número de invitados es adecuado para la mesa if ($guests < $table->min_seats || $guests > $table->max_seats) { return false; } // Calcular la duración basada en el número de invitados // Para mesas con custom schedule no es correcto ++++++++++++++++++++++++++++++++++++ $duration = 3600; //$this->getDuration($guests); // Verificar si la mesa está bloqueada manualmente if ($this->restaurant && method_exists($this->restaurant, 'isTableBlocked')) { if ($this->restaurant->isTableBlocked($tableId, $date_string, $time)) { return false; } } // Obtener el día de la semana (mon, tue, wed, etc.) $dayOfWeek = strtolower(date('D', strtotime($date_string))); // Obtener la configuración de la mesa para este día $tableSchedules = $this->list_of_tables_with_schedule['tableSchedules'] ?? []; $dayConfig = $tableSchedules[$tableId][$dayOfWeek]; // Verificar si la mesa está activa para este día if (!$dayConfig['active']) { return false; } // ESTE NO LO USO // Si el tipo es 'default', la mesa está disponible durante todo el turno /*if ($dayConfig['type'] === 'default') { // Comprobar si el horario pedido está dentro del rango del turno $first_seating = $this->getFirstSeatingForDate($date_string); $last_seating = $this->getLastSeatingForDate($date_string); if ($time >= $first_seating && $time <= $last_seating) { // Verificar que el tiempo + duración no exceda el último slot if ($time + $duration > $last_seating) { return false; } // Verificar si la mesa está libre durante toda la duración if ($this->isTableFreeForTimeRange($tableId, $date_string, $time, $time + $duration, $exclude_booking_id)) { return [ 'id' => $table->id, 'name' => $table->name, 'area_id' => $table->area_id, 'min_seats' => $table->min_seats, 'max_seats' => $table->max_seats, 'mode' => 'single', 'booking_id' => null ]; } } return false; }*/ // Si llegamos aquí, el tipo es 'custom' schedule if (!isset($dayConfig['schedule']) || !is_array($dayConfig['schedule'])) { return false; } //ray('Table IV SCHEDULE ++++ ' . $table->name); //ray($dayConfig); // Verificar si el tiempo solicitado está en alguno de los slots reservables foreach ($dayConfig['schedule'] as $schedule) { if (in_array($time, $schedule['reserve'])) { // En este caso la duration es el propio schedule $duration = $schedule['to'] - $time; // Verificar si la mesa está disponible (no hay reservas que se solapen) if ($this->isTableFreeForTimeRange($tableId, $date_string, $time, $time + $duration, $exclude_booking_id)) { // La mesa está disponible, devolver información de la mesa return [ 'id' => $table->id, 'name' => $table->name, 'area_id' => $table->area_id, 'min_seats' => $table->min_seats, 'max_seats' => $table->max_seats, 'mode' => 'single', 'booking_id' => null ]; } } } return false; } /** * Calcula la hora de finalización para una reserva en un custom schedule * * @param int $startTime Hora de inicio en segundos * @param array $schedule Configuración del bloque de horario * @return int Hora de finalización en segundos */ protected function calculateEndTimeForCustomSchedule($startTime, $schedule) { // Por defecto, usar la hora de finalización del bloque $endTime = $schedule['to']; // Encontrar el siguiente slot disponible para determinar la duración $reserveSlots = $schedule['reserve']; sort($reserveSlots); $currentIndex = array_search($startTime, $reserveSlots); if ($currentIndex !== false && isset($reserveSlots[$currentIndex + 1])) { // Si hay un siguiente slot, usamos ese como límite $nextSlot = $reserveSlots[$currentIndex + 1]; $endTime = min($endTime, $nextSlot); } return $endTime; } /** * Verifica si una mesa está libre para un rango de tiempo específico * * @param int $tableId ID de la mesa * @param string $date_string Fecha en formato Y-m-d * @param int $startTime Hora de inicio en segundos * @param int $endTime Hora de finalización en segundos * @param int|null $exclude_booking_id ID de reserva a excluir * @return bool True si la mesa está disponible */ protected function isTableFreeForTimeRange($tableId, $date_string, $startTime, $endTime, $exclude_booking_id = null) { // Buscar reservas que puedan solaparse con este horario // occupied_or_finished: las FINISHED no bloquean por solapamiento (mesa // libre) pero hacen falta abajo para la regla cannot_duplicate_tables $query = Booking::where('restaurant_id', $this->restaurant_id) ->where('date', $date_string) ->whereIn('status', BookingStatus::occupied_or_finished()); if ($exclude_booking_id) { $query->where('id', '!=', $exclude_booking_id); } $bookings = $query->get(); // Comprobar si cannot_duplicate_tables está activado $cannotDuplicateTables = $this->cannot_duplicate_tables == 1; // Determinar si la mesa tiene custom schedule o default schedule $hasCustomSchedule = false; if (isset($this->list_of_tables_with_schedule['tableSchedules'][$tableId])) { // Obtener el día de la semana $dayOfWeek = strtolower(date('D', strtotime($date_string))); // Verificar si la mesa tiene configuración para este día y es de tipo custom if (isset($this->list_of_tables_with_schedule['tableSchedules'][$tableId][$dayOfWeek]) && $this->list_of_tables_with_schedule['tableSchedules'][$tableId][$dayOfWeek]['active'] && $this->list_of_tables_with_schedule['tableSchedules'][$tableId][$dayOfWeek]['type'] === 'custom') { $hasCustomSchedule = true; } } foreach ($bookings as $booking) { $booking_start = $booking->time; $booking_end = $booking->time + $booking->duration; // Comprobar si esta reserva usa la mesa que estamos verificando $booking_tables = $booking->tablesList; if (in_array($tableId, $booking_tables)) { // Si cannot_duplicate_tables está activado y la mesa NO tiene custom schedule, // no permitir ninguna reserva adicional para esta mesa durante todo el turno // (aplica tambien a las FINISHED: la mesa se vendio para todo el turno) if ($cannotDuplicateTables && !$hasCustomSchedule && $booking->shift_event_id == $this->id) { return false; } // Comprobar solapamiento (una FINISHED ya no ocupa: mesa libre) if ($booking->status != BookingStatus::FINISHED && alexr_is_range_overlapping($startTime, $endTime, $booking_start, $booking_end)) { return false; // La mesa está ocupada en este horario } } } // También verificar si la mesa está bloqueada manualmente en este horario if ($this->restaurant && method_exists($this->restaurant, 'isTableBlocked')) { if ($this->restaurant->isTableBlocked($tableId, $date_string, $startTime)) { return false; } } return true; // La mesa está disponible } public function tablesOnlineOrderedByAreaAndSidebar() { // Listado de mesas ordenadas $tables_ordered = Table::where("restaurant_id", $this->restaurant->id) ->where("bookable_online", 1) ->orderBy("ordering", "ASC") ->get(); // Filter by area online $tables = $tables_ordered->filter(function($table){ return $table->area->bookable_online == 1; })->toArrayEachModel(); // Sort por area uasort($tables, function($a, $b) { return $a['area_id'] <=> $b['area_id']; }); return $tables; } public function groupsOnlineOrderedByAreaAndSidebar() { // Listado de mesas ordenadas $groups_ordered = Combination::where("restaurant_id", $this->restaurant->id) ->where("bookable_online", 1) ->orderBy("ordering", "ASC") ->get(); // Filter by area online $groups = $groups_ordered->filter(function($table){ return $table->area->bookable_online == 1; })->toArrayEachModel(); // Transformas list tables en array $groups = array_map(function($group){ $group['list_tables'] = array_map(function($table){ return $table->toArray(); },$group['list_tables'] ); return $group; }, $groups); // Sort por area uasort($groups, function($a, $b) { return $a['area_id'] <=> $b['area_id']; }); return $groups; } // GRUPOS DE MESAS //-------------------------------------- /** * Determina si un grupo (combinación) con custom schedule está disponible para un día, hora y número de invitados * Similar a isTableWithScheduleAvailable pero para combinaciones de mesas * * @param int $groupId ID del grupo/combinación * @param string $date_string Fecha en formato Y-m-d * @param int $time Hora en segundos desde medianoche * @param int $guests Número de invitados * @param int|null $exclude_booking_id ID de reserva a excluir (para modificaciones) * @return bool|array False si no está disponible, o array con datos del grupo si está disponible */ public function isGroupWidthScheduleAvailable($groupId, $date_string, $time, $guests, $exclude_booking_id = null) { // Obtener la combinación por ID $combination = Combination::where('id', $groupId)->first(); if (!$combination) { return false; } // Comprobar si el número de invitados es adecuado para el grupo if ($guests < $combination->min_seats || $guests > $combination->max_seats) { return false; } // Obtener el día de la semana (mon, tue, wed, etc.) $dayOfWeek = strtolower(date('D', strtotime($date_string))); // Obtener la configuración del grupo para este día $groupSchedules = $this->list_of_tables_with_schedule['groupSchedules'] ?? []; //ray('GroupSchedules:',$groupSchedules); if (!isset($groupSchedules[$groupId][$dayOfWeek])) { return false; } $dayConfig = $groupSchedules[$groupId][$dayOfWeek]; // Verificar si el grupo está activo para este día if (!$dayConfig['active']) { return false; } // Si llegamos aquí, el tipo es 'custom' if (!isset($dayConfig['schedule']) || !is_array($dayConfig['schedule'])) { return false; } //ray($dayConfig); // Verificar si el tiempo solicitado está en alguno de los slots reservables // Custom schedule foreach ($dayConfig['schedule'] as $schedule) { if (in_array($time, $schedule['reserve'])) { // Para grupos con custom schedule, la duración es hasta el final del bloque $duration = $schedule['to'] - $time; //ray('Duration:'.$duration); // Verificar si todas las mesas del grupo están disponibles (no hay reservas que se solapen) if ($this->isGroupFreeForTimeRange($groupId, $date_string, $time, $time + $duration, $exclude_booking_id)) { // Obtener las mesas que componen el grupo //ray($combination); //$groupTables = $this->getGroupTables($combination->toArray(), $date_string); //$groupTables = $combination->list_tables_id; $groupTables = $combination->list_tables; //array of objects $groupTables_array = array_map(function($g){ return $g->toArray(); }, $groupTables); // El grupo está disponible, devolver información del grupo return [ 'id' => $combination->id, 'name' => $combination->name, 'area_id' => $combination->area_id, 'min_seats' => $combination->min_seats, 'max_seats' => $combination->max_seats, 'mode' => 'group', 'booking_id' => null, 'tables' => $groupTables_array, ]; } } } return false; } /** * Verifica si todas las mesas de un grupo están libres para un rango de tiempo específico * * @param int $groupId ID del grupo * @param string $date_string Fecha en formato Y-m-d * @param int $startTime Hora de inicio en segundos * @param int $endTime Hora de finalización en segundos * @param int|null $exclude_booking_id ID de reserva a excluir * @return bool True si todas las mesas del grupo están disponibles */ protected function isGroupFreeForTimeRange($groupId, $date_string, $startTime, $endTime, $exclude_booking_id = null) { // Obtener la combinación para acceder a sus mesas $combination = Combination::where('id', $groupId)->first(); if (!$combination) { return false; } // Obtener todas las mesas que componen el grupo $groupTables = $combination->listTables; if (empty($groupTables)) { return false; } // Verificar que cada mesa del grupo esté libre foreach ($groupTables as $table) { if (!$this->isTableFreeForTimeRange($table->id, $date_string, $startTime, $endTime, $exclude_booking_id)) { return false; // Si alguna mesa no está libre, el grupo no está disponible } } // También verificar si el grupo está bloqueado manualmente en este horario if ($this->restaurant && method_exists($this->restaurant, 'isGroupBlocked')) { if ($this->restaurant->isGroupBlocked($groupId, $date_string, $startTime)) { return false; } } return true; // Todas las mesas del grupo están disponibles } // Esto no me hace falta, no lo he probado /** * Calcula la duración para un grupo con custom schedule * Similar a calculateDurationForTableSchedule pero para grupos * * @param int $groupId ID del grupo * @param object $service Servicio/turno * @param string $date_string Fecha * @param int $startTime Hora de inicio * @param int $guests Número de invitados * @param string $duration_type Tipo de duración ('end_of_slot' o 'next_slot') * @return int Duración en segundos */ protected function calculateDurationForGroupSchedule($groupId, $service, $date_string, $startTime, $guests, $duration_type = 'end_of_slot') { $dayOfWeek = strtolower(date('D', strtotime($date_string))); // Obtener la configuración del grupo para este día $groupSchedules = $service->list_of_tables_with_schedule['groupSchedules'] ?? []; if (!isset($groupSchedules[$groupId][$dayOfWeek])) { // Si no hay configuración específica, usar duración estándar return $service->getDuration($guests); } $dayConfig = $groupSchedules[$groupId][$dayOfWeek]; // Si es tipo default, usar duración estándar if ($dayConfig['type'] === 'default') { return $service->getDuration($guests); } // Para custom schedule, encontrar el bloque correspondiente if (isset($dayConfig['schedule']) && is_array($dayConfig['schedule'])) { foreach ($dayConfig['schedule'] as $schedule) { if (in_array($startTime, $schedule['reserve'])) { if ($duration_type === 'next_slot') { // Buscar el siguiente slot para determinar duración más precisa $reserveSlots = $schedule['reserve']; sort($reserveSlots); $currentIndex = array_search($startTime, $reserveSlots); if ($currentIndex !== false && isset($reserveSlots[$currentIndex + 1])) { return $reserveSlots[$currentIndex + 1] - $startTime; } } // Por defecto, hasta el final del bloque return $schedule['to'] - $startTime; } } } // Fallback a duración estándar return $service->getDuration($guests); } }