PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.8
VikBooking Hotel Booking Engine & PMS v1.8.8
1.8.16 1.8.15 1.8.14 1.8.13 1.8.12 1.8.11 1.8.10 1.8.9 1.8.6 1.8.7 1.8.8 trunk 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 All 37 releases
vikbooking / admin / layouts / quote / manage / script.php

script.php in VikBooking Hotel Booking Engine & PMS 1.8.8, at admin/layouts/quote/manage/script.php

1,336 lines 71.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package VikBooking
4 * @subpackage core
5 * @author E4J s.r.l.
6 * @copyright Copyright (C) 2026 E4J s.r.l. All Rights Reserved.
7 * @license http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL
8 * @link https://vikwp.com
9 */
10
11 // No direct access
12 defined('ABSPATH') or die('No script kiddies please!');
13
14 /**
15 * Obtain vars from arguments received in the layout file.
16 *
17 * @var ?object $quote Optional quote record to edit.
18 * @var array $customer Optional customer default values.
19 * @var array $inquiry Optional inquiry values to populate.
20 * @var ?int $session_id Optional Chat Session ID source.
21 * @var array $prefMessages Optional list of preferred message objects.
22 */
23 extract($displayData);
24
25 // load scripts
26 $vbo_app = VikBooking::getVboApplication();
27 $vbo_app->loadVisualEditorAssets();
28 $vbo_app->loadDatePicker();
29 $vbo_app->loadDatesRangePicker();
30
31 // load environment data
32 $currencysymb = VikBooking::getCurrencySymb();
33 $currencypos = VikBooking::getCurrencyPosition();
34 list($currency_digits, $currency_decimals, $currency_thousands) = explode(':', VikBooking::getNumberFormatData());
35
36 ?>
37 <script>
38 VBOCore.DOMLoaded(() => {
39
40 /**
41 * Whether we are in edit-mode for an existing quote.
42 */
43 const quoteEditMode = <?php echo $quote ? 'true' : 'false'; ?>;
44
45 /**
46 * The current inquiry payload to start populating values.
47 */
48 const inquiryData = <?php echo $inquiry ? json_encode($inquiry) : 'null'; ?>;
49
50 /**
51 * List of preferred message objects.
52 */
53 const prefMessages = <?php echo json_encode($prefMessages); ?>;
54
55 /**
56 * Configure currency object.
57 */
58 VBOCore.getCurrency({
59 symbol: <?php echo json_encode($currencysymb) ?: '"$"'; ?>,
60 position: <?php echo json_encode($currencypos); ?>,
61 digits: <?php echo intval($currency_digits); ?>,
62 decimals: <?php echo json_encode($currency_decimals) ?: '"."'; ?>,
63 thousands: <?php echo json_encode($currency_thousands) ?: '","'; ?>,
64 noDecimals: 1,
65 });
66
67 /**
68 * Register global event delegation on clicks for any quote option (hidden in edit-mode).
69 */
70 const quoteOptions = document.querySelector('.vbo-quote-section-body[data-section="options"]');
71 quoteOptions.addEventListener('click', (e) => {
72
73 /**
74 * Datepicker check-in trigger icon.
75 */
76 if (e.target.matches('.checkindate-trig') || e.target.closest('.checkindate-trig')) {
77 let btnElement = e.target.matches('.checkindate-trig') ? e.target : e.target.closest('.checkindate-trig');
78 let inputElement = btnElement.previousElementSibling;
79 if (inputElement && inputElement.classList.contains('hasDatepicker')) {
80 inputElement.focus();
81 }
82
83 // do not proceed
84 return;
85 }
86
87 /**
88 * Datepicker check-out trigger icon.
89 */
90 if (e.target.matches('.checkoutdate-trig') || e.target.closest('.checkoutdate-trig')) {
91 let btnElement = e.target.matches('.checkoutdate-trig') ? e.target : e.target.closest('.checkoutdate-trig');
92 let inputElement = btnElement.previousElementSibling;
93 if (inputElement) {
94 // find the check-in input field
95 let checkinEl = inputElement.closest('.vbo-quote-option-dates')?.querySelector('input[data-field="checkin"]');
96 if (checkinEl) {
97 checkinEl.focus();
98 }
99 }
100
101 // do not proceed
102 return;
103 }
104
105 /**
106 * Remove quote option button.
107 */
108 if (e.target.matches('.vbo-quote-option-remove-btn') || e.target.closest('.vbo-quote-option-remove-btn')) {
109 e.target.closest('.vbo-quote-new-option-wrap').remove();
110 // re-number every quote option
111 document.querySelectorAll('.vbo-quote-new-option-wrap').forEach((el, index) => {
112 el.querySelector('.vbo-quote-option-number').textContent = ++index;
113 });
114
115 // do not proceed
116 return;
117 }
118
119 /**
120 * Remove quote option room button.
121 */
122 if (e.target.matches('.vbo-quote-option-room-remove-btn') || e.target.closest('.vbo-quote-option-room-remove-btn')) {
123 let quoteOptEl = e.target.closest('.vbo-quote-new-option-wrap');
124 e.target.closest('.vbo-quote-option-new-room-wrap').remove();
125 // re-number every quote option room
126 let quoteOptRoomEl = quoteOptEl.querySelectorAll('.vbo-quote-option-new-room-wrap');
127 quoteOptRoomEl.forEach((el, index) => {
128 el.querySelector('.vbo-quote-option-room-number').textContent = ++index;
129 });
130 if (!quoteOptRoomEl.length) {
131 // hide total element
132 quoteOptEl.querySelector('.vbo-quote-option-total').style.display = 'none';
133 }
134 // set quote-option room counter
135 quoteOptEl.querySelector('[data-counter="rooms"]').textContent = quoteOptRoomEl.length;
136 // update quote-option guests counter
137 setTotalGuestsCount(quoteOptEl);
138 // update solution total amount
139 countQuoteTotal(quoteOptEl);
140
141
142 // do not proceed
143 return;
144 }
145
146 /**
147 * Remove quote option room extra-service button.
148 */
149 if (e.target.matches('.vbo-quote-option-extra-remove-btn') || e.target.closest('.vbo-quote-option-extra-remove-btn')) {
150 let quoteOptEl = e.target.closest('.vbo-quote-new-option-wrap');
151 e.target.closest('.vbo-quote-option-room-new-extra-wrap').remove();
152 // update solution total amount
153 countQuoteTotal(quoteOptEl);
154
155 // do not proceed
156 return;
157 }
158
159 /**
160 * Add-new-quote-option-room button.
161 */
162 if (e.target.matches('.vbo-quote-option-room-add-btn') || e.target.closest('.vbo-quote-option-room-add-btn')) {
163 const quoteOptEl = e.target.closest('.vbo-quote-new-option-wrap');
164 const quoteOptTarget = quoteOptEl?.querySelector('.vbo-quote-option-rooms');
165 // count new option room number
166 const quoteOptRoomNum = quoteOptTarget.querySelectorAll('.vbo-quote-option-new-room-wrap').length + 1;
167 // display total element
168 quoteOptEl.querySelector('.vbo-quote-option-total').style.display = '';
169 // clone helper nodes
170 const cloneEl = document.querySelector('.vbo-quote-helper').querySelector('.vbo-quote-option-new-room-wrap').cloneNode(true);
171 // append cloned nodes
172 quoteOptTarget.appendChild(cloneEl);
173 // set quote-option room number and counter
174 cloneEl.querySelector('.vbo-quote-option-room-number').textContent = quoteOptRoomNum;
175 quoteOptEl.querySelector('[data-counter="rooms"]').textContent = quoteOptRoomNum;
176 // update quote-option guests counter
177 setTotalGuestsCount(quoteOptEl);
178
179 // listen to the change event for the room select element
180 cloneEl.querySelector('select[data-field="listing"]').addEventListener('change', (e) => {
181 // access currently selected listing and stay dates
182 const listingSelEl = e.target;
183 let listingId = listingSelEl.value;
184 let quoteOptionEl = listingSelEl.closest('.vbo-quote-new-option-wrap');
185 let checkinDate = quoteOptionEl.querySelector('input[data-field="checkin"]')?.value;
186 let checkoutDate = quoteOptionEl.querySelector('input[data-field="checkout"]')?.value;
187 let optionRoomEl = listingSelEl.closest('.vbo-quote-option-new-room-wrap');
188 let quoteOptionAvailEl = optionRoomEl.querySelector('.vbo-quote-option-availability').querySelector('span');
189 if (!listingId || !checkinDate || !checkoutDate) {
190 // check nothing
191 quoteOptionAvailEl.textContent = '';
192 return;
193 }
194 // check availability
195 VBOCore.doAjax(
196 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=isroombookable'); ?>",
197 {
198 rid: listingId,
199 fdate: checkinDate,
200 tdate: checkoutDate,
201 },
202 (response) => {
203 if (response?.status == 1) {
204 // room is available on the selected dates
205 quoteOptionAvailEl.classList.remove('vbo-message-error');
206 quoteOptionAvailEl.classList.add('vbo-message-success');
207 quoteOptionAvailEl.textContent = <?php echo json_encode(JText::translate('VBO_AV_CONFIRMED_DATES')); ?>;
208 } else {
209 // room is NOT available on the selected dates
210 quoteOptionAvailEl.classList.remove('vbo-message-success');
211 quoteOptionAvailEl.classList.add('vbo-message-error');
212 if (response?.err) {
213 quoteOptionAvailEl.textContent = response.err;
214 } else {
215 quoteOptionAvailEl.textContent = 'N/A';
216 }
217 // reset selection
218 listingSelEl.value = '';
219 }
220 // load room rates
221 fetchRatePlans(quoteOptionEl, optionRoomEl);
222 },
223 (error) => {
224 // reset selection
225 listingSelEl.value = '';
226 // display message
227 quoteOptionAvailEl.classList.add('vbo-message-error');
228 quoteOptionAvailEl.textContent = '';
229 alert(error.responseText || 'Request for checking the availability failed.');
230 }
231 );
232 });
233
234 // listen to the change event for the room rate select element
235 cloneEl.querySelector('select[data-field="roomrate"]').addEventListener('change', (e) => {
236 // update solution total amount
237 countQuoteTotal(e.target.closest('.vbo-quote-new-option-wrap'));
238 });
239
240 // listen to the input event for the room custom rate element
241 cloneEl.querySelector('input[data-field="customrate"]').addEventListener('input', VBOCore.debounceEvent((e) => {
242 let parentContainer = e.target.closest('.vbo-quote-option-room-value[data-type="customrate"]');
243 let customRateTaxEl = parentContainer.querySelector('.vbo-quote-customrate-idtax');
244 if (customRateTaxEl) {
245 // show or hide the tax rate element for the custom rate
246 customRateTaxEl.style.display = e.target?.value ? '' : 'none';
247 }
248 // update solution total amount
249 countQuoteTotal(e.target.closest('.vbo-quote-new-option-wrap'));
250 }, 200));
251
252 // listen to the input event for the adults input element
253 cloneEl.querySelector('input[data-field="adults"]').addEventListener('input', VBOCore.debounceEvent((e) => {
254 let quoteOptionEl = e.target?.closest('.vbo-quote-new-option-wrap');
255 let optionRoomEl = e.target?.closest('.vbo-quote-option-new-room-wrap');
256 // update total guests counter
257 setTotalGuestsCount(quoteOptionEl);
258 // load room rates
259 fetchRatePlans(quoteOptionEl, optionRoomEl);
260 }, 200));
261
262 // listen to the input event for the children input element
263 cloneEl.querySelector('input[data-field="children"]').addEventListener('input', VBOCore.debounceEvent((e) => {
264 let quoteOptionEl = e.target?.closest('.vbo-quote-new-option-wrap');
265 let optionRoomEl = e.target?.closest('.vbo-quote-option-new-room-wrap');
266 // update total guests counter
267 setTotalGuestsCount(quoteOptionEl);
268 // load room rates
269 fetchRatePlans(quoteOptionEl, optionRoomEl);
270 }, 200));
271
272 // listen to the input event for every room option/extra with multiple quantity
273 cloneEl.querySelectorAll('input[type="number"][data-field="option"]').forEach((roomOptExtraEl) => {
274 roomOptExtraEl.addEventListener('input', VBOCore.debounceEvent((e) => {
275 // update solution total amount
276 countQuoteTotal(e.target.closest('.vbo-quote-new-option-wrap'));
277 }, 200));
278 });
279
280 // listen to the change event for every room option/extra with single quantity
281 cloneEl.querySelectorAll('input[type="checkbox"][data-field="option"]').forEach((roomOptExtraEl) => {
282 roomOptExtraEl.addEventListener('change', (e) => {
283 // update solution total amount
284 countQuoteTotal(e.target.closest('.vbo-quote-new-option-wrap'));
285 });
286 });
287
288 // make every room option/extra label click-able with proper for/id attributes
289 cloneEl.querySelectorAll('.vbo-editbooking-room-service[data-eligible]').forEach((roomOptWrapEl) => {
290 let parentEl = roomOptWrapEl.closest('.vbo-quote-new-option-wrap');
291 let labelEl = roomOptWrapEl.querySelector('.vbo-editbooking-room-service-inner').querySelector('label');
292 let inputEl = roomOptWrapEl.querySelector('.vbo-editbooking-room-service-check').querySelector('input');
293 let inputOptId = inputEl?.getAttribute('data-option-id');
294 if (!inputEl || !inputOptId) {
295 // we ignore select elements for children age as there could be more than one element
296 return;
297 }
298 let solutionIndex = [...parentEl.parentNode.children].indexOf(parentEl) + 1;
299 let attrValue = 'room-sol-opt-inp-' + solutionIndex + '-' + quoteOptRoomNum + '-' + inputOptId;
300 // set proper attributes
301 labelEl.setAttribute('for', attrValue);
302 inputEl.setAttribute('id', attrValue);
303 });
304
305 // do not proceed
306 return;
307 }
308
309 /**
310 * Add-new-quote-option-room-extra button.
311 */
312 if (e.target.matches('.vbo-quote-option-room-extra-add-btn') || e.target.closest('.vbo-quote-option-room-extra-add-btn')) {
313 const quoteOptRoomEl = e.target.closest('.vbo-quote-option-new-room-wrap');
314 const quoteOptTarget = quoteOptRoomEl?.querySelector('.vbo-quote-option-room-extras-list');
315 // clone helper nodes
316 const cloneEl = document.querySelector('.vbo-quote-helper').querySelector('.vbo-quote-option-room-new-extra-wrap').cloneNode(true);
317 // append cloned nodes
318 quoteOptTarget.appendChild(cloneEl);
319
320 // listen to the input event for the room extra service name (name is required for the cost to be applied)
321 cloneEl.querySelector('input[data-field="extra-name"]').addEventListener('input', VBOCore.debounceEvent((e) => {
322 // update solution total amount
323 countQuoteTotal(e.target.closest('.vbo-quote-new-option-wrap'));
324 }, 200));
325
326 // listen to the input event for the room extra service cost
327 cloneEl.querySelector('input[data-field="extra-cost"]').addEventListener('input', VBOCore.debounceEvent((e) => {
328 // update solution total amount
329 countQuoteTotal(e.target.closest('.vbo-quote-new-option-wrap'));
330 }, 200));
331
332 // do not proceed
333 return;
334 }
335
336 });
337
338 /**
339 * Register click event on add-new-quote-option button (hidden in edit-mode).
340 */
341 let quoteOptCounter = 0;
342 document.querySelector('.vbo-quote-option-add-btn')?.addEventListener('click', () => {
343 // increase global counter (that should never be decreased to avoid conflicts with date-picker calendars)
344 quoteOptCounter++;
345 // count new option number
346 const quoteOptNum = quoteOptions.querySelectorAll('.vbo-quote-new-option-wrap').length + 1;
347 // clone helper nodes
348 const cloneEl = document.querySelector('.vbo-quote-helper').querySelector('.vbo-quote-new-option-wrap').cloneNode(true);
349 // set active attribute
350 cloneEl.setAttribute('data-active', 1);
351 // append cloned nodes
352 quoteOptions.appendChild(cloneEl);
353 // set option number
354 cloneEl.querySelector('.vbo-quote-option-number').textContent = quoteOptNum;
355 // get date elements
356 const checkinEl = cloneEl.querySelector('input[data-field="checkin"]');
357 const checkoutEl = cloneEl.querySelector('input[data-field="checkout"]');
358 // build and set unique ID attribute
359 let checkinElSel = 'vbo-quote-option-checkin-' + quoteOptCounter;
360 let checkoutElSel = 'vbo-quote-option-checkout-' + quoteOptCounter;
361 checkinEl.setAttribute('id', checkinElSel);
362 checkoutEl.setAttribute('id', checkoutElSel);
363 // set unique "for" attribute to labels
364 checkinEl.closest('.vbo-quote-option-date').querySelector('label').setAttribute('for', checkinElSel);
365 checkoutEl.closest('.vbo-quote-option-date').querySelector('label').setAttribute('for', checkoutElSel);
366 // render datepicker calendars
367 jQuery('#' + checkinElSel).vboDatesRangePicker({
368 checkout: '#' + checkoutElSel,
369 showOn: "focus",
370 minDate: '0d',
371 maxDate: '+2y',
372 dateFormat: 'yy-mm-dd',
373 numberOfMonths: 2,
374 responsiveNumMonths: {
375 threshold: 860,
376 },
377 onSelect: {
378 checkin: (selectedDate) => {
379 let nowcheckin = jQuery('#' + checkinElSel).vboDatesRangePicker('getCheckinDate');
380 let nowcheckindate = new Date(nowcheckin.getTime());
381 nowcheckindate.setDate(nowcheckindate.getDate());
382 jQuery('#' + checkinElSel).vboDatesRangePicker('checkout', 'minDate', nowcheckindate);
383 // calculate nights
384 let totNights = calcNights(checkinEl.value, checkoutEl.value);
385 cloneEl.querySelector('[data-counter="nights"]').textContent = totNights || '0';
386 if (totNights > 0) {
387 // check if any listings were selected
388 cloneEl.querySelectorAll('select[data-field="listing"]').forEach((listingSel) => {
389 if (listingSel.value) {
390 // trigger change event to re-calculate the availability
391 listingSel.dispatchEvent(new Event('change'));
392 }
393 });
394 }
395 },
396 checkout: (selectedDate) => {
397 if (!jQuery('#' + checkinElSel).vboDatesRangePicker('getCheckoutDate')) {
398 return;
399 }
400 // calculate nights
401 let totNights = calcNights(checkinEl.value, checkoutEl.value);
402 cloneEl.querySelector('[data-counter="nights"]').textContent = totNights || '0';
403 if (totNights > 0) {
404 // check if any listings were selected
405 cloneEl.querySelectorAll('select[data-field="listing"]').forEach((listingSel) => {
406 if (listingSel.value) {
407 // trigger change event to re-calculate the availability
408 listingSel.dispatchEvent(new Event('change'));
409 }
410 });
411 }
412 },
413 },
414 labels: {
415 checkin: <?php echo json_encode(JText::translate('VBPICKUPROOM')); ?>,
416 checkout: <?php echo json_encode(JText::translate('VBRETURNROOM')); ?>,
417 },
418 bottomCommands: {
419 clear: <?php echo json_encode(JText::translate('VBO_CLEAR_DATES')); ?>,
420 close: <?php echo json_encode(JText::translate('VBO_CLOSE')); ?>,
421 onClear: () => {
422 // calculate nights
423 let totNights = calcNights(checkinEl.value, checkoutEl.value);
424 cloneEl.querySelector('[data-counter="nights"]').textContent = '0';
425 },
426 },
427 environment: {
428 section: 'admin',
429 autoHide: true,
430 },
431 });
432 });
433
434 /**
435 * Register event listener for the quote customer selection.
436 */
437 document.addEventListener('vbo-quote-customer-chosen', (e) => {
438 if (e?.detail?.element?.id) {
439 if (e?.detail?.element?.first_name) {
440 document.querySelector('input[data-field="customer-firstname"]').value = e.detail.element.first_name;
441 }
442 if (e?.detail?.element?.last_name) {
443 document.querySelector('input[data-field="customer-lastname"]').value = e.detail.element.last_name;
444 }
445 if (e?.detail?.element?.email) {
446 document.querySelector('input[data-field="customer-email"]').value = e.detail.element.email;
447 }
448 if (e?.detail?.element?.phone) {
449 let customerPhoneFieldEl = document.querySelector('input[data-field="customer-phone"]');
450 if (customerPhoneFieldEl) {
451 customerPhoneFieldEl.value = e.detail.element.phone;
452 customerPhoneFieldEl.dispatchEvent(new Event('input'));
453 customerPhoneFieldEl.dispatchEvent(new Event('blur'));
454 }
455 }
456 if (e?.detail?.element?.country) {
457 document.querySelector('input[data-field="customer-country"]').value = e.detail.element.country;
458 }
459 }
460 });
461
462 /**
463 * Register event listener for the "send later" toggle button (if available).
464 */
465 document.querySelector('input[type="checkbox"][name="quote_send_later"]')?.addEventListener('change', (e) => {
466 let submitBtn = document.querySelector('.vbo-quote-submit-btn');
467 let submitIcn = submitBtn.querySelector('i');
468 submitIcn.setAttribute('class', '');
469 if (e.target?.checked) {
470 submitBtn.lastChild.textContent = <?php echo json_encode(' ' . JText::translate('VBO_SAVE_QUOTE')); ?>;
471 submitIcn.classList.add(...String('<?php echo VikBookingIcons::i('save'); ?>').split(' '));
472 } else {
473 submitBtn.lastChild.textContent = <?php echo json_encode(' ' . JText::translate('VBO_SEND_QUOTE')); ?>;
474 submitIcn.classList.add(...String('<?php echo VikBookingIcons::i('paper-plane'); ?>').split(' '));
475 }
476 });
477
478 /**
479 * Register event listener for the selection of a message template (if available).
480 */
481 document.querySelector('select[data-field="message-preferred-tpl"]')?.addEventListener('change', (e) => {
482 let quoteId = e.target.value;
483 let textAreaEl = document.querySelector('textarea#vbo-quote-message-content');
484 if (!quoteId || !textAreaEl || !Array.isArray(prefMessages)) {
485 return;
486 }
487
488 let messageData = null;
489 prefMessages.forEach((prefMessage) => {
490 if (messageData) {
491 return;
492 }
493 if (prefMessage?.id == quoteId) {
494 messageData = prefMessage;
495 }
496 });
497
498 if (messageData) {
499 textAreaEl.value = messageData.message;
500 textAreaEl.dispatchEvent(new Event('change'));
501 document.querySelector('input[data-field="mail-subject"]').value = messageData.subject || '';
502 document.querySelector('textarea[data-field="notes"]').value = messageData.notes || '';
503 let savePreferred = document.querySelector('input[type="checkbox"][name="quote_message_preferred"]');
504 if (savePreferred) {
505 savePreferred.checked = false;
506 }
507 }
508 });
509
510 /**
511 * Register event listener to obtain the selected country data via phone input.
512 */
513 document.addEventListener('vbo-quote-get-phone-country-data', (e) => {
514 if (e?.detail?.iso2) {
515 document.querySelector('input[data-field="customer-country"]').value = e.detail.iso2;
516 }
517 });
518
519 /**
520 * Register event listener for the quote submit (save new) button.
521 */
522 document.querySelector('.vbo-quote-submit-btn')?.addEventListener('click', (e) => {
523 const submitBtn = e.target.matches('button') ? e.target : e.target.closest('button');
524 const submitIcn = submitBtn.querySelector('i');
525 let submitIcnClass = submitIcn.getAttribute('class');
526
527 const optionElements = document.querySelectorAll('.vbo-quote-new-option-wrap[data-active="1"]');
528 if (!optionElements.length) {
529 alert('Please add at least one booking solution.');
530 return false;
531 }
532
533 // quickly validate that we've got at least one valid booking solution
534 let validSolutions = 0;
535 optionElements.forEach((optionEl) => {
536 let checkinDate = optionEl.querySelector('input[data-field="checkin"]')?.value;
537 let checkoutDate = optionEl.querySelector('input[data-field="checkout"]')?.value;
538 if (!checkinDate || !checkoutDate) {
539 return;
540 }
541 optionEl.querySelectorAll('.vbo-quote-option-new-room-wrap').forEach((optionRoomEl) => {
542 let listingId = optionRoomEl.querySelector('select[data-field="listing"]')?.value;
543 if (listingId) {
544 validSolutions++;
545 }
546 });
547 });
548 if (!validSolutions) {
549 alert('Please configure at least one room booking solution.');
550 return false;
551 }
552
553 // disable button and start loading animation
554 submitBtn.disabled = true;
555 submitIcn.setAttribute('class', '');
556 submitIcn.classList.add(...String('<?php echo VikBookingIcons::i('circle-notch', 'fa-spin fa-fw'); ?>').split(' '));
557
558 // collect quotation values
559 let quoteValues = {
560 name: document.querySelector('input[data-field="quote-name"]')?.value,
561 validity: document.querySelector('input[data-field="valid-until"]')?.value,
562 id_payment: document.querySelector('select[data-field="quote-payment"]')?.value,
563 customer: {
564 id: document.querySelector('select[data-field="customer-id"]')?.value,
565 first_name: document.querySelector('input[data-field="customer-firstname"]')?.value,
566 last_name: document.querySelector('input[data-field="customer-lastname"]')?.value,
567 email: document.querySelector('input[data-field="customer-email"]')?.value,
568 phone: document.querySelector('input[data-field="customer-phone"]')?.value,
569 country: document.querySelector('input[data-field="customer-country"]')?.value,
570 },
571 message: {
572 subject: document.querySelector('input[data-field="mail-subject"]')?.value,
573 content: document.querySelector('textarea[name="quote_message_content"]')?.value,
574 notes: document.querySelector('textarea[data-field="notes"]')?.value,
575 send: document.querySelector('input[type="checkbox"][name="quote_send_later"]')?.checked ? 0 : 1,
576 preferred: document.querySelector('input[type="checkbox"][name="quote_message_preferred"]')?.checked ? 1 : 0,
577 },
578 solutions: [],
579 };
580
581 // iterate all booking solutions
582 optionElements.forEach((optionEl) => {
583 let checkinDate = optionEl.querySelector('input[data-field="checkin"]')?.value;
584 let checkoutDate = optionEl.querySelector('input[data-field="checkout"]')?.value;
585 if (!checkinDate || !checkoutDate) {
586 // skip invalid booking solution
587 return;
588 }
589
590 // build booking solution object
591 let bookingSolution = {
592 checkin: checkinDate,
593 checkout: checkoutDate,
594 rooms: [],
595 };
596
597 // iterate all booking rooms
598 optionEl.querySelectorAll('.vbo-quote-option-new-room-wrap').forEach((optionRoomEl) => {
599 let listingId = optionRoomEl.querySelector('select[data-field="listing"]')?.value;
600 if (!listingId) {
601 // skip invalid booking room
602 return;
603 }
604
605 // room rate plan element
606 let roomRateEl = optionRoomEl.querySelector('select[data-field="roomrate"]');
607
608 // build booking room object
609 let bookingRoom = {
610 id: listingId,
611 adults: optionRoomEl.querySelector('input[data-field="adults"]')?.value,
612 children: optionRoomEl.querySelector('input[data-field="children"]')?.value,
613 id_price: roomRateEl?.value,
614 room_cost: roomRateEl?.value ? Number(roomRateEl.options[roomRateEl.selectedIndex].getAttribute('data-cost')) : 0,
615 cust_cost: optionRoomEl.querySelector('input[data-field="customrate"]')?.value,
616 id_tax: optionRoomEl.querySelector('select[data-field="customrate-idtax"]')?.value,
617 options: [],
618 extras: [],
619 };
620
621 // iterate all booking room option records
622 optionRoomEl.querySelectorAll('.vbo-editbooking-room-service[data-eligible="1"]').forEach((optRecordEl) => {
623 let isChildAge = optRecordEl.getAttribute('data-child-age') == 1;
624 // build booking room option object
625 let bookingRoomOption = {
626 id: optRecordEl.getAttribute('data-option-id'),
627 quantity: 0,
628 cost: 0,
629 is_child_age: isChildAge ? 1 : 0,
630 age_intervals: [],
631 };
632
633 if (isChildAge) {
634 // one drop-down select element per child is expected
635 let childAgeFulfilled = true;
636 optRecordEl.querySelectorAll('select[data-field="child-age"]').forEach((childAgeSelEl, childAgeSelIndex) => {
637 if (!childAgeSelEl.value) {
638 childAgeFulfilled = false;
639 return;
640 }
641 // push child age interval value
642 bookingRoomOption.age_intervals.push(childAgeSelEl.value);
643 // check computed cost
644 let childIntervalCost = Number(childAgeSelEl.options[childAgeSelEl.selectedIndex].getAttribute('data-computed-cost'));
645 if (isNaN(childIntervalCost)) {
646 return;
647 }
648 // update option computed cost
649 bookingRoomOption.cost += childIntervalCost;
650 });
651
652 if (childAgeFulfilled === true) {
653 // push booking room option
654 bookingRoom.options.push(bookingRoomOption);
655 }
656 } else {
657 // regular option
658 let checkboxEl = optRecordEl.querySelector('input[type="checkbox"]');
659 let numberEl = optRecordEl.querySelector('input[type="number"]');
660 let priceEl = optRecordEl.querySelector('.vbo-editbooking-room-service-price');
661 let computedCost = 0;
662 if (checkboxEl && checkboxEl.checked) {
663 // single-quantity option checked
664 computedCost = Number(priceEl.getAttribute('data-computed-cost'));
665 // set option quantity
666 bookingRoomOption.quantity = 1;
667 } else if (numberEl && numberEl.value && parseInt(numberEl.value) > 0) {
668 // multiple quantity option fulfilled
669 let quantity = parseInt(numberEl.value);
670 computedCost = Number(priceEl.getAttribute('data-computed-cost'));
671 if (computedCost && !isNaN(computedCost)) {
672 computedCost = computedCost * quantity;
673 }
674 // set option quantity
675 bookingRoomOption.quantity = quantity;
676 }
677 if (computedCost && !isNaN(computedCost)) {
678 // set option computed cost
679 bookingRoomOption.cost = computedCost;
680 }
681
682 if (bookingRoomOption.quantity || bookingRoomOption.cost) {
683 // push booking room option
684 bookingRoom.options.push(bookingRoomOption);
685 }
686 }
687 });
688
689 // iterate all booking room extra services
690 optionRoomEl.querySelectorAll('.vbo-quote-option-room-new-extra-wrap').forEach((extraEl) => {
691 let extraName = extraEl.querySelector('input[data-field="extra-name"]')?.value;
692 let extraCost = Number(extraEl.querySelector('input[data-field="extra-cost"]')?.value);
693 if (!extraName || !extraCost || isNaN(extraCost)) {
694 // ignore when extra service name or cost are missing or invalid
695 return;
696 }
697
698 // push booking room extra service
699 bookingRoom.extras.push({
700 name: extraName,
701 cost: extraCost,
702 vat: extraEl.querySelector('select[data-field="extra-taxrate"]')?.value,
703 });
704 });
705
706 if ((bookingRoom.id_price && bookingRoom.room_cost) || bookingRoom.cust_cost) {
707 // push booking room
708 bookingSolution.rooms.push(bookingRoom);
709 }
710 });
711
712 if (bookingSolution.rooms.length) {
713 // push booking solution
714 quoteValues.solutions.push(bookingSolution);
715 }
716 });
717
718 // make the request to save the quotation record and related booking solutions
719 VBOCore.doAjax(
720 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=quote.save'); ?>",
721 {
722 quote: quoteValues,
723 session_id: <?php echo (int) $session_id; ?>,
724 },
725 (response) => {
726 if (response?.sending_error) {
727 // display an alert message just for the email sending error
728 alert(response.sending_error);
729 }
730 // display toast message on success
731 VBOToast.enqueue(new VBOToastMessage({
732 title: <?php echo json_encode(JText::translate('VBPVIEWORDERSEIGHT')); ?>,
733 body: <?php echo json_encode(JText::translate('VBOCHECKINSTATUSUPDATED')); ?>,
734 status: VBOToast.SUCCESS_STATUS,
735 delay: {
736 min: 6000,
737 max: 20000,
738 tolerance: 4000,
739 },
740 action: () => {
741 VBOToast.dispose(true);
742 }
743 }));
744 // redirect to bookings list on success
745 location.href = '<?php echo VBOFactory::getPlatform()->getUri()->admin('index.php?option=com_vikbooking&task=orders&quote_saved=1', false); ?>';
746 },
747 (error) => {
748 // display error
749 alert(error.responseText || 'Could not complete the operation.');
750 // restore button
751 submitBtn.disabled = false;
752 submitIcn.setAttribute('class', '');
753 submitIcn.classList.add(...submitIcnClass.split(' '));
754 }
755 );
756 });
757
758 /**
759 * Register event listener for the quote update (edit quote) button.
760 */
761 document.querySelector('.vbo-quote-update-btn')?.addEventListener('click', (e) => {
762 const submitBtn = e.target.matches('button') ? e.target : e.target.closest('button');
763 const submitIcn = submitBtn.querySelector('i');
764 let submitIcnClass = submitIcn.getAttribute('class');
765
766 const quoteId = document.querySelector('.vbo-quote-section-current[data-quote-id]')?.getAttribute('data-quote-id');
767 if (!quoteId) {
768 alert('Could not find the quote ID to update.');
769 return false;
770 }
771
772 // disable button and start loading animation
773 submitBtn.disabled = true;
774 submitIcn.setAttribute('class', '');
775 submitIcn.classList.add(...String('<?php echo VikBookingIcons::i('circle-notch', 'fa-spin fa-fw'); ?>').split(' '));
776
777 // collect quotation values for update
778 let quoteValues = {
779 id: quoteId,
780 name: document.querySelector('input[data-field="quote-name"]')?.value,
781 validity: document.querySelector('input[data-field="valid-until"]')?.value,
782 id_payment: document.querySelector('select[data-field="quote-payment"]')?.value,
783 customer: {
784 id: document.querySelector('select[data-field="customer-id"]')?.value,
785 first_name: document.querySelector('input[data-field="customer-firstname"]')?.value,
786 last_name: document.querySelector('input[data-field="customer-lastname"]')?.value,
787 email: document.querySelector('input[data-field="customer-email"]')?.value,
788 phone: document.querySelector('input[data-field="customer-phone"]')?.value,
789 country: document.querySelector('input[data-field="customer-country"]')?.value,
790 },
791 message: {
792 subject: document.querySelector('input[data-field="mail-subject"]')?.value,
793 content: document.querySelector('textarea[name="quote_message_content"]')?.value,
794 notes: document.querySelector('textarea[data-field="notes"]')?.value,
795 preferred: document.querySelector('input[type="checkbox"][name="quote_message_preferred"]')?.checked ? 1 : 0,
796 },
797 };
798
799 // make the request to update the current quotation record
800 VBOCore.doAjax(
801 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=quote.update'); ?>",
802 {
803 quote: quoteValues,
804 },
805 (response) => {
806 // display toast message on success
807 VBOToast.enqueue(new VBOToastMessage({
808 title: <?php echo json_encode(JText::translate('VBPVIEWORDERSEIGHT')); ?>,
809 body: <?php echo json_encode(JText::translate('VBOCHECKINSTATUSUPDATED')); ?>,
810 status: VBOToast.SUCCESS_STATUS,
811 delay: {
812 min: 6000,
813 max: 20000,
814 tolerance: 4000,
815 },
816 action: () => {
817 VBOToast.dispose(true);
818 }
819 }));
820 // reload the page on success
821 location.reload();
822 },
823 (error) => {
824 // display error
825 alert(error.responseText || 'Could not complete the operation.');
826 // restore button
827 submitBtn.disabled = false;
828 submitIcn.setAttribute('class', '');
829 submitIcn.classList.add(...submitIcnClass.split(' '));
830 }
831 );
832 });
833
834 /**
835 * Register event listener for the quote delete (edit quote) button.
836 */
837 document.querySelector('.vbo-quote-delete-btn')?.addEventListener('click', (e) => {
838 const submitBtn = e.target.matches('button') ? e.target : e.target.closest('button');
839 const submitIcn = submitBtn.querySelector('i');
840 let submitIcnClass = submitIcn.getAttribute('class');
841
842 const quoteId = document.querySelector('.vbo-quote-section-current[data-quote-id]')?.getAttribute('data-quote-id');
843 if (!quoteId) {
844 alert('Could not find the quote ID to delete.');
845 return false;
846 }
847
848 if (!confirm(<?php echo json_encode(JText::translate('VBDELCONFIRM')); ?>)) {
849 return false;
850 }
851
852 // disable button and start loading animation
853 submitBtn.disabled = true;
854 submitIcn.setAttribute('class', '');
855 submitIcn.classList.add(...String('<?php echo VikBookingIcons::i('circle-notch', 'fa-spin fa-fw'); ?>').split(' '));
856
857 // make the request to delete the current quotation record
858 VBOCore.doAjax(
859 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=quote.delete'); ?>",
860 {
861 quote_id: quoteId,
862 },
863 (response) => {
864 // display toast message on success
865 VBOToast.enqueue(new VBOToastMessage({
866 title: <?php echo json_encode(JText::translate('VBPVIEWORDERSEIGHT')); ?>,
867 body: <?php echo json_encode(JText::translate('VBOCHECKINSTATUSUPDATED')); ?>,
868 status: VBOToast.SUCCESS_STATUS,
869 delay: {
870 min: 6000,
871 max: 20000,
872 tolerance: 4000,
873 },
874 action: () => {
875 VBOToast.dispose(true);
876 }
877 }));
878 // redirect to bookings list on success
879 location.href = '<?php echo VBOFactory::getPlatform()->getUri()->admin('index.php?option=com_vikbooking&task=orders&quote_deleted=1', false); ?>';
880 },
881 (error) => {
882 // display error
883 alert(error.responseText || 'Could not complete the operation.');
884 // restore button
885 submitBtn.disabled = false;
886 submitIcn.setAttribute('class', '');
887 submitIcn.classList.add(...submitIcnClass.split(' '));
888 }
889 );
890 });
891
892 /**
893 * Register function to calculate the nights between two date strings.
894 *
895 * @param string checkin The check-in date.
896 * @param string checkout The check-out date.
897 *
898 * @return ?number
899 */
900 const calcNights = (checkin, checkout) => {
901 if (!checkin || !checkout) {
902 return null;
903 }
904 let checkinDate = new Date(checkin);
905 let checkoutDate = new Date(checkout);
906 let utc1 = Date.UTC(checkinDate.getFullYear(), checkinDate.getMonth(), checkinDate.getDate());
907 let utc2 = Date.UTC(checkoutDate.getFullYear(), checkoutDate.getMonth(), checkoutDate.getDate());
908
909 return Math.ceil((utc2 - utc1) / (1000 * 60 * 60 * 24));
910 };
911
912 /**
913 * Register function to update the total guests count within a quote option.
914 *
915 * @param Element quoteOptionEl The quote-option element.
916 *
917 * @return undefined
918 */
919 const setTotalGuestsCount = (quoteOptionEl) => {
920 if (!quoteOptionEl) {
921 return;
922 }
923 let guests = 0;
924 quoteOptionEl.querySelectorAll('input[data-field="adults"]').forEach((field) => {
925 guests += !isNaN(field.value) ? parseInt(field.value) : 0;
926 });
927 quoteOptionEl.querySelectorAll('input[data-field="children"]').forEach((field) => {
928 guests += !isNaN(field.value) ? parseInt(field.value) : 0;
929 });
930 quoteOptionEl.querySelector('[data-counter="guests"]').textContent = guests;
931 };
932
933 /**
934 * Register function to update the rate plans for a room within a quote option.
935 * The function will also load the eligible options to show their pricing info.
936 *
937 * @param Element quoteOptionEl The quote-option element.
938 * @param Element optionRoomEl The quote-option-room element.
939 *
940 * @return undefined
941 */
942 const fetchRatePlans = (quoteOptionEl, optionRoomEl) => {
943 if (!quoteOptionEl || !optionRoomEl) {
944 throw new Error('Invalid element arguments provided.');
945 }
946
947 let listingId = optionRoomEl.querySelector('select[data-field="listing"]')?.value;
948 let roomRateEl = optionRoomEl.querySelector('select[data-field="roomrate"]');
949 let rateErrorEl = optionRoomEl.querySelector('.vbo-quote-label-help[data-type="rate-error"]');
950 let checkinDate = quoteOptionEl.querySelector('input[data-field="checkin"]')?.value;
951 let checkoutDate = quoteOptionEl.querySelector('input[data-field="checkout"]')?.value;
952 let previousRateId = roomRateEl?.value;
953 let numAdults = optionRoomEl.querySelector('input[data-field="adults"]')?.value;
954 let numChildren = optionRoomEl.querySelector('input[data-field="children"]')?.value;
955 let numNights = parseInt(quoteOptionEl.querySelector('[data-counter="nights"]')?.innerText || 1);
956
957 // ensure the number of nights of stay is a number
958 numNights = isNaN(numNights) ? 1 : numNights;
959
960 // always reset the rate plan select element
961 roomRateEl.value = '';
962 roomRateEl.querySelectorAll('option').forEach((opt) => {
963 if (!opt.getAttribute('value')) {
964 // keep the empty option
965 return;
966 }
967 opt.remove();
968 });
969
970 // always empty and hide the rate error element
971 rateErrorEl.textContent = '';
972 rateErrorEl.style.display = 'none';
973
974 if (!listingId || !checkinDate || !checkoutDate) {
975 // do nothing
976 return;
977 }
978 // make the request
979 VBOCore.doAjax(
980 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=calc_rates'); ?>",
981 {
982 id_room: listingId,
983 checkinfdate: checkinDate,
984 checkoutfdate: checkoutDate,
985 num_adults: numAdults,
986 num_children: numChildren,
987 units: 1,
988 only_rates: 1,
989 },
990 (response) => {
991 // list of rate plan objects expected in case of success
992 if (!Array.isArray(response) || typeof response[0] === 'string' || (response[1] || 0) == -1) {
993 // error expected, but the drop down element is already empty
994 if (Array.isArray(response) && typeof response[0] === 'string') {
995 // display the error encountered
996 rateErrorEl.textContent = response[0].replace('e4j.error.', '');
997 rateErrorEl.style.display = '';
998 }
999 } else {
1000 // check if rates are inclusive of tax
1001 let ratesTaxInclusive = <?php echo VikBooking::ivaInclusa() ? 1 : 0; ?>;
1002
1003 // populate option elements
1004 response.forEach((ratePlan) => {
1005 let ratePlanOptEl = document.createElement('option');
1006 ratePlanOptEl.setAttribute('value', ratePlan?.idprice);
1007 ratePlanOptEl.setAttribute('data-cost', ratesTaxInclusive ? ratePlan?.tot : ratePlan?.net);
1008 ratePlanOptEl.textContent = ratePlan?.name || 'Rate plan';
1009 ratePlanOptEl.textContent += ' - ' + (ratesTaxInclusive ? ratePlan?.ftot : ratePlan?.fnet);
1010 if (previousRateId && previousRateId == ratePlan?.idprice) {
1011 ratePlanOptEl.selected = true;
1012 }
1013 roomRateEl.append(ratePlanOptEl);
1014 });
1015
1016 // fetch eligible options
1017 VBOCore.doAjax(
1018 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=listings.get_eligible_options'); ?>",
1019 {
1020 listing_id: listingId,
1021 adults: numAdults,
1022 children: numChildren,
1023 checkin: checkinDate,
1024 checkout: checkoutDate,
1025 },
1026 (optionRecords) => {
1027 if (!Array.isArray(optionRecords)) {
1028 // unexpected response format
1029 console.error('listings.get_eligible_options: unexpected response format.', optionRecords);
1030 return;
1031 }
1032
1033 // obtain all the eligible option record IDs
1034 let eligibleIds = [];
1035 optionRecords.forEach((optRecord) => {
1036 eligibleIds.push(Number(optRecord.id));
1037 });
1038
1039 // display or hide the option record elements based on eligibility
1040 optionRoomEl.querySelectorAll('.vbo-editbooking-room-service[data-option-id]').forEach((optEl) => {
1041 let optionRecordId = Number(optEl.getAttribute('data-option-id'));
1042 // check eligibility
1043 if (!optionRecordId || !eligibleIds.includes(optionRecordId)) {
1044 // hide non-eligible element
1045 optEl.style.display = 'none';
1046 optEl.setAttribute('data-eligible', 0);
1047 } else {
1048 // show eligible element
1049 optEl.style.display = '';
1050 optEl.setAttribute('data-eligible', 1);
1051 }
1052 });
1053
1054 // update computed pricing and mandatory selection for every eligible option record
1055 optionRecords.forEach((optRecord) => {
1056 let optEl = optionRoomEl.querySelector('.vbo-editbooking-room-service[data-option-id="' + optRecord.id + '"]');
1057 let isChildAge = optEl.getAttribute('data-child-age') == 1;
1058 let optPriceEl = optEl.querySelector('.vbo-editbooking-room-service-price');
1059 let optInputWrapEl = optEl.querySelector('.vbo-editbooking-room-service-check');
1060 let isMultiQuantity = optInputWrapEl?.getAttribute('data-multiple-quantity') == 1;
1061
1062 // reset price content
1063 optPriceEl.textContent = '';
1064 optPriceEl.setAttribute('data-computed-cost', '');
1065
1066 // handle type of option
1067 if (isChildAge && optRecord?.ageintervals) {
1068 // handle children age intervals element
1069 optInputWrapEl.innerHTML = '';
1070
1071 if (typeof optRecord?._computed_children_costs === 'object' && typeof optRecord?._computed_age_intervals === 'object') {
1072 // loop over all children
1073 for (ch = 1; ch <= Number(numChildren); ch++) {
1074 if (!optRecord._computed_children_costs[ch] || !optRecord._computed_age_intervals[ch]) {
1075 // unknown values for this child number
1076 continue;
1077 }
1078 if (!Array.isArray(optRecord._computed_children_costs[ch]) || !optRecord._computed_children_costs[ch].length) {
1079 // missing cost values for this child number
1080 continue;
1081 }
1082 if (!Array.isArray(optRecord._computed_age_intervals[ch]) || !optRecord._computed_age_intervals[ch].length) {
1083 // missing interval values for this child number
1084 continue;
1085 }
1086
1087 // build drop down element container
1088 let containerEl = document.createElement('div');
1089 containerEl.classList.add('vbo-quote-child-age-wrap');
1090
1091 // build label element
1092 let labelEl = document.createElement('label');
1093 labelEl.textContent = <?php echo json_encode(JText::translate('VBMAILCHILD') . ' #'); ?> + ch;
1094
1095 // build drop down select element
1096 let selectEl = document.createElement('select');
1097 selectEl.setAttribute('data-field', 'child-age');
1098 selectEl.addEventListener('change', (e) => {
1099 // update solution total amount
1100 countQuoteTotal(quoteOptionEl);
1101 });
1102
1103 // scan all child age intervals and related costs
1104 optRecord._computed_age_intervals[ch].forEach((ageInterval, intervalIndex) => {
1105 let ageIntervalCost = Number(optRecord._computed_children_costs[ch][intervalIndex]);
1106 let ageOptEl = document.createElement('option');
1107 ageOptEl.setAttribute('data-computed-cost', ageIntervalCost);
1108 ageOptEl.value = ++intervalIndex;
1109 ageOptEl.textContent = ageInterval + ' (' + VBOCore.getCurrency().format(ageIntervalCost) + ')';
1110 selectEl.append(ageOptEl);
1111 });
1112
1113 // append elements to DOM
1114 containerEl.append(labelEl);
1115 containerEl.append(selectEl);
1116 optInputWrapEl.append(containerEl);
1117 }
1118 }
1119 } else {
1120 // handle regular option record
1121 optPriceEl.innerHTML = VBOCore.getCurrency().format(optRecord?._computed_cost || optRecord.cost);
1122 optPriceEl.setAttribute('data-computed-cost', optRecord?._computed_cost || optRecord.cost);
1123
1124 // check if we have a mandatory fee
1125 if (optRecord?.forcesel == 1) {
1126 // pre-select state
1127 if (isMultiQuantity) {
1128 // input number expected
1129 let forceSettings = (optRecord?.forceval || '').split('-');
1130 let quantity = parseInt(forceSettings[0] || 1);
1131 if (forceSettings[1] && forceSettings[1] == 1) {
1132 // forced quantity to be multiplied per night of stay
1133 quantity = quantity * numNights;
1134 }
1135 if (forceSettings[2] && forceSettings[2] == 1) {
1136 // forced quantity to be multiplied per child
1137 quantity = quantity * Number(numChildren);
1138 }
1139 optInputWrapEl.querySelector('input').value = quantity;
1140 } else {
1141 // checkbox expected
1142 optInputWrapEl.querySelector('input[type="checkbox"]').checked = true;
1143 }
1144 }
1145 }
1146 });
1147
1148 setTimeout(() => {
1149 // update solution total amount
1150 countQuoteTotal(quoteOptionEl);
1151 }, 200);
1152 },
1153 (error) => {
1154 // hide all options in case of error
1155 optionRoomEl.querySelectorAll('.vbo-editbooking-room-service[data-option-id]').forEach((optEl) => {
1156 optEl.style.display = 'none';
1157 });
1158 }
1159 );
1160 }
1161 },
1162 (error) => {
1163 alert(error.responseText || 'Could not fetch room rates.');
1164 }
1165 );
1166 };
1167
1168 /**
1169 * Register function to count the total amount for the given quote option.
1170 *
1171 * @param Element quoteOptionEl The quote-option element.
1172 *
1173 * @return undefined
1174 */
1175 const countQuoteTotal = (quoteOptionEl) => {
1176 if (!quoteOptionEl) {
1177 return;
1178 }
1179
1180 let totalAmount = 0;
1181
1182 // iterate all rooms within the current quote
1183 quoteOptionEl.querySelectorAll('.vbo-quote-option-new-room-wrap').forEach((quoteOptRoomEl, roomIndex) => {
1184 let listingId = quoteOptRoomEl.querySelector('select[data-field="listing"]')?.value;
1185 if (!listingId) {
1186 // do not increase the total amount counter when room is missing
1187 return;
1188 }
1189 let roomCost = 0;
1190 let roomRateId = quoteOptRoomEl.querySelector('select[data-field="roomrate"]')?.value;
1191 let customRate = quoteOptRoomEl.querySelector('input[data-field="customrate"]')?.value;
1192 if (!roomRateId && !customRate) {
1193 // do not increase the total amount counter when rate is missing, either a rate plan or a custom rate
1194 return;
1195 }
1196 if (customRate) {
1197 roomCost = Number(customRate);
1198 } else {
1199 let roomRateEl = quoteOptRoomEl.querySelector('select[data-field="roomrate"]');
1200 roomCost = Number(roomRateEl.options[roomRateEl.selectedIndex].getAttribute('data-cost'));
1201 }
1202 if (!roomCost || isNaN(roomCost)) {
1203 // do not increase the total amount counter in case of invalid room cost
1204 return;
1205 }
1206
1207 // add room cost to solution total amount
1208 totalAmount += roomCost;
1209
1210 // scan all room eligible and checked options
1211 quoteOptRoomEl.querySelectorAll('.vbo-editbooking-room-service[data-eligible="1"]').forEach((optRecordEl) => {
1212 let optRecordId = optRecordEl.getAttribute('data-option-id');
1213 let isChildAge = optRecordEl.getAttribute('data-child-age') == 1;
1214 if (isChildAge) {
1215 // one drop-down select element per child is expected
1216 optRecordEl.querySelectorAll('select[data-field="child-age"]').forEach((childAgeSelEl, childAgeSelIndex) => {
1217 if (!childAgeSelEl.value) {
1218 return;
1219 }
1220 let childIntervalCost = Number(childAgeSelEl.options[childAgeSelEl.selectedIndex].getAttribute('data-computed-cost'));
1221 if (isNaN(childIntervalCost)) {
1222 return;
1223 }
1224 // add child age interval cost to solution total amount
1225 totalAmount += childIntervalCost;
1226 });
1227 } else {
1228 // regular option
1229 let checkboxEl = optRecordEl.querySelector('input[type="checkbox"]');
1230 let numberEl = optRecordEl.querySelector('input[type="number"]');
1231 let priceEl = optRecordEl.querySelector('.vbo-editbooking-room-service-price');
1232 let computedCost = 0;
1233 if (checkboxEl && checkboxEl.checked) {
1234 // single-quantity option checked
1235 computedCost = Number(priceEl.getAttribute('data-computed-cost'));
1236 } else if (numberEl && numberEl.value && parseInt(numberEl.value) > 0) {
1237 // multiple quantity option fulfilled
1238 let quantity = parseInt(numberEl.value);
1239 computedCost = Number(priceEl.getAttribute('data-computed-cost'));
1240 if (computedCost && !isNaN(computedCost)) {
1241 computedCost = computedCost * quantity;
1242 }
1243 }
1244 if (computedCost && !isNaN(computedCost)) {
1245 // add option cost to solution total amount
1246 totalAmount += computedCost;
1247 }
1248 }
1249 });
1250
1251 // scan all room extra services
1252 quoteOptRoomEl.querySelectorAll('.vbo-quote-option-room-new-extra-wrap').forEach((extraEl) => {
1253 let extraName = extraEl.querySelector('input[data-field="extra-name"]')?.value;
1254 let extraCost = Number(extraEl.querySelector('input[data-field="extra-cost"]')?.value);
1255 if (!extraName || !extraCost || isNaN(extraCost)) {
1256 // ignore when extra service name or cost are missing or invalid
1257 return;
1258 }
1259 // add extra service cost to solution total amount
1260 totalAmount += extraCost;
1261 });
1262 });
1263
1264 // update quote total amount
1265 quoteOptionEl.querySelector('.vbo-quote-option-total-cost').innerHTML = VBOCore.getCurrency().format(totalAmount);
1266 };
1267
1268 /**
1269 * Simulate click event on add-new-quote-option button to add the first option.
1270 * Eventually populate solutions with given inquiry data.
1271 */
1272 if (!quoteEditMode) {
1273 // add the first quote solution when creating a new one
1274 document.querySelector('.vbo-quote-option-add-btn').click();
1275
1276 if (inquiryData?.checkin && inquiryData?.checkout) {
1277 // populate values from inquiry data onto the first quote solution
1278 let quoteSolution = document.querySelector('.vbo-quote-new-option-wrap[data-active="1"]');
1279 if (quoteSolution) {
1280 // set stay dates
1281 quoteSolution.querySelector('input[data-field="checkin"]').value = inquiryData.checkin;
1282 quoteSolution.querySelector('input[data-field="checkout"]').value = inquiryData.checkout;
1283 // calculate and set nights
1284 let totNights = calcNights(inquiryData.checkin, inquiryData.checkout);
1285 quoteSolution.querySelector('[data-counter="nights"]').textContent = totNights || '0';
1286
1287 if (Array.isArray(inquiryData?.parties) && inquiryData.parties.length) {
1288 // select the add-room button for the current solution
1289 let addRoomBtn = quoteSolution.querySelector('.vbo-quote-option-room-add-btn');
1290 // iterate all room parties
1291 inquiryData.parties.forEach((party, partyIndex) => {
1292 // add a new room to the current solution
1293 addRoomBtn.click();
1294 // select the newly added room-booking element
1295 let roomBookingEls = quoteSolution.querySelectorAll('.vbo-quote-option-new-room-wrap');
1296 if (roomBookingEls[partyIndex]) {
1297 // attempt to set the number of guests
1298 if (party?.adults) {
1299 roomBookingEls[partyIndex].querySelector('input[data-field="adults"]').value = party.adults;
1300 }
1301 if (party?.children) {
1302 roomBookingEls[partyIndex].querySelector('input[data-field="children"]').value = party.children;
1303 }
1304 if (Array.isArray(inquiryData?.rooms)) {
1305 let setRoomId = null;
1306 if (inquiryData.rooms[partyIndex]?.id) {
1307 // set exactly preferred room ID
1308 setRoomId = inquiryData.rooms[partyIndex]?.id;
1309 } else if (inquiryData.rooms[0]?.id) {
1310 // set first preferred room ID
1311 setRoomId = inquiryData.rooms[0]?.id;
1312 }
1313 if (setRoomId) {
1314 // set room selected
1315 let roomBookingRoomEl = roomBookingEls[partyIndex].querySelector('select[data-field="listing"]');
1316 roomBookingRoomEl.value = setRoomId;
1317 roomBookingRoomEl.dispatchEvent(new Event('change'));
1318 }
1319 }
1320 }
1321 });
1322 }
1323 }
1324 }
1325 }
1326
1327 /**
1328 * Simulate blur event on phone input field in case it's got a value to format for update.
1329 */
1330 setTimeout(() => {
1331 document.querySelector('input[data-field="customer-phone"]')?.dispatchEvent(new Event('blur'));
1332 }, 200);
1333
1334 });
1335 </script>
1336