PluginProbe
VikBooking Hotel Booking Engine & PMS / trunk
VikBooking Hotel Booking Engine & PMS vtrunk
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 1.7.3 All 36 releases
vikbooking / admin / helpers / widgets / notifications_center.php

notifications_center.php in VikBooking Hotel Booking Engine & PMS trunk, at admin/helpers/widgets/notifications_center.php

1,832 lines 60.5 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 srl
6 * @copyright Copyright (C) 2024 E4J srl. 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 "notifications center".
15 *
16 * @since 1.16.8 (J) - 1.6.8 (WP)
17 */
18 class VikBookingAdminWidgetNotificationsCenter 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 * The number of notifications to show per page.
29 *
30 * @var int
31 */
32 protected $records_per_page = 15;
33
34 /**
35 * The maximum number of groups to display.
36 *
37 * @var int
38 */
39 protected $max_groups = 6;
40
41 /**
42 * The total number of skeleton loading elements.
43 *
44 * @var int
45 */
46 protected $tot_skeletons = 4;
47
48 /**
49 * The distance threshold in pixels between the current scroll
50 * position and the end of the list for triggering the loading
51 * of a next page within an infinite scroll mechanism.
52 *
53 * @var int
54 */
55 protected $px_distance_threshold = 140;
56
57 /**
58 * Class constructor will define the widget name and identifier.
59 */
60 public function __construct()
61 {
62 // call parent constructor
63 parent::__construct();
64
65 $this->widgetName = JText::translate('VBO_W_NOTIFSCENTER_TITLE');
66 $this->widgetDescr = JText::translate('VBO_W_NOTIFSCENTER_DESCR');
67 $this->widgetId = basename(__FILE__, '.php');
68
69 $this->widgetIcon = '<i class="' . VikBookingIcons::i('bell') . '"></i>';
70 $this->widgetStyleName = 'light-orange';
71 }
72
73 /**
74 * This widget monitors the latest notification record ID to schedule
75 * periodic watch data in order to be able to update the badge counter.
76 *
77 * @return object
78 */
79 public function preload()
80 {
81 // JS lang defs
82 JText::script('VBO_NOMORE_NOTIFS');
83 JText::script('VBO_NO_NOTIFS');
84
85 // load assets for datepicker
86 $this->vbo_app->loadDatePicker();
87
88 // count the number of unread notifications
89 $watch_data = new stdClass;
90 $watch_data->badge_count = (new VBONotificationCenter)
91 ->countUnread();
92
93 return $watch_data;
94 }
95
96 /**
97 * Checks for new notifications by using the previous preloaded watch-data.
98 * This widget will actually never dispatch any notifications, but only events.
99 *
100 * @param ?VBONotificationWatchdata $watch_data the preloaded watch-data object.
101 *
102 * @return array data object to watch next and notifications array.
103 *
104 * @see preload()
105 */
106 public function getNotifications(?VBONotificationWatchdata $watch_data = null)
107 {
108 // default empty values
109 $watch_next = null;
110 $notifications = [];
111
112 if (!$watch_data) {
113 return [$watch_next, $notifications];
114 }
115
116 // get the number of unread notifications
117 $unread = (new VBONotificationCenter)
118 ->countUnread();
119
120 // build the next watch data for this widget
121 $watch_next = new stdClass;
122 $watch_next->badge_count = $unread;
123
124 // no notifications to dispatch ever, we simply update the next watch data
125 return [$watch_next, $notifications];
126 }
127
128 /**
129 * Checks for new events to be dispatched by using the previous preloaded watch-data.
130 *
131 * @param ?VBONotificationWatchdata $watch_data the preloaded watch-data object.
132 *
133 * @return array list of event objects to dispatch, if any.
134 *
135 * @see preload()
136 */
137 public function getNotificationEvents(?VBONotificationWatchdata $watch_data = null)
138 {
139 if (!$watch_data) {
140 return [];
141 }
142
143 // check the number of unread notifications
144 $unread = (new VBONotificationCenter)
145 ->countUnread();
146
147 if ((int) $watch_data->get('badge_count', 0) == $unread) {
148 // nothing has changed
149 return [];
150 }
151
152 // return the notification events to dispatch
153 return [
154 'vbo-badge-count' => [
155 'badge_count' => $unread,
156 ],
157 ];
158 }
159
160 /**
161 * Custom method for this widget only to load the notifications from a new group.
162 * The method is called by the admin controller through an AJAX request.
163 * The visibility should be public, it should not exit the process, and
164 * any content sent to output will be returned to the AJAX response.
165 * In this case we return an array because this method requires "return":1.
166 *
167 * @return array
168 */
169 public function loadGroupNotifications()
170 {
171 $input = JFactory::getApplication()->input;
172
173 $wrapper = $input->getString('wrapper', '');
174 $group = $input->getString('group', '');
175 $from_date = $input->getString('from_date', '');
176 $to_date = $input->getString('to_date', '');
177 $unread = $input->getBool('unread', false);
178
179 // access the notification-center object
180 $notifCenter = new VBONotificationCenter;
181
182 // build query filters
183 $filters = [];
184
185 if ($group) {
186 $filters['group'] = $group;
187 }
188
189 if ($from_date || $to_date) {
190 // make the filter a non-scalar value
191 $filters['createdon'] = [];
192
193 if ($from_date) {
194 // push filter with operand
195 $filters['createdon'][] = [
196 'operand' => '>=',
197 'value' => $from_date . ' 00:00:00',
198 ];
199 }
200 if ($to_date) {
201 // push filter with operand
202 $filters['createdon'][] = [
203 'operand' => '<=',
204 'value' => $to_date . ' 23:59:59',
205 ];
206 }
207 }
208
209 if ($unread) {
210 // make the filter a non-scalar value
211 $filters['read'] = [
212 [
213 'operand' => '=',
214 'value' => '0',
215 ],
216 ];
217 }
218
219 // get and build the latest notifications for the requested group
220 $html_content = $this->buildNotificationsHTML(
221 $notifCenter->loadNotifications(0, $this->records_per_page, $filters)
222 );
223
224 // return an associative array of values
225 return [
226 'html' => $html_content,
227 'pages_count' => ceil($notifCenter->countFoundNotifications() / $this->records_per_page),
228 ];
229 }
230
231 /**
232 * Custom method for this widget only to load the next page of notifications.
233 * The method is called by the admin controller through an AJAX request.
234 * The visibility should be public, it should not exit the process, and
235 * any content sent to output will be returned to the AJAX response.
236 * In this case we return an array because this method requires "return":1.
237 *
238 * @return array
239 */
240 public function loadNextNotifications()
241 {
242 $input = JFactory::getApplication()->input;
243
244 $wrapper = $input->getString('wrapper', '');
245 $group = $input->getString('group', '');
246 $from_date = $input->getString('from_date', '');
247 $to_date = $input->getString('to_date', '');
248 $unread = $input->getBool('unread', false);
249 $page_num = $input->getUInt('page_num', 1);
250 $page_num = $page_num ?: 1;
251
252 // access the notification-center object
253 $notifCenter = new VBONotificationCenter;
254
255 // build query filters
256 $filters = [];
257
258 if ($group) {
259 $filters['group'] = $group;
260 }
261
262 if ($from_date || $to_date) {
263 // make the filter a non-scalar value
264 $filters['createdon'] = [];
265
266 if ($from_date) {
267 // push filter with operand
268 $filters['createdon'][] = [
269 'operand' => '>=',
270 'value' => $from_date . ' 00:00:00',
271 ];
272 }
273 if ($to_date) {
274 // push filter with operand
275 $filters['createdon'][] = [
276 'operand' => '<=',
277 'value' => $to_date . ' 23:59:59',
278 ];
279 }
280 }
281
282 if ($unread) {
283 // make the filter a non-scalar value
284 $filters['read'] = [
285 [
286 'operand' => '=',
287 'value' => '0',
288 ],
289 ];
290 }
291
292 // determine the query limit start
293 $lim_start = ($page_num - 1) * $this->records_per_page;
294
295 // get and build the latest notifications for the requested group
296 $html_content = $this->buildNotificationsHTML(
297 $notifCenter->loadNotifications($lim_start, $this->records_per_page, $filters),
298 ($page_num - 1)
299 );
300
301 // return an associative array of values
302 return [
303 'html' => $html_content,
304 'page_number' => $page_num,
305 'pages_count' => ceil($notifCenter->countFoundNotifications() / $this->records_per_page),
306 ];
307 }
308
309 /**
310 * Custom method for this widget only to mark some/all notifications as read.
311 * The method is called by the admin controller through an AJAX request.
312 * The visibility should be public, it should not exit the process, and
313 * any content sent to output will be returned to the AJAX response.
314 * In this case we return an array because this method requires "return":1.
315 *
316 * @return array
317 */
318 public function markNotificationsRead()
319 {
320 $input = JFactory::getApplication()->input;
321
322 $notification_ids = $input->getUInt('notif_ids', [], 'array');
323 $mark_all = $input->getUInt('mark_all', 0);
324
325 if (!$notification_ids && !$mark_all) {
326 // do not proceed or all notifications would be marked as read
327 VBOHttpDocument::getInstance()->close(400, 'No notification IDs to mark as read');
328 }
329
330 // access the notification-center object
331 $notifCenter = new VBONotificationCenter;
332
333 // update the given notification IDs as read and obtain the groups involved
334 $groups = $notifCenter->readNotifications($notification_ids);
335
336 if (!$groups) {
337 VBOHttpDocument::getInstance()->close(404, 'No notifications found for marking as read');
338 }
339
340 // build a list of badge counters per group
341 $group_badge_counters = [];
342 foreach ($groups as $group) {
343 // determine the group key for dispatching the dedicated event
344 $group_key = 'vbo-badge-count' . ($group ? '-' . $group : '');
345
346 // count the unread notifications for this group identifier
347 $group_badge_counters[$group_key] = $notifCenter->countUnread($group ?: '');
348 }
349
350 // return an associative array of group-badge counters
351 return $group_badge_counters;
352 }
353
354 /**
355 * Custom method for this widget only to count the unread notifications per group.
356 * The method is called by the admin controller through an AJAX request.
357 * The visibility should be public, it should not exit the process, and
358 * any content sent to output will be returned to the AJAX response.
359 * In this case we return an array because this method requires "return":1.
360 *
361 * @return array
362 */
363 public function countUnreadNotifications()
364 {
365 // access the notification-center object
366 $notifCenter = new VBONotificationCenter;
367
368 // return an associative array of group-badge counters
369 return [
370 'vbo-badge-count' => $notifCenter->countUnread(),
371 ];
372 }
373
374 /**
375 * Custom method for this widget only to read some criteria-matching notifications.
376 * The method is called by the admin controller through an AJAX request.
377 * The visibility should be public, it should not exit the process, and
378 * any content sent to output will be returned to the AJAX response.
379 * In this case we return an array because this method requires "return":1.
380 *
381 * @return array
382 */
383 public function readMatchingNotifications()
384 {
385 $input = JFactory::getApplication()->input;
386
387 $criteria = $input->get('criteria', [], 'array');
388
389 $read_count = 0;
390
391 if (is_array($criteria) && $criteria) {
392 // access the notification-center object
393 $notifCenter = new VBONotificationCenter;
394
395 // read the matching notifications
396 $read_count = $notifCenter->readMatchingNotifications($criteria);
397 }
398
399 return [
400 'read_count' => $read_count,
401 ];
402 }
403
404 /**
405 * Main method to invoke the widget.
406 *
407 * @param ?VBOMultitaskData $data
408 *
409 * @return void
410 */
411 public function render(?VBOMultitaskData $data = null)
412 {
413 // increase widget's instance counter
414 static::$instance_counter++;
415
416 // check whether the widget is being rendered via AJAX when adding it through the customizer
417 $is_ajax = $this->isAjaxRendering();
418
419 // generate a unique ID for the wrapper instance
420 $wrapper_instance = !$is_ajax ? static::$instance_counter : rand();
421 $wrapper_id = 'vbo-widget-notifscenter-' . $wrapper_instance;
422
423 // check permissions
424 $vbo_auth_bookings = JFactory::getUser()->authorise('core.vbo.bookings', 'com_vikbooking');
425 if (!$vbo_auth_bookings) {
426 // permissions are not met
427 return;
428 }
429
430 // check multitask data
431 $in_menu = false;
432 $suggest_push = false;
433 $js_modal_id = '';
434 $is_modal_rendering = false;
435 if ($data) {
436 // access Multitask data
437 $is_modal_rendering = $data->isModalRendering();
438 if ($is_modal_rendering) {
439 // get modal JS identifier
440 $js_modal_id = $data->getModalJsIdentifier();
441 }
442
443 /**
444 * Check the MultitaskOptions to see if the widget is rendered within
445 * the admin-menu through the Notifications Center trigger button.
446 */
447 $in_menu = (bool) $this->options()->get('inMenu');
448 $suggest_push = $in_menu && $this->options()->get('suggestPush');
449 }
450
451 // get minimum and maximum dates for datepicker filters
452 list($mindate, $maxdate) = $this->getMinDatesNotifications();
453 $mindate = empty($mindate) ? time() : $mindate;
454 $maxdate = empty($maxdate) ? $mindate : $maxdate;
455
456 // access the notification-center object
457 $notifCenter = new VBONotificationCenter;
458
459 // load the latest notifications
460 $notifications = $notifCenter->loadNotifications(0, $this->records_per_page);
461
462 // immediately count the number of pages to show all notifications
463 $pages_count = ceil($notifCenter->countFoundNotifications() / $this->records_per_page);
464
465 ?>
466 <div id="<?php echo $wrapper_id; ?>" class="vbo-admin-widget-wrapper<?php echo $in_menu ? ' vbo-notifications-center-inmenu-widget' : ''; ?>" data-instance="<?php echo $wrapper_instance; ?>">
467 <div class="vbo-admin-widget-head">
468 <div class="vbo-admin-widget-head-inline">
469 <h4><?php echo $this->widgetIcon; ?> <span><?php echo $this->widgetName; ?></span></h4>
470 <div class="vbo-admin-widget-head-commands">
471
472 <div class="vbo-reportwidget-commands">
473 <?php
474 if ($suggest_push) {
475 // suggest to subscribe to push notifications
476 ?>
477 <div class="vbo-widget-notifscenter-suggest-push">
478 <button class="vbo-widget-notifscenter-suggest-push-btn vbo-suggest-notifications-btn" type="button"><?php VikBookingIcons::e('bell', 'can-shake') ?></button>
479 </div>
480 <?php
481 }
482 ?>
483 <div class="vbo-reportwidget-commands-main" style="display: none;">
484 <div class="vbo-reportwidget-command-dates">
485 <div class="vbo-reportwidget-period-name"><?php echo JText::translate('VBNEWRESTRICTIONDATERANGE'); ?></div>
486 <div class="vbo-widget-notifscenter-filter-info"></div>
487 </div>
488 </div>
489 <div class="vbo-reportwidget-command-dots">
490 <span class="vbo-widget-command-togglefilters vbo-widget-notifscenter-togglefilters" onclick="vboWidgetNotifsCenterToggleFilters('<?php echo $wrapper_id; ?>');"><?php VikBookingIcons::e('ellipsis-v'); ?></span>
491 </div>
492 </div>
493 <div class="vbo-reportwidget-filters">
494 <div class="vbo-reportwidget-filter vbo-widget-notifscenter-markread">
495 <span class="vbo-widget-notifscenter-read-all"><?php echo JText::translate('VBO_MARK_ALL_READ'); ?></span>
496 </div>
497 <div class="vbo-reportwidget-filter vbo-widget-notifscenter-onlyunread">
498 <span class="vbo-widget-notifscenter-filter-lbl"><?php echo JText::translate('VBO_ONLY_UNREAD'); ?></span>
499 <span class="vbo-widget-notifscenter-filter-val"><?php echo $this->vbo_app->printYesNoButtons('onlyunread', JText::translate('VBYES'), JText::translate('VBNO'), 0, 1, 0); ?></span>
500 </div>
501 <div class="vbo-reportwidget-filter">
502 <span class="vbo-reportwidget-datepicker">
503 <?php VikBookingIcons::e('calendar', 'vbo-widget-notifscenter-caltrigger'); ?>
504 <input type="text" class="vbo-notifscenter-dtpicker-from" value="" placeholder="<?php echo JHtml::fetch('esc_attr', JText::translate('VBNEWRESTRICTIONDFROMRANGE')); ?>" />
505 </span>
506 </div>
507 <div class="vbo-reportwidget-filter">
508 <span class="vbo-reportwidget-datepicker">
509 <?php VikBookingIcons::e('calendar', 'vbo-widget-notifscenter-caltrigger'); ?>
510 <input type="text" class="vbo-notifscenter-dtpicker-to" value="" placeholder="<?php echo JHtml::fetch('esc_attr', JText::translate('VBNEWRESTRICTIONDTORANGE')); ?>" />
511 </span>
512 </div>
513 <div class="vbo-reportwidget-filter vbo-reportwidget-filter-confirm vbo-widget-notifscenter-filter-confirm">
514 <button type="button" class="btn btn-secondary" onclick="vboWidgetNotifsCenterClearFilters('<?php echo $wrapper_id; ?>');" title="<?php echo JHtml::fetch('esc_attr', JText::translate('VBOSIGNATURECLEAR')); ?>"><?php VikBookingIcons::e('broom'); ?></button>
515 <button type="button" class="btn vbo-config-btn" onclick="vboWidgetNotifsCenterApplyFilters('<?php echo $wrapper_id; ?>');"><?php echo JText::translate('VBADMINNOTESUPD'); ?></button>
516 </div>
517 </div>
518
519 </div>
520 </div>
521 </div>
522 <div class="vbo-widget-notifscenter-wrap">
523 <div class="vbo-widget-notifscenter-groups">
524 <?php
525 foreach ($notifCenter->getGroups() as $k => $group) {
526 $group_badge_val = $group['badge_count'] ?: '';
527 $group_badge_node = $group_badge_val ? '<span class="vbo-widget-notifscenter-group-badge">' . $group_badge_val . '</span>' : '';
528 ?>
529 <div class="vbo-widget-notifscenter-group<?php echo ($k === 0 ? ' vbo-widget-notifscenter-group-active' : ''); ?>">
530 <span class="vbo-widget-notifscenter-group-name" data-badge-count="<?php echo $group_badge_val; ?>" data-group-id="<?php echo $group['id']; ?>"><?php echo $group['name'] . $group_badge_node; ?></span>
531 </div>
532 <?php
533 if (($k + 1) >= $this->max_groups) {
534 break;
535 }
536 }
537 ?>
538 </div>
539 <div class="vbo-widget-notifscenter-list" data-group-id="" data-page-number="1" data-pages-count="<?php echo $pages_count; ?>">
540 <?php
541 // output all notifications
542 echo $this->buildNotificationsHTML($notifications);
543 ?>
544 </div>
545 <div class="vbo-widget-notifscenter-loadmore-hidden" style="display: none;">
546 <button type="button" class="btn vbo-widget-notifscenter-loadmore-manual"><?php VikBookingIcons::e('chevron-right'); ?></button>
547 </div>
548 </div>
549 <?php
550 if (!$notifications) {
551 ?>
552 <div class="vbo-widget-notifscenter-loadmore-info">
553 <p><?php echo JText::translate('VBO_NO_NOTIFS'); ?></p>
554 </div>
555 <?php
556 }
557 ?>
558 </div>
559 <?php
560
561 if (static::$instance_counter === 0 || $is_ajax) {
562 /**
563 * Print the JS code only once for all instances of this widget.
564 */
565 ?>
566 <script type="text/javascript">
567
568 /**
569 * @var array
570 */
571 var vbo_widget_nc_badge_evs = [];
572
573 /**
574 * Toggle filters.
575 */
576 function vboWidgetNotifsCenterToggleFilters(wrapper) {
577 var widget_instance = jQuery('#' + wrapper);
578 if (!widget_instance.length) {
579 return false;
580 }
581
582 widget_instance.find('.vbo-reportwidget-filters').toggle();
583 }
584
585 /**
586 * Datepicker dates selection.
587 */
588 function vboWidgetNotifsCenterCheckDates(selectedDate, inst) {
589 if (selectedDate === null || inst === null) {
590 return;
591 }
592 var cur_from_date = jQuery(this).val();
593 if (jQuery(this).hasClass('vbo-notifscenter-dtpicker-from') && cur_from_date.length) {
594 var nowstart = jQuery(this).datepicker('getDate');
595 var nowstartdate = new Date(nowstart.getTime());
596 jQuery('.vbo-notifscenter-dtpicker-to').datepicker('option', {minDate: nowstartdate});
597 }
598 }
599
600 /**
601 * Applies the selected filters and reloads the notifications.
602 */
603 function vboWidgetNotifsCenterApplyFilters(wrapper) {
604 var widget_instance = jQuery('#' + wrapper);
605 if (!widget_instance.length) {
606 return false;
607 }
608
609 // get the notifications list element
610 let notificationsList = widget_instance
611 .find('.vbo-widget-notifscenter-list');
612
613 // find the currently active group
614 let group = notificationsList
615 .attr('data-group-id');
616
617 group = group ? group : '';
618
619 // get the current filter values
620 let from_date = widget_instance
621 .find('.vbo-notifscenter-dtpicker-from')
622 .val();
623 let to_date = widget_instance
624 .find('.vbo-notifscenter-dtpicker-to')
625 .val();
626 let only_unread = widget_instance
627 .find('.vbo-widget-notifscenter-onlyunread')
628 .find('input[type="checkbox"]')
629 .prop('checked');
630
631 // apply the filter attributes
632 notificationsList
633 .attr('data-date-from', from_date);
634 notificationsList
635 .attr('data-date-to', to_date);
636 notificationsList
637 .attr('data-only-unread', (only_unread ? '1' : ''));
638
639 // update filter information node
640 if (from_date || to_date) {
641 let dfilter_info = '';
642 if (from_date == to_date) {
643 dfilter_info = from_date;
644 } else {
645 dfilter_info = from_date + ' ' + (from_date && to_date ? '- ' : '') + to_date;
646 }
647 widget_instance
648 .find('.vbo-widget-notifscenter-filter-info')
649 .html(dfilter_info);
650 widget_instance
651 .find('.vbo-reportwidget-commands-main')
652 .show();
653 } else {
654 widget_instance
655 .find('.vbo-reportwidget-commands-main')
656 .hide();
657 widget_instance
658 .find('.vbo-widget-notifscenter-filter-info')
659 .html('');
660 }
661
662 // toggle filters
663 vboWidgetNotifsCenterToggleFilters(wrapper);
664
665 // reload the notifications for the current group
666 vboWidgetNotifsCenterLoadGroupNotifs(wrapper, group);
667 }
668
669 /**
670 * Clears the current filters and reloads the notifications.
671 */
672 function vboWidgetNotifsCenterClearFilters(wrapper) {
673 var widget_instance = jQuery('#' + wrapper);
674 if (!widget_instance.length) {
675 return false;
676 }
677
678 // get the notifications list element
679 let notificationsList = widget_instance
680 .find('.vbo-widget-notifscenter-list');
681
682 // find the currently active group
683 let group = notificationsList
684 .attr('data-group-id');
685
686 group = group ? group : '';
687
688 // unset current filters
689 widget_instance
690 .find('.vbo-notifscenter-dtpicker-from')
691 .val('');
692 widget_instance
693 .find('.vbo-notifscenter-dtpicker-to')
694 .val('');
695 widget_instance
696 .find('.vbo-widget-notifscenter-onlyunread')
697 .find('input[type="checkbox"]')
698 .prop('checked', false);
699
700 // update the filter attributes
701 notificationsList
702 .attr('data-date-from', '');
703 notificationsList
704 .attr('data-date-to', '');
705 notificationsList
706 .attr('data-only-unread', '');
707
708 // update filter information node
709 widget_instance
710 .find('.vbo-reportwidget-commands-main')
711 .hide();
712 widget_instance
713 .find('.vbo-widget-notifscenter-filter-info')
714 .html('');
715
716 // toggle filters
717 vboWidgetNotifsCenterToggleFilters(wrapper);
718
719 // reload the notifications for the current group
720 vboWidgetNotifsCenterLoadGroupNotifs(wrapper, group);
721 }
722
723 /**
724 * Returns the skeletons loading HTML.
725 */
726 function vboWidgetNotifsCenterGetSkeletons() {
727 var skeletons = '<div class="vbo-dashboard-guests-latest vbo-widget-notifscenter-skeletons">' + "\n";
728
729 for (var i = 0; i < <?php echo $this->tot_skeletons; ?>; i++) {
730 skeletons += '<div class="vbo-dashboard-guest-activity vbo-dashboard-guest-activity-skeleton">' + "\n";
731 skeletons += ' <div class="vbo-dashboard-guest-activity-avatar">' + "\n";
732 skeletons += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-avatar"></div>' + "\n";
733 skeletons += ' </div>' + "\n";
734 skeletons += ' <div class="vbo-dashboard-guest-activity-content">' + "\n";
735 skeletons += ' <div class="vbo-dashboard-guest-activity-content-head">' + "\n";
736 skeletons += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-title"></div>' + "\n";
737 skeletons += ' </div>' + "\n";
738 skeletons += ' <div class="vbo-dashboard-guest-activity-content-subhead">' + "\n";
739 skeletons += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-subtitle"></div>' + "\n";
740 skeletons += ' </div>' + "\n";
741 skeletons += ' <div class="vbo-dashboard-guest-activity-content-info-msg">' + "\n";
742 skeletons += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-content"></div>' + "\n";
743 skeletons += ' </div>' + "\n";
744 skeletons += ' </div>' + "\n";
745 skeletons += '</div>' + "\n";
746 }
747
748 skeletons += '</div>' + "\n";
749
750 return skeletons;
751 }
752
753 /**
754 * Prepares the loading of the notifications for a group.
755 */
756 function vboWidgetNotifsCenterLoadGroupNotifs(wrapper, group) {
757 var widget_instance = jQuery('#' + wrapper);
758 if (!widget_instance.length) {
759 return false;
760 }
761
762 // remove the currently active group
763 widget_instance
764 .find('.vbo-widget-notifscenter-group-active')
765 .removeClass('vbo-widget-notifscenter-group-active');
766
767 // add the active class to the clicked group
768 widget_instance
769 .find('.vbo-widget-notifscenter-group-name[data-group-id="' + group + '"]')
770 .closest('.vbo-widget-notifscenter-group')
771 .addClass('vbo-widget-notifscenter-group-active');
772
773 // make sure to hide the information block, if any
774 widget_instance
775 .find('.vbo-widget-notifscenter-loadmore-info')
776 .remove();
777
778 // set the new group identifier, reset page counters and populate skeletons
779 widget_instance
780 .find('.vbo-widget-notifscenter-list')
781 .attr('data-group-id', group)
782 .attr('data-page-number', 1)
783 .attr('data-pages-count', 1)
784 .html(vboWidgetNotifsCenterGetSkeletons());
785
786 // reload notifications for the current group
787 vboWidgetNotifsCenterReloadNotifs(wrapper);
788 }
789
790 /**
791 * Reloads the notifications for the current group.
792 */
793 function vboWidgetNotifsCenterReloadNotifs(wrapper) {
794 var notificationsList = document
795 .querySelector('#' + wrapper)
796 .querySelector('.vbo-widget-notifscenter-list');
797
798 if (!notificationsList) {
799 throw new Error('Could not find notifications list element');
800 }
801
802 // remove scroll listener for the current list
803 if (notificationsList.wrapperId) {
804 // unregister the infinite scroll
805 notificationsList
806 .removeEventListener('scroll', vboWidgetNotifsCenterInfiniteScroll);
807 }
808
809 // get the current group
810 var group_id = notificationsList.getAttribute('data-group-id');
811
812 // get the search filters, if any
813 var from_date = notificationsList.getAttribute('data-date-from');
814 var to_date = notificationsList.getAttribute('data-date-to');
815 var only_unread = notificationsList.getAttribute('data-only-unread');
816
817 // the widget method to call
818 var call_method = 'loadGroupNotifications';
819
820 // make a request to load the notifications for the current group
821 VBOCore.doAjax(
822 "<?php echo $this->getExecWidgetAjaxUri(); ?>",
823 {
824 widget_id: "<?php echo $this->getIdentifier(); ?>",
825 call: call_method,
826 return: 1,
827 group: group_id,
828 from_date: from_date,
829 to_date: to_date,
830 unread: only_unread,
831 wrapper: wrapper,
832 tmpl: "component"
833 },
834 (response) => {
835 try {
836 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
837 if (!obj_res.hasOwnProperty(call_method)) {
838 console.error('Unexpected JSON response', obj_res);
839 return false;
840 }
841
842 // replace HTML with the reloaded notifications for the current group and set page infos
843 notificationsList
844 .setAttribute('data-page-number', 1);
845 notificationsList
846 .setAttribute('data-pages-count', obj_res[call_method]['pages_count']);
847 notificationsList
848 .innerHTML = obj_res[call_method]['html'];
849
850 if (!obj_res[call_method]['html']) {
851 // print message for no notifications found
852 notificationsList
853 .innerHTML = '<div class="vbo-widget-notifscenter-loadmore-info"><p>' + Joomla.JText._('VBO_NO_NOTIFS') + '</p></div>';
854 } else {
855 // schedule setup functions
856 setTimeout(() => {
857 // set up infinite scroll listener for the new list
858 vboWidgetNotifsCenterSetupInfiniteScroll(wrapper);
859
860 // set up notifications click listeners
861 vboWidgetNotifsCenterRegisterClickListeners(wrapper);
862 }, 100);
863 }
864 } catch(err) {
865 console.error('could not parse JSON response', err, response);
866 }
867 },
868 (error) => {
869 // display the error
870 alert(error.responseText);
871
872 // remove loading skeletons
873 notificationsList
874 .querySelector('.vbo-widget-notifscenter-skeletons')
875 .remove();
876 }
877 );
878 }
879
880 /**
881 * Loads the next page of notifications.
882 */
883 function vboWidgetNotifsCenterLoadNextPage(wrapper) {
884 var notificationsList = document
885 .querySelector('#' + wrapper)
886 .querySelector('.vbo-widget-notifscenter-list');
887
888 if (!notificationsList) {
889 throw new Error('Could not find notifications list element');
890 }
891
892 // ensure we've got other pages to load
893 var pageNumber = parseInt(notificationsList.getAttribute('data-page-number')) || 1;
894 var pagesCount = parseInt(notificationsList.getAttribute('data-pages-count')) || 1;
895
896 if (pageNumber >= pagesCount) {
897 // no more pages available, abort
898 return;
899 }
900
901 // load the next page of notifications
902
903 // append loading skeletons
904 notificationsList
905 .insertAdjacentHTML('beforeend', vboWidgetNotifsCenterGetSkeletons());
906
907 // get the current group
908 var group_id = notificationsList.getAttribute('data-group-id');
909
910 // get the search filters, if any
911 var from_date = notificationsList.getAttribute('data-date-from');
912 var to_date = notificationsList.getAttribute('data-date-to');
913 var only_unread = notificationsList.getAttribute('data-only-unread');
914
915 // the widget method to call
916 var call_method = 'loadNextNotifications';
917
918 // make a request to load the next page of notifications for the current group
919 VBOCore.doAjax(
920 "<?php echo $this->getExecWidgetAjaxUri(); ?>",
921 {
922 widget_id: "<?php echo $this->getIdentifier(); ?>",
923 call: call_method,
924 return: 1,
925 group: group_id,
926 from_date: from_date,
927 to_date: to_date,
928 unread: only_unread,
929 page_num: parseInt(pageNumber) + 1,
930 wrapper: wrapper,
931 tmpl: "component"
932 },
933 (response) => {
934 try {
935 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
936 if (!obj_res.hasOwnProperty(call_method)) {
937 console.error('Unexpected JSON response', obj_res);
938 return false;
939 }
940
941 // remove loading skeletons
942 notificationsList
943 .querySelector('.vbo-widget-notifscenter-skeletons')
944 .remove();
945
946 // append HTML with the new notifications for the current group and set page infos
947 notificationsList
948 .setAttribute('data-page-number', obj_res[call_method]['page_number']);
949 notificationsList
950 .setAttribute('data-pages-count', obj_res[call_method]['pages_count']);
951 notificationsList
952 .insertAdjacentHTML('beforeend', obj_res[call_method]['html']);
953
954 // turn custom property off for the page loading
955 notificationsList.pageLoading = false;
956
957 // set up notifications click listeners for the new notifications read
958 vboWidgetNotifsCenterRegisterClickListeners(wrapper);
959 } catch(err) {
960 console.error('could not parse JSON response', err, response);
961 }
962 },
963 (error) => {
964 // display the error
965 alert(error.responseText);
966
967 // turn custom property off for the page loading
968 notificationsList.pageLoading = false;
969
970 // remove loading skeletons
971 notificationsList
972 .querySelector('.vbo-widget-notifscenter-skeletons')
973 .remove();
974 }
975 );
976 }
977
978 /**
979 * Setups the infinite scroll loading.
980 */
981 function vboWidgetNotifsCenterSetupInfiniteScroll(wrapper) {
982 var notificationsList = document
983 .querySelector('#' + wrapper)
984 .querySelector('.vbo-widget-notifscenter-list');
985
986 if (!notificationsList) {
987 throw new Error('Could not find notifications list element');
988 }
989
990 // ensure we've got more pages to load
991 var pageNumber = parseInt(notificationsList.getAttribute('data-page-number')) || 1;
992 var pagesCount = parseInt(notificationsList.getAttribute('data-pages-count')) || 1;
993
994 if (pageNumber >= pagesCount) {
995 // no pagination needed
996 return;
997 }
998
999 // get wrapper dimensions
1000 var listViewHeight = notificationsList.offsetHeight;
1001 var listGlobHeight = notificationsList.scrollHeight;
1002 var listScrollTop = notificationsList.scrollTop;
1003
1004 if (listViewHeight >= listGlobHeight) {
1005 // no scrolling detected, show manual loading
1006 document
1007 .querySelector('#' + wrapper)
1008 .querySelector('.vbo-widget-notifscenter-loadmore-hidden')
1009 .style
1010 .display = 'block';
1011
1012 return;
1013 }
1014
1015 // inject custom property to identify the wrapper ID
1016 notificationsList.wrapperId = wrapper;
1017
1018 // register infinite scroll event handler
1019 notificationsList
1020 .addEventListener('scroll', vboWidgetNotifsCenterInfiniteScroll);
1021 }
1022
1023 /**
1024 * Infinite scroll event handler.
1025 */
1026 function vboWidgetNotifsCenterInfiniteScroll(e) {
1027 // access the injected wrapper ID property
1028 var wrapper = e.currentTarget.wrapperId;
1029
1030 if (!wrapper) {
1031 return;
1032 }
1033
1034 // register throttling callback
1035 VBOCore.throttleTimer(() => {
1036 // access the current notifications list
1037 var notificationsList = document
1038 .querySelector('#' + wrapper)
1039 .querySelector('.vbo-widget-notifscenter-list');
1040
1041 // ensure we've got more pages to load
1042 var pageNumber = parseInt(notificationsList.getAttribute('data-page-number')) || 1;
1043 var pagesCount = parseInt(notificationsList.getAttribute('data-pages-count')) || 1;
1044
1045 if (pageNumber >= pagesCount) {
1046 // unregister the infinite scroll
1047 notificationsList
1048 .removeEventListener('scroll', vboWidgetNotifsCenterInfiniteScroll);
1049
1050 // display message for all notifications loaded
1051 if (pagesCount > 1) {
1052 let widget_content = document
1053 .querySelector('#' + wrapper);
1054
1055 // hide the eventually displayed manual loading
1056 widget_content
1057 .querySelector('.vbo-widget-notifscenter-loadmore-hidden')
1058 .style
1059 .display = 'none';
1060
1061 if (!widget_content.querySelector('.vbo-widget-notifscenter-loadmore-info')) {
1062 // append the message stating that all notifications have been displayed
1063 let infoDiv = document
1064 .createElement('div');
1065 infoDiv.classList
1066 .add('vbo-widget-notifscenter-loadmore-info');
1067
1068 let infoTxt = document
1069 .createElement('p');
1070 infoTxt.append(Joomla.JText._('VBO_NOMORE_NOTIFS'));
1071
1072 infoDiv.append(infoTxt);
1073
1074 widget_content.append(infoDiv);
1075 }
1076 }
1077
1078 return;
1079 }
1080
1081 // make sure the loading of a next page isn't running
1082 if (notificationsList.pageLoading) {
1083 // abort
1084 return;
1085 }
1086
1087 // get wrapper dimensions
1088 var listViewHeight = notificationsList.offsetHeight;
1089 var listGlobHeight = notificationsList.scrollHeight;
1090 var listScrollTop = notificationsList.scrollTop;
1091
1092 if (!listScrollTop || listViewHeight >= listGlobHeight) {
1093 // no scrolling detected at all
1094 return;
1095 }
1096
1097 // calculate missing distance to the end of the list
1098 var listEndDistance = listGlobHeight - (listViewHeight + listScrollTop);
1099
1100 if (listEndDistance < <?php echo $this->px_distance_threshold; ?>) {
1101 // inject custom property to identify a next page is loading
1102 notificationsList.pageLoading = true;
1103
1104 // load the next page of notifications
1105 vboWidgetNotifsCenterLoadNextPage(wrapper);
1106 }
1107 }, 500);
1108 }
1109
1110 /**
1111 * Registers the click listener on all the eligible notification entries.
1112 */
1113 function vboWidgetNotifsCenterRegisterClickListeners(wrapper) {
1114 var notifications = document
1115 .querySelector('#' + wrapper)
1116 .querySelector('.vbo-widget-notifscenter-list')
1117 .querySelectorAll('.vbo-widget-notifscenter-notif-wrap:not([data-listening])');
1118
1119 notifications.forEach((notification) => {
1120 // immediately set attribute flag with listening enabled
1121 notification.setAttribute('data-listening', 1);
1122
1123 // check if the notification has to be read
1124 if (notification.classList.contains('vbo-widget-notifscenter-notif-unread')) {
1125 // add click event listener to mark the notification as read
1126 notification.addEventListener('click', vboWidgetNotifsCenterReadNotification);
1127 }
1128
1129 // get main attributes
1130 let idorder = notification.getAttribute('data-idorder');
1131 let idorderota = notification.getAttribute('data-idorderota');
1132
1133 if (idorder || idorderota) {
1134 // add click event listener to open the booking details admin-widget
1135 notification.addEventListener('click', (e) => {
1136 if (e.target && e.target.classList.contains('vbo-notifscenter-cta-btn')) {
1137 // do nothing on a call-to-action button
1138 return;
1139 }
1140 // this event listener will never need to be removed
1141 VBOCore.handleDisplayWidgetNotification({widget_id: 'booking_details'}, {
1142 bid: idorder || idorderota,
1143 modal_options: {
1144 /**
1145 * Overwrite modal options for rendering the admin widget.
1146 * We need to use a different suffix in case this current widget was
1147 * also rendered within a modal, or it would get dismissed in favour
1148 * of the newly opened admin widget.
1149 */
1150 suffix: 'widget_modal_inner_booking_details',
1151 },
1152 });
1153 });
1154 }
1155 });
1156 }
1157
1158 /**
1159 * Read notification click event handler.
1160 */
1161 function vboWidgetNotifsCenterReadNotification(e) {
1162 let notification = e.currentTarget || null;
1163 if (!notification) {
1164 throw new Error('This function requires an event target');
1165 }
1166
1167 // immediately remove the click listener to read this notification
1168 notification.removeEventListener('click', vboWidgetNotifsCenterReadNotification);
1169
1170 // add the "read" class
1171 notification.classList.add('vbo-widget-notifscenter-notif-read');
1172
1173 // remove the "unread" class
1174 notification.classList.remove('vbo-widget-notifscenter-notif-unread');
1175
1176 // the widget method to call
1177 var call_method = 'markNotificationsRead';
1178
1179 // execute the request to update the notification as read
1180 VBOCore.doAjax(
1181 "<?php echo $this->getExecWidgetAjaxUri(); ?>",
1182 {
1183 widget_id: "<?php echo $this->getIdentifier(); ?>",
1184 call: call_method,
1185 return: 1,
1186 notif_ids: [notification.getAttribute('data-notif-id')],
1187 tmpl: "component"
1188 },
1189 (response) => {
1190 try {
1191 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
1192 if (!obj_res.hasOwnProperty(call_method)) {
1193 console.error('Unexpected JSON response', obj_res);
1194 return false;
1195 }
1196
1197 // build events data object
1198 let events_data = {};
1199
1200 // dispatch the events for the groups returned to update the badge counters
1201 for (var ev_name in obj_res[call_method]) {
1202 if (!obj_res[call_method].hasOwnProperty(ev_name)) {
1203 continue;
1204 }
1205
1206 if (ev_name.indexOf('vbo-badge-count') !== 0) {
1207 // not a valid event name
1208 continue;
1209 }
1210
1211 // build the event data object
1212 let event_data = {
1213 badge_count: obj_res[call_method][ev_name],
1214 };
1215
1216 // dispatch the group badge count update event
1217 VBOCore.emitEvent(ev_name, event_data);
1218
1219 // set event data property to object
1220 events_data[ev_name] = event_data;
1221 }
1222
1223 // post message onto broadcast channel for any other browsing context
1224 if (VBOCore.broadcast_watch_events) {
1225 // this will trigger the events on any other browsing context
1226 VBOCore.broadcast_watch_events.postMessage(events_data);
1227 }
1228 } catch (err) {
1229 console.error('could not parse JSON response', err, response);
1230 }
1231 },
1232 (error) => {
1233 // silently log the error
1234 console.error(error.responseText);
1235 }
1236 );
1237 }
1238
1239 /**
1240 * Mark all notifications as read.
1241 */
1242 function vboWidgetNotifsCenterReadAllNotifs(wrapper) {
1243 // toggle filters
1244 vboWidgetNotifsCenterToggleFilters(wrapper);
1245
1246 // remove the unread class from any occurrence
1247 document
1248 .querySelectorAll('.vbo-widget-notifscenter-notif-wrap.vbo-widget-notifscenter-notif-unread')
1249 .forEach((notification) => {
1250 notification.classList.add('vbo-widget-notifscenter-notif-read');
1251 notification.classList.remove('vbo-widget-notifscenter-notif-unread');
1252 });
1253
1254 // the widget method to call
1255 var call_method = 'markNotificationsRead';
1256
1257 // execute the request to update the notification as read
1258 VBOCore.doAjax(
1259 "<?php echo $this->getExecWidgetAjaxUri(); ?>",
1260 {
1261 widget_id: "<?php echo $this->getIdentifier(); ?>",
1262 call: call_method,
1263 return: 1,
1264 notif_ids: [],
1265 mark_all: 1,
1266 tmpl: "component"
1267 },
1268 (response) => {
1269 try {
1270 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
1271 if (!obj_res.hasOwnProperty(call_method)) {
1272 console.error('Unexpected JSON response', obj_res);
1273 return false;
1274 }
1275
1276 // dispatch the events for the groups returned to update the badge counters
1277 for (var ev_name in obj_res[call_method]) {
1278 if (!obj_res[call_method].hasOwnProperty(ev_name)) {
1279 continue;
1280 }
1281 if (ev_name.indexOf('vbo-badge-count') !== 0) {
1282 // not a valid event name
1283 continue;
1284 }
1285 // dispatch the group badge count update event
1286 VBOCore.emitEvent(ev_name, {
1287 badge_count: obj_res[call_method][ev_name],
1288 });
1289 }
1290 } catch (err) {
1291 console.error('could not parse JSON response', err, response);
1292 }
1293 },
1294 (error) => {
1295 // silently log the error
1296 console.error(error.responseText);
1297 }
1298 );
1299 }
1300
1301 /**
1302 * Registers the event listeners for updating the group badge counters.
1303 */
1304 function vboWidgetNotifsCenterRegisterGroupBadgeListeners(wrapper) {
1305 var groups = document
1306 .querySelector('#' + wrapper)
1307 .querySelectorAll('.vbo-widget-notifscenter-group-name');
1308
1309 groups.forEach((group) => {
1310 let group_id = group.getAttribute('data-group-id');
1311 let group_event = 'vbo-badge-count' + (group_id ? '-' + group_id : '');
1312
1313 /**
1314 * Register the listener with a precise handler so that it can be removed upon destruction,
1315 * unlike the global event "vbo-badge-count" for the in-menu Notifications Center handler.
1316 */
1317 document.addEventListener(group_event, vboWidgetNotifsCenterUpdateGroupBadge);
1318
1319 // push event name for later destruction
1320 vbo_widget_nc_badge_evs.push(group_event);
1321 });
1322 }
1323
1324 /**
1325 * Update group badge counter event handler.
1326 */
1327 function vboWidgetNotifsCenterUpdateGroupBadge(e) {
1328 if (!e || !e.detail || !e.detail.hasOwnProperty('badge_count') || isNaN(e.detail['badge_count'])) {
1329 return;
1330 }
1331
1332 // get the current counter
1333 let badge_count = parseInt(e.detail['badge_count']);
1334
1335 // get the event type to identify the group ID
1336 let group_nm_rgx = new RegExp(/^vbo-badge-count-?/);
1337 let group_id = e.type.replace(group_nm_rgx, '');
1338
1339 // parse all group badges of this type-ID in the document
1340 var groups = document
1341 .querySelectorAll('.vbo-widget-notifscenter-group-name[data-group-id="' + group_id + '"]');
1342
1343 groups.forEach((group) => {
1344 // find the child node for better supporting large numbers
1345 let group_badge_element = group.querySelector('.vbo-widget-notifscenter-group-badge');
1346
1347 if (badge_count <= 0) {
1348 // no notifications to be read
1349 group.setAttribute('data-badge-count', '');
1350
1351 // delete the badge element node, if any
1352 if (group_badge_element) {
1353 group_badge_element.remove();
1354 }
1355 } else {
1356 // update badge counter
1357 group.setAttribute('data-badge-count', badge_count);
1358
1359 // append or update badge element node
1360 if (group_badge_element) {
1361 // update badge element node
1362 group_badge_element.innerText = badge_count;
1363 } else {
1364 // append badge element node
1365 let group_badge_node = document.createElement('span');
1366 group_badge_node.className = 'vbo-widget-notifscenter-group-badge';
1367 group_badge_node.innerText = badge_count;
1368 group.appendChild(group_badge_node);
1369 }
1370 }
1371 });
1372 }
1373
1374 /**
1375 * Removes the group badge update event listeners.
1376 */
1377 function vboWidgetNotifsCenterRemoveGroupBadgeListeners(e) {
1378 e.stopPropagation();
1379
1380 // remove event listener so that it will be re-registered
1381 document.removeEventListener(<?php echo $js_modal_id ? "VBOCore.widget_modal_dismissed + '{$js_modal_id}'" : "'vbo_widget_nc_destroy'"; ?>, vboWidgetNotifsCenterRemoveGroupBadgeListeners);
1382
1383 if (vbo_widget_nc_badge_evs && Array.isArray(vbo_widget_nc_badge_evs)) {
1384 vbo_widget_nc_badge_evs.forEach((ev_name, index) => {
1385 // remove event listener for updating the group badge
1386 document.removeEventListener(ev_name, vboWidgetNotifsCenterUpdateGroupBadge);
1387
1388 // remove the current element from the array
1389 vbo_widget_nc_badge_evs.splice(index, 1);
1390 });
1391 }
1392 }
1393
1394 /**
1395 * Register event listener that runs upon widget destruction.
1396 */
1397 document.addEventListener(<?php echo $js_modal_id ? "VBOCore.widget_modal_dismissed + '{$js_modal_id}'" : "'vbo_widget_nc_destroy'"; ?>, vboWidgetNotifsCenterRemoveGroupBadgeListeners);
1398
1399 </script>
1400 <?php
1401 }
1402 ?>
1403
1404 <script type="text/javascript">
1405
1406 jQuery(function() {
1407
1408 // listen to the click event on the group names
1409 jQuery('#<?php echo $wrapper_id; ?>').find('.vbo-widget-notifscenter-group').on('click', function() {
1410 // get clicked group identifier
1411 var group_id = jQuery(this)
1412 .find('.vbo-widget-notifscenter-group-name')
1413 .attr('data-group-id');
1414
1415 // load the notifications for the clicked group, even if it's active
1416 vboWidgetNotifsCenterLoadGroupNotifs('<?php echo $wrapper_id; ?>', group_id);
1417 });
1418
1419 // listen to the "mark all as read" command
1420 jQuery('#<?php echo $wrapper_id; ?>').find('.vbo-widget-notifscenter-read-all').on('click', function() {
1421 vboWidgetNotifsCenterReadAllNotifs('<?php echo $wrapper_id; ?>');
1422 });
1423
1424 // listen to the manual load-more button
1425 jQuery('#<?php echo $wrapper_id; ?>').find('.vbo-widget-notifscenter-loadmore-manual').on('click', function() {
1426 // load the next page of notifications
1427 vboWidgetNotifsCenterLoadNextPage('<?php echo $wrapper_id; ?>');
1428 });
1429
1430 // set up infinite scroll loading
1431 vboWidgetNotifsCenterSetupInfiniteScroll('<?php echo $wrapper_id; ?>');
1432
1433 // set up notifications click listeners
1434 vboWidgetNotifsCenterRegisterClickListeners('<?php echo $wrapper_id; ?>');
1435
1436 // set up group badge update listeners
1437 vboWidgetNotifsCenterRegisterGroupBadgeListeners('<?php echo $wrapper_id; ?>');
1438
1439 // render datepicker calendars for date filters
1440 jQuery('#<?php echo $wrapper_id; ?>').find('.vbo-notifscenter-dtpicker-from, .vbo-notifscenter-dtpicker-to').datepicker({
1441 minDate: "<?php echo date('Y-m-d', $mindate); ?>",
1442 maxDate: "<?php echo date('Y-m-d', $maxdate); ?>",
1443 yearRange: "<?php echo date('Y', $mindate); ?>:<?php echo date('Y', $maxdate); ?>",
1444 changeMonth: true,
1445 changeYear: true,
1446 dateFormat: "yy-mm-dd",
1447 onSelect: vboWidgetNotifsCenterCheckDates
1448 });
1449
1450 // triggering for datepicker calendar icons
1451 jQuery('i.vbo-widget-notifscenter-caltrigger').click(function() {
1452 var jdp = jQuery(this).parent().find('input.hasDatepicker');
1453 if (jdp.length) {
1454 jdp.focus();
1455 }
1456 });
1457
1458 if (<?php echo $suggest_push ? 'true' : 'false'; ?>) {
1459 // suggest to subscribe to push notifications
1460 VBOCore.suggestNotifications('.vbo-widget-notifscenter-suggest-push-btn');
1461 }
1462
1463 });
1464
1465 </script>
1466
1467 <?php
1468 }
1469
1470 /**
1471 * Given a list of notification objects, builds and returns the HTML rendering code.
1472 *
1473 * @param array $notifications list of notification objects to render.
1474 * @param int $page_num optional page number.
1475 *
1476 * @return string
1477 */
1478 protected function buildNotificationsHTML(array $notifications, int $page_num = 0)
1479 {
1480 if (!$notifications) {
1481 return '';
1482 }
1483
1484 // get back-end logo URI
1485 $backlogo = VikBooking::getBackendLogo();
1486
1487 // start output buffering
1488 ob_start();
1489
1490 foreach ($notifications as $index => $notif) {
1491 // obtain notification date info
1492 $date_info = VBORemindersHelper::getInstance()->relativeDatesDiff(JHtml::fetch('date', $notif->createdon, 'Y-m-d H:i:s'));
1493
1494 // build relative date
1495 $relative_dt = $date_info['relative'];
1496 if ($date_info['past'] && ($date_info['today'] || ($date_info['yesterday'] && !$date_info['days']))) {
1497 // recent date to be expressed as hours, minutes or seconds ago
1498 if ($date_info['hours']) {
1499 $rel_time_str = $date_info['hours'] . ' ' . JText::translate(($date_info['hours'] == 1 ? 'VBO_HOUR' : 'VBCONFIGONETENEIGHT'));
1500 } elseif ($date_info['minutes']) {
1501 $rel_time_str = $date_info['minutes'] . ' ' . JText::translate(($date_info['minutes'] == 1 ? 'VBO_MINUTE' : 'VBTRKDIFFMINS'));
1502 } else {
1503 $rel_time_str = $date_info['seconds'] . ' ' . JText::translate(($date_info['seconds'] == 1 ? 'VBO_SECOND' : 'VBTRKDIFFSECS'));
1504 }
1505 $relative_dt = JText::sprintf('VBO_REL_EXP_PAST', strtolower($rel_time_str));
1506 } elseif ($date_info['past'] && $date_info['yesterday'] && $date_info['days'] == 1 && $date_info['hours'] < 6) {
1507 // less than 30 hours ago
1508 $rel_time_str = ($date_info['hours'] + 24) . ' ' . JText::translate('VBCONFIGONETENEIGHT');
1509 $relative_dt = JText::sprintf('VBO_REL_EXP_PAST', strtolower($rel_time_str));
1510 }
1511
1512 // build a human-readable date and time
1513 if ($date_info['today']) {
1514 $human_dtime = JHtml::fetch('date', $notif->createdon, 'H:i:s');
1515 } else {
1516 $human_dtime = JHtml::fetch('date', $notif->createdon, str_replace("/", $this->datesep, $this->df) . ' H:i:s');
1517 }
1518
1519 // get channel logo and name
1520 $channel_logo = '';
1521 $channel_name = '';
1522 if (!empty($notif->channel)) {
1523 $ch_logo_obj = VikBooking::getVcmChannelsLogo($notif->channel, true);
1524 $channel_logo = is_object($ch_logo_obj) ? $ch_logo_obj->getSmallLogoURL() : '';
1525 $channelparts = explode('_', $notif->channel);
1526 $channel_name = isset($channelparts[1]) && strlen((string)$channelparts[1]) ? $channelparts[1] : ucwords($channelparts[0]);
1527 }
1528
1529 // check if the notification group belongs to specific types
1530 $group_website = !strcasecmp($notif->group, 'website');
1531 $group_guests = !strcasecmp($notif->group, 'guests');
1532
1533 // determine the notification group icon and color
1534 $group_badge_icon = '';
1535 $group_badge_cnt = '';
1536 $group_badge_cls = '';
1537 if (!strcasecmp($notif->group, 'website')) {
1538 $group_badge_icon = 'clipboard-list';
1539 $group_badge_cls = 'vbo-badge-group-green';
1540 if (in_array($notif->type, ['p0', 'pn'])) {
1541 $group_badge_icon = 'credit-card';
1542 } elseif ($notif->type == 'info') {
1543 $group_badge_icon = 'bullhorn';
1544 } elseif ($notif->type == 'chat.newmessage') {
1545 $group_badge_icon = 'comment-dots';
1546 }
1547 if (in_array($notif->type, ['cr', 'cw', 'ob'])) {
1548 $group_badge_cls = 'vbo-badge-group-orange';
1549 }
1550 } elseif (!strcasecmp($notif->group, 'otas')) {
1551 $group_badge_icon = 'cloud';
1552 $group_badge_cls = 'vbo-badge-group-purple';
1553 if (in_array($notif->type, ['po', 'vcc_balance', 'vcc_balance_updated'])) {
1554 $group_badge_icon = 'credit-card';
1555 }
1556 if (in_array($notif->type, ['cc', 'ob'])) {
1557 $group_badge_cls = 'vbo-badge-group-orange';
1558 }
1559 } elseif (!strcasecmp($notif->group, 'guests')) {
1560 $group_badge_icon = 'comment-dots';
1561 $group_badge_cls = 'vbo-badge-group-lightblue';
1562 } elseif (!strcasecmp($notif->group, 'cm')) {
1563 $group_badge_icon = 'bullhorn';
1564 $group_badge_cls = 'vbo-badge-group-lightblue';
1565 } elseif (!strcasecmp($notif->group, 'operators')) {
1566 $group_badge_icon = 'user-tie';
1567 $group_badge_cls = 'vbo-badge-group-orange';
1568 if (in_array($notif->type, ['chat.newmessage'])) {
1569 $group_badge_icon = 'comment-dots';
1570 }
1571 if ($notif->type == 'task.unassigned') {
1572 $group_badge_icon = 'tasks';
1573 $group_badge_cls = 'vbo-badge-group-red';
1574 }
1575 } elseif (!strcasecmp($notif->group, 'reports')) {
1576 $group_badge_icon = 'cash-register';
1577 if (strpos($notif->type, 'error') !== false) {
1578 $group_badge_cls = 'vbo-badge-group-red';
1579 } else {
1580 $group_badge_cls = 'vbo-badge-group-green';
1581 }
1582 } elseif (!strcasecmp($notif->group, 'ai')) {
1583 $group_badge_cnt = '<img class="vbo-ai-icn" src="' . VBO_ADMIN_URI . 'resources/channels/ai-icn-white.png" />';
1584 if (strpos($notif->type, 'error') !== false) {
1585 $group_badge_cls = 'vbo-badge-group-red';
1586 } elseif ($notif->type == 'training.drafts') {
1587 $group_badge_cls = 'vbo-badge-group-orange';
1588 } else {
1589 $group_badge_cls = 'vbo-badge-group-green';
1590 }
1591 } elseif (!strcasecmp($notif->group, 'dac')) {
1592 $group_badge_icon = 'fingerprint';
1593 $group_badge_cls = 'vbo-badge-group-green';
1594 if (strpos((string) $notif->type, '.nok') !== false) {
1595 $group_badge_cls = 'vbo-badge-group-red';
1596 }
1597 } elseif (!strcasecmp($notif->group, 'webhook')) {
1598 $group_badge_icon = 'satellite-dish';
1599 $group_badge_cls = 'vbo-badge-group-lightblue';
1600 if (strpos((string) $notif->type, '.nok') !== false || strpos((string) $notif->type, 'error') !== false) {
1601 $group_badge_cls = 'vbo-badge-group-red';
1602 }
1603 if (strpos($notif->type, 'mfa') !== false) {
1604 $group_badge_icon = 'shield';
1605 $group_badge_cls = 'vbo-badge-group-orange';
1606 }
1607 }
1608
1609 ?>
1610 <div
1611 class="vbo-widget-notifscenter-notif-wrap vbo-widget-notifscenter-notif-<?php echo $notif->read ? 'read' : 'unread'; ?>"
1612 data-notif-id="<?php echo $notif->id; ?>"
1613 data-idorder="<?php echo $notif->idorder; ?>"
1614 data-idorderota="<?php echo $notif->idorderota; ?>"
1615 >
1616 <div class="vbo-widget-notifscenter-notif-avatar">
1617 <div class="vbo-customer-info-box">
1618 <div class="vbo-customer-info-box-avatar vbo-customer-avatar-medium">
1619 <?php
1620 if ($group_guests && !empty($notif->avatar)) {
1621 // use notification avatar to support co-hosts, co-traveler and other chat users
1622 ?>
1623 <span class="vbo-widget-notifscenter-cpic-zoom">
1624 <img src="<?php echo strpos($notif->avatar, 'http') === 0 ? $notif->avatar : JUri::root() . $notif->avatar; ?>" decoding="async" loading="lazy" />
1625 </span>
1626 <?php
1627 } elseif (!empty($notif->customer_pic)) {
1628 // use customer profile picture
1629 ?>
1630 <span class="vbo-widget-notifscenter-cpic-zoom">
1631 <img src="<?php echo strpos($notif->customer_pic, 'http') === 0 ? $notif->customer_pic : VBO_SITE_URI . 'resources/uploads/' . $notif->customer_pic; ?>" data-caption="<?php echo JHtml::fetch('esc_attr', (string) $notif->customer_name); ?>" decoding="async" loading="lazy" />
1632 </span>
1633 <?php
1634 } elseif (!empty($notif->avatar)) {
1635 // use notification avatar
1636 ?>
1637 <span class="vbo-widget-notifscenter-cpic-zoom">
1638 <img src="<?php echo strpos($notif->avatar, 'http') === 0 ? $notif->avatar : JUri::root() . $notif->avatar; ?>" decoding="async" loading="lazy" />
1639 </span>
1640 <?php
1641 } elseif (!empty($channel_logo)) {
1642 // use channel logo
1643 ?>
1644 <span class="vbo-tooltip vbo-tooltip-top" data-tooltiptext="<?php echo JHtml::fetch('esc_attr', $channel_name); ?>">
1645 <img src="<?php echo $channel_logo; ?>" />
1646 </span>
1647 <?php
1648 } elseif (!empty($notif->customer_name)) {
1649 // use customer initials
1650 ?>
1651 <span>
1652 <span class="vbo-widget-notifscenter-customer-initials"><?php echo $this->getCustomerInitials($notif->customer_name); ?></span>
1653 </span>
1654 <?php
1655 } elseif (!empty($backlogo)) {
1656 // use back-end logo
1657 ?>
1658 <span>
1659 <img src="<?php echo VBO_ADMIN_URI . "resources/{$backlogo}"; ?>" />
1660 </span>
1661 <?php
1662 } else {
1663 // fallback onto website icon
1664 ?>
1665 <span><?php VikBookingIcons::e('hotel', 'vbo-dashboard-guest-activity-avatar-icon'); ?></span>
1666 <?php
1667 }
1668
1669 // take care of the group badge icon and color, if any
1670 if ($group_badge_icon) {
1671 ?>
1672 <span class="vbo-customer-avatar-badge <?php echo $group_badge_cls; ?>">
1673 <?php VikBookingIcons::e($group_badge_icon); ?>
1674 </span>
1675 <?php
1676 } elseif ($group_badge_cnt) {
1677 ?>
1678 <span class="vbo-customer-avatar-badge <?php echo $group_badge_cls; ?>">
1679 <?php echo $group_badge_cnt; ?>
1680 </span>
1681 <?php
1682 }
1683 ?>
1684 </div>
1685 </div>
1686 </div>
1687 <div class="vbo-widget-notifscenter-notif-details">
1688 <?php
1689 if ($notif->title) {
1690 if (preg_match("/^VBO/", $notif->title)) {
1691 /**
1692 * @todo to be removed on next updates.
1693 */
1694 $notif->title = JText::translate($notif->title);
1695 }
1696 ?>
1697 <div class="vbo-widget-notifscenter-notif-head">
1698 <span class="vbo-widget-notifscenter-notif-title"><?php echo $notif->title; ?></span>
1699 <?php
1700 if (!$group_guests && $notif->customer_name) {
1701 ?>
1702 <span class="vbo-widget-notifscenter-notif-subtitle">&bull; <?php echo $notif->customer_name; ?></span>
1703 <?php
1704 }
1705 ?>
1706 </div>
1707 <?php
1708 }
1709 ?>
1710 <div class="vbo-widget-notifscenter-notif-dt">
1711 <?php
1712 if ($notif->idorder || $notif->idorderota) {
1713 ?>
1714 <span class="label label-info"><?php echo $notif->idorder ?: $notif->idorderota; ?></span>
1715 <?php
1716 }
1717 ?>
1718 <span class="vbo-tooltip vbo-tooltip-<?php echo !$index && !$page_num ? 'bottom' : 'top'; ?>" data-tooltiptext="<?php echo JHtml::fetch('esc_attr', $human_dtime); ?>"><?php echo $relative_dt; ?></span>
1719 </div>
1720 <?php
1721 if ($notif->summary && (!$group_website || !strcasecmp($notif->type, 'info') || strpos($notif->type, 'chat') === 0)) {
1722 ?>
1723 <div class="vbo-widget-notifscenter-notif-summary" data-group-name="<?php echo $notif->group; ?>" data-notif-type="<?php echo $notif->type; ?>">
1724 <span><?php echo htmlspecialchars($notif->summary); ?></span>
1725 </div>
1726 <?php
1727 }
1728 if (is_object($notif->cta_data) && (!empty($notif->cta_data->url) || !empty($notif->cta_data->widget))) {
1729 // call-to-action data available
1730 $cta_btn_lbl = !empty($notif->cta_data->label) ? $notif->cta_data->label : JText::translate('VBO_TAKE_ACTION');
1731 ?>
1732 <div class="vbo-widget-notifscenter-notif-cta">
1733 <?php
1734 if (!empty($notif->cta_data->url)) {
1735 // open "remote" URL
1736 ?>
1737 <a class="btn btn-small vbo-notifscenter-cta-btn" href="<?php echo JHtml::fetch('esc_attr', $notif->cta_data->url); ?>" target="_blank"><?php echo $cta_btn_lbl; ?></a>
1738 <?php
1739 } elseif (!empty($notif->cta_data->widget)) {
1740 // open a specific admin-widget with the necessary options
1741 $def_widget_options = [
1742 'modal_options' => [
1743 'suffix' => 'widget_modal_inner_' . $notif->cta_data->widget,
1744 ],
1745 ];
1746 if (is_object($notif->cta_data->widget_options) && !isset($notif->cta_data->widget_options->modal_options)) {
1747 // inject the suffix property
1748 $notif->cta_data->widget_options->modal_options = new stdClass;
1749 $notif->cta_data->widget_options->modal_options->suffix = 'widget_modal_inner_' . $notif->cta_data->widget;
1750 }
1751 $js_options = !empty($notif->cta_data->widget_options) ? json_encode($notif->cta_data->widget_options) : json_encode($def_widget_options);
1752 $js_command = 'VBOCore.handleDisplayWidgetNotification({widget_id: "' . $notif->cta_data->widget . '"}, ' . $js_options . ');';
1753 ?>
1754 <button type="button" class="btn btn-small vbo-notifscenter-cta-btn" onclick="<?php echo JHtml::fetch('esc_attr', $js_command); ?>"><?php echo $cta_btn_lbl; ?></button>
1755 <?php
1756 }
1757 ?>
1758 </div>
1759 <?php
1760 }
1761 ?>
1762 </div>
1763 </div>
1764 <?php
1765 }
1766
1767 // get the HTML buffer
1768 $output = ob_get_contents();
1769 ob_end_clean();
1770
1771 return $output;
1772 }
1773
1774 /**
1775 * Returns the initials for the given full name.
1776 *
1777 * @param string $name the full name (first + last).
1778 *
1779 * @return string the capitalized name initials.
1780 */
1781 protected function getCustomerInitials(string $name)
1782 {
1783 $parts = explode(' ', trim($name));
1784
1785 $first = $parts[0];
1786 $last = end($parts);
1787
1788 if (function_exists('mb_substr')) {
1789 if ($first != $last) {
1790 return strtoupper(mb_substr($first, 0, 1, 'UTF-8') . mb_substr($last, 0, 1, 'UTF-8'));
1791 }
1792
1793 return strtoupper(mb_substr($first, 0, 2, 'UTF-8'));
1794 }
1795
1796 if ($first != $last) {
1797 return strtoupper(substr($first, 0, 1) . substr($last, 0, 1));
1798 }
1799
1800 return strtoupper(substr($first, 0, 2));
1801 }
1802
1803 /**
1804 * Returns an array with the minimum and maximum dates with notifications.
1805 *
1806 * @return array to be used with list() to get the min/max notification date timestamps.
1807 */
1808 protected function getMinDatesNotifications()
1809 {
1810 $dbo = JFactory::getDbo();
1811
1812 $mindate = null;
1813 $maxdate = null;
1814
1815 $dbo->setQuery(
1816 $dbo->getQuery(true)
1817 ->select('MIN(' . $dbo->qn('createdon') . ') AS ' . $dbo->qn('mindate'))
1818 ->select('MAX(' . $dbo->qn('createdon') . ') AS ' . $dbo->qn('maxdate'))
1819 ->from($dbo->qn('#__vikbooking_notifications'))
1820 );
1821
1822 $info_dates = $dbo->loadObject();
1823
1824 if ($info_dates && !empty($info_dates->mindate)) {
1825 $mindate = strtotime(JHtml::fetch('date', $info_dates->mindate, 'Y-m-d'));
1826 $maxdate = strtotime(JHtml::fetch('date', $info_dates->maxdate, 'Y-m-d'));
1827 }
1828
1829 return [$mindate, $maxdate];
1830 }
1831 }
1832