PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.16
VikBooking Hotel Booking Engine & PMS v1.8.16
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
← All changes | admin/views/overv/tmpl/default.php +1635 -224 1.6.4 → 1.8.16 View file →
@@ -1,10 +1,10 @@
1 1 <?php
2 2 /**
3 3 * @package VikBooking
4 4 * @subpackage com_vikbooking
5 - * @author Alessio Gaggii - e4j - Extensionsforjoomla.com
6 - * @copyright Copyright (C) 2018 e4j - Extensionsforjoomla.com. All rights reserved.
5 + * @author Alessio Gaggii - E4J srl
6 + * @copyright Copyright (C) 2025 E4J srl. All rights reserved.
7 7 * @license GNU General Public License version 2 or later; see LICENSE
8 8 * @link https://vikwp.com
9 9 */
10 10
@@ -16,13 +16,24 @@
16 16 $tsstart = $this->tsstart;
17 17 $lim0 = $this->lim0;
18 18 $navbut = $this->navbut;
19 19
20 +$app = JFactory::getApplication();
21 +$vbo_app = VikBooking::getVboApplication();
22 +
23 +// load context menu assets
24 +$vbo_app->loadContextMenuAssets();
25 +
26 +// prepare JS currency
27 +$vbo_app->prepareJavaScriptCurrency();
28 +
20 29 $vbo_auth_pricing = JFactory::getUser()->authorise('core.vbo.pricing', 'com_vikbooking');
21 -$vbo_app = VikBooking::getVboApplication();
30 +
22 31 JHtml::fetch('script', VBO_SITE_URI.'resources/jquery-ui.min.js');
23 32 JHtml::fetch('script', VBO_ADMIN_URI.'resources/js_upload/jquery.stickytableheaders.min.js');
24 33
34 +$file_base = VBOPlatformDetection::isWordPress() ? 'admin.php' : 'index.php';
35 +
25 36 // JS lang defs
26 37 JText::script('VBSAVE');
27 38 JText::script('VBPVIEWORDERSTHREE');
28 39 JText::script('VBEDITORDERTHREE');
@@ -52,8 +63,9 @@
52 63 JText::script('VBOFEATASSIGNUNIT');
53 64 JText::script('VBO_SPLIT_STAY');
54 65 JText::script('VBO_CONF_SWAP_RNUMB');
55 66 JText::script('VBO_ERR_SUBUN_MOVE_SUBUN');
67 +JText::script('VBO_BTYPE_OVERBOOKING');
56 68
57 69 $days_labels = [
58 70 JText::translate('VBSUN'),
59 71 JText::translate('VBMON'),
@@ -71,9 +83,9 @@
71 83 $df = 'm/d/Y';
72 84 } else {
73 85 $df = 'Y/m/d';
74 86 }
75 -$currencysymb = VikBooking::getCurrencySymb(true);
87 +$currencysymb = VikBooking::getCurrencySymb();
76 88
77 89 $pdebug = VikRequest::getInt('e4j_debug', '', 'request');
78 90 $session = JFactory::getSession();
79 91 $show_type = $session->get('vbUnitsShowType', '');
@@ -79,12 +91,14 @@
79 91 $show_type = $session->get('vbUnitsShowType', '');
80 92 $mnum = $session->get('vbOvwMnum', '1');
81 93 $mnum = intval($mnum);
82 94 $pcategory_id = $session->get('vbOvwCatid', 0);
83 -$cookie = JFactory::getApplication()->input->cookie;
95 +$cookie = $app->input->cookie;
84 96 $cookie_uleft = $cookie->get('vboAovwUleft', '', 'string');
85 97 $cookie_sticky_heads = $cookie->get('vboAovwStheads', 'off', 'string');
98 +$table_scroll_layout = $cookie->get('vboAovwScroll', (count($rows) > 3 ? 'inline' : 'tables'), 'string');
86 99 $colortags = VikBooking::loadBookingsColorTags();
100 +$long_list = (count($rows) * $mnum) > 10;
87 101
88 102 // View mode - Classic or Tags
89 103 $pbmode = $session->get('vbTagsMode', 'classic');
90 104 $tags_view_supported = false;
@@ -123,8 +137,30 @@
123 137 $clone_room['unit_index_str'] = $indexdata;
124 138 array_splice($rows, ($kr + 1 + $index_loop), 0, [$clone_room]);
125 139 $index_loop++;
126 140 }
141 +
142 + /**
143 + * Multi-unit room types with enough sub-units can display a "landing-unit-row" for
144 + * temporarily "parking" the bookings through drag & drop for an easier re-allocation.
145 + * Beside the holding area, we also add a row for the unassigned room bookings.
146 + *
147 + * @since 1.18.2 (J) - 1.8.2 (WP)
148 + * @since 1.18.3 (J) - 1.8.3 (WP) 2 sub-units are sufficient.
149 + */
150 + if ($room['units'] >= 2) {
151 + // set holding area row
152 + $clone_room['unit_index'] = 0;
153 + $clone_room['unit_index_str'] = JText::translate('VBO_HOLDING_AREA');
154 + array_splice($rows, ($kr + 1 + $index_loop), 0, [$clone_room]);
155 + $index_loop++;
156 +
157 + // set unassigned row
158 + $clone_room['unit_index'] = -1;
159 + $clone_room['unit_index_str'] = JText::translate('VBO_MISSING_SUBUNIT');
160 + array_splice($rows, ($kr + 1 + $index_loop), 0, [$clone_room]);
161 + $index_loop++;
162 + }
127 163 }
128 164 }
129 165 }
130 166 }
@@ -142,24 +178,31 @@
142 178 }
143 179 unset($arrbusy['tmplock']);
144 180 }
145 181
182 +/**
183 + * Global closing dates.
184 + *
185 + * @since 1.17.1 (J) - 1.7.1 (WP)
186 + */
187 +$globally_closed = VikBooking::getClosingDates();
188 +
146 189 ?>
147 190 <script type="text/Javascript">
148 191 function vboUnitsLeftOrBooked() {
149 - var set_to = jQuery('#uleftorbooked').val();
192 + let set_to = jQuery('#uleftorbooked').val();
150 193 jQuery('.vbo-overv-avcell[data-units-booked]').each(function() {
151 - var counter_el = jQuery(this).find('a');
194 + let counter_el = jQuery(this).find('a');
152 195 if (!counter_el.length) {
153 196 return;
154 197 }
155 198 counter_el.first().text(jQuery(this).attr('data-'+set_to));
156 199 });
157 - var nd = new Date();
200 + let nd = new Date();
158 201 nd.setTime(nd.getTime() + (365*24*60*60*1000));
159 202 document.cookie = "vboAovwUleft="+set_to+"; expires=" + nd.toUTCString() + "; path=/; SameSite=Lax";
160 203 }
161 -if (jQuery.isFunction(jQuery.fn.tooltip)) {
204 +if (typeof jQuery.fn.tooltip === 'function') {
162 205 jQuery(".hasTooltip").tooltip();
163 206 } else {
164 207 jQuery.fn.tooltip = function(){};
165 208 }
@@ -201,9 +244,9 @@
201 244 }
202 245
203 246 $stickyheaders_cmd_on = '';
204 247 $stickyheaders_cmd_off = '';
205 - if ((count($rows) * $mnum) > 10) {
248 + if ($long_list) {
206 249 if (VBOPlatformDetection::isJoomla()) {
207 250 /**
208 251 * @joomlaonly fixed offset selector is ".navbar"
209 252 */
@@ -218,72 +261,160 @@
218 261 }
219 262 ?>
220 263 <script type="text/javascript">
221 264 function vboToggleStickyTableHeaders(val) {
222 - var sticky_heads_cval = 'off';
265 + let sticky_heads_cval = 'off';
223 266 if (val > 0) {
224 267 sticky_heads_cval = 'on';
225 268 jQuery('table.vboverviewtable').addClass('vbo-overv-sticky-table-head-on');
226 269 jQuery('table.vboverviewtable').removeClass('vbo-overv-sticky-table-head-off');
227 270 <?php echo $stickyheaders_cmd_on; ?>
271 + // stop listening to the document scroll event to handle the fixed table heads
272 + document.removeEventListener('scroll', vboOvervHandleVScrollTableHeads);
273 + // scan all tables for listening to their horizontal scrolling event
274 + let tables = document.querySelectorAll('table.vboverviewtable.vbo-overv-sticky-table-head-off');
275 + tables.forEach((table) => {
276 + // stop listening to the table horizontal scroll events
277 + table.parentNode.removeEventListener('scroll', vboOvervHandleHScrollTableHead);
278 + });
228 279 } else {
229 280 jQuery('table.vboverviewtable').addClass('vbo-overv-sticky-table-head-off');
230 281 jQuery('table.vboverviewtable').removeClass('vbo-overv-sticky-table-head-on');
231 282 <?php echo $stickyheaders_cmd_off; ?>
283 + // listen to the document scroll event to handle the fixed table heads
284 + document.addEventListener('scroll', vboOvervHandleVScrollTableHeads);
285 + // scan all tables for listening to their horizontal scrolling event
286 + let tables = document.querySelectorAll('table.vboverviewtable.vbo-overv-sticky-table-head-off');
287 + tables.forEach((table) => {
288 + table.parentNode.addEventListener('scroll', vboOvervHandleHScrollTableHead);
289 + });
232 290 }
233 - var nd = new Date();
291 + let nd = new Date();
234 292 nd.setTime(nd.getTime() + (365*24*60*60*1000));
235 293 document.cookie = "vboAovwStheads=" + sticky_heads_cval + "; expires=" + nd.toUTCString() + "; path=/; SameSite=Lax";
236 294 }
237 295 </script>
238 296 <?php
239 - if ((count($rows) * $mnum) > 10) {
297 + if ($long_list || $tags_view_supported === true) {
298 + // group into an apposite context menu the layout and view types
240 299 ?>
300 + <script type="text/javascript">
301 + const vboOvervLayoutBtns = [];
302 + <?php
303 + if ($long_list) {
304 + // push buttons to choose between the scroll or table layout
305 + ?>
306 + vboOvervLayoutBtns.push({
307 + class: 'btngroup',
308 + text: <?php echo json_encode(JText::translate('VBO_LAYOUT')); ?>,
309 + disabled: true,
310 + });
311 + vboOvervLayoutBtns.push({
312 + class: 'vbo-context-menu-entry-secondary',
313 + text: <?php echo json_encode(JText::translate('VBO_INLINE_SCROLL')); ?>,
314 + icon: '<?php echo $cookie_sticky_heads == 'off' && $table_scroll_layout == 'inline' ? VikBookingIcons::i('check-square') : VikBookingIcons::i('far fa-square'); ?>',
315 + action: (root, event) => {
316 + let nd = new Date();
317 + nd.setTime(nd.getTime() + (365*24*60*60*1000));
318 + document.cookie = "vboAovwScroll=inline; expires=" + nd.toUTCString() + "; path=/; SameSite=Lax";
319 + vboToggleStickyTableHeaders(0);
320 + document.vboverview.submit();
321 + },
322 + });
323 + vboOvervLayoutBtns.push({
324 + class: 'vbo-context-menu-entry-secondary',
325 + text: <?php echo json_encode(JText::translate('VBO_SCROLL')); ?>,
326 + icon: '<?php echo $cookie_sticky_heads == 'off' && $table_scroll_layout != 'inline' ? VikBookingIcons::i('check-square') : VikBookingIcons::i('far fa-square'); ?>',
327 + action: (root, event) => {
328 + let nd = new Date();
329 + nd.setTime(nd.getTime() + (365*24*60*60*1000));
330 + document.cookie = "vboAovwScroll=tables; expires=" + nd.toUTCString() + "; path=/; SameSite=Lax";
331 + vboToggleStickyTableHeaders(0);
332 + document.vboverview.submit();
333 + },
334 + });
335 + vboOvervLayoutBtns.push({
336 + class: 'vbo-context-menu-entry-secondary',
337 + text: <?php echo json_encode(JText::translate('VBO_TABLE')); ?>,
338 + icon: '<?php echo empty($cookie_sticky_heads) || $cookie_sticky_heads == 'on' ? VikBookingIcons::i('check-square') : VikBookingIcons::i('far fa-square'); ?>',
339 + action: (root, event) => {
340 + vboToggleStickyTableHeaders(1);
341 + document.vboverview.submit();
342 + },
343 + separator: <?php echo $tags_view_supported === true ? 'true' : 'false'; ?>,
344 + });
345 + <?php
346 + }
347 + if ($tags_view_supported === true) {
348 + // push buttons to choose between classic or tags view
349 + ?>
350 + vboOvervLayoutBtns.push({
351 + class: 'btngroup',
352 + text: <?php echo json_encode(JText::translate('VBMENUTHREE')); ?>,
353 + disabled: true,
354 + });
355 + vboOvervLayoutBtns.push({
356 + class: 'vbo-context-menu-entry-secondary',
357 + text: <?php echo json_encode(JText::translate('VBOAVOVWBMODECLASSIC')); ?>,
358 + icon: '<?php echo $pbmode != 'tags' ? VikBookingIcons::i('check-square') : VikBookingIcons::i('far fa-square'); ?>',
359 + action: (root, event) => {
360 + let inp = document.createElement('input');
361 + inp.setAttribute('type', 'hidden');
362 + inp.setAttribute('name', 'bmode');
363 + inp.value = 'classic';
364 + document.vboverview.append(inp);
365 + document.vboverview.submit();
366 + },
367 + });
368 + vboOvervLayoutBtns.push({
369 + class: 'vbo-context-menu-entry-secondary',
370 + text: <?php echo json_encode(JText::translate('VBOAVOVWBMODETAGS')); ?>,
371 + icon: '<?php echo $pbmode == 'tags' ? VikBookingIcons::i('check-square') : VikBookingIcons::i('far fa-square'); ?>',
372 + action: (root, event) => {
373 + let inp = document.createElement('input');
374 + inp.setAttribute('type', 'hidden');
375 + inp.setAttribute('name', 'bmode');
376 + inp.value = 'tags';
377 + document.vboverview.append(inp);
378 + document.vboverview.submit();
379 + },
380 + });
381 + <?php
382 + }
383 + ?>
384 +
385 + jQuery(function() {
386 +
387 + // define the context menu for the actions button
388 + jQuery('.vbo-context-menu-overview-layout').vboContextMenu({
389 + placement: 'bottom-left',
390 + buttons: vboOvervLayoutBtns,
391 + });
392 +
393 + });
394 + </script>
395 +
241 396 <div class="btn-group pull-left">
242 - <select name="stickytheads" onchange="vboToggleStickyTableHeaders(this.value);">
243 - <option value="1"<?php echo empty($cookie_sticky_heads) || $cookie_sticky_heads == 'on' ? ' selected="selected"' : ''; ?>><?php echo JText::translate('VBOVERVIEWSTICKYTHEADON'); ?></option>
244 - <option value="0"<?php echo $cookie_sticky_heads == 'off' ? ' selected="selected"' : ''; ?>><?php echo JText::translate('VBOVERVIEWSTICKYTHEADOFF'); ?></option>
245 - </select>
397 + <button type="button" class="btn vbo-context-menu-btn vbo-context-menu-btn-raw vbo-context-menu-overview-layout">
398 + <span class="vbo-context-menu-lbl"><?php echo JText::translate('VBO_LAYOUT'); ?></span>
399 + <span class="vbo-context-menu-ico"><?php VikBookingIcons::e('sort-down'); ?></span>
400 + </button>
246 401 </div>
247 402 <?php
248 403 }
249 404
250 - if ($tags_view_supported === true) {
251 405 ?>
252 406 <div class="btn-group pull-left">
253 - <div class="vbo-overview-tagsorclassic">
254 - <select name="bmode" onchange="document.vboverview.submit();">
255 - <option value="classic"><?php echo JText::translate('VBOAVOVWBMODECLASSIC'); ?></option>
256 - <option value="tags"<?php echo $pbmode == 'tags' ? ' selected="selected"' : ''; ?>><?php echo JText::translate('VBOAVOVWBMODETAGS'); ?></option>
257 - </select>
258 - </div>
259 407 <?php
260 - if ($pbmode == 'tags' && count($colortags) > 0) {
261 - ?>
262 - <div class="vbo-overview-tagslegend">
263 - <span class="vbo-overview-tagslegend-lbl"><?php echo JText::translate('VBOAVOVWBMODETAGSLBL'); ?></span>
264 - <?php
265 - foreach ($colortags as $ctagk => $ctagv) {
266 - ?>
267 - <div class="vbo-overview-legend-tag">
268 - <div class="vbo-colortag-circle hasTooltip" style="background-color: <?php echo $ctagv['color']; ?>;" title="<?php echo JText::translate($ctagv['name']); ?>"></div>
269 - </div>
270 - <?php
271 - }
272 - ?>
273 - </div>
274 - <?php
275 - }
408 + // display overview context-menu
409 + echo JLayoutHelper::render('overview.actions', ['caller' => 'overv', 'rooms' => $rows]);
276 410 ?>
277 411 </div>
278 - <?php
279 - }
280 - ?>
281 412 <div class="btn-group pull-right">
282 413 <select name="units_show_type" id="uleftorbooked" onchange="vboUnitsLeftOrBooked();"><option value="units-booked"<?php echo (!empty($cookie_uleft) && $cookie_uleft == 'units-booked' ? ' selected="selected"' : ''); ?>><?php echo JText::translate('VBOVERVIEWUBOOKEDFILT'); ?></option><option value="units-left"<?php echo $show_type == 'units-left' || (!empty($cookie_uleft) && $cookie_uleft == 'units-left') ? ' selected="selected"' : ''; ?>><?php echo JText::translate('VBOVERVIEWULEFTFILT'); ?></option></select>
283 414 </div>
284 415 <div class="btn-group pull-right vbo-avov-legend">
285 - <span class="vbo-overview-legend-init"><?php echo JText::translate('VBOVERVIEWLEGEND'); ?></span>
416 + <!-- <span class="vbo-overview-legend-init"><?php echo JText::translate('VBOVERVIEWLEGEND'); ?></span> -->
286 417 <div class="vbo-overview-legend-red">
287 418 <span class="vbo-overview-legend-box">&nbsp;</span>
288 419 <span class="vbo-overview-legend-title"><?php echo JText::translate('VBOVERVIEWLEGRED'); ?></span>
289 420 </div>
@@ -300,32 +431,84 @@
300 431 <span class="vbo-overview-legend-title"><?php echo JText::translate('VBOVERVIEWLEGDND'); ?></span>
301 432 </div>
302 433 </div>
303 434 </div>
435 +<?php
436 +// propagate the current task manager filters
437 +foreach ((array) $app->input->get('tmfilters', [], 'array') as $tm_filter_key => $tm_filter_val) {
438 + if (is_scalar($tm_filter_val)) {
439 + ?>
440 + <input type="hidden" name="tmfilters[<?php echo $tm_filter_key; ?>]" value="<?php echo JHtml::fetch('esc_attr', $tm_filter_val); ?>" />
441 + <?php
442 + } elseif (is_array($tm_filter_val) && $tm_filter_val && array_keys($tm_filter_val) == range(0, count($tm_filter_val) - 1)) {
443 + // add support for linear arrays
444 + foreach ($tm_filter_val as $tm_filter_val_sub) {
445 + ?>
446 + <input type="hidden" name="tmfilters[<?php echo $tm_filter_key; ?>][]" value="<?php echo JHtml::fetch('esc_attr', $tm_filter_val_sub); ?>" />
447 + <?php
448 + }
449 + }
450 +}
451 +?>
304 452 </form>
305 453
306 454 <?php
307 455 $todayymd = date('Y-m-d');
456 +$last_displayed_day_ts = strtotime(date('Y-m-t 23:59:59', strtotime(sprintf('+%d %s', $mnum - 1, (($mnum - 1) === 1 ? 'month' : 'months')), $tsstart)));
457 +$last_displayed_day_info = getdate($last_displayed_day_ts);
308 458 $nowts = getdate($tsstart);
309 459 $curts = $nowts;
310 -for ($mind = 1; $mind <= $mnum; $mind++) {
460 +for ($mind = 1; $mind <= ($table_scroll_layout === 'inline' ? 1 : $mnum); $mind++) {
311 461 $monthname = VikBooking::sayMonth($curts['mon']);
462 + $month_names_cache = [
463 + $curts['mon'] => $monthname,
464 + ];
465 + $displayMonthLabel = sprintf('%s %d', $monthname, $curts['year']);
466 + if ($table_scroll_layout === 'inline') {
467 + $startMonthName = VikBooking::sayMonth($nowts['mon'], true);
468 + $endMonthName = VikBooking::sayMonth($last_displayed_day_info['mon'], true);
469 + if ($nowts['year'] != $last_displayed_day_info['year']) {
470 + $displayMonthLabel = sprintf('%s %d - %s %d', $startMonthName, $nowts['year'], $endMonthName, $last_displayed_day_info['year']);
471 + } else {
472 + if ($mnum > 1) {
473 + $displayMonthLabel = sprintf('%s - %s %d', $startMonthName, $endMonthName, $last_displayed_day_info['year']);
474 + } else {
475 + $displayMonthLabel = sprintf('%s %d', $monthname, $last_displayed_day_info['year']);
476 + }
477 + }
478 + }
312 479 ?>
313 480 <div class="vbo-overv-montable-wrap">
314 481 <div class="vbo-table-responsive">
315 - <table class="vboverviewtable vbo-roverview-table vbo-table <?php echo $cookie_sticky_heads == 'off' ? 'vbo-overv-sticky-table-head-off' : 'vbo-overv-sticky-table-head-on'; ?>">
482 + <table class="vboverviewtable vbo-roverview-table vbo-table <?php echo $cookie_sticky_heads == 'off' ? 'vbo-overv-sticky-table-head-off' : 'vbo-overv-sticky-table-head-on'; ?>" data-table-index="<?php echo ($mind - 1); ?>" data-month-from="<?php echo date('Y-m-01', $curts[0]); ?>" data-month-to="<?php echo $table_scroll_layout === 'inline' ? date('Y-m-d', $last_displayed_day_ts) : date('Y-m-t', $curts[0]); ?>">
316 483 <thead>
317 484 <tr class="vboverviewtablerowone">
318 - <th class="bluedays skip-bluedays-click vbo-overview-month"><?php echo $monthname . " " . $curts['year']; ?></th>
485 + <th class="bluedays skip-bluedays-click vbo-overview-month" data-initial-period="<?php echo JHtml::fetch('esc_attr', $displayMonthLabel); ?>"><?php echo $displayMonthLabel; ?></th>
319 486 <?php
320 487 $moncurts = $curts;
321 488 $mon = $moncurts['mon'];
322 - while ($moncurts['mon'] == $mon) {
489 + // loop until the end of the range of dates, or until the end of the current month
490 + while (($table_scroll_layout === 'inline' && $moncurts[0] < $last_displayed_day_ts) || ($table_scroll_layout !== 'inline' && $moncurts['mon'] == $mon)) {
323 491 $curdayymd = date('Y-m-d', $moncurts[0]);
324 - $read_day = $days_labels[$moncurts['wday']] . ' ' . $moncurts['mday'] . ' ' . $monthname . ' ' . $curts['year'];
492 + if (!($month_names_cache[$moncurts['mon']] ?? null)) {
493 + $month_names_cache[$moncurts['mon']] = VikBooking::sayMonth($moncurts['mon']);
494 + }
495 + $read_day = $days_labels[$moncurts['wday']] . ' ' . $moncurts['mday'] . ' ' . $month_names_cache[$moncurts['mon']] . ' ' . $curts['year'];
496 + $displayDayLabel = $days_labels[$moncurts['wday']];
497 + $headCellClasses = [
498 + 'bluedays',
499 + ($todayymd == $curdayymd ? 'vbo-overv-todaycell' : null),
500 + (isset($this->festivities[$curdayymd]) ? 'vbo-overv-festcell' : null),
501 + ];
502 + if ($moncurts['mday'] == 1 && $table_scroll_layout === 'inline') {
503 + $displayDayLabel = sprintf('%s, %s', VikBooking::sayMonth($moncurts['mon'], true), $days_labels[$moncurts['wday']]);
504 + if ($nowts['mon'] != $moncurts['mon']) {
505 + $headCellClasses[] = 'vbo-overv-cell-newmonth';
506 + }
507 + }
325 508 ?>
326 - <th class="bluedays<?php echo ($todayymd == $curdayymd ? ' vbo-overv-todaycell' : '') . (isset($this->festivities[$curdayymd]) ? ' vbo-overv-festcell' : ''); ?>" data-ymd="<?php echo $curdayymd; ?>" data-readymd="<?php echo $read_day; ?>">
327 - <span class="vbo-overw-tablewday"><?php echo $days_labels[$moncurts['wday']]; ?></span>
509 + <th class="<?php echo implode(' ', array_filter($headCellClasses)); ?>" data-ymd="<?php echo $curdayymd; ?>" data-readymd="<?php echo $read_day; ?>">
510 + <span class="vbo-overw-tablewday"><?php echo $displayDayLabel; ?></span>
328 511 <span class="vbo-overw-tablemday"><?php echo $moncurts['mday']; ?></span>
329 512 </th>
330 513 <?php
331 514 // parse the next day
@@ -340,22 +523,41 @@
340 523 $moncurts = $curts;
341 524 $mon = $moncurts['mon'];
342 525 $room_tags_view = $pbmode == 'tags' && $tags_view_supported === true && $room['units'] <= 1 ? true : false;
343 526 $is_subunit = array_key_exists('unit_index', $room);
527 + $is_subunit_tmp_row = $is_subunit && empty($room['unit_index']);
528 + $is_subunit_unassigned_row = $is_subunit && ($room['unit_index'] ?? 0) == -1;
344 529
530 + // build the list of classes for the current room-row
531 + $row_classes = [
532 + 'vboverviewtablerow',
533 + ($is_subunit ? 'vboverviewtablerow-subunit' : ''),
534 + ($is_subunit_tmp_row ? 'vboverviewtablerow-subunit-tmp' : ''),
535 + ($is_subunit_unassigned_row ? 'vboverviewtablerow-subunit-unassigned' : ''),
536 + (($is_subunit || $room['units'] == 1) && $cookie_sticky_heads == 'off' ? 'vbo-overv-row-snake' : ''),
537 + ];
538 +
539 + // build the main cell icon identifier
540 + $main_cell_icn_id = 'bed';
541 + if ($is_subunit_tmp_row) {
542 + $main_cell_icn_id = 'parking';
543 + } elseif ($is_subunit_unassigned_row) {
544 + $main_cell_icn_id = 'exclamation-triangle';
545 + }
546 +
345 547 ?>
346 - <tr class="vboverviewtablerow<?php echo $is_subunit ? ' vboverviewtablerow-subunit' : ''; ?><?php echo ($is_subunit || $room['units'] == 1) && $cookie_sticky_heads == 'off' ? ' vbo-overv-row-snake' : ''; ?>"<?php echo $is_subunit ? ' data-subroomid="' . $room['id'] . '-' . $room['unit_index'] . '"' : ''; ?>>
548 + <tr class="<?php echo implode(' ', array_filter($row_classes)); ?>"<?php echo !$is_subunit ? ' data-roomid="' . $room['id'] . '"' : ''; ?><?php echo $is_subunit ? ' data-subroomid="' . $room['id'] . '-' . $room['unit_index'] . '"' : ''; ?><?php echo $is_subunit_tmp_row ? ' style="display: none;"' : ''; ?>>
347 549 <?php
348 550 if ($is_subunit) {
349 551 ?>
350 552 <td class="roomname subroomname" data-roomid="<?php echo '-' . $room['id']; ?>">
351 - <span class="vbo-overview-subroomunits"><?php VikBookingIcons::e('bed'); ?></span>
553 + <span class="vbo-overview-subroomunits"><?php VikBookingIcons::e($main_cell_icn_id); ?></span>
352 554 <span class="vbo-overview-subroomname"><?php echo $room['unit_index_str']; ?></span>
353 555 </td>
354 556 <?php
355 557 } else {
356 558 ?>
357 - <td class="roomname" data-roomid="<?php echo $room['id']; ?>">
559 + <td class="roomname" data-roomid="<?php echo $room['id']; ?>" data-units="<?php echo $room['units']; ?>">
358 560 <span class="vbo-overview-room-info">
359 561 <span class="vbo-overview-roomunits"><?php echo $room['units']; ?></span>
360 562 <span class="vbo-overview-roomname"><?php echo $room['name']; ?></span>
361 563 <?php
@@ -369,13 +571,14 @@
369 571 </td>
370 572 <?php
371 573 }
372 574
373 - // loop through every day of the month
575 + // loop until the end of the range of dates, or until the end of the current month
374 576 $room_bids_pool = [];
375 577 $room_bookings = [];
376 - while ($moncurts['mon'] == $mon) {
377 - $dclass = 'vbo-overv-avcell ' . (!$is_subunit ? "notbusy" : "subnotbusy");
578 + while (($table_scroll_layout === 'inline' && $moncurts[0] < $last_displayed_day_ts) || ($table_scroll_layout !== 'inline' && $moncurts['mon'] == $mon)) {
579 + // init cell values
580 + $dclass = 'vbo-grid-avcell vbo-overv-avcell ' . (!$is_subunit ? "notbusy" : "subnotbusy");
378 581 $is_checkin = false;
379 582 $is_sharedcal = false;
380 583 $is_closure = false;
381 584 $lastbidcheckout = null;
@@ -384,8 +587,17 @@
384 587 $bids_pool = [];
385 588 $totfound = 0;
386 589 $prev_day_key = date('Y-m-d', strtotime('-1 day', $moncurts[0]));
387 590 $cur_day_key = date('Y-m-d', $moncurts[0]);
591 +
592 + // check for global closing dates
593 + foreach ($globally_closed as $glob_closed) {
594 + if ($moncurts[0] >= $glob_closed['from'] && $moncurts[0] <= $glob_closed['to']) {
595 + $dclass .= ' vbo-overv-globally-closed';
596 + }
597 + }
598 +
599 + // check availability
388 600 if (!empty($arrbusy[$room['id']]) && !$is_subunit) {
389 601 foreach ($arrbusy[$room['id']] as $b) {
390 602 $tmpone = getdate($b['checkin']);
391 603 $ritts = mktime(0, 0, 0, $tmpone['mon'], $tmpone['mday'], $tmpone['year']);
@@ -394,9 +606,9 @@
394 606 if (!($moncurts[0] >= $ritts && $moncurts[0] < $conts)) {
395 607 // booking does not involve the current day
396 608 continue;
397 609 }
398 - $dclass = "vbo-overv-avcell busy";
610 + $dclass = "vbo-grid-avcell vbo-overv-avcell busy";
399 611 $bid = $b['idorder'];
400 612 $is_sharedcal = !empty($b['sharedcal']) ? true : $is_sharedcal;
401 613 $is_closure = !empty($b['closure']) ? true : $is_closure;
402 614 $bid_str = '-' . $bid . '-';
@@ -421,9 +633,10 @@
421 633 if ($moncurts[0] == $ritts) {
422 634 $dalt = JText::translate('VBPICKUPAT')." ".date('H:i', $b['checkin']);
423 635 $is_checkin = true;
424 636 $lastbidcheckout = $b['checkout'];
425 - $bids_checkins[$bid] = $cur_day_key;
637 + $bids_checkins[$bid] = $bids_checkins[$bid] ?? [];
638 + $bids_checkins[$bid][] = $cur_day_key;
426 639 } elseif ($moncurts[0] == $conts) {
427 640 $dalt = JText::translate('VBRELEASEAT')." ".date('H:i', $b['checkout']);
428 641 }
429 642 $totfound++;
@@ -486,11 +699,11 @@
486 699 $astyle = '';
487 700 if ($room_tags_view === true && $totfound > 0) {
488 701 $last_bid = intval(str_replace('-', '', $bids_pool[(count($bids_pool) - 1)]));
489 702 $binfo = VikBooking::getBookingInfoFromID($last_bid);
490 - if (count($binfo) > 0) {
703 + if ($binfo) {
491 704 $bcolortag = VikBooking::applyBookingColorTag($binfo);
492 - if (count($bcolortag) > 0) {
705 + if ($bcolortag) {
493 706 $bcolortag['name'] = JText::translate($bcolortag['name']);
494 707 $dstyle = " style=\"background-color: ".$bcolortag['color']."; color: ".(array_key_exists('fontcolor', $bcolortag) ? $bcolortag['fontcolor'] : '#ffffff').";\" data-lastbid=\"".$last_bid."\"";
495 708 $astyle = " style=\"color: ".(array_key_exists('fontcolor', $bcolortag) ? $bcolortag['fontcolor'] : '#ffffff').";\"";
496 709 $dclass .= ' vbo-hascolortag';
@@ -496,16 +709,38 @@
496 709 $dclass .= ' vbo-hascolortag';
497 710 }
498 711 }
499 712 }
500 - if ($is_subunit && isset($rooms_features_bookings[$room['id']]) && isset($rooms_bids_pools[$room['id']][$cur_day_key]) && isset($rooms_features_bookings[$room['id']][$room['unit_index']])) {
713 + if ($is_subunit && isset($rooms_bids_pools[$room['id']][$cur_day_key]) && isset($rooms_features_bookings[$room['id']][$room['unit_index']]) && isset($room_bookings_pool[$room['id']][$cur_day_key][$room['unit_index']])) {
501 714 foreach ($rooms_bids_pools[$room['id']][$cur_day_key] as $bid) {
502 715 $bid = intval(str_replace('-', '', $bid));
503 716 if (in_array($bid, $rooms_features_bookings[$room['id']][$room['unit_index']])) {
504 717 $room['units'] = 1;
505 718 $totfound = 1;
506 - $dclass = "vbo-overv-avcell subroom-busy";
507 - $is_checkin = isset($bids_checkins[$bid]) && $bids_checkins[$bid] == $cur_day_key ? true : $is_checkin;
719 + $dclass = "vbo-grid-avcell vbo-overv-avcell subroom-busy";
720 + $is_checkin = isset($bids_checkins[$bid]) && in_array($cur_day_key, $bids_checkins[$bid]) ? true : $is_checkin;
721 + if (count($room_bookings_pool[$room['id']][$cur_day_key]) > 1 && !empty($room_bookings_pool[$room['id']][$cur_day_key][$room['unit_index']]['checkin'])) {
722 + // ensure multi-room bookings display the proper check-in day in case of different nights of stay
723 + $is_checkin = date('Y-m-d', $room_bookings_pool[$room['id']][$cur_day_key][$room['unit_index']]['checkin']) == $cur_day_key;
724 + }
725 +
726 + /**
727 + * In case of mixed room-types (hotels inventory and listings), we allow
728 + * the tags to be applied on sub-units for multi-unit room types.
729 + *
730 + * @since 1.17.5 (J) - 1.7.5 (WP)
731 + */
732 + if ($tags_view_supported && $binfo = VikBooking::getBookingInfoFromID($bid)) {
733 + $bcolortag = VikBooking::applyBookingColorTag($binfo);
734 + if ($bcolortag) {
735 + $bcolortag['name'] = JText::translate($bcolortag['name']);
736 + $dstyle = " style=\"background-color: ".$bcolortag['color']."; color: ".(array_key_exists('fontcolor', $bcolortag) ? $bcolortag['fontcolor'] : '#ffffff').";\" data-lastbid=\"".$bid."\"";
737 + $astyle = " style=\"color: ".(array_key_exists('fontcolor', $bcolortag) ? $bcolortag['fontcolor'] : '#ffffff').";\"";
738 + $dclass .= ' vbo-hascolortag';
739 + }
740 + }
741 +
742 + // abort
508 743 break;
509 744 }
510 745 }
511 746 }
@@ -534,14 +769,34 @@
534 769 $dclass .= ' vbo-roomdaynote-empty';
535 770 $rdaynote_icn = 'far fa-sticky-note';
536 771 }
537 772 $critical_note = '<span class="vbo-roomdaynote-trigger" data-roomday="' . $rdaynote_keyid . '"><i class="' . VikBookingIcons::i($rdaynote_icn, 'vbo-roomdaynote-display') . '"></i></span>';
773 + if ($is_subunit_tmp_row || $is_subunit_unassigned_row) {
774 + $critical_note = '';
775 + }
538 776
777 + /**
778 + * Closures overlapping one real reservation for a single-unit room should still
779 + * allow the display of the booking snake if it's just one real reservation.
780 + */
781 + $closure_on_real_res = false;
782 + if ($totfound === 2 && !$is_subunit && $room['units'] == 1 && $cookie_sticky_heads == 'off' && strpos($dclass, "busy-closure") !== false) {
783 + if (isset($room_bookings_pool[$room['id']]) && isset($room_bookings_pool[$room['id']][$cur_day_key]) && $room_bookings_pool[$room['id']][$cur_day_key]) {
784 + if (isset($room_bookings_pool[$room['id']][$cur_day_key][0]) && !$room_bookings_pool[$room['id']][$cur_day_key][0]['closure']) {
785 + // found a real booking on a date with at least one closure
786 + $closure_on_real_res = true;
787 + }
788 + }
789 + }
790 +
539 791 // prepare cell content
540 - if ($totfound === 1) {
792 + $day_has_closure = false;
793 + if ($totfound === 1 || $closure_on_real_res) {
794 + // start booking snake content
541 795 $day_booking_snake = '';
796 + // collect the current day booking data
542 797 $day_booking_data = [];
543 - if (isset($room_bookings_pool[$room['id']]) && isset($room_bookings_pool[$room['id']][$cur_day_key]) && $room_bookings_pool[$room['id']][$cur_day_key]) {
798 + if (isset($room_bookings_pool[$room['id']][$cur_day_key]) && $room_bookings_pool[$room['id']][$cur_day_key]) {
544 799 if ($is_subunit && isset($room_bookings_pool[$room['id']][$cur_day_key][$room['unit_index']])) {
545 800 $day_booking_data = $room_bookings_pool[$room['id']][$cur_day_key][$room['unit_index']];
546 801 } elseif (!$is_subunit && isset($room_bookings_pool[$room['id']][$cur_day_key][0])) {
547 802 $day_booking_data = $room_bookings_pool[$room['id']][$cur_day_key][0];
@@ -546,9 +801,11 @@
546 801 } elseif (!$is_subunit && isset($room_bookings_pool[$room['id']][$cur_day_key][0])) {
547 802 $day_booking_data = $room_bookings_pool[$room['id']][$cur_day_key][0];
548 803 }
549 804 }
550 - if (isset($room_bookings_pool[$room['id']]) && isset($room_bookings_pool[$room['id']][$prev_day_key]) && $room_bookings_pool[$room['id']][$prev_day_key]) {
805 +
806 + // whether we need to prepend a check-out snake for the last reservation
807 + if (isset($room_bookings_pool[$room['id']][$prev_day_key]) && $room_bookings_pool[$room['id']][$prev_day_key]) {
551 808 if ($is_subunit && isset($room_bookings_pool[$room['id']][$prev_day_key][$room['unit_index']])) {
552 809 if (date('Y-m-d', $room_bookings_pool[$room['id']][$prev_day_key][$room['unit_index']]['checkout']) == $cur_day_key) {
553 810 // prepend checkout snake
554 811 $day_booking_snake .= '<div class="vbo-tableaux-booking vbo-tableaux-booking-singleunit vbo-tableaux-booking-checkout"><span>&nbsp;</span></div>';
@@ -559,8 +816,12 @@
559 816 $day_booking_snake .= '<div class="vbo-tableaux-booking vbo-tableaux-booking-singleunit vbo-tableaux-booking-checkout"><span>&nbsp;</span></div>';
560 817 }
561 818 }
562 819 }
820 +
821 + // update if the current day is actually a closure
822 + $day_has_closure = $day_has_closure || !empty($day_booking_data['closure']);
823 +
563 824 if ($room['units'] == 1 && $day_booking_data && !$day_booking_data['closure']) {
564 825 // build tableaux-style snake container for guest
565 826 $customer_descr = '';
566 827 if ($is_checkin) {
@@ -585,9 +846,9 @@
585 846 if (!empty($booking_avatar_src)) {
586 847 // make sure the alt attribute is not too long in case of broken images
587 848 $booking_avatar_alt = !empty($booking_avatar_alt) && strlen($booking_avatar_alt) > 15 ? '...' . substr($booking_avatar_alt, -12) : $booking_avatar_alt;
588 849 // append booking avatar image
589 - $customer_descr .= '<span class="vbo-tableaux-booking-avatar"><img src="' . $booking_avatar_src . '" class="vbo-tableaux-booking-avatar-img" ' . (!empty($booking_avatar_alt) ? 'alt="' . htmlspecialchars($booking_avatar_alt) . '" ' : '') . '/></span>';
850 + $customer_descr .= '<span class="vbo-tableaux-booking-avatar"><img src="' . $booking_avatar_src . '" class="vbo-tableaux-booking-avatar-img" decoding="async" loading="lazy" ' . (!empty($booking_avatar_alt) ? 'alt="' . htmlspecialchars($booking_avatar_alt) . '" ' : '') . '/></span>';
590 851 }
591 852
592 853 // customer name
593 854 $customer_fullname = trim($day_booking_data['first_name'] . ' ' . $day_booking_data['last_name']);
@@ -637,17 +898,17 @@
637 898 $booking_avatar_alt = $logo_helper->provenience;
638 899 // make sure the alt attribute is not too long in case of broken images
639 900 $booking_avatar_alt = !empty($booking_avatar_alt) && strlen($booking_avatar_alt) > 15 ? '...' . substr($booking_avatar_alt, -12) : $booking_avatar_alt;
640 901 // append booking avatar image
641 - $customer_descr .= '<span class="vbo-tableaux-booking-avatar"><img src="' . $booking_avatar_src . '" class="vbo-tableaux-booking-avatar-img" ' . (!empty($booking_avatar_alt) ? 'alt="' . htmlspecialchars($booking_avatar_alt) . '" ' : '') . '/></span>';
902 + $customer_descr .= '<span class="vbo-tableaux-booking-avatar"><img src="' . $booking_avatar_src . '" class="vbo-tableaux-booking-avatar-img" decoding="async" loading="lazy" ' . (!empty($booking_avatar_alt) ? 'alt="' . htmlspecialchars($booking_avatar_alt) . '" ' : '') . '/></span>';
642 903 }
643 904 }
644 905 // set customer nominative built
645 - $customer_descr .= '<span class="vbo-tableaux-guest-name">' . $customer_nominative . '</span>';;
906 + $customer_descr .= '<span class="vbo-tableaux-guest-name">' . $customer_nominative . '</span>';
646 907 }
647 908 }
648 909 if (!$enoughinfo) {
649 - $customer_descr .= '<span class="vbo-tableaux-guest-name">#' . $day_booking_data['idorder'] . '</span>';;
910 + $customer_descr .= '<span class="vbo-tableaux-guest-name">#' . $day_booking_data['idorder'] . '</span>';
650 911 }
651 912 }
652 913 }
653 914 // set value
@@ -658,24 +919,43 @@
658 919 // not supported with the sticky table headers layout
659 920 $day_booking_snake = '';
660 921 }
661 922
662 - $write_units = strpos($dclass, "subroom-busy") !== false ? '' : $write_units;
663 - $stopdrag = ($mind == $mnum && !is_null($lastbidcheckout) && (int)date('n', $lastbidcheckout) != (int)$mon);
923 + $write_units = strpos($dclass, 'subroom-busy') !== false ? '' : $write_units;
924 + if ($table_scroll_layout === 'inline') {
925 + // one table for the whole range, determine if dragging this cell would move a non-displayed check-out
926 + $stopdrag = $lastbidcheckout && $lastbidcheckout > $last_displayed_day_ts;
927 + // check if we are displaying the first day of a new month
928 + if ($moncurts['mday'] == 1 && $nowts['mon'] != $moncurts['mon']) {
929 + $dclass .= ' vbo-overv-cell-newmonth';
930 + }
931 + } else {
932 + // one table per month, determine if dragging this cell would move a non-displayed check-out
933 + $stopdrag = ($mind == $mnum && !is_null($lastbidcheckout) && (int) date('n', $lastbidcheckout) != (int) $mon);
934 + }
664 935 $dclass .= $is_checkin === true ? ' vbo-checkinday' : '';
936 +
937 + // build proper link
938 + if ($totfound > 1) {
939 + // there must be a closure overlapping a real booking
940 + $cell_link = 'index.php?option=com_vikbooking&task=choosebusy&idroom=' . $room['id'] . '&ts=' . $moncurts[0] . '&goto=overv';
941 + } else {
942 + // regular situation with one booking only
943 + $cell_link = 'index.php?option=com_vikbooking&task=editbusy&cid[]=' . $bid . '&goto=overv';
944 + }
665 945 ?>
666 - <td align="center" class="<?php echo $dclass; ?>"<?php echo $dstyle; ?> data-day="<?php echo $cur_day_key; ?>" data-units-booked="<?php echo $totfound; ?>" data-units-left="<?php echo ($room['units'] - $totfound); ?>" data-bids="<?php echo strpos($dclass, "subroom-busy") !== false ? "-{$bid}-" : implode(',', $bids_pool); ?>">
946 + <td align="center" class="<?php echo $dclass; ?>"<?php echo $dstyle; ?> data-day="<?php echo $cur_day_key; ?>" data-units-booked="<?php echo $totfound; ?>" data-units-left="<?php echo ($room['units'] - $totfound); ?>" data-bids="<?php echo strpos($dclass, 'subroom-busy') !== false ? "-{$bid}-" : implode(',', $bids_pool); ?>">
667 947 <?php
668 948 if (!$day_booking_snake) {
669 - if ($is_checkin === true && !$stopdrag) {
949 + if ($is_checkin === true && !$stopdrag && !$day_has_closure && !$is_subunit_unassigned_row) {
670 950 ?>
671 951 <span class="vbo-draggable-sp" draggable="true">
672 - <a href="index.php?option=com_vikbooking&task=editbusy&cid[]=<?php echo $bid; ?>&goto=overv" class="<?php echo strpos($dclass, "subroom-busy") === false ? 'vbo-overview-redday' : 'vbo-overview-subredday'; ?>"<?php echo $astyle . (!empty($dalt) ? ' title="' . JHtml::fetch('esc_attr', $dalt) . '"' : ''); ?>><?php echo $write_units; ?></a>
952 + <a href="<?php echo $cell_link; ?>" class="<?php echo strpos($dclass, 'subroom-busy') === false ? 'vbo-overview-redday' : 'vbo-overview-subredday'; ?>"<?php echo $astyle . (!empty($dalt) ? ' title="' . JHtml::fetch('esc_attr', $dalt) . '"' : ''); ?>><?php echo $write_units; ?></a>
673 953 </span>
674 954 <?php
675 955 } else {
676 956 ?>
677 - <a href="index.php?option=com_vikbooking&task=editbusy&cid[]=<?php echo $bid; ?>&goto=overv" class="<?php echo strpos($dclass, "subroom-busy") === false ? 'vbo-overview-redday' : 'vbo-overview-subredday'; ?>"<?php echo $astyle . (!empty($dalt) ? ' title="' . JHtml::fetch('esc_attr', $dalt) . '"' : ''); ?>><?php echo $write_units; ?></a>
957 + <a href="<?php echo $cell_link; ?>" class="<?php echo strpos($dclass, 'subroom-busy') === false ? 'vbo-overview-redday' : 'vbo-overview-subredday'; ?>"<?php echo $astyle . (!empty($dalt) ? ' title="' . JHtml::fetch('esc_attr', $dalt) . '"' : ''); ?>><?php echo $write_units; ?></a>
678 958 <?php
679 959 }
680 960 }
681 961 echo $day_booking_snake . $critical_note;
@@ -682,8 +962,14 @@
682 962 ?>
683 963 </td>
684 964 <?php
685 965 } elseif ($totfound > 1) {
966 + if ($table_scroll_layout === 'inline') {
967 + // check if we are displaying the first day of a new month
968 + if ($moncurts['mday'] == 1 && $nowts['mon'] != $moncurts['mon']) {
969 + $dclass .= ' vbo-overv-cell-newmonth';
970 + }
971 + }
686 972 ?>
687 973 <td align="center" class="<?php echo $dclass; ?>"<?php echo $dstyle; ?> data-day="<?php echo $cur_day_key; ?>" data-units-booked="<?php echo $totfound; ?>" data-units-left="<?php echo ($room['units'] - $totfound); ?>" data-bids="<?php echo implode(',', $bids_pool); ?>">
688 974 <a href="index.php?option=com_vikbooking&task=choosebusy&idroom=<?php echo $room['id']; ?>&ts=<?php echo $moncurts[0]; ?>&goto=overv" class="vbo-overview-redday"<?php echo $astyle; ?>><?php echo $write_units; ?></a>
689 975 <?php echo $critical_note; ?>
@@ -690,14 +976,20 @@
690 976 </td>
691 977 <?php
692 978 } else {
693 979 // no booked records
980 + if ($table_scroll_layout === 'inline') {
981 + // check if we are displaying the first day of a new month
982 + if ($moncurts['mday'] == 1 && $nowts['mon'] != $moncurts['mon']) {
983 + $dclass .= ' vbo-overv-cell-newmonth';
984 + }
985 + }
694 986 ?>
695 987 <td align="center" class="<?php echo $dclass; ?>" data-day="<?php echo $cur_day_key; ?>" data-bids="">
696 988 <?php
697 989 if ($cookie_sticky_heads == 'off' && isset($room_bookings_pool[$room['id']]) && isset($room_bookings_pool[$room['id']][$prev_day_key]) && $room_bookings_pool[$room['id']][$prev_day_key]) {
698 990 if ($is_subunit && isset($room_bookings_pool[$room['id']][$prev_day_key][$room['unit_index']])) {
699 - if (date('Y-m-d', $room_bookings_pool[$room['id']][$prev_day_key][$room['unit_index']]['checkout']) == $cur_day_key) {
991 + if (date('Y-m-d', $room_bookings_pool[$room['id']][$prev_day_key][$room['unit_index']]['checkout']) == $cur_day_key && !($room_bookings_pool[$room['id']][$prev_day_key][$room['unit_index']]['closure'] ?? 0)) {
700 992 // prepend checkout snake
701 993 echo '<div class="vbo-tableaux-booking vbo-tableaux-booking-singleunit vbo-tableaux-booking-checkout"><span>&nbsp;</span></div>';
702 994 }
703 995 } elseif (!$is_subunit && $room['units'] == 1 && isset($room_bookings_pool[$room['id']][$prev_day_key][0])) {
@@ -718,35 +1010,146 @@
718 1010 }
719 1011
720 1012 // room row parsed, check if we have to parse a sub-unit next
721 1013 if (!$is_subunit && $room_bids_pool && isset($rooms_features_map[$room['id']])) {
722 - // load bookings for distinctive features when parsing the parent $room array
723 - $room_indexes_bids = VikBooking::loadRoomIndexesBookings($room['id'], $room_bids_pool);
1014 + /**
1015 + * Load bookings for distinctive features when parsing the parent $room array.
1016 + * Load also the eventually unassigned bookings.
1017 + *
1018 + * @since 1.18.2 (J) - 1.8.2 (WP)
1019 + * @since 1.18.3 (J) - 1.8.3 (WP) improved support for multi-room bookings with different stay dates.
1020 + */
1021 + list($room_indexes_bids, $room_unassigned_bids) = VikBooking::loadRoomIndexesBookings($room['id'], $room_bids_pool, $unassigned = true);
1022 + if ($room_indexes_bids || $room_unassigned_bids) {
1023 + // set room bookings pool values
1024 + $rooms_bids_pools[$room['id']] = $room_bids_pool;
1025 + }
724 1026 if ($room_indexes_bids) {
1027 + // set bookings allocated per room index
725 1028 $rooms_features_bookings[$room['id']] = $room_indexes_bids;
726 - $rooms_bids_pools[$room['id']] = $room_bids_pool;
1029 + // build a list of room index positions for every booking ID
1030 + $bid_rindex_pos = [];
1031 + foreach ($room_indexes_bids as $rindex => $rindex_bids) {
1032 + foreach ((array) $rindex_bids as $seek_bid) {
1033 + $bid_rindex_pos[$seek_bid] = $bid_rindex_pos[$seek_bid] ?? [];
1034 + $bid_rindex_pos[$seek_bid][] = $rindex;
1035 + }
1036 + }
727 1037 // map sub-unit room bookings
728 1038 foreach ($room_indexes_bids as $rindex => $rindex_bids) {
729 - if (!is_array($rindex_bids) || !$rindex_bids) {
730 - continue;
1039 + foreach ((array) $rindex_bids as $seek_bid) {
1040 + // collect a list of busy IDs affecting the current room reservation record
1041 + $room_res_busy_ids = [];
1042 + foreach ($room_bookings as $day_ress) {
1043 + foreach ($day_ress as $day_res) {
1044 + if ($day_res['idorder'] == $seek_bid && !in_array($day_res['id'], $room_res_busy_ids)) {
1045 + // push busy ID
1046 + $room_res_busy_ids[] = $day_res['id'];
1047 + }
1048 + }
1049 + }
1050 + // sort the busy IDs affecting the current room reservation record
1051 + sort($room_res_busy_ids);
1052 + // iterate all room bookings
1053 + foreach ($room_bookings as $day_key => $day_ress) {
1054 + // filter all bookings by getting only the needed one(s)
1055 + $eligible_day_reservations = array_values(array_filter($day_ress, function($day_res) use ($seek_bid) {
1056 + // there could be multiple valid bookings in case of multi-room bookings
1057 + return $day_res['idorder'] == $seek_bid;
1058 + }));
1059 + // identify the position of this room index for the current booking ID
1060 + $rindex_res_busy_pos = array_search($rindex, $bid_rindex_pos[$seek_bid] ?? []);
1061 + // identify the busy ID that belongs to the current room index in case of multi-room bookings
1062 + $rindex_res_busy_id = $rindex_res_busy_pos !== false ? ($room_res_busy_ids[$rindex_res_busy_pos] ?? 0) : 0;
1063 + // filter all day reservations by getting only the one with the proper busy ID
1064 + $rindex_day_reservations = array_values(array_filter($eligible_day_reservations, function($res) use ($rindex_res_busy_id) {
1065 + return $rindex_res_busy_id && $res['id'] == $rindex_res_busy_id;
1066 + }));
1067 + if ($rindex_day_reservations[0] ?? []) {
1068 + // allocate the proper room-day-index booking even in case of multi-room bookings
1069 + $room_bookings_pool[$room['id']][$day_key][$rindex] = $rindex_day_reservations[0];
1070 + }
1071 + }
731 1072 }
732 - foreach ($rindex_bids as $seek_bid) {
733 - foreach ($room_bookings as $day_key => $day_ress) {
734 - foreach ($day_ress as $day_res) {
735 - if ($day_res['idorder'] != $seek_bid) {
1073 + }
1074 + }
1075 + if ($room_unassigned_bids) {
1076 + // map room unassigned bookings with missing sub-unit (placeholder index = -1)
1077 + $use_rindex = -1;
1078 + // always set all bookings that are missing a sub-unit, not only the first booking record found
1079 + $rooms_features_bookings[$room['id']][$use_rindex] = $room_unassigned_bids;
1080 + // start unassigned bookings intersection container
1081 + $unassigned_bids_intersect = [];
1082 + // start counter
1083 + $unassigned_bids_count = 0;
1084 + // iterate the unassigned reservations
1085 + foreach ($room_unassigned_bids as $seek_bid) {
1086 + // scan all room bookings
1087 + foreach ($room_bookings as $day_key => $day_ress) {
1088 + foreach ($day_ress as $day_res) {
1089 + if ($day_res['idorder'] != $seek_bid || !empty($day_res['closure'])) {
1090 + // unwanted booking
1091 + continue;
1092 + }
1093 + if ($room_bookings_pool[$room['id']][$day_key][$use_rindex] ?? []) {
1094 + /**
1095 + * Even if there could be multiple room bookings without a sub-unit on the same room and day,
1096 + * we cannot allow to push more than booking, because the row for the missing sub-units is
1097 + * just one, and it is therefore impossible to display multiple reservations under the same day.
1098 + */
1099 + continue;
1100 + }
1101 + /**
1102 + * Make sure the booking with missing sub-unit that we are about to add is not intersecting other
1103 + * bookings for the same room that were previously pushed as missing sub-units. This technique
1104 + * allows us to push unlimited bookings with missing sub-units, rather than just the first one
1105 + * found in the current month (scroll=tables), or in the current period (scroll=inline), but we
1106 + * need to make sure the eventually multiple bookings added are not intersecting with each other,
1107 + * or the display of the bookings would be wrong with overlapping data on certain dates.
1108 + *
1109 + * @since 1.18.6 (J) - 1.8.6 (WP)
1110 + */
1111 + $is_intersecting_others = false;
1112 + foreach ($unassigned_bids_intersect as $previous_bid => $previous_intersection) {
1113 + if ($previous_bid == $day_res['idorder']) {
1114 + // this is fine, we need to keep pushing this booking for all stay dates
736 1115 continue;
737 1116 }
738 - if (!isset($room_bookings_pool[$room['id']])) {
739 - $room_bookings_pool[$room['id']] = [];
1117 + if (strtotime('00:00:00', $day_res['checkin']) <= $previous_intersection['checkout'] && strtotime('00:00:00', $day_res['checkout']) >= $previous_intersection['checkin']) {
1118 + // intersection found, this missing sub-unit room reservation would interfere
1119 + // with another reservation that was previously processed, so we skip it
1120 + $is_intersecting_others = true;
1121 + break;
740 1122 }
741 - if (!isset($room_bookings_pool[$room['id']][$day_key])) {
742 - $room_bookings_pool[$room['id']][$day_key] = [];
743 - }
744 - $room_bookings_pool[$room['id']][$day_key][$rindex] = $day_res;
745 1123 }
1124 + if ($is_intersecting_others) {
1125 + // do not push this booking under the rooms booking pool for this day
1126 + // it can live in the rooms feature bookings array, but not in the pool
1127 + continue;
1128 + }
1129 + // start containers, if needed
1130 + if (!isset($room_bookings_pool[$room['id']])) {
1131 + $room_bookings_pool[$room['id']] = [];
1132 + }
1133 + if (!isset($room_bookings_pool[$room['id']][$day_key])) {
1134 + $room_bookings_pool[$room['id']][$day_key] = [];
1135 + }
1136 + // set room-day booking for the unassigned index
1137 + $room_bookings_pool[$room['id']][$day_key][$use_rindex] = $day_res;
1138 + // set room-day booking intersection data
1139 + $unassigned_bids_intersect[$day_res['idorder']] = [
1140 + 'checkin' => strtotime('00:00:00', $day_res['checkin']),
1141 + 'checkout' => strtotime('00:00:00', $day_res['checkout']),
1142 + ];
1143 + // increase counter
1144 + $unassigned_bids_count++;
746 1145 }
747 1146 }
748 1147 }
1148 + if (!$unassigned_bids_count) {
1149 + // there were probably only closures, so we unset the bookings for the unassigned row index
1150 + $rooms_features_bookings[$room['id']][$use_rindex] = [];
1151 + }
749 1152 }
750 1153 }
751 1154
752 1155 // close row
@@ -775,22 +1178,43 @@
775 1178 if (hasNewBooking === true) {
776 1179 location.reload();
777 1180 }
778 1181 }
779 -function vboCallOpenJModal(identif, baseurl) {
1182 +function vboCallOpenJModal(identif, baseurl, options) {
1183 + options = typeof options !== 'object' ? {} : options;
1184 +
780 1185 if (last_room_click && last_room_click.length) {
781 1186 baseurl += '&cid[]='+last_room_click;
1187 + options.room_id = last_room_click;
782 1188 last_room_click = '';
783 1189 }
784 1190 if (last_date_click && last_date_click.length) {
785 1191 baseurl += '&checkin='+last_date_click;
1192 + options.checkin = last_date_click;
786 1193 last_date_click = '';
787 1194 }
788 1195 if (next_date_click && next_date_click.length) {
789 1196 baseurl += '&checkout='+next_date_click;
1197 + options.checkout = next_date_click;
790 1198 next_date_click = '';
791 1199 }
792 - vboOpenJModal(identif, baseurl);
1200 +
1201 + // determine what type of modal to render
1202 + if (<?php echo VBOFactory::getConfig()->getBool('overv_newbook_legacy_modal', false) ? 'true' : 'false'; ?>) {
1203 + // use the ("legacy") modal from Bootstrap rendering the View calendar within an iFrame
1204 + vboOpenJModal(identif, baseurl);
1205 + } else {
1206 + // by default we rely on the VikBooking's native modal to render the admin-widget "bookings_calendar"
1207 + VBOCore.handleDisplayWidgetNotification({
1208 + widget_id: 'bookings_calendar',
1209 + }, {
1210 + id_room: options?.room_id || null,
1211 + offset: options?.checkin || null,
1212 + day: options?.checkin || null,
1213 + checkout: options?.checkout || null,
1214 + newbook: 1,
1215 + });
1216 + }
793 1217 }
794 1218 </script>
795 1219 <?php
796 1220 echo $vbo_app->getJmodalScript('', 'vboJModalHideCallback();', '');
@@ -820,20 +1244,19 @@
820 1244 <a class="vbo-basenavuri-details" href="index.php?option=com_vikbooking&task=editorder&goto=overv&cid[]=%d" style="display: none;"></a>
821 1245 <a class="vbo-basenavuri-edit" href="index.php?option=com_vikbooking&task=editbusy&goto=overv&cid[]=%d" style="display: none;"></a>
822 1246
823 1247 <script type="text/Javascript">
1248 +const vboOvervMonthNames = <?php echo json_encode($month_names_cache ?? (new stdClass)); ?>;
1249 +
824 1250 var hovtimer;
825 1251 var hovtip = false;
826 1252 var vbodialogorph_on = false;
827 1253 var isdragging = false;
828 -var vboMessages = {
829 - currencySymb: "<?php echo $currencysymb; ?>"
830 -};
831 1254 var debug_mode = '<?php echo $pdebug; ?>';
832 1255 var bctags_count = <?php echo count($colortags); ?>;
833 1256 var bctags_pool = <?php echo json_encode($colortags); ?>;
834 1257 <?php
835 -if (count($colortags) > 0) {
1258 +if ($colortags) {
836 1259 $bctags_tip = '<div class=\"vbo-overview-tip-bctag-subtip-inner\">';
837 1260 foreach ($colortags as $ctagk => $ctagv) {
838 1261 $bctags_tip .= '<div class=\"vbo-overview-tip-bctag-subtip-circle hasTooltip\" data-ctagkey=\"'.$ctagk.'\" data-ctagcolor=\"'.$ctagv['color'].'\" title=\"'.addslashes(JText::translate($ctagv['name'])).'\"><div class=\"vbo-overview-tip-bctag-subtip-circlecont\" style=\"background-color: '.$ctagv['color'].';\"></div></div>';
839 1262 }
@@ -863,9 +1286,9 @@
863 1286 }
864 1287 // check action
865 1288 if (action < 0) {
866 1289 // stop reminding, set cookie
867 - var nd = new Date();
1290 + let nd = new Date();
868 1291 nd.setTime(nd.getTime() + (365*24*60*60*1000));
869 1292 document.cookie = "vboHideOrphans=1; expires=" + nd.toUTCString() + "; path=/; SameSite=Lax";
870 1293 }
871 1294 }
@@ -882,11 +1305,11 @@
882 1305 cellspool.push(cellelem);
883 1306 while (loop === true) {
884 1307 var next = cellelem.next('td');
885 1308 if (next === undefined || !next.length) {
886 - //attempt to go to the month after
887 - var partable = cellelem.closest('tr').closest('table').nextAll('table.vboverviewtable').first();
1309 + // attempt to go to the month after
888 1310 var nextmonth = false;
1311 + var partable = cellelem.closest('.vbo-overv-montable-wrap').next('.vbo-overv-montable-wrap').find('table.vboverviewtable');
889 1312 if (partable !== undefined && partable.length) {
890 1313 partable.find('tr.vboverviewtablerow').each(function() {
891 1314 var roomexists = jQuery(this).find('td').first();
892 1315 if (roomexists !== undefined && roomexists.length) {
@@ -898,9 +1321,9 @@
898 1321 }
899 1322 });
900 1323 }
901 1324 if (nextmonth === false) {
902 - //nothing was found in the month after
1325 + // nothing was found in the month after
903 1326 loop = false;
904 1327 break;
905 1328 }
906 1329 }
@@ -929,10 +1352,10 @@
929 1352 while (loop === true) {
930 1353 var next = cellelem.next('td');
931 1354 if (next === undefined || !next.length) {
932 1355 // attempt to go to the month after
933 - var partable = cellelem.closest('tr').closest('table').nextAll('table.vboverviewtable').first();
934 1356 var nextmonth = false;
1357 + var partable = cellelem.closest('.vbo-overv-montable-wrap').next('.vbo-overv-montable-wrap').find('table.vboverviewtable');
935 1358 if (partable !== undefined && partable.length) {
936 1359 partable.find('tr.vboverviewtablerow').each(function() {
937 1360 var roomexists = jQuery(this).find('td').first();
938 1361 if (roomexists !== undefined && roomexists.length) {
@@ -1002,10 +1425,8 @@
1002 1425 e4j_debug: debug_mode
1003 1426 },
1004 1427 (res) => {
1005 1428 if (res.indexOf('e4j.error') >= 0 ) {
1006 - console.log('doAlterBooking-- Booking ID: '+bid+' - Old Room ID: '+roomid+' - New Room ID: '+landrid+' - New Date From: '+nowdatefrom+' - New Date To: '+nowdateto);
1007 - console.log(res);
1008 1429 alert(res.replace("e4j.error.", ""));
1009 1430 //restore the old cells
1010 1431 jQuery('td.vbo-dragging-cells-tmp').removeClass('vbo-dragging-cells-tmp');
1011 1432 jQuery('td.vbo-dragged-cells-tmp').removeClass('vbo-dragged-cells-tmp');
@@ -1056,9 +1477,9 @@
1056 1477 //some errors occurred after executing certain functions
1057 1478 if (obj_res.message.length) {
1058 1479 alert(obj_res.message);
1059 1480 }
1060 - document.location.href='index.php?option=com_vikbooking&task=overv';
1481 + document.location.href='<?php echo $file_base; ?>?option=com_vikbooking&task=overv';
1061 1482 } else {
1062 1483 finalizeDndUpdate(mustReload, obj_res);
1063 1484 }
1064 1485 }
@@ -1113,13 +1534,13 @@
1113 1534 }
1114 1535 //hide modal window
1115 1536 window.setTimeout(function () {
1116 1537 if (obj_res.vcm.length) {
1117 - var vcmbtn = '<br clear="all"/><button type="button" class="btn btn-danger" onclick="'+(mustReload === true ? 'document.location.href=\'index.php?option=com_vikbooking&task=overv\'' : 'closeEsitDialog();')+'">' + Joomla.JText._('VBANNULLA') + '</button>';
1538 + var vcmbtn = '<br clear="all"/><button type="button" class="btn btn-danger" onclick="'+(mustReload === true ? 'document.location.href=\'<?php echo $file_base; ?>?option=com_vikbooking&task=overv\'' : 'closeEsitDialog();')+'">' + Joomla.JText._('VBANNULLA') + '</button>';
1118 1539 jQuery('#vbo-dnd-response').append(vcmbtn);
1119 1540 } else {
1120 1541 if (mustReload === true) {
1121 - document.location.href='index.php?option=com_vikbooking&task=overv';
1542 + document.location.href='<?php echo $file_base; ?>?option=com_vikbooking&task=overv';
1122 1543 } else {
1123 1544 jQuery('.vbo-info-overlay-block').fadeOut(400, function(){
1124 1545 //clear/reset canvas in case of previous drawing and response text
1125 1546 ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
@@ -1159,10 +1580,10 @@
1159 1580 celldata.push(elem.parent('tr').attr('data-subroomid'));
1160 1581 celldata.push(elem.attr('data-day'));
1161 1582 }
1162 1583 hovtimer = setTimeout(() => {
1163 - if (isdragging) {
1164 - // prevent tooltip from popping up if dragging
1584 + if (isdragging || (vboActionRoomRateData && vboActionRoomRateData?.start)) {
1585 + // prevent tooltip from popping up if dragging or if selecting room rates
1165 1586 unregisterHoveringTooltip();
1166 1587
1167 1588 return;
1168 1589 }
@@ -1169,19 +1590,33 @@
1169 1590
1170 1591 // turn flag on
1171 1592 hovtip = true;
1172 1593
1173 - // set default content
1174 - jQuery(
1175 - "<div class=\"vbo-overview-tipblock\">" +
1176 - "<div class=\"vbo-overview-tipinner\"><span class=\"vbo-overview-tiploading\">" + Joomla.JText._('VIKLOADING') + "</span></div>" +
1177 - "<div class=\"vbo-overview-tipexpander\" style=\"display: none;\"><div class=\"vbo-overview-expandtoggle\"><i class=\"<?php echo VikBookingIcons::i('expand'); ?>\"></i></div></div>" +
1178 - "</div>"
1179 - ).appendTo(elem);
1594 + // calculate cell-element position
1595 + let pos_top = elem.offset().top;
1596 + let pos_left = elem.offset().left;
1597 + let elem_height = elem.outerHeight();
1598 + let screen_width = window?.screen?.width || 0;
1180 1599
1181 - // set default bottom value to have the tooptip displayed above the cell
1182 - jQuery(".vbo-overview-tipblock").css("bottom", "+=" + cellheight);
1600 + // build tooltip block element
1601 + let tooltip_block = jQuery('<div></div>');
1602 + tooltip_block.addClass('vbo-overview-tipblock');
1603 + tooltip_block.append("<div class=\"vbo-overview-tipinner\"><span class=\"vbo-overview-tiploading\">" + Joomla.JText._('VIKLOADING') + "</span></div>");
1604 + tooltip_block.append("<div class=\"vbo-overview-tipexpander\" style=\"display: none;\"><div class=\"vbo-overview-expandtoggle\"><i class=\"<?php echo VikBookingIcons::i('expand'); ?>\"></i></div></div>");
1183 1605
1606 + // calculate block position
1607 + tooltip_block.css('top', (pos_top + elem_height - 16) + 'px');
1608 + if (screen_width > 600 && (pos_left + 400) > screen_width) {
1609 + // place the tooltip starting from right
1610 + tooltip_block.css('left', (pos_left - (400 - elem.outerWidth())) + 'px');
1611 + } else {
1612 + // regular placing starting from left
1613 + tooltip_block.css('left', (pos_left - 6) + 'px');
1614 + }
1615 +
1616 + // append block to body
1617 + tooltip_block.appendTo(jQuery('body'));
1618 +
1184 1619 // load tooltip bookings
1185 1620 loadTooltipBookings(elem.attr('data-bids'), celldata);
1186 1621 }, 1500);
1187 1622 }
@@ -1190,8 +1625,11 @@
1190 1625 clearTimeout(hovtimer);
1191 1626 hovtimer = null;
1192 1627 }
1193 1628
1629 +/**
1630 + * @deprecated 1.16.10 (J) - 1.6.10 (WP)
1631 + */
1194 1632 function adjustHoveringTooltip() {
1195 1633 setTimeout(() => {
1196 1634 var ver_difflim = 35;
1197 1635 var hor_difflim = 20;
@@ -1270,9 +1708,9 @@
1270 1708 // build content
1271 1709 var bcont = "<div class=\"vbo-overview-tip-bookingcont\">";
1272 1710 bcont += "<div class=\"vbo-overview-tip-bookingcont-left\">";
1273 1711 bcont += "<div class=\"vbo-overview-tip-bid\"><span class=\"vbo-overview-tip-lbl\">ID <span class=\"vbo-overview-tip-lbl-innerleft\"><a href=\"" + base_uri_edit.replace('%d', v.id) + "\"><i class=\"<?php echo VikBookingIcons::i('edit'); ?>\"></i></a></span></span><span class=\"vbo-overview-tip-cnt\">"+v.id+"</span></div>";
1274 - bcont += "<div class=\"vbo-overview-tip-bstatus\"><span class=\"vbo-overview-tip-lbl\"><?php echo addslashes(JText::translate('VBPVIEWORDERSEIGHT')); ?></span><span class=\"vbo-overview-tip-cnt\"><div class=\"label "+(v.status == 'confirmed' ? 'label-success' : 'label-warning')+"\">"+v.status_lbl+"</div></span></div>";
1712 + bcont += "<div class=\"vbo-overview-tip-bstatus\"><span class=\"vbo-overview-tip-lbl\"><?php echo addslashes(JText::translate('VBPVIEWORDERSEIGHT')); ?></span><span class=\"vbo-overview-tip-cnt\"><div class=\"badge "+(v.status == 'confirmed' ? 'badge-success' : 'badge-warning')+"\">"+v.status_lbl+"</div></span></div>";
1275 1713 bcont += "<div class=\"vbo-overview-tip-bdate\"><span class=\"vbo-overview-tip-lbl\"><?php echo addslashes(JText::translate('VBPVIEWORDERSONE')); ?></span><span class=\"vbo-overview-tip-cnt\"><a href=\"" + base_uri_details.replace('%d', v.id) + "\">"+v.ts+"</a></span></div>";
1276 1714 if (bctags_count > 0) {
1277 1715 var bctag_title = '';
1278 1716 var bctag_color = '#ffffff';
@@ -1279,9 +1717,9 @@
1279 1717 if (v.colortag.hasOwnProperty('color')) {
1280 1718 bctag_color = v.colortag.color;
1281 1719 bctag_title = v.colortag.name;
1282 1720 }
1283 - bcont += "<div class=\"vbo-overview-tip-bctag-wrap\"><div class=\"vbo-overview-tip-bctag\" data-bid=\""+v.id+"\" data-ctagcolor=\""+bctag_color+"\" style=\"background-color: "+bctag_color+"; color: "+v.colortag.fontcolor+";\" title=\""+bctag_title+"\"><i class=\"vboicn-price-tags\"></i></div></div>";
1721 + bcont += "<div class=\"vbo-overview-tip-bctag-wrap\"><div class=\"vbo-overview-tip-bctag\" data-bid=\""+v.id+"\" data-ctagcolor=\""+bctag_color+"\" style=\"background-color: "+bctag_color+"; color: "+v.colortag.fontcolor+";\"><i class=\"vboicn-price-tags\"></i></div><span class=\"vbo-overview-tip-bctag-name\">" + bctag_title + "</span></div>";
1284 1722 }
1285 1723 bcont += "</div>";
1286 1724 bcont += "<div class=\"vbo-overview-tip-bookingcont-right\">";
1287 1725 bcont += "<div class=\"vbo-overview-tip-bcustomer\"><span class=\"vbo-overview-tip-lbl\"><?php echo addslashes(JText::translate('VBOCUSTOMER')); ?></span><span class=\"vbo-overview-tip-cnt\">"+v.cinfo+"</span></div>";
@@ -1303,11 +1741,11 @@
1303 1741 var subroomids = celldata[0].split('-');
1304 1742 bcont += "<div class=\"vbo-overview-tip-optindexes\"><span class=\"vbo-overview-tip-lbl\"> </span><span class=\"vbo-overview-tip-cnt\"><select onchange=\"vboMoveSubunit('" + v.id + "', '" + subroomids[0] + "', '" + subroomids[1] + "', this.value, '" + celldata[1] + "');\">" + v.optindexes + "</select></span></div>";
1305 1743 }
1306 1744 bcont += "<div class=\"vbo-overview-tip-bookingcont-total\">";
1307 - bcont += "<div class=\"vbo-overview-tip-btot\"><span class=\"vbo-overview-tip-lbl\">" + Joomla.JText._('VBEDITORDERNINE') + "</span><span class=\"vbo-overview-tip-cnt\">" + vboMessages.currencySymb + " " + v.format_tot + "</span></div>";
1745 + bcont += "<div class=\"vbo-overview-tip-btot\"><span class=\"vbo-overview-tip-lbl\">" + Joomla.JText._('VBEDITORDERNINE') + "</span><span class=\"vbo-overview-tip-cnt\">" + VBOCore.getCurrency().display(v.format_tot) + "</span></div>";
1308 1746 if (v.totpaid > 0.00) {
1309 - bcont += "<div class=\"vbo-overview-tip-btot\"><span class=\"vbo-overview-tip-lbl\">" + Joomla.JText._('VBPEDITBUSYTOTPAID') + "</span><span class=\"vbo-overview-tip-cnt\">" + vboMessages.currencySymb + " " + v.format_totpaid + "</span></div>";
1747 + bcont += "<div class=\"vbo-overview-tip-btot vbo-overview-tip-btotpaid\"><span class=\"vbo-overview-tip-lbl\">" + Joomla.JText._('VBPEDITBUSYTOTPAID') + "</span><span class=\"vbo-overview-tip-cnt\">" + VBOCore.getCurrency().display(v.format_totpaid) + "</span></div>";
1310 1748 }
1311 1749 var getnotes = v.adminnotes;
1312 1750 if (getnotes !== null && getnotes.length) {
1313 1751 bcont += "<div class=\"vbo-overview-tip-notes\"><span class=\"vbo-overview-tip-lbl\"><span class=\"vbo-overview-tip-notes-inner\"><i class=\"vboicn-info hasTooltip\" title=\"" + getnotes + "\"></i></span></span></div>";
@@ -1317,11 +1755,15 @@
1317 1755 bcont += "</div>";
1318 1756 container.append(bcont);
1319 1757 jQuery('.vbo-overview-tipexpander').show();
1320 1758 });
1759 +
1760 + /**
1761 + * @deprecated 1.16.10 (J) - 1.6.10 (WP)
1762 + */
1321 1763 // adjust the position so that it won't go under other contents
1322 - adjustHoveringTooltip()
1323 - //
1764 + // adjustHoveringTooltip();
1765 +
1324 1766 jQuery(".hasTooltip").tooltip();
1325 1767 } catch(err) {
1326 1768 // restore
1327 1769 hideVboTooltip();
@@ -1416,14 +1858,27 @@
1416 1858 );
1417 1859 }
1418 1860
1419 1861 /**
1420 - * Move a subunit group of cells to another subroom row. Triggered by the Hover Tooltip, by the room-day bookings modal, or by DnD.
1862 + * Move a subunit group of cells to another subroom row. Triggered by
1863 + * the hovering tooltip, by the room-day bookings modal, or by DnD etc..
1421 1864 */
1422 1865 function vboMoveSubunit(bid, rid, old_rindex, new_rindex, dday) {
1423 - if (!confirm(Joomla.JText._('VBOFEATASSIGNUNIT') + new_rindex.replace('#', '') + '?')) {
1866 + let new_rindex_name = new_rindex.replace('#', '');
1867 + let is_tmp_subunit_row = new_rindex_name == 0;
1868 + let is_from_tmp_row = old_rindex == 0 || old_rindex == -1;
1869 +
1870 + if (new_rindex_name == -1 || (old_rindex == -1 && is_tmp_subunit_row)) {
1871 + // no landing on the unassigned row, or unassigned room cannot land on the holding area
1872 + alert(<?php echo json_encode(JText::translate('VBO_UNASSIGNED_ROW_FORBID')); ?>);
1424 1873 return false;
1425 1874 }
1875 +
1876 + // request confirmation, if needed
1877 + if (!is_tmp_subunit_row && !confirm(Joomla.JText._('VBOFEATASSIGNUNIT') + new_rindex_name + '?')) {
1878 + return false;
1879 + }
1880 +
1426 1881 // check if movement can be made
1427 1882 var cur_tr = jQuery('tr.vboverviewtablerow-subunit[data-subroomid="' + rid + '-' + old_rindex + '"]');
1428 1883 if (!cur_tr || !cur_tr.length) {
1429 1884 console.error('could not find the parent row of the subunit cells');
@@ -1478,8 +1933,9 @@
1478 1933 copydday = copyfirstcell.attr('data-day');
1479 1934 var copydestcell = dest_tr.find('td.subnotbusy[data-day="' + copydday + '"]');
1480 1935 if (!copydestcell || !copydestcell.length) {
1481 1936 console.error('could not find the next free destination cell for ' + copydday);
1937 + alert(<?php echo json_encode(JText::translate('VBBOOKNOTMADE')); ?>);
1482 1938 freedates = false;
1483 1939 loop = false;
1484 1940 }
1485 1941 } else {
@@ -1500,13 +1956,14 @@
1500 1956 tmpl: "component",
1501 1957 bid: bid,
1502 1958 rid: rid,
1503 1959 old_rindex: old_rindex,
1504 - new_rindex: new_rindex
1960 + new_rindex: new_rindex,
1961 + is_tmp_row: is_tmp_subunit_row ? 1 : 0,
1962 + is_from_tmp_row: is_from_tmp_row ? 1 : 0,
1505 1963 },
1506 1964 (res) => {
1507 1965 if (res.indexOf('e4j.error') >= 0 ) {
1508 - console.log(res);
1509 1966 alert(res.replace("e4j.error.", ""));
1510 1967
1511 1968 // restore loading opacity in container
1512 1969 jQuery('.vbo-overview-tipblock').css('opacity', '1');
@@ -1526,8 +1983,13 @@
1526 1983 if (firstcell.hasClass('vbo-checkinday')) {
1527 1984 firstcell.removeClass('vbo-checkinday');
1528 1985 destcell.addClass('vbo-checkinday');
1529 1986 }
1987 + // populate inline styling
1988 + let orig_style = firstcell.attr('style');
1989 + destcell.attr('style', orig_style);
1990 + firstcell.attr('style', '');
1991 + // populate content
1530 1992 firstcell.children().not('.vbo-tableaux-booking-checkout').not('.vbo-roomdaynote-trigger').appendTo(destcell);
1531 1993
1532 1994 // re-bind hover event for tooltip
1533 1995 jQuery(destcell).hover(function() {
@@ -1537,9 +1999,9 @@
1537 1999 // clean up the previous cell from which we moved data
1538 2000 firstcell.removeClass('subroom-busy').addClass('subnotbusy').attr('data-bids', '');
1539 2001
1540 2002 // check if we have a next iteration to perform
1541 - var nextsib = firstcell.next();
2003 + let nextsib = firstcell.next();
1542 2004 if (nextsib && nextsib.length && nextsib.attr('data-bids') && nextsib.attr('data-bids').indexOf('-' + bid + '-') >= 0) {
1543 2005 // prepare data for the next iteration
1544 2006 firstcell = nextsib;
1545 2007 dday = firstcell.attr('data-day');
@@ -1565,11 +2027,14 @@
1565 2027 }
1566 2028 }
1567 2029 }
1568 2030 }
2031 +
2032 + // dispatch the event for the sub-units updated
2033 + VBOCore.emitEvent('vbo-overv-sub-units-updated');
1569 2034 },
1570 2035 (err) => {
1571 - alert("Request Failed");
2036 + alert('Request Failed');
1572 2037 // restore loading opacity in container
1573 2038 jQuery('.vbo-overview-tipblock').css('opacity', '1');
1574 2039 }
1575 2040 );
@@ -1577,11 +2042,21 @@
1577 2042 return true;
1578 2043 }
1579 2044
1580 2045 /**
1581 - * Swap room sub-unit with another sub-unit on an occupied date. Triggered by DnD.
2046 + * Swap room sub-unit with another sub-unit on an occupied date. Triggered by DnD or by manual sub-unit switching.
1582 2047 */
1583 2048 function vboSwapRoomSubunits(bid, bidtwo, rid, old_rindex, new_rindex, dday) {
2049 + if (old_rindex == 0 || new_rindex == 0) {
2050 + // prevent swapping with a room reservation in the holding area (temporary sub-units row)
2051 + alert(<?php echo json_encode(JText::translate('VBO_HOLDING_AREA_NO_SWAP')); ?>);
2052 + return false;
2053 + }
2054 + if (old_rindex == -1 || new_rindex == -1) {
2055 + // prevent swapping with a room reservation in the unassigned row
2056 + alert(<?php echo json_encode(JText::translate('VBO_UNASSIGNED_ROW_FORBID')); ?>);
2057 + return false;
2058 + }
1584 2059 if (!confirm(Joomla.JText._('VBO_CONF_SWAP_RNUMB').replace('%s', old_rindex).replace('%s', new_rindex))) {
1585 2060 return false;
1586 2061 }
1587 2062 if (!bid || !bidtwo || !rid || !old_rindex || !new_rindex || !dday) {
@@ -1588,24 +2063,24 @@
1588 2063 console.error('Missing required arguments', bid, bidtwo, rid, old_rindex, new_rindex, dday);
1589 2064 return false;
1590 2065 }
1591 2066 // check if movement can be made
1592 - var cur_tr = jQuery('tr.vboverviewtablerow-subunit[data-subroomid="' + rid + '-' + old_rindex + '"]');
2067 + let cur_tr = jQuery('tr.vboverviewtablerow-subunit[data-subroomid="' + rid + '-' + old_rindex + '"]');
1593 2068 if (!cur_tr || !cur_tr.length) {
1594 2069 console.error('could not find the parent row of the subunit cells');
1595 2070 return false;
1596 2071 }
1597 - var maincell = cur_tr.find('td.subroom-busy[data-day="' + dday + '"]');
2072 + let maincell = cur_tr.find('td.subroom-busy[data-day="' + dday + '"]');
1598 2073 if (!maincell || !maincell.length) {
1599 2074 console.error('could not find the main cell of the subunit to move');
1600 2075 return false;
1601 2076 }
1602 - var dest_tr = jQuery('tr.vboverviewtablerow-subunit[data-subroomid="' + rid + '-' + new_rindex + '"]');
2077 + let dest_tr = jQuery('tr.vboverviewtablerow-subunit[data-subroomid="' + rid + '-' + new_rindex + '"]');
1603 2078 if (!dest_tr || !dest_tr.length) {
1604 2079 console.error('could not find the destination row for the subunit cells');
1605 2080 return false;
1606 2081 }
1607 - var destcell = dest_tr.find('td.subroom-busy[data-day="' + dday + '"]');
2082 + let destcell = dest_tr.find('td.subroom-busy[data-day="' + dday + '"]');
1608 2083 if (!destcell || !destcell.length) {
1609 2084 console.error('could not find the destination cell of the subunit to move');
1610 2085 return false;
1611 2086 }
@@ -1610,22 +2085,38 @@
1610 2085 return false;
1611 2086 }
1612 2087
1613 2088 // gather the cells for the swop
1614 - var swap_cells_one = [];
1615 - var swap_cells_two = [];
1616 - var swap_dates_one = [];
1617 - var swap_dates_two = [];
1618 - var swap_cell_from = maincell;
1619 - var swap_cell_to = destcell;
2089 + let swap_cells_one = [];
2090 + let swap_cells_two = [];
2091 + let swap_dates_one = [];
2092 + let swap_dates_two = [];
2093 + let swap_styles_one = [];
2094 + let swap_styles_two = [];
2095 + let swap_cell_from = maincell;
2096 + let swap_cell_to = destcell;
1620 2097
1621 2098 // register the elements to swap of the first sub-unit
2099 + if (!swap_cell_from.hasClass('vbo-checkinday')) {
2100 + // attempt to select the first cell of this booking in the current month
2101 + while (true) {
2102 + let prev_cell_from = swap_cell_from.prev();
2103 + if (!prev_cell_from || !prev_cell_from.length) {
2104 + break;
2105 + }
2106 + let prev_bids = prev_cell_from.attr('data-bids');
2107 + if (!prev_bids || prev_bids.indexOf('-' + bid + '-') < 0) {
2108 + break;
2109 + }
2110 + swap_cell_from = prev_cell_from;
2111 + }
2112 + }
1622 2113 while (true) {
1623 2114 if (!swap_cell_from || !swap_cell_from.length) {
1624 2115 // next cell not found
1625 2116 break;
1626 2117 }
1627 - var swap_bids = swap_cell_from.attr('data-bids');
2118 + let swap_bids = swap_cell_from.attr('data-bids');
1628 2119 if (!swap_bids || swap_bids.indexOf('-' + bid + '-') < 0) {
1629 2120 // last occupied cell reached
1630 2121 break;
1631 2122 }
@@ -1631,8 +2122,9 @@
1631 2122 }
1632 2123 // push cloned cell data to move
1633 2124 swap_cells_one.push(swap_cell_from.children().not('.vbo-tableaux-booking-checkout').not('.vbo-roomdaynote-trigger').clone(true));
1634 2125 swap_dates_one.push(swap_cell_from.attr('data-day'));
2126 + swap_styles_one.push(swap_cell_from.attr('style'));
1635 2127 // set next iteration cell
1636 2128 swap_cell_from = swap_cell_from.next();
1637 2129 }
1638 2130
@@ -1641,9 +2133,9 @@
1641 2133 if (!swap_cell_to || !swap_cell_to.length) {
1642 2134 // next cell not found
1643 2135 break;
1644 2136 }
1645 - var swap_bids = swap_cell_to.attr('data-bids');
2137 + let swap_bids = swap_cell_to.attr('data-bids');
1646 2138 if (!swap_bids || swap_bids.indexOf('-' + bidtwo + '-') < 0) {
1647 2139 // last occupied cell reached
1648 2140 break;
1649 2141 }
@@ -1649,28 +2141,33 @@
1649 2141 }
1650 2142 // make sure we are swapping with the check-in day, or if it's a stay-date we need to unshift until the check-in
1651 2143 if (!swap_cells_two.length && !swap_cell_to.hasClass('vbo-checkinday')) {
1652 2144 // prepend first the cells until the check-in or until not found
1653 - var prev_cell = swap_cell_to;
2145 + let prev_cell = swap_cell_to;
1654 2146 while (true) {
1655 2147 prev_cell = prev_cell.prev();
1656 2148 if (!prev_cell || !prev_cell.length) {
1657 2149 break;
1658 2150 }
1659 - var prev_cell_snake = prev_cell.find('.vbo-tableaux-booking').not('.vbo-tableaux-booking-checkout');
2151 + let prev_cell_snake = prev_cell.find('.vbo-tableaux-booking').not('.vbo-tableaux-booking-checkout');
1660 2152 if (prev_cell_snake && prev_cell_snake.length && (prev_cell_snake.hasClass('vbo-tableaux-booking-checkin') || prev_cell_snake.hasClass('vbo-tableaux-booking-stay'))) {
1661 2153 // prepend cloned cell data to move
1662 2154 swap_cells_two.unshift(prev_cell.children().not('.vbo-tableaux-booking-checkout').not('.vbo-roomdaynote-trigger').clone(true));
1663 2155 swap_dates_two.unshift(prev_cell.attr('data-day'));
2156 + swap_styles_two.unshift(prev_cell.attr('style'));
1664 2157 } else {
1665 2158 // no more previous cells
1666 2159 break;
1667 2160 }
2161 + if (prev_cell.hasClass('vbo-checkinday')) {
2162 + break;
2163 + }
1668 2164 }
1669 2165 }
1670 2166 // push cloned cell data to move
1671 2167 swap_cells_two.push(swap_cell_to.children().not('.vbo-tableaux-booking-checkout').not('.vbo-roomdaynote-trigger').clone(true));
1672 2168 swap_dates_two.push(swap_cell_to.attr('data-day'));
2169 + swap_styles_two.push(swap_cell_to.attr('style'));
1673 2170 // set next iteration cell
1674 2171 swap_cell_to = swap_cell_to.next();
1675 2172 }
1676 2173
@@ -1679,9 +2176,9 @@
1679 2176 return false;
1680 2177 }
1681 2178
1682 2179 // walk over the cells of the first room to swap and check the corresponding destination
1683 - var check_destcell = destcell;
2180 + let check_destcell = destcell;
1684 2181 for (let i = 0; i < swap_cells_one.length; i++) {
1685 2182 if (!check_destcell || !check_destcell.length) {
1686 2183 // this cell does not exist, abort
1687 2184 console.error('destination cell to #' + (i + 1) + ' was not found');
@@ -1686,12 +2183,13 @@
1686 2183 // this cell does not exist, abort
1687 2184 console.error('destination cell to #' + (i + 1) + ' was not found');
1688 2185 return false;
1689 2186 }
1690 - var dest_bids = check_destcell.attr('data-bids');
2187 + let dest_bids = check_destcell.attr('data-bids');
1691 2188 if (dest_bids && dest_bids.length && dest_bids.indexOf('-' + bidtwo + '-') < 0) {
1692 2189 // this cell is already occupied, abort
1693 2190 console.error('destination cell to #' + (i + 1) + ' is occupied by another reservation');
2191 + alert(<?php echo json_encode(JText::translate('VBBOOKNOTMADE')); ?>);
1694 2192 return false;
1695 2193 }
1696 2194 // set next destination cell
1697 2195 check_destcell = check_destcell.next();
@@ -1704,12 +2202,13 @@
1704 2202 // this cell does not exist, abort
1705 2203 console.error('destination cell from #' + (i + 1) + ' was not found');
1706 2204 return false;
1707 2205 }
1708 - var dest_bids = check_destcell.attr('data-bids');
2206 + let dest_bids = check_destcell.attr('data-bids');
1709 2207 if (dest_bids && dest_bids.length && dest_bids.indexOf('-' + bid + '-') < 0) {
1710 2208 // this cell is already occupied, abort
1711 2209 console.error('destination cell from #' + (i + 1) + ' is occupied by another reservation');
2210 + alert(<?php echo json_encode(JText::translate('VBBOOKNOTMADE')); ?>);
1712 2211 return false;
1713 2212 }
1714 2213 // set next destination cell
1715 2214 check_destcell = check_destcell.next();
@@ -1727,12 +2226,15 @@
1727 2226 checkin: dday,
1728 2227 tmpl: 'component'
1729 2228 },
1730 2229 (res) => {
2230 + // init values from selection
2231 + let target_cell = destcell;
2232 + let current_cell = maincell;
2233 + let different_lengths = swap_dates_one.length != swap_dates_two.length;
2234 + let swap_diff_dates = false;
2235 +
1731 2236 // move the cells of the first sub-unit onto the cells of the second sub-unit
1732 - var target_cell = destcell;
1733 - var current_cell = maincell;
1734 - var swap_diff_dates = false;
1735 2237 for (let counter = 0; counter < swap_cells_one.length; counter++) {
1736 2238 // remove elements from target
1737 2239 target_cell.children().not('.vbo-tableaux-booking-checkout').not('.vbo-roomdaynote-trigger').remove();
1738 2240 // swap cell elements
@@ -1738,8 +2240,17 @@
1738 2240 // swap cell elements
1739 2241 swap_cells_one[counter].appendTo(target_cell);
1740 2242 // set proper bids attribute
1741 2243 target_cell.attr('data-bids', '-' + bid + '-');
2244 + // restore initial cell styling
2245 + if ((swap_styles_one[counter] || 0)) {
2246 + target_cell.attr('style', swap_styles_one[counter]);
2247 + }
2248 + if (swap_dates_one.length > swap_dates_two.length) {
2249 + if (!(swap_dates_two[counter] || 0) || swap_dates_two.length == counter + 1) {
2250 + current_cell.attr('style', '');
2251 + }
2252 + }
1742 2253 // check if we are swapping with a non check-in day sub-unit occupied cell
1743 2254 if (counter === 0 && !target_cell.hasClass('vbo-checkinday')) {
1744 2255 target_cell.addClass('vbo-checkinday');
1745 2256 swap_diff_dates = true;
@@ -1746,9 +2257,9 @@
1746 2257 } else if (counter > 0 && !target_cell.hasClass('subroom-busy')) {
1747 2258 target_cell.removeClass('subnotbusy').addClass('subroom-busy');
1748 2259 }
1749 2260 // check if the reservations have different nights of stay
1750 - var cell_will_occupy = true;
2261 + let cell_will_occupy = true;
1751 2262 if (counter > 0 && swap_diff_dates && !swap_dates_two.includes(swap_dates_one[counter])) {
1752 2263 // the swapped unit will not occupy this cell, so clean it
1753 2264 cell_will_occupy = false;
1754 2265 } else if ((counter + 1) > swap_cells_two.length) {
@@ -1774,11 +2285,12 @@
1774 2285
1775 2286 // move the cells of the second sub-unit onto the cells of the first sub-unit
1776 2287 target_cell = maincell;
1777 2288 current_cell = destcell;
2289 +
1778 2290 // check if we are swapping with a non check-in day sub-unit occupied cell
1779 2291 if (swap_diff_dates) {
1780 - var prev_land_cell = current_cell;
2292 + let prev_land_cell = current_cell;
1781 2293 while (true) {
1782 2294 prev_land_cell = prev_land_cell.prev();
1783 2295 if (!prev_land_cell || !prev_land_cell.length) {
1784 2296 break;
@@ -1797,8 +2309,9 @@
1797 2309 break;
1798 2310 }
1799 2311 }
1800 2312 }
2313 +
1801 2314 for (let counter = 0; counter < swap_cells_two.length; counter++) {
1802 2315 // remove elements from target
1803 2316 target_cell.children().not('.vbo-tableaux-booking-checkout').not('.vbo-roomdaynote-trigger').remove();
1804 2317 // swap cell elements
@@ -1804,8 +2317,15 @@
1804 2317 // swap cell elements
1805 2318 swap_cells_two[counter].appendTo(target_cell);
1806 2319 // set proper bids attribute
1807 2320 target_cell.attr('data-bids', '-' + bidtwo + '-');
2321 + // restore initial cell styling
2322 + if ((swap_styles_two[counter] || 0)) {
2323 + target_cell.attr('style', swap_styles_two[counter]);
2324 + }
2325 + if (!(swap_dates_one[counter] || 0) && swap_dates_one.length > swap_dates_two.length) {
2326 + current_cell.attr('style', '');
2327 + }
1808 2328 // check if we are swapping with a non check-in day sub-unit occupied cell
1809 2329 if (counter === 0 && !target_cell.hasClass('vbo-checkinday')) {
1810 2330 target_cell.addClass('vbo-checkinday');
1811 2331 if (current_cell.hasClass('vbo-checkinday') && !swap_dates_one.includes(swap_dates_two[counter])) {
@@ -1817,9 +2337,10 @@
1817 2337 if (!target_cell.hasClass('subroom-busy')) {
1818 2338 target_cell.removeClass('subnotbusy').addClass('subroom-busy');
1819 2339 }
1820 2340 // check if the reservations have different nights of stay
1821 - if ((counter + 1) > swap_cells_one.length) {
2341 + if (different_lengths && (swap_dates_two[counter] || 0) && !swap_dates_one.includes(swap_dates_two[counter])) {
2342 + // moving a shorter reservation onto a longer reservation
1822 2343 // remove the children elements from this longer reservation
1823 2344 current_cell.children().not('.vbo-tableaux-booking-checkout').not('.vbo-roomdaynote-trigger').remove();
1824 2345 // remove the occupied class as well
1825 2346 current_cell.removeClass('subroom-busy').addClass('subnotbusy').attr('data-bids', '');
@@ -1824,8 +2345,14 @@
1824 2345 // remove the occupied class as well
1825 2346 current_cell.removeClass('subroom-busy').addClass('subnotbusy').attr('data-bids', '');
1826 2347 // set the occupied class onto the target
1827 2348 target_cell.removeClass('subnotbusy').addClass('subroom-busy');
2349 + // check booking color tags
2350 + let current_cell_styling = current_cell.attr('style');
2351 + // remove custom background color with an inline styling
2352 + current_cell.attr('style', '');
2353 + // restore the original styling on target
2354 + target_cell.attr('style', current_cell_styling);
1828 2355 }
1829 2356 // set next target cell
1830 2357 target_cell = target_cell.next();
1831 2358 // update current cell
@@ -1836,8 +2363,11 @@
1836 2363 if (target_cell && target_cell.length && current_cell && current_cell.length && current_cell.find('.vbo-tableaux-booking-checkout').length) {
1837 2364 // make sure to move just the first check-out snake, because snakes with equal duration may have moved the check-out here already
1838 2365 current_cell.find('.vbo-tableaux-booking-checkout').first().prependTo(target_cell);
1839 2366 }
2367 +
2368 + // dispatch the event for the sub-units updated
2369 + VBOCore.emitEvent('vbo-overv-sub-units-updated');
1840 2370 },
1841 2371 (err) => {
1842 2372 alert(err.responseText);
1843 2373 console.error(err.responseText);
@@ -1857,8 +2387,239 @@
1857 2387 window.open(open_url, '_blank');
1858 2388 }
1859 2389
1860 2390 /**
2391 + * Resolves room assignment for a given booking.
2392 + */
2393 +function vboOvervResolveRoomAssignment(bid, broomid) {
2394 + // the moveset signature to apply, empty by default
2395 + let movesetSignature = null;
2396 +
2397 + // the number of reassignment solutions found
2398 + let solutionsFound = 0;
2399 +
2400 + // list of moveset signatures to skip
2401 + let skipMovesets = [];
2402 +
2403 + // list of booking IDs to skip
2404 + let skipBookings = [];
2405 +
2406 + // init modal body element
2407 + let modalBody = null;
2408 +
2409 + // define the function to find a reassignment solution
2410 + const findReassignmentFn = () => {
2411 + // show loading
2412 + VBOCore.emitEvent('vbo-loading-modal-overv-resolveroomassign');
2413 +
2414 + // make the request to get the bookings information
2415 + VBOCore.doAjax(
2416 + "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=bookings.resolve_room_assignment'); ?>",
2417 + {
2418 + bid: bid,
2419 + room_booking_id: broomid,
2420 + skip_booking_ids: skipBookings,
2421 + skip_moveset_signatures: skipMovesets,
2422 + },
2423 + (res) => {
2424 + // stop loading
2425 + VBOCore.emitEvent('vbo-loading-modal-overv-resolveroomassign');
2426 +
2427 + try {
2428 + let obj_res = typeof res === 'string' ? JSON.parse(res) : res;
2429 +
2430 + // update moveset signature to apply
2431 + movesetSignature = obj_res?.movesetSignature;
2432 +
2433 + // update number of solutions found so far
2434 + solutionsFound = obj_res?.solutionsCount || solutionsFound;
2435 +
2436 + // append HTML content
2437 + (modalBody[0] || modalBody).innerHTML = obj_res?.html;
2438 +
2439 + // show modal apply button group
2440 + applyBtnGroup.style.display = '';
2441 + } catch(e) {
2442 + console.error(e);
2443 + alert('Invalid response.');
2444 + }
2445 + },
2446 + (err) => {
2447 + // display error message
2448 + alert(err.responseText || err);
2449 +
2450 + // dismiss modal
2451 + VBOCore.emitEvent('vbo-dismiss-modal-overv-resolveroomassign');
2452 + }
2453 + );
2454 + };
2455 +
2456 + // define the function to apply a moveset signature
2457 + const applyReassignmentFn = (useSignature) => {
2458 + // determine the signature to use
2459 + useSignature = useSignature || movesetSignature;
2460 +
2461 + // show loading
2462 + VBOCore.emitEvent('vbo-loading-modal-overv-resolveroomassign');
2463 +
2464 + // make the request to apply the moveset
2465 + VBOCore.doAjax(
2466 + "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=bookings.apply_room_assignment'); ?>",
2467 + {
2468 + moveset: useSignature,
2469 + },
2470 + (res) => {
2471 + // dismiss modal
2472 + VBOCore.emitEvent('vbo-dismiss-modal-overv-resolveroomassign');
2473 +
2474 + // reload the page
2475 + location.reload();
2476 + },
2477 + (err) => {
2478 + // display error message
2479 + alert(err.responseText || err);
2480 +
2481 + // stop loading
2482 + VBOCore.emitEvent('vbo-loading-modal-overv-resolveroomassign');
2483 + }
2484 + );
2485 + };
2486 +
2487 + // define the function to apply the reassignment "preview"
2488 + const previewReassignmentFn = () => {
2489 + if (!movesetSignature) {
2490 + throw new Error('Empty moveset signature');
2491 + }
2492 +
2493 + // store the moveset and its status to the admin-dock as temporary data
2494 + VBOCore.getAdminDock().addTemporaryData(
2495 + {
2496 + id: '_tmp',
2497 + persist_id: 'reassignmentmoveset',
2498 + name: <?php echo json_encode(JText::translate('VBO_RESOLVE_ROOM_ASSIGNMENT')); ?>,
2499 + icon: '<?php VikBookingIcons::e('random'); ?>',
2500 + style: 'orange',
2501 + },
2502 + {
2503 + moveset: movesetSignature,
2504 + skipMovesets: skipMovesets,
2505 + skipBookings: skipBookings,
2506 + }
2507 + );
2508 +
2509 + // the "preview" will actually apply the moveset,
2510 + // but it will be possible to undo the changes
2511 + applyReassignmentFn();
2512 + };
2513 +
2514 + // build modal buttons
2515 + let cancelBtn = document.createElement('button');
2516 + cancelBtn.setAttribute('type', 'button');
2517 + cancelBtn.classList.add('btn');
2518 + cancelBtn.textContent = Joomla.JText._('VBANNULLA');
2519 + cancelBtn.addEventListener('click', () => {
2520 + VBOCore.emitEvent('vbo-dismiss-modal-overv-resolveroomassign');
2521 + });
2522 +
2523 + let applyBtn = document.createElement('button');
2524 + applyBtn.setAttribute('type', 'button');
2525 + applyBtn.classList.add('btn', 'btn-primary');
2526 + applyBtn.textContent = <?php echo json_encode(JText::translate('VBAPPLY')); ?>;
2527 + applyBtn.addEventListener('click', () => {
2528 + // apply moveset to reassign room numbers
2529 + applyReassignmentFn();
2530 + });
2531 +
2532 + let actionsBtn = document.createElement('button');
2533 + actionsBtn.setAttribute('type', 'button');
2534 + actionsBtn.classList.add('btn', 'btn-primary');
2535 + actionsBtn.innerHTML = '<?php VikBookingIcons::e('ellipsis-h'); ?>';
2536 +
2537 + // define the context menu for the actions button
2538 + jQuery(actionsBtn).vboContextMenu({
2539 + placement: 'top-right',
2540 + buttons: [
2541 + {
2542 + class: 'btngroup',
2543 + text: '',
2544 + disabled: true,
2545 + visible: function (root, event) {
2546 + // always overwrite title property
2547 + let baseText = <?php echo json_encode(JText::translate('VBO_NUM_SOLUTIONS_FOUND')); ?>;
2548 + this.text = baseText.replace('%d', solutionsFound);
2549 + return solutionsFound ? true : false;
2550 + },
2551 + },
2552 + {
2553 + class: 'vbo-context-menu-entry-secondary',
2554 + text: <?php echo json_encode(JText::translate('VBO_SUGG_ANOTHER_SOL')); ?>,
2555 + icon: '<?php echo VikBookingIcons::i('forward'); ?>',
2556 + separator: false,
2557 + action: (root, event) => {
2558 + // suggest another solution, push current moveset to skip
2559 + skipMovesets.push(movesetSignature);
2560 + // find all bookings set to be excluded
2561 + document.querySelectorAll('.vbo-room-reassignment-move-exclude[data-booking-id]').forEach((excludeEl) => {
2562 + let excludeInput = excludeEl.querySelector('input[type="checkbox"][name*="skip_booking_id"]');
2563 + if (excludeInput && excludeInput.checked) {
2564 + // push booking ID to exclude
2565 + skipBookings.push(excludeEl.getAttribute('data-booking-id'));
2566 + }
2567 + });
2568 + // call function to find another reassignment solution
2569 + findReassignmentFn();
2570 + },
2571 + },
2572 + {
2573 + class: 'vbo-context-menu-entry-secondary',
2574 + text: <?php echo json_encode(JText::translate('VBOPREVIEW')); ?>,
2575 + icon: '<?php echo VikBookingIcons::i('eye'); ?>',
2576 + separator: true,
2577 + action: (root, event) => {
2578 + // call function to build the preview
2579 + previewReassignmentFn();
2580 + },
2581 + },
2582 + {
2583 + class: 'vbo-context-menu-entry-secondary',
2584 + text: <?php echo json_encode(JText::translate('VBO_RESTART')); ?>,
2585 + icon: '<?php echo VikBookingIcons::i('play'); ?>',
2586 + action: (root, event) => {
2587 + // restart matrix search
2588 + skipMovesets = [];
2589 + skipBookings = [];
2590 + // call function to restart finding a reassignment solution
2591 + findReassignmentFn();
2592 + },
2593 + },
2594 + ],
2595 + });
2596 +
2597 + let applyBtnGroup = document.createElement('div');
2598 + applyBtnGroup.classList.add('btn-group', 'vbo-context-menu-btn-group', 'vbo-overv-resolveroomassign-actions');
2599 + applyBtnGroup.style.display = 'none';
2600 + applyBtnGroup.append(applyBtn);
2601 + applyBtnGroup.append(actionsBtn);
2602 +
2603 + // display modal
2604 + modalBody = VBOCore.displayModal({
2605 + suffix: 'overv-resolveroomassign',
2606 + extra_class: 'vbo-modal-rounded vbo-modal-tall',
2607 + title: <?php echo json_encode(JText::translate('VBO_RESOLVE_ROOM_ASSIGNMENT')); ?>,
2608 + draggable: true,
2609 + escape_dismiss: false,
2610 + footer_left: cancelBtn,
2611 + footer_right: applyBtnGroup,
2612 + dismiss_event: 'vbo-dismiss-modal-overv-resolveroomassign',
2613 + loading_event: 'vbo-loading-modal-overv-resolveroomassign',
2614 + loading_body: '<?php VikBookingIcons::e('circle-notch', 'fa-spin fa-fw'); ?>',
2615 + });
2616 +
2617 + // call function to find a reassignment solution
2618 + findReassignmentFn();
2619 +}
2620 +
2621 +/**
1861 2622 * DOM ready state
1862 2623 */
1863 2624 jQuery(function() {
1864 2625 // register to the event emitted when a new booking is created through an admin widget
@@ -1870,8 +2631,35 @@
1870 2631 // reload the page to display the new booking just created
1871 2632 location.reload();
1872 2633 });
1873 2634
2635 + // register to the event dispatched when sub-units are moved
2636 + document.addEventListener('vbo-overv-sub-units-updated', (e) => {
2637 + // take care of the "unassigned" (missing sub-units) rows
2638 + document
2639 + .querySelectorAll('tr.vboverviewtablerow-subunit-unassigned')
2640 + .forEach((row) => {
2641 + let unassigned_cells = row.querySelectorAll('td.subroom-busy').length;
2642 + if (!unassigned_cells) {
2643 + try {
2644 + // get rid of the alert element first
2645 + let main_room_id = row.getAttribute('data-subroomid').split('-')[0];
2646 + row
2647 + .closest('table')
2648 + .querySelector('tr.vboverviewtablerow[data-roomid="' + main_room_id + '"]')
2649 + .querySelector('td.roomname')
2650 + .querySelector('.vbo-overview-alert')
2651 + .remove();
2652 + } catch(e) {
2653 + console.error(e);
2654 + }
2655 +
2656 + // remove the unassigned row for this room-type as there are no more bookings
2657 + row.remove();
2658 + }
2659 + });
2660 + });
2661 +
1874 2662 /* calculate padding to increase draggable area and avoid "display: table" for the parent TD and "display: table-cell" for the draggable SPAN with 100% width and height, middle aligns */
1875 2663 jQuery("td.vbo-checkinday span.vbo-draggable-sp").each(function(k, v) {
1876 2664 var parentheight = jQuery(this).closest('td').height();
1877 2665 var spheight = jQuery(this).height();
@@ -1893,9 +2681,9 @@
1893 2681 if (hovtip === true) {
1894 2682 hideVboTooltip();
1895 2683 }
1896 2684
1897 - var parentcell = jQuery(this).closest('td');
2685 + let parentcell = jQuery(this).closest('td');
1898 2686 if (parentcell.hasClass('busytmplock') || parentcell.hasClass('busy-sharedcalendar')) {
1899 2687 return false;
1900 2688 }
1901 2689
@@ -1900,18 +2688,46 @@
1900 2688 }
1901 2689
1902 2690 // reset pool of booked date cells
1903 2691 cellspool = [];
1904 - var dt = e.originalEvent.dataTransfer;
1905 - var bidstart = parentcell.attr('data-bids');
1906 - var checkind = parentcell.attr('data-day');
1907 - var roomid = parentcell.parent('tr').find('td').first().attr('data-roomid');
2692 +
2693 + let dt = e.originalEvent.dataTransfer;
2694 + let bidstart = parentcell.attr('data-bids');
2695 + let checkind = parentcell.attr('data-day');
2696 + let roomid = parentcell.parent('tr').find('td').first().attr('data-roomid');
1908 2697 if (!bidstart.length || !checkind.length || !roomid.length) {
1909 2698 return false;
1910 2699 }
1911 2700
2701 + // build the drag preview to set
2702 + let dragTarget = e.originalEvent.target;
2703 + if (dragTarget && !dragTarget.matches('.vbo-tableaux-booking')) {
2704 + // attempt to get the parent element
2705 + dragTarget = dragTarget.closest('.vbo-tableaux-booking');
2706 + }
2707 + if (dragTarget && dragTarget.matches('.vbo-tableaux-booking')) {
2708 + // build custom drag node
2709 + let dragImage = dragTarget.cloneNode(true);
2710 + dragImage.style.position = 'absolute';
2711 + dragImage.style.top = '-9999px';
2712 + dragImage.style.left = '-9999px';
2713 + dragImage.style.width = getComputedStyle(dragTarget).width;
2714 + dragImage.classList.add('vbo-booking-drag-preview');
2715 +
2716 + // temporarily append the drag node to the body
2717 + document.body.appendChild(dragImage);
2718 +
2719 + // use the drag node as drag preview
2720 + dt.setDragImage(dragImage, 10, 10);
2721 +
2722 + // remove drag node from body
2723 + setTimeout(() => {
2724 + document.body.removeChild(dragImage);
2725 + }, 0);
2726 + }
2727 +
1912 2728 // count booking nights and set booked date cells
1913 - var totnights = countBookingCells(parentcell, bidstart, roomid);
2729 + let totnights = countBookingCells(parentcell, bidstart, roomid);
1914 2730
1915 2731 // add dragging class
1916 2732 if (jQuery(this).find('.vbo-tableaux-guest-name').length) {
1917 2733 jQuery(this).find('.vbo-tableaux-guest-name').addClass('vbo-dragging-sp');
@@ -1920,13 +2736,30 @@
1920 2736 jQuery(this).addClass('vbo-dragging-sp');
1921 2737 }
1922 2738 }
1923 2739
2740 + // check if the sub-unit temporary row should be displayed
2741 + let room_id_int = Math.abs(parseInt(roomid));
2742 + if (!isNaN(room_id_int) && room_id_int) {
2743 + document
2744 + .querySelectorAll('.vboverviewtablerow-subunit-tmp[data-subroomid="' + room_id_int + '-0"]')
2745 + .forEach((room_tmp_row) => {
2746 + // display possibly hidden row with a small delay in order to not break dragging on some browsers
2747 + setTimeout(() => {
2748 + room_tmp_row.style.display = 'table-row';
2749 + }, 100);
2750 + });
2751 + }
2752 +
1924 2753 // populate dataTransfer data
1925 - var parent_rid = '';
1926 - var subunit_id = '';
2754 + let parent_rid = '';
2755 + let subunit_id = '';
1927 2756 if (parentcell.hasClass('subroom-busy')) {
1928 - var subdata = parentcell.parent('tr').attr('data-subroomid').split('-');
2757 + let subdata = parentcell.parent('tr').attr('data-subroomid').split('-');
2758 + if (!subdata[1] && subdata[2]) {
2759 + // negative index split, probably for the unassigned row
2760 + subdata[1] = '-' + subdata[2];
2761 + }
1929 2762 parent_rid = subdata[0];
1930 2763 subunit_id = subdata[1];
1931 2764 }
1932 2765 dt.setData('Checkin', checkind);
@@ -1942,19 +2775,19 @@
1942 2775 // always prevent default
1943 2776 e.preventDefault();
1944 2777
1945 2778 // gather dragging cell data
1946 - var dt = e.originalEvent.dataTransfer;
1947 - var checkind = dt.getData('Checkin');
1948 - var roomid = dt.getData('Roomid');
1949 - var bid = dt.getData('Bid');
1950 - var totnights = dt.getData('Nights');
1951 - var parent_rid = dt.getData('Parentrid');
1952 - var subunit_id = dt.getData('Subunit');
2779 + let dt = e.originalEvent.dataTransfer;
2780 + let checkind = dt.getData('Checkin');
2781 + let roomid = dt.getData('Roomid');
2782 + let bid = dt.getData('Bid');
2783 + let totnights = dt.getData('Nights');
2784 + let parent_rid = dt.getData('Parentrid');
2785 + let subunit_id = dt.getData('Subunit');
1953 2786
1954 2787 // check if we are hovering something that requires styling
1955 2788 if (e.type === 'dragover') {
1956 - var hover_cell = jQuery(this);
2789 + let hover_cell = jQuery(this);
1957 2790 if (hover_cell.hasClass('subroom-busy') && hover_cell.hasClass('vbo-checkinday')) {
1958 2791 if (hover_cell.attr('data-day') != checkind && totnights > 1) {
1959 2792 hover_cell.find('.vbo-tableaux-booking-checkin').addClass('vbo-droppable-cell');
1960 2793 }
@@ -1972,11 +2805,11 @@
1972 2805 // always reset pool of cells
1973 2806 newcellspool = [];
1974 2807
1975 2808 /* check if drop is allowed */
1976 - var landrow = jQuery(this).closest('tr');
1977 - var landrid = landrow.find('td').first().attr('data-roomid');
1978 - var landbids = jQuery(this).attr('data-bids');
2809 + let landrow = jQuery(this).closest('tr');
2810 + let landrid = landrow.find('td').first().attr('data-roomid');
2811 + let landbids = jQuery(this).attr('data-bids');
1979 2812 if (landrid === undefined || !landrid.length) {
1980 2813 return false;
1981 2814 }
1982 2815
@@ -1987,9 +2820,9 @@
1987 2820 }
1988 2821 }
1989 2822
1990 2823 // build pool of cells
1991 - var freenights = countCellFreeNights(jQuery(this), landrid, totnights, bid);
2824 + let freenights = countCellFreeNights(jQuery(this), landrid, totnights, bid);
1992 2825 if (freenights < totnights) {
1993 2826 alert(Joomla.JText._('VBOVWDNDERRNOTENCELLS').replace('%s', freenights).replace('%d', totnights));
1994 2827 return false;
1995 2828 }
@@ -1994,15 +2827,19 @@
1994 2827 return false;
1995 2828 }
1996 2829
1997 2830 // collect the new dates
1998 - var nowdatefrom = jQuery(newcellspool[0]).attr('data-day');
1999 - var nowdateto = jQuery(newcellspool[(newcellspool.length -1)]).attr('data-day');
2831 + let nowdatefrom = jQuery(newcellspool[0]).attr('data-day');
2832 + let nowdateto = jQuery(newcellspool[(newcellspool.length -1)]).attr('data-day');
2000 2833
2001 2834 // check if we are just moving a sub-unit onto another for the same dates
2002 2835 if (jQuery(this).hasClass('subnotbusy') || jQuery(this).hasClass('subroom-busy')) {
2003 2836 // dropping on a sub-unit is only allowed onto the same room ID, for the same dates (switch room index only)
2004 - var subdata = landrow.attr('data-subroomid').split('-');
2837 + let subdata = landrow.attr('data-subroomid').split('-');
2838 + if (!subdata[1] && subdata[2]) {
2839 + // negative index split, probably for the unassigned row
2840 + subdata[1] = '-' + subdata[2];
2841 + }
2005 2842 if (!subdata || !parent_rid || !subunit_id || parent_rid != subdata[0] || !subdata[1] || subunit_id == subdata[1]) {
2006 2843 // display proper error
2007 2844 if (subdata[1] && subunit_id == subdata[1]) {
2008 2845 // must use the same dates for sub-units
@@ -2042,9 +2879,9 @@
2042 2879 v.addClass('vbo-dragging-cells-tmp');
2043 2880 });
2044 2881
2045 2882 // bookings for multiple rooms should add the same dragging class also to the booking for the other rooms
2046 - var samebidcells = jQuery("td.vbo-checkinday[data-bids='" + bid + "']");
2883 + let samebidcells = jQuery("td.vbo-checkinday[data-bids='" + bid + "']");
2047 2884 if (samebidcells.length > 1) {
2048 2885 jQuery("td[data-bids='" + bid + "']").each(function(k, v) {
2049 2886 if (!jQuery(v).hasClass('vbo-dragging-cells-tmp')) {
2050 2887 jQuery(v).addClass('vbo-dragging-cells-tmp');
@@ -2056,9 +2893,9 @@
2056 2893 });
2057 2894
2058 2895 /* populate and showing loading message */
2059 2896 jQuery('.vbo-loading-dnd-head').text(Joomla.JText._('VBOVWDNDMOVINGBID').replace('%d', bid));
2060 - var movingmess = '';
2897 + let movingmess = '';
2061 2898 if (roomid != landrid) {
2062 2899 movingmess += Joomla.JText._('VBOVWDNDMOVINGROOM').replace('%s', jQuery("td[data-roomid='"+landrid+"']").first().find('span.vbo-overview-roomname').text() );
2063 2900 }
2064 2901 if (nowdatefrom != jQuery(cellspool[0]).attr('data-day') || nowdateto != jQuery(cellspool[(cellspool.length -1)]).attr('data-day')) {
@@ -2086,9 +2923,9 @@
2086 2923 jQuery(document.body).on("dragend", "td.vbo-checkinday span, td.vbo-checkinday .vbo-tableaux-booking-checkin", function(e) {
2087 2924 // stop dragging to restore tooltip functions
2088 2925 isdragging = false;
2089 2926 // we could also access the originally dragged cell
2090 - var dt = e.originalEvent.dataTransfer;
2927 + let dt = e.originalEvent.dataTransfer;
2091 2928 // restore temporary classes
2092 2929 jQuery('.vbo-dragging-sp').removeClass('vbo-dragging-sp');
2093 2930 jQuery('.vbo-droppable-cell').removeClass('vbo-droppable-cell');
2094 2931 });
@@ -2101,9 +2938,9 @@
2101 2938 roomid = roomid && roomid.length ? roomid : '';
2102 2939 last_room_click = '';
2103 2940 last_date_click = '';
2104 2941 next_date_click = '';
2105 - vboCallOpenJModal('vbo-new-res', 'index.php?option=com_vikbooking&task=calendar&cid[]='+roomid+'&checkin='+curday+'&overv=1&tmpl=component');
2942 + vboCallOpenJModal('vbo-new-res', 'index.php?option=com_vikbooking&task=calendar&cid[]='+roomid+'&checkin='+curday+'&overv=1&tmpl=component', {room_id: roomid, checkin: curday});
2106 2943 });
2107 2944
2108 2945 jQuery(document.body).on('click', 'td.busy, td.notbusy, td.busytmplock, td.subroom-busy', function(e) {
2109 2946 if (jQuery(this).hasClass('vbo-widget-booskcal-cell-mday')) {
@@ -2130,31 +2967,59 @@
2130 2967 // trigger mouseleave event to prevent tooltip from showing
2131 2968 jQuery(this).trigger('mouseleave');
2132 2969
2133 2970 // get room name, date and day-bids
2134 - var room_name = '';
2135 - var sub_room_data = '';
2136 - var room_id = 0;
2971 + let room_name = '';
2972 + let sub_room_data = '';
2973 + let room_id = 0;
2137 2974 if (jQuery(this).hasClass('subroom-busy')) {
2138 2975 sub_room_data = jQuery(this).parent('tr').attr('data-subroomid');
2139 2976 room_id = sub_room_data.split('-')[0];
2140 2977 room_name = jQuery('td.roomname[data-roomid="' + room_id + '"]').not('.subroomname').first().find('.vbo-overview-roomname').text();
2141 2978 } else {
2142 - var main_room_cell = jQuery(this).parent('tr').find('td.roomname');
2979 + let main_room_cell = jQuery(this).parent('tr').find('td.roomname');
2143 2980 room_id = main_room_cell.attr('data-roomid');
2144 2981 room_name = main_room_cell.find('.vbo-overview-roomname').text();
2145 2982 }
2146 - var date_ymd = jQuery(this).attr('data-day');
2147 - var date_read = jQuery('.bluedays[data-ymd="' + date_ymd + '"]').attr('data-readymd');
2148 - var date_bids = jQuery(this).attr('data-bids');
2149 - var def_bicon = '<?php VikBookingIcons::e('user', 'vbo-dashboard-guest-activity-avatar-icon'); ?>';
2150 - var closure_i = '<?php VikBookingIcons::e('ban', 'vbo-dashboard-guest-activity-avatar-icon'); ?>';
2983 + let date_ymd = jQuery(this).attr('data-day');
2984 + let date_read = jQuery('.bluedays[data-ymd="' + date_ymd + '"]').attr('data-readymd');
2985 + let date_bids = jQuery(this).attr('data-bids');
2986 + let date_ubk = jQuery(this).attr('data-units-booked');
2987 + let date_ulft = jQuery(this).attr('data-units-left');
2988 + let def_bicon = '<?php VikBookingIcons::e('user', 'vbo-dashboard-guest-activity-avatar-icon'); ?>';
2989 + let closure_i = '<?php VikBookingIcons::e('ban', 'vbo-dashboard-guest-activity-avatar-icon'); ?>';
2151 2990
2991 + let sub_room_index = sub_room_data ? (sub_room_data.split('-')[1] || '') : '';
2992 +
2993 + // build new reservation button
2994 + let newResBtn = null;
2995 + if (date_ubk > 1 || date_ulft > 1 || sub_room_data) {
2996 + newResBtn = document.createElement('button');
2997 + newResBtn.setAttribute('type', 'button');
2998 + newResBtn.classList.add('btn', 'btn-primary');
2999 + newResBtn.textContent = <?php echo json_encode(JText::translate('VBOSHOWQUICKRES')); ?>;
3000 + newResBtn.addEventListener('click', () => {
3001 + // dismiss modal
3002 + VBOCore.emitEvent('vbo-dismiss-modal-overv-rdaybookings');
3003 + // create new reservation
3004 + VBOCore.handleDisplayWidgetNotification({
3005 + widget_id: 'bookings_calendar',
3006 + }, {
3007 + id_room: room_id,
3008 + offset: date_ymd,
3009 + day: date_ymd,
3010 + newbook: 1,
3011 + });
3012 + });
3013 + }
3014 +
2152 3015 // display modal with booking details
2153 - var rday_bookings_modal_body = VBOCore.displayModal({
3016 + let rday_bookings_modal_body = VBOCore.displayModal({
2154 3017 suffix: 'overv-rdaybookings',
2155 - extra_class: 'vbo-modal-rounded vbo-modal-tall vbo-modal-nofooter',
2156 - title: room_name + (sub_room_data ? ' #' + sub_room_data.split('-')[1] : '') + ' - ' + date_read,
3018 + extra_class: 'vbo-modal-rounded vbo-modal-tall' + (!newResBtn ? ' vbo-modal-nofooter' : ''),
3019 + title: room_name + (sub_room_index ? ' #' + sub_room_index : '') + ' - ' + date_read,
3020 + draggable: true,
3021 + footer_right: newResBtn,
2157 3022 dismiss_event: 'vbo-dismiss-modal-overv-rdaybookings',
2158 3023 loading_event: 'vbo-loading-modal-overv-rdaybookings',
2159 3024 loading_body: '<?php VikBookingIcons::e('circle-notch', 'fa-spin fa-fw'); ?>',
2160 3025 });
@@ -2241,12 +3106,23 @@
2241 3106 }
2242 3107 rday_booking.attr('data-idorder', obj_res[b]['id']);
2243 3108 rday_booking.on('click', function(e) {
2244 3109 var click_target = jQuery(e.target);
2245 - if (click_target.is('a') || click_target.is('select') || click_target.is('option')) {
2246 - return false;
3110 + if (click_target.is('a') || click_target.is('select') || click_target.is('option') || click_target.is('button')) {
3111 + return;
2247 3112 }
2248 - vboOvervOpenBooking(jQuery(this).attr('data-idorder'));
3113 + if ((click_target.is('span') && click_target.hasClass('label')) || click_target.is('img') || click_target.is('i')) {
3114 + // open booking details within the apposite admin-widget
3115 + e.stopPropagation();
3116 + VBOCore.handleDisplayWidgetNotification({
3117 + widget_id: 'booking_details',
3118 + }, {
3119 + booking_id: jQuery(this).attr('data-idorder'),
3120 + });
3121 + } else {
3122 + // open booking details on a new tab
3123 + vboOvervOpenBooking(jQuery(this).attr('data-idorder'));
3124 + }
2249 3125 });
2250 3126
2251 3127 // build booking structure
2252 3128 var rday_booking_html = '';
@@ -2251,9 +3127,9 @@
2251 3127 // build booking structure
2252 3128 var rday_booking_html = '';
2253 3129 rday_booking_html += '<div class="vbo-dashboard-guest-activity-avatar">' + "\n";
2254 3130 if (obj_res[b]['avatar_src']) {
2255 - rday_booking_html += '<img class="vbo-dashboard-guest-activity-avatar-profile" src="' + obj_res[b]['avatar_src'] + '" alt="' + obj_res[b]['avatar_alt'] + '" />' + "\n";
3131 + rday_booking_html += '<img class="vbo-dashboard-guest-activity-avatar-profile" src="' + obj_res[b]['avatar_src'] + '" alt="' + obj_res[b]['avatar_alt'] + '" decoding="async" loading="lazy" />' + "\n";
2256 3132 } else if (obj_res[b]['closure']) {
2257 3133 rday_booking_html += closure_i + "\n";
2258 3134 } else {
2259 3135 rday_booking_html += def_bicon + "\n";
@@ -2263,11 +3139,15 @@
2263 3139 rday_booking_html += ' <div class="vbo-dashboard-guest-activity-content-head">' + "\n";
2264 3140 rday_booking_html += ' <div class="vbo-dashboard-guest-activity-content-info-details">' + "\n";
2265 3141 rday_booking_html += ' <h4 class="vbo-w-guestmessages-message-gtitle">' + (!obj_res[b]['closure'] ? obj_res[b]['cinfo'] : obj_res[b]['closure_txt']) + '</h4>' + "\n";
2266 3142 rday_booking_html += ' <div class="vbo-dashboard-guest-activity-content-info-icon">' + "\n";
2267 - rday_booking_html += ' <span class="label label-info">' + obj_res[b]['id'] + '</span> ' + "\n";
3143 + rday_booking_html += ' <span class="badge badge-info">' + obj_res[b]['id'] + '</span> ' + "\n";
3144 + if (obj_res[b]['type'] && obj_res[b]['type'] == 'overbooking') {
3145 + rday_booking_html += ' <span class="label label-error vbo-label-overbooking">' + Joomla.JText._('VBO_BTYPE_OVERBOOKING') + '</span>' + "\n";
3146 + }
2268 3147 rday_booking_html += ' <span class="badge badge-' + badge_type + '">' + obj_res[b]['status_lbl'] + '</span>' + "\n";
2269 - rday_booking_html += ' <span class="vbo-w-guestmessages-message-staydates">' + "\n";
3148 + rday_booking_html += ' <span class="vbo-w-guestmessages-message-staydates">' + "\n";
3149 + rday_booking_html += ' <?php VikBookingIcons::e('calendar-alt'); ?>' + "\n";
2270 3150 rday_booking_html += ' <span class="vbo-w-guestmessages-message-staydates-in">' + obj_res[b]['checkin_short'] + '</span>' + "\n";
2271 3151 rday_booking_html += ' <span class="vbo-w-guestmessages-message-staydates-sep">-</span>' + "\n";
2272 3152 rday_booking_html += ' <span class="vbo-w-guestmessages-message-staydates-out">' + obj_res[b]['checkout_short'] + '</span>' + "\n";
2273 3153 rday_booking_html += ' </span>' + "\n";
@@ -2285,9 +3165,9 @@
2285 3165 }
2286 3166 rday_booking_html += ' </div>' + "\n";
2287 3167 rday_booking_html += ' </div>' + "\n";
2288 3168 rday_booking_html += ' <div class="vbo-dashboard-guest-activity-content-info-msg">' + "\n";
2289 - rday_booking_html += ' <div>' + ota_bid_info + nights_guests.join(', ') + '</div>' + "\n";
3169 + rday_booking_html += ' <div>' + ota_bid_info + '<?php VikBookingIcons::e('bed'); ?> ' + nights_guests.join(', ') + '</div>' + "\n";
2290 3170 if (obj_res[b].hasOwnProperty('sub_units_data')) {
2291 3171 rday_booking_html += ' <div class="vbo-rdaybooking-subunits">' + "\n";
2292 3172 for (let sub_rname in obj_res[b]['sub_units_data']) {
2293 3173 if (!obj_res[b]['sub_units_data'].hasOwnProperty(sub_rname)) {
@@ -2318,8 +3198,13 @@
2318 3198 // append drop down
2319 3199 rday_booking_html += set_subunit_node;
2320 3200 }
2321 3201 rday_booking_html += ' </div>' + "\n";
3202 + } else {
3203 + // allow the system to relocate the room booking record
3204 + rday_booking_html += ' <div class="vbo-rdaybooking-subunits-list vbo-rdaybooking-subunit-relocate-wrap">' + "\n";
3205 + rday_booking_html += ' <button type="button" class="btn vbo-config-btn vbo-reassign-room-unit-btn" data-bid="' + obj_res[b]['id'] + '"><?php VikBookingIcons::e('random'); ?> ' + <?php echo json_encode(JText::translate('VBO_RESOLVE_ROOM_ASSIGNMENT')); ?> + '</button>' + "\n";
3206 + rday_booking_html += ' </div>' + "\n";
2322 3207 }
2323 3208 rday_booking_html += ' </div>' + "\n";
2324 3209 }
2325 3210 rday_booking_html += ' </div>' + "\n";
@@ -2437,16 +3322,28 @@
2437 3322 },
2438 3323 (res) => {
2439 3324 applying_tag = false;
2440 3325 if (res.indexOf('e4j.error') >= 0 ) {
2441 - console.log(res);
2442 3326 alert(res.replace("e4j.error.", ""));
2443 - //restore loading opacity in circles
3327 + // restore loading opacity in circles
2444 3328 jQuery('.vbo-overview-tip-bctag-subtip-circle').css('opacity', '1');
2445 3329 } else {
2446 - var obj_res = typeof res === 'string' ? JSON.parse(res) : res;
2447 - var last_bid = jQuery(clickelem).closest(".vbo-hascolortag").attr('data-lastbid');
2448 - <?php if ($pbmode == 'tags') { echo 'jQuery(".vbo-hascolortag[data-lastbid=\'"+last_bid+"\']").each(function(k, v) { jQuery(this).css("background-color", obj_res.color).find("a").first().css("color", obj_res.fontcolor); });'; } ?>
3330 + // decode the response, if needed
3331 + let obj_res = typeof res === 'string' ? JSON.parse(res) : res;
3332 + // check view mode
3333 + if ('<?php echo $pbmode; ?>' === 'tags') {
3334 + // apply new color to all booking cells
3335 + document
3336 + .querySelectorAll('.vbo-hascolortag[data-lastbid="' + bid + '"]')
3337 + .forEach((bookingCell) => {
3338 + bookingCell.style.backgroundColor = obj_res.color;
3339 + let bookingCellLink = bookingCell.querySelector('a');
3340 + if (bookingCellLink) {
3341 + bookingCellLink.style.color = obj_res.fontcolor;
3342 + }
3343 + });
3344 + }
3345 + // hide tooltip
2449 3346 hideVboTooltip();
2450 3347 }
2451 3348 },
2452 3349 (err) => {
@@ -2470,21 +3367,21 @@
2470 3367 });
2471 3368
2472 3369 jQuery(document).mouseup(function(e) {
2473 3370 if (!hovtip && !vbodialogorph_on) {
2474 - return false;
3371 + return;
2475 3372 }
2476 3373 if (hovtip) {
2477 3374 var vbo_overlay_cont = jQuery(".vbo-overview-tipblock");
2478 3375 if (!vbo_overlay_cont.is(e.target) && vbo_overlay_cont.has(e.target).length === 0) {
2479 3376 hideVboTooltip();
2480 - return true;
3377 + return;
2481 3378 }
2482 3379 if (jQuery(".vbo-overview-tip-bctag-subtip").length) {
2483 3380 var vbo_overlay_subtip_cont = jQuery(".vbo-overview-tip-bctag-wrap");
2484 3381 if (!vbo_overlay_subtip_cont.is(e.target) && vbo_overlay_subtip_cont.has(e.target).length === 0) {
2485 3382 jQuery(".vbo-overview-tip-bctag-subtip").remove();
2486 - return true;
3383 + return;
2487 3384 }
2488 3385 }
2489 3386 }
2490 3387 if (vbodialogorph_on) {
@@ -2515,9 +3412,9 @@
2515 3412 VBOCore.storageSetItem('vbo_overv_subunits_status', storage_subunits_status);
2516 3413 } else {
2517 3414 jQuery("td.roomname[data-roomid='" + roomid + "']").find('span.vbo-overview-subroom-toggle').addClass('vbo-overview-subroom-toggle-active').find('i').removeClass('fa-chevron-down').addClass('fa-chevron-up');
2518 3415 // do not use .show() or "display: block" may be added rather than "display: table-row"
2519 - jQuery("td.subroomname[data-roomid='-" + roomid + "']").parent('tr').css('display', 'table-row');
3416 + jQuery("td.subroomname[data-roomid='-" + roomid + "']").parent('tr').not('.vboverviewtablerow-subunit-tmp').css('display', 'table-row');
2520 3417 // update storage object
2521 3418 var storage_subunits_status = VBOCore.storageGetItem('vbo_overv_subunits_status');
2522 3419 try {
2523 3420 storage_subunits_status = JSON.parse(storage_subunits_status);
@@ -2548,11 +3445,15 @@
2548 3445 // do nothing
2549 3446 }
2550 3447
2551 3448 /* Default state for sticky table heads */
2552 - vboToggleStickyTableHeaders(<?php echo (count($rows) * $mnum) > 10 && $cookie_sticky_heads != 'off' ? 1 : 0; ?>);
3449 + vboToggleStickyTableHeaders(<?php echo $long_list && $cookie_sticky_heads != 'off' ? 1 : 0; ?>);
2553 3450
2554 - // check orphans (only if not disabled with the cookie)
3451 + /**
3452 + * Check orphans (only if not disabled with the cookie).
3453 + *
3454 + * @deprecated 1.18.0 (J) - 1.8.0 (WP) disabled with the "false" condition below.
3455 + */
2555 3456 var hideorphans = false;
2556 3457 var buiscuits = document.cookie;
2557 3458 if (buiscuits.length) {
2558 3459 var hideorphansck = "vboHideOrphans=1";
@@ -2559,9 +3460,9 @@
2559 3460 if (buiscuits.indexOf(hideorphansck) >= 0) {
2560 3461 hideorphans = true;
2561 3462 }
2562 3463 }
2563 - if (!hideorphans && <?php echo $vbo_auth_pricing ? 'true' : 'false'; ?>) {
3464 + if (false && !hideorphans && <?php echo $vbo_auth_pricing ? 'true' : 'false'; ?>) {
2564 3465 // make the request
2565 3466
2566 3467 VBOCore.doAjax(
2567 3468 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=orphanscount'); ?>",
@@ -2587,9 +3488,9 @@
2587 3488 orphans_list += ' <div class="vbo-orphans-info-date">'+obj_res[rid]['rdates'][dind]+'</div>';
2588 3489 }
2589 3490 orphans_list += ' </div>';
2590 3491 orphans_list += ' <div class="vbo-orphans-info-btn">';
2591 - orphans_list += ' <a href="index.php?option=com_vikbooking&task=ratesoverv&cid[]='+rid+'&startdate='+obj_res[rid]['linkd']+'" class="btn btn-primary" target="_blank"><?php echo addslashes(JText::translate('VBORPHANSCHECKBTN')); ?></a>';
3492 + orphans_list += ' <a href="<?php echo $file_base; ?>?option=com_vikbooking&task=ratesoverv&cid[]='+rid+'&startdate='+obj_res[rid]['linkd']+'" class="btn btn-primary" target="_blank"><?php echo addslashes(JText::translate('VBORPHANSCHECKBTN')); ?></a>';
2592 3493 orphans_list += ' </div>';
2593 3494 orphans_list += '</div>';
2594 3495 }
2595 3496 if (orphans_list.length) {
@@ -2599,13 +3500,12 @@
2599 3500 vbodialogorph_on = true;
2600 3501 }
2601 3502 },
2602 3503 (err) => {
2603 - console.log("orphanscount Request Failed");
3504 + console.error("orphanscount Request Failed");
2604 3505 }
2605 3506 );
2606 3507 }
2607 - //
2608 3508
2609 3509 // fests
2610 3510 jQuery(document.body).on("click", "th.bluedays", function() {
2611 3511 if (jQuery(this).hasClass('skip-bluedays-click')) {
@@ -2641,9 +3541,8 @@
2641 3541 // set modal body
2642 3542 jQuery('.vbo-overv-mngfest-wrap').appendTo(fests_modal);
2643 3543 }
2644 3544 });
2645 - //
2646 3545
2647 3546 // room-day notes
2648 3547 jQuery(document.body).on("click", ".vbo-roomdaynote-display", function() {
2649 3548 if (!jQuery(this).closest('.vbo-roomdaynote-trigger').length) {
@@ -2683,12 +3582,528 @@
2683 3582
2684 3583 // populate current room day notes
2685 3584 vboRenderRdayNotes(roomday_info[0], roomday_info[1], roomday_info[2], readymd);
2686 3585 });
2687 - //
3586 +
3587 + /**
3588 + * Check if the unassigned rows, if any, should be hid because of no missing sub-units.
3589 + */
3590 + document
3591 + .querySelectorAll('tr.vboverviewtablerow-subunit-unassigned')
3592 + .forEach((unassigned_row) => {
3593 + let unassigned_cells = unassigned_row.querySelectorAll('td.subroom-busy').length;
3594 + if (!unassigned_cells) {
3595 + // remove the unassigned row for this room-type
3596 + unassigned_row.remove();
3597 + } else {
3598 + // add an alert icon next to the main room row
3599 + try {
3600 + let main_room_id = unassigned_row.getAttribute('data-subroomid').split('-')[0];
3601 + let alert_elem = document.createElement('span');
3602 + alert_elem.classList.add('vbo-overview-alert');
3603 +
3604 + let alert_icon = document.createElement('span');
3605 + alert_icon.classList.add('badge', 'badge-warning');
3606 + alert_icon.innerHTML = '<?php VikBookingIcons::e('exclamation-triangle'); ?>';
3607 + alert_icon.addEventListener('click', () => {
3608 + // find the first room reservation record with missing sub-unit
3609 + let reassignBookingEl = unassigned_row.querySelector('td.subroom-busy.vbo-checkinday[data-lastbid]');
3610 + if (reassignBookingEl) {
3611 + // scroll the table to make the room booking cell visible
3612 + let mainTable = unassigned_row.closest('table');
3613 + let tableWrapper = mainTable.closest('.vbo-table-responsive');
3614 + let firstCellWidth = mainTable
3615 + .querySelector('tr.vboverviewtablerow[data-roomid="' + main_room_id + '"]')
3616 + .querySelector('td.roomname')
3617 + .offsetWidth;
3618 + tableWrapper.scrollTo({
3619 + left: reassignBookingEl.offsetLeft - firstCellWidth - reassignBookingEl.offsetWidth,
3620 + behavior: 'smooth',
3621 + });
3622 +
3623 + // find a reassignment solution for the first booking found
3624 + setTimeout(() => {
3625 + vboOvervResolveRoomAssignment(reassignBookingEl.getAttribute('data-lastbid'));
3626 + }, 200);
3627 + }
3628 + });
3629 +
3630 + alert_elem.append(alert_icon);
3631 +
3632 + unassigned_row
3633 + .closest('table')
3634 + .querySelector('tr.vboverviewtablerow[data-roomid="' + main_room_id + '"]')
3635 + .querySelector('td.roomname')
3636 + .querySelector('.vbo-overview-roomname')
3637 + .insertAdjacentElement('afterend', alert_elem);
3638 + } catch(e) {
3639 + console.error(e);
3640 + }
3641 +
3642 + // restore any previous temporary data for "previewing" a reassignment moveset applied
3643 + let previousMovesetData = VBOCore.getAdminDock().loadTemporaryData(
3644 + {
3645 + id: '_tmp',
3646 + persist_id: 'reassignmentmoveset',
3647 + },
3648 + (movesetData) => {
3649 + // temporary data restored from dock
3650 + let closeBtn = document.createElement('button');
3651 + closeBtn.setAttribute('type', 'button');
3652 + closeBtn.classList.add('btn');
3653 + closeBtn.textContent = <?php echo json_encode(JText::translate('VBO_KEEP_CHANGES')); ?>;
3654 + closeBtn.addEventListener('click', () => {
3655 + // dismiss modal and keep changes
3656 + VBOCore.emitEvent('vbo-dismiss-modal-overv-resolveroomassign-undo');
3657 + });
3658 +
3659 + let undoBtn = document.createElement('button');
3660 + undoBtn.setAttribute('type', 'button');
3661 + undoBtn.classList.add('btn', 'btn-danger');
3662 + undoBtn.textContent = <?php echo json_encode(JText::translate('VBO_UNDO_CHANGES')); ?>;
3663 + undoBtn.addEventListener('click', () => {
3664 + // start loading
3665 + VBOCore.emitEvent('vbo-loading-modal-overv-resolveroomassign-undo');
3666 +
3667 + // make the request to undo the changes made by the moveset
3668 + VBOCore.doAjax(
3669 + "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=bookings.apply_room_assignment'); ?>",
3670 + {
3671 + moveset: movesetData?.moveset,
3672 + undo: 1,
3673 + },
3674 + (res) => {
3675 + // dismiss modal
3676 + VBOCore.emitEvent('vbo-dismiss-modal-overv-resolveroomassign-undo');
3677 +
3678 + // reload the page
3679 + location.reload();
3680 + },
3681 + (err) => {
3682 + // display error message
3683 + alert(err.responseText || err);
3684 +
3685 + // stop loading
3686 + VBOCore.emitEvent('vbo-loading-modal-overv-resolveroomassign-undo');
3687 + }
3688 + );
3689 + });
3690 +
3691 + // display prompt modal to apply or undo the changes just made
3692 + VBOCore.displayModal({
3693 + suffix: 'overv-resolveroomassign-undo',
3694 + extra_class: 'vbo-modal-rounded vbo-modal-prompt',
3695 + title: <?php echo json_encode(JText::translate('VBO_RESOLVE_ROOM_ASSIGNMENT')); ?>,
3696 + body: <?php echo json_encode(sprintf('%s / %s?', JText::translate('VBO_KEEP_CHANGES'), JText::translate('VBO_UNDO_CHANGES'))); ?>,
3697 + draggable: true,
3698 + escape_dismiss: false,
3699 + footer_left: undoBtn,
3700 + footer_right: closeBtn,
3701 + dismiss_event: 'vbo-dismiss-modal-overv-resolveroomassign-undo',
3702 + loading_event: 'vbo-loading-modal-overv-resolveroomassign-undo',
3703 + loading_body: '<?php VikBookingIcons::e('circle-notch', 'fa-spin fa-fw'); ?>',
3704 + });
3705 + },
3706 + (movesetData) => {
3707 + // temporary data removed from dock
3708 + // do nothing as moveset was already applied
3709 + }
3710 + );
3711 + }
3712 + });
3713 +
3714 + /**
3715 + * Add body click event delegation for elements that may be later added to the DOM.
3716 + */
3717 + document.body.addEventListener('click', (e) => {
3718 +
3719 + // resolve room assignment button in modal window
3720 + if (e.target.matches('.vbo-reassign-room-unit-btn[data-bid]') || e.target.closest('.vbo-reassign-room-unit-btn[data-bid]')) {
3721 + const btnEl = !e.target.matches('.vbo-reassign-room-unit-btn[data-bid]') ? e.target.closest('.vbo-reassign-room-unit-btn[data-bid]') : e.target;
3722 + const bid = btnEl.getAttribute('data-bid');
3723 +
3724 + // dismiss modal
3725 + VBOCore.emitEvent('vbo-dismiss-modal-overv-rdaybookings');
3726 +
3727 + // call the function that handles the room assignment for a specific booking
3728 + setTimeout(() => {
3729 + vboOvervResolveRoomAssignment(bid);
3730 + }, 200);
3731 +
3732 + // do not proceed
3733 + return;
3734 + }
3735 +
3736 + // resolve room assignment modal window include/exclude bookings checkbox
3737 + if (e.target.matches('input[type="checkbox"][name*="skip_booking_id"]')) {
3738 + const spanBtn = e.target.closest('.vbo-toggle-double-status');
3739 + if (spanBtn) {
3740 + if (e.target.checked) {
3741 + spanBtn.setAttribute('data-tooltiptext', spanBtn.getAttribute('data-text-disabled'));
3742 + } else {
3743 + spanBtn.setAttribute('data-tooltiptext', spanBtn.getAttribute('data-text-enabled'));
3744 + }
3745 + }
3746 +
3747 + // do not proceed
3748 + return;
3749 + }
3750 +
3751 + });
3752 +
3753 +<?php
3754 +/**
3755 + * Handle fixed table heads when on snake-layout (scroll), which is completely
3756 + * different than the table-layout sticky table headers (handled through jQuery).
3757 + *
3758 + * @since 1.16.9 (J) - 1.6.9 (WP)
3759 + */
3760 +if ($cookie_sticky_heads == 'off') {
3761 + ?>
3762 + // listen to the document scroll event to handle the fixed table heads
3763 + document.addEventListener('scroll', vboOvervHandleVScrollTableHeads);
3764 +
3765 + // scan all tables for listening to their horizontal scrolling event
3766 + let tables = document.querySelectorAll('table.vboverviewtable.vbo-overv-sticky-table-head-off');
3767 + tables.forEach((table) => {
3768 + table.parentNode.addEventListener('scroll', vboOvervHandleHScrollTableHead);
3769 + });
3770 +
3771 + // launch the handling function
3772 + vboOvervHandleHScrollTableHead();
3773 + <?php
3774 +}
3775 +?>
2688 3776 });
2689 3777
2690 3778 /**
3779 + * Document vertical scroll event handler for the fixed table heads.
3780 + *
3781 + * @param object e the document scroll event object.
3782 + *
3783 + * @return void
3784 + *
3785 + * @since 1.16.9 (J) - 1.6.9 (WP)
3786 + */
3787 +function vboOvervHandleVScrollTableHeads(e) {
3788 + // register throttling callback
3789 + VBOCore.throttleTimer(() => {
3790 + // get all eligible tables
3791 + let tables = document.querySelectorAll('table.vboverviewtable.vbo-overv-sticky-table-head-off');
3792 + if (!tables.length) {
3793 + // un-register event
3794 + document.removeEventListener('scroll', vboOvervHandleVScrollTableHeads);
3795 + return;
3796 + }
3797 +
3798 + // pool of fixed table heads
3799 + let fixedTableHeadNodes = vboOvervBuildFixedTableHeads(tables);
3800 +
3801 + if (fixedTableHeadNodes.length) {
3802 + // append to body just the last table head node for the bottom farthest table
3803 + document.body.appendChild(fixedTableHeadNodes[fixedTableHeadNodes.length - 1]);
3804 + }
3805 + }, 400);
3806 +}
3807 +
3808 +/**
3809 + * Table-element horizontal scroll event handler for updating the fixed table heads.
3810 + *
3811 + * @param object e the table-element scroll event object.
3812 + *
3813 + * @return void
3814 + *
3815 + * @since 1.16.9 (J) - 1.6.9 (WP)
3816 + */
3817 +function vboOvervHandleHScrollTableHead(e) {
3818 + // register throttling callback
3819 + VBOCore.throttleTimer(() => {
3820 + // horizontal scrolling happens on the parent DIV node
3821 + let tables = (e?.target || document).querySelectorAll('table.vboverviewtable.vbo-overv-sticky-table-head-off');
3822 + if (tables.length !== 1) {
3823 + // un-register event for missing table
3824 + if (e?.target) {
3825 + e.target.removeEventListener('scroll', vboOvervHandleHScrollTableHead);
3826 + }
3827 + return;
3828 + }
3829 +
3830 + if (<?php echo $table_scroll_layout === 'inline' ? 'true' : 'false'; ?>) {
3831 + // the inline-scroll layout requires to update the visible month name upon scrolling
3832 + let firstVisibleCell = vboOvervFirstVisibleTableHeadNode(tables[0]);
3833 + let periodEl = tables[0].querySelector('.bluedays.vbo-overview-month[data-initial-period]');
3834 + if (firstVisibleCell && periodEl) {
3835 + if (!firstVisibleCell.no_scroll && vboOvervMonthNames[firstVisibleCell.mon]) {
3836 + // display current scroll position month name
3837 + periodEl.textContent = vboOvervMonthNames[firstVisibleCell.mon] + ' ' + firstVisibleCell.year;
3838 + } else {
3839 + // restore the original range/month name
3840 + periodEl.textContent = periodEl.getAttribute('data-initial-period');
3841 + }
3842 + }
3843 + }
3844 +
3845 + // pool of fixed table heads
3846 + let fixedTableHeadNodes = vboOvervBuildFixedTableHeads(tables);
3847 +
3848 + if (fixedTableHeadNodes.length) {
3849 + // append to body the first and only table head node expected
3850 + document.body.appendChild(fixedTableHeadNodes[0]);
3851 + }
3852 + }, 200);
3853 +}
3854 +
3855 +/**
3856 + * Builds the list of element nodes for the fixed table heads.
3857 + *
3858 + * @param object tables The table elements to parse.
3859 + *
3860 + * @return object[]
3861 + *
3862 + * @since 1.16.9 (J) - 1.6.9 (WP)
3863 + */
3864 +function vboOvervBuildFixedTableHeads(tables) {
3865 + // pool of fixed table heads
3866 + let fixedTableHeadNodes = [];
3867 +
3868 + // get document body scroll top value
3869 + let scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
3870 +
3871 + // platform fixed offset top
3872 + let platformAdminBar = <?php echo VBOPlatformDetection::isWordPress() ? 'document.querySelector("#wpadminbar")' : '(document.querySelector(".navbar") || document.querySelector("#subhead-container"))'; ?>;
3873 + let fixedOffsetTop = platformAdminBar ? platformAdminBar.offsetHeight : 0;
3874 + if (fixedOffsetTop) {
3875 + // ensure the responsive mode has got a fixed/absolute positioning
3876 + let adminBarPosition = window.getComputedStyle(platformAdminBar).getPropertyValue('position') || platformAdminBar.style.position || '';
3877 + if (adminBarPosition != 'fixed' && adminBarPosition != 'absolute' && adminBarPosition != 'sticky') {
3878 + // no fixed admin bar
3879 + fixedOffsetTop = 0;
3880 + }
3881 + }
3882 +
3883 + // scan all table elements
3884 + (tables || []).forEach((table, index) => {
3885 + // always check for existing fixed table head element
3886 + let scrollingHeadElement = document.querySelector('.vbo-overv-sticky-scroll-month[data-table-index="' + index + '"]');
3887 + if (scrollingHeadElement) {
3888 + // remove fixed table head element from DOM, will be reconstructed if needed
3889 + scrollingHeadElement.remove();
3890 + }
3891 +
3892 + // get table head element node
3893 + let tableHeadNode = table.querySelector('tr.vboverviewtablerowone');
3894 +
3895 + // get position of current table head
3896 + let tableHeadHeight = tableHeadNode.offsetHeight;
3897 +
3898 + // make sure the table head is NOT visible, hence requires a fixed head
3899 + if (scrollTop > (table.offsetTop + tableHeadHeight - (fixedOffsetTop / 2))) {
3900 + if (scrollTop > (table.offsetTop + table.offsetHeight)) {
3901 + // scrolling went even below the table
3902 + return;
3903 + }
3904 +
3905 + // build main element node
3906 + let fixedTableHead = document.createElement('div');
3907 + fixedTableHead.className = 'vbo-overv-sticky-scroll-month';
3908 + fixedTableHead.style.top = fixedOffsetTop + 'px';
3909 + fixedTableHead.style.left = table.parentNode.getBoundingClientRect().x + 'px';
3910 + fixedTableHead.setAttribute('data-table-index', index);
3911 +
3912 + // inner element node
3913 + let fixedTableHeadWrap = document.createElement('div');
3914 + fixedTableHeadWrap.className = 'vbo-overv-sticky-scroll-month-wrap';
3915 +
3916 + // month-name element node
3917 + let tableHeadMonth = tableHeadNode.querySelector('.vbo-overview-month');
3918 + let fixedTableHeadMonth = document.createElement('div');
3919 + fixedTableHeadMonth.className = 'vbo-overv-sticky-scroll-month-m';
3920 + fixedTableHeadMonth.style.width = tableHeadMonth.offsetWidth + 'px';
3921 + fixedTableHeadMonth.appendChild(
3922 + document.createTextNode(tableHeadMonth.innerText)
3923 + );
3924 + fixedTableHeadWrap.appendChild(fixedTableHeadMonth);
3925 +
3926 + // obtain the list of visible month-day cells
3927 + vboOvervVisibleTableHeadNodes(table, index).forEach((mday, mdayIndex) => {
3928 + // month-day cell element node
3929 + let fixedTableHeadMday = document.createElement('div');
3930 + fixedTableHeadMday.className = 'vbo-overv-sticky-scroll-month-d';
3931 + if (mdayIndex && mday?.mday == 1) {
3932 + fixedTableHeadMday.classList.add('vbo-overv-sticky-scroll-newmonth-d');
3933 + }
3934 + fixedTableHeadMday.style.width = mday['vwidth'] + 'px';
3935 + fixedTableHeadMday.addEventListener('click', () => {
3936 + try {
3937 + // use jQuery to trigger the click event, or plain JS won't work
3938 + /*
3939 + document
3940 + .querySelector('table.vboverviewtable.vbo-overv-sticky-table-head-off[data-table-index="' + index + '"]')
3941 + .querySelector('.bluedays[data-ymd="' + mday['ymd'] + '"]')
3942 + .dispatchEvent(new Event('click'));
3943 + */
3944 + jQuery('table.vboverviewtable.vbo-overv-sticky-table-head-off[data-table-index="' + index + '"]')
3945 + .find('.bluedays[data-ymd="' + mday['ymd'] + '"]')
3946 + .trigger('click');
3947 + } catch(e) {
3948 + // do nothing
3949 + }
3950 + });
3951 +
3952 + // week-day element
3953 + let wdayEl = document.createElement('span');
3954 + wdayEl.className = 'vbo-overv-sticky-scroll-month-day';
3955 + wdayEl.appendChild(
3956 + document.createTextNode(mday['wday'])
3957 + );
3958 + fixedTableHeadMday.appendChild(wdayEl);
3959 +
3960 + // month-day element
3961 + let mdayEl = document.createElement('span');
3962 + mdayEl.className = 'vbo-overv-sticky-scroll-month-day-num';
3963 + mdayEl.appendChild(
3964 + document.createTextNode(mday['mday'])
3965 + );
3966 + fixedTableHeadMday.appendChild(mdayEl);
3967 +
3968 + // append month-day cell element node
3969 + fixedTableHeadWrap.appendChild(fixedTableHeadMday);
3970 + });
3971 +
3972 + // append wrapper
3973 + fixedTableHead.appendChild(fixedTableHeadWrap);
3974 +
3975 + // push the final element node to the pool
3976 + fixedTableHeadNodes.push(fixedTableHead);
3977 + }
3978 + });
3979 +
3980 + return fixedTableHeadNodes;
3981 +}
3982 +
3983 +/**
3984 + * Builds a list of visible month-day cells in a table head.
3985 + *
3986 + * @param object table the NodeList element representing a table.
3987 + * @param number index the NodeList element index we are parsing.
3988 + *
3989 + * @return object[]
3990 + *
3991 + * @since 1.16.9 (J) - 1.6.9 (WP)
3992 + */
3993 +function vboOvervVisibleTableHeadNodes(table, index) {
3994 + // get the main table head element
3995 + let tableHeadElement = table.querySelector('tr.vboverviewtablerowone');
3996 +
3997 + // get month element of the given table
3998 + let monthElement = tableHeadElement.querySelector('.vbo-overview-month');
3999 + let monthName = monthElement.innerText;
4000 + let monthLeft = monthElement.offsetLeft - 2;
4001 + let monthWidth = monthElement.offsetWidth;
4002 +
4003 + // get horizontal scroll left and table width from parent node
4004 + let tableScrollLeft = table.parentNode.scrollLeft;
4005 + let tableViewWidth = table.parentNode.offsetWidth;
4006 +
4007 + // pool of visible month-day cells
4008 + let visibleMonthDays = [];
4009 +
4010 + // scan all visible month-day elements
4011 + tableHeadElement.querySelectorAll('.bluedays:not(.vbo-overview-month)').forEach((cell) => {
4012 + let monthDayLeft = cell.offsetLeft;
4013 + let monthDayWidth = cell.offsetWidth;
4014 +
4015 + // ensure cell is visible from left scroll
4016 + if ((monthDayLeft + monthDayWidth) >= (monthLeft + monthWidth)) {
4017 + // ensure cell is also visible from right scroll
4018 + if ((monthLeft + tableViewWidth) > monthDayLeft) {
4019 + // calculate cell exact visible width
4020 + let cellViewWidth = monthDayWidth;
4021 + if ((monthDayLeft + monthDayWidth) > (monthLeft + tableViewWidth)) {
4022 + // cell is cut from right scroll, count visible width
4023 + cellViewWidth = monthDayWidth - ((monthDayLeft + monthDayWidth) - (tableViewWidth + monthLeft));
4024 + } else if ((monthWidth + tableScrollLeft) > monthDayLeft) {
4025 + // cell is cut from left scroll, count visible width
4026 + cellViewWidth = monthDayWidth - ((monthWidth + tableScrollLeft) - monthDayLeft);
4027 + }
4028 + cellViewWidth = cellViewWidth > monthDayWidth || cellViewWidth < 1 ? 1 : cellViewWidth;
4029 +
4030 + // push cell details
4031 + visibleMonthDays.push({
4032 + wday: cell.querySelector('.vbo-overw-tablewday').innerText,
4033 + mday: cell.querySelector('.vbo-overw-tablemday').innerText,
4034 + ymd: cell.getAttribute('data-ymd'),
4035 + vwidth: cellViewWidth,
4036 + });
4037 + }
4038 + }
4039 + });
4040 +
4041 + return visibleMonthDays;
4042 +}
4043 +
4044 +/**
4045 + * Returns the information of the first visible month-day cell in a table head upon scrolling.
4046 + *
4047 + * @param object table the NodeList element representing a table.
4048 + *
4049 + * @return object|null
4050 + *
4051 + * @since 1.18.6 (J) - 1.8.6 (WP)
4052 + */
4053 +function vboOvervFirstVisibleTableHeadNode(table) {
4054 + // get the main table head element
4055 + let tableHeadElement = table.querySelector('tr.vboverviewtablerowone');
4056 +
4057 + // get month element of the given table
4058 + let monthElement = tableHeadElement.querySelector('.vbo-overview-month');
4059 + let monthLeft = monthElement.offsetLeft - 2;
4060 + let monthWidth = monthElement.offsetWidth;
4061 +
4062 + // get horizontal scroll left and table width from parent node
4063 + let tableScrollLeft = table.parentNode.scrollLeft;
4064 + let tableViewWidth = table.parentNode.offsetWidth;
4065 +
4066 + // attempt to identify the first visible month-day cell
4067 + let firstVisibleMonthDay = null;
4068 +
4069 + // scan all visible month-day elements
4070 + tableHeadElement.querySelectorAll('.bluedays:not(.vbo-overview-month)').forEach((cell) => {
4071 + if (firstVisibleMonthDay) {
4072 + return;
4073 + }
4074 +
4075 + // get cell offsets
4076 + let monthDayLeft = cell.offsetLeft;
4077 + let monthDayWidth = cell.offsetWidth;
4078 +
4079 + // ensure cell is visible from left scroll
4080 + if ((monthDayLeft + monthDayWidth) >= (monthLeft + monthWidth)) {
4081 + // ensure cell is also visible from right scroll
4082 + if ((monthLeft + tableViewWidth) > monthDayLeft) {
4083 + // visible cell found, gather the information
4084 + let ymd = cell.getAttribute('data-ymd');
4085 + let matchData = ymd.match(/^([0-9]{4})-(0?[1-9]|1[0-2])-[0-9]{2}$/);
4086 + if (!matchData) {
4087 + // unexpected cell information
4088 + return;
4089 + }
4090 +
4091 + // set first visible cell properties
4092 + firstVisibleMonthDay = {
4093 + ymd: ymd,
4094 + year: matchData[1],
4095 + mon: Number(matchData[2]),
4096 + no_scroll: !tableScrollLeft ? 1 : 0,
4097 + };
4098 + }
4099 + }
4100 + });
4101 +
4102 + return firstVisibleMonthDay;
4103 +}
4104 +
4105 +/**
2691 4106 * Fests
2692 4107 */
2693 4108 function vboRenderFests(day, daytitle) {
2694 4109 // compose fests information
@@ -2766,9 +4181,8 @@
2766 4181 }
2767 4182 }
2768 4183 elem.closest('.vbo-overlay-fest-details').remove();
2769 4184 } else {
2770 - console.log(res);
2771 4185 alert('Invalid response');
2772 4186 }
2773 4187 },
2774 4188 (err) => {
@@ -2812,9 +4226,8 @@
2812 4226 // reset input fields
2813 4227 jQuery('#vbo-newfest-name').val('');
2814 4228 jQuery('#vbo-newfest-descr').val('');
2815 4229 } catch (e) {
2816 - console.log(res);
2817 4230 alert('Invalid response');
2818 4231 return false;
2819 4232 }
2820 4233 },
@@ -2903,9 +4316,8 @@
2903 4316 jQuery('#vbo-newrdnote-name').val('');
2904 4317 jQuery('#vbo-newrdnote-descr').val('');
2905 4318 jQuery('#vbo-newrdnote-cdays').val('0').trigger('change');
2906 4319 } catch (e) {
2907 - console.log(res);
2908 4320 alert('Invalid response');
2909 4321 return false;
2910 4322 }
2911 4323 },
@@ -2950,9 +4362,8 @@
2950 4362 }
2951 4363 }
2952 4364 elem.closest('.vbo-modal-roomdaynotes-note-details').remove();
2953 4365 } else {
2954 - console.log(res);
2955 4366 alert('Invalid response');
2956 4367 }
2957 4368 },
2958 4369 (err) => {