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

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

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