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

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

3,256 lines 158.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package VikBooking
4 * @subpackage core
5 * @author E4J s.r.l.
6 * @copyright Copyright (C) 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 alert('Invalid rate amount.');
1255 return;
1256 }
1257
1258 if (rateType === 'addsub' && (isNaN(addsub_amount) || addsub_amount <= 0)) {
1259 alert('Invalid amount for increasing/decreasing rates.');
1260 return;
1261 }
1262
1263 // increase action counter
1264 vboMulticalendarActionCounters.apply++;
1265
1266 // check the OTA pricing alteration rules, if any
1267 let ota_pricing = {};
1268 if (invoke_vcm) {
1269 // scan all OTA alteration rules, if any
1270 document.querySelectorAll('.vbo-roverw-setnewrate-ota-pricing-currentvalue[data-alteration]').forEach((elem) => {
1271 // channel alteration string
1272 let alter_string = elem.getAttribute('data-alteration');
1273 if (!alter_string) {
1274 alter_string = '';
1275 }
1276
1277 // access the parent node to get the OTA channel identifier
1278 let ota_id = elem
1279 .closest('.vbo-roverw-setnewrate-vcm-ota-relation-channel[data-otaid]')
1280 .getAttribute('data-otaid');
1281
1282 if (!ota_id || !alter_string || alter_string == '+0%' || alter_string == '+0*') {
1283 // avoid pushing an empty alteration command
1284 return;
1285 }
1286
1287 // push OTA pricing alteration command
1288 ota_pricing[ota_id] = alter_string;
1289 });
1290 }
1291
1292 if (!Object.keys(ota_pricing).length) {
1293 // unset the object for the request
1294 ota_pricing = null;
1295 }
1296
1297 // tell whether derived rate plans should be skipped
1298 let skipDerivedEl = document.querySelector('input[name="overw-skip-derived"]');
1299 let skipDerivedRates = skipDerivedEl && skipDerivedEl.checked ? 1 : 0;
1300
1301 // build the list of update requests (one per listing)
1302 let requestList = [];
1303 listingsInvolved.forEach((listingId) => {
1304 requestList.push({
1305 id_room: listingId,
1306 id_price: vboActionRoomRplansMap[listingId],
1307 rate_type: rateType,
1308 rate: setrate,
1309 addsub_op: addsub_op,
1310 addsub_amount: addsub_amount,
1311 addsub_value: addsub_value,
1312 vcm: invoke_vcm,
1313 minlos: setminlos,
1314 fromdate: VBOCore.formatDate(vboActionRoomRateData.start, 'Y-m-d'),
1315 todate: VBOCore.formatDate(vboActionRoomRateData.end, 'Y-m-d'),
1316 rateclosed: 0,
1317 ota_pricing: ota_pricing,
1318 skip_derived: skipDerivedRates,
1319 });
1320 });
1321
1322 // count the number of requests
1323 let requestCount = requestList.length;
1324
1325 // start the response container for every request
1326 let responseContainer = [];
1327
1328 // start loading
1329 VBOCore.emitEvent('vbo-overv-setnewrates-loading');
1330
1331 // set progress
1332 VBOCore.emitEvent('vbo-overv-setnewrates-progress', {
1333 progress_content: '1 / ' + requestCount,
1334 });
1335
1336 // dispatch the rates update requests
1337 vboActionDispatchRatesUpdateRequests(
1338 requestList,
1339 (obj_res, listingName) => {
1340 // one request was completed, push the result
1341 responseContainer.push(Object.assign(obj_res, {listingName: listingName}));
1342
1343 // update progress
1344 VBOCore.emitEvent('vbo-overv-setnewrates-progress', {
1345 progress_content: (requestList.length ? (requestCount - requestList.length + 1) : requestCount) + ' / ' + requestCount,
1346 });
1347 },
1348 () => {
1349 // process completed
1350 let showOTAResponse = false;
1351 let htmlOTAResponse = [];
1352
1353 // iterate the operation results
1354 responseContainer.forEach((response) => {
1355 if (typeof response !== 'object' || !response.hasOwnProperty('vcm')) {
1356 // nothing to say about this response
1357 return;
1358 }
1359
1360 // turn flag on
1361 showOTAResponse = true;
1362
1363 // build HTML response string and add it to the list
1364 htmlOTAResponse.push(vboActionRenderCMResult(response.vcm, response.listingName));
1365 });
1366
1367 if (showOTAResponse && htmlOTAResponse.length) {
1368 // display the modal with the update operation results
1369 VBOCore.displayModal({
1370 suffix: 'vbo-vcm-rates-res',
1371 extra_class: 'vbo-modal-rounded vbo-modal-tall vbo-modal-nofooter',
1372 title: <?php echo json_encode(JText::translate('VBOVCMRATESRES')); ?>,
1373 body: '<div class="vbo-vcm-rates-res-container' + (htmlOTAResponse.length > 1 ? ' vbo-vcm-ota-multicalendar-response' : '') + '">' + htmlOTAResponse.join("\n") + '</div>',
1374 draggable: true,
1375 });
1376 }
1377
1378 // update new rates on the involved cells
1379 involvedCellsMatrix.forEach((listingCells, rowIndex) => {
1380 listingCells.forEach((selectedCell, cellIndex) => {
1381 let rateAmountEl = selectedCell.querySelector('.vbo-roomrates-cell-rate-amount');
1382 if (!rateAmountEl) {
1383 // cell is occupied by a reservation, nothing to update
1384 return;
1385 }
1386
1387 // access cell day
1388 let cellDay = selectedCell.getAttribute('data-day') || '';
1389
1390 // listing ID and rate plan ID involved
1391 let parsedListingId = null;
1392 let parsedRateId = null;
1393
1394 // build date-part of expected response key by removing leading zeros in month-days and months
1395 let expectedRespKey = cellDay.replace(/-(0)([1-9]{1})/g, "-$2");
1396 let keyParts = expectedRespKey.split('-');
1397 expectedRespKey = [keyParts[2], keyParts[1], keyParts[0]].join('-');
1398
1399 // access parent cell row
1400 let parentRow = selectedCell.closest('tr[data-roomid]');
1401 if (parentRow) {
1402 // set listing ID and rate plan ID involved
1403 parsedListingId = parentRow.getAttribute('data-roomid');
1404 parsedRateId = parentRow.getAttribute('data-rateid') || vboActionRoomRplansMap[parsedListingId];
1405 }
1406
1407 // finalise expected response key parts
1408 expectedRespKey += '-' + (parsedRateId || '');
1409
1410 // scan all operation results to find the desired response property
1411 let responseFound = false;
1412 responseContainer.forEach((responseData) => {
1413 if (responseFound) {
1414 return;
1415 }
1416 if (responseData.hasOwnProperty(expectedRespKey) && responseData[expectedRespKey]['idroom'] == parsedListingId) {
1417 // turn flag on
1418 responseFound = true;
1419 // set rate applied from response obtained
1420 rateAmountEl.innerHTML = VBOCore.getCurrency().format(responseData[expectedRespKey]['cost']);
1421 }
1422 });
1423
1424 if (!responseFound && rateType !== 'addsub' && !isNaN(setrate)) {
1425 // set rate requested to be applied
1426 rateAmountEl.innerHTML = VBOCore.getCurrency().format(setrate);
1427 }
1428
1429 if (setminlos) {
1430 // populate minimum stay
1431 let minlosEl = selectedCell.querySelector('.vbo-roomrates-cell-minlos');
1432 minlosEl.innerHTML = '<?php VikBookingIcons::e('moon'); ?> ' + setminlos;
1433 }
1434
1435 // set updated class to cell
1436 selectedCell.classList.add('vbo-cell-new-update');
1437 });
1438 });
1439
1440 // dismiss the modal upon completion, after having updated the new rates on the involved cells
1441 VBOCore.emitEvent('vbo-overv-setnewrates-dismiss');
1442 },
1443 (err_mess, unrecoverable) => {
1444 // stop loading in case of error
1445 VBOCore.emitEvent('vbo-overv-setnewrates-loading');
1446 }
1447 );
1448 };
1449
1450 /**
1451 * Register function to queue a rates update request.
1452 */
1453 const vboActionQueueRatesUpdate = () => {
1454 let involvedCellsMatrix = vboActionRoomRateGetCellsMatrix(vboActionRoomRateData.start, vboActionRoomRateData.end, vboActionRoomRateData.listingIdStart, vboActionRoomRateData.listingIdEnd);
1455 let listingsInvolved = [];
1456 involvedCellsMatrix.forEach((listingCells, rowIndex) => {
1457 // push listing ID involved
1458 listingsInvolved.push(listingCells[0].closest('tr').getAttribute('data-roomid'));
1459 });
1460
1461 // gather rates modification values
1462 let rateType = document.querySelector('.vbo-roverw-newrwrap').getAttribute('data-rate-type') || 'fixed';
1463 let setminlos = document.querySelector('#roverw-newrestr').value;
1464 let invoke_vcm = document.querySelector('input[name="roverw-newrate-vcm"]:checked') ? 1 : 0;
1465 let addsub_op = rateType === 'addsub' ? document.querySelector('.vbo-roverw-newrcont[data-rate-type="addsub"] [data-rate-type-rule="rmodsop"]')?.value : 0;
1466 let addsub_amount = rateType === 'addsub' ? document.querySelector('.vbo-roverw-newrcont[data-rate-type="addsub"] [data-rate-type-rule="rmodsamount"]')?.value : 0;
1467 let addsub_value = rateType === 'addsub' ? document.querySelector('.vbo-roverw-newrcont[data-rate-type="addsub"] [data-rate-type-rule="rmodsval"]')?.value : 0;
1468 let setrate = parseFloat(document.querySelector('#roverw-newrate').value);
1469
1470 if (!listingsInvolved.length) {
1471 alert('No listings to update.');
1472 return;
1473 }
1474
1475 if (rateType === 'fixed' && (isNaN(setrate) || setrate <= 0)) {
1476 alert('Invalid rate amount.');
1477 return;
1478 }
1479
1480 if (rateType === 'addsub' && (isNaN(addsub_amount) || addsub_amount <= 0)) {
1481 alert('Invalid amount for increasing/decreasing rates.');
1482 return;
1483 }
1484
1485 // increase action counter
1486 vboMulticalendarActionCounters.add_to_queue++;
1487
1488 // check the OTA pricing alteration rules, if any
1489 let ota_pricing = {};
1490 if (invoke_vcm) {
1491 // scan all OTA alteration rules, if any
1492 document.querySelectorAll('.vbo-roverw-setnewrate-ota-pricing-currentvalue[data-alteration]').forEach((elem) => {
1493 // channel alteration string
1494 let alter_string = elem.getAttribute('data-alteration');
1495 if (!alter_string) {
1496 alter_string = '';
1497 }
1498
1499 // access the parent node to get the OTA channel identifier
1500 let ota_id = elem
1501 .closest('.vbo-roverw-setnewrate-vcm-ota-relation-channel[data-otaid]')
1502 .getAttribute('data-otaid');
1503
1504 if (!ota_id || !alter_string || alter_string == '+0%' || alter_string == '+0*') {
1505 // avoid pushing an empty alteration command
1506 return;
1507 }
1508
1509 // push OTA pricing alteration command
1510 ota_pricing[ota_id] = alter_string;
1511 });
1512 }
1513
1514 if (!Object.keys(ota_pricing).length) {
1515 // unset the object for the request
1516 ota_pricing = null;
1517 }
1518
1519 // tell whether derived rate plans should be skipped
1520 let skipDerivedEl = document.querySelector('input[name="overw-skip-derived"]');
1521 let skipDerivedRates = skipDerivedEl && skipDerivedEl.checked ? 1 : 0;
1522
1523 // build the list of update requests (one per listing)
1524 let requestList = [];
1525 listingsInvolved.forEach((listingId) => {
1526 requestList.push({
1527 id_room: listingId,
1528 id_price: vboActionRoomRplansMap[listingId],
1529 _roomName: vboActionRoomNames[listingId],
1530 rate_type: rateType,
1531 rate: setrate,
1532 addsub_op: addsub_op,
1533 addsub_amount: addsub_amount,
1534 addsub_value: addsub_value,
1535 vcm: invoke_vcm,
1536 minlos: setminlos,
1537 fromdate: VBOCore.formatDate(vboActionRoomRateData.start, 'Y-m-d'),
1538 todate: VBOCore.formatDate(vboActionRoomRateData.end, 'Y-m-d'),
1539 rateclosed: 0,
1540 ota_pricing: ota_pricing,
1541 skip_derived: skipDerivedRates,
1542 });
1543 });
1544
1545 // queue the current requests to the admin-dock as temporary data
1546 VBOCore.getAdminDock().addTemporaryData(
1547 {
1548 id: '_tmp',
1549 persist_id: 'setnewrates',
1550 name: <?php echo json_encode(JText::translate('VBO_PENDING_QUEUE')); ?>,
1551 icon: '<?php VikBookingIcons::e('stopwatch'); ?>',
1552 style: 'orange',
1553 },
1554 requestList,
1555 (queueData) => {
1556 // temporary data restored from dock
1557 vboActionDisplayRatesQueue(queueData);
1558 },
1559 (queueData) => {
1560 // temporary data removed from dock
1561 document.querySelectorAll('.vbo-cell-pending-update').forEach((pending_cell) => {
1562 // remove pending update class
1563 pending_cell.classList.remove('vbo-cell-pending-update');
1564 // restore initial rate in case rates were set for increase/decrease
1565 let rateAmountEl = pending_cell.querySelector('.vbo-roomrates-cell-rate-amount');
1566 if (rateAmountEl && pending_cell.getAttribute('data-init-rate')) {
1567 rateAmountEl.innerHTML = VBOCore.getCurrency().format(pending_cell.getAttribute('data-init-rate'));
1568 pending_cell.setAttribute('data-init-rate', '');
1569 }
1570 });
1571 }
1572 );
1573
1574 // update new rates on the involved cells
1575 involvedCellsMatrix.forEach((listingCells, rowIndex) => {
1576 listingCells.forEach((selectedCell, cellIndex) => {
1577 let rateAmountEl = selectedCell.querySelector('.vbo-roomrates-cell-rate-amount');
1578 if (!rateAmountEl) {
1579 // cell is occupied by a reservation, nothing to update
1580 return;
1581 }
1582
1583 // store the initial rate value
1584 if (!selectedCell.getAttribute('data-init-rate')) {
1585 selectedCell.setAttribute('data-init-rate', rateAmountEl.textContent.replace(/[^0-9\.]+/g, ''));
1586 }
1587
1588 // display how the rate will be applied
1589 if (rateType === 'addsub') {
1590 // increase/decrease rates
1591 let addsub_op_char = addsub_op == 1 ? '+' : '-';
1592 if (addsub_value == 1) {
1593 // percent
1594 rateAmountEl.innerHTML = addsub_op_char + ' ' + addsub_amount + '%';
1595 } else {
1596 // fixed
1597 rateAmountEl.innerHTML = addsub_op_char + ' ' + VBOCore.getCurrency().format(addsub_amount);
1598 }
1599 } else {
1600 // fixed rate
1601 rateAmountEl.innerHTML = VBOCore.getCurrency().format(setrate);
1602 }
1603
1604 // handle minimum stay information display
1605 if (setminlos) {
1606 let minlosEl = selectedCell.querySelector('.vbo-roomrates-cell-minlos');
1607 minlosEl.innerHTML = '<?php VikBookingIcons::e('moon'); ?> ' + setminlos;
1608 }
1609 selectedCell.classList.add('vbo-cell-pending-update');
1610 });
1611 });
1612
1613 // dismiss the modal upon completion, after having updated the new rates on the involved cells
1614 VBOCore.emitEvent('vbo-overv-setnewrates-dismiss');
1615 };
1616
1617 /**
1618 * Displays the information about the current rates queue data.
1619 *
1620 * @param Array ratesData The current queue data.
1621 */
1622 const vboActionDisplayRatesQueue = (ratesData) => {
1623 // define the modal cancel button
1624 let cancel_btn = jQuery('<button></button>')
1625 .attr('type', 'button')
1626 .addClass('btn')
1627 .text(<?php echo json_encode(JText::translate('VBANNULLA')); ?>)
1628 .on('click', () => {
1629 // minimize again the temporary data to the dock by dismissing the modal
1630 VBOCore.emitEvent('vbo-overv-setnewrates-queue-dismiss');
1631 });
1632
1633 // define the modal apply button
1634 let apply_btn = jQuery('<button></button>')
1635 .attr('type', 'button')
1636 .addClass('btn btn-success')
1637 .html('<?php VikBookingIcons::e('rocket'); ?> ' + <?php echo json_encode(JText::translate('VBAPPLY')); ?>)
1638 .on('click', function() {
1639 // count the number of requests
1640 let requestCount = ratesData.length;
1641
1642 if (!requestCount) {
1643 throw new Error('Rates queue is empty');
1644 }
1645
1646 // start the response container for every request
1647 let responseContainer = [];
1648
1649 // start loading
1650 VBOCore.emitEvent('vbo-overv-setnewrates-queue-loading');
1651
1652 // set progress
1653 VBOCore.emitEvent('vbo-overv-setnewrates-queue-progress', {
1654 progress_content: '1 / ' + requestCount,
1655 });
1656
1657 // dispatch the rates update requests
1658 vboActionDispatchRatesUpdateRequests(
1659 ratesData,
1660 (obj_res, listingName) => {
1661 // one request was completed, push the result
1662 responseContainer.push(Object.assign(obj_res, {listingName: listingName}));
1663
1664 // update progress
1665 VBOCore.emitEvent('vbo-overv-setnewrates-queue-progress', {
1666 progress_content: (ratesData.length ? (requestCount - ratesData.length + 1) : requestCount) + ' / ' + requestCount,
1667 });
1668 },
1669 () => {
1670 // process completed
1671 let showOTAResponse = false;
1672 let htmlOTAResponse = [];
1673
1674 // iterate the operation results
1675 responseContainer.forEach((response) => {
1676 if (typeof response !== 'object' || !response.hasOwnProperty('vcm')) {
1677 // nothing to say about this response
1678 return;
1679 }
1680
1681 // turn flag on
1682 showOTAResponse = true;
1683
1684 // build HTML response string and add it to the list
1685 htmlOTAResponse.push(vboActionRenderCMResult(response.vcm, response.listingName));
1686 });
1687
1688 if (showOTAResponse && htmlOTAResponse.length) {
1689 // display the modal with the update operation results
1690 VBOCore.displayModal({
1691 suffix: 'vbo-vcm-rates-res',
1692 extra_class: 'vbo-modal-rounded vbo-modal-tall vbo-modal-nofooter',
1693 title: <?php echo json_encode(JText::translate('VBOVCMRATESRES')); ?>,
1694 body: '<div class="vbo-vcm-rates-res-container' + (htmlOTAResponse.length > 1 ? ' vbo-vcm-ota-multicalendar-response' : '') + '">' + htmlOTAResponse.join("\n") + '</div>',
1695 draggable: true,
1696 });
1697 }
1698
1699 // update the involved cells
1700 document.querySelectorAll('.vbo-grid-avcell-rates.vbo-cell-pending-update').forEach((pendingCell) => {
1701 // handle cell class
1702 pendingCell.classList.remove('vbo-cell-pending-update');
1703 pendingCell.classList.add('vbo-cell-new-update');
1704
1705 // handle new rate applied
1706 let rateAmountEl = pendingCell.querySelector('.vbo-roomrates-cell-rate-amount');
1707 if (!rateAmountEl) {
1708 // cell is occupied by a reservation, nothing to update
1709 return;
1710 }
1711
1712 // access cell day
1713 let cellDay = pendingCell.getAttribute('data-day') || '';
1714
1715 // listing ID and rate plan ID involved
1716 let parsedListingId = null;
1717 let parsedRateId = null;
1718
1719 // build date-part of expected response key by removing leading zeros in month-days and months
1720 let expectedRespKey = cellDay.replace(/-(0)([1-9]{1})/g, "-$2");
1721 let keyParts = expectedRespKey.split('-');
1722 expectedRespKey = [keyParts[2], keyParts[1], keyParts[0]].join('-');
1723
1724 // access parent cell row
1725 let parentRow = pendingCell.closest('tr[data-roomid]');
1726 if (parentRow) {
1727 // set listing ID and rate plan ID involved
1728 parsedListingId = parentRow.getAttribute('data-roomid');
1729 parsedRateId = parentRow.getAttribute('data-rateid') || vboActionRoomRplansMap[parsedListingId];
1730 }
1731
1732 // finalise expected response key parts
1733 expectedRespKey += '-' + (parsedRateId || '');
1734
1735 // scan all operation results to find the desired response property
1736 let responseFound = false;
1737 responseContainer.forEach((responseData) => {
1738 if (responseFound) {
1739 return;
1740 }
1741 if (responseData.hasOwnProperty(expectedRespKey) && responseData[expectedRespKey]['idroom'] == parsedListingId) {
1742 // turn flag on
1743 responseFound = true;
1744 // set rate applied from response obtained
1745 rateAmountEl.innerHTML = VBOCore.getCurrency().format(responseData[expectedRespKey]['cost']);
1746 }
1747 });
1748
1749 // if the desired response key was not found, we do nothing, as the queue
1750 // must have already restored the rates data alteration details for this cell
1751 });
1752
1753 // dismiss the modal upon completion, after having updated the involved cells
1754 VBOCore.emitEvent('vbo-overv-setnewrates-queue-dismiss');
1755
1756 // at last, dismiss the admin-dock entry upon completion
1757 VBOCore.getAdminDock().removeDockElementById('_tmp');
1758 },
1759 (err_mess, unrecoverable) => {
1760 // do nothing in case of error when inside a queue of requests
1761 }
1762 );
1763 });
1764
1765 // build modal body
1766 let modalBodyEl = document.createElement('div');
1767 modalBodyEl.classList.add('vbo-rates-queue-wrapper');
1768 ratesData.forEach((updateData) => {
1769 // create queue update container
1770 let updateEl = document.createElement('div');
1771 updateEl.classList.add('vbo-rates-queue-data');
1772
1773 // contain the listing
1774 let updateRoomEl = document.createElement('span');
1775 updateRoomEl.classList.add('vbo-rates-queue-data-listing');
1776 updateRoomEl.innerText = vboActionRoomNames[updateData.id_room] || updateData._roomName || updateData.id_room;
1777 updateEl.append(updateRoomEl);
1778
1779 // contain the dates
1780 let updateDatesEl = document.createElement('span');
1781 updateDatesEl.classList.add('vbo-rates-queue-data-dates');
1782 updateDatesEl.innerHTML = '<span class="badge badge-info">' + updateData.fromdate + '</span> - <span class="badge badge-info">' + updateData.todate + '</span>';
1783 updateEl.append(updateDatesEl);
1784
1785 // contain the rate
1786 let updateRatesEl = document.createElement('span');
1787 updateRatesEl.classList.add('vbo-rates-queue-data-rate');
1788 let updateRatesBadgeEl = document.createElement('span');
1789 updateRatesBadgeEl.classList.add('badge', 'badge-warning');
1790 if (updateData?.rate_type === 'addsub') {
1791 // increase/decrease rates
1792 let addsub_op = updateData?.addsub_op == 1 ? '+' : '-';
1793 if (updateData?.addsub_value == 1) {
1794 // percent
1795 updateRatesBadgeEl.innerHTML = addsub_op + ' ' + updateData.addsub_amount + '%';
1796 } else {
1797 // fixed
1798 updateRatesBadgeEl.innerHTML = addsub_op + ' ' + VBOCore.getCurrency().format(updateData.addsub_amount);
1799 }
1800 } else {
1801 // fixed rate
1802 updateRatesBadgeEl.innerHTML = VBOCore.getCurrency().format(updateData.rate);
1803 }
1804 updateRatesEl.append(updateRatesBadgeEl);
1805 updateEl.append(updateRatesEl);
1806
1807 if (updateData.minlos) {
1808 // contain the minimum stay
1809 let updateMinlosEl = document.createElement('span');
1810 updateMinlosEl.classList.add('vbo-rates-queue-data-minlos');
1811 updateMinlosEl.innerHTML = '<span class="badge"><?php VikBookingIcons::e('moon'); ?> ' + updateData.minlos + '</span>';
1812 updateEl.append(updateMinlosEl);
1813 }
1814
1815 // append queue update container
1816 modalBodyEl.append(updateEl);
1817 });
1818
1819 // display modal
1820 VBOCore.displayModal({
1821 suffix: 'overv_setnewrates_queue_modal',
1822 title: <?php echo json_encode(JText::translate('VBO_PENDING_QUEUE')); ?>,
1823 extra_class: 'vbo-modal-rounded vbo-modal-tall',
1824 body: modalBodyEl,
1825 body_prepend: true,
1826 lock_scroll: true,
1827 draggable: true,
1828 footer_left: cancel_btn,
1829 footer_right: apply_btn,
1830 loading_event: 'vbo-overv-setnewrates-queue-loading',
1831 dismiss_event: 'vbo-overv-setnewrates-queue-dismiss',
1832 progress_event: 'vbo-overv-setnewrates-queue-progress',
1833 loading_body: '<?php VikBookingIcons::e('refresh', 'fa-spin fa-3x fa-fw'); ?>',
1834 onDismiss: () => {
1835 if (!ratesData.length) {
1836 // temporary data queue is empty
1837 return;
1838 }
1839
1840 // minimize again the temporary data to the dock
1841 VBOCore.getAdminDock().addTemporaryData(
1842 {
1843 id: '_tmp',
1844 persist_id: 'setnewrates',
1845 name: <?php echo json_encode(JText::translate('VBO_PENDING_QUEUE')); ?>,
1846 icon: '<?php VikBookingIcons::e('stopwatch'); ?>',
1847 style: 'orange',
1848 },
1849 ratesData,
1850 (queueData) => {
1851 // temporary data restored from dock
1852 vboActionDisplayRatesQueue(queueData);
1853 },
1854 (queueData) => {
1855 // temporary data removed from dock
1856 document.querySelectorAll('.vbo-cell-pending-update').forEach((pending_cell) => {
1857 // remove pending update class
1858 pending_cell.classList.remove('vbo-cell-pending-update');
1859 // restore initial rate in case rates were set for increase/decrease
1860 let rateAmountEl = pending_cell.querySelector('.vbo-roomrates-cell-rate-amount');
1861 if (rateAmountEl && pending_cell.getAttribute('data-init-rate')) {
1862 rateAmountEl.innerHTML = VBOCore.getCurrency().format(pending_cell.getAttribute('data-init-rate'));
1863 pending_cell.setAttribute('data-init-rate', '');
1864 }
1865 });
1866 }
1867 );
1868 },
1869 });
1870 };
1871
1872 /**
1873 * Register function to process a list of rates update requests one after the other.
1874 *
1875 * @param array requests List of rates update request objects.
1876 * @param function onProgress Optional callback when a request is completed.
1877 * @param function onComplete Optional callback when all requests have been completed.
1878 * @param function onError Optional callback in case of request error.
1879 *
1880 * @return void
1881 */
1882 const vboActionDispatchRatesUpdateRequests = (requests, onProgress, onComplete, onError) => {
1883 if (!Array.isArray(requests) || !requests.length) {
1884 if (typeof onComplete === 'function') {
1885 onComplete();
1886 }
1887
1888 // abort
1889 return;
1890 }
1891
1892 // obtain the request to process
1893 const request = requests.shift();
1894
1895 // perform the request
1896 VBOCore.doAjax(
1897 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=pricing.setnewrates'); ?>",
1898 request,
1899 (res) => {
1900 if (typeof res === 'string' && res.indexOf('e4j.error') === 0) {
1901 // display error
1902 let err_mess = res.replace('e4j.error.', '');
1903 alert(err_mess);
1904
1905 if (typeof onError === 'function') {
1906 // unrecoverable error
1907 onError(err_mess, true);
1908 }
1909
1910 // recursively call the same function to process the next request, if any
1911 vboActionDispatchRatesUpdateRequests(requests, onProgress, onComplete, onError);
1912
1913 // abort
1914 return;
1915 }
1916
1917 try {
1918 // check the response
1919 let obj_res = typeof res === 'string' ? JSON.parse(res) : res;
1920
1921 if (typeof onProgress === 'function') {
1922 // call the given function by passing the operation result object and the room name
1923 onProgress(obj_res, (vboActionRoomNames[request.id_room] || request.id_room));
1924 }
1925
1926 // recursively call the same function to process the next request
1927 vboActionDispatchRatesUpdateRequests(requests, onProgress, onComplete, onError);
1928 } catch(err) {
1929 // display error
1930 alert(err);
1931
1932 if (typeof onError === 'function') {
1933 // unrecoverable error
1934 onError(err, true);
1935 }
1936
1937 // recursively call the same function to process the next request, if any
1938 vboActionDispatchRatesUpdateRequests(requests, onProgress, onComplete, onError);
1939 }
1940 },
1941 (err) => {
1942 // display error
1943 let err_mess = err.responseText || 'Request failed due to connection error';
1944 alert(err_mess);
1945
1946 if (typeof onError === 'function') {
1947 // connection error
1948 onError(err_mess, false);
1949 }
1950
1951 // recursively call the same function to process the next request, if any
1952 vboActionDispatchRatesUpdateRequests(requests, onProgress, onComplete, onError);
1953 }
1954 );
1955 };
1956
1957 /**
1958 * Register function to populate the room ota relations upon selecting a matrix of listings + dates.
1959 */
1960 const vboActionOvervSetAllRoomRelations = (listingsInvolved) => {
1961 // build the current day key in Y-m-d format
1962 let start_day_key = VBOCore.formatDate(vboActionRoomRateData.start, 'Y-m-d');
1963 let end_day_key = VBOCore.formatDate(vboActionRoomRateData.end, 'Y-m-d');
1964
1965 // TODO format dates according to settings
1966
1967 let listingsText = listingsInvolved.length > 1 ? listingsInvolved.length + ' ' + <?php echo json_encode(JText::translate('VBO_LISTINGS')); ?> : (vboActionRoomNames[listingsInvolved[0]] || listingsInvolved[0]);
1968
1969 // set operation details first
1970 jQuery('.vbo-overview-action-raterestr-listings').text(listingsText);
1971 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>');
1972
1973 // the room-ota relations wrapper
1974 let wrapper = jQuery('.vbo-roverw-setnewrate-vcm-otas');
1975
1976 // always empty the wrapper
1977 wrapper.html('');
1978
1979 // store a list of parsed channel IDs
1980 let parsedChannelIds = [];
1981
1982 listingsInvolved.forEach((room_id) => {
1983 let rplan_id = vboActionRoomRplansMap[room_id];
1984 if (!room_id || !rplan_id || !vboActionRoomOtaRels.hasOwnProperty(room_id)) {
1985 // nothing to render for the current listing
1986 return;
1987 }
1988
1989 // start counter
1990 let ota_ch_counter = 0;
1991
1992 // build and append room-OTA relations
1993 for (const ota_name in vboActionRoomOtaRels[room_id]['channels']) {
1994 // get the current channel ID
1995 let channel_id = vboActionRoomOtaRels[room_id]['accounts'][ota_ch_counter]['idchannel'];
1996
1997 if (parsedChannelIds.includes(channel_id)) {
1998 // do not display the same channel multiple times
1999 return;
2000 }
2001
2002 // push channel ID parsed
2003 parsedChannelIds.push(channel_id);
2004
2005 // build ota readable name
2006 let ota_read_name = ota_name;
2007 ota_read_name = ota_read_name.replace(/api$/, '');
2008 ota_read_name = ota_read_name.replace(/^(google)(hotel|vr)$/i, '$1 $2');
2009
2010 // build room-ota relation block and elements
2011 let ota_block = jQuery('<div></div>');
2012 ota_block.addClass('vbo-roverw-setnewrate-vcm-ota-relation');
2013
2014 let ota_block_inner = jQuery('<div></div>');
2015 ota_block_inner
2016 .addClass('vbo-roverw-setnewrate-vcm-ota-relation-pricing')
2017 .attr('data-ota', (ota_name + '').toLowerCase());
2018
2019 let ota_block_channel = jQuery('<div></div>');
2020 ota_block_channel
2021 .addClass('vbo-roverw-setnewrate-vcm-ota-relation-channel')
2022 .attr('data-otaid', vboActionRoomOtaRels[room_id]['accounts'][ota_ch_counter]['idchannel'])
2023 .append('<img src="' + vboActionRoomOtaRels[room_id]['channels'][ota_name] + '" />')
2024 .append('<span>' + ota_read_name + '</span>');
2025
2026 let ota_pricing_value = jQuery('<span></span>');
2027 ota_pricing_value
2028 .addClass('vbo-roverw-setnewrate-vcm-ota-pricing-startvalue')
2029 .html('<?php VikBookingIcons::e('circle-notch', 'fa-spin fa-fw'); ?>')
2030 .on('click', function() {
2031 jQuery(this)
2032 .closest('.vbo-roverw-setnewrate-vcm-ota-relation-pricing')
2033 .find('.vbo-roverw-setnewrate-vcm-ota-channel-pricing')
2034 .toggle();
2035 });
2036
2037 let ota_block_pricing = jQuery('<div></div>');
2038 ota_block_pricing
2039 .addClass('vbo-roverw-setnewrate-vcm-ota-channel-pricing')
2040 .css('display', 'none')
2041 .append(jQuery('.vbo-roverw-setnewrate-vcm-ota-pricing-alteration').first().clone());
2042
2043 // register "input" event for select/input elements to control the channel alteration rule overrides
2044 ota_block_pricing.find('select, input').on('input', function() {
2045 let input_elem = jQuery(this);
2046
2047 // get the current channel alteration command
2048 let ota_alteration_command = input_elem
2049 .closest('.vbo-roverw-setnewrate-vcm-ota-relation-pricing')
2050 .find('.vbo-roverw-setnewrate-ota-pricing-currentvalue[data-alteration]')
2051 .attr('data-alteration');
2052
2053 // access alteration rule and input value
2054 let rmod_type = input_elem.attr('data-alter-rule');
2055 let rmod_value = input_elem.val();
2056
2057 if (!ota_alteration_command || !rmod_type || !(rmod_value + '').length) {
2058 return;
2059 }
2060
2061 // check what pricing factor was changed
2062 if (rmod_type == 'rmodsop') {
2063 // increase or decrease rate
2064 let command_old_val = ota_alteration_command.substr(0, 1);
2065 let command_new_val = parseInt(rmod_value) == 1 ? '+' : '-';
2066 ota_alteration_command = ota_alteration_command.replace(command_old_val, command_new_val);
2067 } else if (rmod_type == 'rmodsamount') {
2068 // amount
2069 let command_op = ota_alteration_command.substr(0, 1);
2070 let command_val = ota_alteration_command.substr(-1, 1);
2071 let command_old_val = ota_alteration_command.replace(command_op, '').replace(command_val, '');
2072 let command_new_val = parseFloat(rmod_value);
2073 ota_alteration_command = ota_alteration_command.replace(command_old_val, command_new_val);
2074 } else if (rmod_type == 'rmodsval') {
2075 // percent or absolute
2076 let command_old_val = ota_alteration_command.substr(-1, 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 }
2080
2081 // check if the channel requires a specific currency
2082 let ota_currency_data = input_elem
2083 .closest('.vbo-roverw-setnewrate-vcm-ota-relation-pricing')
2084 .find('.vbo-roverw-setnewrate-ota-pricing-willvalue')
2085 .attr('data-currency');
2086 if (ota_currency_data) {
2087 // decode currency data instructions
2088 try {
2089 ota_currency_data = JSON.parse(ota_currency_data);
2090 } catch (e) {
2091 ota_currency_data = {};
2092 }
2093 }
2094
2095 // define the current channel alteration string (readable)
2096 let ota_alteration_string = ota_alteration_command;
2097
2098 // finalize the current channel alteration string (readable)
2099 let ota_alteration_val = ota_alteration_string.substr(-1, 1);
2100 if (ota_alteration_val != '%') {
2101 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) ?: '"$"'; ?>);
2102 }
2103
2104 // update the alteration rule command attribute
2105 input_elem
2106 .closest('.vbo-roverw-setnewrate-vcm-ota-relation-pricing')
2107 .find('.vbo-roverw-setnewrate-ota-pricing-currentvalue[data-alteration]')
2108 .attr('data-alteration', ota_alteration_command);
2109
2110 // update the alteration rule string tag text
2111 input_elem
2112 .closest('.vbo-roverw-setnewrate-vcm-ota-relation-pricing')
2113 .find('.vbo-roverw-setnewrate-ota-pricing-currentvalue[data-alteration]')
2114 .html(ota_alteration_string);
2115
2116 // get the current rate to set
2117 let current_room_rate = jQuery('#roverw-newrate').val();
2118 let current_rate_type = document.querySelector('.vbo-roverw-newrwrap').getAttribute('data-rates-type');
2119 if (current_room_rate) {
2120 // dispatch the event to trigger the re-calculation of the OTA rates
2121 VBOCore.emitEvent('vbo-roverv-setnewrate-calc-ota-pricing', {
2122 rate: current_rate_type != 'addsub' ? current_room_rate : 0,
2123 });
2124 }
2125 });
2126
2127 // append elements to wrapper
2128 ota_block_channel.append(ota_pricing_value);
2129 ota_block_inner.append(ota_block_channel);
2130 ota_block_inner.append(ota_block_pricing);
2131 ota_block.append(ota_block_inner);
2132 wrapper.append(ota_block);
2133
2134 // increase OTA channel counter
2135 ota_ch_counter++;
2136 }
2137 });
2138
2139 // obtain the details for the first involved listing
2140 let firstListingId = listingsInvolved[0];
2141 let firstRplanId = vboActionRoomRplansMap[listingsInvolved[0]];
2142
2143 // trigger an AJAX request to load the current alteration rules, if any, for the first listing involved
2144 VBOCore.doAjax(
2145 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=pricing.loadOtaAlterationRules'); ?>",
2146 {
2147 room_id: firstListingId,
2148 rate_id: firstRplanId,
2149 },
2150 (res) => {
2151 var obj_res = typeof res === 'string' ? JSON.parse(res) : res;
2152 let alter_room_rates = obj_res['rmod'] == '1' || obj_res['rmod'] == 1;
2153
2154 // scan all room OTAs
2155 jQuery('.vbo-roverw-setnewrate-vcm-otas').find('.vbo-roverw-setnewrate-vcm-ota-relation').each(function(key, elem) {
2156 // get the current OTA identifier and whether pricing is altered
2157 let ota_wrap = jQuery(elem);
2158 let ota_id = ota_wrap.find('.vbo-roverw-setnewrate-vcm-ota-relation-channel').attr('data-otaid');
2159 let alter_ota_rates = alter_room_rates && obj_res['channels'] && (obj_res['channels'].includes(ota_id) || obj_res['channels'].includes(parseInt(ota_id)));
2160 if (!alter_ota_rates && alter_room_rates && obj_res.hasOwnProperty('rmod_channels') && obj_res['rmod_channels'].hasOwnProperty(ota_id)) {
2161 alter_ota_rates = true;
2162 }
2163
2164 // check if the current channel is using a different currency
2165 let ota_currency_data = {};
2166 if (obj_res.hasOwnProperty('cur_rplans') && obj_res['cur_rplans'].hasOwnProperty(ota_id)) {
2167 let ota_check_currency = obj_res['cur_rplans'][ota_id];
2168 if (obj_res.hasOwnProperty('currency_data_options') && obj_res['currency_data_options'].hasOwnProperty(ota_check_currency)) {
2169 // set custom currency data returned
2170 ota_currency_data = obj_res['currency_data_options'][ota_check_currency];
2171 }
2172 }
2173
2174 // build pricing alteration strings
2175 let alteration_command = '';
2176 let alteration_string = '';
2177
2178 // default alteration factors (no pricing alteration rules)
2179 let alter_op = '+';
2180 let alter_amount = '0';
2181 let alter_val = '%';
2182
2183 if (alter_ota_rates) {
2184 // check how rates are altered for this channel
2185 if (obj_res.hasOwnProperty('rmod_channels') && obj_res['rmod_channels'].hasOwnProperty(ota_id)) {
2186 // ota-level pricing alteration rule
2187 if (parseInt(obj_res['rmod_channels'][ota_id]['rmod']) == 1) {
2188 alter_op = parseInt(obj_res['rmod_channels'][ota_id]['rmodop']) == 1 ? '+' : '-';
2189 alter_amount = obj_res['rmod_channels'][ota_id]['rmodamount'];
2190 alter_val = parseInt(obj_res['rmod_channels'][ota_id]['rmodval']) == 1 ? '%' : '*';
2191 }
2192 } else {
2193 // room-level pricing alteration rule
2194 alter_op = parseInt(obj_res['rmodop']) == 1 ? '+' : '-';
2195 alter_amount = obj_res['rmodamount'] || '0';
2196 alter_val = parseInt(obj_res['rmodval']) == 1 ? '%' : '*';
2197 }
2198 }
2199
2200 // construct alteration strings
2201 alteration_command = alter_op + (alter_amount + '') + (alter_val + '');
2202 alteration_string = alter_op + (alter_amount + '') + (alter_val == '%' ? '%' : (ota_currency_data?.symbol || <?php echo json_encode($currencysymb) ?: '"$"'; ?>));
2203
2204 // stop room-ota loading and set alteration string
2205 let alteration_elem = jQuery('<span></span>');
2206 alteration_elem
2207 .addClass('vbo-roverw-setnewrate-ota-pricing-currentvalue')
2208 .attr('data-alteration', alteration_command)
2209 .html(alteration_string);
2210
2211 let will_alter_elem = jQuery('<span></span>').addClass('vbo-roverw-setnewrate-ota-pricing-willvalue');
2212
2213 if (ota_currency_data.symbol) {
2214 // set currency data object
2215 will_alter_elem.attr('data-currency', JSON.stringify(ota_currency_data));
2216 }
2217
2218 // set elements
2219 ota_wrap
2220 .find('.vbo-roverw-setnewrate-vcm-ota-pricing-startvalue')
2221 .html('')
2222 .append(will_alter_elem)
2223 .append(alteration_elem)
2224 .append('<?php VikBookingIcons::e('edit', 'edit-ota-pricing'); ?>');
2225
2226 // populate default values for input element overrides
2227 ota_wrap.find('select[data-alter-rule="rmodsop"]').val(alter_op == '+' ? 1 : 0);
2228 ota_wrap.find('input[data-alter-rule="rmodsamount"]').val(parseInt(alter_amount) > 0 ? alter_amount : '');
2229 ota_wrap.find('select[data-alter-rule="rmodsval"]').val(alter_val == '%' ? 1 : 0);
2230 });
2231
2232 // check the current rate value
2233 let current_room_rate = jQuery('#roverw-newrate').val();
2234 let current_rate_type = document.querySelector('.vbo-roverw-newrwrap').getAttribute('data-rates-type');
2235 if (current_room_rate && current_rate_type != 'addsub') {
2236 // dispatch the event to allow the actual calculation of the OTA rate
2237 VBOCore.emitEvent('vbo-roverv-setnewrate-calc-ota-pricing', {
2238 rate: current_room_rate,
2239 room_id: firstListingId,
2240 rate_id: firstRplanId,
2241 });
2242 }
2243 },
2244 (err) => {
2245 alert(err.responseText || 'Request Failed');
2246 }
2247 );
2248 };
2249
2250 /**
2251 * Restrictions can be updated only if VCM is available and toggled ON, because
2252 * the creation and transmission is made through the Connector Class of VCM.
2253 * On top of that, the OTA pricing alteration rule overrides will toggle a status class.
2254 */
2255 const vboActionVcmRestrictionsSupported = () => {
2256 if (!jQuery('input[name="roverw-newrate-vcm"]').prop('disabled') && jQuery('input[name="roverw-newrate-vcm"]').prop('checked')) {
2257 jQuery('#roverw-newrestr').val('');
2258 jQuery('.vbo-roverw-newrestr-wrap').show();
2259 jQuery('.vbo-roverw-setnewrate-vcm-ota-relation').removeClass('vbo-roverw-setnewrate-vcm-ota-relation-disabled');
2260 } else {
2261 jQuery('.vbo-roverw-newrestr-wrap').hide();
2262 jQuery('.vbo-roverw-setnewrate-vcm-ota-relation').addClass('vbo-roverw-setnewrate-vcm-ota-relation-disabled');
2263 }
2264 };
2265
2266 /**
2267 * Register function for loading the tasks of a given area/project id.
2268 *
2269 * @param number areaId The area/project ID to render.
2270 * @param string from_date The Y-m-d starting date.
2271 * @param string to_date The Y-m-d ending date.
2272 * @param Array room_ids List of listing IDs to include.
2273 * @param function onComplete Optional callback to invoke upon completion.
2274 */
2275 const vboActionLoadAreaTasks = (areaId, from_date, to_date, room_ids, onComplete) => {
2276 let ctx_elem = document
2277 .querySelector('.vbo-context-menu-overview-actions');
2278
2279 let lbl_elem = ctx_elem
2280 .querySelector('.vbo-context-menu-lbl');
2281
2282 let orig_lbl = lbl_elem.innerText;
2283
2284 // start loading animation
2285 ctx_elem.loading = 1;
2286 lbl_elem.innerHTML = '<?php VikBookingIcons::e('circle-notch', 'fa-spin fa-fw'); ?>';
2287
2288 // make the request
2289 VBOCore.doAjax(
2290 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=taskmanager.loadAreaListingTasks'); ?>",
2291 {
2292 area_id: areaId,
2293 room_ids: room_ids,
2294 from_date: from_date,
2295 to_date: to_date,
2296 },
2297 (resp) => {
2298 try {
2299 // decode the response (if needed), and append the content to the modal body
2300 resp = typeof resp === 'string' ? JSON.parse(resp) : resp;
2301
2302 // hide any previously loaded area task, if any
2303 vboActionHideAreaTasks(areaId);
2304
2305 // populate tasks for all listings by scanning all month tables
2306 document
2307 .querySelectorAll('table.vboverviewtable[data-month-from]')
2308 // iterate months
2309 .forEach((month) => {
2310 // detect if we are using the "old" table layout
2311 let isTableLayout = month.classList.contains('vbo-overv-sticky-table-head-on');
2312
2313 month
2314 .querySelectorAll('.roomname[data-roomid]')
2315 // iterate month listings
2316 .forEach((roomRowCell) => {
2317 if (roomRowCell.matches('.subroomname')) {
2318 // skip sub-unit rows
2319 return;
2320 }
2321
2322 // get current listing ID
2323 let listingId = roomRowCell.getAttribute('data-roomid');
2324
2325 if (!resp.listings.hasOwnProperty(listingId) && !resp.listingIds.includes(listingId) && !resp.listingIds.includes(parseInt(listingId))) {
2326 if (resp.listingIds.length) {
2327 // no tasks returned for this uneligible room id
2328 return;
2329 }
2330 }
2331
2332 // access the "parent row"
2333 let parentRow = roomRowCell.closest('tr');
2334
2335 // build area-tasks row for the current listing
2336 let areaTasksRow = document.createElement('tr');
2337 areaTasksRow.classList.add('vbo-tm-row');
2338 areaTasksRow.setAttribute('data-roomid', listingId);
2339 areaTasksRow.setAttribute('data-area-id', areaId);
2340
2341 // build first row-cell and append it to the row
2342 let areaTasksCell = document.createElement('td');
2343 areaTasksCell.classList.add('vbo-tm-row-cell-first');
2344 areaTasksCell.innerHTML = resp.area?.icon_class ? '<i class="' + resp.area.icon_class + '"></i> ' : '';
2345 let areaNameEl = document.createElement('span');
2346 areaNameEl.classList.add('vbo-tm-row-area-name');
2347 areaNameEl.innerText = resp.area.name;
2348 areaTasksCell.append(areaNameEl);
2349 areaTasksRow.append(areaTasksCell);
2350
2351 // iterate over the "parent row" day cells of the current listing and month
2352 parentRow
2353 .querySelectorAll('td.vbo-grid-avcell[data-day]')
2354 // iterate month listing days
2355 .forEach((roomRowDay) => {
2356 // get the day Y-m-d value
2357 let ymd = roomRowDay.getAttribute('data-day');
2358
2359 // build month-day cell with pricing details
2360 let tasksDayCell = document.createElement('td');
2361 tasksDayCell.classList.add('vbo-tm-row-cell-day');
2362 tasksDayCell.setAttribute('data-day', ymd);
2363
2364 // access the tasks information for this day
2365 if (resp.listings.hasOwnProperty(listingId) && resp.listings[listingId].hasOwnProperty(ymd)) {
2366 // iterate all tasks for the current listing and day
2367 let totTasks = resp.listings[listingId][ymd].length;
2368 resp.listings[listingId][ymd].forEach((task, index) => {
2369 if (index > 2) {
2370 // no more tasks to display for the current listing and day
2371 return;
2372 }
2373
2374 // build task element
2375 let taskEl = document.createElement('div');
2376 taskEl.classList.add('vbo-tm-row-cell-task');
2377 taskEl.setAttribute('data-task-id', task.id);
2378 taskEl.setAttribute('data-area-id', task.area_id);
2379 taskEl.setAttribute('data-task-bid', (task.bid + ''));
2380 if (task.color) {
2381 taskEl.classList.add('vbo-tm-color');
2382 taskEl.classList.add(task.color);
2383 }
2384 if (isTableLayout) {
2385 taskEl.classList.add('vbo-tm-row-cell-task-notitle');
2386 taskEl.innerHTML = '&nbsp;';
2387 } else {
2388 taskEl.innerText = task.title;
2389 }
2390
2391 // append task element to current cell
2392 tasksDayCell.append(taskEl);
2393
2394 if ((index + 1) > 2 && totTasks > (2 + 1)) {
2395 // limit the number of tasks per listing per day by displaying a link to the TM view for this day
2396 let exceeding = totTasks - (index + 1);
2397 let moreTasksTxt = '+' + exceeding + ' ' + (exceeding > 1 ? <?php echo json_encode(JText::translate('VBO_TASKS')); ?> : <?php echo json_encode(JText::translate('VBO_TASK')); ?>).toLowerCase();
2398
2399 // build element
2400 let moreTasksEl = document.createElement('div');
2401 moreTasksEl.classList.add('vbo-tm-calendar-month-day-more')
2402 moreTasksEl.setAttribute('data-day', ymd);
2403 moreTasksEl.innerText = moreTasksTxt;
2404 moreTasksEl.addEventListener('click', () => {
2405 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);
2406 });
2407
2408 // append element to current cell
2409 tasksDayCell.append(moreTasksEl);
2410
2411 return;
2412 }
2413 });
2414 }
2415
2416 // append tasks cell to main area-tasks row
2417 areaTasksRow.append(tasksDayCell);
2418 });
2419
2420 // append the main area-tasks row to the DOM
2421 parentRow.insertAdjacentElement('afterend', areaTasksRow);
2422 });
2423 });
2424
2425 // iterating completed, stop loading
2426 lbl_elem.innerHTML = '';
2427 lbl_elem.innerText = orig_lbl;
2428 ctx_elem.loading = 0;
2429
2430 // register events for the area tasks
2431 vboActionRegisterAreaTaskEvents(areaId);
2432
2433 if (typeof onComplete === 'function') {
2434 // invoke the callback upon completion
2435 onComplete(areaId);
2436 }
2437 } catch (err) {
2438 console.error('Error decoding the response', err, resp);
2439 }
2440 },
2441 (error) => {
2442 // display error message
2443 alert(error.responseText);
2444
2445 // stop loading
2446 lbl_elem.innerHTML = '';
2447 lbl_elem.innerText = orig_lbl;
2448 ctx_elem.loading = 0;
2449 }
2450 );
2451 };
2452
2453 /**
2454 * Register function to process a list of area/project IDs for which the tasks should be loaded.
2455 *
2456 * @param Array areaIds The list of area/project IDs to process.
2457 * @param string from_date The Y-m-d starting date.
2458 * @param string to_date The Y-m-d ending date.
2459 * @param Array room_ids List of listing IDs to include.
2460 * @param function onProgress Function to invoke upon progressing to a next request.
2461 */
2462 const vboActionDispatchAreasLoading = (areaIds, from_date, to_date, room_ids, onProgress) => {
2463 if (!Array.isArray(areaIds) || !areaIds.length) {
2464 // abort when the queue is exhausted
2465 return;
2466 }
2467
2468 // obtain the area ID to process
2469 const areaId = areaIds.shift();
2470
2471 // load the tasks for the current area/project ID
2472 vboActionLoadAreaTasks(areaId, from_date, to_date, room_ids, (areaProcessed) => {
2473 if (typeof onProgress === 'function') {
2474 // request completed, invoke the callback upon progressing to the next request to process
2475 onProgress(areaProcessed);
2476 }
2477
2478 // perform a recursive call for the next area/project ID to process, if any
2479 vboActionDispatchAreasLoading(areaIds, from_date, to_date, room_ids, onProgress);
2480 });
2481 };
2482
2483 /**
2484 * Register function to hide the tasks of a given area/project id.
2485 */
2486 const vboActionHideAreaTasks = (areaId) => {
2487 document
2488 .querySelectorAll('.vbo-tm-row[data-area-id="' + areaId + '"]')
2489 .forEach((row) => {
2490 row.remove();
2491 });
2492 };
2493
2494 /**
2495 * Register function to add event listeners for the tasks of a given area (edit task, new task, hover task).
2496 */
2497 const vboActionRegisterAreaTaskEvents = (areaId) => {
2498 // edit and hover existing tasks
2499 document
2500 .querySelectorAll('.vbo-tm-row-cell-task')
2501 .forEach((taskElement) => {
2502 if (taskElement.clickListener) {
2503 // listener added already
2504 return;
2505 }
2506
2507 // get the clicked task, area and booking IDs
2508 const taskId = taskElement.getAttribute('data-task-id');
2509 const areaId = taskElement.getAttribute('data-area-id');
2510 const taskBid = taskElement.getAttribute('data-task-bid');
2511
2512 // define the click event for editing the task
2513 taskElement.addEventListener('click', () => {
2514 // define the modal delete button
2515 let delete_btn = jQuery('<button></button>')
2516 .attr('type', 'button')
2517 .addClass('btn btn-danger')
2518 .text(<?php echo json_encode(JText::translate('VBELIMINA')); ?>)
2519 .on('click', function() {
2520 if (!confirm(<?php echo json_encode(JText::translate('VBDELCONFIRM')); ?>)) {
2521 return false;
2522 }
2523
2524 // disable button to prevent double submissions
2525 let submit_btn = jQuery(this);
2526 submit_btn.prop('disabled', true);
2527
2528 // start loading animation
2529 VBOCore.emitEvent('vbo-tm-edittask-loading');
2530
2531 // make the request
2532 VBOCore.doAjax(
2533 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=taskmanager.deleteTask'); ?>",
2534 {
2535 data: {
2536 id: taskId,
2537 },
2538 },
2539 (resp) => {
2540 // parse all context menu buttons to identify the current area/project id
2541 jQuery('.vbo-context-menu-overview-actions').vboContextMenu('buttons').forEach((btn) => {
2542 if (btn.areaId == areaId) {
2543 btn.activeState = false;
2544 // trigger action to reload the area/project tasks
2545 btn.action();
2546 }
2547 });
2548
2549 // dismiss the modal
2550 VBOCore.emitEvent('vbo-tm-edittask-dismiss');
2551 },
2552 (error) => {
2553 // display error message
2554 alert(error.responseText);
2555
2556 // re-enable submit button
2557 submit_btn.prop('disabled', false);
2558
2559 // stop loading
2560 VBOCore.emitEvent('vbo-tm-edittask-loading');
2561 }
2562 );
2563 });
2564
2565 // define the modal save button
2566 let save_btn = jQuery('<button></button>')
2567 .attr('type', 'button')
2568 .addClass('btn btn-success')
2569 .text(<?php echo json_encode(JText::translate('VBSAVE')); ?>)
2570 .on('click', function() {
2571 // disable button to prevent double submissions
2572 let submit_btn = jQuery(this);
2573 submit_btn.prop('disabled', true);
2574
2575 // start loading animation
2576 VBOCore.emitEvent('vbo-tm-edittask-loading');
2577
2578 // get form data
2579 const taskForm = new FormData(document.querySelector('#vbo-tm-task-manage-form'));
2580
2581 // build query parameters for the request
2582 let qpRequest = new URLSearchParams(taskForm);
2583
2584 // make sure the request always includes the assignees query parameter, even if the list is empty
2585 if (!qpRequest.has('data[assignees][]')) {
2586 qpRequest.append('data[assignees][]', []);
2587 }
2588
2589 // make sure the request always includes the tags query parameter, even if the list is empty
2590 if (!qpRequest.has('data[tags][]')) {
2591 qpRequest.append('data[tags][]', []);
2592 }
2593
2594 // make the request
2595 VBOCore.doAjax(
2596 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=taskmanager.updateTask'); ?>",
2597 qpRequest.toString(),
2598 (resp) => {
2599 // parse all context menu buttons to identify the current area/project id
2600 jQuery('.vbo-context-menu-overview-actions').vboContextMenu('buttons').forEach((btn) => {
2601 if (btn.areaId == areaId) {
2602 btn.activeState = false;
2603 // trigger action to reload the area/project tasks
2604 btn.action();
2605 }
2606 });
2607
2608 // dismiss the modal
2609 VBOCore.emitEvent('vbo-tm-edittask-dismiss');
2610 },
2611 (error) => {
2612 // display error message
2613 alert(error.responseText);
2614
2615 // re-enable submit button
2616 submit_btn.prop('disabled', false);
2617
2618 // stop loading
2619 VBOCore.emitEvent('vbo-tm-edittask-loading');
2620 }
2621 );
2622 });
2623
2624 // display modal
2625 let modalBody = VBOCore.displayModal({
2626 suffix: 'tm_edittask_modal',
2627 title: <?php echo json_encode(JText::translate('VBO_TASK')); ?> + ' #' + taskId,
2628 extra_class: 'vbo-modal-rounded vbo-modal-taller vbo-modal-large',
2629 body_prepend: true,
2630 lock_scroll: true,
2631 escape_dismiss: false,
2632 footer_left: delete_btn,
2633 footer_right: save_btn,
2634 loading_event: 'vbo-tm-edittask-loading',
2635 dismiss_event: 'vbo-tm-edittask-dismiss',
2636 });
2637
2638 // start loading animation
2639 VBOCore.emitEvent('vbo-tm-edittask-loading');
2640
2641 // make the request
2642 VBOCore.doAjax(
2643 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=taskmanager.renderLayout'); ?>",
2644 {
2645 type: 'tasks.managetask',
2646 data: {
2647 task_id: taskId,
2648 area_id: areaId,
2649 form_id: 'vbo-tm-task-manage-form',
2650 },
2651 },
2652 (resp) => {
2653 // stop loading
2654 VBOCore.emitEvent('vbo-tm-edittask-loading');
2655
2656 try {
2657 // decode the response (if needed), and append the content to the modal body
2658 let obj_res = typeof resp === 'string' ? JSON.parse(resp) : resp;
2659 modalBody.append(obj_res['html']);
2660 } catch (err) {
2661 console.error('Error decoding the response', err, resp);
2662 }
2663 },
2664 (error) => {
2665 // display error message
2666 alert(error.responseText);
2667
2668 // stop loading
2669 VBOCore.emitEvent('vbo-tm-edittask-loading');
2670 }
2671 );
2672 });
2673
2674 // define the mouseover/mouseout events for the current task
2675 if (taskBid) {
2676 // highlight the booking(s) assigned to the current task when hovered
2677 taskElement.addEventListener('mouseover', () => {
2678 // highlight all cells for this booking
2679 let bidSnakeElements = document.querySelectorAll('td.vbo-grid-avcell[data-bids*="-' + taskBid + '-"] > .vbo-tableaux-booking');
2680 bidSnakeElements.forEach((snake, index) => {
2681 if (snake.matches('.vbo-tableaux-booking-checkout')) {
2682 // this must be a check-out snake on the check-in date of the desired booking snake
2683 return;
2684 }
2685 // add the highlight class
2686 snake.classList.add('vbo-tableaux-booking-task-highlight');
2687 if (++index == bidSnakeElements.length && !snake.matches('.vbo-tableaux-booking-checkout')) {
2688 // look for the check-out snake for the same booking
2689 let parentCell = snake.closest('td').nextElementSibling;
2690 if (parentCell && parentCell.querySelector('.vbo-tableaux-booking-checkout')) {
2691 // add the highlight class also to the check-out snake
2692 parentCell
2693 .querySelector('.vbo-tableaux-booking-checkout')
2694 .classList
2695 .add('vbo-tableaux-booking-task-highlight');
2696 }
2697 }
2698 });
2699 });
2700
2701 // un-highlight the booking(s) assigned to the current task when mouse goes out
2702 taskElement.addEventListener('mouseout', () => {
2703 document
2704 .querySelectorAll('.vbo-tableaux-booking-task-highlight')
2705 .forEach((el) => {
2706 el.classList.remove('vbo-tableaux-booking-task-highlight');
2707 });
2708 });
2709 }
2710
2711 // turn flag on for listener set
2712 taskElement.clickListener = true;
2713 });
2714
2715 // create new tasks
2716 document
2717 .querySelectorAll('.vbo-tm-row[data-area-id="' + areaId + '"] .vbo-tm-row-cell-day')
2718 .forEach((tmCell) => {
2719 const listingId = tmCell.closest('tr').getAttribute('data-roomid');
2720 const day = tmCell.getAttribute('data-day');
2721
2722 tmCell.addEventListener('click', (e) => {
2723 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'))) {
2724 // an existing task for this cell was clicked (or the "see more" element), so we abort the process
2725 return;
2726 }
2727
2728 // define the modal cancel button
2729 let cancel_btn = jQuery('<button></button>')
2730 .attr('type', 'button')
2731 .addClass('btn')
2732 .text(<?php echo json_encode(JText::translate('VBANNULLA')); ?>)
2733 .on('click', () => {
2734 VBOCore.emitEvent('vbo-tm-newtask-dismiss');
2735 });
2736
2737 // define the modal save button
2738 let save_btn = jQuery('<button></button>')
2739 .attr('type', 'button')
2740 .addClass('btn btn-success')
2741 .text(<?php echo json_encode(JText::translate('VBSAVE')); ?>)
2742 .on('click', function() {
2743 // disable button to prevent double submissions
2744 let submit_btn = jQuery(this);
2745 submit_btn.prop('disabled', true);
2746
2747 // start loading animation
2748 VBOCore.emitEvent('vbo-tm-newtask-loading');
2749
2750 // get form data
2751 const taskForm = new FormData(document.querySelector('#vbo-tm-task-manage-form'));
2752
2753 // build query parameters for the request
2754 let qpRequest = new URLSearchParams(taskForm).toString();
2755
2756 // make the request
2757 VBOCore.doAjax(
2758 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=taskmanager.createTask'); ?>",
2759 qpRequest,
2760 (resp) => {
2761 // parse all context menu buttons to identify the current area/project id
2762 jQuery('.vbo-context-menu-overview-actions').vboContextMenu('buttons').forEach((btn) => {
2763 if (btn.areaId == areaId) {
2764 btn.activeState = false;
2765 // trigger action to reload the area/project tasks
2766 btn.action();
2767 }
2768 });
2769
2770 // dismiss the modal
2771 VBOCore.emitEvent('vbo-tm-newtask-dismiss');
2772 },
2773 (error) => {
2774 // display error message
2775 alert(error.responseText);
2776
2777 // re-enable submit button
2778 submit_btn.prop('disabled', false);
2779
2780 // stop loading
2781 VBOCore.emitEvent('vbo-tm-newtask-loading');
2782 }
2783 );
2784 });
2785
2786 // display modal
2787 let modalBody = VBOCore.displayModal({
2788 suffix: 'tm_newtask_modal',
2789 title: <?php echo json_encode(JText::translate('VBO_NEW_TASK')); ?>,
2790 extra_class: 'vbo-modal-rounded vbo-modal-taller vbo-modal-large',
2791 body_prepend: true,
2792 lock_scroll: true,
2793 escape_dismiss: false,
2794 footer_left: cancel_btn,
2795 footer_right: save_btn,
2796 loading_event: 'vbo-tm-newtask-loading',
2797 dismiss_event: 'vbo-tm-newtask-dismiss',
2798 });
2799
2800 // start loading animation
2801 VBOCore.emitEvent('vbo-tm-newtask-loading');
2802
2803 // make the request
2804 VBOCore.doAjax(
2805 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=taskmanager.renderLayout'); ?>",
2806 {
2807 type: 'tasks.managetask',
2808 data: {
2809 area_id: areaId,
2810 form_id: 'vbo-tm-task-manage-form',
2811 filters: {
2812 calendar_day: day,
2813 id_room: listingId,
2814 },
2815 },
2816 },
2817 (resp) => {
2818 // stop loading
2819 VBOCore.emitEvent('vbo-tm-newtask-loading');
2820
2821 try {
2822 // decode the response (if needed), and append the content to the modal body
2823 let obj_res = typeof resp === 'string' ? JSON.parse(resp) : resp;
2824 modalBody.append(obj_res['html']);
2825 } catch (err) {
2826 console.error('Error decoding the response', err, resp);
2827 }
2828 },
2829 (error) => {
2830 // display error message
2831 alert(error.responseText);
2832
2833 // stop loading
2834 VBOCore.emitEvent('vbo-tm-newtask-loading');
2835 }
2836 );
2837 });
2838 });
2839 };
2840
2841 jQuery(function() {
2842
2843 // all task areas
2844 const taskAreas = <?php echo json_encode($taskAreas); ?>;
2845
2846 // configure the currency object
2847 VBOCore.getCurrency({
2848 symbol: <?php echo json_encode($currencysymb) ?: '"$"'; ?>,
2849 position: <?php echo json_encode(VikBooking::getCurrencyPosition()); ?>,
2850 digits: <?php echo intval($currency_digits); ?>,
2851 decimals: <?php echo json_encode($currency_decimals) ?: '"."'; ?>,
2852 thousands: <?php echo json_encode($currency_thousands) ?: '","'; ?>,
2853 noDecimals: 1,
2854 });
2855
2856 // build context menu button text element
2857 let btnRoomRatesEl = document.createElement('span');
2858 btnRoomRatesEl.classList.add('vbo-ctxmenu-entry-icn');
2859 btnRoomRatesEl.innerHTML = '<span>' + <?php echo json_encode(JText::translate('VBO_RATES_AND_RESTR')); ?> + '</span>';
2860 let icnRoomRatesEl = document.createElement('i');
2861 icnRoomRatesEl.classList.add('vbo-save-pref-ota-rates');
2862 icnRoomRatesEl.classList.add(...(<?php echo json_encode(VikBookingIcons::i('thumbtack', 'vbo-deactivated-icon')); ?>.split(' ')));
2863 if (<?php echo $cookie->get('vboAovwLrr', 0, 'int') ? 'true' : 'false'; ?>) {
2864 icnRoomRatesEl.classList.add('vbo-activated-icon');
2865 }
2866 let icnRoomRatesWrapEl = document.createElement('span');
2867 icnRoomRatesWrapEl.classList.add('vbo-ctxmenu-entry-icn-secondary', 'vbo-tooltip', 'vbo-tooltip-top-left');
2868 icnRoomRatesWrapEl.setAttribute('data-tooltiptext', <?php echo json_encode(JText::translate('VBO_REMEMBER_PREF')); ?>);
2869 icnRoomRatesWrapEl.append(icnRoomRatesEl);
2870 btnRoomRatesEl.append(icnRoomRatesWrapEl);
2871
2872 // build buttons
2873 let btns = [
2874 {
2875 class: 'btngroup',
2876 text: <?php echo json_encode(JText::translate('VBMENUFARES')); ?>,
2877 disabled: true,
2878 },
2879 {
2880 activeState: <?php echo $cookie->get('vboAovwLrr', 0, 'int') ? 'true' : 'false'; ?>,
2881 pinnedState: <?php echo $cookie->get('vboAovwLrr', 0, 'int') ? 'true' : 'false'; ?>,
2882 separator: (taskAreas.length > 0),
2883 class: 'vbo-context-menu-entry-secondary',
2884 text: btnRoomRatesEl,
2885 icon: function() {
2886 return this.activeState ? '<?php echo VikBookingIcons::i('toggle-on', 'vbo-enabled-icon'); ?>' : '<?php echo VikBookingIcons::i('toggle-off'); ?>';
2887 },
2888 action: function(root, event) {
2889 if (event?.target && (event.target[0] || event.target).matches('.vbo-save-pref-ota-rates')) {
2890 // toggle pinned state
2891 this.pinnedState = !this.pinnedState;
2892
2893 // update cookie preference
2894 let nd = new Date();
2895 nd.setTime(nd.getTime() + (365*24*60*60*1000));
2896 document.cookie = "vboAovwLrr=" + (this.pinnedState ? 1 : 0) + "; expires=" + nd.toUTCString() + "; path=/; SameSite=Lax";
2897
2898 // update icon class
2899 icnRoomRatesEl.classList.toggle('vbo-deactivated-icon', !this.pinnedState);
2900 icnRoomRatesEl.classList.toggle('vbo-activated-icon', this.pinnedState);
2901
2902 // do not proceed
2903 return;
2904 }
2905
2906 // get all month tables
2907 let monthTables = document.querySelectorAll('table.vboverviewtable[data-month-from]');
2908 if (!monthTables.length) {
2909 return;
2910 }
2911
2912 // get the date bounds
2913 let from_date = monthTables[0].getAttribute('data-month-from');
2914 let to_date = monthTables[(monthTables.length - 1)].getAttribute('data-month-to');
2915
2916 // get all listing IDs from the first table
2917 let listingIds = [];
2918 monthTables[0]
2919 .querySelectorAll('.roomname[data-roomid]')
2920 .forEach((roomRowCell) => {
2921 if (roomRowCell.matches('.subroomname')) {
2922 // skip sub-unit rows
2923 return;
2924 }
2925 listingIds.push(roomRowCell.getAttribute('data-roomid'));
2926 });
2927
2928 // toggle state
2929 this.activeState = !this.activeState;
2930
2931 if (this.activeState) {
2932 // load room rates
2933 vboActionLoadRoomRates(from_date, to_date, listingIds);
2934 } else {
2935 // hide room rates
2936 vboActionHideRoomRates(true);
2937 }
2938 },
2939 disabled: () => {
2940 let ctx_elem = document
2941 .querySelector('.vbo-context-menu-overview-actions');
2942
2943 return <?php echo !$vbo_auth_pricing ? 'true' : 'false'; ?> || ctx_elem?.loading == 1;
2944 },
2945 },
2946 ];
2947
2948 if (taskAreas.length) {
2949 // push group button
2950 btns.push({
2951 class: 'btngroup',
2952 text: <?php echo json_encode(JText::translate('VBO_TASK_MANAGER')); ?>,
2953 disabled: true,
2954 });
2955
2956 taskAreas.forEach((area, index) => {
2957 // push area/project button
2958 btns.push({
2959 activeState: false,
2960 areaId: area.id,
2961 class: 'vbo-context-menu-entry-secondary',
2962 text: area.name,
2963 separator: false,
2964 icon: function() {
2965 return this.activeState === true ? '<?php echo VikBookingIcons::i('check-square'); ?>' : '<?php echo VikBookingIcons::i('far fa-square'); ?>';
2966 },
2967 action: function(root, event) {
2968 // get all month tables
2969 let monthTables = document.querySelectorAll('table.vboverviewtable[data-month-from]');
2970 if (!monthTables.length) {
2971 return;
2972 }
2973
2974 // get the date bounds
2975 let from_date = monthTables[0].getAttribute('data-month-from');
2976 let to_date = monthTables[(monthTables.length - 1)].getAttribute('data-month-to');
2977
2978 // get all listing IDs from the first table
2979 let listingIds = [];
2980 monthTables[0]
2981 .querySelectorAll('.roomname[data-roomid]')
2982 .forEach((roomRowCell) => {
2983 if (roomRowCell.matches('.subroomname')) {
2984 // skip sub-unit rows
2985 return;
2986 }
2987 listingIds.push(roomRowCell.getAttribute('data-roomid'));
2988 });
2989
2990 // toggle active state
2991 this.activeState = !this.activeState;
2992
2993 if (this.activeState) {
2994 // render tasks for the selected area
2995 vboActionLoadAreaTasks(this.areaId, from_date, to_date, listingIds);
2996 } else {
2997 // hide tasks for the selected area
2998 vboActionHideAreaTasks(this.areaId);
2999 }
3000 },
3001 disabled: () => {
3002 let ctx_elem = document
3003 .querySelector('.vbo-context-menu-overview-actions');
3004
3005 return <?php echo !$vbo_auth_pms ? 'true' : 'false'; ?> || ctx_elem?.loading == 1;
3006 },
3007 });
3008 });
3009 }
3010
3011 // start context menu on the proper actions button element
3012 jQuery('.vbo-context-menu-overview-actions').vboContextMenu({
3013 placement: 'bottom-left',
3014 buttons: btns,
3015 });
3016
3017 // check default state for actions context menu
3018 if (<?php echo $cookie->get('vboAovwLrr', 0, 'int') ? 'true' : 'false'; ?>) {
3019 // load room rates upon page loading according to cookie preferences
3020 setTimeout(() => {
3021 // access "load room rates" button-option
3022 let ctxRoomRatesBtn = jQuery('.vbo-context-menu-overview-actions').vboContextMenu('buttons')[1];
3023 // let the initial state be disabled
3024 ctxRoomRatesBtn.activeState = false;
3025 // invoke action callback to toggle the active state to true
3026 ctxRoomRatesBtn.action();
3027 }, 200);
3028 }
3029
3030 // start context menu on the button element to choose the new rates type
3031 const fixedRateLbl = <?php echo json_encode(JText::translate('VBO_SET_FIXED_RATE')); ?>;
3032 const adjustRatesLbl = <?php echo json_encode(JText::translate('VBO_INCR_DECR_RATES')); ?>;
3033 jQuery('.vbo-context-menu-rate-type').vboContextMenu({
3034 placement: 'bottom-center',
3035 buttons: [
3036 {
3037 activeState: true,
3038 rateType: 'fixed',
3039 class: 'vbo-context-menu-entry-secondary',
3040 text: fixedRateLbl,
3041 separator: false,
3042 icon: function() {
3043 return this.activeState === true ? '<?php echo VikBookingIcons::i('check-square'); ?>' : '<?php echo VikBookingIcons::i('far fa-square'); ?>';
3044 },
3045 action: function(root, event) {
3046 // make the active state property enabled
3047 this.activeState = true;
3048
3049 // update data attribute for the preference chosen
3050 document.querySelector('.vbo-roverw-newrwrap').setAttribute('data-rate-type', 'fixed');
3051
3052 // update element action title
3053 document.querySelector('.vbo-roverw-action-rates-title').textContent = fixedRateLbl;
3054
3055 // toggle visible element for defining the new rate
3056 document.querySelector('.vbo-roverw-newrcont[data-rate-type="fixed"]').style.display = '';
3057 document.querySelector('.vbo-roverw-newrcont[data-rate-type="addsub"]').style.display = 'none';
3058
3059 // parse all context menu buttons to set the proper active state
3060 jQuery('.vbo-context-menu-rate-type').vboContextMenu('buttons').forEach((btn) => {
3061 if (btn.rateType != 'fixed') {
3062 btn.activeState = false;
3063 }
3064 });
3065 },
3066 },
3067 {
3068 activeState: false,
3069 rateType: 'addsub',
3070 class: 'vbo-context-menu-entry-secondary',
3071 text: adjustRatesLbl,
3072 separator: false,
3073 icon: function() {
3074 return this.activeState === true ? '<?php echo VikBookingIcons::i('check-square'); ?>' : '<?php echo VikBookingIcons::i('far fa-square'); ?>';
3075 },
3076 action: function(root, event) {
3077 // make the active state property enabled
3078 this.activeState = true;
3079
3080 // update data attribute for the preference chosen
3081 document.querySelector('.vbo-roverw-newrwrap').setAttribute('data-rate-type', 'addsub');
3082
3083 // update element action title
3084 document.querySelector('.vbo-roverw-action-rates-title').textContent = adjustRatesLbl;
3085
3086 // toggle visible element for defining the new rate
3087 document.querySelector('.vbo-roverw-newrcont[data-rate-type="addsub"]').style.display = '';
3088 document.querySelector('.vbo-roverw-newrcont[data-rate-type="fixed"]').style.display = 'none';
3089
3090 // parse all context menu buttons to set the proper active state
3091 jQuery('.vbo-context-menu-rate-type').vboContextMenu('buttons').forEach((btn) => {
3092 if (btn.rateType != 'addsub') {
3093 btn.activeState = false;
3094 }
3095 });
3096 },
3097 }
3098 ],
3099 });
3100
3101 // register listener to reset the room-rate selection upon Esc keyup event
3102 window.addEventListener('keyup', (e) => {
3103 if (!e.key || e.key != 'Escape') {
3104 return;
3105 }
3106
3107 if (vboActionRoomRateData.start) {
3108 // reset selection
3109 vboActionRoomRateHandleReset();
3110 }
3111 });
3112
3113 // register listener for the "input" event on the "set new rate" input field of type number
3114 document.querySelector('#roverw-newrate').addEventListener('input', VBOCore.debounceEvent((e) => {
3115 // dispatch the event to calculate the new OTA pricing value
3116 VBOCore.emitEvent('vbo-roverv-setnewrate-calc-ota-pricing', {rate: e.target.value});
3117 }, 200));
3118
3119 // register listener for when a new rate is set to update what will be the OTA pricing value
3120 document.addEventListener('vbo-roverv-setnewrate-calc-ota-pricing', VBOCore.debounceEvent((e) => {
3121 if (!e || !e.detail || !e.detail.rate) {
3122 // invalid event data
3123 return;
3124 }
3125
3126 // get the new PMS rate
3127 let rate_amount = parseFloat(e.detail.rate);
3128
3129 // access the currency object
3130 let currencyObj = VBOCore.getCurrency();
3131
3132 // scan all OTA alteration rules, if any
3133 document.querySelectorAll('.vbo-roverw-setnewrate-ota-pricing-currentvalue[data-alteration]').forEach((elem) => {
3134 // channel alteration string
3135 let alter_string = elem.getAttribute('data-alteration');
3136 if (!alter_string) {
3137 alter_string = '+0%';
3138 }
3139
3140 // default alteration factors (no pricing alteration rules)
3141 let alter_op = alter_string.substr(0, 1);
3142 let alter_val = alter_string.substr(-1, 1);
3143 let alter_amount = parseFloat(alter_string.replace(alter_op, '').replace(alter_val, ''));
3144
3145 // calculate what the rate will be on the OTA
3146 let ota_rate_amount = rate_amount;
3147
3148 if (!isNaN(alter_amount) && Math.abs(alter_amount) > 0) {
3149 if (alter_op == '+') {
3150 // increase rate
3151 if (alter_val == '%') {
3152 // percent
3153 let amount_inc = currencyObj.multiply(alter_amount, 0.01);
3154 amount_inc = currencyObj.multiply(rate_amount, amount_inc);
3155 ota_rate_amount = currencyObj.sum(rate_amount, amount_inc);
3156 } else {
3157 // absolute
3158 ota_rate_amount = currencyObj.sum(rate_amount, alter_amount);
3159 }
3160 } else {
3161 // discount rate
3162 if (alter_val == '%') {
3163 // percent
3164 let amount_inc = currencyObj.multiply(alter_amount, 0.01);
3165 amount_inc = currencyObj.multiply(rate_amount, amount_inc);
3166 ota_rate_amount = currencyObj.diff(rate_amount, amount_inc);
3167 } else {
3168 // absolute
3169 ota_rate_amount = currencyObj.diff(rate_amount, alter_amount);
3170 }
3171 }
3172 }
3173
3174 // get the element containing the calculated ota pricing
3175 let will_alter_elem = elem
3176 .closest('.vbo-roverw-setnewrate-vcm-ota-pricing-startvalue')
3177 .querySelector('.vbo-roverw-setnewrate-ota-pricing-willvalue');
3178
3179 // define the currency options
3180 let ota_currency_options = {};
3181
3182 // check if the channel requires a specific currency
3183 let ota_currency_data = will_alter_elem.getAttribute('data-currency');
3184 if (ota_currency_data) {
3185 // decode currency data instructions
3186 try {
3187 ota_currency_data = JSON.parse(ota_currency_data);
3188 } catch (e) {
3189 ota_currency_data = {};
3190 }
3191
3192 // set custom currency options
3193 if (ota_currency_data['symbol']) {
3194 ota_currency_options['symbol'] = ota_currency_data['symbol'];
3195 }
3196 if (ota_currency_data['decimals']) {
3197 ota_currency_options['digits'] = ota_currency_data['decimals'];
3198 }
3199 if (ota_currency_data['decimals_sep']) {
3200 ota_currency_options['decimals'] = ota_currency_data['decimals_sep'];
3201 }
3202 if (ota_currency_data['thousands_sep']) {
3203 ota_currency_options['thousands'] = ota_currency_data['thousands_sep'];
3204 }
3205 }
3206
3207 // set calculated OTA rate value
3208 will_alter_elem.innerHTML = currencyObj.format(ota_rate_amount, ota_currency_options);
3209 });
3210 }, 200));
3211
3212 <?php
3213 // check if some area/project IDs should be rendered
3214 if ($activeAreas) {
3215 ?>
3216 // get all month tables to obtain the date bounds and the listing IDs
3217 let from_date, to_date;
3218 let listingIds = [];
3219 let activeAreas = <?php echo json_encode($activeAreas); ?>;
3220 let monthTables = document.querySelectorAll('table.vboverviewtable[data-month-from]');
3221
3222 if (monthTables.length) {
3223 // get the date bounds
3224 from_date = monthTables[0].getAttribute('data-month-from');
3225 to_date = monthTables[(monthTables.length - 1)].getAttribute('data-month-to');
3226 // get all listing IDs from the first table
3227 monthTables[0]
3228 .querySelectorAll('.roomname[data-roomid]')
3229 .forEach((roomRowCell) => {
3230 if (roomRowCell.matches('.subroomname')) {
3231 // skip sub-unit rows
3232 return;
3233 }
3234 listingIds.push(roomRowCell.getAttribute('data-roomid'));
3235 });
3236 }
3237
3238 if (listingIds.length) {
3239 // dispatch consequent requests, one for each active area ID
3240 vboActionDispatchAreasLoading(activeAreas, from_date, to_date, listingIds, (areaDisplayed) => {
3241 // parse all context menu buttons to identify the current area/project id
3242 jQuery('.vbo-context-menu-overview-actions').vboContextMenu('buttons').forEach((btn) => {
3243 if (btn.areaId == areaDisplayed) {
3244 // enable area/project active state
3245 btn.activeState = true;
3246 }
3247 });
3248 });
3249 }
3250 <?php
3251 }
3252 ?>
3253
3254 });
3255 </script>
3256