PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.7.1
VikBooking Hotel Booking Engine & PMS v1.7.1
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.7.1, at admin/helpers/widgets/bookings_calendar.php

3,439 lines 129.1 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 $mng_rates_restr = VikRequest::getInt('mng_rates_restr', 0, 'request');
69 $bid = VikRequest::getInt('bid', 0, 'request');
70 $bid = !empty($date_dir) ? 0 : $bid;
71
72 // the booking ID can be passed in case of multitask data for this page
73 if (!empty($bid)) {
74 // load the availability of the month when booking starts
75 $booking_info = VikBooking::getBookingInfoFromID($bid);
76 if ($booking_info) {
77 // force the offset to use as start date
78 $offset = date('Y-m-d', $booking_info['checkin']);
79 }
80 }
81
82 // calculate date timestamps interval
83 $now_info = getdate(strtotime($offset));
84 if ($date_dir > 0) {
85 // next month from current offset
86 $from_ts = mktime(0, 0, 0, ($now_info['mon'] + 1), 1, $now_info['year']);
87 $to_ts = mktime(23, 59, 59, ($now_info['mon'] + 1), date('t', $from_ts), $now_info['year']);
88 } elseif ($date_dir < 0) {
89 // prev month from current offset
90 $from_ts = mktime(0, 0, 0, ($now_info['mon'] - 1), 1, $now_info['year']);
91 $to_ts = mktime(23, 59, 59, ($now_info['mon'] - 1), date('t', $from_ts), $now_info['year']);
92 } else {
93 // no navigation, use current offset
94 $from_ts = mktime(0, 0, 0, $now_info['mon'], 1, $now_info['year']);
95 $to_ts = mktime(23, 59, 59, $now_info['mon'], date('t', $now_info[0]), $now_info['year']);
96 }
97
98 // build week days list according to settings
99 $firstwday = (int)VikBooking::getFirstWeekDay();
100 $days_labels = array(
101 JText::translate('VBSUN'),
102 JText::translate('VBMON'),
103 JText::translate('VBTUE'),
104 JText::translate('VBWED'),
105 JText::translate('VBTHU'),
106 JText::translate('VBFRI'),
107 JText::translate('VBSAT'),
108 );
109 $days_indexes = [];
110 for ($i = 0; $i < 7; $i++) {
111 $days_indexes[$i] = (6 - ($firstwday - $i) + 1) % 7;
112 }
113
114 // currency symbol
115 $currencysymb = VikBooking::getCurrencySymb();
116
117 // start looping from the first day of the current month
118 $info_arr = getdate($from_ts);
119
120 // build period name
121 $period_date = VikBooking::sayMonth($info_arr['mon']) . ' ' . $info_arr['year'];
122
123 // invoke availability helper class
124 $av_helper = VikBooking::getAvailabilityInstance();
125
126 // get all rooms and tax rates
127 $all_rooms = $av_helper->loadRooms();
128 $tax_rates = $av_helper->getTaxRates();
129
130 // count maximum units available depending on filter
131 $tot_rooms = 1;
132 if (!empty($room_id) && isset($all_rooms[$room_id])) {
133 // use the units of the filtered room
134 $max_units = $all_rooms[$room_id]['units'];
135 } else {
136 // sum all room units
137 $max_units = 0;
138 $tot_rooms = count($all_rooms);
139 foreach ($all_rooms as $rid => $room) {
140 $max_units += $room['units'];
141 }
142 }
143
144 // build "search name"
145 if (!empty($room_id) && isset($all_rooms[$room_id])) {
146 $search_name = $all_rooms[$room_id]['name'];
147 } else {
148 $search_name = preg_replace("/[^A-Za-z0-9 ]/", '', JText::translate('VBOSTATSALLROOMS'));
149 }
150
151 // load busy records
152 $room_filter = !empty($room_id) ? array($room_id) : array();
153 $busy_records = VikBooking::loadBusyRecords($room_filter, $from_ts, $to_ts);
154
155 // load festivities or room-day notes
156 $festivities = [];
157 $rday_notes = [];
158 if (!empty($room_id) && isset($all_rooms[$room_id])) {
159 // load room-day notes for the given room id
160 $rday_notes = VikBooking::getCriticalDatesInstance()->loadRoomDayNotes(date('Y-m-d', $from_ts), date('Y-m-d', $to_ts), $room_id);
161 } else {
162 // load festivities when no specific room filter set
163 $fests = VikBooking::getFestivitiesInstance();
164 if ($fests->shouldCheckFestivities()) {
165 $fests->storeNextFestivities();
166 }
167 $festivities = $fests->loadFestDates(date('Y-m-d', $from_ts), date('Y-m-d', $to_ts));
168 }
169
170 // date format
171 $dtpicker_df = $this->getDateFormat('jui');
172
173 // start output buffering
174 ob_start();
175
176 // generate calendar
177 $d_count = 0;
178 $mon_lim = $info_arr['mon'];
179 $next_offset = date('Y-m-d', $from_ts);
180
181 ?>
182 <div class="vbo-widget-booskcal-mday-wrap" style="display: none;">
183 <div class="vbo-widget-booskcal-mday-head">
184 <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>
185 <span class="vbo-widget-booskcal-mday-name"></span>
186 </div>
187 <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>
188 <div class="vbo-widget-booskcal-mday-pricing" style="display: none;">
189 <div class="vbo-widget-booskcal-mday-pricing-title">
190 <span><?php echo JText::translate('VBO_RATES_AND_RESTR'); ?></span>
191 </div>
192 <div class="vbo-widget-booskcal-mday-hidden-helper" style="display: none;">
193 <div class="vbo-widget-booskcal-mday-hidden-helper-togglebtn vbo-toggle-small">
194 <?php echo $this->vbo_app->printYesNoButtons('updotas', JText::translate('VBYES'), JText::translate('VBNO'), 1, 1, 0, '', ['blue']); ?>
195 </div>
196 </div>
197 <div class="vbo-widget-booskcal-mday-pricing-data">
198 <div class="vbo-widget-booskcal-mday-pricing-data-cost">
199 <span class="vbo-widget-booskcal-mday-pricing-currency"><?php echo VikBooking::getCurrencySymb(); ?></span>
200 <span class="vbo-widget-booskcal-mday-pricing-cost"></span>
201 </div>
202 <div class="vbo-widget-booskcal-mday-pricing-data-restr">
203 <div class="vbo-widget-booskcal-mday-pricing-data-minlos">
204 <span><?php echo JText::translate('VBOMINIMUMSTAY'); ?></span>
205 <span class="vbo-widget-booskcal-mday-pricing-minlos"></span>
206 </div>
207 <div class="vbo-widget-booskcal-mday-pricing-data-ctad">
208 <span class="vbo-widget-booskcal-mday-pricing-ctad"></span>
209 </div>
210 </div>
211 </div>
212 </div>
213 </div>
214
215 <div class="vbo-widget-booskcal-newbook-wrap" data-ymd="" data-roomid="" style="display: none;">
216 <div class="vbo-widget-booskcal-newbook-head">
217 <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>
218 <span class="vbo-widget-booskcal-newbook-name"><?php echo JText::translate('VBQUICKBOOK'); ?></span>
219 </div>
220 <div class="vbo-widget-booskcal-newbook-cont">
221 <div class="vbo-admin-container vbo-admin-container-full vbo-admin-container-compact">
222 <div class="vbo-params-wrap">
223 <div class="vbo-params-container">
224
225 <div class="vbo-param-container">
226 <div class="vbo-param-label"><?php echo JText::translate('VBPICKUPAT'); ?></div>
227 <div class="vbo-param-setting">
228 <div class="vbo-field-calendar">
229 <div class="input-append">
230 <input type="text" class="vbo-widget-bookscal-checkindt" value="" autocomplete="off" />
231 <button type="button" class="btn btn-secondary vbo-widget-bookscal-checkindt-trigger"><?php VikBookingIcons::e('calendar'); ?></button>
232 </div>
233 </div>
234 </div>
235 </div>
236
237 <div class="vbo-param-container">
238 <div class="vbo-param-label"><?php echo JText::translate('VBRELEASEAT'); ?></div>
239 <div class="vbo-param-setting">
240 <div class="vbo-field-calendar">
241 <div class="input-append">
242 <input type="text" class="vbo-widget-bookscal-checkoutdt" value="" autocomplete="off" />
243 <button type="button" class="btn btn-secondary vbo-widget-bookscal-checkoutdt-trigger"><?php VikBookingIcons::e('calendar'); ?></button>
244 </div>
245 </div>
246 </div>
247 </div>
248
249 <div class="vbo-param-container vbo-toggle-small">
250 <div class="vbo-param-label"><?php echo JText::translate('VBSUBMCLOSEROOM'); ?></div>
251 <div class="vbo-param-setting">
252 <?php echo $this->vbo_app->printYesNoButtons('closeroom', JText::translate('VBYES'), JText::translate('VBNO'), 0, 1, 0, "vboWidgetBooksCalClosure('{$wrapper}');"); ?>
253 </div>
254 </div>
255
256 <div class="vbo-param-container" data-noclosure="1">
257 <div class="vbo-param-label"><?php echo JText::translate('VBOCUSTOMER'); ?></div>
258 <div class="vbo-param-setting">
259 <span class="vbo-assign-customer" onclick="vboWidgetBooksCalAssignCustomer('<?php echo $wrapper; ?>');">
260 <?php VikBookingIcons::e('user-circle'); ?>
261 <span><?php echo JText::translate('VBFILLCUSTFIELDS'); ?></span>
262 </span>
263 <input type="hidden" value="" class="vbo-widget-bookscal-custid" />
264 <input type="hidden" value="" class="vbo-widget-bookscal-custmail" />
265 <input type="hidden" value="" class="vbo-widget-bookscal-custdata" />
266 <input type="hidden" value="" class="vbo-widget-bookscal-country" />
267 <input type="hidden" value="" class="vbo-widget-bookscal-state" />
268 <input type="hidden" value="" class="vbo-widget-bookscal-phone" />
269 <input type="hidden" value="" class="vbo-widget-bookscal-tfname" />
270 <input type="hidden" value="" class="vbo-widget-bookscal-tlname" />
271 <input type="hidden" value="" class="vbo-widget-bookscal-roomcost" />
272 <input type="hidden" value="" class="vbo-widget-bookscal-idprice" />
273 </div>
274 </div>
275
276 <div class="vbo-param-container" data-noclosure="1">
277 <div class="vbo-param-label"><?php echo JText::translate('VBPVIEWROOMSEVEN'); ?></div>
278 <div class="vbo-param-setting">
279 <input type="number" class="vbo-input-number-small vbo-widget-bookscal-units" value="1" min="1" max="99" onchange="vboWidgetBooksCalGetWebsiteRates('<?php echo $wrapper; ?>');" />
280 </div>
281 </div>
282
283 <div class="vbo-param-container" data-noclosure="1">
284 <div class="vbo-param-label"><?php echo JText::translate('VBPVIEWORDERSPEOPLE'); ?></div>
285 <div class="vbo-param-setting">
286 <span class="vbo-quickres-aduchi-wrap">
287 <span class="vbo-quickres-aduchi-inlbl"><?php echo JText::translate('VBEDITORDERADULTS'); ?></span>
288 <input type="number" class="vbo-input-number-small vbo-widget-bookscal-adults" value="2" min="0" max="99" onchange="vboWidgetBooksCalGetWebsiteRates('<?php echo $wrapper; ?>');" />
289 </span>
290 <span class="vbo-quickres-aduchi-wrap">
291 <span class="vbo-quickres-aduchi-inlbl"><?php echo JText::translate('VBEDITORDERCHILDREN'); ?></span>
292 <input type="number" class="vbo-input-number-small vbo-widget-bookscal-children" value="0" min="0" max="99" />
293 </span>
294 </div>
295 </div>
296
297 <div class="vbo-param-container vbo-website-rates-row" data-noclosure="1" data-unavailable="1" style="display: none;">
298 <div class="vbo-param-label"><?php echo JText::translate('VBOWEBSITERATES'); ?></div>
299 <div class="vbo-param-setting">
300 <div class="vbo-website-rates-cont"></div>
301 </div>
302 </div>
303
304 <div class="vbo-param-container vbo-row-custcost" data-noclosure="1">
305 <div class="vbo-param-label"><?php echo JText::translate('VBOROOMCUSTRATEPLANADD'); ?></div>
306 <div class="vbo-param-setting">
307 <div class="vbo-calendar-costs-wrapper">
308 <?php echo $currencysymb; ?> <input type="number" class="vbo-widget-bookscal-custcost" value="" step="any" min="0" onfocus="vboWidgetBooksCalFocusTaxes('<?php echo $wrapper; ?>');" />
309 <?php
310 if ($tax_rates) {
311 ?>
312 <select class="vbo-widget-bookscal-taxid" style="display: none;">
313 <option value=""><?php echo JText::translate('VBNEWOPTFOUR'); ?></option>
314 <?php
315 foreach ($tax_rates as $kiv => $iv) {
316 ?>
317 <option value="<?php echo $iv['id']; ?>"<?php echo $kiv < 1 ? ' selected="selected"' : ''; ?>><?php echo empty($iv['name']) ? "{$iv['aliq']}%" : "{$iv['name']} - {$iv['aliq']}%"; ?></option>
318 <?php
319 }
320 ?>
321 </select>
322 <?php
323 }
324 ?>
325 </div>
326 </div>
327 </div>
328
329 <div class="vbo-param-container vbo-param-confirm-btn">
330 <div class="vbo-param-label"></div>
331 <div class="vbo-param-setting">
332 <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>
333 </div>
334 </div>
335
336 </div>
337 </div>
338 </div>
339 </div>
340 </div>
341
342 <div class="vbo-widget-booskcal-calendar-table-wrap">
343 <?php
344 if (!empty($room_id) && isset($all_rooms[$room_id])) {
345 // allow to manage rates and restrictions
346 ?>
347 <div class="vbo-widget-bookscal-mngrates-toggle vbo-toggle-small">
348 <label for="vbo-wbookscal-mng-rates-restr-on"><?php echo JText::translate('VBO_RATES_AND_RESTR'); ?></label>
349 <?php echo $this->vbo_app->printYesNoButtons('vbo-wbookscal-mng-rates-restr', JText::translate('VBYES'), JText::translate('VBNO'), $mng_rates_restr, 1, 0, "vboWidgetBooksCalManageRatesRestr('{$wrapper}');", ['orange']); ?>
350 </div>
351 <?php
352 }
353 ?>
354
355 <table class="vbadmincaltable vbo-widget-booskcal-calendar-table">
356 <tbody>
357 <tr class="vbadmincaltrmdays">
358 <?php
359 // display week days in the proper order
360 for ($i = 0; $i < 7; $i++) {
361 $d_ind = ($i + $firstwday) < 7 ? ($i + $firstwday) : ($i + $firstwday - 7);
362 ?>
363 <td class="vbo-widget-booskcal-cell-wday"><?php echo $days_labels[$d_ind]; ?></td>
364 <?php
365 }
366 ?>
367 </tr>
368 <tr>
369 <?php
370 // display empty cells until the first week-day of the month
371 for ($i = 0, $n = $days_indexes[$info_arr['wday']]; $i < $n; $i++, $d_count++) {
372 ?>
373 <td class="vbo-widget-booskcal-cell-mday vbo-widget-booskcal-cell-empty">&nbsp;</td>
374 <?php
375 }
376 // display month days
377 while ($info_arr['mon'] == $mon_lim) {
378 if ($d_count > 6) {
379 $d_count = 0;
380 // close current row and open a new one
381 echo "\n</tr>\n<tr>\n";
382 }
383 // count units booked on this day
384 $tot_units_booked = 0;
385 $cell_classes = [];
386 $cell_bids = [];
387 foreach ($busy_records as $rid => $rbusy) {
388 foreach ($rbusy as $b) {
389 $tmpone = getdate($b['checkin']);
390 $ritts = mktime(0, 0, 0, $tmpone['mon'], $tmpone['mday'], $tmpone['year']);
391 $tmptwo = getdate($b['checkout']);
392 $conts = mktime(0, 0, 0, $tmptwo['mon'], $tmptwo['mday'], $tmptwo['year']);
393 if ($info_arr[0] >= $ritts && $info_arr[0] < $conts) {
394 // increase units booked
395 $tot_units_booked++;
396 if ($tot_rooms === 1) {
397 if (!empty($b['closure'])) {
398 // hightlight that this was a closure
399 $cell_classes[] = 'busy-closure';
400 } elseif (!empty($b['sharedcal'])) {
401 // hightlight that this was a reflection from a shared calendar
402 $cell_classes[] = 'busy-sharedcalendar';
403 }
404 }
405 // check if we can push the booking ID involved
406 if (!empty($b['idorder'])) {
407 $cell_bids[] = $b['idorder'];
408 }
409 }
410 }
411 }
412 // check status for this day
413 if ($tot_units_booked > 0) {
414 if ($tot_units_booked < $max_units) {
415 // prepend the "partially-busy" class
416 array_unshift($cell_classes, 'vbo-partially');
417 }
418 // prepend the "busy" cell class so that this will be first
419 array_unshift($cell_classes, 'busy');
420 } else {
421 // set the "free" cell class
422 $cell_classes[] = 'free';
423 }
424 // set ymd values
425 $cell_ymd = date('Y-m-d', $info_arr[0]);
426 if ($cell_ymd == $today_ymd) {
427 // set the "today" cell class
428 $cell_classes[] = 'is-today';
429 }
430 $cell_day_read = VikBooking::sayWeekDay($info_arr['wday']) . ' ' . $info_arr['mday'];
431
432 // count values for this day
433 $has_fests = isset($festivities[$cell_ymd]);
434 $rdnotes_key = $cell_ymd . '_' . $room_id . '_0';
435 $has_rdnotes = isset($rday_notes[$rdnotes_key]);
436
437 ?>
438 <td class="vbo-widget-booskcal-cell-mday <?php echo implode(' ', array_unique($cell_classes)); ?>" onclick="vboWidgetBooksCalMday('<?php echo $wrapper; ?>', this);" data-bids="<?php echo implode(',', array_unique($cell_bids)); ?>" data-ymd="<?php echo $cell_ymd; ?>" data-wday="<?php echo $info_arr['wday']; ?>" data-dayread="<?php echo htmlspecialchars($cell_day_read); ?>" data-cta="0" data-ctd="0">
439 <span class="vbo-widget-booskcal-mday-val"><?php echo $info_arr['mday']; ?></span>
440 <?php
441 if ($has_fests || $has_rdnotes) {
442 ?>
443 <div class="vbo-widget-booskcal-mday-info">
444 <?php
445 if ($has_fests) {
446 ?>
447 <span class="vbo-widget-booskcal-mday-fests"><?php VikBookingIcons::e('birthday-cake'); ?></span>
448 <?php
449 }
450 if ($has_rdnotes) {
451 ?>
452 <span class="vbo-widget-booskcal-mday-rdnotes"><?php VikBookingIcons::e('sticky-note'); ?></span>
453 <?php
454 }
455 ?>
456 </div>
457 <?php
458 }
459 ?>
460 </td>
461 <?php
462 $dayts = mktime(0, 0, 0, $info_arr['mon'], ($info_arr['mday'] + 1), $info_arr['year']);
463 $info_arr = getdate($dayts);
464 $d_count++;
465 }
466 // add empty cells until the end of the row
467 for ($i = $d_count; $i <= 6; $i++) {
468 ?>
469 <td class="vbo-widget-booskcal-cell-mday vbo-widget-booskcal-cell-empty">&nbsp;</td>
470 <?php
471 }
472 ?>
473 </tr>
474 </tbody>
475 </table>
476 </div>
477
478 <script type="text/javascript">
479
480 // render datepicker calendar for dates navigation
481 jQuery('#<?php echo $wrapper; ?>').find('.vbo-widget-bookscal-checkindt, .vbo-widget-bookscal-checkoutdt').datepicker({
482 minDate: "-1m",
483 maxDate: "+3y",
484 yearRange: "<?php echo date('Y'); ?>:<?php echo (date('Y') + 3); ?>",
485 changeMonth: true,
486 changeYear: true,
487 dateFormat: "<?php echo $dtpicker_df; ?>",
488 onSelect: function(selectedDate) {
489 if (!selectedDate) {
490 return;
491 }
492 if (jQuery(this).hasClass('vbo-widget-bookscal-checkindt')) {
493 let nowstart = jQuery(this).datepicker('getDate');
494 let nowstartdate = new Date(nowstart.getTime());
495 jQuery('.vbo-widget-bookscal-checkoutdt').datepicker('option', {minDate: nowstartdate});
496 }
497 // calculate website rates
498 vboWidgetBooksCalGetWebsiteRates('<?php echo $wrapper; ?>');
499 }
500 });
501
502 // triggering for datepicker calendar icon
503 jQuery('#<?php echo $wrapper; ?>').find('.vbo-widget-bookscal-checkindt-trigger, .vbo-widget-bookscal-checkoutdt-trigger').click(function() {
504 var jdp = jQuery(this).parent().find('input.hasDatepicker');
505 if (jdp.length) {
506 jdp.focus();
507 }
508 });
509
510 // check room units
511 var vbo_bookscal_roomfilt = jQuery('#<?php echo $wrapper; ?>').find('.vbo-booskcal-roomid');
512 if (vbo_bookscal_roomfilt.val()) {
513 var vbo_bookscal_room_units = vbo_bookscal_roomfilt.find('option:selected').attr('data-units');
514 if (vbo_bookscal_room_units > 1) {
515 jQuery('#<?php echo $wrapper; ?>').find('.vbo-widget-bookscal-units').attr('max', vbo_bookscal_room_units);
516 } else {
517 jQuery('#<?php echo $wrapper; ?>').find('.vbo-widget-bookscal-units').closest('[data-noclosure="1"]').hide();
518 }
519 } else {
520 jQuery('#<?php echo $wrapper; ?>').find('.vbo-widget-bookscal-units').closest('[data-noclosure="1"]').hide();
521 }
522
523 <?php
524 // check if a specific day was requested through the widget options
525 $force_day = $this->getOption('day', '');
526 if ($force_day && preg_match("/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/", $force_day)) {
527 // trigger the click event over the requested day
528 ?>
529 setTimeout(() => {
530 jQuery('.vbo-widget-booskcal-cell-mday[data-ymd="<?php echo $force_day; ?>"]').trigger('click');
531 }, 100);
532 <?php
533 }
534 ?>
535
536 </script>
537 <?php
538
539 // get the HTML buffer
540 $html_content = ob_get_contents();
541 ob_end_clean();
542
543 // return an associative array of values
544 return array(
545 'html' => $html_content,
546 'offset' => $next_offset,
547 'search_name' => $search_name,
548 'period_date' => $period_date,
549 );
550 }
551
552 /**
553 * Custom method for this widget only to load the bookings of a month-day.
554 * The method is called by the admin controller through an AJAX request.
555 * The visibility should be public, it should not exit the process, and
556 * any content sent to output will be returned to the AJAX response.
557 * In this case we return an array because this method requires "return":1.
558 */
559 public function loadMdayBookings()
560 {
561 // get today's date
562 $today_ymd = date('Y-m-d');
563
564 $page_offset = VikRequest::getInt('page_offset', 0, 'request');
565 $page_length = VikRequest::getInt('page_length', $this->bookings_per_page, 'request');
566 $ymd = VikRequest::getString('ymd', $today_ymd, 'request');
567 $wrapper = VikRequest::getString('wrapper', '', 'request');
568 $room_id = VikRequest::getInt('room_id', 0, 'request');
569
570 // calculate date timestamps interval for the given day
571 $day_info = getdate(strtotime($ymd));
572 $from_ts = mktime(0, 0, 0, $day_info['mon'], $day_info['mday'], $day_info['year']);
573 $to_ts = mktime(23, 59, 59, $day_info['mon'], $day_info['mday'], $day_info['year']);
574
575 // load busy records
576 $room_filter = !empty($room_id) ? array($room_id) : array();
577 $busy_records = VikBooking::loadBusyRecords($room_filter, $from_ts, $to_ts);
578
579 // gather all bookings touching this day
580 $booking_ids = [];
581 foreach ($busy_records as $rid => $rbusy) {
582 foreach ($rbusy as $b) {
583 $tmpone = getdate($b['checkin']);
584 $ritts = mktime(0, 0, 0, $tmpone['mon'], $tmpone['mday'], $tmpone['year']);
585 $tmptwo = getdate($b['checkout']);
586 $conts = mktime(0, 0, 0, $tmptwo['mon'], $tmptwo['mday'], $tmptwo['year']);
587 if ($from_ts >= $ritts && $from_ts < $conts) {
588 if (empty($b['idorder']) || in_array($b['idorder'], $booking_ids)) {
589 continue;
590 }
591 array_push($booking_ids, $b['idorder']);
592 }
593 }
594 }
595
596 // invoke availability helper class
597 $av_helper = VikBooking::getAvailabilityInstance();
598
599 // collect booking information
600 $booking_details = [];
601 foreach ($booking_ids as $bid) {
602 $booking = $av_helper->getBookingDetails($bid);
603 if (!is_array($booking) || !$booking) {
604 continue;
605 }
606 $booking_details[$bid] = $booking;
607 }
608
609 // check if a next page can be available
610 $tot_bookings = count($booking_details);
611 $has_next_page = ($tot_bookings > ($page_length + $page_offset));
612
613 // slice the records, if needed
614 if ($tot_bookings > $page_length) {
615 $booking_details = array_slice($booking_details, $page_offset, $page_length, true);
616 }
617
618 // load cancellations for today (only if a room ID filter is set)
619 $cancellations = [];
620 if (!empty($room_id)) {
621 $cancellations = $this->loadCancellations($room_id, $ymd);
622 }
623
624 // load festivities or room-day notes
625 $festivities = [];
626 $rday_notes = [];
627 if (!empty($room_id)) {
628 // load room-day notes for the given room id
629 $rday_notes = VikBooking::getCriticalDatesInstance()->loadRoomDayNotes(date('Y-m-d', $from_ts), date('Y-m-d', $to_ts), $room_id);
630 } else {
631 // load festivities when no specific room filter set
632 $festivities = VikBooking::getFestivitiesInstance()->loadFestDates(date('Y-m-d', $from_ts), date('Y-m-d', $to_ts));
633 }
634
635 // start output buffering
636 ob_start();
637
638 if ($festivities) {
639 // display the festivities for this day
640 ?>
641 <div class="vbo-widget-booskcal-events vbo-widget-booskcal-fests">
642 <?php
643 foreach ($festivities as $fest_ymd => $fest) {
644 if (empty($fest['festinfo']) || !is_array($fest['festinfo'])) {
645 continue;
646 }
647 foreach ($fest['festinfo'] as $fest_info) {
648 if (!is_object($fest_info) || empty($fest_info->trans_name)) {
649 continue;
650 }
651 ?>
652 <div class="vbo-widget-booskcal-event vbo-widget-booskcal-fest">
653 <strong><?php echo $fest_info->trans_name; ?></strong>
654 <?php
655 if (!empty($fest_info->descr)) {
656 ?>
657 <div><?php echo nl2br($fest_info->descr); ?></div>
658 <?php
659 }
660 ?>
661 </div>
662 <?php
663 }
664 }
665 ?>
666 </div>
667 <?php
668 }
669
670 if ($rday_notes) {
671 // display the room-day notes for this day
672 ?>
673 <div class="vbo-widget-booskcal-events vbo-widget-booskcal-rdaynotes">
674 <?php
675 foreach ($rday_notes as $rday_note) {
676 if (empty($rday_note['info']) || !is_array($rday_note['info'])) {
677 continue;
678 }
679 foreach ($rday_note['info'] as $note_info) {
680 if (!is_object($note_info) || empty($note_info->name)) {
681 continue;
682 }
683 ?>
684 <div class="vbo-widget-booskcal-event vbo-widget-booskcal-rdaynote">
685 <strong><?php echo $note_info->name; ?></strong>
686 <?php
687 if (!empty($note_info->descr)) {
688 ?>
689 <div><?php echo nl2br($note_info->descr); ?></div>
690 <?php
691 }
692 ?>
693 </div>
694 <?php
695 }
696 }
697 ?>
698 </div>
699 <?php
700 }
701
702 if (!$booking_details && !$cancellations) {
703 ?>
704 <p class="info"><?php echo JText::translate('VBNOORDERSFOUND'); ?></p>
705 <?php
706 } else {
707 if (!$booking_details) {
708 ?>
709 <p class="info"><?php echo JText::translate('VBNOORDERSFOUND'); ?></p>
710 <?php
711 }
712
713 // merge confirmed bookings with cancellations (if any)
714 $booking_details = array_merge($booking_details, $cancellations);
715
716 // display all bookings of this day
717 $canc_separator = false;
718 foreach ($booking_details as $booking) {
719 // get channel logo and other details
720 $ch_logo_obj = VikBooking::getVcmChannelsLogo($booking['channel'], true);
721 $channel_logo = is_object($ch_logo_obj) ? $ch_logo_obj->getSmallLogoURL() : '';
722 $nights_lbl = $booking['days'] > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY');
723 $rooms_lbl = !empty($booking['roomsnum']) && $booking['roomsnum'] > 1 ? ', ' . $booking['roomsnum'] . ' ' . JText::translate('VBPVIEWORDERSTHREE') : '';
724 // compose customer name
725 $customer_name = !empty($booking['customer_fullname']) ? $booking['customer_fullname'] : '';
726 if ($booking['closure'] > 0 || !strcasecmp((string) $booking['custdata'], JText::translate('VBDBTEXTROOMCLOSED'))) {
727 $customer_name = '<span class="vbordersroomclosed"><i class="' . VikBookingIcons::i('ban') . '"></i> ' . JText::translate('VBDBTEXTROOMCLOSED') . '</span>';
728 }
729 if (empty($customer_name)) {
730 $customer_name = VikBooking::getFirstCustDataField($booking['custdata']);
731 }
732 // customer country flag
733 $customer_country = '';
734 $customer_cflag = '';
735 if (!empty($booking['customer_country'])) {
736 $customer_country = $booking['customer_country'];
737 } elseif (!empty($booking['country'])) {
738 $customer_country = $booking['country'];
739 }
740 if ($customer_country && is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . 'countries' . DIRECTORY_SEPARATOR . $customer_country . '.png')) {
741 $customer_cflag = '<img src="'.VBO_ADMIN_URI.'resources/countries/' . $customer_country . '.png'.'" title="' . htmlspecialchars($customer_country) . '" class="vbo-country-flag vbo-country-flag-left"/>';
742 }
743
744 // check for cancellations
745 $elem_style = $booking['status'] == 'cancelled' ? 'display: none;' : '';
746 if ($booking['status'] == 'cancelled' && !$canc_separator) {
747 // display the button to show the cancellations
748 $canc_separator = true;
749 ?>
750 <div class="vbo-bookings-status-separator">
751 <button type="button" class="btn btn-small btn-secondary" onclick="vboWidgetBooksCalCancToggle('<?php echo $wrapper; ?>');"><?php echo JText::translate('VBO_SHOW_CANCELLATIONS'); ?></button>
752 </div>
753 <?php
754 }
755
756 ?>
757 <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; ?>">
758 <div class="vbo-dashboard-guest-activity-avatar">
759 <?php
760 if (!empty($channel_logo)) {
761 // channel logo has got the highest priority
762 ?>
763 <img class="vbo-dashboard-guest-activity-avatar-profile" src="<?php echo $channel_logo; ?>" />
764 <?php
765 } elseif (!empty($booking['pic'])) {
766 // customer profile picture
767 ?>
768 <img class="vbo-dashboard-guest-activity-avatar-profile" src="<?php echo strpos((string) $booking['pic'], 'http') === 0 ? $booking['pic'] : VBO_SITE_URI . 'resources/uploads/' . $booking['pic']; ?>" />
769 <?php
770 } else {
771 // we use an icon as fallback
772 VikBookingIcons::e('hotel', 'vbo-dashboard-guest-activity-avatar-icon');
773 }
774 ?>
775 </div>
776 <div class="vbo-dashboard-guest-activity-content">
777 <div class="vbo-dashboard-guest-activity-content-head">
778 <div class="vbo-dashboard-guest-activity-content-info-details">
779 <h4><?php echo $customer_name . $customer_cflag; ?></h4>
780 <div class="vbo-dashboard-guest-activity-content-info-icon">
781 <?php
782 if (!strcasecmp((string) $booking['type'], 'overbooking')) {
783 ?>
784 <span class="label label-error vbo-label-overbooking"><?php echo JText::translate('VBO_BTYPE_' . strtoupper($booking['type'])); ?></span>
785 <?php
786 }
787 if ($booking['status'] == 'cancelled') {
788 ?>
789 <span class="badge badge-danger"><?php echo JText::translate('VBCANCELLED'); ?></span>
790 <?php
791 }
792 ?>
793 <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>
794 </div>
795 </div>
796 <div class="vbo-dashboard-guest-activity-content-info-date">
797 <span>
798 <span class="label label-info"><?php echo $booking['id']; ?></span>
799 </span>
800 <span><?php echo date(str_replace("/", $this->datesep, $this->df) . ' H:i', $booking['ts']); ?></span>
801 </div>
802 </div>
803 </div>
804 </div>
805 <?php
806 }
807
808 // append navigation
809 ?>
810 <div class="vbo-widget-commands vbo-widget-commands-right">
811 <div class="vbo-widget-commands-main">
812 <?php
813 if ($page_offset > 0) {
814 // show backward navigation button
815 ?>
816 <div class="vbo-widget-command-chevron vbo-widget-command-prev">
817 <span class="vbo-widget-command-chevron-prev" onclick="vboWidgetBooksCalMdayNavigate('<?php echo $wrapper; ?>', -1);"><?php VikBookingIcons::e('chevron-left'); ?></span>
818 </div>
819 <?php
820 }
821 if ($has_next_page) {
822 // show forward navigation button
823 ?>
824 <div class="vbo-widget-command-chevron vbo-widget-command-next">
825 <span class="vbo-widget-command-chevron-next" onclick="vboWidgetBooksCalMdayNavigate('<?php echo $wrapper; ?>', 1);"><?php VikBookingIcons::e('chevron-right'); ?></span>
826 </div>
827 <?php
828 }
829 ?>
830 </div>
831 </div>
832 <?php
833
834 // append JS code
835 ?>
836 <script type="text/javascript">
837
838 setTimeout(() => {
839 jQuery('#<?php echo $wrapper; ?>').find('.vbo-widget-booskcal-reservation').on('click', function(e) {
840 let bid = jQuery(this).attr('data-resid');
841 let target = jQuery(e.target);
842 if (target.is('img.vbo-dashboard-guest-activity-avatar-profile') || target.is('i.vbo-dashboard-guest-activity-avatar-icon') || target.is('span.label-info')) {
843 // render booking details within the widget booking details
844 VBOCore.handleDisplayWidgetNotification({widget_id: 'booking_details'}, {
845 bid: bid,
846 modal_options: {
847 /**
848 * Overwrite modal options for rendering the admin widget.
849 * We need to use a different suffix in case this current widget was
850 * also rendered within a modal, or it would get dismissed in favour
851 * of the newly opened admin widget.
852 */
853 suffix: 'widget_modal_inner_booking_details',
854 },
855 });
856 } else {
857 // navigate to booking details page
858 vboWidgetBooksCalOpenBooking(bid);
859 }
860 });
861 }, 300);
862
863 </script>
864 <?php
865 }
866
867 // get the HTML buffer
868 $html_content = ob_get_contents();
869 ob_end_clean();
870
871 // return an associative array of values
872 return array(
873 'html' => $html_content,
874 'tot_bookings' => $tot_bookings,
875 'next_page' => (int)$has_next_page,
876 );
877 }
878
879 /**
880 * Custom method for this widget only to load the custom fields for the new booking.
881 * The method is called by the admin controller through an AJAX request.
882 * The visibility should be public, and an array is requested to be returned.
883 *
884 * @return array
885 *
886 * @since 1.16.0 (J) - 1.6.0 (WP)
887 */
888 public function displayCustomerFilling()
889 {
890 $dbo = JFactory::getDbo();
891
892 $wrapper = VikRequest::getString('wrapper', '', 'request');
893
894 // load custom fields and countries
895 $all_countries = [];
896 $q = "SELECT * FROM `#__vikbooking_custfields` ORDER BY `#__vikbooking_custfields`.`ordering` ASC;";
897 $dbo->setQuery($q);
898 $all_cfields = $dbo->loadAssocList();
899 if ($all_cfields) {
900 $q = "SELECT * FROM `#__vikbooking_countries` ORDER BY `#__vikbooking_countries`.`country_name` ASC;";
901 $dbo->setQuery($q);
902 $all_countries = $dbo->loadAssocList();
903 }
904
905 // start output buffering
906 ob_start();
907
908 ?>
909 <div class="vbo-calendar-cfields-filler" data-wrapper="<?php echo $wrapper; ?>">
910 <div class="vbo-calendar-cfields-topcont">
911 <div class="vbo-calendar-cfields-search">
912 <label for="vbo-searchcust<?php echo $wrapper; ?>"><?php echo JText::translate('VBOSEARCHEXISTCUST'); ?></label>
913 <span id="vbo-searchcust-loading<?php echo $wrapper; ?>" class="vbo-searchcust-loading">
914 <i class="vboicn-hour-glass"></i>
915 </span>
916 <input type="text" id="vbo-searchcust<?php echo $wrapper; ?>" class="vbo-searchcust" autocomplete="off" value="" placeholder="<?php echo htmlspecialchars(JText::translate('VBOSEARCHCUSTBY')); ?>" size="35" />
917 <div id="vbo-searchcust-res<?php echo $wrapper; ?>" class="vbo-searchcust-res"></div>
918 </div>
919 </div>
920 <div class="vbo-calendar-cfields-inner vbo-widget-bookscal-cfields-inner">
921 <input type="hidden" value="" id="vbo-widget-bookscal-cfield-custid<?php echo $wrapper; ?>" />
922 <?php
923 foreach ($all_cfields as $cfield) {
924 if ($cfield['type'] == 'text' && $cfield['isphone'] == 1) {
925 ?>
926 <div class="vbo-calendar-cfield-entry">
927 <label for="cfield<?php echo $cfield['id'] . $wrapper; ?>"><?php echo JText::translate($cfield['name']); ?></label>
928 <span>
929 <?php
930 echo $this->vbo_app->printPhoneInputField(
931 [
932 'id' => 'cfield' . $cfield['id'] . $wrapper,
933 'class' => 'vbo-calendar-cfield-phone',
934 'data-isemail' => '0',
935 'data-isnominative' => '0',
936 'data-isphone' => '1'
937 ],
938 [
939 'fullNumberOnBlur' => true
940 ],
941 $load_assets = false
942 );
943 ?>
944 </span>
945 </div>
946 <?php
947 } elseif ($cfield['type'] == 'text') {
948 ?>
949 <div class="vbo-calendar-cfield-entry">
950 <label for="cfield<?php echo $cfield['id'] . $wrapper; ?>"><?php echo JText::translate($cfield['name']); ?></label>
951 <span>
952 <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"/>
953 </span>
954 </div>
955 <?php
956 } elseif ($cfield['type'] == 'textarea') {
957 ?>
958 <div class="vbo-calendar-cfield-entry">
959 <label for="cfield<?php echo $cfield['id'] . $wrapper; ?>"><?php echo JText::translate($cfield['name']); ?></label>
960 <span>
961 <textarea id="cfield<?php echo $cfield['id'] . $wrapper; ?>" rows="4" cols="35"></textarea>
962 </span>
963 </div>
964 <?php
965 } elseif ($cfield['type'] == 'country') {
966 ?>
967 <div class="vbo-calendar-cfield-entry">
968 <label for="cfield<?php echo $cfield['id'] . $wrapper; ?>"><?php echo JText::translate($cfield['name']); ?></label>
969 <span>
970 <select id="cfield<?php echo $cfield['id'] . $wrapper; ?>" class="vbo-calendar-cfield-country" onchange="vboWidgetBooksCalChangeCountry(this, '<?php echo $wrapper; ?>');">
971 <option value=""> </option>
972 <?php
973 foreach ($all_countries as $country) {
974 ?>
975 <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>
976 <?php
977 }
978 ?>
979 </select>
980 </span>
981 </div>
982 <?php
983 } elseif ($cfield['type'] == 'state') {
984 ?>
985 <div class="vbo-calendar-cfield-entry">
986 <label for="cfield<?php echo $cfield['id'] . $wrapper; ?>"><?php echo JText::translate($cfield['name']); ?></label>
987 <span>
988 <select id="cfield<?php echo $cfield['id'] . $wrapper; ?>" class="vbo-calendar-cfield-state">
989 <option value="">-----</option>
990 </select>
991 </span>
992 </div>
993 <?php
994 }
995 }
996 ?>
997 </div>
998 </div>
999
1000 <script type="text/javascript">
1001
1002 /**
1003 * Pool of customers previous information collected.
1004 */
1005 window['customers_search_vals<?php echo $wrapper; ?>'] = {};
1006
1007 /**
1008 * Search among the customers for the given keyword.
1009 */
1010 function vboWidgetBooksCalCustomerSearch(words) {
1011 jQuery('#vbo-searchcust-res<?php echo $wrapper; ?>').hide().html('');
1012 jQuery('#vbo-searchcust-loading<?php echo $wrapper; ?>').show();
1013
1014 VBOCore.doAjax(
1015 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=searchcustomer'); ?>",
1016 {
1017 kw: words,
1018 selector: 'vbo-widget-custsearchres-entry',
1019 no_script: 1,
1020 tmpl: "component"
1021 },
1022 (cont) => {
1023 // parse the response
1024 if (cont) {
1025 // populate values
1026 var obj_res = typeof cont === 'string' ? JSON.parse(cont) : cont;
1027 window['customers_search_vals<?php echo $wrapper; ?>'] = obj_res[0];
1028 jQuery('#vbo-searchcust-res<?php echo $wrapper; ?>').html(obj_res[1]);
1029
1030 setTimeout(() => {
1031 // de-register listeners
1032 jQuery('#vbo-searchcust<?php echo $wrapper; ?>').off('keydown');
1033 jQuery('#vbo-searchcust-res<?php echo $wrapper; ?>').find('.vbo-widget-custsearchres-entry').off('hover');
1034
1035 // register listeners
1036 var vboCust = jQuery('#vbo-searchcust-res<?php echo $wrapper; ?>').find('.vbo-widget-custsearchres-entry');
1037 var vboCustSelected = null;
1038 jQuery('#vbo-searchcust<?php echo $wrapper; ?>').keydown(function(e) {
1039 if (e.which === 40) {
1040 if (vboCustSelected) {
1041 vboCustSelected.removeClass('vbo-widget-custsearchres-entry-highligthed');
1042 next = vboCustSelected.next();
1043 if (next.length > 0) {
1044 vboCustSelected = next.addClass('vbo-widget-custsearchres-entry-highligthed');
1045 } else {
1046 vboCustSelected = vboCust.eq(0).addClass('vbo-widget-custsearchres-entry-highligthed');
1047 }
1048 } else {
1049 vboCustSelected = vboCust.eq(0).addClass('vbo-widget-custsearchres-entry-highligthed');
1050 }
1051 } else if (e.which === 38) {
1052 if (vboCustSelected) {
1053 vboCustSelected.removeClass('vbo-widget-custsearchres-entry-highligthed');
1054 next = vboCustSelected.prev();
1055 if (next.length > 0) {
1056 vboCustSelected = next.addClass('vbo-widget-custsearchres-entry-highligthed');
1057 } else {
1058 vboCustSelected = vboCust.last().addClass('vbo-widget-custsearchres-entry-highligthed');
1059 }
1060 } else {
1061 vboCustSelected = vboCust.last().addClass('vbo-widget-custsearchres-entry-highligthed');
1062 }
1063 } else if (e.which === 13) {
1064 if (vboCustSelected) {
1065 vboCustSelected.trigger('click');
1066 }
1067 }
1068 });
1069
1070 jQuery('#vbo-searchcust-res<?php echo $wrapper; ?>').find('.vbo-widget-custsearchres-entry').hover(function() {
1071 if (vboCustSelected) {
1072 vboCustSelected.removeClass('vbo-widget-custsearchres-entry-highligthed');
1073 vboCustSelected = null;
1074 }
1075 vboCustSelected = jQuery(this).addClass('vbo-widget-custsearchres-entry-highligthed');
1076 }, function() {
1077 if (vboCustSelected) {
1078 vboCustSelected.removeClass('vbo-widget-custsearchres-entry-highligthed');
1079 vboCustSelected = null;
1080 }
1081 jQuery(this).removeClass('vbo-widget-custsearchres-entry-highligthed');
1082 });
1083 }, 300);
1084 } else {
1085 window['customers_search_vals<?php echo $wrapper; ?>'] = {};
1086 jQuery("#vbo-searchcust-res<?php echo $wrapper; ?>").html("----");
1087 }
1088 jQuery("#vbo-searchcust-res<?php echo $wrapper; ?>").show();
1089 jQuery("#vbo-searchcust-loading<?php echo $wrapper; ?>").hide();
1090 },
1091 (error) => {
1092 jQuery("#vbo-searchcust-loading<?php echo $wrapper; ?>").hide();
1093 console.error(error);
1094 alert(error.responseText);
1095 }
1096 );
1097 }
1098
1099 /**
1100 * Register keyup event listener with a debounce technique for searching customers.
1101 */
1102 document.getElementById('vbo-searchcust<?php echo $wrapper; ?>').addEventListener('keyup', VBOCore.debounceEvent((e) => {
1103 var keywords = jQuery("#vbo-searchcust<?php echo $wrapper; ?>").val();
1104 var chars = keywords ? keywords.length : '';
1105 // we prefer e.key, as e.keyCode is deprecated
1106 var key_pressed = e.key || e.keyCode;
1107 var rgx = new RegExp(/^[A-Za-z0-9]$/);
1108 if (chars > 1 && key_pressed) {
1109 if (key_pressed == 'Enter' || key_pressed == 13 || rgx.test(key_pressed)) {
1110 vboWidgetBooksCalCustomerSearch(keywords);
1111 }
1112 } else {
1113 if (jQuery("#vbo-searchcust-res<?php echo $wrapper; ?>").is(":visible")) {
1114 jQuery("#vbo-searchcust-res<?php echo $wrapper; ?>").hide();
1115 }
1116 }
1117 }, 600));
1118
1119 /**
1120 * Register the click event over the customer search results.
1121 */
1122 jQuery(document).on('click', '#vbo-searchcust-res<?php echo $wrapper; ?> .vbo-widget-custsearchres-entry', function() {
1123 var custid = jQuery(this).attr('data-custid');
1124 var custemail = jQuery(this).attr('data-email');
1125 var custphone = jQuery(this).attr('data-phone');
1126 var custcountry = jQuery(this).attr('data-country');
1127 var custfirstname = jQuery(this).attr('data-firstname');
1128 var custlastname = jQuery(this).attr('data-lastname');
1129
1130 // set customer ID
1131 jQuery('#vbo-widget-bookscal-cfield-custid<?php echo $wrapper; ?>').val(custid);
1132
1133 // check previous custom fields
1134 if (window['customers_search_vals<?php echo $wrapper; ?>'].hasOwnProperty(custid)) {
1135 jQuery.each(window['customers_search_vals<?php echo $wrapper; ?>'][custid], function(cfid, cfval) {
1136 var fill_field = jQuery('#cfield' + cfid + '<?php echo $wrapper; ?>');
1137 if (fill_field.length) {
1138 // set previous value
1139 fill_field.val(cfval);
1140 }
1141 });
1142 }
1143
1144 // always populate basic information on custom fields
1145 var fields_wrap = jQuery('.vbo-calendar-cfields-filler[data-wrapper="<?php echo $wrapper; ?>"]');
1146
1147 if (custcountry.length) {
1148 if (custcountry.length > 3) {
1149 fields_wrap.find('select.vbo-calendar-cfield-country').val(custcountry);
1150 } else {
1151 var country_opt = fields_wrap.find('select.vbo-calendar-cfield-country').find('option[data-ccode="' + custcountry + '"]');
1152 if (country_opt.length) {
1153 fields_wrap.find('select.vbo-calendar-cfield-country').val(country_opt.attr('value'));
1154 }
1155 }
1156 }
1157 fields_wrap.find('input[data-isnominative="1"]').each(function(k, v) {
1158 if (k == 0) {
1159 jQuery(this).val(custfirstname);
1160 return true;
1161 }
1162 if (k == 1) {
1163 jQuery(this).val(custlastname);
1164 return true;
1165 }
1166 return false;
1167 });
1168 fields_wrap.find('input[data-isemail="1"]').val(custemail);
1169 fields_wrap.find('input[data-isphone="1"]').val(custphone);
1170
1171 // set customer upon the selection just made
1172 vboWidgetBooksCalSetCustomer('<?php echo $wrapper; ?>');
1173 });
1174
1175 // focus search customer input field
1176 setTimeout(() => {
1177 jQuery('#vbo-searchcust<?php echo $wrapper; ?>').focus();
1178 }, 500);
1179
1180 </script>
1181 <?php
1182
1183 // get the HTML buffer
1184 $html_content = ob_get_contents();
1185 ob_end_clean();
1186
1187 // return an associative array of values
1188 return [
1189 'html' => $html_content,
1190 ];
1191 }
1192
1193 /**
1194 * Custom method for this widget only to save a new booking with the input values.
1195 * The method is called by the admin controller through an AJAX request.
1196 * The visibility should be public, and an array is requested to be returned.
1197 *
1198 * @return array
1199 *
1200 * @since 1.16.0 (J) - 1.6.0 (WP)
1201 */
1202 public function saveBooking()
1203 {
1204 $wrapper = VikRequest::getString('wrapper', '', 'request');
1205 $forcebooking = VikRequest::getInt('forcebooking', 0, 'request');
1206 $room_id = VikRequest::getInt('room_id', 0, 'request');
1207 $checkin = VikRequest::getString('checkin', '', 'request');
1208 $checkout = VikRequest::getString('checkout', '', 'request');
1209 $closure = VikRequest::getInt('closure', 0, 'request');
1210 $units_closed = VikRequest::getInt('units_closed', 0, 'request');
1211 $units = VikRequest::getInt('units', 1, 'request');
1212 $adults = VikRequest::getInt('adults', 2, 'request');
1213 $children = VikRequest::getInt('children', 0, 'request');
1214 $cust_id = VikRequest::getInt('cust_id', 0, 'request');
1215 $cust_email = VikRequest::getString('cust_email', '', 'request');
1216 $cust_data = VikRequest::getString('cust_data', '', 'request');
1217 $cust_country = VikRequest::getString('cust_country', '', 'request');
1218 $cust_state = VikRequest::getString('cust_state', '', 'request');
1219 $cust_phone = VikRequest::getString('cust_phone', '', 'request');
1220 $cust_tfname = VikRequest::getString('cust_tfname', '', 'request');
1221 $cust_tlname = VikRequest::getString('cust_tlname', '', 'request');
1222 $roomcost = VikRequest::getFloat('roomcost', 0, 'request');
1223 $idprice = VikRequest::getInt('idprice', 0, 'request');
1224 $cust_roomcost = VikRequest::getFloat('cust_roomcost', 0, 'request');
1225 $taxid = VikRequest::getInt('taxid', 0, 'request');
1226
1227 if (empty($room_id) || empty($checkin) || empty($checkout)) {
1228 VBOHttpDocument::getInstance()->close(500, JText::translate('VBO_PLEASE_FILL_FIELDS'));
1229 }
1230
1231 // invoke the reservation model and inject values
1232 $model_res = VBOModelReservation::getInstance([
1233 'force_booking' => $forcebooking,
1234 'set_closed' => $closure,
1235 'units_closed' => $units_closed,
1236 'status' => 'confirmed',
1237 'num_rooms' => $units,
1238 'adults' => $adults,
1239 'children' => $children,
1240 ])->setCustomer([
1241 'id' => $cust_id,
1242 'first_name' => $cust_tfname,
1243 'last_name' => $cust_tlname,
1244 'data' => $cust_data,
1245 'email' => $cust_email,
1246 'country' => $cust_country,
1247 'state' => $cust_state,
1248 'phone' => $cust_phone,
1249 ])->setRoom([
1250 'id' => $room_id,
1251 'cust_cost' => $cust_roomcost,
1252 'room_cost' => $roomcost,
1253 'id_price' => $idprice,
1254 'id_tax' => $taxid,
1255 ]);
1256
1257 // calculate proper check-in and check-out timestamps
1258 list($hcheckin, $mcheckin, $hcheckout, $mcheckout) = $model_res->loadCheckinOutTimes();
1259
1260 // get final stay timestamps
1261 $checkin_ts = VikBooking::getDateTimestamp($checkin, $hcheckin, $mcheckin);
1262 $checkout_ts = VikBooking::getDateTimestamp($checkout, $hcheckout, $mcheckout);
1263
1264 if (!$checkin_ts || !$checkout_ts || $checkin_ts >= $checkout_ts) {
1265 VBOHttpDocument::getInstance()->close(500, JText::translate('ERRINVDATESEASON'));
1266 }
1267
1268 // set stay dates
1269 $model_res->set('checkin', $checkin_ts);
1270 $model_res->set('checkout', $checkout_ts);
1271
1272 // store the reservation
1273 $model_res->create();
1274
1275 // get the new booking ID
1276 $res_id = $model_res->getNewBookingID();
1277 if (!$res_id) {
1278 VBOHttpDocument::getInstance()->close(500, $model_res->getError());
1279 }
1280
1281 return [
1282 'new_booking_id' => $res_id,
1283 'vcm_action' => $model_res->getChannelManagerAction(),
1284 ];
1285 }
1286
1287 /**
1288 * Custom method for this widget only to load the room rates and restrictions.
1289 * The method is called by the admin controller through an AJAX request.
1290 * The visibility should be public, it should not exit the process, and
1291 * any content sent to output will be returned to the AJAX response.
1292 * In this case we return an array because this method requires "return":1.
1293 *
1294 * @since 1.16.10 (J) - 1.6.10 (WP)
1295 */
1296 public function loadRoomRates()
1297 {
1298 $app = JFactory::getApplication();
1299
1300 $from_date = $app->input->getString('from_date', date('Y-m-d'));
1301 $wrapper = $app->input->getString('wrapper', '');
1302 $room_id = $app->input->getInt('room_id', 0);
1303
1304 $to_dt = JFactory::getDate($from_date);
1305 $to_dt->modify('+1 month');
1306 $to_date = $to_dt->format('Y-m-d');
1307
1308 // get room details
1309 $room_data = VikBooking::getRoomInfo($room_id);
1310 if (!$room_data) {
1311 VBOHttpDocument::getInstance($app)->close(404, 'Room not found');
1312 }
1313
1314 try {
1315 // fetch room rates and restrictions
1316 $room_rates = VBOModelPricing::getInstance()->getRoomRates([
1317 'id_room' => $room_data['id'],
1318 'from_date' => $from_date,
1319 'to_date' => $to_date,
1320 'restrictions' => true,
1321 ]);
1322 } catch (Exception $e) {
1323 // propagate the error
1324 VBOHttpDocument::getInstance($app)->close($e->getCode(), $e->getMessage());
1325 }
1326
1327 return [
1328 'rates_restrictions' => $room_rates,
1329 ];
1330 }
1331
1332 /**
1333 * Custom method for this widget only to load all room rates for a given day.
1334 * The method is called by the admin controller through an AJAX request.
1335 * The visibility should be public, it should not exit the process, and
1336 * any content sent to output will be returned to the AJAX response.
1337 * In this case we return an array because this method requires "return":1.
1338 *
1339 * @since 1.16.10 (J) - 1.6.10 (WP)
1340 */
1341 public function loadRoomDayRatePlans()
1342 {
1343 $app = JFactory::getApplication();
1344
1345 $ymd = $app->input->getString('ymd', date('Y-m-d'));
1346 $room_id = $app->input->getInt('room_id', 0);
1347
1348 $from_date_info = getdate(strtotime($ymd));
1349
1350 // get room details
1351 $room_data = VikBooking::getRoomInfo($room_id);
1352 if (!$room_data) {
1353 VBOHttpDocument::getInstance($app)->close(404, 'Room not found');
1354 }
1355
1356 try {
1357 // fetch room rates and restrictions
1358 $room_rates = VBOModelPricing::getInstance()->getRoomRates([
1359 'id_room' => $room_data['id'],
1360 'from_date' => $ymd,
1361 'to_date' => $ymd,
1362 'all_rplans' => true,
1363 'restrictions' => true,
1364 ]);
1365 } catch (Exception $e) {
1366 // propagate the error
1367 VBOHttpDocument::getInstance($app)->close($e->getCode(), $e->getMessage());
1368 }
1369
1370 // access the information about the derived rate plans
1371 $derived_rates_info = [];
1372 foreach (VikBooking::getAvailabilityInstance()->loadRatePlans() as $rate_plan) {
1373 if ($rate_plan['derived_id'] && $rate_plan['derived_data']) {
1374 // build derived information string
1375 $derived_str = ($rate_plan['parent_rate_name'] ?? '') . ' ';
1376 $derived_str .= ($rate_plan['derived_data']['type'] ?? 'percent') == 'absolute' ? VikBooking::getCurrencySymb() . ' ' : '';
1377 $derived_str .= ($rate_plan['derived_data']['mode'] ?? 'discount') == 'discount' ? '-' : '+';
1378 $derived_str .= $rate_plan['derived_data']['value'] ?? 0;
1379 $derived_str .= ($rate_plan['derived_data']['type'] ?? 'percent') == 'percent' ? ' %' : '';
1380
1381 $rate_plan['derived_info'] = $derived_str;
1382
1383 // register derived rate plan
1384 $derived_rates_info[$rate_plan['id']] = $rate_plan;
1385 }
1386 }
1387
1388 return [
1389 'wday' => $from_date_info['wday'],
1390 'room_rates' => $room_rates,
1391 'derived_rates' => $derived_rates_info,
1392 ];
1393 }
1394
1395 /**
1396 * Custom method for this widget only to apply a new cost and/or min-los
1397 * for a specific room, rate plan and day.
1398 * The method is called by the admin controller through an AJAX request.
1399 * The visibility should be public, it should not exit the process, and
1400 * any content sent to output will be returned to the AJAX response.
1401 * In this case we return an array because this method requires "return":1.
1402 *
1403 * @since 1.16.10 (J) - 1.6.10 (WP)
1404 */
1405 public function setRoomDayRateRestiction()
1406 {
1407 $app = JFactory::getApplication();
1408
1409 $ymd = $app->input->getString('ymd', date('Y-m-d'));
1410 $rplan_id = $app->input->getInt('rplan_id', 0);
1411 $room_id = $app->input->getInt('room_id', 0);
1412 $rate = $app->input->getFloat('rate', 0);
1413 $minlos = $app->input->getInt('minlos', 0);
1414 $updotas = $app->input->getBool('updotas', false);
1415
1416 try {
1417 // access the model pricing by binding data
1418 $model = VBOModelPricing::getInstance([
1419 'from_date' => $ymd,
1420 'to_date' => $ymd,
1421 'id_room' => $room_id,
1422 'id_price' => $rplan_id,
1423 'rate' => $rate,
1424 'min_los' => $minlos,
1425 'update_otas' => $updotas,
1426 ]);
1427
1428 // apply the new rate/restrictions
1429 $new_rates = $model->modifyRateRestrictions();
1430 } catch (Exception $e) {
1431 // propagate the error
1432 VCMHttpDocument::getInstance($app)->close($e->getCode(), $e->getMessage());
1433 }
1434
1435 return [
1436 'channels_updated' => $model->getChannelsUpdated(),
1437 'channel_errors' => $model->getChannelErrors(),
1438 'channel_warnings' => $model->getChannelWarnings(),
1439 'new_rates' => $new_rates,
1440 ];
1441 }
1442
1443 /**
1444 * Preload the necessary CSS/JS assets.
1445 *
1446 * @return void
1447 */
1448 public function preload()
1449 {
1450 // load assets
1451 $this->vbo_app->loadDatePicker();
1452 $this->vbo_app->loadPhoneInputFieldAssets();
1453
1454 // JS lang def
1455 JText::script('VBFILLCUSTFIELDS');
1456 JText::script('VBO_PLEASE_FILL_FIELDS');
1457 JText::script('VBDASHUPRESONE');
1458 JText::script('VBANNULLA');
1459 JText::script('VBAPPLY');
1460 JText::script('VBDBTEXTROOMCLOSED');
1461 JText::script('VBO_MARK_UNITS_CLOSED');
1462 JText::script('VBO_MIN_STAY_SHORT');
1463 JText::script('VBO_CTA_SHORT');
1464 JText::script('VBO_CTD_SHORT');
1465 JText::script('VBMAINPAYMENTSEDIT');
1466 JText::script('VBRATESOVWSETNEWRATE');
1467 JText::script('VBOMINIMUMSTAYSET');
1468 JText::script('VBOUPDRATESONCHANNELS');
1469 JText::script('VBOVCMRATESRES');
1470 JText::script('VBO_IS_DERIVED_RATE');
1471 }
1472
1473 /**
1474 * Main method to invoke the widget. Contents will be loaded
1475 * through AJAX requests, not via PHP when the page loads.
1476 *
1477 * @param VBOMultitaskData $data
1478 *
1479 * @return void
1480 */
1481 public function render(VBOMultitaskData $data = null)
1482 {
1483 // increase widget's instance counter
1484 static::$instance_counter++;
1485
1486 // check whether the widget is being rendered via AJAX when adding it through the customizer
1487 $is_ajax = $this->isAjaxRendering();
1488
1489 // generate a unique ID for the sticky notes wrapper instance
1490 $wrapper_instance = !$is_ajax ? static::$instance_counter : rand();
1491 $wrapper_id = 'vbo-widget-booskcal-' . $wrapper_instance;
1492
1493 // get permissions
1494 $vbo_auth_bookings = JFactory::getUser()->authorise('core.vbo.bookings', 'com_vikbooking');
1495 if (!$vbo_auth_bookings) {
1496 // display nothing
1497 return;
1498 }
1499
1500 // invoke availability helper class
1501 $av_helper = VikBooking::getAvailabilityInstance();
1502
1503 // get all rooms
1504 $all_rooms = $av_helper->loadRooms();
1505
1506 // check pricing tax configuration
1507 $prices_vat_included = (int)VikBooking::ivaInclusa();
1508
1509 // currency symbol
1510 $currencysymb = VikBooking::getCurrencySymb();
1511
1512 /**
1513 * This widget can make use of the Select2 jQuery plugin, but we do not preload it in
1514 * order to save resources. If any other widget has preloaded Select2, JS will detect it.
1515 */
1516 $use_nice_select = 0;
1517 if (count($all_rooms) > 1) {
1518 // turn flag on
1519 $use_nice_select = 1;
1520 }
1521
1522 // default dates and values
1523 $now_info = getdate();
1524 $from_ts = mktime(0, 0, 0, $now_info['mon'], 1, $now_info['year']);
1525
1526 // build week days list according to settings
1527 $firstwday = (int)VikBooking::getFirstWeekDay();
1528 $days_labels = array(
1529 JText::translate('VBSUN'),
1530 JText::translate('VBMON'),
1531 JText::translate('VBTUE'),
1532 JText::translate('VBWED'),
1533 JText::translate('VBTHU'),
1534 JText::translate('VBFRI'),
1535 JText::translate('VBSAT'),
1536 );
1537 $days_indexes = [];
1538 for ($i = 0; $i < 7; $i++) {
1539 $days_indexes[$i] = (6 - ($firstwday - $i) + 1) % 7;
1540 }
1541
1542 // check multitask data
1543 $page_bid = 0;
1544 $load_room = 0;
1545 $load_room_rates = 0;
1546 $modal_load_bid = '';
1547 $js_modal_id = '';
1548 if ($data) {
1549 // access Multitask data
1550 $page_bid = $data->getBookingID() ?: $this->options()->fetchBookingId();
1551 $is_modal_rendering = $data->isModalRendering();
1552 if ($page_bid && $is_modal_rendering) {
1553 // load contents according to injected multitask data
1554 $modal_load_bid = $page_bid;
1555 }
1556 if ($is_modal_rendering) {
1557 // get modal JS identifier
1558 $js_modal_id = $data->getModalJsIdentifier();
1559 }
1560
1561 // first check if an overbooking flag was given
1562 $overbooking = $this->getOption('overbooking', 0);
1563 $overbooking = $page_bid && $overbooking ? $page_bid : 0;
1564 if ($overbooking) {
1565 // prepare the options to set for rendering the admin-widget
1566 // correctly and show the overbooking details
1567 $this->prepareOverbookingOptions($overbooking);
1568 }
1569
1570 // check if a specific room ID was set
1571 $load_room = $this->getOption('id_room', 0);
1572
1573 // check if managing room rates was requested
1574 $load_room_rates = $load_room ? $this->getOption('roomrates', 0) : 0;
1575
1576 // check for a custom date (Y-m-d)
1577 $option_offset = $this->getOption('offset', '');
1578 if ($option_offset && preg_match("/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/", $option_offset)) {
1579 // overwrite values
1580 $now_info = getdate(strtotime($option_offset));
1581 $from_ts = mktime(0, 0, 0, $now_info['mon'], 1, $now_info['year']);
1582 }
1583 }
1584
1585 // default labels
1586 $search_name = preg_replace("/[^A-Za-z0-9 ]/", '', JText::translate('VBOSTATSALLROOMS'));
1587 $period_date = VikBooking::sayMonth($now_info['mon']) . ' ' . $now_info['year'];
1588
1589 // start looping from the first day of the current month
1590 $info_arr = getdate($from_ts);
1591
1592 // week days counter
1593 $d_count = 0;
1594 $mon_lim = $info_arr['mon'];
1595
1596 ?>
1597 <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); ?>">
1598 <div class="vbo-admin-widget-head">
1599 <div class="vbo-admin-widget-head-inline">
1600 <h4><?php echo $this->widgetIcon; ?> <span><?php echo $this->widgetName; ?></span></h4>
1601 <div class="vbo-admin-widget-head-commands">
1602
1603 <div class="vbo-reportwidget-commands">
1604 <div class="vbo-reportwidget-commands-main">
1605 <div class="vbo-reportwidget-command-dates">
1606 <div class="vbo-reportwidget-period-name"><?php echo $search_name; ?></div>
1607 <div class="vbo-reportwidget-period-date"><?php echo $period_date; ?></div>
1608 </div>
1609 <div class="vbo-reportwidget-command-chevron vbo-reportwidget-command-prev">
1610 <span class="vbo-widget-booskcal-dt-prev" onclick="vboWidgetBookCalsMonthNav('<?php echo $wrapper_id; ?>', -1);"><?php VikBookingIcons::e('chevron-left'); ?></span>
1611 </div>
1612 <div class="vbo-reportwidget-command-chevron vbo-reportwidget-command-next">
1613 <span class="vbo-widget-booskcal-dt-next" onclick="vboWidgetBookCalsMonthNav('<?php echo $wrapper_id; ?>', 1);"><?php VikBookingIcons::e('chevron-right'); ?></span>
1614 </div>
1615 </div>
1616 </div>
1617
1618 </div>
1619 </div>
1620 </div>
1621 <div class="vbo-widget-booskcal-wrap">
1622 <div class="vbo-widget-booskcal-inner">
1623
1624 <div class="vbo-widget-booskcal-top-wrap">
1625 <div class="vbo-widget-booskcal-filter">
1626 <select class="vbo-booskcal-roomid" onchange="vboWidgetBookCalsSetRoom('<?php echo $wrapper_id; ?>', this.value);">
1627 <option data-units="0"></option>
1628 <?php
1629 foreach ($all_rooms as $rid => $room) {
1630 ?>
1631 <option value="<?php echo $rid; ?>" data-units="<?php echo $room['units']; ?>"<?php echo $rid == $load_room ? ' selected="selected"' : ''; ?>><?php echo $room['name']; ?></option>
1632 <?php
1633 }
1634 ?>
1635 </select>
1636 </div>
1637 <div class="vbo-widget-booskcal-newbook">
1638 <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>
1639 <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>
1640 </div>
1641 </div>
1642
1643 <div class="vbo-widget-booskcal-calendar">
1644
1645 <div class="vbo-widget-booskcal-calendar-table-wrap">
1646
1647 <table class="vbadmincaltable vbo-widget-booskcal-calendar-table">
1648 <tbody>
1649 <tr class="vbadmincaltrmdays">
1650 <?php
1651 // display week days in the proper order
1652 for ($i = 0; $i < 7; $i++) {
1653 $d_ind = ($i + $firstwday) < 7 ? ($i + $firstwday) : ($i + $firstwday - 7);
1654 ?>
1655 <td class="vbo-widget-booskcal-cell-wday"><?php echo $days_labels[$d_ind]; ?></td>
1656 <?php
1657 }
1658 ?>
1659 </tr>
1660 <tr>
1661 <?php
1662 // display empty cells until the first week-day of the month
1663 for ($i = 0, $n = $days_indexes[$info_arr['wday']]; $i < $n; $i++, $d_count++) {
1664 ?>
1665 <td class="vbo-widget-booskcal-cell-mday vbo-widget-booskcal-cell-empty">&nbsp;</td>
1666 <?php
1667 }
1668 // display month days
1669 while ($info_arr['mon'] == $mon_lim) {
1670 if ($d_count > 6) {
1671 $d_count = 0;
1672 // close current row and open a new one
1673 echo "\n</tr>\n<tr>\n";
1674 }
1675 ?>
1676 <td class="vbo-widget-booskcal-cell-mday">
1677 <span class="vbo-widget-booskcal-mday-val"><?php echo $info_arr['mday']; ?></span>
1678 </td>
1679 <?php
1680 $dayts = mktime(0, 0, 0, $info_arr['mon'], ($info_arr['mday'] + 1), $info_arr['year']);
1681 $info_arr = getdate($dayts);
1682 $d_count++;
1683 }
1684 // add empty cells until the end of the row
1685 for ($i = $d_count; $i <= 6; $i++) {
1686 ?>
1687 <td class="vbo-widget-booskcal-cell-mday vbo-widget-booskcal-cell-empty">&nbsp;</td>
1688 <?php
1689 }
1690 ?>
1691 </tr>
1692 </tbody>
1693 </table>
1694
1695 </div>
1696
1697 </div>
1698
1699 </div>
1700 </div>
1701 </div>
1702 <?php
1703
1704 if (static::$instance_counter === 0 || $is_ajax) {
1705 /**
1706 * Print the JS code only once for all instances of this widget.
1707 * The real rendering is made through AJAX, not when the page loads.
1708 */
1709 ?>
1710 <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>
1711
1712 <script type="text/javascript">
1713
1714 /**
1715 * Open the booking details page for the clicked reservation.
1716 */
1717 function vboWidgetBooksCalOpenBooking(id) {
1718 var open_url = jQuery('.vbo-widget-bookscal-basenavuri').first().attr('href');
1719 open_url = open_url.replace('%d', id);
1720 // navigate in a new tab
1721 window.open(open_url, '_blank');
1722 }
1723
1724 /**
1725 * Display the loading skeletons.
1726 */
1727 function vboWidgetBooksCalSkeletons(wrapper) {
1728 var widget_instance = jQuery('#' + wrapper);
1729 if (!widget_instance.length) {
1730 return false;
1731 }
1732
1733 var skelton_html = '<div class="vbo-skeleton-loading vbo-skeleton-loading-mday-cell"></div>';
1734 widget_instance.find('.vbo-widget-booskcal-calendar').find('.vbo-widget-booskcal-cell-mday').attr('class', 'vbo-widget-booskcal-cell-mday').html(skelton_html);
1735 }
1736
1737 /**
1738 * Perform the request to load the bookings calendar.
1739 */
1740 function vboWidgetBooksCalLoad(wrapper, dates_direction, page_bid, load_room_rates) {
1741 var widget_instance = jQuery('#' + wrapper);
1742 if (!widget_instance.length) {
1743 return false;
1744 }
1745
1746 // check if a navigation of dates was requested (0 = no dates nav)
1747 if (typeof dates_direction === 'undefined') {
1748 dates_direction = 0;
1749 }
1750
1751 // check if multitask data passed a booking ID for the current page
1752 var force_bid = 0;
1753 if (typeof page_bid !== 'undefined' && page_bid && !isNaN(page_bid)) {
1754 force_bid = page_bid;
1755 }
1756
1757 // get vars for making the request
1758 var current_offset = widget_instance.attr('data-offset');
1759 var room_id = widget_instance.find('.vbo-booskcal-roomid').val();
1760
1761 // check for rates management flag
1762 var mng_rates_restr = widget_instance.find('input[name="vbo-wbookscal-mng-rates-restr"]').prop('checked');
1763 if (load_room_rates) {
1764 mng_rates_restr = true;
1765 }
1766
1767 // gather options, if any
1768 var options = vbo_widget_books_cal_options_oo || {};
1769 if (vbo_widget_books_cal_options_oo) {
1770 // multitask data options should be used only once ("oo")
1771 vbo_widget_books_cal_options_oo = null;
1772 }
1773
1774 // the widget method to call
1775 var call_method = 'loadBookingsCalendar';
1776
1777 // make a request to load the bookings calendar
1778 VBOCore.doAjax(
1779 "<?php echo $this->getExecWidgetAjaxUri(); ?>",
1780 {
1781 widget_id: "<?php echo $this->getIdentifier(); ?>",
1782 call: call_method,
1783 return: 1,
1784 bid: force_bid,
1785 offset: current_offset,
1786 room_id: room_id,
1787 date_dir: dates_direction,
1788 mng_rates_restr: (mng_rates_restr ? 1 : 0),
1789 wrapper: wrapper,
1790 _options: options,
1791 tmpl: "component"
1792 },
1793 (response) => {
1794 try {
1795 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
1796 if (!obj_res.hasOwnProperty(call_method)) {
1797 console.error('Unexpected JSON response', obj_res);
1798 return false;
1799 }
1800
1801 // set new offset for navigation
1802 widget_instance.attr('data-offset', obj_res[call_method]['offset']);
1803
1804 // update search name and month
1805 widget_instance.find('.vbo-reportwidget-period-name').text(obj_res[call_method]['search_name']);
1806 widget_instance.find('.vbo-reportwidget-period-date').text(obj_res[call_method]['period_date']);
1807
1808 // replace HTML with new bookings calendar
1809 widget_instance.find('.vbo-widget-booskcal-calendar').html(obj_res[call_method]['html']);
1810
1811 // trigger the room rates and restrictions management, if requested
1812 if (room_id && mng_rates_restr) {
1813 setTimeout(() => {
1814 vboWidgetBooksCalManageRatesRestr(wrapper);
1815 }, 200);
1816 }
1817 } catch(err) {
1818 console.error('could not parse JSON response', err, response);
1819 }
1820 },
1821 (error) => {
1822 // remove the skeleton loading
1823 widget_instance.find('.vbo-widget-booskcal-calendar').find('.vbo-skeleton-loading').remove();
1824 console.error(error);
1825 }
1826 );
1827 }
1828
1829 /**
1830 * Toggles the management operations of room rates and restrictions.
1831 */
1832 function vboWidgetBooksCalManageRatesRestr(wrapper) {
1833 var widget_instance = jQuery('#' + wrapper);
1834 if (!widget_instance.length) {
1835 return false;
1836 }
1837
1838 var room_id = widget_instance.find('.vbo-booskcal-roomid').val();
1839 if (!room_id) {
1840 // room ID must be selected
1841 widget_instance.find('.vbo-widget-booskcal-mday-ratesrestr').remove();
1842 return false;
1843 }
1844
1845 if (widget_instance.find('.vbo-widget-booskcal-mday-ratesrestr').length || !widget_instance.find('input[name="vbo-wbookscal-mng-rates-restr"]').prop('checked')) {
1846 // toggle off
1847 widget_instance.find('.vbo-widget-booskcal-mday-ratesrestr').remove();
1848 return false;
1849 }
1850
1851 // show loading
1852 widget_instance.find('.vbo-widget-bookscal-mngrates-toggle').prepend('<span class="vbo-widget-bookscal-mngrates-loading"><?php VikBookingIcons::e('circle-notch', 'fa-spin fa-fw'); ?></span> ');
1853
1854 // month initial date
1855 let from_date = widget_instance.attr('data-offset');
1856
1857 // the widget method to call
1858 var call_method = 'loadRoomRates';
1859
1860 // make the request
1861 VBOCore.doAjax(
1862 "<?php echo $this->getExecWidgetAjaxUri(); ?>",
1863 {
1864 widget_id: "<?php echo $this->getIdentifier(); ?>",
1865 call: call_method,
1866 return: 1,
1867 room_id: room_id,
1868 from_date: from_date,
1869 wrapper: wrapper,
1870 tmpl: "component",
1871 },
1872 (response) => {
1873 // hide loading
1874 widget_instance.find('.vbo-widget-bookscal-mngrates-loading').remove();
1875 try {
1876 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
1877 if (!obj_res.hasOwnProperty(call_method)) {
1878 console.error('Unexpected JSON response', obj_res);
1879 return false;
1880 }
1881
1882 for (const [day, rrestr] of Object.entries(obj_res[call_method]['rates_restrictions'])) {
1883 // get day element
1884 let day_elem = widget_instance.find('.vbo-widget-booskcal-cell-mday[data-ymd="' + day + '"]');
1885
1886 if (!day_elem.length) {
1887 continue;
1888 }
1889
1890 // build HTML
1891 let is_cta = false;
1892 let is_ctd = false;
1893 let rrestr_html = '<div class="vbo-widget-booskcal-mday-ratesrestr">';
1894 rrestr_html += '<div class="vbo-widget-booskcal-mday-roomrate" data-cost="' + rrestr.cost + '">';
1895 rrestr_html += '<span><?php echo $currencysymb; ?> ' + rrestr.formatted_cost + '</span>';
1896 rrestr_html += '</div>';
1897 if ((rrestr.restrictions.minlos || 0) > 0 || rrestr.restrictions.cta || rrestr.restrictions.ctd) {
1898 // display restrictions
1899 is_cta = rrestr.restrictions.cta && rrestr.restrictions.cta.includes('-' + day_elem.attr('data-wday') + '-');
1900 is_ctd = rrestr.restrictions.ctd && rrestr.restrictions.ctd.includes('-' + day_elem.attr('data-wday') + '-');
1901 // build HTML
1902 rrestr_html += '<div class="vbo-widget-booskcal-mday-restrictions" data-minlos="' + (rrestr.restrictions.minlos || 0) + '" data-cta="' + (is_cta ? '1' : '0') + '" data-ctd="' + (is_ctd ? '1' : '0') + '">';
1903 if ((rrestr.restrictions.minlos || 0) > 0) {
1904 // display minimum stay
1905 rrestr_html += '<span>' + Joomla.JText._('VBO_MIN_STAY_SHORT') + ' ' + rrestr.restrictions.minlos + '</span>';
1906 }
1907 if (is_cta) {
1908 // display CTA
1909 rrestr_html += '<span><?php VikBookingIcons::e('ban'); ?> ' + Joomla.JText._('VBO_CTA_SHORT') + '</span>';
1910 }
1911 if (is_ctd) {
1912 // display CTD
1913 rrestr_html += '<span><?php VikBookingIcons::e('ban'); ?> ' + Joomla.JText._('VBO_CTD_SHORT') + '</span>';
1914 }
1915 rrestr_html += '</div>';
1916 }
1917 rrestr_html += '</div>';
1918
1919 // populate cell data attributes
1920 day_elem.attr('data-cta', is_cta ? '1' : '0');
1921 day_elem.attr('data-ctd', is_ctd ? '1' : '0');
1922
1923 // append day-rates-restrictions element
1924 day_elem.append(rrestr_html);
1925 }
1926 } catch(err) {
1927 console.error('could not parse JSON response', err, response);
1928 }
1929 },
1930 (error) => {
1931 // an error occurred
1932 widget_instance.find('.vbo-widget-booskcal-mday-ratesrestr').remove();
1933 widget_instance.find('input[name="vbo-wbookscal-mng-rates-restr"]').prop('checked', false);
1934 // hide loading
1935 widget_instance.find('.vbo-widget-bookscal-mngrates-loading').remove();
1936 // display error
1937 alert(error.responseText || 'An error occurred');
1938 }
1939 );
1940 }
1941
1942 /**
1943 * Perform the request to load the month-day reservations.
1944 */
1945 function vboWidgetBooksCalGetMdayRes(wrapper, ymd) {
1946 var widget_instance = jQuery('#' + wrapper);
1947 if (!widget_instance.length || !ymd) {
1948 return false;
1949 }
1950
1951 // get vars for making the request
1952 var room_id = widget_instance.find('.vbo-booskcal-roomid').val();
1953 var page_offset = widget_instance.find('.vbo-widget-booskcal-mday-list').attr('data-offset');
1954 var page_length = widget_instance.find('.vbo-widget-booskcal-mday-list').attr('data-length');
1955
1956 // the widget method to call
1957 var call_method = 'loadMdayBookings';
1958
1959 // make a request to load the bookings calendar
1960 VBOCore.doAjax(
1961 "<?php echo $this->getExecWidgetAjaxUri(); ?>",
1962 {
1963 widget_id: "<?php echo $this->getIdentifier(); ?>",
1964 call: call_method,
1965 return: 1,
1966 page_offset: page_offset,
1967 page_length: page_length,
1968 ymd: ymd,
1969 room_id: room_id,
1970 wrapper: wrapper,
1971 tmpl: "component"
1972 },
1973 (response) => {
1974 try {
1975 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
1976 if (!obj_res.hasOwnProperty(call_method)) {
1977 console.error('Unexpected JSON response', obj_res);
1978 return false;
1979 }
1980 // replace HTML with month-day reservations
1981 widget_instance.find('.vbo-widget-booskcal-mday-list').html(obj_res[call_method]['html']);
1982 } catch(err) {
1983 console.error('could not parse JSON response', err, response);
1984 }
1985 },
1986 (error) => {
1987 // remove the skeleton loading
1988 widget_instance.find('.vbo-widget-booskcal-mday-list').html('');
1989 console.error(error);
1990 }
1991 );
1992 }
1993
1994 /**
1995 * Navigate between the months and load the bookings calendar.
1996 */
1997 function vboWidgetBookCalsMonthNav(wrapper, direction) {
1998 var widget_instance = jQuery('#' + wrapper);
1999 if (!widget_instance.length) {
2000 return false;
2001 }
2002
2003 // show loading skeletons
2004 vboWidgetBooksCalSkeletons(wrapper);
2005
2006 // launch dates navigation and load records
2007 vboWidgetBooksCalLoad(wrapper, direction);
2008 }
2009
2010 /**
2011 * Change room calendar.
2012 */
2013 function vboWidgetBookCalsSetRoom(wrapper, rid) {
2014 var widget_instance = jQuery('#' + wrapper);
2015 if (!widget_instance.length) {
2016 return false;
2017 }
2018
2019 // check if we are creating a new booking
2020 if (widget_instance.find('.vbo-widget-booskcal-newbook-wrap').is(':visible')) {
2021 // check room units
2022 var vbo_bookscal_roomfilt = widget_instance.find('.vbo-booskcal-roomid');
2023 if (rid) {
2024 // manage units depending on selected room total units
2025 var vbo_bookscal_room_units = vbo_bookscal_roomfilt.find('option:selected').attr('data-units');
2026 if (vbo_bookscal_room_units > 1) {
2027 widget_instance.find('.vbo-widget-bookscal-units').attr('max', vbo_bookscal_room_units).val('1').closest('[data-noclosure="1"]').show();
2028 } else {
2029 widget_instance.find('.vbo-widget-bookscal-units').val('1').closest('[data-noclosure="1"]').hide();
2030 }
2031 } else {
2032 // hide units when no room is selected
2033 vbo_bookscal_roomfilt.closest('[data-noclosure="1"]').hide();
2034 }
2035
2036 // check the closure status
2037 if (widget_instance.find('input[name="closeroom"]').prop('checked')) {
2038 // reset values
2039 widget_instance.find('.vbo-widget-bookscal-units').val('1');
2040 widget_instance.find('[data-noclosure="1"]').hide();
2041 }
2042
2043 // attempt to update the website rates
2044 vboWidgetBooksCalGetWebsiteRates(wrapper);
2045
2046 // do nothing else when adding a new booking
2047 return;
2048 }
2049
2050 // show loading skeletons
2051 vboWidgetBooksCalSkeletons(wrapper);
2052
2053 // let the records be loaded for this new room filter
2054 vboWidgetBooksCalLoad(wrapper, 0);
2055 }
2056
2057 /**
2058 * Generate the HTML skeleton string to the month-day reservations.
2059 */
2060 function vboWidgetBooksCalMdaySkeleton() {
2061 var monthday_loading = '';
2062 monthday_loading += '<div class="vbo-dashboard-guest-activity vbo-dashboard-guest-activity-skeleton">';
2063 monthday_loading += ' <div class="vbo-dashboard-guest-activity-avatar">';
2064 monthday_loading += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-avatar"></div>';
2065 monthday_loading += ' </div>';
2066 monthday_loading += ' <div class="vbo-dashboard-guest-activity-content">';
2067 monthday_loading += ' <div class="vbo-dashboard-guest-activity-content-head">';
2068 monthday_loading += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-title"></div>';
2069 monthday_loading += ' </div>';
2070 monthday_loading += ' <div class="vbo-dashboard-guest-activity-content-subhead">';
2071 monthday_loading += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-subtitle"></div>';
2072 monthday_loading += ' </div>';
2073 monthday_loading += ' <div class="vbo-dashboard-guest-activity-content-info-msg">';
2074 monthday_loading += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-content"></div>';
2075 monthday_loading += ' </div>';
2076 monthday_loading += ' </div>';
2077 monthday_loading += '</div>';
2078
2079 return monthday_loading;
2080 }
2081
2082 /**
2083 * Enter the month-day view mode from monthly view.
2084 */
2085 function vboWidgetBooksCalMday(wrapper, element) {
2086 var widget_instance = jQuery('#' + wrapper);
2087 if (!widget_instance.length) {
2088 return false;
2089 }
2090
2091 // get cell element
2092 var cell = jQuery(element);
2093 if (!cell || !cell.length) {
2094 return false;
2095 }
2096
2097 // set month-day title
2098 var day_read = cell.attr('data-dayread');
2099 widget_instance.find('.vbo-widget-booskcal-mday-name').text(day_read);
2100
2101 // get cell ymd value
2102 var day_ymd = cell.attr('data-ymd');
2103
2104 // always update the proper ymd day
2105 widget_instance.find('.vbo-widget-booskcal-mday-list').attr('data-ymd', day_ymd);
2106
2107 // get pre-loaded booking ids
2108 var tot_day_res = 0;
2109 var day_bids = cell.attr('data-bids');
2110 if (day_bids && day_bids.length) {
2111 tot_day_res = day_bids.split(',').length;
2112 }
2113
2114 // populate loading skeletons for month-day bookings
2115 var monthday_loading = vboWidgetBooksCalMdaySkeleton();
2116 if (tot_day_res > 1) {
2117 // double up the loading skeletons
2118 monthday_loading = monthday_loading + monthday_loading;
2119 }
2120 widget_instance.find('.vbo-widget-booskcal-mday-list').html(monthday_loading);
2121
2122 // toggle elements
2123 widget_instance.find('.vbo-widget-booskcal-calendar-table-wrap').hide();
2124 widget_instance.find('.vbo-widget-booskcal-newbook-wrap').hide();
2125 widget_instance.find('.vbo-widget-booskcal-mday-wrap').show();
2126
2127 // check for room rates management
2128 let cell_rates_restr = cell.find('.vbo-widget-booskcal-mday-ratesrestr');
2129 if (cell_rates_restr.length) {
2130 // build day-room pricing details
2131 let room_rates_restr = {
2132 cost: cell_rates_restr.find('.vbo-widget-booskcal-mday-roomrate').attr('data-cost'),
2133 min_los: 0,
2134 is_cta: 0,
2135 is_ctd: 0,
2136 };
2137
2138 // check for room restrictions for this day
2139 let cell_restrictions = cell_rates_restr.find('.vbo-widget-booskcal-mday-restrictions');
2140 if (cell_restrictions.length) {
2141 room_rates_restr['min_los'] = parseInt(cell_restrictions.attr('data-minlos'));
2142 room_rates_restr['is_cta'] = parseInt(cell_restrictions.attr('data-cta'));
2143 room_rates_restr['is_ctd'] = parseInt(cell_restrictions.attr('data-ctd'));
2144 }
2145
2146 // populate room pricing details
2147 let mday_pricing_cell = widget_instance.find('.vbo-widget-booskcal-mday-pricing');
2148 mday_pricing_cell.find('.vbo-widget-booskcal-mday-pricing-cost').text(room_rates_restr['cost']);
2149 mday_pricing_cell.find('.vbo-widget-booskcal-mday-pricing-minlos').text(room_rates_restr['min_los']);
2150 let ctad_data = [];
2151 if (room_rates_restr['is_cta']) {
2152 ctad_data.push(Joomla.JText._('VBO_CTA_SHORT'));
2153 }
2154 if (room_rates_restr['is_ctd']) {
2155 ctad_data.push(Joomla.JText._('VBO_CTD_SHORT'));
2156 }
2157 mday_pricing_cell.find('.vbo-widget-booskcal-mday-pricing-ctad').text(ctad_data.join(', '));
2158
2159 // display room pricing details
2160 mday_pricing_cell.show();
2161
2162 // show loading
2163 mday_pricing_cell.find('.vbo-widget-booskcal-mday-pricing-title').find('span').append(' <span class="vbo-widget-bookscal-mday-mngrates-loading"><?php VikBookingIcons::e('circle-notch', 'fa-spin fa-fw'); ?></span>');
2164
2165 // the widget method to call
2166 var call_method = 'loadRoomDayRatePlans';
2167
2168 // make a request to load the bookings calendar
2169 VBOCore.doAjax(
2170 "<?php echo $this->getExecWidgetAjaxUri(); ?>",
2171 {
2172 widget_id: "<?php echo $this->getIdentifier(); ?>",
2173 call: call_method,
2174 return: 1,
2175 ymd: day_ymd,
2176 room_id: widget_instance.find('.vbo-booskcal-roomid').val(),
2177 wrapper: wrapper,
2178 tmpl: "component"
2179 },
2180 (response) => {
2181 // stop loading
2182 mday_pricing_cell.find('.vbo-widget-bookscal-mday-mngrates-loading').remove();
2183 try {
2184 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
2185 if (!obj_res.hasOwnProperty(call_method) || !obj_res[call_method]['room_rates'].hasOwnProperty(day_ymd)) {
2186 console.error('Unexpected JSON response', obj_res);
2187 return false;
2188 }
2189
2190 // remove the pricing node that will be rebuilt
2191 mday_pricing_cell.find('.vbo-widget-booskcal-mday-pricing-data-cost').remove();
2192
2193 // display the cost for each rate plan and related name
2194 obj_res[call_method]['room_rates'][day_ymd].forEach((tariff, index) => {
2195 // build rate plan HTML
2196 let day_rplan_html = '<div class="vbo-widget-booskcal-mday-pricing-data-cost" data-rplan-id="' + tariff['idprice'] + '" data-ymd="' + day_ymd + '" data-index="' + index + '">';
2197 // check for derived rate data
2198 let derived_info = '';
2199 if (obj_res[call_method]['derived_rates'].hasOwnProperty(tariff['idprice'])) {
2200 // build derived information HTML string
2201 derived_info = '<span class="badge badge-warning" title="' + obj_res[call_method]['derived_rates'][tariff['idprice']]['derived_info'] + '"><?php VikBookingIcons::e('link'); ?></span> ';
2202 }
2203 // keep building
2204 day_rplan_html += '<span class="vbo-widget-booskcal-mday-pricing-rplan">' + derived_info + tariff['name'] + '</span> ';
2205 day_rplan_html += '<span class="vbo-widget-booskcal-mday-pricing-currency"><?php echo VikBooking::getCurrencySymb(); ?></span> ';
2206 day_rplan_html += '<span class="vbo-widget-booskcal-mday-pricing-cost">' + tariff['formatted_cost'] + '</span> ';
2207 day_rplan_html += '<a class="vbo-widget-booskcal-mday-pricing-edit" href="JavaScript: void(0);" onclick="vboWidgetBooksCalToggleEditSetRate(this, \'' + wrapper + '\');">' + Joomla.JText._('VBMAINPAYMENTSEDIT') + '</a>';
2208 // edit container
2209 day_rplan_html += '<div class="vbo-widget-booskcal-mday-pricing-edit-wrap" style="display: none;">';
2210 day_rplan_html += ' <div class="vbo-widget-booskcal-mday-pricing-edit-block vbo-widget-booskcal-mday-pricing-edit-cost">';
2211 day_rplan_html += ' <label>' + Joomla.JText._('VBRATESOVWSETNEWRATE') + '</label>';
2212 day_rplan_html += ' <div class="vbo-widget-booskcal-mday-pricing-edit-input vbo-input-currency-wrap">';
2213 day_rplan_html += ' <span class="vbo-widget-booskcal-mday-pricing-edit-currency"><?php echo VikBooking::getCurrencySymb(); ?></span> ';
2214 day_rplan_html += ' <input class="vbo-widget-booskcal-mday-pricing-edit-newcost" type="number" value="' + tariff['cost'] + '" min="0" />';
2215 day_rplan_html += ' </div>';
2216 day_rplan_html += ' </div>';
2217 day_rplan_html += ' <div class="vbo-widget-booskcal-mday-pricing-edit-block vbo-widget-booskcal-mday-pricing-edit-minlos">';
2218 day_rplan_html += ' <label>' + Joomla.JText._('VBOMINIMUMSTAYSET') + '</label>';
2219 day_rplan_html += ' <div class="vbo-widget-booskcal-mday-pricing-edit-input">';
2220 day_rplan_html += ' <input class="vbo-widget-booskcal-mday-pricing-edit-newminlos" type="number" value="' + tariff['restrictions']['minlos'] + '" min="0" max="99" />';
2221 day_rplan_html += ' </div>';
2222 day_rplan_html += ' </div>';
2223 day_rplan_html += ' <div class="vbo-widget-booskcal-mday-pricing-edit-block vbo-widget-booskcal-mday-pricing-edit-otas">';
2224 day_rplan_html += ' <label for="vbo-widget-booskcal-mday-pricing-edit-updotas-' + index + '">' + Joomla.JText._('VBOUPDRATESONCHANNELS') + '</label>';
2225 day_rplan_html += ' <div class="vbo-widget-booskcal-mday-pricing-edit-input">';
2226 day_rplan_html += ' <input class="vbo-widget-booskcal-mday-pricing-edit-updotas" id="vbo-widget-booskcal-mday-pricing-edit-updotas-' + index + '" type="checkbox" value="1" checked />';
2227 day_rplan_html += ' </div>';
2228 day_rplan_html += ' </div>';
2229 day_rplan_html += ' <div class="vbo-widget-booskcal-mday-pricing-edit-block vbo-widget-booskcal-mday-pricing-edit-save">';
2230 day_rplan_html += ' <button type="button" class="btn vbo-btn-black" onclick="vboWidgetBooksCalSetRateRestriction(this, \'' + wrapper + '\');">' + Joomla.JText._('VBAPPLY') + '</button>';
2231 day_rplan_html += ' </div>';
2232 day_rplan_html += '</div>';
2233 // close block
2234 day_rplan_html += '</div>';
2235
2236 // update restrictions from the AJAX response data
2237 mday_pricing_cell.find('.vbo-widget-booskcal-mday-pricing-minlos').text(tariff['restrictions']['minlos']);
2238 let ctad_data = [];
2239 if (tariff['restrictions']['cta'] && tariff['restrictions']['cta'].includes('-' + obj_res[call_method]['wday'] + '-')) {
2240 ctad_data.push(Joomla.JText._('VBO_CTA_SHORT'));
2241 }
2242 if (tariff['restrictions']['ctd'] && tariff['restrictions']['ctd'].includes('-' + obj_res[call_method]['wday'] + '-')) {
2243 ctad_data.push(Joomla.JText._('VBO_CTD_SHORT'));
2244 }
2245 mday_pricing_cell.find('.vbo-widget-booskcal-mday-pricing-ctad').text(ctad_data.join(', '));
2246
2247 // prepend pricing information block
2248 mday_pricing_cell.find('.vbo-widget-booskcal-mday-pricing-data').prepend(day_rplan_html);
2249 });
2250
2251 // replace raw checkbox with the hidden toggle button
2252 setTimeout(() => {
2253 widget_instance.find('.vbo-widget-booskcal-mday-pricing-data-cost').each(function(index, block) {
2254 // obtain the proper index to use for this rate plan
2255 let block_index = jQuery(this).attr('data-index');
2256
2257 // build the identifier string value for this toggle button
2258 let toggle_id = 'vbo-widget-booskcal-mday-pricing-edit-updotas-' + block_index;
2259
2260 // get toggle button element
2261 let nice_toggle_el = widget_instance.find('.vbo-widget-booskcal-mday-hidden-helper-togglebtn').first().clone();
2262
2263 // adjust attributes to make them unique
2264 nice_toggle_el.find('input').addClass('vbo-widget-booskcal-mday-pricing-edit-updotas').attr('id', toggle_id);
2265 nice_toggle_el.find('label').attr('for', toggle_id);
2266
2267 // replace checkbox element with toggle button element
2268 jQuery(this)
2269 .find('.vbo-widget-booskcal-mday-pricing-edit-otas')
2270 .find('input.vbo-widget-booskcal-mday-pricing-edit-updotas')
2271 .replaceWith(nice_toggle_el);
2272 });
2273 }, 100);
2274 } catch(err) {
2275 console.error('could not parse JSON response', err, response);
2276 }
2277 },
2278 (error) => {
2279 // stop loading
2280 mday_pricing_cell.find('.vbo-widget-bookscal-mday-mngrates-loading').remove();
2281 // display error
2282 alert(error.responseText || 'An error occurred while loading the rate plan details.');
2283 }
2284 );
2285 } else {
2286 // make sure to hide the room pricing details
2287 widget_instance.find('.vbo-widget-booskcal-mday-pricing').hide();
2288 }
2289
2290 // launch month-day bookings retrieval
2291 vboWidgetBooksCalGetMdayRes(wrapper, day_ymd);
2292 }
2293
2294 /**
2295 * Applies the new rate and restriction set for a room rate.
2296 */
2297 function vboWidgetBooksCalSetRateRestriction(elem, wrapper) {
2298 var widget_instance = jQuery('#' + wrapper);
2299 if (!widget_instance.length) {
2300 return false;
2301 }
2302
2303 // the clicked element
2304 let btn = jQuery(elem);
2305
2306 // the rate plan block
2307 let rplan_block = btn.closest('.vbo-widget-booskcal-mday-pricing-data-cost');
2308
2309 // gather the rate plan ID
2310 let rplan_id = rplan_block.attr('data-rplan-id');
2311
2312 // disable button to prevent duplicate triggers
2313 btn.prop('disabled', true);
2314
2315 // start loading
2316 btn.append(' <?php VikBookingIcons::e('circle-notch', 'fa-spin fa-fw'); ?>');
2317
2318 // the widget method to call
2319 var call_method = 'setRoomDayRateRestiction';
2320
2321 // make a request to load the bookings calendar
2322 VBOCore.doAjax(
2323 "<?php echo $this->getExecWidgetAjaxUri(); ?>",
2324 {
2325 widget_id: "<?php echo $this->getIdentifier(); ?>",
2326 call: call_method,
2327 return: 1,
2328 ymd: rplan_block.attr('data-ymd'),
2329 rplan_id: rplan_id,
2330 room_id: widget_instance.find('.vbo-booskcal-roomid').val(),
2331 rate: rplan_block.find('.vbo-widget-booskcal-mday-pricing-edit-newcost').val(),
2332 minlos: rplan_block.find('.vbo-widget-booskcal-mday-pricing-edit-newminlos').val(),
2333 updotas: (rplan_block.find('.vbo-widget-booskcal-mday-pricing-edit-updotas').prop('checked') ? 1 : 0),
2334 wrapper: wrapper,
2335 tmpl: "component"
2336 },
2337 (response) => {
2338 // restore initial values
2339 btn.prop('disabled', false);
2340 btn.find('i').remove();
2341 try {
2342 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
2343 if (!obj_res.hasOwnProperty(call_method)) {
2344 console.error('Unexpected JSON response', obj_res);
2345 return false;
2346 }
2347
2348 // set new price
2349 rplan_block.find('.vbo-widget-booskcal-mday-pricing-cost').text(
2350 rplan_block.find('.vbo-widget-booskcal-mday-pricing-edit-newcost').val()
2351 );
2352
2353 // set success icon
2354 btn.append(' <?php VikBookingIcons::e('check-circle'); ?>');
2355
2356 // remove any rate/restriction data from the monthly view
2357 widget_instance.find('.vbo-widget-booskcal-mday-ratesrestr').remove();
2358
2359 // check if we should render the Channel Manager update result
2360 if (obj_res[call_method].hasOwnProperty('new_rates') && obj_res[call_method]['new_rates'].hasOwnProperty('vcm')) {
2361 // render results
2362 vboWidgetBooksCalRenderChannelManagerResult(obj_res[call_method]['new_rates']['vcm']);
2363 }
2364
2365 // finalize the operation by going back to the monthly view with a little timeout
2366 setTimeout(() => {
2367 // reload rates and restrictions
2368 vboWidgetBooksCalManageRatesRestr(wrapper);
2369
2370 // navigate back to the monthly view
2371 vboWidgetBooksCalMonth(wrapper);
2372 }, 200);
2373 } catch(err) {
2374 console.error('could not parse JSON response', err, response);
2375 }
2376 },
2377 (error) => {
2378 // restore initial values
2379 btn.prop('disabled', false);
2380 btn.find('i').remove();
2381 // display error
2382 alert(error.responseText || 'An error occurred while applying the room rate and restriction.');
2383 }
2384 );
2385 }
2386
2387 /**
2388 * Renders the results of the Channel Manager update request for rates/restrictions.
2389 * Supports multiple rate plans due to derived/linkage rules.
2390 */
2391 function vboWidgetBooksCalRenderChannelManagerResult(vcm_response) {
2392 if (!Array.isArray(vcm_response)) {
2393 // make sure the result is a list of result objects
2394 vcm_response = [vcm_response];
2395 }
2396
2397 // compose modal body
2398 var htmlres = '<div class="vbo-vcm-rates-res-container">';
2399
2400 vcm_response.forEach((obj) => {
2401 htmlres += '<div class="vbo-vcm-rates-res-rplan-wrap">';
2402
2403 if (obj.hasOwnProperty('rplan_name')) {
2404 htmlres += '<div class="vbo-vcm-rates-res-rplan-data">';
2405 htmlres += '<strong>' + obj['rplan_name'] + '</strong>';
2406 if (obj.hasOwnProperty('is_derived') && obj['is_derived']) {
2407 htmlres += ' <span class="label label-info">' + Joomla.JText._('VBO_IS_DERIVED_RATE') + '</span>';
2408 }
2409 htmlres += '</div>';
2410 }
2411
2412 if (obj.hasOwnProperty('channels_success')) {
2413 htmlres += '<div class="vbo-vcm-rates-res-success">';
2414 for (var ch_id in obj['channels_success']) {
2415 htmlres += '<div class="vbo-vcm-rates-res-channel">';
2416 htmlres += ' <div class="vbo-vcm-rates-res-channel-esit">';
2417 htmlres += ' <i class="<?php echo VikBookingIcons::i('check'); ?>"></i>';
2418 htmlres += ' </div>';
2419 htmlres += ' <div class="vbo-vcm-rates-res-channel-logo">';
2420 if (obj['channels_updated'].hasOwnProperty(ch_id) && obj['channels_updated'][ch_id]['logo'].length) {
2421 htmlres += '<img src="'+obj['channels_updated'][ch_id]['logo']+'" />';
2422 } else {
2423 htmlres += '<span>'+obj['channels_success'][ch_id]+'</span>';
2424 }
2425 htmlres += ' </div>';
2426 htmlres += '</div>';
2427 }
2428
2429 if (obj.hasOwnProperty('channels_bkdown')) {
2430 htmlres += '<div class="vbo-vcm-rates-res-bkdown">';
2431 htmlres += ' <div><pre>'+obj['channels_bkdown']+'</pre></div>';
2432 htmlres += '</div>';
2433 }
2434 htmlres += '</div>';
2435 }
2436
2437 if (obj.hasOwnProperty('channels_warnings')) {
2438 htmlres += '<div class="vbo-vcm-rates-res-warning">';
2439 for (var ch_id in obj['channels_warnings']) {
2440 htmlres += '<div class="vbo-vcm-rates-res-channel">';
2441 htmlres += ' <div class="vbo-vcm-rates-res-channel-esit">';
2442 htmlres += ' <i class="<?php echo VikBookingIcons::i('exclamation-triangle'); ?>"></i>';
2443 htmlres += ' </div>';
2444 htmlres += ' <div class="vbo-vcm-rates-res-channel-logo">';
2445 if (obj['channels_updated'].hasOwnProperty(ch_id) && obj['channels_updated'][ch_id]['logo'].length) {
2446 htmlres += '<img src="'+obj['channels_updated'][ch_id]['logo']+'" />';
2447 } else if (obj['channels_updated'].hasOwnProperty(ch_id)) {
2448 htmlres += '<span>'+obj['channels_updated'][ch_id]['name']+'</span>';
2449 }
2450 htmlres += ' </div>';
2451 htmlres += ' <div class="vbo-vcm-rates-res-channel-det">';
2452 htmlres += ' <pre>'+obj['channels_warnings'][ch_id]+'</pre>';
2453 htmlres += ' </div>';
2454 htmlres += '</div>';
2455 }
2456 htmlres += '</div>';
2457 }
2458
2459 if (obj.hasOwnProperty('channels_errors')) {
2460 htmlres += '<div class="vbo-vcm-rates-res-error">';
2461 for (var ch_id in obj['channels_errors']) {
2462 htmlres += '<div class="vbo-vcm-rates-res-channel">';
2463 htmlres += ' <div class="vbo-vcm-rates-res-channel-esit">';
2464 htmlres += ' <i class="<?php echo VikBookingIcons::i('times'); ?>"></i>';
2465 htmlres += ' </div>';
2466 htmlres += ' <div class="vbo-vcm-rates-res-channel-logo">';
2467 if (obj['channels_updated'].hasOwnProperty(ch_id) && obj['channels_updated'][ch_id]['logo'].length) {
2468 htmlres += ' <img src="'+obj['channels_updated'][ch_id]['logo']+'" />';
2469 } else if (obj['channels_updated'].hasOwnProperty(ch_id)) {
2470 htmlres += ' <span>'+obj['channels_updated'][ch_id]['name']+'</span>';
2471 }
2472 htmlres += ' </div>';
2473 htmlres += ' <div class="vbo-vcm-rates-res-channel-det">';
2474 htmlres += ' <pre>'+obj['channels_errors'][ch_id]+'</pre>';
2475 htmlres += ' </div>';
2476 htmlres += '</div>';
2477 }
2478 htmlres += '</div>';
2479 }
2480
2481 htmlres += '</div>';
2482 });
2483
2484 // close container
2485 htmlres += '</div>';
2486
2487 // display results within a modal window
2488 VBOCore.displayModal({
2489 suffix: 'vbo-vcm-rates-res',
2490 extra_class: 'vbo-modal-rounded vbo-modal-dialog',
2491 title: Joomla.JText._('VBOVCMRATESRES'),
2492 body: htmlres,
2493 draggable: true,
2494 });
2495 }
2496
2497 /**
2498 * Toggles the block for setting a new rate/restriction.
2499 */
2500 function vboWidgetBooksCalToggleEditSetRate(elem, wrapper) {
2501 let set_rate_block = jQuery(elem).closest('.vbo-widget-booskcal-mday-pricing-data-cost').find('.vbo-widget-booskcal-mday-pricing-edit-wrap');
2502 if (set_rate_block.is(':visible')) {
2503 // hide clicked block
2504 set_rate_block.hide();
2505 } else {
2506 // hide any other rate plan block
2507 jQuery('#' + wrapper).find('.vbo-widget-booskcal-mday-pricing-edit-wrap').hide();
2508 // show clicked block
2509 set_rate_block.show();
2510 }
2511 }
2512
2513 /**
2514 * Navigate between the various pages of the month-day bookings.
2515 */
2516 function vboWidgetBooksCalMdayNavigate(wrapper, direction) {
2517 var widget_instance = jQuery('#' + wrapper);
2518 if (!widget_instance.length) {
2519 return false;
2520 }
2521
2522 // get bookings container
2523 var bookings_list = widget_instance.find('.vbo-widget-booskcal-mday-list');
2524
2525 // show loading skeletons
2526 var monthday_loading = vboWidgetBooksCalMdaySkeleton();
2527 bookings_list.html(monthday_loading + monthday_loading);
2528
2529 // get current offset and length (MUST be numbers, not strings)
2530 var current_offset = parseInt(bookings_list.attr('data-offset'));
2531 var current_length = parseInt(bookings_list.attr('data-length'));
2532 var day_ymd = bookings_list.attr('data-ymd');
2533
2534 // check direction and update offsets for nav
2535 if (direction > 0) {
2536 // navigate forward
2537 bookings_list.attr('data-offset', (current_offset + current_length));
2538 } else {
2539 // navigate backward
2540 var new_offset = current_offset - current_length;
2541 new_offset = new_offset >= 0 ? new_offset : 0;
2542 bookings_list.attr('data-offset', new_offset);
2543 }
2544
2545 // launch month-day bookings retrieval
2546 vboWidgetBooksCalGetMdayRes(wrapper, day_ymd);
2547 }
2548
2549 /**
2550 * Go back to the montly/month-day view from the month-day/new-booking view.
2551 */
2552 function vboWidgetBooksCalMonth(wrapper) {
2553 var widget_instance = jQuery('#' + wrapper);
2554 if (!widget_instance.length) {
2555 return false;
2556 }
2557
2558 var current_ymd = '';
2559 var current_rid = '';
2560 if (widget_instance.find('.vbo-widget-booskcal-newbook-wrap').is(':visible')) {
2561 current_ymd = widget_instance.find('.vbo-widget-booskcal-newbook-wrap').attr('data-ymd');
2562 current_rid = widget_instance.find('.vbo-widget-booskcal-newbook-wrap').attr('data-roomid');
2563 if (current_rid != widget_instance.find('.vbo-booskcal-roomid').val()) {
2564 // going back from the new-booking view detected a change of the room filter, so we do a reset
2565
2566 // show loading skeletons
2567 vboWidgetBooksCalSkeletons(wrapper);
2568
2569 // let the records be loaded for this new room filter
2570 vboWidgetBooksCalLoad(wrapper, 0);
2571
2572 // do not proceed
2573 return;
2574 }
2575 }
2576
2577 // toggle elements depending on the current view
2578 widget_instance.find('.vbo-widget-booskcal-newbook-wrap').hide().attr('data-ymd', '');
2579 if (current_ymd) {
2580 // back to the month-day view
2581 widget_instance.find('.vbo-widget-booskcal-calendar-table-wrap').hide();
2582 widget_instance.find('.vbo-widget-booskcal-mday-wrap').show();
2583 } else {
2584 // back to the monthly view
2585 widget_instance.find('.vbo-widget-booskcal-mday-wrap').hide();
2586 widget_instance.find('.vbo-widget-booskcal-mday-pricing').hide();
2587 widget_instance.find('.vbo-widget-booskcal-mday-pricing-edit-wrap').remove();
2588 widget_instance.find('.vbo-widget-booskcal-mday-list').html('').attr('data-ymd', '').attr('data-offset', '0');
2589 widget_instance.find('.vbo-widget-booskcal-calendar-table-wrap').show();
2590 }
2591 }
2592
2593 /**
2594 * Display the new booking interface by hiding the other elements.
2595 */
2596 function vboWidgetBookCalsNewBooking(wrapper) {
2597 var widget_instance = jQuery('#' + wrapper);
2598 if (!widget_instance.length) {
2599 return false;
2600 }
2601
2602 var current_ymd = '';
2603 var current_rid = widget_instance.find('.vbo-booskcal-roomid').val();
2604 if (widget_instance.find('.vbo-widget-booskcal-mday-wrap').is(':visible')) {
2605 current_ymd = widget_instance.find('.vbo-widget-booskcal-mday-list').attr('data-ymd');
2606 }
2607
2608 // toggle elements
2609 widget_instance.find('.vbo-widget-booskcal-calendar-table-wrap').hide();
2610 widget_instance.find('.vbo-widget-booskcal-mday-wrap').hide();
2611 widget_instance.find('.vbo-widget-booskcal-newbook-wrap').show().attr('data-ymd', current_ymd).attr('data-roomid', current_rid);
2612
2613 if (current_ymd) {
2614 // set the check-in date
2615 var ymd_parts = current_ymd.split('-');
2616 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)));
2617 // trigger the onSelect function in datepicker
2618 jQuery('.ui-datepicker-current-day').click();
2619 }
2620 }
2621
2622 /**
2623 * Toggle the room-closure status when creating a new booking.
2624 */
2625 function vboWidgetBooksCalClosure(wrapper) {
2626 var widget_instance = jQuery('#' + wrapper);
2627 if (!widget_instance.length) {
2628 return false;
2629 }
2630
2631 if (widget_instance.find('input[name="closeroom"]').prop('checked')) {
2632 widget_instance.find('.vbo-param-container[data-noclosure="1"]').hide();
2633 } else {
2634 widget_instance.find('.vbo-param-container[data-noclosure="1"]').show();
2635 // check selected room
2636 var vbo_bookscal_roomfilt = widget_instance.find('.vbo-booskcal-roomid');
2637 if (vbo_bookscal_roomfilt.val()) {
2638 var vbo_bookscal_room_units = vbo_bookscal_roomfilt.find('option:selected').attr('data-units');
2639 if (vbo_bookscal_room_units < 2) {
2640 widget_instance.find('.vbo-widget-bookscal-units').closest('[data-noclosure="1"]').hide();
2641 }
2642 } else {
2643 widget_instance.find('.vbo-widget-bookscal-units').closest('[data-noclosure="1"]').hide();
2644 }
2645 // hide website rates if not available
2646 widget_instance.find('.vbo-param-container[data-unavailable="1"]').hide();
2647 }
2648 }
2649
2650 /**
2651 * Open the modal to assign an existing or a new customer to the booking.
2652 */
2653 function vboWidgetBooksCalAssignCustomer(wrapper) {
2654 var widget_instance = jQuery('#' + wrapper);
2655 if (!widget_instance.length) {
2656 return false;
2657 }
2658
2659 var modal_body = VBOCore.displayModal({
2660 extra_class: 'vbo-modal-rounded vbo-modal-tall',
2661 title: Joomla.JText._('VBFILLCUSTFIELDS'),
2662 footer_left: '<button type="button" class="btn" onclick="VBOCore.emitEvent(\'vbo-widget-booskcal-assigncustomer-dismiss\');">' + Joomla.JText._('VBANNULLA') + '</button>',
2663 footer_right: '<button type="button" class="btn btn-success" onclick="vboWidgetBooksCalSetCustomer(\'' + wrapper + '\');">' + Joomla.JText._('VBAPPLY') + '</button>',
2664 dismiss_event: 'vbo-widget-booskcal-assigncustomer-dismiss',
2665 loading_event: 'vbo-widget-booskcal-assigncustomer-loading',
2666 onDismiss: (e) => {
2667 if (!e || !e.detail) {
2668 // no event data received, maybe the modal was simply dismissed
2669 return;
2670 }
2671
2672 // parse data received within the dismiss event
2673 try {
2674 let customer_data = JSON.parse(e.detail);
2675
2676 // set values received
2677 widget_instance.find('.vbo-widget-bookscal-custid').val((customer_data['id'] || ''));
2678 widget_instance.find('.vbo-widget-bookscal-custmail').val((customer_data['email'] || ''));
2679 widget_instance.find('.vbo-widget-bookscal-custdata').val((customer_data['data'] || ''));
2680 widget_instance.find('.vbo-widget-bookscal-country').val((customer_data['country'] || ''));
2681 widget_instance.find('.vbo-widget-bookscal-state').val((customer_data['state'] || ''));
2682 widget_instance.find('.vbo-widget-bookscal-phone').val((customer_data['phone'] || ''));
2683
2684 let tot_names = customer_data['nominatives'] ? customer_data['nominatives'].length : 0;
2685 if (tot_names > 1) {
2686 widget_instance.find('.vbo-widget-bookscal-tfname').val(customer_data['nominatives'][0]);
2687 widget_instance.find('.vbo-widget-bookscal-tlname').val(customer_data['nominatives'][1]);
2688 }
2689 if (tot_names) {
2690 widget_instance.find('.vbo-assign-customer').find('span').text(customer_data['nominatives'].join(' '));
2691 } else {
2692 widget_instance.find('.vbo-assign-customer').find('span').text(Joomla.JText._('VBFILLCUSTFIELDS'));
2693 }
2694 } catch(e) {
2695 // log the error and abort
2696 console.error(e);
2697 return;
2698 }
2699 },
2700 });
2701
2702 // start loading
2703 VBOCore.emitEvent('vbo-widget-booskcal-assigncustomer-loading');
2704
2705 // the widget method to call
2706 var call_method = 'displayCustomerFilling';
2707
2708 // make a request to load the bookings calendar
2709 VBOCore.doAjax(
2710 "<?php echo $this->getExecWidgetAjaxUri(); ?>",
2711 {
2712 widget_id: "<?php echo $this->getIdentifier(); ?>",
2713 call: call_method,
2714 return: 1,
2715 wrapper: wrapper,
2716 tmpl: "component"
2717 },
2718 (response) => {
2719 // stop loading
2720 VBOCore.emitEvent('vbo-widget-booskcal-assigncustomer-loading');
2721 try {
2722 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
2723 if (!obj_res.hasOwnProperty(call_method)) {
2724 // dismiss modal and abort
2725 VBOCore.emitEvent('vbo-widget-booskcal-assigncustomer-dismiss');
2726 console.error('Unexpected JSON response', obj_res);
2727 return false;
2728 }
2729
2730 // append modal content
2731 modal_body.append(obj_res[call_method]['html']);
2732 } catch(err) {
2733 console.error('could not parse JSON response', err, response);
2734 }
2735 },
2736 (error) => {
2737 // stop loading
2738 VBOCore.emitEvent('vbo-widget-booskcal-assigncustomer-loading');
2739 // display error
2740 alert(error.responseText);
2741 // dismiss modal
2742 VBOCore.emitEvent('vbo-widget-booskcal-assigncustomer-dismiss');
2743 }
2744 );
2745 }
2746
2747 /**
2748 * Applies the provided customer information when creating a new booking.
2749 */
2750 function vboWidgetBooksCalSetCustomer(wrapper) {
2751 var widget_instance = jQuery('#' + wrapper);
2752 if (!widget_instance.length) {
2753 return false;
2754 }
2755
2756 var cfields_cont = '';
2757 var cfield_vals = {
2758 id: jQuery('#vbo-widget-bookscal-cfield-custid' + wrapper).val(),
2759 email: '',
2760 phone: '',
2761 nominatives: [],
2762 country: '',
2763 state: '',
2764 data: '',
2765 };
2766
2767 jQuery('.vbo-calendar-cfields-filler[data-wrapper="' + wrapper + '"]').find('.vbo-calendar-cfield-entry').each(function() {
2768 var cfield_entry = jQuery(this);
2769 var cfield_name = cfield_entry.find('label').text();
2770 var cfield_input = cfield_entry.find('span').find('input');
2771 var cfield_textarea = cfield_entry.find('span').find('textarea');
2772 var cfield_select = cfield_entry.find('span').find('select.vbo-calendar-cfield-country');
2773 var cfield_state = cfield_entry.find('span').find('select.vbo-calendar-cfield-state');
2774 var cfield_cont = '';
2775 if (cfield_input.length) {
2776 cfield_cont = cfield_input.val();
2777 if (cfield_input.attr('data-isemail') == '1' && cfield_cont && cfield_cont.length) {
2778 cfield_vals['email'] = cfield_cont;
2779 }
2780 if (cfield_input.attr('data-isphone') == '1') {
2781 cfield_vals['phone'] = cfield_cont;
2782 }
2783 if (cfield_input.attr('data-isnominative') == '1') {
2784 cfield_vals['nominatives'].push(cfield_cont);
2785 }
2786 } else if (cfield_textarea.length) {
2787 cfield_cont = cfield_textarea.val();
2788 } else if (cfield_select.length) {
2789 cfield_cont = cfield_select.val();
2790 if (cfield_cont && cfield_cont.length) {
2791 var country_code = jQuery('option:selected', cfield_select).attr('data-ccode');
2792 if (country_code && country_code.length) {
2793 cfield_vals['country'] = country_code;
2794 }
2795 }
2796 } else if (cfield_state.length) {
2797 cfield_cont = cfield_state.val();
2798 cfield_vals['state'] = cfield_cont;
2799 }
2800 if (cfield_cont && cfield_cont.length) {
2801 cfields_cont += cfield_name + ": " + cfield_cont + "\r\n";
2802 }
2803 });
2804
2805 if (!cfields_cont.length) {
2806 // empty information
2807 alert(Joomla.JText._('VBO_PLEASE_FILL_FIELDS'));
2808
2809 // do not proceed
2810 return false;
2811 }
2812
2813 // clean up last new lines
2814 cfields_cont = cfields_cont.replace(/\r\n+$/, "");
2815
2816 // set raw customer data string
2817 cfield_vals['data'] = cfields_cont;
2818
2819 // dimiss the modal by injecting the customer object information
2820 VBOCore.emitEvent('vbo-widget-booskcal-assigncustomer-dismiss', JSON.stringify(cfield_vals));
2821 }
2822
2823 /**
2824 * Reloads a list of states according to the given country.
2825 */
2826 function vboWidgetBooksCalReloadStates(country_3_code, wrapper) {
2827 var states_elem = jQuery('.vbo-calendar-cfields-filler[data-wrapper="' + wrapper + '"]').find('select.vbo-calendar-cfield-state');
2828
2829 if (!states_elem.length || !country_3_code || !country_3_code.length) {
2830 return;
2831 }
2832
2833 // unset the current states/provinces
2834 states_elem.html('');
2835
2836 // make a request to load the states/provinces of the selected country
2837 VBOCore.doAjax(
2838 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=states.load_from_country'); ?>",
2839 {
2840 country_3_code: country_3_code,
2841 tmpl: "component"
2842 },
2843 (response) => {
2844 try {
2845 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
2846 if (!obj_res) {
2847 console.error('Unexpected JSON response', obj_res);
2848 return false;
2849 }
2850
2851 // append empty value
2852 states_elem.append('<option value="">-----</option>');
2853
2854 for (var i = 0; i < obj_res.length; i++) {
2855 // append state
2856 states_elem.append('<option value="' + obj_res[i]['state_2_code'] + '">' + obj_res[i]['state_name'] + '</option>');
2857 }
2858 } catch(err) {
2859 console.error('could not parse JSON response', err, response);
2860 }
2861 },
2862 (error) => {
2863 console.error(error);
2864 }
2865 );
2866 }
2867
2868 /**
2869 * Triggers when a custom field of type country is changed.
2870 */
2871 function vboWidgetBooksCalChangeCountry(elem, wrapper) {
2872 var sel_country = jQuery(elem).find('option:selected');
2873 if (!sel_country.length) {
2874 return false;
2875 }
2876
2877 // trigger event for phone number
2878 jQuery('.vbo-calendar-cfield-phone').trigger('vboupdatephonenumber', sel_country.attr('data-c2code'));
2879
2880 // reload state/province
2881 vboWidgetBooksCalReloadStates(sel_country.attr('data-ccode'), wrapper);
2882 }
2883
2884 /**
2885 * Calculates the website rates according to the input values.
2886 */
2887 function vboWidgetBooksCalGetWebsiteRates(wrapper) {
2888 var widget_instance = jQuery('#' + wrapper);
2889 if (!widget_instance.length) {
2890 return false;
2891 }
2892
2893 // unset previously selected rates, if any
2894 widget_instance.find('.vbo-widget-bookscal-roomcost').val('');
2895 widget_instance.find('.vbo-widget-bookscal-idprice').val('');
2896 widget_instance.find('.vbo-widget-bookscal-custcost').val('').attr('readonly', false);
2897 widget_instance.find('.vbo-cal-wbrate-wrap').removeClass('vbo-cal-wbrate-wrap-selected');
2898
2899 // gather values
2900 var is_closing = widget_instance.find('input[name="closeroom"]').prop('checked');
2901 var room_id = widget_instance.find('.vbo-booskcal-roomid').val();
2902 var checkinfdate = widget_instance.find('.vbo-widget-bookscal-checkindt').val();
2903 var checkoutfdate = widget_instance.find('.vbo-widget-bookscal-checkoutdt').val();
2904 var adults = widget_instance.find('.vbo-widget-bookscal-adults').val();
2905 var children = widget_instance.find('.vbo-widget-bookscal-children').val();
2906 var units = widget_instance.find('.vbo-widget-bookscal-units').val();
2907
2908 if (is_closing || !room_id || !checkinfdate || !checkoutfdate) {
2909 // do not proceed
2910 widget_instance.find('.vbo-website-rates-row').hide().attr('data-unavailable', '1');
2911 return;
2912 }
2913
2914 // tax settings
2915 var vbo_tax_included = <?php echo $prices_vat_included; ?>;
2916
2917 // make the request
2918 VBOCore.doAjax(
2919 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=calc_rates'); ?>",
2920 {
2921 id_room: room_id,
2922 checkinfdate: checkinfdate,
2923 checkoutfdate: checkoutfdate,
2924 num_nights: 0,
2925 num_adults: adults,
2926 num_children: children,
2927 units: units,
2928 only_rates: 1,
2929 tmpl: "component"
2930 },
2931 (resp) => {
2932 try {
2933 obj_res = typeof resp === 'string' ? JSON.parse(resp) : resp;
2934 if (!obj_res[0].hasOwnProperty('idprice')) {
2935 widget_instance.find('.vbo-website-rates-row').hide().attr('data-unavailable', '1');
2936 return false;
2937 }
2938
2939 // display the rates obtained
2940 var wrhtml = "";
2941 for (var i in obj_res) {
2942 if (!obj_res.hasOwnProperty(i)) {
2943 continue;
2944 }
2945 if (!vbo_tax_included && obj_res[i].hasOwnProperty('net') && obj_res[i].hasOwnProperty('fnet')) {
2946 obj_res[i]['tot'] = obj_res[i]['net'];
2947 obj_res[i]['ftot'] = obj_res[i]['fnet'];
2948 }
2949 wrhtml += "<div class=\"vbo-cal-wbrate-wrap\" onclick=\"vboWidgetBooksCalSelWebsiteRate(this, '" + wrapper + "');\">";
2950 wrhtml += " <div class=\"vbo-cal-wbrate-inner\">";
2951 wrhtml += " <span class=\"vbo-cal-wbrate-name\" data-idprice=\"" + obj_res[i]['idprice'] + "\">" + obj_res[i]['name'] + "</span>";
2952 wrhtml += " <span class=\"vbo-cal-wbrate-cost\" data-cost=\"" + obj_res[i]['tot'] + "\">" + obj_res[i]['ftot'] + "</span>";
2953 wrhtml += " </div>";
2954 wrhtml += "</div>";
2955 }
2956 widget_instance.find('.vbo-website-rates-cont').html(wrhtml);
2957 widget_instance.find('.vbo-website-rates-row').fadeIn().attr('data-unavailable', '0');
2958 } catch(err) {
2959 widget_instance.find('.vbo-website-rates-row').hide().attr('data-unavailable', '1');
2960 console.error("could not parse JSON response", resp);
2961 return false;
2962 }
2963 },
2964 (error) => {
2965 widget_instance.find('.vbo-website-rates-row').hide().attr('data-unavailable', '1');
2966 console.error("Error calculating the rates", error);
2967 }
2968 );
2969 }
2970
2971 /**
2972 * Attempts to display the tax rates drop down for the custom rate.
2973 */
2974 function vboWidgetBooksCalFocusTaxes(wrapper) {
2975 var widget_instance = jQuery('#' + wrapper);
2976 if (!widget_instance.length) {
2977 return false;
2978 }
2979
2980 var tax_rates_sel = widget_instance.find('.vbo-widget-bookscal-taxid');
2981 if (tax_rates_sel.length) {
2982 tax_rates_sel.show();
2983 }
2984 }
2985
2986 /**
2987 * Select a website rate plan.
2988 */
2989 function vboWidgetBooksCalSelWebsiteRate(elem, wrapper) {
2990 var widget_instance = jQuery('#' + wrapper);
2991 if (!widget_instance.length) {
2992 return false;
2993 }
2994
2995 var rate = jQuery(elem);
2996 var idprice = rate.find('.vbo-cal-wbrate-name').attr('data-idprice');
2997 var cost = rate.find('.vbo-cal-wbrate-cost').attr('data-cost');
2998 var prev_idprice = widget_instance.find('.vbo-widget-bookscal-idprice').val();
2999
3000 // reset all selected classes
3001 widget_instance.find('.vbo-cal-wbrate-wrap').removeClass('vbo-cal-wbrate-wrap-selected');
3002 if (prev_idprice && prev_idprice == idprice) {
3003 // rate plan has been de-selected
3004 widget_instance.find('.vbo-widget-bookscal-idprice').val("");
3005 widget_instance.find('.vbo-widget-bookscal-roomcost').val("");
3006 widget_instance.find('.vbo-widget-bookscal-custcost').attr('readonly', false);
3007 } else {
3008 // rate plan has been selected
3009 rate.addClass('vbo-cal-wbrate-wrap-selected');
3010 widget_instance.find('.vbo-widget-bookscal-idprice').val(idprice);
3011 widget_instance.find('.vbo-widget-bookscal-roomcost').val(cost);
3012 widget_instance.find('.vbo-widget-bookscal-custcost').attr('readonly', true);
3013 }
3014 }
3015
3016 /**
3017 * Save a new booking.
3018 */
3019 function vboWidgetBooksCalSaveBooking(wrapper) {
3020 var widget_instance = jQuery('#' + wrapper);
3021 if (!widget_instance.length) {
3022 return false;
3023 }
3024
3025 // gather all the necessary information
3026 var room_id = widget_instance.find('.vbo-booskcal-roomid').val();
3027 var checkin = widget_instance.find('.vbo-widget-bookscal-checkindt').val();
3028 var checkout = widget_instance.find('.vbo-widget-bookscal-checkoutdt').val();
3029 var closure = widget_instance.find('input[name="closeroom"]').prop('checked') ? 1 : 0;
3030 var units = widget_instance.find('.vbo-widget-bookscal-units').val();
3031 var adults = widget_instance.find('.vbo-widget-bookscal-adults').val();
3032 var children = widget_instance.find('.vbo-widget-bookscal-children').val();
3033 var cust_id = widget_instance.find('.vbo-widget-bookscal-custid').val();
3034 var cust_email = widget_instance.find('.vbo-widget-bookscal-custmail').val();
3035 var cust_data = widget_instance.find('.vbo-widget-bookscal-custdata').val();
3036 var cust_country = widget_instance.find('.vbo-widget-bookscal-country').val();
3037 var cust_state = widget_instance.find('.vbo-widget-bookscal-state').val();
3038 var cust_phone = widget_instance.find('.vbo-widget-bookscal-phone').val();
3039 var cust_tfname = widget_instance.find('.vbo-widget-bookscal-tfname').val();
3040 var cust_tlname = widget_instance.find('.vbo-widget-bookscal-tlname').val();
3041 var roomcost = widget_instance.find('.vbo-widget-bookscal-roomcost').val();
3042 var idprice = widget_instance.find('.vbo-widget-bookscal-idprice').val();
3043 var cust_roomcost = widget_instance.find('.vbo-widget-bookscal-custcost').val();
3044 var taxid = widget_instance.find('.vbo-widget-bookscal-taxid').val();
3045
3046 if (!room_id || !checkin || !checkout) {
3047 // missing information
3048 alert(Joomla.JText._('VBO_PLEASE_FILL_FIELDS'));
3049
3050 // abort
3051 return false;
3052 }
3053
3054 // check if suggesting to mark the rooms as closed is necessary
3055 var set_units_closed = 0;
3056 var current_room_units = 0;
3057 var vbo_bookscal_roomfilt = widget_instance.find('.vbo-booskcal-roomid');
3058 if (vbo_bookscal_roomfilt.val()) {
3059 var current_room_units = vbo_bookscal_roomfilt.find('option:selected').attr('data-units');
3060 }
3061 if (!cust_id && !cust_data && !closure && current_room_units > 1) {
3062 // no closure, no customer email, multiple units, ask if the units should be marked as closed
3063 if (confirm(Joomla.JText._('VBO_MARK_UNITS_CLOSED'))) {
3064 set_units_closed = 1;
3065 cust_data = Joomla.JText._('VBDBTEXTROOMCLOSED');
3066 }
3067 }
3068
3069 if (!set_units_closed && !cust_id && !cust_data && !closure) {
3070 // missing information
3071 alert(Joomla.JText._('VBO_PLEASE_FILL_FIELDS'));
3072
3073 // abort
3074 return false;
3075 }
3076
3077 // loading content
3078 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>';
3079
3080 // show loading
3081 widget_instance.find('.vbo-widget-booskcal-newbook-cont').prepend(loading_content);
3082
3083 // the widget method to call
3084 var call_method = 'saveBooking';
3085
3086 // make a request to save the booking
3087 VBOCore.doAjax(
3088 "<?php echo $this->getExecWidgetAjaxUri(); ?>",
3089 {
3090 widget_id: "<?php echo $this->getIdentifier(); ?>",
3091 call: call_method,
3092 return: 1,
3093 room_id: room_id,
3094 checkin: checkin,
3095 checkout: checkout,
3096 closure: closure,
3097 units_closed: set_units_closed,
3098 units: units,
3099 adults: adults,
3100 children: children,
3101 cust_id: cust_id,
3102 cust_email: cust_email,
3103 cust_data: cust_data,
3104 cust_country: cust_country,
3105 cust_state: cust_state,
3106 cust_phone: cust_phone,
3107 cust_tfname: cust_tfname,
3108 cust_tlname: cust_tlname,
3109 roomcost: roomcost,
3110 idprice: idprice,
3111 cust_roomcost: cust_roomcost,
3112 taxid: taxid,
3113 wrapper: wrapper,
3114 tmpl: "component"
3115 },
3116 (response) => {
3117 // stop loading
3118 widget_instance.find('.vbo-modal-overlay-content-backdrop').remove();
3119 try {
3120 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
3121 if (!obj_res.hasOwnProperty(call_method)) {
3122 console.error('Unexpected JSON response', obj_res);
3123 return false;
3124 }
3125
3126 // show last booking created
3127 widget_instance.find('.vbo-widget-booskcal-newbook-start').hide();
3128 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']);
3129
3130 // check if should suggest to run VCM
3131 if (obj_res[call_method]['vcm_action']) {
3132 widget_instance.append('<p class="info" onclick="jQuery(this).remove();">' + obj_res[call_method]['vcm_action'] + '</p>');
3133 }
3134
3135 // register the last booking ID created
3136 vbo_widget_books_cal_last_new_bid = obj_res[call_method]['new_booking_id'];
3137
3138 // reload bookings calendar
3139
3140 // show loading skeletons
3141 vboWidgetBooksCalSkeletons(wrapper);
3142
3143 // let the records be loaded for this new room filter
3144 vboWidgetBooksCalLoad(wrapper, 0);
3145 } catch(err) {
3146 console.error('could not parse JSON response', err, response);
3147 }
3148 },
3149 (error) => {
3150 // stop loading
3151 widget_instance.find('.vbo-modal-overlay-content-backdrop').remove();
3152 // display error
3153 console.error(error);
3154 alert(error.responseText);
3155 }
3156 );
3157 }
3158
3159 /**
3160 * Triggers when the newly created booking button is clicked.
3161 */
3162 function vboWidgetBookCalsOpenNewBooking(wrapper) {
3163 var widget_instance = jQuery('#' + wrapper);
3164 if (!widget_instance.length) {
3165 return false;
3166 }
3167
3168 var bid = widget_instance.find('.vbo-widget-booskcal-newbook-id').attr('data-bookingid');
3169 // open the booking
3170 vboWidgetBooksCalOpenBooking(bid);
3171
3172 // switch back to the regular button to create a new booking
3173 widget_instance.find('.vbo-widget-booskcal-newbook-id').hide();
3174 widget_instance.find('.vbo-widget-booskcal-newbook-start').show();
3175 }
3176
3177 /**
3178 * Toggles the cancelled reservations.
3179 */
3180 function vboWidgetBooksCalCancToggle(wrapper) {
3181 var widget_instance = jQuery('#' + wrapper);
3182 if (!widget_instance.length) {
3183 return false;
3184 }
3185
3186 widget_instance.find('[data-type="cancelled"]').toggle();
3187 }
3188
3189 /**
3190 * Triggers when the multitask panel opens.
3191 */
3192 function vboWidgetBooksCalMultitaskOpen(wrapper) {
3193 var widget_instance = jQuery('#' + wrapper);
3194 if (!widget_instance.length) {
3195 return false;
3196 }
3197
3198 // check if a booking ID was set for this page
3199 var page_bid = widget_instance.attr('data-pagebid');
3200 if (!page_bid || page_bid < 1) {
3201 return false;
3202 }
3203
3204 // show loading skeletons
3205 vboWidgetBooksCalSkeletons(wrapper);
3206
3207 // load data by injecting the current booking ID
3208 vboWidgetBooksCalLoad(wrapper, 0, page_bid);
3209 }
3210
3211 </script>
3212 <?php
3213 }
3214 ?>
3215
3216 <script type="text/javascript">
3217
3218 // store the lastly created booking ID
3219 var vbo_widget_books_cal_last_new_bid = null;
3220
3221 // store widget options, if any
3222 var vbo_widget_books_cal_options_oo = <?php echo json_encode($this->getOptions()); ?>;
3223
3224 jQuery(function() {
3225
3226 // when document is ready, load bookings calendar for this widget's instance
3227 vboWidgetBooksCalLoad('<?php echo $wrapper_id; ?>', 0, '<?php echo $modal_load_bid; ?>', <?php echo $load_room_rates ? 'true' : 'false'; ?>);
3228
3229 // convert the select to a Select2 element
3230 if (typeof jQuery.fn.select2 !== 'undefined' && <?php echo $use_nice_select; ?>) {
3231 jQuery('#<?php echo $wrapper_id; ?>').find('select.vbo-booskcal-roomid').select2({
3232 width: "100%",
3233 placeholder: "<?php echo htmlspecialchars(VikBooking::strTrimLiteral(JText::translate('VBOREPORTSROOMFILT'))); ?>",
3234 allowClear: true
3235 });
3236 }
3237
3238 // subscribe to the multitask-panel-open event
3239 document.addEventListener(VBOCore.multitask_open_event, function() {
3240 vboWidgetBooksCalMultitaskOpen('<?php echo $wrapper_id; ?>');
3241 });
3242
3243 // subscribe to the multitask-panel-close event to emit the event for the lastly created booking ID
3244 document.addEventListener(VBOCore.multitask_close_event, function() {
3245 if (vbo_widget_books_cal_last_new_bid) {
3246 // emit the event with data for anyone who is listening to it
3247 VBOCore.emitEvent('vbo_new_booking_created', {
3248 bid: vbo_widget_books_cal_last_new_bid
3249 });
3250 }
3251 });
3252
3253 <?php
3254 if ($js_modal_id) {
3255 // widget can be dismissed through the modal
3256 ?>
3257 // subscribe to the modal-dismissed event to emit the event for the lastly created booking ID
3258 document.addEventListener(VBOCore.widget_modal_dismissed + '<?php echo $js_modal_id; ?>', function() {
3259 if (vbo_widget_books_cal_last_new_bid) {
3260 // emit the event with data for anyone who is listening to it
3261 VBOCore.emitEvent('vbo_new_booking_created', {
3262 bid: vbo_widget_books_cal_last_new_bid
3263 });
3264 }
3265 });
3266 <?php
3267 }
3268 ?>
3269
3270 });
3271
3272 </script>
3273
3274 <?php
3275 }
3276
3277 /**
3278 * Helper method to load the booking cancellations for the given room and date.
3279 *
3280 * @param int $room_id the Vik Booking room ID.
3281 * @param string $ymd the current calendar date in Y-m-d format.
3282 *
3283 * @return array list of involved booking cancellations.
3284 *
3285 * @since 1.16.1 (J) - 1.6.1 (WP)
3286 */
3287 protected function loadCancellations($room_id, $ymd)
3288 {
3289 $dbo = JFactory::getDbo();
3290
3291 if (empty($room_id) || empty($ymd)) {
3292 return [];
3293 }
3294
3295 $stay_date_info = getdate(strtotime($ymd));
3296 $lim_ts_to = mktime(23, 59, 59, $stay_date_info['mon'], $stay_date_info['mday'], $stay_date_info['year']);
3297
3298 $q = $dbo->getQuery(true);
3299
3300 $q->select($dbo->qn('o') . '.*');
3301 $q->from($dbo->qn('#__vikbooking_orders', 'o'));
3302 $q->leftjoin($dbo->qn('#__vikbooking_ordersrooms', 'or') . ' ON ' . $dbo->qn('o.id') . ' = ' . $dbo->qn('or.idorder'));
3303 $q->where($dbo->qn('o.status') . ' = ' . $dbo->q('cancelled'));
3304 $q->where($dbo->qn('o.checkin') . ' <= ' . $lim_ts_to);
3305 $q->where($dbo->qn('o.checkout') . ' > ' . $lim_ts_to);
3306 $q->where($dbo->qn('or.idroom') . ' = ' . (int)$room_id);
3307 $q->group($dbo->qn('o.id'));
3308 $q->order($dbo->qn('o.id') . ' ASC');
3309
3310 $dbo->setQuery($q);
3311 $cancellations = $dbo->loadAssocList();
3312
3313 // join the customer information with a separate query as this is faster than joining two more tables at once
3314 foreach ($cancellations as &$canc_book) {
3315 $q = $dbo->getQuery(true);
3316
3317 $q->select($dbo->qn('co.idcustomer'));
3318 $q->select('CONCAT_WS(" ", ' . $dbo->qn('c.first_name') . ', ' . $dbo->qn('c.last_name') . ') AS ' . $dbo->qn('customer_fullname'));
3319 $q->select($dbo->qn('c.country', 'customer_country'));
3320 $q->select($dbo->qn('c.pic'));
3321 $q->from($dbo->qn('#__vikbooking_customers_orders', 'co'));
3322 $q->leftjoin($dbo->qn('#__vikbooking_customers', 'c') . ' ON ' . $dbo->qn('c.id') . ' = ' . $dbo->qn('co.idcustomer'));
3323 $q->where($dbo->qn('co.idorder') . ' = ' . (int)$canc_book['id']);
3324
3325 $dbo->setQuery($q);
3326 $customer_data = $dbo->loadAssoc();
3327
3328 if ($customer_data) {
3329 // merge properties
3330 $canc_book = array_merge($canc_book, $customer_data);
3331 }
3332 }
3333
3334 // unset last reference
3335 unset($canc_book);
3336
3337 // return the list of cancellation bookings
3338 return $cancellations;
3339 }
3340
3341 /**
3342 * When Multitask Data Options are injected with an overbooking flag, this
3343 * method loads the overbooking details and sets the proper option values.
3344 *
3345 * @param int $bid the overbooking reservation ID.
3346 *
3347 * @return void
3348 *
3349 * @since 1.16.8 (J) - 1.6.8 (WP)
3350 */
3351 protected function prepareOverbookingOptions($bid)
3352 {
3353 // get the booking details
3354 $booking_info = VikBooking::getBookingInfoFromID($bid);
3355 if (!$booking_info || strcasecmp((string) $booking_info['type'], 'overbooking')) {
3356 return;
3357 }
3358
3359 $booking_checkin_dt = date('Y-m-d', $booking_info['checkin']);
3360 $booking_checkout_dt = date('Y-m-d', $booking_info['checkout']);
3361
3362 // access the availability helper object
3363 $av_helper = VikBooking::getAvailabilityInstance()
3364 ->setStayDates($booking_checkin_dt, $booking_checkout_dt);
3365
3366 // get stay date timestamps
3367 list($checkin_ts, $checkout_ts) = $av_helper->getStayDates(true);
3368
3369 // count length of stay and nights involved
3370 $tot_nights = $av_helper->countNightsOfStay();
3371 $groupdays = VikBooking::getGroupDays($checkin_ts, $checkout_ts, $tot_nights);
3372
3373 // get all rooms involved
3374 $booking_rooms = VikBooking::loadOrdersRoomsData($booking_info['id']);
3375 $room_ids = array_unique(array_column($booking_rooms, 'idroom'));
3376
3377 // load all the occupied records for the involved rooms
3378 $busy_records = VikBooking::loadBusyRecords($room_ids, $checkin_ts, strtotime('+1 day', $checkout_ts));
3379
3380 // scan all rooms to find the first date in overbooking state
3381 foreach ($booking_rooms as $booking_room) {
3382 $room_id = $booking_room['idroom'];
3383 $room_data = VikBooking::getRoomInfo($room_id, ['units']);
3384 if (empty($room_data['units']) || !isset($busy_records[$room_id])) {
3385 // room is no longer on db or has got no occupied records
3386 continue;
3387 }
3388
3389 // scan all nights involved
3390 foreach ($groupdays as $gday) {
3391 // start counter
3392 $bfound = 0;
3393
3394 // scan all the occupied records
3395 foreach ($busy_records[$room_id] as $bu) {
3396 $busy_info_in = getdate($bu['checkin']);
3397 $busy_info_out = getdate($bu['checkout']);
3398 $busy_in_ts = mktime(0, 0, 0, $busy_info_in['mon'], $busy_info_in['mday'], $busy_info_in['year']);
3399 $busy_out_ts = mktime(0, 0, 0, $busy_info_out['mon'], $busy_info_out['mday'], $busy_info_out['year']);
3400 if ($gday >= $busy_in_ts && $gday <= $busy_out_ts) {
3401 // room is occupied
3402 $bfound++;
3403 }
3404
3405 // check if the room is overbooked
3406 if ($bfound > $room_data['units']) {
3407 // overbooking date found, inject data and abort
3408 $overbooking_dt = date('Y-m-d', $gday);
3409
3410 // set "offset" and "day" widget options
3411 $this->setOption('offset', $overbooking_dt);
3412 $this->setOption('day', $overbooking_dt);
3413
3414 // set room ID widget option
3415 $this->setOption('id_room', $room_id);
3416
3417 // abort for information found
3418 return;
3419 }
3420 }
3421 }
3422 }
3423
3424 // fallback onto setting the first values available
3425
3426 // set "offset" and "day" widget options
3427 $this->setOption('offset', $booking_checkin_dt);
3428 $this->setOption('day', $booking_checkin_dt);
3429
3430 $booking_rooms = VikBooking::loadOrdersRoomsData($booking_info['id']);
3431 foreach ($booking_rooms as $booking_room) {
3432 // set room ID widget option
3433 $this->setOption('id_room', $booking_room['idroom']);
3434
3435 break;
3436 }
3437 }
3438 }
3439