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
vikbooking / admin / views / overv / tmpl / default.php

default.php in VikBooking Hotel Booking Engine & PMS 1.8.16, at admin/views/overv/tmpl/default.php

4,461 lines 177.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package VikBooking
4 * @subpackage com_vikbooking
5 * @author Alessio Gaggii - E4J srl
6 * @copyright Copyright (C) 2025 E4J srl. All rights reserved.
7 * @license GNU General Public License version 2 or later; see LICENSE
8 * @link https://vikwp.com
9 */
10
11 defined('ABSPATH') or die('No script kiddies please!');
12
13 $rows = $this->rows;
14 $arrbusy = $this->arrbusy;
15 $wmonthsel = $this->wmonthsel;
16 $tsstart = $this->tsstart;
17 $lim0 = $this->lim0;
18 $navbut = $this->navbut;
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
29 $vbo_auth_pricing = JFactory::getUser()->authorise('core.vbo.pricing', 'com_vikbooking');
30
31 JHtml::fetch('script', VBO_SITE_URI.'resources/jquery-ui.min.js');
32 JHtml::fetch('script', VBO_ADMIN_URI.'resources/js_upload/jquery.stickytableheaders.min.js');
33
34 $file_base = VBOPlatformDetection::isWordPress() ? 'admin.php' : 'index.php';
35
36 // JS lang defs
37 JText::script('VBSAVE');
38 JText::script('VBPVIEWORDERSTHREE');
39 JText::script('VBEDITORDERTHREE');
40 JText::script('VBDAYS');
41 JText::script('VBDAY');
42 JText::script('VBMAILADULTS');
43 JText::script('VBMAILADULT');
44 JText::script('VBMAILCHILDREN');
45 JText::script('VBMAILCHILD');
46 JText::script('VBO_MISSING_SUBUNIT');
47 JText::script('VBO_SHOW_CANCELLATIONS');
48 JText::script('VBO_DRAG_SUBUNITS_SAMEDATE');
49 JText::script('VBPVIEWORDERCHANNEL');
50 JText::script('VBANNULLA');
51 JText::script('VIKLOADING');
52 JText::script('VBOVWDNDERRNOTENCELLS');
53 JText::script('VBOVWDNDMOVINGBID');
54 JText::script('VBOVWDNDMOVINGROOM');
55 JText::script('VBOVWDNDMOVINGDATES');
56 JText::script('VBEDITORDERROOMSNUM');
57 JText::script('VBEDITORDERADULTS');
58 JText::script('VBPICKUPAT');
59 JText::script('VBRELEASEAT');
60 JText::script('VBEDITORDERCHILDREN');
61 JText::script('VBEDITORDERNINE');
62 JText::script('VBPEDITBUSYTOTPAID');
63 JText::script('VBOFEATASSIGNUNIT');
64 JText::script('VBO_SPLIT_STAY');
65 JText::script('VBO_CONF_SWAP_RNUMB');
66 JText::script('VBO_ERR_SUBUN_MOVE_SUBUN');
67 JText::script('VBO_BTYPE_OVERBOOKING');
68
69 $days_labels = [
70 JText::translate('VBSUN'),
71 JText::translate('VBMON'),
72 JText::translate('VBTUE'),
73 JText::translate('VBWED'),
74 JText::translate('VBTHU'),
75 JText::translate('VBFRI'),
76 JText::translate('VBSAT')
77 ];
78
79 $nowdf = VikBooking::getDateFormat();
80 if ($nowdf == "%d/%m/%Y") {
81 $df = 'd/m/Y';
82 } elseif ($nowdf == "%m/%d/%Y") {
83 $df = 'm/d/Y';
84 } else {
85 $df = 'Y/m/d';
86 }
87 $currencysymb = VikBooking::getCurrencySymb();
88
89 $pdebug = VikRequest::getInt('e4j_debug', '', 'request');
90 $session = JFactory::getSession();
91 $show_type = $session->get('vbUnitsShowType', '');
92 $mnum = $session->get('vbOvwMnum', '1');
93 $mnum = intval($mnum);
94 $pcategory_id = $session->get('vbOvwCatid', 0);
95 $cookie = $app->input->cookie;
96 $cookie_uleft = $cookie->get('vboAovwUleft', '', 'string');
97 $cookie_sticky_heads = $cookie->get('vboAovwStheads', 'off', 'string');
98 $table_scroll_layout = $cookie->get('vboAovwScroll', (count($rows) > 3 ? 'inline' : 'tables'), 'string');
99 $colortags = VikBooking::loadBookingsColorTags();
100 $long_list = (count($rows) * $mnum) > 10;
101
102 // View mode - Classic or Tags
103 $pbmode = $session->get('vbTagsMode', 'classic');
104 $tags_view_supported = false;
105
106 // Room Units Distinctive Features
107 $rooms_features_map = [];
108 $rooms_features_bookings = [];
109 $rooms_bids_pools = [];
110 $room_bookings_pool = [];
111 $bids_checkins = [];
112 $index_loop = 0;
113 foreach ($rows as $kr => $room) {
114 if ($room['units'] <= 1) {
115 $tags_view_supported = true;
116 } else {
117 if (!empty($room['params']) && $room['units'] <= 150) {
118 //sub-room units only if room type has 150 units at most
119 $room_params = json_decode($room['params'], true);
120 if (is_array($room_params) && isset($room_params['features']) && is_array($room_params['features'])) {
121 $rooms_features_map[$room['id']] = [];
122 foreach ($room_params['features'] as $rind => $rfeatures) {
123 foreach ($rfeatures as $fname => $fval) {
124 if (strlen($fval)) {
125 // $rooms_features_map[$room['id']][$rind] = '#'.$rind.' - '.JText::translate($fname).': '.$fval;
126 $rooms_features_map[$room['id']][$rind] = "#{$rind} - {$fval}";
127 break;
128 }
129 }
130 }
131 if (!$rooms_features_map[$room['id']]) {
132 unset($rooms_features_map[$room['id']]);
133 } else {
134 foreach ($rooms_features_map[$room['id']] as $rind => $indexdata) {
135 $clone_room = $room;
136 $clone_room['unit_index'] = (int)$rind;
137 $clone_room['unit_index_str'] = $indexdata;
138 array_splice($rows, ($kr + 1 + $index_loop), 0, [$clone_room]);
139 $index_loop++;
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 }
163 }
164 }
165 }
166 }
167 }
168
169 if (!$tags_view_supported) {
170 $pbmode = 'classic';
171 }
172
173 // locked (stand-by) records
174 $arrlocked = [];
175 if (array_key_exists('tmplock', $arrbusy)) {
176 if (count($arrbusy['tmplock']) > 0) {
177 $arrlocked = $arrbusy['tmplock'];
178 }
179 unset($arrbusy['tmplock']);
180 }
181
182 /**
183 * Global closing dates.
184 *
185 * @since 1.17.1 (J) - 1.7.1 (WP)
186 */
187 $globally_closed = VikBooking::getClosingDates();
188
189 ?>
190 <script type="text/Javascript">
191 function vboUnitsLeftOrBooked() {
192 let set_to = jQuery('#uleftorbooked').val();
193 jQuery('.vbo-overv-avcell[data-units-booked]').each(function() {
194 let counter_el = jQuery(this).find('a');
195 if (!counter_el.length) {
196 return;
197 }
198 counter_el.first().text(jQuery(this).attr('data-'+set_to));
199 });
200 let nd = new Date();
201 nd.setTime(nd.getTime() + (365*24*60*60*1000));
202 document.cookie = "vboAovwUleft="+set_to+"; expires=" + nd.toUTCString() + "; path=/; SameSite=Lax";
203 }
204 if (typeof jQuery.fn.tooltip === 'function') {
205 jQuery(".hasTooltip").tooltip();
206 } else {
207 jQuery.fn.tooltip = function(){};
208 }
209 var vboFests = <?php echo json_encode($this->festivities); ?>;
210 var vboRdayNotes = <?php echo json_encode($this->rdaynotes); ?>;
211 </script>
212 <form action="index.php?option=com_vikbooking&amp;task=overv" method="post" name="vboverview" class="vbo-avov-form">
213 <div class="btn-toolbar vbo-avov-toolbar" id="filter-bar" style="width: 100%; display: inline-block;">
214 <div class="btn-group pull-left">
215 <?php echo $wmonthsel; ?>
216 </div>
217 <div class="btn-group pull-left">
218 <select name="mnum" onchange="document.vboverview.submit();">
219 <?php
220 for($i = 1; $i <= 18; $i++) {
221 ?>
222 <option value="<?php echo $i; ?>"<?php echo $i == $mnum ? ' selected="selected"' : ''; ?>><?php echo JText::translate('VBOVWNUMMONTHS').' '.$i; ?></option>
223 <?php
224 }
225 ?>
226 </select>
227 </div>
228 <?php
229 if (count($this->categories)) {
230 ?>
231 <div class="btn-group pull-left">
232 <select name="category_id" onchange="document.vboverview.submit();">
233 <option value="0">- <?php echo JText::translate('VBPVIEWROOMTWO'); ?> -</option>
234 <?php
235 foreach ($this->categories as $catid => $catname) {
236 ?>
237 <option value="<?php echo $catid; ?>"<?php echo $catid == $pcategory_id ? ' selected="selected"' : ''; ?>><?php echo $catname; ?></option>
238 <?php
239 }
240 ?>
241 </select>
242 </div>
243 <?php
244 }
245
246 $stickyheaders_cmd_on = '';
247 $stickyheaders_cmd_off = '';
248 if ($long_list) {
249 if (VBOPlatformDetection::isJoomla()) {
250 /**
251 * @joomlaonly fixed offset selector is ".navbar"
252 */
253 $stickyheaders_cmd_on = "jQuery('table.vboverviewtable').stickyTableHeaders({cacheHeaderHeight: true, fixedOffset: (jQuery('.navbar').length ? jQuery('.navbar') : jQuery('#subhead-container'))});";
254 } else {
255 /**
256 * @wponly fixed offset selector is "#wpadminbar"
257 */
258 $stickyheaders_cmd_on = "jQuery('table.vboverviewtable').stickyTableHeaders({cacheHeaderHeight: true, fixedOffset: jQuery('#wpadminbar')});";
259 }
260 $stickyheaders_cmd_off = "jQuery('table.vboverviewtable').stickyTableHeaders('destroy'); jQuery('th.bluedays').attr('style', '');";
261 }
262 ?>
263 <script type="text/javascript">
264 function vboToggleStickyTableHeaders(val) {
265 let sticky_heads_cval = 'off';
266 if (val > 0) {
267 sticky_heads_cval = 'on';
268 jQuery('table.vboverviewtable').addClass('vbo-overv-sticky-table-head-on');
269 jQuery('table.vboverviewtable').removeClass('vbo-overv-sticky-table-head-off');
270 <?php echo $stickyheaders_cmd_on; ?>
271 // stop listening to the document scroll event to handle the fixed table heads
272 document.removeEventListener('scroll', vboOvervHandleVScrollTableHeads);
273 // scan all tables for listening to their horizontal scrolling event
274 let tables = document.querySelectorAll('table.vboverviewtable.vbo-overv-sticky-table-head-off');
275 tables.forEach((table) => {
276 // stop listening to the table horizontal scroll events
277 table.parentNode.removeEventListener('scroll', vboOvervHandleHScrollTableHead);
278 });
279 } else {
280 jQuery('table.vboverviewtable').addClass('vbo-overv-sticky-table-head-off');
281 jQuery('table.vboverviewtable').removeClass('vbo-overv-sticky-table-head-on');
282 <?php echo $stickyheaders_cmd_off; ?>
283 // listen to the document scroll event to handle the fixed table heads
284 document.addEventListener('scroll', vboOvervHandleVScrollTableHeads);
285 // scan all tables for listening to their horizontal scrolling event
286 let tables = document.querySelectorAll('table.vboverviewtable.vbo-overv-sticky-table-head-off');
287 tables.forEach((table) => {
288 table.parentNode.addEventListener('scroll', vboOvervHandleHScrollTableHead);
289 });
290 }
291 let nd = new Date();
292 nd.setTime(nd.getTime() + (365*24*60*60*1000));
293 document.cookie = "vboAovwStheads=" + sticky_heads_cval + "; expires=" + nd.toUTCString() + "; path=/; SameSite=Lax";
294 }
295 </script>
296 <?php
297 if ($long_list || $tags_view_supported === true) {
298 // group into an apposite context menu the layout and view types
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
396 <div class="btn-group pull-left">
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>
401 </div>
402 <?php
403 }
404
405 ?>
406 <div class="btn-group pull-left">
407 <?php
408 // display overview context-menu
409 echo JLayoutHelper::render('overview.actions', ['caller' => 'overv', 'rooms' => $rows]);
410 ?>
411 </div>
412 <div class="btn-group pull-right">
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>
414 </div>
415 <div class="btn-group pull-right vbo-avov-legend">
416 <!-- <span class="vbo-overview-legend-init"><?php echo JText::translate('VBOVERVIEWLEGEND'); ?></span> -->
417 <div class="vbo-overview-legend-red">
418 <span class="vbo-overview-legend-box">&nbsp;</span>
419 <span class="vbo-overview-legend-title"><?php echo JText::translate('VBOVERVIEWLEGRED'); ?></span>
420 </div>
421 <div class="vbo-overview-legend-yellow">
422 <span class="vbo-overview-legend-box">&nbsp;</span>
423 <span class="vbo-overview-legend-title"><?php echo JText::translate('VBOVERVIEWLEGYELLOW'); ?></span>
424 </div>
425 <div class="vbo-overview-legend-green">
426 <span class="vbo-overview-legend-box">&nbsp;</span>
427 <span class="vbo-overview-legend-title"><?php echo JText::translate('VBOVERVIEWLEGGREEN'); ?></span>
428 </div>
429 <div class="vbo-overview-legend-green vbo-overview-legend-dnd">
430 <span class="vbo-overview-legend-box"><i class="vboicn-enlarge" style="margin: 1px; display: block; text-align: center;"></i></span>
431 <span class="vbo-overview-legend-title"><?php echo JText::translate('VBOVERVIEWLEGDND'); ?></span>
432 </div>
433 </div>
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 ?>
452 </form>
453
454 <?php
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);
458 $nowts = getdate($tsstart);
459 $curts = $nowts;
460 for ($mind = 1; $mind <= ($table_scroll_layout === 'inline' ? 1 : $mnum); $mind++) {
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 }
479 ?>
480 <div class="vbo-overv-montable-wrap">
481 <div class="vbo-table-responsive">
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]); ?>">
483 <thead>
484 <tr class="vboverviewtablerowone">
485 <th class="bluedays skip-bluedays-click vbo-overview-month" data-initial-period="<?php echo JHtml::fetch('esc_attr', $displayMonthLabel); ?>"><?php echo $displayMonthLabel; ?></th>
486 <?php
487 $moncurts = $curts;
488 $mon = $moncurts['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)) {
491 $curdayymd = date('Y-m-d', $moncurts[0]);
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 }
508 ?>
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>
511 <span class="vbo-overw-tablemday"><?php echo $moncurts['mday']; ?></span>
512 </th>
513 <?php
514 // parse the next day
515 $moncurts = getdate(mktime(0, 0, 0, $moncurts['mon'], ($moncurts['mday'] + 1), $moncurts['year']));
516 }
517 ?>
518 </tr>
519 </thead>
520 <tbody>
521 <?php
522 foreach ($rows as $room) {
523 $moncurts = $curts;
524 $mon = $moncurts['mon'];
525 $room_tags_view = $pbmode == 'tags' && $tags_view_supported === true && $room['units'] <= 1 ? true : false;
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;
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
547 ?>
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;"' : ''; ?>>
549 <?php
550 if ($is_subunit) {
551 ?>
552 <td class="roomname subroomname" data-roomid="<?php echo '-' . $room['id']; ?>">
553 <span class="vbo-overview-subroomunits"><?php VikBookingIcons::e($main_cell_icn_id); ?></span>
554 <span class="vbo-overview-subroomname"><?php echo $room['unit_index_str']; ?></span>
555 </td>
556 <?php
557 } else {
558 ?>
559 <td class="roomname" data-roomid="<?php echo $room['id']; ?>" data-units="<?php echo $room['units']; ?>">
560 <span class="vbo-overview-room-info">
561 <span class="vbo-overview-roomunits"><?php echo $room['units']; ?></span>
562 <span class="vbo-overview-roomname"><?php echo $room['name']; ?></span>
563 <?php
564 if (isset($rooms_features_map[$room['id']])) {
565 ?>
566 <span class="vbo-overview-subroom-toggle"><i class="<?php echo VikBookingIcons::i('chevron-down', 'hasTooltip'); ?>" style="margin: 0;" title="<?php echo JHtml::fetch('esc_attr', JText::translate('VBOVERVIEWTOGGLESUBROOM')); ?>"></i></span>
567 <?php
568 }
569 ?>
570 </span>
571 </td>
572 <?php
573 }
574
575 // loop until the end of the range of dates, or until the end of the current month
576 $room_bids_pool = [];
577 $room_bookings = [];
578 while (($table_scroll_layout === 'inline' && $moncurts[0] < $last_displayed_day_ts) || ($table_scroll_layout !== 'inline' && $moncurts['mon'] == $mon)) {
579 // init cell values
580 $dclass = 'vbo-grid-avcell vbo-overv-avcell ' . (!$is_subunit ? "notbusy" : "subnotbusy");
581 $is_checkin = false;
582 $is_sharedcal = false;
583 $is_closure = false;
584 $lastbidcheckout = null;
585 $dalt = "";
586 $bid = "";
587 $bids_pool = [];
588 $totfound = 0;
589 $prev_day_key = date('Y-m-d', strtotime('-1 day', $moncurts[0]));
590 $cur_day_key = date('Y-m-d', $moncurts[0]);
591
592 // check for global closing dates
593 foreach ($globally_closed as $glob_closed) {
594 if ($moncurts[0] >= $glob_closed['from'] && $moncurts[0] <= $glob_closed['to']) {
595 $dclass .= ' vbo-overv-globally-closed';
596 }
597 }
598
599 // check availability
600 if (!empty($arrbusy[$room['id']]) && !$is_subunit) {
601 foreach ($arrbusy[$room['id']] as $b) {
602 $tmpone = getdate($b['checkin']);
603 $ritts = mktime(0, 0, 0, $tmpone['mon'], $tmpone['mday'], $tmpone['year']);
604 $tmptwo = getdate($b['checkout']);
605 $conts = mktime(0, 0, 0, $tmptwo['mon'], $tmptwo['mday'], $tmptwo['year']);
606 if (!($moncurts[0] >= $ritts && $moncurts[0] < $conts)) {
607 // booking does not involve the current day
608 continue;
609 }
610 $dclass = "vbo-grid-avcell vbo-overv-avcell busy";
611 $bid = $b['idorder'];
612 $is_sharedcal = !empty($b['sharedcal']) ? true : $is_sharedcal;
613 $is_closure = !empty($b['closure']) ? true : $is_closure;
614 $bid_str = '-' . $bid . '-';
615 if (!in_array($bid_str, $bids_pool)) {
616 $bids_pool[] = $bid_str;
617 }
618 if (isset($rooms_features_map[$room['id']])) {
619 // multi-unit room with distinctive features defined
620 if (!isset($room_bids_pool[$cur_day_key])) {
621 $room_bids_pool[$cur_day_key] = [];
622 $room_bookings[$cur_day_key] = [];
623 }
624 $room_bids_pool[$cur_day_key][] = (int)$bid;
625 $room_bookings[$cur_day_key][] = $b;
626 } elseif ($room['units'] == 1) {
627 // single-unit room
628 if (!isset($room_bookings[$cur_day_key])) {
629 $room_bookings[$cur_day_key] = [];
630 }
631 $room_bookings[$cur_day_key][] = $b;
632 }
633 if ($moncurts[0] == $ritts) {
634 $dalt = JText::translate('VBPICKUPAT')." ".date('H:i', $b['checkin']);
635 $is_checkin = true;
636 $lastbidcheckout = $b['checkout'];
637 $bids_checkins[$bid] = $bids_checkins[$bid] ?? [];
638 $bids_checkins[$bid][] = $cur_day_key;
639 } elseif ($moncurts[0] == $conts) {
640 $dalt = JText::translate('VBRELEASEAT')." ".date('H:i', $b['checkout']);
641 }
642 $totfound++;
643 }
644 }
645
646 // locked (stand-by) records
647 if ($room_tags_view === true && isset($arrlocked[$room['id']]) && $arrlocked[$room['id']] && !$is_subunit) {
648 foreach ($arrlocked[$room['id']] as $l) {
649 $tmpone = getdate($l['checkin']);
650 $ritts = mktime(0, 0, 0, $tmpone['mon'], $tmpone['mday'], $tmpone['year']);
651 $tmptwo = getdate($l['checkout']);
652 $conts = mktime(0, 0, 0, $tmptwo['mon'], $tmptwo['mday'], $tmptwo['year']);
653 if ($moncurts[0] >= $ritts && $moncurts[0] < $conts) {
654 $dclass = strpos($dclass, "notbusy") !== false ? "busytmplock" : "busy busytmplock";
655 $bid = $l['idorder'];
656 if (!in_array($bid, $bids_pool)) {
657 if (count($bids_pool) > 0) {
658 array_unshift($bids_pool, '-'.$bid.'-');
659 } else {
660 $bids_pool[] = '-'.$bid.'-';
661 }
662 }
663 if ($moncurts[0] == $ritts) {
664 $dalt = JText::translate('VBPICKUPAT')." ".date('H:i', $l['checkin']);
665 } elseif ($moncurts[0] == $conts) {
666 $dalt = JText::translate('VBRELEASEAT')." ".date('H:i', $l['checkout']);
667 }
668 $totfound++;
669 }
670 }
671 }
672
673 // handle single-unit room reservations
674 if ($room['units'] == 1 && !isset($rooms_features_map[$room['id']]) && isset($room_bookings[$cur_day_key]) && $room_bookings[$cur_day_key]) {
675 // set the first (and only) booking
676 $day_booking = [];
677 foreach ($room_bookings[$cur_day_key] as $day_res) {
678 if (!$day_res['closure'] && !$day_res['sharedcal']) {
679 $day_booking = $day_res;
680 break;
681 }
682 }
683 if ($day_booking) {
684 if (!isset($room_bookings_pool[$room['id']])) {
685 $room_bookings_pool[$room['id']] = [];
686 }
687 if (!isset($room_bookings_pool[$room['id']][$cur_day_key])) {
688 $room_bookings_pool[$room['id']][$cur_day_key] = [];
689 }
690 $room_bookings_pool[$room['id']][$cur_day_key][0] = $day_booking;
691 }
692 }
693
694 $useday = $moncurts['mday'] < 10 ? "0{$moncurts['mday']}" : $moncurts['mday'];
695 $dclass .= $totfound < $room['units'] && $totfound > 0 ? ' vbo-partially' : '';
696 $dclass .= $is_sharedcal ? ' busy-sharedcalendar' : '';
697 $dclass .= $is_closure ? ' busy-closure' : '';
698 $dstyle = '';
699 $astyle = '';
700 if ($room_tags_view === true && $totfound > 0) {
701 $last_bid = intval(str_replace('-', '', $bids_pool[(count($bids_pool) - 1)]));
702 $binfo = VikBooking::getBookingInfoFromID($last_bid);
703 if ($binfo) {
704 $bcolortag = VikBooking::applyBookingColorTag($binfo);
705 if ($bcolortag) {
706 $bcolortag['name'] = JText::translate($bcolortag['name']);
707 $dstyle = " style=\"background-color: ".$bcolortag['color']."; color: ".(array_key_exists('fontcolor', $bcolortag) ? $bcolortag['fontcolor'] : '#ffffff').";\" data-lastbid=\"".$last_bid."\"";
708 $astyle = " style=\"color: ".(array_key_exists('fontcolor', $bcolortag) ? $bcolortag['fontcolor'] : '#ffffff').";\"";
709 $dclass .= ' vbo-hascolortag';
710 }
711 }
712 }
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']])) {
714 foreach ($rooms_bids_pools[$room['id']][$cur_day_key] as $bid) {
715 $bid = intval(str_replace('-', '', $bid));
716 if (in_array($bid, $rooms_features_bookings[$room['id']][$room['unit_index']])) {
717 $room['units'] = 1;
718 $totfound = 1;
719 $dclass = "vbo-grid-avcell vbo-overv-avcell subroom-busy";
720 $is_checkin = isset($bids_checkins[$bid]) && in_array($cur_day_key, $bids_checkins[$bid]) ? true : $is_checkin;
721 if (count($room_bookings_pool[$room['id']][$cur_day_key]) > 1 && !empty($room_bookings_pool[$room['id']][$cur_day_key][$room['unit_index']]['checkin'])) {
722 // ensure multi-room bookings display the proper check-in day in case of different nights of stay
723 $is_checkin = date('Y-m-d', $room_bookings_pool[$room['id']][$cur_day_key][$room['unit_index']]['checkin']) == $cur_day_key;
724 }
725
726 /**
727 * In case of mixed room-types (hotels inventory and listings), we allow
728 * the tags to be applied on sub-units for multi-unit room types.
729 *
730 * @since 1.17.5 (J) - 1.7.5 (WP)
731 */
732 if ($tags_view_supported && $binfo = VikBooking::getBookingInfoFromID($bid)) {
733 $bcolortag = VikBooking::applyBookingColorTag($binfo);
734 if ($bcolortag) {
735 $bcolortag['name'] = JText::translate($bcolortag['name']);
736 $dstyle = " style=\"background-color: ".$bcolortag['color']."; color: ".(array_key_exists('fontcolor', $bcolortag) ? $bcolortag['fontcolor'] : '#ffffff').";\" data-lastbid=\"".$bid."\"";
737 $astyle = " style=\"color: ".(array_key_exists('fontcolor', $bcolortag) ? $bcolortag['fontcolor'] : '#ffffff').";\"";
738 $dclass .= ' vbo-hascolortag';
739 }
740 }
741
742 // abort
743 break;
744 }
745 }
746 }
747 $write_units = $show_type == 'units-left' || (!empty($cookie_uleft) && $cookie_uleft == 'units-left') ? ($room['units'] - $totfound) : $totfound;
748 // check today's date
749 $curdayymd = date('Y-m-d', $moncurts[0]);
750 if ($todayymd == $curdayymd) {
751 $dclass .= ' vbo-overv-todaycell';
752 }
753 if (isset($this->festivities[$curdayymd])) {
754 $dclass .= ' vbo-overv-festcell';
755 }
756
757 /**
758 * Critical dates defined at room-day level.
759 *
760 * @since 1.13.5 (J) - 1.3.5 (WP)
761 */
762 $rdaynote_keyid = $cur_day_key . '_' . $room['id'] . '_' . (isset($room['unit_index']) ? $room['unit_index'] : '0');
763 if (isset($this->rdaynotes[$rdaynote_keyid])) {
764 // note exists for this combination of date, room ID and subunit
765 $dclass .= ' vbo-roomdaynote-full';
766 $rdaynote_icn = 'sticky-note';
767 } else {
768 // no notes for this cell
769 $dclass .= ' vbo-roomdaynote-empty';
770 $rdaynote_icn = 'far fa-sticky-note';
771 }
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 }
776
777 /**
778 * Closures overlapping one real reservation for a single-unit room should still
779 * allow the display of the booking snake if it's just one real reservation.
780 */
781 $closure_on_real_res = false;
782 if ($totfound === 2 && !$is_subunit && $room['units'] == 1 && $cookie_sticky_heads == 'off' && strpos($dclass, "busy-closure") !== false) {
783 if (isset($room_bookings_pool[$room['id']]) && isset($room_bookings_pool[$room['id']][$cur_day_key]) && $room_bookings_pool[$room['id']][$cur_day_key]) {
784 if (isset($room_bookings_pool[$room['id']][$cur_day_key][0]) && !$room_bookings_pool[$room['id']][$cur_day_key][0]['closure']) {
785 // found a real booking on a date with at least one closure
786 $closure_on_real_res = true;
787 }
788 }
789 }
790
791 // prepare cell content
792 $day_has_closure = false;
793 if ($totfound === 1 || $closure_on_real_res) {
794 // start booking snake content
795 $day_booking_snake = '';
796 // collect the current day booking data
797 $day_booking_data = [];
798 if (isset($room_bookings_pool[$room['id']][$cur_day_key]) && $room_bookings_pool[$room['id']][$cur_day_key]) {
799 if ($is_subunit && isset($room_bookings_pool[$room['id']][$cur_day_key][$room['unit_index']])) {
800 $day_booking_data = $room_bookings_pool[$room['id']][$cur_day_key][$room['unit_index']];
801 } elseif (!$is_subunit && isset($room_bookings_pool[$room['id']][$cur_day_key][0])) {
802 $day_booking_data = $room_bookings_pool[$room['id']][$cur_day_key][0];
803 }
804 }
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]) {
808 if ($is_subunit && isset($room_bookings_pool[$room['id']][$prev_day_key][$room['unit_index']])) {
809 if (date('Y-m-d', $room_bookings_pool[$room['id']][$prev_day_key][$room['unit_index']]['checkout']) == $cur_day_key) {
810 // prepend checkout snake
811 $day_booking_snake .= '<div class="vbo-tableaux-booking vbo-tableaux-booking-singleunit vbo-tableaux-booking-checkout"><span>&nbsp;</span></div>';
812 }
813 } elseif (!$is_subunit && $room['units'] == 1 && isset($room_bookings_pool[$room['id']][$prev_day_key][0])) {
814 if (date('Y-m-d', $room_bookings_pool[$room['id']][$prev_day_key][0]['checkout']) == $cur_day_key) {
815 // prepend checkout snake
816 $day_booking_snake .= '<div class="vbo-tableaux-booking vbo-tableaux-booking-singleunit vbo-tableaux-booking-checkout"><span>&nbsp;</span></div>';
817 }
818 }
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
824 if ($room['units'] == 1 && $day_booking_data && !$day_booking_data['closure']) {
825 // build tableaux-style snake container for guest
826 $customer_descr = '';
827 if ($is_checkin) {
828 // customer details
829 if (!empty($day_booking_data['first_name']) || !empty($day_booking_data['last_name'])) {
830 // check if we need to display a profile picture or a channel logo
831 $booking_avatar_src = null;
832 $booking_avatar_alt = null;
833 if (!empty($day_booking_data['pic'])) {
834 // customer profile picture
835 $booking_avatar_src = strpos($day_booking_data['pic'], 'http') === 0 ? $day_booking_data['pic'] : VBO_SITE_URI . 'resources/uploads/' . $day_booking_data['pic'];
836 $booking_avatar_alt = basename($booking_avatar_src);
837 } elseif (!empty($day_booking_data['idorderota']) && !empty($day_booking_data['channel'])) {
838 // channel logo
839 $logo_helper = VikBooking::getVcmChannelsLogo($day_booking_data['channel'], $get_istance = true);
840 if ($logo_helper !== false) {
841 $booking_avatar_src = $logo_helper->getSmallLogoURL();
842 $booking_avatar_alt = $logo_helper->provenience;
843 }
844 }
845
846 if (!empty($booking_avatar_src)) {
847 // make sure the alt attribute is not too long in case of broken images
848 $booking_avatar_alt = !empty($booking_avatar_alt) && strlen($booking_avatar_alt) > 15 ? '...' . substr($booking_avatar_alt, -12) : $booking_avatar_alt;
849 // append booking avatar image
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>';
851 }
852
853 // customer name
854 $customer_fullname = trim($day_booking_data['first_name'] . ' ' . $day_booking_data['last_name']);
855 if (strlen($customer_fullname) > 26) {
856 if (function_exists('mb_substr')) {
857 $customer_fullname = trim(mb_substr($customer_fullname, 0, 26, 'UTF-8')) . '..';
858 } else {
859 $customer_fullname = trim(substr($customer_fullname, 0, 26)) . '..';
860 }
861 }
862 $customer_descr .= '<span class="vbo-tableaux-guest-name">' . $customer_fullname . '</span>';
863 } else {
864 // parse the customer data string
865 $custdata_parts = explode("\n", $day_booking_data['custdata']);
866 $enoughinfo = false;
867 if (count($custdata_parts) > 2 && strpos($custdata_parts[0], ':') !== false && strpos($custdata_parts[1], ':') !== false) {
868 // get the first two fields
869 $custvalues = array();
870 foreach ($custdata_parts as $custdet) {
871 if (strlen($custdet) < 1) {
872 continue;
873 }
874 $custdet_parts = explode(':', $custdet);
875 if (count($custdet_parts) >= 2) {
876 unset($custdet_parts[0]);
877 array_push($custvalues, trim(implode(':', $custdet_parts)));
878 }
879 if (count($custvalues) > 1) {
880 break;
881 }
882 }
883 if (count($custvalues) > 1) {
884 $enoughinfo = true;
885 $customer_nominative = trim(implode(' ', $custvalues));
886 if (strlen($customer_nominative) > 26) {
887 if (function_exists('mb_substr')) {
888 $customer_nominative = trim(mb_substr($customer_nominative, 0, 26, 'UTF-8')) . '..';
889 } else {
890 $customer_nominative = trim(substr($customer_nominative, 0, 26)) . '..';
891 }
892 }
893 if (!empty($day_booking_data['idorderota']) && !empty($day_booking_data['channel'])) {
894 // add support for the channel logo for the imported OTA reservations with no customer record
895 $logo_helper = VikBooking::getVcmChannelsLogo($day_booking_data['channel'], $get_istance = true);
896 if ($logo_helper !== false) {
897 $booking_avatar_src = $logo_helper->getSmallLogoURL();
898 $booking_avatar_alt = $logo_helper->provenience;
899 // make sure the alt attribute is not too long in case of broken images
900 $booking_avatar_alt = !empty($booking_avatar_alt) && strlen($booking_avatar_alt) > 15 ? '...' . substr($booking_avatar_alt, -12) : $booking_avatar_alt;
901 // append booking avatar image
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>';
903 }
904 }
905 // set customer nominative built
906 $customer_descr .= '<span class="vbo-tableaux-guest-name">' . $customer_nominative . '</span>';
907 }
908 }
909 if (!$enoughinfo) {
910 $customer_descr .= '<span class="vbo-tableaux-guest-name">#' . $day_booking_data['idorder'] . '</span>';
911 }
912 }
913 }
914 // set value
915 $day_booking_snake .= '<div class="vbo-tableaux-booking vbo-tableaux-booking-singleunit ' . ($is_checkin ? 'vbo-tableaux-booking-checkin' : 'vbo-tableaux-booking-stay') . '"' . ($is_checkin ? ' data-nights="' . $day_booking_data['days'] . '" draggable="true"' : '') . '><span>' . ($is_checkin ? $customer_descr : '&nbsp;') . '</span></div>';
916 }
917
918 if ($cookie_sticky_heads != 'off') {
919 // not supported with the sticky table headers layout
920 $day_booking_snake = '';
921 }
922
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 }
935 $dclass .= $is_checkin === true ? ' vbo-checkinday' : '';
936
937 // build proper link
938 if ($totfound > 1) {
939 // there must be a closure overlapping a real booking
940 $cell_link = 'index.php?option=com_vikbooking&task=choosebusy&idroom=' . $room['id'] . '&ts=' . $moncurts[0] . '&goto=overv';
941 } else {
942 // regular situation with one booking only
943 $cell_link = 'index.php?option=com_vikbooking&task=editbusy&cid[]=' . $bid . '&goto=overv';
944 }
945 ?>
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); ?>">
947 <?php
948 if (!$day_booking_snake) {
949 if ($is_checkin === true && !$stopdrag && !$day_has_closure && !$is_subunit_unassigned_row) {
950 ?>
951 <span class="vbo-draggable-sp" draggable="true">
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>
953 </span>
954 <?php
955 } else {
956 ?>
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>
958 <?php
959 }
960 }
961 echo $day_booking_snake . $critical_note;
962 ?>
963 </td>
964 <?php
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 }
972 ?>
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); ?>">
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>
975 <?php echo $critical_note; ?>
976 </td>
977 <?php
978 } else {
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 }
986 ?>
987 <td align="center" class="<?php echo $dclass; ?>" data-day="<?php echo $cur_day_key; ?>" data-bids="">
988 <?php
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]) {
990 if ($is_subunit && isset($room_bookings_pool[$room['id']][$prev_day_key][$room['unit_index']])) {
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)) {
992 // prepend checkout snake
993 echo '<div class="vbo-tableaux-booking vbo-tableaux-booking-singleunit vbo-tableaux-booking-checkout"><span>&nbsp;</span></div>';
994 }
995 } elseif (!$is_subunit && $room['units'] == 1 && isset($room_bookings_pool[$room['id']][$prev_day_key][0])) {
996 if (date('Y-m-d', $room_bookings_pool[$room['id']][$prev_day_key][0]['checkout']) == $cur_day_key) {
997 // prepend checkout snake
998 echo '<div class="vbo-tableaux-booking vbo-tableaux-booking-singleunit vbo-tableaux-booking-checkout"><span>&nbsp;</span></div>';
999 }
1000 }
1001 }
1002 echo $critical_note;
1003 ?>
1004 </td>
1005 <?php
1006 }
1007
1008 // iterate to next day
1009 $moncurts = getdate(mktime(0, 0, 0, $moncurts['mon'], ($moncurts['mday'] + 1), $moncurts['year']));
1010 }
1011
1012 // room row parsed, check if we have to parse a sub-unit next
1013 if (!$is_subunit && $room_bids_pool && isset($rooms_features_map[$room['id']])) {
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 }
1026 if ($room_indexes_bids) {
1027 // set bookings allocated per room index
1028 $rooms_features_bookings[$room['id']] = $room_indexes_bids;
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 }
1037 // map sub-unit room bookings
1038 foreach ($room_indexes_bids as $rindex => $rindex_bids) {
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 }
1072 }
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
1115 continue;
1116 }
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;
1122 }
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++;
1145 }
1146 }
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 }
1152 }
1153 }
1154
1155 // close row
1156 ?>
1157 </tr>
1158 <?php
1159 }
1160 ?>
1161 </tbody>
1162 </table>
1163 </div>
1164 </div>
1165 <?php
1166 // iterate to next month
1167 $curts = getdate(mktime(0, 0, 0, ($nowts['mon'] + $mind), $nowts['mday'], $nowts['year']));
1168 }
1169
1170 //Prepare modal
1171 ?>
1172 <script type="text/javascript">
1173 var hasNewBooking = false;
1174 var last_room_click = '',
1175 last_date_click = '',
1176 next_date_click = '';
1177 function vboJModalHideCallback() {
1178 if (hasNewBooking === true) {
1179 location.reload();
1180 }
1181 }
1182 function vboCallOpenJModal(identif, baseurl, options) {
1183 options = typeof options !== 'object' ? {} : options;
1184
1185 if (last_room_click && last_room_click.length) {
1186 baseurl += '&cid[]='+last_room_click;
1187 options.room_id = last_room_click;
1188 last_room_click = '';
1189 }
1190 if (last_date_click && last_date_click.length) {
1191 baseurl += '&checkin='+last_date_click;
1192 options.checkin = last_date_click;
1193 last_date_click = '';
1194 }
1195 if (next_date_click && next_date_click.length) {
1196 baseurl += '&checkout='+next_date_click;
1197 options.checkout = next_date_click;
1198 next_date_click = '';
1199 }
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 }
1217 }
1218 </script>
1219 <?php
1220 echo $vbo_app->getJmodalScript('', 'vboJModalHideCallback();', '');
1221 echo $vbo_app->getJmodalHtml('vbo-new-res', JText::translate('VBOSHOWQUICKRES'));
1222 //end Prepare modal
1223 ?>
1224 <div class="vbo-ovrv-flt-butn" onclick="vboCallOpenJModal('vbo-new-res', 'index.php?option=com_vikbooking&task=calendar&overv=1&tmpl=component');"><span><i class="vboicn-user-plus"></i> <?php echo JText::translate('VBOSHOWQUICKRES'); ?></span></div>
1225 <div class="vbo-info-overlay-block">
1226 <div class="vbo-info-overlay-loading-dnd">
1227 <span class="vbo-loading-dnd-head"></span>
1228 <span class="vbo-loading-dnd-body"></span>
1229 <span class="vbo-loading-dnd-footer"><?php echo JText::translate('VIKLOADING'); ?></span>
1230 <span id="vbo-dnd-response" class="vbo-loading-dnd-response"></span>
1231 <canvas id="vbo-dnd-canvas-success" height="250"></canvas>
1232 </div>
1233 </div>
1234
1235 <form action="index.php?option=com_vikbooking" method="post" name="adminForm" id="adminForm">
1236 <input type="hidden" name="option" value="com_vikbooking" />
1237 <input type="hidden" name="task" value="overv" />
1238 <input type="hidden" name="month" value="<?php echo $tsstart; ?>" />
1239 <input type="hidden" name="mnum" value="<?php echo $mnum; ?>" />
1240 <input type="hidden" name="category_id" value="<?php echo $pcategory_id; ?>" />
1241 <?php echo '<br/>'.$navbut; ?>
1242 </form>
1243
1244 <a class="vbo-basenavuri-details" href="index.php?option=com_vikbooking&task=editorder&goto=overv&cid[]=%d" style="display: none;"></a>
1245 <a class="vbo-basenavuri-edit" href="index.php?option=com_vikbooking&task=editbusy&goto=overv&cid[]=%d" style="display: none;"></a>
1246
1247 <script type="text/Javascript">
1248 const vboOvervMonthNames = <?php echo json_encode($month_names_cache ?? (new stdClass)); ?>;
1249
1250 var hovtimer;
1251 var hovtip = false;
1252 var vbodialogorph_on = false;
1253 var isdragging = false;
1254 var debug_mode = '<?php echo $pdebug; ?>';
1255 var bctags_count = <?php echo count($colortags); ?>;
1256 var bctags_pool = <?php echo json_encode($colortags); ?>;
1257 <?php
1258 if ($colortags) {
1259 $bctags_tip = '<div class=\"vbo-overview-tip-bctag-subtip-inner\">';
1260 foreach ($colortags as $ctagk => $ctagv) {
1261 $bctags_tip .= '<div class=\"vbo-overview-tip-bctag-subtip-circle hasTooltip\" data-ctagkey=\"'.$ctagk.'\" data-ctagcolor=\"'.$ctagv['color'].'\" title=\"'.addslashes(JText::translate($ctagv['name'])).'\"><div class=\"vbo-overview-tip-bctag-subtip-circlecont\" style=\"background-color: '.$ctagv['color'].';\"></div></div>';
1262 }
1263 $bctags_tip .= '</div>';
1264 ?>
1265 var bctags_tip = "<?php echo $bctags_tip; ?>";
1266 <?php
1267 }
1268 ?>
1269 </script>
1270
1271 <script type="text/Javascript">
1272 /**
1273 * Render the units view mode
1274 */
1275 vboUnitsLeftOrBooked();
1276
1277 /**
1278 * Orphans dialog
1279 */
1280 function hideVboDialogOverv(action) {
1281 if (vbodialogorph_on === true) {
1282 jQuery(".vbo-orphans-overlay-block").fadeOut(400, function () {
1283 jQuery(".vbo-info-overlay-content").show();
1284 });
1285 vbodialogorph_on = false;
1286 }
1287 // check action
1288 if (action < 0) {
1289 // stop reminding, set cookie
1290 let nd = new Date();
1291 nd.setTime(nd.getTime() + (365*24*60*60*1000));
1292 document.cookie = "vboHideOrphans=1; expires=" + nd.toUTCString() + "; path=/; SameSite=Lax";
1293 }
1294 }
1295
1296 /* DnD global vars */
1297 var cellspool = [];
1298 var newcellspool = [];
1299
1300 /* DnD Count the number of consecutive cells for this booking */
1301 function countBookingCells(cellobj, bidstart, roomid) {
1302 var totnights = 1;
1303 var loop = true;
1304 var cellelem = cellobj;
1305 cellspool.push(cellelem);
1306 while (loop === true) {
1307 var next = cellelem.next('td');
1308 if (next === undefined || !next.length) {
1309 // attempt to go to the month after
1310 var nextmonth = false;
1311 var partable = cellelem.closest('.vbo-overv-montable-wrap').next('.vbo-overv-montable-wrap').find('table.vboverviewtable');
1312 if (partable !== undefined && partable.length) {
1313 partable.find('tr.vboverviewtablerow').each(function() {
1314 var roomexists = jQuery(this).find('td').first();
1315 if (roomexists !== undefined && roomexists.length) {
1316 if (roomexists.attr('data-roomid') == roomid) {
1317 nextmonth = true;
1318 next = roomexists.next('td');
1319 return true;
1320 }
1321 }
1322 });
1323 }
1324 if (nextmonth === false) {
1325 // nothing was found in the month after
1326 loop = false;
1327 break;
1328 }
1329 }
1330 cellelem = next;
1331 var nextbids = cellelem.attr('data-bids');
1332 if (nextbids.length && nextbids.indexOf(bidstart) >= 0) {
1333 cellspool.push(cellelem);
1334 totnights++;
1335 } else {
1336 loop = false;
1337 break;
1338 }
1339 }
1340 return totnights;
1341 }
1342
1343 /* DnD Count the number of consecutive free date-cells for moving the booking onto the landing cell selected for drop */
1344 function countCellFreeNights(landobj, roomid, totnights, moving_bids) {
1345 var freenights = 1;
1346 var loop = true;
1347 var cellelem = landobj;
1348
1349 // populate cells pool
1350 newcellspool.push(cellelem);
1351
1352 while (loop === true) {
1353 var next = cellelem.next('td');
1354 if (next === undefined || !next.length) {
1355 // attempt to go to the month after
1356 var nextmonth = false;
1357 var partable = cellelem.closest('.vbo-overv-montable-wrap').next('.vbo-overv-montable-wrap').find('table.vboverviewtable');
1358 if (partable !== undefined && partable.length) {
1359 partable.find('tr.vboverviewtablerow').each(function() {
1360 var roomexists = jQuery(this).find('td').first();
1361 if (roomexists !== undefined && roomexists.length) {
1362 if (roomexists.attr('data-roomid') == roomid) {
1363 nextmonth = true;
1364 next = roomexists.next('td');
1365 return true;
1366 }
1367 }
1368 });
1369 }
1370 if (nextmonth === false) {
1371 // nothing was found in the month after
1372 loop = false;
1373 break;
1374 }
1375 }
1376 cellelem = next;
1377 var cell_bids = cellelem.attr('data-bids');
1378 if (!cellelem.hasClass('busy') || (cellelem.hasClass('busy') && cellelem.hasClass('vbo-partially'))) {
1379 // bookings of 1 night can stop here because there is availability for one day
1380 if (parseInt(totnights) === 1) {
1381 loop = false;
1382 break;
1383 }
1384 // push free cell
1385 newcellspool.push(cellelem);
1386 freenights++;
1387 if (freenights >= totnights) {
1388 loop = false;
1389 break;
1390 }
1391 } else {
1392 // cell is occupied, check if it's occupied by the booking we are moving or not
1393 if (!cell_bids || cell_bids != moving_bids) {
1394 loop = false;
1395 break;
1396 }
1397 // cell is occupied by the same booking we are moving, so this cell counts as free
1398 newcellspool.push(cellelem);
1399 freenights++;
1400 if (freenights >= totnights) {
1401 loop = false;
1402 break;
1403 }
1404 }
1405 }
1406
1407 return freenights;
1408 }
1409
1410 /* DnD function to perform the ajax request of the booking modification */
1411 function doAlterBooking(bid, roomid, landrid) {
1412 var nowdatefrom = jQuery(newcellspool[0]).attr('data-day');
1413 var nowdateto = jQuery(newcellspool[(newcellspool.length -1)]).attr('data-day');
1414
1415 // perform the request
1416 VBOCore.doAjax(
1417 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=alterbooking'); ?>",
1418 {
1419 tmpl: "component",
1420 idorder: bid,
1421 oldidroom: roomid,
1422 idroom: landrid,
1423 fromdate: nowdatefrom,
1424 todate: nowdateto,
1425 e4j_debug: debug_mode
1426 },
1427 (res) => {
1428 if (res.indexOf('e4j.error') >= 0 ) {
1429 alert(res.replace("e4j.error.", ""));
1430 //restore the old cells
1431 jQuery('td.vbo-dragging-cells-tmp').removeClass('vbo-dragging-cells-tmp');
1432 jQuery('td.vbo-dragged-cells-tmp').removeClass('vbo-dragged-cells-tmp');
1433 //
1434 jQuery(".vbo-info-overlay-block").hide();
1435 } else {
1436 //move to the new cells and if there are already bookings on those dates, reload the page without moving the blocks
1437 var obj_res = typeof res === 'string' ? JSON.parse(res) : res;
1438 var mustReload = obj_res.esit < 1 ? true : false;
1439 //always force reload if booking was made for more than one room
1440 var samebidcells = jQuery("td.vbo-checkinday[data-bids='"+bid+"']");
1441 if (samebidcells.length > 1) {
1442 mustReload = true;
1443 }
1444 //
1445 jQuery(newcellspool).each(function(k, v) {
1446 var cur_units_booked = parseInt(jQuery(cellspool[k]).find('a').parent('td').attr('data-units-booked'));
1447 if ((v.hasClass('busy') && v.attr('data-bids') != jQuery(cellspool[k]).attr('data-bids')) || isNaN(cur_units_booked) || cur_units_booked > 1) {
1448 mustReload = true;
1449 return false;
1450 }
1451 });
1452 if (mustReload !== true) {
1453 /* switch cells */
1454 var switchmap = [];
1455 jQuery(cellspool).each(function(k, v) {
1456 var switchcell = {
1457 hcont: v.html(),
1458 cl: v.attr('class'),
1459 bids: v.attr('data-bids'),
1460 tit: v.attr('title')
1461 };
1462 switchmap[k] = switchcell;
1463 v.html("").attr('class', 'notbusy').attr('data-bids', '').attr('title', '');
1464 });
1465 jQuery(switchmap).each(function(k, v) {
1466 jQuery(newcellspool[k]).html(v.hcont).attr('class', v.cl).attr('data-bids', v.bids).attr('title', v.tit);
1467 // re-bind hover event
1468 jQuery(newcellspool[k]).hover(function() {
1469 registerHoveringTooltip(this);
1470 }, unregisterHoveringTooltip);
1471 //
1472 });
1473 jQuery('td.vbo-dragging-cells-tmp').removeClass('vbo-dragging-cells-tmp');
1474 jQuery('td.vbo-dragged-cells-tmp').removeClass('vbo-dragged-cells-tmp');
1475 }
1476 if (obj_res.esit < 1) {
1477 //some errors occurred after executing certain functions
1478 if (obj_res.message.length) {
1479 alert(obj_res.message);
1480 }
1481 document.location.href='<?php echo $file_base; ?>?option=com_vikbooking&task=overv';
1482 } else {
1483 finalizeDndUpdate(mustReload, obj_res);
1484 }
1485 }
1486 },
1487 (err) => {
1488 alert("Request Failed");
1489 // restore the old cells
1490 jQuery('td.vbo-dragging-cells-tmp').removeClass('vbo-dragging-cells-tmp');
1491 jQuery('td.vbo-dragged-cells-tmp').removeClass('vbo-dragged-cells-tmp');
1492 //
1493 jQuery(".vbo-info-overlay-block").hide();
1494 }
1495 );
1496 }
1497
1498 /* DnD function to animate a success checkmark. The function may refresh the page once complete as this could be launched when there are multiple units for the rooms */
1499 function finalizeDndUpdate(mustReload, obj_res) {
1500 jQuery('#vbo-dnd-response').html(obj_res.message+' '+obj_res.vcm);
1501 jQuery('.vbo-loading-dnd-footer').hide();
1502 var start = 100;
1503 var mid = 145;
1504 var end = 250;
1505 var width = 22;
1506 var leftX = start;
1507 var leftY = start;
1508 var rightX = mid - (width / 2.7);
1509 var rightY = mid + (width / 2.7);
1510 var animationSpeed = 20;
1511 var closingdelay = 700;
1512 var ctx = document.getElementById('vbo-dnd-canvas-success').getContext('2d');
1513 ctx.lineWidth = width;
1514 ctx.strokeStyle = 'rgba(0, 150, 0, 1)';
1515 for (var i = start; i < mid; i++) {
1516 var drawLeft = window.setTimeout(function () {
1517 ctx.beginPath();
1518 ctx.moveTo(start, start);
1519 ctx.lineTo(leftX, leftY);
1520 ctx.stroke();
1521 leftX++;
1522 leftY++;
1523 }, 1 + (i * animationSpeed) / 3);
1524 }
1525 for (var i = mid; i < end; i++) {
1526 var drawRight = window.setTimeout(function () {
1527 ctx.beginPath();
1528 ctx.moveTo(leftX, leftY);
1529 ctx.lineTo(rightX, rightY);
1530 ctx.stroke();
1531 rightX++;
1532 rightY--;
1533 }, 1 + (i * animationSpeed) / 3);
1534 }
1535 //hide modal window
1536 window.setTimeout(function () {
1537 if (obj_res.vcm.length) {
1538 var vcmbtn = '<br clear="all"/><button type="button" class="btn btn-danger" onclick="'+(mustReload === true ? 'document.location.href=\'<?php echo $file_base; ?>?option=com_vikbooking&task=overv\'' : 'closeEsitDialog();')+'">' + Joomla.JText._('VBANNULLA') + '</button>';
1539 jQuery('#vbo-dnd-response').append(vcmbtn);
1540 } else {
1541 if (mustReload === true) {
1542 document.location.href='<?php echo $file_base; ?>?option=com_vikbooking&task=overv';
1543 } else {
1544 jQuery('.vbo-info-overlay-block').fadeOut(400, function(){
1545 //clear/reset canvas in case of previous drawing and response text
1546 ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
1547 jQuery('#vbo-dnd-response').html("");
1548 //
1549 });
1550 }
1551 }
1552 }, closingdelay + (i * animationSpeed) / 3);
1553 //
1554 }
1555
1556 /* DnD function that can be called by those with VCM that have disabled the automated updates. Simply closes the modal window */
1557 function closeEsitDialog() {
1558 jQuery('.vbo-info-overlay-block').fadeOut(400, function(){
1559 //clear/reset canvas in case of previous drawing and response text
1560 var ctx = document.getElementById('vbo-dnd-canvas-success').getContext('2d');
1561 ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
1562 jQuery('#vbo-dnd-response').html("");
1563 //
1564 });
1565 }
1566
1567 /* Hover Tooltip functions */
1568 function registerHoveringTooltip(that) {
1569 if (hovtip) {
1570 return false;
1571 }
1572 if (hovtimer) {
1573 clearTimeout(hovtimer);
1574 hovtimer = null;
1575 }
1576 var elem = jQuery(that);
1577 var cellheight = elem.outerHeight();
1578 var celldata = new Array();
1579 if (elem.hasClass('subroom-busy')) {
1580 celldata.push(elem.parent('tr').attr('data-subroomid'));
1581 celldata.push(elem.attr('data-day'));
1582 }
1583 hovtimer = setTimeout(() => {
1584 if (isdragging || (vboActionRoomRateData && vboActionRoomRateData?.start)) {
1585 // prevent tooltip from popping up if dragging or if selecting room rates
1586 unregisterHoveringTooltip();
1587
1588 return;
1589 }
1590
1591 // turn flag on
1592 hovtip = true;
1593
1594 // calculate cell-element position
1595 let pos_top = elem.offset().top;
1596 let pos_left = elem.offset().left;
1597 let elem_height = elem.outerHeight();
1598 let screen_width = window?.screen?.width || 0;
1599
1600 // build tooltip block element
1601 let tooltip_block = jQuery('<div></div>');
1602 tooltip_block.addClass('vbo-overview-tipblock');
1603 tooltip_block.append("<div class=\"vbo-overview-tipinner\"><span class=\"vbo-overview-tiploading\">" + Joomla.JText._('VIKLOADING') + "</span></div>");
1604 tooltip_block.append("<div class=\"vbo-overview-tipexpander\" style=\"display: none;\"><div class=\"vbo-overview-expandtoggle\"><i class=\"<?php echo VikBookingIcons::i('expand'); ?>\"></i></div></div>");
1605
1606 // calculate block position
1607 tooltip_block.css('top', (pos_top + elem_height - 16) + 'px');
1608 if (screen_width > 600 && (pos_left + 400) > screen_width) {
1609 // place the tooltip starting from right
1610 tooltip_block.css('left', (pos_left - (400 - elem.outerWidth())) + 'px');
1611 } else {
1612 // regular placing starting from left
1613 tooltip_block.css('left', (pos_left - 6) + 'px');
1614 }
1615
1616 // append block to body
1617 tooltip_block.appendTo(jQuery('body'));
1618
1619 // load tooltip bookings
1620 loadTooltipBookings(elem.attr('data-bids'), celldata);
1621 }, 1500);
1622 }
1623
1624 function unregisterHoveringTooltip() {
1625 clearTimeout(hovtimer);
1626 hovtimer = null;
1627 }
1628
1629 /**
1630 * @deprecated 1.16.10 (J) - 1.6.10 (WP)
1631 */
1632 function adjustHoveringTooltip() {
1633 setTimeout(() => {
1634 var ver_difflim = 35;
1635 var hor_difflim = 20;
1636 var tip_block = jQuery('.vbo-overview-tipblock');
1637 if (!tip_block || !tip_block.length) {
1638 return;
1639 }
1640 var table_wrap = tip_block.closest('.vbo-overv-montable-wrap');
1641
1642 if (tip_block.outerHeight() > table_wrap.outerHeight()) {
1643 // tooltip is too tall to fit in the table, render the modal instead
1644 tip_block.hide().closest('td.busy').trigger('click');
1645 return;
1646 }
1647
1648 // vertical positioning
1649 var otop = tip_block.offset().top;
1650 if (otop > 0 && otop < ver_difflim) {
1651 // adjust tooltip position
1652 tip_block.css('bottom', '-=' + (ver_difflim - otop));
1653 } else if (otop < table_wrap.offset().top) {
1654 // tooltip exceeds wrapping table, move it underneath the cell
1655 var extra_padding_top = 0;
1656 var tip_inner_padtop = tip_block.find('.vbo-overview-tipinner').css('padding-top');
1657 if (tip_inner_padtop) {
1658 // get only numbers
1659 extra_padding_top = tip_inner_padtop.replace(/[^0-9]/g, '');
1660 extra_padding_top = !isNaN(extra_padding_top) ? parseInt(extra_padding_top) : 0;
1661 }
1662 tip_block.css('bottom', 'auto').css('top', (tip_block.parent('td').outerHeight() - extra_padding_top));
1663 }
1664
1665 // horizontal positioning
1666 if (tip_block.offset().left < table_wrap.offset().left) {
1667 var left_diff = table_wrap.offset().left - tip_block.offset().left;
1668 if (left_diff > hor_difflim) {
1669 // tooltip exceeds table on the left, move it to the right of the cell (i.e 1st day of month)
1670 tip_block.css('right', tip_block.css('left'));
1671 tip_block.css('left', 'unset');
1672 }
1673 }
1674 }, 60);
1675 }
1676
1677 function hideVboTooltip() {
1678 jQuery('.vbo-overview-tipblock').remove();
1679 hovtip = false;
1680 }
1681
1682 function loadTooltipBookings(bids, celldata) {
1683 if (!bids || bids === undefined || !bids.length) {
1684 hideVboTooltip();
1685 return false;
1686 }
1687
1688 var subroomdata = celldata.length ? celldata[0] : '';
1689
1690 // perform the request
1691 VBOCore.doAjax(
1692 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=getbookingsinfo'); ?>",
1693 {
1694 tmpl: "component",
1695 idorders: bids,
1696 subroom: subroomdata
1697 },
1698 (res) => {
1699 try {
1700 var obj_res = typeof res === 'string' ? JSON.parse(res) : res;
1701 jQuery('.vbo-overview-tiploading').remove();
1702 var container = jQuery('.vbo-overview-tipinner');
1703 jQuery(obj_res).each(function(k, v) {
1704 // get base navigation URIs
1705 var base_uri_details = jQuery('.vbo-basenavuri-details').attr('href');
1706 var base_uri_edit = jQuery('.vbo-basenavuri-edit').attr('href');
1707
1708 // build content
1709 var bcont = "<div class=\"vbo-overview-tip-bookingcont\">";
1710 bcont += "<div class=\"vbo-overview-tip-bookingcont-left\">";
1711 bcont += "<div class=\"vbo-overview-tip-bid\"><span class=\"vbo-overview-tip-lbl\">ID <span class=\"vbo-overview-tip-lbl-innerleft\"><a href=\"" + base_uri_edit.replace('%d', v.id) + "\"><i class=\"<?php echo VikBookingIcons::i('edit'); ?>\"></i></a></span></span><span class=\"vbo-overview-tip-cnt\">"+v.id+"</span></div>";
1712 bcont += "<div class=\"vbo-overview-tip-bstatus\"><span class=\"vbo-overview-tip-lbl\"><?php echo addslashes(JText::translate('VBPVIEWORDERSEIGHT')); ?></span><span class=\"vbo-overview-tip-cnt\"><div class=\"badge "+(v.status == 'confirmed' ? 'badge-success' : 'badge-warning')+"\">"+v.status_lbl+"</div></span></div>";
1713 bcont += "<div class=\"vbo-overview-tip-bdate\"><span class=\"vbo-overview-tip-lbl\"><?php echo addslashes(JText::translate('VBPVIEWORDERSONE')); ?></span><span class=\"vbo-overview-tip-cnt\"><a href=\"" + base_uri_details.replace('%d', v.id) + "\">"+v.ts+"</a></span></div>";
1714 if (bctags_count > 0) {
1715 var bctag_title = '';
1716 var bctag_color = '#ffffff';
1717 if (v.colortag.hasOwnProperty('color')) {
1718 bctag_color = v.colortag.color;
1719 bctag_title = v.colortag.name;
1720 }
1721 bcont += "<div class=\"vbo-overview-tip-bctag-wrap\"><div class=\"vbo-overview-tip-bctag\" data-bid=\""+v.id+"\" data-ctagcolor=\""+bctag_color+"\" style=\"background-color: "+bctag_color+"; color: "+v.colortag.fontcolor+";\"><i class=\"vboicn-price-tags\"></i></div><span class=\"vbo-overview-tip-bctag-name\">" + bctag_title + "</span></div>";
1722 }
1723 bcont += "</div>";
1724 bcont += "<div class=\"vbo-overview-tip-bookingcont-right\">";
1725 bcont += "<div class=\"vbo-overview-tip-bcustomer\"><span class=\"vbo-overview-tip-lbl\"><?php echo addslashes(JText::translate('VBOCUSTOMER')); ?></span><span class=\"vbo-overview-tip-cnt\">"+v.cinfo+"</span></div>";
1726 if (v.roomsnum > 1) {
1727 bcont += "<div class=\"vbo-overview-tip-brooms\"><span class=\"vbo-overview-tip-lbl\">" + Joomla.JText._('VBEDITORDERROOMSNUM') + (parseInt(v.roomsnum) > 1 ? " (" + v.roomsnum + ")" : "") + "</span><span class=\"vbo-overview-tip-cnt\">" + v.room_names + "</span></div>";
1728 }
1729 bcont += "<div class=\"vbo-overview-tip-bguests\"><span class=\"vbo-overview-tip-lbl\">" + Joomla.JText._('VBDAYS') + (v.split_stay > 0 ? ' (' + Joomla.JText._('VBO_SPLIT_STAY') + ')' : '') + "</span><span class=\"vbo-overview-tip-cnt hasTooltip\" title=\"" + Joomla.JText._('VBPICKUPAT') + " " + v.checkin + " - " + Joomla.JText._('VBRELEASEAT') + " " + v.checkout + "\">" + v.days + ", " + Joomla.JText._('VBEDITORDERADULTS') + ": " + v.tot_adults + (v.tot_children > 0 ? ", " + Joomla.JText._('VBEDITORDERCHILDREN') + ": " + v.tot_children : "") + "</span></div>";
1730 if (v.hasOwnProperty('rindexes')) {
1731 for (var rindexk in v.rindexes) {
1732 if (v.rindexes.hasOwnProperty(rindexk)) {
1733 bcont += "<div class=\"vbo-overview-tip-brindexes\"><span class=\"vbo-overview-tip-lbl\">" + rindexk + "</span><span class=\"vbo-overview-tip-cnt\">" + v.rindexes[rindexk] + "</span></div>";
1734 }
1735 }
1736 }
1737 if (v.hasOwnProperty('channelimg')) {
1738 bcont += "<div class=\"vbo-overview-tip-bprovenience\"><span class=\"vbo-overview-tip-lbl\">" + Joomla.JText._('VBPVIEWORDERCHANNEL') + "</span><span class=\"vbo-overview-tip-cnt\">" + v.channelimg + "</span></div>";
1739 }
1740 if (v.hasOwnProperty('optindexes') && celldata.length) {
1741 var subroomids = celldata[0].split('-');
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>";
1743 }
1744 bcont += "<div class=\"vbo-overview-tip-bookingcont-total\">";
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>";
1746 if (v.totpaid > 0.00) {
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>";
1748 }
1749 var getnotes = v.adminnotes;
1750 if (getnotes !== null && getnotes.length) {
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>";
1752 }
1753 bcont += "</div>";
1754 bcont += "</div>";
1755 bcont += "</div>";
1756 container.append(bcont);
1757 jQuery('.vbo-overview-tipexpander').show();
1758 });
1759
1760 /**
1761 * @deprecated 1.16.10 (J) - 1.6.10 (WP)
1762 */
1763 // adjust the position so that it won't go under other contents
1764 // adjustHoveringTooltip();
1765
1766 jQuery(".hasTooltip").tooltip();
1767 } catch(err) {
1768 // restore
1769 hideVboTooltip();
1770 // display error
1771 console.error('could not parse JSON response', err, res);
1772 alert('Could not parse JSON response');
1773 }
1774 },
1775 (err) => {
1776 // restore
1777 hideVboTooltip();
1778 // display error
1779 console.error(err);
1780 alert(err.responseText);
1781 }
1782 );
1783 }
1784
1785 /**
1786 * Set a sub-unit to a room booking. Triggered by the room-day bookings modal
1787 */
1788 function vboSetSubunit(bid, rid, orkey, rindex) {
1789 if (!(rindex + '').length) {
1790 return false;
1791 }
1792
1793 if (!confirm(Joomla.JText._('VBOFEATASSIGNUNIT') + rindex + '?')) {
1794 return false;
1795 }
1796
1797 // show loading
1798 VBOCore.emitEvent('vbo-loading-modal-overv-rdaybookings');
1799
1800 // make the request to set the room sub-unit
1801 VBOCore.doAjax(
1802 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=bookings.set_room_booking_subunit'); ?>",
1803 {
1804 bid: bid,
1805 rid: rid,
1806 orkey: orkey,
1807 rindex: rindex,
1808 tmpl: 'component'
1809 },
1810 (res) => {
1811 // dismiss the modal (no need to stop the loading)
1812 VBOCore.emitEvent('vbo-dismiss-modal-overv-rdaybookings');
1813
1814 try {
1815 var obj_res = typeof res === 'string' ? JSON.parse(res) : res;
1816 if (!obj_res.hasOwnProperty('nights') || !Array.isArray(obj_res['nights']) || !obj_res['nights'].length) {
1817 // invalid response
1818 throw new Error('Invalid response');
1819 }
1820
1821 // check if cells can be occupied
1822 var sub_rows = jQuery('tr.vboverviewtablerow-subunit[data-subroomid="' + rid + '-' + rindex + '"]');
1823 if (!sub_rows || !sub_rows.length) {
1824 console.error('Could not find any room sub-unit row');
1825 return false;
1826 }
1827
1828 // loop through all sub-unit rows to match the cells for the nights affected
1829 sub_rows.each(function() {
1830 var sub_row = jQuery(this);
1831 obj_res['nights'].forEach((ymd, kindex) => {
1832 let sub_row_cell = sub_row.find('td[data-day="' + ymd + '"]');
1833 if (sub_row_cell.length) {
1834 sub_row_cell.removeClass('subnotbusy').addClass('subroom-busy');
1835 if (kindex == 0) {
1836 sub_row_cell.addClass('vbo-checkinday');
1837 }
1838 sub_row_cell.attr('data-bids', '-' + obj_res['bid'] + '-');
1839 if (!sub_row_cell.find('span.vbo-draggable-sp').length) {
1840 sub_row_cell.prepend('<span class="vbo-draggable-sp"><a class="vbo-overview-subredday">&bull;</a></span>');
1841 }
1842 }
1843 });
1844 });
1845 } catch(err) {
1846 console.error('could not parse JSON response', err, res);
1847 alert('Could not parse JSON response');
1848 }
1849 },
1850 (err) => {
1851 // stop loading
1852 VBOCore.emitEvent('vbo-loading-modal-overv-rdaybookings');
1853
1854 // log and display the error
1855 console.error(err);
1856 alert(err.responseText);
1857 }
1858 );
1859 }
1860
1861 /**
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..
1864 */
1865 function vboMoveSubunit(bid, rid, old_rindex, new_rindex, dday) {
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')); ?>);
1873 return false;
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
1881 // check if movement can be made
1882 var cur_tr = jQuery('tr.vboverviewtablerow-subunit[data-subroomid="' + rid + '-' + old_rindex + '"]');
1883 if (!cur_tr || !cur_tr.length) {
1884 console.error('could not find the parent row of the subunit cells');
1885 return false;
1886 }
1887 var maincell = cur_tr.find('td.subroom-busy[data-day="' + dday + '"]');
1888 if (!maincell || !maincell.length) {
1889 console.error('could not find the main cell of the subunit to move');
1890 return false;
1891 }
1892 var dest_tr = jQuery('tr.vboverviewtablerow-subunit[data-subroomid="' + rid + '-' + new_rindex + '"]');
1893 if (!dest_tr || !dest_tr.length) {
1894 console.error('could not find the destination row for the subunit cells');
1895 return false;
1896 }
1897 var targetbids = maincell.attr('data-bids');
1898 if (targetbids.indexOf('-' + bid + '-') < 0) {
1899 console.error('given bid does not match with cell bids', bid, targetbids);
1900 return false;
1901 }
1902 var firstcell = maincell;
1903 var loop = true;
1904 while (loop === true) {
1905 var prevsib = firstcell.prev();
1906 if (prevsib && prevsib.length && prevsib.attr('data-bids') && prevsib.attr('data-bids').indexOf('-' + bid + '-') >= 0) {
1907 firstcell = prevsib;
1908 } else {
1909 loop = false;
1910 }
1911 }
1912 // make sure to get the check-in day (first cell)
1913 dday = firstcell.attr('data-day');
1914 var destcell = dest_tr.find('td.subnotbusy[data-day="' + dday + '"]');
1915 if (!destcell || !destcell.length) {
1916 // first destination cell is occupied, check if swap is allowed
1917 if (dest_tr.find('td.subroom-busy[data-day="' + dday + '"]').length) {
1918 var landbids = dest_tr.find('td.subroom-busy[data-day="' + dday + '"]').attr('data-bids');
1919 return vboSwapRoomSubunits(bid, landbids.replaceAll('-', ''), rid, old_rindex, new_rindex, dday);
1920 }
1921 console.error('could not find the first free destination cell');
1922 return false;
1923 }
1924 // check if all dates are free before making movements. Redundant, but useful.
1925 var freedates = true;
1926 var copyfirstcell = firstcell;
1927 var copydday = dday;
1928 loop = true;
1929 while (loop === true) {
1930 var nextsib = copyfirstcell.next();
1931 if (nextsib && nextsib.length && nextsib.attr('data-bids') && nextsib.attr('data-bids').indexOf('-' + bid + '-') >= 0) {
1932 copyfirstcell = nextsib;
1933 copydday = copyfirstcell.attr('data-day');
1934 var copydestcell = dest_tr.find('td.subnotbusy[data-day="' + copydday + '"]');
1935 if (!copydestcell || !copydestcell.length) {
1936 console.error('could not find the next free destination cell for ' + copydday);
1937 alert(<?php echo json_encode(JText::translate('VBBOOKNOTMADE')); ?>);
1938 freedates = false;
1939 loop = false;
1940 }
1941 } else {
1942 loop = false;
1943 }
1944 }
1945 if (freedates === false) {
1946 return false;
1947 }
1948
1949 // loading opacity
1950 jQuery('.vbo-overview-tipblock').css('opacity', '0.6');
1951
1952 // perform the request
1953 VBOCore.doAjax(
1954 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=switchRoomIndex'); ?>",
1955 {
1956 tmpl: "component",
1957 bid: bid,
1958 rid: rid,
1959 old_rindex: old_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,
1963 },
1964 (res) => {
1965 if (res.indexOf('e4j.error') >= 0 ) {
1966 alert(res.replace("e4j.error.", ""));
1967
1968 // restore loading opacity in container
1969 jQuery('.vbo-overview-tipblock').css('opacity', '1');
1970
1971 // abort
1972 return;
1973 }
1974
1975 // hide tooltip
1976 hideVboTooltip();
1977
1978 // loop for moving cells
1979 loop = true;
1980 while (loop === true) {
1981 // populate destination cell attributes, classes and content
1982 destcell.removeClass('subnotbusy').addClass('subroom-busy').attr('data-bids', firstcell.attr('data-bids'));
1983 if (firstcell.hasClass('vbo-checkinday')) {
1984 firstcell.removeClass('vbo-checkinday');
1985 destcell.addClass('vbo-checkinday');
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
1992 firstcell.children().not('.vbo-tableaux-booking-checkout').not('.vbo-roomdaynote-trigger').appendTo(destcell);
1993
1994 // re-bind hover event for tooltip
1995 jQuery(destcell).hover(function() {
1996 registerHoveringTooltip(this);
1997 }, unregisterHoveringTooltip);
1998
1999 // clean up the previous cell from which we moved data
2000 firstcell.removeClass('subroom-busy').addClass('subnotbusy').attr('data-bids', '');
2001
2002 // check if we have a next iteration to perform
2003 let nextsib = firstcell.next();
2004 if (nextsib && nextsib.length && nextsib.attr('data-bids') && nextsib.attr('data-bids').indexOf('-' + bid + '-') >= 0) {
2005 // prepare data for the next iteration
2006 firstcell = nextsib;
2007 dday = firstcell.attr('data-day');
2008 destcell = dest_tr.find('td.subnotbusy[data-day="' + dday + '"]');
2009 if (!destcell || !destcell.length) {
2010 console.error('could not find the next free destination cell');
2011 loop = false;
2012 }
2013 } else {
2014 // all cells were moved, break the loop
2015 loop = false;
2016 // check if we need to move the checkout snake as well
2017 if (nextsib && nextsib.find('.vbo-tableaux-booking-checkout').length) {
2018 dday = nextsib.attr('data-day');
2019 destcell = dest_tr.find('td.subnotbusy[data-day="' + dday + '"]');
2020 if (!destcell || !destcell.length) {
2021 // the landing checkout snake may be on an occupied date
2022 destcell = dest_tr.find('td.subroom-busy[data-day="' + dday + '"]');
2023 }
2024 if (destcell && destcell.length) {
2025 // prepend the checkout snake to the next cell
2026 nextsib.find('.vbo-tableaux-booking-checkout').prependTo(destcell);
2027 }
2028 }
2029 }
2030 }
2031
2032 // dispatch the event for the sub-units updated
2033 VBOCore.emitEvent('vbo-overv-sub-units-updated');
2034 },
2035 (err) => {
2036 alert('Request Failed');
2037 // restore loading opacity in container
2038 jQuery('.vbo-overview-tipblock').css('opacity', '1');
2039 }
2040 );
2041
2042 return true;
2043 }
2044
2045 /**
2046 * Swap room sub-unit with another sub-unit on an occupied date. Triggered by DnD or by manual sub-unit switching.
2047 */
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 }
2059 if (!confirm(Joomla.JText._('VBO_CONF_SWAP_RNUMB').replace('%s', old_rindex).replace('%s', new_rindex))) {
2060 return false;
2061 }
2062 if (!bid || !bidtwo || !rid || !old_rindex || !new_rindex || !dday) {
2063 console.error('Missing required arguments', bid, bidtwo, rid, old_rindex, new_rindex, dday);
2064 return false;
2065 }
2066 // check if movement can be made
2067 let cur_tr = jQuery('tr.vboverviewtablerow-subunit[data-subroomid="' + rid + '-' + old_rindex + '"]');
2068 if (!cur_tr || !cur_tr.length) {
2069 console.error('could not find the parent row of the subunit cells');
2070 return false;
2071 }
2072 let maincell = cur_tr.find('td.subroom-busy[data-day="' + dday + '"]');
2073 if (!maincell || !maincell.length) {
2074 console.error('could not find the main cell of the subunit to move');
2075 return false;
2076 }
2077 let dest_tr = jQuery('tr.vboverviewtablerow-subunit[data-subroomid="' + rid + '-' + new_rindex + '"]');
2078 if (!dest_tr || !dest_tr.length) {
2079 console.error('could not find the destination row for the subunit cells');
2080 return false;
2081 }
2082 let destcell = dest_tr.find('td.subroom-busy[data-day="' + dday + '"]');
2083 if (!destcell || !destcell.length) {
2084 console.error('could not find the destination cell of the subunit to move');
2085 return false;
2086 }
2087
2088 // gather the cells for the swop
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;
2097
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 }
2113 while (true) {
2114 if (!swap_cell_from || !swap_cell_from.length) {
2115 // next cell not found
2116 break;
2117 }
2118 let swap_bids = swap_cell_from.attr('data-bids');
2119 if (!swap_bids || swap_bids.indexOf('-' + bid + '-') < 0) {
2120 // last occupied cell reached
2121 break;
2122 }
2123 // push cloned cell data to move
2124 swap_cells_one.push(swap_cell_from.children().not('.vbo-tableaux-booking-checkout').not('.vbo-roomdaynote-trigger').clone(true));
2125 swap_dates_one.push(swap_cell_from.attr('data-day'));
2126 swap_styles_one.push(swap_cell_from.attr('style'));
2127 // set next iteration cell
2128 swap_cell_from = swap_cell_from.next();
2129 }
2130
2131 // register the elements to swap of the second sub-unit
2132 while (true) {
2133 if (!swap_cell_to || !swap_cell_to.length) {
2134 // next cell not found
2135 break;
2136 }
2137 let swap_bids = swap_cell_to.attr('data-bids');
2138 if (!swap_bids || swap_bids.indexOf('-' + bidtwo + '-') < 0) {
2139 // last occupied cell reached
2140 break;
2141 }
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
2143 if (!swap_cells_two.length && !swap_cell_to.hasClass('vbo-checkinday')) {
2144 // prepend first the cells until the check-in or until not found
2145 let prev_cell = swap_cell_to;
2146 while (true) {
2147 prev_cell = prev_cell.prev();
2148 if (!prev_cell || !prev_cell.length) {
2149 break;
2150 }
2151 let prev_cell_snake = prev_cell.find('.vbo-tableaux-booking').not('.vbo-tableaux-booking-checkout');
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'))) {
2153 // prepend cloned cell data to move
2154 swap_cells_two.unshift(prev_cell.children().not('.vbo-tableaux-booking-checkout').not('.vbo-roomdaynote-trigger').clone(true));
2155 swap_dates_two.unshift(prev_cell.attr('data-day'));
2156 swap_styles_two.unshift(prev_cell.attr('style'));
2157 } else {
2158 // no more previous cells
2159 break;
2160 }
2161 if (prev_cell.hasClass('vbo-checkinday')) {
2162 break;
2163 }
2164 }
2165 }
2166 // push cloned cell data to move
2167 swap_cells_two.push(swap_cell_to.children().not('.vbo-tableaux-booking-checkout').not('.vbo-roomdaynote-trigger').clone(true));
2168 swap_dates_two.push(swap_cell_to.attr('data-day'));
2169 swap_styles_two.push(swap_cell_to.attr('style'));
2170 // set next iteration cell
2171 swap_cell_to = swap_cell_to.next();
2172 }
2173
2174 if (!swap_cells_one.length || !swap_cells_two.length) {
2175 console.error('could not gather the cells to swap', swap_cells_one, swap_cells_two);
2176 return false;
2177 }
2178
2179 // walk over the cells of the first room to swap and check the corresponding destination
2180 let check_destcell = destcell;
2181 for (let i = 0; i < swap_cells_one.length; i++) {
2182 if (!check_destcell || !check_destcell.length) {
2183 // this cell does not exist, abort
2184 console.error('destination cell to #' + (i + 1) + ' was not found');
2185 return false;
2186 }
2187 let dest_bids = check_destcell.attr('data-bids');
2188 if (dest_bids && dest_bids.length && dest_bids.indexOf('-' + bidtwo + '-') < 0) {
2189 // this cell is already occupied, abort
2190 console.error('destination cell to #' + (i + 1) + ' is occupied by another reservation');
2191 alert(<?php echo json_encode(JText::translate('VBBOOKNOTMADE')); ?>);
2192 return false;
2193 }
2194 // set next destination cell
2195 check_destcell = check_destcell.next();
2196 }
2197
2198 // walk over the cells of the second room to swap and check the corresponding destination
2199 check_destcell = maincell;
2200 for (let i = 0; i < swap_cells_two.length; i++) {
2201 if (!check_destcell || !check_destcell.length) {
2202 // this cell does not exist, abort
2203 console.error('destination cell from #' + (i + 1) + ' was not found');
2204 return false;
2205 }
2206 let dest_bids = check_destcell.attr('data-bids');
2207 if (dest_bids && dest_bids.length && dest_bids.indexOf('-' + bid + '-') < 0) {
2208 // this cell is already occupied, abort
2209 console.error('destination cell from #' + (i + 1) + ' is occupied by another reservation');
2210 alert(<?php echo json_encode(JText::translate('VBBOOKNOTMADE')); ?>);
2211 return false;
2212 }
2213 // set next destination cell
2214 check_destcell = check_destcell.next();
2215 }
2216
2217 // make the AJAX request and swap cells only in case of success
2218 VBOCore.doAjax(
2219 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=bookings.swap_room_subunits'); ?>",
2220 {
2221 bid_one: bid,
2222 bid_two: bidtwo,
2223 rid: rid,
2224 index_one: old_rindex,
2225 index_two: new_rindex,
2226 checkin: dday,
2227 tmpl: 'component'
2228 },
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
2236 // move the cells of the first sub-unit onto the cells of the second sub-unit
2237 for (let counter = 0; counter < swap_cells_one.length; counter++) {
2238 // remove elements from target
2239 target_cell.children().not('.vbo-tableaux-booking-checkout').not('.vbo-roomdaynote-trigger').remove();
2240 // swap cell elements
2241 swap_cells_one[counter].appendTo(target_cell);
2242 // set proper bids attribute
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 }
2253 // check if we are swapping with a non check-in day sub-unit occupied cell
2254 if (counter === 0 && !target_cell.hasClass('vbo-checkinday')) {
2255 target_cell.addClass('vbo-checkinday');
2256 swap_diff_dates = true;
2257 } else if (counter > 0 && !target_cell.hasClass('subroom-busy')) {
2258 target_cell.removeClass('subnotbusy').addClass('subroom-busy');
2259 }
2260 // check if the reservations have different nights of stay
2261 let cell_will_occupy = true;
2262 if (counter > 0 && swap_diff_dates && !swap_dates_two.includes(swap_dates_one[counter])) {
2263 // the swapped unit will not occupy this cell, so clean it
2264 cell_will_occupy = false;
2265 } else if ((counter + 1) > swap_cells_two.length) {
2266 // this reservation is longer than the other
2267 cell_will_occupy = false;
2268 }
2269 if (!cell_will_occupy) {
2270 // remove the children elements from this longer reservation
2271 current_cell.children().not('.vbo-tableaux-booking-checkout').not('.vbo-roomdaynote-trigger').remove();
2272 // remove the occupied class as well
2273 current_cell.removeClass('subroom-busy').addClass('subnotbusy').attr('data-bids', '');
2274 }
2275 // set next target cell
2276 target_cell = target_cell.next();
2277 // update current cell
2278 current_cell = current_cell.next();
2279 }
2280
2281 // move the checkout snake of the first sub-unit
2282 if (target_cell && target_cell.length && current_cell && current_cell.length && current_cell.find('.vbo-tableaux-booking-checkout').length) {
2283 current_cell.find('.vbo-tableaux-booking-checkout').prependTo(target_cell);
2284 }
2285
2286 // move the cells of the second sub-unit onto the cells of the first sub-unit
2287 target_cell = maincell;
2288 current_cell = destcell;
2289
2290 // check if we are swapping with a non check-in day sub-unit occupied cell
2291 if (swap_diff_dates) {
2292 let prev_land_cell = current_cell;
2293 while (true) {
2294 prev_land_cell = prev_land_cell.prev();
2295 if (!prev_land_cell || !prev_land_cell.length) {
2296 break;
2297 }
2298 if (prev_land_cell.hasClass('subroom-busy')) {
2299 // overwrite first cell to move and to land
2300 current_cell = prev_land_cell;
2301 target_cell = target_cell.prev();
2302 // empty this cell because no one will occupy it
2303 current_cell.children().not('.vbo-tableaux-booking-checkout').not('.vbo-roomdaynote-trigger').remove();
2304 // make the cell free
2305 current_cell.removeClass('subroom-busy').addClass('subnotbusy').attr('data-bids', '');
2306 }
2307 if (prev_land_cell.hasClass('vbo-checkinday')) {
2308 // we have reached the first cell of this booking
2309 break;
2310 }
2311 }
2312 }
2313
2314 for (let counter = 0; counter < swap_cells_two.length; counter++) {
2315 // remove elements from target
2316 target_cell.children().not('.vbo-tableaux-booking-checkout').not('.vbo-roomdaynote-trigger').remove();
2317 // swap cell elements
2318 swap_cells_two[counter].appendTo(target_cell);
2319 // set proper bids attribute
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 }
2328 // check if we are swapping with a non check-in day sub-unit occupied cell
2329 if (counter === 0 && !target_cell.hasClass('vbo-checkinday')) {
2330 target_cell.addClass('vbo-checkinday');
2331 if (current_cell.hasClass('vbo-checkinday') && !swap_dates_one.includes(swap_dates_two[counter])) {
2332 current_cell.removeClass('vbo-checkinday');
2333 }
2334 } else if (counter > 0 && target_cell.hasClass('vbo-checkinday')) {
2335 target_cell.removeClass('vbo-checkinday');
2336 }
2337 if (!target_cell.hasClass('subroom-busy')) {
2338 target_cell.removeClass('subnotbusy').addClass('subroom-busy');
2339 }
2340 // check if the reservations have different nights of stay
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
2343 // remove the children elements from this longer reservation
2344 current_cell.children().not('.vbo-tableaux-booking-checkout').not('.vbo-roomdaynote-trigger').remove();
2345 // remove the occupied class as well
2346 current_cell.removeClass('subroom-busy').addClass('subnotbusy').attr('data-bids', '');
2347 // set the occupied class onto the target
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);
2355 }
2356 // set next target cell
2357 target_cell = target_cell.next();
2358 // update current cell
2359 current_cell = current_cell.next();
2360 }
2361
2362 // move the checkout snake of the second sub-unit
2363 if (target_cell && target_cell.length && current_cell && current_cell.length && current_cell.find('.vbo-tableaux-booking-checkout').length) {
2364 // make sure to move just the first check-out snake, because snakes with equal duration may have moved the check-out here already
2365 current_cell.find('.vbo-tableaux-booking-checkout').first().prependTo(target_cell);
2366 }
2367
2368 // dispatch the event for the sub-units updated
2369 VBOCore.emitEvent('vbo-overv-sub-units-updated');
2370 },
2371 (err) => {
2372 alert(err.responseText);
2373 console.error(err.responseText);
2374 }
2375 );
2376
2377 return true;
2378 }
2379
2380 /**
2381 * Open a booking in a new tab.
2382 */
2383 function vboOvervOpenBooking(bid) {
2384 var open_url = jQuery('.vbo-basenavuri-details').attr('href');
2385 open_url = open_url.replace('%d', bid);
2386 // navigate in a new tab
2387 window.open(open_url, '_blank');
2388 }
2389
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 /**
2622 * DOM ready state
2623 */
2624 jQuery(function() {
2625 // register to the event emitted when a new booking is created through an admin widget
2626 document.addEventListener('vbo_new_booking_created', (e) => {
2627 if (!e || !e.detail || !e.detail.hasOwnProperty('bid') || !e.detail['bid']) {
2628 // do nothing
2629 return;
2630 }
2631 // reload the page to display the new booking just created
2632 location.reload();
2633 });
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
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 */
2663 jQuery("td.vbo-checkinday span.vbo-draggable-sp").each(function(k, v) {
2664 var parentheight = jQuery(this).closest('td').height();
2665 var spheight = jQuery(this).height();
2666 var padsp = Math.floor((parentheight - spheight) / 2);
2667 jQuery(this).css({"padding": padsp + "px 0"});
2668 });
2669
2670 /* Expand/Collapse tooltip */
2671 jQuery(document.body).on("click", ".vbo-overview-expandtoggle", function() {
2672 jQuery(this).closest('.vbo-overview-tipblock').toggleClass('vbo-overview-tipblock-expanded');
2673 });
2674
2675 /* DnD Start */
2676
2677 /* DnD Event dragstart */
2678 jQuery(document.body).on("dragstart", "td.vbo-checkinday span, td.vbo-checkinday .vbo-tableaux-booking-checkin", function(e) {
2679 // start dragging and prevent tooltip from popping up
2680 isdragging = true;
2681 if (hovtip === true) {
2682 hideVboTooltip();
2683 }
2684
2685 let parentcell = jQuery(this).closest('td');
2686 if (parentcell.hasClass('busytmplock') || parentcell.hasClass('busy-sharedcalendar')) {
2687 return false;
2688 }
2689
2690 // reset pool of booked date cells
2691 cellspool = [];
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');
2697 if (!bidstart.length || !checkind.length || !roomid.length) {
2698 return false;
2699 }
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
2728 // count booking nights and set booked date cells
2729 let totnights = countBookingCells(parentcell, bidstart, roomid);
2730
2731 // add dragging class
2732 if (jQuery(this).find('.vbo-tableaux-guest-name').length) {
2733 jQuery(this).find('.vbo-tableaux-guest-name').addClass('vbo-dragging-sp');
2734 } else {
2735 if (!jQuery(this).hasClass('vbo-tableaux-booking-avatar')) {
2736 jQuery(this).addClass('vbo-dragging-sp');
2737 }
2738 }
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
2753 // populate dataTransfer data
2754 let parent_rid = '';
2755 let subunit_id = '';
2756 if (parentcell.hasClass('subroom-busy')) {
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 }
2762 parent_rid = subdata[0];
2763 subunit_id = subdata[1];
2764 }
2765 dt.setData('Checkin', checkind);
2766 dt.setData('Roomid', roomid);
2767 dt.setData('Bid', bidstart);
2768 dt.setData('Nights', totnights);
2769 dt.setData('Parentrid', parent_rid);
2770 dt.setData('Subunit', subunit_id);
2771 });
2772
2773 /* DnD Events dragenter dragover drop */
2774 jQuery(document.body).on("dragenter dragover drop", "td.notbusy, td.busy, td.subnotbusy, td.subroom-busy", function(e) {
2775 // always prevent default
2776 e.preventDefault();
2777
2778 // gather dragging cell data
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');
2786
2787 // check if we are hovering something that requires styling
2788 if (e.type === 'dragover') {
2789 let hover_cell = jQuery(this);
2790 if (hover_cell.hasClass('subroom-busy') && hover_cell.hasClass('vbo-checkinday')) {
2791 if (hover_cell.attr('data-day') != checkind && totnights > 1) {
2792 hover_cell.find('.vbo-tableaux-booking-checkin').addClass('vbo-droppable-cell');
2793 }
2794 }
2795 // do not proceed any further
2796 return;
2797 }
2798
2799 // only look for drop
2800 if (e.type !== 'drop') {
2801 // ignore if we are not dropping the element
2802 return;
2803 }
2804
2805 // always reset pool of cells
2806 newcellspool = [];
2807
2808 /* check if drop is allowed */
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');
2812 if (landrid === undefined || !landrid.length) {
2813 return false;
2814 }
2815
2816 if (jQuery(this).hasClass('busy') && !jQuery(this).hasClass('vbo-partially')) {
2817 if (bid != landbids) {
2818 // landed on an occupied date but not for the same booking ID so drop is not allowed on this day
2819 return false;
2820 }
2821 }
2822
2823 // build pool of cells
2824 let freenights = countCellFreeNights(jQuery(this), landrid, totnights, bid);
2825 if (freenights < totnights) {
2826 alert(Joomla.JText._('VBOVWDNDERRNOTENCELLS').replace('%s', freenights).replace('%d', totnights));
2827 return false;
2828 }
2829
2830 // collect the new dates
2831 let nowdatefrom = jQuery(newcellspool[0]).attr('data-day');
2832 let nowdateto = jQuery(newcellspool[(newcellspool.length -1)]).attr('data-day');
2833
2834 // check if we are just moving a sub-unit onto another for the same dates
2835 if (jQuery(this).hasClass('subnotbusy') || jQuery(this).hasClass('subroom-busy')) {
2836 // dropping on a sub-unit is only allowed onto the same room ID, for the same dates (switch room index only)
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 }
2842 if (!subdata || !parent_rid || !subunit_id || parent_rid != subdata[0] || !subdata[1] || subunit_id == subdata[1]) {
2843 // display proper error
2844 if (subdata[1] && subunit_id == subdata[1]) {
2845 // must use the same dates for sub-units
2846 alert(Joomla.JText._('VBO_DRAG_SUBUNITS_SAMEDATE'));
2847 } else {
2848 // only sub-units can be moved onto rows for sub-units
2849 alert(Joomla.JText._('VBO_ERR_SUBUN_MOVE_SUBUN'));
2850 }
2851 return false;
2852 }
2853
2854 // sub-unit drag&drop is only allowed on the same date to change sub-unit index
2855 if (nowdatefrom != checkind) {
2856 alert(Joomla.JText._('VBO_DRAG_SUBUNITS_SAMEDATE'));
2857 return false;
2858 }
2859
2860 // attempt to perform the switch-room-index request
2861 if (jQuery(this).hasClass('subnotbusy')) {
2862 // move sub-unit to a different index
2863 if (!vboMoveSubunit(bid.replaceAll('-', ''), parent_rid, subunit_id, subdata[1], checkind)) {
2864 return false;
2865 }
2866 } else {
2867 // dropped on an occupied sub-unit, so ask for swap confirmation
2868 if (!vboSwapRoomSubunits(bid.replaceAll('-', ''), landbids.replaceAll('-', ''), parent_rid, subunit_id, subdata[1], checkind)) {
2869 return false;
2870 }
2871 }
2872
2873 // do not proceed
2874 return;
2875 }
2876
2877 /* populate temporary class for the new cells */
2878 jQuery(cellspool).each(function(k, v) {
2879 v.addClass('vbo-dragging-cells-tmp');
2880 });
2881
2882 // bookings for multiple rooms should add the same dragging class also to the booking for the other rooms
2883 let samebidcells = jQuery("td.vbo-checkinday[data-bids='" + bid + "']");
2884 if (samebidcells.length > 1) {
2885 jQuery("td[data-bids='" + bid + "']").each(function(k, v) {
2886 if (!jQuery(v).hasClass('vbo-dragging-cells-tmp')) {
2887 jQuery(v).addClass('vbo-dragging-cells-tmp');
2888 }
2889 });
2890 }
2891 jQuery(newcellspool).each(function(k, v) {
2892 v.addClass('vbo-dragged-cells-tmp');
2893 });
2894
2895 /* populate and showing loading message */
2896 jQuery('.vbo-loading-dnd-head').text(Joomla.JText._('VBOVWDNDMOVINGBID').replace('%d', bid));
2897 let movingmess = '';
2898 if (roomid != landrid) {
2899 movingmess += Joomla.JText._('VBOVWDNDMOVINGROOM').replace('%s', jQuery("td[data-roomid='"+landrid+"']").first().find('span.vbo-overview-roomname').text() );
2900 }
2901 if (nowdatefrom != jQuery(cellspool[0]).attr('data-day') || nowdateto != jQuery(cellspool[(cellspool.length -1)]).attr('data-day')) {
2902 if (roomid != landrid) {
2903 movingmess += ', ';
2904 }
2905 movingmess += Joomla.JText._('VBOVWDNDMOVINGDATES').replace('%s', nowdatefrom + ' - ' + nowdateto);
2906 }
2907
2908 // hide tooltip, if any
2909 hideVboTooltip();
2910
2911 // display moving message
2912 jQuery('.vbo-loading-dnd-body').text(movingmess);
2913 jQuery('.vbo-info-overlay-block').fadeIn();
2914 jQuery('.vbo-loading-dnd-footer').show();
2915
2916 /* fire the Ajax request after 1.5 seconds just for giving visibility to the loading message */
2917 setTimeout(function() {
2918 doAlterBooking(bid, roomid, landrid);
2919 }, 1500);
2920 });
2921
2922 /* DnD Event dragend: remove class, attribute and dataTransfer Data if drag ends on the same position as dragstart or onto an invalid date */
2923 jQuery(document.body).on("dragend", "td.vbo-checkinday span, td.vbo-checkinday .vbo-tableaux-booking-checkin", function(e) {
2924 // stop dragging to restore tooltip functions
2925 isdragging = false;
2926 // we could also access the originally dragged cell
2927 let dt = e.originalEvent.dataTransfer;
2928 // restore temporary classes
2929 jQuery('.vbo-dragging-sp').removeClass('vbo-dragging-sp');
2930 jQuery('.vbo-droppable-cell').removeClass('vbo-droppable-cell');
2931 });
2932 /* DnD End */
2933
2934 /* Show New Reservation Form & Modal with bookings - Start */
2935 jQuery('td.busy, td.notbusy').dblclick(function() {
2936 var curday = jQuery(this).attr('data-day');
2937 var roomid = jQuery(this).parent('tr').find('td.roomname').attr('data-roomid');
2938 roomid = roomid && roomid.length ? roomid : '';
2939 last_room_click = '';
2940 last_date_click = '';
2941 next_date_click = '';
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});
2943 });
2944
2945 jQuery(document.body).on('click', 'td.busy, td.notbusy, td.busytmplock, td.subroom-busy', function(e) {
2946 if (jQuery(this).hasClass('vbo-widget-booskcal-cell-mday')) {
2947 return;
2948 }
2949
2950 if (jQuery(this).hasClass('busy') || jQuery(this).hasClass('busytmplock') || jQuery(this).hasClass('subroom-busy')) {
2951 // make sure the clicked target is not inside the tooltip
2952 if (e && e.target) {
2953 if (jQuery(e.target).closest('.vbo-overview-tipblock').length) {
2954 // abort when click originated from the tooltip
2955 return;
2956 }
2957 if (jQuery(e.target).is('a')) {
2958 // abort when click is made on a link (View choosebusy)
2959 return;
2960 }
2961 if (jQuery(e.target).hasClass('vbo-roomdaynote-display') || jQuery(e.target).hasClass('vbo-roomdaynote-trigger')) {
2962 // abort when click is made on a room-day note
2963 return;
2964 }
2965 }
2966
2967 // trigger mouseleave event to prevent tooltip from showing
2968 jQuery(this).trigger('mouseleave');
2969
2970 // get room name, date and day-bids
2971 let room_name = '';
2972 let sub_room_data = '';
2973 let room_id = 0;
2974 if (jQuery(this).hasClass('subroom-busy')) {
2975 sub_room_data = jQuery(this).parent('tr').attr('data-subroomid');
2976 room_id = sub_room_data.split('-')[0];
2977 room_name = jQuery('td.roomname[data-roomid="' + room_id + '"]').not('.subroomname').first().find('.vbo-overview-roomname').text();
2978 } else {
2979 let main_room_cell = jQuery(this).parent('tr').find('td.roomname');
2980 room_id = main_room_cell.attr('data-roomid');
2981 room_name = main_room_cell.find('.vbo-overview-roomname').text();
2982 }
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'); ?>';
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
3015 // display modal with booking details
3016 let rday_bookings_modal_body = VBOCore.displayModal({
3017 suffix: 'overv-rdaybookings',
3018 extra_class: 'vbo-modal-rounded vbo-modal-tall' + (!newResBtn ? ' vbo-modal-nofooter' : ''),
3019 title: room_name + (sub_room_index ? ' #' + sub_room_index : '') + ' - ' + date_read,
3020 draggable: true,
3021 footer_right: newResBtn,
3022 dismiss_event: 'vbo-dismiss-modal-overv-rdaybookings',
3023 loading_event: 'vbo-loading-modal-overv-rdaybookings',
3024 loading_body: '<?php VikBookingIcons::e('circle-notch', 'fa-spin fa-fw'); ?>',
3025 });
3026
3027 // show loading
3028 VBOCore.emitEvent('vbo-loading-modal-overv-rdaybookings');
3029
3030 // make the request to get the bookings information
3031 VBOCore.doAjax(
3032 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=getbookingsinfo'); ?>",
3033 {
3034 status: 'any',
3035 idroom: room_id,
3036 stay_date: date_ymd,
3037 idorders: date_bids,
3038 subroom: sub_room_data,
3039 sharedcal: (jQuery(this).hasClass('busy-sharedcalendar') ? 1 : 0),
3040 tmpl: 'component'
3041 },
3042 (res) => {
3043 // stop loading
3044 VBOCore.emitEvent('vbo-loading-modal-overv-rdaybookings');
3045 try {
3046 var obj_res = typeof res === 'string' ? JSON.parse(res) : res;
3047
3048 // build the HTML response nodes
3049 var rday_bookings_wrap = jQuery('<div></div>').addClass('vbo-dashboard-guests-latest');
3050 var rday_bookings_list = jQuery('<div></div>').addClass('vbo-dashboard-guest-messages-list');
3051
3052 // flag to indicate if the button to toggle the cancelled reservations was displayed
3053 var show_canc_res_flag = false;
3054
3055 // loop through all bookings
3056 for (var b in obj_res) {
3057 if (!obj_res.hasOwnProperty(b)) {
3058 continue;
3059 }
3060 // nights and guests
3061 var nights_guests = [
3062 obj_res[b]['roomsnum'] + ' ' + Joomla.JText._((obj_res[b]['roomsnum'] > 1 ? 'VBPVIEWORDERSTHREE' : 'VBEDITORDERTHREE')),
3063 obj_res[b]['days'] + ' ' + Joomla.JText._((obj_res[b]['days'] > 1 ? 'VBDAYS' : 'VBDAY')),
3064 obj_res[b]['tot_adults'] + ' ' + Joomla.JText._((obj_res[b]['tot_adults'] > 1 ? 'VBMAILADULTS' : 'VBMAILADULT'))
3065 ];
3066 if (obj_res[b]['tot_children'] > 0) {
3067 nights_guests.push(obj_res[b]['tot_children'] + ' ' + Joomla.JText._((obj_res[b]['tot_children'] > 1 ? 'VBMAILCHILDREN' : 'VBMAILCHILD')));
3068 }
3069
3070 // OTA booking ID
3071 var ota_bid_info = '';
3072 if (obj_res[b].hasOwnProperty('idorderota') && obj_res[b].hasOwnProperty('channel') && obj_res[b]['idorderota'] && obj_res[b]['channel']) {
3073 if (obj_res[b]['idorderota'].length <= 16) {
3074 // try to display the information only for API channels
3075 ota_bid_info = '<span class="label label-info">' + obj_res[b]['idorderota'] + '</span> ';
3076 }
3077 }
3078
3079 // the separator between confirmed and cancelled reservations
3080 var res_type_separator = null;
3081
3082 // booking status and badge
3083 var badge_type = 'warning';
3084 if (obj_res[b]['status'] == 'confirmed') {
3085 badge_type = 'success';
3086 } else if (obj_res[b]['status'] == 'cancelled') {
3087 badge_type = 'danger';
3088 if (!show_canc_res_flag) {
3089 // build the separator between confirmed and cancelled reservations to toggle the latter ones
3090 res_type_separator = jQuery('<div></div>').addClass('vbo-bookings-status-separator');
3091 var btn_separator = jQuery('<button></button>').attr('type', 'button').addClass('btn btn-small btn-secondary').text(Joomla.JText._('VBO_SHOW_CANCELLATIONS'));
3092 btn_separator.on('click', function() {
3093 jQuery(this).closest('.vbo-dashboard-guest-messages-list').find('[data-type="cancelled"]').toggle();
3094 });
3095 res_type_separator.append(btn_separator);
3096 // turn flag on at the very first cancelled reservation found
3097 show_canc_res_flag = true;
3098 }
3099 }
3100
3101 // build main booking node
3102 var rday_booking = jQuery('<div></div>').addClass('vbo-dashboard-guest-activity vbo-w-guestmessages-message');
3103 if (badge_type == 'danger') {
3104 // hide cancelled booking by default and set proper attribute
3105 rday_booking.attr('data-type', 'cancelled').css('display', 'none');
3106 }
3107 rday_booking.attr('data-idorder', obj_res[b]['id']);
3108 rday_booking.on('click', function(e) {
3109 var click_target = jQuery(e.target);
3110 if (click_target.is('a') || click_target.is('select') || click_target.is('option') || click_target.is('button')) {
3111 return;
3112 }
3113 if ((click_target.is('span') && click_target.hasClass('label')) || click_target.is('img') || click_target.is('i')) {
3114 // open booking details within the apposite admin-widget
3115 e.stopPropagation();
3116 VBOCore.handleDisplayWidgetNotification({
3117 widget_id: 'booking_details',
3118 }, {
3119 booking_id: jQuery(this).attr('data-idorder'),
3120 });
3121 } else {
3122 // open booking details on a new tab
3123 vboOvervOpenBooking(jQuery(this).attr('data-idorder'));
3124 }
3125 });
3126
3127 // build booking structure
3128 var rday_booking_html = '';
3129 rday_booking_html += '<div class="vbo-dashboard-guest-activity-avatar">' + "\n";
3130 if (obj_res[b]['avatar_src']) {
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";
3132 } else if (obj_res[b]['closure']) {
3133 rday_booking_html += closure_i + "\n";
3134 } else {
3135 rday_booking_html += def_bicon + "\n";
3136 }
3137 rday_booking_html += '</div>' + "\n";
3138 rday_booking_html += '<div class="vbo-dashboard-guest-activity-content">' + "\n";
3139 rday_booking_html += ' <div class="vbo-dashboard-guest-activity-content-head">' + "\n";
3140 rday_booking_html += ' <div class="vbo-dashboard-guest-activity-content-info-details">' + "\n";
3141 rday_booking_html += ' <h4 class="vbo-w-guestmessages-message-gtitle">' + (!obj_res[b]['closure'] ? obj_res[b]['cinfo'] : obj_res[b]['closure_txt']) + '</h4>' + "\n";
3142 rday_booking_html += ' <div class="vbo-dashboard-guest-activity-content-info-icon">' + "\n";
3143 rday_booking_html += ' <span class="badge badge-info">' + obj_res[b]['id'] + '</span> ' + "\n";
3144 if (obj_res[b]['type'] && obj_res[b]['type'] == 'overbooking') {
3145 rday_booking_html += ' <span class="label label-error vbo-label-overbooking">' + Joomla.JText._('VBO_BTYPE_OVERBOOKING') + '</span>' + "\n";
3146 }
3147 rday_booking_html += ' <span class="badge badge-' + badge_type + '">' + obj_res[b]['status_lbl'] + '</span>' + "\n";
3148 rday_booking_html += ' <span class="vbo-w-guestmessages-message-staydates">' + "\n";
3149 rday_booking_html += ' <?php VikBookingIcons::e('calendar-alt'); ?>' + "\n";
3150 rday_booking_html += ' <span class="vbo-w-guestmessages-message-staydates-in">' + obj_res[b]['checkin_short'] + '</span>' + "\n";
3151 rday_booking_html += ' <span class="vbo-w-guestmessages-message-staydates-sep">-</span>' + "\n";
3152 rday_booking_html += ' <span class="vbo-w-guestmessages-message-staydates-out">' + obj_res[b]['checkout_short'] + '</span>' + "\n";
3153 rday_booking_html += ' </span>' + "\n";
3154 rday_booking_html += ' </div>' + "\n";
3155 rday_booking_html += ' </div>' + "\n";
3156 rday_booking_html += ' <div class="vbo-dashboard-guest-activity-content-info-date">' + "\n";
3157 rday_booking_html += ' <span>' + obj_res[b]['book_date'] + '</span>' + "\n";
3158 rday_booking_html += ' <span>' + obj_res[b]['book_time'] + '</span>' + "\n";
3159 if (obj_res[b].hasOwnProperty('meals_included')) {
3160 rday_booking_html += ' <div class="vbo-wider-badges-wrap">' + "\n";
3161 obj_res[b]['meals_included'].forEach((meal_enum) => {
3162 rday_booking_html += ' <div class="badge badge-info">' + meal_enum + '</div>' + "\n";
3163 });
3164 rday_booking_html += ' </div>' + "\n";
3165 }
3166 rday_booking_html += ' </div>' + "\n";
3167 rday_booking_html += ' </div>' + "\n";
3168 rday_booking_html += ' <div class="vbo-dashboard-guest-activity-content-info-msg">' + "\n";
3169 rday_booking_html += ' <div>' + ota_bid_info + '<?php VikBookingIcons::e('bed'); ?> ' + nights_guests.join(', ') + '</div>' + "\n";
3170 if (obj_res[b].hasOwnProperty('sub_units_data')) {
3171 rday_booking_html += ' <div class="vbo-rdaybooking-subunits">' + "\n";
3172 for (let sub_rname in obj_res[b]['sub_units_data']) {
3173 if (!obj_res[b]['sub_units_data'].hasOwnProperty(sub_rname)) {
3174 continue;
3175 }
3176 rday_booking_html += '<span class="label label-success">' + (obj_res[b]['roomsnum'] > 1 ? sub_rname + ' ' : '') + '#' + obj_res[b]['sub_units_data'][sub_rname] + '</span>' + "\n";
3177 }
3178 rday_booking_html += ' </div>' + "\n";
3179 } else if (obj_res[b].hasOwnProperty('missing_index') && obj_res[b]['missing_index']) {
3180 rday_booking_html += ' <div class="vbo-rdaybooking-subunits">' + "\n";
3181 rday_booking_html += ' <span class="label label-warning">' + Joomla.JText._('VBO_MISSING_SUBUNIT') + '</span>' + "\n";
3182 if (obj_res[b].hasOwnProperty('av_room_indexes')) {
3183 // build drop downs to set the sub-unit to each room with no data
3184 rday_booking_html += ' <div class="vbo-rdaybooking-subunits-list">' + "\n";
3185 for (var av_rindex in obj_res[b]['av_room_indexes']) {
3186 if (!obj_res[b]['av_room_indexes'].hasOwnProperty(av_rindex) || !obj_res[b]['av_room_indexes'][av_rindex].hasOwnProperty('name') || !obj_res[b]['av_room_indexes'][av_rindex].hasOwnProperty('list')) {
3187 continue;
3188 }
3189 var set_subunit_node = '<select onchange="vboSetSubunit(\'' + obj_res[b]['id'] + '\', \'' + obj_res[b]['av_room_indexes'][av_rindex]['rid'] + '\', \'' + av_rindex + '\', this.value);">' + "\n";
3190 set_subunit_node += '<option value="">' + obj_res[b]['av_room_indexes'][av_rindex]['name'] + '</option>' + "\n";
3191 for (var rindex_k in obj_res[b]['av_room_indexes'][av_rindex]['list']) {
3192 if (!obj_res[b]['av_room_indexes'][av_rindex]['list'].hasOwnProperty(rindex_k)) {
3193 continue;
3194 }
3195 set_subunit_node += '<option value="' + rindex_k + '">' + obj_res[b]['av_room_indexes'][av_rindex]['list'][rindex_k] + '</option>' + "\n";
3196 }
3197 set_subunit_node += '</select>' + "\n";
3198 // append drop down
3199 rday_booking_html += set_subunit_node;
3200 }
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";
3207 }
3208 rday_booking_html += ' </div>' + "\n";
3209 }
3210 rday_booking_html += ' </div>' + "\n";
3211 rday_booking_html += '</div>' + "\n";
3212
3213 // set booking HTML to node
3214 rday_booking.append(rday_booking_html);
3215
3216 if (res_type_separator !== null) {
3217 // append the separator to toggle the cancelled reservations
3218 rday_bookings_list.append(res_type_separator);
3219 }
3220
3221 // append booking node to list
3222 rday_bookings_list.append(rday_booking);
3223 }
3224
3225 // finalize response nodes
3226 rday_bookings_wrap.append(rday_bookings_list);
3227
3228 // append the response
3229 rday_bookings_modal_body.append(rday_bookings_wrap);
3230 } catch(err) {
3231 console.error('could not parse JSON response', err, res);
3232 alert('Could not parse JSON response');
3233 }
3234 },
3235 (err) => {
3236 // stop loading and display alert message
3237 VBOCore.emitEvent('vbo-loading-modal-overv-rdaybookings');
3238 console.error(err);
3239 alert(err.responseText);
3240 }
3241 );
3242 }
3243
3244 if (jQuery(this).hasClass('busy') && !jQuery(this).hasClass('vbo-partially')) {
3245 // abort when fully booked date
3246 return;
3247 }
3248
3249 if (jQuery(this).hasClass('busytmplock') || jQuery(this).hasClass('subroom-busy')) {
3250 // abort when pending record or sub-room
3251 return;
3252 }
3253
3254 // update clicked dates info
3255 var curday = jQuery(this).attr('data-day');
3256 var roomid = jQuery(this).parent('tr').find('td.roomname').attr('data-roomid');
3257 roomid = roomid && roomid.length ? roomid : '';
3258 last_room_click = roomid;
3259 if (!last_date_click) {
3260 last_date_click = curday;
3261 } else if (last_date_click && next_date_click) {
3262 last_date_click = curday;
3263 next_date_click = '';
3264 } else {
3265 var from_info = new Date(last_date_click);
3266 var to_info = new Date(curday);
3267 if (from_info.getTime() < to_info.getTime()) {
3268 next_date_click = curday;
3269 } else {
3270 last_date_click = curday;
3271 next_date_click = '';
3272 }
3273 }
3274
3275 // button shake effect
3276 jQuery('.vbo-ovrv-flt-butn').addClass('vbo-ovrv-flt-butn-shake');
3277 setTimeout(function() {
3278 jQuery('.vbo-ovrv-flt-butn').removeClass('vbo-ovrv-flt-butn-shake');
3279 }, 1000);
3280 });
3281 /* Show New Reservation Form & Modal with bookings - End */
3282
3283 /* Hover Tooltip Start */
3284 jQuery('td.busy, td.busytmplock, td.subroom-busy').not('.vbo-widget-booskcal-cell-mday').hover(function() {
3285 registerHoveringTooltip(this);
3286 }, unregisterHoveringTooltip);
3287
3288 jQuery(document.body).on('click', '.vbo-overview-tip-bctag', function() {
3289 if (!jQuery(this).parent().find(".vbo-overview-tip-bctag-subtip").length) {
3290 jQuery(".vbo-overview-tip-bctag-subtip").remove();
3291 var cur_color = jQuery(this).attr("data-ctagcolor");
3292 var cur_bid = jQuery(this).attr("data-bid");
3293 jQuery(this).after("<div class=\"vbo-overview-tip-bctag-subtip\">"+bctags_tip+"</div>");
3294 jQuery(this).parent().find(".vbo-overview-tip-bctag-subtip").find(".vbo-overview-tip-bctag-subtip-circle[data-ctagcolor='"+cur_color+"']").addClass("vbo-overview-tip-bctag-activecircle").css('border-color', cur_color);
3295 jQuery(this).parent().find(".vbo-overview-tip-bctag-subtip").find(".vbo-overview-tip-bctag-subtip-circle").attr('data-bid', cur_bid);
3296 jQuery(".vbo-overview-tip-bctag-subtip .hasTooltip").tooltip();
3297 } else {
3298 jQuery(".vbo-overview-tip-bctag-subtip").remove();
3299 }
3300 });
3301
3302 var applying_tag = false;
3303
3304 jQuery(document.body).on('click', '.vbo-overview-tip-bctag-subtip-circle', function() {
3305 if (applying_tag === true) {
3306 return false;
3307 }
3308 applying_tag = true;
3309 var clickelem = jQuery(this);
3310 var ctagkey = clickelem.attr('data-ctagkey');
3311 var bid = clickelem.attr('data-bid');
3312 // set opacity to circles as loading
3313 jQuery('.vbo-overview-tip-bctag-subtip-circle').css('opacity', '0.6');
3314
3315 // perform the request
3316 VBOCore.doAjax(
3317 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=setbookingtag'); ?>",
3318 {
3319 tmpl: "component",
3320 idorder: bid,
3321 tagkey: ctagkey
3322 },
3323 (res) => {
3324 applying_tag = false;
3325 if (res.indexOf('e4j.error') >= 0 ) {
3326 alert(res.replace("e4j.error.", ""));
3327 // restore loading opacity in circles
3328 jQuery('.vbo-overview-tip-bctag-subtip-circle').css('opacity', '1');
3329 } else {
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
3346 hideVboTooltip();
3347 }
3348 },
3349 (err) => {
3350 applying_tag = false;
3351 alert("Request Failed");
3352 // restore loading opacity in circles
3353 jQuery('.vbo-overview-tip-bctag-subtip-circle').css('opacity', '1');
3354 }
3355 );
3356 });
3357
3358 jQuery(document).keydown(function(e) {
3359 if ( e.keyCode == 27 ) {
3360 if (hovtip === true) {
3361 hideVboTooltip();
3362 }
3363 if (vbodialogorph_on === true) {
3364 hideVboDialogOverv(1);
3365 }
3366 }
3367 });
3368
3369 jQuery(document).mouseup(function(e) {
3370 if (!hovtip && !vbodialogorph_on) {
3371 return;
3372 }
3373 if (hovtip) {
3374 var vbo_overlay_cont = jQuery(".vbo-overview-tipblock");
3375 if (!vbo_overlay_cont.is(e.target) && vbo_overlay_cont.has(e.target).length === 0) {
3376 hideVboTooltip();
3377 return;
3378 }
3379 if (jQuery(".vbo-overview-tip-bctag-subtip").length) {
3380 var vbo_overlay_subtip_cont = jQuery(".vbo-overview-tip-bctag-wrap");
3381 if (!vbo_overlay_subtip_cont.is(e.target) && vbo_overlay_subtip_cont.has(e.target).length === 0) {
3382 jQuery(".vbo-overview-tip-bctag-subtip").remove();
3383 return;
3384 }
3385 }
3386 }
3387 if (vbodialogorph_on) {
3388 var vbo_overlay_cont = jQuery(".vbo-info-overlay-content");
3389 if (!vbo_overlay_cont.is(e.target) && vbo_overlay_cont.has(e.target).length === 0) {
3390 hideVboDialogOverv(1);
3391 }
3392 }
3393 });
3394 /* Hover Tooltip End */
3395
3396 /* Toggle Sub-units */
3397 jQuery('.vbo-overview-subroom-toggle').click(function() {
3398 var roomid = jQuery(this).closest('td').attr('data-roomid');
3399 if (jQuery(this).hasClass('vbo-overview-subroom-toggle-active')) {
3400 jQuery("td.roomname[data-roomid='" + roomid + "']").find('span.vbo-overview-subroom-toggle').removeClass('vbo-overview-subroom-toggle-active').find('i').removeClass('fa-chevron-up').addClass('fa-chevron-down');
3401 // do not use .hide() or "display: none" may not work due to forced "display: table-row"
3402 jQuery("td.subroomname[data-roomid='-" + roomid + "']").parent('tr').css('display', 'none');
3403 // update storage object
3404 var storage_subunits_status = VBOCore.storageGetItem('vbo_overv_subunits_status');
3405 try {
3406 storage_subunits_status = JSON.parse(storage_subunits_status);
3407 storage_subunits_status = storage_subunits_status || {};
3408 } catch(e) {
3409 storage_subunits_status = {};
3410 }
3411 storage_subunits_status[roomid] = 0;
3412 VBOCore.storageSetItem('vbo_overv_subunits_status', storage_subunits_status);
3413 } else {
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');
3415 // do not use .show() or "display: block" may be added rather than "display: table-row"
3416 jQuery("td.subroomname[data-roomid='-" + roomid + "']").parent('tr').not('.vboverviewtablerow-subunit-tmp').css('display', 'table-row');
3417 // update storage object
3418 var storage_subunits_status = VBOCore.storageGetItem('vbo_overv_subunits_status');
3419 try {
3420 storage_subunits_status = JSON.parse(storage_subunits_status);
3421 storage_subunits_status = storage_subunits_status || {};
3422 } catch(e) {
3423 storage_subunits_status = {};
3424 }
3425 storage_subunits_status[roomid] = 1;
3426 VBOCore.storageSetItem('vbo_overv_subunits_status', storage_subunits_status);
3427 }
3428 });
3429
3430 /* Display sub-units based on storage status */
3431 var storage_subunits_defstatus = VBOCore.storageGetItem('vbo_overv_subunits_status');
3432 try {
3433 storage_subunits_defstatus = JSON.parse(storage_subunits_defstatus);
3434 storage_subunits_defstatus = storage_subunits_defstatus || {};
3435 for (let rid in storage_subunits_defstatus) {
3436 if (!storage_subunits_defstatus.hasOwnProperty(rid)) {
3437 continue;
3438 }
3439 if (storage_subunits_defstatus[rid]) {
3440 // trigger click event to display the sub-units
3441 jQuery('td.roomname[data-roomid="' + rid + '"]').find('.vbo-overview-subroom-toggle').trigger('click');
3442 }
3443 }
3444 } catch(e) {
3445 // do nothing
3446 }
3447
3448 /* Default state for sticky table heads */
3449 vboToggleStickyTableHeaders(<?php echo $long_list && $cookie_sticky_heads != 'off' ? 1 : 0; ?>);
3450
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 */
3456 var hideorphans = false;
3457 var buiscuits = document.cookie;
3458 if (buiscuits.length) {
3459 var hideorphansck = "vboHideOrphans=1";
3460 if (buiscuits.indexOf(hideorphansck) >= 0) {
3461 hideorphans = true;
3462 }
3463 }
3464 if (false && !hideorphans && <?php echo $vbo_auth_pricing ? 'true' : 'false'; ?>) {
3465 // make the request
3466
3467 VBOCore.doAjax(
3468 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=orphanscount'); ?>",
3469 {
3470 from: '<?php echo date($df, $tsstart); ?>',
3471 months: <?php echo $mnum; ?>,
3472 tmpl: "component"
3473 },
3474 (res) => {
3475 var obj_res = typeof res === 'string' ? JSON.parse(res) : res;
3476 var orphans_list = '';
3477 for (var rid in obj_res) {
3478 if (!obj_res.hasOwnProperty(rid)) {
3479 continue;
3480 }
3481 orphans_list += '<div class="vbo-orphans-info-room">';
3482 orphans_list += ' <h4 class="vbo-orphans-roomname">'+obj_res[rid]['name']+'</h4>';
3483 orphans_list += ' <div class="vbo-orphans-info-dates">';
3484 for (var dind in obj_res[rid]['rdates']) {
3485 if (!obj_res[rid]['rdates'].hasOwnProperty(dind)) {
3486 continue;
3487 }
3488 orphans_list += ' <div class="vbo-orphans-info-date">'+obj_res[rid]['rdates'][dind]+'</div>';
3489 }
3490 orphans_list += ' </div>';
3491 orphans_list += ' <div class="vbo-orphans-info-btn">';
3492 orphans_list += ' <a href="<?php echo $file_base; ?>?option=com_vikbooking&task=ratesoverv&cid[]='+rid+'&startdate='+obj_res[rid]['linkd']+'" class="btn btn-primary" target="_blank"><?php echo addslashes(JText::translate('VBORPHANSCHECKBTN')); ?></a>';
3493 orphans_list += ' </div>';
3494 orphans_list += '</div>';
3495 }
3496 if (orphans_list.length) {
3497 // show the modal
3498 jQuery('.vbo-orphans-info-list').html(orphans_list);
3499 jQuery('.vbo-orphans-overlay-block').fadeIn();
3500 vbodialogorph_on = true;
3501 }
3502 },
3503 (err) => {
3504 console.error("orphanscount Request Failed");
3505 }
3506 );
3507 }
3508
3509 // fests
3510 jQuery(document.body).on("click", "th.bluedays", function() {
3511 if (jQuery(this).hasClass('skip-bluedays-click')) {
3512 return;
3513 }
3514 var ymd = jQuery(this).attr('data-ymd');
3515 var daytitle = jQuery(this).attr('data-readymd');
3516 if (jQuery(this).hasClass('vbo-overv-festcell')) {
3517 // cell has fests
3518 if (!vboFests.hasOwnProperty(ymd)) {
3519 return;
3520 }
3521 vboRenderFests(ymd, daytitle);
3522 } else {
3523 // let the admin create a new fest
3524
3525 // update ymd key for the selected date, useful for adding new fests
3526 jQuery('.vbo-overlay-fests-addnew').attr('data-ymd', ymd);
3527
3528 // unset content and display modal for just adding a new fest
3529 jQuery('.vbo-overlay-fests-list').html('');
3530 var fests_modal = VBOCore.displayModal({
3531 suffix: 'overv-mng-fests',
3532 extra_class: 'vbo-modal-rounded vbo-modal-tall',
3533 title: '<?php VikBookingIcons::e('star'); ?> <span>' + daytitle + '</span>',
3534 footer_right: '<button type="button" class="btn btn-success" onclick="vboAddFest();">' + Joomla.JText._('VBSAVE') + '</button>',
3535 dismiss_event: 'vbo-dismiss-modal-overv-mng-fests',
3536 onDismiss: () => {
3537 jQuery('.vbo-overv-mngfest-wrap').appendTo('.vbo-overv-mngfest-block');
3538 },
3539 });
3540
3541 // set modal body
3542 jQuery('.vbo-overv-mngfest-wrap').appendTo(fests_modal);
3543 }
3544 });
3545
3546 // room-day notes
3547 jQuery(document.body).on("click", ".vbo-roomdaynote-display", function() {
3548 if (!jQuery(this).closest('.vbo-roomdaynote-trigger').length) {
3549 return;
3550 }
3551 var daytitle = new Array;
3552 var roomday_info = jQuery(this).closest('.vbo-roomdaynote-trigger').attr('data-roomday').split('_');
3553 // readable day
3554 var readymd = roomday_info[0];
3555 if (jQuery('.bluedays[data-ymd="' + roomday_info[0] + '"]').length) {
3556 readymd = jQuery('.bluedays[data-ymd="' + roomday_info[0] + '"]').attr('data-readymd');
3557 }
3558 daytitle.push(readymd);
3559 // room name
3560 if (jQuery('.roomname[data-roomid="' + roomday_info[1] + '"]').length) {
3561 daytitle.push(jQuery('.roomname[data-roomid="' + roomday_info[1] + '"]').first().find('.vbo-overview-roomname').text());
3562 }
3563 // sub-unit
3564 if (parseInt(roomday_info[2]) > 0 && jQuery('.subroomname[data-roomid="-' + roomday_info[1] + '"]').length) {
3565 daytitle.push(jQuery('.subroomname[data-roomid="-' + roomday_info[1] + '"]').find('.vbo-overview-subroomname').eq((parseInt(roomday_info[2]) - 1)).text());
3566 }
3567
3568 // display modal
3569 var rdaynotes_modal = VBOCore.displayModal({
3570 suffix: 'overv-mng-rdaynotes',
3571 extra_class: 'vbo-modal-rounded vbo-modal-tall',
3572 title: '<?php VikBookingIcons::e('comment'); ?> <span>' + daytitle.join(', ') + '</span>',
3573 footer_right: '<button type="button" class="btn btn-success" onclick="vboAddRoomDayNote();">' + Joomla.JText._('VBSAVE') + '</button>',
3574 dismiss_event: 'vbo-dismiss-modal-overv-mng-rdaynotes',
3575 onDismiss: () => {
3576 jQuery('.vbo-overv-mngroomdaynotes-wrap').appendTo('.vbo-overv-mngroomdaynotes-block');
3577 },
3578 });
3579
3580 // set modal body
3581 jQuery('.vbo-overv-mngroomdaynotes-wrap').appendTo(rdaynotes_modal);
3582
3583 // populate current room day notes
3584 vboRenderRdayNotes(roomday_info[0], roomday_info[1], roomday_info[2], readymd);
3585 });
3586
3587 /**
3588 * Check if the unassigned rows, if any, should be hid because of no missing sub-units.
3589 */
3590 document
3591 .querySelectorAll('tr.vboverviewtablerow-subunit-unassigned')
3592 .forEach((unassigned_row) => {
3593 let unassigned_cells = unassigned_row.querySelectorAll('td.subroom-busy').length;
3594 if (!unassigned_cells) {
3595 // remove the unassigned row for this room-type
3596 unassigned_row.remove();
3597 } else {
3598 // add an alert icon next to the main room row
3599 try {
3600 let main_room_id = unassigned_row.getAttribute('data-subroomid').split('-')[0];
3601 let alert_elem = document.createElement('span');
3602 alert_elem.classList.add('vbo-overview-alert');
3603
3604 let alert_icon = document.createElement('span');
3605 alert_icon.classList.add('badge', 'badge-warning');
3606 alert_icon.innerHTML = '<?php VikBookingIcons::e('exclamation-triangle'); ?>';
3607 alert_icon.addEventListener('click', () => {
3608 // find the first room reservation record with missing sub-unit
3609 let reassignBookingEl = unassigned_row.querySelector('td.subroom-busy.vbo-checkinday[data-lastbid]');
3610 if (reassignBookingEl) {
3611 // scroll the table to make the room booking cell visible
3612 let mainTable = unassigned_row.closest('table');
3613 let tableWrapper = mainTable.closest('.vbo-table-responsive');
3614 let firstCellWidth = mainTable
3615 .querySelector('tr.vboverviewtablerow[data-roomid="' + main_room_id + '"]')
3616 .querySelector('td.roomname')
3617 .offsetWidth;
3618 tableWrapper.scrollTo({
3619 left: reassignBookingEl.offsetLeft - firstCellWidth - reassignBookingEl.offsetWidth,
3620 behavior: 'smooth',
3621 });
3622
3623 // find a reassignment solution for the first booking found
3624 setTimeout(() => {
3625 vboOvervResolveRoomAssignment(reassignBookingEl.getAttribute('data-lastbid'));
3626 }, 200);
3627 }
3628 });
3629
3630 alert_elem.append(alert_icon);
3631
3632 unassigned_row
3633 .closest('table')
3634 .querySelector('tr.vboverviewtablerow[data-roomid="' + main_room_id + '"]')
3635 .querySelector('td.roomname')
3636 .querySelector('.vbo-overview-roomname')
3637 .insertAdjacentElement('afterend', alert_elem);
3638 } catch(e) {
3639 console.error(e);
3640 }
3641
3642 // restore any previous temporary data for "previewing" a reassignment moveset applied
3643 let previousMovesetData = VBOCore.getAdminDock().loadTemporaryData(
3644 {
3645 id: '_tmp',
3646 persist_id: 'reassignmentmoveset',
3647 },
3648 (movesetData) => {
3649 // temporary data restored from dock
3650 let closeBtn = document.createElement('button');
3651 closeBtn.setAttribute('type', 'button');
3652 closeBtn.classList.add('btn');
3653 closeBtn.textContent = <?php echo json_encode(JText::translate('VBO_KEEP_CHANGES')); ?>;
3654 closeBtn.addEventListener('click', () => {
3655 // dismiss modal and keep changes
3656 VBOCore.emitEvent('vbo-dismiss-modal-overv-resolveroomassign-undo');
3657 });
3658
3659 let undoBtn = document.createElement('button');
3660 undoBtn.setAttribute('type', 'button');
3661 undoBtn.classList.add('btn', 'btn-danger');
3662 undoBtn.textContent = <?php echo json_encode(JText::translate('VBO_UNDO_CHANGES')); ?>;
3663 undoBtn.addEventListener('click', () => {
3664 // start loading
3665 VBOCore.emitEvent('vbo-loading-modal-overv-resolveroomassign-undo');
3666
3667 // make the request to undo the changes made by the moveset
3668 VBOCore.doAjax(
3669 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=bookings.apply_room_assignment'); ?>",
3670 {
3671 moveset: movesetData?.moveset,
3672 undo: 1,
3673 },
3674 (res) => {
3675 // dismiss modal
3676 VBOCore.emitEvent('vbo-dismiss-modal-overv-resolveroomassign-undo');
3677
3678 // reload the page
3679 location.reload();
3680 },
3681 (err) => {
3682 // display error message
3683 alert(err.responseText || err);
3684
3685 // stop loading
3686 VBOCore.emitEvent('vbo-loading-modal-overv-resolveroomassign-undo');
3687 }
3688 );
3689 });
3690
3691 // display prompt modal to apply or undo the changes just made
3692 VBOCore.displayModal({
3693 suffix: 'overv-resolveroomassign-undo',
3694 extra_class: 'vbo-modal-rounded vbo-modal-prompt',
3695 title: <?php echo json_encode(JText::translate('VBO_RESOLVE_ROOM_ASSIGNMENT')); ?>,
3696 body: <?php echo json_encode(sprintf('%s / %s?', JText::translate('VBO_KEEP_CHANGES'), JText::translate('VBO_UNDO_CHANGES'))); ?>,
3697 draggable: true,
3698 escape_dismiss: false,
3699 footer_left: undoBtn,
3700 footer_right: closeBtn,
3701 dismiss_event: 'vbo-dismiss-modal-overv-resolveroomassign-undo',
3702 loading_event: 'vbo-loading-modal-overv-resolveroomassign-undo',
3703 loading_body: '<?php VikBookingIcons::e('circle-notch', 'fa-spin fa-fw'); ?>',
3704 });
3705 },
3706 (movesetData) => {
3707 // temporary data removed from dock
3708 // do nothing as moveset was already applied
3709 }
3710 );
3711 }
3712 });
3713
3714 /**
3715 * Add body click event delegation for elements that may be later added to the DOM.
3716 */
3717 document.body.addEventListener('click', (e) => {
3718
3719 // resolve room assignment button in modal window
3720 if (e.target.matches('.vbo-reassign-room-unit-btn[data-bid]') || e.target.closest('.vbo-reassign-room-unit-btn[data-bid]')) {
3721 const btnEl = !e.target.matches('.vbo-reassign-room-unit-btn[data-bid]') ? e.target.closest('.vbo-reassign-room-unit-btn[data-bid]') : e.target;
3722 const bid = btnEl.getAttribute('data-bid');
3723
3724 // dismiss modal
3725 VBOCore.emitEvent('vbo-dismiss-modal-overv-rdaybookings');
3726
3727 // call the function that handles the room assignment for a specific booking
3728 setTimeout(() => {
3729 vboOvervResolveRoomAssignment(bid);
3730 }, 200);
3731
3732 // do not proceed
3733 return;
3734 }
3735
3736 // resolve room assignment modal window include/exclude bookings checkbox
3737 if (e.target.matches('input[type="checkbox"][name*="skip_booking_id"]')) {
3738 const spanBtn = e.target.closest('.vbo-toggle-double-status');
3739 if (spanBtn) {
3740 if (e.target.checked) {
3741 spanBtn.setAttribute('data-tooltiptext', spanBtn.getAttribute('data-text-disabled'));
3742 } else {
3743 spanBtn.setAttribute('data-tooltiptext', spanBtn.getAttribute('data-text-enabled'));
3744 }
3745 }
3746
3747 // do not proceed
3748 return;
3749 }
3750
3751 });
3752
3753 <?php
3754 /**
3755 * Handle fixed table heads when on snake-layout (scroll), which is completely
3756 * different than the table-layout sticky table headers (handled through jQuery).
3757 *
3758 * @since 1.16.9 (J) - 1.6.9 (WP)
3759 */
3760 if ($cookie_sticky_heads == 'off') {
3761 ?>
3762 // listen to the document scroll event to handle the fixed table heads
3763 document.addEventListener('scroll', vboOvervHandleVScrollTableHeads);
3764
3765 // scan all tables for listening to their horizontal scrolling event
3766 let tables = document.querySelectorAll('table.vboverviewtable.vbo-overv-sticky-table-head-off');
3767 tables.forEach((table) => {
3768 table.parentNode.addEventListener('scroll', vboOvervHandleHScrollTableHead);
3769 });
3770
3771 // launch the handling function
3772 vboOvervHandleHScrollTableHead();
3773 <?php
3774 }
3775 ?>
3776 });
3777
3778 /**
3779 * Document vertical scroll event handler for the fixed table heads.
3780 *
3781 * @param object e the document scroll event object.
3782 *
3783 * @return void
3784 *
3785 * @since 1.16.9 (J) - 1.6.9 (WP)
3786 */
3787 function vboOvervHandleVScrollTableHeads(e) {
3788 // register throttling callback
3789 VBOCore.throttleTimer(() => {
3790 // get all eligible tables
3791 let tables = document.querySelectorAll('table.vboverviewtable.vbo-overv-sticky-table-head-off');
3792 if (!tables.length) {
3793 // un-register event
3794 document.removeEventListener('scroll', vboOvervHandleVScrollTableHeads);
3795 return;
3796 }
3797
3798 // pool of fixed table heads
3799 let fixedTableHeadNodes = vboOvervBuildFixedTableHeads(tables);
3800
3801 if (fixedTableHeadNodes.length) {
3802 // append to body just the last table head node for the bottom farthest table
3803 document.body.appendChild(fixedTableHeadNodes[fixedTableHeadNodes.length - 1]);
3804 }
3805 }, 400);
3806 }
3807
3808 /**
3809 * Table-element horizontal scroll event handler for updating the fixed table heads.
3810 *
3811 * @param object e the table-element scroll event object.
3812 *
3813 * @return void
3814 *
3815 * @since 1.16.9 (J) - 1.6.9 (WP)
3816 */
3817 function vboOvervHandleHScrollTableHead(e) {
3818 // register throttling callback
3819 VBOCore.throttleTimer(() => {
3820 // horizontal scrolling happens on the parent DIV node
3821 let tables = (e?.target || document).querySelectorAll('table.vboverviewtable.vbo-overv-sticky-table-head-off');
3822 if (tables.length !== 1) {
3823 // un-register event for missing table
3824 if (e?.target) {
3825 e.target.removeEventListener('scroll', vboOvervHandleHScrollTableHead);
3826 }
3827 return;
3828 }
3829
3830 if (<?php echo $table_scroll_layout === 'inline' ? 'true' : 'false'; ?>) {
3831 // the inline-scroll layout requires to update the visible month name upon scrolling
3832 let firstVisibleCell = vboOvervFirstVisibleTableHeadNode(tables[0]);
3833 let periodEl = tables[0].querySelector('.bluedays.vbo-overview-month[data-initial-period]');
3834 if (firstVisibleCell && periodEl) {
3835 if (!firstVisibleCell.no_scroll && vboOvervMonthNames[firstVisibleCell.mon]) {
3836 // display current scroll position month name
3837 periodEl.textContent = vboOvervMonthNames[firstVisibleCell.mon] + ' ' + firstVisibleCell.year;
3838 } else {
3839 // restore the original range/month name
3840 periodEl.textContent = periodEl.getAttribute('data-initial-period');
3841 }
3842 }
3843 }
3844
3845 // pool of fixed table heads
3846 let fixedTableHeadNodes = vboOvervBuildFixedTableHeads(tables);
3847
3848 if (fixedTableHeadNodes.length) {
3849 // append to body the first and only table head node expected
3850 document.body.appendChild(fixedTableHeadNodes[0]);
3851 }
3852 }, 200);
3853 }
3854
3855 /**
3856 * Builds the list of element nodes for the fixed table heads.
3857 *
3858 * @param object tables The table elements to parse.
3859 *
3860 * @return object[]
3861 *
3862 * @since 1.16.9 (J) - 1.6.9 (WP)
3863 */
3864 function vboOvervBuildFixedTableHeads(tables) {
3865 // pool of fixed table heads
3866 let fixedTableHeadNodes = [];
3867
3868 // get document body scroll top value
3869 let scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
3870
3871 // platform fixed offset top
3872 let platformAdminBar = <?php echo VBOPlatformDetection::isWordPress() ? 'document.querySelector("#wpadminbar")' : '(document.querySelector(".navbar") || document.querySelector("#subhead-container"))'; ?>;
3873 let fixedOffsetTop = platformAdminBar ? platformAdminBar.offsetHeight : 0;
3874 if (fixedOffsetTop) {
3875 // ensure the responsive mode has got a fixed/absolute positioning
3876 let adminBarPosition = window.getComputedStyle(platformAdminBar).getPropertyValue('position') || platformAdminBar.style.position || '';
3877 if (adminBarPosition != 'fixed' && adminBarPosition != 'absolute' && adminBarPosition != 'sticky') {
3878 // no fixed admin bar
3879 fixedOffsetTop = 0;
3880 }
3881 }
3882
3883 // scan all table elements
3884 (tables || []).forEach((table, index) => {
3885 // always check for existing fixed table head element
3886 let scrollingHeadElement = document.querySelector('.vbo-overv-sticky-scroll-month[data-table-index="' + index + '"]');
3887 if (scrollingHeadElement) {
3888 // remove fixed table head element from DOM, will be reconstructed if needed
3889 scrollingHeadElement.remove();
3890 }
3891
3892 // get table head element node
3893 let tableHeadNode = table.querySelector('tr.vboverviewtablerowone');
3894
3895 // get position of current table head
3896 let tableHeadHeight = tableHeadNode.offsetHeight;
3897
3898 // make sure the table head is NOT visible, hence requires a fixed head
3899 if (scrollTop > (table.offsetTop + tableHeadHeight - (fixedOffsetTop / 2))) {
3900 if (scrollTop > (table.offsetTop + table.offsetHeight)) {
3901 // scrolling went even below the table
3902 return;
3903 }
3904
3905 // build main element node
3906 let fixedTableHead = document.createElement('div');
3907 fixedTableHead.className = 'vbo-overv-sticky-scroll-month';
3908 fixedTableHead.style.top = fixedOffsetTop + 'px';
3909 fixedTableHead.style.left = table.parentNode.getBoundingClientRect().x + 'px';
3910 fixedTableHead.setAttribute('data-table-index', index);
3911
3912 // inner element node
3913 let fixedTableHeadWrap = document.createElement('div');
3914 fixedTableHeadWrap.className = 'vbo-overv-sticky-scroll-month-wrap';
3915
3916 // month-name element node
3917 let tableHeadMonth = tableHeadNode.querySelector('.vbo-overview-month');
3918 let fixedTableHeadMonth = document.createElement('div');
3919 fixedTableHeadMonth.className = 'vbo-overv-sticky-scroll-month-m';
3920 fixedTableHeadMonth.style.width = tableHeadMonth.offsetWidth + 'px';
3921 fixedTableHeadMonth.appendChild(
3922 document.createTextNode(tableHeadMonth.innerText)
3923 );
3924 fixedTableHeadWrap.appendChild(fixedTableHeadMonth);
3925
3926 // obtain the list of visible month-day cells
3927 vboOvervVisibleTableHeadNodes(table, index).forEach((mday, mdayIndex) => {
3928 // month-day cell element node
3929 let fixedTableHeadMday = document.createElement('div');
3930 fixedTableHeadMday.className = 'vbo-overv-sticky-scroll-month-d';
3931 if (mdayIndex && mday?.mday == 1) {
3932 fixedTableHeadMday.classList.add('vbo-overv-sticky-scroll-newmonth-d');
3933 }
3934 fixedTableHeadMday.style.width = mday['vwidth'] + 'px';
3935 fixedTableHeadMday.addEventListener('click', () => {
3936 try {
3937 // use jQuery to trigger the click event, or plain JS won't work
3938 /*
3939 document
3940 .querySelector('table.vboverviewtable.vbo-overv-sticky-table-head-off[data-table-index="' + index + '"]')
3941 .querySelector('.bluedays[data-ymd="' + mday['ymd'] + '"]')
3942 .dispatchEvent(new Event('click'));
3943 */
3944 jQuery('table.vboverviewtable.vbo-overv-sticky-table-head-off[data-table-index="' + index + '"]')
3945 .find('.bluedays[data-ymd="' + mday['ymd'] + '"]')
3946 .trigger('click');
3947 } catch(e) {
3948 // do nothing
3949 }
3950 });
3951
3952 // week-day element
3953 let wdayEl = document.createElement('span');
3954 wdayEl.className = 'vbo-overv-sticky-scroll-month-day';
3955 wdayEl.appendChild(
3956 document.createTextNode(mday['wday'])
3957 );
3958 fixedTableHeadMday.appendChild(wdayEl);
3959
3960 // month-day element
3961 let mdayEl = document.createElement('span');
3962 mdayEl.className = 'vbo-overv-sticky-scroll-month-day-num';
3963 mdayEl.appendChild(
3964 document.createTextNode(mday['mday'])
3965 );
3966 fixedTableHeadMday.appendChild(mdayEl);
3967
3968 // append month-day cell element node
3969 fixedTableHeadWrap.appendChild(fixedTableHeadMday);
3970 });
3971
3972 // append wrapper
3973 fixedTableHead.appendChild(fixedTableHeadWrap);
3974
3975 // push the final element node to the pool
3976 fixedTableHeadNodes.push(fixedTableHead);
3977 }
3978 });
3979
3980 return fixedTableHeadNodes;
3981 }
3982
3983 /**
3984 * Builds a list of visible month-day cells in a table head.
3985 *
3986 * @param object table the NodeList element representing a table.
3987 * @param number index the NodeList element index we are parsing.
3988 *
3989 * @return object[]
3990 *
3991 * @since 1.16.9 (J) - 1.6.9 (WP)
3992 */
3993 function vboOvervVisibleTableHeadNodes(table, index) {
3994 // get the main table head element
3995 let tableHeadElement = table.querySelector('tr.vboverviewtablerowone');
3996
3997 // get month element of the given table
3998 let monthElement = tableHeadElement.querySelector('.vbo-overview-month');
3999 let monthName = monthElement.innerText;
4000 let monthLeft = monthElement.offsetLeft - 2;
4001 let monthWidth = monthElement.offsetWidth;
4002
4003 // get horizontal scroll left and table width from parent node
4004 let tableScrollLeft = table.parentNode.scrollLeft;
4005 let tableViewWidth = table.parentNode.offsetWidth;
4006
4007 // pool of visible month-day cells
4008 let visibleMonthDays = [];
4009
4010 // scan all visible month-day elements
4011 tableHeadElement.querySelectorAll('.bluedays:not(.vbo-overview-month)').forEach((cell) => {
4012 let monthDayLeft = cell.offsetLeft;
4013 let monthDayWidth = cell.offsetWidth;
4014
4015 // ensure cell is visible from left scroll
4016 if ((monthDayLeft + monthDayWidth) >= (monthLeft + monthWidth)) {
4017 // ensure cell is also visible from right scroll
4018 if ((monthLeft + tableViewWidth) > monthDayLeft) {
4019 // calculate cell exact visible width
4020 let cellViewWidth = monthDayWidth;
4021 if ((monthDayLeft + monthDayWidth) > (monthLeft + tableViewWidth)) {
4022 // cell is cut from right scroll, count visible width
4023 cellViewWidth = monthDayWidth - ((monthDayLeft + monthDayWidth) - (tableViewWidth + monthLeft));
4024 } else if ((monthWidth + tableScrollLeft) > monthDayLeft) {
4025 // cell is cut from left scroll, count visible width
4026 cellViewWidth = monthDayWidth - ((monthWidth + tableScrollLeft) - monthDayLeft);
4027 }
4028 cellViewWidth = cellViewWidth > monthDayWidth || cellViewWidth < 1 ? 1 : cellViewWidth;
4029
4030 // push cell details
4031 visibleMonthDays.push({
4032 wday: cell.querySelector('.vbo-overw-tablewday').innerText,
4033 mday: cell.querySelector('.vbo-overw-tablemday').innerText,
4034 ymd: cell.getAttribute('data-ymd'),
4035 vwidth: cellViewWidth,
4036 });
4037 }
4038 }
4039 });
4040
4041 return visibleMonthDays;
4042 }
4043
4044 /**
4045 * Returns the information of the first visible month-day cell in a table head upon scrolling.
4046 *
4047 * @param object table the NodeList element representing a table.
4048 *
4049 * @return object|null
4050 *
4051 * @since 1.18.6 (J) - 1.8.6 (WP)
4052 */
4053 function vboOvervFirstVisibleTableHeadNode(table) {
4054 // get the main table head element
4055 let tableHeadElement = table.querySelector('tr.vboverviewtablerowone');
4056
4057 // get month element of the given table
4058 let monthElement = tableHeadElement.querySelector('.vbo-overview-month');
4059 let monthLeft = monthElement.offsetLeft - 2;
4060 let monthWidth = monthElement.offsetWidth;
4061
4062 // get horizontal scroll left and table width from parent node
4063 let tableScrollLeft = table.parentNode.scrollLeft;
4064 let tableViewWidth = table.parentNode.offsetWidth;
4065
4066 // attempt to identify the first visible month-day cell
4067 let firstVisibleMonthDay = null;
4068
4069 // scan all visible month-day elements
4070 tableHeadElement.querySelectorAll('.bluedays:not(.vbo-overview-month)').forEach((cell) => {
4071 if (firstVisibleMonthDay) {
4072 return;
4073 }
4074
4075 // get cell offsets
4076 let monthDayLeft = cell.offsetLeft;
4077 let monthDayWidth = cell.offsetWidth;
4078
4079 // ensure cell is visible from left scroll
4080 if ((monthDayLeft + monthDayWidth) >= (monthLeft + monthWidth)) {
4081 // ensure cell is also visible from right scroll
4082 if ((monthLeft + tableViewWidth) > monthDayLeft) {
4083 // visible cell found, gather the information
4084 let ymd = cell.getAttribute('data-ymd');
4085 let matchData = ymd.match(/^([0-9]{4})-(0?[1-9]|1[0-2])-[0-9]{2}$/);
4086 if (!matchData) {
4087 // unexpected cell information
4088 return;
4089 }
4090
4091 // set first visible cell properties
4092 firstVisibleMonthDay = {
4093 ymd: ymd,
4094 year: matchData[1],
4095 mon: Number(matchData[2]),
4096 no_scroll: !tableScrollLeft ? 1 : 0,
4097 };
4098 }
4099 }
4100 });
4101
4102 return firstVisibleMonthDay;
4103 }
4104
4105 /**
4106 * Fests
4107 */
4108 function vboRenderFests(day, daytitle) {
4109 // compose fests information
4110 var fests_html = '';
4111 if (vboFests[day] && vboFests[day]['festinfo'] && vboFests[day]['festinfo'].length) {
4112 for (var i = 0; i < vboFests[day]['festinfo'].length; i++) {
4113 var fest = vboFests[day]['festinfo'][i];
4114 fests_html += '<div class="vbo-overlay-fest-details">';
4115 fests_html += ' <div class="vbo-fest-info">';
4116 fests_html += ' <div class="vbo-fest-name">' + fest['trans_name'] + '</div>';
4117 fests_html += ' <div class="vbo-fest-desc">' + fest['descr'].replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1' + '<br />' + '$2') + '</div>';
4118 fests_html += ' </div>';
4119 fests_html += ' <div class="vbo-fest-cmds">';
4120 fests_html += ' <button type="button" class="btn btn-danger" onclick="vboRemoveFest(\'' + day + '\', \'' + i + '\', \'' + fest['type'] + '\', this);"><?php VikBookingIcons::e('trash-alt', 'no-margin'); ?></button>';
4121 fests_html += ' </div>';
4122 fests_html += '</div>';
4123 }
4124 }
4125
4126 // update ymd key for the selected date, useful for adding new fests
4127 jQuery('.vbo-overlay-fests-addnew').attr('data-ymd', day);
4128
4129 // set content and display modal
4130 jQuery('.vbo-overlay-fests-list').html(fests_html);
4131
4132 if (typeof daytitle !== 'undefined') {
4133 // display modal only when we are not deleting a fest
4134 var fests_modal = VBOCore.displayModal({
4135 suffix: 'overv-mng-fests',
4136 extra_class: 'vbo-modal-rounded vbo-modal-tall',
4137 title: '<?php VikBookingIcons::e('star'); ?> <span>' + daytitle + '</span>',
4138 footer_right: '<button type="button" class="btn btn-success" onclick="vboAddFest();">' + Joomla.JText._('VBSAVE') + '</button>',
4139 dismiss_event: 'vbo-dismiss-modal-overv-mng-fests',
4140 onDismiss: () => {
4141 jQuery('.vbo-overv-mngfest-wrap').appendTo('.vbo-overv-mngfest-block');
4142 },
4143 });
4144
4145 // set modal body
4146 jQuery('.vbo-overv-mngfest-wrap').appendTo(fests_modal);
4147 }
4148 }
4149
4150 function vboRemoveFest(day, index, fest_type, that) {
4151 if (!confirm('<?php echo addslashes(JText::translate('VBDELCONFIRM')); ?>')) {
4152 return false;
4153 }
4154 var elem = jQuery(that);
4155 // make the AJAX request to the controller to remove this fest from the DB
4156 VBOCore.doAjax(
4157 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=remove_fest'); ?>",
4158 {
4159 tmpl: "component",
4160 dt: day,
4161 ind: index,
4162 type: fest_type
4163 },
4164 (res) => {
4165 if (res.indexOf('e4j.ok') >= 0) {
4166 // delete fest also from the json-decode array of objects
4167 if (vboFests[day] && vboFests[day]['festinfo']) {
4168 // use splice to remove the desired index from array, or delete would not make the length of the array change
4169 vboFests[day]['festinfo'].splice(index, 1);
4170 // re-build indexes of delete buttons, fundamental for removing the right index at next click
4171 vboRenderFests(day);
4172 if (!vboFests[day]['festinfo'].length) {
4173 // delete also this date object from fests
4174 delete vboFests[day];
4175 // no more fests, remove the class for this date from all cells
4176 jQuery('th.bluedays[data-ymd="'+day+'"]').removeClass('vbo-overv-festcell');
4177 jQuery('td.notbusy[data-day="'+day+'"]').removeClass('vbo-overv-festcell');
4178 jQuery('td.subnotbusy[data-day="'+day+'"]').removeClass('vbo-overv-festcell');
4179 jQuery('td.busy[data-day="'+day+'"]').removeClass('vbo-overv-festcell');
4180 jQuery('td.subroom-busy[data-day="'+day+'"]').removeClass('vbo-overv-festcell');
4181 }
4182 }
4183 elem.closest('.vbo-overlay-fest-details').remove();
4184 } else {
4185 alert('Invalid response');
4186 }
4187 },
4188 (err) => {
4189 alert('Request failed');
4190 }
4191 );
4192 }
4193
4194 function vboAddFest() {
4195 var ymd = jQuery('.vbo-overlay-fests-addnew').attr('data-ymd');
4196 var fest_name = jQuery('#vbo-newfest-name').val();
4197 var fest_descr = jQuery('#vbo-newfest-descr').val();
4198 if (!fest_name || !fest_name.length) {
4199 return false;
4200 }
4201 // make the AJAX request to the controller to add this fest to the DB
4202 VBOCore.doAjax(
4203 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=add_fest'); ?>",
4204 {
4205 tmpl: "component",
4206 dt: ymd,
4207 type: "custom",
4208 name: fest_name,
4209 descr: fest_descr
4210 },
4211 (res) => {
4212 // parse the JSON response that contains the fest object for the passed date
4213 try {
4214 var stored_fest = typeof res === 'string' ? JSON.parse(res) : res;
4215 if (!vboFests.hasOwnProperty(stored_fest['dt'])) {
4216 // we need to add the proper class to all cells to show that there is a fest
4217 jQuery('th.bluedays[data-ymd="'+stored_fest['dt']+'"]').addClass('vbo-overv-festcell');
4218 jQuery('td.notbusy[data-day="'+stored_fest['dt']+'"]').addClass('vbo-overv-festcell');
4219 jQuery('td.subnotbusy[data-day="'+stored_fest['dt']+'"]').addClass('vbo-overv-festcell');
4220 jQuery('td.busy[data-day="'+stored_fest['dt']+'"]').addClass('vbo-overv-festcell');
4221 jQuery('td.subroom-busy[data-day="'+stored_fest['dt']+'"]').addClass('vbo-overv-festcell');
4222 }
4223 vboFests[stored_fest['dt']] = stored_fest;
4224 // hide modal
4225 VBOCore.emitEvent('vbo-dismiss-modal-overv-mng-fests');
4226 // reset input fields
4227 jQuery('#vbo-newfest-name').val('');
4228 jQuery('#vbo-newfest-descr').val('');
4229 } catch (e) {
4230 alert('Invalid response');
4231 return false;
4232 }
4233 },
4234 (err) => {
4235 alert('Request failed');
4236 }
4237 );
4238 }
4239
4240 /**
4241 * Room-day notes
4242 */
4243 var rdaynote_icn_full = '<?php echo VikBookingIcons::i('sticky-note', 'vbo-roomdaynote-display'); ?>';
4244 var rdaynote_icn_empty = '<?php echo VikBookingIcons::i('far fa-sticky-note', 'vbo-roomdaynote-display'); ?>';
4245 function vboRenderRdayNotes(day, idroom, subunit, readymd) {
4246 // compose fests information
4247 var notes_html = '';
4248 var keyid = day + '_' + idroom + '_' + subunit;
4249 if (vboRdayNotes.hasOwnProperty(keyid) && vboRdayNotes[keyid]['info'] && vboRdayNotes[keyid]['info'].length) {
4250 for (var i = 0; i < vboRdayNotes[keyid]['info'].length; i++) {
4251 var note_data = vboRdayNotes[keyid]['info'][i];
4252 notes_html += '<div class="vbo-overlay-fest-details vbo-modal-roomdaynotes-note-details">';
4253 notes_html += ' <div class="vbo-fest-info vbo-modal-roomdaynotes-note-info">';
4254 notes_html += ' <div class="vbo-fest-name vbo-modal-roomdaynotes-note-name">' + note_data['name'] + '</div>';
4255 notes_html += ' <div class="vbo-fest-desc vbo-modal-roomdaynotes-note-desc">' + note_data['descr'].replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1' + '<br />' + '$2') + '</div>';
4256 notes_html += ' </div>';
4257 notes_html += ' <div class="vbo-fest-cmds vbo-modal-roomdaynotes-note-cmds">';
4258 notes_html += ' <button type="button" class="btn btn-danger" onclick="vboRemoveRdayNote(\'' + i + '\', \'' + day + '\', \'' + idroom + '\', \'' + subunit + '\', \'' + note_data['type'] + '\', this);"><?php VikBookingIcons::e('trash-alt', 'no-margin'); ?></button>';
4259 notes_html += ' </div>';
4260 notes_html += '</div>';
4261 }
4262 }
4263 // update attributes keys for the selected date, useful for adding new notes
4264 jQuery('.vbo-modal-roomdaynotes-addnew').attr('data-ymd', day).attr('data-roomid', idroom).attr('data-subroomid', subunit);
4265 if (readymd !== null) {
4266 jQuery('.vbo-modal-roomdaynotes-addnew').attr('data-readymd', readymd);
4267 jQuery('.vbo-newrdnote-dayto-val').text(readymd);
4268 }
4269 // set content and display modal
4270 jQuery('.vbo-modal-roomdaynotes-list').html(notes_html);
4271 }
4272
4273 function vboAddRoomDayNote(that) {
4274 var mainelem = jQuery('.vbo-modal-roomdaynotes-addnew');
4275 var ymd = mainelem.attr('data-ymd');
4276 var roomid = mainelem.attr('data-roomid');
4277 var subroomid = mainelem.attr('data-subroomid');
4278 var note_name = jQuery('#vbo-newrdnote-name').val();
4279 var note_descr = jQuery('#vbo-newrdnote-descr').val();
4280 var note_cdays = jQuery('#vbo-newrdnote-cdays').val();
4281 if (!note_name.length && !note_descr.length) {
4282 alert('Missing required fields');
4283 return false;
4284 }
4285 // make the AJAX request to the controller to add this note to the DB
4286 VBOCore.doAjax(
4287 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=add_roomdaynote'); ?>",
4288 {
4289 tmpl: "component",
4290 dt: ymd,
4291 idroom: roomid,
4292 subunit: subroomid,
4293 type: "custom",
4294 name: note_name,
4295 descr: note_descr,
4296 cdays: note_cdays
4297 },
4298 (res) => {
4299 // parse the JSON response that contains the note object for the passed date
4300 try {
4301 var stored_notes = typeof res === 'string' ? JSON.parse(res) : res;
4302 for (var keyid in stored_notes) {
4303 if (!stored_notes.hasOwnProperty(keyid)) {
4304 continue;
4305 }
4306 if (!vboRdayNotes.hasOwnProperty(keyid) && jQuery('.vbo-roomdaynote-trigger[data-roomday="' + keyid + '"]').length) {
4307 // we need to add the proper class to the cell for this note (if it's visible)
4308 jQuery('.vbo-roomdaynote-trigger[data-roomday="' + keyid + '"]').parent('td').removeClass('vbo-roomdaynote-empty').addClass('vbo-roomdaynote-full').find('i').attr('class', rdaynote_icn_full);
4309 }
4310 // update global object with the new notes in any case
4311 vboRdayNotes[keyid] = stored_notes[keyid];
4312 }
4313 // hide modal
4314 VBOCore.emitEvent('vbo-dismiss-modal-overv-mng-rdaynotes');
4315 // reset input fields
4316 jQuery('#vbo-newrdnote-name').val('');
4317 jQuery('#vbo-newrdnote-descr').val('');
4318 jQuery('#vbo-newrdnote-cdays').val('0').trigger('change');
4319 } catch (e) {
4320 alert('Invalid response');
4321 return false;
4322 }
4323 },
4324 (err) => {
4325 alert('Request failed');
4326 }
4327 );
4328 }
4329
4330 function vboRemoveRdayNote(index, day, idroom, subunit, note_type, that) {
4331 if (!confirm('<?php echo addslashes(JText::translate('VBDELCONFIRM')); ?>')) {
4332 return false;
4333 }
4334 var elem = jQuery(that);
4335 // make the AJAX request to the controller to remove this note from the DB
4336 VBOCore.doAjax(
4337 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=remove_roomdaynote'); ?>",
4338 {
4339 tmpl: "component",
4340 dt: day,
4341 idroom: idroom,
4342 subunit: subunit,
4343 ind: index,
4344 type: note_type
4345 },
4346 (res) => {
4347 if (res.indexOf('e4j.ok') >= 0) {
4348 var keyid = day + '_' + idroom + '_' + subunit;
4349 // delete note also from the json-decode array of objects
4350 if (vboRdayNotes[keyid] && vboRdayNotes[keyid]['info']) {
4351 // use splice to remove the desired index from array, or delete would not make the length of the array change
4352 vboRdayNotes[keyid]['info'].splice(index, 1);
4353 // re-build indexes of delete buttons, fundamental for removing the right index at next click
4354 vboRenderRdayNotes(day, idroom, subunit, null);
4355 if (!vboRdayNotes[keyid]['info'].length) {
4356 // delete also this date object from notes
4357 delete vboRdayNotes[keyid];
4358 // no more notes, update the proper class attribute for this cell (should be visible)
4359 if (jQuery('.vbo-roomdaynote-trigger[data-roomday="' + keyid + '"]').length) {
4360 jQuery('.vbo-roomdaynote-trigger[data-roomday="' + keyid + '"]').parent('td').removeClass('vbo-roomdaynote-full').addClass('vbo-roomdaynote-empty').find('i').attr('class', rdaynote_icn_empty);
4361 }
4362 }
4363 }
4364 elem.closest('.vbo-modal-roomdaynotes-note-details').remove();
4365 } else {
4366 alert('Invalid response');
4367 }
4368 },
4369 (err) => {
4370 alert('Request failed');
4371 }
4372 );
4373 }
4374
4375 function vboRdayNoteCdaysCount() {
4376 var cdays = parseInt(jQuery('#vbo-newrdnote-cdays').val());
4377 var defymd = jQuery('.vbo-modal-roomdaynotes-addnew').attr('data-ymd');
4378 var defreadymd = jQuery('.vbo-modal-roomdaynotes-addnew').attr('data-readymd');
4379 defreadymd = !defreadymd || !defreadymd.length ? defymd : defreadymd;
4380 if (isNaN(cdays) || cdays < 1) {
4381 jQuery('.vbo-newrdnote-dayto-val').text(defreadymd);
4382 return;
4383 }
4384 // calculate target (until) date
4385 var targetdate = new Date(defymd);
4386 targetdate.setDate(targetdate.getDate() + cdays);
4387 var target_y = targetdate.getFullYear();
4388 var target_m = targetdate.getMonth() + 1;
4389 target_m = target_m < 10 ? '0' + target_m : target_m;
4390 var target_d = targetdate.getDate();
4391 target_d = target_d < 10 ? '0' + target_d : target_d;
4392 // display target date
4393 var display_target = target_y + '-' + target_m + '-' + target_d;
4394 // check if we can get the "read ymd property"
4395 if (jQuery('.bluedays[data-ymd="' + display_target + '"]').length) {
4396 display_target = jQuery('.bluedays[data-ymd="' + display_target + '"]').attr('data-readymd');
4397 }
4398 jQuery('.vbo-newrdnote-dayto-val').text(display_target);
4399 }
4400 </script>
4401
4402 <div class="vbo-orphans-overlay-block">
4403 <a class="vbo-info-overlay-close" href="javascript: void(0);"></a>
4404 <div class="vbo-info-overlay-content vbo-info-overlay-content-orphans">
4405 <h3><?php echo $vbo_app->createPopover(array('title' => JText::translate('VBORPHANSFOUND'), 'content' => JText::translate('VBORPHANSFOUNDSHELP'), 'icon_class' => VikBookingIcons::i('exclamation-triangle'))); ?> <?php echo JText::translate('VBORPHANSFOUND'); ?></h3>
4406 <div class="vbo-info-overlay-scroll-content">
4407 <div class="vbo-orphans-info-list"></div>
4408 </div>
4409 <div class="vbo-orphans-info-cmds">
4410 <div class="vbo-orphans-info-cmd">
4411 <button type="button" class="btn btn-success" onclick="javascript: hideVboDialogOverv(1);"><?php echo JText::translate('VBOBTNKEEPREMIND'); ?></button>
4412 </div>
4413 <div class="vbo-orphans-info-cmd">
4414 <button type="button" class="btn btn-danger" onclick="javascript: hideVboDialogOverv(-1);"><?php echo JText::translate('VBOBTNDONTREMIND'); ?></button>
4415 </div>
4416 </div>
4417 </div>
4418 </div>
4419
4420 <div class="vbo-overv-mngfest-block" style="display: none;">
4421 <div class="vbo-overv-mngfest-wrap">
4422 <div class="vbo-overlay-fests-list"></div>
4423 <div class="vbo-overlay-fests-addnew" data-ymd="">
4424 <h4><?php echo JText::translate('VBOADDCUSTOMFESTTODAY'); ?></h4>
4425 <div class="vbo-overlay-fests-addnew-elem">
4426 <label for="vbo-newfest-name"><?php echo JText::translate('VBPVIEWPLACESONE'); ?></label>
4427 <input type="text" id="vbo-newfest-name" value="" />
4428 </div>
4429 <div class="vbo-overlay-fests-addnew-elem">
4430 <label for="vbo-newfest-descr"><?php echo JText::translate('VBPLACEDESCR'); ?></label>
4431 <textarea id="vbo-newfest-descr"></textarea>
4432 </div>
4433 </div>
4434 </div>
4435 </div>
4436
4437 <div class="vbo-overv-mngroomdaynotes-block" style="display: none;">
4438 <div class="vbo-overv-mngroomdaynotes-wrap">
4439 <div class="vbo-modal-roomdaynotes-list"></div>
4440 <div class="vbo-modal-roomdaynotes-addnew" data-readymd="" data-ymd="" data-roomid="" data-subroomid="">
4441 <h4><?php echo JText::translate('VBOADDCUSTOMFESTTODAY'); ?></h4>
4442 <div class="vbo-modal-roomdaynotes-addnew-elem">
4443 <label for="vbo-newrdnote-name"><?php echo JText::translate('VBPVIEWPLACESONE'); ?></label>
4444 <input type="text" id="vbo-newrdnote-name" value="" />
4445 </div>
4446 <div class="vbo-modal-roomdaynotes-addnew-elem">
4447 <label for="vbo-newrdnote-descr"><?php echo JText::translate('VBPLACEDESCR'); ?></label>
4448 <textarea id="vbo-newrdnote-descr"></textarea>
4449 </div>
4450 <div class="vbo-modal-roomdaynotes-addnew-elem">
4451 <label for="vbo-newrdnote-cdays"><?php echo JText::translate('VBOCONSECUTIVEDAYS'); ?></label>
4452 <input type="number" id="vbo-newrdnote-cdays" min="0" max="365" value="0" onchange="vboRdayNoteCdaysCount();" onkeyup="vboRdayNoteCdaysCount();" />
4453 <span class="vbo-newrdnote-dayto">
4454 <span class="vbo-newrdnote-dayto-lbl"><?php echo JText::translate('VBOUNTIL'); ?></span>
4455 <span class="vbo-newrdnote-dayto-val"></span>
4456 </span>
4457 </div>
4458 </div>
4459 </div>
4460 </div>
4461