PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.15
VikBooking Hotel Booking Engine & PMS v1.8.15
1.8.15 1.8.14 1.8.13 1.8.12 1.8.11 1.8.10 1.8.9 1.8.6 1.8.7 1.8.8 trunk 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 1.7.3 All 36 releases
vikbooking / admin / layouts / overview / actions.php

actions.php in VikBooking Hotel Booking Engine & PMS 1.8.15, at admin/layouts/overview/actions.php

3,268 lines 158.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package VikBooking
4 * @subpackage core
5 * @author E4J s.r.l.
6 * @copyright Copyright (C) 2025 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 string $caller The identifier of who's calling this layout file.
18 * @var array $rooms List of room records involved.
19 * @var array $tmfilters Optional list of task manager filters.
20 */
21 extract($displayData);
22
23 // access the application
24 $app = JFactory::getApplication();
25 $vbo_app = VikBooking::getVboApplication();
26 $cookie = $app->input->cookie;
27
28 // load context menu assets
29 $vbo_app->loadContextMenuAssets();
30
31 // preload chat assets
32 VBOFactory::getChatMediator()->useAssets();
33
34 // gather permissions
35 $vbo_auth_pricing = JFactory::getUser()->authorise('core.vbo.pricing', 'com_vikbooking');
36 $vbo_auth_pms = JFactory::getUser()->authorise('core.vbo.pms', 'com_vikbooking');
37
38 // load all the existing task manager areas/projects
39 $taskAreas = VBOTaskModelArea::getInstance()->getItems();
40
41 // load all room rate plans to detect derived rate plans
42 $allRatePlans = VikBooking::getAvailabilityInstance(true)->loadRatePlans(true);
43 $hasDerivedRates = (bool) array_filter($allRatePlans, function($rp) {
44 return !empty($rp['derived_id']) && !empty($rp['parent_rate_id']);
45 });
46
47 // currency symbol and formatting options
48 $currencysymb = VikBooking::getCurrencySymb();
49 list($currency_digits, $currency_decimals, $currency_thousands) = explode(':', VikBooking::getNumberFormatData());
50
51 // check whether VCM is available
52 $vcm_enabled = VikBooking::vcmAutoUpdate();
53
54 // build room-ota relations for pricing alterations, if any
55 $room_ota_relations = [];
56 foreach (array_column(($rooms ?? []), 'id') as $rid) {
57 // always get a new instance of the VikChannelManagerLogos class
58 $vcm_logos = VikBooking::getVcmChannelsLogo('', true);
59 // load channels (firsr) and accounts (after) for this listing
60 $room_ota_channels = is_object($vcm_logos) && method_exists($vcm_logos, 'getVboRoomLogosMapped') ? $vcm_logos->getVboRoomLogosMapped($rid) : [];
61 $room_ota_accounts = is_object($vcm_logos) && method_exists($vcm_logos, 'getRoomOtaAccounts') ? $vcm_logos->getRoomOtaAccounts() : [];
62 // filter channels not available as accounts (i.e. iCal)
63 if (count($room_ota_channels) != count(($room_ota_accounts[$rid] ?? []))) {
64 $ota_account_names = array_map('strtolower', array_column(($room_ota_accounts[$rid] ?? []), 'channel'));
65 $room_ota_channels = array_filter($room_ota_channels, function($chid) use ($ota_account_names) {
66 return in_array(strtolower($chid), $ota_account_names);
67 }, ARRAY_FILTER_USE_KEY);
68 }
69 if ($room_ota_channels && ($room_ota_accounts[$rid] ?? [])) {
70 $room_ota_relations[$rid] = [
71 'channels' => $room_ota_channels,
72 'accounts' => $room_ota_accounts[$rid],
73 ];
74 }
75 }
76
77 // build room names map
78 $room_names_map = array_combine(array_column(($rooms ?? []), 'id'), array_column(($rooms ?? []), 'name'));
79
80 // access the current task manager filters
81 $tmfilters = (array) (($tmfilters ?? []) ?: $app->input->get('tmfilters', [], 'array'));
82
83 // check if the tasks should be automatically loaded for certain area/project IDs
84 $activeAreas = array_values(array_filter(array_map('intval', $tmfilters['area_ids'] ?? [])));
85
86 ?>
87
88 <button type="button" class="btn vbo-context-menu-btn vbo-context-menu-btn-raw vbo-context-menu-overview-actions">
89 <span class="vbo-context-menu-lbl"><?php echo JText::translate('VBCRONACTIONS'); ?></span>
90 <span class="vbo-context-menu-ico"><?php VikBookingIcons::e('sort-down'); ?></span>
91 </button>
92
93 <div class="vbo-overview-action-raterestr-helper" style="display: none;">
94
95 <div class="vbo-overview-action-raterestr-wrap">
96 <div class="vbo-overview-action-raterestr-info">
97 <div class="vbo-overview-action-raterestr-listings-info">
98 <?php VikBookingIcons::e('bed'); ?>
99 <span class="vbo-overview-action-raterestr-listings"></span>
100 </div>
101 <div class="vbo-overview-action-raterestr-dates"></div>
102 </div>
103 <div class="vbo-roverw-setnewrate">
104 <div class="vbo-roverw-setnewrate-title">
105 <h4><?php VikBookingIcons::e('tag'); ?> <?php echo JText::translate('VBO_RATES_AND_RESTR'); ?></h4>
106 <div class="vbo-roverw-setnewrate-skip-derived vbo-toggle-small vbo-toggle-mini" style="<?php echo !$hasDerivedRates ? 'display: none;' : ''; ?>">
107 <label for="overw-skip-derived-on" class="vbo-roverw-setnewrate-skip-derived-lbl"><?php echo JText::translate('VBO_SKIP_DERIVED_RPLANS'); ?></label>
108 <?php echo $vbo_app->printYesNoButtons('overw-skip-derived', JText::translate('VBYES'), JText::translate('VBNO'), 0, 1, 0); ?>
109 </div>
110 </div>
111 <div class="vbo-roverw-flexnew">
112 <div class="vbo-roverw-newrwrap" data-rate-type="fixed">
113 <h4>
114 <button type="button" class="vbo-btn-transparent vbo-context-menu-rate-type">
115 <span class="vbo-transparent-wrap">
116 <?php VikBookingIcons::e('edit'); ?>
117 <span class="vbo-roverw-action-rates-title"><?php echo JText::translate('VBO_SET_FIXED_RATE'); ?></span>
118 <?php VikBookingIcons::e('chevron-down'); ?>
119 </span>
120 </button>
121 </h4>
122 <div class="vbo-roverw-newrcont" data-rate-type="fixed">
123 <label for="roverw-newrate" class="vbo-roverw-setnewrate-currency"><?php echo $currencysymb; ?></label>
124 <input type="number" step="any" min="0" id="roverw-newrate" value="" placeholder="" size="7" />
125 </div>
126 <div class="vbo-roverw-newrcont" data-rate-type="addsub" style="display: none;">
127 <div class="vbo-roverw-setnewrate-addsub-elem">
128 <select data-rate-type-rule="rmodsop">
129 <option value="1">+</option>
130 <option value="0">-</option>
131 </select>
132 </div>
133 <div class="vbo-roverw-setnewrate-addsub-elem">
134 <input type="number" value="" step="any" min="0" data-rate-type-rule="rmodsamount" />
135 </div>
136 <div class="vbo-roverw-setnewrate-addsub-elem">
137 <select data-rate-type-rule="rmodsval">
138 <option value="0"><?php echo $currencysymb; ?></option>
139 <option value="1">%</option>
140 </select>
141 </div>
142 </div>
143 </div>
144 <div class="vbo-roverw-newrestr-wrap" style="display: none;">
145 <div class="vbo-roverw-newrestrcont">
146 <h4><?php VikBookingIcons::e('ban'); ?> <?php echo JText::translate('VBOMINIMUMSTAYSET'); ?></h4>
147 <div class="vbo-roverw-newrestrcont-inner">
148 <label for="roverw-newrestr" class="vbo-roverw-setnewrestr-lbl"><?php echo JText::translate('VBDAYS'); ?></label>
149 <input type="number" step="1" min="0" id="roverw-newrestr" value="" size="7" />
150 </div>
151 </div>
152 </div>
153 </div>
154 <div class="vbo-roverw-setnewrate-inner">
155 <div class="vbo-roverw-setnewrate-vcm">
156 <div class="vbo-roverw-setnewrate-vcm-head">
157 <span class="<?php echo $vcm_enabled < 0 ? 'vbo-vcm-notinstalled' : 'vbo-vcm-installed'; ?>">
158 <?php echo $vbo_app->createPopover(array('title' => JText::translate('VBOUPDRATESONCHANNELS'), 'content' => ($vcm_enabled < 0 ? JText::translate('VBCONFIGVCMAUTOUPDMISS') : JText::translate('VBOUPDRATESONCHANNELSHELP')), 'icon_class' => VikBookingIcons::i('rocket'))); ?>
159 <?php echo JText::translate('VBOUPDRATESONCHANNELS'); ?>
160 </span>
161 </div>
162 <div class="vbo-roverw-setnewrate-vcm-body vbo-toggle-small">
163 <?php
164 echo $vbo_app->printYesNoButtons('roverw-newrate-vcm', JText::translate('VBYES'), JText::translate('VBNO'), ($vcm_enabled > 0 ? 1 : 0), 1, 0, 'vboActionVcmRestrictionsSupported();', ['blue']);
165
166 if ($vcm_enabled < 0) {
167 // disable the toggle button when VCM is not available
168 ?>
169 <script type="text/javascript">
170 jQuery(function() {
171 jQuery('input[name="roverw-newrate-vcm"]').prop('disabled', true);
172 });
173 </script>
174 <?php
175 }
176 ?>
177 </div>
178 <div class="vbo-roverw-setnewrate-vcm-otas"></div>
179 </div>
180 </div>
181 </div>
182 </div>
183
184 <div class="vbo-roverw-setnewrate-vcm-ota-pricing-alteration">
185 <div class="vbo-roverw-setnewrate-vcm-ota-alteration-elem">
186 <select data-alter-rule="rmodsop">
187 <option value="1">+</option>
188 <option value="0">-</option>
189 </select>
190 </div>
191 <div class="vbo-roverw-setnewrate-vcm-ota-alteration-elem">
192 <input type="number" value="" step="any" min="0" data-alter-rule="rmodsamount" />
193 </div>
194 <div class="vbo-roverw-setnewrate-vcm-ota-alteration-elem">
195 <select data-alter-rule="rmodsval">
196 <option value="1">%</option>
197 <option value="0"><?php echo $currencysymb; ?></option>
198 </select>
199 </div>
200 </div>
201
202 </div>
203
204 <script type="text/javascript">
205 /**
206 * Register room-ota relations map.
207 */
208 const vboActionRoomOtaRels = <?php echo json_encode($room_ota_relations); ?>;
209
210 /**
211 * Register room namings map.
212 */
213 const vboActionRoomNames = <?php echo json_encode($room_names_map); ?>;
214
215 /**
216 * Register room rate plans map.
217 */
218 const vboActionRoomRplansMap = {};
219
220 /**
221 * Register multi-calendar action counters.
222 */
223 const vboMulticalendarActionCounters = {
224 apply: 0,
225 add_to_queue: 0,
226 };
227
228 /**
229 * Register cells matrix selection abort controller (can be re-declared).
230 */
231 let vboRateMatrixController = new AbortController();
232
233 /**
234 * Register function for loading the room rates.
235 */
236 const vboActionLoadRoomRates = (from_date, to_date, room_ids) => {
237 let ctx_elem = document
238 .querySelector('.vbo-context-menu-overview-actions');
239
240 let lbl_elem = ctx_elem
241 .querySelector('.vbo-context-menu-lbl');
242
243 let orig_lbl = lbl_elem.innerText;
244
245 // start loading animation
246 ctx_elem.loading = 1;
247 lbl_elem.innerHTML = '<?php VikBookingIcons::e('circle-notch', 'fa-spin fa-fw'); ?>';
248
249 // make the request
250 VBOCore.doAjax(
251 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=pricing.loadRoomRates'); ?>",
252 {
253 room_ids: room_ids,
254 from_date: from_date,
255 to_date: to_date,
256 restrictions: true,
257 },
258 (resp) => {
259 try {
260 // decode the response (if needed), and append the content to the modal body
261 resp = typeof resp === 'string' ? JSON.parse(resp) : resp;
262
263 // hide any previously loaded room rate row, if any
264 vboActionHideRoomRates();
265
266 // get the date object for today at midnight as limit for past dates
267 let todayMidnight = new Date();
268 todayMidnight.setHours(0, 0, 0, 0);
269
270 // populate rates for all listings by scanning all month tables
271 document
272 .querySelectorAll('table.vboverviewtable[data-month-from]')
273 // iterate months
274 .forEach((month) => {
275 month
276 .querySelectorAll('.roomname[data-roomid]')
277 // iterate month listings
278 .forEach((roomRowCell) => {
279 if (roomRowCell.matches('.subroomname')) {
280 // skip sub-unit rows
281 return;
282 }
283
284 // get current listing ID
285 let listingId = roomRowCell.getAttribute('data-roomid');
286
287 if (!resp.hasOwnProperty(listingId)) {
288 // no rates returned for this room id
289 return;
290 }
291
292 // access the "parent row"
293 let parentRow = roomRowCell.closest('tr');
294
295 // check whether the listing is a multi-unit room-type
296 let isMultiUnit = (roomRowCell.getAttribute('data-units') || 1) > 1;
297
298 // build room-rates row for the current listing
299 let ratesRow = document.createElement('tr');
300 ratesRow.classList.add('vbo-roomrates-row');
301 ratesRow.setAttribute('data-roomid', listingId);
302
303 // declare flag for rate plan id
304 let ratePlanId = '';
305
306 // build first row-cell and append it to the row
307 let ratesCell = document.createElement('td');
308 ratesCell.classList.add('vbo-roomrates-cell-first');
309 ratesCell.innerHTML = '<?php VikBookingIcons::e('calculator'); ?>';
310 let cellNameEl = document.createElement('span');
311 cellNameEl.classList.add('vbo-roomrates-cell-name');
312 cellNameEl.innerText = <?php echo json_encode(JText::translate('VBO_RATES_AND_RESTR')); ?>;
313 ratesCell.append(cellNameEl);
314 ratesRow.append(ratesCell);
315
316 // iterate over the "parent row" day cells of the current listing and month
317 parentRow
318 .querySelectorAll('td.vbo-grid-avcell[data-day]')
319 // iterate month listing days
320 .forEach((roomRowDay) => {
321 // get the day Y-m-d value
322 let ymd = roomRowDay.getAttribute('data-day');
323 let [parseYear, parseMonth, parseDay] = ymd.split('-').map(Number);
324 let ymdMidnight = new Date(parseYear, parseMonth - 1, parseDay);
325
326 // check whether the day is in the past
327 let isPast = todayMidnight > ymdMidnight;
328
329 // build month-day cell with pricing details
330 let rateDayCell = document.createElement(isMultiUnit ? 'td' : 'span');
331 // differentiate a multi-unit room row with independent cell from a single-unit cell on main availability row
332 rateDayCell.classList.add(isMultiUnit ? 'vbo-roomrates-cell-day' : 'vbo-grid-cell-rate');
333 if (isMultiUnit) {
334 // set data attribute to independent cell
335 rateDayCell.setAttribute('data-day', ymd);
336 // add the same class to independent cell to identify a rate-cell
337 rateDayCell.classList.add('vbo-grid-avcell-rates');
338 // check if it's a past date
339 if (isPast) {
340 rateDayCell.setAttribute('data-ispast', '1');
341 }
342 } else {
343 // add class to parent cell that will be containing the rates
344 roomRowDay.classList.add('vbo-grid-avcell-rates');
345 // check if it's a past date
346 if (isPast) {
347 roomRowDay.setAttribute('data-ispast', '1');
348 }
349 }
350
351 // access the pricing information for this day
352 if (resp[listingId].hasOwnProperty(ymd)) {
353 if (!ratePlanId) {
354 // set rate plan id value-flag for data attribute on main rate row
355 ratePlanId = resp[listingId][ymd].idprice;
356 ratesRow.setAttribute('data-rateid', ratePlanId);
357 // update global rate plans map
358 vboActionRoomRplansMap[listingId] = resp[listingId][ymd].idprice;
359 }
360
361 // build rate amount element
362 let rateAmountEl = document.createElement('span');
363 rateAmountEl.classList.add('vbo-roomrates-cell-rate-amount');
364 rateAmountEl.innerHTML = VBOCore.getCurrency().format(resp[listingId][ymd].cost);
365
366 // append rate amount element to cell
367 rateDayCell.append(rateAmountEl);
368
369 if ((resp[listingId][ymd]?.restrictions?.minlos || 0) > 0) {
370 // build min-los element
371 let minLosEl = document.createElement('span');
372 minLosEl.classList.add('vbo-roomrates-cell-minlos');
373 minLosEl.innerHTML = '<?php VikBookingIcons::e('moon'); ?> ' + resp[listingId][ymd].restrictions.minlos;
374
375 // append min-los element to cell
376 rateDayCell.append(minLosEl);
377 }
378 }
379
380 if (isMultiUnit) {
381 // append rate cell to main rate row
382 ratesRow.append(rateDayCell);
383 } else if (roomRowDay.matches('.notbusy')) {
384 // single-unit listing with free cell, append the rate information to the parent row, under the current cell
385 roomRowDay.insertAdjacentElement('beforeend', rateDayCell);
386 }
387 });
388
389 if (isMultiUnit) {
390 // append the main rate row to the DOM
391 parentRow.insertAdjacentElement('afterend', ratesRow);
392 }
393 });
394 });
395
396 // iterating completed, stop loading
397 lbl_elem.innerHTML = '';
398 lbl_elem.innerText = orig_lbl;
399 ctx_elem.loading = 0;
400
401 // register events for the room-rate cells
402 vboActionRegisterRoomRateEvents();
403
404 // restore any previous temporary data for setting new rates within a queue
405 let previousQueue = VBOCore.getAdminDock().loadTemporaryData(
406 {
407 id: '_tmp',
408 persist_id: 'setnewrates',
409 },
410 (queueData) => {
411 // temporary data restored from dock
412 vboActionDisplayRatesQueue(queueData);
413 },
414 (queueData) => {
415 // temporary data removed from dock
416 document.querySelectorAll('.vbo-cell-pending-update').forEach((pending_cell) => {
417 // remove pending update class
418 pending_cell.classList.remove('vbo-cell-pending-update');
419 // restore initial rate in case rates were set for increase/decrease
420 let rateAmountEl = pending_cell.querySelector('.vbo-roomrates-cell-rate-amount');
421 if (rateAmountEl && pending_cell.getAttribute('data-init-rate')) {
422 rateAmountEl.innerHTML = VBOCore.getCurrency().format(pending_cell.getAttribute('data-init-rate'));
423 pending_cell.setAttribute('data-init-rate', '');
424 }
425 });
426 }
427 );
428
429 if (previousQueue && previousQueue?.data && Array.isArray(previousQueue?.data)) {
430 // ensure the cells of the previous queue will get the pending update class
431 let previousQueueData = previousQueue.data;
432 let todayMidnight = new Date();
433 todayMidnight.setHours(0, 0, 0, 0);
434
435 // get all month tables to find the requested listing-day cells
436 let monthTables = document
437 .querySelectorAll('table.vboverviewtable[data-month-from]');
438
439 // scan the previous request data objects
440 previousQueueData.forEach((prevData) => {
441 if (!prevData?.id_room || !prevData?.fromdate || !prevData?.todate) {
442 // invalid rates data format
443 return;
444 }
445
446 // pre-flight check to identify the row for the current listing
447 let listingRateRow = document.querySelector('.vbo-roomrates-row[data-roomid="' + prevData.id_room + '"]');
448 if (!listingRateRow) {
449 // may be a single-unit listing
450 listingRateRow = document.querySelector('.vboverviewtablerow[data-roomid="' + prevData.id_room + '"]');
451 }
452
453 if (!listingRateRow) {
454 // listing no longer in the page, regardless of the month
455 return;
456 }
457
458 // parse the date elements
459 let [parseFromYear, parseFromMonth, parseFromDay] = prevData.fromdate.split('-').map(Number);
460 let [parseToYear, parseToMonth, parseToDay] = prevData.todate.split('-').map(Number);
461
462 // build the date objects
463 let iterDate = new Date(parseFromYear, parseFromMonth - 1, parseFromDay);
464 let end = new Date(parseToYear, parseToMonth - 1, parseToDay);
465
466 // loop through the dates interval
467 while (iterDate <= end) {
468 if (iterDate < todayMidnight) {
469 // go to next day by cloning the date object
470 iterDate = new Date(iterDate.setDate(iterDate.getDate() + 1));
471 iterDate.setHours(0, 0, 0, 0);
472
473 // do not proceed with a date in the past
474 continue;
475 }
476
477 // build the current day key in Y-m-d format
478 let day_key = VBOCore.formatDate(iterDate, 'Y-m-d');
479
480 // iterate over all months to identify the requested listing-day rate cell
481 let listingDayRateCell = null;
482 monthTables.forEach((monthTable) => {
483 if (listingDayRateCell) {
484 // desired cell was found already
485 return;
486 }
487
488 // identify, again, the row for the current listing, but in the current month
489 let listingRateRow = monthTable.querySelector('.vbo-roomrates-row[data-roomid="' + prevData.id_room + '"]');
490 if (!listingRateRow) {
491 // may be a single-unit listing
492 listingRateRow = monthTable.querySelector('.vboverviewtablerow[data-roomid="' + prevData.id_room + '"]');
493 }
494
495 if (!listingRateRow) {
496 // should not happen as it was found above during the pre-flight check
497 return;
498 }
499
500 // query the desired cell
501 listingDayRateCell = listingRateRow
502 .querySelector('.vbo-grid-avcell-rates[data-day="' + day_key + '"]');
503 });
504
505 if (!listingDayRateCell) {
506 // go to next day by cloning the date object
507 iterDate = new Date(iterDate.setDate(iterDate.getDate() + 1));
508 iterDate.setHours(0, 0, 0, 0);
509
510 // listing cell date no longer in the document
511 continue;
512 }
513
514 // find rate cell for single-unit listing
515 let dayRateCellAmount = listingDayRateCell.querySelector('.vbo-grid-cell-rate');
516 if (!dayRateCellAmount) {
517 // must be a multi-unit listing with a dedicated cell for the rate
518 dayRateCellAmount = listingDayRateCell;
519 }
520
521 if (dayRateCellAmount) {
522 // restore the pending update class
523 listingDayRateCell.classList.add('vbo-cell-pending-update');
524
525 let rateAmountEl = listingDayRateCell.querySelector('.vbo-roomrates-cell-rate-amount');
526 if (rateAmountEl) {
527 // store the initial rate value
528 if (!listingDayRateCell.getAttribute('data-init-rate')) {
529 listingDayRateCell.setAttribute('data-init-rate', rateAmountEl.textContent.replace(/[^0-9\.]+/g, ''));
530 }
531
532 // cell is not occupied by a reservation, update the rate from the previous queue
533 if (prevData?.rate_type === 'addsub') {
534 // increase/decrease rates
535 let addsub_op = prevData?.addsub_op == 1 ? '+' : '-';
536 if (prevData?.addsub_value == 1) {
537 // percent
538 rateAmountEl.innerHTML = addsub_op + ' ' + prevData.addsub_amount + '%';
539 } else {
540 // fixed
541 rateAmountEl.innerHTML = addsub_op + ' ' + VBOCore.getCurrency().format(prevData.addsub_amount);
542 }
543 } else {
544 // fixed rate
545 rateAmountEl.innerHTML = VBOCore.getCurrency().format(prevData.rate);
546 }
547
548 if (prevData.minlos) {
549 // update minimum stay from the previous queue
550 let minlosEl = listingDayRateCell.querySelector('.vbo-roomrates-cell-minlos');
551 minlosEl.innerHTML = '<?php VikBookingIcons::e('moon'); ?> ' + prevData.minlos;
552 }
553 }
554 }
555
556 // go to next day by cloning the date object
557 iterDate = new Date(iterDate.setDate(iterDate.getDate() + 1));
558 iterDate.setHours(0, 0, 0, 0);
559 }
560 });
561 }
562 } catch (err) {
563 console.error('Error decoding the response', err, resp);
564 }
565 },
566 (error) => {
567 // display error message
568 alert(error.responseText);
569
570 // stop loading
571 lbl_elem.innerHTML = '';
572 lbl_elem.innerText = orig_lbl;
573 ctx_elem.loading = 0;
574 }
575 );
576 };
577
578 /**
579 * Register function to hide the room rates.
580 */
581 const vboActionHideRoomRates = (unregisterEvents) => {
582 // hide rows for multi-unit room-types
583 document
584 .querySelectorAll('.vbo-roomrates-row')
585 .forEach((row) => {
586 row.remove();
587 });
588
589 // hide rate cells for single-unit listings
590 document
591 .querySelectorAll('.vbo-grid-cell-rate')
592 .forEach((cell) => {
593 cell.closest('td').classList.remove('vbo-grid-avcell-rates');
594 cell.remove();
595 });
596
597 if (unregisterEvents) {
598 // use the abort controller to destroy all the events previously registered
599 vboRateMatrixController.abort();
600 }
601 };
602
603 /**
604 * Build default object to handle the selection of the room rates.
605 */
606 const vboActionRoomRateData = {
607 start: null,
608 end: null,
609 listingIdStart: null,
610 listingIdEnd: null,
611 traverseDir: null,
612 listingIds: [],
613 };
614
615 /**
616 * Register function to gather the list of involved room rate cells matrix for selection.
617 *
618 * @param Date start Date object for the first selection.
619 * @param Date end Date object for the last selection.
620 * @param number listingStart Listing ID for the first selection.
621 * @param number listingEnd Listing ID for the last selection.
622 *
623 * @return Array Linear array of rows (listings) and list of dates.
624 */
625 const vboActionRoomRateGetCellsMatrix = (start, end, listingStart, listingEnd) => {
626 let matrix = [];
627
628 if (!start instanceof Date || !end instanceof Date) {
629 // abort for invalid arguments
630 return matrix;
631 }
632
633 // start the pool of listings involved in the selection
634 let listingsInvolved = [listingStart];
635
636 // start container for the first day-cell clicked on the initial listing
637 let initialCell;
638
639 // build the start day key in Y-m-d format
640 let start_day_key = VBOCore.formatDate(start, 'Y-m-d');
641
642 // identify the initial cell clicked
643 initialCell = document
644 .querySelector('.vbo-roomrates-row[data-roomid="' + listingStart + '"] .vbo-grid-avcell-rates[data-day="' + start_day_key + '"]');
645
646 if (!initialCell) {
647 // single-unit listings have a different row class
648 initialCell = document
649 .querySelector('.vboverviewtablerow[data-roomid="' + listingStart + '"] .vbo-grid-avcell-rates[data-day="' + start_day_key + '"]');
650 }
651
652 // check whether the selection affects multiple listings and eventually gather their IDs
653 if (listingStart != listingEnd) {
654 // traverse row elements heading toward the document end (down) to identify the landing listing ID row and all listings involved
655 let nextSiblingRow = initialCell.closest('tr').nextElementSibling;
656 while (nextSiblingRow != null) {
657 if ((!nextSiblingRow.matches('tr.vbo-roomrates-row') && !nextSiblingRow.matches('tr.vboverviewtablerow')) || nextSiblingRow.matches('.vboverviewtablerow-subunit')) {
658 // invalid or sub-unit row
659 // go to the next listing row downwards
660 nextSiblingRow = nextSiblingRow.nextElementSibling;
661 continue;
662 }
663
664 let currentListing = nextSiblingRow.getAttribute('data-roomid');
665
666 if (!currentListing || isNaN(currentListing)) {
667 // invalid row
668 // go to the next listing row downwards
669 nextSiblingRow = nextSiblingRow.nextElementSibling;
670 continue;
671 }
672
673 // push listing ID involved
674 listingsInvolved.push(currentListing);
675
676 if (currentListing == listingEnd) {
677 // all rows found
678 vboActionRoomRateData.traverseDir = 'down';
679 break;
680 }
681
682 // go to the next listing row downwards
683 nextSiblingRow = nextSiblingRow.nextElementSibling;
684 }
685
686 // check if the end of the selection was made upwards
687 if (!listingsInvolved.includes(listingEnd)) {
688 // reset pool of listings involved
689 listingsInvolved = [listingStart];
690
691 // traverse row elements heading toward the document root (up) to identify the landing listing ID row and all listings involved
692 let prevSiblingRow = initialCell.closest('tr').previousElementSibling;
693 while (prevSiblingRow != null) {
694 if ((!prevSiblingRow.matches('tr.vbo-roomrates-row') && !prevSiblingRow.matches('tr.vboverviewtablerow')) || prevSiblingRow.matches('.vboverviewtablerow-subunit')) {
695 // invalid or sub-unit row
696 // go to the previous listing row upwards
697 prevSiblingRow = prevSiblingRow.previousElementSibling;
698 continue;
699 }
700
701 let currentListing = prevSiblingRow.getAttribute('data-roomid');
702
703 if (!currentListing || isNaN(currentListing)) {
704 // invalid row
705 // go to the previous listing row upwards
706 prevSiblingRow = prevSiblingRow.previousElementSibling;
707 continue;
708 }
709
710 // push listing ID involved
711 listingsInvolved.push(currentListing);
712
713 if (currentListing == listingEnd) {
714 // all rows found
715 vboActionRoomRateData.traverseDir = 'up';
716 break;
717 }
718
719 // go to the previous listing row upwards
720 prevSiblingRow = prevSiblingRow.previousElementSibling;
721 }
722 }
723
724 if (!listingsInvolved.includes(listingEnd)) {
725 // could not identify the landing row, neither by traversing the document downwards, nor upwards
726 // reset the pool of listings involved
727 listingsInvolved = [listingStart];
728 }
729 }
730
731 // update listing IDs involved
732 vboActionRoomRateData.listingIds = listingsInvolved;
733
734 // iterate all listing rows (IDs) involved to build the matrix of rows and cells selected
735 listingsInvolved.forEach((listingId) => {
736 // start matrix row for the current listing
737 let listingCells = [];
738
739 // clone the start date object
740 let iterDate = new Date(start);
741 iterDate.setHours(0, 0, 0, 0);
742
743 // loop through the dates interval
744 while (iterDate <= end) {
745 // build the current day key in Y-m-d format
746 let day_key = VBOCore.formatDate(iterDate, 'Y-m-d');
747
748 // query the desired cell
749 let listingDayRateCell = document
750 .querySelector('.vbo-roomrates-row[data-roomid="' + listingId + '"] .vbo-grid-avcell-rates[data-day="' + day_key + '"]');
751
752 if (!listingDayRateCell) {
753 // single-unit listings have a different row class
754 listingDayRateCell = document
755 .querySelector('.vboverviewtablerow[data-roomid="' + listingId + '"] .vbo-grid-avcell-rates[data-day="' + day_key + '"]');
756 }
757
758 if (listingDayRateCell) {
759 // push listing cell
760 listingCells.push(listingDayRateCell);
761 }
762
763 // go to next day by cloning again the date object
764 iterDate = new Date(iterDate.setDate(iterDate.getDate() + 1));
765 iterDate.setHours(0, 0, 0, 0);
766 }
767
768 if (listingCells.length) {
769 // push row to matrix for the current listing
770 matrix.push(listingCells);
771 }
772 });
773
774 return matrix;
775 };
776
777 /**
778 * Register function to handle the click event on a room rate cell.
779 */
780 const vboActionRoomRateHandleClick = (cell, event) => {
781 if (vboActionRoomRateData.end) {
782 // selection terminated
783 return;
784 }
785
786 let day = cell.getAttribute('data-day'), listingId;
787
788 if (!day) {
789 return;
790 }
791
792 let [parseYear, parseMonth, parseDay] = day.split('-').map(Number);
793
794 if (cell.matches('.vbo-roomrates-cell-day')) {
795 // multi-unit room row cell
796 listingId = cell.closest('tr.vbo-roomrates-row').getAttribute('data-roomid');
797 } else {
798 // single-unit room row cell
799 listingId = cell.closest('tr').querySelector('td.roomname').getAttribute('data-roomid');
800 }
801
802 if (!listingId) {
803 return;
804 }
805
806 if (!vboActionRoomRateData.start) {
807 // start selection
808 let startDate = new Date(parseYear, parseMonth - 1, parseDay);
809 let todayMidnight = new Date();
810 todayMidnight.setHours(0, 0, 0, 0);
811
812 if (todayMidnight > startDate) {
813 // starting a selection on a past date is forbidden
814 return;
815 }
816
817 // populate values to start the selection
818 vboActionRoomRateData.start = startDate;
819 vboActionRoomRateData.listingIdStart = listingId;
820
821 // set cell class
822 cell.classList.add('vbo-cell-selected');
823 cell.classList.add('vbo-cell-selected-first');
824
825 // abort
826 return;
827 }
828
829 let endDate = new Date(parseYear, parseMonth - 1, parseDay);
830
831 if (endDate < vboActionRoomRateData.start) {
832 // date in the past clicked, reset selection
833 vboActionRoomRateHandleReset();
834
835 // start selection
836 vboActionRoomRateData.start = endDate;
837
838 // set cell class
839 cell.classList.add('vbo-cell-selected');
840 cell.classList.add('vbo-cell-selected-first');
841
842 // abort
843 return;
844 }
845
846 // stop event propagation
847 if (event) {
848 event.preventDefault();
849 event.stopPropagation();
850 }
851
852 // terminate the selection
853 vboActionRoomRateData.end = endDate;
854 vboActionRoomRateData.listingIdEnd = listingId;
855 cell.classList.add('vbo-cell-selected');
856 cell.classList.add('vbo-cell-selected-last');
857
858 // obtain the listings involved
859 let listingsInvolved = [];
860 let involvedCellsMatrix = vboActionRoomRateGetCellsMatrix(vboActionRoomRateData.start, vboActionRoomRateData.end, vboActionRoomRateData.listingIdStart, vboActionRoomRateData.listingIdEnd);
861 // iterate over all the involved cells to apply the selected class
862 involvedCellsMatrix.forEach((listingCells, rowIndex) => {
863 // push listing ID involved
864 listingsInvolved.push(listingCells[0].closest('tr').getAttribute('data-roomid'));
865 });
866
867 // define the modal cancel button
868 let cancel_btn = jQuery('<button></button>')
869 .attr('type', 'button')
870 .addClass('btn')
871 .text(<?php echo json_encode(JText::translate('VBANNULLA')); ?>)
872 .on('click', () => {
873 VBOCore.emitEvent('vbo-overv-setnewrates-dismiss');
874 });
875
876 // let check default action depending on counters
877 let def_action = vboMulticalendarActionCounters.add_to_queue && vboMulticalendarActionCounters.add_to_queue > vboMulticalendarActionCounters.apply ? 'queue' : 'apply';
878
879 // define the modal apply buttons content
880 let apply_btn = jQuery('<button></button>')
881 .attr('type', 'button')
882 .addClass('btn')
883 .addClass('btn-primary')
884 .text(def_action == 'queue' ? <?php echo json_encode(JText::translate('VBO_ADD_TO_QUEUE')); ?> : <?php echo json_encode(JText::translate('VBAPPLY')); ?>)
885 .on('click', () => {
886 if (def_action == 'queue') {
887 // queue the rates update request
888 vboActionQueueRatesUpdate();
889 } else {
890 // apply new rates
891 vboActionApplyNewRates();
892 }
893 });
894
895 let actions_btn = jQuery('<button></button>')
896 .attr('type', 'button')
897 .addClass('btn')
898 .addClass('btn-primary')
899 .html('<?php VikBookingIcons::e('ellipsis-h'); ?>');
900
901 // define the context menu for the actions button
902 jQuery(actions_btn).vboContextMenu({
903 placement: 'top-left',
904 buttons: [
905 {
906 class: 'vbo-context-menu-entry-secondary',
907 text: <?php echo json_encode(JText::translate('VBAPPLY')); ?>,
908 icon: '<?php echo VikBookingIcons::i('rocket'); ?>',
909 separator: true,
910 action: (root, event) => {
911 // apply new rates
912 vboActionApplyNewRates();
913 },
914 },
915 {
916 class: 'vbo-context-menu-entry-secondary',
917 text: <?php echo json_encode(JText::translate('VBO_ADD_TO_QUEUE')); ?>,
918 icon: '<?php echo VikBookingIcons::i('stopwatch'); ?>',
919 action: (root, event) => {
920 // queue the rates update request
921 vboActionQueueRatesUpdate();
922 },
923 },
924 ],
925 });
926
927 let apply_btn_content = jQuery('<div></div>')
928 .addClass('btn-group')
929 .addClass('vbo-context-menu-btn-group')
930 .append(apply_btn)
931 .append(actions_btn);
932
933 // populate OTA relations and operation details
934 vboActionOvervSetAllRoomRelations(listingsInvolved);
935
936 // check if restrictions can be managed
937 vboActionVcmRestrictionsSupported();
938
939 // handle modal display with a small delay
940 setTimeout(() => {
941 // display modal
942 let modalBody = VBOCore.displayModal({
943 suffix: 'overv_setnewrates_modal',
944 title: <?php echo json_encode(JText::translate('VBRATESOVWSETNEWRATE')); ?>,
945 extra_class: 'vbo-modal-rounded vbo-modal-tall',
946 body_prepend: true,
947 lock_scroll: true,
948 draggable: true,
949 footer_left: cancel_btn,
950 footer_right: apply_btn_content,
951 loading_event: 'vbo-overv-setnewrates-loading',
952 dismiss_event: 'vbo-overv-setnewrates-dismiss',
953 progress_event: 'vbo-overv-setnewrates-progress',
954 loading_body: '<?php VikBookingIcons::e('refresh', 'fa-spin fa-3x fa-fw'); ?>',
955 onDismiss: () => {
956 // reset dates selection
957 vboActionRoomRateHandleReset();
958
959 // reset room-ota relations
960 jQuery('.vbo-roverw-setnewrate-vcm-otas').html('');
961
962 // move the element back to its original position
963 jQuery('.vbo-overview-action-raterestr-wrap').appendTo('.vbo-overview-action-raterestr-helper');
964 },
965 });
966
967 jQuery('.vbo-overview-action-raterestr-wrap').appendTo(modalBody);
968 }, 100);
969 };
970
971 /**
972 * Register function to handle the mouseover event on a room rate cell.
973 */
974 const vboActionRoomRateHandleHover = (cell) => {
975 if (!vboActionRoomRateData.start || vboActionRoomRateData.end) {
976 // abort when the selection has not been started or has been terminated
977 return;
978 }
979
980 let day = cell.getAttribute('data-day'), listingId;
981
982 if (!day) {
983 return;
984 }
985
986 let [parseYear, parseMonth, parseDay] = day.split('-').map(Number);
987
988 if (cell.matches('.vbo-roomrates-cell-day')) {
989 // multi-unit room row cell
990 listingId = cell.closest('tr.vbo-roomrates-row').getAttribute('data-roomid');
991 } else {
992 // single-unit room row cell
993 listingId = cell.closest('tr').querySelector('td.roomname').getAttribute('data-roomid');
994 }
995
996 if (!listingId) {
997 return;
998 }
999
1000 let dayDate = new Date(parseYear, parseMonth - 1, parseDay);
1001
1002 // always delete the selected class from any middle-cell
1003 document
1004 .querySelectorAll('.vbo-cell-selected')
1005 .forEach((prevCell) => {
1006 // remove the selected class from any middle-cell
1007 prevCell.classList.remove('vbo-cell-selected');
1008 prevCell.classList.remove('vbo-cell-selected-last');
1009 prevCell.classList.remove('vbo-cell-selected-middle-row-down');
1010 prevCell.classList.remove('vbo-cell-selected-middle-row-up');
1011 if (!prevCell.classList.contains('vbo-cell-selected-initial')) {
1012 prevCell.classList.remove('vbo-cell-selected-first');
1013 }
1014 });
1015
1016 if (dayDate >= vboActionRoomRateData.start) {
1017 // date in the future hovered
1018 let involvedCellsMatrix = vboActionRoomRateGetCellsMatrix(vboActionRoomRateData.start, dayDate, vboActionRoomRateData.listingIdStart, listingId);
1019 // iterate over all the involved cells to apply the selected class
1020 involvedCellsMatrix.forEach((listingCells, rowIndex) => {
1021 listingCells.forEach((selectedCell, cellIndex) => {
1022 selectedCell.classList.add('vbo-cell-selected');
1023 if (!rowIndex && !cellIndex) {
1024 // initial cell identified
1025 selectedCell.classList.add('vbo-cell-selected-initial');
1026 }
1027 if (rowIndex > 0 && vboActionRoomRateData.traverseDir) {
1028 // middle row-cell, check if downwards or upwards
1029 selectedCell.classList.add('vbo-cell-selected-middle-row-' + vboActionRoomRateData.traverseDir);
1030 }
1031 if (cellIndex == 0) {
1032 // first row-cell
1033 selectedCell.classList.add('vbo-cell-selected-first');
1034 } else if (++cellIndex == listingCells.length) {
1035 // last row-cell
1036 selectedCell.classList.add('vbo-cell-selected-last');
1037 }
1038 });
1039 });
1040 } else {
1041 // date in the past hovered, remove the rest of the classes
1042 document
1043 .querySelectorAll('.vbo-cell-selected-initial')
1044 .forEach((prevCell) => {
1045 // remove the selected class from the initial cell
1046 prevCell.classList.remove('vbo-cell-selected-initial');
1047 prevCell.classList.remove('vbo-cell-selected-first');
1048 });
1049 }
1050 };
1051
1052 /**
1053 * Register function to handle the reset of the room rate cell selections.
1054 */
1055 const vboActionRoomRateHandleReset = () => {
1056 // reset values
1057 vboActionRoomRateData.start = null;
1058 vboActionRoomRateData.end = null;
1059 vboActionRoomRateData.listingIdStart = null;
1060 vboActionRoomRateData.listingIdEnd = null;
1061 vboActionRoomRateData.traverseDir = null;
1062 vboActionRoomRateData.listingIds = [];
1063
1064 // remove room-rate selected class from cells
1065 document
1066 .querySelectorAll('.vbo-cell-selected')
1067 .forEach((cell) => {
1068 cell.classList.remove('vbo-cell-selected');
1069 cell.classList.remove('vbo-cell-selected-first');
1070 cell.classList.remove('vbo-cell-selected-last');
1071 cell.classList.remove('vbo-cell-selected-initial');
1072 cell.classList.remove('vbo-cell-selected-middle-row-down');
1073 cell.classList.remove('vbo-cell-selected-middle-row-up');
1074 });
1075 };
1076
1077 /**
1078 * Register function to add event listeners for the room rate cells.
1079 */
1080 const vboActionRegisterRoomRateEvents = () => {
1081 // make sure to clean any previous abort controller usage
1082 vboRateMatrixController.abort();
1083 vboRateMatrixController = new AbortController();
1084
1085 // scan all rate cells
1086 document
1087 .querySelectorAll('.vbo-grid-avcell-rates')
1088 .forEach((rateCell) => {
1089 // click listener
1090 rateCell.addEventListener('click', (e) => {
1091 if (!e || !e.target) {
1092 return;
1093 }
1094
1095 let element = e.target;
1096
1097 if (!element.matches('.vbo-grid-avcell-rates')) {
1098 element = element.closest('.vbo-grid-avcell-rates');
1099 }
1100
1101 vboActionRoomRateHandleClick(element, e);
1102 }, {
1103 signal: vboRateMatrixController.signal,
1104 });
1105
1106 // mouseover listener
1107 rateCell.addEventListener('mouseover', (e) => {
1108 if (!e || !e.target) {
1109 return;
1110 }
1111
1112 let element = e.target;
1113
1114 if (!element.matches('.vbo-grid-avcell-rates')) {
1115 element = element.closest('.vbo-grid-avcell-rates');
1116 }
1117
1118 vboActionRoomRateHandleHover(element);
1119 }, {
1120 signal: vboRateMatrixController.signal,
1121 });
1122 });
1123 };
1124
1125 /**
1126 * Register function to render the CM operation results into HTML.
1127 *
1128 * @param object vcm_response The raw "setnewrates" endpoint response key "vcm".
1129 * @param string listingName The name of the listing involved.
1130 *
1131 * @return string
1132 */
1133 const vboActionRenderCMResult = (vcm_response, listingName) => {
1134 if (!Array.isArray(vcm_response)) {
1135 // make sure the result is a list of result objects
1136 vcm_response = [vcm_response];
1137 }
1138
1139 let htmlres = '';
1140
1141 vcm_response.forEach((obj) => {
1142 htmlres += '<div class="vbo-vcm-rates-res-rplan-wrap">';
1143
1144 if (obj.hasOwnProperty('rplan_name')) {
1145 htmlres += '<div class="vbo-vcm-rates-res-rplan-data">';
1146 htmlres += '<strong>' + listingName + ' - ' + obj['rplan_name'] + '</strong>';
1147 if (obj.hasOwnProperty('is_derived') && obj['is_derived']) {
1148 htmlres += ' <span class="label label-info">' + <?php echo json_encode(JText::translate('VBO_IS_DERIVED_RATE')); ?> + '</span>';
1149 }
1150 htmlres += '</div>';
1151 }
1152
1153 if (obj.hasOwnProperty('channels_success')) {
1154 htmlres += '<div class="vbo-vcm-rates-res-success">';
1155 for (let ch_id in obj['channels_success']) {
1156 htmlres += '<div class="vbo-vcm-rates-res-channel">';
1157 htmlres += ' <div class="vbo-vcm-rates-res-channel-esit">';
1158 htmlres += ' <i class="<?php echo VikBookingIcons::i('check'); ?>"></i>';
1159 htmlres += ' </div>';
1160 htmlres += ' <div class="vbo-vcm-rates-res-channel-logo">';
1161 if (obj['channels_updated'].hasOwnProperty(ch_id) && obj['channels_updated'][ch_id]['logo'].length) {
1162 htmlres += '<img src="'+obj['channels_updated'][ch_id]['logo']+'" />';
1163 } else {
1164 htmlres += '<span>'+obj['channels_success'][ch_id]+'</span>';
1165 }
1166 htmlres += ' </div>';
1167 htmlres += '</div>';
1168 }
1169
1170 if (obj.hasOwnProperty('channels_bkdown')) {
1171 htmlres += '<div class="vbo-vcm-rates-res-bkdown">';
1172 htmlres += ' <div><pre>'+obj['channels_bkdown']+'</pre></div>';
1173 htmlres += '</div>';
1174 }
1175 htmlres += '</div>';
1176 }
1177
1178 if (obj.hasOwnProperty('channels_warnings')) {
1179 htmlres += '<div class="vbo-vcm-rates-res-warning">';
1180 for (let ch_id in obj['channels_warnings']) {
1181 htmlres += '<div class="vbo-vcm-rates-res-channel">';
1182 htmlres += ' <div class="vbo-vcm-rates-res-channel-esit">';
1183 htmlres += ' <i class="<?php echo VikBookingIcons::i('exclamation-triangle'); ?>"></i>';
1184 htmlres += ' </div>';
1185 htmlres += ' <div class="vbo-vcm-rates-res-channel-logo">';
1186 if (obj['channels_updated'].hasOwnProperty(ch_id) && obj['channels_updated'][ch_id]['logo'].length) {
1187 htmlres += '<img src="'+obj['channels_updated'][ch_id]['logo']+'" />';
1188 } else if (obj['channels_updated'].hasOwnProperty(ch_id)) {
1189 htmlres += '<span>'+obj['channels_updated'][ch_id]['name']+'</span>';
1190 }
1191 htmlres += ' </div>';
1192 htmlres += ' <div class="vbo-vcm-rates-res-channel-det">';
1193 htmlres += ' <pre>'+obj['channels_warnings'][ch_id]+'</pre>';
1194 htmlres += ' </div>';
1195 htmlres += '</div>';
1196 }
1197 htmlres += '</div>';
1198 }
1199
1200 if (obj.hasOwnProperty('channels_errors')) {
1201 htmlres += '<div class="vbo-vcm-rates-res-error">';
1202 for (let ch_id in obj['channels_errors']) {
1203 htmlres += '<div class="vbo-vcm-rates-res-channel">';
1204 htmlres += ' <div class="vbo-vcm-rates-res-channel-esit">';
1205 htmlres += ' <i class="<?php echo VikBookingIcons::i('times'); ?>"></i>';
1206 htmlres += ' </div>';
1207 htmlres += ' <div class="vbo-vcm-rates-res-channel-logo">';
1208 if (obj['channels_updated'].hasOwnProperty(ch_id) && obj['channels_updated'][ch_id]['logo'].length) {
1209 htmlres += ' <img src="'+obj['channels_updated'][ch_id]['logo']+'" />';
1210 } else if (obj['channels_updated'].hasOwnProperty(ch_id)) {
1211 htmlres += ' <span>'+obj['channels_updated'][ch_id]['name']+'</span>';
1212 }
1213 htmlres += ' </div>';
1214 htmlres += ' <div class="vbo-vcm-rates-res-channel-det">';
1215 htmlres += ' <pre>'+obj['channels_errors'][ch_id]+'</pre>';
1216 htmlres += ' </div>';
1217 htmlres += '</div>';
1218 }
1219 htmlres += '</div>';
1220 }
1221
1222 htmlres += '</div>';
1223 });
1224
1225 return htmlres;
1226 };
1227
1228 /**
1229 * Register function to apply new rates after a matrix selection.
1230 */
1231 const vboActionApplyNewRates = () => {
1232 let involvedCellsMatrix = vboActionRoomRateGetCellsMatrix(vboActionRoomRateData.start, vboActionRoomRateData.end, vboActionRoomRateData.listingIdStart, vboActionRoomRateData.listingIdEnd);
1233 let listingsInvolved = [];
1234 involvedCellsMatrix.forEach((listingCells, rowIndex) => {
1235 // push listing ID involved
1236 listingsInvolved.push(listingCells[0].closest('tr').getAttribute('data-roomid'));
1237 });
1238
1239 // gather rates modification values
1240 let rateType = document.querySelector('.vbo-roverw-newrwrap').getAttribute('data-rate-type') || 'fixed';
1241 let setminlos = document.querySelector('#roverw-newrestr').value;
1242 let invoke_vcm = document.querySelector('input[name="roverw-newrate-vcm"]:checked') ? 1 : 0;
1243 let addsub_op = rateType === 'addsub' ? document.querySelector('.vbo-roverw-newrcont[data-rate-type="addsub"] [data-rate-type-rule="rmodsop"]')?.value : 0;
1244 let addsub_amount = rateType === 'addsub' ? document.querySelector('.vbo-roverw-newrcont[data-rate-type="addsub"] [data-rate-type-rule="rmodsamount"]')?.value : 0;
1245 let addsub_value = rateType === 'addsub' ? document.querySelector('.vbo-roverw-newrcont[data-rate-type="addsub"] [data-rate-type-rule="rmodsval"]')?.value : 0;
1246 let setrate = parseFloat(document.querySelector('#roverw-newrate').value);
1247
1248 if (!listingsInvolved.length) {
1249 alert('No listings to update.');
1250 return;
1251 }
1252
1253 if (rateType === 'fixed' && (isNaN(setrate) || setrate <= 0)) {
1254 if (setminlos && setminlos > 0) {
1255 alert(<?php echo json_encode(JText::translate('VBO_INCR_ZERO_RESTR_TIP')); ?>);
1256 } else {
1257 // incomplete form
1258 alert('Invalid rate amount.');
1259 }
1260 return;
1261 }
1262
1263 if (rateType === 'addsub' && (isNaN(addsub_amount) || addsub_amount <= 0)) {
1264 if (!setminlos || setminlos <= 0) {
1265 alert('Invalid amount for increasing/decreasing rates.');
1266 return;
1267 } else {
1268 // allow to change just the minimum stay by forcing the operation to increase rates by 0
1269 addsub_op = 1;
1270 addsub_amount = 0;
1271 addsub_value = 0;
1272 }
1273 }
1274
1275 // increase action counter
1276 vboMulticalendarActionCounters.apply++;
1277
1278 // check the OTA pricing alteration rules, if any
1279 let ota_pricing = {};
1280 if (invoke_vcm) {
1281 // scan all OTA alteration rules, if any
1282 document.querySelectorAll('.vbo-roverw-setnewrate-ota-pricing-currentvalue[data-alteration]').forEach((elem) => {
1283 // channel alteration string
1284 let alter_string = elem.getAttribute('data-alteration');
1285 if (!alter_string) {
1286 alter_string = '';
1287 }
1288
1289 // access the parent node to get the OTA channel identifier
1290 let ota_id = elem
1291 .closest('.vbo-roverw-setnewrate-vcm-ota-relation-channel[data-otaid]')
1292 .getAttribute('data-otaid');
1293
1294 if (!ota_id || !alter_string || alter_string == '+0%' || alter_string == '+0*') {
1295 // avoid pushing an empty alteration command
1296 return;
1297 }
1298
1299 // push OTA pricing alteration command
1300 ota_pricing[ota_id] = alter_string;
1301 });
1302 }
1303
1304 if (!Object.keys(ota_pricing).length) {
1305 // unset the object for the request
1306 ota_pricing = null;
1307 }
1308
1309 // tell whether derived rate plans should be skipped
1310 let skipDerivedEl = document.querySelector('input[name="overw-skip-derived"]');
1311 let skipDerivedRates = skipDerivedEl && skipDerivedEl.checked ? 1 : 0;
1312
1313 // build the list of update requests (one per listing)
1314 let requestList = [];
1315 listingsInvolved.forEach((listingId) => {
1316 requestList.push({
1317 id_room: listingId,
1318 id_price: vboActionRoomRplansMap[listingId],
1319 rate_type: rateType,
1320 rate: setrate,
1321 addsub_op: addsub_op,
1322 addsub_amount: addsub_amount,
1323 addsub_value: addsub_value,
1324 vcm: invoke_vcm,
1325 minlos: setminlos,
1326 fromdate: VBOCore.formatDate(vboActionRoomRateData.start, 'Y-m-d'),
1327 todate: VBOCore.formatDate(vboActionRoomRateData.end, 'Y-m-d'),
1328 rateclosed: 0,
1329 ota_pricing: ota_pricing,
1330 skip_derived: skipDerivedRates,
1331 });
1332 });
1333
1334 // count the number of requests
1335 let requestCount = requestList.length;
1336
1337 // start the response container for every request
1338 let responseContainer = [];
1339
1340 // start loading
1341 VBOCore.emitEvent('vbo-overv-setnewrates-loading');
1342
1343 // set progress
1344 VBOCore.emitEvent('vbo-overv-setnewrates-progress', {
1345 progress_content: '1 / ' + requestCount,
1346 });
1347
1348 // dispatch the rates update requests
1349 vboActionDispatchRatesUpdateRequests(
1350 requestList,
1351 (obj_res, listingName) => {
1352 // one request was completed, push the result
1353 responseContainer.push(Object.assign(obj_res, {listingName: listingName}));
1354
1355 // update progress
1356 VBOCore.emitEvent('vbo-overv-setnewrates-progress', {
1357 progress_content: (requestList.length ? (requestCount - requestList.length + 1) : requestCount) + ' / ' + requestCount,
1358 });
1359 },
1360 () => {
1361 // process completed
1362 let showOTAResponse = false;
1363 let htmlOTAResponse = [];
1364
1365 // iterate the operation results
1366 responseContainer.forEach((response) => {
1367 if (typeof response !== 'object' || !response.hasOwnProperty('vcm')) {
1368 // nothing to say about this response
1369 return;
1370 }
1371
1372 // turn flag on
1373 showOTAResponse = true;
1374
1375 // build HTML response string and add it to the list
1376 htmlOTAResponse.push(vboActionRenderCMResult(response.vcm, response.listingName));
1377 });
1378
1379 if (showOTAResponse && htmlOTAResponse.length) {
1380 // display the modal with the update operation results
1381 VBOCore.displayModal({
1382 suffix: 'vbo-vcm-rates-res',
1383 extra_class: 'vbo-modal-rounded vbo-modal-tall vbo-modal-nofooter',
1384 title: <?php echo json_encode(JText::translate('VBOVCMRATESRES')); ?>,
1385 body: '<div class="vbo-vcm-rates-res-container' + (htmlOTAResponse.length > 1 ? ' vbo-vcm-ota-multicalendar-response' : '') + '">' + htmlOTAResponse.join("\n") + '</div>',
1386 draggable: true,
1387 });
1388 }
1389
1390 // update new rates on the involved cells
1391 involvedCellsMatrix.forEach((listingCells, rowIndex) => {
1392 listingCells.forEach((selectedCell, cellIndex) => {
1393 let rateAmountEl = selectedCell.querySelector('.vbo-roomrates-cell-rate-amount');
1394 if (!rateAmountEl) {
1395 // cell is occupied by a reservation, nothing to update
1396 return;
1397 }
1398
1399 // access cell day
1400 let cellDay = selectedCell.getAttribute('data-day') || '';
1401
1402 // listing ID and rate plan ID involved
1403 let parsedListingId = null;
1404 let parsedRateId = null;
1405
1406 // build date-part of expected response key by removing leading zeros in month-days and months
1407 let expectedRespKey = cellDay.replace(/-(0)([1-9]{1})/g, "-$2");
1408 let keyParts = expectedRespKey.split('-');
1409 expectedRespKey = [keyParts[2], keyParts[1], keyParts[0]].join('-');
1410
1411 // access parent cell row
1412 let parentRow = selectedCell.closest('tr[data-roomid]');
1413 if (parentRow) {
1414 // set listing ID and rate plan ID involved
1415 parsedListingId = parentRow.getAttribute('data-roomid');
1416 parsedRateId = parentRow.getAttribute('data-rateid') || vboActionRoomRplansMap[parsedListingId];
1417 }
1418
1419 // finalise expected response key parts
1420 expectedRespKey += '-' + (parsedRateId || '');
1421
1422 // scan all operation results to find the desired response property
1423 let responseFound = false;
1424 responseContainer.forEach((responseData) => {
1425 if (responseFound) {
1426 return;
1427 }
1428 if (responseData.hasOwnProperty(expectedRespKey) && responseData[expectedRespKey]['idroom'] == parsedListingId) {
1429 // turn flag on
1430 responseFound = true;
1431 // set rate applied from response obtained
1432 rateAmountEl.innerHTML = VBOCore.getCurrency().format(responseData[expectedRespKey]['cost']);
1433 }
1434 });
1435
1436 if (!responseFound && rateType !== 'addsub' && !isNaN(setrate)) {
1437 // set rate requested to be applied
1438 rateAmountEl.innerHTML = VBOCore.getCurrency().format(setrate);
1439 }
1440
1441 if (setminlos) {
1442 // populate minimum stay
1443 let minlosEl = selectedCell.querySelector('.vbo-roomrates-cell-minlos');
1444 minlosEl.innerHTML = '<?php VikBookingIcons::e('moon'); ?> ' + setminlos;
1445 }
1446
1447 // set updated class to cell
1448 selectedCell.classList.add('vbo-cell-new-update');
1449 });
1450 });
1451
1452 // dismiss the modal upon completion, after having updated the new rates on the involved cells
1453 VBOCore.emitEvent('vbo-overv-setnewrates-dismiss');
1454 },
1455 (err_mess, unrecoverable) => {
1456 // stop loading in case of error
1457 VBOCore.emitEvent('vbo-overv-setnewrates-loading');
1458 }
1459 );
1460 };
1461
1462 /**
1463 * Register function to queue a rates update request.
1464 */
1465 const vboActionQueueRatesUpdate = () => {
1466 let involvedCellsMatrix = vboActionRoomRateGetCellsMatrix(vboActionRoomRateData.start, vboActionRoomRateData.end, vboActionRoomRateData.listingIdStart, vboActionRoomRateData.listingIdEnd);
1467 let listingsInvolved = [];
1468 involvedCellsMatrix.forEach((listingCells, rowIndex) => {
1469 // push listing ID involved
1470 listingsInvolved.push(listingCells[0].closest('tr').getAttribute('data-roomid'));
1471 });
1472
1473 // gather rates modification values
1474 let rateType = document.querySelector('.vbo-roverw-newrwrap').getAttribute('data-rate-type') || 'fixed';
1475 let setminlos = document.querySelector('#roverw-newrestr').value;
1476 let invoke_vcm = document.querySelector('input[name="roverw-newrate-vcm"]:checked') ? 1 : 0;
1477 let addsub_op = rateType === 'addsub' ? document.querySelector('.vbo-roverw-newrcont[data-rate-type="addsub"] [data-rate-type-rule="rmodsop"]')?.value : 0;
1478 let addsub_amount = rateType === 'addsub' ? document.querySelector('.vbo-roverw-newrcont[data-rate-type="addsub"] [data-rate-type-rule="rmodsamount"]')?.value : 0;
1479 let addsub_value = rateType === 'addsub' ? document.querySelector('.vbo-roverw-newrcont[data-rate-type="addsub"] [data-rate-type-rule="rmodsval"]')?.value : 0;
1480 let setrate = parseFloat(document.querySelector('#roverw-newrate').value);
1481
1482 if (!listingsInvolved.length) {
1483 alert('No listings to update.');
1484 return;
1485 }
1486
1487 if (rateType === 'fixed' && (isNaN(setrate) || setrate <= 0)) {
1488 alert('Invalid rate amount.');
1489 return;
1490 }
1491
1492 if (rateType === 'addsub' && (isNaN(addsub_amount) || addsub_amount <= 0)) {
1493 alert('Invalid amount for increasing/decreasing rates.');
1494 return;
1495 }
1496
1497 // increase action counter
1498 vboMulticalendarActionCounters.add_to_queue++;
1499
1500 // check the OTA pricing alteration rules, if any
1501 let ota_pricing = {};
1502 if (invoke_vcm) {
1503 // scan all OTA alteration rules, if any
1504 document.querySelectorAll('.vbo-roverw-setnewrate-ota-pricing-currentvalue[data-alteration]').forEach((elem) => {
1505 // channel alteration string
1506 let alter_string = elem.getAttribute('data-alteration');
1507 if (!alter_string) {
1508 alter_string = '';
1509 }
1510
1511 // access the parent node to get the OTA channel identifier
1512 let ota_id = elem
1513 .closest('.vbo-roverw-setnewrate-vcm-ota-relation-channel[data-otaid]')
1514 .getAttribute('data-otaid');
1515
1516 if (!ota_id || !alter_string || alter_string == '+0%' || alter_string == '+0*') {
1517 // avoid pushing an empty alteration command
1518 return;
1519 }
1520
1521 // push OTA pricing alteration command
1522 ota_pricing[ota_id] = alter_string;
1523 });
1524 }
1525
1526 if (!Object.keys(ota_pricing).length) {
1527 // unset the object for the request
1528 ota_pricing = null;
1529 }
1530
1531 // tell whether derived rate plans should be skipped
1532 let skipDerivedEl = document.querySelector('input[name="overw-skip-derived"]');
1533 let skipDerivedRates = skipDerivedEl && skipDerivedEl.checked ? 1 : 0;
1534
1535 // build the list of update requests (one per listing)
1536 let requestList = [];
1537 listingsInvolved.forEach((listingId) => {
1538 requestList.push({
1539 id_room: listingId,
1540 id_price: vboActionRoomRplansMap[listingId],
1541 _roomName: vboActionRoomNames[listingId],
1542 rate_type: rateType,
1543 rate: setrate,
1544 addsub_op: addsub_op,
1545 addsub_amount: addsub_amount,
1546 addsub_value: addsub_value,
1547 vcm: invoke_vcm,
1548 minlos: setminlos,
1549 fromdate: VBOCore.formatDate(vboActionRoomRateData.start, 'Y-m-d'),
1550 todate: VBOCore.formatDate(vboActionRoomRateData.end, 'Y-m-d'),
1551 rateclosed: 0,
1552 ota_pricing: ota_pricing,
1553 skip_derived: skipDerivedRates,
1554 });
1555 });
1556
1557 // queue the current requests to the admin-dock as temporary data
1558 VBOCore.getAdminDock().addTemporaryData(
1559 {
1560 id: '_tmp',
1561 persist_id: 'setnewrates',
1562 name: <?php echo json_encode(JText::translate('VBO_PENDING_QUEUE')); ?>,
1563 icon: '<?php VikBookingIcons::e('stopwatch'); ?>',
1564 style: 'orange',
1565 },
1566 requestList,
1567 (queueData) => {
1568 // temporary data restored from dock
1569 vboActionDisplayRatesQueue(queueData);
1570 },
1571 (queueData) => {
1572 // temporary data removed from dock
1573 document.querySelectorAll('.vbo-cell-pending-update').forEach((pending_cell) => {
1574 // remove pending update class
1575 pending_cell.classList.remove('vbo-cell-pending-update');
1576 // restore initial rate in case rates were set for increase/decrease
1577 let rateAmountEl = pending_cell.querySelector('.vbo-roomrates-cell-rate-amount');
1578 if (rateAmountEl && pending_cell.getAttribute('data-init-rate')) {
1579 rateAmountEl.innerHTML = VBOCore.getCurrency().format(pending_cell.getAttribute('data-init-rate'));
1580 pending_cell.setAttribute('data-init-rate', '');
1581 }
1582 });
1583 }
1584 );
1585
1586 // update new rates on the involved cells
1587 involvedCellsMatrix.forEach((listingCells, rowIndex) => {
1588 listingCells.forEach((selectedCell, cellIndex) => {
1589 let rateAmountEl = selectedCell.querySelector('.vbo-roomrates-cell-rate-amount');
1590 if (!rateAmountEl) {
1591 // cell is occupied by a reservation, nothing to update
1592 return;
1593 }
1594
1595 // store the initial rate value
1596 if (!selectedCell.getAttribute('data-init-rate')) {
1597 selectedCell.setAttribute('data-init-rate', rateAmountEl.textContent.replace(/[^0-9\.]+/g, ''));
1598 }
1599
1600 // display how the rate will be applied
1601 if (rateType === 'addsub') {
1602 // increase/decrease rates
1603 let addsub_op_char = addsub_op == 1 ? '+' : '-';
1604 if (addsub_value == 1) {
1605 // percent
1606 rateAmountEl.innerHTML = addsub_op_char + ' ' + addsub_amount + '%';
1607 } else {
1608 // fixed
1609 rateAmountEl.innerHTML = addsub_op_char + ' ' + VBOCore.getCurrency().format(addsub_amount);
1610 }
1611 } else {
1612 // fixed rate
1613 rateAmountEl.innerHTML = VBOCore.getCurrency().format(setrate);
1614 }
1615
1616 // handle minimum stay information display
1617 if (setminlos) {
1618 let minlosEl = selectedCell.querySelector('.vbo-roomrates-cell-minlos');
1619 minlosEl.innerHTML = '<?php VikBookingIcons::e('moon'); ?> ' + setminlos;
1620 }
1621 selectedCell.classList.add('vbo-cell-pending-update');
1622 });
1623 });
1624
1625 // dismiss the modal upon completion, after having updated the new rates on the involved cells
1626 VBOCore.emitEvent('vbo-overv-setnewrates-dismiss');
1627 };
1628
1629 /**
1630 * Displays the information about the current rates queue data.
1631 *
1632 * @param Array ratesData The current queue data.
1633 */
1634 const vboActionDisplayRatesQueue = (ratesData) => {
1635 // define the modal cancel button
1636 let cancel_btn = jQuery('<button></button>')
1637 .attr('type', 'button')
1638 .addClass('btn')
1639 .text(<?php echo json_encode(JText::translate('VBANNULLA')); ?>)
1640 .on('click', () => {
1641 // minimize again the temporary data to the dock by dismissing the modal
1642 VBOCore.emitEvent('vbo-overv-setnewrates-queue-dismiss');
1643 });
1644
1645 // define the modal apply button
1646 let apply_btn = jQuery('<button></button>')
1647 .attr('type', 'button')
1648 .addClass('btn btn-success')
1649 .html('<?php VikBookingIcons::e('rocket'); ?> ' + <?php echo json_encode(JText::translate('VBAPPLY')); ?>)
1650 .on('click', function() {
1651 // count the number of requests
1652 let requestCount = ratesData.length;
1653
1654 if (!requestCount) {
1655 throw new Error('Rates queue is empty');
1656 }
1657
1658 // start the response container for every request
1659 let responseContainer = [];
1660
1661 // start loading
1662 VBOCore.emitEvent('vbo-overv-setnewrates-queue-loading');
1663
1664 // set progress
1665 VBOCore.emitEvent('vbo-overv-setnewrates-queue-progress', {
1666 progress_content: '1 / ' + requestCount,
1667 });
1668
1669 // dispatch the rates update requests
1670 vboActionDispatchRatesUpdateRequests(
1671 ratesData,
1672 (obj_res, listingName) => {
1673 // one request was completed, push the result
1674 responseContainer.push(Object.assign(obj_res, {listingName: listingName}));
1675
1676 // update progress
1677 VBOCore.emitEvent('vbo-overv-setnewrates-queue-progress', {
1678 progress_content: (ratesData.length ? (requestCount - ratesData.length + 1) : requestCount) + ' / ' + requestCount,
1679 });
1680 },
1681 () => {
1682 // process completed
1683 let showOTAResponse = false;
1684 let htmlOTAResponse = [];
1685
1686 // iterate the operation results
1687 responseContainer.forEach((response) => {
1688 if (typeof response !== 'object' || !response.hasOwnProperty('vcm')) {
1689 // nothing to say about this response
1690 return;
1691 }
1692
1693 // turn flag on
1694 showOTAResponse = true;
1695
1696 // build HTML response string and add it to the list
1697 htmlOTAResponse.push(vboActionRenderCMResult(response.vcm, response.listingName));
1698 });
1699
1700 if (showOTAResponse && htmlOTAResponse.length) {
1701 // display the modal with the update operation results
1702 VBOCore.displayModal({
1703 suffix: 'vbo-vcm-rates-res',
1704 extra_class: 'vbo-modal-rounded vbo-modal-tall vbo-modal-nofooter',
1705 title: <?php echo json_encode(JText::translate('VBOVCMRATESRES')); ?>,
1706 body: '<div class="vbo-vcm-rates-res-container' + (htmlOTAResponse.length > 1 ? ' vbo-vcm-ota-multicalendar-response' : '') + '">' + htmlOTAResponse.join("\n") + '</div>',
1707 draggable: true,
1708 });
1709 }
1710
1711 // update the involved cells
1712 document.querySelectorAll('.vbo-grid-avcell-rates.vbo-cell-pending-update').forEach((pendingCell) => {
1713 // handle cell class
1714 pendingCell.classList.remove('vbo-cell-pending-update');
1715 pendingCell.classList.add('vbo-cell-new-update');
1716
1717 // handle new rate applied
1718 let rateAmountEl = pendingCell.querySelector('.vbo-roomrates-cell-rate-amount');
1719 if (!rateAmountEl) {
1720 // cell is occupied by a reservation, nothing to update
1721 return;
1722 }
1723
1724 // access cell day
1725 let cellDay = pendingCell.getAttribute('data-day') || '';
1726
1727 // listing ID and rate plan ID involved
1728 let parsedListingId = null;
1729 let parsedRateId = null;
1730
1731 // build date-part of expected response key by removing leading zeros in month-days and months
1732 let expectedRespKey = cellDay.replace(/-(0)([1-9]{1})/g, "-$2");
1733 let keyParts = expectedRespKey.split('-');
1734 expectedRespKey = [keyParts[2], keyParts[1], keyParts[0]].join('-');
1735
1736 // access parent cell row
1737 let parentRow = pendingCell.closest('tr[data-roomid]');
1738 if (parentRow) {
1739 // set listing ID and rate plan ID involved
1740 parsedListingId = parentRow.getAttribute('data-roomid');
1741 parsedRateId = parentRow.getAttribute('data-rateid') || vboActionRoomRplansMap[parsedListingId];
1742 }
1743
1744 // finalise expected response key parts
1745 expectedRespKey += '-' + (parsedRateId || '');
1746
1747 // scan all operation results to find the desired response property
1748 let responseFound = false;
1749 responseContainer.forEach((responseData) => {
1750 if (responseFound) {
1751 return;
1752 }
1753 if (responseData.hasOwnProperty(expectedRespKey) && responseData[expectedRespKey]['idroom'] == parsedListingId) {
1754 // turn flag on
1755 responseFound = true;
1756 // set rate applied from response obtained
1757 rateAmountEl.innerHTML = VBOCore.getCurrency().format(responseData[expectedRespKey]['cost']);
1758 }
1759 });
1760
1761 // if the desired response key was not found, we do nothing, as the queue
1762 // must have already restored the rates data alteration details for this cell
1763 });
1764
1765 // dismiss the modal upon completion, after having updated the involved cells
1766 VBOCore.emitEvent('vbo-overv-setnewrates-queue-dismiss');
1767
1768 // at last, dismiss the admin-dock entry upon completion
1769 VBOCore.getAdminDock().removeDockElementById('_tmp');
1770 },
1771 (err_mess, unrecoverable) => {
1772 // do nothing in case of error when inside a queue of requests
1773 }
1774 );
1775 });
1776
1777 // build modal body
1778 let modalBodyEl = document.createElement('div');
1779 modalBodyEl.classList.add('vbo-rates-queue-wrapper');
1780 ratesData.forEach((updateData) => {
1781 // create queue update container
1782 let updateEl = document.createElement('div');
1783 updateEl.classList.add('vbo-rates-queue-data');
1784
1785 // contain the listing
1786 let updateRoomEl = document.createElement('span');
1787 updateRoomEl.classList.add('vbo-rates-queue-data-listing');
1788 updateRoomEl.innerText = vboActionRoomNames[updateData.id_room] || updateData._roomName || updateData.id_room;
1789 updateEl.append(updateRoomEl);
1790
1791 // contain the dates
1792 let updateDatesEl = document.createElement('span');
1793 updateDatesEl.classList.add('vbo-rates-queue-data-dates');
1794 updateDatesEl.innerHTML = '<span class="badge badge-info">' + updateData.fromdate + '</span> - <span class="badge badge-info">' + updateData.todate + '</span>';
1795 updateEl.append(updateDatesEl);
1796
1797 // contain the rate
1798 let updateRatesEl = document.createElement('span');
1799 updateRatesEl.classList.add('vbo-rates-queue-data-rate');
1800 let updateRatesBadgeEl = document.createElement('span');
1801 updateRatesBadgeEl.classList.add('badge', 'badge-warning');
1802 if (updateData?.rate_type === 'addsub') {
1803 // increase/decrease rates
1804 let addsub_op = updateData?.addsub_op == 1 ? '+' : '-';
1805 if (updateData?.addsub_value == 1) {
1806 // percent
1807 updateRatesBadgeEl.innerHTML = addsub_op + ' ' + updateData.addsub_amount + '%';
1808 } else {
1809 // fixed
1810 updateRatesBadgeEl.innerHTML = addsub_op + ' ' + VBOCore.getCurrency().format(updateData.addsub_amount);
1811 }
1812 } else {
1813 // fixed rate
1814 updateRatesBadgeEl.innerHTML = VBOCore.getCurrency().format(updateData.rate);
1815 }
1816 updateRatesEl.append(updateRatesBadgeEl);
1817 updateEl.append(updateRatesEl);
1818
1819 if (updateData.minlos) {
1820 // contain the minimum stay
1821 let updateMinlosEl = document.createElement('span');
1822 updateMinlosEl.classList.add('vbo-rates-queue-data-minlos');
1823 updateMinlosEl.innerHTML = '<span class="badge"><?php VikBookingIcons::e('moon'); ?> ' + updateData.minlos + '</span>';
1824 updateEl.append(updateMinlosEl);
1825 }
1826
1827 // append queue update container
1828 modalBodyEl.append(updateEl);
1829 });
1830
1831 // display modal
1832 VBOCore.displayModal({
1833 suffix: 'overv_setnewrates_queue_modal',
1834 title: <?php echo json_encode(JText::translate('VBO_PENDING_QUEUE')); ?>,
1835 extra_class: 'vbo-modal-rounded vbo-modal-tall',
1836 body: modalBodyEl,
1837 body_prepend: true,
1838 lock_scroll: true,
1839 draggable: true,
1840 footer_left: cancel_btn,
1841 footer_right: apply_btn,
1842 loading_event: 'vbo-overv-setnewrates-queue-loading',
1843 dismiss_event: 'vbo-overv-setnewrates-queue-dismiss',
1844 progress_event: 'vbo-overv-setnewrates-queue-progress',
1845 loading_body: '<?php VikBookingIcons::e('refresh', 'fa-spin fa-3x fa-fw'); ?>',
1846 onDismiss: () => {
1847 if (!ratesData.length) {
1848 // temporary data queue is empty
1849 return;
1850 }
1851
1852 // minimize again the temporary data to the dock
1853 VBOCore.getAdminDock().addTemporaryData(
1854 {
1855 id: '_tmp',
1856 persist_id: 'setnewrates',
1857 name: <?php echo json_encode(JText::translate('VBO_PENDING_QUEUE')); ?>,
1858 icon: '<?php VikBookingIcons::e('stopwatch'); ?>',
1859 style: 'orange',
1860 },
1861 ratesData,
1862 (queueData) => {
1863 // temporary data restored from dock
1864 vboActionDisplayRatesQueue(queueData);
1865 },
1866 (queueData) => {
1867 // temporary data removed from dock
1868 document.querySelectorAll('.vbo-cell-pending-update').forEach((pending_cell) => {
1869 // remove pending update class
1870 pending_cell.classList.remove('vbo-cell-pending-update');
1871 // restore initial rate in case rates were set for increase/decrease
1872 let rateAmountEl = pending_cell.querySelector('.vbo-roomrates-cell-rate-amount');
1873 if (rateAmountEl && pending_cell.getAttribute('data-init-rate')) {
1874 rateAmountEl.innerHTML = VBOCore.getCurrency().format(pending_cell.getAttribute('data-init-rate'));
1875 pending_cell.setAttribute('data-init-rate', '');
1876 }
1877 });
1878 }
1879 );
1880 },
1881 });
1882 };
1883
1884 /**
1885 * Register function to process a list of rates update requests one after the other.
1886 *
1887 * @param array requests List of rates update request objects.
1888 * @param function onProgress Optional callback when a request is completed.
1889 * @param function onComplete Optional callback when all requests have been completed.
1890 * @param function onError Optional callback in case of request error.
1891 *
1892 * @return void
1893 */
1894 const vboActionDispatchRatesUpdateRequests = (requests, onProgress, onComplete, onError) => {
1895 if (!Array.isArray(requests) || !requests.length) {
1896 if (typeof onComplete === 'function') {
1897 onComplete();
1898 }
1899
1900 // abort
1901 return;
1902 }
1903
1904 // obtain the request to process
1905 const request = requests.shift();
1906
1907 // perform the request
1908 VBOCore.doAjax(
1909 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=pricing.setnewrates'); ?>",
1910 request,
1911 (res) => {
1912 if (typeof res === 'string' && res.indexOf('e4j.error') === 0) {
1913 // display error
1914 let err_mess = res.replace('e4j.error.', '');
1915 alert(err_mess);
1916
1917 if (typeof onError === 'function') {
1918 // unrecoverable error
1919 onError(err_mess, true);
1920 }
1921
1922 // recursively call the same function to process the next request, if any
1923 vboActionDispatchRatesUpdateRequests(requests, onProgress, onComplete, onError);
1924
1925 // abort
1926 return;
1927 }
1928
1929 try {
1930 // check the response
1931 let obj_res = typeof res === 'string' ? JSON.parse(res) : res;
1932
1933 if (typeof onProgress === 'function') {
1934 // call the given function by passing the operation result object and the room name
1935 onProgress(obj_res, (vboActionRoomNames[request.id_room] || request.id_room));
1936 }
1937
1938 // recursively call the same function to process the next request
1939 vboActionDispatchRatesUpdateRequests(requests, onProgress, onComplete, onError);
1940 } catch(err) {
1941 // display error
1942 alert(err);
1943
1944 if (typeof onError === 'function') {
1945 // unrecoverable error
1946 onError(err, true);
1947 }
1948
1949 // recursively call the same function to process the next request, if any
1950 vboActionDispatchRatesUpdateRequests(requests, onProgress, onComplete, onError);
1951 }
1952 },
1953 (err) => {
1954 // display error
1955 let err_mess = err.responseText || 'Request failed due to connection error';
1956 alert(err_mess);
1957
1958 if (typeof onError === 'function') {
1959 // connection error
1960 onError(err_mess, false);
1961 }
1962
1963 // recursively call the same function to process the next request, if any
1964 vboActionDispatchRatesUpdateRequests(requests, onProgress, onComplete, onError);
1965 }
1966 );
1967 };
1968
1969 /**
1970 * Register function to populate the room ota relations upon selecting a matrix of listings + dates.
1971 */
1972 const vboActionOvervSetAllRoomRelations = (listingsInvolved) => {
1973 // build the current day key in Y-m-d format
1974 let start_day_key = VBOCore.formatDate(vboActionRoomRateData.start, 'Y-m-d');
1975 let end_day_key = VBOCore.formatDate(vboActionRoomRateData.end, 'Y-m-d');
1976
1977 // TODO format dates according to settings
1978
1979 let listingsText = listingsInvolved.length > 1 ? listingsInvolved.length + ' ' + <?php echo json_encode(JText::translate('VBO_LISTINGS')); ?> : (vboActionRoomNames[listingsInvolved[0]] || listingsInvolved[0]);
1980
1981 // set operation details first
1982 jQuery('.vbo-overview-action-raterestr-listings').text(listingsText);
1983 jQuery('.vbo-overview-action-raterestr-dates').html('<span class="badge badge-info">' + start_day_key + '</span> - <span class="badge badge-info">' + end_day_key + '</span>');
1984
1985 // the room-ota relations wrapper
1986 let wrapper = jQuery('.vbo-roverw-setnewrate-vcm-otas');
1987
1988 // always empty the wrapper
1989 wrapper.html('');
1990
1991 // store a list of parsed channel IDs
1992 let parsedChannelIds = [];
1993
1994 listingsInvolved.forEach((room_id) => {
1995 let rplan_id = vboActionRoomRplansMap[room_id];
1996 if (!room_id || !rplan_id || !vboActionRoomOtaRels.hasOwnProperty(room_id)) {
1997 // nothing to render for the current listing
1998 return;
1999 }
2000
2001 // start counter
2002 let ota_ch_counter = 0;
2003
2004 // build and append room-OTA relations
2005 for (const ota_name in vboActionRoomOtaRels[room_id]['channels']) {
2006 // get the current channel ID
2007 let channel_id = vboActionRoomOtaRels[room_id]['accounts'][ota_ch_counter]['idchannel'];
2008
2009 if (parsedChannelIds.includes(channel_id)) {
2010 // do not display the same channel multiple times
2011 return;
2012 }
2013
2014 // push channel ID parsed
2015 parsedChannelIds.push(channel_id);
2016
2017 // build ota readable name
2018 let ota_read_name = ota_name;
2019 ota_read_name = ota_read_name.replace(/api$/, '');
2020 ota_read_name = ota_read_name.replace(/^(google)(hotel|vr)$/i, '$1 $2');
2021
2022 // build room-ota relation block and elements
2023 let ota_block = jQuery('<div></div>');
2024 ota_block.addClass('vbo-roverw-setnewrate-vcm-ota-relation');
2025
2026 let ota_block_inner = jQuery('<div></div>');
2027 ota_block_inner
2028 .addClass('vbo-roverw-setnewrate-vcm-ota-relation-pricing')
2029 .attr('data-ota', (ota_name + '').toLowerCase());
2030
2031 let ota_block_channel = jQuery('<div></div>');
2032 ota_block_channel
2033 .addClass('vbo-roverw-setnewrate-vcm-ota-relation-channel')
2034 .attr('data-otaid', vboActionRoomOtaRels[room_id]['accounts'][ota_ch_counter]['idchannel'])
2035 .append('<img src="' + vboActionRoomOtaRels[room_id]['channels'][ota_name] + '" />')
2036 .append('<span>' + ota_read_name + '</span>');
2037
2038 let ota_pricing_value = jQuery('<span></span>');
2039 ota_pricing_value
2040 .addClass('vbo-roverw-setnewrate-vcm-ota-pricing-startvalue')
2041 .html('<?php VikBookingIcons::e('circle-notch', 'fa-spin fa-fw'); ?>')
2042 .on('click', function() {
2043 jQuery(this)
2044 .closest('.vbo-roverw-setnewrate-vcm-ota-relation-pricing')
2045 .find('.vbo-roverw-setnewrate-vcm-ota-channel-pricing')
2046 .toggle();
2047 });
2048
2049 let ota_block_pricing = jQuery('<div></div>');
2050 ota_block_pricing
2051 .addClass('vbo-roverw-setnewrate-vcm-ota-channel-pricing')
2052 .css('display', 'none')
2053 .append(jQuery('.vbo-roverw-setnewrate-vcm-ota-pricing-alteration').first().clone());
2054
2055 // register "input" event for select/input elements to control the channel alteration rule overrides
2056 ota_block_pricing.find('select, input').on('input', function() {
2057 let input_elem = jQuery(this);
2058
2059 // get the current channel alteration command
2060 let ota_alteration_command = input_elem
2061 .closest('.vbo-roverw-setnewrate-vcm-ota-relation-pricing')
2062 .find('.vbo-roverw-setnewrate-ota-pricing-currentvalue[data-alteration]')
2063 .attr('data-alteration');
2064
2065 // access alteration rule and input value
2066 let rmod_type = input_elem.attr('data-alter-rule');
2067 let rmod_value = input_elem.val();
2068
2069 if (!ota_alteration_command || !rmod_type || !(rmod_value + '').length) {
2070 return;
2071 }
2072
2073 // check what pricing factor was changed
2074 if (rmod_type == 'rmodsop') {
2075 // increase or decrease rate
2076 let command_old_val = ota_alteration_command.substr(0, 1);
2077 let command_new_val = parseInt(rmod_value) == 1 ? '+' : '-';
2078 ota_alteration_command = ota_alteration_command.replace(command_old_val, command_new_val);
2079 } else if (rmod_type == 'rmodsamount') {
2080 // amount
2081 let command_op = ota_alteration_command.substr(0, 1);
2082 let command_val = ota_alteration_command.substr(-1, 1);
2083 let command_old_val = ota_alteration_command.replace(command_op, '').replace(command_val, '');
2084 let command_new_val = parseFloat(rmod_value);
2085 ota_alteration_command = ota_alteration_command.replace(command_old_val, command_new_val);
2086 } else if (rmod_type == 'rmodsval') {
2087 // percent or absolute
2088 let command_old_val = ota_alteration_command.substr(-1, 1);
2089 let command_new_val = parseInt(rmod_value) == 1 ? '%' : '*';
2090 ota_alteration_command = ota_alteration_command.replace(command_old_val, command_new_val);
2091 }
2092
2093 // check if the channel requires a specific currency
2094 let ota_currency_data = input_elem
2095 .closest('.vbo-roverw-setnewrate-vcm-ota-relation-pricing')
2096 .find('.vbo-roverw-setnewrate-ota-pricing-willvalue')
2097 .attr('data-currency');
2098 if (ota_currency_data) {
2099 // decode currency data instructions
2100 try {
2101 ota_currency_data = JSON.parse(ota_currency_data);
2102 } catch (e) {
2103 ota_currency_data = {};
2104 }
2105 }
2106
2107 // define the current channel alteration string (readable)
2108 let ota_alteration_string = ota_alteration_command;
2109
2110 // finalize the current channel alteration string (readable)
2111 let ota_alteration_val = ota_alteration_string.substr(-1, 1);
2112 if (ota_alteration_val != '%') {
2113 ota_alteration_string = ota_alteration_string.replace(ota_alteration_val, '') + ((ota_currency_data && ota_currency_data?.symbol ? ota_currency_data.symbol : '') || <?php echo json_encode($currencysymb) ?: '"$"'; ?>);
2114 }
2115
2116 // update the alteration rule command attribute
2117 input_elem
2118 .closest('.vbo-roverw-setnewrate-vcm-ota-relation-pricing')
2119 .find('.vbo-roverw-setnewrate-ota-pricing-currentvalue[data-alteration]')
2120 .attr('data-alteration', ota_alteration_command);
2121
2122 // update the alteration rule string tag text
2123 input_elem
2124 .closest('.vbo-roverw-setnewrate-vcm-ota-relation-pricing')
2125 .find('.vbo-roverw-setnewrate-ota-pricing-currentvalue[data-alteration]')
2126 .html(ota_alteration_string);
2127
2128 // get the current rate to set
2129 let current_room_rate = jQuery('#roverw-newrate').val();
2130 let current_rate_type = document.querySelector('.vbo-roverw-newrwrap').getAttribute('data-rates-type');
2131 if (current_room_rate) {
2132 // dispatch the event to trigger the re-calculation of the OTA rates
2133 VBOCore.emitEvent('vbo-roverv-setnewrate-calc-ota-pricing', {
2134 rate: current_rate_type != 'addsub' ? current_room_rate : 0,
2135 });
2136 }
2137 });
2138
2139 // append elements to wrapper
2140 ota_block_channel.append(ota_pricing_value);
2141 ota_block_inner.append(ota_block_channel);
2142 ota_block_inner.append(ota_block_pricing);
2143 ota_block.append(ota_block_inner);
2144 wrapper.append(ota_block);
2145
2146 // increase OTA channel counter
2147 ota_ch_counter++;
2148 }
2149 });
2150
2151 // obtain the details for the first involved listing
2152 let firstListingId = listingsInvolved[0];
2153 let firstRplanId = vboActionRoomRplansMap[listingsInvolved[0]];
2154
2155 // trigger an AJAX request to load the current alteration rules, if any, for the first listing involved
2156 VBOCore.doAjax(
2157 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=pricing.loadOtaAlterationRules'); ?>",
2158 {
2159 room_id: firstListingId,
2160 rate_id: firstRplanId,
2161 },
2162 (res) => {
2163 var obj_res = typeof res === 'string' ? JSON.parse(res) : res;
2164 let alter_room_rates = obj_res['rmod'] == '1' || obj_res['rmod'] == 1;
2165
2166 // scan all room OTAs
2167 jQuery('.vbo-roverw-setnewrate-vcm-otas').find('.vbo-roverw-setnewrate-vcm-ota-relation').each(function(key, elem) {
2168 // get the current OTA identifier and whether pricing is altered
2169 let ota_wrap = jQuery(elem);
2170 let ota_id = ota_wrap.find('.vbo-roverw-setnewrate-vcm-ota-relation-channel').attr('data-otaid');
2171 let alter_ota_rates = alter_room_rates && obj_res['channels'] && (obj_res['channels'].includes(ota_id) || obj_res['channels'].includes(parseInt(ota_id)));
2172 if (!alter_ota_rates && alter_room_rates && obj_res.hasOwnProperty('rmod_channels') && obj_res['rmod_channels'].hasOwnProperty(ota_id)) {
2173 alter_ota_rates = true;
2174 }
2175
2176 // check if the current channel is using a different currency
2177 let ota_currency_data = {};
2178 if (obj_res.hasOwnProperty('cur_rplans') && obj_res['cur_rplans'].hasOwnProperty(ota_id)) {
2179 let ota_check_currency = obj_res['cur_rplans'][ota_id];
2180 if (obj_res.hasOwnProperty('currency_data_options') && obj_res['currency_data_options'].hasOwnProperty(ota_check_currency)) {
2181 // set custom currency data returned
2182 ota_currency_data = obj_res['currency_data_options'][ota_check_currency];
2183 }
2184 }
2185
2186 // build pricing alteration strings
2187 let alteration_command = '';
2188 let alteration_string = '';
2189
2190 // default alteration factors (no pricing alteration rules)
2191 let alter_op = '+';
2192 let alter_amount = '0';
2193 let alter_val = '%';
2194
2195 if (alter_ota_rates) {
2196 // check how rates are altered for this channel
2197 if (obj_res.hasOwnProperty('rmod_channels') && obj_res['rmod_channels'].hasOwnProperty(ota_id)) {
2198 // ota-level pricing alteration rule
2199 if (parseInt(obj_res['rmod_channels'][ota_id]['rmod']) == 1) {
2200 alter_op = parseInt(obj_res['rmod_channels'][ota_id]['rmodop']) == 1 ? '+' : '-';
2201 alter_amount = obj_res['rmod_channels'][ota_id]['rmodamount'];
2202 alter_val = parseInt(obj_res['rmod_channels'][ota_id]['rmodval']) == 1 ? '%' : '*';
2203 }
2204 } else {
2205 // room-level pricing alteration rule
2206 alter_op = parseInt(obj_res['rmodop']) == 1 ? '+' : '-';
2207 alter_amount = obj_res['rmodamount'] || '0';
2208 alter_val = parseInt(obj_res['rmodval']) == 1 ? '%' : '*';
2209 }
2210 }
2211
2212 // construct alteration strings
2213 alteration_command = alter_op + (alter_amount + '') + (alter_val + '');
2214 alteration_string = alter_op + (alter_amount + '') + (alter_val == '%' ? '%' : (ota_currency_data?.symbol || <?php echo json_encode($currencysymb) ?: '"$"'; ?>));
2215
2216 // stop room-ota loading and set alteration string
2217 let alteration_elem = jQuery('<span></span>');
2218 alteration_elem
2219 .addClass('vbo-roverw-setnewrate-ota-pricing-currentvalue')
2220 .attr('data-alteration', alteration_command)
2221 .html(alteration_string);
2222
2223 let will_alter_elem = jQuery('<span></span>').addClass('vbo-roverw-setnewrate-ota-pricing-willvalue');
2224
2225 if (ota_currency_data.symbol) {
2226 // set currency data object
2227 will_alter_elem.attr('data-currency', JSON.stringify(ota_currency_data));
2228 }
2229
2230 // set elements
2231 ota_wrap
2232 .find('.vbo-roverw-setnewrate-vcm-ota-pricing-startvalue')
2233 .html('')
2234 .append(will_alter_elem)
2235 .append(alteration_elem)
2236 .append('<?php VikBookingIcons::e('edit', 'edit-ota-pricing'); ?>');
2237
2238 // populate default values for input element overrides
2239 ota_wrap.find('select[data-alter-rule="rmodsop"]').val(alter_op == '+' ? 1 : 0);
2240 ota_wrap.find('input[data-alter-rule="rmodsamount"]').val(parseInt(alter_amount) > 0 ? alter_amount : '');
2241 ota_wrap.find('select[data-alter-rule="rmodsval"]').val(alter_val == '%' ? 1 : 0);
2242 });
2243
2244 // check the current rate value
2245 let current_room_rate = jQuery('#roverw-newrate').val();
2246 let current_rate_type = document.querySelector('.vbo-roverw-newrwrap').getAttribute('data-rates-type');
2247 if (current_room_rate && current_rate_type != 'addsub') {
2248 // dispatch the event to allow the actual calculation of the OTA rate
2249 VBOCore.emitEvent('vbo-roverv-setnewrate-calc-ota-pricing', {
2250 rate: current_room_rate,
2251 room_id: firstListingId,
2252 rate_id: firstRplanId,
2253 });
2254 }
2255 },
2256 (err) => {
2257 alert(err.responseText || 'Request Failed');
2258 }
2259 );
2260 };
2261
2262 /**
2263 * Restrictions can be updated only if VCM is available and toggled ON, because
2264 * the creation and transmission is made through the Connector Class of VCM.
2265 * On top of that, the OTA pricing alteration rule overrides will toggle a status class.
2266 */
2267 const vboActionVcmRestrictionsSupported = () => {
2268 if (!jQuery('input[name="roverw-newrate-vcm"]').prop('disabled') && jQuery('input[name="roverw-newrate-vcm"]').prop('checked')) {
2269 jQuery('#roverw-newrestr').val('');
2270 jQuery('.vbo-roverw-newrestr-wrap').show();
2271 jQuery('.vbo-roverw-setnewrate-vcm-ota-relation').removeClass('vbo-roverw-setnewrate-vcm-ota-relation-disabled');
2272 } else {
2273 jQuery('.vbo-roverw-newrestr-wrap').hide();
2274 jQuery('.vbo-roverw-setnewrate-vcm-ota-relation').addClass('vbo-roverw-setnewrate-vcm-ota-relation-disabled');
2275 }
2276 };
2277
2278 /**
2279 * Register function for loading the tasks of a given area/project id.
2280 *
2281 * @param number areaId The area/project ID to render.
2282 * @param string from_date The Y-m-d starting date.
2283 * @param string to_date The Y-m-d ending date.
2284 * @param Array room_ids List of listing IDs to include.
2285 * @param function onComplete Optional callback to invoke upon completion.
2286 */
2287 const vboActionLoadAreaTasks = (areaId, from_date, to_date, room_ids, onComplete) => {
2288 let ctx_elem = document
2289 .querySelector('.vbo-context-menu-overview-actions');
2290
2291 let lbl_elem = ctx_elem
2292 .querySelector('.vbo-context-menu-lbl');
2293
2294 let orig_lbl = lbl_elem.innerText;
2295
2296 // start loading animation
2297 ctx_elem.loading = 1;
2298 lbl_elem.innerHTML = '<?php VikBookingIcons::e('circle-notch', 'fa-spin fa-fw'); ?>';
2299
2300 // make the request
2301 VBOCore.doAjax(
2302 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=taskmanager.loadAreaListingTasks'); ?>",
2303 {
2304 area_id: areaId,
2305 room_ids: room_ids,
2306 from_date: from_date,
2307 to_date: to_date,
2308 },
2309 (resp) => {
2310 try {
2311 // decode the response (if needed), and append the content to the modal body
2312 resp = typeof resp === 'string' ? JSON.parse(resp) : resp;
2313
2314 // hide any previously loaded area task, if any
2315 vboActionHideAreaTasks(areaId);
2316
2317 // populate tasks for all listings by scanning all month tables
2318 document
2319 .querySelectorAll('table.vboverviewtable[data-month-from]')
2320 // iterate months
2321 .forEach((month) => {
2322 // detect if we are using the "old" table layout
2323 let isTableLayout = month.classList.contains('vbo-overv-sticky-table-head-on');
2324
2325 month
2326 .querySelectorAll('.roomname[data-roomid]')
2327 // iterate month listings
2328 .forEach((roomRowCell) => {
2329 if (roomRowCell.matches('.subroomname')) {
2330 // skip sub-unit rows
2331 return;
2332 }
2333
2334 // get current listing ID
2335 let listingId = roomRowCell.getAttribute('data-roomid');
2336
2337 if (!resp.listings.hasOwnProperty(listingId) && !resp.listingIds.includes(listingId) && !resp.listingIds.includes(parseInt(listingId))) {
2338 if (resp.listingIds.length) {
2339 // no tasks returned for this uneligible room id
2340 return;
2341 }
2342 }
2343
2344 // access the "parent row"
2345 let parentRow = roomRowCell.closest('tr');
2346
2347 // build area-tasks row for the current listing
2348 let areaTasksRow = document.createElement('tr');
2349 areaTasksRow.classList.add('vbo-tm-row');
2350 areaTasksRow.setAttribute('data-roomid', listingId);
2351 areaTasksRow.setAttribute('data-area-id', areaId);
2352
2353 // build first row-cell and append it to the row
2354 let areaTasksCell = document.createElement('td');
2355 areaTasksCell.classList.add('vbo-tm-row-cell-first');
2356 areaTasksCell.innerHTML = resp.area?.icon_class ? '<i class="' + resp.area.icon_class + '"></i> ' : '';
2357 let areaNameEl = document.createElement('span');
2358 areaNameEl.classList.add('vbo-tm-row-area-name');
2359 areaNameEl.innerText = resp.area.name;
2360 areaTasksCell.append(areaNameEl);
2361 areaTasksRow.append(areaTasksCell);
2362
2363 // iterate over the "parent row" day cells of the current listing and month
2364 parentRow
2365 .querySelectorAll('td.vbo-grid-avcell[data-day]')
2366 // iterate month listing days
2367 .forEach((roomRowDay) => {
2368 // get the day Y-m-d value
2369 let ymd = roomRowDay.getAttribute('data-day');
2370
2371 // build month-day cell with pricing details
2372 let tasksDayCell = document.createElement('td');
2373 tasksDayCell.classList.add('vbo-tm-row-cell-day');
2374 tasksDayCell.setAttribute('data-day', ymd);
2375
2376 // access the tasks information for this day
2377 if (resp.listings.hasOwnProperty(listingId) && resp.listings[listingId].hasOwnProperty(ymd)) {
2378 // iterate all tasks for the current listing and day
2379 let totTasks = resp.listings[listingId][ymd].length;
2380 resp.listings[listingId][ymd].forEach((task, index) => {
2381 if (index > 2) {
2382 // no more tasks to display for the current listing and day
2383 return;
2384 }
2385
2386 // build task element
2387 let taskEl = document.createElement('div');
2388 taskEl.classList.add('vbo-tm-row-cell-task');
2389 taskEl.setAttribute('data-task-id', task.id);
2390 taskEl.setAttribute('data-area-id', task.area_id);
2391 taskEl.setAttribute('data-task-bid', (task.bid + ''));
2392 if (task.color) {
2393 taskEl.classList.add('vbo-tm-color');
2394 taskEl.classList.add(task.color);
2395 }
2396 if (isTableLayout) {
2397 taskEl.classList.add('vbo-tm-row-cell-task-notitle');
2398 taskEl.innerHTML = '&nbsp;';
2399 } else {
2400 taskEl.innerText = task.title;
2401 }
2402
2403 // append task element to current cell
2404 tasksDayCell.append(taskEl);
2405
2406 if ((index + 1) > 2 && totTasks > (2 + 1)) {
2407 // limit the number of tasks per listing per day by displaying a link to the TM view for this day
2408 let exceeding = totTasks - (index + 1);
2409 let moreTasksTxt = '+' + exceeding + ' ' + (exceeding > 1 ? <?php echo json_encode(JText::translate('VBO_TASKS')); ?> : <?php echo json_encode(JText::translate('VBO_TASK')); ?>).toLowerCase();
2410
2411 // build element
2412 let moreTasksEl = document.createElement('div');
2413 moreTasksEl.classList.add('vbo-tm-calendar-month-day-more')
2414 moreTasksEl.setAttribute('data-day', ymd);
2415 moreTasksEl.innerText = moreTasksTxt;
2416 moreTasksEl.addEventListener('click', () => {
2417 window.location.href = '<?php echo VBOFactory::getPlatform()->getUri()->admin('index.php?option=com_vikbooking&view=taskmanager&mode=calendar&filters[calendar_type]=day&filters[calendar_day]=%s', false); ?>'.replace('%s', ymd);
2418 });
2419
2420 // append element to current cell
2421 tasksDayCell.append(moreTasksEl);
2422
2423 return;
2424 }
2425 });
2426 }
2427
2428 // append tasks cell to main area-tasks row
2429 areaTasksRow.append(tasksDayCell);
2430 });
2431
2432 // append the main area-tasks row to the DOM
2433 parentRow.insertAdjacentElement('afterend', areaTasksRow);
2434 });
2435 });
2436
2437 // iterating completed, stop loading
2438 lbl_elem.innerHTML = '';
2439 lbl_elem.innerText = orig_lbl;
2440 ctx_elem.loading = 0;
2441
2442 // register events for the area tasks
2443 vboActionRegisterAreaTaskEvents(areaId);
2444
2445 if (typeof onComplete === 'function') {
2446 // invoke the callback upon completion
2447 onComplete(areaId);
2448 }
2449 } catch (err) {
2450 console.error('Error decoding the response', err, resp);
2451 }
2452 },
2453 (error) => {
2454 // display error message
2455 alert(error.responseText);
2456
2457 // stop loading
2458 lbl_elem.innerHTML = '';
2459 lbl_elem.innerText = orig_lbl;
2460 ctx_elem.loading = 0;
2461 }
2462 );
2463 };
2464
2465 /**
2466 * Register function to process a list of area/project IDs for which the tasks should be loaded.
2467 *
2468 * @param Array areaIds The list of area/project IDs to process.
2469 * @param string from_date The Y-m-d starting date.
2470 * @param string to_date The Y-m-d ending date.
2471 * @param Array room_ids List of listing IDs to include.
2472 * @param function onProgress Function to invoke upon progressing to a next request.
2473 */
2474 const vboActionDispatchAreasLoading = (areaIds, from_date, to_date, room_ids, onProgress) => {
2475 if (!Array.isArray(areaIds) || !areaIds.length) {
2476 // abort when the queue is exhausted
2477 return;
2478 }
2479
2480 // obtain the area ID to process
2481 const areaId = areaIds.shift();
2482
2483 // load the tasks for the current area/project ID
2484 vboActionLoadAreaTasks(areaId, from_date, to_date, room_ids, (areaProcessed) => {
2485 if (typeof onProgress === 'function') {
2486 // request completed, invoke the callback upon progressing to the next request to process
2487 onProgress(areaProcessed);
2488 }
2489
2490 // perform a recursive call for the next area/project ID to process, if any
2491 vboActionDispatchAreasLoading(areaIds, from_date, to_date, room_ids, onProgress);
2492 });
2493 };
2494
2495 /**
2496 * Register function to hide the tasks of a given area/project id.
2497 */
2498 const vboActionHideAreaTasks = (areaId) => {
2499 document
2500 .querySelectorAll('.vbo-tm-row[data-area-id="' + areaId + '"]')
2501 .forEach((row) => {
2502 row.remove();
2503 });
2504 };
2505
2506 /**
2507 * Register function to add event listeners for the tasks of a given area (edit task, new task, hover task).
2508 */
2509 const vboActionRegisterAreaTaskEvents = (areaId) => {
2510 // edit and hover existing tasks
2511 document
2512 .querySelectorAll('.vbo-tm-row-cell-task')
2513 .forEach((taskElement) => {
2514 if (taskElement.clickListener) {
2515 // listener added already
2516 return;
2517 }
2518
2519 // get the clicked task, area and booking IDs
2520 const taskId = taskElement.getAttribute('data-task-id');
2521 const areaId = taskElement.getAttribute('data-area-id');
2522 const taskBid = taskElement.getAttribute('data-task-bid');
2523
2524 // define the click event for editing the task
2525 taskElement.addEventListener('click', () => {
2526 // define the modal delete button
2527 let delete_btn = jQuery('<button></button>')
2528 .attr('type', 'button')
2529 .addClass('btn btn-danger')
2530 .text(<?php echo json_encode(JText::translate('VBELIMINA')); ?>)
2531 .on('click', function() {
2532 if (!confirm(<?php echo json_encode(JText::translate('VBDELCONFIRM')); ?>)) {
2533 return false;
2534 }
2535
2536 // disable button to prevent double submissions
2537 let submit_btn = jQuery(this);
2538 submit_btn.prop('disabled', true);
2539
2540 // start loading animation
2541 VBOCore.emitEvent('vbo-tm-edittask-loading');
2542
2543 // make the request
2544 VBOCore.doAjax(
2545 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=taskmanager.deleteTask'); ?>",
2546 {
2547 data: {
2548 id: taskId,
2549 },
2550 },
2551 (resp) => {
2552 // parse all context menu buttons to identify the current area/project id
2553 jQuery('.vbo-context-menu-overview-actions').vboContextMenu('buttons').forEach((btn) => {
2554 if (btn.areaId == areaId) {
2555 btn.activeState = false;
2556 // trigger action to reload the area/project tasks
2557 btn.action();
2558 }
2559 });
2560
2561 // dismiss the modal
2562 VBOCore.emitEvent('vbo-tm-edittask-dismiss');
2563 },
2564 (error) => {
2565 // display error message
2566 alert(error.responseText);
2567
2568 // re-enable submit button
2569 submit_btn.prop('disabled', false);
2570
2571 // stop loading
2572 VBOCore.emitEvent('vbo-tm-edittask-loading');
2573 }
2574 );
2575 });
2576
2577 // define the modal save button
2578 let save_btn = jQuery('<button></button>')
2579 .attr('type', 'button')
2580 .addClass('btn btn-success')
2581 .text(<?php echo json_encode(JText::translate('VBSAVE')); ?>)
2582 .on('click', function() {
2583 // disable button to prevent double submissions
2584 let submit_btn = jQuery(this);
2585 submit_btn.prop('disabled', true);
2586
2587 // start loading animation
2588 VBOCore.emitEvent('vbo-tm-edittask-loading');
2589
2590 // get form data
2591 const taskForm = new FormData(document.querySelector('#vbo-tm-task-manage-form'));
2592
2593 // build query parameters for the request
2594 let qpRequest = new URLSearchParams(taskForm);
2595
2596 // make sure the request always includes the assignees query parameter, even if the list is empty
2597 if (!qpRequest.has('data[assignees][]')) {
2598 qpRequest.append('data[assignees][]', []);
2599 }
2600
2601 // make sure the request always includes the tags query parameter, even if the list is empty
2602 if (!qpRequest.has('data[tags][]')) {
2603 qpRequest.append('data[tags][]', []);
2604 }
2605
2606 // make the request
2607 VBOCore.doAjax(
2608 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=taskmanager.updateTask'); ?>",
2609 qpRequest.toString(),
2610 (resp) => {
2611 // parse all context menu buttons to identify the current area/project id
2612 jQuery('.vbo-context-menu-overview-actions').vboContextMenu('buttons').forEach((btn) => {
2613 if (btn.areaId == areaId) {
2614 btn.activeState = false;
2615 // trigger action to reload the area/project tasks
2616 btn.action();
2617 }
2618 });
2619
2620 // dismiss the modal
2621 VBOCore.emitEvent('vbo-tm-edittask-dismiss');
2622 },
2623 (error) => {
2624 // display error message
2625 alert(error.responseText);
2626
2627 // re-enable submit button
2628 submit_btn.prop('disabled', false);
2629
2630 // stop loading
2631 VBOCore.emitEvent('vbo-tm-edittask-loading');
2632 }
2633 );
2634 });
2635
2636 // display modal
2637 let modalBody = VBOCore.displayModal({
2638 suffix: 'tm_edittask_modal',
2639 title: <?php echo json_encode(JText::translate('VBO_TASK')); ?> + ' #' + taskId,
2640 extra_class: 'vbo-modal-rounded vbo-modal-taller vbo-modal-large',
2641 body_prepend: true,
2642 lock_scroll: true,
2643 escape_dismiss: false,
2644 footer_left: delete_btn,
2645 footer_right: save_btn,
2646 loading_event: 'vbo-tm-edittask-loading',
2647 dismiss_event: 'vbo-tm-edittask-dismiss',
2648 });
2649
2650 // start loading animation
2651 VBOCore.emitEvent('vbo-tm-edittask-loading');
2652
2653 // make the request
2654 VBOCore.doAjax(
2655 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=taskmanager.renderLayout'); ?>",
2656 {
2657 type: 'tasks.managetask',
2658 data: {
2659 task_id: taskId,
2660 area_id: areaId,
2661 form_id: 'vbo-tm-task-manage-form',
2662 },
2663 },
2664 (resp) => {
2665 // stop loading
2666 VBOCore.emitEvent('vbo-tm-edittask-loading');
2667
2668 try {
2669 // decode the response (if needed), and append the content to the modal body
2670 let obj_res = typeof resp === 'string' ? JSON.parse(resp) : resp;
2671 modalBody.append(obj_res['html']);
2672 } catch (err) {
2673 console.error('Error decoding the response', err, resp);
2674 }
2675 },
2676 (error) => {
2677 // display error message
2678 alert(error.responseText);
2679
2680 // stop loading
2681 VBOCore.emitEvent('vbo-tm-edittask-loading');
2682 }
2683 );
2684 });
2685
2686 // define the mouseover/mouseout events for the current task
2687 if (taskBid) {
2688 // highlight the booking(s) assigned to the current task when hovered
2689 taskElement.addEventListener('mouseover', () => {
2690 // highlight all cells for this booking
2691 let bidSnakeElements = document.querySelectorAll('td.vbo-grid-avcell[data-bids*="-' + taskBid + '-"] > .vbo-tableaux-booking');
2692 bidSnakeElements.forEach((snake, index) => {
2693 if (snake.matches('.vbo-tableaux-booking-checkout')) {
2694 // this must be a check-out snake on the check-in date of the desired booking snake
2695 return;
2696 }
2697 // add the highlight class
2698 snake.classList.add('vbo-tableaux-booking-task-highlight');
2699 if (++index == bidSnakeElements.length && !snake.matches('.vbo-tableaux-booking-checkout')) {
2700 // look for the check-out snake for the same booking
2701 let parentCell = snake.closest('td').nextElementSibling;
2702 if (parentCell && parentCell.querySelector('.vbo-tableaux-booking-checkout')) {
2703 // add the highlight class also to the check-out snake
2704 parentCell
2705 .querySelector('.vbo-tableaux-booking-checkout')
2706 .classList
2707 .add('vbo-tableaux-booking-task-highlight');
2708 }
2709 }
2710 });
2711 });
2712
2713 // un-highlight the booking(s) assigned to the current task when mouse goes out
2714 taskElement.addEventListener('mouseout', () => {
2715 document
2716 .querySelectorAll('.vbo-tableaux-booking-task-highlight')
2717 .forEach((el) => {
2718 el.classList.remove('vbo-tableaux-booking-task-highlight');
2719 });
2720 });
2721 }
2722
2723 // turn flag on for listener set
2724 taskElement.clickListener = true;
2725 });
2726
2727 // create new tasks
2728 document
2729 .querySelectorAll('.vbo-tm-row[data-area-id="' + areaId + '"] .vbo-tm-row-cell-day')
2730 .forEach((tmCell) => {
2731 const listingId = tmCell.closest('tr').getAttribute('data-roomid');
2732 const day = tmCell.getAttribute('data-day');
2733
2734 tmCell.addEventListener('click', (e) => {
2735 if (e.target && (e.target.matches('.vbo-tm-row-cell-task') || e.target.parentNode.matches('.vbo-tm-row-cell-task') || e.target.matches('.vbo-tm-calendar-month-day-more'))) {
2736 // an existing task for this cell was clicked (or the "see more" element), so we abort the process
2737 return;
2738 }
2739
2740 // define the modal cancel button
2741 let cancel_btn = jQuery('<button></button>')
2742 .attr('type', 'button')
2743 .addClass('btn')
2744 .text(<?php echo json_encode(JText::translate('VBANNULLA')); ?>)
2745 .on('click', () => {
2746 VBOCore.emitEvent('vbo-tm-newtask-dismiss');
2747 });
2748
2749 // define the modal save button
2750 let save_btn = jQuery('<button></button>')
2751 .attr('type', 'button')
2752 .addClass('btn btn-success')
2753 .text(<?php echo json_encode(JText::translate('VBSAVE')); ?>)
2754 .on('click', function() {
2755 // disable button to prevent double submissions
2756 let submit_btn = jQuery(this);
2757 submit_btn.prop('disabled', true);
2758
2759 // start loading animation
2760 VBOCore.emitEvent('vbo-tm-newtask-loading');
2761
2762 // get form data
2763 const taskForm = new FormData(document.querySelector('#vbo-tm-task-manage-form'));
2764
2765 // build query parameters for the request
2766 let qpRequest = new URLSearchParams(taskForm).toString();
2767
2768 // make the request
2769 VBOCore.doAjax(
2770 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=taskmanager.createTask'); ?>",
2771 qpRequest,
2772 (resp) => {
2773 // parse all context menu buttons to identify the current area/project id
2774 jQuery('.vbo-context-menu-overview-actions').vboContextMenu('buttons').forEach((btn) => {
2775 if (btn.areaId == areaId) {
2776 btn.activeState = false;
2777 // trigger action to reload the area/project tasks
2778 btn.action();
2779 }
2780 });
2781
2782 // dismiss the modal
2783 VBOCore.emitEvent('vbo-tm-newtask-dismiss');
2784 },
2785 (error) => {
2786 // display error message
2787 alert(error.responseText);
2788
2789 // re-enable submit button
2790 submit_btn.prop('disabled', false);
2791
2792 // stop loading
2793 VBOCore.emitEvent('vbo-tm-newtask-loading');
2794 }
2795 );
2796 });
2797
2798 // display modal
2799 let modalBody = VBOCore.displayModal({
2800 suffix: 'tm_newtask_modal',
2801 title: <?php echo json_encode(JText::translate('VBO_NEW_TASK')); ?>,
2802 extra_class: 'vbo-modal-rounded vbo-modal-taller vbo-modal-large',
2803 body_prepend: true,
2804 lock_scroll: true,
2805 escape_dismiss: false,
2806 footer_left: cancel_btn,
2807 footer_right: save_btn,
2808 loading_event: 'vbo-tm-newtask-loading',
2809 dismiss_event: 'vbo-tm-newtask-dismiss',
2810 });
2811
2812 // start loading animation
2813 VBOCore.emitEvent('vbo-tm-newtask-loading');
2814
2815 // make the request
2816 VBOCore.doAjax(
2817 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=taskmanager.renderLayout'); ?>",
2818 {
2819 type: 'tasks.managetask',
2820 data: {
2821 area_id: areaId,
2822 form_id: 'vbo-tm-task-manage-form',
2823 filters: {
2824 calendar_day: day,
2825 id_room: listingId,
2826 },
2827 },
2828 },
2829 (resp) => {
2830 // stop loading
2831 VBOCore.emitEvent('vbo-tm-newtask-loading');
2832
2833 try {
2834 // decode the response (if needed), and append the content to the modal body
2835 let obj_res = typeof resp === 'string' ? JSON.parse(resp) : resp;
2836 modalBody.append(obj_res['html']);
2837 } catch (err) {
2838 console.error('Error decoding the response', err, resp);
2839 }
2840 },
2841 (error) => {
2842 // display error message
2843 alert(error.responseText);
2844
2845 // stop loading
2846 VBOCore.emitEvent('vbo-tm-newtask-loading');
2847 }
2848 );
2849 });
2850 });
2851 };
2852
2853 jQuery(function() {
2854
2855 // all task areas
2856 const taskAreas = <?php echo json_encode($taskAreas); ?>;
2857
2858 // configure the currency object
2859 VBOCore.getCurrency({
2860 symbol: <?php echo json_encode($currencysymb) ?: '"$"'; ?>,
2861 position: <?php echo json_encode(VikBooking::getCurrencyPosition()); ?>,
2862 digits: <?php echo intval($currency_digits); ?>,
2863 decimals: <?php echo json_encode($currency_decimals) ?: '"."'; ?>,
2864 thousands: <?php echo json_encode($currency_thousands) ?: '","'; ?>,
2865 noDecimals: 1,
2866 });
2867
2868 // build context menu button text element
2869 let btnRoomRatesEl = document.createElement('span');
2870 btnRoomRatesEl.classList.add('vbo-ctxmenu-entry-icn');
2871 btnRoomRatesEl.innerHTML = '<span>' + <?php echo json_encode(JText::translate('VBO_RATES_AND_RESTR')); ?> + '</span>';
2872 let icnRoomRatesEl = document.createElement('i');
2873 icnRoomRatesEl.classList.add('vbo-save-pref-ota-rates');
2874 icnRoomRatesEl.classList.add(...(<?php echo json_encode(VikBookingIcons::i('thumbtack', 'vbo-deactivated-icon')); ?>.split(' ')));
2875 if (<?php echo $cookie->get('vboAovwLrr', 0, 'int') ? 'true' : 'false'; ?>) {
2876 icnRoomRatesEl.classList.add('vbo-activated-icon');
2877 }
2878 let icnRoomRatesWrapEl = document.createElement('span');
2879 icnRoomRatesWrapEl.classList.add('vbo-ctxmenu-entry-icn-secondary', 'vbo-tooltip', 'vbo-tooltip-top-left');
2880 icnRoomRatesWrapEl.setAttribute('data-tooltiptext', <?php echo json_encode(JText::translate('VBO_REMEMBER_PREF')); ?>);
2881 icnRoomRatesWrapEl.append(icnRoomRatesEl);
2882 btnRoomRatesEl.append(icnRoomRatesWrapEl);
2883
2884 // build buttons
2885 let btns = [
2886 {
2887 class: 'btngroup',
2888 text: <?php echo json_encode(JText::translate('VBMENUFARES')); ?>,
2889 disabled: true,
2890 },
2891 {
2892 activeState: <?php echo $cookie->get('vboAovwLrr', 0, 'int') ? 'true' : 'false'; ?>,
2893 pinnedState: <?php echo $cookie->get('vboAovwLrr', 0, 'int') ? 'true' : 'false'; ?>,
2894 separator: (taskAreas.length > 0),
2895 class: 'vbo-context-menu-entry-secondary',
2896 text: btnRoomRatesEl,
2897 icon: function() {
2898 return this.activeState ? '<?php echo VikBookingIcons::i('toggle-on', 'vbo-enabled-icon'); ?>' : '<?php echo VikBookingIcons::i('toggle-off'); ?>';
2899 },
2900 action: function(root, event) {
2901 if (event?.target && (event.target[0] || event.target).matches('.vbo-save-pref-ota-rates')) {
2902 // toggle pinned state
2903 this.pinnedState = !this.pinnedState;
2904
2905 // update cookie preference
2906 let nd = new Date();
2907 nd.setTime(nd.getTime() + (365*24*60*60*1000));
2908 document.cookie = "vboAovwLrr=" + (this.pinnedState ? 1 : 0) + "; expires=" + nd.toUTCString() + "; path=/; SameSite=Lax";
2909
2910 // update icon class
2911 icnRoomRatesEl.classList.toggle('vbo-deactivated-icon', !this.pinnedState);
2912 icnRoomRatesEl.classList.toggle('vbo-activated-icon', this.pinnedState);
2913
2914 // do not proceed
2915 return;
2916 }
2917
2918 // get all month tables
2919 let monthTables = document.querySelectorAll('table.vboverviewtable[data-month-from]');
2920 if (!monthTables.length) {
2921 return;
2922 }
2923
2924 // get the date bounds
2925 let from_date = monthTables[0].getAttribute('data-month-from');
2926 let to_date = monthTables[(monthTables.length - 1)].getAttribute('data-month-to');
2927
2928 // get all listing IDs from the first table
2929 let listingIds = [];
2930 monthTables[0]
2931 .querySelectorAll('.roomname[data-roomid]')
2932 .forEach((roomRowCell) => {
2933 if (roomRowCell.matches('.subroomname')) {
2934 // skip sub-unit rows
2935 return;
2936 }
2937 listingIds.push(roomRowCell.getAttribute('data-roomid'));
2938 });
2939
2940 // toggle state
2941 this.activeState = !this.activeState;
2942
2943 if (this.activeState) {
2944 // load room rates
2945 vboActionLoadRoomRates(from_date, to_date, listingIds);
2946 } else {
2947 // hide room rates
2948 vboActionHideRoomRates(true);
2949 }
2950 },
2951 disabled: () => {
2952 let ctx_elem = document
2953 .querySelector('.vbo-context-menu-overview-actions');
2954
2955 return <?php echo !$vbo_auth_pricing ? 'true' : 'false'; ?> || ctx_elem?.loading == 1;
2956 },
2957 },
2958 ];
2959
2960 if (taskAreas.length) {
2961 // push group button
2962 btns.push({
2963 class: 'btngroup',
2964 text: <?php echo json_encode(JText::translate('VBO_TASK_MANAGER')); ?>,
2965 disabled: true,
2966 });
2967
2968 taskAreas.forEach((area, index) => {
2969 // push area/project button
2970 btns.push({
2971 activeState: false,
2972 areaId: area.id,
2973 class: 'vbo-context-menu-entry-secondary',
2974 text: area.name,
2975 separator: false,
2976 icon: function() {
2977 return this.activeState === true ? '<?php echo VikBookingIcons::i('check-square'); ?>' : '<?php echo VikBookingIcons::i('far fa-square'); ?>';
2978 },
2979 action: function(root, event) {
2980 // get all month tables
2981 let monthTables = document.querySelectorAll('table.vboverviewtable[data-month-from]');
2982 if (!monthTables.length) {
2983 return;
2984 }
2985
2986 // get the date bounds
2987 let from_date = monthTables[0].getAttribute('data-month-from');
2988 let to_date = monthTables[(monthTables.length - 1)].getAttribute('data-month-to');
2989
2990 // get all listing IDs from the first table
2991 let listingIds = [];
2992 monthTables[0]
2993 .querySelectorAll('.roomname[data-roomid]')
2994 .forEach((roomRowCell) => {
2995 if (roomRowCell.matches('.subroomname')) {
2996 // skip sub-unit rows
2997 return;
2998 }
2999 listingIds.push(roomRowCell.getAttribute('data-roomid'));
3000 });
3001
3002 // toggle active state
3003 this.activeState = !this.activeState;
3004
3005 if (this.activeState) {
3006 // render tasks for the selected area
3007 vboActionLoadAreaTasks(this.areaId, from_date, to_date, listingIds);
3008 } else {
3009 // hide tasks for the selected area
3010 vboActionHideAreaTasks(this.areaId);
3011 }
3012 },
3013 disabled: () => {
3014 let ctx_elem = document
3015 .querySelector('.vbo-context-menu-overview-actions');
3016
3017 return <?php echo !$vbo_auth_pms ? 'true' : 'false'; ?> || ctx_elem?.loading == 1;
3018 },
3019 });
3020 });
3021 }
3022
3023 // start context menu on the proper actions button element
3024 jQuery('.vbo-context-menu-overview-actions').vboContextMenu({
3025 placement: 'bottom-left',
3026 buttons: btns,
3027 });
3028
3029 // check default state for actions context menu
3030 if (<?php echo $cookie->get('vboAovwLrr', 0, 'int') ? 'true' : 'false'; ?>) {
3031 // load room rates upon page loading according to cookie preferences
3032 setTimeout(() => {
3033 // access "load room rates" button-option
3034 let ctxRoomRatesBtn = jQuery('.vbo-context-menu-overview-actions').vboContextMenu('buttons')[1];
3035 // let the initial state be disabled
3036 ctxRoomRatesBtn.activeState = false;
3037 // invoke action callback to toggle the active state to true
3038 ctxRoomRatesBtn.action();
3039 }, 200);
3040 }
3041
3042 // start context menu on the button element to choose the new rates type
3043 const fixedRateLbl = <?php echo json_encode(JText::translate('VBO_SET_FIXED_RATE')); ?>;
3044 const adjustRatesLbl = <?php echo json_encode(JText::translate('VBO_INCR_DECR_RATES')); ?>;
3045 jQuery('.vbo-context-menu-rate-type').vboContextMenu({
3046 placement: 'bottom-center',
3047 buttons: [
3048 {
3049 activeState: true,
3050 rateType: 'fixed',
3051 class: 'vbo-context-menu-entry-secondary',
3052 text: fixedRateLbl,
3053 separator: false,
3054 icon: function() {
3055 return this.activeState === true ? '<?php echo VikBookingIcons::i('check-square'); ?>' : '<?php echo VikBookingIcons::i('far fa-square'); ?>';
3056 },
3057 action: function(root, event) {
3058 // make the active state property enabled
3059 this.activeState = true;
3060
3061 // update data attribute for the preference chosen
3062 document.querySelector('.vbo-roverw-newrwrap').setAttribute('data-rate-type', 'fixed');
3063
3064 // update element action title
3065 document.querySelector('.vbo-roverw-action-rates-title').textContent = fixedRateLbl;
3066
3067 // toggle visible element for defining the new rate
3068 document.querySelector('.vbo-roverw-newrcont[data-rate-type="fixed"]').style.display = '';
3069 document.querySelector('.vbo-roverw-newrcont[data-rate-type="addsub"]').style.display = 'none';
3070
3071 // parse all context menu buttons to set the proper active state
3072 jQuery('.vbo-context-menu-rate-type').vboContextMenu('buttons').forEach((btn) => {
3073 if (btn.rateType != 'fixed') {
3074 btn.activeState = false;
3075 }
3076 });
3077 },
3078 },
3079 {
3080 activeState: false,
3081 rateType: 'addsub',
3082 class: 'vbo-context-menu-entry-secondary',
3083 text: adjustRatesLbl,
3084 separator: false,
3085 icon: function() {
3086 return this.activeState === true ? '<?php echo VikBookingIcons::i('check-square'); ?>' : '<?php echo VikBookingIcons::i('far fa-square'); ?>';
3087 },
3088 action: function(root, event) {
3089 // make the active state property enabled
3090 this.activeState = true;
3091
3092 // update data attribute for the preference chosen
3093 document.querySelector('.vbo-roverw-newrwrap').setAttribute('data-rate-type', 'addsub');
3094
3095 // update element action title
3096 document.querySelector('.vbo-roverw-action-rates-title').textContent = adjustRatesLbl;
3097
3098 // toggle visible element for defining the new rate
3099 document.querySelector('.vbo-roverw-newrcont[data-rate-type="addsub"]').style.display = '';
3100 document.querySelector('.vbo-roverw-newrcont[data-rate-type="fixed"]').style.display = 'none';
3101
3102 // parse all context menu buttons to set the proper active state
3103 jQuery('.vbo-context-menu-rate-type').vboContextMenu('buttons').forEach((btn) => {
3104 if (btn.rateType != 'addsub') {
3105 btn.activeState = false;
3106 }
3107 });
3108 },
3109 }
3110 ],
3111 });
3112
3113 // register listener to reset the room-rate selection upon Esc keyup event
3114 window.addEventListener('keyup', (e) => {
3115 if (!e.key || e.key != 'Escape') {
3116 return;
3117 }
3118
3119 if (vboActionRoomRateData.start) {
3120 // reset selection
3121 vboActionRoomRateHandleReset();
3122 }
3123 });
3124
3125 // register listener for the "input" event on the "set new rate" input field of type number
3126 document.querySelector('#roverw-newrate').addEventListener('input', VBOCore.debounceEvent((e) => {
3127 // dispatch the event to calculate the new OTA pricing value
3128 VBOCore.emitEvent('vbo-roverv-setnewrate-calc-ota-pricing', {rate: e.target.value});
3129 }, 200));
3130
3131 // register listener for when a new rate is set to update what will be the OTA pricing value
3132 document.addEventListener('vbo-roverv-setnewrate-calc-ota-pricing', VBOCore.debounceEvent((e) => {
3133 if (!e || !e.detail || !e.detail.rate) {
3134 // invalid event data
3135 return;
3136 }
3137
3138 // get the new PMS rate
3139 let rate_amount = parseFloat(e.detail.rate);
3140
3141 // access the currency object
3142 let currencyObj = VBOCore.getCurrency();
3143
3144 // scan all OTA alteration rules, if any
3145 document.querySelectorAll('.vbo-roverw-setnewrate-ota-pricing-currentvalue[data-alteration]').forEach((elem) => {
3146 // channel alteration string
3147 let alter_string = elem.getAttribute('data-alteration');
3148 if (!alter_string) {
3149 alter_string = '+0%';
3150 }
3151
3152 // default alteration factors (no pricing alteration rules)
3153 let alter_op = alter_string.substr(0, 1);
3154 let alter_val = alter_string.substr(-1, 1);
3155 let alter_amount = parseFloat(alter_string.replace(alter_op, '').replace(alter_val, ''));
3156
3157 // calculate what the rate will be on the OTA
3158 let ota_rate_amount = rate_amount;
3159
3160 if (!isNaN(alter_amount) && Math.abs(alter_amount) > 0) {
3161 if (alter_op == '+') {
3162 // increase rate
3163 if (alter_val == '%') {
3164 // percent
3165 let amount_inc = currencyObj.multiply(alter_amount, 0.01);
3166 amount_inc = currencyObj.multiply(rate_amount, amount_inc);
3167 ota_rate_amount = currencyObj.sum(rate_amount, amount_inc);
3168 } else {
3169 // absolute
3170 ota_rate_amount = currencyObj.sum(rate_amount, alter_amount);
3171 }
3172 } else {
3173 // discount rate
3174 if (alter_val == '%') {
3175 // percent
3176 let amount_inc = currencyObj.multiply(alter_amount, 0.01);
3177 amount_inc = currencyObj.multiply(rate_amount, amount_inc);
3178 ota_rate_amount = currencyObj.diff(rate_amount, amount_inc);
3179 } else {
3180 // absolute
3181 ota_rate_amount = currencyObj.diff(rate_amount, alter_amount);
3182 }
3183 }
3184 }
3185
3186 // get the element containing the calculated ota pricing
3187 let will_alter_elem = elem
3188 .closest('.vbo-roverw-setnewrate-vcm-ota-pricing-startvalue')
3189 .querySelector('.vbo-roverw-setnewrate-ota-pricing-willvalue');
3190
3191 // define the currency options
3192 let ota_currency_options = {};
3193
3194 // check if the channel requires a specific currency
3195 let ota_currency_data = will_alter_elem.getAttribute('data-currency');
3196 if (ota_currency_data) {
3197 // decode currency data instructions
3198 try {
3199 ota_currency_data = JSON.parse(ota_currency_data);
3200 } catch (e) {
3201 ota_currency_data = {};
3202 }
3203
3204 // set custom currency options
3205 if (ota_currency_data['symbol']) {
3206 ota_currency_options['symbol'] = ota_currency_data['symbol'];
3207 }
3208 if (ota_currency_data['decimals']) {
3209 ota_currency_options['digits'] = ota_currency_data['decimals'];
3210 }
3211 if (ota_currency_data['decimals_sep']) {
3212 ota_currency_options['decimals'] = ota_currency_data['decimals_sep'];
3213 }
3214 if (ota_currency_data['thousands_sep']) {
3215 ota_currency_options['thousands'] = ota_currency_data['thousands_sep'];
3216 }
3217 }
3218
3219 // set calculated OTA rate value
3220 will_alter_elem.innerHTML = currencyObj.format(ota_rate_amount, ota_currency_options);
3221 });
3222 }, 200));
3223
3224 <?php
3225 // check if some area/project IDs should be rendered
3226 if ($activeAreas) {
3227 ?>
3228 // get all month tables to obtain the date bounds and the listing IDs
3229 let from_date, to_date;
3230 let listingIds = [];
3231 let activeAreas = <?php echo json_encode($activeAreas); ?>;
3232 let monthTables = document.querySelectorAll('table.vboverviewtable[data-month-from]');
3233
3234 if (monthTables.length) {
3235 // get the date bounds
3236 from_date = monthTables[0].getAttribute('data-month-from');
3237 to_date = monthTables[(monthTables.length - 1)].getAttribute('data-month-to');
3238 // get all listing IDs from the first table
3239 monthTables[0]
3240 .querySelectorAll('.roomname[data-roomid]')
3241 .forEach((roomRowCell) => {
3242 if (roomRowCell.matches('.subroomname')) {
3243 // skip sub-unit rows
3244 return;
3245 }
3246 listingIds.push(roomRowCell.getAttribute('data-roomid'));
3247 });
3248 }
3249
3250 if (listingIds.length) {
3251 // dispatch consequent requests, one for each active area ID
3252 vboActionDispatchAreasLoading(activeAreas, from_date, to_date, listingIds, (areaDisplayed) => {
3253 // parse all context menu buttons to identify the current area/project id
3254 jQuery('.vbo-context-menu-overview-actions').vboContextMenu('buttons').forEach((btn) => {
3255 if (btn.areaId == areaDisplayed) {
3256 // enable area/project active state
3257 btn.activeState = true;
3258 }
3259 });
3260 });
3261 }
3262 <?php
3263 }
3264 ?>
3265
3266 });
3267 </script>
3268