sanitizeSpecificDates($selectedDates, $timezoneObject); $calendarMonth = $this->sanitizeCalendarMonth($calendarMonth, $timezoneObject); if ($range === 'specific_dates' && $selectedDates) { $start = new \DateTime($selectedDates[0] . ' 00:00:00', $timezoneObject); $end = new \DateTime(end($selectedDates) . ' 23:59:59', $timezoneObject); return [ 'key' => $range, 'start' => $start->format('Y-m-d H:i:s'), 'end' => $end->format('Y-m-d H:i:s'), 'days' => count($selectedDates), 'selected_dates' => $selectedDates ]; } if ($range === 'specific_dates' && $calendarMonth) { $start = new \DateTime($calendarMonth . '-01 00:00:00', $timezoneObject); $end = clone $start; $end->modify('last day of this month')->setTime(23, 59, 59); return [ 'key' => $range, 'start' => $start->format('Y-m-d H:i:s'), 'end' => $end->format('Y-m-d H:i:s'), 'days' => (int) $end->format('j') ]; } $start = new \DateTime('today', $timezoneObject); $rangeDays = [ 'next_3_days' => 3, 'this_week' => 7, 'next_week' => 7, 'next_14_days' => 14 ]; if ($range === 'next_week') { $start->modify('+7 days'); } elseif (!isset($rangeDays[$range])) { $range = 'next_3_days'; } $end = clone $start; $end->modify('+' . ($rangeDays[$range] - 1) . ' days')->setTime(23, 59, 59); return [ 'key' => $range, 'start' => $start->format('Y-m-d H:i:s'), 'end' => $end->format('Y-m-d H:i:s'), 'days' => $rangeDays[$range] ]; } /** * Initialises the FluentBooking time-slot service, fetches available spots, and returns formatted day/slot arrays. * * Applies the fluent_booking/available_slots_for_view filter before formatting. * Throws on service init failure so callers can handle it at the appropriate level. * * @param object $event * @param array $rangeData * @param string $timezone * @param Ticket|null $ticket * @param int|null $duration * @throws \Exception * @return array */ public function fetchFormattedDays($event, $rangeData, $timezone, Ticket $ticket = null, $duration = null) { $timeSlotService = \FluentBooking\App\Hooks\Handlers\TimeSlotServiceHandler::initService($event->calendar, $event); if (is_wp_error($timeSlotService)) { throw new \Exception(esc_html($timeSlotService->get_error_message())); } $availableSpots = $timeSlotService->getAvailableSpots($rangeData['start'], $timezone, (int) $duration); if (is_wp_error($availableSpots)) { $availableSpots = []; } $availableSpots = apply_filters( 'fluent_booking/available_slots_for_view', array_filter((array) $availableSpots), $event, $event->calendar, $timezone, (int) $duration ); return $this->formatDays($availableSpots, $rangeData, $timezone, $event, $ticket, $duration); } /** * Validates user-submitted slot start times against live FluentBooking availability. * * Returns only slots that are still bookable. * * @param array $selectedSlots * @param object $event * @param Ticket $ticket * @param int|null $duration * @return array */ public function sanitizeSelectedSlots($selectedSlots, $event, Ticket $ticket, $duration = null) { if (!is_array($selectedSlots)) { return []; } $timezone = $this->sanitizeTimezone('', $event->calendar->author_timezone); $timezoneObject = new \DateTimeZone($timezone); $selectedStarts = []; $selectedDates = []; foreach (array_slice($selectedSlots, 0, 10) as $slot) { if (!is_array($slot)) { continue; } $startValue = sanitize_text_field(Arr::get($slot, 'start')); if (!$startValue) { continue; } try { $startDate = new \DateTime($startValue, $timezoneObject); $selectedStarts[] = $startDate->format('Y-m-d H:i:s'); $selectedDates[] = $startDate->format('Y-m-d'); } catch (\Exception $exception) { continue; } } if (!$selectedStarts) { return []; } $selectedStarts = array_values(array_unique($selectedStarts)); $rangeData = $this->getAvailabilityRange('specific_dates', $timezone, $selectedDates); if (empty(Arr::get($rangeData, 'selected_dates'))) { return []; } try { $formattedDays = $this->fetchFormattedDays($event, $rangeData, $timezone, $ticket, $duration); } catch (\Exception $e) { return []; } $availableSlots = []; foreach ($formattedDays as $day) { foreach ((array) Arr::get($day, 'slots', []) as $slot) { $availableSlots[$slot['start']] = $slot; } } $slots = []; foreach ($selectedStarts as $selectedStart) { if (empty($availableSlots[$selectedStart])) { continue; } $availableSlot = $availableSlots[$selectedStart]; $slots[] = [ 'display_text' => $availableSlot['display_text'], 'time_label' => $availableSlot['time_label'], 'start' => $availableSlot['start'], 'booking_url' => $availableSlot['booking_url'] ]; } return $slots; } /** * Appends the link token to the booking_url of each slot so individual slot links are traceable back to this send. * * @param array $selectedSlots * @param string $linkToken * @return array */ public function addTokenToSlots($selectedSlots, $linkToken) { if (!$selectedSlots || !is_array($selectedSlots)) { return []; } return array_map(function ($slot) use ($linkToken) { if (!empty($slot['booking_url'])) { $slot['booking_url'] = FluentBookingService::addBookingLinkToken($slot['booking_url'], $linkToken); } return $slot; }, $selectedSlots); } /** * Builds the HTML block of grouped time-slot links for insertion into an agent reply. * * @param array $selectedSlots * @param object $event * @param string $timezone * @return string */ public function formatSlotsHtml($selectedSlots, $event, $timezone = '') { if (!$selectedSlots) { return ''; } $timezone = sanitize_text_field($timezone) ?: $this->sanitizeTimezone('', Arr::get((array) $event->calendar, 'author_timezone', '')); $timezoneObject = new \DateTimeZone($timezone); $groupedSlots = []; foreach ($selectedSlots as $slot) { $startValue = $slot['start'] ?? ''; $slotUrl = $slot['booking_url'] ?? ''; if (!$startValue) { continue; } $start = new \DateTime($startValue, $timezoneObject); $dateKey = $start->format('Y-m-d'); $groupedSlots[$dateKey] = $groupedSlots[$dateKey] ?? [ 'heading' => wp_date('l, F j', $start->getTimestamp(), $timezoneObject), 'slots' => [] ]; $groupedSlots[$dateKey]['slots'][] = [ 'label' => $slot['time_label'], 'url' => esc_url($slotUrl) ]; } if (!$groupedSlots) { return ''; } $eventTitle = ''; if (!empty($event->title)) { $eventTitle = sanitize_text_field($event->title); } elseif (!empty($event->calendar_title)) { $eventTitle = sanitize_text_field($event->calendar_title); } $eventDuration = $this->formatMeetingDuration((int) $event->getDuration()); $html = '
' . esc_html($eventTitle) . '
'; } if ($eventDuration) { $html .= ''; } if ($timezone) { $html .= ''; } foreach ($groupedSlots as $group) { $html .= '' . esc_html($group['heading']) . '
'; $html .= '