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

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