File "guest_reviews.php"

Full Path: /home/romayxjt/public_html/wp-content/plugins/vikbooking/admin/helpers/widgets/guest_reviews.php
File size: 30.78 KB
MIME-type: text/x-php
Charset: utf-8

<?php
/**
 * @package     VikBooking
 * @subpackage  com_vikbooking
 * @author      Alessio Gaggii - e4j - Extensionsforjoomla.com
 * @copyright   Copyright (C) 2018 e4j - Extensionsforjoomla.com. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE
 * @link        https://vikwp.com
 */

defined('ABSPATH') or die('No script kiddies please!');

/**
 * Class handler for admin widget "guest reviews".
 * 
 * @since 	1.16.10 (J) - 1.6.10 (WP)
 */
class VikBookingAdminWidgetGuestReviews extends VikBookingAdminWidget
{
	/**
	 * The instance counter of this widget.
	 *
	 * @var 	int
	 */
	protected static $instance_counter = -1;

	/**
	 * Tells whether VCM is installed and updated.
	 * 
	 * @var 	bool
	 */
	protected $vcm_exists = true;

	/**
	 * VCM language definitions map.
	 * 
	 * @var 	bool
	 */
	protected $vcm_lang_defs = [
		'channel_logo' 	    => 'VBOCHANNEL',
		'created_timestamp' => 'VCMPVIEWORDERSVBONE',
		'reply' 			=> 'VCMREVREPLYREV',
		'reviewee_response' => 'VCMREVREPLYREV',
		'reviewer' 			=> 'VCMPVIEWORDERSVBTWO',
		'country_code' 		=> 'VCMTACHOTELCOUNTRY',
		'name' 				=> 'VCMBCAHFIRSTNAME',
		'reservation_id' 	=> 'VCMSMARTBALBID',
		'review_id' 		=> 'VCMREVIEWID',
		'scoring' 			=> 'VCMREVIEWSCORE',
		'value' 			=> 'VCMGREVVALUE',
		'value_for_money' 	=> 'VCMGREVVALUE',
		'clean' 			=> 'VCMGREVCLEAN',
		'cleanliness' 		=> 'VCMGREVCLEAN',
		'comfort' 			=> 'VCMGREVCOMFORT',
		'location' 			=> 'VCMGREVLOCATION',
		'facilities' 		=> 'VCMGREVFACILITIES',
		'staff' 			=> 'VCMGREVSTAFF',
		'review_score' 		=> 'VCMTOTALSCORE',
		'content' 			=> 'VCMGREVCONTENT',
		'message' 			=> 'VCMGREVMESSAGE',
		'text' 				=> 'VCMGREVMESSAGE',
		'headline' 			=> 'VCMGREVMESSAGE',
		'language_code' 	=> 'VCMBCAHLANGUAGE',
		'negative' 			=> 'VCMGREVNEGATIVE',
		'positive' 			=> 'VCMGREVPOSITIVE',
		'public_review' 	=> 'VCMGREVPOSITIVE',
	];

	/**
	 * Class constructor will define the widget name and identifier.
	 */
	public function __construct()
	{
		// call parent constructor
		parent::__construct();

		$this->widgetName = JText::translate('VBOPANELREVIEWS');
		$this->widgetDescr = JText::translate('VBOGUESTREVSVCMREQ');
		$this->widgetId = basename(__FILE__, '.php');

		// define widget and icon and style name
		$this->widgetIcon = '<i class="' . VikBookingIcons::i('star') . '"></i>';
		$this->widgetStyleName = 'yellow';

		// whether VCM is available and up to date
		$this->vcm_exists = class_exists('VCMReviewHelper');

		// avoid queries on certain pages, as VCM may not have been activated yet
		if (VBOPlatformDetection::isWordPress() && $this->vcm_exists) {
			global $pagenow;
			if (isset($pagenow) && in_array($pagenow, ['update.php', 'plugins.php', 'plugin-install.php'])) {
				$this->vcm_exists = false;
			}
		}

		if ($this->vcm_exists) {
			// load the VCM admin language file
			$vcm_admin_lang_path = '';
			if (VBOPlatformDetection::isJoomla()) {
				$vcm_admin_lang_path = JPATH_ADMINISTRATOR;
			} elseif (defined('VIKCHANNELMANAGER_ADMIN_LANG')) {
				$vcm_admin_lang_path = VIKCHANNELMANAGER_ADMIN_LANG;
			} elseif (VBOPlatformDetection::isWordPress()) {
				// if running within Vik Booking, the constant VIKCHANNELMANAGER_ADMIN_LANG may not be available
				$vcm_admin_lang_path = str_replace('vikbooking', 'vikchannelmanager', VIKBOOKING_ADMIN_LANG);
			}

			if ($vcm_admin_lang_path) {
				$lang = JFactory::getLanguage();
				$lang->load('com_vikchannelmanager', $vcm_admin_lang_path);
				if (VBOPlatformDetection::isWordPress() && defined('VIKCHANNELMANAGER_LIBRARIES')) {
					/**
					 * @wponly  load language admin handler as well for WP.
					 * 			We do this only because of WordPress, but in a way also compatible with Joomla as
					 * 			the constant VIKCHANNELMANAGER_LIBRARIES and method attachHandler are not in Joomla.
					 */
					$lang->attachHandler(VIKCHANNELMANAGER_LIBRARIES . DIRECTORY_SEPARATOR . 'language' . DIRECTORY_SEPARATOR . 'admin.php', 'vikchannelmanager');
				}
			}
		}
	}

	/**
	 * Preload the necessary CSS/JS assets from VCM.
	 * 
	 * @return 	void
	 */
	public function preload()
	{
		if ($this->vcm_exists) {
			// VCM JS language defs
			foreach ($this->vcm_lang_defs as $vcm_lang_def) {
				JText::script($vcm_lang_def);
			}

			// JS language defs (VBO & VCM)
			JText::script('VBO_REPLY');
			JText::script('VBDASHBOOKINGID');
			JText::script('VBO_PLEASE_FILL_FIELDS');
			JText::script('VCM_AI_CHAT_TOOLTIP');
		}
	}

	/**
	 * AJAX endpoint to load a guest review.
	 * 
	 * @return 	void
	 */
	public function loadGuestReview()
	{
		$dbo   = JFactory::getDbo();
		$input = JFactory::getApplication()->input;

		$load_id  = $input->getInt('review_id', 0);
		$nav_type = $input->getAlnum('nav_type', '');
		$wrapper  = $input->getString('wrapper', '');

		$has_next = true;
		$has_prev = true;

		$operator = '=';
		$asc_desc = 'DESC';
		if (!strcasecmp($nav_type, 'gt')) {
			$operator = '>=';
			$asc_desc = 'ASC';
		} elseif (!strcasecmp($nav_type, 'lt')) {
			$operator = '<=';
			$asc_desc = 'DESC';
		}

		$q = $dbo->getQuery(true)
			->select('*')
			->from($dbo->qn('#__vikchannelmanager_otareviews'));

		if ($load_id) {
			$q->where($dbo->qn('id') . " {$operator} " . $load_id);
			$q->order($dbo->qn('id') . ' ' . $asc_desc);
		} else {
			$q->order($dbo->qn('dt') . ' DESC');
			// no next review
			$has_next = false;
		}

		$dbo->setQuery($q, 0, 1);

		try {
			$review = $dbo->loadObject();

			if (!$review && $load_id) {
				// the query did not produce any result
				$q->clear('order')->clear('where');
				$q->order($dbo->qn('dt') . ' DESC');
				// try to get the most recent review
				$dbo->setQuery($q, 0, 1);
				$review = $dbo->loadObject();
				// no next review
				$has_next = false;
			}

			if ($review && $review->id == 1) {
				// no prev review
				$has_prev = false;
			}
		} catch (Exception $e) {
			$review = null;
		}

		// start output buffering
		ob_start();

		$current_review_id = ($review ?? (new stdClass))->id ?? 0;
		$can_reply = 0;
		$has_reply = 0;
		$review_data = null;

		if (!$review) {
			?>
			<p class="info"><?php echo JText::translate('VBO_NO_RECORDS_FOUND'); ?></p>
			<?php
		} else {
			// let VCM parse the review object
			$review_helper = VCMReviewHelper::getInstance($review);
			$review_data = $review_helper->parseObject();

			// get the review channel details
			$channel_details = $review_helper->getChannelDetails();
			if (!empty($channel_details['logo'])) {
				// prepend channel logo property
				$review_data->{$review->id} = (object) array_merge(['channel_logo' => $channel_details['logo']], (array) $review_data->{$review->id});
			}

			// access additional details
			$can_reply = (int) $review_helper->canReply();
			$has_reply = (int) $review_helper->hasReply();

			if ($can_reply) {
				// print the HTML structure for the host reply to the guest review
				?>
			<div class="vbo-widget-guest-reviews-reply-wrap" data-review-id="<?php echo $review->id; ?>" data-otareview-id="<?php echo $review->review_id ?? ''; ?>">
				<div class="vbo-widget-guest-reviews-reply-inner">
					<label for="<?php echo $wrapper; ?>-reply-text"><?php echo JText::translate('VCMREVREPLYREV'); ?></label>
					<textarea id="<?php echo $wrapper; ?>-reply-text" class="vbo-widget-guest-reviews-reply-txt"></textarea>
					<div class="vbo-widget-guest-reviews-reply-actions">
						<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>
						<button type="button" class="btn btn-success vbo-widget-guest-reviews-reply-action-submit"><?php echo JText::translate('VCMBCAHSUBMIT'); ?></button>
					</div>
				</div>
			</div>
				<?php
			}
		}

		// get the HTML buffer
		$html_content = ob_get_contents();
		ob_end_clean();

		// return an associative array of values
		return [
			'html' 		    => ($html_content ?: ''),
			'review_id'     => $current_review_id,
			'review_data'   => $review_data,
			'review_record' => $review ?? (new stdClass),
			'channel'       => $channel_details,
			'has_next' 		=> $has_next,
			'has_prev' 		=> $has_prev,
			'can_reply'     => $can_reply,
			'has_reply'     => $has_reply,
		];
	}

	/**
	 * @inheritDoc
	 */
	public function render(?VBOMultitaskData $data = null)
	{
		// increase widget's instance counter
		static::$instance_counter++;

		// check whether the widget is being rendered via AJAX when adding it through the customizer
		$is_ajax = $this->isAjaxRendering();

		// generate a unique ID for the guest reviews wrapper instance
		$wrapper_instance = !$is_ajax ? static::$instance_counter : rand();
		$wrapper_id = 'vbo-widget-guest-reviews-' . $wrapper_instance;

		// this widget will work only if VCM is available and updated, and if permissions are met
		$vbo_auth_bookings = JFactory::getUser()->authorise('core.vbo.bookings', 'com_vikbooking');
		if (!$this->vcm_exists || !$vbo_auth_bookings) {
			return;
		}

		// multitask data and options
		$review_id  = 0;
		$booking_id = 0;
		if ($data && $data->isModalRendering()) {
			// check if a specific review ID was set
			$review_id = (int) $this->options()->get('review_id', 0);
			$ota_review_id = (string) $this->options()->get('ota_review_id', '');

			// check if a booking ID was set
			$booking_id = (int) $this->options()->fetchBookingId();

			if ($booking_id && !$review_id) {
				$review_id = $this->getReviewIDFromBooking($booking_id);
			}

			if (!$review_id && $ota_review_id) {
				$review_id = $this->getReviewIDFromOtaId($ota_review_id);
			}
		}

		?>
		<div class="vbo-admin-widget-wrapper">
			<div class="vbo-admin-widget-head">
				<div class="vbo-admin-widget-head-inline">
					<h4><?php echo $this->widgetIcon; ?> <span><?php echo $this->widgetName; ?></span></h4>
					<div class="vbo-admin-widget-head-commands">
					<?php
					if ($this->vcm_exists) {
						?>
						<div class="vbo-reportwidget-commands">
							<div class="vbo-reportwidget-commands-main">
								<div class="vbo-reportwidget-command-dates vbo-widget-guestreviews-topdets vbo-widget-guestreviews-topdets-reply" style="display: none;">
									<button type="button" class="btn btn-rounded btn-light-green vbo-btn-review-reply"><?php echo JText::translate('VBO_REPLY'); ?></button>
								</div>
								<div class="vbo-reportwidget-command-dates vbo-widget-guestreviews-topdets vbo-widget-guestreviews-topdets-info" style="display: none;">
									<div class="vbo-reportwidget-period-name vbo-widget-guestreviews-topdets-cname"></div>
									<div class="vbo-reportwidget-period-date">
										<span class="label label-info vbo-widget-guestreviews-topdets-bid" data-bid="0"></span>
									</div>
								</div>
								<div class="vbo-reportwidget-command-chevron vbo-reportwidget-command-prev">
									<span class="vbo-widget-guestreviews-go-prev" onclick="vboWidgetGuestReviewsNavigate('<?php echo $wrapper_id; ?>', -1);"><?php VikBookingIcons::e('chevron-left'); ?></span>
								</div>
								<div class="vbo-reportwidget-command-chevron vbo-reportwidget-command-next">
									<span class="vbo-widget-guestreviews-go-next" onclick="vboWidgetGuestReviewsNavigate('<?php echo $wrapper_id; ?>', 1);"><?php VikBookingIcons::e('chevron-right'); ?></span>
								</div>
							</div>
						</div>
					<?php
					}
					?>
					</div>
				</div>
			</div>
			<div id="<?php echo $wrapper_id; ?>" class="vbo-widget-guestreviews-wrap" data-review-id="<?php echo $review_id; ?>">
				<div class="vbo-widget-guestreviews-content">
				<?php
				if (!$this->vcm_exists) {
					?>
					<p class="info"><?php echo JText::translate('VBOGUESTREVSVCMREQ'); ?></p>
					<?php
				}
				?>
				</div>
			</div>
		</div>

		<?php
		if (static::$instance_counter === 0 || $is_ajax) {
			// some JS functions should be loaded once per widget instance

			// attempt to load the OTA review category tags
			$ota_category_tags = [];
			if (class_exists('VCMAirbnbReview')) {
				// load the Airbnb review category tags that could have been
				// selected by the guest during the review for the host
				$ota_category_tags = VCMAirbnbReview::getCategoryTags('guest_review_host');
			}
			?>

		<script type="text/javascript">

			/**
			 * The review objects container.
			 */
			var vbo_wgr_review_objects = {};

			/**
			 * language definitions map.
			 */
			var vbo_wgr_langdefs_map = <?php echo json_encode($this->vcm_lang_defs); ?>;

			/**
			 * Review macro-group string.
			 */
			var vbo_wgr_json_macrogr = '';

			/**
			 * Review category tags.
			 */
			var vbo_wgr_category_tags = <?php echo json_encode((object) $ota_category_tags) ?>;

			/**
			 * Icons and values to render the review scoring.
			 */
			var vbo_wgr_full_staricn = '<?php VikBookingIcons::e('star', 'vbo-review-star vbo-review-star-full') ?>';
			var vbo_wgr_void_staricn = '<?php VikBookingIcons::e('star', 'vbo-review-star') ?>';
			var vbo_wgr_servicescore = false;

			/**
			 * Display the loading skeletons.
			 */
			function vboWidgetGuestReviewsSkeletons(wrapper) {
				let widget_instance = jQuery('#' + wrapper);
				if (!widget_instance.length) {
					return false;
				}

				let skeleton_html = '<div class="vbo-widget-spinnner-loading">';
				skeleton_html += '<div class="vbo-widget-spinnner-loading-inner">';
				skeleton_html += '<?php VikBookingIcons::e('circle-notch', 'fa-spin fa-fw'); ?>';
				skeleton_html += '</div>';
				skeleton_html += '</div>';
				widget_instance.find('.vbo-widget-guestreviews-content').html(skeleton_html);
			}

			/**
			 * Starts a randomly timed answer typing.
			 */
			function vboWidgetGuestReviewsTypeAnswer(textarea, words, min, max) {
				if (isNaN(min) || min < 0) {
					min = 0;
				}

				if (isNaN(max) || max < 0) {
					max = 512;
				}

				return new Promise((resolve) => {
					vboWidgetGuestReviewsTypeAnswerRecursive(resolve, textarea, words, min, max);
				});
			}

			/**
			 * Recursively registers the next words to type with a random timer.
			 */
			function vboWidgetGuestReviewsTypeAnswerRecursive(resolve, textarea, words, min, max) {
				if (words.length == 0) {
					// typed all the provided words
					resolve();
				} else {
					// register timeout to append the next word
					setTimeout(() => {
						let val = textarea.val();
						// extract word and append it within the textarea value
						textarea.val((val.length ? val + ' ' : '') + words.shift());
						// keep going until we reach the end of the queue
						vboWidgetGuestReviewsTypeAnswerRecursive(resolve, textarea, words, min, max);
					}, Math.floor(Math.random() * (max - min + 1) + min));
				}
			}

			/**
			 * Navigate between the various reviews.
			 */
			function vboWidgetGuestReviewsNavigate(wrapper, direction) {
				let widget_instance = jQuery('#' + wrapper);
				if (!widget_instance.length) {
					return false;
				}

				// current ID
				let current_id = parseInt(widget_instance.attr('data-review-id'));

				// ID to load and navigation type
				let load_id  = current_id;
				let nav_type = '';

				if (direction && direction > 0) {
					load_id += 1;
					nav_type = 'gt';
				} else if (direction && direction < 0 && load_id > 1) {
					load_id -= 1;
					nav_type = 'lt';
				}

				// set new ID
				widget_instance.attr('data-review-id', load_id);

				// show loading skeletons
				vboWidgetGuestReviewsSkeletons(wrapper);

				// launch navigation
				vboWidgetGuestReviewsLoad(wrapper, nav_type);
			}

			/**
			 * Perform the request to load the guest review.
			 */
			function vboWidgetGuestReviewsLoad(wrapper, nav_type) {
				let widget_instance = jQuery('#' + wrapper);
				if (!widget_instance.length) {
					return false;
				}

				// hide top details before making the request
				widget_instance.parent().find('.vbo-widget-guestreviews-topdets-info, .vbo-widget-guestreviews-topdets-reply').hide();

				// reset values for parsing the review object
				vbo_wgr_json_macrogr = '';
				vbo_wgr_servicescore = false;

				let load_id = widget_instance.attr('data-review-id');

				// the widget method to call
				let call_method = 'loadGuestReview';

				// make a request to load the bookings calendar
				VBOCore.doAjax(
					"<?php echo $this->getExecWidgetAjaxUri(); ?>",
					{
						widget_id: "<?php echo $this->getIdentifier(); ?>",
						call:      call_method,
						return:    1,
						review_id: load_id,
						nav_type:  nav_type,
						wrapper:   wrapper,
						tmpl:      "component"
					},
					(response) => {
						try {
							var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
							if (!obj_res.hasOwnProperty(call_method)) {
								console.error('Unexpected JSON response', obj_res);
								return false;
							}

							let review_id = obj_res[call_method]['review_id'];

							// set current review ID
							widget_instance.attr('data-review-id', review_id);

							// merge review data objects (caching is not really needed as we always perform an AJAX request)
							vbo_wgr_review_objects = Object.assign(vbo_wgr_review_objects, obj_res[call_method]['review_data']);

							// the review language
							let review_lang = obj_res[call_method]['review_record']['lang'];

							// set and show top details
							let top_details = widget_instance.parent();
							let review_bid  = (obj_res[call_method]['review_record']['idorder'] || '0');
							top_details.find('.vbo-widget-guestreviews-topdets-cname').text((obj_res[call_method]['review_record']['customer_name'] || ' '));
							top_details.find('.vbo-widget-guestreviews-topdets-bid')
								.attr('data-bid', review_bid)
								.html('<?php VikBookingIcons::e('external-link'); ?> ' + Joomla.JText._('VBDASHBOOKINGID') + ' ' + review_bid);
							top_details.find('.vbo-widget-guestreviews-topdets-info').show();

							// check if a reply is allowed
							if (obj_res[call_method]['can_reply']) {
								top_details.find('.vbo-widget-guestreviews-topdets-reply').show();
							}

							// check for paging limits
							if (obj_res[call_method]['has_prev']) {
								top_details.find('.vbo-widget-guestreviews-go-prev').show();
							} else {
								top_details.find('.vbo-widget-guestreviews-go-prev').hide();
							}

							if (obj_res[call_method]['has_next']) {
								top_details.find('.vbo-widget-guestreviews-go-next').show();
							} else {
								top_details.find('.vbo-widget-guestreviews-go-next').hide();
							}

							// replace HTML with new data
							widget_instance.find('.vbo-widget-guestreviews-content').html(
								vboWidgetGuestReviewsStringifyObject(vbo_wgr_review_objects[review_id], review_id) + obj_res[call_method]['html']
							);

							if (obj_res[call_method]['can_reply']) {
								// register event to handle the reply functions, manual or through AI
								setTimeout(() => {
									// AI write review
									widget_instance.find('.vbo-widget-guest-reviews-reply-action-ai.ai-write-review').on('click', function() {
										// disable button and start animation
										jQuery(this).prop('disabled', true).html('<?php VikBookingIcons::e('spinner', 'fa-spin'); ?>');

										// build review object
										const review  = vbo_wgr_review_objects[review_id];
										const channel = obj_res[call_method]['channel'];

										let customer = review?.reviewer?.name;
										let score = review?.scoring?.review_score;
										let content = [];

										if (channel.uniquekey == '<?php echo VikChannelManagerConfig::BOOKING; ?>') {
											// Booking.com
											content.push(review?.content?.headline);
											content.push(review?.content?.negative);
											content.push(review?.content?.positive);
										} else if (channel.uniquekey == '<?php echo VikChannelManagerConfig::AIRBNBAPI; ?>') {
											// Airbnb API
											content.push(review?.content?.public_review);
											content = content.concat(Object.values(review?.comments || {}));
										} else if (channel.uniquekey == 0) {
											// Website
											content.push(review?.content?.message);
										}

										if (typeof score !== 'undefined') {
											content.push('Overall score: ' + score + '/10');
										}

										// get rid of the empty comments
										content = content.filter(s => s);

										new Promise((resolve, reject) => {
											if (!content.length) {
												reject(null);
												return false;
											}

											VBOCore.doAjax(
												'<?php echo VikBooking::ajaxUrl('index.php?option=com_vikchannelmanager&task=ai.review'); ?>',
												{
													customer: customer,
													review: content.join("\n"),
													language: review_lang,
												},
												(response) => {
													resolve(response);
												},
												(error) => {
													reject(error.responseText || error.statusText || 'An error has occurred');
												}
											);
										}).then(async (response) => {
											const replyTextarea = widget_instance.find('textarea.vbo-widget-guest-reviews-reply-txt');
											replyTextarea.val('');
											await vboWidgetGuestReviewsTypeAnswer(replyTextarea, (response.review || '').split(/ +/), 32, 128);
										}).catch((error) => {
											if (error) {
												alert(error);
											}
										}).finally(() => {
											// enable button again
											jQuery(this).prop('disabled', false).text(Joomla.JText._('VCM_AI_CHAT_TOOLTIP'));
										});
									});

									// reply submit button
									widget_instance.find('.vbo-widget-guest-reviews-reply-action-submit').on('click', function() {
										// disable button
										jQuery(this).prop('disabled', true);

										// the text for the reply
										let replyText = widget_instance.find('textarea.vbo-widget-guest-reviews-reply-txt').val();

										if (!replyText) {
											alert(Joomla.JText._('VBO_PLEASE_FILL_FIELDS'));
											return false;
										}

										// perform the request
										VBOCore.doAjax(
											'<?php echo VikBooking::ajaxUrl('index.php?option=com_vikchannelmanager&task=review.reply'); ?>',
											{
												review_id:     obj_res[call_method].review_record?.id,
												ota_review_id: obj_res[call_method].review_record?.review_id,
												uniquekey:     obj_res[call_method].channel?.uniquekey,
												reply_text:    replyText,
											},
											(response) => {
												// remove buttons
												widget_instance.find('.vbo-widget-guest-reviews-reply-actions').remove();

												// remove textarea
												widget_instance.find('textarea.vbo-widget-guest-reviews-reply-txt').remove();

												// append reply review text
												widget_instance.find('.vbo-widget-guest-reviews-reply-inner').append('<div class="vbo-widget-guest-reviews-replied-text">' + replyText + '</div>');

												// prepend success icon to label
												widget_instance.find('.vbo-widget-guest-reviews-reply-inner').find('label').prepend('<span class="badge badge-success"><?php VikBookingIcons::e('check-circle'); ?></span> ');

												// hide the top-details "reply" button
												widget_instance.parent().find('.vbo-widget-guestreviews-topdets-reply').hide();
											},
											(error) => {
												// enable button
												jQuery(this).prop('disabled', false);

												// display alert
												alert(error.responseText || error.statusText || 'An error has occurred');
											}
										);
									});
								}, 200);
							}
						} catch(err) {
							console.error('could not parse JSON response', err, response);
						}
					},
					(error) => {
						console.error(error);
						alert(error.responseText);
						// remove the skeleton loading
						widget_instance.find('.vbo-widget-guestreviews-content').find('.vbo-skeleton-loading').remove();
					}
				);
			}

			/**
			 * Ensures the argument is a non-empty object.
			 */
			function vboWidgetGuestReviewsIsObject(obj) {
				if (typeof obj != 'object') {
					return false;
				}

				for (var jk in obj) {
					return obj.hasOwnProperty(jk);
				}
			}

			/**
			 * Turns strings into words with the first letter upper case.
			 */
			function vboWidgetGuestReviewsUcWords(str) {
				return (str + '').replace(/^(.)|\s+(.)/g, function ($1) {
					return $1.toUpperCase();
				});
			}

			/**
			 * Recursively parse a review object to build the HTML structure for representation.
			 */
			function vboWidgetGuestReviewsStringifyObject(obj, idrev) {
				var stringified = '';
				var otareview = (vbo_wgr_review_objects.hasOwnProperty(idrev) && vbo_wgr_review_objects[idrev].hasOwnProperty('channel') && vbo_wgr_review_objects[idrev]['channel'] === null ? false : true);

				for (var jk in obj) {
					let objValue = obj[jk];

					if (!obj.hasOwnProperty(jk) || objValue === null || jk == 'can_reply') {
						continue;
					}

					var usekey = jk.replace(' ', '_').toLowerCase();
					var prop_name = vbo_wgr_langdefs_map.hasOwnProperty(usekey) ? Joomla.JText._(vbo_wgr_langdefs_map[usekey]) : vboWidgetGuestReviewsUcWords(jk.replace(/_/g, ' '));

					if (vboWidgetGuestReviewsIsObject(objValue)) {
						// update macrogroup
						vbo_wgr_json_macrogr = jk.replace(' ', '-').replace('_', '-').toLowerCase();
						// use this property container as a group-label
						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>';
						// recursive call for inner object
						stringified += vboWidgetGuestReviewsStringifyObject(objValue, idrev);
					} else {
						if (jk === 'reviewee_response' && typeof objValue === 'string' && objValue) {
							// overwrite special property with just a string as a value, to be treated as an object
							stringified += '<div class="vbo-review-json-entry vbo-review-json-entry-group vbo-review-json-revieweeresponse">';
						} else {
							// regular property
							stringified += '<div class="vbo-review-json-entry vbo-review-json-' + vbo_wgr_json_macrogr + '">';
						}
						stringified += '<span class="vbo-review-json-key">' + prop_name + '</span>';
						if (!otareview && vbo_wgr_json_macrogr == 'scoring' && (jk != 'review_score' || (jk == 'review_score' && !vbo_wgr_servicescore))) {
							// star rating for website review to a service or global review
							vbo_wgr_servicescore = true;
							stringified += '<span class="vbo-review-json-value">';
							var starscount = Math.floor((parseInt(objValue) / 2));
							for (var s = 1; s <= starscount; s++) {
								stringified += vbo_wgr_full_staricn;
							}
							for (var s = (starscount + 1); s <= 5; s++) {
								stringified += vbo_wgr_void_staricn;
							}
							stringified += '</span>';
						} else {
							vbo_wgr_servicescore = false;
							// check if the value is an URL of the photo/logo
							if (jk == 'photo' && objValue.indexOf('http') >= 0) {
								objValue = '<span class="vbo-review-guest-avatar-wrap"><img class="vbo-review-guest-avatar" src="' + objValue + '" /></span>';
							} else if (jk == 'channel_logo' && objValue.indexOf('http') >= 0) {
								objValue = '<span class="vbo-review-channel-logo-wrap"><img class="vbo-review-channel-logo" src="' + objValue + '" /></span>';
							}

							// build additional element classes
							var value_classes = [];
							if (jk == 'review_score') {
								value_classes.push('vbo-review-json-totscore');
							}
							if (vbo_wgr_json_macrogr == 'scoring' && !isNaN(objValue)) {
								value_classes.push('vbo-review-json-servscore');
							}
							if (jk == 'reviewee_response') {
								value_classes.push('vbo-review-json-revieweeresponse');
							}

							// check if the value should be normalized
							if (vbo_wgr_json_macrogr == 'category-tags' && typeof objValue === 'string' && objValue) {
								// review category tags are expected to be comma separated
								let category_tags = [];
								objValue.split(',').forEach((tag) => {
									tag = tag.trim().toLowerCase();
									if (vbo_wgr_category_tags.hasOwnProperty(tag)) {
										// push normalized tag
										category_tags.push(vbo_wgr_category_tags[tag]['descr']);
									} else {
										// push raw (unknown) tag
										category_tags.push(tag);
									}
								});

								// replace object value
								objValue = category_tags.join(', ').toLowerCase() + '.';
								objValue = objValue.substr(0, 1).toUpperCase() + objValue.substr(1);
							}

							stringified += '<span class="vbo-review-json-value' + (value_classes.length ? ' ' + value_classes.join(' ') : '') + '">' + objValue + '</span>';
						}
						stringified += '</div>';
					}
				}

				return stringified;
			}

		</script>
		<?php
		}
		?>

		<script type="text/javascript">

			jQuery(function() {

				// when document is ready, load the latest guest review
				vboWidgetGuestReviewsLoad('<?php echo $wrapper_id; ?>', '');

				// register click event for the review ID
				jQuery('#<?php echo $wrapper_id; ?>').parent().find('.vbo-widget-guestreviews-topdets-bid').on('click', function() {
					let bid = jQuery(this).attr('data-bid');
					VBOCore.handleDisplayWidgetNotification({widget_id: 'booking_details'}, {
						booking_id: bid,
						modal_options: {
							/**
							 * Overwrite modal options for rendering the admin widget.
							 * We need to use a different suffix in case this current widget was
							 * also rendered within a modal, or it would get dismissed in favour
							 * of the newly opened admin widget.
							 */
							suffix: 'widget_modal_inner_booking_details',
						},
					});
				});

				// register click event for the reply button
				jQuery('#<?php echo $wrapper_id; ?>').parent().find('.vbo-btn-review-reply').on('click', function() {
					let reply_wrap = jQuery('#<?php echo $wrapper_id; ?>').find('.vbo-widget-guest-reviews-reply-wrap');
					let modal_body = jQuery('#<?php echo $wrapper_id; ?>').closest('.vbo-modal-overlay-content-body-scroll');
					if (modal_body.length) {
						modal_body.animate({scrollTop: reply_wrap.offset().top - 20}, {duration: 400});
					} else {
						jQuery('html,body').animate({scrollTop: reply_wrap.offset().top - 20}, {duration: 400});
					}
					reply_wrap.find('textarea').focus();
				});

			});

		</script>

		<?php
	}

	/**
	 * Attempts to find the review ID from the given booking ID.
	 * 
	 * @param 	int 	$booking_id 	The reservation ID.
	 * 
	 * @return 	int
	 */
	protected function getReviewIDFromBooking($booking_id)
	{
		$dbo = JFactory::getDbo();

		$dbo->setQuery(
			$dbo->getQuery(true)
				->select($dbo->qn('id'))
				->from($dbo->qn('#__vikchannelmanager_otareviews'))
				->where($dbo->qn('idorder') . ' = ' . (int) $booking_id)
		);

		try {
			return (int) $dbo->loadResult();
		} catch(Exception $e) {
			// do nothing
		}

		return 0;
	}

	/**
	 * Attempts to find the review ID from the given OTA review ID.
	 * 
	 * @param 	string 	$ota_id 	The OTA review ID.
	 * 
	 * @return 	int
	 */
	protected function getReviewIDFromOtaId($ota_id)
	{
		$dbo = JFactory::getDbo();

		$dbo->setQuery(
			$dbo->getQuery(true)
				->select($dbo->qn('id'))
				->from($dbo->qn('#__vikchannelmanager_otareviews'))
				->where($dbo->qn('review_id') . ' = ' . $dbo->q($ota_id))
		);

		try {
			return (int) $dbo->loadResult();
		} catch(Exception $e) {
			// do nothing
		}

		return 0;
	}
}