isActive(); $eventTypes = is_array($eventTypes) ? $eventTypes : ($active ? $this->getEventTypes() : []); return [ 'active' => $active, 'configured' => $active && !empty($eventTypes), 'admin_url' => esc_url_raw(admin_url('admin.php?page=fluent-booking#/')), 'message' => $active ? '' : __('FluentBooking is not installed or active.', 'fluent-support') ]; } /** * Returns all active, publicly-accessible event types the current user can read, shaped for the frontend selector. * * @return array */ public function getEventTypes() { if (!$this->isActive()) { return []; } $events = \FluentBooking\App\Models\CalendarSlot::query() ->with('calendar') ->where('status', 'active') ->orderBy('id', 'DESC') ->get(); $eventTypes = []; foreach ($events as $event) { if (!$event->calendar || !self::canUseEvent($event)) { continue; } $eventTypes[] = [ 'id' => (int) $event->id, 'title' => sanitize_text_field($event->title), 'duration' => (int) $event->duration, 'calendar_title' => sanitize_text_field($event->calendar->title), ]; } return $eventTypes; } /** * Generates a tokenised booking URL and both HTML and plain-text email content. * * Each selected slot is validated against live availability before being embedded. * * @param Ticket $ticket * @param int $eventId * @param string $message * @param array $selectedSlots * @param string $timezone * @throws \Exception * @return array */ public function createBookingLink(Ticket $ticket, $eventId, $message = '', $selectedSlots = [], $timezone = '') { if (!$this->isActive()) { throw new \Exception(esc_html__('FluentBooking is not installed or active.', 'fluent-support')); } $event = \FluentBooking\App\Models\CalendarSlot::with('calendar')->find($eventId); if (!$event || !$event->calendar || !self::canUseEvent($event)) { throw new \Exception(esc_html__('Selected FluentBooking event type is not available.', 'fluent-support')); } $linkToken = bin2hex(random_bytes(16)); $bookingUrl = self::addBookingLinkToken(self::getEventUrl($event, $ticket), $linkToken); $message = trim(wp_unslash($message)); $availability = new BookingAvailabilityHelper(); $selectedSlots = $availability->sanitizeSelectedSlots($selectedSlots, $event, $ticket, (int) $event->getDuration()); $selectedSlots = $availability->addTokenToSlots($selectedSlots, $linkToken); $slotsHtml = $availability->formatSlotsHtml($selectedSlots, $event, $timezone); $htmlParts = []; if ($message !== '') { $htmlParts[] = '
' . esc_html($message) . '
'; } if ($slotsHtml) { $htmlParts[] = $slotsHtml; } $htmlParts[] = '' . esc_html__('See all available times', 'fluent-support') . '
'; return [ 'html' => wp_kses_post(implode('', $htmlParts)), 'plain_text' => $availability->formatSlotsPlainText($message, $selectedSlots, $bookingUrl), ]; } /** * Fetches and formats available time slots for the given event and date range, grouped by day for the UI preview. * * @param int $eventId * @param string $range * @param string $timezone * @param int|null $duration * @param Ticket|null $ticket * @param array $selectedDates * @param string $calendarMonth * @throws \Exception * @return array */ public function getAvailabilitySlots($eventId, $range = 'next_3_days', $timezone = '', $duration = null, Ticket $ticket = null, $selectedDates = [], $calendarMonth = '') { if (!$this->isActive()) { throw new \Exception(esc_html__('FluentBooking is not installed or active.', 'fluent-support')); } $event = \FluentBooking\App\Models\CalendarSlot::with('calendar')->find($eventId); if (!$event || !$event->calendar || !self::canUseEvent($event)) { throw new \Exception(esc_html__('Selected FluentBooking event type is not available.', 'fluent-support')); } $availability = new BookingAvailabilityHelper(); $timezone = $availability->sanitizeTimezone($timezone, $event->calendar->author_timezone); $rangeData = $availability->getAvailabilityRange($range, $timezone, $selectedDates, $calendarMonth); $days = array_map(function ($day) { $day['slots'] = array_map(function ($slot) { return [ 'id' => $slot['id'], 'start' => $slot['start'], 'time_label' => $slot['time_label'], ]; }, $day['slots']); return $day; }, $availability->fetchFormattedDays($event, $rangeData, $timezone, $ticket, (int) $event->getDuration($duration))); return [ 'timezone' => $timezone, 'max_lookup_date' => $event->getMaxLookUpDate(), 'days' => $days, ]; } /** * Returns upcoming and past bookings for the ticket's customer. * * When stored link tokens exist the query is scoped to those event types and date range. * * @param Ticket $ticket * @return array */ public function getTicketMeetings(Ticket $ticket) { $links = new BookingLinkManager(); $generatedLinks = $links->getLinksForTicket($ticket); $meetings = [ 'upcoming' => [], 'past' => [], 'message' => '', ]; if (!$this->isActive() || !$ticket->customer || !$ticket->customer->email) { $meetings['message'] = __('FluentBooking is not installed or the ticket customer has no email address.', 'fluent-support'); return $meetings; } $eventIds = array_values(array_unique(array_filter(array_column($generatedLinks, 'event_type_id')))); $earliestLinkDate = $links->getEarliestLinkDate($generatedLinks); $bookingQuery = \FluentBooking\App\Models\Booking::query() ->with(['calendar', 'calendar_event']) ->where('email', sanitize_email($ticket->customer->email)) ->where('status', '!=', 'reserved'); if ($eventIds) { $bookingQuery->whereIn('event_id', $eventIds); } if ($earliestLinkDate) { $bookingQuery->where('created_at', '>=', $earliestLinkDate); } $bookings = $bookingQuery->orderBy('start_time', 'DESC')->limit(10)->get(); foreach ($bookings as $booking) { $formatted = $links->formatBooking($booking); if (strtotime($booking->end_time) >= time() && $booking->status === 'scheduled') { $meetings['upcoming'][] = $formatted; } else { $meetings['past'][] = $formatted; } } usort($meetings['upcoming'], function ($a, $b) { return strtotime($a['start_time']) <=> strtotime($b['start_time']); }); if (!$bookings->count() && $generatedLinks) { $meetings['message'] = __('No meeting booked yet from these links.', 'fluent-support'); } elseif (!$bookings->count()) { $meetings['message'] = __('No FluentBooking meetings found for this customer.', 'fluent-support'); } return $meetings; } /** * Hook: fluent_booking/after_booking_scheduled. * * Appends an internal timeline note when a customer books via a link sent from this ticket. * * @param object $booking * @param object $calendarSlot * @param array $bookingData * @return void */ public function handleBookingScheduled($booking, $calendarSlot, $bookingData = []) { $links = new BookingLinkManager(); $ticket = $links->getTicketFromBooking($booking, $bookingData); if (!$ticket || $links->hasTimelineNote($booking)) { return; } $formatted = $links->formatBooking($booking); $personId = (int) $ticket->agent_id ?: (int) $ticket->customer_id; $eventTitle = Arr::get($formatted, 'event_title') ?: __('Meeting', 'fluent-support'); $content = sprintf( '%1$s