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 / bulk_messaging.php

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

1,271 lines 45.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package VikBooking
4 * @subpackage com_vikbooking
5 * @author Alessio Gaggii - e4j - Extensionsforjoomla.com
6 * @copyright Copyright (C) 2018 e4j - Extensionsforjoomla.com. All rights reserved.
7 * @license GNU General Public License version 2 or later; see LICENSE
8 * @link https://vikwp.com
9 */
10
11 defined('ABSPATH') or die('No script kiddies please!');
12
13 /**
14 * Class handler for admin widget "bulk messaging".
15 *
16 * @since 1.16.7 (J) - 1.6.7 (WP)
17 */
18 class VikBookingAdminWidgetBulkMessaging extends VikBookingAdminWidget
19 {
20 /**
21 * The instance counter of this widget. Since we do not load individual parameters
22 * for each widget's instance, we use a static counter to determine its settings.
23 *
24 * @var int
25 */
26 protected static $instance_counter = -1;
27
28 /**
29 * Class constructor will define the widget name and identifier.
30 */
31 public function __construct()
32 {
33 // call parent constructor
34 parent::__construct();
35
36 $this->widgetName = JText::translate('VBO_W_BULKMESSAGING_TITLE');
37 $this->widgetDescr = JText::translate('VBO_W_BULKMESSAGING_DESCR');
38 $this->widgetId = basename(__FILE__, '.php');
39
40 // define widget and icon and style name
41 $this->widgetIcon = '<i class="' . VikBookingIcons::i('bullhorn') . '"></i>';
42 $this->widgetStyleName = 'yellow';
43
44 // load widget's settings
45 $this->widgetSettings = $this->loadSettings();
46 if (!is_object($this->widgetSettings)) {
47 $this->widgetSettings = new stdClass;
48 }
49 }
50
51 /**
52 * Custom method for this widget only to load the reservations.
53 * The method is called by the admin controller through an AJAX request.
54 * The visibility should be public, it should not exit the process, and
55 * any content sent to output will be returned to the AJAX response.
56 * In this case we return an array because this method requires "return":1.
57 */
58 public function loadBookings()
59 {
60 // get today's date
61 $today_ymd = date('Y-m-d');
62
63 $wrapper = VikRequest::getString('wrapper', '', 'request');
64 $type = VikRequest::getString('type', 'stayover', 'request');
65 $from_dt = VikRequest::getString('from_dt', $today_ymd, 'request');
66 $to_dt = VikRequest::getString('to_dt', '', 'request') ?: $from_dt;
67
68 if (empty($from_dt)) {
69 VBOHttpDocument::getInstance()->close(500, JText::translate('VBO_PLEASE_FILL_FIELDS'));
70 }
71
72 // get date timestamps
73 $from_ts = VikBooking::getDateTimestamp($from_dt, 0, 0);
74 $to_ts = VikBooking::getDateTimestamp($to_dt, 23, 59, 59);
75 $from_ts_end = VikBooking::getDateTimestamp($from_dt, 23, 59, 59);
76
77 // query the db
78 $dbo = JFactory::getDbo();
79
80 $q = $dbo->getQuery(true)
81 ->select($dbo->qn('o') . '.*')
82 ->select($dbo->qn('co.idcustomer'))
83 ->select('CONCAT_WS(" ", ' . $dbo->qn('c.first_name') . ', ' . $dbo->qn('c.last_name') . ') AS ' . $dbo->qn('customer_fullname'))
84 ->select($dbo->qn('c.country', 'customer_country'))
85 ->select($dbo->qn('c.pic'))
86 ->from($dbo->qn('#__vikbooking_orders', 'o'))
87 ->leftJoin($dbo->qn('#__vikbooking_customers_orders', 'co') . ' ON ' . $dbo->qn('co.idorder') . ' = ' . $dbo->qn('o.id'))
88 ->leftJoin($dbo->qn('#__vikbooking_customers', 'c') . ' ON ' . $dbo->qn('c.id') . ' = ' . $dbo->qn('co.idcustomer'))
89 ->where($dbo->qn('o.closure') . ' = 0');
90
91 if ($type == 'arrival') {
92 $q->where($dbo->qn('o.checkin') . ' >= ' . $from_ts);
93 $q->where($dbo->qn('o.checkin') . ' <= ' . $to_ts);
94 } elseif ($type == 'departure') {
95 $q->where($dbo->qn('o.checkout') . ' >= ' . $from_ts);
96 $q->where($dbo->qn('o.checkout') . ' <= ' . $to_ts);
97 } elseif ($type == 'bookdate') {
98 $q->where($dbo->qn('o.ts') . ' >= ' . $from_ts);
99 $q->where($dbo->qn('o.ts') . ' <= ' . $to_ts);
100 } else {
101 // stayover
102 $q->where($dbo->qn('o.checkin') . ' < ' . $from_ts_end);
103 $q->where($dbo->qn('o.checkout') . ' > ' . $to_ts);
104 }
105
106 $dbo->setQuery($q);
107 $bookings = $dbo->loadAssocList();
108
109 // total checkboxes checked
110 $tot_checked = 0;
111
112 // first booking ID "checked"
113 $first_checked_bid = 0;
114 $widget_id = $this->widgetId;
115
116 // start output buffering
117 ob_start();
118
119 if (!$bookings) {
120 ?>
121 <p class="info"><?php echo JText::translate('VBNOORDERSFOUND'); ?></p>
122 <?php
123 } else {
124 // display all bookings of this day
125 foreach ($bookings as $ind => $booking) {
126 // get channel logo and other details
127 $ch_logo_obj = VikBooking::getVcmChannelsLogo($booking['channel'], true);
128 $channel_logo = is_object($ch_logo_obj) ? $ch_logo_obj->getSmallLogoURL() : '';
129 $nights_lbl = $booking['days'] > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY');
130 $rooms_lbl = !empty($booking['roomsnum']) && $booking['roomsnum'] > 1 ? ', ' . $booking['roomsnum'] . ' ' . JText::translate('VBPVIEWORDERSTHREE') : '';
131
132 // compose customer name
133 $customer_name = !empty($booking['customer_fullname']) ? $booking['customer_fullname'] : '';
134 if ($booking['closure'] > 0 || !strcasecmp($booking['custdata'], JText::translate('VBDBTEXTROOMCLOSED'))) {
135 $customer_name = '<span class="vbordersroomclosed"><i class="' . VikBookingIcons::i('ban') . '"></i> ' . JText::translate('VBDBTEXTROOMCLOSED') . '</span>';
136 }
137 if (empty($customer_name)) {
138 $customer_name = VikBooking::getFirstCustDataField($booking['custdata']);
139 }
140
141 // customer country flag
142 $customer_country = '';
143 $customer_cflag = '';
144 if (!empty($booking['customer_country'])) {
145 $customer_country = $booking['customer_country'];
146 } elseif (!empty($booking['country'])) {
147 $customer_country = $booking['country'];
148 }
149 if ($customer_country && is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . 'countries' . DIRECTORY_SEPARATOR . $customer_country . '.png')) {
150 $customer_cflag = '<img src="'.VBO_ADMIN_URI.'resources/countries/' . $customer_country . '.png'.'" title="' . htmlspecialchars($customer_country) . '" class="vbo-country-flag vbo-country-flag-left"/>';
151 }
152
153 // check for any previous event triggered by this widget for the current reservation
154 $last_notified = null;
155 $history_obj = VikBooking::getBookingHistoryInstance($booking['id']);
156 $prev_ev_data = $history_obj->getEventsWithData('CE', function($data) use ($widget_id) {
157 return (is_object($data) && !empty($data->widget) && $data->widget == $widget_id);
158 }, $onlydata = false);
159 if (is_array($prev_ev_data) && $prev_ev_data) {
160 $last_notified = $prev_ev_data[0]['dt'];
161 }
162
163 // default checked status
164 $booking_is_checked = ($booking['status'] == 'confirmed' && !$last_notified);
165 if ($booking_is_checked) {
166 $tot_checked++;
167 if (!$first_checked_bid) {
168 $first_checked_bid = $booking['id'];
169 }
170 }
171
172 ?>
173 <div class="vbo-dashboard-guest-activity vbo-widget-bulkmess-reservation" data-type="<?php echo $booking['status']; ?>" data-resid="<?php echo $booking['id']; ?>">
174 <div class="vbo-widget-bulkmess-ckbox">
175 <input type="checkbox" value="<?php echo $booking['id']; ?>" <?php echo $booking_is_checked ? 'checked ' : ''; ?>/>
176 </div>
177 <div class="vbo-dashboard-guest-activity-avatar">
178 <?php
179 if (!empty($channel_logo)) {
180 // channel logo has got the highest priority
181 ?>
182 <img class="vbo-dashboard-guest-activity-avatar-profile" src="<?php echo $channel_logo; ?>" />
183 <?php
184 } elseif (!empty($booking['pic'])) {
185 // customer profile picture
186 ?>
187 <img class="vbo-dashboard-guest-activity-avatar-profile" src="<?php echo strpos($booking['pic'], 'http') === 0 ? $booking['pic'] : VBO_SITE_URI . 'resources/uploads/' . $booking['pic']; ?>" />
188 <?php
189 } else {
190 // we use an icon as fallback
191 VikBookingIcons::e('hotel', 'vbo-dashboard-guest-activity-avatar-icon');
192 }
193 ?>
194 </div>
195 <div class="vbo-dashboard-guest-activity-content">
196 <div class="vbo-dashboard-guest-activity-content-head">
197 <div class="vbo-dashboard-guest-activity-content-info-details">
198 <h4><?php echo $customer_name . $customer_cflag; ?></h4>
199 <div class="vbo-dashboard-guest-activity-content-info-icon">
200 <?php
201 if ($booking['status'] == 'cancelled') {
202 ?>
203 <span class="badge badge-danger"><?php echo JText::translate('VBCANCELLED'); ?></span>
204 <?php
205 } elseif ($booking['status'] == 'standby') {
206 ?>
207 <span class="badge badge-warning"><?php echo JText::translate('VBSTANDBY'); ?></span>
208 <?php
209 }
210 ?>
211 <span><?php VikBookingIcons::e('plane-arrival'); ?> <?php echo date(str_replace("/", $this->datesep, $this->df), $booking['checkin']); ?> - <?php echo $booking['days'] . ' ' . $nights_lbl . $rooms_lbl; ?></span>
212 <?php
213 if ($last_notified) {
214 ?>
215 <span class="vbo-widget-bulkmess-notified vbo-tooltip vbo-tooltip-top" data-tooltiptext="<?php echo JHtml::fetch('esc_attr', JText::translate('VBOBOOKHISTORYTCE')); ?>"><?php VikBookingIcons::e('envelope'); ?> <?php echo JHtml::fetch('date', $hist['dt'], 'Y-m-d H:i:s'); ?></span>
216 <?php
217 }
218 ?>
219 </div>
220 </div>
221 <div class="vbo-dashboard-guest-activity-content-info-date">
222 <span class="vbo-widget-bulkmess-openbook" onclick="vboWidgetBulkMessOpenBooking('<?php echo $booking['id']; ?>');">
223 <span class="label label-info"><?php VikBookingIcons::e('eye'); ?> <?php echo $booking['id']; ?></span>
224 </span>
225 <span><?php echo date(str_replace("/", $this->datesep, $this->df) . ' H:i', $booking['ts']); ?></span>
226 </div>
227 </div>
228 </div>
229 </div>
230 <?php
231 }
232 }
233
234 ?>
235 <script type="text/javascript">
236
237 jQuery(function() {
238
239 /**
240 * Register the first checked booking ID to let the mail preview work.
241 */
242 if (typeof window['vbo_current_bid'] === 'undefined') {
243 window['vbo_current_bid'] = '<?php echo $first_checked_bid; ?>';
244 }
245
246 /**
247 * Prepare the elements to toggle the checkbox on click.
248 */
249 jQuery('#<?php echo $wrapper; ?>').find('.vbo-widget-bulkmess-reservation').on('click', function(e) {
250 if (jQuery(e.target).hasClass('.vbo-widget-bulkmess-openbook') || jQuery(e.target).closest('.vbo-widget-bulkmess-openbook').length) {
251 return;
252 }
253
254 if (!jQuery(e.target).is('input[type="checkbox"]')) {
255 var ckbox = jQuery(this).find('input[type="checkbox"]');
256 if (ckbox.prop('checked')) {
257 ckbox.prop('checked', false);
258 } else {
259 ckbox.prop('checked', true);
260 }
261 }
262
263 // update total checked count
264 var checked_stats = vboWidgetBulkMessCountChecked('<?php echo $wrapper; ?>');
265
266 // update status on bookings step
267 jQuery('#<?php echo $wrapper; ?>').find('.vbo-widget-bulkmess-step-bookings').find('.vbo-widget-bulkmess-step-status').text(checked_stats['tot_checked'] + ' / ' + checked_stats['tot_bookings']);
268 });
269
270 });
271
272 </script>
273 <?php
274
275 // get the HTML buffer
276 $html_content = ob_get_contents();
277 ob_end_clean();
278
279 // return an associative array of values
280 return [
281 'html' => $html_content,
282 'tot_bookings' => count($bookings),
283 'tot_checked' => $tot_checked,
284 ];
285 }
286
287 /**
288 * Custom method for this widget only to send the communication message.
289 * The method is called by the admin controller through an AJAX request.
290 * The visibility should be public, it should not exit the process, and
291 * any content sent to output will be returned to the AJAX response.
292 * In this case we return an array because this method requires "return":1.
293 */
294 public function sendOneMessage()
295 {
296 $wrapper = VikRequest::getString('wrapper', '', 'request');
297 $bid = VikRequest::getInt('bid', 0, 'request');
298 $index = VikRequest::getInt('index', 0, 'request');
299 $message = VikRequest::getString('message', '', 'request', VIKREQUEST_ALLOWHTML);
300 $subject = VikRequest::getString('subject', '', 'request');
301
302 $booking = VikBooking::getBookingInfoFromID($bid);
303
304 if (!$booking) {
305 VBOHttpDocument::getInstance()->close(500, JText::translate('VBPEDITBUSYONE'));
306 }
307
308 if (empty($message)) {
309 VBOHttpDocument::getInstance()->close(500, JText::translate('VBO_PLEASE_FILL_FIELDS'));
310 }
311
312 // inject the customer information
313 $customer = VikBooking::getCPinInstance()->getCustomerFromBooking($booking['id']);
314 $booking['customer'] = $customer;
315
316 if ($index === 0) {
317 // update widget's settings with last message and subject
318 $this->widgetSettings->last_subject = $subject;
319 $this->widgetSettings->last_message = $message;
320 $this->updateSettings(json_encode($this->widgetSettings));
321 }
322
323 // determine the message dispatching method for this booking
324 $sending_method = 'eMail';
325
326 // check if support for OTA messaging is available
327 $ota_messaging_supported = class_exists('VCMChatMessaging');
328
329 if ($ota_messaging_supported && VCMChatMessaging::getInstance($booking)->supportsOtaMessaging($mandatory = true)) {
330 // set flag to identify an OTA messaging notification over the regular email
331 $sending_method = 'Message';
332 } elseif (preg_match("/^no-email-[^@]+@[a-z0-9\.]+\.com$/i", $booking['custmail'])) {
333 // false email address, typical of Airbnb, ignore reservation record
334 // this statement should be entered only if VCM is outdated, or the Guest Messaging API will be used
335 VBOHttpDocument::getInstance()->close(500, 'The booking ID ' . $booking['id'] . ' does not have a valid email address that can be notified.');
336 }
337
338 if ($sending_method === 'eMail' && empty($booking['custmail'])) {
339 VBOHttpDocument::getInstance()->close(500, 'The booking ID ' . $booking['id'] . ' is missing the guest email address.');
340 }
341
342 // language translation
343 $lang = JFactory::getLanguage();
344 $vbo_tn = VikBooking::getTranslator();
345 $vbo_tn::$force_tolang = null;
346 $website_def_lang = $vbo_tn->getDefaultLang();
347 if (!empty($booking['lang'])) {
348 if ($lang->getTag() != $booking['lang']) {
349 if (VBOPlatformDetection::isWordPress()) {
350 // wp
351 $lang->load('com_vikbooking', VIKBOOKING_SITE_LANG, $booking['lang'], true);
352 $lang->load('com_vikbooking', VIKBOOKING_ADMIN_LANG, $booking['lang'], true);
353 } else {
354 // J
355 $lang->load('com_vikbooking', JPATH_SITE, $booking['lang'], true);
356 $lang->load('com_vikbooking', JPATH_ADMINISTRATOR, $booking['lang'], true);
357 $lang->load('joomla', JPATH_SITE, $booking['lang'], true);
358 $lang->load('joomla', JPATH_ADMINISTRATOR, $booking['lang'], true);
359 }
360 }
361 if ($website_def_lang != $booking['lang']) {
362 // force the translation to start because contents should be translated
363 $vbo_tn::$force_tolang = $booking['lang'];
364 }
365 }
366
367 // dispatch the message to the guest reservation
368 if ($sending_method === 'eMail') {
369 // regular email notification
370 $message = $this->notifyThroughEmail($booking, $message, $subject);
371 } else {
372 // notification through guest messaging API
373 $message = $this->notifyThroughMessage($booking, $message);
374 }
375
376 // update history for this booking with the information about the communication sent
377 VikBooking::getBookingHistoryInstance($booking['id'])
378 ->setExtraData(['widget' => $this->widgetId])
379 ->store('CE', $message);
380
381 // return an associative array of values
382 return [
383 'result' => 1,
384 ];
385 }
386
387 /**
388 * Preload the necessary CSS/JS assets.
389 *
390 * @return void
391 */
392 public function preload()
393 {
394 // load assets
395 $this->vbo_app->loadDatePicker();
396
397 if (VBOPlatformDetection::isJoomla()) {
398 // load assets
399 $this->vbo_app->loadVisualEditorAssets();
400 } else {
401 // load lang defs
402 $this->vbo_app->loadVisualEditorDefinitions();
403 }
404
405 // JS lang defs
406 JText::script('VBO_PLEASE_SELECT');
407 JText::script('VBPVIEWORDERSPEOPLE');
408 JText::script('VBO_WANT_PROCEED');
409 JText::script('VBO_CONT_WRAPPER');
410 JText::script('VBO_CONT_WRAPPER_HELP');
411 }
412
413 /**
414 * Main method to invoke the widget. Contents will be loaded
415 * through AJAX requests, not via PHP when the page loads.
416 *
417 * @param ?VBOMultitaskData $data
418 *
419 * @return void
420 */
421 public function render(?VBOMultitaskData $data = null)
422 {
423 // increase widget's instance counter
424 static::$instance_counter++;
425
426 // check whether the widget is being rendered via AJAX when adding it through the customizer
427 $is_ajax = $this->isAjaxRendering();
428
429 // generate a unique ID for the sticky notes wrapper instance
430 $wrapper_instance = !$is_ajax ? static::$instance_counter : rand();
431 $wrapper_id = 'vbo-widget-bulkmess-' . $wrapper_instance;
432
433 // get permissions
434 $vbo_auth_bookings = JFactory::getUser()->authorise('core.vbo.bookings', 'com_vikbooking');
435 if (!$vbo_auth_bookings) {
436 // display nothing
437 return;
438 }
439
440 // date format
441 $dtpicker_df = $this->getDateFormat('jui');
442
443 // build the default message subject
444 $def_subject = !empty($this->widgetSettings->last_subject) ? $this->widgetSettings->last_subject : JText::sprintf('VBOMAILSUBJECT', VikBooking::getFrontTitle());
445
446 ?>
447 <div id="<?php echo $wrapper_id; ?>" class="vbo-admin-widget-wrapper" data-instance="<?php echo $wrapper_instance; ?>">
448 <div class="vbo-admin-widget-head">
449 <div class="vbo-admin-widget-head-inline">
450 <h4><?php echo $this->widgetIcon; ?> <span><?php echo $this->widgetName; ?></span></h4>
451 </div>
452 </div>
453 <div class="vbo-widget-bulkmess-wrap">
454 <div class="vbo-widget-bulkmess-steps">
455
456 <div class="vbo-widget-bulkmess-step vbo-widget-bulkmess-step-search">
457 <div class="vbo-widget-bulkmess-step-title" onclick="vboWidgetBulkMessToggleStep('<?php echo $wrapper_id; ?>', 'search');">
458 <h4><?php VikBookingIcons::e('calendar'); ?> <?php echo JText::translate('VBODASHSEARCHKEYS'); ?></h4>
459 <span class="vbo-widget-bulkmess-step-status"></span>
460 </div>
461 <div class="vbo-widget-bulkmess-step-content">
462 <div class="vbo-admin-container vbo-admin-container-full vbo-admin-container-compact">
463 <div class="vbo-params-wrap">
464 <div class="vbo-params-container">
465
466 <div class="vbo-param-container">
467 <div class="vbo-param-label"><?php echo JText::translate('VBNEWRESTRICTIONDFROMRANGE'); ?></div>
468 <div class="vbo-param-setting">
469 <div class="vbo-field-calendar">
470 <div class="input-append">
471 <input type="text" class="vbo-widget-bulkmess-fromdt" value="" autocomplete="off" />
472 <button type="button" class="btn btn-secondary vbo-widget-bulkmess-fromdt-trigger"><?php VikBookingIcons::e('calendar'); ?></button>
473 </div>
474 </div>
475 </div>
476 </div>
477
478 <div class="vbo-param-container">
479 <div class="vbo-param-label"><?php echo JText::translate('VBNEWRESTRICTIONDTORANGE'); ?></div>
480 <div class="vbo-param-setting">
481 <div class="vbo-field-calendar">
482 <div class="input-append">
483 <input type="text" class="vbo-widget-bulkmess-todt" value="" autocomplete="off" />
484 <button type="button" class="btn btn-secondary vbo-widget-bulkmess-todt-trigger"><?php VikBookingIcons::e('calendar'); ?></button>
485 </div>
486 </div>
487 </div>
488 </div>
489
490 <div class="vbo-param-container">
491 <div class="vbo-param-label"><?php echo JText::translate('VBPSHOWSEASONSTHREE'); ?></div>
492 <div class="vbo-param-setting">
493 <select class="vbo-widget-bulkmess-type">
494 <option value="stayover"><?php echo JText::translate('VBOTYPESTAYOVER'); ?></option>
495 <option value="arrival"><?php echo JText::translate('VBOTYPEARRIVAL'); ?></option>
496 <option value="departure"><?php echo JText::translate('VBOTYPEDEPARTURE'); ?></option>
497 <option value="bookdate"><?php echo JText::translate('VBPEDITBUSYTWO'); ?></option>
498 </select>
499 </div>
500 </div>
501
502 <div class="vbo-param-container vbo-param-confirm-btn">
503 <div class="vbo-param-label"></div>
504 <div class="vbo-param-setting">
505 <button type="button" class="btn btn-primary vbo-btn-wide vbo-widget-bulkmess-loadbtn" onclick="vboWidgetBulkMessLoadBookings('<?php echo $wrapper_id; ?>');"><span><?php echo JText::translate('VBJQCALNEXT'); ?></span> <?php VikBookingIcons::e('chevron-right'); ?></button>
506 </div>
507 </div>
508
509 </div>
510 </div>
511 </div>
512 </div>
513 </div>
514
515 <div class="vbo-widget-bulkmess-step vbo-widget-bulkmess-step-hidden vbo-widget-bulkmess-step-bookings">
516 <div class="vbo-widget-bulkmess-step-title" onclick="vboWidgetBulkMessToggleStep('<?php echo $wrapper_id; ?>', 'bookings');">
517 <h4><?php VikBookingIcons::e('users'); ?> <?php echo JText::translate('VBMENUCUSTOMERS'); ?></h4>
518 <span class="vbo-widget-bulkmess-step-status"></span>
519 </div>
520 <div class="vbo-widget-bulkmess-step-content">
521 <div class="vbo-widget-bulkmess-bookings-actions">
522 <button type="button" class="btn btn-small" onclick="vboWidgetBulkMessSelectAll('<?php echo $wrapper_id; ?>');"><?php echo JText::translate('VBINVSELECTALL'); ?></button>
523 </div>
524 <div class="vbo-dashboard-guests-latest vbo-widget-bulkmess-bookings-list">
525 <p class="info"><?php echo JText::translate('VBNOORDERSFOUND'); ?></p>
526 </div>
527 <div class="vbo-widget-bulkmess-gonext" style="display: none;">
528 <button type="button" class="btn btn-primary vbo-btn-wide" onclick="vboWidgetBulkMessGoNext('<?php echo $wrapper_id; ?>', 'message');"><span><?php echo JText::translate('VBJQCALNEXT'); ?></span> <?php VikBookingIcons::e('chevron-right'); ?></button>
529 </div>
530 </div>
531 </div>
532
533 <div class="vbo-widget-bulkmess-step vbo-widget-bulkmess-step-hidden vbo-widget-bulkmess-step-message">
534 <div class="vbo-widget-bulkmess-step-title" onclick="vboWidgetBulkMessToggleStep('<?php echo $wrapper_id; ?>', 'message');">
535 <h4><?php VikBookingIcons::e('envelope'); ?> <?php echo JText::translate('VBSENDEMAILCUSTCONT'); ?></h4>
536 <span class="vbo-widget-bulkmess-step-status"></span>
537 </div>
538 <div class="vbo-widget-bulkmess-step-content">
539 <div class="vbo-admin-container vbo-admin-container-full vbo-admin-container-compact">
540 <div class="vbo-params-wrap">
541 <div class="vbo-params-container">
542
543 <div class="vbo-param-container">
544 <div class="vbo-param-label"><?php echo JText::translate('VBSENDEMAILCUSTSUBJ'); ?></div>
545 <div class="vbo-param-setting">
546 <input type="text" class="vbo-widget-bulkmess-subject" value="<?php echo JHtml::fetch('esc_attr', $def_subject); ?>" autocomplete="off" />
547 </div>
548 </div>
549
550 </div>
551 </div>
552 </div>
553
554 <div class="vbo-widget-bulkmess-message">
555 <?php echo $this->renderEditor(); ?>
556 </div>
557 <div class="vbo-widget-bulkmess-gonext">
558 <button type="button" class="btn btn-primary vbo-btn-wide" onclick="vboWidgetBulkMessGoNext('<?php echo $wrapper_id; ?>', 'send');"><span><?php echo JText::translate('VBO_SEND'); ?></span> <?php VikBookingIcons::e('chevron-right'); ?></button>
559 </div>
560 </div>
561 </div>
562
563 <div class="vbo-widget-bulkmess-step vbo-widget-bulkmess-step-hidden vbo-widget-bulkmess-step-send">
564 <div class="vbo-widget-bulkmess-step-title" onclick="vboWidgetBulkMessToggleStep('<?php echo $wrapper_id; ?>', 'send');">
565 <h4><?php VikBookingIcons::e('rocket'); ?> <?php echo JText::translate('VBO_SEND_MESSAGES'); ?></h4>
566 <span class="vbo-widget-bulkmess-step-status"></span>
567 </div>
568 <div class="vbo-widget-bulkmess-step-content">
569 <div class="vbo-widget-bulkmess-progress">
570 <div class="vbo-widget-bulkmess-progress-inner">
571 <progress value="0" max="100">0 / 0</progress>
572 </div>
573 </div>
574 </div>
575 </div>
576
577 </div>
578 </div>
579 </div>
580 <?php
581
582 if (static::$instance_counter === 0 || $is_ajax) {
583 /**
584 * Print the JS code only once for all instances of this widget.
585 * The real rendering is made through AJAX, not when the page loads.
586 */
587 ?>
588
589 <script type="text/javascript">
590
591 var vbo_widget_bulkmess_icn_load = '<?php echo VikBookingIcons::i('spinner', 'fa-spin fa-fw'); ?>';
592 var vbo_widget_bulkmess_icn_next = '<?php echo VikBookingIcons::i('chevron-right'); ?>';
593
594 /**
595 * Perform the request to load the bookings.
596 */
597 function vboWidgetBulkMessLoadBookings(wrapper) {
598 var widget_instance = jQuery('#' + wrapper);
599 if (!widget_instance.length) {
600 return false;
601 }
602
603 // get vars for making the request
604 var from_dt = widget_instance.find('.vbo-widget-bulkmess-fromdt').val();
605 var to_dt = widget_instance.find('.vbo-widget-bulkmess-todt').val();
606 var type = widget_instance.find('.vbo-widget-bulkmess-type').val();
607 var typelbl = widget_instance.find('.vbo-widget-bulkmess-type').find('option:selected').text();
608 typelbl = typelbl ? typelbl : 'Stayover';
609
610 // set loading icon
611 widget_instance.find('.vbo-widget-bulkmess-step-search')
612 .find('.vbo-widget-bulkmess-loadbtn')
613 .prop('disabled', true)
614 .find('i')
615 .attr('class', vbo_widget_bulkmess_icn_load);
616
617 // the widget method to call
618 var call_method = 'loadBookings';
619
620 // make a request to load the bookings
621 VBOCore.doAjax(
622 "<?php echo $this->getExecWidgetAjaxUri(); ?>",
623 {
624 widget_id: "<?php echo $this->getIdentifier(); ?>",
625 call: call_method,
626 return: 1,
627 from_dt: from_dt,
628 to_dt: to_dt,
629 type: type,
630 wrapper: wrapper,
631 tmpl: "component"
632 },
633 (response) => {
634 try {
635 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
636 if (!obj_res.hasOwnProperty(call_method)) {
637 console.error('Unexpected JSON response', obj_res);
638 return false;
639 }
640
641 // replace HTML with new bookings calendar
642 widget_instance.find('.vbo-widget-bulkmess-bookings-list').html(obj_res[call_method]['html']);
643
644 // update search step status
645 widget_instance.find('.vbo-widget-bulkmess-step-search').find('.vbo-widget-bulkmess-step-status').text(typelbl);
646
647 // check if we got results
648 if (obj_res[call_method]['tot_bookings'] > 0) {
649 // hide all steps except the bookings list
650 widget_instance.find('.vbo-widget-bulkmess-step').not('.vbo-widget-bulkmess-step-bookings').addClass('vbo-widget-bulkmess-step-hidden');
651 // show the button to go next
652 widget_instance.find('.vbo-widget-bulkmess-step-bookings').find('.vbo-widget-bulkmess-gonext').show();
653 } else {
654 // hide the button to go next
655 widget_instance.find('.vbo-widget-bulkmess-step-bookings').find('.vbo-widget-bulkmess-gonext').hide();
656 }
657
658 // display the bookings list step
659 widget_instance.find('.vbo-widget-bulkmess-step-bookings').removeClass('vbo-widget-bulkmess-step-hidden');
660
661 // update bookings step status
662 widget_instance.find('.vbo-widget-bulkmess-step-bookings').find('.vbo-widget-bulkmess-step-status').text(obj_res[call_method]['tot_checked'] + ' / ' + obj_res[call_method]['tot_bookings']);
663
664 // restore next icon
665 widget_instance.find('.vbo-widget-bulkmess-step-search')
666 .find('.vbo-widget-bulkmess-loadbtn')
667 .prop('disabled', false)
668 .find('i')
669 .attr('class', vbo_widget_bulkmess_icn_next);
670 } catch(err) {
671 console.error('could not parse JSON response', err, response);
672 widget_instance.find('.vbo-widget-bulkmess-bookings-list').html('');
673 }
674 },
675 (error) => {
676 widget_instance.find('.vbo-widget-bulkmess-bookings-list').html('');
677 console.error(error);
678 alert(error.responseText);
679 }
680 );
681 }
682
683 /**
684 * Renders the booking details widget.
685 */
686 function vboWidgetBulkMessOpenBooking(bid) {
687 VBOCore.handleDisplayWidgetNotification({widget_id: 'booking_details'}, {
688 booking_id: bid,
689 modal_options: {
690 /**
691 * Overwrite modal options for rendering the admin widget.
692 * We need to use a different suffix in case this current widget was
693 * also rendered within a modal, or it would get dismissed in favour
694 * of the newly opened admin widget.
695 */
696 suffix: 'widget_modal_inner_booking_details',
697 },
698 });
699 }
700
701 /**
702 * Completes a step.
703 */
704 function vboWidgetBulkMessGoNext(wrapper, stepname) {
705 var widget_instance = jQuery('#' + wrapper);
706 if (!widget_instance.length) {
707 return false;
708 }
709
710 if (stepname == 'message' || stepname == 'send') {
711 // count the number of selected bookings
712 var checked_stats = vboWidgetBulkMessCountChecked(wrapper);
713 if (!checked_stats['tot_checked']) {
714 alert(Joomla.JText._('VBO_PLEASE_SELECT'));
715 return false;
716 }
717
718 // update step status if about to build the message
719 if (stepname == 'message') {
720 widget_instance.find('.vbo-widget-bulkmess-step-message').find('.vbo-widget-bulkmess-step-status').text(Joomla.JText._('VBPVIEWORDERSPEOPLE') + ': ' + checked_stats['tot_checked']);
721 }
722
723 // ask for confirmation before sending
724 if (stepname == 'send') {
725 if (!confirm(Joomla.JText._('VBO_WANT_PROCEED'))) {
726 return false;
727 } else {
728 // start sending
729 vboWidgetBulkMessDoSend(wrapper);
730 }
731 }
732 }
733
734 // hide all steps except the next one
735 widget_instance.find('.vbo-widget-bulkmess-step').not('.vbo-widget-bulkmess-step-' + stepname).addClass('vbo-widget-bulkmess-step-hidden');
736
737 // display the next step
738 widget_instance.find('.vbo-widget-bulkmess-step-' + stepname).removeClass('vbo-widget-bulkmess-step-hidden');
739 }
740
741 /**
742 * Starts the process to send the communication messages.
743 */
744 function vboWidgetBulkMessDoSend(wrapper) {
745 var widget_instance = jQuery('#' + wrapper);
746 if (!widget_instance.length) {
747 return false;
748 }
749
750 // gather the list of booking IDs to notify
751 var bids_pool = [];
752 widget_instance.find('.vbo-widget-bulkmess-step-bookings')
753 .find('input[type="checkbox"]:checked')
754 .each(function() {
755 bids_pool.push(jQuery(this).val());
756 });
757
758 if (!bids_pool.length) {
759 alert(Joomla.JText._('VBO_PLEASE_SELECT'));
760 return false;
761 }
762
763 // hide all buttons to go next
764 widget_instance.find('.vbo-widget-bulkmess-gonext').hide();
765
766 // update step status
767 widget_instance.find('.vbo-widget-bulkmess-step-send').find('.vbo-widget-bulkmess-step-status').html('<?php VikBookingIcons::e('spinner', 'fa-spin fa-fw'); ?>');
768
769 // trigger the recursive async sending process
770 vboWodgetBulkMessRecursiveAsyncSend(0, bids_pool, wrapper);
771 }
772
773 /**
774 * Recursive function to asynchronously dispatch the messages.
775 */
776 function vboWodgetBulkMessRecursiveAsyncSend(i, pool, wrapper) {
777 var widget_instance = jQuery('#' + wrapper);
778 if (!widget_instance.length) {
779 throw new Error('Widget instance not found');
780 }
781
782 if (i >= pool.length) {
783 // process completed, update status
784 widget_instance.find('.vbo-widget-bulkmess-step-send')
785 .find('.vbo-widget-bulkmess-step-status')
786 .html('<?php VikBookingIcons::e('check-circle'); ?>');
787
788 // do not continue
789 return;
790 }
791
792 if (!pool.hasOwnProperty(i) || !pool[i]) {
793 throw new Error('Invalid index argument');
794 }
795
796 if (i === 0) {
797 // start the progress bar
798 widget_instance.find('.vbo-widget-bulkmess-step-send')
799 .find('progress')
800 .attr('value', 0)
801 .attr('max', pool.length)
802 .text('0 / ' + pool.length);
803 }
804
805 // gather message subject and content
806 var subject = widget_instance.find('.vbo-widget-bulkmess-subject').val();
807 var message = widget_instance.find('.vbo-widget-bulkmess-messagecont').val();
808
809 // the widget method to call
810 var call_method = 'sendOneMessage';
811
812 // make a request to send the message
813 VBOCore.doAjax(
814 "<?php echo $this->getExecWidgetAjaxUri(); ?>",
815 {
816 widget_id: "<?php echo $this->getIdentifier(); ?>",
817 call: call_method,
818 return: 1,
819 bid: pool[i],
820 index: i,
821 message: message,
822 subject: subject,
823 wrapper: wrapper,
824 tmpl: "component"
825 },
826 (response) => {
827 try {
828 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
829 if (!obj_res.hasOwnProperty(call_method)) {
830 console.error('Unexpected JSON response', obj_res);
831 return false;
832 }
833
834 // update progress bar
835 widget_instance.find('.vbo-widget-bulkmess-step-send')
836 .find('progress')
837 .attr('value', (i + 1))
838 .text((i + 1) + ' / ' + pool.length);
839
840 // go next
841 vboWodgetBulkMessRecursiveAsyncSend(i + 1, pool, wrapper);
842
843 } catch(err) {
844 // log and display error
845 console.error('could not parse JSON response', err, response);
846 alert('could not parse JSON response');
847
848 // go next either way
849 vboWodgetBulkMessRecursiveAsyncSend(i + 1, pool, wrapper);
850 }
851 },
852 (error) => {
853 // log and display error
854 console.error(error);
855 alert(error.responseText);
856
857 // update progress bar
858 widget_instance.find('.vbo-widget-bulkmess-step-send')
859 .find('progress')
860 .attr('value', (i + 1))
861 .text((i + 1) + ' / ' + pool.length);
862
863 // go next either way
864 vboWodgetBulkMessRecursiveAsyncSend(i + 1, pool, wrapper);
865 }
866 );
867 }
868
869 /**
870 * Toggles the visibility of a step.
871 */
872 function vboWidgetBulkMessToggleStep(wrapper, stepname) {
873 var widget_instance = jQuery('#' + wrapper);
874 if (!widget_instance.length) {
875 return false;
876 }
877
878 // display the current step
879 widget_instance.find('.vbo-widget-bulkmess-step-' + stepname).toggleClass('vbo-widget-bulkmess-step-hidden');
880 }
881
882 /**
883 * Counts the number of bookings checked.
884 */
885 function vboWidgetBulkMessCountChecked(wrapper) {
886 var widget_instance = jQuery('#' + wrapper);
887 if (!widget_instance.length) {
888 return false;
889 }
890
891 var container = widget_instance.find('.vbo-widget-bulkmess-step-bookings');
892
893 var tot_bookings = container.find('input[type="checkbox"]').length;
894 var tot_checked = container.find('input[type="checkbox"]:checked').length;
895
896 if (tot_checked) {
897 // overwrite the first checked booking ID to let the mail preview work
898 window['vbo_current_bid'] = container.find('input[type="checkbox"]:checked').first().attr('value');
899 }
900
901 return {
902 tot_checked: tot_checked,
903 tot_bookings: tot_bookings
904 };
905 }
906
907 /**
908 * Toggles the checked status for all bookings.
909 */
910 function vboWidgetBulkMessSelectAll(wrapper) {
911 var widget_instance = jQuery('#' + wrapper);
912 if (!widget_instance.length) {
913 return false;
914 }
915
916 var checked_stats = vboWidgetBulkMessCountChecked(wrapper);
917 if (!checked_stats['tot_bookings']) {
918 return false;
919 }
920
921 var container = widget_instance.find('.vbo-widget-bulkmess-step-bookings');
922 var tot_elements = checked_stats['tot_bookings'];
923
924 if (checked_stats['tot_checked'] < checked_stats['tot_bookings']) {
925 // select all
926 container.find('input[type="checkbox"]').prop('checked', true);
927 } else {
928 // select none
929 container.find('input[type="checkbox"]').prop('checked', false);
930 tot_elements = 0;
931 }
932
933 // update status on bookings step
934 container.find('.vbo-widget-bulkmess-step-status').text(tot_elements + ' / ' + checked_stats['tot_bookings']);
935 }
936
937 </script>
938 <?php
939 }
940 ?>
941
942 <script type="text/javascript">
943
944 jQuery(function() {
945
946 // render datepicker calendar for dates navigation
947 jQuery('#<?php echo $wrapper_id; ?>').find('.vbo-widget-bulkmess-fromdt, .vbo-widget-bulkmess-todt').datepicker({
948 minDate: "-1y",
949 maxDate: "+3y",
950 yearRange: "<?php echo (date('Y') - 2); ?>:<?php echo (date('Y') + 3); ?>",
951 changeMonth: true,
952 changeYear: true,
953 dateFormat: "<?php echo $dtpicker_df; ?>",
954 onSelect: function(selectedDate) {
955 if (!selectedDate) {
956 return;
957 }
958 if (jQuery(this).hasClass('vbo-widget-bulkmess-fromdt')) {
959 let nowstart = jQuery(this).datepicker('getDate');
960 let nowstartdate = new Date(nowstart.getTime());
961 jQuery('.vbo-widget-bulkmess-todt').datepicker('option', {minDate: nowstartdate});
962 }
963 }
964 });
965
966 // triggering for datepicker calendar icon
967 jQuery('#<?php echo $wrapper_id; ?>').find('.vbo-widget-bulkmess-fromdt-trigger, .vbo-widget-bulkmess-todt-trigger').click(function() {
968 var jdp = jQuery(this).parent().find('input.hasDatepicker');
969 if (jdp.length) {
970 jdp.focus();
971 }
972 });
973
974 });
975
976 </script>
977
978 <?php
979 }
980
981 /**
982 * Renders the editor to compose the message.
983 *
984 * @return string
985 */
986 private function renderEditor()
987 {
988 // load assets
989 $this->vbo_app->loadVisualEditorAssets();
990
991 // build a list of all special tags for the visual editor
992 $special_tags_base = [
993 '{customer_name}',
994 '{customer_pin}',
995 '{booking_id}',
996 '{checkin_date}',
997 '{checkout_date}',
998 '{num_nights}',
999 '{rooms_booked}',
1000 '{tot_adults}',
1001 '{tot_children}',
1002 '{tot_guests}',
1003 '{total}',
1004 '{total_paid}',
1005 '{remaining_balance}',
1006 '{booking_link}',
1007 ];
1008
1009 // load all conditional text special tags
1010 $condtext_tags = array_keys(VikBooking::getConditionalRulesInstance()->getSpecialTags());
1011
1012 /**
1013 * Load Door Access Control special tags from the configured integration providers.
1014 *
1015 * @since 1.18.4 (J) - 1.8.4 (WP)
1016 */
1017 $dac_tags = VBOFactory::getDoorAccessControl()->getInstalledSpecialTags();
1018
1019 // join special tags with conditional texts and DAC to construct a list of editor buttons,
1020 // displayed within the toolbar of Quill editor
1021 $editor_btns = array_merge($special_tags_base, $condtext_tags, $dac_tags);
1022
1023 // build the default message value
1024 $def_message = !empty($this->widgetSettings->last_message) ? $this->widgetSettings->last_message : $this->getDefaultMessage();
1025
1026 return $this->vbo_app->renderVisualEditor(
1027 'vbo_widget_bulkmess_mess',
1028 $def_message,
1029 [
1030 'class' => 'vbo-widget-bulkmess-messagecont',
1031 'style' => 'width: 96%; height: 150px;',
1032 ],
1033 [
1034 'modes' => [
1035 'visual',
1036 'text',
1037 ],
1038 ],
1039 $editor_btns
1040 );
1041 }
1042
1043 /**
1044 * Returns the default message.
1045 *
1046 * @return string
1047 */
1048 private function getDefaultMessage()
1049 {
1050 $message = '';
1051 $logo_html = '';
1052 $sitelogo = VBOFactory::getConfig()->get('sitelogo');
1053 $company_name = VikBooking::getFrontTitle();
1054
1055 if ($sitelogo && is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources'. DIRECTORY_SEPARATOR . $sitelogo)) {
1056 $logo_html = '<p style="text-align: center;">'
1057 . '<img src="' . VBO_ADMIN_URI . 'resources/' . $sitelogo . '" alt="' . htmlspecialchars($company_name) . '" /></p>'
1058 . "\n";
1059 }
1060
1061 $message =
1062 <<<HTML
1063 $logo_html
1064 <h1 style="text-align: center;">
1065 <span style="font-family: verdana;">$company_name</span>
1066 </h1>
1067 <hr class="vbo-editor-hl-mailwrapper">
1068 <h4>Dear {customer_name},</h4>
1069 <p><br></p>
1070 <p>This is a message for your stay from {checkin_date} to {checkout_date}.</p>
1071 <p><br></p>
1072 <p><br></p>
1073 <p>Thank you.</p>
1074 <p>$company_name</p>
1075 <hr class="vbo-editor-hl-mailwrapper">
1076 <p><br></p>
1077 HTML
1078 ;
1079
1080 return $message;
1081 }
1082
1083 /**
1084 * Sends a message to the guest through a regular email.
1085 *
1086 * @param array $booking the reservation record to notify.
1087 * @param string $message the message to send.
1088 * @param string $subject the email subject.
1089 *
1090 * @return string the message built and sent.
1091 */
1092 private function notifyThroughEmail(array $booking, $message, $subject)
1093 {
1094 // fetch booked room details
1095 $booking_rooms = VikBooking::loadOrdersRoomsData($booking['id']);
1096
1097 // translate contents
1098 $vbo_tn = VikBooking::getTranslator();
1099 $vbo_tn->translateContents($booking_rooms, '#__vikbooking_rooms', ['id' => 'idroom', 'name' => 'room_name']);
1100
1101 $message = $this->parseCustomerEmailTemplate($message, $booking, $booking_rooms, $vbo_tn);
1102
1103 $is_html = (strpos($message, '<') !== false || strpos($message, '</') !== false);
1104 if ($is_html && !preg_match("/(<\/?br\/?>)+/", $message)) {
1105 // when no br tags found, apply nl2br
1106 $message = nl2br($message);
1107 }
1108
1109 // get sender email address
1110 $admin_sendermail = VikBooking::getSenderMail();
1111
1112 if (!$this->vbo_app->sendMail($admin_sendermail, $admin_sendermail, $booking['custmail'], $admin_sendermail, $subject, $message, $is_html)) {
1113 VBOHttpDocument::getInstance()->close(500, 'Sending the email message to the booking ID ' . $booking['id'] . ' failed.');
1114 }
1115
1116 return $message;
1117 }
1118
1119 /**
1120 * Sends a message to the guest through a regular email.
1121 *
1122 * @param array $booking the reservation record to notify.
1123 * @param string $message the message to send.
1124 *
1125 * @return string the message built and sent.
1126 */
1127 private function notifyThroughMessage(array $booking, $message)
1128 {
1129 // fetch booked room details
1130 $booking_rooms = VikBooking::loadOrdersRoomsData($booking['id']);
1131
1132 // translate contents
1133 $vbo_tn = VikBooking::getTranslator();
1134 $vbo_tn->translateContents($booking_rooms, '#__vikbooking_rooms', ['id' => 'idroom', 'name' => 'room_name']);
1135
1136 $message = $this->parseCustomerEmailTemplate($message, $booking, $booking_rooms, $vbo_tn);
1137 if (empty($message)) {
1138 VBOHttpDocument::getInstance()->close(500, 'Message for the booking ID ' . $booking['id'] . ' is empty.');
1139 }
1140
1141 $messaging = VCMChatMessaging::getInstance($booking);
1142 $result = $messaging->setMessage($message)
1143 ->sendGuestMessage();
1144
1145 if (!$result && $error = $messaging->getError()) {
1146 // terminate with the error description
1147 VBOHttpDocument::getInstance()->close(500, "Message could not be sent to guest - Booking ID {$booking['id']} ({$booking['customer_name']}): {$error}");
1148 }
1149
1150 return $message;
1151 }
1152
1153 /**
1154 * Composes the actual email message by parsing special tokens.
1155 *
1156 * @param string $message the message content for the email.
1157 * @param array $booking booking array record.
1158 * @param array $booking_rooms list of rooms booked.
1159 * @param array $vbo_tn translator object.
1160 *
1161 * @return string the email message content ready to be sent.
1162 */
1163 private function parseCustomerEmailTemplate($message, $booking, $booking_rooms, $vbo_tn = null)
1164 {
1165 $tpl = $message;
1166
1167 /**
1168 * Parse all conditional text rules.
1169 */
1170 VikBooking::getConditionalRulesInstance()
1171 ->set(array('booking', 'rooms'), array($booking, $booking_rooms))
1172 ->parseTokens($tpl);
1173
1174 /**
1175 * Parse all Door Access Control tags.
1176 *
1177 * @since 1.18.4 (J) - 1.8.4 (WP)
1178 */
1179 VBOFactory::getDoorAccessControl()
1180 ->parseTokens((new VBOBookingRegistry($booking, $booking_rooms)), $tpl);
1181
1182 // normalize customer details
1183 if (empty($booking['customer_name']) && !empty($booking['customer'])) {
1184 $booking['customer_name'] = trim($booking['first_name'] . ' ' . $booking['last_name']);
1185 }
1186
1187 $vbo_df = VikBooking::getDateFormat();
1188 $df = $vbo_df == "%d/%m/%Y" ? 'd/m/Y' : ($vbo_df == "%m/%d/%Y" ? 'm/d/Y' : 'Y-m-d');
1189 $tpl = str_replace('{customer_name}', ($booking['customer_name'] ?: ''), $tpl);
1190 $tpl = str_replace('{booking_id}', $booking['id'], $tpl);
1191 $tpl = str_replace('{checkin_date}', date($df, $booking['checkin']), $tpl);
1192 $tpl = str_replace('{checkout_date}', date($df, $booking['checkout']), $tpl);
1193 $tpl = str_replace('{num_nights}', $booking['days'], $tpl);
1194 $rooms_booked = [];
1195 $tot_adults = 0;
1196 $tot_children = 0;
1197 $tot_guests = 0;
1198 foreach ($booking_rooms as $broom) {
1199 if (array_key_exists($broom['room_name'], $rooms_booked)) {
1200 $rooms_booked[$broom['room_name']] += 1;
1201 } else {
1202 $rooms_booked[$broom['room_name']] = 1;
1203 }
1204 $tot_adults += (int)$broom['adults'];
1205 $tot_children += (int)$broom['children'];
1206 $tot_guests += ((int)$broom['adults'] + (int)$broom['children']);
1207 }
1208 $tpl = str_replace('{tot_adults}', $tot_adults, $tpl);
1209 $tpl = str_replace('{tot_children}', $tot_children, $tpl);
1210 $tpl = str_replace('{tot_guests}', $tot_guests, $tpl);
1211 $rooms_booked_quant = [];
1212 foreach ($rooms_booked as $rname => $quant) {
1213 $rooms_booked_quant[] = ($quant > 1 ? $quant.' ' : '').$rname;
1214 }
1215 $tpl = str_replace('{rooms_booked}', implode(', ', $rooms_booked_quant), $tpl);
1216 $tpl = str_replace('{total}', VikBooking::numberFormat($booking['total']), $tpl);
1217 $tpl = str_replace('{total_paid}', VikBooking::numberFormat($booking['totpaid']), $tpl);
1218 $remaining_bal = $booking['total'] - $booking['totpaid'];
1219 $tpl = str_replace('{remaining_balance}', VikBooking::numberFormat($remaining_bal), $tpl);
1220 $tpl = str_replace('{customer_pin}', (($booking['customer_pin'] ?? '') ?: ($booking['customer']['pin'] ?? '')), $tpl);
1221
1222 $use_sid = empty($booking['sid']) && !empty($booking['idorderota']) ? $booking['idorderota'] : $booking['sid'];
1223
1224 $bestitemid = VikBooking::findProperItemIdType(['booking'], (!empty($booking['lang']) ? $booking['lang'] : null));
1225 $lang_suffix = $bestitemid && !empty($booking['lang']) ? '&lang=' . $booking['lang'] : '';
1226 $book_link = VikBooking::externalroute("index.php?option=com_vikbooking&view=booking&sid=" . $use_sid . "&ts=" . $booking['ts'] . $lang_suffix, false, (!empty($bestitemid) ? $bestitemid : null));
1227
1228 $tpl = str_replace('{booking_link}', $book_link, $tpl);
1229
1230 /**
1231 * Rooms Distinctive Features parsing
1232 */
1233 preg_match_all('/\{roomfeature ([a-zA-Z0-9 ]+)\}/U', $tpl, $matches);
1234 if (isset($matches[1]) && $matches[1]) {
1235 foreach ($matches[1] as $reqf) {
1236 $rooms_features = [];
1237 foreach ($booking_rooms as $broom) {
1238 $distinctive_features = [];
1239 $rparams = json_decode($broom['params'], true);
1240 if (array_key_exists('features', $rparams) && count($rparams['features']) > 0 && array_key_exists('roomindex', $broom) && !empty($broom['roomindex']) && array_key_exists($broom['roomindex'], $rparams['features'])) {
1241 $distinctive_features = $rparams['features'][$broom['roomindex']];
1242 }
1243 if (!count($distinctive_features)) {
1244 continue;
1245 }
1246 $feature_found = false;
1247 foreach ($distinctive_features as $dfk => $dfv) {
1248 if (stripos($dfk, $reqf) !== false) {
1249 $feature_found = $dfk;
1250 if (strlen(trim($dfk)) == strlen(trim($reqf))) {
1251 break;
1252 }
1253 }
1254 }
1255 if ($feature_found !== false && strlen((string)$distinctive_features[$feature_found])) {
1256 $rooms_features[] = $distinctive_features[$feature_found];
1257 }
1258 }
1259 if ($rooms_features) {
1260 $rpval = implode(', ', $rooms_features);
1261 } else {
1262 $rpval = '';
1263 }
1264 $tpl = str_replace("{roomfeature ".$reqf."}", $rpval, $tpl);
1265 }
1266 }
1267
1268 return $tpl;
1269 }
1270 }
1271