PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.6.8
VikBooking Hotel Booking Engine & PMS v1.6.8
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 / helpers / widgets / bookings_calendar.php

bookings_calendar.php in VikBooking Hotel Booking Engine & PMS 1.6.8, at admin/helpers/widgets/bookings_calendar.php

2,713 lines 98.4 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 - Extensionsforjoomla.com
6 * @copyright Copyright (C) 2018 e4j - Extensionsforjoomla.com. 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 /**
14 * Class handler for admin widget "bookings calendar".
15 *
16 * @since 1.15.0 (J) - 1.5.0 (WP)
17 */
18 class VikBookingAdminWidgetBookingsCalendar extends VikBookingAdminWidget
19 {
20 /**
21 * The instance counter of this widget. Since we do not load individual parameters
22 * for each widget's instance, we use a static counter to determine its settings.
23 *
24 * @var int
25 */
26 protected static $instance_counter = -1;
27
28 /**
29 * Default number of bookings per page.
30 *
31 * @var int
32 */
33 protected $bookings_per_page = 6;
34
35 /**
36 * Class constructor will define the widget name and identifier.
37 */
38 public function __construct()
39 {
40 // call parent constructor
41 parent::__construct();
42
43 $this->widgetName = JText::translate('VBO_W_BOOKSCAL_TITLE');
44 $this->widgetDescr = JText::translate('VBO_W_BOOKSCAL_DESCR');
45 $this->widgetId = basename(__FILE__, '.php');
46
47 // define widget and icon and style name
48 $this->widgetIcon = '<i class="' . VikBookingIcons::i('calendar') . '"></i>';
49 $this->widgetStyleName = 'brown';
50 }
51
52 /**
53 * Custom method for this widget only to load the bookings calendar.
54 * The method is called by the admin controller through an AJAX request.
55 * The visibility should be public, it should not exit the process, and
56 * any content sent to output will be returned to the AJAX response.
57 * In this case we return an array because this method requires "return":1.
58 */
59 public function loadBookingsCalendar()
60 {
61 // get today's date
62 $today_ymd = date('Y-m-d');
63
64 $wrapper = VikRequest::getString('wrapper', '', 'request');
65 $offset = VikRequest::getString('offset', $today_ymd, 'request');
66 $room_id = VikRequest::getInt('room_id', 0, 'request');
67 $date_dir = VikRequest::getInt('date_dir', 0, 'request');
68 $bid = VikRequest::getInt('bid', 0, 'request');
69 $bid = !empty($date_dir) ? 0 : $bid;
70
71 // the booking ID can be passed in case of multitask data for this page
72 if (!empty($bid)) {
73 // load the availability of the month when booking starts
74 $booking_info = VikBooking::getBookingInfoFromID($bid);
75 if ($booking_info) {
76 // force the offset to use as start date
77 $offset = date('Y-m-d', $booking_info['checkin']);
78 }
79 }
80
81 // calculate date timestamps interval
82 $now_info = getdate(strtotime($offset));
83 if ($date_dir > 0) {
84 // next month from current offset
85 $from_ts = mktime(0, 0, 0, ($now_info['mon'] + 1), 1, $now_info['year']);
86 $to_ts = mktime(23, 59, 59, ($now_info['mon'] + 1), date('t', $from_ts), $now_info['year']);
87 } elseif ($date_dir < 0) {
88 // prev month from current offset
89 $from_ts = mktime(0, 0, 0, ($now_info['mon'] - 1), 1, $now_info['year']);
90 $to_ts = mktime(23, 59, 59, ($now_info['mon'] - 1), date('t', $from_ts), $now_info['year']);
91 } else {
92 // no navigation, use current offset
93 $from_ts = mktime(0, 0, 0, $now_info['mon'], 1, $now_info['year']);
94 $to_ts = mktime(23, 59, 59, $now_info['mon'], date('t', $now_info[0]), $now_info['year']);
95 }
96
97 // build week days list according to settings
98 $firstwday = (int)VikBooking::getFirstWeekDay();
99 $days_labels = array(
100 JText::translate('VBSUN'),
101 JText::translate('VBMON'),
102 JText::translate('VBTUE'),
103 JText::translate('VBWED'),
104 JText::translate('VBTHU'),
105 JText::translate('VBFRI'),
106 JText::translate('VBSAT'),
107 );
108 $days_indexes = [];
109 for ($i = 0; $i < 7; $i++) {
110 $days_indexes[$i] = (6 - ($firstwday - $i) + 1) % 7;
111 }
112
113 // currency symbol
114 $currencysymb = VikBooking::getCurrencySymb();
115
116 // start looping from the first day of the current month
117 $info_arr = getdate($from_ts);
118
119 // build period name
120 $period_date = VikBooking::sayMonth($info_arr['mon']) . ' ' . $info_arr['year'];
121
122 // invoke availability helper class
123 $av_helper = VikBooking::getAvailabilityInstance();
124
125 // get all rooms and tax rates
126 $all_rooms = $av_helper->loadRooms();
127 $tax_rates = $av_helper->getTaxRates();
128
129 // count maximum units available depending on filter
130 $tot_rooms = 1;
131 if (!empty($room_id) && isset($all_rooms[$room_id])) {
132 // use the units of the filtered room
133 $max_units = $all_rooms[$room_id]['units'];
134 } else {
135 // sum all room units
136 $max_units = 0;
137 $tot_rooms = count($all_rooms);
138 foreach ($all_rooms as $rid => $room) {
139 $max_units += $room['units'];
140 }
141 }
142
143 // build "search name"
144 if (!empty($room_id) && isset($all_rooms[$room_id])) {
145 $search_name = $all_rooms[$room_id]['name'];
146 } else {
147 $search_name = preg_replace("/[^A-Za-z0-9 ]/", '', JText::translate('VBOSTATSALLROOMS'));
148 }
149
150 // load busy records
151 $room_filter = !empty($room_id) ? array($room_id) : array();
152 $busy_records = VikBooking::loadBusyRecords($room_filter, $from_ts, $to_ts);
153
154 // load festivities or room-day notes
155 $festivities = [];
156 $rday_notes = [];
157 if (!empty($room_id) && isset($all_rooms[$room_id])) {
158 // load room-day notes for the given room id
159 $rday_notes = VikBooking::getCriticalDatesInstance()->loadRoomDayNotes(date('Y-m-d', $from_ts), date('Y-m-d', $to_ts), $room_id);
160 } else {
161 // load festivities when no specific room filter set
162 $fests = VikBooking::getFestivitiesInstance();
163 if ($fests->shouldCheckFestivities()) {
164 $fests->storeNextFestivities();
165 }
166 $festivities = $fests->loadFestDates(date('Y-m-d', $from_ts), date('Y-m-d', $to_ts));
167 }
168
169 // date format
170 $dtpicker_df = $this->getDateFormat('jui');
171
172 // start output buffering
173 ob_start();
174
175 // generate calendar
176 $d_count = 0;
177 $mon_lim = $info_arr['mon'];
178 $next_offset = date('Y-m-d', $from_ts);
179
180 ?>
181 <div class="vbo-widget-booskcal-mday-wrap" style="display: none;">
182 <div class="vbo-widget-booskcal-mday-head">
183 <a class="vbo-widget-booskcal-mday-back" href="JavaScript: void(0);" onclick="vboWidgetBooksCalMonth('<?php echo $wrapper; ?>');"><?php VikBookingIcons::e('chevron-left'); ?> <?php echo $period_date; ?></a>
184 <span class="vbo-widget-booskcal-mday-name"></span>
185 </div>
186 <div class="vbo-dashboard-guests-latest vbo-widget-booskcal-mday-list" data-ymd="" data-offset="0" data-length="<?php echo $this->bookings_per_page; ?>"></div>
187 </div>
188
189 <div class="vbo-widget-booskcal-newbook-wrap" data-ymd="" data-roomid="" style="display: none;">
190 <div class="vbo-widget-booskcal-newbook-head">
191 <a class="vbo-widget-booskcal-newbook-back" href="JavaScript: void(0);" onclick="vboWidgetBooksCalMonth('<?php echo $wrapper; ?>');"><?php VikBookingIcons::e('chevron-left'); ?> <?php echo JText::translate('VBANNULLA'); ?></a>
192 <span class="vbo-widget-booskcal-newbook-name"><?php echo JText::translate('VBQUICKBOOK'); ?></span>
193 </div>
194 <div class="vbo-widget-booskcal-newbook-cont">
195 <div class="vbo-admin-container vbo-admin-container-full vbo-admin-container-compact">
196 <div class="vbo-params-wrap">
197 <div class="vbo-params-container">
198
199 <div class="vbo-param-container">
200 <div class="vbo-param-label"><?php echo JText::translate('VBPICKUPAT'); ?></div>
201 <div class="vbo-param-setting">
202 <div class="vbo-field-calendar">
203 <div class="input-append">
204 <input type="text" class="vbo-widget-bookscal-checkindt" value="" autocomplete="off" />
205 <button type="button" class="btn btn-secondary vbo-widget-bookscal-checkindt-trigger"><?php VikBookingIcons::e('calendar'); ?></button>
206 </div>
207 </div>
208 </div>
209 </div>
210
211 <div class="vbo-param-container">
212 <div class="vbo-param-label"><?php echo JText::translate('VBRELEASEAT'); ?></div>
213 <div class="vbo-param-setting">
214 <div class="vbo-field-calendar">
215 <div class="input-append">
216 <input type="text" class="vbo-widget-bookscal-checkoutdt" value="" autocomplete="off" />
217 <button type="button" class="btn btn-secondary vbo-widget-bookscal-checkoutdt-trigger"><?php VikBookingIcons::e('calendar'); ?></button>
218 </div>
219 </div>
220 </div>
221 </div>
222
223 <div class="vbo-param-container vbo-toggle-small">
224 <div class="vbo-param-label"><?php echo JText::translate('VBSUBMCLOSEROOM'); ?></div>
225 <div class="vbo-param-setting">
226 <?php echo $this->vbo_app->printYesNoButtons('closeroom', JText::translate('VBYES'), JText::translate('VBNO'), 0, 1, 0, "vboWidgetBooksCalClosure('{$wrapper}');"); ?>
227 </div>
228 </div>
229
230 <div class="vbo-param-container" data-noclosure="1">
231 <div class="vbo-param-label"><?php echo JText::translate('VBOCUSTOMER'); ?></div>
232 <div class="vbo-param-setting">
233 <span class="vbo-assign-customer" onclick="vboWidgetBooksCalAssignCustomer('<?php echo $wrapper; ?>');">
234 <?php VikBookingIcons::e('user-circle'); ?>
235 <span><?php echo JText::translate('VBFILLCUSTFIELDS'); ?></span>
236 </span>
237 <input type="hidden" value="" class="vbo-widget-bookscal-custid" />
238 <input type="hidden" value="" class="vbo-widget-bookscal-custmail" />
239 <input type="hidden" value="" class="vbo-widget-bookscal-custdata" />
240 <input type="hidden" value="" class="vbo-widget-bookscal-country" />
241 <input type="hidden" value="" class="vbo-widget-bookscal-state" />
242 <input type="hidden" value="" class="vbo-widget-bookscal-phone" />
243 <input type="hidden" value="" class="vbo-widget-bookscal-tfname" />
244 <input type="hidden" value="" class="vbo-widget-bookscal-tlname" />
245 <input type="hidden" value="" class="vbo-widget-bookscal-roomcost" />
246 <input type="hidden" value="" class="vbo-widget-bookscal-idprice" />
247 </div>
248 </div>
249
250 <div class="vbo-param-container" data-noclosure="1">
251 <div class="vbo-param-label"><?php echo JText::translate('VBPVIEWROOMSEVEN'); ?></div>
252 <div class="vbo-param-setting">
253 <input type="number" class="vbo-input-number-small vbo-widget-bookscal-units" value="1" min="1" max="99" onchange="vboWidgetBooksCalGetWebsiteRates('<?php echo $wrapper; ?>');" />
254 </div>
255 </div>
256
257 <div class="vbo-param-container" data-noclosure="1">
258 <div class="vbo-param-label"><?php echo JText::translate('VBPVIEWORDERSPEOPLE'); ?></div>
259 <div class="vbo-param-setting">
260 <span class="vbo-quickres-aduchi-wrap">
261 <span class="vbo-quickres-aduchi-inlbl"><?php echo JText::translate('VBEDITORDERADULTS'); ?></span>
262 <input type="number" class="vbo-input-number-small vbo-widget-bookscal-adults" value="2" min="0" max="99" onchange="vboWidgetBooksCalGetWebsiteRates('<?php echo $wrapper; ?>');" />
263 </span>
264 <span class="vbo-quickres-aduchi-wrap">
265 <span class="vbo-quickres-aduchi-inlbl"><?php echo JText::translate('VBEDITORDERCHILDREN'); ?></span>
266 <input type="number" class="vbo-input-number-small vbo-widget-bookscal-children" value="0" min="0" max="99" />
267 </span>
268 </div>
269 </div>
270
271 <div class="vbo-param-container vbo-website-rates-row" data-noclosure="1" data-unavailable="1" style="display: none;">
272 <div class="vbo-param-label"><?php echo JText::translate('VBOWEBSITERATES'); ?></div>
273 <div class="vbo-param-setting">
274 <div class="vbo-website-rates-cont"></div>
275 </div>
276 </div>
277
278 <div class="vbo-param-container vbo-row-custcost" data-noclosure="1">
279 <div class="vbo-param-label"><?php echo JText::translate('VBOROOMCUSTRATEPLANADD'); ?></div>
280 <div class="vbo-param-setting">
281 <div class="vbo-calendar-costs-wrapper">
282 <?php echo $currencysymb; ?> <input type="number" class="vbo-widget-bookscal-custcost" value="" step="any" min="0" onfocus="vboWidgetBooksCalFocusTaxes('<?php echo $wrapper; ?>');" />
283 <?php
284 if ($tax_rates) {
285 ?>
286 <select class="vbo-widget-bookscal-taxid" style="display: none;">
287 <option value=""><?php echo JText::translate('VBNEWOPTFOUR'); ?></option>
288 <?php
289 foreach ($tax_rates as $kiv => $iv) {
290 ?>
291 <option value="<?php echo $iv['id']; ?>"<?php echo $kiv < 1 ? ' selected="selected"' : ''; ?>><?php echo empty($iv['name']) ? "{$iv['aliq']}%" : "{$iv['name']} - {$iv['aliq']}%"; ?></option>
292 <?php
293 }
294 ?>
295 </select>
296 <?php
297 }
298 ?>
299 </div>
300 </div>
301 </div>
302
303 <div class="vbo-param-container vbo-param-confirm-btn">
304 <div class="vbo-param-label"></div>
305 <div class="vbo-param-setting">
306 <button type="button" class="btn btn-success vbo-btn-wide" onclick="vboWidgetBooksCalSaveBooking('<?php echo $wrapper; ?>');"><?php VikBookingIcons::e('save'); ?> <?php echo JText::translate('VBSAVE'); ?></button>
307 </div>
308 </div>
309
310 </div>
311 </div>
312 </div>
313 </div>
314 </div>
315
316 <table class="vbadmincaltable vbo-widget-booskcal-calendar-table">
317 <tbody>
318 <tr class="vbadmincaltrmdays">
319 <?php
320 // display week days in the proper order
321 for ($i = 0; $i < 7; $i++) {
322 $d_ind = ($i + $firstwday) < 7 ? ($i + $firstwday) : ($i + $firstwday - 7);
323 ?>
324 <td class="vbo-widget-booskcal-cell-wday"><?php echo $days_labels[$d_ind]; ?></td>
325 <?php
326 }
327 ?>
328 </tr>
329 <tr>
330 <?php
331 // display empty cells until the first week-day of the month
332 for ($i = 0, $n = $days_indexes[$info_arr['wday']]; $i < $n; $i++, $d_count++) {
333 ?>
334 <td class="vbo-widget-booskcal-cell-mday vbo-widget-booskcal-cell-empty">&nbsp;</td>
335 <?php
336 }
337 // display month days
338 while ($info_arr['mon'] == $mon_lim) {
339 if ($d_count > 6) {
340 $d_count = 0;
341 // close current row and open a new one
342 echo "\n</tr>\n<tr>\n";
343 }
344 // count units booked on this day
345 $tot_units_booked = 0;
346 $cell_classes = [];
347 $cell_bids = [];
348 foreach ($busy_records as $rid => $rbusy) {
349 foreach ($rbusy as $b) {
350 $tmpone = getdate($b['checkin']);
351 $ritts = mktime(0, 0, 0, $tmpone['mon'], $tmpone['mday'], $tmpone['year']);
352 $tmptwo = getdate($b['checkout']);
353 $conts = mktime(0, 0, 0, $tmptwo['mon'], $tmptwo['mday'], $tmptwo['year']);
354 if ($info_arr[0] >= $ritts && $info_arr[0] < $conts) {
355 // increase units booked
356 $tot_units_booked++;
357 if ($tot_rooms === 1) {
358 if (!empty($b['closure'])) {
359 // hightlight that this was a closure
360 $cell_classes[] = 'busy-closure';
361 } elseif (!empty($b['sharedcal'])) {
362 // hightlight that this was a reflection from a shared calendar
363 $cell_classes[] = 'busy-sharedcalendar';
364 }
365 }
366 // check if we can push the booking ID involved
367 if (!empty($b['idorder'])) {
368 $cell_bids[] = $b['idorder'];
369 }
370 }
371 }
372 }
373 // check status for this day
374 if ($tot_units_booked > 0) {
375 if ($tot_units_booked < $max_units) {
376 // prepend the "partially-busy" class
377 array_unshift($cell_classes, 'vbo-partially');
378 }
379 // prepend the "busy" cell class so that this will be first
380 array_unshift($cell_classes, 'busy');
381 } else {
382 // set the "free" cell class
383 $cell_classes[] = 'free';
384 }
385 // set ymd values
386 $cell_ymd = date('Y-m-d', $info_arr[0]);
387 if ($cell_ymd == $today_ymd) {
388 // set the "today" cell class
389 $cell_classes[] = 'is-today';
390 }
391 $cell_day_read = VikBooking::sayWeekDay($info_arr['wday']) . ' ' . $info_arr['mday'];
392
393 // count values for this day
394 $has_fests = isset($festivities[$cell_ymd]);
395 $rdnotes_key = $cell_ymd . '_' . $room_id . '_0';
396 $has_rdnotes = isset($rday_notes[$rdnotes_key]);
397
398 ?>
399 <td class="vbo-widget-booskcal-cell-mday <?php echo implode(' ', $cell_classes); ?>" onclick="vboWidgetBooksCalMday('<?php echo $wrapper; ?>', this);" data-bids="<?php echo implode(',', array_unique($cell_bids)); ?>" data-ymd="<?php echo $cell_ymd; ?>" data-dayread="<?php echo htmlspecialchars($cell_day_read); ?>">
400 <span class="vbo-widget-booskcal-mday-val"><?php echo $info_arr['mday']; ?></span>
401 <?php
402 if ($has_fests || $has_rdnotes) {
403 ?>
404 <div class="vbo-widget-booskcal-mday-info">
405 <?php
406 if ($has_fests) {
407 ?>
408 <span class="vbo-widget-booskcal-mday-fests"><?php VikBookingIcons::e('birthday-cake'); ?></span>
409 <?php
410 }
411 if ($has_rdnotes) {
412 ?>
413 <span class="vbo-widget-booskcal-mday-rdnotes"><?php VikBookingIcons::e('sticky-note'); ?></span>
414 <?php
415 }
416 ?>
417 </div>
418 <?php
419 }
420 ?>
421 </td>
422 <?php
423 $dayts = mktime(0, 0, 0, $info_arr['mon'], ($info_arr['mday'] + 1), $info_arr['year']);
424 $info_arr = getdate($dayts);
425 $d_count++;
426 }
427 // add empty cells until the end of the row
428 for ($i = $d_count; $i <= 6; $i++) {
429 ?>
430 <td class="vbo-widget-booskcal-cell-mday vbo-widget-booskcal-cell-empty">&nbsp;</td>
431 <?php
432 }
433 ?>
434 </tr>
435 </tbody>
436 </table>
437
438 <script type="text/javascript">
439
440 // render datepicker calendar for dates navigation
441 jQuery('#<?php echo $wrapper; ?>').find('.vbo-widget-bookscal-checkindt, .vbo-widget-bookscal-checkoutdt').datepicker({
442 minDate: "-1m",
443 maxDate: "+3y",
444 yearRange: "<?php echo date('Y'); ?>:<?php echo (date('Y') + 3); ?>",
445 changeMonth: true,
446 changeYear: true,
447 dateFormat: "<?php echo $dtpicker_df; ?>",
448 onSelect: function(selectedDate) {
449 if (!selectedDate) {
450 return;
451 }
452 if (jQuery(this).hasClass('vbo-widget-bookscal-checkindt')) {
453 let nowstart = jQuery(this).datepicker('getDate');
454 let nowstartdate = new Date(nowstart.getTime());
455 jQuery('.vbo-widget-bookscal-checkoutdt').datepicker('option', {minDate: nowstartdate});
456 }
457 // calculate website rates
458 vboWidgetBooksCalGetWebsiteRates('<?php echo $wrapper; ?>');
459 }
460 });
461
462 // triggering for datepicker calendar icon
463 jQuery('#<?php echo $wrapper; ?>').find('.vbo-widget-bookscal-checkindt-trigger, .vbo-widget-bookscal-checkoutdt-trigger').click(function() {
464 var jdp = jQuery(this).parent().find('input.hasDatepicker');
465 if (jdp.length) {
466 jdp.focus();
467 }
468 });
469
470 // check room units
471 var vbo_bookscal_roomfilt = jQuery('#<?php echo $wrapper; ?>').find('.vbo-booskcal-roomid');
472 if (vbo_bookscal_roomfilt.val()) {
473 var vbo_bookscal_room_units = vbo_bookscal_roomfilt.find('option:selected').attr('data-units');
474 if (vbo_bookscal_room_units > 1) {
475 jQuery('#<?php echo $wrapper; ?>').find('.vbo-widget-bookscal-units').attr('max', vbo_bookscal_room_units);
476 } else {
477 jQuery('#<?php echo $wrapper; ?>').find('.vbo-widget-bookscal-units').closest('[data-noclosure="1"]').hide();
478 }
479 } else {
480 jQuery('#<?php echo $wrapper; ?>').find('.vbo-widget-bookscal-units').closest('[data-noclosure="1"]').hide();
481 }
482
483 <?php
484 // check if a specific day was requested through the widget options
485 $force_day = $this->getOption('day', '');
486 if ($force_day && preg_match("/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/", $force_day)) {
487 // trigger the click event over the requested day
488 ?>
489 setTimeout(() => {
490 jQuery('.vbo-widget-booskcal-cell-mday[data-ymd="<?php echo $force_day; ?>"]').trigger('click');
491 }, 100);
492 <?php
493 }
494 ?>
495
496 </script>
497 <?php
498
499 // get the HTML buffer
500 $html_content = ob_get_contents();
501 ob_end_clean();
502
503 // return an associative array of values
504 return array(
505 'html' => $html_content,
506 'offset' => $next_offset,
507 'search_name' => $search_name,
508 'period_date' => $period_date,
509 );
510 }
511
512 /**
513 * Custom method for this widget only to load the bookings of a month-day.
514 * The method is called by the admin controller through an AJAX request.
515 * The visibility should be public, it should not exit the process, and
516 * any content sent to output will be returned to the AJAX response.
517 * In this case we return an array because this method requires "return":1.
518 */
519 public function loadMdayBookings()
520 {
521 // get today's date
522 $today_ymd = date('Y-m-d');
523
524 $page_offset = VikRequest::getInt('page_offset', 0, 'request');
525 $page_length = VikRequest::getInt('page_length', $this->bookings_per_page, 'request');
526 $ymd = VikRequest::getString('ymd', $today_ymd, 'request');
527 $wrapper = VikRequest::getString('wrapper', '', 'request');
528 $room_id = VikRequest::getInt('room_id', 0, 'request');
529
530 // calculate date timestamps interval for the given day
531 $day_info = getdate(strtotime($ymd));
532 $from_ts = mktime(0, 0, 0, $day_info['mon'], $day_info['mday'], $day_info['year']);
533 $to_ts = mktime(23, 59, 59, $day_info['mon'], $day_info['mday'], $day_info['year']);
534
535 // load busy records
536 $room_filter = !empty($room_id) ? array($room_id) : array();
537 $busy_records = VikBooking::loadBusyRecords($room_filter, $from_ts, $to_ts);
538
539 // gather all bookings touching this day
540 $booking_ids = [];
541 foreach ($busy_records as $rid => $rbusy) {
542 foreach ($rbusy as $b) {
543 $tmpone = getdate($b['checkin']);
544 $ritts = mktime(0, 0, 0, $tmpone['mon'], $tmpone['mday'], $tmpone['year']);
545 $tmptwo = getdate($b['checkout']);
546 $conts = mktime(0, 0, 0, $tmptwo['mon'], $tmptwo['mday'], $tmptwo['year']);
547 if ($from_ts >= $ritts && $from_ts < $conts) {
548 if (empty($b['idorder']) || in_array($b['idorder'], $booking_ids)) {
549 continue;
550 }
551 array_push($booking_ids, $b['idorder']);
552 }
553 }
554 }
555
556 // invoke availability helper class
557 $av_helper = VikBooking::getAvailabilityInstance();
558
559 // collect booking information
560 $booking_details = [];
561 foreach ($booking_ids as $bid) {
562 $booking = $av_helper->getBookingDetails($bid);
563 if (!is_array($booking) || !$booking) {
564 continue;
565 }
566 $booking_details[$bid] = $booking;
567 }
568
569 // check if a next page can be available
570 $tot_bookings = count($booking_details);
571 $has_next_page = ($tot_bookings > ($page_length + $page_offset));
572
573 // slice the records, if needed
574 if ($tot_bookings > $page_length) {
575 $booking_details = array_slice($booking_details, $page_offset, $page_length, true);
576 }
577
578 // load cancellations for today (only if a room ID filter is set)
579 $cancellations = [];
580 if (!empty($room_id)) {
581 $cancellations = $this->loadCancellations($room_id, $ymd);
582 }
583
584 // load festivities or room-day notes
585 $festivities = [];
586 $rday_notes = [];
587 if (!empty($room_id)) {
588 // load room-day notes for the given room id
589 $rday_notes = VikBooking::getCriticalDatesInstance()->loadRoomDayNotes(date('Y-m-d', $from_ts), date('Y-m-d', $to_ts), $room_id);
590 } else {
591 // load festivities when no specific room filter set
592 $festivities = VikBooking::getFestivitiesInstance()->loadFestDates(date('Y-m-d', $from_ts), date('Y-m-d', $to_ts));
593 }
594
595 // start output buffering
596 ob_start();
597
598 if ($festivities) {
599 // display the festivities for this day
600 ?>
601 <div class="vbo-widget-booskcal-events vbo-widget-booskcal-fests">
602 <?php
603 foreach ($festivities as $fest_ymd => $fest) {
604 if (empty($fest['festinfo']) || !is_array($fest['festinfo'])) {
605 continue;
606 }
607 foreach ($fest['festinfo'] as $fest_info) {
608 if (!is_object($fest_info) || empty($fest_info->trans_name)) {
609 continue;
610 }
611 ?>
612 <div class="vbo-widget-booskcal-event vbo-widget-booskcal-fest">
613 <strong><?php echo $fest_info->trans_name; ?></strong>
614 <?php
615 if (!empty($fest_info->descr)) {
616 ?>
617 <div><?php echo nl2br($fest_info->descr); ?></div>
618 <?php
619 }
620 ?>
621 </div>
622 <?php
623 }
624 }
625 ?>
626 </div>
627 <?php
628 }
629
630 if ($rday_notes) {
631 // display the room-day notes for this day
632 ?>
633 <div class="vbo-widget-booskcal-events vbo-widget-booskcal-rdaynotes">
634 <?php
635 foreach ($rday_notes as $rday_note) {
636 if (empty($rday_note['info']) || !is_array($rday_note['info'])) {
637 continue;
638 }
639 foreach ($rday_note['info'] as $note_info) {
640 if (!is_object($note_info) || empty($note_info->name)) {
641 continue;
642 }
643 ?>
644 <div class="vbo-widget-booskcal-event vbo-widget-booskcal-rdaynote">
645 <strong><?php echo $note_info->name; ?></strong>
646 <?php
647 if (!empty($note_info->descr)) {
648 ?>
649 <div><?php echo nl2br($note_info->descr); ?></div>
650 <?php
651 }
652 ?>
653 </div>
654 <?php
655 }
656 }
657 ?>
658 </div>
659 <?php
660 }
661
662 if (!$booking_details && !$cancellations) {
663 ?>
664 <p class="info"><?php echo JText::translate('VBNOORDERSFOUND'); ?></p>
665 <?php
666 } else {
667 if (!$booking_details) {
668 ?>
669 <p class="info"><?php echo JText::translate('VBNOORDERSFOUND'); ?></p>
670 <?php
671 }
672
673 // merge confirmed bookings with cancellations (if any)
674 $booking_details = array_merge($booking_details, $cancellations);
675
676 // display all bookings of this day
677 $canc_separator = false;
678 foreach ($booking_details as $booking) {
679 // get channel logo and other details
680 $ch_logo_obj = VikBooking::getVcmChannelsLogo($booking['channel'], true);
681 $channel_logo = is_object($ch_logo_obj) ? $ch_logo_obj->getSmallLogoURL() : '';
682 $nights_lbl = $booking['days'] > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY');
683 $rooms_lbl = !empty($booking['roomsnum']) && $booking['roomsnum'] > 1 ? ', ' . $booking['roomsnum'] . ' ' . JText::translate('VBPVIEWORDERSTHREE') : '';
684 // compose customer name
685 $customer_name = !empty($booking['customer_fullname']) ? $booking['customer_fullname'] : '';
686 if ($booking['closure'] > 0 || !strcasecmp($booking['custdata'], JText::translate('VBDBTEXTROOMCLOSED'))) {
687 $customer_name = '<span class="vbordersroomclosed"><i class="' . VikBookingIcons::i('ban') . '"></i> ' . JText::translate('VBDBTEXTROOMCLOSED') . '</span>';
688 }
689 if (empty($customer_name)) {
690 $customer_name = VikBooking::getFirstCustDataField($booking['custdata']);
691 }
692 // customer country flag
693 $customer_country = '';
694 $customer_cflag = '';
695 if (!empty($booking['customer_country'])) {
696 $customer_country = $booking['customer_country'];
697 } elseif (!empty($booking['country'])) {
698 $customer_country = $booking['country'];
699 }
700 if ($customer_country && is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . 'countries' . DIRECTORY_SEPARATOR . $customer_country . '.png')) {
701 $customer_cflag = '<img src="'.VBO_ADMIN_URI.'resources/countries/' . $customer_country . '.png'.'" title="' . htmlspecialchars($customer_country) . '" class="vbo-country-flag vbo-country-flag-left"/>';
702 }
703
704 // check for cancellations
705 $elem_style = $booking['status'] == 'cancelled' ? 'display: none;' : '';
706 if ($booking['status'] == 'cancelled' && !$canc_separator) {
707 // display the button to show the cancellations
708 $canc_separator = true;
709 ?>
710 <div class="vbo-bookings-status-separator">
711 <button type="button" class="btn btn-small btn-secondary" onclick="vboWidgetBooksCalCancToggle('<?php echo $wrapper; ?>');"><?php echo JText::translate('VBO_SHOW_CANCELLATIONS'); ?></button>
712 </div>
713 <?php
714 }
715
716 ?>
717 <div class="vbo-dashboard-guest-activity vbo-widget-booskcal-reservation" data-type="<?php echo $booking['status']; ?>" data-resid="<?php echo $booking['id']; ?>" style="<?php echo $elem_style; ?>">
718 <div class="vbo-dashboard-guest-activity-avatar">
719 <?php
720 if (!empty($channel_logo)) {
721 // channel logo has got the highest priority
722 ?>
723 <img class="vbo-dashboard-guest-activity-avatar-profile" src="<?php echo $channel_logo; ?>" />
724 <?php
725 } elseif (!empty($booking['pic'])) {
726 // customer profile picture
727 ?>
728 <img class="vbo-dashboard-guest-activity-avatar-profile" src="<?php echo strpos($booking['pic'], 'http') === 0 ? $booking['pic'] : VBO_SITE_URI . 'resources/uploads/' . $booking['pic']; ?>" />
729 <?php
730 } else {
731 // we use an icon as fallback
732 VikBookingIcons::e('hotel', 'vbo-dashboard-guest-activity-avatar-icon');
733 }
734 ?>
735 </div>
736 <div class="vbo-dashboard-guest-activity-content">
737 <div class="vbo-dashboard-guest-activity-content-head">
738 <div class="vbo-dashboard-guest-activity-content-info-details">
739 <h4><?php echo $customer_name . $customer_cflag; ?></h4>
740 <div class="vbo-dashboard-guest-activity-content-info-icon">
741 <?php
742 if (!strcasecmp($booking['type'], 'overbooking')) {
743 ?>
744 <span class="label label-error vbo-label-overbooking"><?php echo JText::translate('VBO_BTYPE_' . strtoupper($booking['type'])); ?></span>
745 <?php
746 }
747 if ($booking['status'] == 'cancelled') {
748 ?>
749 <span class="badge badge-danger"><?php echo JText::translate('VBCANCELLED'); ?></span>
750 <?php
751 }
752 ?>
753 <span><?php VikBookingIcons::e('plane-arrival'); ?> <?php echo date(str_replace("/", $this->datesep, $this->df), $booking['checkin']); ?> - <?php echo $booking['days'] . ' ' . $nights_lbl . $rooms_lbl; ?></span>
754 </div>
755 </div>
756 <div class="vbo-dashboard-guest-activity-content-info-date">
757 <span>
758 <span class="label label-info"><?php echo $booking['id']; ?></span>
759 </span>
760 <span><?php echo date(str_replace("/", $this->datesep, $this->df) . ' H:i', $booking['ts']); ?></span>
761 </div>
762 </div>
763 </div>
764 </div>
765 <?php
766 }
767
768 // append navigation
769 ?>
770 <div class="vbo-widget-commands vbo-widget-commands-right">
771 <div class="vbo-widget-commands-main">
772 <?php
773 if ($page_offset > 0) {
774 // show backward navigation button
775 ?>
776 <div class="vbo-widget-command-chevron vbo-widget-command-prev">
777 <span class="vbo-widget-command-chevron-prev" onclick="vboWidgetBooksCalMdayNavigate('<?php echo $wrapper; ?>', -1);"><?php VikBookingIcons::e('chevron-left'); ?></span>
778 </div>
779 <?php
780 }
781 if ($has_next_page) {
782 // show forward navigation button
783 ?>
784 <div class="vbo-widget-command-chevron vbo-widget-command-next">
785 <span class="vbo-widget-command-chevron-next" onclick="vboWidgetBooksCalMdayNavigate('<?php echo $wrapper; ?>', 1);"><?php VikBookingIcons::e('chevron-right'); ?></span>
786 </div>
787 <?php
788 }
789 ?>
790 </div>
791 </div>
792 <?php
793
794 // append JS code
795 ?>
796 <script type="text/javascript">
797
798 setTimeout(() => {
799 jQuery('#<?php echo $wrapper; ?>').find('.vbo-widget-booskcal-reservation').on('click', function(e) {
800 let bid = jQuery(this).attr('data-resid');
801 let target = jQuery(e.target);
802 if (target.is('img.vbo-dashboard-guest-activity-avatar-profile') || target.is('i.vbo-dashboard-guest-activity-avatar-icon') || target.is('span.label-info')) {
803 // render booking details within the widget booking details
804 VBOCore.handleDisplayWidgetNotification({widget_id: 'booking_details'}, {
805 bid: bid,
806 modal_options: {
807 /**
808 * Overwrite modal options for rendering the admin widget.
809 * We need to use a different suffix in case this current widget was
810 * also rendered within a modal, or it would get dismissed in favour
811 * of the newly opened admin widget.
812 */
813 suffix: 'widget_modal_inner_booking_details',
814 },
815 });
816 } else {
817 // navigate to booking details page
818 vboWidgetBooksCalOpenBooking(bid);
819 }
820 });
821 }, 300);
822
823 </script>
824 <?php
825 }
826
827 // get the HTML buffer
828 $html_content = ob_get_contents();
829 ob_end_clean();
830
831 // return an associative array of values
832 return array(
833 'html' => $html_content,
834 'tot_bookings' => $tot_bookings,
835 'next_page' => (int)$has_next_page,
836 );
837 }
838
839 /**
840 * Custom method for this widget only to load the custom fields for the new booking.
841 * The method is called by the admin controller through an AJAX request.
842 * The visibility should be public, and an array is requested to be returned.
843 *
844 * @return array
845 *
846 * @since 1.16.0 (J) - 1.6.0 (WP)
847 */
848 public function displayCustomerFilling()
849 {
850 $dbo = JFactory::getDbo();
851
852 $wrapper = VikRequest::getString('wrapper', '', 'request');
853
854 // load custom fields and countries
855 $all_countries = [];
856 $q = "SELECT * FROM `#__vikbooking_custfields` ORDER BY `#__vikbooking_custfields`.`ordering` ASC;";
857 $dbo->setQuery($q);
858 $all_cfields = $dbo->loadAssocList();
859 if ($all_cfields) {
860 $q = "SELECT * FROM `#__vikbooking_countries` ORDER BY `#__vikbooking_countries`.`country_name` ASC;";
861 $dbo->setQuery($q);
862 $all_countries = $dbo->loadAssocList();
863 }
864
865 // start output buffering
866 ob_start();
867
868 ?>
869 <div class="vbo-calendar-cfields-filler" data-wrapper="<?php echo $wrapper; ?>">
870 <div class="vbo-calendar-cfields-topcont">
871 <div class="vbo-calendar-cfields-search">
872 <label for="vbo-searchcust<?php echo $wrapper; ?>"><?php echo JText::translate('VBOSEARCHEXISTCUST'); ?></label>
873 <span id="vbo-searchcust-loading<?php echo $wrapper; ?>" class="vbo-searchcust-loading">
874 <i class="vboicn-hour-glass"></i>
875 </span>
876 <input type="text" id="vbo-searchcust<?php echo $wrapper; ?>" class="vbo-searchcust" autocomplete="off" value="" placeholder="<?php echo htmlspecialchars(JText::translate('VBOSEARCHCUSTBY')); ?>" size="35" />
877 <div id="vbo-searchcust-res<?php echo $wrapper; ?>" class="vbo-searchcust-res"></div>
878 </div>
879 </div>
880 <div class="vbo-calendar-cfields-inner vbo-widget-bookscal-cfields-inner">
881 <input type="hidden" value="" id="vbo-widget-bookscal-cfield-custid<?php echo $wrapper; ?>" />
882 <?php
883 foreach ($all_cfields as $cfield) {
884 if ($cfield['type'] == 'text' && $cfield['isphone'] == 1) {
885 ?>
886 <div class="vbo-calendar-cfield-entry">
887 <label for="cfield<?php echo $cfield['id'] . $wrapper; ?>"><?php echo JText::translate($cfield['name']); ?></label>
888 <span>
889 <?php
890 echo $this->vbo_app->printPhoneInputField(
891 [
892 'id' => 'cfield' . $cfield['id'] . $wrapper,
893 'class' => 'vbo-calendar-cfield-phone',
894 'data-isemail' => '0',
895 'data-isnominative' => '0',
896 'data-isphone' => '1'
897 ],
898 [
899 'fullNumberOnBlur' => true
900 ],
901 $load_assets = false
902 );
903 ?>
904 </span>
905 </div>
906 <?php
907 } elseif ($cfield['type'] == 'text') {
908 ?>
909 <div class="vbo-calendar-cfield-entry">
910 <label for="cfield<?php echo $cfield['id'] . $wrapper; ?>"><?php echo JText::translate($cfield['name']); ?></label>
911 <span>
912 <input type="text" id="cfield<?php echo $cfield['id'] . $wrapper; ?>" data-isemail="<?php echo ($cfield['isemail'] == 1 ? '1' : '0'); ?>" data-isnominative="<?php echo ($cfield['isnominative'] == 1 ? '1' : '0'); ?>" data-isphone="0" value="" size="35"/>
913 </span>
914 </div>
915 <?php
916 } elseif ($cfield['type'] == 'textarea') {
917 ?>
918 <div class="vbo-calendar-cfield-entry">
919 <label for="cfield<?php echo $cfield['id'] . $wrapper; ?>"><?php echo JText::translate($cfield['name']); ?></label>
920 <span>
921 <textarea id="cfield<?php echo $cfield['id'] . $wrapper; ?>" rows="4" cols="35"></textarea>
922 </span>
923 </div>
924 <?php
925 } elseif ($cfield['type'] == 'country') {
926 ?>
927 <div class="vbo-calendar-cfield-entry">
928 <label for="cfield<?php echo $cfield['id'] . $wrapper; ?>"><?php echo JText::translate($cfield['name']); ?></label>
929 <span>
930 <select id="cfield<?php echo $cfield['id'] . $wrapper; ?>" class="vbo-calendar-cfield-country" onchange="vboWidgetBooksCalChangeCountry(this, '<?php echo $wrapper; ?>');">
931 <option value=""> </option>
932 <?php
933 foreach ($all_countries as $country) {
934 ?>
935 <option value="<?php echo $country['country_name']; ?>" data-ccode="<?php echo $country['country_3_code']; ?>" data-c2code="<?php echo $country['country_2_code']; ?>"><?php echo $country['country_name']; ?></option>
936 <?php
937 }
938 ?>
939 </select>
940 </span>
941 </div>
942 <?php
943 } elseif ($cfield['type'] == 'state') {
944 ?>
945 <div class="vbo-calendar-cfield-entry">
946 <label for="cfield<?php echo $cfield['id'] . $wrapper; ?>"><?php echo JText::translate($cfield['name']); ?></label>
947 <span>
948 <select id="cfield<?php echo $cfield['id'] . $wrapper; ?>" class="vbo-calendar-cfield-state">
949 <option value="">-----</option>
950 </select>
951 </span>
952 </div>
953 <?php
954 }
955 }
956 ?>
957 </div>
958 </div>
959
960 <script type="text/javascript">
961
962 /**
963 * Pool of customers previous information collected.
964 */
965 window['customers_search_vals<?php echo $wrapper; ?>'] = {};
966
967 /**
968 * Search among the customers for the given keyword.
969 */
970 function vboWidgetBooksCalCustomerSearch(words) {
971 jQuery('#vbo-searchcust-res<?php echo $wrapper; ?>').hide().html('');
972 jQuery('#vbo-searchcust-loading<?php echo $wrapper; ?>').show();
973
974 VBOCore.doAjax(
975 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=searchcustomer'); ?>",
976 {
977 kw: words,
978 selector: 'vbo-widget-custsearchres-entry',
979 no_script: 1,
980 tmpl: "component"
981 },
982 (cont) => {
983 // parse the response
984 if (cont) {
985 // populate values
986 var obj_res = typeof cont === 'string' ? JSON.parse(cont) : cont;
987 window['customers_search_vals<?php echo $wrapper; ?>'] = obj_res[0];
988 jQuery('#vbo-searchcust-res<?php echo $wrapper; ?>').html(obj_res[1]);
989
990 setTimeout(() => {
991 // de-register listeners
992 jQuery('#vbo-searchcust<?php echo $wrapper; ?>').off('keydown');
993 jQuery('#vbo-searchcust-res<?php echo $wrapper; ?>').find('.vbo-widget-custsearchres-entry').off('hover');
994
995 // register listeners
996 var vboCust = jQuery('#vbo-searchcust-res<?php echo $wrapper; ?>').find('.vbo-widget-custsearchres-entry');
997 var vboCustSelected = null;
998 jQuery('#vbo-searchcust<?php echo $wrapper; ?>').keydown(function(e) {
999 if (e.which === 40) {
1000 if (vboCustSelected) {
1001 vboCustSelected.removeClass('vbo-widget-custsearchres-entry-highligthed');
1002 next = vboCustSelected.next();
1003 if (next.length > 0) {
1004 vboCustSelected = next.addClass('vbo-widget-custsearchres-entry-highligthed');
1005 } else {
1006 vboCustSelected = vboCust.eq(0).addClass('vbo-widget-custsearchres-entry-highligthed');
1007 }
1008 } else {
1009 vboCustSelected = vboCust.eq(0).addClass('vbo-widget-custsearchres-entry-highligthed');
1010 }
1011 } else if (e.which === 38) {
1012 if (vboCustSelected) {
1013 vboCustSelected.removeClass('vbo-widget-custsearchres-entry-highligthed');
1014 next = vboCustSelected.prev();
1015 if (next.length > 0) {
1016 vboCustSelected = next.addClass('vbo-widget-custsearchres-entry-highligthed');
1017 } else {
1018 vboCustSelected = vboCust.last().addClass('vbo-widget-custsearchres-entry-highligthed');
1019 }
1020 } else {
1021 vboCustSelected = vboCust.last().addClass('vbo-widget-custsearchres-entry-highligthed');
1022 }
1023 } else if (e.which === 13) {
1024 if (vboCustSelected) {
1025 vboCustSelected.trigger('click');
1026 }
1027 }
1028 });
1029
1030 jQuery('#vbo-searchcust-res<?php echo $wrapper; ?>').find('.vbo-widget-custsearchres-entry').hover(function() {
1031 if (vboCustSelected) {
1032 vboCustSelected.removeClass('vbo-widget-custsearchres-entry-highligthed');
1033 vboCustSelected = null;
1034 }
1035 vboCustSelected = jQuery(this).addClass('vbo-widget-custsearchres-entry-highligthed');
1036 }, function() {
1037 if (vboCustSelected) {
1038 vboCustSelected.removeClass('vbo-widget-custsearchres-entry-highligthed');
1039 vboCustSelected = null;
1040 }
1041 jQuery(this).removeClass('vbo-widget-custsearchres-entry-highligthed');
1042 });
1043 }, 300);
1044 } else {
1045 window['customers_search_vals<?php echo $wrapper; ?>'] = {};
1046 jQuery("#vbo-searchcust-res<?php echo $wrapper; ?>").html("----");
1047 }
1048 jQuery("#vbo-searchcust-res<?php echo $wrapper; ?>").show();
1049 jQuery("#vbo-searchcust-loading<?php echo $wrapper; ?>").hide();
1050 },
1051 (error) => {
1052 jQuery("#vbo-searchcust-loading<?php echo $wrapper; ?>").hide();
1053 console.error(error);
1054 alert(error.responseText);
1055 }
1056 );
1057 }
1058
1059 /**
1060 * Register keyup event listener with a debounce technique for searching customers.
1061 */
1062 document.getElementById('vbo-searchcust<?php echo $wrapper; ?>').addEventListener('keyup', VBOCore.debounceEvent((e) => {
1063 var keywords = jQuery("#vbo-searchcust<?php echo $wrapper; ?>").val();
1064 var chars = keywords ? keywords.length : '';
1065 // we prefer e.key, as e.keyCode is deprecated
1066 var key_pressed = e.key || e.keyCode;
1067 var rgx = new RegExp(/^[A-Za-z0-9]$/);
1068 if (chars > 1 && key_pressed) {
1069 if (key_pressed == 'Enter' || key_pressed == 13 || rgx.test(key_pressed)) {
1070 vboWidgetBooksCalCustomerSearch(keywords);
1071 }
1072 } else {
1073 if (jQuery("#vbo-searchcust-res<?php echo $wrapper; ?>").is(":visible")) {
1074 jQuery("#vbo-searchcust-res<?php echo $wrapper; ?>").hide();
1075 }
1076 }
1077 }, 600));
1078
1079 /**
1080 * Register the click event over the customer search results.
1081 */
1082 jQuery(document).on('click', '#vbo-searchcust-res<?php echo $wrapper; ?> .vbo-widget-custsearchres-entry', function() {
1083 var custid = jQuery(this).attr('data-custid');
1084 var custemail = jQuery(this).attr('data-email');
1085 var custphone = jQuery(this).attr('data-phone');
1086 var custcountry = jQuery(this).attr('data-country');
1087 var custfirstname = jQuery(this).attr('data-firstname');
1088 var custlastname = jQuery(this).attr('data-lastname');
1089
1090 // set customer ID
1091 jQuery('#vbo-widget-bookscal-cfield-custid<?php echo $wrapper; ?>').val(custid);
1092
1093 // check previous custom fields
1094 if (window['customers_search_vals<?php echo $wrapper; ?>'].hasOwnProperty(custid)) {
1095 jQuery.each(window['customers_search_vals<?php echo $wrapper; ?>'][custid], function(cfid, cfval) {
1096 var fill_field = jQuery('#cfield' + cfid + '<?php echo $wrapper; ?>');
1097 if (fill_field.length) {
1098 // set previous value
1099 fill_field.val(cfval);
1100 }
1101 });
1102 }
1103
1104 // always populate basic information on custom fields
1105 var fields_wrap = jQuery('.vbo-calendar-cfields-filler[data-wrapper="<?php echo $wrapper; ?>"]');
1106
1107 if (custcountry.length) {
1108 if (custcountry.length > 3) {
1109 fields_wrap.find('select.vbo-calendar-cfield-country').val(custcountry);
1110 } else {
1111 var country_opt = fields_wrap.find('select.vbo-calendar-cfield-country').find('option[data-ccode="' + custcountry + '"]');
1112 if (country_opt.length) {
1113 fields_wrap.find('select.vbo-calendar-cfield-country').val(country_opt.attr('value'));
1114 }
1115 }
1116 }
1117 fields_wrap.find('input[data-isnominative="1"]').each(function(k, v) {
1118 if (k == 0) {
1119 jQuery(this).val(custfirstname);
1120 return true;
1121 }
1122 if (k == 1) {
1123 jQuery(this).val(custlastname);
1124 return true;
1125 }
1126 return false;
1127 });
1128 fields_wrap.find('input[data-isemail="1"]').val(custemail);
1129 fields_wrap.find('input[data-isphone="1"]').val(custphone);
1130
1131 // set customer upon the selection just made
1132 vboWidgetBooksCalSetCustomer('<?php echo $wrapper; ?>');
1133 });
1134
1135 // focus search customer input field
1136 setTimeout(() => {
1137 jQuery('#vbo-searchcust<?php echo $wrapper; ?>').focus();
1138 }, 500);
1139
1140 </script>
1141 <?php
1142
1143 // get the HTML buffer
1144 $html_content = ob_get_contents();
1145 ob_end_clean();
1146
1147 // return an associative array of values
1148 return [
1149 'html' => $html_content,
1150 ];
1151 }
1152
1153 /**
1154 * Custom method for this widget only to save a new booking with the input values.
1155 * The method is called by the admin controller through an AJAX request.
1156 * The visibility should be public, and an array is requested to be returned.
1157 *
1158 * @return array
1159 *
1160 * @since 1.16.0 (J) - 1.6.0 (WP)
1161 */
1162 public function saveBooking()
1163 {
1164 $wrapper = VikRequest::getString('wrapper', '', 'request');
1165 $forcebooking = VikRequest::getInt('forcebooking', 0, 'request');
1166 $room_id = VikRequest::getInt('room_id', 0, 'request');
1167 $checkin = VikRequest::getString('checkin', '', 'request');
1168 $checkout = VikRequest::getString('checkout', '', 'request');
1169 $closure = VikRequest::getInt('closure', 0, 'request');
1170 $units_closed = VikRequest::getInt('units_closed', 0, 'request');
1171 $units = VikRequest::getInt('units', 1, 'request');
1172 $adults = VikRequest::getInt('adults', 2, 'request');
1173 $children = VikRequest::getInt('children', 0, 'request');
1174 $cust_id = VikRequest::getInt('cust_id', 0, 'request');
1175 $cust_email = VikRequest::getString('cust_email', '', 'request');
1176 $cust_data = VikRequest::getString('cust_data', '', 'request');
1177 $cust_country = VikRequest::getString('cust_country', '', 'request');
1178 $cust_state = VikRequest::getString('cust_state', '', 'request');
1179 $cust_phone = VikRequest::getString('cust_phone', '', 'request');
1180 $cust_tfname = VikRequest::getString('cust_tfname', '', 'request');
1181 $cust_tlname = VikRequest::getString('cust_tlname', '', 'request');
1182 $roomcost = VikRequest::getFloat('roomcost', 0, 'request');
1183 $idprice = VikRequest::getInt('idprice', 0, 'request');
1184 $cust_roomcost = VikRequest::getFloat('cust_roomcost', 0, 'request');
1185 $taxid = VikRequest::getInt('taxid', 0, 'request');
1186
1187 if (empty($room_id) || empty($checkin) || empty($checkout)) {
1188 VBOHttpDocument::getInstance()->close(500, JText::translate('VBO_PLEASE_FILL_FIELDS'));
1189 }
1190
1191 // invoke the reservation model and inject values
1192 $model_res = VBOModelReservation::getInstance([
1193 'force_booking' => $forcebooking,
1194 'set_closed' => $closure,
1195 'units_closed' => $units_closed,
1196 'status' => 'confirmed',
1197 'num_rooms' => $units,
1198 'adults' => $adults,
1199 'children' => $children,
1200 ])->setCustomer([
1201 'id' => $cust_id,
1202 'first_name' => $cust_tfname,
1203 'last_name' => $cust_tlname,
1204 'data' => $cust_data,
1205 'email' => $cust_email,
1206 'country' => $cust_country,
1207 'state' => $cust_state,
1208 'phone' => $cust_phone,
1209 ])->setRoom([
1210 'id' => $room_id,
1211 'cust_cost' => $cust_roomcost,
1212 'room_cost' => $roomcost,
1213 'id_price' => $idprice,
1214 'id_tax' => $taxid,
1215 ]);
1216
1217 // calculate proper check-in and check-out timestamps
1218 list($hcheckin, $mcheckin, $hcheckout, $mcheckout) = $model_res->loadCheckinOutTimes();
1219
1220 // get final stay timestamps
1221 $checkin_ts = VikBooking::getDateTimestamp($checkin, $hcheckin, $mcheckin);
1222 $checkout_ts = VikBooking::getDateTimestamp($checkout, $hcheckout, $mcheckout);
1223
1224 if (!$checkin_ts || !$checkout_ts || $checkin_ts >= $checkout_ts) {
1225 VBOHttpDocument::getInstance()->close(500, JText::translate('ERRINVDATESEASON'));
1226 }
1227
1228 // set stay dates
1229 $model_res->set('checkin', $checkin_ts);
1230 $model_res->set('checkout', $checkout_ts);
1231
1232 // store the reservation
1233 $model_res->create();
1234
1235 // get the new booking ID
1236 $res_id = $model_res->getNewBookingID();
1237 if (!$res_id) {
1238 VBOHttpDocument::getInstance()->close(500, $model_res->getError());
1239 }
1240
1241 return [
1242 'new_booking_id' => $res_id,
1243 'vcm_action' => $model_res->getChannelManagerAction(),
1244 ];
1245 }
1246
1247 /**
1248 * Preload the necessary CSS/JS assets.
1249 *
1250 * @return void
1251 */
1252 public function preload()
1253 {
1254 // load assets
1255 $this->vbo_app->loadDatePicker();
1256 $this->vbo_app->loadPhoneInputFieldAssets();
1257
1258 // JS lang def
1259 JText::script('VBFILLCUSTFIELDS');
1260 JText::script('VBO_PLEASE_FILL_FIELDS');
1261 JText::script('VBDASHUPRESONE');
1262 JText::script('VBANNULLA');
1263 JText::script('VBAPPLY');
1264 JText::script('VBDBTEXTROOMCLOSED');
1265 JText::script('VBO_MARK_UNITS_CLOSED');
1266 }
1267
1268 /**
1269 * Main method to invoke the widget. Contents will be loaded
1270 * through AJAX requests, not via PHP when the page loads.
1271 *
1272 * @param VBOMultitaskData $data
1273 *
1274 * @return void
1275 */
1276 public function render(VBOMultitaskData $data = null)
1277 {
1278 // increase widget's instance counter
1279 static::$instance_counter++;
1280
1281 // check whether the widget is being rendered via AJAX when adding it through the customizer
1282 $is_ajax = $this->isAjaxRendering();
1283
1284 // generate a unique ID for the sticky notes wrapper instance
1285 $wrapper_instance = !$is_ajax ? static::$instance_counter : rand();
1286 $wrapper_id = 'vbo-widget-booskcal-' . $wrapper_instance;
1287
1288 // get permissions
1289 $vbo_auth_bookings = JFactory::getUser()->authorise('core.vbo.bookings', 'com_vikbooking');
1290 if (!$vbo_auth_bookings) {
1291 // display nothing
1292 return;
1293 }
1294
1295 // invoke availability helper class
1296 $av_helper = VikBooking::getAvailabilityInstance();
1297
1298 // get all rooms
1299 $all_rooms = $av_helper->loadRooms();
1300
1301 // check pricing tax configuration
1302 $prices_vat_included = (int)VikBooking::ivaInclusa();
1303
1304 /**
1305 * This widget can make use of the Select2 jQuery plugin, but we do not preload it in
1306 * order to save resources. If any other widget has preloaded Select2, JS will detect it.
1307 */
1308 $use_nice_select = 0;
1309 if (count($all_rooms) > 1) {
1310 // turn flag on
1311 $use_nice_select = 1;
1312 }
1313
1314 // default dates and values
1315 $now_info = getdate();
1316 $from_ts = mktime(0, 0, 0, $now_info['mon'], 1, $now_info['year']);
1317
1318 // build week days list according to settings
1319 $firstwday = (int)VikBooking::getFirstWeekDay();
1320 $days_labels = array(
1321 JText::translate('VBSUN'),
1322 JText::translate('VBMON'),
1323 JText::translate('VBTUE'),
1324 JText::translate('VBWED'),
1325 JText::translate('VBTHU'),
1326 JText::translate('VBFRI'),
1327 JText::translate('VBSAT'),
1328 );
1329 $days_indexes = [];
1330 for ($i = 0; $i < 7; $i++) {
1331 $days_indexes[$i] = (6 - ($firstwday - $i) + 1) % 7;
1332 }
1333
1334 // check multitask data
1335 $page_bid = 0;
1336 $load_room = 0;
1337 $modal_load_bid = '';
1338 $js_modal_id = '';
1339 if ($data) {
1340 // access Multitask data
1341 $page_bid = $data->getBookingID() ?: $this->options()->fetchBookingId();
1342 $is_modal_rendering = $data->isModalRendering();
1343 if ($page_bid && $is_modal_rendering) {
1344 // load contents according to injected multitask data
1345 $modal_load_bid = $page_bid;
1346 }
1347 if ($is_modal_rendering) {
1348 // get modal JS identifier
1349 $js_modal_id = $data->getModalJsIdentifier();
1350 }
1351
1352 // first check if an overbooking flag was given
1353 $overbooking = $this->getOption('overbooking', 0);
1354 $overbooking = $page_bid && $overbooking ? $page_bid : 0;
1355 if ($overbooking) {
1356 // prepare the options to set for rendering the admin-widget
1357 // correctly and show the overbooking details
1358 $this->prepareOverbookingOptions($overbooking);
1359 }
1360
1361 // check if a specific room ID was set
1362 $load_room = $this->getOption('id_room', 0);
1363
1364 // check for a custom date (Y-m-d)
1365 $option_offset = $this->getOption('offset', '');
1366 if ($option_offset && preg_match("/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/", $option_offset)) {
1367 // overwrite values
1368 $now_info = getdate(strtotime($option_offset));
1369 $from_ts = mktime(0, 0, 0, $now_info['mon'], 1, $now_info['year']);
1370 }
1371 }
1372
1373 // default labels
1374 $search_name = preg_replace("/[^A-Za-z0-9 ]/", '', JText::translate('VBOSTATSALLROOMS'));
1375 $period_date = VikBooking::sayMonth($now_info['mon']) . ' ' . $now_info['year'];
1376
1377 // start looping from the first day of the current month
1378 $info_arr = getdate($from_ts);
1379
1380 // week days counter
1381 $d_count = 0;
1382 $mon_lim = $info_arr['mon'];
1383
1384 ?>
1385 <div id="<?php echo $wrapper_id; ?>" class="vbo-admin-widget-wrapper" data-instance="<?php echo $wrapper_instance; ?>" data-pagebid="<?php echo $page_bid; ?>" data-offset="<?php echo date('Y-m-d', $from_ts); ?>">
1386 <div class="vbo-admin-widget-head">
1387 <div class="vbo-admin-widget-head-inline">
1388 <h4><?php echo $this->widgetIcon; ?> <span><?php echo $this->widgetName; ?></span></h4>
1389 <div class="vbo-admin-widget-head-commands">
1390
1391 <div class="vbo-reportwidget-commands">
1392 <div class="vbo-reportwidget-commands-main">
1393 <div class="vbo-reportwidget-command-dates">
1394 <div class="vbo-reportwidget-period-name"><?php echo $search_name; ?></div>
1395 <div class="vbo-reportwidget-period-date"><?php echo $period_date; ?></div>
1396 </div>
1397 <div class="vbo-reportwidget-command-chevron vbo-reportwidget-command-prev">
1398 <span class="vbo-widget-booskcal-dt-prev" onclick="vboWidgetBookCalsMonthNav('<?php echo $wrapper_id; ?>', -1);"><?php VikBookingIcons::e('chevron-left'); ?></span>
1399 </div>
1400 <div class="vbo-reportwidget-command-chevron vbo-reportwidget-command-next">
1401 <span class="vbo-widget-booskcal-dt-next" onclick="vboWidgetBookCalsMonthNav('<?php echo $wrapper_id; ?>', 1);"><?php VikBookingIcons::e('chevron-right'); ?></span>
1402 </div>
1403 </div>
1404 </div>
1405
1406 </div>
1407 </div>
1408 </div>
1409 <div class="vbo-widget-booskcal-wrap">
1410 <div class="vbo-widget-booskcal-inner">
1411
1412 <div class="vbo-widget-booskcal-top-wrap">
1413 <div class="vbo-widget-booskcal-filter">
1414 <select class="vbo-booskcal-roomid" onchange="vboWidgetBookCalsSetRoom('<?php echo $wrapper_id; ?>', this.value);">
1415 <option data-units="0"></option>
1416 <?php
1417 foreach ($all_rooms as $rid => $room) {
1418 ?>
1419 <option value="<?php echo $rid; ?>" data-units="<?php echo $room['units']; ?>"<?php echo $rid == $load_room ? ' selected="selected"' : ''; ?>><?php echo $room['name']; ?></option>
1420 <?php
1421 }
1422 ?>
1423 </select>
1424 </div>
1425 <div class="vbo-widget-booskcal-newbook">
1426 <button type="button" class="btn btn-success vbo-widget-booskcal-newbook-start" onclick="vboWidgetBookCalsNewBooking('<?php echo $wrapper_id; ?>');"><?php VikBookingIcons::e('plus-circle'); ?> <?php echo JText::translate('VBO_NEW_BOOKING'); ?></button>
1427 <button type="button" class="btn btn-success vbo-widget-booskcal-newbook-id" data-bookingid="" onclick="vboWidgetBookCalsOpenNewBooking('<?php echo $wrapper_id; ?>');" style="display: none;"><?php VikBookingIcons::e('check-circle'); ?> <span></span></button>
1428 </div>
1429 </div>
1430
1431 <div class="vbo-widget-booskcal-calendar">
1432
1433 <table class="vbadmincaltable vbo-widget-booskcal-calendar-table">
1434 <tbody>
1435 <tr class="vbadmincaltrmdays">
1436 <?php
1437 // display week days in the proper order
1438 for ($i = 0; $i < 7; $i++) {
1439 $d_ind = ($i + $firstwday) < 7 ? ($i + $firstwday) : ($i + $firstwday - 7);
1440 ?>
1441 <td class="vbo-widget-booskcal-cell-wday"><?php echo $days_labels[$d_ind]; ?></td>
1442 <?php
1443 }
1444 ?>
1445 </tr>
1446 <tr>
1447 <?php
1448 // display empty cells until the first week-day of the month
1449 for ($i = 0, $n = $days_indexes[$info_arr['wday']]; $i < $n; $i++, $d_count++) {
1450 ?>
1451 <td class="vbo-widget-booskcal-cell-mday vbo-widget-booskcal-cell-empty">&nbsp;</td>
1452 <?php
1453 }
1454 // display month days
1455 while ($info_arr['mon'] == $mon_lim) {
1456 if ($d_count > 6) {
1457 $d_count = 0;
1458 // close current row and open a new one
1459 echo "\n</tr>\n<tr>\n";
1460 }
1461 ?>
1462 <td class="vbo-widget-booskcal-cell-mday">
1463 <span class="vbo-widget-booskcal-mday-val"><?php echo $info_arr['mday']; ?></span>
1464 </td>
1465 <?php
1466 $dayts = mktime(0, 0, 0, $info_arr['mon'], ($info_arr['mday'] + 1), $info_arr['year']);
1467 $info_arr = getdate($dayts);
1468 $d_count++;
1469 }
1470 // add empty cells until the end of the row
1471 for ($i = $d_count; $i <= 6; $i++) {
1472 ?>
1473 <td class="vbo-widget-booskcal-cell-mday vbo-widget-booskcal-cell-empty">&nbsp;</td>
1474 <?php
1475 }
1476 ?>
1477 </tr>
1478 </tbody>
1479 </table>
1480
1481 </div>
1482
1483 </div>
1484 </div>
1485 </div>
1486 <?php
1487
1488 if (static::$instance_counter === 0 || $is_ajax) {
1489 /**
1490 * Print the JS code only once for all instances of this widget.
1491 * The real rendering is made through AJAX, not when the page loads.
1492 */
1493 ?>
1494 <a class="vbo-widget-bookscal-basenavuri" href="<?php echo VBOFactory::getPlatform()->getUri()->admin('index.php?option=com_vikbooking&task=editorder&cid[]=%d', $xhtml = false); ?>" style="display: none;"></a>
1495
1496 <script type="text/javascript">
1497
1498 /**
1499 * Open the booking details page for the clicked reservation.
1500 */
1501 function vboWidgetBooksCalOpenBooking(id) {
1502 var open_url = jQuery('.vbo-widget-bookscal-basenavuri').first().attr('href');
1503 open_url = open_url.replace('%d', id);
1504 // navigate in a new tab
1505 window.open(open_url, '_blank');
1506 }
1507
1508 /**
1509 * Display the loading skeletons.
1510 */
1511 function vboWidgetBooksCalSkeletons(wrapper) {
1512 var widget_instance = jQuery('#' + wrapper);
1513 if (!widget_instance.length) {
1514 return false;
1515 }
1516
1517 var skelton_html = '<div class="vbo-skeleton-loading vbo-skeleton-loading-mday-cell"></div>';
1518 widget_instance.find('.vbo-widget-booskcal-calendar').find('.vbo-widget-booskcal-cell-mday').attr('class', 'vbo-widget-booskcal-cell-mday').html(skelton_html);
1519 }
1520
1521 /**
1522 * Perform the request to load the bookings calendar.
1523 */
1524 function vboWidgetBooksCalLoad(wrapper, dates_direction, page_bid) {
1525 var widget_instance = jQuery('#' + wrapper);
1526 if (!widget_instance.length) {
1527 return false;
1528 }
1529
1530 // check if a navigation of dates was requested (0 = no dates nav)
1531 if (typeof dates_direction === 'undefined') {
1532 dates_direction = 0;
1533 }
1534
1535 // check if multitask data passed a booking ID for the current page
1536 var force_bid = 0;
1537 if (typeof page_bid !== 'undefined' && page_bid && !isNaN(page_bid)) {
1538 force_bid = page_bid;
1539 }
1540
1541 // get vars for making the request
1542 var current_offset = widget_instance.attr('data-offset');
1543 var room_id = widget_instance.find('.vbo-booskcal-roomid').val();
1544
1545 // gather options, if any
1546 var options = vbo_widget_books_cal_options_oo || {};
1547 if (vbo_widget_books_cal_options_oo) {
1548 // multitask data options should be used only once ("oo")
1549 vbo_widget_books_cal_options_oo = null;
1550 }
1551
1552 // the widget method to call
1553 var call_method = 'loadBookingsCalendar';
1554
1555 // make a request to load the bookings calendar
1556 VBOCore.doAjax(
1557 "<?php echo $this->getExecWidgetAjaxUri(); ?>",
1558 {
1559 widget_id: "<?php echo $this->getIdentifier(); ?>",
1560 call: call_method,
1561 return: 1,
1562 bid: force_bid,
1563 offset: current_offset,
1564 room_id: room_id,
1565 date_dir: dates_direction,
1566 wrapper: wrapper,
1567 _options: options,
1568 tmpl: "component"
1569 },
1570 (response) => {
1571 try {
1572 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
1573 if (!obj_res.hasOwnProperty(call_method)) {
1574 console.error('Unexpected JSON response', obj_res);
1575 return false;
1576 }
1577
1578 // set new offset for navigation
1579 widget_instance.attr('data-offset', obj_res[call_method]['offset']);
1580
1581 // update search name and month
1582 widget_instance.find('.vbo-reportwidget-period-name').text(obj_res[call_method]['search_name']);
1583 widget_instance.find('.vbo-reportwidget-period-date').text(obj_res[call_method]['period_date']);
1584
1585 // replace HTML with new bookings calendar
1586 widget_instance.find('.vbo-widget-booskcal-calendar').html(obj_res[call_method]['html']);
1587 } catch(err) {
1588 console.error('could not parse JSON response', err, response);
1589 }
1590 },
1591 (error) => {
1592 // remove the skeleton loading
1593 widget_instance.find('.vbo-widget-booskcal-calendar').find('.vbo-skeleton-loading').remove();
1594 console.error(error);
1595 }
1596 );
1597 }
1598
1599 /**
1600 * Perform the request to load the month-day reservations.
1601 */
1602 function vboWidgetBooksCalGetMdayRes(wrapper, ymd) {
1603 var widget_instance = jQuery('#' + wrapper);
1604 if (!widget_instance.length || !ymd) {
1605 return false;
1606 }
1607
1608 // get vars for making the request
1609 var room_id = widget_instance.find('.vbo-booskcal-roomid').val();
1610 var page_offset = widget_instance.find('.vbo-widget-booskcal-mday-list').attr('data-offset');
1611 var page_length = widget_instance.find('.vbo-widget-booskcal-mday-list').attr('data-length');
1612
1613 // the widget method to call
1614 var call_method = 'loadMdayBookings';
1615
1616 // make a request to load the bookings calendar
1617 VBOCore.doAjax(
1618 "<?php echo $this->getExecWidgetAjaxUri(); ?>",
1619 {
1620 widget_id: "<?php echo $this->getIdentifier(); ?>",
1621 call: call_method,
1622 return: 1,
1623 page_offset: page_offset,
1624 page_length: page_length,
1625 ymd: ymd,
1626 room_id: room_id,
1627 wrapper: wrapper,
1628 tmpl: "component"
1629 },
1630 (response) => {
1631 try {
1632 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
1633 if (!obj_res.hasOwnProperty(call_method)) {
1634 console.error('Unexpected JSON response', obj_res);
1635 return false;
1636 }
1637 // replace HTML with month-day reservations
1638 widget_instance.find('.vbo-widget-booskcal-mday-list').html(obj_res[call_method]['html']);
1639 } catch(err) {
1640 console.error('could not parse JSON response', err, response);
1641 }
1642 },
1643 (error) => {
1644 // remove the skeleton loading
1645 widget_instance.find('.vbo-widget-booskcal-mday-list').html('');
1646 console.error(error);
1647 }
1648 );
1649 }
1650
1651 /**
1652 * Navigate between the months and load the bookings calendar.
1653 */
1654 function vboWidgetBookCalsMonthNav(wrapper, direction) {
1655 var widget_instance = jQuery('#' + wrapper);
1656 if (!widget_instance.length) {
1657 return false;
1658 }
1659
1660 // show loading skeletons
1661 vboWidgetBooksCalSkeletons(wrapper);
1662
1663 // launch dates navigation and load records
1664 vboWidgetBooksCalLoad(wrapper, direction);
1665 }
1666
1667 /**
1668 * Change room calendar.
1669 */
1670 function vboWidgetBookCalsSetRoom(wrapper, rid) {
1671 var widget_instance = jQuery('#' + wrapper);
1672 if (!widget_instance.length) {
1673 return false;
1674 }
1675
1676 // check if we are creating a new booking
1677 if (widget_instance.find('.vbo-widget-booskcal-newbook-wrap').is(':visible')) {
1678 // check room units
1679 var vbo_bookscal_roomfilt = widget_instance.find('.vbo-booskcal-roomid');
1680 if (rid) {
1681 // manage units depending on selected room total units
1682 var vbo_bookscal_room_units = vbo_bookscal_roomfilt.find('option:selected').attr('data-units');
1683 if (vbo_bookscal_room_units > 1) {
1684 widget_instance.find('.vbo-widget-bookscal-units').attr('max', vbo_bookscal_room_units).val('1').closest('[data-noclosure="1"]').show();
1685 } else {
1686 widget_instance.find('.vbo-widget-bookscal-units').val('1').closest('[data-noclosure="1"]').hide();
1687 }
1688 } else {
1689 // hide units when no room is selected
1690 vbo_bookscal_roomfilt.closest('[data-noclosure="1"]').hide();
1691 }
1692
1693 // check the closure status
1694 if (widget_instance.find('input[name="closeroom"]').prop('checked')) {
1695 // reset values
1696 widget_instance.find('.vbo-widget-bookscal-units').val('1');
1697 widget_instance.find('[data-noclosure="1"]').hide();
1698 }
1699
1700 // attempt to update the website rates
1701 vboWidgetBooksCalGetWebsiteRates(wrapper);
1702
1703 // do nothing else when adding a new booking
1704 return;
1705 }
1706
1707 // show loading skeletons
1708 vboWidgetBooksCalSkeletons(wrapper);
1709
1710 // let the records be loaded for this new room filter
1711 vboWidgetBooksCalLoad(wrapper, 0);
1712 }
1713
1714 /**
1715 * Generate the HTML skeleton string to the month-day reservations.
1716 */
1717 function vboWidgetBooksCalMdaySkeleton() {
1718 var monthday_loading = '';
1719 monthday_loading += '<div class="vbo-dashboard-guest-activity vbo-dashboard-guest-activity-skeleton">';
1720 monthday_loading += ' <div class="vbo-dashboard-guest-activity-avatar">';
1721 monthday_loading += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-avatar"></div>';
1722 monthday_loading += ' </div>';
1723 monthday_loading += ' <div class="vbo-dashboard-guest-activity-content">';
1724 monthday_loading += ' <div class="vbo-dashboard-guest-activity-content-head">';
1725 monthday_loading += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-title"></div>';
1726 monthday_loading += ' </div>';
1727 monthday_loading += ' <div class="vbo-dashboard-guest-activity-content-subhead">';
1728 monthday_loading += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-subtitle"></div>';
1729 monthday_loading += ' </div>';
1730 monthday_loading += ' <div class="vbo-dashboard-guest-activity-content-info-msg">';
1731 monthday_loading += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-content"></div>';
1732 monthday_loading += ' </div>';
1733 monthday_loading += ' </div>';
1734 monthday_loading += '</div>';
1735
1736 return monthday_loading;
1737 }
1738
1739 /**
1740 * Enter the month-day view mode from monthly view.
1741 */
1742 function vboWidgetBooksCalMday(wrapper, element) {
1743 var widget_instance = jQuery('#' + wrapper);
1744 if (!widget_instance.length) {
1745 return false;
1746 }
1747
1748 // get cell element
1749 var cell = jQuery(element);
1750 if (!cell || !cell.length) {
1751 return false;
1752 }
1753
1754 // set month-day title
1755 var day_read = cell.attr('data-dayread');
1756 widget_instance.find('.vbo-widget-booskcal-mday-name').text(day_read);
1757
1758 // get cell ymd value
1759 var day_ymd = cell.attr('data-ymd');
1760
1761 // always update the proper ymd day
1762 widget_instance.find('.vbo-widget-booskcal-mday-list').attr('data-ymd', day_ymd);
1763
1764 // get pre-loaded booking ids
1765 var tot_day_res = 0;
1766 var day_bids = cell.attr('data-bids');
1767 if (day_bids && day_bids.length) {
1768 tot_day_res = day_bids.split(',').length;
1769 }
1770
1771 // populate loading skeletons for month-day bookings
1772 var monthday_loading = vboWidgetBooksCalMdaySkeleton();
1773 if (tot_day_res > 1) {
1774 // double up the loading skeletons
1775 monthday_loading = monthday_loading + monthday_loading;
1776 }
1777 widget_instance.find('.vbo-widget-booskcal-mday-list').html(monthday_loading);
1778
1779 // toggle elements
1780 widget_instance.find('.vbo-widget-booskcal-calendar-table').hide();
1781 widget_instance.find('.vbo-widget-booskcal-newbook-wrap').hide();
1782 widget_instance.find('.vbo-widget-booskcal-mday-wrap').show();
1783
1784 // launch month-day bookings retrieval
1785 vboWidgetBooksCalGetMdayRes(wrapper, day_ymd);
1786 }
1787
1788 /**
1789 * Navigate between the various pages of the month-day bookings.
1790 */
1791 function vboWidgetBooksCalMdayNavigate(wrapper, direction) {
1792 var widget_instance = jQuery('#' + wrapper);
1793 if (!widget_instance.length) {
1794 return false;
1795 }
1796
1797 // get bookings container
1798 var bookings_list = widget_instance.find('.vbo-widget-booskcal-mday-list');
1799
1800 // show loading skeletons
1801 var monthday_loading = vboWidgetBooksCalMdaySkeleton();
1802 bookings_list.html(monthday_loading + monthday_loading);
1803
1804 // get current offset and length (MUST be numbers, not strings)
1805 var current_offset = parseInt(bookings_list.attr('data-offset'));
1806 var current_length = parseInt(bookings_list.attr('data-length'));
1807 var day_ymd = bookings_list.attr('data-ymd');
1808
1809 // check direction and update offsets for nav
1810 if (direction > 0) {
1811 // navigate forward
1812 bookings_list.attr('data-offset', (current_offset + current_length));
1813 } else {
1814 // navigate backward
1815 var new_offset = current_offset - current_length;
1816 new_offset = new_offset >= 0 ? new_offset : 0;
1817 bookings_list.attr('data-offset', new_offset);
1818 }
1819
1820 // launch month-day bookings retrieval
1821 vboWidgetBooksCalGetMdayRes(wrapper, day_ymd);
1822 }
1823
1824 /**
1825 * Go back to the montly/month-day view from the month-day/new-booking view.
1826 */
1827 function vboWidgetBooksCalMonth(wrapper) {
1828 var widget_instance = jQuery('#' + wrapper);
1829 if (!widget_instance.length) {
1830 return false;
1831 }
1832
1833 var current_ymd = '';
1834 var current_rid = '';
1835 if (widget_instance.find('.vbo-widget-booskcal-newbook-wrap').is(':visible')) {
1836 current_ymd = widget_instance.find('.vbo-widget-booskcal-newbook-wrap').attr('data-ymd');
1837 current_rid = widget_instance.find('.vbo-widget-booskcal-newbook-wrap').attr('data-roomid');
1838 if (current_rid != widget_instance.find('.vbo-booskcal-roomid').val()) {
1839 // going back from the new-booking view detected a change of the room filter, so we do a reset
1840
1841 // show loading skeletons
1842 vboWidgetBooksCalSkeletons(wrapper);
1843
1844 // let the records be loaded for this new room filter
1845 vboWidgetBooksCalLoad(wrapper, 0);
1846
1847 // do not proceed
1848 return;
1849 }
1850 }
1851
1852 // toggle elements depending on the current view
1853 widget_instance.find('.vbo-widget-booskcal-newbook-wrap').hide().attr('data-ymd', '');
1854 if (current_ymd) {
1855 // back to the month-day view
1856 widget_instance.find('.vbo-widget-booskcal-calendar-table').hide();
1857 widget_instance.find('.vbo-widget-booskcal-mday-wrap').show();
1858 } else {
1859 // back to the monthly view
1860 widget_instance.find('.vbo-widget-booskcal-mday-wrap').hide();
1861 widget_instance.find('.vbo-widget-booskcal-mday-list').html('').attr('data-ymd', '').attr('data-offset', '0');
1862 widget_instance.find('.vbo-widget-booskcal-calendar-table').show();
1863 }
1864 }
1865
1866 /**
1867 * Display the new booking interface by hiding the other elements.
1868 */
1869 function vboWidgetBookCalsNewBooking(wrapper) {
1870 var widget_instance = jQuery('#' + wrapper);
1871 if (!widget_instance.length) {
1872 return false;
1873 }
1874
1875 var current_ymd = '';
1876 var current_rid = widget_instance.find('.vbo-booskcal-roomid').val();
1877 if (widget_instance.find('.vbo-widget-booskcal-mday-wrap').is(':visible')) {
1878 current_ymd = widget_instance.find('.vbo-widget-booskcal-mday-list').attr('data-ymd');
1879 }
1880
1881 // toggle elements
1882 widget_instance.find('.vbo-widget-booskcal-calendar-table').hide();
1883 widget_instance.find('.vbo-widget-booskcal-mday-wrap').hide();
1884 widget_instance.find('.vbo-widget-booskcal-newbook-wrap').show().attr('data-ymd', current_ymd).attr('data-roomid', current_rid);
1885
1886 if (current_ymd) {
1887 // set the check-in date
1888 var ymd_parts = current_ymd.split('-');
1889 widget_instance.find('.vbo-widget-bookscal-checkindt').datepicker('setDate', new Date(ymd_parts[0], parseInt(ymd_parts[1]) - 1, parseInt(ymd_parts[2], 0, 0, 0)));
1890 // trigger the onSelect function in datepicker
1891 jQuery('.ui-datepicker-current-day').click();
1892 }
1893 }
1894
1895 /**
1896 * Toggle the room-closure status when creating a new booking.
1897 */
1898 function vboWidgetBooksCalClosure(wrapper) {
1899 var widget_instance = jQuery('#' + wrapper);
1900 if (!widget_instance.length) {
1901 return false;
1902 }
1903
1904 if (widget_instance.find('input[name="closeroom"]').prop('checked')) {
1905 widget_instance.find('.vbo-param-container[data-noclosure="1"]').hide();
1906 } else {
1907 widget_instance.find('.vbo-param-container[data-noclosure="1"]').show();
1908 // check selected room
1909 var vbo_bookscal_roomfilt = widget_instance.find('.vbo-booskcal-roomid');
1910 if (vbo_bookscal_roomfilt.val()) {
1911 var vbo_bookscal_room_units = vbo_bookscal_roomfilt.find('option:selected').attr('data-units');
1912 if (vbo_bookscal_room_units < 2) {
1913 widget_instance.find('.vbo-widget-bookscal-units').closest('[data-noclosure="1"]').hide();
1914 }
1915 } else {
1916 widget_instance.find('.vbo-widget-bookscal-units').closest('[data-noclosure="1"]').hide();
1917 }
1918 // hide website rates if not available
1919 widget_instance.find('.vbo-param-container[data-unavailable="1"]').hide();
1920 }
1921 }
1922
1923 /**
1924 * Open the modal to assign an existing or a new customer to the booking.
1925 */
1926 function vboWidgetBooksCalAssignCustomer(wrapper) {
1927 var widget_instance = jQuery('#' + wrapper);
1928 if (!widget_instance.length) {
1929 return false;
1930 }
1931
1932 var modal_body = VBOCore.displayModal({
1933 extra_class: 'vbo-modal-rounded vbo-modal-tall',
1934 title: Joomla.JText._('VBFILLCUSTFIELDS'),
1935 footer_left: '<button type="button" class="btn" onclick="VBOCore.emitEvent(\'vbo-widget-booskcal-assigncustomer-dismiss\');">' + Joomla.JText._('VBANNULLA') + '</button>',
1936 footer_right: '<button type="button" class="btn btn-success" onclick="vboWidgetBooksCalSetCustomer(\'' + wrapper + '\');">' + Joomla.JText._('VBAPPLY') + '</button>',
1937 dismiss_event: 'vbo-widget-booskcal-assigncustomer-dismiss',
1938 loading_event: 'vbo-widget-booskcal-assigncustomer-loading',
1939 onDismiss: (e) => {
1940 if (!e || !e.detail) {
1941 // no event data received, maybe the modal was simply dismissed
1942 return;
1943 }
1944
1945 // parse data received within the dismiss event
1946 try {
1947 let customer_data = JSON.parse(e.detail);
1948
1949 // set values received
1950 widget_instance.find('.vbo-widget-bookscal-custid').val((customer_data['id'] || ''));
1951 widget_instance.find('.vbo-widget-bookscal-custmail').val((customer_data['email'] || ''));
1952 widget_instance.find('.vbo-widget-bookscal-custdata').val((customer_data['data'] || ''));
1953 widget_instance.find('.vbo-widget-bookscal-country').val((customer_data['country'] || ''));
1954 widget_instance.find('.vbo-widget-bookscal-state').val((customer_data['state'] || ''));
1955 widget_instance.find('.vbo-widget-bookscal-phone').val((customer_data['phone'] || ''));
1956
1957 let tot_names = customer_data['nominatives'] ? customer_data['nominatives'].length : 0;
1958 if (tot_names > 1) {
1959 widget_instance.find('.vbo-widget-bookscal-tfname').val(customer_data['nominatives'][0]);
1960 widget_instance.find('.vbo-widget-bookscal-tlname').val(customer_data['nominatives'][1]);
1961 }
1962 if (tot_names) {
1963 widget_instance.find('.vbo-assign-customer').find('span').text(customer_data['nominatives'].join(' '));
1964 } else {
1965 widget_instance.find('.vbo-assign-customer').find('span').text(Joomla.JText._('VBFILLCUSTFIELDS'));
1966 }
1967 } catch(e) {
1968 // log the error and abort
1969 console.error(e);
1970 return;
1971 }
1972 },
1973 });
1974
1975 // start loading
1976 VBOCore.emitEvent('vbo-widget-booskcal-assigncustomer-loading');
1977
1978 // the widget method to call
1979 var call_method = 'displayCustomerFilling';
1980
1981 // make a request to load the bookings calendar
1982 VBOCore.doAjax(
1983 "<?php echo $this->getExecWidgetAjaxUri(); ?>",
1984 {
1985 widget_id: "<?php echo $this->getIdentifier(); ?>",
1986 call: call_method,
1987 return: 1,
1988 wrapper: wrapper,
1989 tmpl: "component"
1990 },
1991 (response) => {
1992 // stop loading
1993 VBOCore.emitEvent('vbo-widget-booskcal-assigncustomer-loading');
1994 try {
1995 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
1996 if (!obj_res.hasOwnProperty(call_method)) {
1997 // dismiss modal and abort
1998 VBOCore.emitEvent('vbo-widget-booskcal-assigncustomer-dismiss');
1999 console.error('Unexpected JSON response', obj_res);
2000 return false;
2001 }
2002
2003 // append modal content
2004 modal_body.append(obj_res[call_method]['html']);
2005 } catch(err) {
2006 console.error('could not parse JSON response', err, response);
2007 }
2008 },
2009 (error) => {
2010 // stop loading
2011 VBOCore.emitEvent('vbo-widget-booskcal-assigncustomer-loading');
2012 // display error
2013 alert(error.responseText);
2014 // dismiss modal
2015 VBOCore.emitEvent('vbo-widget-booskcal-assigncustomer-dismiss');
2016 }
2017 );
2018 }
2019
2020 /**
2021 * Applies the provided customer information when creating a new booking.
2022 */
2023 function vboWidgetBooksCalSetCustomer(wrapper) {
2024 var widget_instance = jQuery('#' + wrapper);
2025 if (!widget_instance.length) {
2026 return false;
2027 }
2028
2029 var cfields_cont = '';
2030 var cfield_vals = {
2031 id: jQuery('#vbo-widget-bookscal-cfield-custid' + wrapper).val(),
2032 email: '',
2033 phone: '',
2034 nominatives: [],
2035 country: '',
2036 state: '',
2037 data: '',
2038 };
2039
2040 jQuery('.vbo-calendar-cfields-filler[data-wrapper="' + wrapper + '"]').find('.vbo-calendar-cfield-entry').each(function() {
2041 var cfield_entry = jQuery(this);
2042 var cfield_name = cfield_entry.find('label').text();
2043 var cfield_input = cfield_entry.find('span').find('input');
2044 var cfield_textarea = cfield_entry.find('span').find('textarea');
2045 var cfield_select = cfield_entry.find('span').find('select.vbo-calendar-cfield-country');
2046 var cfield_state = cfield_entry.find('span').find('select.vbo-calendar-cfield-state');
2047 var cfield_cont = '';
2048 if (cfield_input.length) {
2049 cfield_cont = cfield_input.val();
2050 if (cfield_input.attr('data-isemail') == '1' && cfield_cont && cfield_cont.length) {
2051 cfield_vals['email'] = cfield_cont;
2052 }
2053 if (cfield_input.attr('data-isphone') == '1') {
2054 cfield_vals['phone'] = cfield_cont;
2055 }
2056 if (cfield_input.attr('data-isnominative') == '1') {
2057 cfield_vals['nominatives'].push(cfield_cont);
2058 }
2059 } else if (cfield_textarea.length) {
2060 cfield_cont = cfield_textarea.val();
2061 } else if (cfield_select.length) {
2062 cfield_cont = cfield_select.val();
2063 if (cfield_cont && cfield_cont.length) {
2064 var country_code = jQuery('option:selected', cfield_select).attr('data-ccode');
2065 if (country_code && country_code.length) {
2066 cfield_vals['country'] = country_code;
2067 }
2068 }
2069 } else if (cfield_state.length) {
2070 cfield_cont = cfield_state.val();
2071 cfield_vals['state'] = cfield_cont;
2072 }
2073 if (cfield_cont && cfield_cont.length) {
2074 cfields_cont += cfield_name + ": " + cfield_cont + "\r\n";
2075 }
2076 });
2077
2078 if (!cfields_cont.length) {
2079 // empty information
2080 alert(Joomla.JText._('VBO_PLEASE_FILL_FIELDS'));
2081
2082 // do not proceed
2083 return false;
2084 }
2085
2086 // clean up last new lines
2087 cfields_cont = cfields_cont.replace(/\r\n+$/, "");
2088
2089 // set raw customer data string
2090 cfield_vals['data'] = cfields_cont;
2091
2092 // dimiss the modal by injecting the customer object information
2093 VBOCore.emitEvent('vbo-widget-booskcal-assigncustomer-dismiss', JSON.stringify(cfield_vals));
2094 }
2095
2096 /**
2097 * Reloads a list of states according to the given country.
2098 */
2099 function vboWidgetBooksCalReloadStates(country_3_code, wrapper) {
2100 var states_elem = jQuery('.vbo-calendar-cfields-filler[data-wrapper="' + wrapper + '"]').find('select.vbo-calendar-cfield-state');
2101
2102 if (!states_elem.length || !country_3_code || !country_3_code.length) {
2103 return;
2104 }
2105
2106 // unset the current states/provinces
2107 states_elem.html('');
2108
2109 // make a request to load the states/provinces of the selected country
2110 VBOCore.doAjax(
2111 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=states.load_from_country'); ?>",
2112 {
2113 country_3_code: country_3_code,
2114 tmpl: "component"
2115 },
2116 (response) => {
2117 try {
2118 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
2119 if (!obj_res) {
2120 console.error('Unexpected JSON response', obj_res);
2121 return false;
2122 }
2123
2124 // append empty value
2125 states_elem.append('<option value="">-----</option>');
2126
2127 for (var i = 0; i < obj_res.length; i++) {
2128 // append state
2129 states_elem.append('<option value="' + obj_res[i]['state_2_code'] + '">' + obj_res[i]['state_name'] + '</option>');
2130 }
2131 } catch(err) {
2132 console.error('could not parse JSON response', err, response);
2133 }
2134 },
2135 (error) => {
2136 console.error(error);
2137 }
2138 );
2139 }
2140
2141 /**
2142 * Triggers when a custom field of type country is changed.
2143 */
2144 function vboWidgetBooksCalChangeCountry(elem, wrapper) {
2145 var sel_country = jQuery(elem).find('option:selected');
2146 if (!sel_country.length) {
2147 return false;
2148 }
2149
2150 // trigger event for phone number
2151 jQuery('.vbo-calendar-cfield-phone').trigger('vboupdatephonenumber', sel_country.attr('data-c2code'));
2152
2153 // reload state/province
2154 vboWidgetBooksCalReloadStates(sel_country.attr('data-ccode'), wrapper);
2155 }
2156
2157 /**
2158 * Calculates the website rates according to the input values.
2159 */
2160 function vboWidgetBooksCalGetWebsiteRates(wrapper) {
2161 var widget_instance = jQuery('#' + wrapper);
2162 if (!widget_instance.length) {
2163 return false;
2164 }
2165
2166 // unset previously selected rates, if any
2167 widget_instance.find('.vbo-widget-bookscal-roomcost').val('');
2168 widget_instance.find('.vbo-widget-bookscal-idprice').val('');
2169 widget_instance.find('.vbo-widget-bookscal-custcost').val('').attr('readonly', false);
2170 widget_instance.find('.vbo-cal-wbrate-wrap').removeClass('vbo-cal-wbrate-wrap-selected');
2171
2172 // gather values
2173 var is_closing = widget_instance.find('input[name="closeroom"]').prop('checked');
2174 var room_id = widget_instance.find('.vbo-booskcal-roomid').val();
2175 var checkinfdate = widget_instance.find('.vbo-widget-bookscal-checkindt').val();
2176 var checkoutfdate = widget_instance.find('.vbo-widget-bookscal-checkoutdt').val();
2177 var adults = widget_instance.find('.vbo-widget-bookscal-adults').val();
2178 var children = widget_instance.find('.vbo-widget-bookscal-children').val();
2179 var units = widget_instance.find('.vbo-widget-bookscal-units').val();
2180
2181 if (is_closing || !room_id || !checkinfdate || !checkoutfdate) {
2182 // do not proceed
2183 widget_instance.find('.vbo-website-rates-row').hide().attr('data-unavailable', '1');
2184 return;
2185 }
2186
2187 // tax settings
2188 var vbo_tax_included = <?php echo $prices_vat_included; ?>;
2189
2190 // make the request
2191 VBOCore.doAjax(
2192 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=calc_rates'); ?>",
2193 {
2194 id_room: room_id,
2195 checkinfdate: checkinfdate,
2196 checkoutfdate: checkoutfdate,
2197 num_nights: 0,
2198 num_adults: adults,
2199 num_children: children,
2200 units: units,
2201 only_rates: 1,
2202 tmpl: "component"
2203 },
2204 (resp) => {
2205 try {
2206 obj_res = typeof resp === 'string' ? JSON.parse(resp) : resp;
2207 if (!obj_res[0].hasOwnProperty('idprice')) {
2208 widget_instance.find('.vbo-website-rates-row').hide().attr('data-unavailable', '1');
2209 console.log(resp);
2210 return false;
2211 }
2212
2213 // display the rates obtained
2214 var wrhtml = "";
2215 for (var i in obj_res) {
2216 if (!obj_res.hasOwnProperty(i)) {
2217 continue;
2218 }
2219 if (!vbo_tax_included && obj_res[i].hasOwnProperty('net') && obj_res[i].hasOwnProperty('fnet')) {
2220 obj_res[i]['tot'] = obj_res[i]['net'];
2221 obj_res[i]['ftot'] = obj_res[i]['fnet'];
2222 }
2223 wrhtml += "<div class=\"vbo-cal-wbrate-wrap\" onclick=\"vboWidgetBooksCalSelWebsiteRate(this, '" + wrapper + "');\">";
2224 wrhtml += " <div class=\"vbo-cal-wbrate-inner\">";
2225 wrhtml += " <span class=\"vbo-cal-wbrate-name\" data-idprice=\"" + obj_res[i]['idprice'] + "\">" + obj_res[i]['name'] + "</span>";
2226 wrhtml += " <span class=\"vbo-cal-wbrate-cost\" data-cost=\"" + obj_res[i]['tot'] + "\">" + obj_res[i]['ftot'] + "</span>";
2227 wrhtml += " </div>";
2228 wrhtml += "</div>";
2229 }
2230 widget_instance.find('.vbo-website-rates-cont').html(wrhtml);
2231 widget_instance.find('.vbo-website-rates-row').fadeIn().attr('data-unavailable', '0');
2232 } catch(err) {
2233 widget_instance.find('.vbo-website-rates-row').hide().attr('data-unavailable', '1');
2234 console.error("could not parse JSON response", resp);
2235 return false;
2236 }
2237 },
2238 (error) => {
2239 widget_instance.find('.vbo-website-rates-row').hide().attr('data-unavailable', '1');
2240 console.error("Error calculating the rates", error);
2241 }
2242 );
2243 }
2244
2245 /**
2246 * Attempts to display the tax rates drop down for the custom rate.
2247 */
2248 function vboWidgetBooksCalFocusTaxes(wrapper) {
2249 var widget_instance = jQuery('#' + wrapper);
2250 if (!widget_instance.length) {
2251 return false;
2252 }
2253
2254 var tax_rates_sel = widget_instance.find('.vbo-widget-bookscal-taxid');
2255 if (tax_rates_sel.length) {
2256 tax_rates_sel.show();
2257 }
2258 }
2259
2260 /**
2261 * Select a website rate plan.
2262 */
2263 function vboWidgetBooksCalSelWebsiteRate(elem, wrapper) {
2264 var widget_instance = jQuery('#' + wrapper);
2265 if (!widget_instance.length) {
2266 return false;
2267 }
2268
2269 var rate = jQuery(elem);
2270 var idprice = rate.find('.vbo-cal-wbrate-name').attr('data-idprice');
2271 var cost = rate.find('.vbo-cal-wbrate-cost').attr('data-cost');
2272 var prev_idprice = widget_instance.find('.vbo-widget-bookscal-idprice').val();
2273
2274 // reset all selected classes
2275 widget_instance.find('.vbo-cal-wbrate-wrap').removeClass('vbo-cal-wbrate-wrap-selected');
2276 if (prev_idprice && prev_idprice == idprice) {
2277 // rate plan has been de-selected
2278 widget_instance.find('.vbo-widget-bookscal-idprice').val("");
2279 widget_instance.find('.vbo-widget-bookscal-roomcost').val("");
2280 widget_instance.find('.vbo-widget-bookscal-custcost').attr('readonly', false);
2281 } else {
2282 // rate plan has been selected
2283 rate.addClass('vbo-cal-wbrate-wrap-selected');
2284 widget_instance.find('.vbo-widget-bookscal-idprice').val(idprice);
2285 widget_instance.find('.vbo-widget-bookscal-roomcost').val(cost);
2286 widget_instance.find('.vbo-widget-bookscal-custcost').attr('readonly', true);
2287 }
2288 }
2289
2290 /**
2291 * Save a new booking.
2292 */
2293 function vboWidgetBooksCalSaveBooking(wrapper) {
2294 var widget_instance = jQuery('#' + wrapper);
2295 if (!widget_instance.length) {
2296 return false;
2297 }
2298
2299 // gather all the necessary information
2300 var room_id = widget_instance.find('.vbo-booskcal-roomid').val();
2301 var checkin = widget_instance.find('.vbo-widget-bookscal-checkindt').val();
2302 var checkout = widget_instance.find('.vbo-widget-bookscal-checkoutdt').val();
2303 var closure = widget_instance.find('input[name="closeroom"]').prop('checked') ? 1 : 0;
2304 var units = widget_instance.find('.vbo-widget-bookscal-units').val();
2305 var adults = widget_instance.find('.vbo-widget-bookscal-adults').val();
2306 var children = widget_instance.find('.vbo-widget-bookscal-children').val();
2307 var cust_id = widget_instance.find('.vbo-widget-bookscal-custid').val();
2308 var cust_email = widget_instance.find('.vbo-widget-bookscal-custmail').val();
2309 var cust_data = widget_instance.find('.vbo-widget-bookscal-custdata').val();
2310 var cust_country = widget_instance.find('.vbo-widget-bookscal-country').val();
2311 var cust_state = widget_instance.find('.vbo-widget-bookscal-state').val();
2312 var cust_phone = widget_instance.find('.vbo-widget-bookscal-phone').val();
2313 var cust_tfname = widget_instance.find('.vbo-widget-bookscal-tfname').val();
2314 var cust_tlname = widget_instance.find('.vbo-widget-bookscal-tlname').val();
2315 var roomcost = widget_instance.find('.vbo-widget-bookscal-roomcost').val();
2316 var idprice = widget_instance.find('.vbo-widget-bookscal-idprice').val();
2317 var cust_roomcost = widget_instance.find('.vbo-widget-bookscal-custcost').val();
2318 var taxid = widget_instance.find('.vbo-widget-bookscal-taxid').val();
2319
2320 if (!room_id || !checkin || !checkout) {
2321 // missing information
2322 alert(Joomla.JText._('VBO_PLEASE_FILL_FIELDS'));
2323
2324 // abort
2325 return false;
2326 }
2327
2328 // check if suggesting to mark the rooms as closed is necessary
2329 var set_units_closed = 0;
2330 var current_room_units = 0;
2331 var vbo_bookscal_roomfilt = widget_instance.find('.vbo-booskcal-roomid');
2332 if (vbo_bookscal_roomfilt.val()) {
2333 var current_room_units = vbo_bookscal_roomfilt.find('option:selected').attr('data-units');
2334 }
2335 if (!cust_id && !cust_data && !closure && current_room_units > 1) {
2336 // no closure, no customer email, multiple units, ask if the units should be marked as closed
2337 if (confirm(Joomla.JText._('VBO_MARK_UNITS_CLOSED'))) {
2338 set_units_closed = 1;
2339 cust_data = Joomla.JText._('VBDBTEXTROOMCLOSED');
2340 }
2341 }
2342
2343 if (!set_units_closed && !cust_id && !cust_data && !closure) {
2344 // missing information
2345 alert(Joomla.JText._('VBO_PLEASE_FILL_FIELDS'));
2346
2347 // abort
2348 return false;
2349 }
2350
2351 // loading content
2352 var loading_content = '<div class="vbo-modal-overlay-content-backdrop"><div class="vbo-modal-overlay-content-backdrop-body">' + VBOCore.options.default_loading_body + '</div></div>';
2353
2354 // show loading
2355 widget_instance.find('.vbo-widget-booskcal-newbook-cont').prepend(loading_content);
2356
2357 // the widget method to call
2358 var call_method = 'saveBooking';
2359
2360 // make a request to save the booking
2361 VBOCore.doAjax(
2362 "<?php echo $this->getExecWidgetAjaxUri(); ?>",
2363 {
2364 widget_id: "<?php echo $this->getIdentifier(); ?>",
2365 call: call_method,
2366 return: 1,
2367 room_id: room_id,
2368 checkin: checkin,
2369 checkout: checkout,
2370 closure: closure,
2371 units_closed: set_units_closed,
2372 units: units,
2373 adults: adults,
2374 children: children,
2375 cust_id: cust_id,
2376 cust_email: cust_email,
2377 cust_data: cust_data,
2378 cust_country: cust_country,
2379 cust_state: cust_state,
2380 cust_phone: cust_phone,
2381 cust_tfname: cust_tfname,
2382 cust_tlname: cust_tlname,
2383 roomcost: roomcost,
2384 idprice: idprice,
2385 cust_roomcost: cust_roomcost,
2386 taxid: taxid,
2387 wrapper: wrapper,
2388 tmpl: "component"
2389 },
2390 (response) => {
2391 // stop loading
2392 widget_instance.find('.vbo-modal-overlay-content-backdrop').remove();
2393 try {
2394 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
2395 if (!obj_res.hasOwnProperty(call_method)) {
2396 console.error('Unexpected JSON response', obj_res);
2397 return false;
2398 }
2399
2400 // show last booking created
2401 widget_instance.find('.vbo-widget-booskcal-newbook-start').hide();
2402 widget_instance.find('.vbo-widget-booskcal-newbook-id').attr('data-bookingid', obj_res[call_method]['new_booking_id']).show().find('span').text(Joomla.JText._('VBDASHUPRESONE') + ': ' + obj_res[call_method]['new_booking_id']);
2403
2404 // check if should suggest to run VCM
2405 if (obj_res[call_method]['vcm_action']) {
2406 widget_instance.append('<p class="info" onclick="jQuery(this).remove();">' + obj_res[call_method]['vcm_action'] + '</p>');
2407 }
2408
2409 // register the last booking ID created
2410 vbo_widget_books_cal_last_new_bid = obj_res[call_method]['new_booking_id'];
2411
2412 // reload bookings calendar
2413
2414 // show loading skeletons
2415 vboWidgetBooksCalSkeletons(wrapper);
2416
2417 // let the records be loaded for this new room filter
2418 vboWidgetBooksCalLoad(wrapper, 0);
2419 } catch(err) {
2420 console.error('could not parse JSON response', err, response);
2421 }
2422 },
2423 (error) => {
2424 // stop loading
2425 widget_instance.find('.vbo-modal-overlay-content-backdrop').remove();
2426 // display error
2427 console.error(error);
2428 alert(error.responseText);
2429 }
2430 );
2431 }
2432
2433 /**
2434 * Triggers when the newly created booking button is clicked.
2435 */
2436 function vboWidgetBookCalsOpenNewBooking(wrapper) {
2437 var widget_instance = jQuery('#' + wrapper);
2438 if (!widget_instance.length) {
2439 return false;
2440 }
2441
2442 var bid = widget_instance.find('.vbo-widget-booskcal-newbook-id').attr('data-bookingid');
2443 // open the booking
2444 vboWidgetBooksCalOpenBooking(bid);
2445
2446 // switch back to the regular button to create a new booking
2447 widget_instance.find('.vbo-widget-booskcal-newbook-id').hide();
2448 widget_instance.find('.vbo-widget-booskcal-newbook-start').show();
2449 }
2450
2451 /**
2452 * Toggles the cancelled reservations.
2453 */
2454 function vboWidgetBooksCalCancToggle(wrapper) {
2455 var widget_instance = jQuery('#' + wrapper);
2456 if (!widget_instance.length) {
2457 return false;
2458 }
2459
2460 widget_instance.find('[data-type="cancelled"]').toggle();
2461 }
2462
2463 /**
2464 * Triggers when the multitask panel opens.
2465 */
2466 function vboWidgetBooksCalMultitaskOpen(wrapper) {
2467 var widget_instance = jQuery('#' + wrapper);
2468 if (!widget_instance.length) {
2469 return false;
2470 }
2471
2472 // check if a booking ID was set for this page
2473 var page_bid = widget_instance.attr('data-pagebid');
2474 if (!page_bid || page_bid < 1) {
2475 return false;
2476 }
2477
2478 // show loading skeletons
2479 vboWidgetBooksCalSkeletons(wrapper);
2480
2481 // load data by injecting the current booking ID
2482 vboWidgetBooksCalLoad(wrapper, 0, page_bid);
2483 }
2484
2485 </script>
2486 <?php
2487 }
2488 ?>
2489
2490 <script type="text/javascript">
2491
2492 // store the lastly created booking ID
2493 var vbo_widget_books_cal_last_new_bid = null;
2494
2495 // store widget options, if any
2496 var vbo_widget_books_cal_options_oo = <?php echo json_encode($this->getOptions()); ?>;
2497
2498 jQuery(function() {
2499
2500 // when document is ready, load bookings calendar for this widget's instance
2501 vboWidgetBooksCalLoad('<?php echo $wrapper_id; ?>', 0, '<?php echo $modal_load_bid; ?>');
2502
2503 // convert the select to a Select2 element
2504 if (typeof jQuery.fn.select2 !== 'undefined' && <?php echo $use_nice_select; ?>) {
2505 jQuery('#<?php echo $wrapper_id; ?>').find('select.vbo-booskcal-roomid').select2({
2506 width: "100%",
2507 placeholder: "<?php echo htmlspecialchars(VikBooking::strTrimLiteral(JText::translate('VBOREPORTSROOMFILT'))); ?>",
2508 allowClear: true
2509 });
2510 }
2511
2512 // subscribe to the multitask-panel-open event
2513 document.addEventListener(VBOCore.multitask_open_event, function() {
2514 vboWidgetBooksCalMultitaskOpen('<?php echo $wrapper_id; ?>');
2515 });
2516
2517 // subscribe to the multitask-panel-close event to emit the event for the lastly created booking ID
2518 document.addEventListener(VBOCore.multitask_close_event, function() {
2519 if (vbo_widget_books_cal_last_new_bid) {
2520 // emit the event with data for anyone who is listening to it
2521 VBOCore.emitEvent('vbo_new_booking_created', {
2522 bid: vbo_widget_books_cal_last_new_bid
2523 });
2524 }
2525 });
2526
2527 <?php
2528 if ($js_modal_id) {
2529 // widget can be dismissed through the modal
2530 ?>
2531 // subscribe to the modal-dismissed event to emit the event for the lastly created booking ID
2532 document.addEventListener(VBOCore.widget_modal_dismissed + '<?php echo $js_modal_id; ?>', function() {
2533 if (vbo_widget_books_cal_last_new_bid) {
2534 // emit the event with data for anyone who is listening to it
2535 VBOCore.emitEvent('vbo_new_booking_created', {
2536 bid: vbo_widget_books_cal_last_new_bid
2537 });
2538 }
2539 });
2540 <?php
2541 }
2542 ?>
2543
2544 });
2545
2546 </script>
2547
2548 <?php
2549 }
2550
2551 /**
2552 * Helper method to load the booking cancellations for the given room and date.
2553 *
2554 * @param int $room_id the Vik Booking room ID.
2555 * @param string $ymd the current calendar date in Y-m-d format.
2556 *
2557 * @return array list of involved booking cancellations.
2558 *
2559 * @since 1.16.1 (J) - 1.6.1 (WP)
2560 */
2561 protected function loadCancellations($room_id, $ymd)
2562 {
2563 $dbo = JFactory::getDbo();
2564
2565 if (empty($room_id) || empty($ymd)) {
2566 return [];
2567 }
2568
2569 $stay_date_info = getdate(strtotime($ymd));
2570 $lim_ts_to = mktime(23, 59, 59, $stay_date_info['mon'], $stay_date_info['mday'], $stay_date_info['year']);
2571
2572 $q = $dbo->getQuery(true);
2573
2574 $q->select($dbo->qn('o') . '.*');
2575 $q->from($dbo->qn('#__vikbooking_orders', 'o'));
2576 $q->leftjoin($dbo->qn('#__vikbooking_ordersrooms', 'or') . ' ON ' . $dbo->qn('o.id') . ' = ' . $dbo->qn('or.idorder'));
2577 $q->where($dbo->qn('o.status') . ' = ' . $dbo->q('cancelled'));
2578 $q->where($dbo->qn('o.checkin') . ' <= ' . $lim_ts_to);
2579 $q->where($dbo->qn('o.checkout') . ' > ' . $lim_ts_to);
2580 $q->where($dbo->qn('or.idroom') . ' = ' . (int)$room_id);
2581 $q->group($dbo->qn('o.id'));
2582 $q->order($dbo->qn('o.id') . ' ASC');
2583
2584 $dbo->setQuery($q);
2585 $cancellations = $dbo->loadAssocList();
2586
2587 // join the customer information with a separate query as this is faster than joining two more tables at once
2588 foreach ($cancellations as &$canc_book) {
2589 $q = $dbo->getQuery(true);
2590
2591 $q->select($dbo->qn('co.idcustomer'));
2592 $q->select('CONCAT_WS(" ", ' . $dbo->qn('c.first_name') . ', ' . $dbo->qn('c.last_name') . ') AS ' . $dbo->qn('customer_fullname'));
2593 $q->select($dbo->qn('c.country', 'customer_country'));
2594 $q->select($dbo->qn('c.pic'));
2595 $q->from($dbo->qn('#__vikbooking_customers_orders', 'co'));
2596 $q->leftjoin($dbo->qn('#__vikbooking_customers', 'c') . ' ON ' . $dbo->qn('c.id') . ' = ' . $dbo->qn('co.idcustomer'));
2597 $q->where($dbo->qn('co.idorder') . ' = ' . (int)$canc_book['id']);
2598
2599 $dbo->setQuery($q);
2600 $customer_data = $dbo->loadAssoc();
2601
2602 if ($customer_data) {
2603 // merge properties
2604 $canc_book = array_merge($canc_book, $customer_data);
2605 }
2606 }
2607
2608 // unset last reference
2609 unset($canc_book);
2610
2611 // return the list of cancellation bookings
2612 return $cancellations;
2613 }
2614
2615 /**
2616 * When Multitask Data Options are injected with an overbooking flag, this
2617 * method loads the overbooking details and sets the proper option values.
2618 *
2619 * @param int $bid the overbooking reservation ID.
2620 *
2621 * @return void
2622 *
2623 * @since 1.16.8 (J) - 1.6.8 (WP)
2624 */
2625 protected function prepareOverbookingOptions($bid)
2626 {
2627 // get the booking details
2628 $booking_info = VikBooking::getBookingInfoFromID($bid);
2629 if (!$booking_info || strcasecmp((string) $booking_info['type'], 'overbooking')) {
2630 return;
2631 }
2632
2633 $booking_checkin_dt = date('Y-m-d', $booking_info['checkin']);
2634 $booking_checkout_dt = date('Y-m-d', $booking_info['checkout']);
2635
2636 // access the availability helper object
2637 $av_helper = VikBooking::getAvailabilityInstance()
2638 ->setStayDates($booking_checkin_dt, $booking_checkout_dt);
2639
2640 // get stay date timestamps
2641 list($checkin_ts, $checkout_ts) = $av_helper->getStayDates(true);
2642
2643 // count length of stay and nights involved
2644 $tot_nights = $av_helper->countNightsOfStay();
2645 $groupdays = VikBooking::getGroupDays($checkin_ts, $checkout_ts, $tot_nights);
2646
2647 // get all rooms involved
2648 $booking_rooms = VikBooking::loadOrdersRoomsData($booking_info['id']);
2649 $room_ids = array_unique(array_column($booking_rooms, 'idroom'));
2650
2651 // load all the occupied records for the involved rooms
2652 $busy_records = VikBooking::loadBusyRecords($room_ids, $checkin_ts, strtotime('+1 day', $checkout_ts));
2653
2654 // scan all rooms to find the first date in overbooking state
2655 foreach ($booking_rooms as $booking_room) {
2656 $room_id = $booking_room['idroom'];
2657 $room_data = VikBooking::getRoomInfo($room_id, ['units']);
2658 if (empty($room_data['units']) || !isset($busy_records[$room_id])) {
2659 // room is no longer on db or has got no occupied records
2660 continue;
2661 }
2662
2663 // scan all nights involved
2664 foreach ($groupdays as $gday) {
2665 // start counter
2666 $bfound = 0;
2667
2668 // scan all the occupied records
2669 foreach ($busy_records[$room_id] as $bu) {
2670 $busy_info_in = getdate($bu['checkin']);
2671 $busy_info_out = getdate($bu['checkout']);
2672 $busy_in_ts = mktime(0, 0, 0, $busy_info_in['mon'], $busy_info_in['mday'], $busy_info_in['year']);
2673 $busy_out_ts = mktime(0, 0, 0, $busy_info_out['mon'], $busy_info_out['mday'], $busy_info_out['year']);
2674 if ($gday >= $busy_in_ts && $gday <= $busy_out_ts) {
2675 // room is occupied
2676 $bfound++;
2677 }
2678
2679 // check if the room is overbooked
2680 if ($bfound > $room_data['units']) {
2681 // overbooking date found, inject data and abort
2682 $overbooking_dt = date('Y-m-d', $gday);
2683
2684 // set "offset" and "day" widget options
2685 $this->setOption('offset', $overbooking_dt);
2686 $this->setOption('day', $overbooking_dt);
2687
2688 // set room ID widget option
2689 $this->setOption('id_room', $room_id);
2690
2691 // abort for information found
2692 return;
2693 }
2694 }
2695 }
2696 }
2697
2698 // fallback onto setting the first values available
2699
2700 // set "offset" and "day" widget options
2701 $this->setOption('offset', $booking_checkin_dt);
2702 $this->setOption('day', $booking_checkin_dt);
2703
2704 $booking_rooms = VikBooking::loadOrdersRoomsData($booking_info['id']);
2705 foreach ($booking_rooms as $booking_room) {
2706 // set room ID widget option
2707 $this->setOption('id_room', $booking_room['idroom']);
2708
2709 break;
2710 }
2711 }
2712 }
2713