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

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