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 +1250 -203 1.7.2 → 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,10 +16,19 @@
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
25 34 $file_base = VBOPlatformDetection::isWordPress() ? 'admin.php' : 'index.php';
@@ -74,9 +83,9 @@
74 83 $df = 'm/d/Y';
75 84 } else {
76 85 $df = 'Y/m/d';
77 86 }
78 -$currencysymb = VikBooking::getCurrencySymb(true);
87 +$currencysymb = VikBooking::getCurrencySymb();
79 88
80 89 $pdebug = VikRequest::getInt('e4j_debug', '', 'request');
81 90 $session = JFactory::getSession();
82 91 $show_type = $session->get('vbUnitsShowType', '');
@@ -82,12 +91,14 @@
82 91 $show_type = $session->get('vbUnitsShowType', '');
83 92 $mnum = $session->get('vbOvwMnum', '1');
84 93 $mnum = intval($mnum);
85 94 $pcategory_id = $session->get('vbOvwCatid', 0);
86 -$cookie = JFactory::getApplication()->input->cookie;
95 +$cookie = $app->input->cookie;
87 96 $cookie_uleft = $cookie->get('vboAovwUleft', '', 'string');
88 97 $cookie_sticky_heads = $cookie->get('vboAovwStheads', 'off', 'string');
98 +$table_scroll_layout = $cookie->get('vboAovwScroll', (count($rows) > 3 ? 'inline' : 'tables'), 'string');
89 99 $colortags = VikBooking::loadBookingsColorTags();
100 +$long_list = (count($rows) * $mnum) > 10;
90 101
91 102 // View mode - Classic or Tags
92 103 $pbmode = $session->get('vbTagsMode', 'classic');
93 104 $tags_view_supported = false;
@@ -126,8 +137,30 @@
126 137 $clone_room['unit_index_str'] = $indexdata;
127 138 array_splice($rows, ($kr + 1 + $index_loop), 0, [$clone_room]);
128 139 $index_loop++;
129 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 + }
130 163 }
131 164 }
132 165 }
133 166 }
@@ -155,21 +188,21 @@
155 188
156 189 ?>
157 190 <script type="text/Javascript">
158 191 function vboUnitsLeftOrBooked() {
159 - var set_to = jQuery('#uleftorbooked').val();
192 + let set_to = jQuery('#uleftorbooked').val();
160 193 jQuery('.vbo-overv-avcell[data-units-booked]').each(function() {
161 - var counter_el = jQuery(this).find('a');
194 + let counter_el = jQuery(this).find('a');
162 195 if (!counter_el.length) {
163 196 return;
164 197 }
165 198 counter_el.first().text(jQuery(this).attr('data-'+set_to));
166 199 });
167 - var nd = new Date();
200 + let nd = new Date();
168 201 nd.setTime(nd.getTime() + (365*24*60*60*1000));
169 202 document.cookie = "vboAovwUleft="+set_to+"; expires=" + nd.toUTCString() + "; path=/; SameSite=Lax";
170 203 }
171 -if (jQuery.isFunction(jQuery.fn.tooltip)) {
204 +if (typeof jQuery.fn.tooltip === 'function') {
172 205 jQuery(".hasTooltip").tooltip();
173 206 } else {
174 207 jQuery.fn.tooltip = function(){};
175 208 }
@@ -211,9 +244,9 @@
211 244 }
212 245
213 246 $stickyheaders_cmd_on = '';
214 247 $stickyheaders_cmd_off = '';
215 - if ((count($rows) * $mnum) > 10) {
248 + if ($long_list) {
216 249 if (VBOPlatformDetection::isJoomla()) {
217 250 /**
218 251 * @joomlaonly fixed offset selector is ".navbar"
219 252 */
@@ -228,9 +261,9 @@
228 261 }
229 262 ?>
230 263 <script type="text/javascript">
231 264 function vboToggleStickyTableHeaders(val) {
232 - var sticky_heads_cval = 'off';
265 + let sticky_heads_cval = 'off';
233 266 if (val > 0) {
234 267 sticky_heads_cval = 'on';
235 268 jQuery('table.vboverviewtable').addClass('vbo-overv-sticky-table-head-on');
236 269 jQuery('table.vboverviewtable').removeClass('vbo-overv-sticky-table-head-off');
@@ -254,63 +287,134 @@
254 287 tables.forEach((table) => {
255 288 table.parentNode.addEventListener('scroll', vboOvervHandleHScrollTableHead);
256 289 });
257 290 }
258 - var nd = new Date();
291 + let nd = new Date();
259 292 nd.setTime(nd.getTime() + (365*24*60*60*1000));
260 293 document.cookie = "vboAovwStheads=" + sticky_heads_cval + "; expires=" + nd.toUTCString() + "; path=/; SameSite=Lax";
261 294 }
262 295 </script>
263 296 <?php
264 - 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
265 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 +
266 396 <div class="btn-group pull-left">
267 - <select name="stickytheads" onchange="vboToggleStickyTableHeaders(this.value);">
268 - <optgroup label="<?php echo JHtml::fetch('esc_attr', JText::translate('VBO_LAYOUT')); ?>">
269 - <option value="1"<?php echo empty($cookie_sticky_heads) || $cookie_sticky_heads == 'on' ? ' selected="selected"' : ''; ?>><?php echo JText::translate('VBO_TABLE'); ?></option>
270 - <option value="0"<?php echo $cookie_sticky_heads == 'off' ? ' selected="selected"' : ''; ?>><?php echo JText::translate('VBO_SCROLL'); ?></option>
271 - </optgroup>
272 - </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>
273 401 </div>
274 402 <?php
275 403 }
276 404
277 - if ($tags_view_supported === true) {
278 405 ?>
279 406 <div class="btn-group pull-left">
280 - <div class="vbo-overview-tagsorclassic">
281 - <select name="bmode" onchange="document.vboverview.submit();">
282 - <option value="classic"><?php echo JText::translate('VBOAVOVWBMODECLASSIC'); ?></option>
283 - <option value="tags"<?php echo $pbmode == 'tags' ? ' selected="selected"' : ''; ?>><?php echo JText::translate('VBOAVOVWBMODETAGS'); ?></option>
284 - </select>
285 - </div>
286 407 <?php
287 - if ($pbmode == 'tags' && count($colortags) > 0) {
288 - ?>
289 - <div class="vbo-overview-tagslegend">
290 - <span class="vbo-overview-tagslegend-lbl"><?php echo JText::translate('VBOAVOVWBMODETAGSLBL'); ?></span>
291 - <?php
292 - foreach ($colortags as $ctagk => $ctagv) {
293 - ?>
294 - <div class="vbo-overview-legend-tag">
295 - <div class="vbo-colortag-circle hasTooltip" style="background-color: <?php echo $ctagv['color']; ?>;" title="<?php echo JText::translate($ctagv['name']); ?>"></div>
296 - </div>
297 - <?php
298 - }
299 - ?>
300 - </div>
301 - <?php
302 - }
408 + // display overview context-menu
409 + echo JLayoutHelper::render('overview.actions', ['caller' => 'overv', 'rooms' => $rows]);
303 410 ?>
304 411 </div>
305 - <?php
306 - }
307 - ?>
308 412 <div class="btn-group pull-right">
309 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>
310 414 </div>
311 415 <div class="btn-group pull-right vbo-avov-legend">
312 - <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> -->
313 417 <div class="vbo-overview-legend-red">
314 418 <span class="vbo-overview-legend-box">&nbsp;</span>
315 419 <span class="vbo-overview-legend-title"><?php echo JText::translate('VBOVERVIEWLEGRED'); ?></span>
316 420 </div>
@@ -327,32 +431,84 @@
327 431 <span class="vbo-overview-legend-title"><?php echo JText::translate('VBOVERVIEWLEGDND'); ?></span>
328 432 </div>
329 433 </div>
330 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 +?>
331 452 </form>
332 453
333 454 <?php
334 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);
335 458 $nowts = getdate($tsstart);
336 459 $curts = $nowts;
337 -for ($mind = 1; $mind <= $mnum; $mind++) {
460 +for ($mind = 1; $mind <= ($table_scroll_layout === 'inline' ? 1 : $mnum); $mind++) {
338 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 + }
339 479 ?>
340 480 <div class="vbo-overv-montable-wrap">
341 481 <div class="vbo-table-responsive">
342 - <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); ?>">
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]); ?>">
343 483 <thead>
344 484 <tr class="vboverviewtablerowone">
345 - <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>
346 486 <?php
347 487 $moncurts = $curts;
348 488 $mon = $moncurts['mon'];
349 - 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)) {
350 491 $curdayymd = date('Y-m-d', $moncurts[0]);
351 - $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 + }
352 508 ?>
353 - <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; ?>">
354 - <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>
355 511 <span class="vbo-overw-tablemday"><?php echo $moncurts['mday']; ?></span>
356 512 </th>
357 513 <?php
358 514 // parse the next day
@@ -367,22 +523,41 @@
367 523 $moncurts = $curts;
368 524 $mon = $moncurts['mon'];
369 525 $room_tags_view = $pbmode == 'tags' && $tags_view_supported === true && $room['units'] <= 1 ? true : false;
370 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;
371 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 +
372 547 ?>
373 - <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;"' : ''; ?>>
374 549 <?php
375 550 if ($is_subunit) {
376 551 ?>
377 552 <td class="roomname subroomname" data-roomid="<?php echo '-' . $room['id']; ?>">
378 - <span class="vbo-overview-subroomunits"><?php VikBookingIcons::e('bed'); ?></span>
553 + <span class="vbo-overview-subroomunits"><?php VikBookingIcons::e($main_cell_icn_id); ?></span>
379 554 <span class="vbo-overview-subroomname"><?php echo $room['unit_index_str']; ?></span>
380 555 </td>
381 556 <?php
382 557 } else {
383 558 ?>
384 - <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']; ?>">
385 560 <span class="vbo-overview-room-info">
386 561 <span class="vbo-overview-roomunits"><?php echo $room['units']; ?></span>
387 562 <span class="vbo-overview-roomname"><?php echo $room['name']; ?></span>
388 563 <?php
@@ -396,12 +571,12 @@
396 571 </td>
397 572 <?php
398 573 }
399 574
400 - // 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
401 576 $room_bids_pool = [];
402 577 $room_bookings = [];
403 - while ($moncurts['mon'] == $mon) {
578 + while (($table_scroll_layout === 'inline' && $moncurts[0] < $last_displayed_day_ts) || ($table_scroll_layout !== 'inline' && $moncurts['mon'] == $mon)) {
404 579 // init cell values
405 580 $dclass = 'vbo-grid-avcell vbo-overv-avcell ' . (!$is_subunit ? "notbusy" : "subnotbusy");
406 581 $is_checkin = false;
407 582 $is_sharedcal = false;
@@ -458,9 +633,10 @@
458 633 if ($moncurts[0] == $ritts) {
459 634 $dalt = JText::translate('VBPICKUPAT')." ".date('H:i', $b['checkin']);
460 635 $is_checkin = true;
461 636 $lastbidcheckout = $b['checkout'];
462 - $bids_checkins[$bid] = $cur_day_key;
637 + $bids_checkins[$bid] = $bids_checkins[$bid] ?? [];
638 + $bids_checkins[$bid][] = $cur_day_key;
463 639 } elseif ($moncurts[0] == $conts) {
464 640 $dalt = JText::translate('VBRELEASEAT')." ".date('H:i', $b['checkout']);
465 641 }
466 642 $totfound++;
@@ -523,11 +699,11 @@
523 699 $astyle = '';
524 700 if ($room_tags_view === true && $totfound > 0) {
525 701 $last_bid = intval(str_replace('-', '', $bids_pool[(count($bids_pool) - 1)]));
526 702 $binfo = VikBooking::getBookingInfoFromID($last_bid);
527 - if (count($binfo) > 0) {
703 + if ($binfo) {
528 704 $bcolortag = VikBooking::applyBookingColorTag($binfo);
529 - if (count($bcolortag) > 0) {
705 + if ($bcolortag) {
530 706 $bcolortag['name'] = JText::translate($bcolortag['name']);
531 707 $dstyle = " style=\"background-color: ".$bcolortag['color']."; color: ".(array_key_exists('fontcolor', $bcolortag) ? $bcolortag['fontcolor'] : '#ffffff').";\" data-lastbid=\"".$last_bid."\"";
532 708 $astyle = " style=\"color: ".(array_key_exists('fontcolor', $bcolortag) ? $bcolortag['fontcolor'] : '#ffffff').";\"";
533 709 $dclass .= ' vbo-hascolortag';
@@ -533,9 +709,9 @@
533 709 $dclass .= ' vbo-hascolortag';
534 710 }
535 711 }
536 712 }
537 - 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']])) {
538 714 foreach ($rooms_bids_pools[$room['id']][$cur_day_key] as $bid) {
539 715 $bid = intval(str_replace('-', '', $bid));
540 716 if (in_array($bid, $rooms_features_bookings[$room['id']][$room['unit_index']])) {
541 717 $room['units'] = 1;
@@ -540,9 +716,31 @@
540 716 if (in_array($bid, $rooms_features_bookings[$room['id']][$room['unit_index']])) {
541 717 $room['units'] = 1;
542 718 $totfound = 1;
543 719 $dclass = "vbo-grid-avcell vbo-overv-avcell subroom-busy";
544 - $is_checkin = isset($bids_checkins[$bid]) && $bids_checkins[$bid] == $cur_day_key ? true : $is_checkin;
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
545 743 break;
546 744 }
547 745 }
548 746 }
@@ -571,8 +769,11 @@
571 769 $dclass .= ' vbo-roomdaynote-empty';
572 770 $rdaynote_icn = 'far fa-sticky-note';
573 771 }
574 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 + }
575 776
576 777 /**
577 778 * Closures overlapping one real reservation for a single-unit room should still
578 779 * allow the display of the booking snake if it's just one real reservation.
@@ -587,12 +788,15 @@
587 788 }
588 789 }
589 790
590 791 // prepare cell content
792 + $day_has_closure = false;
591 793 if ($totfound === 1 || $closure_on_real_res) {
794 + // start booking snake content
592 795 $day_booking_snake = '';
796 + // collect the current day booking data
593 797 $day_booking_data = [];
594 - 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]) {
595 799 if ($is_subunit && isset($room_bookings_pool[$room['id']][$cur_day_key][$room['unit_index']])) {
596 800 $day_booking_data = $room_bookings_pool[$room['id']][$cur_day_key][$room['unit_index']];
597 801 } elseif (!$is_subunit && isset($room_bookings_pool[$room['id']][$cur_day_key][0])) {
598 802 $day_booking_data = $room_bookings_pool[$room['id']][$cur_day_key][0];
@@ -597,9 +801,11 @@
597 801 } elseif (!$is_subunit && isset($room_bookings_pool[$room['id']][$cur_day_key][0])) {
598 802 $day_booking_data = $room_bookings_pool[$room['id']][$cur_day_key][0];
599 803 }
600 804 }
601 - 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]) {
602 808 if ($is_subunit && isset($room_bookings_pool[$room['id']][$prev_day_key][$room['unit_index']])) {
603 809 if (date('Y-m-d', $room_bookings_pool[$room['id']][$prev_day_key][$room['unit_index']]['checkout']) == $cur_day_key) {
604 810 // prepend checkout snake
605 811 $day_booking_snake .= '<div class="vbo-tableaux-booking vbo-tableaux-booking-singleunit vbo-tableaux-booking-checkout"><span>&nbsp;</span></div>';
@@ -610,8 +816,12 @@
610 816 $day_booking_snake .= '<div class="vbo-tableaux-booking vbo-tableaux-booking-singleunit vbo-tableaux-booking-checkout"><span>&nbsp;</span></div>';
611 817 }
612 818 }
613 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 +
614 824 if ($room['units'] == 1 && $day_booking_data && !$day_booking_data['closure']) {
615 825 // build tableaux-style snake container for guest
616 826 $customer_descr = '';
617 827 if ($is_checkin) {
@@ -636,9 +846,9 @@
636 846 if (!empty($booking_avatar_src)) {
637 847 // make sure the alt attribute is not too long in case of broken images
638 848 $booking_avatar_alt = !empty($booking_avatar_alt) && strlen($booking_avatar_alt) > 15 ? '...' . substr($booking_avatar_alt, -12) : $booking_avatar_alt;
639 849 // append booking avatar image
640 - $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>';
641 851 }
642 852
643 853 // customer name
644 854 $customer_fullname = trim($day_booking_data['first_name'] . ' ' . $day_booking_data['last_name']);
@@ -688,9 +898,9 @@
688 898 $booking_avatar_alt = $logo_helper->provenience;
689 899 // make sure the alt attribute is not too long in case of broken images
690 900 $booking_avatar_alt = !empty($booking_avatar_alt) && strlen($booking_avatar_alt) > 15 ? '...' . substr($booking_avatar_alt, -12) : $booking_avatar_alt;
691 901 // append booking avatar image
692 - $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>';
693 903 }
694 904 }
695 905 // set customer nominative built
696 906 $customer_descr .= '<span class="vbo-tableaux-guest-name">' . $customer_nominative . '</span>';
@@ -709,10 +919,20 @@
709 919 // not supported with the sticky table headers layout
710 920 $day_booking_snake = '';
711 921 }
712 922
713 - $write_units = strpos($dclass, "subroom-busy") !== false ? '' : $write_units;
714 - $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 + }
715 935 $dclass .= $is_checkin === true ? ' vbo-checkinday' : '';
716 936
717 937 // build proper link
718 938 if ($totfound > 1) {
@@ -722,20 +942,20 @@
722 942 // regular situation with one booking only
723 943 $cell_link = 'index.php?option=com_vikbooking&task=editbusy&cid[]=' . $bid . '&goto=overv';
724 944 }
725 945 ?>
726 - <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); ?>">
727 947 <?php
728 948 if (!$day_booking_snake) {
729 - if ($is_checkin === true && !$stopdrag) {
949 + if ($is_checkin === true && !$stopdrag && !$day_has_closure && !$is_subunit_unassigned_row) {
730 950 ?>
731 951 <span class="vbo-draggable-sp" draggable="true">
732 - <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>
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>
733 953 </span>
734 954 <?php
735 955 } else {
736 956 ?>
737 - <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>
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>
738 958 <?php
739 959 }
740 960 }
741 961 echo $day_booking_snake . $critical_note;
@@ -742,8 +962,14 @@
742 962 ?>
743 963 </td>
744 964 <?php
745 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 + }
746 972 ?>
747 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); ?>">
748 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>
749 975 <?php echo $critical_note; ?>
@@ -750,14 +976,20 @@
750 976 </td>
751 977 <?php
752 978 } else {
753 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 + }
754 986 ?>
755 987 <td align="center" class="<?php echo $dclass; ?>" data-day="<?php echo $cur_day_key; ?>" data-bids="">
756 988 <?php
757 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]) {
758 990 if ($is_subunit && isset($room_bookings_pool[$room['id']][$prev_day_key][$room['unit_index']])) {
759 - 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)) {
760 992 // prepend checkout snake
761 993 echo '<div class="vbo-tableaux-booking vbo-tableaux-booking-singleunit vbo-tableaux-booking-checkout"><span>&nbsp;</span></div>';
762 994 }
763 995 } elseif (!$is_subunit && $room['units'] == 1 && isset($room_bookings_pool[$room['id']][$prev_day_key][0])) {
@@ -778,35 +1010,146 @@
778 1010 }
779 1011
780 1012 // room row parsed, check if we have to parse a sub-unit next
781 1013 if (!$is_subunit && $room_bids_pool && isset($rooms_features_map[$room['id']])) {
782 - // load bookings for distinctive features when parsing the parent $room array
783 - $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 + }
784 1026 if ($room_indexes_bids) {
1027 + // set bookings allocated per room index
785 1028 $rooms_features_bookings[$room['id']] = $room_indexes_bids;
786 - $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 + }
787 1037 // map sub-unit room bookings
788 1038 foreach ($room_indexes_bids as $rindex => $rindex_bids) {
789 - if (!is_array($rindex_bids) || !$rindex_bids) {
790 - 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 + }
791 1072 }
792 - foreach ($rindex_bids as $seek_bid) {
793 - foreach ($room_bookings as $day_key => $day_ress) {
794 - foreach ($day_ress as $day_res) {
795 - 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
796 1115 continue;
797 1116 }
798 - if (!isset($room_bookings_pool[$room['id']])) {
799 - $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;
800 1122 }
801 - if (!isset($room_bookings_pool[$room['id']][$day_key])) {
802 - $room_bookings_pool[$room['id']][$day_key] = [];
803 - }
804 - $room_bookings_pool[$room['id']][$day_key][$rindex] = $day_res;
805 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++;
806 1145 }
807 1146 }
808 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 + }
809 1152 }
810 1153 }
811 1154
812 1155 // close row
@@ -835,22 +1178,43 @@
835 1178 if (hasNewBooking === true) {
836 1179 location.reload();
837 1180 }
838 1181 }
839 -function vboCallOpenJModal(identif, baseurl) {
1182 +function vboCallOpenJModal(identif, baseurl, options) {
1183 + options = typeof options !== 'object' ? {} : options;
1184 +
840 1185 if (last_room_click && last_room_click.length) {
841 1186 baseurl += '&cid[]='+last_room_click;
1187 + options.room_id = last_room_click;
842 1188 last_room_click = '';
843 1189 }
844 1190 if (last_date_click && last_date_click.length) {
845 1191 baseurl += '&checkin='+last_date_click;
1192 + options.checkin = last_date_click;
846 1193 last_date_click = '';
847 1194 }
848 1195 if (next_date_click && next_date_click.length) {
849 1196 baseurl += '&checkout='+next_date_click;
1197 + options.checkout = next_date_click;
850 1198 next_date_click = '';
851 1199 }
852 - 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 + }
853 1217 }
854 1218 </script>
855 1219 <?php
856 1220 echo $vbo_app->getJmodalScript('', 'vboJModalHideCallback();', '');
@@ -880,15 +1244,14 @@
880 1244 <a class="vbo-basenavuri-details" href="index.php?option=com_vikbooking&task=editorder&goto=overv&cid[]=%d" style="display: none;"></a>
881 1245 <a class="vbo-basenavuri-edit" href="index.php?option=com_vikbooking&task=editbusy&goto=overv&cid[]=%d" style="display: none;"></a>
882 1246
883 1247 <script type="text/Javascript">
1248 +const vboOvervMonthNames = <?php echo json_encode($month_names_cache ?? (new stdClass)); ?>;
1249 +
884 1250 var hovtimer;
885 1251 var hovtip = false;
886 1252 var vbodialogorph_on = false;
887 1253 var isdragging = false;
888 -var vboMessages = {
889 - currencySymb: "<?php echo $currencysymb; ?>"
890 -};
891 1254 var debug_mode = '<?php echo $pdebug; ?>';
892 1255 var bctags_count = <?php echo count($colortags); ?>;
893 1256 var bctags_pool = <?php echo json_encode($colortags); ?>;
894 1257 <?php
@@ -923,9 +1286,9 @@
923 1286 }
924 1287 // check action
925 1288 if (action < 0) {
926 1289 // stop reminding, set cookie
927 - var nd = new Date();
1290 + let nd = new Date();
928 1291 nd.setTime(nd.getTime() + (365*24*60*60*1000));
929 1292 document.cookie = "vboHideOrphans=1; expires=" + nd.toUTCString() + "; path=/; SameSite=Lax";
930 1293 }
931 1294 }
@@ -942,11 +1305,11 @@
942 1305 cellspool.push(cellelem);
943 1306 while (loop === true) {
944 1307 var next = cellelem.next('td');
945 1308 if (next === undefined || !next.length) {
946 - //attempt to go to the month after
947 - var partable = cellelem.closest('tr').closest('table').nextAll('table.vboverviewtable').first();
1309 + // attempt to go to the month after
948 1310 var nextmonth = false;
1311 + var partable = cellelem.closest('.vbo-overv-montable-wrap').next('.vbo-overv-montable-wrap').find('table.vboverviewtable');
949 1312 if (partable !== undefined && partable.length) {
950 1313 partable.find('tr.vboverviewtablerow').each(function() {
951 1314 var roomexists = jQuery(this).find('td').first();
952 1315 if (roomexists !== undefined && roomexists.length) {
@@ -958,9 +1321,9 @@
958 1321 }
959 1322 });
960 1323 }
961 1324 if (nextmonth === false) {
962 - //nothing was found in the month after
1325 + // nothing was found in the month after
963 1326 loop = false;
964 1327 break;
965 1328 }
966 1329 }
@@ -989,10 +1352,10 @@
989 1352 while (loop === true) {
990 1353 var next = cellelem.next('td');
991 1354 if (next === undefined || !next.length) {
992 1355 // attempt to go to the month after
993 - var partable = cellelem.closest('tr').closest('table').nextAll('table.vboverviewtable').first();
994 1356 var nextmonth = false;
1357 + var partable = cellelem.closest('.vbo-overv-montable-wrap').next('.vbo-overv-montable-wrap').find('table.vboverviewtable');
995 1358 if (partable !== undefined && partable.length) {
996 1359 partable.find('tr.vboverviewtablerow').each(function() {
997 1360 var roomexists = jQuery(this).find('td').first();
998 1361 if (roomexists !== undefined && roomexists.length) {
@@ -1062,10 +1425,8 @@
1062 1425 e4j_debug: debug_mode
1063 1426 },
1064 1427 (res) => {
1065 1428 if (res.indexOf('e4j.error') >= 0 ) {
1066 - // console.log('doAlterBooking-- Booking ID: '+bid+' - Old Room ID: '+roomid+' - New Room ID: '+landrid+' - New Date From: '+nowdatefrom+' - New Date To: '+nowdateto);
1067 - // console.log(res);
1068 1429 alert(res.replace("e4j.error.", ""));
1069 1430 //restore the old cells
1070 1431 jQuery('td.vbo-dragging-cells-tmp').removeClass('vbo-dragging-cells-tmp');
1071 1432 jQuery('td.vbo-dragged-cells-tmp').removeClass('vbo-dragged-cells-tmp');
@@ -1219,10 +1580,10 @@
1219 1580 celldata.push(elem.parent('tr').attr('data-subroomid'));
1220 1581 celldata.push(elem.attr('data-day'));
1221 1582 }
1222 1583 hovtimer = setTimeout(() => {
1223 - if (isdragging) {
1224 - // 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
1225 1586 unregisterHoveringTooltip();
1226 1587
1227 1588 return;
1228 1589 }
@@ -1380,11 +1741,11 @@
1380 1741 var subroomids = celldata[0].split('-');
1381 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>";
1382 1743 }
1383 1744 bcont += "<div class=\"vbo-overview-tip-bookingcont-total\">";
1384 - 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>";
1385 1746 if (v.totpaid > 0.00) {
1386 - 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\">" + 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>";
1387 1748 }
1388 1749 var getnotes = v.adminnotes;
1389 1750 if (getnotes !== null && getnotes.length) {
1390 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>";
@@ -1497,14 +1858,27 @@
1497 1858 );
1498 1859 }
1499 1860
1500 1861 /**
1501 - * 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..
1502 1864 */
1503 1865 function vboMoveSubunit(bid, rid, old_rindex, new_rindex, dday) {
1504 - 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')); ?>);
1505 1873 return false;
1506 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 +
1507 1881 // check if movement can be made
1508 1882 var cur_tr = jQuery('tr.vboverviewtablerow-subunit[data-subroomid="' + rid + '-' + old_rindex + '"]');
1509 1883 if (!cur_tr || !cur_tr.length) {
1510 1884 console.error('could not find the parent row of the subunit cells');
@@ -1559,8 +1933,9 @@
1559 1933 copydday = copyfirstcell.attr('data-day');
1560 1934 var copydestcell = dest_tr.find('td.subnotbusy[data-day="' + copydday + '"]');
1561 1935 if (!copydestcell || !copydestcell.length) {
1562 1936 console.error('could not find the next free destination cell for ' + copydday);
1937 + alert(<?php echo json_encode(JText::translate('VBBOOKNOTMADE')); ?>);
1563 1938 freedates = false;
1564 1939 loop = false;
1565 1940 }
1566 1941 } else {
@@ -1581,13 +1956,14 @@
1581 1956 tmpl: "component",
1582 1957 bid: bid,
1583 1958 rid: rid,
1584 1959 old_rindex: old_rindex,
1585 - 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,
1586 1963 },
1587 1964 (res) => {
1588 1965 if (res.indexOf('e4j.error') >= 0 ) {
1589 - // console.log(res);
1590 1966 alert(res.replace("e4j.error.", ""));
1591 1967
1592 1968 // restore loading opacity in container
1593 1969 jQuery('.vbo-overview-tipblock').css('opacity', '1');
@@ -1607,8 +1983,13 @@
1607 1983 if (firstcell.hasClass('vbo-checkinday')) {
1608 1984 firstcell.removeClass('vbo-checkinday');
1609 1985 destcell.addClass('vbo-checkinday');
1610 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
1611 1992 firstcell.children().not('.vbo-tableaux-booking-checkout').not('.vbo-roomdaynote-trigger').appendTo(destcell);
1612 1993
1613 1994 // re-bind hover event for tooltip
1614 1995 jQuery(destcell).hover(function() {
@@ -1618,9 +1999,9 @@
1618 1999 // clean up the previous cell from which we moved data
1619 2000 firstcell.removeClass('subroom-busy').addClass('subnotbusy').attr('data-bids', '');
1620 2001
1621 2002 // check if we have a next iteration to perform
1622 - var nextsib = firstcell.next();
2003 + let nextsib = firstcell.next();
1623 2004 if (nextsib && nextsib.length && nextsib.attr('data-bids') && nextsib.attr('data-bids').indexOf('-' + bid + '-') >= 0) {
1624 2005 // prepare data for the next iteration
1625 2006 firstcell = nextsib;
1626 2007 dday = firstcell.attr('data-day');
@@ -1646,11 +2027,14 @@
1646 2027 }
1647 2028 }
1648 2029 }
1649 2030 }
2031 +
2032 + // dispatch the event for the sub-units updated
2033 + VBOCore.emitEvent('vbo-overv-sub-units-updated');
1650 2034 },
1651 2035 (err) => {
1652 - alert("Request Failed");
2036 + alert('Request Failed');
1653 2037 // restore loading opacity in container
1654 2038 jQuery('.vbo-overview-tipblock').css('opacity', '1');
1655 2039 }
1656 2040 );
@@ -1658,11 +2042,21 @@
1658 2042 return true;
1659 2043 }
1660 2044
1661 2045 /**
1662 - * 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.
1663 2047 */
1664 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 + }
1665 2059 if (!confirm(Joomla.JText._('VBO_CONF_SWAP_RNUMB').replace('%s', old_rindex).replace('%s', new_rindex))) {
1666 2060 return false;
1667 2061 }
1668 2062 if (!bid || !bidtwo || !rid || !old_rindex || !new_rindex || !dday) {
@@ -1669,24 +2063,24 @@
1669 2063 console.error('Missing required arguments', bid, bidtwo, rid, old_rindex, new_rindex, dday);
1670 2064 return false;
1671 2065 }
1672 2066 // check if movement can be made
1673 - 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 + '"]');
1674 2068 if (!cur_tr || !cur_tr.length) {
1675 2069 console.error('could not find the parent row of the subunit cells');
1676 2070 return false;
1677 2071 }
1678 - var maincell = cur_tr.find('td.subroom-busy[data-day="' + dday + '"]');
2072 + let maincell = cur_tr.find('td.subroom-busy[data-day="' + dday + '"]');
1679 2073 if (!maincell || !maincell.length) {
1680 2074 console.error('could not find the main cell of the subunit to move');
1681 2075 return false;
1682 2076 }
1683 - 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 + '"]');
1684 2078 if (!dest_tr || !dest_tr.length) {
1685 2079 console.error('could not find the destination row for the subunit cells');
1686 2080 return false;
1687 2081 }
1688 - var destcell = dest_tr.find('td.subroom-busy[data-day="' + dday + '"]');
2082 + let destcell = dest_tr.find('td.subroom-busy[data-day="' + dday + '"]');
1689 2083 if (!destcell || !destcell.length) {
1690 2084 console.error('could not find the destination cell of the subunit to move');
1691 2085 return false;
1692 2086 }
@@ -1691,22 +2085,38 @@
1691 2085 return false;
1692 2086 }
1693 2087
1694 2088 // gather the cells for the swop
1695 - var swap_cells_one = [];
1696 - var swap_cells_two = [];
1697 - var swap_dates_one = [];
1698 - var swap_dates_two = [];
1699 - var swap_cell_from = maincell;
1700 - 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;
1701 2097
1702 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 + }
1703 2113 while (true) {
1704 2114 if (!swap_cell_from || !swap_cell_from.length) {
1705 2115 // next cell not found
1706 2116 break;
1707 2117 }
1708 - var swap_bids = swap_cell_from.attr('data-bids');
2118 + let swap_bids = swap_cell_from.attr('data-bids');
1709 2119 if (!swap_bids || swap_bids.indexOf('-' + bid + '-') < 0) {
1710 2120 // last occupied cell reached
1711 2121 break;
1712 2122 }
@@ -1712,8 +2122,9 @@
1712 2122 }
1713 2123 // push cloned cell data to move
1714 2124 swap_cells_one.push(swap_cell_from.children().not('.vbo-tableaux-booking-checkout').not('.vbo-roomdaynote-trigger').clone(true));
1715 2125 swap_dates_one.push(swap_cell_from.attr('data-day'));
2126 + swap_styles_one.push(swap_cell_from.attr('style'));
1716 2127 // set next iteration cell
1717 2128 swap_cell_from = swap_cell_from.next();
1718 2129 }
1719 2130
@@ -1722,9 +2133,9 @@
1722 2133 if (!swap_cell_to || !swap_cell_to.length) {
1723 2134 // next cell not found
1724 2135 break;
1725 2136 }
1726 - var swap_bids = swap_cell_to.attr('data-bids');
2137 + let swap_bids = swap_cell_to.attr('data-bids');
1727 2138 if (!swap_bids || swap_bids.indexOf('-' + bidtwo + '-') < 0) {
1728 2139 // last occupied cell reached
1729 2140 break;
1730 2141 }
@@ -1730,28 +2141,33 @@
1730 2141 }
1731 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
1732 2143 if (!swap_cells_two.length && !swap_cell_to.hasClass('vbo-checkinday')) {
1733 2144 // prepend first the cells until the check-in or until not found
1734 - var prev_cell = swap_cell_to;
2145 + let prev_cell = swap_cell_to;
1735 2146 while (true) {
1736 2147 prev_cell = prev_cell.prev();
1737 2148 if (!prev_cell || !prev_cell.length) {
1738 2149 break;
1739 2150 }
1740 - 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');
1741 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'))) {
1742 2153 // prepend cloned cell data to move
1743 2154 swap_cells_two.unshift(prev_cell.children().not('.vbo-tableaux-booking-checkout').not('.vbo-roomdaynote-trigger').clone(true));
1744 2155 swap_dates_two.unshift(prev_cell.attr('data-day'));
2156 + swap_styles_two.unshift(prev_cell.attr('style'));
1745 2157 } else {
1746 2158 // no more previous cells
1747 2159 break;
1748 2160 }
2161 + if (prev_cell.hasClass('vbo-checkinday')) {
2162 + break;
2163 + }
1749 2164 }
1750 2165 }
1751 2166 // push cloned cell data to move
1752 2167 swap_cells_two.push(swap_cell_to.children().not('.vbo-tableaux-booking-checkout').not('.vbo-roomdaynote-trigger').clone(true));
1753 2168 swap_dates_two.push(swap_cell_to.attr('data-day'));
2169 + swap_styles_two.push(swap_cell_to.attr('style'));
1754 2170 // set next iteration cell
1755 2171 swap_cell_to = swap_cell_to.next();
1756 2172 }
1757 2173
@@ -1760,9 +2176,9 @@
1760 2176 return false;
1761 2177 }
1762 2178
1763 2179 // walk over the cells of the first room to swap and check the corresponding destination
1764 - var check_destcell = destcell;
2180 + let check_destcell = destcell;
1765 2181 for (let i = 0; i < swap_cells_one.length; i++) {
1766 2182 if (!check_destcell || !check_destcell.length) {
1767 2183 // this cell does not exist, abort
1768 2184 console.error('destination cell to #' + (i + 1) + ' was not found');
@@ -1767,12 +2183,13 @@
1767 2183 // this cell does not exist, abort
1768 2184 console.error('destination cell to #' + (i + 1) + ' was not found');
1769 2185 return false;
1770 2186 }
1771 - var dest_bids = check_destcell.attr('data-bids');
2187 + let dest_bids = check_destcell.attr('data-bids');
1772 2188 if (dest_bids && dest_bids.length && dest_bids.indexOf('-' + bidtwo + '-') < 0) {
1773 2189 // this cell is already occupied, abort
1774 2190 console.error('destination cell to #' + (i + 1) + ' is occupied by another reservation');
2191 + alert(<?php echo json_encode(JText::translate('VBBOOKNOTMADE')); ?>);
1775 2192 return false;
1776 2193 }
1777 2194 // set next destination cell
1778 2195 check_destcell = check_destcell.next();
@@ -1785,12 +2202,13 @@
1785 2202 // this cell does not exist, abort
1786 2203 console.error('destination cell from #' + (i + 1) + ' was not found');
1787 2204 return false;
1788 2205 }
1789 - var dest_bids = check_destcell.attr('data-bids');
2206 + let dest_bids = check_destcell.attr('data-bids');
1790 2207 if (dest_bids && dest_bids.length && dest_bids.indexOf('-' + bid + '-') < 0) {
1791 2208 // this cell is already occupied, abort
1792 2209 console.error('destination cell from #' + (i + 1) + ' is occupied by another reservation');
2210 + alert(<?php echo json_encode(JText::translate('VBBOOKNOTMADE')); ?>);
1793 2211 return false;
1794 2212 }
1795 2213 // set next destination cell
1796 2214 check_destcell = check_destcell.next();
@@ -1808,12 +2226,15 @@
1808 2226 checkin: dday,
1809 2227 tmpl: 'component'
1810 2228 },
1811 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 +
1812 2236 // move the cells of the first sub-unit onto the cells of the second sub-unit
1813 - var target_cell = destcell;
1814 - var current_cell = maincell;
1815 - var swap_diff_dates = false;
1816 2237 for (let counter = 0; counter < swap_cells_one.length; counter++) {
1817 2238 // remove elements from target
1818 2239 target_cell.children().not('.vbo-tableaux-booking-checkout').not('.vbo-roomdaynote-trigger').remove();
1819 2240 // swap cell elements
@@ -1819,8 +2240,17 @@
1819 2240 // swap cell elements
1820 2241 swap_cells_one[counter].appendTo(target_cell);
1821 2242 // set proper bids attribute
1822 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 + }
1823 2253 // check if we are swapping with a non check-in day sub-unit occupied cell
1824 2254 if (counter === 0 && !target_cell.hasClass('vbo-checkinday')) {
1825 2255 target_cell.addClass('vbo-checkinday');
1826 2256 swap_diff_dates = true;
@@ -1827,9 +2257,9 @@
1827 2257 } else if (counter > 0 && !target_cell.hasClass('subroom-busy')) {
1828 2258 target_cell.removeClass('subnotbusy').addClass('subroom-busy');
1829 2259 }
1830 2260 // check if the reservations have different nights of stay
1831 - var cell_will_occupy = true;
2261 + let cell_will_occupy = true;
1832 2262 if (counter > 0 && swap_diff_dates && !swap_dates_two.includes(swap_dates_one[counter])) {
1833 2263 // the swapped unit will not occupy this cell, so clean it
1834 2264 cell_will_occupy = false;
1835 2265 } else if ((counter + 1) > swap_cells_two.length) {
@@ -1855,11 +2285,12 @@
1855 2285
1856 2286 // move the cells of the second sub-unit onto the cells of the first sub-unit
1857 2287 target_cell = maincell;
1858 2288 current_cell = destcell;
2289 +
1859 2290 // check if we are swapping with a non check-in day sub-unit occupied cell
1860 2291 if (swap_diff_dates) {
1861 - var prev_land_cell = current_cell;
2292 + let prev_land_cell = current_cell;
1862 2293 while (true) {
1863 2294 prev_land_cell = prev_land_cell.prev();
1864 2295 if (!prev_land_cell || !prev_land_cell.length) {
1865 2296 break;
@@ -1878,8 +2309,9 @@
1878 2309 break;
1879 2310 }
1880 2311 }
1881 2312 }
2313 +
1882 2314 for (let counter = 0; counter < swap_cells_two.length; counter++) {
1883 2315 // remove elements from target
1884 2316 target_cell.children().not('.vbo-tableaux-booking-checkout').not('.vbo-roomdaynote-trigger').remove();
1885 2317 // swap cell elements
@@ -1885,8 +2317,15 @@
1885 2317 // swap cell elements
1886 2318 swap_cells_two[counter].appendTo(target_cell);
1887 2319 // set proper bids attribute
1888 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 + }
1889 2328 // check if we are swapping with a non check-in day sub-unit occupied cell
1890 2329 if (counter === 0 && !target_cell.hasClass('vbo-checkinday')) {
1891 2330 target_cell.addClass('vbo-checkinday');
1892 2331 if (current_cell.hasClass('vbo-checkinday') && !swap_dates_one.includes(swap_dates_two[counter])) {
@@ -1898,9 +2337,10 @@
1898 2337 if (!target_cell.hasClass('subroom-busy')) {
1899 2338 target_cell.removeClass('subnotbusy').addClass('subroom-busy');
1900 2339 }
1901 2340 // check if the reservations have different nights of stay
1902 - 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
1903 2343 // remove the children elements from this longer reservation
1904 2344 current_cell.children().not('.vbo-tableaux-booking-checkout').not('.vbo-roomdaynote-trigger').remove();
1905 2345 // remove the occupied class as well
1906 2346 current_cell.removeClass('subroom-busy').addClass('subnotbusy').attr('data-bids', '');
@@ -1905,8 +2345,14 @@
1905 2345 // remove the occupied class as well
1906 2346 current_cell.removeClass('subroom-busy').addClass('subnotbusy').attr('data-bids', '');
1907 2347 // set the occupied class onto the target
1908 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);
1909 2355 }
1910 2356 // set next target cell
1911 2357 target_cell = target_cell.next();
1912 2358 // update current cell
@@ -1917,8 +2363,11 @@
1917 2363 if (target_cell && target_cell.length && current_cell && current_cell.length && current_cell.find('.vbo-tableaux-booking-checkout').length) {
1918 2364 // make sure to move just the first check-out snake, because snakes with equal duration may have moved the check-out here already
1919 2365 current_cell.find('.vbo-tableaux-booking-checkout').first().prependTo(target_cell);
1920 2366 }
2367 +
2368 + // dispatch the event for the sub-units updated
2369 + VBOCore.emitEvent('vbo-overv-sub-units-updated');
1921 2370 },
1922 2371 (err) => {
1923 2372 alert(err.responseText);
1924 2373 console.error(err.responseText);
@@ -1938,8 +2387,239 @@
1938 2387 window.open(open_url, '_blank');
1939 2388 }
1940 2389
1941 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 +/**
1942 2622 * DOM ready state
1943 2623 */
1944 2624 jQuery(function() {
1945 2625 // register to the event emitted when a new booking is created through an admin widget
@@ -1951,8 +2631,35 @@
1951 2631 // reload the page to display the new booking just created
1952 2632 location.reload();
1953 2633 });
1954 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 +
1955 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 */
1956 2663 jQuery("td.vbo-checkinday span.vbo-draggable-sp").each(function(k, v) {
1957 2664 var parentheight = jQuery(this).closest('td').height();
1958 2665 var spheight = jQuery(this).height();
@@ -1974,9 +2681,9 @@
1974 2681 if (hovtip === true) {
1975 2682 hideVboTooltip();
1976 2683 }
1977 2684
1978 - var parentcell = jQuery(this).closest('td');
2685 + let parentcell = jQuery(this).closest('td');
1979 2686 if (parentcell.hasClass('busytmplock') || parentcell.hasClass('busy-sharedcalendar')) {
1980 2687 return false;
1981 2688 }
1982 2689
@@ -1981,18 +2688,46 @@
1981 2688 }
1982 2689
1983 2690 // reset pool of booked date cells
1984 2691 cellspool = [];
1985 - var dt = e.originalEvent.dataTransfer;
1986 - var bidstart = parentcell.attr('data-bids');
1987 - var checkind = parentcell.attr('data-day');
1988 - 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');
1989 2697 if (!bidstart.length || !checkind.length || !roomid.length) {
1990 2698 return false;
1991 2699 }
1992 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 +
1993 2728 // count booking nights and set booked date cells
1994 - var totnights = countBookingCells(parentcell, bidstart, roomid);
2729 + let totnights = countBookingCells(parentcell, bidstart, roomid);
1995 2730
1996 2731 // add dragging class
1997 2732 if (jQuery(this).find('.vbo-tableaux-guest-name').length) {
1998 2733 jQuery(this).find('.vbo-tableaux-guest-name').addClass('vbo-dragging-sp');
@@ -2001,13 +2736,30 @@
2001 2736 jQuery(this).addClass('vbo-dragging-sp');
2002 2737 }
2003 2738 }
2004 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 +
2005 2753 // populate dataTransfer data
2006 - var parent_rid = '';
2007 - var subunit_id = '';
2754 + let parent_rid = '';
2755 + let subunit_id = '';
2008 2756 if (parentcell.hasClass('subroom-busy')) {
2009 - 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 + }
2010 2762 parent_rid = subdata[0];
2011 2763 subunit_id = subdata[1];
2012 2764 }
2013 2765 dt.setData('Checkin', checkind);
@@ -2023,19 +2775,19 @@
2023 2775 // always prevent default
2024 2776 e.preventDefault();
2025 2777
2026 2778 // gather dragging cell data
2027 - var dt = e.originalEvent.dataTransfer;
2028 - var checkind = dt.getData('Checkin');
2029 - var roomid = dt.getData('Roomid');
2030 - var bid = dt.getData('Bid');
2031 - var totnights = dt.getData('Nights');
2032 - var parent_rid = dt.getData('Parentrid');
2033 - 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');
2034 2786
2035 2787 // check if we are hovering something that requires styling
2036 2788 if (e.type === 'dragover') {
2037 - var hover_cell = jQuery(this);
2789 + let hover_cell = jQuery(this);
2038 2790 if (hover_cell.hasClass('subroom-busy') && hover_cell.hasClass('vbo-checkinday')) {
2039 2791 if (hover_cell.attr('data-day') != checkind && totnights > 1) {
2040 2792 hover_cell.find('.vbo-tableaux-booking-checkin').addClass('vbo-droppable-cell');
2041 2793 }
@@ -2053,11 +2805,11 @@
2053 2805 // always reset pool of cells
2054 2806 newcellspool = [];
2055 2807
2056 2808 /* check if drop is allowed */
2057 - var landrow = jQuery(this).closest('tr');
2058 - var landrid = landrow.find('td').first().attr('data-roomid');
2059 - 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');
2060 2812 if (landrid === undefined || !landrid.length) {
2061 2813 return false;
2062 2814 }
2063 2815
@@ -2068,9 +2820,9 @@
2068 2820 }
2069 2821 }
2070 2822
2071 2823 // build pool of cells
2072 - var freenights = countCellFreeNights(jQuery(this), landrid, totnights, bid);
2824 + let freenights = countCellFreeNights(jQuery(this), landrid, totnights, bid);
2073 2825 if (freenights < totnights) {
2074 2826 alert(Joomla.JText._('VBOVWDNDERRNOTENCELLS').replace('%s', freenights).replace('%d', totnights));
2075 2827 return false;
2076 2828 }
@@ -2075,15 +2827,19 @@
2075 2827 return false;
2076 2828 }
2077 2829
2078 2830 // collect the new dates
2079 - var nowdatefrom = jQuery(newcellspool[0]).attr('data-day');
2080 - 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');
2081 2833
2082 2834 // check if we are just moving a sub-unit onto another for the same dates
2083 2835 if (jQuery(this).hasClass('subnotbusy') || jQuery(this).hasClass('subroom-busy')) {
2084 2836 // dropping on a sub-unit is only allowed onto the same room ID, for the same dates (switch room index only)
2085 - 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 + }
2086 2842 if (!subdata || !parent_rid || !subunit_id || parent_rid != subdata[0] || !subdata[1] || subunit_id == subdata[1]) {
2087 2843 // display proper error
2088 2844 if (subdata[1] && subunit_id == subdata[1]) {
2089 2845 // must use the same dates for sub-units
@@ -2123,9 +2879,9 @@
2123 2879 v.addClass('vbo-dragging-cells-tmp');
2124 2880 });
2125 2881
2126 2882 // bookings for multiple rooms should add the same dragging class also to the booking for the other rooms
2127 - var samebidcells = jQuery("td.vbo-checkinday[data-bids='" + bid + "']");
2883 + let samebidcells = jQuery("td.vbo-checkinday[data-bids='" + bid + "']");
2128 2884 if (samebidcells.length > 1) {
2129 2885 jQuery("td[data-bids='" + bid + "']").each(function(k, v) {
2130 2886 if (!jQuery(v).hasClass('vbo-dragging-cells-tmp')) {
2131 2887 jQuery(v).addClass('vbo-dragging-cells-tmp');
@@ -2137,9 +2893,9 @@
2137 2893 });
2138 2894
2139 2895 /* populate and showing loading message */
2140 2896 jQuery('.vbo-loading-dnd-head').text(Joomla.JText._('VBOVWDNDMOVINGBID').replace('%d', bid));
2141 - var movingmess = '';
2897 + let movingmess = '';
2142 2898 if (roomid != landrid) {
2143 2899 movingmess += Joomla.JText._('VBOVWDNDMOVINGROOM').replace('%s', jQuery("td[data-roomid='"+landrid+"']").first().find('span.vbo-overview-roomname').text() );
2144 2900 }
2145 2901 if (nowdatefrom != jQuery(cellspool[0]).attr('data-day') || nowdateto != jQuery(cellspool[(cellspool.length -1)]).attr('data-day')) {
@@ -2167,9 +2923,9 @@
2167 2923 jQuery(document.body).on("dragend", "td.vbo-checkinday span, td.vbo-checkinday .vbo-tableaux-booking-checkin", function(e) {
2168 2924 // stop dragging to restore tooltip functions
2169 2925 isdragging = false;
2170 2926 // we could also access the originally dragged cell
2171 - var dt = e.originalEvent.dataTransfer;
2927 + let dt = e.originalEvent.dataTransfer;
2172 2928 // restore temporary classes
2173 2929 jQuery('.vbo-dragging-sp').removeClass('vbo-dragging-sp');
2174 2930 jQuery('.vbo-droppable-cell').removeClass('vbo-droppable-cell');
2175 2931 });
@@ -2182,9 +2938,9 @@
2182 2938 roomid = roomid && roomid.length ? roomid : '';
2183 2939 last_room_click = '';
2184 2940 last_date_click = '';
2185 2941 next_date_click = '';
2186 - 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});
2187 2943 });
2188 2944
2189 2945 jQuery(document.body).on('click', 'td.busy, td.notbusy, td.busytmplock, td.subroom-busy', function(e) {
2190 2946 if (jQuery(this).hasClass('vbo-widget-booskcal-cell-mday')) {
@@ -2211,32 +2967,59 @@
2211 2967 // trigger mouseleave event to prevent tooltip from showing
2212 2968 jQuery(this).trigger('mouseleave');
2213 2969
2214 2970 // get room name, date and day-bids
2215 - var room_name = '';
2216 - var sub_room_data = '';
2217 - var room_id = 0;
2971 + let room_name = '';
2972 + let sub_room_data = '';
2973 + let room_id = 0;
2218 2974 if (jQuery(this).hasClass('subroom-busy')) {
2219 2975 sub_room_data = jQuery(this).parent('tr').attr('data-subroomid');
2220 2976 room_id = sub_room_data.split('-')[0];
2221 2977 room_name = jQuery('td.roomname[data-roomid="' + room_id + '"]').not('.subroomname').first().find('.vbo-overview-roomname').text();
2222 2978 } else {
2223 - var main_room_cell = jQuery(this).parent('tr').find('td.roomname');
2979 + let main_room_cell = jQuery(this).parent('tr').find('td.roomname');
2224 2980 room_id = main_room_cell.attr('data-roomid');
2225 2981 room_name = main_room_cell.find('.vbo-overview-roomname').text();
2226 2982 }
2227 - var date_ymd = jQuery(this).attr('data-day');
2228 - var date_read = jQuery('.bluedays[data-ymd="' + date_ymd + '"]').attr('data-readymd');
2229 - var date_bids = jQuery(this).attr('data-bids');
2230 - var def_bicon = '<?php VikBookingIcons::e('user', 'vbo-dashboard-guest-activity-avatar-icon'); ?>';
2231 - 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'); ?>';
2232 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 +
2233 3015 // display modal with booking details
2234 - var rday_bookings_modal_body = VBOCore.displayModal({
3016 + let rday_bookings_modal_body = VBOCore.displayModal({
2235 3017 suffix: 'overv-rdaybookings',
2236 - extra_class: 'vbo-modal-rounded vbo-modal-tall vbo-modal-nofooter',
2237 - 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,
2238 3020 draggable: true,
3021 + footer_right: newResBtn,
2239 3022 dismiss_event: 'vbo-dismiss-modal-overv-rdaybookings',
2240 3023 loading_event: 'vbo-loading-modal-overv-rdaybookings',
2241 3024 loading_body: '<?php VikBookingIcons::e('circle-notch', 'fa-spin fa-fw'); ?>',
2242 3025 });
@@ -2323,10 +3106,10 @@
2323 3106 }
2324 3107 rday_booking.attr('data-idorder', obj_res[b]['id']);
2325 3108 rday_booking.on('click', function(e) {
2326 3109 var click_target = jQuery(e.target);
2327 - if (click_target.is('a') || click_target.is('select') || click_target.is('option')) {
2328 - return false;
3110 + if (click_target.is('a') || click_target.is('select') || click_target.is('option') || click_target.is('button')) {
3111 + return;
2329 3112 }
2330 3113 if ((click_target.is('span') && click_target.hasClass('label')) || click_target.is('img') || click_target.is('i')) {
2331 3114 // open booking details within the apposite admin-widget
2332 3115 e.stopPropagation();
@@ -2344,9 +3127,9 @@
2344 3127 // build booking structure
2345 3128 var rday_booking_html = '';
2346 3129 rday_booking_html += '<div class="vbo-dashboard-guest-activity-avatar">' + "\n";
2347 3130 if (obj_res[b]['avatar_src']) {
2348 - 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";
2349 3132 } else if (obj_res[b]['closure']) {
2350 3133 rday_booking_html += closure_i + "\n";
2351 3134 } else {
2352 3135 rday_booking_html += def_bicon + "\n";
@@ -2415,8 +3198,13 @@
2415 3198 // append drop down
2416 3199 rday_booking_html += set_subunit_node;
2417 3200 }
2418 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";
2419 3207 }
2420 3208 rday_booking_html += ' </div>' + "\n";
2421 3209 }
2422 3210 rday_booking_html += ' </div>' + "\n";
@@ -2534,16 +3322,28 @@
2534 3322 },
2535 3323 (res) => {
2536 3324 applying_tag = false;
2537 3325 if (res.indexOf('e4j.error') >= 0 ) {
2538 - // console.log(res);
2539 3326 alert(res.replace("e4j.error.", ""));
2540 - //restore loading opacity in circles
3327 + // restore loading opacity in circles
2541 3328 jQuery('.vbo-overview-tip-bctag-subtip-circle').css('opacity', '1');
2542 3329 } else {
2543 - var obj_res = typeof res === 'string' ? JSON.parse(res) : res;
2544 - var last_bid = jQuery(clickelem).closest(".vbo-hascolortag").attr('data-lastbid');
2545 - <?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
2546 3346 hideVboTooltip();
2547 3347 }
2548 3348 },
2549 3349 (err) => {
@@ -2567,21 +3367,21 @@
2567 3367 });
2568 3368
2569 3369 jQuery(document).mouseup(function(e) {
2570 3370 if (!hovtip && !vbodialogorph_on) {
2571 - return false;
3371 + return;
2572 3372 }
2573 3373 if (hovtip) {
2574 3374 var vbo_overlay_cont = jQuery(".vbo-overview-tipblock");
2575 3375 if (!vbo_overlay_cont.is(e.target) && vbo_overlay_cont.has(e.target).length === 0) {
2576 3376 hideVboTooltip();
2577 - return true;
3377 + return;
2578 3378 }
2579 3379 if (jQuery(".vbo-overview-tip-bctag-subtip").length) {
2580 3380 var vbo_overlay_subtip_cont = jQuery(".vbo-overview-tip-bctag-wrap");
2581 3381 if (!vbo_overlay_subtip_cont.is(e.target) && vbo_overlay_subtip_cont.has(e.target).length === 0) {
2582 3382 jQuery(".vbo-overview-tip-bctag-subtip").remove();
2583 - return true;
3383 + return;
2584 3384 }
2585 3385 }
2586 3386 }
2587 3387 if (vbodialogorph_on) {
@@ -2612,9 +3412,9 @@
2612 3412 VBOCore.storageSetItem('vbo_overv_subunits_status', storage_subunits_status);
2613 3413 } else {
2614 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');
2615 3415 // do not use .show() or "display: block" may be added rather than "display: table-row"
2616 - 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');
2617 3417 // update storage object
2618 3418 var storage_subunits_status = VBOCore.storageGetItem('vbo_overv_subunits_status');
2619 3419 try {
2620 3420 storage_subunits_status = JSON.parse(storage_subunits_status);
@@ -2645,11 +3445,15 @@
2645 3445 // do nothing
2646 3446 }
2647 3447
2648 3448 /* Default state for sticky table heads */
2649 - 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; ?>);
2650 3450
2651 - // 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 + */
2652 3456 var hideorphans = false;
2653 3457 var buiscuits = document.cookie;
2654 3458 if (buiscuits.length) {
2655 3459 var hideorphansck = "vboHideOrphans=1";
@@ -2656,9 +3460,9 @@
2656 3460 if (buiscuits.indexOf(hideorphansck) >= 0) {
2657 3461 hideorphans = true;
2658 3462 }
2659 3463 }
2660 - if (!hideorphans && <?php echo $vbo_auth_pricing ? 'true' : 'false'; ?>) {
3464 + if (false && !hideorphans && <?php echo $vbo_auth_pricing ? 'true' : 'false'; ?>) {
2661 3465 // make the request
2662 3466
2663 3467 VBOCore.doAjax(
2664 3468 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=orphanscount'); ?>",
@@ -2696,13 +3500,12 @@
2696 3500 vbodialogorph_on = true;
2697 3501 }
2698 3502 },
2699 3503 (err) => {
2700 - console.log("orphanscount Request Failed");
3504 + console.error("orphanscount Request Failed");
2701 3505 }
2702 3506 );
2703 3507 }
2704 - //
2705 3508
2706 3509 // fests
2707 3510 jQuery(document.body).on("click", "th.bluedays", function() {
2708 3511 if (jQuery(this).hasClass('skip-bluedays-click')) {
@@ -2738,9 +3541,8 @@
2738 3541 // set modal body
2739 3542 jQuery('.vbo-overv-mngfest-wrap').appendTo(fests_modal);
2740 3543 }
2741 3544 });
2742 - //
2743 3545
2744 3546 // room-day notes
2745 3547 jQuery(document.body).on("click", ".vbo-roomdaynote-display", function() {
2746 3548 if (!jQuery(this).closest('.vbo-roomdaynote-trigger').length) {
@@ -2780,10 +3582,175 @@
2780 3582
2781 3583 // populate current room day notes
2782 3584 vboRenderRdayNotes(roomday_info[0], roomday_info[1], roomday_info[2], readymd);
2783 3585 });
2784 - //
2785 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 +
2786 3753 <?php
2787 3754 /**
2788 3755 * Handle fixed table heads when on snake-layout (scroll), which is completely
2789 3756 * different than the table-layout sticky table headers (handled through jQuery).
@@ -2799,8 +3766,11 @@
2799 3766 let tables = document.querySelectorAll('table.vboverviewtable.vbo-overv-sticky-table-head-off');
2800 3767 tables.forEach((table) => {
2801 3768 table.parentNode.addEventListener('scroll', vboOvervHandleHScrollTableHead);
2802 3769 });
3770 +
3771 + // launch the handling function
3772 + vboOvervHandleHScrollTableHead();
2803 3773 <?php
2804 3774 }
2805 3775 ?>
2806 3776 });
@@ -2847,15 +3817,32 @@
2847 3817 function vboOvervHandleHScrollTableHead(e) {
2848 3818 // register throttling callback
2849 3819 VBOCore.throttleTimer(() => {
2850 3820 // horizontal scrolling happens on the parent DIV node
2851 - let tables = e.target.querySelectorAll('table.vboverviewtable.vbo-overv-sticky-table-head-off');
3821 + let tables = (e?.target || document).querySelectorAll('table.vboverviewtable.vbo-overv-sticky-table-head-off');
2852 3822 if (tables.length !== 1) {
2853 3823 // un-register event for missing table
2854 - e.target.removeEventListener('scroll', vboOvervHandleHScrollTableHead);
3824 + if (e?.target) {
3825 + e.target.removeEventListener('scroll', vboOvervHandleHScrollTableHead);
3826 + }
2855 3827 return;
2856 3828 }
2857 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 +
2858 3845 // pool of fixed table heads
2859 3846 let fixedTableHeadNodes = vboOvervBuildFixedTableHeads(tables);
2860 3847
2861 3848 if (fixedTableHeadNodes.length) {
@@ -2885,10 +3872,10 @@
2885 3872 let platformAdminBar = <?php echo VBOPlatformDetection::isWordPress() ? 'document.querySelector("#wpadminbar")' : '(document.querySelector(".navbar") || document.querySelector("#subhead-container"))'; ?>;
2886 3873 let fixedOffsetTop = platformAdminBar ? platformAdminBar.offsetHeight : 0;
2887 3874 if (fixedOffsetTop) {
2888 3875 // ensure the responsive mode has got a fixed/absolute positioning
2889 - let adminBarPosition = platformAdminBar.style.position || '';
2890 - if (adminBarPosition != 'fixed' && adminBarPosition != 'absolute') {
3876 + let adminBarPosition = window.getComputedStyle(platformAdminBar).getPropertyValue('position') || platformAdminBar.style.position || '';
3877 + if (adminBarPosition != 'fixed' && adminBarPosition != 'absolute' && adminBarPosition != 'sticky') {
2891 3878 // no fixed admin bar
2892 3879 fixedOffsetTop = 0;
2893 3880 }
2894 3881 }
@@ -2936,12 +3923,15 @@
2936 3923 );
2937 3924 fixedTableHeadWrap.appendChild(fixedTableHeadMonth);
2938 3925
2939 3926 // obtain the list of visible month-day cells
2940 - vboOvervVisibleTableHeadNodes(table, index).forEach((mday) => {
3927 + vboOvervVisibleTableHeadNodes(table, index).forEach((mday, mdayIndex) => {
2941 3928 // month-day cell element node
2942 3929 let fixedTableHeadMday = document.createElement('div');
2943 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 + }
2944 3934 fixedTableHeadMday.style.width = mday['vwidth'] + 'px';
2945 3935 fixedTableHeadMday.addEventListener('click', () => {
2946 3936 try {
2947 3937 // use jQuery to trigger the click event, or plain JS won't work
@@ -3051,8 +4041,69 @@
3051 4041 return visibleMonthDays;
3052 4042 }
3053 4043
3054 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 +/**
3055 4106 * Fests
3056 4107 */
3057 4108 function vboRenderFests(day, daytitle) {
3058 4109 // compose fests information
@@ -3130,9 +4181,8 @@
3130 4181 }
3131 4182 }
3132 4183 elem.closest('.vbo-overlay-fest-details').remove();
3133 4184 } else {
3134 - // console.log(res);
3135 4185 alert('Invalid response');
3136 4186 }
3137 4187 },
3138 4188 (err) => {
@@ -3176,9 +4226,8 @@
3176 4226 // reset input fields
3177 4227 jQuery('#vbo-newfest-name').val('');
3178 4228 jQuery('#vbo-newfest-descr').val('');
3179 4229 } catch (e) {
3180 - // console.log(res);
3181 4230 alert('Invalid response');
3182 4231 return false;
3183 4232 }
3184 4233 },
@@ -3267,9 +4316,8 @@
3267 4316 jQuery('#vbo-newrdnote-name').val('');
3268 4317 jQuery('#vbo-newrdnote-descr').val('');
3269 4318 jQuery('#vbo-newrdnote-cdays').val('0').trigger('change');
3270 4319 } catch (e) {
3271 - // console.log(res);
3272 4320 alert('Invalid response');
3273 4321 return false;
3274 4322 }
3275 4323 },
@@ -3314,9 +4362,8 @@
3314 4362 }
3315 4363 }
3316 4364 elem.closest('.vbo-modal-roomdaynotes-note-details').remove();
3317 4365 } else {
3318 - // console.log(res);
3319 4366 alert('Invalid response');
3320 4367 }
3321 4368 },
3322 4369 (err) => {