PluginProbe
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution / 2.5.0
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution v2.5.0
2.5.0 2.4.0 2.3.0 2.2.5 2.2.0 2.1.2 2.1.1 trunk 1.10.0 1.10.01 1.10.02 1.5.0 1.5.01 1.5.02 1.5.1 1.5.10 1.5.20 1.5.21 1.5.22 1.5.23 1.5.24 1.5.25 1.6.0 1.7.0 1.7.1 All 34 releases
← All changes | app/Http/Controllers/SchedulesController.php +481 -107 1.5.02 → 2.5.0 View file →
@@ -1,124 +1,198 @@
1 1 <?php
2 2
3 3 namespace FluentBooking\App\Http\Controllers;
4 4
5 -use FluentBooking\App\App;
6 5 use FluentBooking\App\Models\Booking;
7 6 use FluentBooking\App\Models\Calendar;
7 +use FluentBooking\App\Models\CalendarSlot;
8 8 use FluentBooking\App\Models\BookingActivity;
9 -use FluentBooking\App\Models\Meta;
9 +use FluentBooking\Framework\Database\Orm\ModelNotFoundException;
10 +use FluentBooking\App\Services\EmailNotificationService;
10 11 use FluentBooking\App\Services\Helper;
12 +use FluentBooking\Framework\Support\Arr;
11 13 use FluentBooking\App\Services\CurrenciesHelper;
12 -use FluentBooking\Framework\Support\Arr;
13 14 use FluentBooking\Framework\Http\Request\Request;
14 15 use FluentBooking\App\Services\PermissionManager;
15 16 use FluentBooking\App\Services\CalendarService;
17 +use FluentBooking\App\Services\ExportHelper;
18 +use FluentBooking\App\Services\Integrations\FluentCRM\CrmContactService;
19 +use FluentBooking\App\Services\Integrations\FluentCart\CustomerProfileService;
20 +use FluentCrm\App\Services\PermissionManager as CrmPermissionManager;
16 21
17 22 class SchedulesController extends Controller
18 23 {
19 24 public function index(Request $request)
20 25 {
21 - $filters = $request->get('filters', []);
26 + $author = $this->resolveAuthor($request);
22 27
23 - $period = Arr::get($filters, 'period', 'upcoming');
28 + $query = $this->buildSchedulesQuery($request, $author);
24 29
25 - $eventId = Arr::get($filters, 'event');
30 + $query->groupBy('group_id');
26 31
27 - $eventType = Arr::get($filters, 'event_type');
32 + $schedules = $query->paginate();
28 33
29 - $query = Booking::with(['calendar_event']);
34 + foreach ($schedules as $schedule) {
35 + $this->formatBooking($schedule);
36 + }
30 37
31 - $author = Arr::get($filters, 'author');
38 + $data = [
39 + 'schedules' => $schedules,
40 + 'timezone' => 'UTC'
41 + ];
32 42
33 - if ($author !== 'all') {
34 - $author = (int)$author;
43 + $data['calendar_event_lists'] = CalendarService::getCalendarOptionsByTitle();
44 +
45 + if ($author == 'me') {
46 + $slotOptions = CalendarService::getSlotOptions(null, get_current_user_id());
47 + $data['slot_options'] = $slotOptions;
35 48 }
36 49
37 - if (!PermissionManager::userCanSeeAllBookings()) {
38 - $authorCalendar = Calendar::where('user_id', get_current_user_id())
39 - ->where('type', 'simple')
40 - ->first();
50 + if ($request->get('page') == 1) {
51 + $this->addCountsForFirstPage($author, $data);
52 + }
41 53
42 - if($authorCalendar) {
43 - $author = $authorCalendar->id;
44 - }
54 + return $data;
55 + }
56 +
57 + public function export(Request $request)
58 + {
59 + $limit = (int) apply_filters('fluent_booking/data_export_limit', 2000);
60 +
61 + $author = $this->resolveAuthor($request);
62 +
63 + $query = $this->buildSchedulesQuery($request, $author);
64 +
65 + $relations = [
66 + 'calendar_event',
67 + 'user',
68 + 'booking_activities' => function ($q) {
69 + $q->where('type', 'cancel_reason');
70 + },
71 + 'booking_meta',
72 + ];
73 +
74 + if (defined('FLUENT_BOOKING_PRO_DIR_FILE')) {
75 + $relations[] = 'payment_order.transaction';
45 76 }
46 77
47 - if ($author && $author !== 'all') {
48 - $query->where('calendar_id', $author);
78 + $query->with($relations);
49 79
50 - if ($eventId && $eventId !== 'all') {
51 - $query->where('event_id', $eventId);
52 - }
80 + $total = (clone $query)->withoutEagerLoads()->count();
81 + $limited = $total > $limit;
53 82
54 - if ($eventType && $eventType !== 'all') {
55 - $query->where('event_type', $eventType);
56 - }
57 - }
83 + $bookings = $query->take($limit)->get();
58 84
59 - do_action_ref_array('fluent_booking/schedules_query', [&$query]);
85 + $rows = $bookings->map(function ($booking) {
86 + $row = ExportHelper::mapBooking($booking);
87 + return apply_filters('fluent_booking/booking_export_row', $row, $booking);
88 + })->all();
60 89
61 - $query->applyComputedStatus($period);
90 + $rows = apply_filters('fluent_booking/booking_export_columns', $rows, $bookings);
62 91
63 - if ($period == 'upcoming') {
64 - $query = $query->orderBy('start_time', 'ASC');
65 - } else if ($period == 'latest_bookings') {
66 - $query = $query->orderBy('created_at', 'DESC');
67 - } else if ($period == 'no_show') {
68 - $query = $query->orderBy('start_time', 'DESC');
69 - } else if ($period == 'latest_bookings') {
70 - $query = $query->orderBy('id', 'DESC');
92 + return [
93 + 'bookings' => $rows,
94 + 'limited' => $limited,
95 + 'total' => $total,
96 + ];
97 + }
98 +
99 + protected function resolveAuthor(Request $request)
100 + {
101 + $author = Arr::get($request->get('filters', []), 'author');
102 +
103 + if (is_numeric($author)) {
104 + $author = (int) $author;
71 105 } else {
72 - $query = $query->orderBy('start_time', 'DESC');
106 + $author = sanitize_text_field($author);
73 107 }
74 108
75 - $query->groupBy('group_id');
109 + $hasPermission = PermissionManager::userCanSeeAllBookings();
76 110
77 - $search = Arr::get($filters, 'search');
111 + if ($hasPermission) {
112 + return $author;
113 + }
78 114
79 - if (!empty($search)) {
80 - $author = 'all';
81 - $query = $query->orderBy('start_time', 'DESC');
82 - $query = $query->searchBy($search);
115 + if (!$author || $author == 'all') {
116 + $authorCalendar = Calendar::where('user_id', get_current_user_id())
117 + ->where('type', 'simple')
118 + ->first();
119 +
120 + $author = $authorCalendar ? $authorCalendar->id : '';
83 121 }
84 122
85 - $schedules = $query->paginate();
123 + return $author;
124 + }
86 125
87 - foreach ($schedules as $schedule) {
88 - $this->formatBooking($schedule);
126 + protected function buildSchedulesQuery(Request $request, $author = null)
127 + {
128 + $filters = $request->get('filters', []);
129 + $search = $request->getSafe('search');
130 + $eventVal = Arr::get($filters, 'event');
131 + $eventId = is_numeric($eventVal) ? (int) $eventVal : 0;
132 + $eventType = sanitize_text_field(Arr::get($filters, 'event_type'));
133 + $period = sanitize_text_field(Arr::get($filters, 'period', 'upcoming'));
134 + $range = array_map('sanitize_text_field', Arr::get($filters, 'range', []));
135 + $email = sanitize_email(Arr::get($filters, 'email', ''));
136 +
137 + $allowedPeriods = ['upcoming', 'completed', 'cancelled', 'pending', 'no_show', 'latest_bookings', 'all'];
138 + if (!in_array($period, $allowedPeriods, true)) {
139 + $period = 'upcoming';
89 140 }
90 141
91 - $data = [
92 - 'schedules' => $schedules,
93 - 'timezone' => 'UTC'
94 - ];
142 + $query = Booking::with(['calendar_event']);
95 143
96 - $data['calendar_event_lists'] = CalendarService::getCalendarOptionsByTitle();
144 + $hasPermission = PermissionManager::userCanSeeAllBookings();
97 145
98 - if ($author && $author != 'all') {
99 - $slotOptions = CalendarService::getSlotOptions($author);
100 - $data['slot_options'] = $slotOptions;
146 + if (!$hasPermission || $author == 'me') {
147 + $query->where('host_user_id', get_current_user_id());
101 148 }
102 149
103 - if ($request->get('page') == 1) {
104 - $pendingQuery = Booking::query()
105 - ->whereIn('status', ['pending', 'reserved'])
106 - ->distinct('group_id');
107 - if ($author && $author !== 'all') {
108 - $pendingCount = $pendingQuery->where('calendar_id', $author)->count('group_id');
109 - } else {
110 - $pendingCount = $pendingQuery->count('group_id');
150 + if ($author && $author !== 'all') {
151 + if ($author != 'me') {
152 + $query->where('calendar_id', $author);
111 153 }
154 + if ($eventId > 0) {
155 + $query->where('event_id', $eventId);
156 + }
157 + if ($eventType && $eventType !== 'all') {
158 + $query->where('event_type', $eventType);
159 + }
160 + }
112 161
113 - $data['no_show_count'] = Booking::where('status', 'no_show')->count();
114 - $data['pending_count'] = $pendingCount;
115 - $data['cancelled_count'] = Booking::where('status', 'cancelled')->count();
162 + if (!empty($email) && is_email($email)) {
163 + $query->where('email', $email);
116 164 }
117 165
118 - return $data;
166 + do_action_ref_array('fluent_booking/schedules_query', [&$query]);
167 +
168 + $query->applyDateRangeFilter($range);
169 + $query->applyComputedStatus($period);
170 + $query->applyBookingOrderByStatus($period);
171 +
172 + if (!empty($search)) {
173 + $query->searchBy($search);
174 + }
175 +
176 + return $query;
119 177 }
120 178
179 + private function addCountsForFirstPage($author, &$data)
180 + {
181 + $bookingQuery = Booking::query()
182 + ->when(!PermissionManager::userCanSeeAllBookings() || $author == 'me', function($query) {
183 + return $query->where('host_user_id', get_current_user_id());
184 + })
185 + ->when($author && is_numeric($author), function($query) use ($author) {
186 + return $query->where('calendar_id', $author);
187 + })
188 + ->distinct('group_id');
189 +
190 + $data['pending_count'] = (clone $bookingQuery)->whereIn('status', ['pending', 'reserved'])->count('group_id');
191 + $data['no_show_count'] = (clone $bookingQuery)->where('status', 'no_show')->count('group_id');
192 + $data['cancelled_count'] = (clone $bookingQuery)->where('status', 'cancelled')->count('group_id');
193 + }
194 +
121 195 public function patchBooking(Request $request, $bookingId)
122 196 {
123 197 $booking = Booking::findOrFail($bookingId);
124 198 $oldBooking = clone $booking;
@@ -144,9 +218,10 @@
144 218 'email',
145 219 'phone',
146 220 'first_name',
147 221 'last_name',
148 - 'status'
222 + 'status',
223 + 'payment_status'
149 224 ];
150 225
151 226 if (!in_array($column, $validColumns)) {
152 227 return $this->sendError(['message' => __('Invalid column', 'fluent-booking')]);
@@ -165,10 +240,19 @@
165 240 if (!in_array($value, ['scheduled', 'completed', 'cancelled', 'rejected', 'no_show'])) {
166 241 return $this->sendError(['message' => __('Invalid status', 'fluent-booking')]);
167 242 }
168 243
244 + if (in_array($booking->status, ['cancelled', 'rejected'])) {
245 + return $this->sendError(['message' => __('A cancelled or rejected booking can not be changed', 'fluent-booking')]);
246 + }
247 +
169 248 if ($value == 'scheduled' && $booking->payment_method && $booking->payment_order) {
170 249 $order = $booking->payment_order;
250 +
251 + if (in_array($order->status, ['refunded', 'partially-refunded'])) {
252 + return $this->sendError(['message' => __('A refunded payment can not be marked as paid', 'fluent-booking')]);
253 + }
254 +
171 255 $order->total_paid = $order->total_amount;
172 256 $order->completed_at = gmdate('Y-m-d H:i:s'); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
173 257 $order->status = 'paid';
174 258 $order->save();
@@ -174,9 +258,9 @@
174 258 $order->save();
175 259
176 260 $updateData['payment_status'] = 'paid';
177 261
178 - do_action('fluent_booking/log_booking_activity', $this->getPaymentLog($booking->id));
262 + do_action('fluent_booking/log_booking_activity', $this->getPaymentPaidLog($booking->id));
179 263
180 264 do_action('fluent_booking/payment/update_payment_status_paid', $booking);
181 265
182 266 } else if ($value == 'scheduled') {
@@ -183,28 +267,66 @@
183 267 do_action('fluent_booking/log_booking_activity', $this->getConfirmLog($booking->id));
184 268 }
185 269
186 270 if ($value == 'cancelled') {
187 - $cancelReason = sanitize_text_field($data['cancel_reason']);
271 + $cancelReason = sanitize_text_field(Arr::get($data, 'cancel_reason', ''));
188 272 $booking->cancelMeeting($cancelReason, 'host', get_current_user_id());
189 - return [
190 - 'message' => __('The booking has been cancelled', 'fluent-booking')
191 - ];
192 273 }
193 274
194 275 if ($value == 'rejected') {
195 - $rejectReason = sanitize_text_field($data['reject_reason']);
276 + $rejectReason = sanitize_text_field(Arr::get($data, 'reject_reason', ''));
196 277 $booking->rejectMeeting($rejectReason, get_current_user_id());
278 + }
279 +
280 + if (in_array($value, ['cancelled', 'rejected'])) {
281 + // cancelMeeting() and rejectMeeting() refuse some statuses without changing the booking.
282 + if ($booking->status != $value) {
283 + /* translators: %s: Booking status */
284 + return $this->sendError(['message' => sprintf(__('This booking can not be %s', 'fluent-booking'), $value)]);
285 + }
286 +
287 + if ($booking->payment_method && Arr::get($data, 'refund_payment') == 'yes') {
288 + do_action('fluent_booking/refund_payment_' . $booking->payment_method, $booking, $booking->calendar_event);
289 + }
197 290 return [
198 - 'message' => __('The booking has been rejected', 'fluent-booking')
291 + /* translators: %s: Booking status */
292 + 'message' => sprintf(__('The booking has been %s', 'fluent-booking'), $value)
199 293 ];
200 294 }
201 295
202 - if ($booking->payment_method && Arr::get($data, 'refund_payment') == 'yes' && in_array($value, ['cancelled', 'rejected'])) {
203 - do_action('fluent_booking/refund_payment_' . $booking->payment_method, $booking, $booking->calendar_event);
296 + $updateAll = Arr::isTrue($data, 'update_all') && $booking->isMultiGuestBooking();
297 +
298 + if ($updateAll && in_array($value, ['no_show', 'completed'])) {
299 + Booking::where('event_id', $booking->event_id)
300 + ->where('group_id', $booking->group_id)
301 + ->update([
302 + 'status' => $value
303 + ]);
304 + return [
305 + /* translators: %s: Booking status */
306 + 'message' => sprintf(__('The booking has been %s', 'fluent-booking'), $value)
307 + ];
204 308 }
205 309 }
206 310
311 + if ($column == 'payment_status') {
312 + if (!in_array($value, ['pending', 'paid'])) {
313 + return $this->sendError(['message' => __('Invalid payment status', 'fluent-booking')]);
314 + }
315 +
316 + if ($value == 'paid') {
317 + do_action('fluent_booking/log_booking_activity', $this->getPaymentPaidLog($booking->id));
318 + }
319 +
320 + if ($value == 'pending') {
321 + do_action('fluent_booking/log_booking_activity', $this->getPaymentPendingLog($booking->id));
322 + }
323 +
324 + if ($booking->payment_order) {
325 + do_action('fluent_booking/payment/status_changed', $booking->payment_order, $booking, $value);
326 + }
327 + }
328 +
207 329 $updateData[$column] = $value;
208 330 $booking->fill($updateData);
209 331 $booking->save();
210 332
@@ -223,9 +345,9 @@
223 345 do_action('fluent_booking/after_patch_booking_' . $column, $booking, $booking->calendar_event, $oldBooking->{$column});
224 346
225 347 return [
226 348 /* translators: Updated column name */
227 - 'message' => sprintf(__('%s has been updated', 'fluent-booking'), $column)
349 + 'message' => sprintf(__('%s has been updated', 'fluent-booking'), ucfirst($column))
228 350 ];
229 351 }
230 352
231 353 public function getBooking(Request $request, $bookingId)
@@ -232,11 +354,9 @@
232 354 {
233 355 $booking = Booking::with('calendar_event');
234 356
235 357 if (!PermissionManager::userCanSeeAllBookings()) {
236 - $booking->whereHas('calendar', function ($q) {
237 - $q->where('user_id', get_current_user_id());
238 - });
358 + $booking->whereHostAccess(get_current_user_id());
239 359 }
240 360
241 361 $booking = $booking->findOrFail($bookingId);
242 362 $booking = $this->formatBooking($booking);
@@ -272,16 +392,38 @@
272 392 'message' => __('Booking Deleted Successfully!', 'fluent-booking')
273 393 ];
274 394 }
275 395
396 + public function sendConfirmationEmail(Request $request, $bookingId)
397 + {
398 + $booking = Booking::with(['calendar', 'calendar_event'])->findOrFail($bookingId);
399 +
400 + $emailTo = $request->get('email_to', 'guest');
401 +
402 + $notifications = $booking->calendar_event->getNotifications();
403 +
404 + $email = Arr::get($notifications, 'booking_conf_attendee.email', []);
405 + if ($emailTo == 'host') {
406 + $email = Arr::get($notifications, 'booking_conf_host.email', []);
407 + }
408 +
409 + $result = EmailNotificationService::emailOnBooked($booking, $email, $emailTo, 'scheduled', true);
410 +
411 + if (!$result) {
412 + return $this->sendError(['message' => __('Notification sending failed', 'fluent-booking')]);
413 + }
414 +
415 + return [
416 + 'message' => __('Notification sent successfully', 'fluent-booking')
417 + ];
418 + }
419 +
276 420 public function getGroupAttendees(Request $request, $groupId)
277 421 {
278 422 $booking = Booking::with('slot');
279 423
280 424 if (!PermissionManager::userCanSeeAllBookings()) {
281 - $booking->whereHas('calendar', function ($q) {
282 - $q->where('user_id', get_current_user_id());
283 - });
425 + $booking->whereHostAccess(get_current_user_id());
284 426 }
285 427
286 428 $booking = $booking->where('group_id', $groupId)->first();
287 429
@@ -289,9 +431,9 @@
289 431 return $this->sendError(['message' => __('Invalid group id or the event is not a group event', 'fluent-booking')]);
290 432 }
291 433
292 434 $attendees = Booking::where('group_id', $booking->group_id);
293 - $search = sanitize_text_field($request->get('search'));
435 + $search = $request->getSafe('search');
294 436
295 437 if (!empty($search)) {
296 438 $attendees = $attendees->searchBy($search);
297 439 }
@@ -307,9 +449,12 @@
307 449 }
308 450
309 451 public function getBookingActivities(Request $request, $bookingId)
310 452 {
453 + $this->resolveOwnedBookingOrFail($bookingId);
454 +
311 455 $activities = BookingActivity::where('booking_id', $bookingId)
456 + ->where('type', '!=', BookingActivity::TYPE_NOTE)
312 457 ->orderBy('id', 'DESC')
313 458 ->get();
314 459
315 460 return [
@@ -318,32 +463,53 @@
318 463 }
319 464
320 465 public function getBookingMetaInfo(Request $request, $bookingId)
321 466 {
322 - $booking = Booking::findOrFail($bookingId);
467 + $booking = $this->resolveOwnedBookingOrFail($bookingId);
323 468
324 469 $activities = BookingActivity::where('booking_id', $booking->id)
470 + ->where('type', '!=', BookingActivity::TYPE_NOTE)
325 471 ->orderBy('id', 'DESC')
326 472 ->get();
327 473
474 + $activities->each(function ($activity) {
475 + $activity->description = wp_unslash($activity->description);
476 + });
477 +
328 478 $sidebarContents = [];
329 479 $mainBodyContents = [];
330 480
331 - if (defined('FLUENTCRM')) {
332 - $profileHtml = fluentcrm_get_crm_profile_html($booking->email, false);
333 - if ($profileHtml) {
334 - $sidebarContents[] = [
335 - 'id' => 'fluent_crm_profule',
336 - 'title' => __('CRM Profile', 'fluent-booking'),
337 - 'content' => $profileHtml
338 - ];
339 - }
481 + $canReadCrm = CrmContactService::isActive() && CrmPermissionManager::currentUserCan('fcrm_read_contacts');
482 + $crmProfile = $canReadCrm ? CrmContactService::getProfileData($booking->email) : null;
483 + if ($crmProfile) {
484 + $sidebarContents[] = [
485 + 'id' => 'fluent_crm_profule',
486 + 'title' => __('CRM Profile', 'fluent-booking'),
487 + 'type' => 'crm_profile',
488 + 'profile' => $crmProfile,
489 + ];
340 490 }
341 491
492 + $cartProfile = CustomerProfileService::canView()
493 + ? CustomerProfileService::getProfileData($booking->email)
494 + : null;
495 + if ($cartProfile) {
496 + $sidebarContents[] = [
497 + 'id' => 'fluent_cart_profile',
498 + 'title' => __('Cart Profile', 'fluent-booking'),
499 + 'type' => 'cart_profile',
500 + 'profile' => $cartProfile,
501 + ];
502 + }
503 +
342 504 $order = null;
343 - if ($booking->payment_method && $booking->payment_order) {
505 + if ($booking->payment_status && $booking->payment_order) {
344 506 $order = $booking->payment_order;
345 - $order->load(['items', 'transaction']);
507 + $relations = ['items', 'transaction'];
508 + if (method_exists($order, 'discounts')) {
509 + $relations[] = 'discounts';
510 + }
511 + $order->load($relations);
346 512 $order->currency_sign = CurrenciesHelper::getCurrencySign($order->currency);
347 513 }
348 514
349 515 $mainBodyContents = apply_filters('fluent_booking/booking_meta_info_main_meta', $mainBodyContents, $booking);
@@ -356,8 +522,198 @@
356 522 'main_body_contents' => $mainBodyContents
357 523 ];
358 524 }
359 525
526 + /**
527 + * Return the CRM contact state for a booking plus the full tag/list option
528 + * sets, so the admin can manage tags/lists inline from the CRM Profile card.
529 + */
530 + public function getCrmContact(Request $request, $bookingId)
531 + {
532 + $booking = $this->resolveOwnedBookingOrFail($bookingId);
533 +
534 + if (!CrmContactService::isActive()) {
535 + return $this->sendError([
536 + 'message' => __('FluentCRM is not active.', 'fluent-booking')
537 + ]);
538 + }
539 +
540 + if (!CrmPermissionManager::currentUserCan('fcrm_read_contacts')) {
541 + return $this->sendError([
542 + 'message' => __('You do not have permission to read CRM contacts.', 'fluent-booking')
543 + ], 403);
544 + }
545 +
546 + $state = CrmContactService::getContactState($booking->email);
547 +
548 + if (!$state) {
549 + return $this->sendError([
550 + 'message' => __('No CRM contact found for this booking.', 'fluent-booking')
551 + ], 404);
552 + }
553 +
554 + return $state;
555 + }
556 +
557 + /**
558 + * Guest-field prefill for the CRM "Book Appointment" action.
559 + * Keyed by contact id, so it carries its own CRM-read gate.
560 + */
561 + public function getCrmContactPrefill(Request $request)
562 + {
563 + if (!CrmContactService::isActive()) {
564 + return $this->sendError([
565 + 'message' => __('FluentCRM is not active.', 'fluent-booking')
566 + ]);
567 + }
568 +
569 + if (!CrmPermissionManager::currentUserCan('fcrm_read_contacts')) {
570 + return $this->sendError([
571 + 'message' => __('You do not have permission to read CRM contacts.', 'fluent-booking')
572 + ], 403);
573 + }
574 +
575 + $contactId = absint($request->get('contact_id'));
576 +
577 + if (!$contactId) {
578 + return $this->sendError([
579 + 'message' => __('Invalid contact.', 'fluent-booking')
580 + ], 422);
581 + }
582 +
583 + $prefill = CrmContactService::getBookingPrefillData($contactId);
584 +
585 + if (!$prefill) {
586 + return $this->sendError([
587 + 'message' => __('No CRM contact found.', 'fluent-booking')
588 + ], 404);
589 + }
590 +
591 + return [
592 + 'prefill' => $prefill,
593 + ];
594 + }
595 +
596 + /**
597 + * Typeahead search for the admin booking modal's CRM contact picker.
598 + * Same intersection gate as getCrmContactPrefill: booking access (policy) AND fcrm_read_contacts.
599 + */
600 + public function searchCrmContacts(Request $request)
601 + {
602 + if (!CrmContactService::isActive()) {
603 + return $this->sendError([
604 + 'message' => __('FluentCRM is not active.', 'fluent-booking')
605 + ]);
606 + }
607 +
608 + if (!CrmPermissionManager::currentUserCan('fcrm_read_contacts')) {
609 + return $this->sendError([
610 + 'message' => __('You do not have permission to read CRM contacts.', 'fluent-booking')
611 + ], 403);
612 + }
613 +
614 + $search = sanitize_text_field($request->get('search', ''));
615 +
616 + return [
617 + 'contacts' => CrmContactService::searchContacts($search),
618 + ];
619 + }
620 +
621 + /**
622 + * Bounded, searchable tag/list options for the CRM Profile picker.
623 + */
624 + public function getCrmOptions(Request $request, $bookingId)
625 + {
626 + $this->resolveOwnedBookingOrFail($bookingId);
627 +
628 + if (!CrmContactService::isActive()) {
629 + return $this->sendError([
630 + 'message' => __('FluentCRM is not active.', 'fluent-booking')
631 + ]);
632 + }
633 +
634 + if (!CrmPermissionManager::currentUserCan('fcrm_read_contacts')) {
635 + return $this->sendError([
636 + 'message' => __('You do not have permission to read CRM contacts.', 'fluent-booking')
637 + ], 403);
638 + }
639 +
640 + $type = $request->get('type') === 'lists' ? 'lists' : 'tags';
641 + $search = sanitize_text_field($request->get('search', ''));
642 +
643 + return [
644 + 'options' => CrmContactService::getOptions($type, $search),
645 + ];
646 + }
647 +
648 + public function updateCrmTags(Request $request, $bookingId)
649 + {
650 + return $this->updateCrmTaxonomy($request, $bookingId, 'tags');
651 + }
652 +
653 + public function updateCrmLists(Request $request, $bookingId)
654 + {
655 + return $this->updateCrmTaxonomy($request, $bookingId, 'lists');
656 + }
657 +
658 + private function updateCrmTaxonomy(Request $request, $bookingId, $type)
659 + {
660 + $booking = $this->resolveOwnedBookingOrFail($bookingId);
661 +
662 + if (!CrmContactService::isActive()) {
663 + return $this->sendError([
664 + 'message' => __('FluentCRM is not active.', 'fluent-booking')
665 + ]);
666 + }
667 +
668 + if (!CrmPermissionManager::currentUserCan('fcrm_manage_contacts')) {
669 + return $this->sendError([
670 + 'message' => __('You do not have permission to manage CRM contacts.', 'fluent-booking')
671 + ], 403);
672 + }
673 +
674 + $requestKey = $type === 'tags' ? 'tag_ids' : 'list_ids';
675 + $desired = (array) $request->get($requestKey, []);
676 +
677 + $result = CrmContactService::syncTaxonomy($booking->email, $type, $desired);
678 +
679 + if ($result === null) {
680 + return $this->sendError([
681 + 'message' => __('No CRM contact found for this booking.', 'fluent-booking')
682 + ], 404);
683 + }
684 +
685 + return $this->sendSuccess(array_merge([
686 + 'message' => __('CRM contact updated successfully.', 'fluent-booking'),
687 + ], $result));
688 + }
689 +
690 + private function resolveOwnedBookingOrFail($bookingId)
691 + {
692 + $booking = Booking::findOrFail($bookingId);
693 +
694 + if (PermissionManager::userCanSeeAllBookings()) {
695 + return $booking;
696 + }
697 +
698 + $allowedIds = $booking->getHostIds();
699 +
700 + if (PermissionManager::userCan('manage_own_calendar')) {
701 + if ($event = CalendarSlot::find($booking->event_id)) {
702 + $allowedIds = array_merge($allowedIds, $event->getHostIds());
703 + }
704 + }
705 +
706 + if (in_array(get_current_user_id(), $allowedIds)) {
707 + return $booking;
708 + }
709 +
710 + $exception = new ModelNotFoundException();
711 + $exception->setModel(Booking::class, [$bookingId]);
712 + // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
713 + throw $exception;
714 + }
715 +
360 716 private function formatBooking(&$booking)
361 717 {
362 718 $autoCompleteTimeOut = (int) Helper::getGlobalAdminSetting('auto_complete_timing', 60) * 60; // 10 minutes
363 719
@@ -364,9 +720,10 @@
364 720 if (in_array($booking->status, ['scheduled', 'pending']) && (time() - strtotime($booking->end_time)) > $autoCompleteTimeOut) {
365 721 $bookingStatus = $booking->status == 'pending' ? 'cancelled' : 'completed';
366 722 $booking->status = $bookingStatus;
367 723 $booking->save();
368 - do_action('fluent_booking/booking_schedule_' . $bookingStatus, $booking, $booking->calendar_event);
724 + $hookName = $bookingStatus === 'cancelled' ? 'auto_cancelled' : $bookingStatus;
725 + do_action('fluent_booking/booking_schedule_' . $hookName, $booking, $booking->calendar_event);
369 726 }
370 727
371 728 if ($booking->isMultiHostBooking()) {
372 729 $booking->host_profiles = $booking->getHostProfiles();
@@ -380,8 +737,9 @@
380 737 }
381 738
382 739 $booking->title = $booking->getBookingTitle(true);
383 740 $booking->author = $booking->getHostDetails(false);
741 + $booking->details = $booking->getConfirmationData(true);
384 742 $booking->location = $booking->getLocationDetailsHtml();
385 743 $booking->reschedule_url = $booking->getRescheduleUrl();
386 744 $booking->happening_status = $booking->getOngoingStatus();
387 745 $booking->booking_status_text = $booking->getBookingStatus();
@@ -392,32 +750,48 @@
392 750
393 751 return $booking;
394 752 }
395 753
396 - private function getPaymentLog($bookingId)
754 + private function getConfirmedBy()
397 755 {
756 + $confirmedBy = 'host';
757 + $userId = get_current_user_id();
758 + if ($userId && $user = get_user_by('ID', $userId)) {
759 + $confirmedBy = $user->display_name;
760 + }
761 +
762 + return $confirmedBy;
763 + }
764 +
765 + private function getPaymentPaidLog($bookingId)
766 + {
398 767 return [
399 768 'booking_id' => $bookingId,
400 769 'status' => 'closed',
401 770 'type' => 'success',
402 771 'title' => __('Payment Successfully Completed', 'fluent-booking'),
403 - 'description' => __('Payment marked as paid by admin', 'fluent-booking')
772 + 'description' => __('Payment marked as paid by ', 'fluent-booking') . $this->getConfirmedBy()
404 773 ];
405 774 }
406 775
776 + private function getPaymentPendingLog($bookingId)
777 + {
778 + return [
779 + 'booking_id' => $bookingId,
780 + 'status' => 'closed',
781 + 'type' => 'success',
782 + 'title' => __('Payment Successfully Marked as Pending', 'fluent-booking'),
783 + 'description' => __('Payment marked as pending by ', 'fluent-booking') . $this->getConfirmedBy()
784 + ];
785 + }
786 +
407 787 private function getConfirmLog($bookingId)
408 788 {
409 - $confirmedBy = 'host';
410 - $userId = get_current_user_id();
411 - if ($userId && $user = get_user_by('ID', $userId)) {
412 - $confirmedBy = $user->display_name;
413 - }
414 -
415 789 return [
416 790 'booking_id' => $bookingId,
417 791 'status' => 'closed',
418 792 'type' => 'success',
419 793 'title' => __('Booking Confirmed', 'fluent-booking'),
420 - 'description' => __('Booking has been confirmed by ', 'fluent-booking') . $confirmedBy
794 + 'description' => __('Booking has been confirmed by ', 'fluent-booking') . $this->getConfirmedBy()
421 795 ];
422 796 }
423 797 }