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 / Services / Integrations / FluentForms / FluentFormInit.php

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

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