PluginProbe
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution / trunk
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution vtrunk
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 1.7.2 All 33 releases
← All changes | app/Http/Controllers/SchedulesController.php +462 -105 1.5.01trunk 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')]);
@@ -174,9 +249,9 @@
174 249 $order->save();
175 250
176 251 $updateData['payment_status'] = 'paid';
177 252
178 - do_action('fluent_booking/log_booking_activity', $this->getPaymentLog($booking->id));
253 + do_action('fluent_booking/log_booking_activity', $this->getPaymentPaidLog($booking->id));
179 254
180 255 do_action('fluent_booking/payment/update_payment_status_paid', $booking);
181 256
182 257 } else if ($value == 'scheduled') {
@@ -185,26 +260,58 @@
185 260
186 261 if ($value == 'cancelled') {
187 262 $cancelReason = sanitize_text_field($data['cancel_reason']);
188 263 $booking->cancelMeeting($cancelReason, 'host', get_current_user_id());
189 - return [
190 - 'message' => __('The booking has been cancelled', 'fluent-booking')
191 - ];
192 264 }
193 265
194 266 if ($value == 'rejected') {
195 267 $rejectReason = sanitize_text_field($data['reject_reason']);
196 268 $booking->rejectMeeting($rejectReason, get_current_user_id());
269 + }
270 +
271 + if (in_array($value, ['cancelled', 'rejected'])) {
272 + if ($booking->payment_method && Arr::get($data, 'refund_payment') == 'yes') {
273 + do_action('fluent_booking/refund_payment_' . $booking->payment_method, $booking, $booking->calendar_event);
274 + }
197 275 return [
198 - 'message' => __('The booking has been rejected', 'fluent-booking')
276 + /* translators: %s: Booking status */
277 + 'message' => sprintf(__('The booking has been %s', 'fluent-booking'), $value)
199 278 ];
200 279 }
201 280
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);
281 + $updateAll = Arr::isTrue($data, 'update_all') && $booking->isMultiGuestBooking();
282 +
283 + if ($updateAll && in_array($value, ['no_show', 'completed'])) {
284 + Booking::where('event_id', $booking->event_id)
285 + ->where('group_id', $booking->group_id)
286 + ->update([
287 + 'status' => $value
288 + ]);
289 + return [
290 + /* translators: %s: Booking status */
291 + 'message' => sprintf(__('The booking has been %s', 'fluent-booking'), $value)
292 + ];
204 293 }
205 294 }
206 295
296 + if ($column == 'payment_status') {
297 + if (!in_array($value, ['pending', 'paid'])) {
298 + return $this->sendError(['message' => __('Invalid payment status', 'fluent-booking')]);
299 + }
300 +
301 + if ($value == 'paid') {
302 + do_action('fluent_booking/log_booking_activity', $this->getPaymentPaidLog($booking->id));
303 + }
304 +
305 + if ($value == 'pending') {
306 + do_action('fluent_booking/log_booking_activity', $this->getPaymentPendingLog($booking->id));
307 + }
308 +
309 + if ($booking->payment_order) {
310 + do_action('fluent_booking/payment/status_changed', $booking->payment_order, $booking, $value);
311 + }
312 + }
313 +
207 314 $updateData[$column] = $value;
208 315 $booking->fill($updateData);
209 316 $booking->save();
210 317
@@ -223,9 +330,9 @@
223 330 do_action('fluent_booking/after_patch_booking_' . $column, $booking, $booking->calendar_event, $oldBooking->{$column});
224 331
225 332 return [
226 333 /* translators: Updated column name */
227 - 'message' => sprintf(__('%s has been updated', 'fluent-booking'), $column)
334 + 'message' => sprintf(__('%s has been updated', 'fluent-booking'), ucfirst($column))
228 335 ];
229 336 }
230 337
231 338 public function getBooking(Request $request, $bookingId)
@@ -232,11 +339,9 @@
232 339 {
233 340 $booking = Booking::with('calendar_event');
234 341
235 342 if (!PermissionManager::userCanSeeAllBookings()) {
236 - $booking->whereHas('calendar', function ($q) {
237 - $q->where('user_id', get_current_user_id());
238 - });
343 + $booking->whereHostAccess(get_current_user_id());
239 344 }
240 345
241 346 $booking = $booking->findOrFail($bookingId);
242 347 $booking = $this->formatBooking($booking);
@@ -272,16 +377,38 @@
272 377 'message' => __('Booking Deleted Successfully!', 'fluent-booking')
273 378 ];
274 379 }
275 380
381 + public function sendConfirmationEmail(Request $request, $bookingId)
382 + {
383 + $booking = Booking::with(['calendar', 'calendar_event'])->findOrFail($bookingId);
384 +
385 + $emailTo = $request->get('email_to', 'guest');
386 +
387 + $notifications = $booking->calendar_event->getNotifications();
388 +
389 + $email = Arr::get($notifications, 'booking_conf_attendee.email', []);
390 + if ($emailTo == 'host') {
391 + $email = Arr::get($notifications, 'booking_conf_host.email', []);
392 + }
393 +
394 + $result = EmailNotificationService::emailOnBooked($booking, $email, $emailTo, 'scheduled', true);
395 +
396 + if (!$result) {
397 + return $this->sendError(['message' => __('Notification sending failed', 'fluent-booking')]);
398 + }
399 +
400 + return [
401 + 'message' => __('Notification sent successfully', 'fluent-booking')
402 + ];
403 + }
404 +
276 405 public function getGroupAttendees(Request $request, $groupId)
277 406 {
278 407 $booking = Booking::with('slot');
279 408
280 409 if (!PermissionManager::userCanSeeAllBookings()) {
281 - $booking->whereHas('calendar', function ($q) {
282 - $q->where('user_id', get_current_user_id());
283 - });
410 + $booking->whereHostAccess(get_current_user_id());
284 411 }
285 412
286 413 $booking = $booking->where('group_id', $groupId)->first();
287 414
@@ -289,9 +416,9 @@
289 416 return $this->sendError(['message' => __('Invalid group id or the event is not a group event', 'fluent-booking')]);
290 417 }
291 418
292 419 $attendees = Booking::where('group_id', $booking->group_id);
293 - $search = sanitize_text_field($request->get('search'));
420 + $search = $request->getSafe('search');
294 421
295 422 if (!empty($search)) {
296 423 $attendees = $attendees->searchBy($search);
297 424 }
@@ -307,8 +434,10 @@
307 434 }
308 435
309 436 public function getBookingActivities(Request $request, $bookingId)
310 437 {
438 + $this->resolveOwnedBookingOrFail($bookingId);
439 +
311 440 $activities = BookingActivity::where('booking_id', $bookingId)
312 441 ->orderBy('id', 'DESC')
313 442 ->get();
314 443
@@ -318,32 +447,52 @@
318 447 }
319 448
320 449 public function getBookingMetaInfo(Request $request, $bookingId)
321 450 {
322 - $booking = Booking::findOrFail($bookingId);
451 + $booking = $this->resolveOwnedBookingOrFail($bookingId);
323 452
324 453 $activities = BookingActivity::where('booking_id', $booking->id)
325 454 ->orderBy('id', 'DESC')
326 455 ->get();
456 +
457 + $activities->each(function ($activity) {
458 + $activity->description = wp_unslash($activity->description);
459 + });
327 460
328 461 $sidebarContents = [];
329 462 $mainBodyContents = [];
330 463
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 - }
464 + $canReadCrm = CrmContactService::isActive() && CrmPermissionManager::currentUserCan('fcrm_read_contacts');
465 + $crmProfile = $canReadCrm ? CrmContactService::getProfileData($booking->email) : null;
466 + if ($crmProfile) {
467 + $sidebarContents[] = [
468 + 'id' => 'fluent_crm_profule',
469 + 'title' => __('CRM Profile', 'fluent-booking'),
470 + 'type' => 'crm_profile',
471 + 'profile' => $crmProfile,
472 + ];
340 473 }
341 474
475 + $cartProfile = CustomerProfileService::canView()
476 + ? CustomerProfileService::getProfileData($booking->email)
477 + : null;
478 + if ($cartProfile) {
479 + $sidebarContents[] = [
480 + 'id' => 'fluent_cart_profile',
481 + 'title' => __('Cart Profile', 'fluent-booking'),
482 + 'type' => 'cart_profile',
483 + 'profile' => $cartProfile,
484 + ];
485 + }
486 +
342 487 $order = null;
343 - if ($booking->payment_method && $booking->payment_order) {
488 + if ($booking->payment_status && $booking->payment_order) {
344 489 $order = $booking->payment_order;
345 - $order->load(['items', 'transaction']);
490 + $relations = ['items', 'transaction'];
491 + if (method_exists($order, 'discounts')) {
492 + $relations[] = 'discounts';
493 + }
494 + $order->load($relations);
346 495 $order->currency_sign = CurrenciesHelper::getCurrencySign($order->currency);
347 496 }
348 497
349 498 $mainBodyContents = apply_filters('fluent_booking/booking_meta_info_main_meta', $mainBodyContents, $booking);
@@ -356,8 +505,198 @@
356 505 'main_body_contents' => $mainBodyContents
357 506 ];
358 507 }
359 508
509 + /**
510 + * Return the CRM contact state for a booking plus the full tag/list option
511 + * sets, so the admin can manage tags/lists inline from the CRM Profile card.
512 + */
513 + public function getCrmContact(Request $request, $bookingId)
514 + {
515 + $booking = $this->resolveOwnedBookingOrFail($bookingId);
516 +
517 + if (!CrmContactService::isActive()) {
518 + return $this->sendError([
519 + 'message' => __('FluentCRM is not active.', 'fluent-booking')
520 + ]);
521 + }
522 +
523 + if (!CrmPermissionManager::currentUserCan('fcrm_read_contacts')) {
524 + return $this->sendError([
525 + 'message' => __('You do not have permission to read CRM contacts.', 'fluent-booking')
526 + ], 403);
527 + }
528 +
529 + $state = CrmContactService::getContactState($booking->email);
530 +
531 + if (!$state) {
532 + return $this->sendError([
533 + 'message' => __('No CRM contact found for this booking.', 'fluent-booking')
534 + ], 404);
535 + }
536 +
537 + return $state;
538 + }
539 +
540 + /**
541 + * Guest-field prefill for the CRM "Book Appointment" action.
542 + * Keyed by contact id, so it carries its own CRM-read gate.
543 + */
544 + public function getCrmContactPrefill(Request $request)
545 + {
546 + if (!CrmContactService::isActive()) {
547 + return $this->sendError([
548 + 'message' => __('FluentCRM is not active.', 'fluent-booking')
549 + ]);
550 + }
551 +
552 + if (!CrmPermissionManager::currentUserCan('fcrm_read_contacts')) {
553 + return $this->sendError([
554 + 'message' => __('You do not have permission to read CRM contacts.', 'fluent-booking')
555 + ], 403);
556 + }
557 +
558 + $contactId = absint($request->get('contact_id'));
559 +
560 + if (!$contactId) {
561 + return $this->sendError([
562 + 'message' => __('Invalid contact.', 'fluent-booking')
563 + ], 422);
564 + }
565 +
566 + $prefill = CrmContactService::getBookingPrefillData($contactId);
567 +
568 + if (!$prefill) {
569 + return $this->sendError([
570 + 'message' => __('No CRM contact found.', 'fluent-booking')
571 + ], 404);
572 + }
573 +
574 + return [
575 + 'prefill' => $prefill,
576 + ];
577 + }
578 +
579 + /**
580 + * Typeahead search for the admin booking modal's CRM contact picker.
581 + * Same intersection gate as getCrmContactPrefill: booking access (policy) AND fcrm_read_contacts.
582 + */
583 + public function searchCrmContacts(Request $request)
584 + {
585 + if (!CrmContactService::isActive()) {
586 + return $this->sendError([
587 + 'message' => __('FluentCRM is not active.', 'fluent-booking')
588 + ]);
589 + }
590 +
591 + if (!CrmPermissionManager::currentUserCan('fcrm_read_contacts')) {
592 + return $this->sendError([
593 + 'message' => __('You do not have permission to read CRM contacts.', 'fluent-booking')
594 + ], 403);
595 + }
596 +
597 + $search = sanitize_text_field($request->get('search', ''));
598 +
599 + return [
600 + 'contacts' => CrmContactService::searchContacts($search),
601 + ];
602 + }
603 +
604 + /**
605 + * Bounded, searchable tag/list options for the CRM Profile picker.
606 + */
607 + public function getCrmOptions(Request $request, $bookingId)
608 + {
609 + $this->resolveOwnedBookingOrFail($bookingId);
610 +
611 + if (!CrmContactService::isActive()) {
612 + return $this->sendError([
613 + 'message' => __('FluentCRM is not active.', 'fluent-booking')
614 + ]);
615 + }
616 +
617 + if (!CrmPermissionManager::currentUserCan('fcrm_read_contacts')) {
618 + return $this->sendError([
619 + 'message' => __('You do not have permission to read CRM contacts.', 'fluent-booking')
620 + ], 403);
621 + }
622 +
623 + $type = $request->get('type') === 'lists' ? 'lists' : 'tags';
624 + $search = sanitize_text_field($request->get('search', ''));
625 +
626 + return [
627 + 'options' => CrmContactService::getOptions($type, $search),
628 + ];
629 + }
630 +
631 + public function updateCrmTags(Request $request, $bookingId)
632 + {
633 + return $this->updateCrmTaxonomy($request, $bookingId, 'tags');
634 + }
635 +
636 + public function updateCrmLists(Request $request, $bookingId)
637 + {
638 + return $this->updateCrmTaxonomy($request, $bookingId, 'lists');
639 + }
640 +
641 + private function updateCrmTaxonomy(Request $request, $bookingId, $type)
642 + {
643 + $booking = $this->resolveOwnedBookingOrFail($bookingId);
644 +
645 + if (!CrmContactService::isActive()) {
646 + return $this->sendError([
647 + 'message' => __('FluentCRM is not active.', 'fluent-booking')
648 + ]);
649 + }
650 +
651 + if (!CrmPermissionManager::currentUserCan('fcrm_manage_contacts')) {
652 + return $this->sendError([
653 + 'message' => __('You do not have permission to manage CRM contacts.', 'fluent-booking')
654 + ], 403);
655 + }
656 +
657 + $requestKey = $type === 'tags' ? 'tag_ids' : 'list_ids';
658 + $desired = (array) $request->get($requestKey, []);
659 +
660 + $result = CrmContactService::syncTaxonomy($booking->email, $type, $desired);
661 +
662 + if ($result === null) {
663 + return $this->sendError([
664 + 'message' => __('No CRM contact found for this booking.', 'fluent-booking')
665 + ], 404);
666 + }
667 +
668 + return $this->sendSuccess(array_merge([
669 + 'message' => __('CRM contact updated successfully.', 'fluent-booking'),
670 + ], $result));
671 + }
672 +
673 + private function resolveOwnedBookingOrFail($bookingId)
674 + {
675 + $booking = Booking::findOrFail($bookingId);
676 +
677 + if (PermissionManager::userCanSeeAllBookings()) {
678 + return $booking;
679 + }
680 +
681 + $allowedIds = $booking->getHostIds();
682 +
683 + if (PermissionManager::userCan('manage_own_calendar')) {
684 + if ($event = CalendarSlot::find($booking->event_id)) {
685 + $allowedIds = array_merge($allowedIds, $event->getHostIds());
686 + }
687 + }
688 +
689 + if (in_array(get_current_user_id(), $allowedIds)) {
690 + return $booking;
691 + }
692 +
693 + $exception = new ModelNotFoundException();
694 + $exception->setModel(Booking::class, [$bookingId]);
695 + // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
696 + throw $exception;
697 + }
698 +
360 699 private function formatBooking(&$booking)
361 700 {
362 701 $autoCompleteTimeOut = (int) Helper::getGlobalAdminSetting('auto_complete_timing', 60) * 60; // 10 minutes
363 702
@@ -364,9 +703,10 @@
364 703 if (in_array($booking->status, ['scheduled', 'pending']) && (time() - strtotime($booking->end_time)) > $autoCompleteTimeOut) {
365 704 $bookingStatus = $booking->status == 'pending' ? 'cancelled' : 'completed';
366 705 $booking->status = $bookingStatus;
367 706 $booking->save();
368 - do_action('fluent_booking/booking_schedule_' . $bookingStatus, $booking, $booking->calendar_event);
707 + $hookName = $bookingStatus === 'cancelled' ? 'auto_cancelled' : $bookingStatus;
708 + do_action('fluent_booking/booking_schedule_' . $hookName, $booking, $booking->calendar_event);
369 709 }
370 710
371 711 if ($booking->isMultiHostBooking()) {
372 712 $booking->host_profiles = $booking->getHostProfiles();
@@ -380,8 +720,9 @@
380 720 }
381 721
382 722 $booking->title = $booking->getBookingTitle(true);
383 723 $booking->author = $booking->getHostDetails(false);
724 + $booking->details = $booking->getConfirmationData(true);
384 725 $booking->location = $booking->getLocationDetailsHtml();
385 726 $booking->reschedule_url = $booking->getRescheduleUrl();
386 727 $booking->happening_status = $booking->getOngoingStatus();
387 728 $booking->booking_status_text = $booking->getBookingStatus();
@@ -392,32 +733,48 @@
392 733
393 734 return $booking;
394 735 }
395 736
396 - private function getPaymentLog($bookingId)
737 + private function getConfirmedBy()
397 738 {
739 + $confirmedBy = 'host';
740 + $userId = get_current_user_id();
741 + if ($userId && $user = get_user_by('ID', $userId)) {
742 + $confirmedBy = $user->display_name;
743 + }
744 +
745 + return $confirmedBy;
746 + }
747 +
748 + private function getPaymentPaidLog($bookingId)
749 + {
398 750 return [
399 751 'booking_id' => $bookingId,
400 752 'status' => 'closed',
401 753 'type' => 'success',
402 754 'title' => __('Payment Successfully Completed', 'fluent-booking'),
403 - 'description' => __('Payment marked as paid by admin', 'fluent-booking')
755 + 'description' => __('Payment marked as paid by ', 'fluent-booking') . $this->getConfirmedBy()
404 756 ];
405 757 }
406 758
759 + private function getPaymentPendingLog($bookingId)
760 + {
761 + return [
762 + 'booking_id' => $bookingId,
763 + 'status' => 'closed',
764 + 'type' => 'success',
765 + 'title' => __('Payment Successfully Marked as Pending', 'fluent-booking'),
766 + 'description' => __('Payment marked as pending by ', 'fluent-booking') . $this->getConfirmedBy()
767 + ];
768 + }
769 +
407 770 private function getConfirmLog($bookingId)
408 771 {
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 772 return [
416 773 'booking_id' => $bookingId,
417 774 'status' => 'closed',
418 775 'type' => 'success',
419 776 'title' => __('Booking Confirmed', 'fluent-booking'),
420 - 'description' => __('Booking has been confirmed by ', 'fluent-booking') . $confirmedBy
777 + 'description' => __('Booking has been confirmed by ', 'fluent-booking') . $this->getConfirmedBy()
421 778 ];
422 779 }
423 780 }