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

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