verify_nonce($restaurant_id) ) { wp_send_json_error(['message' => __eva('Invalid nonce. Forbidden to access data.')]); } // Check restaurant exists $restaurant = \Alexr\Models\Restaurant::where('active', 1)->where('id', $restaurant_id)->first(); if (!$restaurant) { wp_send_json_error(['message' => __eva('Restaurant is not active.')]); } // Check widget exists $widget_id = intval($_REQUEST['widgetId']); $w_form = \Alexr\Settings\WidgetForm::where('id', $widget_id)->first(); if (!$w_form) { wp_send_json_error(['message' => __eva('Widget not found.')]); } // Get the date and guests $date = sanitize_text_field($_REQUEST['date']); $guests = intval($_REQUEST['guests']); // Handle booking modification if needed $exclude_booking_id = $this->getExcludeBookingId(); //$exclude_booking_id = null; //ray('exclude booking id: ' . $exclude_booking_id); // Check service exists if not asking for all services $service_id = sanitize_text_field($_REQUEST['serviceId']); $service = null; if ($service_id != 'all'){ $service_id = intval($service_id); $service = alexr_get_service($service_id); if (!$service) { wp_send_json_error(['message' => __eva('Service not found.')]); } } $days_in_advance = $w_form->form_config['max_days_in_advance']; // Search slots available for date $result = $this->calculateResultsSlotsAvailable($restaurant, $date, $service_id, $service, $guests, $widget_id, $days_in_advance, $exclude_booking_id); // Check if alternative dates should be shown when no slots available if ($result['is_available'] === false) { $show_alternative_dates = $this->getShowAlternativeDays($w_form); if ($show_alternative_dates == 'yes') { $date_today = evavel_now_timezone_formatted($restaurant->timezone, 'Y-m-d'); $alternative_dates = $this->findAlternativeDates( $date, $date_today, 3, // días antes 3, // días después 7, // intervalo de búsqueda antes 7, // intervalo de búsqueda después [ 'restaurant' => $restaurant, 'service_id' => $service_id, 'service' => $service, 'guests' => $guests, 'widget_id' => $widget_id, 'days_in_advance' => $days_in_advance ] ); $result['otherDatesSlots'] = $alternative_dates; // Si no encuentro ningun dia if (empty($result['otherDatesSlots'])) { $result['otherDatesSlots'] = false; } } else { $result['otherDatesSlots'] = false; } } // Add the request to the result $result['request'] = [ 'restaurantId' => $restaurant_id, 'guests' => $guests, 'date' => $date, 'widgetId' => $widget_id, 'serviceId' => $service_id ]; $result['bookingsIdMonth'] = $this->getBookingsYearMonth($restaurant_id, substr($date, 0, 7)); $result['message_not_available'] = $w_form->getMessageSlotsNotAvailable(); // NO ME HACE FALTA, ya tengo get_month_available_dates // Available dates para este service solamente // Hay que sobreescribirlo porque al optimizar el calculo de slots en el servicio // ya no calculo los dates available, solo viene la primera fecha y eso es un problema /*$daysAvailableWithShifts = $restaurant->availableDates($widget_id, true, false); //ray($daysAvailableWithShifts); if ($service_id == 'all') { $daysAvailable = array_keys($daysAvailableWithShifts); } else { // Filtrar solo las fechas que contengan el service_id específico $daysAvailable = []; foreach ($daysAvailableWithShifts as $date => $serviceIds) { if (in_array($service_id, $serviceIds)) { $daysAvailable[] = $date; } } } $result['daysAvailable'] = $daysAvailable;*/ //$endtime = microtime(true); //ray('TIME get_service_slots: ' . ($endtime - $start_time) . ' seconds'); wp_send_json_success($result); } /** * Get exclude_booking_id from modification request * * @return int|null */ private function getExcludeBookingId() { $modify_booking_uuid = isset($_REQUEST['modify_booking_uuid']) ? sanitize_text_field($_REQUEST['modify_booking_uuid']) : null; $exclude_booking_id = null; if ($modify_booking_uuid) { $booking_to_modify = \Alexr\Models\Booking::where('uuid', $modify_booking_uuid) ->where('status', '!=', \Alexr\Enums\BookingStatus::DELETED) ->first(); if ($booking_to_modify) { $exclude_booking_id = $booking_to_modify->id; } } return $exclude_booking_id; } /** * Calculate slots available for the date * Handles both "all services" and specific service * * @param \Alexr\Models\Restaurant $restaurant Restaurant instance * @param string $date Date in Y-m-d format * @param string|int $service_id Service ID or 'all' * @param \Alexr\Settings\Shift|\Alexr\Settings\Event|null $service Service instance * @param int $guests Number of guests * @param int $widget_id Widget ID * @param int $days_in_advance Days in advance for booking * @param int|null $exclude_booking_id Booking ID to exclude * @return array Result array with availability info */ protected function calculateResultsSlotsAvailable($restaurant, $date, $service_id, $service, $guests, $widget_id, $days_in_advance, $exclude_booking_id = null) { // If service_id is 'all', process all services if ($service_id == 'all') { $services = $restaurant->getShiftsAndEvents($widget_id); // Initialize result structure $result = [ 'guests_min' => 1000, 'guests_max' => 1, 'daysAvailable' => [], 'resultDate' => [ 'date' => $date, 'slots' => [] ] ]; // Process each service foreach ($services as $service) { if ($service instanceof \Alexr\Settings\Shift || $service instanceof \Alexr\Settings\Event) { // Use appropriate method based on availability type if ($service->availability_type === 'tables_schedule') { $service_result = $this->calculateSlotsForOneServiceWithTablesSchedule($service, $date, $guests, $restaurant, $days_in_advance, $exclude_booking_id); } else { $service_result = $this->calculateSlotsForOneService($service, $date, $guests, $restaurant, $days_in_advance, $exclude_booking_id); } // Update min/max guests if ($service_result['guests_min'] < $result['guests_min']) { $result['guests_min'] = $service_result['guests_min']; } if ($service_result['guests_max'] > $result['guests_max']) { $result['guests_max'] = $service_result['guests_max']; } // Merge available days foreach ($service_result['daysAvailable'] as $day) { if (!in_array($day, $result['daysAvailable'])) { $result['daysAvailable'][] = $day; } } // Merge slots foreach ($service_result['resultDate']['slots'] as $slot) { $result['resultDate']['slots'][] = $slot; } } } } else { // For a specific service, determine the appropriate method based on availability type if ($service->availability_type === 'tables_schedule') { $result = $this->calculateSlotsForOneServiceWithTablesSchedule($service, $date, $guests, $restaurant, $days_in_advance, $exclude_booking_id); //ray($result); } else { $result = $this->calculateSlotsForOneService($service, $date, $guests, $restaurant, $days_in_advance, $exclude_booking_id); } } // Check if date is closed if ($restaurant->isDateClosed($date)) { $result['resultDate']['slots'] = []; } // Determine overall availability $is_available = false; foreach ($result['resultDate']['slots'] as $slot) { if (isset($slot['available']) && $slot['available'] === true) { $is_available = true; break; } } $result['is_available'] = $is_available; // Attach map of offers referenced by matched offer rules in the slots. // The widget uses this to render the menu selection step without a round-trip. $result['offers'] = $this->collectOffersForSlots($result['resultDate']['slots'], $restaurant); return $result; } /** * Walk the formatted slots, collect unique offer_ids referenced from matched * offer rules and load the Offer records so the widget can render the menu step. * * @param array $slots * @param \Alexr\Models\Restaurant $restaurant * * @return array map of offer_id => offer payload */ protected function collectOffersForSlots($slots, $restaurant) { $offers_map = []; if (!is_array($slots) || empty($slots)) return $offers_map; // Collect unique offer ids referenced by matched rules $ids = []; foreach ($slots as $slot) { if (!isset($slot['offer_rule']) || !is_array($slot['offer_rule'])) continue; $rule = $slot['offer_rule']; if (!isset($rule['offer_id'])) continue; $offer_id = intval($rule['offer_id']); if ($offer_id <= 0) continue; $ids[$offer_id] = true; } if (empty($ids)) return $offers_map; // Restaurant currency so the widget shows menu prices in the right // currency (the menu step is rendered before any payment is computed). $currency = $restaurant->currency; $currency_symbol = \Alexr\Enums\CurrencyType::symbolFor($currency); $offers = \Alexr\Settings\Offer::where('restaurant_id', $restaurant->id) ->whereIn('id', array_keys($ids)) ->get(); foreach ($offers as $offer) { $sections = $offer->sections; if (is_string($sections) && $sections !== '') { $decoded = json_decode($sections, true); if (is_array($decoded)) $sections = $decoded; } if (!is_array($sections)) $sections = []; $offers_map[ $offer->id ] = [ 'id' => $offer->id, 'name' => $offer->name, 'enabled' => (bool) $offer->enabled, 'sections' => $sections, 'currency' => strtolower($currency), 'currency_symbol' => $currency_symbol, ]; } return $offers_map; } /** * Walk formatted slots and attach the matched offer_rule (if any) to each. * No-op when offers are inactive on the service or PRO is not loaded. * * @param array $formatted_slots * @param \Alexr\Settings\Shift|\Alexr\Settings\Event $service * @param string $date * @param int $party party size — used by the rule matcher to enforce * min_guests / max_guests on each offer rule. * * @return array formatted_slots with optional offer_rule key on each slot */ protected function attachOfferRuleToSlots($formatted_slots, $service, $date, $party = null) { if (!method_exists($service, 'isOffersActive')) return $formatted_slots; if (!$service->isOffersActive()) return $formatted_slots; foreach ($formatted_slots as &$slot) { $rule = $service->getOfferRuleMatched($date, $slot['time'], $party); if ($rule !== null) { $slot['offer_rule'] = $rule; } } unset($slot); return $formatted_slots; } /** * Calculate slots for all services * * @param \Alexr\Models\Restaurant $restaurant Restaurant instance * @param string $date Date in Y-m-d format * @param int $guests Number of guests * @param int $widget_id Widget ID * @param int $days_in_advance Days in advance for booking * @param int|null $exclude_booking_id Booking ID to exclude * @return array Results combined from all services */ protected function calculateSlotsForAllServices($restaurant, $date, $guests, $widget_id, $days_in_advance, $exclude_booking_id = null) { $services = $restaurant->getShiftsAndEvents($widget_id); // Initialize result structure $result = [ 'guests_min' => 1000, 'guests_max' => 1, 'daysAvailable' => [], 'resultDate' => [ 'date' => $date, 'slots' => [] ] ]; // Process each service foreach ($services as $service) { if ($service instanceof \Alexr\Settings\Shift || $service instanceof \Alexr\Settings\Event) { $service_result = $this->calculateSlotsForOneService($service, $date, $guests, $restaurant, $days_in_advance, $exclude_booking_id); // Update min/max guests if ($service_result['guests_min'] < $result['guests_min']) { $result['guests_min'] = $service_result['guests_min']; } if ($service_result['guests_max'] > $result['guests_max']) { $result['guests_max'] = $service_result['guests_max']; } // Merge available days foreach ($service_result['daysAvailable'] as $day) { if (!in_array($day, $result['daysAvailable'])) { $result['daysAvailable'][] = $day; } } // Merge slots foreach ($service_result['resultDate']['slots'] as $slot) { $result['resultDate']['slots'][] = $slot; } } } return $result; } /** * Calculate slots for one specific service * * @param \Alexr\Settings\Shift|\Alexr\Settings\Event $service Service object * @param string $date Date in Y-m-d format * @param int $guests Number of guests * @param \Alexr\Models\Restaurant $restaurant Restaurant instance * @param int $days_in_advance Days in advance for booking * @param int|null $exclude_booking_id Booking ID to exclude * @return array Results for this service */ protected function calculateSlotsForOneService($service, $date, $guests, $restaurant, $days_in_advance, $exclude_booking_id = null) { //ray('CALL calculateSlotsForOneService'); // Get available slots based on availability type $available_slots = $service->getAllSlotsAvailable($guests, $date, $exclude_booking_id); $all_slots = $service->getAllBookableSlots($date); $duration = $service->getDuration($guests); // Format slots with availability information $formatted_slots = []; foreach ($all_slots as $slot) { $formatted_slots[] = [ 'available' => in_array($slot, $available_slots), 'name' => $service->name, 'translations' => $service->translations, 'id' => $service->id, 'date' => $date, 'time' => $slot, 'duration' => $duration, 'backcolor' => $service->color, 'textcolor' => $service->color_text ]; } // Check if service is blocked for online bookings if (!$restaurant->isOnlineBookingsEnabled($service->id, $date)) { $formatted_slots = []; } // Attach matched offer rule (if any) per slot $formatted_slots = $this->attachOfferRuleToSlots($formatted_slots, $service, $date, $guests); // Get days available for this service // ¡OPTIMIZACIÓN CRÍTICA! // Solo calculamos daysAvailable si realmente lo necesitamos // Para get_service_slots de un día específico, no lo necesitamos // ANTES (LENTO): //$daysAvailable = $service->availableDates($restaurant->timezone, $days_in_advance); // DESPUÉS (RÁPIDO): // Solo verificamos si el día específico está disponible $daysAvailable = []; if ($service->hasSomeSlotBookable($date, $restaurant->timezone)) { // @TODO Esto genera un problema porque necesito saber los dias disponibles para el servicio ***** $daysAvailable = [$date]; // Solo el día solicitado } return [ 'guests_min' => $service->min_covers_reservation, 'guests_max' => $service->max_covers_reservation, 'daysAvailable' => $daysAvailable, 'resultDate' => [ 'date' => $date, 'slots' => $formatted_slots ] ]; } /** * Función para integrar el nuevo método tables_schedule con get_service_slots * Esta función se añadiría dentro del Trait Alexr_ajax_action_get_service_slots */ function calculateSlotsForOneServiceWithTablesSchedule($service, $date, $guests, $restaurant, $days_in_advance, $exclude_booking_id = null) { // Si el método de disponibilidad no es tables_schedule, usar el método estándar if ($service->availability_type !== 'tables_schedule') { return $this->calculateSlotsForOneService($service, $date, $guests, $restaurant, $days_in_advance, $exclude_booking_id); } // Para tables_schedule, necesitamos un enfoque diferente $all_slots = $service->getAllSlotsForTablesWithSchedule($date, $guests, $exclude_booking_id); $all_bookable_slots = $service->getAllBookableSlots($date); //ray($all_slots); //ray($all_bookable_slots); // Deberia incluir la duracion del slot para custom schedules? // https://claude.ai/chat/4e3039b0-a640-47f4-9943-300a83a8ebaa $duration = $service->getDuration($guests); // Filter closed slots $filters = [ new \Alexr\Settings\Filters\FilterByClosedSlots ]; foreach($filters as $filter) { $all_slots = $filter->apply($service, $all_slots, $guests, $date, 0); } // Formatear los slots con información de disponibilidad $formatted_slots = []; foreach ($all_bookable_slots as $slot) { $formatted_slots[] = [ 'available' => in_array($slot, $all_slots), 'name' => $service->name, 'translations' => $service->translations, 'id' => $service->id, 'date' => $date, 'time' => $slot, 'duration' => $duration, 'backcolor' => $service->color, 'textcolor' => $service->color_text ]; } // Comprobar si el servicio está bloqueado para reservas online if (!$restaurant->isOnlineBookingsEnabled($service->id, $date)) { $formatted_slots = []; } // Adjuntar la offer rule emparejada (si existe) a cada slot $formatted_slots = $this->attachOfferRuleToSlots($formatted_slots, $service, $date, $guests); // Obtener días disponibles para este servicio //$daysAvailable = $service->availableDates($restaurant->timezone, $days_in_advance); // OPTIMIZACIÓN: Solo el día específico $daysAvailable = []; if (!empty(array_filter($formatted_slots, function($slot) { return $slot['available']; }))) { $daysAvailable = [$date]; } return [ 'guests_min' => $service->min_covers_reservation, 'guests_max' => $service->max_covers_reservation, 'daysAvailable' => $daysAvailable, 'resultDate' => [ 'date' => $date, 'slots' => $formatted_slots ] ]; } }