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

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

974 lines 47.7 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) 2026 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 "quotes".
15 *
16 * @since 1.18.8 (J) - 1.8.8 (WP)
17 */
18 class VikBookingAdminWidgetQuotes 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 records to show per page.
29 *
30 * @var int
31 */
32 protected $records_per_page = 6;
33
34 /**
35 * The total number of skeleton loading elements.
36 *
37 * @var int
38 */
39 protected $tot_skeletons = 4;
40
41 /**
42 * The distance threshold in pixels between the current scroll
43 * position and the end of the list for triggering the loading
44 * of a next page within an infinite scroll mechanism.
45 *
46 * @var int
47 */
48 protected $px_distance_threshold = 140;
49
50 /**
51 * Class constructor will define the widget name and identifier.
52 */
53 public function __construct()
54 {
55 // call parent constructor
56 parent::__construct();
57
58 $this->widgetName = JText::translate('VBO_QUOTES');
59 $this->widgetDescr = JText::translate('VBO_W_QUOTES_DESCR');
60 $this->widgetId = basename(__FILE__, '.php');
61
62 $this->widgetIcon = '<i class="' . VikBookingIcons::i('file-alt') . '"></i>';
63 $this->widgetStyleName = 'orange';
64 }
65
66 /**
67 * Custom method for this widget only to load the next page of quote records.
68 * The method is called by the admin controller through an AJAX request.
69 * The visibility should be public, it should not exit the process, and
70 * any content sent to output will be returned to the AJAX response.
71 * In this case we return an array because this method requires "return":1.
72 *
73 * @return array
74 */
75 public function loadNextQuotes()
76 {
77 $input = JFactory::getApplication()->input;
78
79 $wrapper = $input->getString('wrapper', '');
80 $unsent = $input->getBool('unsent', false);
81 $page_num = $input->getUInt('page_num', 1);
82 $page_num = $page_num ?: 1;
83
84 // access the quote model
85 $quoteModel = VBOMvcModel::getInstance('quote');
86
87 // build query filters
88 $filters = [];
89
90 if ($unsent) {
91 // filter by those quotes that were not sent before
92 $filters['sent'] = [
93 [
94 'operand' => '=',
95 'value' => '0',
96 ],
97 ];
98 }
99
100 // determine the query limit start
101 $lim_start = ($page_num - 1) * $this->records_per_page;
102
103 // get and build the latest quotes
104 $html_content = $this->buildQuotesHTML(
105 $quoteModel->loadBookingRecords($lim_start, $this->records_per_page, ['filters' => $filters]),
106 ($page_num - 1)
107 );
108
109 // return an associative array of values
110 return [
111 'html' => $html_content,
112 'page_number' => $page_num,
113 'pages_count' => ceil($quoteModel->countRecordsFound() / $this->records_per_page),
114 ];
115 }
116
117 /**
118 * Main method to invoke the widget.
119 *
120 * @param ?VBOMultitaskData $data
121 *
122 * @return void
123 */
124 public function render(?VBOMultitaskData $data = null)
125 {
126 // increase widget's instance counter
127 static::$instance_counter++;
128
129 // check whether the widget is being rendered via AJAX
130 $is_ajax = $this->isAjaxRendering();
131
132 // generate a unique ID for the wrapper instance
133 $wrapper_instance = !$is_ajax ? static::$instance_counter : rand();
134 $wrapper_id = 'vbo-widget-quotes-' . $wrapper_instance;
135
136 // check permissions
137 $vbo_auth_bookings = JFactory::getUser()->authorise('core.vbo.bookings', 'com_vikbooking');
138 if (!$vbo_auth_bookings) {
139 // permissions are not met
140 return;
141 }
142
143 // check multitask data
144 $js_modal_id = '';
145 $is_modal_rendering = false;
146 if ($data) {
147 // access Multitask data
148 $is_modal_rendering = $data->isModalRendering();
149 if ($is_modal_rendering) {
150 // get modal JS identifier
151 $js_modal_id = $data->getModalJsIdentifier();
152 }
153 }
154
155 // check if a booking ID was requested for loading (ignore multi-task data)
156 $bookingId = $this->options()->fetchBookingId();
157
158 // access the quote model
159 $quoteModel = VBOMvcModel::getInstance('quote');
160
161 // start quotes list
162 $quotes = [];
163
164 // load all quotes or the one assigned to the given booking ID
165 if ($bookingId) {
166 // make sure the booking exists
167 $bookingRecord = VikBooking::getBookingInfoFromID((int) $bookingId);
168 if (!empty($bookingRecord['idquote'])) {
169 // load the involved quote record
170 $quotes = $quoteModel->loadBookingRecords(0, 1, [
171 'filters' => [
172 'id' => (int) $bookingRecord['idquote'],
173 ],
174 ]);
175 }
176 } else {
177 // load latest quotes by default
178 $quotes = $quoteModel->loadBookingRecords(0, $this->records_per_page);
179 }
180
181 // immediately count the number of pages to show all quotes
182 $pages_count = ceil($quoteModel->countRecordsFound() / $this->records_per_page);
183
184 ?>
185 <div id="<?php echo $wrapper_id; ?>" class="vbo-admin-widget-wrapper" data-instance="<?php echo $wrapper_instance; ?>">
186 <div class="vbo-admin-widget-head">
187 <div class="vbo-admin-widget-head-inline">
188 <h4><?php echo $this->widgetIcon; ?> <span><?php echo $this->widgetName; ?></span></h4>
189 </div>
190 </div>
191 <div class="vbo-w-quote-wrapper">
192 <div class="vbo-w-quote-items-list" data-page-number="1" data-pages-count="<?php echo $pages_count; ?>">
193 <?php
194 // output all quotes
195 echo $this->buildQuotesHTML($quotes);
196
197 // check if we are displaying one quote from the requested booking ID
198 if ($bookingId && count($quotes) === 1) {
199 ?>
200 <div class="vbo-w-list-back">
201 <span><?php VikBookingIcons::e('arrow-left'); ?> <?php echo JText::translate('VBO_SEE_ALL'); ?></span>
202 </div>
203 <?php
204 }
205
206 // check if we have no results
207 if (!$quotes) {
208 ?>
209 <div class="vbo-widget-quotes-loadmore-info">
210 <p><?php echo JText::translate('VBO_NO_RECORDS_FOUND'); ?></p>
211 </div>
212 <?php
213 }
214 ?>
215 </div>
216 <div class="vbo-widget-quotes-loadmore-hidden" style="display: none;">
217 <button type="button" class="btn vbo-widget-quotes-loadmore-manual"><?php echo JText::translate('VBO_LOAD_MORE'); ?> <?php VikBookingIcons::e('chevron-right', 'icn-nomargin'); ?></button>
218 </div>
219 </div>
220 <div class="vbo-w-quote-helper-wrap" style="display: none;">
221 <div class="vbo-w-quote-send-tplmessage">
222 <div class="vbo-admin-container vbo-admin-container-full vbo-admin-container-compact">
223 <div class="vbo-params-wrap">
224 <div class="vbo-params-container">
225 <div class="vbo-params-block">
226 <div class="vbo-param-container">
227 <div class="vbo-param-label"><?php echo JText::translate('VBO_MESSAGE_TEMPLATE'); ?></div>
228 <div class="vbo-param-setting">
229 <select data-field="send-ma-tpl"></select>
230 </div>
231 </div>
232 <div class="vbo-param-container">
233 <div class="vbo-param-label"><?php echo JText::translate('VBOPREVIEW'); ?></div>
234 <div class="vbo-param-setting">
235 <div class="vbo-ma-send-tplmessage-preview"></div>
236 </div>
237 </div>
238 </div>
239 </div>
240 </div>
241 </div>
242 </div>
243 </div>
244 </div>
245 <?php
246
247 if (static::$instance_counter === 0 || $is_ajax) {
248 /**
249 * Print the JS code only once for all instances of this widget.
250 */
251 ?>
252 <script type="text/javascript">
253
254 /**
255 * Returns the skeletons loading HTML.
256 */
257 function vboWidgetQuotesGetSkeletons() {
258 var skeletons = '<div class="vbo-dashboard-guests-latest vbo-widget-quotes-skeletons">' + "\n";
259
260 for (var i = 0; i < <?php echo $this->tot_skeletons; ?>; i++) {
261 skeletons += '<div class="vbo-dashboard-guest-activity vbo-dashboard-guest-activity-skeleton">' + "\n";
262 skeletons += ' <div class="vbo-dashboard-guest-activity-avatar">' + "\n";
263 skeletons += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-avatar"></div>' + "\n";
264 skeletons += ' </div>' + "\n";
265 skeletons += ' <div class="vbo-dashboard-guest-activity-content">' + "\n";
266 skeletons += ' <div class="vbo-dashboard-guest-activity-content-head">' + "\n";
267 skeletons += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-title"></div>' + "\n";
268 skeletons += ' </div>' + "\n";
269 skeletons += ' <div class="vbo-dashboard-guest-activity-content-subhead">' + "\n";
270 skeletons += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-subtitle"></div>' + "\n";
271 skeletons += ' </div>' + "\n";
272 skeletons += ' <div class="vbo-dashboard-guest-activity-content-info-msg">' + "\n";
273 skeletons += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-content"></div>' + "\n";
274 skeletons += ' </div>' + "\n";
275 skeletons += ' </div>' + "\n";
276 skeletons += '</div>' + "\n";
277 }
278
279 skeletons += '</div>' + "\n";
280
281 return skeletons;
282 }
283
284 /**
285 * Loads the next page of records.
286 */
287 function vboWidgetQuotesLoadNextPage(wrapper, reload) {
288 const recordsList = document
289 .querySelector('#' + wrapper)
290 ?.querySelector('.vbo-w-quote-items-list');
291
292 if (!recordsList) {
293 throw new Error('Could not find records list element');
294 }
295
296 // ensure we've got other pages to load
297 let pageNumber = parseInt(recordsList.getAttribute('data-page-number')) || 1;
298 let pagesCount = parseInt(recordsList.getAttribute('data-pages-count')) || 1;
299
300 if (reload) {
301 // force the page to reload from 0
302 pageNumber = -1;
303 pagesCount = pageNumber + 2;
304 }
305
306 if (pageNumber >= pagesCount) {
307 // no more pages available, abort
308 return;
309 }
310
311 // load the next page of records (or reload them from start)
312
313 // append loading skeletons
314 recordsList
315 .insertAdjacentHTML('beforeend', vboWidgetQuotesGetSkeletons());
316
317 // the widget method to call
318 let call_method = 'loadNextQuotes';
319
320 // make a request to load the next page of records
321 VBOCore.doAjax(
322 "<?php echo $this->getExecWidgetAjaxUri(); ?>",
323 {
324 widget_id: "<?php echo $this->getIdentifier(); ?>",
325 call: call_method,
326 return: 1,
327 page_num: parseInt(pageNumber) + 1,
328 wrapper: wrapper,
329 tmpl: "component"
330 },
331 (response) => {
332 try {
333 if (!response.hasOwnProperty(call_method)) {
334 console.error('Unexpected JSON response', response);
335 return false;
336 }
337
338 // remove loading skeletons
339 recordsList
340 .querySelector('.vbo-widget-quotes-skeletons')
341 .remove();
342
343 // append HTML with the new records and set page infos
344 recordsList
345 .setAttribute('data-page-number', response[call_method]['page_number']);
346 recordsList
347 .setAttribute('data-pages-count', response[call_method]['pages_count']);
348 recordsList
349 .insertAdjacentHTML('beforeend', response[call_method]['html']);
350
351 // turn custom property off for the page loading
352 recordsList.pageLoading = false;
353
354 if (reload) {
355 // set up infinite scroll loading
356 vboWidgetQuotesSetupInfiniteScroll(wrapper);
357 }
358
359 // set up records click listeners for the new records read
360 vboWidgetQuotesRegisterClickListeners(wrapper);
361 } catch(err) {
362 console.error('could not parse JSON response', err, response);
363 }
364 },
365 (error) => {
366 // display the error
367 alert(error.responseText);
368
369 // turn custom property off for the page loading
370 recordsList.pageLoading = false;
371
372 // remove loading skeletons
373 recordsList
374 .querySelector('.vbo-widget-quotes-skeletons')
375 .remove();
376 }
377 );
378 }
379
380 /**
381 * Setups the infinite scroll loading.
382 */
383 function vboWidgetQuotesSetupInfiniteScroll(wrapper) {
384 const recordsList = document
385 .querySelector('#' + wrapper)
386 .querySelector('.vbo-w-quote-items-list');
387
388 if (!recordsList) {
389 throw new Error('Could not find quote list element');
390 }
391
392 // ensure we've got more pages to load
393 let pageNumber = parseInt(recordsList.getAttribute('data-page-number')) || 1;
394 let pagesCount = parseInt(recordsList.getAttribute('data-pages-count')) || 1;
395
396 if (pageNumber >= pagesCount) {
397 // no pagination needed
398 return;
399 }
400
401 // get wrapper dimensions
402 let listViewHeight = recordsList.offsetHeight;
403 let listGlobHeight = recordsList.scrollHeight;
404 let listScrollTop = recordsList.scrollTop;
405
406 if (listViewHeight >= listGlobHeight) {
407 // no scrolling detected, show manual loading
408 document
409 .querySelector('#' + wrapper)
410 .querySelector('.vbo-widget-quotes-loadmore-hidden')
411 .style
412 .display = 'block';
413
414 return;
415 }
416
417 // inject custom property to identify the wrapper ID
418 recordsList.wrapperId = wrapper;
419
420 // register infinite scroll event handler
421 recordsList
422 .addEventListener('scroll', vboWidgetQuotesInfiniteScroll);
423 }
424
425 /**
426 * Infinite scroll event handler.
427 */
428 function vboWidgetQuotesInfiniteScroll(e) {
429 // access the injected wrapper ID property
430 let wrapper = e.currentTarget.wrapperId;
431
432 if (!wrapper) {
433 return;
434 }
435
436 // register throttling callback
437 VBOCore.throttleTimer(() => {
438 // access the current records list
439 const recordsList = document
440 .querySelector('#' + wrapper)
441 ?.querySelector('.vbo-w-quote-items-list');
442
443 if (!recordsList) {
444 return;
445 }
446
447 // ensure we've got more pages to load
448 let pageNumber = parseInt(recordsList.getAttribute('data-page-number')) || 1;
449 let pagesCount = parseInt(recordsList.getAttribute('data-pages-count')) || 1;
450
451 if (pageNumber >= pagesCount) {
452 // unregister the infinite scroll
453 recordsList
454 .removeEventListener('scroll', vboWidgetQuotesInfiniteScroll);
455
456 // display message for all records loaded
457 if (pagesCount > 1) {
458 let widget_content = document
459 .querySelector('#' + wrapper);
460
461 // hide the eventually displayed manual loading
462 widget_content
463 .querySelector('.vbo-widget-quotes-loadmore-hidden')
464 .style
465 .display = 'none';
466
467 if (!widget_content.querySelector('.vbo-widget-quotes-loadmore-info')) {
468 // append the message stating that all records have been displayed
469 let infoDiv = document
470 .createElement('div');
471 infoDiv.classList
472 .add('vbo-widget-quotes-loadmore-info');
473
474 let infoTxt = document
475 .createElement('p');
476 infoTxt.append(<?php echo json_encode(JText::translate('VBO_NOMORE_RECORDS_DISPLAY')); ?>);
477
478 infoDiv.append(infoTxt);
479
480 widget_content.querySelector('.vbo-w-quote-items-list').append(infoDiv);
481 }
482 }
483
484 return;
485 }
486
487 // make sure the loading of a next page isn't running
488 if (recordsList.pageLoading) {
489 // abort
490 return;
491 }
492
493 // get wrapper dimensions
494 let listViewHeight = recordsList.offsetHeight;
495 let listGlobHeight = recordsList.scrollHeight;
496 let listScrollTop = recordsList.scrollTop;
497
498 if (!listScrollTop || listViewHeight >= listGlobHeight) {
499 // no scrolling detected at all
500 return;
501 }
502
503 // calculate missing distance to the end of the list
504 let listEndDistance = listGlobHeight - (listViewHeight + listScrollTop);
505
506 if (listEndDistance < <?php echo $this->px_distance_threshold; ?>) {
507 // inject custom property to identify a next page is loading
508 recordsList.pageLoading = true;
509
510 // load the next page of records
511 vboWidgetQuotesLoadNextPage(wrapper);
512 }
513 }, 500);
514 }
515
516 /**
517 * Registers the click listener on all the eligible record entries.
518 */
519 function vboWidgetQuotesRegisterClickListeners(wrapper) {
520 const wrapperEl = document.querySelector('#' + wrapper);
521 const quotes = wrapperEl
522 .querySelector('.vbo-w-quote-items-list')
523 .querySelectorAll('.vbo-w-quote-item:not([data-listening])');
524
525 quotes.forEach((quote) => {
526 // get quote ID
527 const quoteId = quote.getAttribute('data-quote-id');
528
529 // immediately set attribute flag with listening enabled
530 quote.setAttribute('data-listening', 1);
531
532 // register click event to edit the quote itself
533 quote.querySelector('.vbo-w-quote-item-head-edit').addEventListener('click', (e) => {
534 // build link element and simulate the click on it
535 const link = document.createElement('a');
536 link.href = '<?php echo VBOFactory::getPlatform()->getUri()->admin('index.php?option=com_vikbooking&view=managequote', false); ?>' + '&quote_id=' + quoteId;
537 link.target = '_blank';
538 link.rel = 'noopener noreferrer';
539 link.click();
540 });
541
542 // register click event to edit the booking solution
543 quote.querySelectorAll('.vbo-w-quote-solution-edit').forEach((solutionEdit) => {
544 solutionEdit.addEventListener('click', (e) => {
545 // get booking ID
546 const bookingId = e.target.closest('.vbo-w-quote-solution').getAttribute('data-booking-id');
547 // build link element and simulate the click on it
548 const link = document.createElement('a');
549 link.href = '<?php echo VBOFactory::getPlatform()->getUri()->admin('index.php?option=com_vikbooking&task=editbusy', false); ?>' + '&cid[0]=' + bookingId;
550 link.target = '_blank';
551 link.rel = 'noopener noreferrer';
552 link.click();
553 });
554 });
555
556 // register click event to quickly see the booking details
557 quote.querySelectorAll('.vbo-w-quote-solution-revid').forEach((solutionLink) => {
558 solutionLink.addEventListener('click', (e) => {
559 // get booking ID
560 const bookingId = e.target.closest('.vbo-w-quote-solution').getAttribute('data-booking-id');
561 // open widget
562 VBOCore.handleDisplayWidgetNotification({widget_id: 'booking_details'}, {
563 bid: bookingId,
564 modal_options: {
565 suffix: 'widget_modal_inner_booking_details',
566 },
567 });
568 });
569 });
570
571 // register click event to send the quote via email
572 quote.querySelector('.vbo-w-quote-send-btn')?.addEventListener('click', (e) => {
573 const btnEl = e.target.matches('button') ? e.target : e.target.closest('button');
574 const icnEl = btnEl.querySelector('i');
575 let icnClass = icnEl.getAttribute('class');
576
577 // build modal buttons element
578 let modalBtnsEl = document.createElement('div');
579
580 // modal send via email button
581 let mailBtnEl = document.createElement('button');
582 mailBtnEl.setAttribute('type', 'button');
583 mailBtnEl.classList.add('btn', 'vbo-stacked-btn', 'vbo-gray-icon-btn');
584 mailBtnEl.innerHTML = '<?php VikBookingIcons::e('envelope'); ?> Email';
585 mailBtnEl.addEventListener('click', () => {
586 // dismiss modal
587 VBOCore.emitEvent('wquote-choose-send-method-dismiss');
588
589 // start loading
590 btnEl.disabled = true;
591 icnEl.setAttribute('class', '');
592 icnEl.classList.add(...String('<?php echo VikBookingIcons::i('circle-notch', 'fa-spin fa-fw'); ?>').split(' '));
593
594 // make the request
595 VBOCore.doAjax(
596 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=quote.sendMail'); ?>",
597 {
598 quote_id: quoteId,
599 },
600 (response) => {
601 // change button to show the message was sent
602 btnEl.classList.add('btn-success');
603 btnEl.classList.remove('btn-primary');
604 btnEl.innerHTML = '<?php VikBookingIcons::e('check'); ?> ' + <?php echo json_encode(JText::translate('VBO_SENT')); ?>;
605 },
606 (error) => {
607 alert(error.responseText || 'An error occurred.');
608 btnEl.disabled = false;
609 icnEl.setAttribute('class', '');
610 icnEl.classList.add(...String(icnClass).split(' '));
611 }
612 );
613 });
614 modalBtnsEl.append(mailBtnEl);
615
616 // modal send via whatsapp button
617 let messagingBtnEl = document.createElement('button');
618 messagingBtnEl.setAttribute('type', 'button');
619 messagingBtnEl.classList.add('btn', 'vbo-stacked-btn', 'vbo-green-icon-btn');
620 messagingBtnEl.innerHTML = '<?php VikBookingIcons::e('fab fa-whatsapp', 'vbo-enabled-icon'); ?> WhatsApp';
621 messagingBtnEl.addEventListener('click', () => {
622 // build modal buttons
623 let cancelBtn = document.createElement('button');
624 cancelBtn.setAttribute('type', 'button');
625 cancelBtn.classList.add('btn');
626 cancelBtn.textContent = <?php echo json_encode(JText::translate('VBANNULLA')); ?>;
627 cancelBtn.addEventListener('click', () => {
628 VBOCore.emitEvent('wquote-choose-macc-data-dismiss');
629 });
630 let sendBtn = document.createElement('button');
631 sendBtn.setAttribute('type', 'button');
632 sendBtn.classList.add('btn', 'btn-primary');
633 sendBtn.innerHTML = '<?php VikBookingIcons::e('paper-plane'); ?> ' + <?php echo json_encode(JText::translate('VBO_SEND_MESSAGE')); ?>;
634 sendBtn.addEventListener('click', () => {
635 // gather messaging account configuration to send
636 let configSelEl = document.querySelector('select[data-field="send-ma-tpl"][data-active-choice="1"]');
637 if (!configSelEl || !configSelEl.value) {
638 alert('Please select a valid messaging account configuration.');
639 return;
640 }
641
642 // obtain selected template details
643 let tplIdentifierParts = configSelEl.value.split(':');
644
645 // dismiss modal to choose the messaging account configuration
646 VBOCore.emitEvent('wquote-choose-macc-data-dismiss');
647
648 // dismiss modal to choose the sending method
649 VBOCore.emitEvent('wquote-choose-send-method-dismiss');
650
651 // start loading
652 btnEl.disabled = true;
653 icnEl.setAttribute('class', '');
654 icnEl.classList.add(...String('<?php echo VikBookingIcons::i('circle-notch', 'fa-spin fa-fw'); ?>').split(' '));
655
656 // make the request
657 VBOCore.doAjax(
658 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=quote.sendMessaging'); ?>",
659 {
660 account_id: tplIdentifierParts[0],
661 phone_id: tplIdentifierParts[1],
662 config_id: tplIdentifierParts[2],
663 quote_id: quoteId,
664 },
665 (response) => {
666 // change button to show the message was sent
667 btnEl.classList.add('btn-success');
668 btnEl.classList.remove('btn-primary');
669 btnEl.innerHTML = '<?php VikBookingIcons::e('check'); ?> ' + <?php echo json_encode(JText::translate('VBO_SENT')); ?>;
670 },
671 (error) => {
672 alert(error.responseText || 'An error occurred.');
673 btnEl.disabled = false;
674 icnEl.setAttribute('class', '');
675 icnEl.classList.add(...String(icnClass).split(' '));
676 }
677 );
678 });
679
680 // display modal for choosing the messaging configuration data to use for sending the quote
681 let messagingDataBody = VBOCore.displayModal({
682 suffix: 'wquote-choose-macc-data',
683 extra_class: 'vbo-modal-rounded',
684 title: <?php echo json_encode(sprintf('%s #{id} - %s', JText::translate('VBO_BTYPE_QUOTE'), JText::translate('VBO_SEND_MESSAGE'))); ?>.replace('{id}', quoteId),
685 draggable: false,
686 footer_left: cancelBtn,
687 footer_right: sendBtn,
688 dismiss_event: 'wquote-choose-macc-data-dismiss',
689 loading_event: 'wquote-choose-macc-data-loading',
690 });
691
692 // start loading
693 VBOCore.emitEvent('wquote-choose-macc-data-loading');
694
695 // make the request to load all messaging configurations data
696 VBOCore.doAjax(
697 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=quote.getMessagingConfigurations'); ?>",
698 {
699 quote_id: quoteId,
700 },
701 (response) => {
702 if (!Array.isArray(response?.data) || !response.data.length) {
703 alert('No messaging accounts configured through the Channel Manager.');
704 VBOCore.emitEvent('wquote-choose-macc-data-dismiss');
705 return;
706 }
707 // stop loading
708 VBOCore.emitEvent('wquote-choose-macc-data-loading');
709 // save response data
710 const maConfigs = response.data;
711 // clone helper
712 const cloneHelper = wrapperEl.querySelector('.vbo-w-quote-send-tplmessage').cloneNode(true);
713 const clonedSelEl = cloneHelper.querySelector('select[data-field="send-ma-tpl"]');
714 const previewEl = cloneHelper.querySelector('.vbo-ma-send-tplmessage-preview');
715 // identify select element as active choice
716 clonedSelEl.setAttribute('data-active-choice', 1);
717 // set messaging account configuration options
718 maConfigs.forEach((maConfig, maIndex) => {
719 let configOptEl = document.createElement('option');
720 configOptEl.value = maConfig?.identifier;
721 configOptEl.textContent = (maConfig?.name || '') + ' (' + (maConfig?.lang || '?') + ')';
722 if (!maIndex) {
723 configOptEl.selected = true;
724 }
725 clonedSelEl.append(configOptEl);
726 });
727 // register change event for the template preview
728 clonedSelEl.addEventListener('change', (e) => {
729 const configId = e.target.value;
730 let configFound = false;
731 previewEl.innerHTML = '';
732 maConfigs.forEach((maConfig) => {
733 if (configFound) {
734 return;
735 }
736 if (maConfig.identifier == configId) {
737 // update preview content and turn flag on
738 configFound = true;
739 previewEl.innerHTML = maConfig?.preview_html || '';
740 }
741 });
742 });
743 // append helper to modal body
744 (messagingDataBody[0] || messagingDataBody).append(cloneHelper);
745 // trigger change event to load the first preview
746 clonedSelEl.dispatchEvent(new Event('change'));
747 },
748 (error) => {
749 alert(error.responseText || 'An error occurred.');
750 VBOCore.emitEvent('wquote-choose-macc-data-dismiss');
751 }
752 );
753 });
754 modalBtnsEl.append(messagingBtnEl);
755
756 // display modal for choosing the quote sending method (stacked buttons)
757 VBOCore.displayModal({
758 suffix: 'wquote-choose-send-method',
759 extra_class: 'vbo-modal-rounded vbo-modal-choice vbo-modal-footer-stacked',
760 title: <?php echo json_encode(JText::translate('VBO_CHOOSE_SEND_METHOD')); ?>,
761 body: <?php echo json_encode(JText::translate('VBO_HOW_SEND_MESSAGE')); ?>,
762 draggable: false,
763 lock_scroll: true,
764 footer_center: modalBtnsEl,
765 dismiss_event: 'wquote-choose-send-method-dismiss',
766 });
767 });
768 });
769
770 // check if we have a button to go back and see all quotes (booking ID injected)
771 wrapperEl.querySelector('.vbo-w-list-back')?.addEventListener('click', () => {
772 const listEl = wrapperEl?.querySelector('.vbo-w-quote-items-list');
773 // set counters to force the loading of the first page (0 will be increased to 1)
774 listEl.setAttribute('data-page-number', -1);
775 // make sure to use a value greater than zero
776 listEl.setAttribute('data-pages-count', 1);
777 // empty the list of quote items
778 listEl.querySelectorAll('.vbo-w-quote-item').forEach((quoteItem) => {
779 quoteItem.remove();
780 });
781 // get rid of the see all button
782 wrapperEl.querySelector('.vbo-w-list-back').remove();
783 // re-load records from the first page
784 vboWidgetQuotesLoadNextPage(wrapper, true);
785 });
786 }
787
788 </script>
789 <?php
790 }
791 ?>
792
793 <script type="text/javascript">
794
795 VBOCore.DOMLoaded(() => {
796
797 // listen to the manual load-more button
798 document.getElementById('<?php echo $wrapper_id; ?>').querySelector('.vbo-widget-quotes-loadmore-manual')?.addEventListener('click', (e) => {
799 // load the next page of records
800 vboWidgetQuotesLoadNextPage('<?php echo $wrapper_id; ?>');
801 });
802
803 // set up infinite scroll loading
804 vboWidgetQuotesSetupInfiniteScroll('<?php echo $wrapper_id; ?>');
805
806 // set up quotes click listeners
807 vboWidgetQuotesRegisterClickListeners('<?php echo $wrapper_id; ?>');
808
809 });
810
811 </script>
812
813 <?php
814 }
815
816 /**
817 * Given a list of quote objects, builds and returns the HTML rendering code.
818 *
819 * @param array $quotes List of quote objects to render.
820 * @param int $page_num Optional page number.
821 *
822 * @return string
823 */
824 protected function buildQuotesHTML(array $quotes, int $page_num = 0)
825 {
826 if (!$quotes) {
827 return '';
828 }
829
830 // start output buffering
831 ob_start();
832
833 foreach ($quotes as $quote) {
834 // tell if the quote is expired
835 $isExpired = false;
836 if (!empty($quote->valid_until) && JFactory::getDate($quote->valid_until)->getTimestamp() < time()) {
837 $isExpired = true;
838 }
839 ?>
840 <div class="vbo-w-quote-item" data-quote-id="<?php echo $quote->id; ?>">
841
842 <div class="vbo-w-quote-item-head">
843 <div class="vbo-w-quote-item-head-left">
844 <h4><?php
845 if ($quote->preferred) {
846 ?>
847 <span class="vbo-w-quote-preferred"><?php VikBookingIcons::e('star', 'icn-nomargin vbo-yellow'); ?></span>
848 <?php
849 }
850 echo $quote->name;
851 ?></h4>
852 <div class="vbo-w-quote-creation-dt">
853 <span><?php echo JHtml::fetch('date', $quote->created_on, 'd M Y H:i'); ?></span>
854 </div>
855 <div class="vbo-w-quote-customer">
856 <span class="vbo-w-quote-customer-name"><?php VikBookingIcons::e('user'); ?> <?php echo trim(sprintf('%s %s', (string) $quote->first_name, (string) $quote->last_name)); ?></span>
857 <?php
858 if (!empty($quote->email)) {
859 ?>
860 <span class="vbo-w-quote-customer-email"><?php VikBookingIcons::e('envelope'); ?> <?php echo $quote->email; ?></span>
861 <?php
862 }
863 if (!empty($quote->phone)) {
864 ?>
865 <span class="vbo-w-quote-customer-phone"><a href="tel:<?php echo preg_replace('/[^0-9\+]+/', '', $quote->phone); ?>"><?php VikBookingIcons::e('phone'); ?> <?php echo $quote->phone; ?></a></span>
866 <?php
867 }
868 ?>
869 </div>
870 </div>
871 <div class="vbo-w-quote-item-head-right" data-section="status">
872 <span class="vbo-w-quote-item-head-validity<?php echo $isExpired ? ' text-red' : ''; ?>"><?php echo sprintf('%s %s', JText::translate('VBO_EXPIRES'), ($quote->valid_until ? JHtml::fetch('date', $quote->valid_until, 'd M Y') : '----')); ?></span>
873 <?php
874 if ($quote->viewed) {
875 ?>
876 <span class="badge-medium badge-transp-blue vbo-bold vbo-w-quote-item-head-status"><?php VikBookingIcons::e('check-double'); ?> <?php echo JText::translate('VBO_OPENED'); ?></span>
877 <?php
878 if (!empty($quote->email) || !empty($quote->phone)) {
879 ?>
880 <button type="button" class="btn btn-small vbo-w-quote-item-head-status vbo-w-quote-send-btn"><?php VikBookingIcons::e('paper-plane'); ?> <?php echo JText::translate('VBO_SEND'); ?></button>
881 <?php
882 }
883 } elseif ($quote->sent) {
884 ?>
885 <button type="button" class="btn btn-primary btn-small vbo-w-quote-item-head-status vbo-w-quote-send-btn"><?php VikBookingIcons::e('check'); ?> <?php echo JText::translate('VBO_SENT'); ?></button>
886 <?php
887 } elseif (!empty($quote->email) || !empty($quote->phone)) {
888 ?>
889 <button type="button" class="btn btn-small vbo-w-quote-item-head-status vbo-w-quote-send-btn"><?php VikBookingIcons::e('paper-plane'); ?> <?php echo JText::translate('VBO_SEND'); ?></button>
890 <?php
891 }
892 ?>
893 <span class="vbo-w-quote-item-head-edit"><?php VikBookingIcons::e('pencil-alt', 'icn-nomargin'); ?></span>
894 </div>
895 </div>
896
897 <div class="vbo-w-quote-item-body">
898 <?php
899 foreach ($quote->solutions as $indexSol => $solution) {
900 // count booking solution values
901 $totRooms = count($solution->rooms);
902 $totAdults = array_sum(array_column($solution->rooms, 'adults'));
903 $totChildren = array_sum(array_column($solution->rooms, 'children'));
904 ?>
905 <div class="vbo-w-quote-solution" data-booking-id="<?php echo $solution->id; ?>">
906 <div class="vbo-w-quote-solution-roominfo">
907 <div class="vbo-w-quote-solution-title">
908 <h4><?php echo sprintf('%s #%d', JText::translate('VBO_OPTION'), ++$indexSol); ?></h4>
909 <a class="badge badge-info vbo-w-quote-solution-revid"><?php VikBookingIcons::e('external-link'); ?> <?php echo sprintf('#%d', $solution->id); ?></a>
910 <div class="vbo-w-quote-solution-bookdates">
911 <?php VikBookingIcons::e('calendar'); ?>
912 <span><?php echo JHtml::fetch('date', date('Y-m-d', $solution->checkin), 'd M Y'); ?></span>
913 <?php VikBookingIcons::e('arrow-right'); ?>
914 <span><?php echo JHtml::fetch('date', date('Y-m-d', $solution->checkout), 'd M Y'); ?></span>
915 </div>
916 </div>
917 <div class="vbo-w-quote-solution-infoparty">
918 <span><?php VikBookingIcons::e('bed'); ?> <?php echo sprintf('%d %s', $totRooms, JText::translate($totRooms == 1 ? 'VBEDITORDERTHREE' : 'VBPVIEWORDERSTHREE')); ?></span>
919 <span><?php VikBookingIcons::e('male'); ?> <?php echo sprintf('%d %s', $totAdults, JText::translate($totAdults == 1 ? 'VBMAILADULT' : 'VBMAILADULTS')); ?></span>
920 <span><?php VikBookingIcons::e('baby'); ?> <?php echo sprintf('%d %s', $totChildren, JText::translate($totChildren == 1 ? 'VBMAILCHILD' : 'VBMAILCHILDREN')); ?></span>
921 </div>
922 <div class="vbo-w-quote-solution-rooms">
923 <?php
924 foreach ($solution->rooms as $solutionRoom) {
925 ?>
926 <span class="badge-small"><?php echo $solutionRoom->name; ?></span>
927 <?php
928 }
929 ?>
930 </div>
931 </div>
932 <div class="vbo-w-quote-solution-priceinfo-wrap">
933 <div class="vbo-w-quote-solution-priceinfo">
934 <div class="vbo-w-quote-solution-price"><?php echo VikBooking::formatCurrencyNumber(VikBooking::numberFormat($solution->total), VikBooking::getCurrencySymb()); ?></div>
935 <div class="vbo-w-quote-solution-info">
936 <?php
937 if ($solution->status == 'confirmed') {
938 ?>
939 <span class="badge-medium badge-icon badge-success vbo-bold"><?php VikBookingIcons::e('check'); ?> <?php echo JText::translate('VBCONFIRMED'); ?></span>
940 <?php
941 } elseif ($solution->status == 'standby') {
942 ?>
943 <span class="badge-medium badge-icon badge-warning vbo-bold"><?php VikBookingIcons::e('clock'); ?> <?php echo JText::translate('VBSTANDBY'); ?></span>
944 <?php
945 } elseif ($solution->status == 'cancelled') {
946 ?>
947 <span class="badge-medium badge-icon badge-error vbo-bold"><?php VikBookingIcons::e('ban'); ?> <?php echo JText::translate('VBCANCELLED'); ?></span>
948 <?php
949 }
950 ?>
951 </div>
952 </div>
953 <div class="vbo-w-quote-solution-edit">
954 <?php VikBookingIcons::e('pencil-alt'); ?>
955 </div>
956 </div>
957 </div>
958 <?php
959 }
960 ?>
961 </div>
962
963 </div>
964 <?php
965 }
966
967 // get the HTML buffer
968 $output = ob_get_contents();
969 ob_end_clean();
970
971 return $output;
972 }
973 }
974