PluginProbe
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution / 2.1.2
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution v2.1.2
2.4.0 2.3.0 2.2.5 2.2.0 2.1.2 2.1.1 trunk 1.10.0 1.10.01 1.10.02 1.5.0 1.5.01 1.5.02 1.5.1 1.5.10 1.5.20 1.5.21 1.5.22 1.5.23 1.5.24 1.5.25 1.6.0 1.7.0 1.7.1 1.7.2 All 33 releases
fluent-booking / app / Services / Integrations / FluentForms / FluentFormInit.php

FluentFormInit.php in Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution 2.1.2, at app/Services/Integrations/FluentForms/FluentFormInit.php

535 lines 20.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\Services\Integrations\FluentForms;
4
5 use FluentBooking\App\Services\BookingFieldService;
6 use FluentBooking\App\Services\LocationService;
7 use FluentBooking\Framework\Support\Arr;
8 use FluentBooking\App\Models\CalendarSlot;
9 use FluentBooking\App\Services\DateTimeHelper;
10 use FluentBooking\App\Services\BookingService;
11 use FluentBooking\App\Services\TimeSlotService;
12 use FluentBooking\App\Hooks\Handlers\TimeSlotServiceHandler;
13 use FluentBooking\App\Hooks\Handlers\FrontEndHandler;
14 use FluentForm\App\Models\Submission;
15 use FluentForm\App\Modules\Form\FormFieldsParser;
16 use FluentForm\App\Services\FormBuilder\ShortCodeParser;
17
18
19 class FluentFormInit
20 {
21 protected $hostId;
22
23 public function init()
24 {
25 $this->registerHooks();
26 $this->registerIntegrations();
27 }
28
29 public function registerHooks()
30 {
31 add_action('fluentform/validate_input_item_fcal_booking', [$this, 'handleValidations'], 10, 3);
32 add_action('fluentform/notify_on_form_submit', [$this, 'handleFormSubmitted'], 10, 3);
33 add_action('fluentform/conversational_question', [$this, 'loadConversationalAsset'], 10, 3);
34
35 add_filter('fluentform/conversational_field_types', function ($fieldTypes) {
36 $fieldTypes['fcal_booking'] = 'FlowFormCustomType';
37 return $fieldTypes;
38 });
39
40 add_filter('fluentform/conversational_accepted_field_elements', function ($elements) {
41 $elements[] = 'fcal_booking';
42 return $elements;
43 });
44
45 add_action('fluent_booking/booking_meta_info_main_meta_fluentform', [$this, 'pushFormDataToBooking'], 10, 2);
46 }
47
48 public function registerIntegrations()
49 {
50 add_action('init', function () {
51 new BookingElement();
52 });
53 }
54
55 public function handleValidations($error, $field, $formData)
56 {
57 if ($error) {
58 return $error;
59 }
60
61 $name = Arr::get($field, 'name');
62
63 if (!isset($formData[$name])) {
64 return $error;
65 }
66
67 $isRequired = Arr::get($field, 'rules.required.value');
68
69 $bookingData = Arr::get($formData, $name);
70
71 if ($bookingData) {
72 $bookingData = json_decode($bookingData, true);
73 } else {
74 $bookingData = [];
75 }
76
77 if ($isRequired) {
78 if (empty($bookingData['start_time']) || empty($bookingData['timezone'])) {
79 $error = Arr::get($field, 'rules.required.message');
80 if (!$error) {
81 // translators: %s is the label of the required field
82 $error = sprintf(__('%s field is required', 'fluent-booking'), Arr::get($field, 'raw.settings.label'));
83 }
84 return $error;
85 }
86 } else if (empty($bookingData['start_time'])) {
87 return $error;
88 }
89
90 $eventId = Arr::get($field, 'raw.settings.event_id');
91 $calendarEvent = CalendarSlot::find($eventId);
92
93 if (!$calendarEvent || $calendarEvent->status != 'active') {
94 return __('Sorry, the host is not accepting any new bookings at the moment.', 'fluent-booking');
95 }
96
97 $duration = $calendarEvent->getDuration(Arr::get($bookingData, 'duration', null));
98
99 $startTime = Arr::get($bookingData, 'start_time');
100 $timeZone = $bookingData['timezone'];
101
102 $startDateTime = DateTimeHelper::convertToUtc($startTime, $timeZone);
103 $endDateTime = gmdate('Y-m-d H:i:s', strtotime($startDateTime) + ($duration * 60));
104
105 $timeSlotService = TimeSlotServiceHandler::initService($calendarEvent->calendar, $calendarEvent);
106
107 if (is_wp_error($timeSlotService)) {
108 return TimeSlotServiceHandler::sendError($timeSlotService, $calendarEvent, $timeZone);
109 }
110
111 $availableSpot = $timeSlotService->isSpotAvailable($startDateTime, $endDateTime, $duration);
112
113 if (!$availableSpot) {
114 $message = __('This selected time slot is not available. Maybe someone booked the spot just a few seconds ago.', 'fluent-booking');
115 wp_send_json(['errors' => [$message]], 422);
116 }
117
118 if ($calendarEvent->isRoundRobin()) {
119 $this->hostId = $timeSlotService->hostUserId;
120 }
121
122 if (!is_user_logged_in()) {
123 $fieldError = '';
124 // Now check if the email field is given or not
125 $emailFieldKey = Arr::get($field, 'raw.settings.cal_guest_fields.email_field');
126 if (!$emailFieldKey) {
127 $fieldError = __('Email is required for this appointment. Looks like this field does not have email field selected.', 'fluent-booking');
128 } else {
129 $email = Arr::get($formData, $emailFieldKey);
130 if (!$email || !is_email($email)) {
131 $fieldError = __('Email is required for this appointment. Please provide a valid email', 'fluent-booking');
132 }
133 }
134
135 if ($fieldError) {
136 return $fieldError;
137 }
138 }
139
140 $locationFieldKey = $this->getLocationFieldKey($calendarEvent);
141
142 if ($locationFieldKey) {
143 $requiredKeys = [];
144 if ($locationFieldKey == 'location') {
145 $locationFieldKey = 'location_config';
146 }
147
148 $userInputData = Arr::get($bookingData, 'form.' . $locationFieldKey);
149
150 if (in_array($locationFieldKey, ['phone_number', 'address'])) {
151 $requiredKeys[] = $locationFieldKey;
152 } else if ($locationFieldKey == 'location_config') {
153 $requiredKeys[] = 'location_config.driver';
154 $selectedLocation = LocationService::getLocationDetails($calendarEvent, $userInputData, $bookingData['form']);
155 $selectedLocationDriver = Arr::get($selectedLocation, 'type');
156 if (in_array($selectedLocationDriver, ['in_person_guest', 'phone_guest'])) {
157 $requiredKeys[] = 'location_config.user_location_input';
158 }
159 }
160
161 foreach ($requiredKeys as $requiredKey) {
162 if (!Arr::get($bookingData['form'], $requiredKey)) {
163 return __('Please provide a valid location for this meeting', 'fluent-booking');
164 }
165 }
166 }
167
168 /*
169 * We are decoding the data with valid array
170 */
171 add_filter('fluentform/insert_response_data', function ($data) use ($name, $calendarEvent) {
172
173 if (isset($data[$name]) && is_string($data[$name])) {
174 $bookingArr = json_decode($data[$name], true);
175 $validData = array_filter(Arr::only($bookingArr, ['start_time', 'timezone', 'duration', 'form.location_config', 'form.phone_number', 'form.address']));
176 $extendedData = array_filter(Arr::only(Arr::get($bookingArr, 'form', []), ['location_config', 'phone_number', 'address']));
177
178 if ($extendedData) {
179 $validData = array_merge($validData, $extendedData);
180 }
181
182 if ($validData) {
183 $validData['duration'] = $calendarEvent->getDuration(Arr::get($validData, 'duration', null));
184 $validData['end_time'] = gmdate('Y-m-d H:i:s', strtotime($bookingArr['start_time']) + ($validData['duration'] * 60));
185 }
186
187 $data[$name] = (array)$validData;
188 }
189
190 return $data;
191 });
192
193
194 return '';
195 }
196
197 public function handleFormSubmitted($entryId, $formDataX, $form)
198 {
199 $fields = FormFieldsParser::getInputs($form, ['rules', 'raw', 'name']);
200
201 $bookingFields = array_filter($fields, function ($field) {
202 return $field['element'] == 'fcal_booking';
203 });
204
205 if (!$bookingFields) {
206 return;
207 }
208
209 if (\FluentForm\App\Helpers\Helper::getSubmissionMeta($entryId, 'fluent_booking_id')) {
210 return; // Already processed
211 }
212
213 $entry = wpFluent()->table('fluentform_submissions')
214 ->where('id', $entryId)
215 ->first();
216
217 if (!$entry) {
218 return;
219 }
220
221 $formData = json_decode($entry->response, true);
222
223 foreach ($bookingFields as $bookingField) {
224 $fieldName = Arr::get($bookingField, 'raw.attributes.name');
225 $ffFieldData = Arr::get($formData, $fieldName);
226
227 if (!$ffFieldData) {
228 continue;
229 }
230
231 if (is_string($ffFieldData)) {
232 $ffFieldData = json_decode($ffFieldData, true);
233 }
234
235 if (empty($ffFieldData['timezone']) || empty($ffFieldData['start_time']) || empty($ffFieldData['duration'])) {
236 continue;
237 }
238
239 $eventId = Arr::get($bookingField, 'raw.settings.event_id');
240 $event = CalendarSlot::find($eventId);
241
242 if (!$event || $event->status != 'active') {
243 continue;
244 }
245
246 $submittedData = json_decode($entry->response, true);
247
248 $emailFieldKey = Arr::get($bookingField, 'raw.settings.cal_guest_fields.email_field');
249 $guestEmail = '';
250 $guestName = '';
251 if ($emailFieldKey) {
252 $guestEmail = Arr::get($submittedData, $emailFieldKey);
253 $nameFieldKey = Arr::get($bookingField, 'raw.settings.cal_guest_fields.name_field');
254
255 $guestName = Arr::get($submittedData, $nameFieldKey);
256 if (is_array($guestName)) {
257 $guestName = implode(' ', $guestName);
258 }
259 }
260
261 if (!$guestEmail) {
262 $guestEmail = Arr::get($submittedData, 'email');
263 $guestName = Arr::get($submittedData, 'names');
264 if (is_array($guestName)) {
265 $guestName = implode(' ', $guestName);
266 }
267 }
268
269 if (!$guestEmail && $entry->user_id) {
270 $user = get_user_by('id', $entry->user_id);
271 if ($user) {
272 $guestEmail = $user->user_email;
273 $guestName = trim($user->first_name . ' ' . $user->last_name);
274 if (!$guestName) {
275 $guestName = $user->display_name;
276 }
277 }
278 }
279
280 if (!$guestEmail || !is_email($guestEmail)) {
281 do_action('fluentform/log_data', [
282 'parent_source_id' => $form->id,
283 'source_type' => 'submission_item',
284 'source_id' => $entry->id,
285 'component' => 'FluentBooking',
286 'status' => 'error',
287 'title' => __('Appointment could not be created', 'fluent-booking'),
288 'description' => __('Appointment could not be created because email is not given or invalid', 'fluent-booking'),
289 ]);
290 continue;
291 }
292
293 $startTime = $ffFieldData['start_time'];
294 $timeZone = $ffFieldData['timezone'];
295 $duration = $ffFieldData['duration'];
296
297 $startDateTime = DateTimeHelper::convertToUtc($startTime, $timeZone);
298
299 $bookingData = [
300 'start_time' => $startDateTime,
301 'name' => $guestName,
302 'email' => $guestEmail,
303 'person_time_zone' => sanitize_text_field($ffFieldData['timezone']),
304 'source' => 'fluentform',
305 'source_id' => $entry->id,
306 'status' => 'scheduled',
307 'source_url' => $entry->source_url,
308 'ip_address' => $entry->ip,
309 'event_type' => $event->event_type,
310 'slot_minutes' => $duration
311 ];
312
313 if ($event->isConfirmationRequired($startDateTime)) {
314 $bookingData['status'] = 'pending';
315 }
316
317 if ($entry->user_id) {
318 $bookingData['person_user_id'] = $entry->user_id;
319 }
320
321 if ($this->hostId) {
322 $bookingData['host_user_id'] = $this->hostId;
323 }
324
325 $selectedLocation = LocationService::getLocationDetails($event, Arr::get($ffFieldData, 'location_config', []), $ffFieldData);
326 if ($selectedLocation['type'] == 'phone_guest') {
327 $bookingData['phone'] = sanitize_textarea_field($selectedLocation['description']);
328 } else if (!empty($ffFieldData['address'])) {
329 $bookingData['address'] = sanitize_textarea_field($ffFieldData['address']);
330 }
331 $bookingData['location_details'] = $selectedLocation;
332
333 try {
334 $booking = BookingService::createBooking($bookingData, $event);
335
336 \FluentForm\App\Helpers\Helper::getSubmissionMeta($entry->id, 'fluent_booking_id', $booking->id);
337
338 $fieldData = $submittedData[$fieldName];
339 $fieldData['booking_id'] = $booking->id;
340
341 $submittedData[$fieldName] = (array)$fieldData;
342
343 wpFluent()->table('fluentform_submissions')
344 ->where('id', $entryId)
345 ->update([
346 'response' => wp_json_encode($submittedData, JSON_UNESCAPED_UNICODE)
347 ]);
348
349 do_action('fluentform/log_data', [
350 'parent_source_id' => $form->id,
351 'source_type' => 'submission_item',
352 'source_id' => $entry->id,
353 'component' => 'FluentBooking',
354 'status' => 'info',
355 'title' => __('Booking has been created on FluentBooking', 'fluent-booking'),
356 /* translators: %1$s is the opening anchor tag, %2$s is the closing anchor tag. */
357 'description' => sprintf(__('A new appointment has been created on FluentBooking. %1$sView Booking Details%2$s', 'fluent-booking'), '<a rel="noopener" href="' . $booking->getAdminViewUrl() . '" target="_blank">', '</a>'),
358 ]);
359
360 } catch (\Exception $exception) {
361 do_action('fluentform/log_data', [
362 'parent_source_id' => $form->id,
363 'source_type' => 'submission_item',
364 'source_id' => $entry->id,
365 'component' => 'FluentBooking',
366 'status' => 'error',
367 'title' => __('Failed to create booking', 'fluent-booking'),
368 'description' => $exception->getMessage(),
369 ]);
370 }
371 }
372 }
373
374 public function loadConversationalAsset($question, $field, $form)
375 {
376 if ('fcal_booking' === $field['element']) {
377
378 $calendarEventId = Arr::get($field, 'settings.event_id');
379 $calendarEvent = CalendarSlot::find($calendarEventId);
380
381 if (!$calendarEvent || !$calendarEvent->calendar) {
382 return;
383 }
384
385 [$localizeData, $elementId] = $this->getLocalizedData($calendarEvent, $field, $form);
386
387 wp_enqueue_script(
388 'fluent_booking',
389 FLUENT_BOOKING_URL . 'assets/public/js/fluentform-conversational.js',
390 [],
391 FLUENT_BOOKING_ASSETS_VERSION,
392 true
393 );
394
395 if (BookingFieldService::hasPhoneNumberField($localizeData['form_fields'])) {
396 wp_enqueue_script('fluent-booking-phone-field', FLUENT_BOOKING_URL . 'assets/public/js/phone-field.js', [], FLUENT_BOOKING_ASSETS_VERSION, true);
397 $inlineStyle = '.fcal_phone_wrapper .flag { background: url(' . esc_url(FLUENT_BOOKING_URL . 'assets/images/flags_responsive.png') . ') no-repeat;background-size: 100%;}';
398 wp_add_inline_style('fluent-booking-phone-field', $inlineStyle);
399 }
400
401 wp_localize_script('fluent_booking', 'fcal_public_vars_' . $question['id'], $localizeData);
402 wp_localize_script('fluent_booking', 'fluentCalendarPublicVars', (new FrontEndHandler())->getGlobalVars());
403 }
404 }
405
406 public function pushFormDataToBooking($meta, $booking)
407 {
408 if (!$booking->source_id) {
409 return $meta;
410 }
411
412 try {
413 $submission = Submission::find($booking->source_id);
414
415 if (!$submission) {
416 return $meta;
417 }
418
419 $response = json_decode($submission->response);
420
421 $smartCode = '{all_data}';
422
423 if ($submission->payment_total) {
424 $smartCode .= '<h3>' . __('Related Payments', 'fluent-booking') . '</h3>{payment.receipt}';
425 }
426
427 $entryHtmlData = ShortCodeParser::parse(
428 $smartCode,
429 $submission->id,
430 $response,
431 $submission->form,
432 false,
433 true
434 );
435
436 $entryHtmlData .= '<p><a target="_blank" rel="noopener" href="' . admin_url('admin.php?page=fluent_forms&route=entries&form_id=' . $submission->form_id . '#/entries/' . $submission->id) . '">' . __('View Form Submission', 'fluent-booking') . '</a></p>';
437
438 $meta[] = [
439 'id' => 'fluentform',
440 'title' => __('Related Form Data', 'fluent-booking'),
441 'content' => $entryHtmlData
442 ];
443 } catch (\Exception $e) {
444
445 }
446
447 return $meta;
448 }
449
450 public function getLocalizedData($calendarEvent, $data, $form)
451 {
452 $element_id = $this->makeElementId($data, $form);
453
454 $calendar = $calendarEvent->calendar;
455
456 $settings = Arr::get($data, 'settings');
457
458 $name = Arr::get($data, 'attributes.name');
459
460 $localizeData = (new FrontEndHandler())->getCalendarEventVars($calendar, $calendarEvent);
461
462 $localizeData['name'] = $name;
463 $localizeData['settings'] = $settings;
464
465 $isHostEnabled = Arr::get($localizeData['settings']['cal_guest_fields'], 'host_info', 'hide') == 'show';
466
467 $showHostInfo = $isHostEnabled || Arr::isTrue($calendarEvent->settings, 'multi_duration.enabled');
468
469 if ($showHostInfo) {
470 $localizeData['disable_author'] = false;
471 } else {
472 $localizeData['disable_author'] = true;
473 }
474
475 if(!empty($form->instance_css_class)) {
476 $localizeData['form_instance'] = $form->instance_css_class;
477 }
478
479 $locationFieldKey = $this->getLocationFieldKey($calendarEvent);
480 if ($locationFieldKey) {
481 $formFields = $localizeData['form_fields'];
482 $formFields = array_filter($formFields, function ($field) use ($locationFieldKey) {
483 return $field['name'] == $locationFieldKey;
484 });
485
486 $localizeData['form_fields'] = array_values($formFields);
487 } else {
488 $localizeData['form_fields'] = [];
489 }
490
491 return [$localizeData, $element_id];
492 }
493
494 private function getLocationFieldKey($calendarEvent)
495 {
496 $locationFieldKey = '';
497 if ($calendarEvent->isPhoneRequired()) {
498 $locationFieldKey = 'phone_number';
499 } else if ($calendarEvent->isAddressRequired()) {
500 $locationFieldKey = 'address';
501 } else if ($calendarEvent->isLocationFieldRequired()) {
502 $locationFieldKey = 'location';
503 }
504 return $locationFieldKey;
505 }
506
507 /**
508 * Build unique ID concatenating form id and name attribute
509 *
510 * @param array $data $form
511 *
512 * @return string for id value
513 */
514 protected function makeElementId($data, $form)
515 {
516 if (isset($data['attributes']['name'])) {
517 $formInstance = \FluentForm\App\Helpers\Helper::$formInstance;
518 if (!empty($data['attributes']['id'])) {
519 return $data['attributes']['id'];
520 }
521 $elementName = $data['attributes']['name'];
522 $elementName = str_replace(['[', ']', ' '], '_', $elementName);
523
524 $suffix = esc_attr($form->id);
525 if ($formInstance > 1) {
526 $suffix = $suffix . '_' . $formInstance;
527 }
528
529 $suffix .= '_' . $elementName;
530
531 return 'ff_' . esc_attr($suffix);
532 }
533 }
534 }
535