PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.12
VikBooking Hotel Booking Engine & PMS v1.8.12
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.12, at admin/layouts/quote/manage/script.php

1,348 lines 71.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 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 valid until help icon.
521 */
522 document.querySelector('.vbo-quote-validity-date-help')?.addEventListener('click', (e) => {
523 VBOCore.displayModal({
524 extra_class: 'vbo-modal-rounded vbo-modal-tooltip',
525 body: <?php echo json_encode(JText::translate('VBO_LOCK_UNTIL_HELP')); ?>,
526 lock_scroll: true,
527 draggable: false,
528 });
529 });
530
531 /**
532 * Register event listener for the quote submit (save new) button.
533 */
534 document.querySelector('.vbo-quote-submit-btn')?.addEventListener('click', (e) => {
535 const submitBtn = e.target.matches('button') ? e.target : e.target.closest('button');
536 const submitIcn = submitBtn.querySelector('i');
537 let submitIcnClass = submitIcn.getAttribute('class');
538
539 const optionElements = document.querySelectorAll('.vbo-quote-new-option-wrap[data-active="1"]');
540 if (!optionElements.length) {
541 alert('Please add at least one booking solution.');
542 return false;
543 }
544
545 // quickly validate that we've got at least one valid booking solution
546 let validSolutions = 0;
547 optionElements.forEach((optionEl) => {
548 let checkinDate = optionEl.querySelector('input[data-field="checkin"]')?.value;
549 let checkoutDate = optionEl.querySelector('input[data-field="checkout"]')?.value;
550 if (!checkinDate || !checkoutDate) {
551 return;
552 }
553 optionEl.querySelectorAll('.vbo-quote-option-new-room-wrap').forEach((optionRoomEl) => {
554 let listingId = optionRoomEl.querySelector('select[data-field="listing"]')?.value;
555 if (listingId) {
556 validSolutions++;
557 }
558 });
559 });
560 if (!validSolutions) {
561 alert('Please configure at least one room booking solution.');
562 return false;
563 }
564
565 // disable button and start loading animation
566 submitBtn.disabled = true;
567 submitIcn.setAttribute('class', '');
568 submitIcn.classList.add(...String('<?php echo VikBookingIcons::i('circle-notch', 'fa-spin fa-fw'); ?>').split(' '));
569
570 // collect quotation values
571 let quoteValues = {
572 name: document.querySelector('input[data-field="quote-name"]')?.value,
573 validity: document.querySelector('input[data-field="valid-until"]')?.value,
574 id_payment: document.querySelector('select[data-field="quote-payment"]')?.value,
575 customer: {
576 id: document.querySelector('select[data-field="customer-id"]')?.value,
577 first_name: document.querySelector('input[data-field="customer-firstname"]')?.value,
578 last_name: document.querySelector('input[data-field="customer-lastname"]')?.value,
579 email: document.querySelector('input[data-field="customer-email"]')?.value,
580 phone: document.querySelector('input[data-field="customer-phone"]')?.value,
581 country: document.querySelector('input[data-field="customer-country"]')?.value,
582 },
583 message: {
584 subject: document.querySelector('input[data-field="mail-subject"]')?.value,
585 content: document.querySelector('textarea[name="quote_message_content"]')?.value,
586 notes: document.querySelector('textarea[data-field="notes"]')?.value,
587 send: document.querySelector('input[type="checkbox"][name="quote_send_later"]')?.checked ? 0 : 1,
588 preferred: document.querySelector('input[type="checkbox"][name="quote_message_preferred"]')?.checked ? 1 : 0,
589 },
590 solutions: [],
591 };
592
593 // iterate all booking solutions
594 optionElements.forEach((optionEl) => {
595 let checkinDate = optionEl.querySelector('input[data-field="checkin"]')?.value;
596 let checkoutDate = optionEl.querySelector('input[data-field="checkout"]')?.value;
597 if (!checkinDate || !checkoutDate) {
598 // skip invalid booking solution
599 return;
600 }
601
602 // build booking solution object
603 let bookingSolution = {
604 checkin: checkinDate,
605 checkout: checkoutDate,
606 rooms: [],
607 };
608
609 // iterate all booking rooms
610 optionEl.querySelectorAll('.vbo-quote-option-new-room-wrap').forEach((optionRoomEl) => {
611 let listingId = optionRoomEl.querySelector('select[data-field="listing"]')?.value;
612 if (!listingId) {
613 // skip invalid booking room
614 return;
615 }
616
617 // room rate plan element
618 let roomRateEl = optionRoomEl.querySelector('select[data-field="roomrate"]');
619
620 // build booking room object
621 let bookingRoom = {
622 id: listingId,
623 adults: optionRoomEl.querySelector('input[data-field="adults"]')?.value,
624 children: optionRoomEl.querySelector('input[data-field="children"]')?.value,
625 id_price: roomRateEl?.value,
626 room_cost: roomRateEl?.value ? Number(roomRateEl.options[roomRateEl.selectedIndex].getAttribute('data-cost')) : 0,
627 cust_cost: optionRoomEl.querySelector('input[data-field="customrate"]')?.value,
628 id_tax: optionRoomEl.querySelector('select[data-field="customrate-idtax"]')?.value,
629 options: [],
630 extras: [],
631 };
632
633 // iterate all booking room option records
634 optionRoomEl.querySelectorAll('.vbo-editbooking-room-service[data-eligible="1"]').forEach((optRecordEl) => {
635 let isChildAge = optRecordEl.getAttribute('data-child-age') == 1;
636 // build booking room option object
637 let bookingRoomOption = {
638 id: optRecordEl.getAttribute('data-option-id'),
639 quantity: 0,
640 cost: 0,
641 is_child_age: isChildAge ? 1 : 0,
642 age_intervals: [],
643 };
644
645 if (isChildAge) {
646 // one drop-down select element per child is expected
647 let childAgeFulfilled = true;
648 optRecordEl.querySelectorAll('select[data-field="child-age"]').forEach((childAgeSelEl, childAgeSelIndex) => {
649 if (!childAgeSelEl.value) {
650 childAgeFulfilled = false;
651 return;
652 }
653 // push child age interval value
654 bookingRoomOption.age_intervals.push(childAgeSelEl.value);
655 // check computed cost
656 let childIntervalCost = Number(childAgeSelEl.options[childAgeSelEl.selectedIndex].getAttribute('data-computed-cost'));
657 if (isNaN(childIntervalCost)) {
658 return;
659 }
660 // update option computed cost
661 bookingRoomOption.cost += childIntervalCost;
662 });
663
664 if (childAgeFulfilled === true) {
665 // push booking room option
666 bookingRoom.options.push(bookingRoomOption);
667 }
668 } else {
669 // regular option
670 let checkboxEl = optRecordEl.querySelector('input[type="checkbox"]');
671 let numberEl = optRecordEl.querySelector('input[type="number"]');
672 let priceEl = optRecordEl.querySelector('.vbo-editbooking-room-service-price');
673 let computedCost = 0;
674 if (checkboxEl && checkboxEl.checked) {
675 // single-quantity option checked
676 computedCost = Number(priceEl.getAttribute('data-computed-cost'));
677 // set option quantity
678 bookingRoomOption.quantity = 1;
679 } else if (numberEl && numberEl.value && parseInt(numberEl.value) > 0) {
680 // multiple quantity option fulfilled
681 let quantity = parseInt(numberEl.value);
682 computedCost = Number(priceEl.getAttribute('data-computed-cost'));
683 if (computedCost && !isNaN(computedCost)) {
684 computedCost = computedCost * quantity;
685 }
686 // set option quantity
687 bookingRoomOption.quantity = quantity;
688 }
689 if (computedCost && !isNaN(computedCost)) {
690 // set option computed cost
691 bookingRoomOption.cost = computedCost;
692 }
693
694 if (bookingRoomOption.quantity || bookingRoomOption.cost) {
695 // push booking room option
696 bookingRoom.options.push(bookingRoomOption);
697 }
698 }
699 });
700
701 // iterate all booking room extra services
702 optionRoomEl.querySelectorAll('.vbo-quote-option-room-new-extra-wrap').forEach((extraEl) => {
703 let extraName = extraEl.querySelector('input[data-field="extra-name"]')?.value;
704 let extraCost = Number(extraEl.querySelector('input[data-field="extra-cost"]')?.value);
705 if (!extraName || !extraCost || isNaN(extraCost)) {
706 // ignore when extra service name or cost are missing or invalid
707 return;
708 }
709
710 // push booking room extra service
711 bookingRoom.extras.push({
712 name: extraName,
713 cost: extraCost,
714 vat: extraEl.querySelector('select[data-field="extra-taxrate"]')?.value,
715 });
716 });
717
718 if ((bookingRoom.id_price && bookingRoom.room_cost) || bookingRoom.cust_cost) {
719 // push booking room
720 bookingSolution.rooms.push(bookingRoom);
721 }
722 });
723
724 if (bookingSolution.rooms.length) {
725 // push booking solution
726 quoteValues.solutions.push(bookingSolution);
727 }
728 });
729
730 // make the request to save the quotation record and related booking solutions
731 VBOCore.doAjax(
732 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=quote.save'); ?>",
733 {
734 quote: quoteValues,
735 session_id: <?php echo (int) $session_id; ?>,
736 },
737 (response) => {
738 if (response?.sending_error) {
739 // display an alert message just for the email sending error
740 alert(response.sending_error);
741 }
742 // display toast message on success
743 VBOToast.enqueue(new VBOToastMessage({
744 title: <?php echo json_encode(JText::translate('VBPVIEWORDERSEIGHT')); ?>,
745 body: <?php echo json_encode(JText::translate('VBOCHECKINSTATUSUPDATED')); ?>,
746 status: VBOToast.SUCCESS_STATUS,
747 delay: {
748 min: 6000,
749 max: 20000,
750 tolerance: 4000,
751 },
752 action: () => {
753 VBOToast.dispose(true);
754 }
755 }));
756 // redirect to bookings list on success
757 location.href = '<?php echo VBOFactory::getPlatform()->getUri()->admin('index.php?option=com_vikbooking&task=orders&quote_saved=1', false); ?>';
758 },
759 (error) => {
760 // display error
761 alert(error.responseText || 'Could not complete the operation.');
762 // restore button
763 submitBtn.disabled = false;
764 submitIcn.setAttribute('class', '');
765 submitIcn.classList.add(...submitIcnClass.split(' '));
766 }
767 );
768 });
769
770 /**
771 * Register event listener for the quote update (edit quote) button.
772 */
773 document.querySelector('.vbo-quote-update-btn')?.addEventListener('click', (e) => {
774 const submitBtn = e.target.matches('button') ? e.target : e.target.closest('button');
775 const submitIcn = submitBtn.querySelector('i');
776 let submitIcnClass = submitIcn.getAttribute('class');
777
778 const quoteId = document.querySelector('.vbo-quote-section-current[data-quote-id]')?.getAttribute('data-quote-id');
779 if (!quoteId) {
780 alert('Could not find the quote ID to update.');
781 return false;
782 }
783
784 // disable button and start loading animation
785 submitBtn.disabled = true;
786 submitIcn.setAttribute('class', '');
787 submitIcn.classList.add(...String('<?php echo VikBookingIcons::i('circle-notch', 'fa-spin fa-fw'); ?>').split(' '));
788
789 // collect quotation values for update
790 let quoteValues = {
791 id: quoteId,
792 name: document.querySelector('input[data-field="quote-name"]')?.value,
793 validity: document.querySelector('input[data-field="valid-until"]')?.value,
794 id_payment: document.querySelector('select[data-field="quote-payment"]')?.value,
795 customer: {
796 id: document.querySelector('select[data-field="customer-id"]')?.value,
797 first_name: document.querySelector('input[data-field="customer-firstname"]')?.value,
798 last_name: document.querySelector('input[data-field="customer-lastname"]')?.value,
799 email: document.querySelector('input[data-field="customer-email"]')?.value,
800 phone: document.querySelector('input[data-field="customer-phone"]')?.value,
801 country: document.querySelector('input[data-field="customer-country"]')?.value,
802 },
803 message: {
804 subject: document.querySelector('input[data-field="mail-subject"]')?.value,
805 content: document.querySelector('textarea[name="quote_message_content"]')?.value,
806 notes: document.querySelector('textarea[data-field="notes"]')?.value,
807 preferred: document.querySelector('input[type="checkbox"][name="quote_message_preferred"]')?.checked ? 1 : 0,
808 },
809 };
810
811 // make the request to update the current quotation record
812 VBOCore.doAjax(
813 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=quote.update'); ?>",
814 {
815 quote: quoteValues,
816 },
817 (response) => {
818 // display toast message on success
819 VBOToast.enqueue(new VBOToastMessage({
820 title: <?php echo json_encode(JText::translate('VBPVIEWORDERSEIGHT')); ?>,
821 body: <?php echo json_encode(JText::translate('VBOCHECKINSTATUSUPDATED')); ?>,
822 status: VBOToast.SUCCESS_STATUS,
823 delay: {
824 min: 6000,
825 max: 20000,
826 tolerance: 4000,
827 },
828 action: () => {
829 VBOToast.dispose(true);
830 }
831 }));
832 // reload the page on success
833 location.reload();
834 },
835 (error) => {
836 // display error
837 alert(error.responseText || 'Could not complete the operation.');
838 // restore button
839 submitBtn.disabled = false;
840 submitIcn.setAttribute('class', '');
841 submitIcn.classList.add(...submitIcnClass.split(' '));
842 }
843 );
844 });
845
846 /**
847 * Register event listener for the quote delete (edit quote) button.
848 */
849 document.querySelector('.vbo-quote-delete-btn')?.addEventListener('click', (e) => {
850 const submitBtn = e.target.matches('button') ? e.target : e.target.closest('button');
851 const submitIcn = submitBtn.querySelector('i');
852 let submitIcnClass = submitIcn.getAttribute('class');
853
854 const quoteId = document.querySelector('.vbo-quote-section-current[data-quote-id]')?.getAttribute('data-quote-id');
855 if (!quoteId) {
856 alert('Could not find the quote ID to delete.');
857 return false;
858 }
859
860 if (!confirm(<?php echo json_encode(JText::translate('VBDELCONFIRM')); ?>)) {
861 return false;
862 }
863
864 // disable button and start loading animation
865 submitBtn.disabled = true;
866 submitIcn.setAttribute('class', '');
867 submitIcn.classList.add(...String('<?php echo VikBookingIcons::i('circle-notch', 'fa-spin fa-fw'); ?>').split(' '));
868
869 // make the request to delete the current quotation record
870 VBOCore.doAjax(
871 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=quote.delete'); ?>",
872 {
873 quote_id: quoteId,
874 },
875 (response) => {
876 // display toast message on success
877 VBOToast.enqueue(new VBOToastMessage({
878 title: <?php echo json_encode(JText::translate('VBPVIEWORDERSEIGHT')); ?>,
879 body: <?php echo json_encode(JText::translate('VBOCHECKINSTATUSUPDATED')); ?>,
880 status: VBOToast.SUCCESS_STATUS,
881 delay: {
882 min: 6000,
883 max: 20000,
884 tolerance: 4000,
885 },
886 action: () => {
887 VBOToast.dispose(true);
888 }
889 }));
890 // redirect to bookings list on success
891 location.href = '<?php echo VBOFactory::getPlatform()->getUri()->admin('index.php?option=com_vikbooking&task=orders&quote_deleted=1', false); ?>';
892 },
893 (error) => {
894 // display error
895 alert(error.responseText || 'Could not complete the operation.');
896 // restore button
897 submitBtn.disabled = false;
898 submitIcn.setAttribute('class', '');
899 submitIcn.classList.add(...submitIcnClass.split(' '));
900 }
901 );
902 });
903
904 /**
905 * Register function to calculate the nights between two date strings.
906 *
907 * @param string checkin The check-in date.
908 * @param string checkout The check-out date.
909 *
910 * @return ?number
911 */
912 const calcNights = (checkin, checkout) => {
913 if (!checkin || !checkout) {
914 return null;
915 }
916 let checkinDate = new Date(checkin);
917 let checkoutDate = new Date(checkout);
918 let utc1 = Date.UTC(checkinDate.getFullYear(), checkinDate.getMonth(), checkinDate.getDate());
919 let utc2 = Date.UTC(checkoutDate.getFullYear(), checkoutDate.getMonth(), checkoutDate.getDate());
920
921 return Math.ceil((utc2 - utc1) / (1000 * 60 * 60 * 24));
922 };
923
924 /**
925 * Register function to update the total guests count within a quote option.
926 *
927 * @param Element quoteOptionEl The quote-option element.
928 *
929 * @return undefined
930 */
931 const setTotalGuestsCount = (quoteOptionEl) => {
932 if (!quoteOptionEl) {
933 return;
934 }
935 let guests = 0;
936 quoteOptionEl.querySelectorAll('input[data-field="adults"]').forEach((field) => {
937 guests += !isNaN(field.value) ? parseInt(field.value) : 0;
938 });
939 quoteOptionEl.querySelectorAll('input[data-field="children"]').forEach((field) => {
940 guests += !isNaN(field.value) ? parseInt(field.value) : 0;
941 });
942 quoteOptionEl.querySelector('[data-counter="guests"]').textContent = guests;
943 };
944
945 /**
946 * Register function to update the rate plans for a room within a quote option.
947 * The function will also load the eligible options to show their pricing info.
948 *
949 * @param Element quoteOptionEl The quote-option element.
950 * @param Element optionRoomEl The quote-option-room element.
951 *
952 * @return undefined
953 */
954 const fetchRatePlans = (quoteOptionEl, optionRoomEl) => {
955 if (!quoteOptionEl || !optionRoomEl) {
956 throw new Error('Invalid element arguments provided.');
957 }
958
959 let listingId = optionRoomEl.querySelector('select[data-field="listing"]')?.value;
960 let roomRateEl = optionRoomEl.querySelector('select[data-field="roomrate"]');
961 let rateErrorEl = optionRoomEl.querySelector('.vbo-quote-label-help[data-type="rate-error"]');
962 let checkinDate = quoteOptionEl.querySelector('input[data-field="checkin"]')?.value;
963 let checkoutDate = quoteOptionEl.querySelector('input[data-field="checkout"]')?.value;
964 let previousRateId = roomRateEl?.value;
965 let numAdults = optionRoomEl.querySelector('input[data-field="adults"]')?.value;
966 let numChildren = optionRoomEl.querySelector('input[data-field="children"]')?.value;
967 let numNights = parseInt(quoteOptionEl.querySelector('[data-counter="nights"]')?.innerText || 1);
968
969 // ensure the number of nights of stay is a number
970 numNights = isNaN(numNights) ? 1 : numNights;
971
972 // always reset the rate plan select element
973 roomRateEl.value = '';
974 roomRateEl.querySelectorAll('option').forEach((opt) => {
975 if (!opt.getAttribute('value')) {
976 // keep the empty option
977 return;
978 }
979 opt.remove();
980 });
981
982 // always empty and hide the rate error element
983 rateErrorEl.textContent = '';
984 rateErrorEl.style.display = 'none';
985
986 if (!listingId || !checkinDate || !checkoutDate) {
987 // do nothing
988 return;
989 }
990 // make the request
991 VBOCore.doAjax(
992 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=calc_rates'); ?>",
993 {
994 id_room: listingId,
995 checkinfdate: checkinDate,
996 checkoutfdate: checkoutDate,
997 num_adults: numAdults,
998 num_children: numChildren,
999 units: 1,
1000 only_rates: 1,
1001 },
1002 (response) => {
1003 // list of rate plan objects expected in case of success
1004 if (!Array.isArray(response) || typeof response[0] === 'string' || (response[1] || 0) == -1) {
1005 // error expected, but the drop down element is already empty
1006 if (Array.isArray(response) && typeof response[0] === 'string') {
1007 // display the error encountered
1008 rateErrorEl.textContent = response[0].replace('e4j.error.', '');
1009 rateErrorEl.style.display = '';
1010 }
1011 } else {
1012 // check if rates are inclusive of tax
1013 let ratesTaxInclusive = <?php echo VikBooking::ivaInclusa() ? 1 : 0; ?>;
1014
1015 // populate option elements
1016 response.forEach((ratePlan) => {
1017 let ratePlanOptEl = document.createElement('option');
1018 ratePlanOptEl.setAttribute('value', ratePlan?.idprice);
1019 ratePlanOptEl.setAttribute('data-cost', ratesTaxInclusive ? ratePlan?.tot : ratePlan?.net);
1020 ratePlanOptEl.textContent = ratePlan?.name || 'Rate plan';
1021 ratePlanOptEl.textContent += ' - ' + (ratesTaxInclusive ? ratePlan?.ftot : ratePlan?.fnet);
1022 if (previousRateId && previousRateId == ratePlan?.idprice) {
1023 ratePlanOptEl.selected = true;
1024 }
1025 roomRateEl.append(ratePlanOptEl);
1026 });
1027
1028 // fetch eligible options
1029 VBOCore.doAjax(
1030 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=listings.get_eligible_options'); ?>",
1031 {
1032 listing_id: listingId,
1033 adults: numAdults,
1034 children: numChildren,
1035 checkin: checkinDate,
1036 checkout: checkoutDate,
1037 },
1038 (optionRecords) => {
1039 if (!Array.isArray(optionRecords)) {
1040 // unexpected response format
1041 console.error('listings.get_eligible_options: unexpected response format.', optionRecords);
1042 return;
1043 }
1044
1045 // obtain all the eligible option record IDs
1046 let eligibleIds = [];
1047 optionRecords.forEach((optRecord) => {
1048 eligibleIds.push(Number(optRecord.id));
1049 });
1050
1051 // display or hide the option record elements based on eligibility
1052 optionRoomEl.querySelectorAll('.vbo-editbooking-room-service[data-option-id]').forEach((optEl) => {
1053 let optionRecordId = Number(optEl.getAttribute('data-option-id'));
1054 // check eligibility
1055 if (!optionRecordId || !eligibleIds.includes(optionRecordId)) {
1056 // hide non-eligible element
1057 optEl.style.display = 'none';
1058 optEl.setAttribute('data-eligible', 0);
1059 } else {
1060 // show eligible element
1061 optEl.style.display = '';
1062 optEl.setAttribute('data-eligible', 1);
1063 }
1064 });
1065
1066 // update computed pricing and mandatory selection for every eligible option record
1067 optionRecords.forEach((optRecord) => {
1068 let optEl = optionRoomEl.querySelector('.vbo-editbooking-room-service[data-option-id="' + optRecord.id + '"]');
1069 let isChildAge = optEl.getAttribute('data-child-age') == 1;
1070 let optPriceEl = optEl.querySelector('.vbo-editbooking-room-service-price');
1071 let optInputWrapEl = optEl.querySelector('.vbo-editbooking-room-service-check');
1072 let isMultiQuantity = optInputWrapEl?.getAttribute('data-multiple-quantity') == 1;
1073
1074 // reset price content
1075 optPriceEl.textContent = '';
1076 optPriceEl.setAttribute('data-computed-cost', '');
1077
1078 // handle type of option
1079 if (isChildAge && optRecord?.ageintervals) {
1080 // handle children age intervals element
1081 optInputWrapEl.innerHTML = '';
1082
1083 if (typeof optRecord?._computed_children_costs === 'object' && typeof optRecord?._computed_age_intervals === 'object') {
1084 // loop over all children
1085 for (ch = 1; ch <= Number(numChildren); ch++) {
1086 if (!optRecord._computed_children_costs[ch] || !optRecord._computed_age_intervals[ch]) {
1087 // unknown values for this child number
1088 continue;
1089 }
1090 if (!Array.isArray(optRecord._computed_children_costs[ch]) || !optRecord._computed_children_costs[ch].length) {
1091 // missing cost values for this child number
1092 continue;
1093 }
1094 if (!Array.isArray(optRecord._computed_age_intervals[ch]) || !optRecord._computed_age_intervals[ch].length) {
1095 // missing interval values for this child number
1096 continue;
1097 }
1098
1099 // build drop down element container
1100 let containerEl = document.createElement('div');
1101 containerEl.classList.add('vbo-quote-child-age-wrap');
1102
1103 // build label element
1104 let labelEl = document.createElement('label');
1105 labelEl.textContent = <?php echo json_encode(JText::translate('VBMAILCHILD') . ' #'); ?> + ch;
1106
1107 // build drop down select element
1108 let selectEl = document.createElement('select');
1109 selectEl.setAttribute('data-field', 'child-age');
1110 selectEl.addEventListener('change', (e) => {
1111 // update solution total amount
1112 countQuoteTotal(quoteOptionEl);
1113 });
1114
1115 // scan all child age intervals and related costs
1116 optRecord._computed_age_intervals[ch].forEach((ageInterval, intervalIndex) => {
1117 let ageIntervalCost = Number(optRecord._computed_children_costs[ch][intervalIndex]);
1118 let ageOptEl = document.createElement('option');
1119 ageOptEl.setAttribute('data-computed-cost', ageIntervalCost);
1120 ageOptEl.value = ++intervalIndex;
1121 ageOptEl.textContent = ageInterval + ' (' + VBOCore.getCurrency().format(ageIntervalCost) + ')';
1122 selectEl.append(ageOptEl);
1123 });
1124
1125 // append elements to DOM
1126 containerEl.append(labelEl);
1127 containerEl.append(selectEl);
1128 optInputWrapEl.append(containerEl);
1129 }
1130 }
1131 } else {
1132 // handle regular option record
1133 optPriceEl.innerHTML = VBOCore.getCurrency().format(optRecord?._computed_cost || optRecord.cost);
1134 optPriceEl.setAttribute('data-computed-cost', optRecord?._computed_cost || optRecord.cost);
1135
1136 // check if we have a mandatory fee
1137 if (optRecord?.forcesel == 1) {
1138 // pre-select state
1139 if (isMultiQuantity) {
1140 // input number expected
1141 let forceSettings = (optRecord?.forceval || '').split('-');
1142 let quantity = parseInt(forceSettings[0] || 1);
1143 if (forceSettings[1] && forceSettings[1] == 1) {
1144 // forced quantity to be multiplied per night of stay
1145 quantity = quantity * numNights;
1146 }
1147 if (forceSettings[2] && forceSettings[2] == 1) {
1148 // forced quantity to be multiplied per child
1149 quantity = quantity * Number(numChildren);
1150 }
1151 optInputWrapEl.querySelector('input').value = quantity;
1152 } else {
1153 // checkbox expected
1154 optInputWrapEl.querySelector('input[type="checkbox"]').checked = true;
1155 }
1156 }
1157 }
1158 });
1159
1160 setTimeout(() => {
1161 // update solution total amount
1162 countQuoteTotal(quoteOptionEl);
1163 }, 200);
1164 },
1165 (error) => {
1166 // hide all options in case of error
1167 optionRoomEl.querySelectorAll('.vbo-editbooking-room-service[data-option-id]').forEach((optEl) => {
1168 optEl.style.display = 'none';
1169 });
1170 }
1171 );
1172 }
1173 },
1174 (error) => {
1175 alert(error.responseText || 'Could not fetch room rates.');
1176 }
1177 );
1178 };
1179
1180 /**
1181 * Register function to count the total amount for the given quote option.
1182 *
1183 * @param Element quoteOptionEl The quote-option element.
1184 *
1185 * @return undefined
1186 */
1187 const countQuoteTotal = (quoteOptionEl) => {
1188 if (!quoteOptionEl) {
1189 return;
1190 }
1191
1192 let totalAmount = 0;
1193
1194 // iterate all rooms within the current quote
1195 quoteOptionEl.querySelectorAll('.vbo-quote-option-new-room-wrap').forEach((quoteOptRoomEl, roomIndex) => {
1196 let listingId = quoteOptRoomEl.querySelector('select[data-field="listing"]')?.value;
1197 if (!listingId) {
1198 // do not increase the total amount counter when room is missing
1199 return;
1200 }
1201 let roomCost = 0;
1202 let roomRateId = quoteOptRoomEl.querySelector('select[data-field="roomrate"]')?.value;
1203 let customRate = quoteOptRoomEl.querySelector('input[data-field="customrate"]')?.value;
1204 if (!roomRateId && !customRate) {
1205 // do not increase the total amount counter when rate is missing, either a rate plan or a custom rate
1206 return;
1207 }
1208 if (customRate) {
1209 roomCost = Number(customRate);
1210 } else {
1211 let roomRateEl = quoteOptRoomEl.querySelector('select[data-field="roomrate"]');
1212 roomCost = Number(roomRateEl.options[roomRateEl.selectedIndex].getAttribute('data-cost'));
1213 }
1214 if (!roomCost || isNaN(roomCost)) {
1215 // do not increase the total amount counter in case of invalid room cost
1216 return;
1217 }
1218
1219 // add room cost to solution total amount
1220 totalAmount += roomCost;
1221
1222 // scan all room eligible and checked options
1223 quoteOptRoomEl.querySelectorAll('.vbo-editbooking-room-service[data-eligible="1"]').forEach((optRecordEl) => {
1224 let optRecordId = optRecordEl.getAttribute('data-option-id');
1225 let isChildAge = optRecordEl.getAttribute('data-child-age') == 1;
1226 if (isChildAge) {
1227 // one drop-down select element per child is expected
1228 optRecordEl.querySelectorAll('select[data-field="child-age"]').forEach((childAgeSelEl, childAgeSelIndex) => {
1229 if (!childAgeSelEl.value) {
1230 return;
1231 }
1232 let childIntervalCost = Number(childAgeSelEl.options[childAgeSelEl.selectedIndex].getAttribute('data-computed-cost'));
1233 if (isNaN(childIntervalCost)) {
1234 return;
1235 }
1236 // add child age interval cost to solution total amount
1237 totalAmount += childIntervalCost;
1238 });
1239 } else {
1240 // regular option
1241 let checkboxEl = optRecordEl.querySelector('input[type="checkbox"]');
1242 let numberEl = optRecordEl.querySelector('input[type="number"]');
1243 let priceEl = optRecordEl.querySelector('.vbo-editbooking-room-service-price');
1244 let computedCost = 0;
1245 if (checkboxEl && checkboxEl.checked) {
1246 // single-quantity option checked
1247 computedCost = Number(priceEl.getAttribute('data-computed-cost'));
1248 } else if (numberEl && numberEl.value && parseInt(numberEl.value) > 0) {
1249 // multiple quantity option fulfilled
1250 let quantity = parseInt(numberEl.value);
1251 computedCost = Number(priceEl.getAttribute('data-computed-cost'));
1252 if (computedCost && !isNaN(computedCost)) {
1253 computedCost = computedCost * quantity;
1254 }
1255 }
1256 if (computedCost && !isNaN(computedCost)) {
1257 // add option cost to solution total amount
1258 totalAmount += computedCost;
1259 }
1260 }
1261 });
1262
1263 // scan all room extra services
1264 quoteOptRoomEl.querySelectorAll('.vbo-quote-option-room-new-extra-wrap').forEach((extraEl) => {
1265 let extraName = extraEl.querySelector('input[data-field="extra-name"]')?.value;
1266 let extraCost = Number(extraEl.querySelector('input[data-field="extra-cost"]')?.value);
1267 if (!extraName || !extraCost || isNaN(extraCost)) {
1268 // ignore when extra service name or cost are missing or invalid
1269 return;
1270 }
1271 // add extra service cost to solution total amount
1272 totalAmount += extraCost;
1273 });
1274 });
1275
1276 // update quote total amount
1277 quoteOptionEl.querySelector('.vbo-quote-option-total-cost').innerHTML = VBOCore.getCurrency().format(totalAmount);
1278 };
1279
1280 /**
1281 * Simulate click event on add-new-quote-option button to add the first option.
1282 * Eventually populate solutions with given inquiry data.
1283 */
1284 if (!quoteEditMode) {
1285 // add the first quote solution when creating a new one
1286 document.querySelector('.vbo-quote-option-add-btn').click();
1287
1288 if (inquiryData?.checkin && inquiryData?.checkout) {
1289 // populate values from inquiry data onto the first quote solution
1290 let quoteSolution = document.querySelector('.vbo-quote-new-option-wrap[data-active="1"]');
1291 if (quoteSolution) {
1292 // set stay dates
1293 quoteSolution.querySelector('input[data-field="checkin"]').value = inquiryData.checkin;
1294 quoteSolution.querySelector('input[data-field="checkout"]').value = inquiryData.checkout;
1295 // calculate and set nights
1296 let totNights = calcNights(inquiryData.checkin, inquiryData.checkout);
1297 quoteSolution.querySelector('[data-counter="nights"]').textContent = totNights || '0';
1298
1299 if (Array.isArray(inquiryData?.parties) && inquiryData.parties.length) {
1300 // select the add-room button for the current solution
1301 let addRoomBtn = quoteSolution.querySelector('.vbo-quote-option-room-add-btn');
1302 // iterate all room parties
1303 inquiryData.parties.forEach((party, partyIndex) => {
1304 // add a new room to the current solution
1305 addRoomBtn.click();
1306 // select the newly added room-booking element
1307 let roomBookingEls = quoteSolution.querySelectorAll('.vbo-quote-option-new-room-wrap');
1308 if (roomBookingEls[partyIndex]) {
1309 // attempt to set the number of guests
1310 if (party?.adults) {
1311 roomBookingEls[partyIndex].querySelector('input[data-field="adults"]').value = party.adults;
1312 }
1313 if (party?.children) {
1314 roomBookingEls[partyIndex].querySelector('input[data-field="children"]').value = party.children;
1315 }
1316 if (Array.isArray(inquiryData?.rooms)) {
1317 let setRoomId = null;
1318 if (inquiryData.rooms[partyIndex]?.id) {
1319 // set exactly preferred room ID
1320 setRoomId = inquiryData.rooms[partyIndex]?.id;
1321 } else if (inquiryData.rooms[0]?.id) {
1322 // set first preferred room ID
1323 setRoomId = inquiryData.rooms[0]?.id;
1324 }
1325 if (setRoomId) {
1326 // set room selected
1327 let roomBookingRoomEl = roomBookingEls[partyIndex].querySelector('select[data-field="listing"]');
1328 roomBookingRoomEl.value = setRoomId;
1329 roomBookingRoomEl.dispatchEvent(new Event('change'));
1330 }
1331 }
1332 }
1333 });
1334 }
1335 }
1336 }
1337 }
1338
1339 /**
1340 * Simulate blur event on phone input field in case it's got a value to format for update.
1341 */
1342 setTimeout(() => {
1343 document.querySelector('input[data-field="customer-phone"]')?.dispatchEvent(new Event('blur'));
1344 }, 200);
1345
1346 });
1347 </script>
1348