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
fluent-booking / app / Http / Controllers / CalendarController.php

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

1,126 lines 47.4 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\Availability;
6 use FluentBooking\App\Models\Calendar;
7 use FluentBooking\App\Models\CalendarSlot;
8 use FluentBooking\App\Services\Helper;
9 use FluentBooking\App\Services\LandingPage\LandingPageHelper;
10 use FluentBooking\App\Services\PermissionManager;
11 use FluentBooking\App\Services\AvailabilityService;
12 use FluentBooking\App\Services\SanitizeService;
13 use FluentBooking\App\Services\CalendarService;
14 use FluentBooking\App\Services\OnboardingService;
15 use FluentBooking\App\Services\CalendarEventService;
16 use FluentBooking\App\Services\BookingFieldService;
17 use FluentBooking\App\Hooks\Handlers\AdminMenuHandler;
18 use FluentBooking\Framework\Http\Request\Request;
19 use FluentBooking\Framework\Support\Arr;
20
21 class CalendarController extends Controller
22 {
23 public function getAllCalendars(Request $request)
24 {
25 do_action('fluent_booking/before_get_all_calendars', $request);
26
27 $search = sanitize_text_field(Arr::get($request->get('query'), 'search'));
28 $calendarType = sanitize_text_field(Arr::get($request->get('query'), 'calendarType'));
29
30 $applySearchFilter = function($query) use ($search) {
31 $query->where('status', '!=', 'expired');
32 if (!empty($search)) {
33 $query->where('title', 'LIKE', '%' . $search . '%');
34 }
35 };
36
37 $calendarsQuery = Calendar::with(['metas', 'slots' => function($query) use ($applySearchFilter) {
38 $query->where($applySearchFilter);
39 }])
40 ->where('status', '!=', 'expired');
41
42 if (!empty($search)) {
43 $calendarsQuery->whereHas('slots', $applySearchFilter);
44 }
45
46 if (!empty($calendarType) && $calendarType != 'all') {
47 $calendarsQuery->where('type', $calendarType);
48 }
49
50 $calendarsQuery = $calendarsQuery->latest();
51
52 $hasPermission = PermissionManager::hasAllCalendarAccess(true);
53
54 if (!$hasPermission) {
55 $attachedCalendarIds = CalendarService::getAttachedCalendarIds($calendarsQuery);
56 $calendarsQuery->whereIn('id', $attachedCalendarIds);
57 }
58
59 $calendars = $calendarsQuery->paginate();
60
61 foreach ($calendars as $calendar) {
62 $calendar->author_profile = $calendar->getAuthorProfile();
63 $calendar->public_url = $calendar->getLandingPageUrl();
64 $calendar->event_order = $calendar->getMeta('event_order');
65
66 if (!$hasPermission) {
67 $calendar->setRelation('slots', $calendar->slots->filter(function ($slot) {
68 return CalendarEventService::isSharedCalendarEvent($slot);
69 })->values());
70 }
71
72 foreach ($calendar->slots as $slot) {
73 $slot->setRelation('calendar', $calendar);
74 $slot->shortcode = '[fluent_booking id="' . $slot->id . '"]';
75 $slot->public_url = $slot->getPublicUrl();
76 $slot->duration = $slot->getDefaultDuration();
77 $slot->price_total = $slot->getEventPrice();
78 $slot->location_fields = $slot->getLocationFields();
79 $slot->author_profiles = $slot->isMultiHostEvent() ? $slot->getAuthorProfiles() : [];
80 do_action_ref_array('fluent_booking/calendar_slot', [&$slot]);
81 $slot->unsetRelation('calendar');
82 }
83
84 if(empty($calendar->author_profile['ID'])) {
85 $calendar->generic_error = '<p style="color: var(--fcal-danger-fg); margin:0;">Connected Host user is missing</p>';
86 }
87
88 do_action_ref_array('fluent_booking/calendar', [&$calendar, 'lists']);
89 }
90
91 $data = [
92 'calendars' => $calendars
93 ];
94
95 if (in_array('calendar_event_lists', $request->get('with', []))) {
96 $data['calendar_event_lists'] = [
97 'hosts' => CalendarService::getCalendarOptionsByTitle('only_hosts'),
98 'teams' => CalendarService::getCalendarOptionsByTitle('only_teams'),
99 'events' => CalendarService::getCalendarOptionsByTitle('only_events')
100 ];
101 }
102
103 return $data;
104 }
105
106 public function checkSlug(Request $request)
107 {
108 $slug = sanitize_text_field(trim($request->get('slug')));
109
110 if (!Helper::isCalendarSlugAvailable($slug, true)) {
111 return $this->sendError([
112 'message' => __('The provided slug is not available. Please choose a different one', 'fluent-booking')
113 ], 422);
114 }
115
116 return [
117 'status' => true,
118 'message' => __('The provided slug is available', 'fluent-booking')
119 ];
120 }
121
122 public function getNewEventLocationFields(Request $request)
123 {
124 $eventType = SanitizeService::checkCollection(
125 sanitize_text_field($request->get('event_type', 'single')),
126 CalendarSlot::getEventTypes(),
127 'single'
128 );
129
130 // Resolve the organizer the same way createCalendar() does, so the
131 // connection checks run against the host the event will be saved under.
132 $canAssignOthers = PermissionManager::canManageOtherHosts();
133
134 $userId = get_current_user_id();
135 $requestedUserId = (int) $request->get('user_id');
136 if ($requestedUserId && $canAssignOthers) {
137 $userId = $requestedUserId;
138 }
139
140 $calendarEvent = new CalendarSlot();
141 $calendarEvent->event_type = $eventType;
142
143 if ($calendarEvent->isMultiHostEvent()) {
144 $teamMembers = array_values(array_unique(array_filter(
145 array_map('intval', (array) $request->get('team_members', []))
146 )));
147
148 if (!PermissionManager::canAssignHosts($teamMembers, [$userId])) {
149 return $this->sendError([
150 'message' => __('You are not allowed to create a calendar for another user', 'fluent-booking')
151 ], 403);
152 }
153
154 if ($teamMembers && !in_array($userId, $teamMembers, true)) {
155 $userId = reset($teamMembers);
156 }
157
158 $calendarEvent->settings = ['team_members' => $teamMembers];
159 }
160
161 $calendarEvent->user_id = $userId;
162
163 return [
164 'location_fields' => $calendarEvent->getLocationFields()
165 ];
166 }
167
168 public function createCalendar(Request $request)
169 {
170 $data = $request->get('calendar');
171
172 $rules = [
173 'author_timezone' => 'required',
174 'slot.duration' => 'required|numeric|min:5',
175 'slot.event_type' => 'required',
176 'slot.availability_type' => 'required',
177 'slot.schedule_type' => 'required',
178 'slot.title' => 'required',
179 'slot.weekly_schedules' => 'required_if:slot.schedule_type,weekly_schedules',
180 'slot.location_settings.*.type' => 'required',
181 'slot.location_settings.*.host_phone_number' => 'required_if:location_settings.*.type,phone_organizer'
182 ];
183
184 $messages = [
185 'author_timezone.required' => __('Author timezone field is required', 'fluent-booking'),
186 'slot.duration.required' => __('Event duration field is required', 'fluent-booking'),
187 'slot.event_type.required' => __('Event type field is required', 'fluent-booking'),
188 'slot.availability_type.required' => __('Event availability type field is required', 'fluent-booking'),
189 'slot.schedule_type.required' => __('Event schedule type field is required', 'fluent-booking'),
190 'slot.title.required' => __('Event title field is required', 'fluent-booking'),
191 'slot.weekly_schedules.required_if' => __('Event weekly schedules field is required', 'fluent-booking'),
192 'slot.location_settings.*.type.required' => __('Event location type field is required', 'fluent-booking'),
193 'slot.location_settings.*.host_phone_number.required_if' => __('Event location host phone number field is required', 'fluent-booking')
194 ];
195
196 $validationConfig = apply_filters('fluent_booking/create_calendar_validation_rule', [
197 'rules' => $rules,
198 'messages' => $messages
199 ], $data);
200
201 $this->validate($data, $validationConfig['rules'], $validationConfig['messages']);
202
203 do_action('fluent_booking/before_create_calendar', $data, $this);
204
205 if (!empty($data['user_id']) && PermissionManager::canManageOtherHosts()) {
206 $user = get_user_by('ID', $data['user_id']);
207 } else {
208 $user = get_user_by('ID', get_current_user_id());
209 }
210
211 if (!$user) {
212 return $this->sendError([
213 'message' => __('User not found', 'fluent-booking')
214 ], 422);
215 }
216
217 $onboardinFeatures = $request->get('features');
218 if (!empty($onboardinFeatures)) {
219 $installableAddons = SanitizeService::sanitizeAddons($onboardinFeatures);
220 OnboardingService::installAddons($installableAddons);
221 }
222
223 $type = SanitizeService::checkCollection(
224 sanitize_text_field(Arr::get($data, 'type', 'simple')),
225 ['simple', 'team', 'event'],
226 'simple'
227 );
228
229 $isHostCalendar = $type == 'simple' ? true : false;
230
231 if ($isHostCalendar && Calendar::where('user_id', $user->ID)->where('type', 'simple')->first()) {
232 return $this->sendError([
233 'message' => __('The user already has a calendar. Please delete it first to create a new one', 'fluent-booking')
234 ], 422);
235 }
236
237 if ($isHostCalendar) {
238 $userName = $user->user_login;
239 if (is_email($userName)) {
240 $userName = explode('@', $userName);
241 $userName = $userName[0] . '-' . time();
242 }
243 $data['slug'] = sanitize_title($userName, '', 'display');
244 }
245
246 $slot = $data['slot'];
247
248 if (!$isHostCalendar) {
249 $title = sanitize_text_field(Arr::get($data, 'title', ''));
250 $data['slug'] = sanitize_title($title, '', 'display');
251 $teamMembers = array_values(array_filter(
252 array_map('intval', (array) Arr::get($slot, 'settings.team_members', []))
253 ));
254
255 cache_users($teamMembers);
256
257 foreach ($teamMembers as $memberId) {
258 if (!get_user_by('ID', $memberId)) {
259 return $this->sendError([
260 'message' => __('Invalid Team Member', 'fluent-booking')
261 ], 422);
262 }
263 }
264
265 // Gated even when the creator is listed too, not only when they are absent.
266 if (!PermissionManager::canAssignHosts($teamMembers, [$user->ID])) {
267 return $this->sendError([
268 'message' => __('You are not allowed to create a calendar for another user', 'fluent-booking')
269 ], 403);
270 }
271
272 if (!in_array($user->ID, $teamMembers, true)) {
273 $user = get_user_by('ID', reset($teamMembers));
274 if (!$user) {
275 return $this->sendError([
276 'message' => __('Invalid Team Member', 'fluent-booking')
277 ], 422);
278 }
279 }
280 }
281
282 if (!Helper::isCalendarSlugAvailable($data['slug'], true)) {
283 $data['slug'] .= '-' . time();
284 }
285
286 if (!empty($data['slug'])) {
287 $slug = trim(sanitize_text_field($data['slug']));
288 if (!Helper::isCalendarSlugAvailable($slug, true)) {
289 return $this->sendError([
290 'message' => __('The provided slug is not available. Please choose a different one', 'fluent-booking')
291 ], 422);
292 }
293
294 $personName = trim($user->first_name . ' ' . $user->last_name);
295 if (!$personName) {
296 $personName = $user->display_name;
297 }
298
299 $calendarData = [
300 'slug' => $slug,
301 'user_id' => $user->ID,
302 'title' => $isHostCalendar ? $personName : $title,
303 'type' => $type,
304 'author_timezone' => sanitize_text_field($data['author_timezone']) ?: 'UTC',
305 ];
306 $calendar = Calendar::create($calendarData);
307 } else {
308 $calendar = Calendar::where('user_id', $user->ID)->where('type', 'simple')->first();
309 }
310
311 if (!$calendar) {
312 return $this->sendError([
313 'message' => __('Calendar could not be found. Please try again', 'fluent-booking')
314 ], 422);
315 }
316
317 $weeklySchedule = Arr::get($data, 'slot.weekly_schedules', []);
318
319 $availability = AvailabilityService::maybeCreateAvailability($calendar, $weeklySchedule);
320
321 $title = (!empty($slot['title'])) ? sanitize_text_field($slot['title']) : $slot['duration'] . ' Minute Meeting';
322
323 $slotData = [
324 'title' => $title,
325 'slug' => Helper::generateSlotSlug((int)$slot['duration'] . 'min', $calendar),
326 'calendar_id' => $calendar->id,
327 'user_id' => $calendar->user_id,
328 'duration' => (int)$slot['duration'],
329 'description' => wp_kses_post(Arr::get($slot, 'description')),
330 'settings' => [
331 'team_members' => !$isHostCalendar ? $teamMembers : [],
332 'schedule_type' => sanitize_text_field($slot['schedule_type']),
333 'weekly_schedules' => SanitizeService::weeklySchedules($slot['weekly_schedules'], $calendar->author_timezone, 'UTC', true)
334 ],
335 'status' => SanitizeService::checkCollection($slot['status'], ['active', 'draft']),
336 'color_schema' => sanitize_text_field(Arr::get($slot, 'color_schema', '#0099ff')),
337 'event_type' => sanitize_text_field(Arr::get($slot, 'event_type')),
338 'availability_type' => SanitizeService::checkCollection($slot['availability_type'], ['existing_schedule', 'custom'], 'existing_schedule'),
339 'availability_id' => (int)$availability->id,
340 'location_type' => sanitize_text_field(Arr::get($slot, 'location_type')),
341 'location_heading' => wp_kses_post(Arr::get($slot, 'location_heading')),
342 'location_settings' => SanitizeService::locationSettings(Arr::get($slot, 'location_settings', [])),
343 ];
344
345 $slotData['settings'] = wp_parse_args($slotData['settings'], (new CalendarSlot())->getSlotSettingsSchema());
346
347 $slotData = apply_filters('fluent_booking/create_calendar_event_data', $slotData, $calendar);
348
349 $slot = CalendarSlot::create($slotData);
350
351 do_action('fluent_booking/after_create_calendar', $calendar);
352
353 do_action('fluent_booking/after_create_calendar_slot', $slot, $calendar);
354
355 return [
356 'calendar' => $calendar,
357 'slot' => $slot,
358 'redirect_url' => Helper::getAppBaseUrl('calendars/' . $calendar->id . '/slot-settings/' . $slot->id)
359 ];
360 }
361
362 public function getCalendar(Request $request, $calendarId)
363 {
364 $calendar = Calendar::with(['slots' => function ($query) {
365 $query->where('status', '!=', 'expired');
366 }])->findOrFail($calendarId);
367
368 $calendar->author_profile = $calendar->getAuthorProfile();
369 $calendar->event_order = $calendar->getMeta('event_order');
370
371 if (!PermissionManager::hasAllCalendarAccess(true)) {
372 $calendar->setRelation('slots', $calendar->slots->filter(function ($slot) {
373 return CalendarEventService::isSharedCalendarEvent($slot);
374 })->values());
375 }
376
377 foreach ($calendar->slots as $slot) {
378 $slot->setRelation('calendar', $calendar);
379 $slot->shortcode = '[fluent_booking id="' . $slot->id . '"]';
380 $slot->public_url = $slot->getPublicUrl();
381 $slot->duration = $slot->getDefaultDuration();
382 $slot->price_total = $slot->getEventPrice();
383 $slot->location_fields = $slot->getLocationFields();
384 $slot->author_profiles = $slot->isMultiHostEvent() ? $slot->getAuthorProfiles() : [];
385 do_action_ref_array('fluent_booking/calendar_slot', [&$slot]);
386 $slot->unsetRelation('calendar');
387 }
388
389 $data = [
390 'calendar' => $calendar
391 ];
392
393 if (in_array('settings_menu', $request->get('with', []))) {
394 $data['settings_menu'] = AdminMenuHandler::getCalendarSettingsMenuItems($calendar);
395 }
396
397 if (in_array('public_url', $request->get('with', []))) {
398 $data['public_url'] = $calendar->getLandingPageUrl();
399 }
400
401 return $data;
402 }
403
404 public function getSharingSettings(Request $request, $calendarId)
405 {
406 $calendar = Calendar::findOrFail($calendarId);
407
408 return [
409 'settings' => LandingPageHelper::getSettings($calendar),
410 'share_url' => $calendar->getLandingPageUrl(true),
411 'public_url' => $this->getSharePublicUrl($calendar, intval($request->get('event_id')))
412 ];
413 }
414
415 public function saveSharingSettings(Request $request, $calendarId)
416 {
417 $calendar = Calendar::findOrFail($calendarId);
418
419 $calendarDataItems = Arr::only($request->get('calendar_data', []), ['title', 'timezone', 'description', 'calendar_avatar', 'featured_image', 'phone']);
420
421 if ($calendarDataItems) {
422 $this->validate($calendarDataItems, [
423 'title' => 'required',
424 'calendar_avatar' => 'nullable|url',
425 'featured_image' => 'nullable|url'
426 ]);
427
428 $updatedTimezone = sanitize_text_field(Arr::get($calendarDataItems, 'timezone'));
429 if ($updatedTimezone && $updatedTimezone != $calendar->author_timezone) {
430 CalendarService::updateCalendarEventsSchedule($calendarId, $calendar->author_timezone, $updatedTimezone);
431 $calendar->author_timezone = $updatedTimezone;
432 }
433
434 $calendar->title = sanitize_text_field(Arr::get($calendarDataItems, 'title'));
435 $calendar->description = wp_kses_post(Arr::get($calendarDataItems, 'description'));
436 $calendar->updateMeta('profile_photo_url', sanitize_url(Arr::get($calendarDataItems, 'calendar_avatar')));
437 $calendar->updateMeta('featured_image_url', sanitize_url(Arr::get($calendarDataItems, 'featured_image')));
438 $calendar->save();
439
440 if ($calendar->user) {
441 $calendar->user->updateMeta('host_phone', sanitize_text_field(Arr::get($calendarDataItems, 'phone')));
442 }
443 }
444
445 $sharingSettings = $request->get('landing_page_settings', []);
446 LandingPageHelper::updateSettings($calendar, $sharingSettings);
447
448 return [
449 'message' => __('Landing Page settings has been updated', 'fluent-booking'),
450 'public_url' => $this->getSharePublicUrl($calendar, intval($request->get('event_id')))
451 ];
452 }
453
454 private function getSharePublicUrl($calendar, $eventId)
455 {
456 if ($eventId) {
457 $event = CalendarSlot::where('calendar_id', $calendar->id)
458 ->where('id', $eventId)
459 ->first();
460 if ($event) {
461 return $event->getPublicUrl();
462 }
463 }
464
465 return $calendar->getLandingPageUrl();
466 }
467
468 public function updateCalendar(Request $request, $calendarId)
469 {
470 $data = $request->all();
471
472 $calendar = Calendar::findOrFail($calendarId);
473
474 do_action_ref_array('fluent_booking/before_update_calendar', [&$calendar, $data]);
475
476 $calendar->description = wp_kses_post($request->get('description'));
477 $calendar->save();
478 do_action('fluent_booking/after_update_calendar', $calendar, $data);
479
480 $calendar->author_profile = $calendar->getAuthorProfile();
481
482 do_action_ref_array('fluent_booking/calendar', [&$calendar, 'update']);
483
484 return [
485 'calendar' => $calendar,
486 'message' => __('Calendar has been updated successfully', 'fluent-booking')
487 ];
488 }
489
490 public function getEvent(Request $request, $calendarId, $eventId)
491 {
492 $calendarEvent = CalendarSlot::where('calendar_id', $calendarId)->with(['calendar.user'])->findOrFail($eventId);
493
494 $calendarEvent->author_profile = $calendarEvent->getAuthorProfile();
495
496 $calendarEvent->calendar->author_profile = $calendarEvent->calendar->getAuthorProfile();
497
498 $calendarEvent->public_url = $calendarEvent->getPublicUrl();
499
500 $eventSettings = $calendarEvent->settings;
501
502 $eventSettings['weekly_schedules'] = SanitizeService::weeklySchedules($eventSettings['weekly_schedules'], 'UTC', $calendarEvent->calendar->author_timezone);
503
504 $eventSettings['date_overrides'] = (object)SanitizeService::slotDateOverrides(Arr::get($eventSettings, 'date_overrides', []), 'UTC', $calendarEvent->calendar->author_timezone, $calendarEvent);
505
506 $eventSettings['location_fields'] = $calendarEvent->getLocationFields();
507
508 $eventSettings['hosts_schedules'] = $calendarEvent->getHostsSchedules();
509
510 $calendarEvent->settings = apply_filters('fluent_booking/get_calendar_event_settings', $eventSettings, $calendarEvent, $calendarEvent->calendar);
511
512 $data = [
513 'calendar_event' => $calendarEvent
514 ];
515
516 if (in_array('calendar', $this->request->get('with', []))) {
517 $calendar = $calendarEvent->calendar;
518 $calendar->author_profile = $calendar->getAuthorProfile();
519 $data['calendar'] = $calendar;
520 }
521
522 if (in_array('smart_codes', $this->request->get('with', []))) {
523 $data['smart_codes'] = [
524 'texts' => Helper::getEditorShortCodes($calendarEvent),
525 'html' => Helper::getEditorShortCodes($calendarEvent, true)
526 ];
527 }
528
529 if (in_array('settings_menu', $this->request->get('with', []))) {
530 $data['settings_menu'] = AdminMenuHandler::getEventSettingsMenuItems($calendarEvent);
531 }
532
533 if (in_array('calendar_event_lists', $this->request->get('with', []))) {
534 $data['calendar_event_lists'] = CalendarService::getCalendarOptionsByTitle();
535 }
536
537 return $data;
538 }
539
540 public function getEventSchema(Request $request, $calendarId)
541 {
542 $calendar = Calendar::findOrFail($calendarId);
543
544 $schema = (new CalendarSlot())->getEventSchema($calendar);
545
546 return [
547 'slot' => $schema
548 ];
549 }
550
551 public function getAvailabilitySettings(Request $request, $calendarId, $eventId)
552 {
553 $availableSchedules = AvailabilityService::availabilitySchedules();
554
555 $scheduleOptions = AvailabilityService::getScheduleOptions();
556
557 return [
558 'schedule_options' => $scheduleOptions,
559 'available_schedules' => $availableSchedules
560 ];
561 }
562
563 public function createCalendarEvent(Request $request, $calendarId)
564 {
565 $calendar = Calendar::findOrFail($calendarId);
566
567 $slot = $request->all();
568
569 $rules = [
570 'title' => 'required',
571 'duration' => 'required|numeric|min:5',
572 'status' => 'required',
573 'event_type' => 'required',
574 'location_settings.*.type' => 'required',
575 'location_settings.*.title' => 'required_if:location_settings.*.type,custom',
576 'location_settings.*.description' => 'required_if:location_settings.*.type,address_organizer',
577 'location_settings.*.host_phone_number' => 'required_if:location_settings.*.type,phone_organizer'
578 ];
579
580 $messages = [
581 'title.required' => __('Event title field is required', 'fluent-booking'),
582 'duration.required' => __('Event duration field is required', 'fluent-booking'),
583 'status.required' => __('Event status field is required', 'fluent-booking'),
584 'event_type.required' => __('Event type field is required', 'fluent-booking'),
585 'location_settings.*.type.required' => __('Event location type field is required', 'fluent-booking'),
586 'location_settings.*.title.required_if' => __('Event location title field is required', 'fluent-booking'),
587 'location_settings.*.description.required_if' => __('Event location description field is required', 'fluent-booking'),
588 'location_settings.*.host_phone_number.required_if' => __('Event location host phone number field is required', 'fluent-booking')
589 ];
590
591 $validationConfig = apply_filters('fluent_booking/create_calendar_event_validation_rule', [
592 'rules' => $rules,
593 'messages' => $messages
594 ], $slot);
595
596 $this->validate($slot, $validationConfig['rules'], $validationConfig['messages']);
597
598 $teamMembers = array_values(array_unique(array_filter(
599 array_map('intval', (array) Arr::get($slot, 'settings.team_members', []))
600 )));
601
602 cache_users($teamMembers);
603
604 foreach ($teamMembers as $memberId) {
605 if (!get_user_by('ID', $memberId)) {
606 return $this->sendError([
607 'message' => __('Invalid Team Member', 'fluent-booking')
608 ], 422);
609 }
610 }
611
612 if ($teamMembers && !PermissionManager::canAssignHosts($teamMembers, $calendar->getMemberIds())) {
613 return $this->sendError([
614 'message' => __('You are not allowed to create a calendar for another user', 'fluent-booking')
615 ], 403);
616 }
617
618 $availability = AvailabilityService::getDefaultSchedule($calendar->user_id);
619
620 $slotData = [
621 'title' => sanitize_text_field($slot['title']),
622 'slug' => Helper::generateSlotSlug($slot['duration'] . 'min', $calendar),
623 'calendar_id' => $calendar->id,
624 'user_id' => $calendar->user_id,
625 'duration' => (int)$slot['duration'],
626 'description' => wp_kses_post(Arr::get($slot, 'description')),
627 'settings' => [
628 'schedule_type' => sanitize_text_field($slot['settings']['schedule_type']),
629 'weekly_schedules' => SanitizeService::weeklySchedules($slot['settings']['weekly_schedules'], $calendar->author_timezone, 'UTC', true),
630 'date_overrides' => SanitizeService::slotDateOverrides(Arr::get($slot['settings'], 'date_overrides', []), $calendar->author_timezone, 'UTC', null, true),
631 'range_type' => sanitize_text_field(Arr::get($slot['settings'], 'range_type')),
632 'range_days' => (int)(Arr::get($slot['settings'], 'range_days', 60)) ?: 60,
633 'range_date_between' => SanitizeService::rangeDateBetween(Arr::get($slot['settings'], 'range_date_between', ['', ''])),
634 'schedule_conditions' => SanitizeService::scheduleConditions(Arr::get($slot['settings'], 'schedule_conditions', [])),
635 'buffer_time_before' => sanitize_text_field(Arr::get($slot['settings'], 'buffer_time_before', '0')),
636 'buffer_time_after' => sanitize_text_field(Arr::get($slot['settings'], 'buffer_time_after', '0')),
637 'slot_interval' => sanitize_text_field(Arr::get($slot['settings'], 'slot_interval', '')),
638 'team_members' => $teamMembers
639 ],
640 'status' => SanitizeService::checkCollection($slot['status'], ['active', 'draft'], 'active'),
641 'color_schema' => sanitize_text_field(Arr::get($slot, 'color_schema', '#0099ff')),
642 'event_type' => SanitizeService::checkCollection(sanitize_text_field(Arr::get($slot, 'event_type')), CalendarSlot::getEventTypes(), 'single'),
643 'availability_type' => 'existing_schedule',
644 'availability_id' => $availability ? $availability->id : null,
645 'location_type' => sanitize_text_field(Arr::get($slot, 'location_type')),
646 'location_settings' => SanitizeService::locationSettings(Arr::get($slot, 'location_settings', [])),
647 'max_book_per_slot' => (int)Arr::get($slot, 'max_book_per_slot', 1),
648 'is_display_spots' => (bool)Arr::get($slot, 'is_display_spots', false),
649 ];
650
651 $slotData = apply_filters('fluent_booking/create_calendar_event_data', $slotData, $calendar);
652
653 do_action('fluent_booking/before_create_event', $calendar, $slotData);
654
655 $createdSlot = CalendarSlot::create($slotData);
656
657 do_action('fluent_booking/after_create_event', $calendar, $createdSlot);
658
659 $calendar->updateEventOrder($createdSlot->id);
660
661 return [
662 'message' => __('New Event Type has been created successfully', 'fluent-booking'),
663 'slot' => $createdSlot
664 ];
665 }
666
667 public function updateEventDetails(Request $request, $calendarId, $eventId)
668 {
669 $data = $request->all();
670
671 $event = CalendarSlot::where('calendar_id', $calendarId)->findOrFail($eventId);
672
673 $rules = [
674 'title' => 'required',
675 'duration' => 'required|numeric',
676 'location_settings.*.type' => 'required',
677 'location_settings.*.title' => 'required_if:location_settings.*.type,in_person_organizer',
678 'location_settings.*.host_phone_number' => 'required_if:location_settings.*.type,phone_organizer'
679 ];
680
681 $messages = [
682 'title.required' => __('Event title field is required', 'fluent-booking'),
683 'duration.required' => __('Event duration field is required', 'fluent-booking'),
684 'location_settings.*.type.required' => __('Event location type field is required', 'fluent-booking'),
685 'location_settings.*.title.required_if' => __('Event location title field is required', 'fluent-booking'),
686 'location_settings.*.host_phone_number.required_if' => __('Event location host phone number field is required', 'fluent-booking')
687 ];
688
689 if ('group' === $event->event_type) {
690 $rules = array_merge($rules, [
691 'max_book_per_slot' => 'required|numeric|min:1',
692 'is_display_spots' => 'required|min:0|max:1',
693 ]);
694 $messages = array_merge($messages, [
695 'max_book_per_slot.required' => __('Event max book per slot field is required', 'fluent-booking'),
696 'is_display_spots.required' => __('Event is display spots field is required', 'fluent-booking')
697 ]);
698 } else {
699 $rules = array_merge($rules, [
700 'multi_duration.default_duration' => 'required_if:multi_duration.enabled,true',
701 'multi_duration.available_durations' => 'required_if:multi_duration.enabled,true'
702 ]);
703 $messages = array_merge($messages, [
704 'multi_duration.default_duration.required_if' => __('Event default duration is required', 'fluent-booking'),
705 'multi_duration.available_durations.required_if' => __('Event available durations is required', 'fluent-booking')
706 ]);
707 }
708
709 $validationConfig = apply_filters('fluent_booking/update_event_details_validation_rule', [
710 'rules' => $rules,
711 'messages' => $messages
712 ], $event);
713
714 $this->validate($data, $validationConfig['rules'], $validationConfig['messages']);
715
716 $event->title = sanitize_text_field($data['title']);
717 $event->duration = (int)$data['duration'];
718 $event->status = SanitizeService::checkCollection($data['status'], ['active', 'draft']);
719 $event->color_schema = sanitize_text_field(Arr::get($data, 'color_schema', '#0099ff'));
720 $event->description = wp_kses_post(Arr::get($data, 'description'));
721 $event->max_book_per_slot = (int)Arr::get($data, 'max_book_per_slot');
722 $event->is_display_spots = (bool)Arr::get($data, 'is_display_spots');
723 $event->location_settings = SanitizeService::locationSettings(Arr::get($data, 'location_settings', []));
724
725 $event->settings = [
726 'multi_duration' => [
727 'enabled' => Arr::isTrue($data, 'multi_duration.enabled'),
728 'default_duration' => Arr::get($data, 'multi_duration.default_duration', ''),
729 'available_durations' => array_map('sanitize_text_field', Arr::get($data, 'multi_duration.available_durations', []))
730 ]
731 ];
732
733 $event->save();
734
735 do_action('fluent_booking/after_update_event_details', $event);
736
737 return [
738 'message' => __('Data has been updated', 'fluent-booking'),
739 'event' => $event
740 ];
741 }
742
743 public function updateEventAvailability(Request $request, $calendarId, $eventId)
744 {
745 $data = $request->all();
746
747 $event = CalendarSlot::where('calendar_id', $calendarId)->findOrFail($eventId);
748
749 $eventSettings = [
750 'schedule_type' => sanitize_text_field(Arr::get($data, 'schedule_type')),
751 'weekly_schedules' => SanitizeService::weeklySchedules(Arr::get($data, 'weekly_schedules'), $event->calendar->author_timezone, 'UTC', true),
752 'date_overrides' => SanitizeService::slotDateOverrides(Arr::get($data, 'date_overrides', []), $event->calendar->author_timezone, 'UTC', null, true),
753 'range_type' => sanitize_text_field(Arr::get($data, 'range_type')),
754 'range_days' => (int)(Arr::get($data, 'range_days', 60)) ?: 60,
755 'range_date_between' => SanitizeService::rangeDateBetween(Arr::get($data, 'range_date_between', ['', ''])),
756 'common_schedule' => Arr::isTrue($data, 'common_schedule', false)
757 ];
758
759 $hostsSchedules = [];
760
761 if ($event->isTeamEvent()) {
762 $hostsSchedules = array_map('intval', array_combine(
763 array_map('intval', array_keys(Arr::get($data, 'hosts_schedules', []))),
764 array_map('intval', Arr::get($data, 'hosts_schedules', []))
765 ));
766 }
767
768 $availabilityId = (int)Arr::get($data, 'availability_id');
769 $availabilityType = SanitizeService::checkCollection(Arr::get($data, 'availability_type'), ['existing_schedule', 'custom']);
770
771 $submittedIds = array_values(array_filter(array_unique(array_merge(
772 [$availabilityType === 'existing_schedule' ? $availabilityId : 0],
773 array_values($hostsSchedules)
774 ))));
775
776 $usableIds = [];
777 $scheduleOwners = [];
778
779 if ($submittedIds) {
780 $usableIds = array_map('intval', AvailabilityService::usableAvailabilityQuery()
781 ->whereIn('id', $submittedIds)->pluck('id')->toArray());
782
783 $scheduleOwners = array_map('intval', Availability::whereIn('id', $submittedIds)
784 ->pluck('object_id', 'id')->toArray());
785 }
786
787 foreach ($hostsSchedules as $hostId => $scheduleId) {
788 if (($scheduleOwners[$scheduleId] ?? 0) === (int)$hostId) {
789 continue;
790 }
791
792 if (!in_array($scheduleId, $usableIds, true)) {
793 return $this->sendError([
794 'message' => __('You are not allowed to use the selected schedule', 'fluent-booking')
795 ], 403);
796 }
797 }
798
799 if ($hostsSchedules) {
800 $eventSettings['hosts_schedules'] = $hostsSchedules;
801 }
802
803 $eventHostIds = array_map('intval', array_merge(
804 $event->getHostIds(),
805 [$event->user_id, $event->calendar->user_id]
806 ));
807
808 if ($availabilityType === 'existing_schedule' && $availabilityId
809 && !in_array($availabilityId, $usableIds, true)
810 && !in_array($scheduleOwners[$availabilityId] ?? 0, $eventHostIds, true)) {
811 return $this->sendError([
812 'message' => __('You are not allowed to use the selected schedule', 'fluent-booking')
813 ], 403);
814 }
815
816 $event->settings = $eventSettings;
817
818 $event->availability_id = $availabilityId;
819 $event->availability_type = $availabilityType;
820
821 $event->save();
822
823 return [
824 'message' => __('Data has been updated', 'fluent-booking'),
825 'event' => $event
826 ];
827 }
828
829 public function updateEventLimits(Request $request, $calendarId, $eventId)
830 {
831 $data = $request->all();
832
833 $event = CalendarSlot::where('calendar_id', $calendarId)->findOrFail($eventId);
834
835 $event->settings = [
836 'schedule_conditions' => SanitizeService::scheduleConditions(Arr::get($data['settings'], 'schedule_conditions', [])),
837 'buffer_time_before' => sanitize_text_field(Arr::get($data, 'settings.buffer_time_before', '0')),
838 'buffer_time_after' => sanitize_text_field(Arr::get($data, 'settings.buffer_time_after', '0')),
839 'slot_interval' => sanitize_text_field(Arr::get($data, 'settings.slot_interval', '')),
840 'booking_frequency' => [
841 'enabled' => Arr::isTrue($data, 'settings.booking_frequency.enabled'),
842 'limits' => $this->sanitize_mapped_data(Arr::get($data, 'settings.booking_frequency.limits'))
843 ],
844 'booking_duration' => [
845 'enabled' => Arr::isTrue($data, 'settings.booking_duration.enabled'),
846 'limits' => $this->sanitize_mapped_data(Arr::get($data, 'settings.booking_duration.limits'))
847 ],
848 'lock_timezone' => [
849 'enabled' => Arr::isTrue($data, 'settings.lock_timezone.enabled'),
850 'timezone' => sanitize_text_field(Arr::get($data, 'settings.lock_timezone.timezone'))
851 ],
852 ];
853
854 $event->save();
855
856 return [
857 'message' => __('Data has been updated', 'fluent-booking'),
858 'event' => $event
859 ];
860 }
861
862 public function patchCalendarEvent(Request $request, $calendarId, $eventId)
863 {
864 $slot = CalendarSlot::where('calendar_id', $calendarId)->findOrFail($eventId);
865
866 $status = $request->get('status');
867
868 if ($status) {
869 $slot->status = $status;
870 $slot->save();
871 }
872
873 return [
874 'message' => __('Data has been updated', 'fluent-booking')
875 ];
876
877 }
878
879 public function cloneCalendarEvent(Request $request, $calendarId, $eventId)
880 {
881 $newCalendarId = intval($request->get('new_calendar_id')) ?: $calendarId;
882
883 if (!PermissionManager::canWriteCalendar($newCalendarId)) {
884 return $this->sendError([
885 'message' => __('You do not have permission to write to the destination calendar.', 'fluent-booking')
886 ], 403);
887 }
888
889 $calendar = Calendar::findOrFail($newCalendarId);
890
891 $originalEvent = CalendarSlot::with('event_metas')->where('calendar_id', $calendarId)->findOrFail($eventId);
892
893 $teamMembers = Arr::get($originalEvent->settings, 'team_members', []);
894
895 // Cloning into another calendar carries the source hosts along with it.
896 if ($teamMembers && $calendar->id != $calendarId
897 && !PermissionManager::canAssignHosts($teamMembers, $calendar->getMemberIds())) {
898 return $this->sendError([
899 'message' => __('You are not allowed to create a calendar for another user', 'fluent-booking')
900 ], 403);
901 }
902
903 $clonedEvent = $originalEvent->replicate();
904
905 $clonedEvent->hash = null;
906
907 $clonedEvent->calendar_id = $calendar->id;
908
909 $clonedEvent->user_id = $calendar->user_id;
910
911 $clonedEvent->title = $originalEvent->title . ' (clone)';
912
913 $clonedEvent->slug = Helper::generateSlotSlug($clonedEvent->duration . 'min', $calendar);
914
915 $clonedEvent->save();
916
917 $calendar->updateEventOrder($clonedEvent->id);
918
919 $eventsMeta = $originalEvent->event_metas;
920
921 foreach ($eventsMeta as $meta) {
922 $clonedMeta = $meta->replicate();
923 $clonedMeta->object_id = $clonedEvent->id;
924 $clonedMeta->save();
925 }
926
927 return [
928 'slot' => $clonedEvent,
929 'message' => __('The Event Type has been cloned successfully', 'fluent-booking')
930 ];
931 }
932
933 public function saveCalendarEventOrder(Request $request, $calendarId)
934 {
935 $calendar = Calendar::findOrFail($calendarId);
936
937 $eventOrder = array_map('intval', $request->get('event_order', []));
938
939 $calendar->updateMeta('event_order', array_filter($eventOrder));
940
941 return [
942 'calendar' => $calendar,
943 'message' => __('Event order has been updated', 'fluent-booking')
944 ];
945 }
946
947 public function cloneEventEmailNotification(Request $request, $calendarId, $eventId)
948 {
949 $calendarEvent = CalendarSlot::where('calendar_id', $calendarId)->findOrFail($eventId);
950
951 $fromEventId = intval($request->get('from_event_id'));
952
953 if (!$fromEventId || !PermissionManager::canUpdateCalendarEvent($fromEventId)) {
954 return $this->sendError([
955 'message' => __('You do not have permission to clone from the selected event.', 'fluent-booking')
956 ], 403);
957 }
958
959 $fromCalendarEvent = CalendarSlot::findOrFail($fromEventId);
960
961 $notification = $fromCalendarEvent->getNotifications(true);
962
963 $calendarEvent->setNotifications($notification);
964
965 $calendarEvent->save();
966
967 return [
968 'message' => __('The Notification has been cloned successfully', 'fluent-booking'),
969 'notifications' => $notification
970 ];
971 }
972
973 public function getEventEmailNotifications(Request $request, $calendarId, $eventId)
974 {
975 $calendarEvent = CalendarSlot::where('calendar_id', $calendarId)->findOrFail($eventId);
976
977 /*
978 * Confirmation Email to Attendee
979 * Confirmation Email to Organizer
980 * Reminder Email to Attendee [before 1 day, 1 hour, 30 minutes, 5 minutes]
981 * Cancelled By Organizer to Attendee
982 * Cancelled By Attendee to Organizer
983 */
984 $data = [
985 'notifications' => $calendarEvent->getNotifications(true)
986 ];
987
988 if (in_array('smart_codes', $request->get('with', []))) {
989 $data['smart_codes'] = [
990 'texts' => Helper::getEditorShortCodes($calendarEvent),
991 'html' => Helper::getEditorShortCodes($calendarEvent, true)
992 ];
993 }
994
995 return $data;
996 }
997
998 public function saveEventEmailNotifications(Request $request, $calendarId, $eventId)
999 {
1000 $slot = CalendarSlot::where('calendar_id', $calendarId)->findOrFail($eventId);
1001
1002 $notifications = $request->get('notifications', []);
1003
1004 $formattedNotifications = [];
1005
1006 foreach ($notifications as $key => $value) {
1007 $formattedNotifications[$key] = [
1008 'title' => sanitize_text_field(Arr::get($value, 'title')),
1009 'enabled' => Arr::isTrue($value, 'enabled'),
1010 'email' => $this->sanitize_mapped_data(Arr::get($value, 'email')),
1011 'is_host' => Arr::isTrue($value, 'is_host')
1012 ];
1013 }
1014
1015 $slot->setNotifications($formattedNotifications);
1016
1017 return [
1018 'message' => __('Notifications has been saved', 'fluent-booking')
1019 ];
1020 }
1021
1022 public function getEventBookingFields(Request $request, $calendarId, $eventId)
1023 {
1024 $calendarEvent = CalendarSlot::where('calendar_id', $calendarId)->findOrFail($eventId);
1025
1026 $data = [
1027 'fields' => $calendarEvent->getBookingFields()
1028 ];
1029
1030 if (in_array('smart_codes', $request->get('with', []))) {
1031 $data['smart_codes'] = [
1032 'texts' => Helper::getEditorShortCodes($calendarEvent),
1033 'html' => Helper::getEditorShortCodes($calendarEvent, true)
1034 ];
1035 }
1036
1037 return $data;
1038 }
1039
1040 public function saveEventBookingFields(Request $request, $calendarId, $eventId)
1041 {
1042 $calendarEvent = CalendarSlot::where('calendar_id', $calendarId)->findOrFail($eventId);
1043
1044 $bookingFields = $request->get('booking_fields');
1045
1046 $formattedFields = BookingFieldService::sanitizeBookingFields($bookingFields, $calendarEvent);
1047
1048 $calendarEvent->setBookingFields($formattedFields);
1049
1050 return [
1051 'message' => __('Fields has been updated', 'fluent-booking')
1052 ];
1053 }
1054
1055 public function getEventPaymentSettings($calendarId, $eventId)
1056 {
1057 $calendarEvent = CalendarSlot::where('calendar_id', $calendarId)->findOrFail($eventId);
1058
1059 $config = [
1060 'native_enabled' => Helper::isPaymentEnabled(),
1061 'stripe_configured' => Helper::isPaymentConfigured('stripe'),
1062 'paypal_configured' => Helper::isPaymentConfigured('paypal'),
1063 'offline_configured' => Helper::isPaymentConfigured('offline'),
1064 'native_config_link' => Helper::getAppBaseUrl('settings/payment-methods/stripe'),
1065 'woo_config_link' => Helper::getAppBaseUrl('settings/configure-integrations/global-modules'),
1066 'has_cart' => defined('FLUENTCART_VERSION'),
1067 'has_woo' => defined('WC_PLUGIN_FILE'),
1068 'woo_enabled' => defined('WC_PLUGIN_FILE') && Helper::isModuleEnabled('woo')
1069 ];
1070
1071 $data = apply_filters('fluent_booking/payment/get_payment_settings', [
1072 'settings' => $calendarEvent->getPaymentSettings(),
1073 'config' => $config
1074 ], $calendarEvent);
1075
1076 return $data;
1077 }
1078
1079 public function deleteCalendarEvent(Request $request, $calendarId, $calendarEventId)
1080 {
1081 $calendar = Calendar::query()->findOrFail($calendarId);
1082
1083 $calendarEvent = CalendarSlot::query()->where('calendar_id', $calendar->id)->findOrFail($calendarEventId);
1084
1085 $calendar->updateEventOrder($calendarEvent->id);
1086
1087 do_action('fluent_booking/before_delete_calendar_event', $calendarEvent, $calendar);
1088
1089 $calendarEvent->delete();
1090
1091 do_action('fluent_booking/after_delete_calendar_event', $calendarEventId, $calendar);
1092
1093 return [
1094 'message' => __('Calendar Event has been deleted', 'fluent-booking')
1095 ];
1096 }
1097
1098 private function sanitize_mapped_data($settings)
1099 {
1100 $sanitizerMap = [
1101 'value' => 'intval',
1102 'unit' => 'sanitize_text_field',
1103 'subject' => 'sanitize_text_field',
1104 'body' => 'fcal_sanitize_html',
1105 'additional_recipients' => 'sanitize_text_field'
1106 ];
1107
1108 return Helper::fcal_backend_sanitizer($settings, $sanitizerMap);
1109 }
1110
1111 public function deleteCalendar(Request $request, $calendarId)
1112 {
1113 $calendar = Calendar::findOrFail($calendarId);
1114
1115 do_action('fluent_booking/before_delete_calendar', $calendar);
1116
1117 $calendar->delete();
1118
1119 do_action('fluent_booking/after_delete_calendar', $calendarId);
1120
1121 return [
1122 'message' => __('Calendar Deleted Successfully!', 'fluent-booking')
1123 ];
1124 }
1125 }
1126