resolveAuthor($request); $query = $this->buildSchedulesQuery($request, $author); $query->groupBy('group_id'); $schedules = $query->paginate(); foreach ($schedules as $schedule) { $this->formatBooking($schedule); } $data = [ 'schedules' => $schedules, 'timezone' => 'UTC' ]; $data['calendar_event_lists'] = CalendarService::getCalendarOptionsByTitle(); if ($author == 'me') { $slotOptions = CalendarService::getSlotOptions(null, get_current_user_id()); $data['slot_options'] = $slotOptions; } if ($request->get('page') == 1) { $this->addCountsForFirstPage($author, $data); } return $data; } public function export(Request $request) { $limit = (int) apply_filters('fluent_booking/data_export_limit', 2000); $author = $this->resolveAuthor($request); $query = $this->buildSchedulesQuery($request, $author); $relations = [ 'calendar_event', 'user', 'booking_activities' => function ($q) { $q->where('type', 'cancel_reason'); }, 'booking_meta', ]; if (defined('FLUENT_BOOKING_PRO_DIR_FILE')) { $relations[] = 'payment_order.transaction'; } $query->with($relations); $total = (clone $query)->withoutEagerLoads()->count(); $limited = $total > $limit; $bookings = $query->take($limit)->get(); $rows = $bookings->map(function ($booking) { $row = ExportHelper::mapBooking($booking); return apply_filters('fluent_booking/booking_export_row', $row, $booking); })->all(); $rows = apply_filters('fluent_booking/booking_export_columns', $rows, $bookings); return [ 'bookings' => $rows, 'limited' => $limited, 'total' => $total, ]; } protected function resolveAuthor(Request $request) { $author = Arr::get($request->get('filters', []), 'author'); if (is_numeric($author)) { $author = (int) $author; } else { $author = sanitize_text_field($author); } $hasPermission = PermissionManager::userCanSeeAllBookings(); if ($hasPermission) { return $author; } if (!$author || $author == 'all') { $authorCalendar = Calendar::where('user_id', get_current_user_id()) ->where('type', 'simple') ->first(); $author = $authorCalendar ? $authorCalendar->id : ''; } return $author; } protected function buildSchedulesQuery(Request $request, $author = null) { $filters = $request->get('filters', []); $search = $request->getSafe('search'); $eventVal = Arr::get($filters, 'event'); $eventId = is_numeric($eventVal) ? (int) $eventVal : 0; $eventType = sanitize_text_field(Arr::get($filters, 'event_type')); $period = sanitize_text_field(Arr::get($filters, 'period', 'upcoming')); $range = array_map('sanitize_text_field', Arr::get($filters, 'range', [])); $email = sanitize_email(Arr::get($filters, 'email', '')); $allowedPeriods = ['upcoming', 'completed', 'cancelled', 'pending', 'no_show', 'latest_bookings', 'all']; if (!in_array($period, $allowedPeriods, true)) { $period = 'upcoming'; } $query = Booking::with(['calendar_event']); $hasPermission = PermissionManager::userCanSeeAllBookings(); if (!$hasPermission || $author == 'me') { $query->where('host_user_id', get_current_user_id()); } if ($author && $author !== 'all') { if ($author != 'me') { $query->where('calendar_id', $author); } if ($eventId > 0) { $query->where('event_id', $eventId); } if ($eventType && $eventType !== 'all') { $query->where('event_type', $eventType); } } if (!empty($email) && is_email($email)) { $query->where('email', $email); } do_action_ref_array('fluent_booking/schedules_query', [&$query]); $query->applyDateRangeFilter($range); $query->applyComputedStatus($period); $query->applyBookingOrderByStatus($period); if (!empty($search)) { $query->searchBy($search); } return $query; } private function addCountsForFirstPage($author, &$data) { $bookingQuery = Booking::query() ->when($author == 'me', function($query) { return $query->where('host_user_id', get_current_user_id()); }) ->when($author && is_numeric($author), function($query) use ($author) { return $query->where('calendar_id', $author); }) ->distinct('group_id'); $data['pending_count'] = (clone $bookingQuery)->whereIn('status', ['pending', 'reserved'])->count('group_id'); $data['no_show_count'] = (clone $bookingQuery)->where('status', 'no_show')->count('group_id'); $data['cancelled_count'] = (clone $bookingQuery)->where('status', 'cancelled')->count('group_id'); } public function patchBooking(Request $request, $bookingId) { $booking = Booking::findOrFail($bookingId); $oldBooking = clone $booking; $data = $request->all(); $this->validate($data, [ 'column' => 'required', ]); do_action('fluent_booking/before_patch_booking_schedule', $booking, $data); $value = $request->get('value'); $column = $data['column']; if ($booking->{$column} == $value) { return $this->sendError(['message' => __('No changes found', 'fluent-booking')]); } $validColumns = [ 'internal_note', 'email', 'phone', 'first_name', 'last_name', 'status', 'payment_status' ]; if (!in_array($column, $validColumns)) { return $this->sendError(['message' => __('Invalid column', 'fluent-booking')]); } if ($column === 'email') { if (!$value || !is_email($value)) { return $this->sendError(['message' => __('Invalid email address', 'fluent-booking')]); } $value = sanitize_email($value); } else { $value = sanitize_text_field($value); } if ($column == 'status') { if (!in_array($value, ['scheduled', 'completed', 'cancelled', 'rejected', 'no_show'])) { return $this->sendError(['message' => __('Invalid status', 'fluent-booking')]); } if ($value == 'scheduled' && $booking->payment_method && $booking->payment_order) { $order = $booking->payment_order; $order->total_paid = $order->total_amount; $order->completed_at = gmdate('Y-m-d H:i:s'); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date $order->status = 'paid'; $order->save(); $updateData['payment_status'] = 'paid'; do_action('fluent_booking/log_booking_activity', $this->getPaymentPaidLog($booking->id)); do_action('fluent_booking/payment/update_payment_status_paid', $booking); } else if ($value == 'scheduled') { do_action('fluent_booking/log_booking_activity', $this->getConfirmLog($booking->id)); } if ($value == 'cancelled') { $cancelReason = sanitize_text_field($data['cancel_reason']); $booking->cancelMeeting($cancelReason, 'host', get_current_user_id()); } if ($value == 'rejected') { $rejectReason = sanitize_text_field($data['reject_reason']); $booking->rejectMeeting($rejectReason, get_current_user_id()); } if (in_array($value, ['cancelled', 'rejected'])) { if ($booking->payment_method && Arr::get($data, 'refund_payment') == 'yes') { do_action('fluent_booking/refund_payment_' . $booking->payment_method, $booking, $booking->calendar_event); } return [ /* translators: %s: Booking status */ 'message' => sprintf(__('The booking has been %s', 'fluent-booking'), $value) ]; } $updateAll = Arr::isTrue($data, 'update_all') && $booking->isMultiGuestBooking(); if ($updateAll && in_array($value, ['no_show', 'completed'])) { Booking::where('event_id', $booking->event_id) ->where('group_id', $booking->group_id) ->update([ 'status' => $value ]); return [ /* translators: %s: Booking status */ 'message' => sprintf(__('The booking has been %s', 'fluent-booking'), $value) ]; } } if ($column == 'payment_status') { if (!in_array($value, ['pending', 'paid'])) { return $this->sendError(['message' => __('Invalid payment status', 'fluent-booking')]); } if ($value == 'paid') { do_action('fluent_booking/log_booking_activity', $this->getPaymentPaidLog($booking->id)); } if ($value == 'pending') { do_action('fluent_booking/log_booking_activity', $this->getPaymentPendingLog($booking->id)); } if ($booking->payment_order) { do_action('fluent_booking/payment/status_changed', $booking->payment_order, $booking, $value); } } $updateData[$column] = $value; $booking->fill($updateData); $booking->save(); if ($column === 'status') { do_action('fluent_booking/booking_schedule_' . $value, $booking, $booking->calendar_event); do_action('fluent_booking/pre_after_booking_' . $value, $booking, $booking->calendar_event); $booking = Booking::with(['calendar_event', 'calendar'])->find($booking->id); do_action('fluent_booking/after_booking_' . $value, $booking, $booking->calendar_event, $booking); } do_action('fluent_booking/after_patch_booking_schedule', $booking, $oldBooking); do_action('fluent_booking/after_patch_booking_' . $column, $booking, $booking->calendar_event, $oldBooking->{$column}); return [ /* translators: Updated column name */ 'message' => sprintf(__('%s has been updated', 'fluent-booking'), ucfirst($column)) ]; } public function getBooking(Request $request, $bookingId) { $booking = Booking::with('calendar_event'); if (!PermissionManager::userCanSeeAllBookings()) { $booking->whereHostAccess(get_current_user_id()); } $booking = $booking->findOrFail($bookingId); $booking = $this->formatBooking($booking); do_action_ref_array('fluent_booking/booking_schedule', [&$booking]); $data = [ 'schedule' => $booking ]; if (in_array('all_data', $this->request->get('with', []))) { $data = array_merge($data, $this->getBookingMetaInfo($request, $bookingId)); } return $data; } public function deleteBooking(Request $request, $bookingId) { $booking = Booking::findOrFail($bookingId); do_action('fluent_booking/before_delete_booking', $booking); $booking->delete(); do_action('fluent_booking/after_delete_booking', $bookingId); if ($booking->isMultiGuestBooking()) { Booking::where('event_id', $booking->event_id)->where('group_id', $booking->group_id)->delete(); } return [ 'message' => __('Booking Deleted Successfully!', 'fluent-booking') ]; } public function sendConfirmationEmail(Request $request, $bookingId) { $booking = Booking::with(['calendar', 'calendar_event'])->findOrFail($bookingId); $emailTo = $request->get('email_to', 'guest'); $notifications = $booking->calendar_event->getNotifications(); $email = Arr::get($notifications, 'booking_conf_attendee.email', []); if ($emailTo == 'host') { $email = Arr::get($notifications, 'booking_conf_host.email', []); } $result = EmailNotificationService::emailOnBooked($booking, $email, $emailTo, 'scheduled', true); if (!$result) { return $this->sendError(['message' => __('Notification sending failed', 'fluent-booking')]); } return [ 'message' => __('Notification sent successfully', 'fluent-booking') ]; } public function getGroupAttendees(Request $request, $groupId) { $booking = Booking::with('slot'); if (!PermissionManager::userCanSeeAllBookings()) { $booking->whereHostAccess(get_current_user_id()); } $booking = $booking->where('group_id', $groupId)->first(); if (!$booking || !$booking->isMultiGuestBooking()) { return $this->sendError(['message' => __('Invalid group id or the event is not a group event', 'fluent-booking')]); } $attendees = Booking::where('group_id', $booking->group_id); $search = $request->getSafe('search'); if (!empty($search)) { $attendees = $attendees->searchBy($search); } $attendees = $attendees->paginate(); foreach ($attendees as $attendee) { $attendee = $this->formatBooking($attendee); } return [ 'attendees' => $attendees ]; } public function getBookingActivities(Request $request, $bookingId) { $this->resolveOwnedBookingOrFail($bookingId); $activities = BookingActivity::where('booking_id', $bookingId) ->orderBy('id', 'DESC') ->get(); return [ 'activities' => $activities ]; } public function getBookingMetaInfo(Request $request, $bookingId) { $booking = $this->resolveOwnedBookingOrFail($bookingId); $activities = BookingActivity::where('booking_id', $booking->id) ->orderBy('id', 'DESC') ->get(); $activities->each(function ($activity) { $activity->description = wp_unslash($activity->description); }); $sidebarContents = []; $mainBodyContents = []; $crmProfile = CrmContactService::getProfileData($booking->email); if ($crmProfile) { $sidebarContents[] = [ 'id' => 'fluent_crm_profule', 'title' => __('CRM Profile', 'fluent-booking'), 'type' => 'crm_profile', 'profile' => $crmProfile, ]; } $order = null; if ($booking->payment_status && $booking->payment_order) { $order = $booking->payment_order; $relations = ['items', 'transaction']; if (method_exists($order, 'discounts')) { $relations[] = 'discounts'; } $order->load($relations); $order->currency_sign = CurrenciesHelper::getCurrencySign($order->currency); } $mainBodyContents = apply_filters('fluent_booking/booking_meta_info_main_meta', $mainBodyContents, $booking); $mainBodyContents = apply_filters('fluent_booking/booking_meta_info_main_meta_' . $booking->source, $mainBodyContents, $booking); return [ 'activities' => $activities, 'sidebar_contents' => $sidebarContents, 'payment_order' => $order, 'main_body_contents' => $mainBodyContents ]; } /** * Return the CRM contact state for a booking plus the full tag/list option * sets, so the admin can manage tags/lists inline from the CRM Profile card. */ public function getCrmContact(Request $request, $bookingId) { $booking = $this->resolveOwnedBookingOrFail($bookingId); if (!CrmContactService::isActive()) { return $this->sendError([ 'message' => __('FluentCRM is not active.', 'fluent-booking') ]); } $state = CrmContactService::getContactState($booking->email); if (!$state) { return $this->sendError([ 'message' => __('No CRM contact found for this booking.', 'fluent-booking') ], 404); } return $state; } /** * Guest-field prefill for the CRM "Book Appointment" action. * Keyed by contact id, so it carries its own CRM-read gate. */ public function getCrmContactPrefill(Request $request) { if (!CrmContactService::isActive()) { return $this->sendError([ 'message' => __('FluentCRM is not active.', 'fluent-booking') ]); } if (!CrmPermissionManager::currentUserCan('fcrm_read_contacts')) { return $this->sendError([ 'message' => __('You do not have permission to read CRM contacts.', 'fluent-booking') ], 403); } $contactId = absint($request->get('contact_id')); if (!$contactId) { return $this->sendError([ 'message' => __('Invalid contact.', 'fluent-booking') ], 422); } $prefill = CrmContactService::getBookingPrefillData($contactId); if (!$prefill) { return $this->sendError([ 'message' => __('No CRM contact found.', 'fluent-booking') ], 404); } return [ 'prefill' => $prefill, ]; } /** * Typeahead search for the admin booking modal's CRM contact picker. * Same intersection gate as getCrmContactPrefill: booking access (policy) AND fcrm_read_contacts. */ public function searchCrmContacts(Request $request) { if (!CrmContactService::isActive()) { return $this->sendError([ 'message' => __('FluentCRM is not active.', 'fluent-booking') ]); } if (!CrmPermissionManager::currentUserCan('fcrm_read_contacts')) { return $this->sendError([ 'message' => __('You do not have permission to read CRM contacts.', 'fluent-booking') ], 403); } $search = sanitize_text_field($request->get('search', '')); return [ 'contacts' => CrmContactService::searchContacts($search), ]; } /** * Bounded, searchable tag/list options for the CRM Profile picker. */ public function getCrmOptions(Request $request, $bookingId) { $this->resolveOwnedBookingOrFail($bookingId); if (!CrmContactService::isActive()) { return $this->sendError([ 'message' => __('FluentCRM is not active.', 'fluent-booking') ]); } $type = $request->get('type') === 'lists' ? 'lists' : 'tags'; $search = sanitize_text_field($request->get('search', '')); return [ 'options' => CrmContactService::getOptions($type, $search), ]; } public function updateCrmTags(Request $request, $bookingId) { return $this->updateCrmTaxonomy($request, $bookingId, 'tags'); } public function updateCrmLists(Request $request, $bookingId) { return $this->updateCrmTaxonomy($request, $bookingId, 'lists'); } private function updateCrmTaxonomy(Request $request, $bookingId, $type) { $booking = $this->resolveOwnedBookingOrFail($bookingId); if (!CrmContactService::isActive()) { return $this->sendError([ 'message' => __('FluentCRM is not active.', 'fluent-booking') ]); } $requestKey = $type === 'tags' ? 'tag_ids' : 'list_ids'; $desired = (array) $request->get($requestKey, []); $result = CrmContactService::syncTaxonomy($booking->email, $type, $desired); if ($result === null) { return $this->sendError([ 'message' => __('No CRM contact found for this booking.', 'fluent-booking') ], 404); } return $this->sendSuccess(array_merge([ 'message' => __('CRM contact updated successfully.', 'fluent-booking'), ], $result)); } private function resolveOwnedBookingOrFail($bookingId) { $booking = Booking::findOrFail($bookingId); if (PermissionManager::userCanSeeAllBookings()) { return $booking; } $allowedIds = $booking->getHostIds(); if (PermissionManager::userCan('manage_own_calendar')) { if ($event = CalendarSlot::find($booking->event_id)) { $allowedIds = array_merge($allowedIds, $event->getHostIds()); } } if (in_array(get_current_user_id(), $allowedIds)) { return $booking; } $exception = new ModelNotFoundException(); $exception->setModel(Booking::class, [$bookingId]); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped throw $exception; } private function formatBooking(&$booking) { $autoCompleteTimeOut = (int) Helper::getGlobalAdminSetting('auto_complete_timing', 60) * 60; // 10 minutes if (in_array($booking->status, ['scheduled', 'pending']) && (time() - strtotime($booking->end_time)) > $autoCompleteTimeOut) { $bookingStatus = $booking->status == 'pending' ? 'cancelled' : 'completed'; $booking->status = $bookingStatus; $booking->save(); $hookName = $bookingStatus === 'cancelled' ? 'auto_cancelled' : $bookingStatus; do_action('fluent_booking/booking_schedule_' . $hookName, $booking, $booking->calendar_event); } if ($booking->isMultiHostBooking()) { $booking->host_profiles = $booking->getHostProfiles(); } if ($booking->isMultiGuestBooking()) { $booking->booked_count = Booking::where('group_id', $booking->group_id) ->whereIn('status', ['scheduled', 'completed'])->count(); } else { $booking->additional_guests = $booking->getAdditionalGuests(); } $booking->title = $booking->getBookingTitle(true); $booking->author = $booking->getHostDetails(false); $booking->details = $booking->getConfirmationData(true); $booking->location = $booking->getLocationDetailsHtml(); $booking->reschedule_url = $booking->getRescheduleUrl(); $booking->happening_status = $booking->getOngoingStatus(); $booking->booking_status_text = $booking->getBookingStatus(); $booking->payment_status_text = $booking->getPaymentStatus(); $booking->custom_form_data = $booking->getCustomFormData(); do_action_ref_array('fluent_booking/format_booking_schedule', [&$booking]); return $booking; } private function getConfirmedBy() { $confirmedBy = 'host'; $userId = get_current_user_id(); if ($userId && $user = get_user_by('ID', $userId)) { $confirmedBy = $user->display_name; } return $confirmedBy; } private function getPaymentPaidLog($bookingId) { return [ 'booking_id' => $bookingId, 'status' => 'closed', 'type' => 'success', 'title' => __('Payment Successfully Completed', 'fluent-booking'), 'description' => __('Payment marked as paid by ', 'fluent-booking') . $this->getConfirmedBy() ]; } private function getPaymentPendingLog($bookingId) { return [ 'booking_id' => $bookingId, 'status' => 'closed', 'type' => 'success', 'title' => __('Payment Successfully Marked as Pending', 'fluent-booking'), 'description' => __('Payment marked as pending by ', 'fluent-booking') . $this->getConfirmedBy() ]; } private function getConfirmLog($bookingId) { return [ 'booking_id' => $bookingId, 'status' => 'closed', 'type' => 'success', 'title' => __('Booking Confirmed', 'fluent-booking'), 'description' => __('Booking has been confirmed by ', 'fluent-booking') . $this->getConfirmedBy() ]; } }