| 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 |
|