PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.7.2
VikBooking Hotel Booking Engine & PMS v1.7.2
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_messages.php

guest_messages.php in VikBooking Hotel Booking Engine & PMS 1.7.2, at admin/helpers/widgets/guest_messages.php

1,591 lines 57.3 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 messages".
15 *
16 * @since 1.16.0 (J) - 1.6.0 (WP)
17 */
18 class VikBookingAdminWidgetGuestMessages 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 * Number of messages per page. Should be an even number.
29 *
30 * @var int
31 */
32 protected $messages_per_page = 6;
33
34 /**
35 * Today Y-m-d string
36 *
37 * @var string
38 */
39 protected $today_ymd = null;
40
41 /**
42 * The path to the VCM lib to see if it's available.
43 *
44 * @var string
45 */
46 protected $vcm_lib_path = '';
47
48 /**
49 * Tells whether VCM is installed and updated.
50 *
51 * @var bool
52 */
53 protected $vcm_exists = true;
54
55 /**
56 * Class constructor will define the widget name and identifier.
57 */
58 public function __construct()
59 {
60 // call parent constructor
61 parent::__construct();
62
63 $this->widgetName = JText::translate('VBO_W_GUESTMESSAGES_TITLE');
64 $this->widgetDescr = JText::translate('VBO_W_GUESTMESSAGES_DESCR');
65 $this->widgetId = basename(__FILE__, '.php');
66
67 // define widget and icon and style name
68 $this->widgetIcon = '<i class="' . VikBookingIcons::i('comment-dots') . '"></i>';
69 $this->widgetStyleName = 'light-orange';
70
71 // load widget's settings
72 $this->widgetSettings = $this->loadSettings();
73 if (!is_object($this->widgetSettings)) {
74 $this->widgetSettings = new stdClass;
75 }
76
77 // today Y-m-d date
78 $this->today_ymd = date('Y-m-d');
79
80 // the path to the VCM library
81 $this->vcm_lib_path = VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php';
82
83 // whether VCM is available
84 if (is_file($this->vcm_lib_path)) {
85 if (!class_exists('VikChannelManager') || !method_exists('VikChannelManager', 'getLatestFromGuests')) {
86 // VCM is outdated
87 $this->vcm_exists = false;
88 }
89
90 // attempt to require the chat handler
91 try {
92 VikBooking::getVcmChatInstance($oid = 0, $channel = null);
93 } catch (Exception $e) {
94 // do nothing
95 }
96
97 // make sure VCM is up to date for this widget
98 if (!class_exists('VCMChatHandler') || !method_exists('VCMChatHandler', 'loadChatAssets')) {
99 // VCM is outdated (>= 1.8.11 required)
100 $this->vcm_exists = false;
101 }
102 } else {
103 $this->vcm_exists = false;
104 }
105
106 // avoid queries on certain pages, as VCM may not have been activated yet
107 if (VBOPlatformDetection::isWordPress() && $this->vcm_exists) {
108 global $pagenow;
109 if (isset($pagenow) && in_array($pagenow, ['update.php', 'plugins.php', 'plugin-install.php'])) {
110 $this->vcm_exists = false;
111 }
112 }
113 }
114
115 /**
116 * Preload the necessary CSS/JS assets from VCM.
117 *
118 * @return void
119 */
120 public function preload()
121 {
122 if ($this->vcm_exists) {
123 // load chat assets from VCM
124 VCMChatHandler::loadChatAssets();
125
126 // datepicker calendar
127 $this->vbo_app->loadDatePicker();
128
129 // additional language defs
130 JText::script('VBO_NO_REPLY_NEEDED');
131 JText::script('VBO_WANT_PROCEED');
132 JText::script('VBOSIGNATURECLEAR');
133 JText::script('VBODASHSEARCHKEYS');
134 }
135 }
136
137 /**
138 * Custom method for this widget only to load the latest guest messages.
139 * The method is called by the admin controller through an AJAX request.
140 * The visibility should be public, it should not exit the process, and
141 * an associative array is returned thanks to the request value "return":1.
142 *
143 * It's the actual rendering of the widget which also allows navigation.
144 */
145 public function loadMessages()
146 {
147 $input = JFactory::getApplication()->input;
148
149 $bid_convo = $input->getString('bid_convo', '');
150 $filters = $input->get('filters', [], 'array');
151 $offset = $input->getInt('offset', 0);
152 $length = $input->getInt('length', $this->messages_per_page);
153 $wrapper = $input->getString('wrapper', '');
154
155 if (!$this->vcm_exists) {
156 VBOHttpDocument::getInstance()->close(500, 'Vik Channel Manager is either not available or outdated');
157 }
158
159 // check messages custom limit per page
160 if ($length > 0 && $length != $this->messages_per_page) {
161 // update widget settings
162 $this->widgetSettings->limpage = $length;
163 $this->updateSettings(json_encode($this->widgetSettings));
164 }
165
166 // build search filters
167 $search_filters = [
168 'guest_name' => $filters['guest_name'] ?? '',
169 'message' => $filters['message'] ?? '',
170 'sender' => $filters['sender'] ?? '',
171 'fromdt' => $filters['fromdt'] ?? '',
172 'todt' => $filters['todt'] ?? '',
173 'unread' => (int) ($filters['unread'] ?? 0),
174 'ai_sort' => (int) ($filters['ai_sort'] ?? 0),
175 ];
176
177 // filter out empty search filter values
178 $search_filters = array_filter($search_filters);
179
180 if (!empty($search_filters['fromdt'])) {
181 // convert the date string from local format to military
182 $search_filters['fromdt'] = date('Y-m-d H:i:s', VikBooking::getDateTimestamp($search_filters['fromdt'], 0, 0, 0));
183 // convert date from local timezone to UTC
184 $search_filters['fromdt'] = JFactory::getDate($search_filters['fromdt'], date_default_timezone_get())->format('Y-m-d H:i:s');
185 }
186
187 if (!empty($search_filters['todt'])) {
188 // convert the date string from local format to military
189 $search_filters['todt'] = date('Y-m-d H:i:s', VikBooking::getDateTimestamp($search_filters['todt'], 23, 59, 59));
190 // convert date from local timezone to UTC
191 $search_filters['todt'] = JFactory::getDate($search_filters['todt'], date_default_timezone_get())->format('Y-m-d H:i:s');
192 }
193
194 // initiate the chat messaging object
195 $chat_messaging = class_exists('VCMChatMessaging') ? VCMChatMessaging::getInstance() : null;
196
197 // last error description
198 $latest_error = '';
199
200 // load latest messages
201 $latest_messages = [];
202
203 try {
204 /**
205 * Search filters require an updated VCM version.
206 *
207 * @since 1.16.9 (J) - 1.6.9 (WP)
208 * @requires VCM >= 1.8.27 (1.9.5 "unread", "ai_sort")
209 */
210 if ($search_filters && $chat_messaging && method_exists($chat_messaging, 'searchMessages')) {
211 // search for specific messages with the specified search filters
212 $latest_messages = $chat_messaging->searchMessages($search_filters, $offset, $length);
213 } else {
214 // regular loading of the latest guest messages with no search filters
215 $latest_messages = VikChannelManager::getLatestFromGuests(['guest_messages'], $offset, $length);
216 }
217 } catch (Exception $e) {
218 // do nothing, but populate the error description
219 $latest_error = $e->getMessage();
220 }
221
222 // the multitask data and notifications can request a specific conversation to be opened
223 $bubble_convo = null;
224 if ($bid_convo) {
225 // make sure the requested booking ID was fetched from the most recent guest messages
226 foreach ($latest_messages as $gmessage) {
227 if ($bid_convo == $gmessage->idorder) {
228 // specific conversation to bubble found
229 $bubble_convo = $bid_convo;
230 break;
231 }
232 if ($bid_convo == $gmessage->idorderota && strcasecmp((string)$gmessage->channel, 'vikbooking')) {
233 // specific OTA conversation to bubble found
234 $bubble_convo = $bid_convo;
235 break;
236 }
237 }
238 if (!$bubble_convo && $chat_messaging) {
239 // updated VCM versions will allow us to fetch one conversation by booking ID
240 $booking_messages = $chat_messaging->loadBookingGuestThreads($bid_convo);
241 if ($booking_messages) {
242 // append the requested conversation so that it will bubble
243 $latest_messages = array_merge($latest_messages, $booking_messages);
244 // turn flag on
245 $bubble_convo = $bid_convo;
246 }
247 }
248 }
249
250 // current year Y and timestamp
251 $current_y = date('Y');
252 $current_ts = time();
253
254 // start output buffering
255 ob_start();
256
257 if ($latest_error) {
258 ?>
259 <p class="err"><?php echo $latest_error; ?></p>
260 <?php
261 }
262
263 if (!$latest_messages) {
264 ?>
265 <p class="info"><?php echo JText::translate('VBO_NO_RECORDS_FOUND'); ?></p>
266 <?php
267 }
268
269 // count total messages
270 $tot_messages = count($latest_messages);
271
272 foreach ($latest_messages as $ind => $gmessage) {
273 $gmessage_content = $gmessage->content;
274 if (empty($gmessage_content)) {
275 $gmessage_content = '.....';
276 } elseif (strlen($gmessage_content) > 90) {
277 if (function_exists('mb_substr')) {
278 $gmessage_content = mb_substr($gmessage_content, 0, 90, 'UTF-8');
279 } else {
280 $gmessage_content = substr($gmessage_content, 0, 90);
281 }
282 $gmessage_content .= '...';
283 }
284
285 // build extra classes for main element
286 $wrap_classes = [];
287 if ($ind === 0) {
288 $wrap_classes[] = 'vbo-w-guestmessages-message-first';
289 } elseif ($ind == ($tot_messages - 1)) {
290 $wrap_classes[] = 'vbo-w-guestmessages-message-last';
291 }
292 if (empty($gmessage->read_dt) && !strcasecmp($gmessage->sender_type, 'guest')) {
293 $wrap_classes[] = 'vbo-w-guestmessages-message-new';
294 }
295
296 ?>
297 <div
298 class="vbo-dashboard-guest-activity vbo-w-guestmessages-message<?php echo $wrap_classes ? ' ' . implode(' ', $wrap_classes) : ''; ?>"
299 data-idorder="<?php echo $gmessage->idorder; ?>"
300 data-idthread="<?php echo !empty($gmessage->id_thread) ? $gmessage->id_thread : ''; ?>"
301 data-idmessage="<?php echo !empty($gmessage->id_message) ? $gmessage->id_message : ''; ?>"
302 data-noreply-needed="<?php echo $gmessage->no_reply_needed ?: 0; ?>"
303 onclick="vboWidgetGuestMessagesOpenChat('<?php echo $gmessage->idorder; ?>');"
304 >
305 <div class="vbo-dashboard-guest-activity-avatar">
306 <?php
307 if (!empty($gmessage->guest_avatar)) {
308 // highest priority goes to the profile picture, not always available
309 ?>
310 <img class="vbo-dashboard-guest-activity-avatar-profile" src="<?php echo $gmessage->guest_avatar; ?>" />
311 <?php
312 } elseif (!empty($gmessage->pic)) {
313 // customer profile picture is not the same as the photo avatar
314 ?>
315 <img class="vbo-dashboard-guest-activity-avatar-profile" src="<?php echo strpos($gmessage->pic, 'http') === 0 ? $gmessage->pic : VBO_SITE_URI . 'resources/uploads/' . $gmessage->pic; ?>" />
316 <?php
317 } elseif (!empty($gmessage->channel_logo)) {
318 // channel logo goes as second option
319 ?>
320 <img class="vbo-dashboard-guest-activity-avatar-profile" src="<?php echo $gmessage->channel_logo; ?>" />
321 <?php
322 } else {
323 // we use an icon as fallback
324 VikBookingIcons::e('user', 'vbo-dashboard-guest-activity-avatar-icon');
325 }
326
327 // check for AI priority enum
328 if (!empty($gmessage->ai_priority)) {
329 // display AI-calculated priority icon and apposite class (expected "high", "medium" or "low")
330 $priority_icon = 'flag';
331 $priority_class = preg_replace("/[^a-z]/", '', strtolower((string) $gmessage->ai_priority));
332 $priority_value = ucwords((string) $gmessage->ai_priority);
333 if (!strcasecmp(trim((string) $gmessage->ai_priority), 'high')) {
334 $priority_value = JText::translate('VBO_PRIORITY_HIGH');
335 } elseif (!strcasecmp(trim((string) $gmessage->ai_priority), 'medium')) {
336 $priority_value = JText::translate('VBO_PRIORITY_MEDIUM');
337 } elseif (!strcasecmp(trim((string) $gmessage->ai_priority), 'low')) {
338 $priority_value = JText::translate('VBO_PRIORITY_LOW');
339 }
340 ?>
341 <div class="vbo-w-guestmessages-message-aipriority">
342 <span class="vbo-w-guestmessages-message-aipriority-icn <?php echo $priority_class; ?> vbo-tooltip vbo-tooltip-top" data-tooltiptext="<?php echo JHtml::fetch('esc_attr', $priority_value); ?>"><?php VikBookingIcons::e($priority_icon, $priority_class); ?></span>
343 </div>
344 <?php
345 }
346 ?>
347 </div>
348 <div class="vbo-dashboard-guest-activity-content">
349 <div class="vbo-dashboard-guest-activity-content-head">
350 <div class="vbo-dashboard-guest-activity-content-info-details">
351 <h4 class="vbo-w-guestmessages-message-gtitle"><span><?php
352 if (!$gmessage->first_name && !$gmessage->last_name) {
353 echo JText::translate('VBO_GUEST');
354 } else {
355 echo $gmessage->first_name . (!empty($gmessage->last_name) ? ' ' . $gmessage->last_name : '');
356 }
357 ?></span><?php
358 if (empty($gmessage->read_dt) && !strcasecmp($gmessage->sender_type, 'guest')) {
359 // print also an icon to inform that the message was not read
360 echo ' ';
361 VikBookingIcons::e('envelope', 'message-new');
362 } elseif (($gmessage->replied ?? 1) == 0 && !strcasecmp($gmessage->sender_type, 'guest')) {
363 /**
364 * Display a label to show that the message was not replied.
365 *
366 * @since 1.16.9 (J) - 1.6.9 (WP)
367 *
368 * @requires VCM >= 1.8.27
369 */
370 echo ' <span class="label label-small message-unreplied">';
371 VikBookingIcons::e('comments', 'message-reply');
372 echo ' ' . JText::translate('VBO_REPLY') . '</span>';
373 }
374
375 /**
376 * Display the AI message category, if available.
377 *
378 * @since 1.17.3 (J) - 1.7.3 (WP)
379 *
380 * @requires VCM >= 1.9.5
381 */
382 if (!empty($gmessage->ai_category)) {
383 echo ' <span class="label label-small message-ai-category">';
384 VikBookingIcons::e('tag');
385 echo ' ' . $gmessage->ai_category . '</span>';
386 }
387 ?></h4>
388 <div class="vbo-dashboard-guest-activity-content-info-icon">
389 <?php
390 if (!empty($gmessage->b_status)) {
391 switch ($gmessage->b_status) {
392 case 'standby':
393 $badge_class = 'badge-warning';
394 $badge_text = JText::translate('VBSTANDBY');
395 break;
396 case 'cancelled':
397 $badge_class = 'badge-danger';
398 $badge_text = JText::translate('VBCANCELLED');
399 break;
400 default:
401 $badge_class = 'badge-success';
402 $badge_text = JText::translate('VBCONFIRMED');
403 if (!empty($gmessage->b_checkout) && $gmessage->b_checkout < $current_ts) {
404 $badge_text = JText::translate('VBOCHECKEDSTATUSOUT');
405 }
406 break;
407 }
408 ?>
409 <span class="badge <?php echo $badge_class; ?>"><?php echo $badge_text; ?></span>
410 <?php
411 }
412 if (!empty($gmessage->b_checkin)) {
413 $stay_info_in = getdate($gmessage->b_checkin);
414 $stay_info_out = getdate($gmessage->b_checkout);
415 $str_checkin = date('d', $gmessage->b_checkin);
416 $str_checkin .= $stay_info_in['mon'] != $stay_info_out['mon'] ? ' ' . VikBooking::sayMonth($stay_info_in['mon'], $short = true) : '';
417 $str_checkout = date('d', $gmessage->b_checkout) . ' ' . VikBooking::sayMonth($stay_info_out['mon'], $short = true);
418 if ($stay_info_in['year'] != $stay_info_out['year'] || $stay_info_in['year'] != $current_y || $stay_info_out['year'] != $current_y) {
419 $str_checkout .= ' ' . $stay_info_in['year'];
420 }
421 ?>
422 <span class="vbo-w-guestmessages-message-staydates">
423 <span class="vbo-w-guestmessages-message-staydates-in"><?php echo $str_checkin; ?></span>
424 <span class="vbo-w-guestmessages-message-staydates-sep">-</span>
425 <span class="vbo-w-guestmessages-message-staydates-out"><?php echo $str_checkout; ?></span>
426 </span>
427 <?php
428 }
429 ?>
430 </div>
431 </div>
432 <div class="vbo-dashboard-guest-activity-content-info-date">
433 <span><?php echo JHtml::fetch('date', $gmessage->last_updated, 'H:i'); ?></span>
434 <?php
435 if (JHtml::fetch('date', $gmessage->last_updated, 'Y-m-d') != $this->today_ymd) {
436 // format and print the date
437 ?>
438 <span><?php echo JHtml::fetch('date', $gmessage->last_updated, str_replace('/', $this->datesep, $this->df)); ?></span>
439 <?php
440 } else {
441 // print "today"
442 ?>
443 <span><?php echo JText::translate('VBTODAY'); ?></span>
444 <?php
445 }
446 ?>
447 </div>
448 </div>
449 <div class="vbo-dashboard-guest-activity-content-info-msg">
450 <p><?php echo $gmessage_content; ?></p>
451 </div>
452 </div>
453 </div>
454 <?php
455 }
456
457 // append navigation
458 ?>
459 <div class="vbo-guestactivitywidget-commands">
460 <div class="vbo-guestactivitywidget-commands-main">
461 <?php
462 if ($offset > 0) {
463 // show backward navigation button
464 ?>
465 <div class="vbo-guestactivitywidget-command-chevron vbo-guestactivitywidget-command-prev">
466 <span class="vbo-guestactivitywidget-prev" onclick="vboWidgetGuestMessagesNavigate('<?php echo $wrapper; ?>', -1);"><?php VikBookingIcons::e('chevron-left'); ?></span>
467 </div>
468 <?php
469 }
470 if ($latest_messages) {
471 // count current page number
472 $page_number = 1;
473 if ($offset > 0) {
474 $page_number = floor($offset / $length) + 1;
475 $page_number = $page_number > 0 ? $page_number : 1;
476 }
477 ?>
478 <div class="vbo-guestactivitywidget-command-chevron vbo-guestactivitywidget-command-page">
479 <span class="vbo-guestactivitywidget-page"><?php echo JText::sprintf('VBO_PAGE_NUMBER', $page_number); ?></span>
480 </div>
481 <div class="vbo-guestactivitywidget-command-chevron vbo-guestactivitywidget-command-next">
482 <span class="vbo-guestactivitywidget-next" onclick="vboWidgetGuestMessagesNavigate('<?php echo $wrapper; ?>', 1);"><?php VikBookingIcons::e('chevron-right'); ?></span>
483 </div>
484 <?php
485 }
486 ?>
487 </div>
488 </div>
489
490 <script type="text/javascript">
491 setTimeout(() => {
492 <?php
493 // check if there were actually no filters applied
494 if (!$search_filters || (count($search_filters) === 1 && ($search_filters['ai_sort'] ?? 0))) {
495 ?>
496 jQuery('#<?php echo $wrapper; ?>-filters').hide();
497 <?php
498 }
499
500 // check if results were sorted through AI
501 if ($search_filters['ai_sort'] ?? 0) {
502 ?>
503 jQuery('#<?php echo $wrapper; ?>-aipowered').fadeIn();
504 <?php
505 } else {
506 ?>
507 jQuery('#<?php echo $wrapper; ?>-aipowered').hide();
508 <?php
509 }
510 ?>
511 }, 200);
512 </script>
513 <?php
514
515 // check if we should bubble a specific conversation
516 if ($bubble_convo) {
517 ?>
518 <script type="text/javascript">
519 setTimeout(() => {
520 vboWidgetGuestMessagesOpenChat('<?php echo $bubble_convo; ?>');
521 }, 300);
522 </script>
523 <?php
524 }
525
526 // append the total number of messages displayed, the current offset and the latest message datetime
527 $latest_datetime = !$search_filters && $tot_messages > 0 && $offset === 0 ? $latest_messages[0]->last_updated : null;
528
529 // get the HTML buffer
530 $html_content = ob_get_contents();
531 ob_end_clean();
532
533 // return an associative array of values
534 return [
535 'html' => $html_content,
536 'tot_messages' => $tot_messages,
537 'offset' => ($offset + $length),
538 'latest_dt' => $latest_datetime,
539 ];
540 }
541
542 /**
543 * Custom method for this widget only to watch the latest guest messages.
544 * The method is called by the admin controller through an AJAX request.
545 * The visibility should be public, it should not exit the process, and
546 * any content sent to output will be returned to the AJAX response.
547 *
548 * Outputs the new number of messages found from the latest datetime.
549 */
550 public function watchMessages()
551 {
552 $latest_dt = VikRequest::getString('latest_dt', '', 'request');
553 if (empty($latest_dt)) {
554 echo '0';
555 return;
556 }
557
558 if (!$this->vcm_exists) {
559 VBOHttpDocument::getInstance()->close(500, 'Vik Channel Manager is either not available or outdated');
560 }
561
562 // load the latest guest message (one is sufficient)
563 $latest_messages = [];
564 try {
565 $latest_messages = VikChannelManager::getLatestFromGuests(['guest_messages'], 0, 1);
566 } catch (Exception $e) {
567 // do nothing
568 }
569
570 if (!count($latest_messages) || $latest_messages[0]->last_updated == $latest_dt) {
571 // no newest messages found
572 echo '0';
573 return;
574 }
575
576 // print 1 to indicate that new messages should be reloaded
577 echo '1';
578 }
579
580 /**
581 * Custom method for this widget only to render the chat of a booking.
582 * The method is called by the admin controller through an AJAX request.
583 * The visibility should be public, it should not exit the process, and
584 * no values should be returned, as the response must be sent to output
585 * in case the JS/CSS assets will be echoed within the response.
586 *
587 * Returns the necessary HTML code to render the chat.
588 */
589 public function renderChat()
590 {
591 $bid = VikRequest::getInt('bid', 0, 'request');
592
593 $booking = VikBooking::getBookingInfoFromID($bid);
594
595 if (!$booking) {
596 VBOHttpDocument::getInstance()->close(404, 'Could not find booking');
597 }
598
599 // initialize chat instance by getting the proper channel name
600 if (empty($booking['channel'])) {
601 // front-end reservation chat handler
602 $chat_channel = 'vikbooking';
603 } else {
604 $channelparts = explode('_', $booking['channel']);
605 // check if this is a meta search channel
606 $is_meta_search = false;
607 if (preg_match("/(customer).*[0-9]$/", $channelparts[0]) || !strcasecmp($channelparts[0], 'googlehotel') || !strcasecmp($channelparts[0], 'trivago')) {
608 $is_meta_search = empty($booking['idorderota']);
609 }
610 if ($is_meta_search) {
611 // customer of type sales channel should use front-end reservation chat handler
612 $chat_channel = 'vikbooking';
613 } else {
614 // let the getInstance method validate the channel chat handler
615 $chat_channel = $booking['channel'];
616 }
617 }
618 $messaging = VikBooking::getVcmChatInstance($booking['id'], $chat_channel);
619
620 if (is_null($messaging)) {
621 VBOHttpDocument::getInstance()->close(500, 'Could not render chat');
622 }
623
624 // send content to output
625 echo $messaging->renderChat([
626 'hideThreads' => 1,
627 ], $load_assets = false);
628 }
629
630 /**
631 * Custom method for this widget only to update the thread of a booking.
632 * The method is called by the admin controller through an AJAX request.
633 * The visibility should be public, it should not exit the process, and
634 * no values should be returned, as the response must be sent to output.
635 *
636 * Returns a successful string or throws an error.
637 */
638 public function setNoReplyNeededThread()
639 {
640 $bid = VikRequest::getInt('bid', 0, 'request');
641 $id_thread = VikRequest::getInt('id_thread', 0, 'request');
642 $status = VikRequest::getInt('status', 0, 'request');
643
644 $booking = VikBooking::getBookingInfoFromID($bid);
645
646 if (!$booking || empty($id_thread)) {
647 VBOHttpDocument::getInstance()->close(404, 'Could not find booking thread');
648 }
649
650 // build thread object for update
651 $thread = new stdClass;
652 $thread->id = $id_thread;
653 $thread->idorder = $bid;
654 $thread->no_reply_needed = !$status ? 1 : 0;
655
656 $dbo = JFactory::getDbo();
657
658 if (!$dbo->updateObject('#__vikchannelmanager_threads', $thread, ['id', 'idorder'])) {
659 VBOHttpDocument::getInstance()->close(500, 'Could not update thread');
660 }
661
662 echo '1';
663 }
664
665 /**
666 * Custom method for this widget only to load the listing details for the booking.
667 * The method is called by the admin controller through an AJAX request.
668 * The visibility should be public, it should not exit the process, and
669 * an associative array is returned thanks to the request value "return":1.
670 *
671 * Needed to provide additional information to the host about the booked listing(s).
672 */
673 public function loadListingDetails()
674 {
675 $bid = VikRequest::getInt('bid', 0, 'request');
676
677 $booking_rooms = VikBooking::loadOrdersRoomsData($bid);
678
679 if (!$this->vcm_exists || !$booking_rooms) {
680 VBOHttpDocument::getInstance()->close(500, 'Could not obtain the listing information');
681 }
682
683 return [
684 'listings' => array_column($booking_rooms, 'room_name'),
685 ];
686 }
687
688 public function render(VBOMultitaskData $data = null)
689 {
690 // increase widget's instance counter
691 static::$instance_counter++;
692
693 // check whether the widget is being rendered via AJAX when adding it through the customizer
694 $is_ajax = $this->isAjaxRendering();
695
696 // generate a unique ID for the guest messages wrapper instance
697 $wrapper_instance = !$is_ajax ? static::$instance_counter : rand();
698 $wrapper_id = 'vbo-widget-guest-messages-' . $wrapper_instance;
699
700 // this widget will work only if VCM is available and updated, and if permissions are met
701 $vbo_auth_bookings = JFactory::getUser()->authorise('core.vbo.bookings', 'com_vikbooking');
702 if (!$this->vcm_exists || !$vbo_auth_bookings) {
703 return;
704 }
705
706 // multitask data event identifier for clearing intervals
707 $js_intvals_id = '';
708 $wrap_extra_class = '';
709 $bid_convo = 0;
710 $unread_filter = false;
711 $sort_by_ai = false;
712 $contains_filter = '';
713 if ($data && $data->isModalRendering()) {
714 // access Multitask data
715 $js_intvals_id = $data->getModalJsIdentifier();
716
717 // check if a specific conversation should be opened
718 if ($data->get('id_message', 0)) {
719 $bid_convo = $data->getBookingId();
720 } else {
721 $bid_convo = $this->options()->fetchBookingId();
722 }
723
724 // set an extra class
725 $wrap_extra_class = ' vbo-w-guestmessages-wrapmodal';
726
727 // check for unread messages filter
728 $unread_filter = (bool) $this->options()->get('unread', '');
729
730 // check for AI sort filter
731 $sort_by_ai = (bool) $this->options()->get('ai_sort', '');
732
733 // check if a specific filter was set for searching guest messages
734 $contains_filter = (string) $this->options()->get('message_contains', '');
735
736 // check custom limit per page only when in modal rendering
737 if (($this->widgetSettings->limpage ?? 0) > 0) {
738 // set custom limit from widget settings
739 $this->messages_per_page = (int) $this->widgetSettings->limpage;
740 }
741
742 // check for limit filter injected
743 $limpage_filter = (int) $this->options()->get('limpage', 0);
744 if ($limpage_filter > 0) {
745 // set custom limit
746 $this->messages_per_page = $limpage_filter;
747 }
748 }
749
750 ?>
751 <div class="vbo-admin-widget-wrapper">
752 <div class="vbo-admin-widget-head">
753 <div class="vbo-admin-widget-head-inline">
754 <h4><?php echo $this->widgetIcon; ?> <span><?php echo $this->widgetName; ?></span></h4>
755 <div class="vbo-admin-widget-head-commands">
756 <div class="vbo-reportwidget-commands">
757 <div class="vbo-reportwidget-commands-main">
758 <div class="vbo-reportwidget-command-dates">
759 <div id="<?php echo $wrapper_id; ?>-filters" class="vbo-reportwidget-period-name" style="<?php echo !$contains_filter && !$unread_filter ? 'display: none;' : ''; ?>"><?php VikBookingIcons::e('filter'); ?> <?php echo JText::translate('VBO_FILTERS_APPLIED'); ?></div>
760 </div>
761 <div id="<?php echo $wrapper_id; ?>-aipowered" class="vbo-admin-widget-head-ai-powered" style="display: none;">
762 <span class="label label-info"><?php echo JText::translate('VBO_AI_LABEL_DEF'); ?></span>
763 </div>
764 </div>
765 <div class="vbo-reportwidget-command-dots">
766 <span class="vbo-widget-command-togglefilters vbo-widget-guest-messages-togglefilters" onclick="vboWidgetGuestMessagesOpenSettings('<?php echo $wrapper_id; ?>');">
767 <?php VikBookingIcons::e('ellipsis-v'); ?>
768 </span>
769 </div>
770 </div>
771 </div>
772 </div>
773 </div>
774 <div id="<?php echo $wrapper_id; ?>" class="vbo-dashboard-guests-latest<?php echo $wrap_extra_class; ?>" data-offset="0" data-length="<?php echo $this->messages_per_page; ?>" data-eventsid="<?php echo $js_intvals_id; ?>" data-latestdt="">
775 <div class="vbo-dashboard-guest-messages-inner">
776 <div class="vbo-dashboard-guest-messages-list">
777 <?php
778 for ($i = 0; $i < $this->messages_per_page; $i++) {
779 ?>
780 <div class="vbo-dashboard-guest-activity vbo-dashboard-guest-activity-skeleton">
781 <div class="vbo-dashboard-guest-activity-avatar">
782 <div class="vbo-skeleton-loading vbo-skeleton-loading-avatar"></div>
783 </div>
784 <div class="vbo-dashboard-guest-activity-content">
785 <div class="vbo-dashboard-guest-activity-content-head">
786 <div class="vbo-skeleton-loading vbo-skeleton-loading-title"></div>
787 </div>
788 <div class="vbo-dashboard-guest-activity-content-subhead">
789 <div class="vbo-skeleton-loading vbo-skeleton-loading-subtitle"></div>
790 </div>
791 <div class="vbo-dashboard-guest-activity-content-info-msg">
792 <div class="vbo-skeleton-loading vbo-skeleton-loading-content"></div>
793 </div>
794 </div>
795 </div>
796 <?php
797 }
798 ?>
799 </div>
800 </div>
801 <div class="vbo-widget-guest-messages-filters-hidden" style="display: none;">
802 <div class="vbo-widget-guest-messages-filters-wrap">
803 <div class="vbo-admin-container vbo-admin-container-full vbo-admin-container-compact">
804 <div class="vbo-params-wrap">
805 <div class="vbo-params-container">
806 <div class="vbo-params-block">
807
808 <div class="vbo-param-container">
809 <div class="vbo-param-label"><?php echo JText::translate('VBCUSTOMERNOMINATIVE'); ?></div>
810 <div class="vbo-param-setting">
811 <input type="text" class="vbo-widget-guest-messages-guestname" value="" autocomplete="off" />
812 <span class="vbo-param-setting-comment"><?php echo JText::translate('VBO_FIRST_NAME_ACCURATE_HELP'); ?></span>
813 </div>
814 </div>
815
816 <div class="vbo-param-container">
817 <div class="vbo-param-label"><?php echo JText::translate('VBSENDEMAILCUSTCONT'); ?></div>
818 <div class="vbo-param-setting">
819 <input type="text" class="vbo-widget-guest-messages-messcontains" value="<?php echo JHtml::fetch('esc_attr', $contains_filter); ?>" autocomplete="off" />
820 <span class="vbo-param-setting-comment"><?php echo JText::translate('VBO_MESSAGE_CONTAINS_HELP'); ?></span>
821 </div>
822 </div>
823
824 <div class="vbo-param-container vbo-toggle-small">
825 <div class="vbo-param-label"><?php echo JText::translate('VBO_UNREAD_MESSAGES'); ?></div>
826 <div class="vbo-param-setting">
827 <?php echo $this->vbo_app->printYesNoButtons('unread', JText::translate('VBYES'), JText::translate('VBNO'), (int) $unread_filter, 1, 0, '', ['blue']); ?>
828 </div>
829 </div>
830
831 <div class="vbo-param-container vbo-toggle-small">
832 <div class="vbo-param-label"><?php echo JText::translate('VBO_AI_LABEL_DEF') . ' ' . JText::translate('VBO_SORT_BY_PRIORITY'); ?></div>
833 <div class="vbo-param-setting">
834 <?php echo $this->vbo_app->printYesNoButtons('ai_sort', JText::translate('VBYES'), JText::translate('VBNO'), (int) $sort_by_ai, 1, 0, '', ['gold']); ?>
835 </div>
836 </div>
837
838 <div class="vbo-param-container vbo-toggle-small">
839 <div class="vbo-param-label"><?php echo JText::translate('VBO_SENDER'); ?></div>
840 <div class="vbo-param-setting">
841 <div class="vbo-widget-guest-messages-multistate">
842 <?php
843 echo $this->vbo_app->multiStateToggleSwitchField(
844 'sender' . $wrapper_instance,
845 '',
846 [
847 'guest',
848 'hotel',
849 ],
850 [
851 [
852 'value' => JText::translate('VBO_GUEST'),
853 ],
854 [
855 'value' => 'Hotel',
856 ],
857 ],
858 [
859 [
860 'label_class' => 'vik-multiswitch-text vik-multiswitch-radiobtn-guest',
861 'input' => [
862 'class' => 'vbo-widget-guest-messages-filter-sender',
863 ],
864 ],
865 [
866 'label_class' => 'vik-multiswitch-text vik-multiswitch-radiobtn-hotel',
867 'input' => [
868 'class' => 'vbo-widget-guest-messages-filter-sender',
869 ],
870 ],
871 ],
872 [
873 'class' => 'vik-multiswitch-noanimation',
874 ]
875 );
876 ?>
877 </div>
878 </div>
879 </div>
880
881 <div class="vbo-param-container">
882 <div class="vbo-param-label"><?php echo JText::translate('VBNEWRESTRICTIONDFROMRANGE'); ?></div>
883 <div class="vbo-param-setting">
884 <div class="vbo-field-calendar">
885 <div class="input-append">
886 <input type="text" class="vbo-widget-guest-messages-fromdt" value="" autocomplete="off" />
887 <button type="button" class="btn btn-secondary vbo-widget-guest-messages-fromdt-trigger"><?php VikBookingIcons::e('calendar'); ?></button>
888 </div>
889 </div>
890 <span class="vbo-param-setting-comment"><?php echo JText::translate('VBO_FROM_DT_HELP'); ?></span>
891 </div>
892 </div>
893
894 <div class="vbo-param-container">
895 <div class="vbo-param-label"><?php echo JText::translate('VBNEWRESTRICTIONDTORANGE'); ?></div>
896 <div class="vbo-param-setting">
897 <div class="vbo-field-calendar">
898 <div class="input-append">
899 <input type="text" class="vbo-widget-guest-messages-todt" value="" autocomplete="off" />
900 <button type="button" class="btn btn-secondary vbo-widget-guest-messages-todt-trigger"><?php VikBookingIcons::e('calendar'); ?></button>
901 </div>
902 </div>
903 <span class="vbo-param-setting-comment"><?php echo JText::translate('VBO_TO_DT_HELP'); ?></span>
904 </div>
905 </div>
906
907 <div class="vbo-param-container">
908 <div class="vbo-param-label"><?php echo JText::translate('VBO_MESS_PER_PAGE'); ?></div>
909 <div class="vbo-param-setting">
910 <input type="number" class="vbo-widget-guest-messages-limpage" min="1" max="100" value="<?php echo $this->messages_per_page; ?>" />
911 </div>
912 </div>
913
914 </div>
915 </div>
916 </div>
917 </div>
918 </div>
919 </div>
920 </div>
921 </div>
922
923 <?php
924 if (static::$instance_counter === 0 || $is_ajax) {
925 // HTML helper tag for URL routing and some JS functions should be loaded once per widget instance
926 $admin_file_base = VBOPlatformDetection::isWordPress() ? 'admin.php' : 'index.php';
927 ?>
928
929 <script type="text/javascript">
930
931 /**
932 * Open the settings to search/filter the guest messages.
933 */
934 function vboWidgetGuestMessagesOpenSettings(wrapper) {
935 var widget_instance = jQuery('#' + wrapper);
936 if (!widget_instance.length) {
937 return false;
938 }
939
940 // define unique modal event name to avoid conflicts
941 var eventsid = widget_instance.attr('data-eventsid') || (Math.floor(Math.random() * 100000));
942 var modal_dismiss_event = 'dismiss-modal-wguestmessages-search' + eventsid;
943
944 // the hidden container of the search input fields
945 var search_elements = widget_instance.find('.vbo-widget-guest-messages-filters-wrap');
946
947 // build the button element to apply the search filters
948 var apply_search_btn = document.createElement('button');
949 apply_search_btn.setAttribute('type', 'button');
950 apply_search_btn.classList.add('btn', 'btn-success');
951 apply_search_btn.append(document.createTextNode(Joomla.JText._('VBODASHSEARCHKEYS')));
952 apply_search_btn.addEventListener('click', () => {
953 VBOCore.emitEvent(modal_dismiss_event, JSON.stringify({applyfilters: 1, wrapper: wrapper}));
954 });
955
956 // build the button element to clear the search filters
957 var clear_search_btn = document.createElement('button');
958 clear_search_btn.setAttribute('type', 'button');
959 clear_search_btn.classList.add('btn');
960 clear_search_btn.append(document.createTextNode(Joomla.JText._('VBOSIGNATURECLEAR')));
961 clear_search_btn.addEventListener('click', () => {
962 VBOCore.emitEvent(modal_dismiss_event, JSON.stringify({clearfilters: 1, wrapper: wrapper}));
963 });
964
965 var search_modal_body = VBOCore.displayModal({
966 suffix: 'wguestmessages-search',
967 extra_class: 'vbo-modal-rounded vbo-modal-dialog',
968 title: '<?php echo JHtml::fetch('esc_attr', JText::translate('VBO_W_GUESTMESSAGES_TITLE')); ?> - ' + Joomla.JText._('VBODASHSEARCHKEYS'),
969 body_prepend: true,
970 draggable: true,
971 footer_left: clear_search_btn,
972 footer_right: apply_search_btn,
973 dismiss_event: modal_dismiss_event,
974 onDismiss: (e) => {
975 // always move back the search input fields
976 search_elements.appendTo(widget_instance.find('.vbo-widget-guest-messages-filters-hidden'));
977
978 if (!e || !e.detail) {
979 // no event data received, maybe the modal was simply dismissed
980 jQuery('#' + wrapper + '-filters').hide();
981 return;
982 }
983
984 // parse data received within the dismiss event
985 try {
986 let commands = JSON.parse(e.detail);
987
988 if (!commands['wrapper']) {
989 return;
990 }
991
992 if (commands['applyfilters']) {
993 // display filters applied label
994 jQuery('#' + wrapper + '-filters').show();
995 // reset offset to 0
996 widget_instance.attr('data-offset', 0);
997 // show loading skeletons
998 vboWidgetGuestMessagesSkeletons(commands['wrapper']);
999 // reload guest messages for this widget's instance with filters set
1000 vboWidgetGuestMessagesLoad(commands['wrapper'], null);
1001 }
1002
1003 if (commands['clearfilters']) {
1004 // clear filters
1005 widget_instance.find('.vbo-widget-guest-messages-filters-wrap').find('input[type="text"]').val('');
1006 widget_instance.find('.vbo-widget-guest-messages-filters-wrap').find('input[type="number"]').val('');
1007 widget_instance.find('.vbo-widget-guest-messages-filters-wrap').find('input[type="checkbox"]').prop('checked', false);
1008 widget_instance.find('.vbo-widget-guest-messages-filters-wrap').find('input[type="radio"]').prop('checked', false);
1009 // hide filters applied label
1010 jQuery('#' + wrapper + '-filters').hide();
1011 // reset offset to 0
1012 widget_instance.attr('data-offset', 0);
1013 // show loading skeletons
1014 vboWidgetGuestMessagesSkeletons(commands['wrapper']);
1015 // reload guest messages for this widget's instance with filters cleared
1016 vboWidgetGuestMessagesLoad(commands['wrapper'], null);
1017 }
1018 } catch(e) {
1019 // abort
1020 return;
1021 }
1022 },
1023 });
1024
1025 // move the search filter fields to the modal body
1026 search_elements.appendTo(search_modal_body);
1027 }
1028
1029 /**
1030 * Open the chat for the clicked booking guest message
1031 */
1032 function vboWidgetGuestMessagesOpenChat(id) {
1033 // clicked message
1034 var message_el = jQuery('.vbo-w-guestmessages-message[data-idorder="' + id + '"]').first();
1035
1036 if (message_el.hasClass('vbo-w-guestmessages-message-new')) {
1037 // get rid of the "new/unread" status
1038 message_el.removeClass('vbo-w-guestmessages-message-new');
1039 if (message_el.find('i.message-new').length) {
1040 message_el.find('i.message-new').remove();
1041 }
1042 }
1043
1044 // modal events unique id to avoid conflicts
1045 var eventsid = message_el.closest('.vbo-dashboard-guests-latest').attr('data-eventsid') || (Math.floor(Math.random() * 100000));
1046
1047 // define unique modal event names to avoid conflicts
1048 var modal_dismiss_event = 'dismiss-modal-wguestmessages-chat' + eventsid;
1049 var modal_loading_event = 'loading-modal-wguestmessages-chat' + eventsid;
1050
1051 // check for multiple instances of this widget, maybe because of clicked notifications while another instance was displayed
1052 if (jQuery('.vbo-w-guestmessages-message[data-idorder="' + id + '"]').length > 1) {
1053 // multiple instances found
1054 if (message_el.closest('.vbo-dashboard-guests-latest').attr('data-eventsid')) {
1055 // dismiss the previous modal and keep using the same event id to ensure a de-registration of the modal events
1056 VBOCore.emitEvent(modal_dismiss_event);
1057 } else {
1058 // fallback to using a random events id to avoid conflicts
1059 eventsid = Math.floor(Math.random() * 100000);
1060 }
1061 }
1062
1063 // build modal content
1064 var chat_head_title = jQuery('<span></span>');
1065 var chat_head_title_wrap = jQuery('<span></span>').addClass('vbo-modal-wguestmessages-chat-info');
1066 var chat_head_title_top = jQuery('<span></span>').addClass('vbo-modal-wguestmessages-chat-info-customer');
1067 var chat_head_title_bot = jQuery('<span></span>').addClass('vbo-modal-wguestmessages-chat-info-booking');
1068
1069 var chat_head_title_img = jQuery('<span></span>').addClass('vbo-modal-wguestmessages-chat-guestavatar');
1070 var chat_head_title_txt = jQuery('<span></span>').addClass('vbo-modal-wguestmessages-chat-guestname');
1071 var chat_head_title_bid = jQuery('<span></span>').addClass('vbo-modal-wguestmessages-chat-guestbid');
1072 var chat_head_open_bid = jQuery('<a></a>').addClass('badge badge-info').attr('href', 'JavaScript: void(0);').text(id).on('click', () => {
1073 VBOCore.handleDisplayWidgetNotification({
1074 widget_id: 'booking_details'
1075 }, {
1076 bid: id,
1077 modal_options: {
1078 suffix: 'widget_modal_inner_booking_details',
1079 }
1080 });
1081 });
1082 chat_head_title_bid.append(chat_head_open_bid);
1083
1084 var guest_avatar = message_el.find('.vbo-dashboard-guest-activity-avatar img');
1085 var guest_name = message_el.find('.vbo-w-guestmessages-message-gtitle').find('span').first().text();
1086 chat_head_title_txt.text(guest_name);
1087 if (guest_avatar && guest_avatar.length) {
1088 chat_head_title_img.append(guest_avatar.clone());
1089 chat_head_title.append(chat_head_title_img);
1090 }
1091 chat_head_title_top.append(chat_head_title_txt);
1092 chat_head_title_top.append(chat_head_title_bid);
1093
1094 chat_head_title_bot.append(message_el.find('.vbo-dashboard-guest-activity-content-info-icon').html());
1095
1096 // register callback for no-reply-needed click
1097 var no_reply_needed_el = jQuery('<a></a>').addClass('label').attr('href', 'JavaScript: void(0);').text(Joomla.JText._('VBO_NO_REPLY_NEEDED'));
1098 if (message_el.attr('data-noreply-needed') == 1) {
1099 // this thread was marked as no-reply needed
1100 no_reply_needed_el.addClass('label-danger');
1101 }
1102 no_reply_needed_el.on('click', () => {
1103 if (confirm(Joomla.JText._('VBO_WANT_PROCEED'))) {
1104 var id_thread = message_el.attr('data-idthread');
1105 if (!id_thread || !id_thread.length) {
1106 return false;
1107 }
1108
1109 // perform the request to toggle the thread as no-reply-needed
1110 var call_method = 'setNoReplyNeededThread';
1111
1112 VBOCore.doAjax(
1113 "<?php echo $this->getExecWidgetAjaxUri(); ?>",
1114 {
1115 widget_id: "<?php echo $this->getIdentifier(); ?>",
1116 call: call_method,
1117 bid: id,
1118 id_thread: id_thread,
1119 status: message_el.attr('data-noreply-needed'),
1120 tmpl: "component"
1121 },
1122 (response) => {
1123 try {
1124 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
1125 if (!obj_res.hasOwnProperty(call_method)) {
1126 console.error('Unexpected JSON response', obj_res);
1127 return false;
1128 }
1129
1130 // emit the event to close (dismiss) the modal
1131 VBOCore.emitEvent(modal_dismiss_event);
1132
1133 // reload the widget
1134 vboWidgetGuestMessagesLoad(message_el.closest('.vbo-dashboard-guests-latest').attr('id'));
1135 } catch(err) {
1136 console.error('could not parse JSON response', err, response);
1137 }
1138 },
1139 (error) => {
1140 console.error(error);
1141 }
1142 );
1143 }
1144 });
1145
1146 // append element that will contain the listing details involved
1147 var listing_details_parent_el = jQuery('<span></span>').addClass('vbo-w-guestmessages-message-stayrooms');
1148 var listing_details_el = jQuery('<span></span>').addClass('vbo-w-guestmessages-message-listings').text('...');
1149 listing_details_parent_el.append(listing_details_el);
1150 chat_head_title_bot.append(listing_details_parent_el);
1151
1152 // append no-reply-needed element
1153 chat_head_title_bot.append(no_reply_needed_el);
1154
1155 // append additional elements
1156 chat_head_title_wrap.append(chat_head_title_top);
1157 chat_head_title_wrap.append(chat_head_title_bot);
1158
1159 chat_head_title.append(chat_head_title_wrap);
1160
1161 // display modal
1162 var chat_modal_body = VBOCore.displayModal({
1163 suffix: 'wguestmessages-chat',
1164 extra_class: 'vbo-modal-rounded vbo-modal-tall vbo-modal-nofooter',
1165 title: chat_head_title,
1166 draggable: true,
1167 dismiss_event: modal_dismiss_event,
1168 onDismiss: () => {
1169 if (typeof VCMChat !== 'undefined') {
1170 VCMChat.getInstance().destroy();
1171 }
1172 },
1173 loading_event: modal_loading_event,
1174 });
1175
1176 // start loading
1177 VBOCore.emitEvent(modal_loading_event);
1178
1179 // perform the request to render the chat in the apposite modal wrapper
1180 var call_method = 'renderChat';
1181
1182 VBOCore.doAjax(
1183 "<?php echo $this->getExecWidgetAjaxUri(); ?>",
1184 {
1185 widget_id: "<?php echo $this->getIdentifier(); ?>",
1186 call: call_method,
1187 bid: id,
1188 tmpl: "component"
1189 },
1190 (response) => {
1191 // stop loading
1192 VBOCore.emitEvent(modal_loading_event);
1193
1194 try {
1195 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
1196 if (!obj_res.hasOwnProperty(call_method)) {
1197 console.error('Unexpected JSON response', obj_res);
1198 return false;
1199 }
1200
1201 // append HTML code to render the chat
1202 chat_modal_body.html(obj_res[call_method]);
1203
1204 // register scroll to bottom with a small delay
1205 setTimeout(() => {
1206 if (typeof VCMChat !== 'undefined') {
1207 VCMChat.getInstance().scrollToBottom();
1208 }
1209 }, 150);
1210 } catch(err) {
1211 console.error('could not parse JSON response', err, response);
1212 }
1213 },
1214 (error) => {
1215 // stop loading
1216 VBOCore.emitEvent(modal_loading_event);
1217 // display error
1218 console.error(error);
1219 alert(error.responseText);
1220 }
1221 );
1222
1223 // perform the request to load the listing details involved
1224 VBOCore.doAjax(
1225 "<?php echo $this->getExecWidgetAjaxUri(); ?>",
1226 {
1227 widget_id: "<?php echo $this->getIdentifier(); ?>",
1228 call: 'loadListingDetails',
1229 return: 1,
1230 bid: id,
1231 tmpl: "component"
1232 },
1233 (response) => {
1234 try {
1235 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
1236 if (!obj_res.hasOwnProperty('loadListingDetails')) {
1237 console.error('Unexpected JSON response', obj_res);
1238 return false;
1239 }
1240
1241 if (!Array.isArray(obj_res['loadListingDetails']['listings']) || !obj_res['loadListingDetails']['listings'].length) {
1242 // remove listing details element
1243 chat_head_title_bot.find('.vbo-w-guestmessages-message-stayrooms').remove();
1244
1245 return false;
1246 }
1247
1248 // set listing details
1249 chat_head_title_bot.find('.vbo-w-guestmessages-message-listings').html('<?php VikBookingIcons::e('home'); ?> ' + obj_res['loadListingDetails']['listings'].join(', '));
1250 } catch(err) {
1251 console.error('could not parse JSON response', err, response);
1252 }
1253 },
1254 (error) => {
1255 // log error
1256 console.error(error);
1257 }
1258 );
1259 }
1260
1261 /**
1262 * Display the loading skeletons.
1263 */
1264 function vboWidgetGuestMessagesSkeletons(wrapper) {
1265 var widget_instance = jQuery('#' + wrapper);
1266 if (!widget_instance.length) {
1267 return false;
1268 }
1269 widget_instance.find('.vbo-dashboard-guest-messages-list').html('');
1270 for (var i = 0; i < <?php echo $this->messages_per_page; ?>; i++) {
1271 var skeleton = '';
1272 skeleton += '<div class="vbo-dashboard-guest-activity vbo-dashboard-guest-activity-skeleton">';
1273 skeleton += ' <div class="vbo-dashboard-guest-activity-avatar">';
1274 skeleton += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-avatar"></div>';
1275 skeleton += ' </div>';
1276 skeleton += ' <div class="vbo-dashboard-guest-activity-content">';
1277 skeleton += ' <div class="vbo-dashboard-guest-activity-content-head">';
1278 skeleton += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-title"></div>';
1279 skeleton += ' </div>';
1280 skeleton += ' <div class="vbo-dashboard-guest-activity-content-subhead">';
1281 skeleton += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-subtitle"></div>';
1282 skeleton += ' </div>';
1283 skeleton += ' <div class="vbo-dashboard-guest-activity-content-info-msg">';
1284 skeleton += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-content"></div>';
1285 skeleton += ' </div>';
1286 skeleton += ' </div>';
1287 skeleton += '</div>';
1288 // append skeleton
1289 jQuery(skeleton).appendTo(widget_instance.find('.vbo-dashboard-guest-messages-list'));
1290 }
1291 }
1292
1293 /**
1294 * Perform the request to load the latest messages.
1295 */
1296 function vboWidgetGuestMessagesLoad(wrapper, bid_convo) {
1297 var widget_instance = jQuery('#' + wrapper);
1298 if (!widget_instance.length) {
1299 return false;
1300 }
1301
1302 var current_offset = parseInt(widget_instance.attr('data-offset'));
1303 var length_per_page = parseInt(widget_instance.attr('data-length'));
1304
1305 // build search filter values
1306 var filters = {
1307 guest_name: widget_instance.find('input.vbo-widget-guest-messages-guestname').val(),
1308 message: widget_instance.find('input.vbo-widget-guest-messages-messcontains').val(),
1309 sender: widget_instance.find('input.vbo-widget-guest-messages-filter-sender[type="radio"]:checked').val(),
1310 fromdt: widget_instance.find('input.vbo-widget-guest-messages-fromdt').val(),
1311 todt: widget_instance.find('input.vbo-widget-guest-messages-todt').val(),
1312 unread: (widget_instance.find('input[name="unread"]').prop('checked') ? 1 : 0),
1313 ai_sort: (widget_instance.find('input[name="ai_sort"]').prop('checked') ? 1 : 0),
1314 };
1315
1316 // custom messages limit per page
1317 let limpage = widget_instance.find('input.vbo-widget-guest-messages-limpage').val();
1318 if (!isNaN(limpage) && limpage > 0 && limpage != length_per_page) {
1319 // update value
1320 length_per_page = limpage;
1321 // set new limit
1322 widget_instance.attr('data-length', limpage);
1323 }
1324
1325 // the widget method to call
1326 var call_method = 'loadMessages';
1327
1328 // make a request to load the messages
1329 VBOCore.doAjax(
1330 "<?php echo $this->getExecWidgetAjaxUri(); ?>",
1331 {
1332 widget_id: "<?php echo $this->getIdentifier(); ?>",
1333 call: call_method,
1334 return: 1,
1335 bid_convo: bid_convo,
1336 filters: filters,
1337 offset: current_offset,
1338 length: length_per_page,
1339 wrapper: wrapper,
1340 tmpl: "component"
1341 },
1342 (response) => {
1343 try {
1344 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
1345 if (!obj_res.hasOwnProperty(call_method)) {
1346 console.error('Unexpected JSON response', obj_res);
1347 return false;
1348 }
1349
1350 // replace HTML with new messages
1351 widget_instance.find('.vbo-dashboard-guest-messages-list').html(obj_res[call_method]['html']);
1352
1353 // check if latest datetime is set
1354 if (obj_res[call_method]['latest_dt']) {
1355 widget_instance.attr('data-latestdt', obj_res[call_method]['latest_dt']);
1356 }
1357
1358 // check results
1359 if (!isNaN(obj_res[call_method]['tot_messages']) && parseInt(obj_res[call_method]['tot_messages']) < 1) {
1360 // no results can indicate the offset is invalid or too high
1361 if (!isNaN(current_offset) && parseInt(current_offset) > 0) {
1362 // reset offset to 0
1363 widget_instance.attr('data-offset', 0);
1364 // show loading skeletons
1365 vboWidgetGuestMessagesSkeletons(wrapper);
1366 // reload the first page
1367 vboWidgetGuestMessagesLoad(wrapper);
1368 }
1369 } else {
1370 if (bid_convo) {
1371 // emit the event to read all notifications in the current context
1372 VBOCore.emitEvent('vbo-nc-read-notifications', {
1373 criteria: {
1374 group: 'guests',
1375 idorder: bid_convo,
1376 }
1377 });
1378 }
1379 }
1380 } catch(err) {
1381 console.error('could not parse JSON response', err, response);
1382 }
1383 },
1384 (error) => {
1385 // remove the skeleton loading
1386 widget_instance.find('.vbo-dashboard-guest-messages-list').find('.vbo-dashboard-guest-activity-skeleton').remove();
1387 console.error(error);
1388 }
1389 );
1390 }
1391
1392 /**
1393 * Navigate between the various pages of the messages.
1394 */
1395 function vboWidgetGuestMessagesNavigate(wrapper, direction) {
1396 var widget_instance = jQuery('#' + wrapper);
1397 if (!widget_instance.length) {
1398 return false;
1399 }
1400
1401 // current offset
1402 var current_offset = parseInt(widget_instance.attr('data-offset'));
1403
1404 // steps per type
1405 var steps = parseInt(widget_instance.attr('data-length'));
1406
1407 // show loading skeletons
1408 vboWidgetGuestMessagesSkeletons(wrapper);
1409
1410 // check direction and update offset for next nav
1411 if (direction > 0) {
1412 // navigate forward
1413 widget_instance.attr('data-offset', (current_offset + steps));
1414 } else {
1415 // navigate backward
1416 var new_offset = current_offset - steps;
1417 new_offset = new_offset >= 0 ? new_offset : 0;
1418 widget_instance.attr('data-offset', new_offset);
1419 }
1420
1421 // launch navigation
1422 vboWidgetGuestMessagesLoad(wrapper);
1423 }
1424
1425 /**
1426 * Watch periodically if there are new messages to be displayed.
1427 */
1428 function vboWidgetGuestMessagesWatch(wrapper) {
1429 var widget_instance = jQuery('#' + wrapper);
1430 if (!widget_instance.length) {
1431 return false;
1432 }
1433
1434 var latest_dt = widget_instance.attr('data-latestdt');
1435 if (!latest_dt || !latest_dt.length) {
1436 return false;
1437 }
1438
1439 // the widget method to call
1440 var call_method = 'watchMessages';
1441
1442 // make a request to watch the messages
1443 VBOCore.doAjax(
1444 "<?php echo $this->getExecWidgetAjaxUri(); ?>",
1445 {
1446 widget_id: "<?php echo $this->getIdentifier(); ?>",
1447 call: call_method,
1448 latest_dt: latest_dt,
1449 tmpl: "component"
1450 },
1451 (response) => {
1452 try {
1453 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
1454 if (!obj_res.hasOwnProperty(call_method)) {
1455 console.error('Unexpected JSON response', obj_res);
1456 return false;
1457 }
1458 // response will contain the number of new messages
1459 if (isNaN(obj_res[call_method]) || parseInt(obj_res[call_method]) < 1) {
1460 // do nothing
1461 return;
1462 }
1463 // new messages found, reset the offset and re-load the first page
1464 widget_instance.attr('data-offset', 0);
1465 // show loading skeletons
1466 vboWidgetGuestMessagesSkeletons(wrapper);
1467 // reload the first page
1468 vboWidgetGuestMessagesLoad(wrapper);
1469 } catch(err) {
1470 console.error('could not parse JSON response', err, response);
1471 }
1472 },
1473 (error) => {
1474 // do nothing
1475 console.error(error);
1476 }
1477 );
1478 }
1479
1480 /**
1481 * Subscribe to the event emitted by VCM's chat handler when replying to a guest message.
1482 */
1483 document.addEventListener('vcm-guest-message-replied', (e) => {
1484 if (!e || !e.detail) {
1485 return;
1486 }
1487
1488 // pool of messaging elements to scan
1489 let elements = [];
1490
1491 // gather the supported event detail properties
1492 let idorder = e.detail.hasOwnProperty('idorder') ? e.detail['idorder'] : null;
1493 let idthread = e.detail.hasOwnProperty('idthread') ? e.detail['idthread'] : null;
1494 let idmessage = e.detail.hasOwnProperty('idmessage') ? e.detail['idmessage'] : null;
1495
1496 // check if some eligible elements can be fetched
1497 if (idorder) {
1498 elements = document.querySelectorAll('.vbo-w-guestmessages-message[data-idorder="' + idorder + '"]');
1499 } else if (idthread) {
1500 elements = document.querySelectorAll('.vbo-w-guestmessages-message[data-idthread="' + idthread + '"]');
1501 } else if (idmessage) {
1502 elements = document.querySelectorAll('.vbo-w-guestmessages-message[data-idmessage="' + idmessage + '"]');
1503 }
1504
1505 // scan all the elements from which the "reply" label should be removed, if any
1506 elements.forEach((element) => {
1507 let unreplied = element.querySelector('.message-unreplied');
1508 if (unreplied) {
1509 // remove node stating that the guest message needs a reply
1510 unreplied.remove();
1511 }
1512 });
1513 });
1514
1515 </script>
1516 <?php
1517 }
1518 ?>
1519
1520 <script type="text/javascript">
1521
1522 jQuery(function() {
1523
1524 // when document is ready, load latest messages for this widget's instance
1525 vboWidgetGuestMessagesLoad('<?php echo $wrapper_id; ?>', '<?php echo $bid_convo; ?>');
1526
1527 // make sure we've got no other chat instances on the same page (editorder)
1528 if (jQuery('#vbmessagingdiv').length) {
1529 if (typeof VCMChat !== 'undefined') {
1530 VCMChat.getInstance().destroy();
1531 }
1532 jQuery('#vbmessagingdiv').html('<p class="info"><?php echo JHtml::fetch('esc_attr', JText::translate('VBO_W_GUESTMESSAGES_TITLE')); ?> - <?php echo JHtml::fetch('esc_attr', JText::translate('VBO_MULTITASK_PANEL')); ?></p>');
1533 }
1534
1535 // set interval for loading new messages automatically
1536 var watch_intv = setInterval(function() {
1537 vboWidgetGuestMessagesWatch('<?php echo $wrapper_id; ?>');
1538 }, 60000);
1539
1540 // render datepicker calendar for dates navigation
1541 jQuery('#<?php echo $wrapper_id; ?>').find('.vbo-widget-guest-messages-fromdt, .vbo-widget-guest-messages-todt').datepicker({
1542 maxDate: "+1d",
1543 yearRange: "<?php echo (date('Y') - 3); ?>:<?php echo date('Y'); ?>",
1544 changeMonth: true,
1545 changeYear: true,
1546 dateFormat: "<?php echo $this->getDateFormat('jui'); ?>",
1547 onSelect: function(selectedDate) {
1548 if (!selectedDate) {
1549 return;
1550 }
1551 if (jQuery(this).hasClass('vbo-widget-guest-messages-fromdt')) {
1552 let nowstart = jQuery(this).datepicker('getDate');
1553 let nowstartdate = new Date(nowstart.getTime());
1554 jQuery('.vbo-widget-guest-messages-todt').datepicker('option', {minDate: nowstartdate});
1555 }
1556 }
1557 });
1558
1559 // triggering for datepicker calendar icon
1560 jQuery('#<?php echo $wrapper_id; ?>').find('.vbo-widget-guest-messages-fromdt-trigger, .vbo-widget-guest-messages-todt-trigger').click(function() {
1561 var jdp = jQuery(this).parent().find('input.hasDatepicker');
1562 if (jdp.length) {
1563 jdp.focus();
1564 }
1565 });
1566
1567 <?php
1568 if ($js_intvals_id) {
1569 // widget can be dismissed through the modal
1570 ?>
1571 document.addEventListener(VBOCore.widget_modal_dismissed + '<?php echo $js_intvals_id; ?>', (e) => {
1572 // clear interval for notifications
1573 clearInterval(watch_intv);
1574
1575 if (jQuery('#vbmessagingdiv').length) {
1576 // reload the page for the previously removed chat in the editorder page
1577 location.reload();
1578 }
1579 });
1580 <?php
1581 }
1582 ?>
1583
1584 });
1585
1586 </script>
1587
1588 <?php
1589 }
1590 }
1591