PluginProbe
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution / 2.5.0
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution v2.5.0
2.5.0 2.4.0 2.3.0 2.2.5 2.2.0 2.1.2 2.1.1 trunk 1.10.0 1.10.01 1.10.02 1.5.0 1.5.01 1.5.02 1.5.1 1.5.10 1.5.20 1.5.21 1.5.22 1.5.23 1.5.24 1.5.25 1.6.0 1.7.0 1.7.1 All 34 releases
← All changes | app/Http/Controllers/CalendarController.php +352 -127 1.5.10 → 2.5.0 View file →
@@ -1,19 +1,19 @@
1 1 <?php
2 2
3 3 namespace FluentBooking\App\Http\Controllers;
4 4
5 -use FluentBooking\App\Models\Booking;
5 +use FluentBooking\App\Models\Availability;
6 6 use FluentBooking\App\Models\Calendar;
7 7 use FluentBooking\App\Models\CalendarSlot;
8 -use FluentBooking\App\Models\Availability;
9 8 use FluentBooking\App\Services\Helper;
10 -use FluentBooking\App\Services\CurrenciesHelper;
11 9 use FluentBooking\App\Services\LandingPage\LandingPageHelper;
12 10 use FluentBooking\App\Services\PermissionManager;
13 11 use FluentBooking\App\Services\AvailabilityService;
14 12 use FluentBooking\App\Services\SanitizeService;
15 13 use FluentBooking\App\Services\CalendarService;
14 +use FluentBooking\App\Services\OnboardingService;
15 +use FluentBooking\App\Services\CalendarEventService;
16 16 use FluentBooking\App\Services\BookingFieldService;
17 17 use FluentBooking\App\Hooks\Handlers\AdminMenuHandler;
18 18 use FluentBooking\Framework\Http\Request\Request;
19 19 use FluentBooking\Framework\Support\Arr;
@@ -33,23 +33,28 @@
33 33 $query->where('title', 'LIKE', '%' . $search . '%');
34 34 }
35 35 };
36 36
37 - $calendarsQuery = Calendar::with(['slots' => function($query) use ($applySearchFilter) {
38 - $query->where('status', '!=', 'expired')
39 - ->where($applySearchFilter);
40 - }])
41 - ->where('status', '!=', 'expired')
42 - ->whereHas('slots', $applySearchFilter);
37 + $calendarsQuery = Calendar::with(['metas', 'slots' => function($query) use ($applySearchFilter) {
38 + $query->where($applySearchFilter);
39 + }])
40 + ->where('status', '!=', 'expired');
43 41
42 + if (!empty($search)) {
43 + $calendarsQuery->whereHas('slots', $applySearchFilter);
44 + }
45 +
44 46 if (!empty($calendarType) && $calendarType != 'all') {
45 - $calendarsQuery = $calendarsQuery->where('type', $calendarType);
47 + $calendarsQuery->where('type', $calendarType);
46 48 }
47 49
48 50 $calendarsQuery = $calendarsQuery->latest();
49 51
50 - if (!PermissionManager::hasAllCalendarAccess(true)) {
51 - $calendarsQuery->where('user_id', get_current_user_id());
52 + $hasPermission = PermissionManager::hasAllCalendarAccess(true);
53 +
54 + if (!$hasPermission) {
55 + $attachedCalendarIds = CalendarService::getAttachedCalendarIds($calendarsQuery);
56 + $calendarsQuery->whereIn('id', $attachedCalendarIds);
52 57 }
53 58
54 59 $calendars = $calendarsQuery->paginate();
55 60
@@ -55,20 +60,30 @@
55 60
56 61 foreach ($calendars as $calendar) {
57 62 $calendar->author_profile = $calendar->getAuthorProfile();
58 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 +
59 72 foreach ($calendar->slots as $slot) {
73 + $slot->setRelation('calendar', $calendar);
60 74 $slot->shortcode = '[fluent_booking id="' . $slot->id . '"]';
61 75 $slot->public_url = $slot->getPublicUrl();
62 76 $slot->duration = $slot->getDefaultDuration();
63 - $slot->price_total = $slot->getPricingTotal();
77 + $slot->price_total = $slot->getEventPrice();
64 78 $slot->location_fields = $slot->getLocationFields();
65 79 $slot->author_profiles = $slot->isMultiHostEvent() ? $slot->getAuthorProfiles() : [];
66 80 do_action_ref_array('fluent_booking/calendar_slot', [&$slot]);
81 + $slot->unsetRelation('calendar');
67 82 }
68 83
69 84 if(empty($calendar->author_profile['ID'])) {
70 - $calendar->generic_error = '<p style="color: red; margin:0;">Connected Host user is missing</p>';
85 + $calendar->generic_error = '<p style="color: var(--fcal-danger-fg); margin:0;">Connected Host user is missing</p>';
71 86 }
72 87
73 88 do_action_ref_array('fluent_booking/calendar', [&$calendar, 'lists']);
74 89 }
@@ -98,12 +113,59 @@
98 113 ], 422);
99 114 }
100 115
101 116 return [
102 - 'status' => true
117 + 'status' => true,
118 + 'message' => __('The provided slug is available', 'fluent-booking')
103 119 ];
104 120 }
105 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 +
106 168 public function createCalendar(Request $request)
107 169 {
108 170 $data = $request->get('calendar');
109 171
@@ -139,9 +201,9 @@
139 201 $this->validate($data, $validationConfig['rules'], $validationConfig['messages']);
140 202
141 203 do_action('fluent_booking/before_create_calendar', $data, $this);
142 204
143 - if (!empty($data['user_id']) && PermissionManager::userCan('invite_team_members')) {
205 + if (!empty($data['user_id']) && PermissionManager::canManageOtherHosts()) {
144 206 $user = get_user_by('ID', $data['user_id']);
145 207 } else {
146 208 $user = get_user_by('ID', get_current_user_id());
147 209 }
@@ -151,15 +213,25 @@
151 213 'message' => __('User not found', 'fluent-booking')
152 214 ], 422);
153 215 }
154 216
155 - $type = sanitize_text_field(Arr::get($data, 'type', 'simple'));
217 + $onboardinFeatures = $request->get('features');
218 + if (!empty($onboardinFeatures)) {
219 + $installableAddons = SanitizeService::sanitizeAddons($onboardinFeatures);
220 + OnboardingService::installAddons($installableAddons);
221 + }
156 222
223 + $type = SanitizeService::checkCollection(
224 + sanitize_text_field(Arr::get($data, 'type', 'simple')),
225 + ['simple', 'team', 'event'],
226 + 'simple'
227 + );
228 +
157 229 $isHostCalendar = $type == 'simple' ? true : false;
158 230
159 231 if ($isHostCalendar && Calendar::where('user_id', $user->ID)->where('type', 'simple')->first()) {
160 232 return $this->sendError([
161 - 'message' => __('The user already have a calendar. Please delete it first to create a new one', 'fluent-booking')
233 + 'message' => __('The user already has a calendar. Please delete it first to create a new one', 'fluent-booking')
162 234 ], 422);
163 235 }
164 236
165 237 if ($isHostCalendar) {
@@ -175,10 +247,30 @@
175 247
176 248 if (!$isHostCalendar) {
177 249 $title = sanitize_text_field(Arr::get($data, 'title', ''));
178 250 $data['slug'] = sanitize_title($title, '', 'display');
179 - $teamMembers = array_map('intval', Arr::get($slot, 'settings.team_members', []));
180 - if (!in_array($user->ID, $teamMembers)) {
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)) {
181 273 $user = get_user_by('ID', reset($teamMembers));
182 274 if (!$user) {
183 275 return $this->sendError([
184 276 'message' => __('Invalid Team Member', 'fluent-booking')
@@ -221,33 +313,25 @@
221 313 'message' => __('Calendar could not be found. Please try again', 'fluent-booking')
222 314 ], 422);
223 315 }
224 316
225 - $availability = AvailabilityService::getDefaultSchedule($calendar->user_id);
317 + $weeklySchedule = Arr::get($data, 'slot.weekly_schedules', []);
226 318
227 - if (!$availability) {
228 - $weeklySchedule = Arr::get($data, 'slot.weekly_schedules', []);
319 + $availability = AvailabilityService::maybeCreateAvailability($calendar, $weeklySchedule);
229 320
230 - $defaultSchedule = AvailabilityService::createScheduleSchema(
231 - $calendar->user_id, 'Weekly Hours', true, $calendar->author_timezone, 'UTC', $weeklySchedule
232 - );
233 -
234 - $availability = Availability::create($defaultSchedule);
235 - }
236 -
237 321 $title = (!empty($slot['title'])) ? sanitize_text_field($slot['title']) : $slot['duration'] . ' Minute Meeting';
238 322
239 323 $slotData = [
240 324 'title' => $title,
241 - 'slug' => Helper::generateSlotSlug($slot['duration'] . 'min', $calendar),
325 + 'slug' => Helper::generateSlotSlug((int)$slot['duration'] . 'min', $calendar),
242 326 'calendar_id' => $calendar->id,
243 327 'user_id' => $calendar->user_id,
244 328 'duration' => (int)$slot['duration'],
245 - 'description' => sanitize_textarea_field(Arr::get($slot, 'description')),
329 + 'description' => wp_kses_post(Arr::get($slot, 'description')),
246 330 'settings' => [
247 331 'team_members' => !$isHostCalendar ? $teamMembers : [],
248 332 'schedule_type' => sanitize_text_field($slot['schedule_type']),
249 - 'weekly_schedules' => SanitizeService::weeklySchedules($slot['weekly_schedules'], $calendar->author_timezone, 'UTC')
333 + 'weekly_schedules' => SanitizeService::weeklySchedules($slot['weekly_schedules'], $calendar->author_timezone, 'UTC', true)
250 334 ],
251 335 'status' => SanitizeService::checkCollection($slot['status'], ['active', 'draft']),
252 336 'color_schema' => sanitize_text_field(Arr::get($slot, 'color_schema', '#0099ff')),
253 337 'event_type' => sanitize_text_field(Arr::get($slot, 'event_type')),
@@ -281,9 +365,28 @@
281 365 $query->where('status', '!=', 'expired');
282 366 }])->findOrFail($calendarId);
283 367
284 368 $calendar->author_profile = $calendar->getAuthorProfile();
369 + $calendar->event_order = $calendar->getMeta('event_order');
285 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 +
286 389 $data = [
287 390 'calendar' => $calendar
288 391 ];
289 392
@@ -290,8 +393,12 @@
290 393 if (in_array('settings_menu', $request->get('with', []))) {
291 394 $data['settings_menu'] = AdminMenuHandler::getCalendarSettingsMenuItems($calendar);
292 395 }
293 396
397 + if (in_array('public_url', $request->get('with', []))) {
398 + $data['public_url'] = $calendar->getLandingPageUrl();
399 + }
400 +
294 401 return $data;
295 402 }
296 403
297 404 public function getSharingSettings(Request $request, $calendarId)
@@ -298,10 +405,11 @@
298 405 {
299 406 $calendar = Calendar::findOrFail($calendarId);
300 407
301 408 return [
302 - 'settings' => LandingPageHelper::getSettings($calendar),
303 - 'share_url' => $calendar->getLandingPageUrl(true)
409 + 'settings' => LandingPageHelper::getSettings($calendar),
410 + 'share_url' => $calendar->getLandingPageUrl(true),
411 + 'public_url' => $this->getSharePublicUrl($calendar, intval($request->get('event_id')))
304 412 ];
305 413 }
306 414
307 415 public function saveSharingSettings(Request $request, $calendarId)
@@ -312,9 +420,10 @@
312 420
313 421 if ($calendarDataItems) {
314 422 $this->validate($calendarDataItems, [
315 423 'title' => 'required',
316 - 'calendar_avatar' => 'url'
424 + 'calendar_avatar' => 'nullable|url',
425 + 'featured_image' => 'nullable|url'
317 426 ]);
318 427
319 428 $updatedTimezone = sanitize_text_field(Arr::get($calendarDataItems, 'timezone'));
320 429 if ($updatedTimezone && $updatedTimezone != $calendar->author_timezone) {
@@ -323,11 +432,11 @@
323 432 }
324 433
325 434 $calendar->title = sanitize_text_field(Arr::get($calendarDataItems, 'title'));
326 435 $calendar->description = wp_kses_post(Arr::get($calendarDataItems, 'description'));
327 - $calendar->save();
328 436 $calendar->updateMeta('profile_photo_url', sanitize_url(Arr::get($calendarDataItems, 'calendar_avatar')));
329 437 $calendar->updateMeta('featured_image_url', sanitize_url(Arr::get($calendarDataItems, 'featured_image')));
438 + $calendar->save();
330 439
331 440 if ($calendar->user) {
332 441 $calendar->user->updateMeta('host_phone', sanitize_text_field(Arr::get($calendarDataItems, 'phone')));
333 442 }
@@ -336,12 +445,27 @@
336 445 $sharingSettings = $request->get('landing_page_settings', []);
337 446 LandingPageHelper::updateSettings($calendar, $sharingSettings);
338 447
339 448 return [
340 - 'message' => __('Landing Page settings has been updated', 'fluent-booking')
449 + 'message' => __('Landing Page settings has been updated', 'fluent-booking'),
450 + 'public_url' => $this->getSharePublicUrl($calendar, intval($request->get('event_id')))
341 451 ];
342 452 }
343 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 +
344 468 public function updateCalendar(Request $request, $calendarId)
345 469 {
346 470 $data = $request->all();
347 471
@@ -362,11 +486,11 @@
362 486 'message' => __('Calendar has been updated successfully', 'fluent-booking')
363 487 ];
364 488 }
365 489
366 - public function getEvent(Request $request, $calendarId, $slotId)
490 + public function getEvent(Request $request, $calendarId, $eventId)
367 491 {
368 - $calendarEvent = CalendarSlot::where('calendar_id', $calendarId)->with(['calendar.user'])->findOrFail($slotId);
492 + $calendarEvent = CalendarSlot::where('calendar_id', $calendarId)->with(['calendar.user'])->findOrFail($eventId);
369 493
370 494 $calendarEvent->author_profile = $calendarEvent->getAuthorProfile();
371 495
372 496 $calendarEvent->calendar->author_profile = $calendarEvent->calendar->getAuthorProfile();
@@ -380,8 +504,10 @@
380 504 $eventSettings['date_overrides'] = (object)SanitizeService::slotDateOverrides(Arr::get($eventSettings, 'date_overrides', []), 'UTC', $calendarEvent->calendar->author_timezone, $calendarEvent);
381 505
382 506 $eventSettings['location_fields'] = $calendarEvent->getLocationFields();
383 507
508 + $eventSettings['hosts_schedules'] = $calendarEvent->getHostsSchedules();
509 +
384 510 $calendarEvent->settings = apply_filters('fluent_booking/get_calendar_event_settings', $eventSettings, $calendarEvent, $calendarEvent->calendar);
385 511
386 512 $data = [
387 513 'calendar_event' => $calendarEvent
@@ -414,37 +540,16 @@
414 540 public function getEventSchema(Request $request, $calendarId)
415 541 {
416 542 $calendar = Calendar::findOrFail($calendarId);
417 543
418 - $userCalendarId = $calendar->type == 'simple' ? $calendarId : null;
544 + $schema = (new CalendarSlot())->getEventSchema($calendar);
419 545
420 - $settingsSchema = (new CalendarSlot())->getSlotSettingsSchema($userCalendarId);
421 -
422 - $schema = [
423 - 'title' => '',
424 - 'status' => 'active',
425 - 'description' => '',
426 - 'duration' => '30',
427 - 'color_schema' => '#0099ff',
428 - 'calendar' => $calendar,
429 - 'settings' => $settingsSchema,
430 - 'max_book_per_slot' => 1,
431 - 'location_settings' => [
432 - [
433 - 'type' => '',
434 - 'title' => '',
435 - 'description' => '',
436 - 'host_phone_number' => ''
437 - ]
438 - ]
439 - ];
440 -
441 546 return [
442 547 'slot' => $schema
443 548 ];
444 549 }
445 550
446 - public function getAvailabilitySettings(Request $request, $calendarId, $slotId)
551 + public function getAvailabilitySettings(Request $request, $calendarId, $eventId)
447 552 {
448 553 $availableSchedules = AvailabilityService::availabilitySchedules();
449 554
450 555 $scheduleOptions = AvailabilityService::getScheduleOptions();
@@ -489,21 +594,41 @@
489 594 ], $slot);
490 595
491 596 $this->validate($slot, $validationConfig['rules'], $validationConfig['messages']);
492 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 +
493 618 $availability = AvailabilityService::getDefaultSchedule($calendar->user_id);
494 619
495 620 $slotData = [
496 - 'title' => $slot['title'],
621 + 'title' => sanitize_text_field($slot['title']),
497 622 'slug' => Helper::generateSlotSlug($slot['duration'] . 'min', $calendar),
498 623 'calendar_id' => $calendar->id,
499 624 'user_id' => $calendar->user_id,
500 625 'duration' => (int)$slot['duration'],
501 - 'description' => sanitize_textarea_field(Arr::get($slot, 'description')),
626 + 'description' => wp_kses_post(Arr::get($slot, 'description')),
502 627 'settings' => [
503 628 'schedule_type' => sanitize_text_field($slot['settings']['schedule_type']),
504 - 'weekly_schedules' => SanitizeService::weeklySchedules($slot['settings']['weekly_schedules'], $calendar->author_timezone, 'UTC'),
505 - 'date_overrides' => SanitizeService::slotDateOverrides(Arr::get($slot['settings'], 'date_overrides', []), $calendar->author_timezone, 'UTC'),
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),
506 631 'range_type' => sanitize_text_field(Arr::get($slot['settings'], 'range_type')),
507 632 'range_days' => (int)(Arr::get($slot['settings'], 'range_days', 60)) ?: 60,
508 633 'range_date_between' => SanitizeService::rangeDateBetween(Arr::get($slot['settings'], 'range_date_between', ['', ''])),
509 634 'schedule_conditions' => SanitizeService::scheduleConditions(Arr::get($slot['settings'], 'schedule_conditions', [])),
@@ -509,13 +634,13 @@
509 634 'schedule_conditions' => SanitizeService::scheduleConditions(Arr::get($slot['settings'], 'schedule_conditions', [])),
510 635 'buffer_time_before' => sanitize_text_field(Arr::get($slot['settings'], 'buffer_time_before', '0')),
511 636 'buffer_time_after' => sanitize_text_field(Arr::get($slot['settings'], 'buffer_time_after', '0')),
512 637 'slot_interval' => sanitize_text_field(Arr::get($slot['settings'], 'slot_interval', '')),
513 - 'team_members' => array_map('intval', Arr::get($slot['settings'], 'team_members', []))
638 + 'team_members' => $teamMembers
514 639 ],
515 640 'status' => SanitizeService::checkCollection($slot['status'], ['active', 'draft'], 'active'),
516 641 'color_schema' => sanitize_text_field(Arr::get($slot, 'color_schema', '#0099ff')),
517 - 'event_type' => sanitize_text_field(Arr::get($slot, 'event_type')),
642 + 'event_type' => SanitizeService::checkCollection(sanitize_text_field(Arr::get($slot, 'event_type')), CalendarSlot::getEventTypes(), 'single'),
518 643 'availability_type' => 'existing_schedule',
519 644 'availability_id' => $availability ? $availability->id : null,
520 645 'location_type' => sanitize_text_field(Arr::get($slot, 'location_type')),
521 646 'location_settings' => SanitizeService::locationSettings(Arr::get($slot, 'location_settings', [])),
@@ -530,8 +655,10 @@
530 655 $createdSlot = CalendarSlot::create($slotData);
531 656
532 657 do_action('fluent_booking/after_create_event', $calendar, $createdSlot);
533 658
659 + $calendar->updateEventOrder($createdSlot->id);
660 +
534 661 return [
535 662 'message' => __('New Event Type has been created successfully', 'fluent-booking'),
536 663 'slot' => $createdSlot
537 664 ];
@@ -589,9 +716,9 @@
589 716 $event->title = sanitize_text_field($data['title']);
590 717 $event->duration = (int)$data['duration'];
591 718 $event->status = SanitizeService::checkCollection($data['status'], ['active', 'draft']);
592 719 $event->color_schema = sanitize_text_field(Arr::get($data, 'color_schema', '#0099ff'));
593 - $event->description = sanitize_textarea_field(Arr::get($data, 'description'));
720 + $event->description = wp_kses_post(Arr::get($data, 'description'));
594 721 $event->max_book_per_slot = (int)Arr::get($data, 'max_book_per_slot');
595 722 $event->is_display_spots = (bool)Arr::get($data, 'is_display_spots');
596 723 $event->location_settings = SanitizeService::locationSettings(Arr::get($data, 'location_settings', []));
597 724
@@ -618,12 +745,12 @@
618 745 $data = $request->all();
619 746
620 747 $event = CalendarSlot::where('calendar_id', $calendarId)->findOrFail($eventId);
621 748
622 - $event->settings = [
749 + $eventSettings = [
623 750 'schedule_type' => sanitize_text_field(Arr::get($data, 'schedule_type')),
624 - 'weekly_schedules' => SanitizeService::weeklySchedules(Arr::get($data, 'weekly_schedules'), $event->calendar->author_timezone, 'UTC'),
625 - 'date_overrides' => SanitizeService::slotDateOverrides(Arr::get($data, 'date_overrides', []), $event->calendar->author_timezone, 'UTC'),
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),
626 753 'range_type' => sanitize_text_field(Arr::get($data, 'range_type')),
627 754 'range_days' => (int)(Arr::get($data, 'range_days', 60)) ?: 60,
628 755 'range_date_between' => SanitizeService::rangeDateBetween(Arr::get($data, 'range_date_between', ['', ''])),
629 756 'common_schedule' => Arr::isTrue($data, 'common_schedule', false)
@@ -628,11 +755,70 @@
628 755 'range_date_between' => SanitizeService::rangeDateBetween(Arr::get($data, 'range_date_between', ['', ''])),
629 756 'common_schedule' => Arr::isTrue($data, 'common_schedule', false)
630 757 ];
631 758
632 - $event->availability_id = (int)Arr::get($data, 'availability_id');
633 - $event->availability_type = SanitizeService::checkCollection(Arr::get($data, 'availability_type'), ['existing_schedule', 'custom']);
759 + $hostsSchedules = [];
634 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 +
635 821 $event->save();
636 822
637 823 return [
638 824 'message' => __('Data has been updated', 'fluent-booking'),
@@ -672,11 +858,11 @@
672 858 'event' => $event
673 859 ];
674 860 }
675 861
676 - public function patchCalendarEvent(Request $request, $calendarId, $slotId)
862 + public function patchCalendarEvent(Request $request, $calendarId, $eventId)
677 863 {
678 - $slot = CalendarSlot::where('calendar_id', $calendarId)->findOrFail($slotId);
864 + $slot = CalendarSlot::where('calendar_id', $calendarId)->findOrFail($eventId);
679 865
680 866 $status = $request->get('status');
681 867
682 868 if ($status) {
@@ -693,14 +879,32 @@
693 879 public function cloneCalendarEvent(Request $request, $calendarId, $eventId)
694 880 {
695 881 $newCalendarId = intval($request->get('new_calendar_id')) ?: $calendarId;
696 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 +
697 889 $calendar = Calendar::findOrFail($newCalendarId);
698 890
699 - $originalEvent = CalendarSlot::where('calendar_id', $calendarId)->findOrFail($eventId);
891 + $originalEvent = CalendarSlot::with('event_metas')->where('calendar_id', $calendarId)->findOrFail($eventId);
700 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 +
701 903 $clonedEvent = $originalEvent->replicate();
702 904
905 + $clonedEvent->hash = null;
906 +
703 907 $clonedEvent->calendar_id = $calendar->id;
704 908
705 909 $clonedEvent->user_id = $calendar->user_id;
706 910
@@ -709,11 +913,11 @@
709 913 $clonedEvent->slug = Helper::generateSlotSlug($clonedEvent->duration . 'min', $calendar);
710 914
711 915 $clonedEvent->save();
712 916
713 - $eventsMeta = $originalEvent->getCalendarEventsMeta();
917 + $calendar->updateEventOrder($clonedEvent->id);
714 918
715 - $integrationsMeta = $originalEvent->getIntegrationsMeta();
919 + $eventsMeta = $originalEvent->event_metas;
716 920
717 921 foreach ($eventsMeta as $meta) {
718 922 $clonedMeta = $meta->replicate();
719 923 $clonedMeta->object_id = $clonedEvent->id;
@@ -719,17 +923,25 @@
719 923 $clonedMeta->object_id = $clonedEvent->id;
720 924 $clonedMeta->save();
721 925 }
722 926
723 - foreach ($integrationsMeta as $meta) {
724 - $clonedMeta = $meta->replicate();
725 - $clonedMeta->object_id = $clonedEvent->id;
726 - $clonedMeta->save();
727 - }
927 + return [
928 + 'slot' => $clonedEvent,
929 + 'message' => __('The Event Type has been cloned successfully', 'fluent-booking')
930 + ];
931 + }
728 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 +
729 941 return [
730 - 'message' => __('The Event Type has been cloned successfully', 'fluent-booking'),
731 - 'slot' => $clonedEvent
942 + 'calendar' => $calendar,
943 + 'message' => __('Event order has been updated', 'fluent-booking')
732 944 ];
733 945 }
734 946
735 947 public function cloneEventEmailNotification(Request $request, $calendarId, $eventId)
@@ -737,8 +949,14 @@
737 949 $calendarEvent = CalendarSlot::where('calendar_id', $calendarId)->findOrFail($eventId);
738 950
739 951 $fromEventId = intval($request->get('from_event_id'));
740 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 +
741 959 $fromCalendarEvent = CalendarSlot::findOrFail($fromEventId);
742 960
743 961 $notification = $fromCalendarEvent->getNotifications(true);
744 962
@@ -751,11 +969,11 @@
751 969 'notifications' => $notification
752 970 ];
753 971 }
754 972
755 - public function getEventEmailNotifications(Request $request, $calendarId, $slotId)
973 + public function getEventEmailNotifications(Request $request, $calendarId, $eventId)
756 974 {
757 - $calendarEvent = CalendarSlot::where('calendar_id', $calendarId)->findOrFail($slotId);
975 + $calendarEvent = CalendarSlot::where('calendar_id', $calendarId)->findOrFail($eventId);
758 976
759 977 /*
760 978 * Confirmation Email to Attendee
761 979 * Confirmation Email to Organizer
@@ -776,11 +994,11 @@
776 994
777 995 return $data;
778 996 }
779 997
780 - public function saveEventEmailNotifications(Request $request, $calendarId, $slotId)
998 + public function saveEventEmailNotifications(Request $request, $calendarId, $eventId)
781 999 {
782 - $slot = CalendarSlot::where('calendar_id', $calendarId)->findOrFail($slotId);
1000 + $slot = CalendarSlot::where('calendar_id', $calendarId)->findOrFail($eventId);
783 1001
784 1002 $notifications = $request->get('notifications', []);
785 1003
786 1004 $formattedNotifications = [];
@@ -800,15 +1018,24 @@
800 1018 'message' => __('Notifications has been saved', 'fluent-booking')
801 1019 ];
802 1020 }
803 1021
804 - public function getEventBookingFields(Request $request, $calendarId, $slotId)
1022 + public function getEventBookingFields(Request $request, $calendarId, $eventId)
805 1023 {
806 - $slot = CalendarSlot::where('calendar_id', $calendarId)->findOrFail($slotId);
1024 + $calendarEvent = CalendarSlot::where('calendar_id', $calendarId)->findOrFail($eventId);
807 1025
808 - return [
809 - 'fields' => $slot->getBookingFields()
1026 + $data = [
1027 + 'fields' => $calendarEvent->getBookingFields()
810 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;
811 1038 }
812 1039
813 1040 public function saveEventBookingFields(Request $request, $calendarId, $eventId)
814 1041 {
@@ -815,55 +1042,53 @@
815 1042 $calendarEvent = CalendarSlot::where('calendar_id', $calendarId)->findOrFail($eventId);
816 1043
817 1044 $bookingFields = $request->get('booking_fields');
818 1045
819 - $optionRequiredFields = ['dropdown', 'radio', 'checkbox-group', 'multi-select'];
1046 + $formattedFields = BookingFieldService::sanitizeBookingFields($bookingFields, $calendarEvent);
820 1047
821 - $formattedFields = [];
1048 + $calendarEvent->setBookingFields($formattedFields);
822 1049
823 - $textFields = ['type', 'name', 'label', 'placeholder', 'limit', 'help_text', 'date_format'];
824 - $booleanFields = ['enabled', 'required', 'system_defined', 'disable_alter', 'is_sms_number'];
1050 + return [
1051 + 'message' => __('Fields has been updated', 'fluent-booking')
1052 + ];
1053 + }
825 1054
826 - foreach ($bookingFields as $value) {
827 - if (empty($value['name'])) {
828 - $value['name'] = BookingFieldService::generateFieldName($calendarEvent, $value['label']);
829 - }
1055 + public function getEventPaymentSettings($calendarId, $eventId)
1056 + {
1057 + $calendarEvent = CalendarSlot::where('calendar_id', $calendarId)->findOrFail($eventId);
830 1058
831 - $textValues = array_map('sanitize_text_field', Arr::only($value, $textFields));
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 + ];
832 1070
833 - $booleanValues = array_map(function ($valueItem) {
834 - return $valueItem === true || $valueItem === 'true' || $valueItem == 1;
835 - }, Arr::only($value, $booleanFields));
1071 + $data = apply_filters('fluent_booking/payment/get_payment_settings', [
1072 + 'settings' => $calendarEvent->getPaymentSettings(),
1073 + 'config' => $config
1074 + ], $calendarEvent);
836 1075
837 - $formattedField = array_merge($textValues, $booleanValues);
838 -
839 - $formattedField['index'] = (int)Arr::get($value, 'index');
840 - if ($value['type'] == 'payment' && $calendarEvent->type === 'paid') {
841 - $formattedField['payment_items'] = Arr::get($value, 'payment_items');
842 - $formattedField['currency_sign'] = CurrenciesHelper::getGlobalCurrencySign();
843 - }
844 - if (in_array(Arr::get($value, 'type'), $optionRequiredFields)) {
845 - $sanitizedOptions = array_map('sanitize_text_field', Arr::get($value, 'options'));
846 - $formattedField['options'] = $sanitizedOptions;
847 - }
848 -
849 - $formattedFields[] = $formattedField;
850 - }
851 -
852 - $calendarEvent->setBookingFields($formattedFields);
853 -
854 - return [
855 - 'message' => __('Fields has been updated', 'fluent-booking')
856 - ];
1076 + return $data;
857 1077 }
858 1078
859 1079 public function deleteCalendarEvent(Request $request, $calendarId, $calendarEventId)
860 1080 {
861 1081 $calendar = Calendar::query()->findOrFail($calendarId);
1082 +
862 1083 $calendarEvent = CalendarSlot::query()->where('calendar_id', $calendar->id)->findOrFail($calendarEventId);
863 1084
1085 + $calendar->updateEventOrder($calendarEvent->id);
1086 +
864 1087 do_action('fluent_booking/before_delete_calendar_event', $calendarEvent, $calendar);
1088 +
865 1089 $calendarEvent->delete();
1090 +
866 1091 do_action('fluent_booking/after_delete_calendar_event', $calendarEventId, $calendar);
867 1092
868 1093 return [
869 1094 'message' => __('Calendar Event has been deleted', 'fluent-booking')