PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.7.0
VikBooking Hotel Booking Engine & PMS v1.7.0
1.8.16 1.8.15 1.8.14 1.8.13 1.8.12 1.8.11 1.8.10 1.8.9 1.8.6 1.8.7 1.8.8 trunk 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 All 37 releases
vikbooking / admin / helpers / widgets / guest_reviews.php

guest_reviews.php in VikBooking Hotel Booking Engine & PMS 1.7.0, at admin/helpers/widgets/guest_reviews.php

920 lines 30.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package VikBooking
4 * @subpackage com_vikbooking
5 * @author Alessio Gaggii - e4j - Extensionsforjoomla.com
6 * @copyright Copyright (C) 2018 e4j - Extensionsforjoomla.com. All rights reserved.
7 * @license GNU General Public License version 2 or later; see LICENSE
8 * @link https://vikwp.com
9 */
10
11 defined('ABSPATH') or die('No script kiddies please!');
12
13 /**
14 * Class handler for admin widget "guest reviews".
15 *
16 * @since 1.16.10 (J) - 1.6.10 (WP)
17 */
18 class VikBookingAdminWidgetGuestReviews extends VikBookingAdminWidget
19 {
20 /**
21 * The instance counter of this widget.
22 *
23 * @var int
24 */
25 protected static $instance_counter = -1;
26
27 /**
28 * Tells whether VCM is installed and updated.
29 *
30 * @var bool
31 */
32 protected $vcm_exists = true;
33
34 /**
35 * VCM language definitions map.
36 *
37 * @var bool
38 */
39 protected $vcm_lang_defs = [
40 'channel_logo' => 'VBOCHANNEL',
41 'created_timestamp' => 'VCMPVIEWORDERSVBONE',
42 'reply' => 'VCMREVREPLYREV',
43 'reviewee_response' => 'VCMREVREPLYREV',
44 'reviewer' => 'VCMPVIEWORDERSVBTWO',
45 'country_code' => 'VCMTACHOTELCOUNTRY',
46 'name' => 'VCMBCAHFIRSTNAME',
47 'reservation_id' => 'VCMSMARTBALBID',
48 'review_id' => 'VCMREVIEWID',
49 'scoring' => 'VCMREVIEWSCORE',
50 'value' => 'VCMGREVVALUE',
51 'value_for_money' => 'VCMGREVVALUE',
52 'clean' => 'VCMGREVCLEAN',
53 'cleanliness' => 'VCMGREVCLEAN',
54 'comfort' => 'VCMGREVCOMFORT',
55 'location' => 'VCMGREVLOCATION',
56 'facilities' => 'VCMGREVFACILITIES',
57 'staff' => 'VCMGREVSTAFF',
58 'review_score' => 'VCMTOTALSCORE',
59 'content' => 'VCMGREVCONTENT',
60 'message' => 'VCMGREVMESSAGE',
61 'text' => 'VCMGREVMESSAGE',
62 'headline' => 'VCMGREVMESSAGE',
63 'language_code' => 'VCMBCAHLANGUAGE',
64 'negative' => 'VCMGREVNEGATIVE',
65 'positive' => 'VCMGREVPOSITIVE',
66 'public_review' => 'VCMGREVPOSITIVE',
67 ];
68
69 /**
70 * Class constructor will define the widget name and identifier.
71 */
72 public function __construct()
73 {
74 // call parent constructor
75 parent::__construct();
76
77 $this->widgetName = JText::translate('VBOPANELREVIEWS');
78 $this->widgetDescr = JText::translate('VBOGUESTREVSVCMREQ');
79 $this->widgetId = basename(__FILE__, '.php');
80
81 // define widget and icon and style name
82 $this->widgetIcon = '<i class="' . VikBookingIcons::i('star') . '"></i>';
83 $this->widgetStyleName = 'yellow';
84
85 // whether VCM is available and up to date
86 $this->vcm_exists = class_exists('VCMReviewHelper');
87
88 // avoid queries on certain pages, as VCM may not have been activated yet
89 if (VBOPlatformDetection::isWordPress() && $this->vcm_exists) {
90 global $pagenow;
91 if (isset($pagenow) && in_array($pagenow, ['update.php', 'plugins.php', 'plugin-install.php'])) {
92 $this->vcm_exists = false;
93 }
94 }
95
96 if ($this->vcm_exists) {
97 // load the VCM admin language file
98 $vcm_admin_lang_path = '';
99 if (VBOPlatformDetection::isJoomla()) {
100 $vcm_admin_lang_path = JPATH_ADMINISTRATOR;
101 } elseif (defined('VIKCHANNELMANAGER_ADMIN_LANG')) {
102 $vcm_admin_lang_path = VIKCHANNELMANAGER_ADMIN_LANG;
103 } elseif (VBOPlatformDetection::isWordPress()) {
104 // if running within Vik Booking, the constant VIKCHANNELMANAGER_ADMIN_LANG may not be available
105 $vcm_admin_lang_path = str_replace('vikbooking', 'vikchannelmanager', VIKBOOKING_ADMIN_LANG);
106 }
107
108 if ($vcm_admin_lang_path) {
109 $lang = JFactory::getLanguage();
110 $lang->load('com_vikchannelmanager', $vcm_admin_lang_path);
111 if (VBOPlatformDetection::isWordPress() && defined('VIKCHANNELMANAGER_LIBRARIES')) {
112 /**
113 * @wponly load language admin handler as well for WP.
114 * We do this only because of WordPress, but in a way also compatible with Joomla as
115 * the constant VIKCHANNELMANAGER_LIBRARIES and method attachHandler are not in Joomla.
116 */
117 $lang->attachHandler(VIKCHANNELMANAGER_LIBRARIES . DIRECTORY_SEPARATOR . 'language' . DIRECTORY_SEPARATOR . 'admin.php', 'vikchannelmanager');
118 }
119 }
120 }
121 }
122
123 /**
124 * Preload the necessary CSS/JS assets from VCM.
125 *
126 * @return void
127 */
128 public function preload()
129 {
130 if ($this->vcm_exists) {
131 // VCM JS language defs
132 foreach ($this->vcm_lang_defs as $vcm_lang_def) {
133 JText::script($vcm_lang_def);
134 }
135
136 // JS language defs (VBO & VCM)
137 JText::script('VBO_REPLY');
138 JText::script('VBDASHBOOKINGID');
139 JText::script('VBO_PLEASE_FILL_FIELDS');
140 JText::script('VCM_AI_CHAT_TOOLTIP');
141 }
142 }
143
144 /**
145 * AJAX endpoint to load a guest review.
146 *
147 * @return void
148 */
149 public function loadGuestReview()
150 {
151 $dbo = JFactory::getDbo();
152 $input = JFactory::getApplication()->input;
153
154 $load_id = $input->getInt('review_id', 0);
155 $nav_type = $input->getAlnum('nav_type', '');
156 $wrapper = $input->getString('wrapper', '');
157
158 $has_next = true;
159 $has_prev = true;
160
161 $operator = '=';
162 $asc_desc = 'DESC';
163 if (!strcasecmp($nav_type, 'gt')) {
164 $operator = '>=';
165 $asc_desc = 'ASC';
166 } elseif (!strcasecmp($nav_type, 'lt')) {
167 $operator = '<=';
168 $asc_desc = 'DESC';
169 }
170
171 $q = $dbo->getQuery(true)
172 ->select('*')
173 ->from($dbo->qn('#__vikchannelmanager_otareviews'));
174
175 if ($load_id) {
176 $q->where($dbo->qn('id') . " {$operator} " . $load_id);
177 $q->order($dbo->qn('id') . ' ' . $asc_desc);
178 } else {
179 $q->order($dbo->qn('dt') . ' DESC');
180 // no next review
181 $has_next = false;
182 }
183
184 $dbo->setQuery($q, 0, 1);
185
186 try {
187 $review = $dbo->loadObject();
188
189 if (!$review && $load_id) {
190 // the query did not produce any result
191 $q->clear('order')->clear('where');
192 $q->order($dbo->qn('dt') . ' DESC');
193 // try to get the most recent review
194 $dbo->setQuery($q, 0, 1);
195 $review = $dbo->loadObject();
196 // no next review
197 $has_next = false;
198 }
199
200 if ($review && $review->id == 1) {
201 // no prev review
202 $has_prev = false;
203 }
204 } catch (Exception $e) {
205 $review = null;
206 }
207
208 // start output buffering
209 ob_start();
210
211 $current_review_id = ($review ?? (new stdClass))->id ?? 0;
212 $can_reply = 0;
213 $has_reply = 0;
214 $review_data = null;
215
216 if (!$review) {
217 ?>
218 <p class="info"><?php echo JText::translate('VBO_NO_RECORDS_FOUND'); ?></p>
219 <?php
220 } else {
221 // let VCM parse the review object
222 $review_helper = VCMReviewHelper::getInstance($review);
223 $review_data = $review_helper->parseObject();
224
225 // get the review channel details
226 $channel_details = $review_helper->getChannelDetails();
227 if (!empty($channel_details['logo'])) {
228 // prepend channel logo property
229 $review_data->{$review->id} = (object) array_merge(['channel_logo' => $channel_details['logo']], (array) $review_data->{$review->id});
230 }
231
232 // access additional details
233 $can_reply = (int) $review_helper->canReply();
234 $has_reply = (int) $review_helper->hasReply();
235
236 if ($can_reply) {
237 // print the HTML structure for the host reply to the guest review
238 ?>
239 <div class="vbo-widget-guest-reviews-reply-wrap" data-review-id="<?php echo $review->id; ?>" data-otareview-id="<?php echo $review->review_id ?? ''; ?>">
240 <div class="vbo-widget-guest-reviews-reply-inner">
241 <label for="<?php echo $wrapper; ?>-reply-text"><?php echo JText::translate('VCMREVREPLYREV'); ?></label>
242 <textarea id="<?php echo $wrapper; ?>-reply-text" class="vbo-widget-guest-reviews-reply-txt"></textarea>
243 <div class="vbo-widget-guest-reviews-reply-actions">
244 <button type="button" class="btn btn-primary vbo-widget-guest-reviews-reply-action-ai ai-write-review"><?php echo JText::translate('VCM_AI_CHAT_TOOLTIP'); ?></button>
245 <button type="button" class="btn btn-success vbo-widget-guest-reviews-reply-action-submit"><?php echo JText::translate('VCMBCAHSUBMIT'); ?></button>
246 </div>
247 </div>
248 </div>
249 <?php
250 }
251 }
252
253 // get the HTML buffer
254 $html_content = ob_get_contents();
255 ob_end_clean();
256
257 // return an associative array of values
258 return [
259 'html' => ($html_content ?: ''),
260 'review_id' => $current_review_id,
261 'review_data' => $review_data,
262 'review_record' => $review ?? (new stdClass),
263 'channel' => $channel_details,
264 'has_next' => $has_next,
265 'has_prev' => $has_prev,
266 'can_reply' => $can_reply,
267 'has_reply' => $has_reply,
268 ];
269 }
270
271 /**
272 * @inheritDoc
273 */
274 public function render(VBOMultitaskData $data = null)
275 {
276 // increase widget's instance counter
277 static::$instance_counter++;
278
279 // check whether the widget is being rendered via AJAX when adding it through the customizer
280 $is_ajax = $this->isAjaxRendering();
281
282 // generate a unique ID for the guest reviews wrapper instance
283 $wrapper_instance = !$is_ajax ? static::$instance_counter : rand();
284 $wrapper_id = 'vbo-widget-guest-reviews-' . $wrapper_instance;
285
286 // this widget will work only if VCM is available and updated, and if permissions are met
287 $vbo_auth_bookings = JFactory::getUser()->authorise('core.vbo.bookings', 'com_vikbooking');
288 if (!$this->vcm_exists || !$vbo_auth_bookings) {
289 return;
290 }
291
292 // multitask data and options
293 $review_id = 0;
294 $booking_id = 0;
295 if ($data && $data->isModalRendering()) {
296 // check if a specific review ID was set
297 $review_id = (int) $this->options()->get('review_id', 0);
298 $ota_review_id = (string) $this->options()->get('ota_review_id', '');
299
300 // check if a booking ID was set
301 $booking_id = (int) $this->options()->fetchBookingId();
302
303 if ($booking_id && !$review_id) {
304 $review_id = $this->getReviewIDFromBooking($booking_id);
305 }
306
307 if (!$review_id && $ota_review_id) {
308 $review_id = $this->getReviewIDFromOtaId($ota_review_id);
309 }
310 }
311
312 ?>
313 <div class="vbo-admin-widget-wrapper">
314 <div class="vbo-admin-widget-head">
315 <div class="vbo-admin-widget-head-inline">
316 <h4><?php echo $this->widgetIcon; ?> <span><?php echo $this->widgetName; ?></span></h4>
317 <div class="vbo-admin-widget-head-commands">
318 <?php
319 if ($this->vcm_exists) {
320 ?>
321 <div class="vbo-reportwidget-commands">
322 <div class="vbo-reportwidget-commands-main">
323 <div class="vbo-reportwidget-command-dates vbo-widget-guestreviews-topdets vbo-widget-guestreviews-topdets-reply" style="display: none;">
324 <button type="button" class="btn btn-rounded btn-light-green vbo-btn-review-reply"><?php echo JText::translate('VBO_REPLY'); ?></button>
325 </div>
326 <div class="vbo-reportwidget-command-dates vbo-widget-guestreviews-topdets vbo-widget-guestreviews-topdets-info" style="display: none;">
327 <div class="vbo-reportwidget-period-name vbo-widget-guestreviews-topdets-cname"></div>
328 <div class="vbo-reportwidget-period-date">
329 <span class="label label-info vbo-widget-guestreviews-topdets-bid" data-bid="0"></span>
330 </div>
331 </div>
332 <div class="vbo-reportwidget-command-chevron vbo-reportwidget-command-prev">
333 <span class="vbo-widget-guestreviews-go-prev" onclick="vboWidgetGuestReviewsNavigate('<?php echo $wrapper_id; ?>', -1);"><?php VikBookingIcons::e('chevron-left'); ?></span>
334 </div>
335 <div class="vbo-reportwidget-command-chevron vbo-reportwidget-command-next">
336 <span class="vbo-widget-guestreviews-go-next" onclick="vboWidgetGuestReviewsNavigate('<?php echo $wrapper_id; ?>', 1);"><?php VikBookingIcons::e('chevron-right'); ?></span>
337 </div>
338 </div>
339 </div>
340 <?php
341 }
342 ?>
343 </div>
344 </div>
345 </div>
346 <div id="<?php echo $wrapper_id; ?>" class="vbo-widget-guestreviews-wrap" data-review-id="<?php echo $review_id; ?>">
347 <div class="vbo-widget-guestreviews-content">
348 <?php
349 if (!$this->vcm_exists) {
350 ?>
351 <p class="info"><?php echo JText::translate('VBOGUESTREVSVCMREQ'); ?></p>
352 <?php
353 }
354 ?>
355 </div>
356 </div>
357 </div>
358
359 <?php
360 if (static::$instance_counter === 0 || $is_ajax) {
361 // some JS functions should be loaded once per widget instance
362
363 // attempt to load the OTA review category tags
364 $ota_category_tags = [];
365 if (class_exists('VCMAirbnbReview')) {
366 // load the Airbnb review category tags that could have been
367 // selected by the guest during the review for the host
368 $ota_category_tags = VCMAirbnbReview::getCategoryTags('guest_review_host');
369 }
370 ?>
371
372 <script type="text/javascript">
373
374 /**
375 * The review objects container.
376 */
377 var vbo_wgr_review_objects = {};
378
379 /**
380 * language definitions map.
381 */
382 var vbo_wgr_langdefs_map = <?php echo json_encode($this->vcm_lang_defs); ?>;
383
384 /**
385 * Review macro-group string.
386 */
387 var vbo_wgr_json_macrogr = '';
388
389 /**
390 * Review category tags.
391 */
392 var vbo_wgr_category_tags = <?php echo json_encode((object) $ota_category_tags) ?>;
393
394 /**
395 * Icons and values to render the review scoring.
396 */
397 var vbo_wgr_full_staricn = '<?php VikBookingIcons::e('star', 'vbo-review-star vbo-review-star-full') ?>';
398 var vbo_wgr_void_staricn = '<?php VikBookingIcons::e('star', 'vbo-review-star') ?>';
399 var vbo_wgr_servicescore = false;
400
401 /**
402 * Display the loading skeletons.
403 */
404 function vboWidgetGuestReviewsSkeletons(wrapper) {
405 let widget_instance = jQuery('#' + wrapper);
406 if (!widget_instance.length) {
407 return false;
408 }
409
410 let skeleton_html = '<div class="vbo-widget-spinnner-loading">';
411 skeleton_html += '<div class="vbo-widget-spinnner-loading-inner">';
412 skeleton_html += '<?php VikBookingIcons::e('circle-notch', 'fa-spin fa-fw'); ?>';
413 skeleton_html += '</div>';
414 skeleton_html += '</div>';
415 widget_instance.find('.vbo-widget-guestreviews-content').html(skeleton_html);
416 }
417
418 /**
419 * Starts a randomly timed answer typing.
420 */
421 function vboWidgetGuestReviewsTypeAnswer(textarea, words, min, max) {
422 if (isNaN(min) || min < 0) {
423 min = 0;
424 }
425
426 if (isNaN(max) || max < 0) {
427 max = 512;
428 }
429
430 return new Promise((resolve) => {
431 vboWidgetGuestReviewsTypeAnswerRecursive(resolve, textarea, words, min, max);
432 });
433 }
434
435 /**
436 * Recursively registers the next words to type with a random timer.
437 */
438 function vboWidgetGuestReviewsTypeAnswerRecursive(resolve, textarea, words, min, max) {
439 if (words.length == 0) {
440 // typed all the provided words
441 resolve();
442 } else {
443 // register timeout to append the next word
444 setTimeout(() => {
445 let val = textarea.val();
446 // extract word and append it within the textarea value
447 textarea.val((val.length ? val + ' ' : '') + words.shift());
448 // keep going until we reach the end of the queue
449 vboWidgetGuestReviewsTypeAnswerRecursive(resolve, textarea, words, min, max);
450 }, Math.floor(Math.random() * (max - min + 1) + min));
451 }
452 }
453
454 /**
455 * Navigate between the various reviews.
456 */
457 function vboWidgetGuestReviewsNavigate(wrapper, direction) {
458 let widget_instance = jQuery('#' + wrapper);
459 if (!widget_instance.length) {
460 return false;
461 }
462
463 // current ID
464 let current_id = parseInt(widget_instance.attr('data-review-id'));
465
466 // ID to load and navigation type
467 let load_id = current_id;
468 let nav_type = '';
469
470 if (direction && direction > 0) {
471 load_id += 1;
472 nav_type = 'gt';
473 } else if (direction && direction < 0 && load_id > 1) {
474 load_id -= 1;
475 nav_type = 'lt';
476 }
477
478 // set new ID
479 widget_instance.attr('data-review-id', load_id);
480
481 // show loading skeletons
482 vboWidgetGuestReviewsSkeletons(wrapper);
483
484 // launch navigation
485 vboWidgetGuestReviewsLoad(wrapper, nav_type);
486 }
487
488 /**
489 * Perform the request to load the guest review.
490 */
491 function vboWidgetGuestReviewsLoad(wrapper, nav_type) {
492 let widget_instance = jQuery('#' + wrapper);
493 if (!widget_instance.length) {
494 return false;
495 }
496
497 // hide top details before making the request
498 widget_instance.parent().find('.vbo-widget-guestreviews-topdets-info, .vbo-widget-guestreviews-topdets-reply').hide();
499
500 // reset values for parsing the review object
501 vbo_wgr_json_macrogr = '';
502 vbo_wgr_servicescore = false;
503
504 let load_id = widget_instance.attr('data-review-id');
505
506 // the widget method to call
507 let call_method = 'loadGuestReview';
508
509 // make a request to load the bookings calendar
510 VBOCore.doAjax(
511 "<?php echo $this->getExecWidgetAjaxUri(); ?>",
512 {
513 widget_id: "<?php echo $this->getIdentifier(); ?>",
514 call: call_method,
515 return: 1,
516 review_id: load_id,
517 nav_type: nav_type,
518 wrapper: wrapper,
519 tmpl: "component"
520 },
521 (response) => {
522 try {
523 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
524 if (!obj_res.hasOwnProperty(call_method)) {
525 console.error('Unexpected JSON response', obj_res);
526 return false;
527 }
528
529 let review_id = obj_res[call_method]['review_id'];
530
531 // set current review ID
532 widget_instance.attr('data-review-id', review_id);
533
534 // merge review data objects (caching is not really needed as we always perform an AJAX request)
535 vbo_wgr_review_objects = Object.assign(vbo_wgr_review_objects, obj_res[call_method]['review_data']);
536
537 // set and show top details
538 let top_details = widget_instance.parent();
539 let review_bid = (obj_res[call_method]['review_record']['idorder'] || '0');
540 top_details.find('.vbo-widget-guestreviews-topdets-cname').text((obj_res[call_method]['review_record']['customer_name'] || ' '));
541 top_details.find('.vbo-widget-guestreviews-topdets-bid')
542 .attr('data-bid', review_bid)
543 .html('<?php VikBookingIcons::e('external-link'); ?> ' + Joomla.JText._('VBDASHBOOKINGID') + ' ' + review_bid);
544 top_details.find('.vbo-widget-guestreviews-topdets-info').show();
545
546 // check if a reply is allowed
547 if (obj_res[call_method]['can_reply']) {
548 top_details.find('.vbo-widget-guestreviews-topdets-reply').show();
549 }
550
551 // check for paging limits
552 if (obj_res[call_method]['has_prev']) {
553 top_details.find('.vbo-widget-guestreviews-go-prev').show();
554 } else {
555 top_details.find('.vbo-widget-guestreviews-go-prev').hide();
556 }
557
558 if (obj_res[call_method]['has_next']) {
559 top_details.find('.vbo-widget-guestreviews-go-next').show();
560 } else {
561 top_details.find('.vbo-widget-guestreviews-go-next').hide();
562 }
563
564 // replace HTML with new data
565 widget_instance.find('.vbo-widget-guestreviews-content').html(
566 vboWidgetGuestReviewsStringifyObject(vbo_wgr_review_objects[review_id], review_id) + obj_res[call_method]['html']
567 );
568
569 if (obj_res[call_method]['can_reply']) {
570 // register event to handle the reply functions, manual or through AI
571 setTimeout(() => {
572 // AI write review
573 widget_instance.find('.vbo-widget-guest-reviews-reply-action-ai.ai-write-review').on('click', function() {
574 // disable button and start animation
575 jQuery(this).prop('disabled', true).html('<?php VikBookingIcons::e('spinner', 'fa-spin'); ?>');
576
577 // build review object
578 const review = vbo_wgr_review_objects[review_id];
579 const channel = obj_res[call_method]['channel'];
580
581 let customer = review?.reviewer?.name;
582 let score = review?.scoring?.review_score;
583 let content = [];
584
585 if (channel.uniquekey == '<?php echo VikChannelManagerConfig::BOOKING; ?>') {
586 // Booking.com
587 content.push(review?.content?.headline);
588 content.push(review?.content?.negative);
589 content.push(review?.content?.positive);
590 } else if (channel.uniquekey == '<?php echo VikChannelManagerConfig::AIRBNBAPI; ?>') {
591 // Airbnb API
592 content.push(review?.content?.public_review);
593 content = content.concat(Object.values(review?.comments || {}));
594 } else if (channel.uniquekey == 0) {
595 // Website
596 content.push(review?.content?.message);
597 }
598
599 if (typeof score !== 'undefined') {
600 content.push('Overall score: ' + score + '/10');
601 }
602
603 // get rid of the empty comments
604 content = content.filter(s => s);
605
606 new Promise((resolve, reject) => {
607 if (!content.length) {
608 reject(null);
609 return false;
610 }
611
612 VBOCore.doAjax(
613 '<?php echo VikBooking::ajaxUrl('index.php?option=com_vikchannelmanager&task=ai.review'); ?>',
614 {
615 customer: customer,
616 review: content.join("\n"),
617 },
618 (response) => {
619 resolve(response);
620 },
621 (error) => {
622 reject(error.responseText || error.statusText || 'An error has occurred');
623 }
624 );
625 }).then(async (response) => {
626 const replyTextarea = widget_instance.find('textarea.vbo-widget-guest-reviews-reply-txt');
627 replyTextarea.val('');
628 await vboWidgetGuestReviewsTypeAnswer(replyTextarea, (response.review || '').split(/ +/), 32, 128);
629 }).catch((error) => {
630 if (error) {
631 alert(error);
632 }
633 }).finally(() => {
634 // enable button again
635 jQuery(this).prop('disabled', false).text(Joomla.JText._('VCM_AI_CHAT_TOOLTIP'));
636 });
637 });
638
639 // reply submit button
640 widget_instance.find('.vbo-widget-guest-reviews-reply-action-submit').on('click', function() {
641 // disable button
642 jQuery(this).prop('disabled', true);
643
644 // the text for the reply
645 let replyText = widget_instance.find('textarea.vbo-widget-guest-reviews-reply-txt').val();
646
647 if (!replyText) {
648 alert(Joomla.JText._('VBO_PLEASE_FILL_FIELDS'));
649 return false;
650 }
651
652 // perform the request
653 VBOCore.doAjax(
654 '<?php echo VikBooking::ajaxUrl('index.php?option=com_vikchannelmanager&task=review.reply'); ?>',
655 {
656 review_id: obj_res[call_method].review_record?.id,
657 ota_review_id: obj_res[call_method].review_record?.review_id,
658 uniquekey: obj_res[call_method].channel?.uniquekey,
659 reply_text: replyText,
660 },
661 (response) => {
662 // remove buttons
663 widget_instance.find('.vbo-widget-guest-reviews-reply-actions').remove();
664
665 // remove textarea
666 widget_instance.find('textarea.vbo-widget-guest-reviews-reply-txt').remove();
667
668 // append reply review text
669 widget_instance.find('.vbo-widget-guest-reviews-reply-inner').append('<div class="vbo-widget-guest-reviews-replied-text">' + replyText + '</div>');
670
671 // prepend success icon to label
672 widget_instance.find('.vbo-widget-guest-reviews-reply-inner').find('label').prepend('<span class="badge badge-success"><?php VikBookingIcons::e('check-circle'); ?></span> ');
673
674 // hide the top-details "reply" button
675 widget_instance.parent().find('.vbo-widget-guestreviews-topdets-reply').hide();
676 },
677 (error) => {
678 // enable button
679 jQuery(this).prop('disabled', false);
680
681 // display alert
682 alert(error.responseText || error.statusText || 'An error has occurred');
683 }
684 );
685 });
686 }, 200);
687 }
688 } catch(err) {
689 console.error('could not parse JSON response', err, response);
690 }
691 },
692 (error) => {
693 console.error(error);
694 alert(error.responseText);
695 // remove the skeleton loading
696 widget_instance.find('.vbo-widget-guestreviews-content').find('.vbo-skeleton-loading').remove();
697 }
698 );
699 }
700
701 /**
702 * Ensures the argument is a non-empty object.
703 */
704 function vboWidgetGuestReviewsIsObject(obj) {
705 if (typeof obj != 'object') {
706 return false;
707 }
708
709 for (var jk in obj) {
710 return obj.hasOwnProperty(jk);
711 }
712 }
713
714 /**
715 * Turns strings into words with the first letter upper case.
716 */
717 function vboWidgetGuestReviewsUcWords(str) {
718 return (str + '').replace(/^(.)|\s+(.)/g, function ($1) {
719 return $1.toUpperCase();
720 });
721 }
722
723 /**
724 * Recursively parse a review object to build the HTML structure for representation.
725 */
726 function vboWidgetGuestReviewsStringifyObject(obj, idrev) {
727 var stringified = '';
728 var otareview = (vbo_wgr_review_objects.hasOwnProperty(idrev) && vbo_wgr_review_objects[idrev].hasOwnProperty('channel') && vbo_wgr_review_objects[idrev]['channel'] === null ? false : true);
729
730 for (var jk in obj) {
731 let objValue = obj[jk];
732
733 if (!obj.hasOwnProperty(jk) || objValue === null || jk == 'can_reply') {
734 continue;
735 }
736
737 var usekey = jk.replace(' ', '_').toLowerCase();
738 var prop_name = vbo_wgr_langdefs_map.hasOwnProperty(usekey) ? Joomla.JText._(vbo_wgr_langdefs_map[usekey]) : vboWidgetGuestReviewsUcWords(jk.replace(/_/g, ' '));
739
740 if (vboWidgetGuestReviewsIsObject(objValue)) {
741 // update macrogroup
742 vbo_wgr_json_macrogr = jk.replace(' ', '-').replace('_', '-').toLowerCase();
743 // use this property container as a group-label
744 stringified += '<div class="vbo-review-json-entry vbo-review-json-entry-group vbo-review-json-' + vbo_wgr_json_macrogr + '"><span class="vbo-review-json-key">' + prop_name + '</span></div>';
745 // recursive call for inner object
746 stringified += vboWidgetGuestReviewsStringifyObject(objValue, idrev);
747 } else {
748 if (jk === 'reviewee_response' && typeof objValue === 'string' && objValue) {
749 // overwrite special property with just a string as a value, to be treated as an object
750 stringified += '<div class="vbo-review-json-entry vbo-review-json-entry-group vbo-review-json-revieweeresponse">';
751 } else {
752 // regular property
753 stringified += '<div class="vbo-review-json-entry vbo-review-json-' + vbo_wgr_json_macrogr + '">';
754 }
755 stringified += '<span class="vbo-review-json-key">' + prop_name + '</span>';
756 if (!otareview && vbo_wgr_json_macrogr == 'scoring' && (jk != 'review_score' || (jk == 'review_score' && !vbo_wgr_servicescore))) {
757 // star rating for website review to a service or global review
758 vbo_wgr_servicescore = true;
759 stringified += '<span class="vbo-review-json-value">';
760 var starscount = Math.floor((parseInt(objValue) / 2));
761 for (var s = 1; s <= starscount; s++) {
762 stringified += vbo_wgr_full_staricn;
763 }
764 for (var s = (starscount + 1); s <= 5; s++) {
765 stringified += vbo_wgr_void_staricn;
766 }
767 stringified += '</span>';
768 } else {
769 vbo_wgr_servicescore = false;
770 // check if the value is an URL of the photo/logo
771 if (jk == 'photo' && objValue.indexOf('http') >= 0) {
772 objValue = '<span class="vbo-review-guest-avatar-wrap"><img class="vbo-review-guest-avatar" src="' + objValue + '" /></span>';
773 } else if (jk == 'channel_logo' && objValue.indexOf('http') >= 0) {
774 objValue = '<span class="vbo-review-channel-logo-wrap"><img class="vbo-review-channel-logo" src="' + objValue + '" /></span>';
775 }
776
777 // build additional element classes
778 var value_classes = [];
779 if (jk == 'review_score') {
780 value_classes.push('vbo-review-json-totscore');
781 }
782 if (vbo_wgr_json_macrogr == 'scoring' && !isNaN(objValue)) {
783 value_classes.push('vbo-review-json-servscore');
784 }
785 if (jk == 'reviewee_response') {
786 value_classes.push('vbo-review-json-revieweeresponse');
787 }
788
789 // check if the value should be normalized
790 if (vbo_wgr_json_macrogr == 'category-tags' && typeof objValue === 'string' && objValue) {
791 // review category tags are expected to be comma separated
792 let category_tags = [];
793 objValue.split(',').forEach((tag) => {
794 tag = tag.trim().toLowerCase();
795 if (vbo_wgr_category_tags.hasOwnProperty(tag)) {
796 // push normalized tag
797 category_tags.push(vbo_wgr_category_tags[tag]['descr']);
798 } else {
799 // push raw (unknown) tag
800 category_tags.push(tag);
801 }
802 });
803
804 // replace object value
805 objValue = category_tags.join(', ').toLowerCase() + '.';
806 objValue = objValue.substr(0, 1).toUpperCase() + objValue.substr(1);
807 }
808
809 stringified += '<span class="vbo-review-json-value' + (value_classes.length ? ' ' + value_classes.join(' ') : '') + '">' + objValue + '</span>';
810 }
811 stringified += '</div>';
812 }
813 }
814
815 return stringified;
816 }
817
818 </script>
819 <?php
820 }
821 ?>
822
823 <script type="text/javascript">
824
825 jQuery(function() {
826
827 // when document is ready, load the latest guest review
828 vboWidgetGuestReviewsLoad('<?php echo $wrapper_id; ?>', '');
829
830 // register click event for the review ID
831 jQuery('#<?php echo $wrapper_id; ?>').parent().find('.vbo-widget-guestreviews-topdets-bid').on('click', function() {
832 let bid = jQuery(this).attr('data-bid');
833 VBOCore.handleDisplayWidgetNotification({widget_id: 'booking_details'}, {
834 booking_id: bid,
835 modal_options: {
836 /**
837 * Overwrite modal options for rendering the admin widget.
838 * We need to use a different suffix in case this current widget was
839 * also rendered within a modal, or it would get dismissed in favour
840 * of the newly opened admin widget.
841 */
842 suffix: 'widget_modal_inner_booking_details',
843 },
844 });
845 });
846
847 // register click event for the reply button
848 jQuery('#<?php echo $wrapper_id; ?>').parent().find('.vbo-btn-review-reply').on('click', function() {
849 let reply_wrap = jQuery('#<?php echo $wrapper_id; ?>').find('.vbo-widget-guest-reviews-reply-wrap');
850 let modal_body = jQuery('#<?php echo $wrapper_id; ?>').closest('.vbo-modal-overlay-content-body-scroll');
851 if (modal_body.length) {
852 modal_body.animate({scrollTop: reply_wrap.offset().top - 20}, {duration: 400});
853 } else {
854 jQuery('html,body').animate({scrollTop: reply_wrap.offset().top - 20}, {duration: 400});
855 }
856 reply_wrap.find('textarea').focus();
857 });
858
859 });
860
861 </script>
862
863 <?php
864 }
865
866 /**
867 * Attempts to find the review ID from the given booking ID.
868 *
869 * @param int $booking_id The reservation ID.
870 *
871 * @return int
872 */
873 protected function getReviewIDFromBooking($booking_id)
874 {
875 $dbo = JFactory::getDbo();
876
877 $dbo->setQuery(
878 $dbo->getQuery(true)
879 ->select($dbo->qn('id'))
880 ->from($dbo->qn('#__vikchannelmanager_otareviews'))
881 ->where($dbo->qn('idorder') . ' = ' . (int) $booking_id)
882 );
883
884 try {
885 return (int) $dbo->loadResult();
886 } catch(Exception $e) {
887 // do nothing
888 }
889
890 return 0;
891 }
892
893 /**
894 * Attempts to find the review ID from the given OTA review ID.
895 *
896 * @param string $ota_id The OTA review ID.
897 *
898 * @return int
899 */
900 protected function getReviewIDFromOtaId($ota_id)
901 {
902 $dbo = JFactory::getDbo();
903
904 $dbo->setQuery(
905 $dbo->getQuery(true)
906 ->select($dbo->qn('id'))
907 ->from($dbo->qn('#__vikchannelmanager_otareviews'))
908 ->where($dbo->qn('review_id') . ' = ' . $dbo->q($ota_id))
909 );
910
911 try {
912 return (int) $dbo->loadResult();
913 } catch(Exception $e) {
914 // do nothing
915 }
916
917 return 0;
918 }
919 }
920