PluginProbe
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution / 2.1.2
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution v2.1.2
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
fluent-booking / app / Http / Controllers / SchedulesController.php

SchedulesController.php in Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution 2.1.2, at app/Http/Controllers/SchedulesController.php

606 lines 20.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentBooking\App\Http\Controllers;
4
5 use FluentBooking\App\Models\Booking;
6 use FluentBooking\App\Models\Calendar;
7 use FluentBooking\App\Models\CalendarSlot;
8 use FluentBooking\App\Models\BookingActivity;
9 use FluentBooking\Framework\Database\Orm\ModelNotFoundException;
10 use FluentBooking\App\Services\EmailNotificationService;
11 use FluentBooking\App\Services\Helper;
12 use FluentBooking\Framework\Support\Arr;
13 use FluentBooking\App\Services\CurrenciesHelper;
14 use FluentBooking\Framework\Http\Request\Request;
15 use FluentBooking\App\Services\PermissionManager;
16 use FluentBooking\App\Services\CalendarService;
17 use FluentBooking\App\Services\ExportHelper;
18
19 class SchedulesController extends Controller
20 {
21 public function index(Request $request)
22 {
23 $author = $this->resolveAuthor($request);
24
25 $query = $this->buildSchedulesQuery($request, $author);
26
27 $query->groupBy('group_id');
28
29 $schedules = $query->paginate();
30
31 foreach ($schedules as $schedule) {
32 $this->formatBooking($schedule);
33 }
34
35 $data = [
36 'schedules' => $schedules,
37 'timezone' => 'UTC'
38 ];
39
40 $data['calendar_event_lists'] = CalendarService::getCalendarOptionsByTitle();
41
42 if ($author == 'me') {
43 $slotOptions = CalendarService::getSlotOptions(null, get_current_user_id());
44 $data['slot_options'] = $slotOptions;
45 }
46
47 if ($request->get('page') == 1) {
48 $this->addCountsForFirstPage($author, $data);
49 }
50
51 return $data;
52 }
53
54 public function export(Request $request)
55 {
56 $limit = (int) apply_filters('fluent_booking/data_export_limit', 2000);
57
58 $author = $this->resolveAuthor($request);
59
60 $query = $this->buildSchedulesQuery($request, $author);
61
62 $relations = [
63 'calendar_event',
64 'user',
65 'booking_activities' => function ($q) {
66 $q->where('type', 'cancel_reason');
67 },
68 'booking_meta',
69 ];
70
71 if (defined('FLUENT_BOOKING_PRO_DIR_FILE')) {
72 $relations[] = 'payment_order.transaction';
73 }
74
75 $query->with($relations);
76
77 $total = (clone $query)->withoutEagerLoads()->count();
78 $limited = $total > $limit;
79
80 $bookings = $query->take($limit)->get();
81
82 $rows = $bookings->map(function ($booking) {
83 $row = ExportHelper::mapBooking($booking);
84 return apply_filters('fluent_booking/booking_export_row', $row, $booking);
85 })->all();
86
87 $rows = apply_filters('fluent_booking/booking_export_columns', $rows, $bookings);
88
89 return [
90 'bookings' => $rows,
91 'limited' => $limited,
92 'total' => $total,
93 ];
94 }
95
96 protected function resolveAuthor(Request $request)
97 {
98 $author = Arr::get($request->get('filters', []), 'author');
99
100 if (is_numeric($author)) {
101 $author = (int) $author;
102 } else {
103 $author = sanitize_text_field($author);
104 }
105
106 $hasPermission = PermissionManager::userCanSeeAllBookings();
107
108 if ($hasPermission) {
109 return $author;
110 }
111
112 if (!$author || $author == 'all') {
113 $authorCalendar = Calendar::where('user_id', get_current_user_id())
114 ->where('type', 'simple')
115 ->first();
116
117 $author = $authorCalendar ? $authorCalendar->id : '';
118 }
119
120 return $author;
121 }
122
123 protected function buildSchedulesQuery(Request $request, $author = null)
124 {
125 $filters = $request->get('filters', []);
126 $search = $request->getSafe('search');
127 $eventVal = Arr::get($filters, 'event');
128 $eventId = is_numeric($eventVal) ? (int) $eventVal : 0;
129 $eventType = sanitize_text_field(Arr::get($filters, 'event_type'));
130 $period = sanitize_text_field(Arr::get($filters, 'period', 'upcoming'));
131 $range = array_map('sanitize_text_field', Arr::get($filters, 'range', []));
132 $email = sanitize_email(Arr::get($filters, 'email', ''));
133
134 $allowedPeriods = ['upcoming', 'completed', 'cancelled', 'pending', 'no_show', 'latest_bookings', 'all'];
135 if (!in_array($period, $allowedPeriods, true)) {
136 $period = 'upcoming';
137 }
138
139 $query = Booking::with(['calendar_event']);
140
141 $hasPermission = PermissionManager::userCanSeeAllBookings();
142
143 if (!$hasPermission || $author == 'me') {
144 $query->where('host_user_id', get_current_user_id());
145 }
146
147 if ($author && $author !== 'all') {
148 if ($author != 'me') {
149 $query->where('calendar_id', $author);
150 }
151 if ($eventId > 0) {
152 $query->where('event_id', $eventId);
153 }
154 if ($eventType && $eventType !== 'all') {
155 $query->where('event_type', $eventType);
156 }
157 }
158
159 if (!empty($email) && is_email($email)) {
160 $query->where('email', $email);
161 }
162
163 do_action_ref_array('fluent_booking/schedules_query', [&$query]);
164
165 $query->applyDateRangeFilter($range);
166 $query->applyComputedStatus($period);
167 $query->applyBookingOrderByStatus($period);
168
169 if (!empty($search)) {
170 $query->searchBy($search);
171 }
172
173 return $query;
174 }
175
176 private function addCountsForFirstPage($author, &$data)
177 {
178 $bookingQuery = Booking::query()
179 ->when($author == 'me', function($query) {
180 return $query->where('host_user_id', get_current_user_id());
181 })
182 ->when($author && is_numeric($author), function($query) use ($author) {
183 return $query->where('calendar_id', $author);
184 })
185 ->distinct('group_id');
186
187 $data['pending_count'] = (clone $bookingQuery)->whereIn('status', ['pending', 'reserved'])->count('group_id');
188 $data['no_show_count'] = (clone $bookingQuery)->where('status', 'no_show')->count('group_id');
189 $data['cancelled_count'] = (clone $bookingQuery)->where('status', 'cancelled')->count('group_id');
190 }
191
192 public function patchBooking(Request $request, $bookingId)
193 {
194 $booking = Booking::findOrFail($bookingId);
195 $oldBooking = clone $booking;
196
197 $data = $request->all();
198
199 $this->validate($data, [
200 'column' => 'required',
201 ]);
202
203 do_action('fluent_booking/before_patch_booking_schedule', $booking, $data);
204
205 $value = $request->get('value');
206
207 $column = $data['column'];
208
209 if ($booking->{$column} == $value) {
210 return $this->sendError(['message' => __('No changes found', 'fluent-booking')]);
211 }
212
213 $validColumns = [
214 'internal_note',
215 'email',
216 'phone',
217 'first_name',
218 'last_name',
219 'status',
220 'payment_status'
221 ];
222
223 if (!in_array($column, $validColumns)) {
224 return $this->sendError(['message' => __('Invalid column', 'fluent-booking')]);
225 }
226
227 if ($column === 'email') {
228 if (!$value || !is_email($value)) {
229 return $this->sendError(['message' => __('Invalid email address', 'fluent-booking')]);
230 }
231 $value = sanitize_email($value);
232 } else {
233 $value = sanitize_text_field($value);
234 }
235
236 if ($column == 'status') {
237 if (!in_array($value, ['scheduled', 'completed', 'cancelled', 'rejected', 'no_show'])) {
238 return $this->sendError(['message' => __('Invalid status', 'fluent-booking')]);
239 }
240
241 if ($value == 'scheduled' && $booking->payment_method && $booking->payment_order) {
242 $order = $booking->payment_order;
243 $order->total_paid = $order->total_amount;
244 $order->completed_at = gmdate('Y-m-d H:i:s'); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
245 $order->status = 'paid';
246 $order->save();
247
248 $updateData['payment_status'] = 'paid';
249
250 do_action('fluent_booking/log_booking_activity', $this->getPaymentPaidLog($booking->id));
251
252 do_action('fluent_booking/payment/update_payment_status_paid', $booking);
253
254 } else if ($value == 'scheduled') {
255 do_action('fluent_booking/log_booking_activity', $this->getConfirmLog($booking->id));
256 }
257
258 if ($value == 'cancelled') {
259 $cancelReason = sanitize_text_field($data['cancel_reason']);
260 $booking->cancelMeeting($cancelReason, 'host', get_current_user_id());
261 }
262
263 if ($value == 'rejected') {
264 $rejectReason = sanitize_text_field($data['reject_reason']);
265 $booking->rejectMeeting($rejectReason, get_current_user_id());
266 }
267
268 if (in_array($value, ['cancelled', 'rejected'])) {
269 if ($booking->payment_method && Arr::get($data, 'refund_payment') == 'yes') {
270 do_action('fluent_booking/refund_payment_' . $booking->payment_method, $booking, $booking->calendar_event);
271 }
272 return [
273 /* translators: %s: Booking status */
274 'message' => sprintf(__('The booking has been %s', 'fluent-booking'), $value)
275 ];
276 }
277
278 $updateAll = Arr::isTrue($data, 'update_all') && $booking->isMultiGuestBooking();
279
280 if ($updateAll && in_array($value, ['no_show', 'completed'])) {
281 Booking::where('event_id', $booking->event_id)
282 ->where('group_id', $booking->group_id)
283 ->update([
284 'status' => $value
285 ]);
286 return [
287 /* translators: %s: Booking status */
288 'message' => sprintf(__('The booking has been %s', 'fluent-booking'), $value)
289 ];
290 }
291 }
292
293 if ($column == 'payment_status') {
294 if (!in_array($value, ['pending', 'paid'])) {
295 return $this->sendError(['message' => __('Invalid payment status', 'fluent-booking')]);
296 }
297
298 if ($value == 'paid') {
299 do_action('fluent_booking/log_booking_activity', $this->getPaymentPaidLog($booking->id));
300 }
301
302 if ($value == 'pending') {
303 do_action('fluent_booking/log_booking_activity', $this->getPaymentPendingLog($booking->id));
304 }
305
306 if ($booking->payment_order) {
307 do_action('fluent_booking/payment/status_changed', $booking->payment_order, $booking, $value);
308 }
309 }
310
311 $updateData[$column] = $value;
312 $booking->fill($updateData);
313 $booking->save();
314
315 if ($column === 'status') {
316 do_action('fluent_booking/booking_schedule_' . $value, $booking, $booking->calendar_event);
317
318 do_action('fluent_booking/pre_after_booking_' . $value, $booking, $booking->calendar_event);
319
320 $booking = Booking::with(['calendar_event', 'calendar'])->find($booking->id);
321
322 do_action('fluent_booking/after_booking_' . $value, $booking, $booking->calendar_event, $booking);
323 }
324
325 do_action('fluent_booking/after_patch_booking_schedule', $booking, $oldBooking);
326
327 do_action('fluent_booking/after_patch_booking_' . $column, $booking, $booking->calendar_event, $oldBooking->{$column});
328
329 return [
330 /* translators: Updated column name */
331 'message' => sprintf(__('%s has been updated', 'fluent-booking'), ucfirst($column))
332 ];
333 }
334
335 public function getBooking(Request $request, $bookingId)
336 {
337 $booking = Booking::with('calendar_event');
338
339 if (!PermissionManager::userCanSeeAllBookings()) {
340 $booking->whereHas('calendar', function ($q) {
341 $q->where('user_id', get_current_user_id());
342 });
343 }
344
345 $booking = $booking->findOrFail($bookingId);
346 $booking = $this->formatBooking($booking);
347
348 do_action_ref_array('fluent_booking/booking_schedule', [&$booking]);
349
350 $data = [
351 'schedule' => $booking
352 ];
353
354 if (in_array('all_data', $this->request->get('with', []))) {
355 $data = array_merge($data, $this->getBookingMetaInfo($request, $bookingId));
356 }
357
358 return $data;
359 }
360
361 public function deleteBooking(Request $request, $bookingId)
362 {
363 $booking = Booking::findOrFail($bookingId);
364
365 do_action('fluent_booking/before_delete_booking', $booking);
366
367 $booking->delete();
368
369 do_action('fluent_booking/after_delete_booking', $bookingId);
370
371 if ($booking->isMultiGuestBooking()) {
372 Booking::where('event_id', $booking->event_id)->where('group_id', $booking->group_id)->delete();
373 }
374
375 return [
376 'message' => __('Booking Deleted Successfully!', 'fluent-booking')
377 ];
378 }
379
380 public function sendConfirmationEmail(Request $request, $bookingId)
381 {
382 $booking = Booking::with(['calendar', 'calendar_event'])->findOrFail($bookingId);
383
384 $emailTo = $request->get('email_to', 'guest');
385
386 $notifications = $booking->calendar_event->getNotifications();
387
388 $email = Arr::get($notifications, 'booking_conf_attendee.email', []);
389 if ($emailTo == 'host') {
390 $email = Arr::get($notifications, 'booking_conf_host.email', []);
391 }
392
393 $result = EmailNotificationService::emailOnBooked($booking, $email, $emailTo, 'scheduled', true);
394
395 if (!$result) {
396 return $this->sendError(['message' => __('Notification sending failed', 'fluent-booking')]);
397 }
398
399 return [
400 'message' => __('Notification sent successfully', 'fluent-booking')
401 ];
402 }
403
404 public function getGroupAttendees(Request $request, $groupId)
405 {
406 $booking = Booking::with('slot');
407
408 if (!PermissionManager::userCanSeeAllBookings()) {
409 $booking->whereHas('calendar', function ($q) {
410 $q->where('user_id', get_current_user_id());
411 });
412 }
413
414 $booking = $booking->where('group_id', $groupId)->first();
415
416 if (!$booking || !$booking->isMultiGuestBooking()) {
417 return $this->sendError(['message' => __('Invalid group id or the event is not a group event', 'fluent-booking')]);
418 }
419
420 $attendees = Booking::where('group_id', $booking->group_id);
421 $search = $request->getSafe('search');
422
423 if (!empty($search)) {
424 $attendees = $attendees->searchBy($search);
425 }
426 $attendees = $attendees->paginate();
427
428 foreach ($attendees as $attendee) {
429 $attendee = $this->formatBooking($attendee);
430 }
431
432 return [
433 'attendees' => $attendees
434 ];
435 }
436
437 public function getBookingActivities(Request $request, $bookingId)
438 {
439 $this->resolveOwnedBookingOrFail($bookingId);
440
441 $activities = BookingActivity::where('booking_id', $bookingId)
442 ->orderBy('id', 'DESC')
443 ->get();
444
445 return [
446 'activities' => $activities
447 ];
448 }
449
450 public function getBookingMetaInfo(Request $request, $bookingId)
451 {
452 $booking = $this->resolveOwnedBookingOrFail($bookingId);
453
454 $activities = BookingActivity::where('booking_id', $booking->id)
455 ->orderBy('id', 'DESC')
456 ->get();
457
458 $activities->each(function ($activity) {
459 $activity->description = wp_unslash($activity->description);
460 });
461
462 $sidebarContents = [];
463 $mainBodyContents = [];
464
465 if (defined('FLUENTCRM')) {
466 $profileHtml = fluentcrm_get_crm_profile_html($booking->email, false);
467 if ($profileHtml) {
468 $sidebarContents[] = [
469 'id' => 'fluent_crm_profule',
470 'title' => __('CRM Profile', 'fluent-booking'),
471 'content' => $profileHtml
472 ];
473 }
474 }
475
476 $order = null;
477 if ($booking->payment_status && $booking->payment_order) {
478 $order = $booking->payment_order;
479 $relations = ['items', 'transaction'];
480 if (method_exists($order, 'discounts')) {
481 $relations[] = 'discounts';
482 }
483 $order->load($relations);
484 $order->currency_sign = CurrenciesHelper::getCurrencySign($order->currency);
485 }
486
487 $mainBodyContents = apply_filters('fluent_booking/booking_meta_info_main_meta', $mainBodyContents, $booking);
488 $mainBodyContents = apply_filters('fluent_booking/booking_meta_info_main_meta_' . $booking->source, $mainBodyContents, $booking);
489
490 return [
491 'activities' => $activities,
492 'sidebar_contents' => $sidebarContents,
493 'payment_order' => $order,
494 'main_body_contents' => $mainBodyContents
495 ];
496 }
497
498 private function resolveOwnedBookingOrFail($bookingId)
499 {
500 $booking = Booking::findOrFail($bookingId);
501
502 if (PermissionManager::userCanSeeAllBookings()) {
503 return $booking;
504 }
505
506 $allowedIds = $booking->getHostIds();
507
508 if (PermissionManager::userCan('manage_own_calendar')) {
509 if ($event = CalendarSlot::find($booking->event_id)) {
510 $allowedIds = array_merge($allowedIds, $event->getHostIds());
511 }
512 }
513
514 if (in_array(get_current_user_id(), $allowedIds)) {
515 return $booking;
516 }
517
518 $exception = new ModelNotFoundException();
519 $exception->setModel(Booking::class, [$bookingId]);
520 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
521 throw $exception;
522 }
523
524 private function formatBooking(&$booking)
525 {
526 $autoCompleteTimeOut = (int) Helper::getGlobalAdminSetting('auto_complete_timing', 60) * 60; // 10 minutes
527
528 if (in_array($booking->status, ['scheduled', 'pending']) && (time() - strtotime($booking->end_time)) > $autoCompleteTimeOut) {
529 $bookingStatus = $booking->status == 'pending' ? 'cancelled' : 'completed';
530 $booking->status = $bookingStatus;
531 $booking->save();
532 $hookName = $bookingStatus === 'cancelled' ? 'auto_cancelled' : $bookingStatus;
533 do_action('fluent_booking/booking_schedule_' . $hookName, $booking, $booking->calendar_event);
534 }
535
536 if ($booking->isMultiHostBooking()) {
537 $booking->host_profiles = $booking->getHostProfiles();
538 }
539
540 if ($booking->isMultiGuestBooking()) {
541 $booking->booked_count = Booking::where('group_id', $booking->group_id)
542 ->whereIn('status', ['scheduled', 'completed'])->count();
543 } else {
544 $booking->additional_guests = $booking->getAdditionalGuests();
545 }
546
547 $booking->title = $booking->getBookingTitle(true);
548 $booking->author = $booking->getHostDetails(false);
549 $booking->details = $booking->getConfirmationData(true);
550 $booking->location = $booking->getLocationDetailsHtml();
551 $booking->reschedule_url = $booking->getRescheduleUrl();
552 $booking->happening_status = $booking->getOngoingStatus();
553 $booking->booking_status_text = $booking->getBookingStatus();
554 $booking->payment_status_text = $booking->getPaymentStatus();
555 $booking->custom_form_data = $booking->getCustomFormData();
556
557 do_action_ref_array('fluent_booking/format_booking_schedule', [&$booking]);
558
559 return $booking;
560 }
561
562 private function getConfirmedBy()
563 {
564 $confirmedBy = 'host';
565 $userId = get_current_user_id();
566 if ($userId && $user = get_user_by('ID', $userId)) {
567 $confirmedBy = $user->display_name;
568 }
569
570 return $confirmedBy;
571 }
572
573 private function getPaymentPaidLog($bookingId)
574 {
575 return [
576 'booking_id' => $bookingId,
577 'status' => 'closed',
578 'type' => 'success',
579 'title' => __('Payment Successfully Completed', 'fluent-booking'),
580 'description' => __('Payment marked as paid by ', 'fluent-booking') . $this->getConfirmedBy()
581 ];
582 }
583
584 private function getPaymentPendingLog($bookingId)
585 {
586 return [
587 'booking_id' => $bookingId,
588 'status' => 'closed',
589 'type' => 'success',
590 'title' => __('Payment Successfully Marked as Pending', 'fluent-booking'),
591 'description' => __('Payment marked as pending by ', 'fluent-booking') . $this->getConfirmedBy()
592 ];
593 }
594
595 private function getConfirmLog($bookingId)
596 {
597 return [
598 'booking_id' => $bookingId,
599 'status' => 'closed',
600 'type' => 'success',
601 'title' => __('Booking Confirmed', 'fluent-booking'),
602 'description' => __('Booking has been confirmed by ', 'fluent-booking') . $this->getConfirmedBy()
603 ];
604 }
605 }
606