PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.9
VikBooking Hotel Booking Engine & PMS v1.8.9
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.8.9, at admin/helpers/widgets/guest_reviews.php

925 lines 30.8 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 // the review language
538 let review_lang = obj_res[call_method]['review_record']['lang'];
539
540 // set and show top details
541 let top_details = widget_instance.parent();
542 let review_bid = (obj_res[call_method]['review_record']['idorder'] || '0');
543 top_details.find('.vbo-widget-guestreviews-topdets-cname').text((obj_res[call_method]['review_record']['customer_name'] || ' '));
544 top_details.find('.vbo-widget-guestreviews-topdets-bid')
545 .attr('data-bid', review_bid)
546 .html('<?php VikBookingIcons::e('external-link'); ?> ' + Joomla.JText._('VBDASHBOOKINGID') + ' ' + review_bid);
547 top_details.find('.vbo-widget-guestreviews-topdets-info').show();
548
549 // check if a reply is allowed
550 if (obj_res[call_method]['can_reply']) {
551 top_details.find('.vbo-widget-guestreviews-topdets-reply').show();
552 }
553
554 // check for paging limits
555 if (obj_res[call_method]['has_prev']) {
556 top_details.find('.vbo-widget-guestreviews-go-prev').show();
557 } else {
558 top_details.find('.vbo-widget-guestreviews-go-prev').hide();
559 }
560
561 if (obj_res[call_method]['has_next']) {
562 top_details.find('.vbo-widget-guestreviews-go-next').show();
563 } else {
564 top_details.find('.vbo-widget-guestreviews-go-next').hide();
565 }
566
567 // replace HTML with new data
568 widget_instance.find('.vbo-widget-guestreviews-content').html(
569 vboWidgetGuestReviewsStringifyObject(vbo_wgr_review_objects[review_id], review_id) + obj_res[call_method]['html']
570 );
571
572 if (obj_res[call_method]['can_reply']) {
573 // register event to handle the reply functions, manual or through AI
574 setTimeout(() => {
575 // AI write review
576 widget_instance.find('.vbo-widget-guest-reviews-reply-action-ai.ai-write-review').on('click', function() {
577 // disable button and start animation
578 jQuery(this).prop('disabled', true).html('<?php VikBookingIcons::e('spinner', 'fa-spin'); ?>');
579
580 // build review object
581 const review = vbo_wgr_review_objects[review_id];
582 const channel = obj_res[call_method]['channel'];
583
584 let customer = review?.reviewer?.name;
585 let score = review?.scoring?.review_score;
586 let content = [];
587
588 if (channel.uniquekey == '<?php echo VikChannelManagerConfig::BOOKING; ?>') {
589 // Booking.com
590 content.push(review?.content?.headline);
591 content.push(review?.content?.negative);
592 content.push(review?.content?.positive);
593 } else if (channel.uniquekey == '<?php echo VikChannelManagerConfig::AIRBNBAPI; ?>') {
594 // Airbnb API
595 content.push(review?.content?.public_review);
596 content = content.concat(Object.values(review?.comments || {}));
597 } else if (channel.uniquekey == 0) {
598 // Website
599 content.push(review?.content?.message);
600 }
601
602 if (typeof score !== 'undefined') {
603 content.push('Overall score: ' + score + '/10');
604 }
605
606 // get rid of the empty comments
607 content = content.filter(s => s);
608
609 new Promise((resolve, reject) => {
610 if (!content.length) {
611 reject(null);
612 return false;
613 }
614
615 VBOCore.doAjax(
616 '<?php echo VikBooking::ajaxUrl('index.php?option=com_vikchannelmanager&task=ai.review'); ?>',
617 {
618 customer: customer,
619 review: content.join("\n"),
620 language: review_lang,
621 id_order: review_bid,
622 },
623 (response) => {
624 resolve(response);
625 },
626 (error) => {
627 reject(error.responseText || error.statusText || 'An error has occurred');
628 }
629 );
630 }).then(async (response) => {
631 const replyTextarea = widget_instance.find('textarea.vbo-widget-guest-reviews-reply-txt');
632 replyTextarea.val('');
633 await vboWidgetGuestReviewsTypeAnswer(replyTextarea, (response.review || '').split(/ +/), 32, 128);
634 }).catch((error) => {
635 if (error) {
636 alert(error);
637 }
638 }).finally(() => {
639 // enable button again
640 jQuery(this).prop('disabled', false).text(Joomla.JText._('VCM_AI_CHAT_TOOLTIP'));
641 });
642 });
643
644 // reply submit button
645 widget_instance.find('.vbo-widget-guest-reviews-reply-action-submit').on('click', function() {
646 // disable button
647 jQuery(this).prop('disabled', true);
648
649 // the text for the reply
650 let replyText = widget_instance.find('textarea.vbo-widget-guest-reviews-reply-txt').val();
651
652 if (!replyText) {
653 alert(Joomla.JText._('VBO_PLEASE_FILL_FIELDS'));
654 return false;
655 }
656
657 // perform the request
658 VBOCore.doAjax(
659 '<?php echo VikBooking::ajaxUrl('index.php?option=com_vikchannelmanager&task=review.reply'); ?>',
660 {
661 review_id: obj_res[call_method].review_record?.id,
662 ota_review_id: obj_res[call_method].review_record?.review_id,
663 uniquekey: obj_res[call_method].channel?.uniquekey,
664 reply_text: replyText,
665 },
666 (response) => {
667 // remove buttons
668 widget_instance.find('.vbo-widget-guest-reviews-reply-actions').remove();
669
670 // remove textarea
671 widget_instance.find('textarea.vbo-widget-guest-reviews-reply-txt').remove();
672
673 // append reply review text
674 widget_instance.find('.vbo-widget-guest-reviews-reply-inner').append('<div class="vbo-widget-guest-reviews-replied-text">' + replyText + '</div>');
675
676 // prepend success icon to label
677 widget_instance.find('.vbo-widget-guest-reviews-reply-inner').find('label').prepend('<span class="badge badge-success"><?php VikBookingIcons::e('check-circle'); ?></span> ');
678
679 // hide the top-details "reply" button
680 widget_instance.parent().find('.vbo-widget-guestreviews-topdets-reply').hide();
681 },
682 (error) => {
683 // enable button
684 jQuery(this).prop('disabled', false);
685
686 // display alert
687 alert(error.responseText || error.statusText || 'An error has occurred');
688 }
689 );
690 });
691 }, 200);
692 }
693 } catch(err) {
694 console.error('could not parse JSON response', err, response);
695 }
696 },
697 (error) => {
698 console.error(error);
699 alert(error.responseText);
700 // remove the skeleton loading
701 widget_instance.find('.vbo-widget-guestreviews-content').find('.vbo-skeleton-loading').remove();
702 }
703 );
704 }
705
706 /**
707 * Ensures the argument is a non-empty object.
708 */
709 function vboWidgetGuestReviewsIsObject(obj) {
710 if (typeof obj != 'object') {
711 return false;
712 }
713
714 for (var jk in obj) {
715 return obj.hasOwnProperty(jk);
716 }
717 }
718
719 /**
720 * Turns strings into words with the first letter upper case.
721 */
722 function vboWidgetGuestReviewsUcWords(str) {
723 return (str + '').replace(/^(.)|\s+(.)/g, function ($1) {
724 return $1.toUpperCase();
725 });
726 }
727
728 /**
729 * Recursively parse a review object to build the HTML structure for representation.
730 */
731 function vboWidgetGuestReviewsStringifyObject(obj, idrev) {
732 var stringified = '';
733 var otareview = (vbo_wgr_review_objects.hasOwnProperty(idrev) && vbo_wgr_review_objects[idrev].hasOwnProperty('channel') && vbo_wgr_review_objects[idrev]['channel'] === null ? false : true);
734
735 for (var jk in obj) {
736 let objValue = obj[jk];
737
738 if (!obj.hasOwnProperty(jk) || objValue === null || jk == 'can_reply') {
739 continue;
740 }
741
742 var usekey = jk.replace(' ', '_').toLowerCase();
743 var prop_name = vbo_wgr_langdefs_map.hasOwnProperty(usekey) ? Joomla.JText._(vbo_wgr_langdefs_map[usekey]) : vboWidgetGuestReviewsUcWords(jk.replace(/_/g, ' '));
744
745 if (vboWidgetGuestReviewsIsObject(objValue)) {
746 // update macrogroup
747 vbo_wgr_json_macrogr = jk.replace(' ', '-').replace('_', '-').toLowerCase();
748 // use this property container as a group-label
749 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>';
750 // recursive call for inner object
751 stringified += vboWidgetGuestReviewsStringifyObject(objValue, idrev);
752 } else {
753 if (jk === 'reviewee_response' && typeof objValue === 'string' && objValue) {
754 // overwrite special property with just a string as a value, to be treated as an object
755 stringified += '<div class="vbo-review-json-entry vbo-review-json-entry-group vbo-review-json-revieweeresponse">';
756 } else {
757 // regular property
758 stringified += '<div class="vbo-review-json-entry vbo-review-json-' + vbo_wgr_json_macrogr + '">';
759 }
760 stringified += '<span class="vbo-review-json-key">' + prop_name + '</span>';
761 if (!otareview && vbo_wgr_json_macrogr == 'scoring' && (jk != 'review_score' || (jk == 'review_score' && !vbo_wgr_servicescore))) {
762 // star rating for website review to a service or global review
763 vbo_wgr_servicescore = true;
764 stringified += '<span class="vbo-review-json-value">';
765 var starscount = Math.floor((parseInt(objValue) / 2));
766 for (var s = 1; s <= starscount; s++) {
767 stringified += vbo_wgr_full_staricn;
768 }
769 for (var s = (starscount + 1); s <= 5; s++) {
770 stringified += vbo_wgr_void_staricn;
771 }
772 stringified += '</span>';
773 } else {
774 vbo_wgr_servicescore = false;
775 // check if the value is an URL of the photo/logo
776 if (jk == 'photo' && objValue.indexOf('http') >= 0) {
777 objValue = '<span class="vbo-review-guest-avatar-wrap"><img class="vbo-review-guest-avatar" src="' + objValue + '" /></span>';
778 } else if (jk == 'channel_logo' && objValue.indexOf('http') >= 0) {
779 objValue = '<span class="vbo-review-channel-logo-wrap"><img class="vbo-review-channel-logo" src="' + objValue + '" /></span>';
780 }
781
782 // build additional element classes
783 var value_classes = [];
784 if (jk == 'review_score') {
785 value_classes.push('vbo-review-json-totscore');
786 }
787 if (vbo_wgr_json_macrogr == 'scoring' && !isNaN(objValue)) {
788 value_classes.push('vbo-review-json-servscore');
789 }
790 if (jk == 'reviewee_response') {
791 value_classes.push('vbo-review-json-revieweeresponse');
792 }
793
794 // check if the value should be normalized
795 if (vbo_wgr_json_macrogr == 'category-tags' && typeof objValue === 'string' && objValue) {
796 // review category tags are expected to be comma separated
797 let category_tags = [];
798 objValue.split(',').forEach((tag) => {
799 tag = tag.trim().toLowerCase();
800 if (vbo_wgr_category_tags.hasOwnProperty(tag)) {
801 // push normalized tag
802 category_tags.push(vbo_wgr_category_tags[tag]['descr']);
803 } else {
804 // push raw (unknown) tag
805 category_tags.push(tag);
806 }
807 });
808
809 // replace object value
810 objValue = category_tags.join(', ').toLowerCase() + '.';
811 objValue = objValue.substr(0, 1).toUpperCase() + objValue.substr(1);
812 }
813
814 stringified += '<span class="vbo-review-json-value' + (value_classes.length ? ' ' + value_classes.join(' ') : '') + '">' + objValue + '</span>';
815 }
816 stringified += '</div>';
817 }
818 }
819
820 return stringified;
821 }
822
823 </script>
824 <?php
825 }
826 ?>
827
828 <script type="text/javascript">
829
830 jQuery(function() {
831
832 // when document is ready, load the latest guest review
833 vboWidgetGuestReviewsLoad('<?php echo $wrapper_id; ?>', '');
834
835 // register click event for the review ID
836 jQuery('#<?php echo $wrapper_id; ?>').parent().find('.vbo-widget-guestreviews-topdets-bid').on('click', function() {
837 let bid = jQuery(this).attr('data-bid');
838 VBOCore.handleDisplayWidgetNotification({widget_id: 'booking_details'}, {
839 booking_id: bid,
840 modal_options: {
841 /**
842 * Overwrite modal options for rendering the admin widget.
843 * We need to use a different suffix in case this current widget was
844 * also rendered within a modal, or it would get dismissed in favour
845 * of the newly opened admin widget.
846 */
847 suffix: 'widget_modal_inner_booking_details',
848 },
849 });
850 });
851
852 // register click event for the reply button
853 jQuery('#<?php echo $wrapper_id; ?>').parent().find('.vbo-btn-review-reply').on('click', function() {
854 let reply_wrap = jQuery('#<?php echo $wrapper_id; ?>').find('.vbo-widget-guest-reviews-reply-wrap');
855 let modal_body = jQuery('#<?php echo $wrapper_id; ?>').closest('.vbo-modal-overlay-content-body-scroll');
856 if (modal_body.length) {
857 modal_body.animate({scrollTop: reply_wrap.offset().top - 20}, {duration: 400});
858 } else {
859 jQuery('html,body').animate({scrollTop: reply_wrap.offset().top - 20}, {duration: 400});
860 }
861 reply_wrap.find('textarea').focus();
862 });
863
864 });
865
866 </script>
867
868 <?php
869 }
870
871 /**
872 * Attempts to find the review ID from the given booking ID.
873 *
874 * @param int $booking_id The reservation ID.
875 *
876 * @return int
877 */
878 protected function getReviewIDFromBooking($booking_id)
879 {
880 $dbo = JFactory::getDbo();
881
882 $dbo->setQuery(
883 $dbo->getQuery(true)
884 ->select($dbo->qn('id'))
885 ->from($dbo->qn('#__vikchannelmanager_otareviews'))
886 ->where($dbo->qn('idorder') . ' = ' . (int) $booking_id)
887 );
888
889 try {
890 return (int) $dbo->loadResult();
891 } catch(Exception $e) {
892 // do nothing
893 }
894
895 return 0;
896 }
897
898 /**
899 * Attempts to find the review ID from the given OTA review ID.
900 *
901 * @param string $ota_id The OTA review ID.
902 *
903 * @return int
904 */
905 protected function getReviewIDFromOtaId($ota_id)
906 {
907 $dbo = JFactory::getDbo();
908
909 $dbo->setQuery(
910 $dbo->getQuery(true)
911 ->select($dbo->qn('id'))
912 ->from($dbo->qn('#__vikchannelmanager_otareviews'))
913 ->where($dbo->qn('review_id') . ' = ' . $dbo->q($ota_id))
914 );
915
916 try {
917 return (int) $dbo->loadResult();
918 } catch(Exception $e) {
919 // do nothing
920 }
921
922 return 0;
923 }
924 }
925