<?php /** * @package VikBooking * @subpackage com_vikbooking * @author Alessio Gaggii - e4j - Extensionsforjoomla.com * @copyright Copyright (C) 2018 e4j - Extensionsforjoomla.com. All rights reserved. * @license GNU General Public License version 2 or later; see LICENSE * @link https://vikwp.com */ defined('ABSPATH') or die('No script kiddies please!'); /** * Class handler for admin widget "bookings calendar". * * @since 1.15.0 (J) - 1.5.0 (WP) */ class VikBookingAdminWidgetBookingsCalendar extends VikBookingAdminWidget { /** * The instance counter of this widget. Since we do not load individual parameters * for each widget's instance, we use a static counter to determine its settings. * * @var int */ protected static $instance_counter = -1; /** * Default number of bookings per page. * * @var int */ protected $bookings_per_page = 6; /** * Class constructor will define the widget name and identifier. */ public function __construct() { // call parent constructor parent::__construct(); $this->widgetName = JText::translate('VBO_W_BOOKSCAL_TITLE'); $this->widgetDescr = JText::translate('VBO_W_BOOKSCAL_DESCR'); $this->widgetId = basename(__FILE__, '.php'); // define widget and icon and style name $this->widgetIcon = '<i class="' . VikBookingIcons::i('calendar') . '"></i>'; $this->widgetStyleName = 'brown'; } /** * Custom method for this widget only to load the bookings calendar. * The method is called by the admin controller through an AJAX request. * The visibility should be public, it should not exit the process, and * any content sent to output will be returned to the AJAX response. * In this case we return an array because this method requires "return":1. */ public function loadBookingsCalendar() { // get today's date and timestamp $today_ymd = date('Y-m-d'); $today_ts = time(); $wrapper = VikRequest::getString('wrapper', '', 'request'); $offset = VikRequest::getString('offset', $today_ymd, 'request'); $room_id = VikRequest::getInt('room_id', 0, 'request'); $date_dir = VikRequest::getInt('date_dir', 0, 'request'); $mng_rates_restr = VikRequest::getInt('mng_rates_restr', 0, 'request'); $bid = VikRequest::getInt('bid', 0, 'request'); $bid = !empty($date_dir) ? 0 : $bid; // the booking ID can be passed in case of multitask data for this page if (!empty($bid)) { // load the availability of the month when booking starts $booking_info = VikBooking::getBookingInfoFromID($bid); if ($booking_info) { // force the offset to use as start date $offset = date('Y-m-d', $booking_info['checkin']); } } // calculate date timestamps interval $now_info = getdate(strtotime($offset)); if ($date_dir > 0) { // next month from current offset $from_ts = mktime(0, 0, 0, ($now_info['mon'] + 1), 1, $now_info['year']); $to_ts = mktime(23, 59, 59, ($now_info['mon'] + 1), date('t', $from_ts), $now_info['year']); } elseif ($date_dir < 0) { // prev month from current offset $from_ts = mktime(0, 0, 0, ($now_info['mon'] - 1), 1, $now_info['year']); $to_ts = mktime(23, 59, 59, ($now_info['mon'] - 1), date('t', $from_ts), $now_info['year']); } else { // no navigation, use current offset $from_ts = mktime(0, 0, 0, $now_info['mon'], 1, $now_info['year']); $to_ts = mktime(23, 59, 59, $now_info['mon'], date('t', $now_info[0]), $now_info['year']); } // build week days list according to settings $firstwday = (int)VikBooking::getFirstWeekDay(); $days_labels = array( JText::translate('VBSUN'), JText::translate('VBMON'), JText::translate('VBTUE'), JText::translate('VBWED'), JText::translate('VBTHU'), JText::translate('VBFRI'), JText::translate('VBSAT'), ); $days_indexes = []; for ($i = 0; $i < 7; $i++) { $days_indexes[$i] = (6 - ($firstwday - $i) + 1) % 7; } // detect mobile device $is_mobile = VikBooking::detectUserAgent(false, false); // currency symbol $currencysymb = VikBooking::getCurrencySymb(); // start looping from the first day of the current month $info_arr = getdate($from_ts); // build period name $period_date = VikBooking::sayMonth($info_arr['mon']) . ' ' . $info_arr['year']; // invoke availability helper class $av_helper = VikBooking::getAvailabilityInstance(true); // get all rooms and tax rates $all_rooms = $av_helper->loadRooms(); $tax_rates = $av_helper->getTaxRates(); // count maximum units available depending on filter $tot_rooms = 1; if (!empty($room_id) && isset($all_rooms[$room_id])) { // use the units of the filtered room $max_units = $all_rooms[$room_id]['units']; } else { // sum all room units $max_units = 0; $tot_rooms = count($all_rooms); foreach ($all_rooms as $rid => $room) { $max_units += $room['units']; } } // build "search name" if (!empty($room_id) && isset($all_rooms[$room_id])) { $search_name = $all_rooms[$room_id]['name']; } else { $search_name = preg_replace("/[^A-Za-z0-9 ]/", '', JText::translate('VBOSTATSALLROOMS')); } // load busy records $room_filter = !empty($room_id) ? array($room_id) : array(); $busy_records = VikBooking::loadBusyRecords($room_filter, $from_ts, $to_ts); // load festivities or room-day notes $festivities = []; $rday_notes = []; if (!empty($room_id) && isset($all_rooms[$room_id])) { // load room-day notes for the given room id $rday_notes = VikBooking::getCriticalDatesInstance()->loadRoomDayNotes(date('Y-m-d', $from_ts), date('Y-m-d', $to_ts), $room_id); } else { // load festivities when no specific room filter set $fests = VikBooking::getFestivitiesInstance(); if ($fests->shouldCheckFestivities()) { $fests->storeNextFestivities(); } $festivities = $fests->loadFestDates(date('Y-m-d', $from_ts), date('Y-m-d', $to_ts)); } // date format $dtpicker_df = $this->getDateFormat('jui'); // start output buffering ob_start(); // generate calendar $d_count = 0; $mon_lim = $info_arr['mon']; $next_offset = date('Y-m-d', $from_ts); ?> <div class="vbo-widget-booskcal-mday-wrap" style="display: none;"> <div class="vbo-widget-booskcal-mday-head"> <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> <span class="vbo-widget-booskcal-mday-name"></span> </div> <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> <div class="vbo-widget-booskcal-mday-pricing" style="display: none;"> <div class="vbo-widget-booskcal-mday-pricing-title"> <span><?php echo JText::translate('VBO_RATES_AND_RESTR'); ?></span> </div> <div class="vbo-widget-booskcal-mday-hidden-helper" style="display: none;"> <div class="vbo-widget-booskcal-mday-hidden-helper-togglebtn vbo-toggle-small"> <?php echo $this->vbo_app->printYesNoButtons('updotas', JText::translate('VBYES'), JText::translate('VBNO'), 1, 1, 0, '', ['blue']); ?> </div> </div> <div class="vbo-widget-booskcal-mday-pricing-data"> <div class="vbo-widget-booskcal-mday-pricing-data-cost"> <span class="vbo-widget-booskcal-mday-pricing-currency"><?php echo $currencysymb; ?></span> <span class="vbo-widget-booskcal-mday-pricing-cost"></span> </div> <div class="vbo-widget-booskcal-mday-pricing-data-restr"> <div class="vbo-widget-booskcal-mday-pricing-data-minlos"> <span><?php echo JText::translate('VBOMINIMUMSTAY'); ?></span> <span class="vbo-widget-booskcal-mday-pricing-minlos"></span> </div> <div class="vbo-widget-booskcal-mday-pricing-data-ctad"> <span class="vbo-widget-booskcal-mday-pricing-ctad"></span> </div> </div> </div> </div> </div> <div class="vbo-widget-booskcal-newbook-wrap" data-ymd="" data-roomid="" style="display: none;"> <div class="vbo-widget-booskcal-newbook-head"> <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> <span class="vbo-widget-booskcal-newbook-name"><?php echo JText::translate('VBQUICKBOOK'); ?></span> </div> <div class="vbo-widget-booskcal-newbook-cont"> <div class="vbo-admin-container vbo-admin-container-full vbo-admin-container-compact"> <div class="vbo-params-wrap"> <div class="vbo-params-container"> <div class="vbo-params-block"> <div class="vbo-param-container"> <div class="vbo-param-label"><?php echo JText::translate('VBPICKUPAT'); ?></div> <div class="vbo-param-setting"> <div class="vbo-field-calendar"> <div class="input-append"> <input type="text" class="vbo-widget-bookscal-checkindt" value="" autocomplete="off" /> <button type="button" class="btn btn-secondary vbo-widget-bookscal-checkindt-trigger"><?php VikBookingIcons::e('calendar'); ?></button> </div> </div> </div> </div> <div class="vbo-param-container"> <div class="vbo-param-label"><?php echo JText::translate('VBRELEASEAT'); ?></div> <div class="vbo-param-setting"> <div class="vbo-field-calendar"> <div class="input-append"> <input type="text" class="vbo-widget-bookscal-checkoutdt" value="" autocomplete="off" /> <button type="button" class="btn btn-secondary vbo-widget-bookscal-checkoutdt-trigger"><?php VikBookingIcons::e('calendar'); ?></button> </div> </div> <span class="vbo-param-setting-comment vbo-widget-bookscal-nights-counter" style="display: none;"></span> </div> </div> <div class="vbo-param-container vbo-toggle-small"> <div class="vbo-param-label"><?php echo JText::translate('VBSUBMCLOSEROOM'); ?></div> <div class="vbo-param-setting"> <?php echo $this->vbo_app->printYesNoButtons('closeroom', JText::translate('VBYES'), JText::translate('VBNO'), 0, 1, 0, "vboWidgetBooksCalClosure('{$wrapper}');"); ?> </div> </div> <div class="vbo-param-container" data-noclosure="1"> <div class="vbo-param-label"><?php echo JText::translate('VBOCUSTOMER'); ?></div> <div class="vbo-param-setting"> <span class="vbo-assign-customer" onclick="vboWidgetBooksCalAssignCustomer('<?php echo $wrapper; ?>');"> <?php VikBookingIcons::e('user-circle'); ?> <span><?php echo JText::translate('VBFILLCUSTFIELDS'); ?></span> </span> <input type="hidden" value="" class="vbo-widget-bookscal-custid" /> <input type="hidden" value="" class="vbo-widget-bookscal-custmail" /> <input type="hidden" value="" class="vbo-widget-bookscal-custdata" /> <input type="hidden" value="" class="vbo-widget-bookscal-country" /> <input type="hidden" value="" class="vbo-widget-bookscal-state" /> <input type="hidden" value="" class="vbo-widget-bookscal-phone" /> <input type="hidden" value="" class="vbo-widget-bookscal-tfname" /> <input type="hidden" value="" class="vbo-widget-bookscal-tlname" /> <input type="hidden" value="" class="vbo-widget-bookscal-roomcost" /> <input type="hidden" value="" class="vbo-widget-bookscal-idprice" /> </div> </div> <div class="vbo-param-container" data-noclosure="1"> <div class="vbo-param-label"><?php echo JText::translate('VBPVIEWROOMSEVEN'); ?></div> <div class="vbo-param-setting"> <input type="number" class="vbo-input-number-small vbo-widget-bookscal-units" value="1" min="1" max="99" onchange="vboWidgetBooksCalGetWebsiteRates('<?php echo $wrapper; ?>');" /> </div> </div> <div class="vbo-param-container" data-noclosure="1"> <div class="vbo-param-label"><?php echo JText::translate('VBPVIEWORDERSPEOPLE'); ?></div> <div class="vbo-param-setting"> <span class="vbo-quickres-aduchi-wrap"> <span class="vbo-quickres-aduchi-inlbl"><?php echo JText::translate('VBEDITORDERADULTS'); ?></span> <input type="number" class="vbo-input-number-small vbo-widget-bookscal-adults" value="2" min="0" max="99" onchange="vboWidgetBooksCalGetWebsiteRates('<?php echo $wrapper; ?>');" /> </span> <span class="vbo-quickres-aduchi-wrap"> <span class="vbo-quickres-aduchi-inlbl"><?php echo JText::translate('VBEDITORDERCHILDREN'); ?></span> <input type="number" class="vbo-input-number-small vbo-widget-bookscal-children" value="0" min="0" max="99" /> </span> </div> </div> <div class="vbo-param-container vbo-website-rates-row" data-noclosure="1" data-unavailable="1" style="display: none;"> <div class="vbo-param-label"><?php echo JText::translate('VBOWEBSITERATES'); ?></div> <div class="vbo-param-setting"> <div class="vbo-website-rates-cont"></div> </div> </div> <div class="vbo-param-container vbo-row-custcost" data-noclosure="1"> <div class="vbo-param-label"><?php echo JText::translate('VBOROOMCUSTRATEPLANADD'); ?></div> <div class="vbo-param-setting"> <div class="vbo-calendar-costs-wrapper"> <?php echo $currencysymb; ?> <input type="number" class="vbo-widget-bookscal-custcost" value="" step="any" min="0" onfocus="vboWidgetBooksCalFocusTaxes('<?php echo $wrapper; ?>');" /> <?php if ($tax_rates) { ?> <select class="vbo-widget-bookscal-taxid" style="display: none;"> <option value=""><?php echo JText::translate('VBNEWOPTFOUR'); ?></option> <?php foreach ($tax_rates as $kiv => $iv) { ?> <option value="<?php echo $iv['id']; ?>"<?php echo $kiv < 1 ? ' selected="selected"' : ''; ?>><?php echo empty($iv['name']) ? "{$iv['aliq']}%" : "{$iv['name']} - {$iv['aliq']}%"; ?></option> <?php } ?> </select> <?php } ?> </div> </div> </div> <div class="vbo-param-container vbo-param-confirm-btn"> <div class="vbo-param-label"></div> <div class="vbo-param-setting"> <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> </div> </div> </div> </div> </div> </div> </div> </div> <div class="vbo-widget-booskcal-calendar-table-wrap"> <?php if (!empty($room_id) && isset($all_rooms[$room_id])) { // allow to manage rates and restrictions ?> <div class="vbo-widget-bookscal-mngrates-toggle vbo-toggle-small"> <label for="vbo-wbookscal-mng-rates-restr-on"><?php echo JText::translate('VBO_RATES_AND_RESTR'); ?></label> <?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']); ?> </div> <?php } ?> <table class="vbadmincaltable vbo-widget-booskcal-calendar-table"> <tbody> <tr class="vbadmincaltrmdays"> <?php // display week days in the proper order for ($i = 0; $i < 7; $i++) { $d_ind = ($i + $firstwday) < 7 ? ($i + $firstwday) : ($i + $firstwday - 7); ?> <td class="vbo-widget-booskcal-cell-wday"><?php echo $days_labels[$d_ind]; ?></td> <?php } ?> </tr> <tr> <?php // display empty cells until the first week-day of the month for ($i = 0, $n = $days_indexes[$info_arr['wday']]; $i < $n; $i++, $d_count++) { ?> <td class="vbo-widget-booskcal-cell-mday vbo-widget-booskcal-cell-empty">&nbsp;</td> <?php } // display month days while ($info_arr['mon'] == $mon_lim) { if ($d_count > 6) { $d_count = 0; // close current row and open a new one echo "\n</tr>\n<tr>\n"; } // count units booked on this day $tot_units_booked = 0; $cell_classes = []; $cell_bids = []; foreach ($busy_records as $rid => $rbusy) { foreach ($rbusy as $b) { $tmpone = getdate($b['checkin']); $ritts = mktime(0, 0, 0, $tmpone['mon'], $tmpone['mday'], $tmpone['year']); $tmptwo = getdate($b['checkout']); $conts = mktime(0, 0, 0, $tmptwo['mon'], $tmptwo['mday'], $tmptwo['year']); if ($info_arr[0] >= $ritts && $info_arr[0] < $conts) { // increase units booked $tot_units_booked++; if ($tot_rooms === 1) { if (!empty($b['closure'])) { // hightlight that this was a closure $cell_classes[] = 'busy-closure'; } elseif (!empty($b['sharedcal'])) { // hightlight that this was a reflection from a shared calendar $cell_classes[] = 'busy-sharedcalendar'; } } // check if we can push the booking ID involved if (!empty($b['idorder'])) { $cell_bids[] = $b['idorder']; } } } } // check status for this day if ($tot_units_booked > 0) { if ($tot_units_booked < $max_units) { // prepend the "partially-busy" class array_unshift($cell_classes, 'vbo-partially'); } // prepend the "busy" cell class so that this will be first array_unshift($cell_classes, 'busy'); } else { // set the "free" cell class $cell_classes[] = 'free'; } // set ymd values $cell_ymd = date('Y-m-d', $info_arr[0]); if ($cell_ymd == $today_ymd) { // set the "today" cell class $cell_classes[] = 'is-today'; } elseif ($info_arr[0] < $today_ts) { // set the "past" cell class $cell_classes[] = 'past'; } $cell_day_read = VikBooking::sayWeekDay($info_arr['wday']) . ' ' . $info_arr['mday']; // count values for this day $has_fests = isset($festivities[$cell_ymd]); $rdnotes_key = $cell_ymd . '_' . $room_id . '_0'; $has_rdnotes = isset($rday_notes[$rdnotes_key]); ?> <td class="vbo-widget-booskcal-cell-mday <?php echo implode(' ', array_unique($cell_classes)); ?>" onclick="vboWidgetBooksCalMday('<?php echo $wrapper; ?>', this);" data-bids="<?php echo implode(',', array_unique($cell_bids)); ?>" data-ymd="<?php echo $cell_ymd; ?>" data-wday="<?php echo $info_arr['wday']; ?>" data-dayread="<?php echo htmlspecialchars($cell_day_read); ?>" data-cta="0" data-ctd="0"> <span class="vbo-widget-booskcal-mday-val"><?php echo $info_arr['mday']; ?></span> <?php if ($has_fests || $has_rdnotes) { ?> <div class="vbo-widget-booskcal-mday-info"> <?php if ($has_fests) { ?> <span class="vbo-widget-booskcal-mday-fests"><?php VikBookingIcons::e('birthday-cake'); ?></span> <?php } if ($has_rdnotes) { ?> <span class="vbo-widget-booskcal-mday-rdnotes"><?php VikBookingIcons::e('sticky-note'); ?></span> <?php } ?> </div> <?php } ?> </td> <?php $dayts = mktime(0, 0, 0, $info_arr['mon'], ($info_arr['mday'] + 1), $info_arr['year']); $info_arr = getdate($dayts); $d_count++; } // add empty cells until the end of the row for ($i = $d_count; $i <= 6; $i++) { ?> <td class="vbo-widget-booskcal-cell-mday vbo-widget-booskcal-cell-empty">&nbsp;</td> <?php } ?> </tr> </tbody> </table> </div> <script type="text/javascript"> // render DRP calendar for dates selection jQuery('#<?php echo $wrapper; ?>').find('.vbo-widget-bookscal-checkindt').vboDatesRangePicker({ checkout: jQuery('#<?php echo $wrapper; ?>').find('.vbo-widget-bookscal-checkoutdt'), showOn: "focus", minDate: "-1m", maxDate: "+3y", yearRange: "<?php echo date('Y'); ?>:<?php echo (date('Y') + 3); ?>", changeMonth: true, changeYear: true, dateFormat: "<?php echo $dtpicker_df; ?>", numberOfMonths: <?php echo $is_mobile ? 1 : 2; ?>, responsiveNumMonths: { threshold: 860, }, onSelect: { checkin: (selectedDate) => { if (!selectedDate) { return; } let nowstart = jQuery('#<?php echo $wrapper; ?>').find('.vbo-widget-bookscal-checkindt').vboDatesRangePicker('getCheckinDate'); let nowstartdate = new Date(nowstart.getTime()); jQuery('#<?php echo $wrapper; ?>').find('.vbo-widget-bookscal-checkindt').vboDatesRangePicker('checkout', 'minDate', nowstartdate); // calculate website rates vboWidgetBooksCalGetWebsiteRates('<?php echo $wrapper; ?>'); }, checkout: (selectedDate) => { if (!selectedDate) { return; } // calculate website rates vboWidgetBooksCalGetWebsiteRates('<?php echo $wrapper; ?>'); }, }, labels: { checkin: Joomla.JText._('VBPICKUPROOM'), checkout: Joomla.JText._('VBRETURNROOM'), }, bottomCommands: { clear: Joomla.JText._('VBO_CLEAR_DATES'), close: Joomla.JText._('VBO_CLOSE'), onClear: () => { vbCalcNights(); }, }, environment: { section: 'admin', autoHide: true, }, }); // triggering for datepicker calendar icon jQuery('#<?php echo $wrapper; ?>').find('.vbo-widget-bookscal-checkindt-trigger, .vbo-widget-bookscal-checkoutdt-trigger').click(function() { let dp = jQuery(this).parent().find('input[type="text"]'); if (!dp.length) { return; } if (dp.hasClass('hasDatepicker')) { dp.focus(); } else if (dp.hasClass('vbo-widget-bookscal-checkoutdt')) { jQuery('#<?php echo $wrapper; ?>').find('.vbo-widget-bookscal-checkindt').focus(); } }); // check room units var vbo_bookscal_roomfilt = jQuery('#<?php echo $wrapper; ?>').find('.vbo-booskcal-roomid'); if (vbo_bookscal_roomfilt.val()) { var vbo_bookscal_room_units = vbo_bookscal_roomfilt.find('option:selected').attr('data-units'); if (vbo_bookscal_room_units > 1) { jQuery('#<?php echo $wrapper; ?>').find('.vbo-widget-bookscal-units').attr('max', vbo_bookscal_room_units); } else { jQuery('#<?php echo $wrapper; ?>').find('.vbo-widget-bookscal-units').closest('[data-noclosure="1"]').hide(); } } else { jQuery('#<?php echo $wrapper; ?>').find('.vbo-widget-bookscal-units').closest('[data-noclosure="1"]').hide(); } <?php // check if a specific day was requested through the widget options $force_day = $this->getOption('day', ''); if ($force_day && preg_match("/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/", $force_day)) { // trigger the click event over the requested day ?> setTimeout(() => { jQuery('.vbo-widget-booskcal-cell-mday[data-ymd="<?php echo $force_day; ?>"]').trigger('click'); }, 100); <?php } // check if a new booking was forced to be created if ((bool) $this->getOption('newbook', 0)) { ?> setTimeout(() => { vboWidgetBookCalsNewBooking('<?php echo $wrapper; ?>'); }, 150); <?php } ?> </script> <?php // get the HTML buffer $html_content = ob_get_contents(); ob_end_clean(); // return an associative array of values return array( 'html' => $html_content, 'offset' => $next_offset, 'search_name' => $search_name, 'period_date' => $period_date, ); } /** * Custom method for this widget only to load the bookings of a month-day. * The method is called by the admin controller through an AJAX request. * The visibility should be public, it should not exit the process, and * any content sent to output will be returned to the AJAX response. * In this case we return an array because this method requires "return":1. */ public function loadMdayBookings() { // get today's date $today_ymd = date('Y-m-d'); $page_offset = VikRequest::getInt('page_offset', 0, 'request'); $page_length = VikRequest::getInt('page_length', $this->bookings_per_page, 'request'); $ymd = VikRequest::getString('ymd', $today_ymd, 'request'); $wrapper = VikRequest::getString('wrapper', '', 'request'); $room_id = VikRequest::getInt('room_id', 0, 'request'); // calculate date timestamps interval for the given day $day_info = getdate(strtotime($ymd)); $from_ts = mktime(0, 0, 0, $day_info['mon'], $day_info['mday'], $day_info['year']); $to_ts = mktime(23, 59, 59, $day_info['mon'], $day_info['mday'], $day_info['year']); // load busy records $room_filter = !empty($room_id) ? array($room_id) : array(); $busy_records = VikBooking::loadBusyRecords($room_filter, $from_ts, $to_ts); // gather all bookings touching this day $booking_ids = []; foreach ($busy_records as $rid => $rbusy) { foreach ($rbusy as $b) { $tmpone = getdate($b['checkin']); $ritts = mktime(0, 0, 0, $tmpone['mon'], $tmpone['mday'], $tmpone['year']); $tmptwo = getdate($b['checkout']); $conts = mktime(0, 0, 0, $tmptwo['mon'], $tmptwo['mday'], $tmptwo['year']); if ($from_ts >= $ritts && $from_ts < $conts) { if (empty($b['idorder']) || in_array($b['idorder'], $booking_ids)) { continue; } array_push($booking_ids, $b['idorder']); } } } // invoke availability helper class $av_helper = VikBooking::getAvailabilityInstance(true); // collect booking information $booking_details = []; foreach ($booking_ids as $bid) { $booking = $av_helper->getBookingDetails($bid); if (!is_array($booking) || !$booking) { continue; } $booking_details[$bid] = $booking; } // check if a next page can be available $tot_bookings = count($booking_details); $has_next_page = ($tot_bookings > ($page_length + $page_offset)); // slice the records, if needed if ($tot_bookings > $page_length) { $booking_details = array_slice($booking_details, $page_offset, $page_length, true); } // load cancellations for today (only if a room ID filter is set) $cancellations = []; if (!empty($room_id)) { $cancellations = $this->loadCancellations($room_id, $ymd); } // load festivities or room-day notes $festivities = []; $rday_notes = []; if (!empty($room_id)) { // load room-day notes for the given room id $rday_notes = VikBooking::getCriticalDatesInstance()->loadRoomDayNotes(date('Y-m-d', $from_ts), date('Y-m-d', $to_ts), $room_id); } else { // load festivities when no specific room filter set $festivities = VikBooking::getFestivitiesInstance()->loadFestDates(date('Y-m-d', $from_ts), date('Y-m-d', $to_ts)); } // start output buffering ob_start(); if ($festivities) { // display the festivities for this day ?> <div class="vbo-widget-booskcal-events vbo-widget-booskcal-fests"> <?php foreach ($festivities as $fest_ymd => $fest) { if (empty($fest['festinfo']) || !is_array($fest['festinfo'])) { continue; } foreach ($fest['festinfo'] as $fest_info) { if (!is_object($fest_info) || empty($fest_info->trans_name)) { continue; } ?> <div class="vbo-widget-booskcal-event vbo-widget-booskcal-fest"> <strong><?php echo $fest_info->trans_name; ?></strong> <?php if (!empty($fest_info->descr)) { ?> <div><?php echo nl2br($fest_info->descr); ?></div> <?php } ?> </div> <?php } } ?> </div> <?php } if ($rday_notes) { // display the room-day notes for this day ?> <div class="vbo-widget-booskcal-events vbo-widget-booskcal-rdaynotes"> <?php foreach ($rday_notes as $rday_note) { if (empty($rday_note['info']) || !is_array($rday_note['info'])) { continue; } foreach ($rday_note['info'] as $note_info) { if (!is_object($note_info) || empty($note_info->name)) { continue; } ?> <div class="vbo-widget-booskcal-event vbo-widget-booskcal-rdaynote"> <strong><?php echo $note_info->name; ?></strong> <?php if (!empty($note_info->descr)) { ?> <div><?php echo nl2br($note_info->descr); ?></div> <?php } ?> </div> <?php } } ?> </div> <?php } if (!$booking_details && !$cancellations) { ?> <p class="info"><?php echo JText::translate('VBNOORDERSFOUND'); ?></p> <?php } else { if (!$booking_details) { ?> <p class="info"><?php echo JText::translate('VBNOORDERSFOUND'); ?></p> <?php } // merge confirmed bookings with cancellations (if any) $booking_details = array_merge($booking_details, $cancellations); // display all bookings of this day $canc_separator = false; foreach ($booking_details as $booking) { // get channel logo and other details $ch_logo_obj = VikBooking::getVcmChannelsLogo($booking['channel'], true); $channel_logo = is_object($ch_logo_obj) ? $ch_logo_obj->getSmallLogoURL() : ''; $nights_lbl = $booking['days'] > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY'); $rooms_lbl = !empty($booking['roomsnum']) && $booking['roomsnum'] > 1 ? ', ' . $booking['roomsnum'] . ' ' . JText::translate('VBPVIEWORDERSTHREE') : ''; // compose customer name $customer_name = !empty($booking['customer_fullname']) ? $booking['customer_fullname'] : ''; if ($booking['closure'] > 0 || !strcasecmp((string) $booking['custdata'], JText::translate('VBDBTEXTROOMCLOSED'))) { $customer_name = '<span class="vbordersroomclosed"><i class="' . VikBookingIcons::i('ban') . '"></i> ' . JText::translate('VBDBTEXTROOMCLOSED') . '</span>'; } if (empty($customer_name)) { $customer_name = VikBooking::getFirstCustDataField($booking['custdata']); } // customer country flag $customer_country = ''; $customer_cflag = ''; if (!empty($booking['customer_country'])) { $customer_country = $booking['customer_country']; } elseif (!empty($booking['country'])) { $customer_country = $booking['country']; } if ($customer_country && is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . 'countries' . DIRECTORY_SEPARATOR . $customer_country . '.png')) { $customer_cflag = '<img src="'.VBO_ADMIN_URI.'resources/countries/' . $customer_country . '.png'.'" title="' . htmlspecialchars($customer_country) . '" class="vbo-country-flag vbo-country-flag-left"/>'; } // check for cancellations $elem_style = $booking['status'] == 'cancelled' ? 'display: none;' : ''; if ($booking['status'] == 'cancelled' && !$canc_separator) { // display the button to show the cancellations $canc_separator = true; ?> <div class="vbo-bookings-status-separator"> <button type="button" class="btn btn-small btn-secondary" onclick="vboWidgetBooksCalCancToggle('<?php echo $wrapper; ?>');"><?php echo JText::translate('VBO_SHOW_CANCELLATIONS'); ?></button> </div> <?php } ?> <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; ?>"> <div class="vbo-dashboard-guest-activity-avatar"> <?php if (!empty($channel_logo)) { // channel logo has got the highest priority ?> <img class="vbo-dashboard-guest-activity-avatar-profile" src="<?php echo $channel_logo; ?>" /> <?php } elseif (!empty($booking['pic'])) { // customer profile picture ?> <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']; ?>" /> <?php } else { // we use an icon as fallback VikBookingIcons::e('hotel', 'vbo-dashboard-guest-activity-avatar-icon'); } ?> </div> <div class="vbo-dashboard-guest-activity-content"> <div class="vbo-dashboard-guest-activity-content-head"> <div class="vbo-dashboard-guest-activity-content-info-details"> <h4><?php echo $customer_name . $customer_cflag; ?></h4> <div class="vbo-dashboard-guest-activity-content-info-icon"> <?php if (!strcasecmp((string) $booking['type'], 'overbooking')) { ?> <span class="label label-error vbo-label-overbooking"><?php echo JText::translate('VBO_BTYPE_' . strtoupper($booking['type'])); ?></span> <?php } if ($booking['status'] == 'cancelled') { ?> <span class="badge badge-danger"><?php echo JText::translate('VBCANCELLED'); ?></span> <?php } ?> <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> </div> </div> <div class="vbo-dashboard-guest-activity-content-info-date"> <span> <span class="label label-info"><?php echo $booking['id']; ?></span> </span> <span><?php echo date(str_replace("/", $this->datesep, $this->df) . ' H:i', $booking['ts']); ?></span> </div> </div> </div> </div> <?php } // append navigation ?> <div class="vbo-widget-commands vbo-widget-commands-right"> <div class="vbo-widget-commands-main"> <?php if ($page_offset > 0) { // show backward navigation button ?> <div class="vbo-widget-command-chevron vbo-widget-command-prev"> <span class="vbo-widget-command-chevron-prev" onclick="vboWidgetBooksCalMdayNavigate('<?php echo $wrapper; ?>', -1);"><?php VikBookingIcons::e('chevron-left'); ?></span> </div> <?php } if ($has_next_page) { // show forward navigation button ?> <div class="vbo-widget-command-chevron vbo-widget-command-next"> <span class="vbo-widget-command-chevron-next" onclick="vboWidgetBooksCalMdayNavigate('<?php echo $wrapper; ?>', 1);"><?php VikBookingIcons::e('chevron-right'); ?></span> </div> <?php } ?> </div> </div> <?php // append JS code ?> <script type="text/javascript"> setTimeout(() => { jQuery('#<?php echo $wrapper; ?>').find('.vbo-widget-booskcal-reservation').on('click', function(e) { let bid = jQuery(this).attr('data-resid'); let target = jQuery(e.target); if (target.is('img.vbo-dashboard-guest-activity-avatar-profile') || target.is('i.vbo-dashboard-guest-activity-avatar-icon') || target.is('span.label-info')) { // render booking details within the widget booking details VBOCore.handleDisplayWidgetNotification({widget_id: 'booking_details'}, { bid: bid, modal_options: { /** * Overwrite modal options for rendering the admin widget. * We need to use a different suffix in case this current widget was * also rendered within a modal, or it would get dismissed in favour * of the newly opened admin widget. */ suffix: 'widget_modal_inner_booking_details', }, }); } else { // navigate to booking details page vboWidgetBooksCalOpenBooking(bid); } }); }, 300); </script> <?php } // get the HTML buffer $html_content = ob_get_contents(); ob_end_clean(); // return an associative array of values return array( 'html' => $html_content, 'tot_bookings' => $tot_bookings, 'next_page' => (int)$has_next_page, ); } /** * Custom method for this widget only to load the custom fields for the new booking. * The method is called by the admin controller through an AJAX request. * The visibility should be public, and an array is requested to be returned. * * @return array * * @since 1.16.0 (J) - 1.6.0 (WP) */ public function displayCustomerFilling() { $dbo = JFactory::getDbo(); $wrapper = VikRequest::getString('wrapper', '', 'request'); // load custom fields and countries $all_countries = []; $q = "SELECT * FROM `#__vikbooking_custfields` ORDER BY `#__vikbooking_custfields`.`ordering` ASC;"; $dbo->setQuery($q); $all_cfields = $dbo->loadAssocList(); if ($all_cfields) { $q = "SELECT * FROM `#__vikbooking_countries` ORDER BY `#__vikbooking_countries`.`country_name` ASC;"; $dbo->setQuery($q); $all_countries = $dbo->loadAssocList(); } // start output buffering ob_start(); ?> <div class="vbo-calendar-cfields-filler" data-wrapper="<?php echo $wrapper; ?>"> <div class="vbo-calendar-cfields-topcont"> <div class="vbo-calendar-cfields-search"> <div class="vbo-singleselect-inline-elems-wrap vbo-search-elems-wrap"> <?php /** * Display a search elements dropdown for searching customers. * * @since 1.18.0 (J) - 1.8.0 (WP) */ echo $this->vbo_app->renderSearchElementsDropDown([ 'id' => 'vbo-widget-books-cal-search-customer-' . $wrapper, 'elements' => 'customers', 'placeholder' => JText::translate('VBOSEARCHEXISTCUST'), 'allow_clear' => true, 'attributes' => [ 'name' => 'widget_books_cal[id_customer]', ], 'style_selection' => true, 'selection_class' => 'vbo-sel2-selected-search-elem-full', 'selection_event' => 'vbo-widget-books-cal-choose-customer-' . $wrapper, 'load_assets' => false, 'width' => '300px', ]); ?> </div> </div> </div> <div class="vbo-calendar-cfields-inner vbo-widget-bookscal-cfields-inner"> <input type="hidden" value="" id="vbo-widget-bookscal-cfield-custid<?php echo $wrapper; ?>" /> <?php foreach ($all_cfields as $cfield) { if ($cfield['type'] == 'text' && $cfield['isphone'] == 1) { ?> <div class="vbo-calendar-cfield-entry"> <label for="cfield<?php echo $cfield['id'] . $wrapper; ?>"><?php echo JText::translate($cfield['name']); ?></label> <span> <?php echo $this->vbo_app->printPhoneInputField( [ 'id' => 'cfield' . $cfield['id'] . $wrapper, 'class' => 'vbo-calendar-cfield-phone', 'data-isemail' => '0', 'data-isnominative' => '0', 'data-isphone' => '1' ], [ 'fullNumberOnBlur' => true ], $load_assets = false ); ?> </span> </div> <?php } elseif ($cfield['type'] == 'text') { ?> <div class="vbo-calendar-cfield-entry"> <label for="cfield<?php echo $cfield['id'] . $wrapper; ?>"><?php echo JText::translate($cfield['name']); ?></label> <span> <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"/> </span> </div> <?php } elseif ($cfield['type'] == 'textarea') { ?> <div class="vbo-calendar-cfield-entry"> <label for="cfield<?php echo $cfield['id'] . $wrapper; ?>"><?php echo JText::translate($cfield['name']); ?></label> <span> <textarea id="cfield<?php echo $cfield['id'] . $wrapper; ?>" rows="4" cols="35"></textarea> </span> </div> <?php } elseif ($cfield['type'] == 'country') { ?> <div class="vbo-calendar-cfield-entry"> <label for="cfield<?php echo $cfield['id'] . $wrapper; ?>"><?php echo JText::translate($cfield['name']); ?></label> <span> <select id="cfield<?php echo $cfield['id'] . $wrapper; ?>" class="vbo-calendar-cfield-country" onchange="vboWidgetBooksCalChangeCountry(this, '<?php echo $wrapper; ?>');"> <option value=""> </option> <?php foreach ($all_countries as $country) { ?> <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> <?php } ?> </select> </span> </div> <?php } elseif ($cfield['type'] == 'state') { ?> <div class="vbo-calendar-cfield-entry"> <label for="cfield<?php echo $cfield['id'] . $wrapper; ?>"><?php echo JText::translate($cfield['name']); ?></label> <span> <select id="cfield<?php echo $cfield['id'] . $wrapper; ?>" class="vbo-calendar-cfield-state"> <option value="">-----</option> </select> </span> </div> <?php } } ?> </div> </div> <script type="text/javascript"> // focus search customer input field setTimeout(() => { jQuery('#vbo-widget-books-cal-search-customer-<?php echo $wrapper; ?>').select2('open'); }, 500); </script> <?php // get the HTML buffer $html_content = ob_get_contents(); ob_end_clean(); // return an associative array of values return [ 'html' => $html_content, ]; } /** * Custom method for this widget only to save a new booking with the input values. * The method is called by the admin controller through an AJAX request. * The visibility should be public, and an array is requested to be returned. * * @return array * * @since 1.16.0 (J) - 1.6.0 (WP) */ public function saveBooking() { $wrapper = VikRequest::getString('wrapper', '', 'request'); $forcebooking = VikRequest::getInt('forcebooking', 0, 'request'); $room_id = VikRequest::getInt('room_id', 0, 'request'); $checkin = VikRequest::getString('checkin', '', 'request'); $checkout = VikRequest::getString('checkout', '', 'request'); $closure = VikRequest::getInt('closure', 0, 'request'); $units_closed = VikRequest::getInt('units_closed', 0, 'request'); $units = VikRequest::getInt('units', 1, 'request'); $adults = VikRequest::getInt('adults', 2, 'request'); $children = VikRequest::getInt('children', 0, 'request'); $cust_id = VikRequest::getInt('cust_id', 0, 'request'); $cust_email = VikRequest::getString('cust_email', '', 'request'); $cust_data = VikRequest::getString('cust_data', '', 'request'); $cust_country = VikRequest::getString('cust_country', '', 'request'); $cust_state = VikRequest::getString('cust_state', '', 'request'); $cust_phone = VikRequest::getString('cust_phone', '', 'request'); $cust_tfname = VikRequest::getString('cust_tfname', '', 'request'); $cust_tlname = VikRequest::getString('cust_tlname', '', 'request'); $roomcost = VikRequest::getFloat('roomcost', 0, 'request'); $idprice = VikRequest::getInt('idprice', 0, 'request'); $cust_roomcost = VikRequest::getFloat('cust_roomcost', 0, 'request'); $taxid = VikRequest::getInt('taxid', 0, 'request'); if (empty($room_id) || empty($checkin) || empty($checkout)) { VBOHttpDocument::getInstance()->close(500, JText::translate('VBO_PLEASE_FILL_FIELDS')); } // invoke the reservation model and inject values $model_res = VBOModelReservation::getInstance([ 'force_booking' => $forcebooking, 'set_closed' => $closure, 'units_closed' => $units_closed, 'status' => 'confirmed', 'num_rooms' => $units, 'adults' => $adults, 'children' => $children, ])->setCustomer([ 'id' => $cust_id, 'first_name' => $cust_tfname, 'last_name' => $cust_tlname, 'data' => $cust_data, 'email' => $cust_email, 'country' => $cust_country, 'state' => $cust_state, 'phone' => $cust_phone, ])->setRoom([ 'id' => $room_id, 'cust_cost' => $cust_roomcost, 'room_cost' => $roomcost, 'id_price' => $idprice, 'id_tax' => $taxid, ]); // calculate proper check-in and check-out timestamps list($hcheckin, $mcheckin, $hcheckout, $mcheckout) = $model_res->loadCheckinOutTimes(); // get final stay timestamps $checkin_ts = VikBooking::getDateTimestamp($checkin, $hcheckin, $mcheckin); $checkout_ts = VikBooking::getDateTimestamp($checkout, $hcheckout, $mcheckout); if (!$checkin_ts || !$checkout_ts || $checkin_ts >= $checkout_ts) { VBOHttpDocument::getInstance()->close(500, JText::translate('ERRINVDATESEASON')); } // set stay dates $model_res->set('checkin', $checkin_ts); $model_res->set('checkout', $checkout_ts); // store the reservation $model_res->create(); // get the new booking ID $res_id = $model_res->getNewBookingID(); if (!$res_id) { VBOHttpDocument::getInstance()->close(500, $model_res->getError()); } return [ 'new_booking_id' => $res_id, 'vcm_action' => $model_res->getChannelManagerAction(), ]; } /** * Custom method for this widget only to load the room rates and restrictions. * The method is called by the admin controller through an AJAX request. * The visibility should be public, it should not exit the process, and * any content sent to output will be returned to the AJAX response. * In this case we return an array because this method requires "return":1. * * @since 1.16.10 (J) - 1.6.10 (WP) */ public function loadRoomRates() { $app = JFactory::getApplication(); $from_date = $app->input->getString('from_date', date('Y-m-d')); $wrapper = $app->input->getString('wrapper', ''); $room_id = $app->input->getInt('room_id', 0); $to_dt = JFactory::getDate($from_date); $to_dt->modify('+1 month'); $to_date = $to_dt->format('Y-m-d'); // get room details $room_data = VikBooking::getRoomInfo($room_id); if (!$room_data) { VBOHttpDocument::getInstance($app)->close(404, 'Room not found'); } try { // fetch room rates and restrictions $room_rates = VBOModelPricing::getInstance()->getRoomRates([ 'id_room' => $room_data['id'], 'from_date' => $from_date, 'to_date' => $to_date, 'restrictions' => true, ]); } catch (Exception $e) { // propagate the error VBOHttpDocument::getInstance($app)->close($e->getCode(), $e->getMessage()); } return [ 'rates_restrictions' => $room_rates, ]; } /** * Custom method for this widget only to load all room rates for a given day. * The method is called by the admin controller through an AJAX request. * The visibility should be public, it should not exit the process, and * any content sent to output will be returned to the AJAX response. * In this case we return an array because this method requires "return":1. * * @since 1.16.10 (J) - 1.6.10 (WP) */ public function loadRoomDayRatePlans() { $app = JFactory::getApplication(); $ymd = $app->input->getString('ymd', date('Y-m-d')); $room_id = $app->input->getInt('room_id', 0); $from_date_info = getdate(strtotime($ymd)); // get room details $room_data = VikBooking::getRoomInfo($room_id); if (!$room_data) { VBOHttpDocument::getInstance($app)->close(404, 'Room not found'); } try { // fetch room rates and restrictions $room_rates = VBOModelPricing::getInstance()->getRoomRates([ 'id_room' => $room_data['id'], 'from_date' => $ymd, 'to_date' => $ymd, 'all_rplans' => true, 'restrictions' => true, ]); } catch (Exception $e) { // propagate the error VBOHttpDocument::getInstance($app)->close($e->getCode(), $e->getMessage()); } // access the information about the derived rate plans $derived_rates_info = []; foreach (VikBooking::getAvailabilityInstance(true)->loadRatePlans() as $rate_plan) { if ($rate_plan['derived_id'] && $rate_plan['derived_data']) { // build derived information string $derived_str = ($rate_plan['parent_rate_name'] ?? '') . ' '; $derived_str .= ($rate_plan['derived_data']['type'] ?? 'percent') == 'absolute' ? VikBooking::getCurrencySymb() . ' ' : ''; $derived_str .= ($rate_plan['derived_data']['mode'] ?? 'discount') == 'discount' ? '-' : '+'; $derived_str .= $rate_plan['derived_data']['value'] ?? 0; $derived_str .= ($rate_plan['derived_data']['type'] ?? 'percent') == 'percent' ? ' %' : ''; $rate_plan['derived_info'] = $derived_str; // register derived rate plan $derived_rates_info[$rate_plan['id']] = $rate_plan; } } /** * Build room-ota relations for pricing alterations, if any. * * @since 1.17.3 (J) - 1.7.3 (WP) */ $room_ota_relations = []; // always get a new instance of the VikChannelManagerLogos class $vcm_logos = VikBooking::getVcmChannelsLogo('', true); // load channels (firsr) and accounts (after) for this listing $room_ota_channels = is_object($vcm_logos) && method_exists($vcm_logos, 'getVboRoomLogosMapped') ? $vcm_logos->getVboRoomLogosMapped($room_data['id']) : []; $room_ota_accounts = is_object($vcm_logos) && method_exists($vcm_logos, 'getRoomOtaAccounts') ? $vcm_logos->getRoomOtaAccounts() : []; // filter channels not available as accounts (i.e. iCal) if (count($room_ota_channels) != count(($room_ota_accounts[$room_data['id']] ?? []))) { $ota_account_names = array_map('strtolower', array_column(($room_ota_accounts[$room_data['id']] ?? []), 'channel')); $room_ota_channels = array_filter($room_ota_channels, function($chid) use ($ota_account_names) { return in_array(strtolower($chid), $ota_account_names); }, ARRAY_FILTER_USE_KEY); } if ($room_ota_channels && ($room_ota_accounts[$room_data['id']] ?? [])) { $room_ota_relations[$room_data['id']] = [ 'channels' => $room_ota_channels, 'accounts' => $room_ota_accounts[$room_data['id']], ]; } return [ 'wday' => $from_date_info['wday'], 'room_rates' => $room_rates, 'derived_rates' => $derived_rates_info, 'ota_relations' => $room_ota_relations ?: (new stdClass), ]; } /** * Custom method for this widget only to apply a new cost and/or min-los * for a specific room, rate plan and day. * The method is called by the admin controller through an AJAX request. * The visibility should be public, it should not exit the process, and * any content sent to output will be returned to the AJAX response. * In this case we return an array because this method requires "return":1. * * @since 1.16.10 (J) - 1.6.10 (WP) */ public function setRoomDayRateRestiction() { $app = JFactory::getApplication(); $ymd = $app->input->getString('ymd', date('Y-m-d')); $rplan_id = $app->input->getInt('rplan_id', 0); $room_id = $app->input->getInt('room_id', 0); $rate = $app->input->getFloat('rate', 0); $minlos = $app->input->getInt('minlos', 0); $updotas = $app->input->getBool('updotas', false); $otapricing = $app->input->get('ota_pricing', [], 'array'); try { // access the model pricing by binding data $model = VBOModelPricing::getInstance([ 'from_date' => $ymd, 'to_date' => $ymd, 'id_room' => $room_id, 'id_price' => $rplan_id, 'rate' => $rate, 'min_los' => $minlos, 'update_otas' => $updotas, 'ota_pricing' => $otapricing, ]); // apply the new rate/restrictions $new_rates = $model->modifyRateRestrictions(); } catch (Exception $e) { // propagate the error VCMHttpDocument::getInstance($app)->close($e->getCode(), $e->getMessage()); } return [ 'channels_updated' => $model->getChannelsUpdated(), 'channel_errors' => $model->getChannelErrors(), 'channel_warnings' => $model->getChannelWarnings(), 'new_rates' => $new_rates, ]; } /** * Preload the necessary CSS/JS assets. * * @return void */ public function preload() { // load assets $this->vbo_app->loadSelect2(); $this->vbo_app->loadDatePicker(); $this->vbo_app->loadDatesRangePicker(); $this->vbo_app->loadPhoneInputFieldAssets(); // JS lang def JText::script('VBFILLCUSTFIELDS'); JText::script('VBO_PLEASE_FILL_FIELDS'); JText::script('VBDASHUPRESONE'); JText::script('VBANNULLA'); JText::script('VBAPPLY'); JText::script('VBDBTEXTROOMCLOSED'); JText::script('VBO_MARK_UNITS_CLOSED'); JText::script('VBO_MIN_STAY_SHORT'); JText::script('VBO_CTA_SHORT'); JText::script('VBO_CTD_SHORT'); JText::script('VBMAINPAYMENTSEDIT'); JText::script('VBRATESOVWSETNEWRATE'); JText::script('VBOMINIMUMSTAYSET'); JText::script('VBOUPDRATESONCHANNELS'); JText::script('VBOVCMRATESRES'); JText::script('VBO_IS_DERIVED_RATE'); JText::script('VBDAYS'); } /** * @inheritDoc * * @since 1.18.0 (J) - 1.8.0 (WP) */ public function getWidgetDetails() { // get common widget details from parent abstract class $details = parent::getWidgetDetails(); // append the modal rendering information $details['modal'] = [ 'add_class' => 'vbo-modal-taller', ]; return $details; } /** * Main method to invoke the widget. Contents will be loaded * through AJAX requests, not via PHP when the page loads. * * @param ?VBOMultitaskData $data * * @return void */ public function render(?VBOMultitaskData $data = null) { // increase widget's instance counter static::$instance_counter++; // check whether the widget is being rendered via AJAX when adding it through the customizer $is_ajax = $this->isAjaxRendering(); // generate a unique ID for the sticky notes wrapper instance $wrapper_instance = !$is_ajax ? static::$instance_counter : rand(); $wrapper_id = 'vbo-widget-booskcal-' . $wrapper_instance; // get permissions $vbo_auth_bookings = JFactory::getUser()->authorise('core.vbo.bookings', 'com_vikbooking'); if (!$vbo_auth_bookings) { // display nothing return; } // invoke availability helper class $av_helper = VikBooking::getAvailabilityInstance(true); // get all rooms $all_rooms = $av_helper->loadRooms(); // load room mini thumbnails $mini_thumbnails = VBORoomHelper::getInstance()->loadMiniThumbnails($all_rooms); // check pricing tax configuration $prices_vat_included = (int)VikBooking::ivaInclusa(); // currency symbol and formatting options $currencysymb = VikBooking::getCurrencySymb(); list($currency_digits, $currency_decimals, $currency_thousands) = explode(':', VikBooking::getNumberFormatData()); // determine whether select2 is needed for the room selection $use_nice_select = 0; if (count($all_rooms) > 1) { // turn flag on $use_nice_select = 1; } // default dates and values $now_info = getdate(); $from_ts = mktime(0, 0, 0, $now_info['mon'], 1, $now_info['year']); // build week days list according to settings $firstwday = (int)VikBooking::getFirstWeekDay(); $days_labels = array( JText::translate('VBSUN'), JText::translate('VBMON'), JText::translate('VBTUE'), JText::translate('VBWED'), JText::translate('VBTHU'), JText::translate('VBFRI'), JText::translate('VBSAT'), ); $days_indexes = []; for ($i = 0; $i < 7; $i++) { $days_indexes[$i] = (6 - ($firstwday - $i) + 1) % 7; } // check multitask data $page_bid = 0; $load_room = 0; $load_room_rates = 0; $modal_load_bid = ''; $js_modal_id = ''; if ($data) { // access Multitask data $page_bid = $data->getBookingID() ?: $this->options()->fetchBookingId(); $is_modal_rendering = $data->isModalRendering(); if ($page_bid && $is_modal_rendering) { // load contents according to injected multitask data $modal_load_bid = $page_bid; } if ($is_modal_rendering) { // get modal JS identifier $js_modal_id = $data->getModalJsIdentifier(); } // first check if an overbooking flag was given $overbooking = $this->getOption('overbooking', 0); $overbooking = $page_bid && $overbooking ? $page_bid : 0; if ($overbooking) { // prepare the options to set for rendering the admin-widget // correctly and show the overbooking details $this->prepareOverbookingOptions($overbooking); } // check if a specific room ID was set $load_room = $this->getOption('id_room', 0); // check if managing room rates was requested $load_room_rates = $load_room ? $this->getOption('roomrates', 0) : 0; // check for a custom date (Y-m-d) $option_offset = $this->getOption('offset', ''); if ($option_offset && preg_match("/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/", $option_offset)) { // overwrite values $now_info = getdate(strtotime($option_offset)); $from_ts = mktime(0, 0, 0, $now_info['mon'], 1, $now_info['year']); } } // default labels $search_name = preg_replace("/[^A-Za-z0-9 ]/", '', JText::translate('VBOSTATSALLROOMS')); $period_date = VikBooking::sayMonth($now_info['mon']) . ' ' . $now_info['year']; // start looping from the first day of the current month $info_arr = getdate($from_ts); // week days counter $d_count = 0; $mon_lim = $info_arr['mon']; ?> <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); ?>"> <div class="vbo-admin-widget-head"> <div class="vbo-admin-widget-head-inline"> <h4><?php echo $this->widgetIcon; ?> <span><?php echo $this->widgetName; ?></span></h4> <div class="vbo-admin-widget-head-commands"> <div class="vbo-reportwidget-commands"> <div class="vbo-reportwidget-commands-main"> <div class="vbo-reportwidget-command-dates"> <div class="vbo-reportwidget-period-name"><?php echo $search_name; ?></div> <div class="vbo-reportwidget-period-date"><?php echo $period_date; ?></div> </div> <div class="vbo-reportwidget-command-chevron vbo-reportwidget-command-prev"> <span class="vbo-widget-booskcal-dt-prev" onclick="vboWidgetBookCalsMonthNav('<?php echo $wrapper_id; ?>', -1);"><?php VikBookingIcons::e('chevron-left'); ?></span> </div> <div class="vbo-reportwidget-command-chevron vbo-reportwidget-command-next"> <span class="vbo-widget-booskcal-dt-next" onclick="vboWidgetBookCalsMonthNav('<?php echo $wrapper_id; ?>', 1);"><?php VikBookingIcons::e('chevron-right'); ?></span> </div> </div> </div> </div> </div> </div> <div class="vbo-widget-booskcal-wrap"> <div class="vbo-widget-booskcal-inner"> <div class="vbo-widget-booskcal-top-wrap"> <div class="vbo-widget-booskcal-filter"> <select class="vbo-booskcal-roomid" onchange="vboWidgetBookCalsSetRoom('<?php echo $wrapper_id; ?>', this.value);"> <option data-units="0"></option> <?php foreach ($all_rooms as $rid => $room) { ?> <option value="<?php echo $rid; ?>" data-units="<?php echo $room['units']; ?>"<?php echo $rid == $load_room ? ' selected="selected"' : ''; ?>><?php echo $room['name']; ?></option> <?php } ?> </select> </div> <div class="vbo-widget-booskcal-newbook"> <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> <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> </div> </div> <div class="vbo-widget-booskcal-calendar"> <div class="vbo-widget-booskcal-calendar-table-wrap"> <table class="vbadmincaltable vbo-widget-booskcal-calendar-table"> <tbody> <tr class="vbadmincaltrmdays"> <?php // display week days in the proper order for ($i = 0; $i < 7; $i++) { $d_ind = ($i + $firstwday) < 7 ? ($i + $firstwday) : ($i + $firstwday - 7); ?> <td class="vbo-widget-booskcal-cell-wday"><?php echo $days_labels[$d_ind]; ?></td> <?php } ?> </tr> <tr> <?php // display empty cells until the first week-day of the month for ($i = 0, $n = $days_indexes[$info_arr['wday']]; $i < $n; $i++, $d_count++) { ?> <td class="vbo-widget-booskcal-cell-mday vbo-widget-booskcal-cell-empty">&nbsp;</td> <?php } // display month days while ($info_arr['mon'] == $mon_lim) { if ($d_count > 6) { $d_count = 0; // close current row and open a new one echo "\n</tr>\n<tr>\n"; } ?> <td class="vbo-widget-booskcal-cell-mday"> <span class="vbo-widget-booskcal-mday-val"><?php echo $info_arr['mday']; ?></span> </td> <?php $dayts = mktime(0, 0, 0, $info_arr['mon'], ($info_arr['mday'] + 1), $info_arr['year']); $info_arr = getdate($dayts); $d_count++; } // add empty cells until the end of the row for ($i = $d_count; $i <= 6; $i++) { ?> <td class="vbo-widget-booskcal-cell-mday vbo-widget-booskcal-cell-empty">&nbsp;</td> <?php } ?> </tr> </tbody> </table> </div> </div> </div> </div> <div class="vbo-widget-booskcal-html-helper" style="display: none;"> <div class="vbo-roverw-setnewrate-vcm-ota-pricing-alteration"> <div class="vbo-roverw-setnewrate-vcm-ota-alteration-elem"> <select data-alter-rule="rmodsop"> <option value="1">+</option> <option value="0">-</option> </select> </div> <div class="vbo-roverw-setnewrate-vcm-ota-alteration-elem"> <input type="number" value="" step="any" min="0" data-alter-rule="rmodsamount" /> </div> <div class="vbo-roverw-setnewrate-vcm-ota-alteration-elem"> <select data-alter-rule="rmodsval"> <option value="1">%</option> <option value="0"><?php echo $currencysymb; ?></option> </select> </div> </div> </div> </div> <?php if (static::$instance_counter === 0 || $is_ajax) { /** * Print the JS code only once for all instances of this widget. * The real rendering is made through AJAX, not when the page loads. */ ?> <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> <script type="text/javascript"> /** * The current room-ota relations and currency options. */ var vboWidgetBooksCalRoomOtaRels = {}; var vbo_currency_symbol = "<?php echo $currencysymb; ?>"; var vbo_currency_digits = "<?php echo $currency_digits; ?>"; var vbo_currency_decimals = "<?php echo $currency_decimals; ?>"; var vbo_currency_thousands = "<?php echo $currency_thousands; ?>"; /** * Room mini thumbnails. */ var vbo_widget_books_cal_mini_thumbs = <?php echo json_encode($mini_thumbnails); ?>; /** * Open the booking details page for the clicked reservation. */ function vboWidgetBooksCalOpenBooking(id) { var open_url = jQuery('.vbo-widget-bookscal-basenavuri').first().attr('href'); open_url = open_url.replace('%d', id); // navigate in a new tab window.open(open_url, '_blank'); } /** * Display the loading skeletons. */ function vboWidgetBooksCalSkeletons(wrapper) { var widget_instance = jQuery('#' + wrapper); if (!widget_instance.length) { return false; } var skelton_html = '<div class="vbo-skeleton-loading vbo-skeleton-loading-mday-cell"></div>'; widget_instance.find('.vbo-widget-booskcal-calendar').find('.vbo-widget-booskcal-cell-mday').attr('class', 'vbo-widget-booskcal-cell-mday').html(skelton_html); } /** * Perform the request to load the bookings calendar. */ function vboWidgetBooksCalLoad(wrapper, dates_direction, page_bid, load_room_rates) { var widget_instance = jQuery('#' + wrapper); if (!widget_instance.length) { return false; } // check if a navigation of dates was requested (0 = no dates nav) if (typeof dates_direction === 'undefined') { dates_direction = 0; } // check if multitask data passed a booking ID for the current page var force_bid = 0; if (typeof page_bid !== 'undefined' && page_bid && !isNaN(page_bid)) { force_bid = page_bid; } // get vars for making the request var current_offset = widget_instance.attr('data-offset'); var room_id = widget_instance.find('.vbo-booskcal-roomid').val(); // check for rates management flag var mng_rates_restr = widget_instance.find('input[name="vbo-wbookscal-mng-rates-restr"]').prop('checked'); if (load_room_rates) { mng_rates_restr = true; } // gather options, if any var options = vbo_widget_books_cal_options_oo || {}; if (vbo_widget_books_cal_options_oo) { // multitask data options should be used only once ("oo") vbo_widget_books_cal_options_oo = null; } // the widget method to call var call_method = 'loadBookingsCalendar'; // make a request to load the bookings calendar VBOCore.doAjax( "<?php echo $this->getExecWidgetAjaxUri(); ?>", { widget_id: "<?php echo $this->getIdentifier(); ?>", call: call_method, return: 1, bid: force_bid, offset: current_offset, room_id: room_id, date_dir: dates_direction, mng_rates_restr: (mng_rates_restr ? 1 : 0), wrapper: wrapper, _options: options, tmpl: "component" }, (response) => { try { var obj_res = typeof response === 'string' ? JSON.parse(response) : response; if (!obj_res.hasOwnProperty(call_method)) { console.error('Unexpected JSON response', obj_res); return false; } // set new offset for navigation widget_instance.attr('data-offset', obj_res[call_method]['offset']); // update search name and month widget_instance.find('.vbo-reportwidget-period-name').text(obj_res[call_method]['search_name']); widget_instance.find('.vbo-reportwidget-period-date').text(obj_res[call_method]['period_date']); // replace HTML with new bookings calendar widget_instance.find('.vbo-widget-booskcal-calendar').html(obj_res[call_method]['html']); // trigger the room rates and restrictions management, if requested if (room_id && mng_rates_restr) { setTimeout(() => { vboWidgetBooksCalManageRatesRestr(wrapper); }, 200); } } catch(err) { console.error('could not parse JSON response', err, response); } }, (error) => { // remove the skeleton loading widget_instance.find('.vbo-widget-booskcal-calendar').find('.vbo-skeleton-loading').remove(); console.error(error); } ); } /** * Toggles the management operations of room rates and restrictions. */ function vboWidgetBooksCalManageRatesRestr(wrapper) { var widget_instance = jQuery('#' + wrapper); if (!widget_instance.length) { return false; } var room_id = widget_instance.find('.vbo-booskcal-roomid').val(); if (!room_id) { // room ID must be selected widget_instance.find('.vbo-widget-booskcal-mday-ratesrestr').remove(); return false; } if (widget_instance.find('.vbo-widget-booskcal-mday-ratesrestr').length || !widget_instance.find('input[name="vbo-wbookscal-mng-rates-restr"]').prop('checked')) { // toggle off widget_instance.find('.vbo-widget-booskcal-mday-ratesrestr').remove(); return false; } // show loading 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> '); // month initial date let from_date = widget_instance.attr('data-offset'); // the widget method to call var call_method = 'loadRoomRates'; // make the request VBOCore.doAjax( "<?php echo $this->getExecWidgetAjaxUri(); ?>", { widget_id: "<?php echo $this->getIdentifier(); ?>", call: call_method, return: 1, room_id: room_id, from_date: from_date, wrapper: wrapper, tmpl: "component", }, (response) => { // hide loading widget_instance.find('.vbo-widget-bookscal-mngrates-loading').remove(); try { var obj_res = typeof response === 'string' ? JSON.parse(response) : response; if (!obj_res.hasOwnProperty(call_method)) { console.error('Unexpected JSON response', obj_res); return false; } for (const [day, rrestr] of Object.entries(obj_res[call_method]['rates_restrictions'])) { // get day element let day_elem = widget_instance.find('.vbo-widget-booskcal-cell-mday[data-ymd="' + day + '"]'); if (!day_elem.length) { continue; } // build HTML let is_cta = false; let is_ctd = false; let rrestr_html = '<div class="vbo-widget-booskcal-mday-ratesrestr">'; rrestr_html += '<div class="vbo-widget-booskcal-mday-roomrate" data-cost="' + rrestr.cost + '">'; rrestr_html += '<span><?php echo $currencysymb; ?> ' + rrestr.formatted_cost + '</span>'; rrestr_html += '</div>'; if ((rrestr.restrictions.minlos || 0) > 0 || rrestr.restrictions.cta || rrestr.restrictions.ctd) { // display restrictions is_cta = rrestr.restrictions.cta && rrestr.restrictions.cta.includes('-' + day_elem.attr('data-wday') + '-'); is_ctd = rrestr.restrictions.ctd && rrestr.restrictions.ctd.includes('-' + day_elem.attr('data-wday') + '-'); // build HTML 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') + '">'; if ((rrestr.restrictions.minlos || 0) > 0) { // display minimum stay rrestr_html += '<span>' + Joomla.JText._('VBO_MIN_STAY_SHORT') + ' ' + rrestr.restrictions.minlos + '</span>'; } if (is_cta) { // display CTA rrestr_html += '<span><?php VikBookingIcons::e('ban'); ?> ' + Joomla.JText._('VBO_CTA_SHORT') + '</span>'; } if (is_ctd) { // display CTD rrestr_html += '<span><?php VikBookingIcons::e('ban'); ?> ' + Joomla.JText._('VBO_CTD_SHORT') + '</span>'; } rrestr_html += '</div>'; } rrestr_html += '</div>'; // populate cell data attributes day_elem.attr('data-cta', is_cta ? '1' : '0'); day_elem.attr('data-ctd', is_ctd ? '1' : '0'); // append day-rates-restrictions element day_elem.append(rrestr_html); } } catch(err) { console.error('could not parse JSON response', err, response); } }, (error) => { // an error occurred widget_instance.find('.vbo-widget-booskcal-mday-ratesrestr').remove(); widget_instance.find('input[name="vbo-wbookscal-mng-rates-restr"]').prop('checked', false); // hide loading widget_instance.find('.vbo-widget-bookscal-mngrates-loading').remove(); // display error alert(error.responseText || 'An error occurred'); } ); } /** * Perform the request to load the month-day reservations. */ function vboWidgetBooksCalGetMdayRes(wrapper, ymd) { var widget_instance = jQuery('#' + wrapper); if (!widget_instance.length || !ymd) { return false; } // get vars for making the request var room_id = widget_instance.find('.vbo-booskcal-roomid').val(); var page_offset = widget_instance.find('.vbo-widget-booskcal-mday-list').attr('data-offset'); var page_length = widget_instance.find('.vbo-widget-booskcal-mday-list').attr('data-length'); // the widget method to call var call_method = 'loadMdayBookings'; // make a request to load the bookings calendar VBOCore.doAjax( "<?php echo $this->getExecWidgetAjaxUri(); ?>", { widget_id: "<?php echo $this->getIdentifier(); ?>", call: call_method, return: 1, page_offset: page_offset, page_length: page_length, ymd: ymd, room_id: room_id, wrapper: wrapper, tmpl: "component" }, (response) => { try { var obj_res = typeof response === 'string' ? JSON.parse(response) : response; if (!obj_res.hasOwnProperty(call_method)) { console.error('Unexpected JSON response', obj_res); return false; } // replace HTML with month-day reservations widget_instance.find('.vbo-widget-booskcal-mday-list').html(obj_res[call_method]['html']); } catch(err) { console.error('could not parse JSON response', err, response); } }, (error) => { // remove the skeleton loading widget_instance.find('.vbo-widget-booskcal-mday-list').html(''); console.error(error); } ); } /** * Navigate between the months and load the bookings calendar. */ function vboWidgetBookCalsMonthNav(wrapper, direction) { var widget_instance = jQuery('#' + wrapper); if (!widget_instance.length) { return false; } // show loading skeletons vboWidgetBooksCalSkeletons(wrapper); // launch dates navigation and load records vboWidgetBooksCalLoad(wrapper, direction); } /** * Change room calendar. */ function vboWidgetBookCalsSetRoom(wrapper, rid) { var widget_instance = jQuery('#' + wrapper); if (!widget_instance.length) { return false; } // check if we are creating a new booking if (widget_instance.find('.vbo-widget-booskcal-newbook-wrap').is(':visible')) { // check room units var vbo_bookscal_roomfilt = widget_instance.find('.vbo-booskcal-roomid'); if (rid) { // manage units depending on selected room total units var vbo_bookscal_room_units = vbo_bookscal_roomfilt.find('option:selected').attr('data-units'); if (vbo_bookscal_room_units > 1) { widget_instance.find('.vbo-widget-bookscal-units').attr('max', vbo_bookscal_room_units).val('1').closest('[data-noclosure="1"]').show(); } else { widget_instance.find('.vbo-widget-bookscal-units').val('1').closest('[data-noclosure="1"]').hide(); } } else { // hide units when no room is selected vbo_bookscal_roomfilt.closest('[data-noclosure="1"]').hide(); } // check the closure status if (widget_instance.find('input[name="closeroom"]').prop('checked')) { // reset values widget_instance.find('.vbo-widget-bookscal-units').val('1'); widget_instance.find('[data-noclosure="1"]').hide(); } // attempt to update the website rates vboWidgetBooksCalGetWebsiteRates(wrapper); // do nothing else when adding a new booking return; } // show loading skeletons vboWidgetBooksCalSkeletons(wrapper); // let the records be loaded for this new room filter vboWidgetBooksCalLoad(wrapper, 0); } /** * Generate the HTML skeleton string to the month-day reservations. */ function vboWidgetBooksCalMdaySkeleton() { var monthday_loading = ''; monthday_loading += '<div class="vbo-dashboard-guest-activity vbo-dashboard-guest-activity-skeleton">'; monthday_loading += ' <div class="vbo-dashboard-guest-activity-avatar">'; monthday_loading += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-avatar"></div>'; monthday_loading += ' </div>'; monthday_loading += ' <div class="vbo-dashboard-guest-activity-content">'; monthday_loading += ' <div class="vbo-dashboard-guest-activity-content-head">'; monthday_loading += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-title"></div>'; monthday_loading += ' </div>'; monthday_loading += ' <div class="vbo-dashboard-guest-activity-content-subhead">'; monthday_loading += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-subtitle"></div>'; monthday_loading += ' </div>'; monthday_loading += ' <div class="vbo-dashboard-guest-activity-content-info-msg">'; monthday_loading += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-content"></div>'; monthday_loading += ' </div>'; monthday_loading += ' </div>'; monthday_loading += '</div>'; return monthday_loading; } /** * Enter the month-day view mode from monthly view. */ function vboWidgetBooksCalMday(wrapper, element) { var widget_instance = jQuery('#' + wrapper); if (!widget_instance.length) { return false; } // get cell element var cell = jQuery(element); if (!cell || !cell.length) { return false; } // subscribe to the set new rate event for calculating the ota pricing information document.addEventListener('vbo-wbookscal-setnewrate-calc-ota-pricing-' + wrapper, vboWidgetBooksCalSetNewRateCalcOtaPricing); // set month-day title var day_read = cell.attr('data-dayread'); widget_instance.find('.vbo-widget-booskcal-mday-name').text(day_read); // get cell ymd value var day_ymd = cell.attr('data-ymd'); // always update the proper ymd day widget_instance.find('.vbo-widget-booskcal-mday-list').attr('data-ymd', day_ymd); // get pre-loaded booking ids var tot_day_res = 0; var day_bids = cell.attr('data-bids'); if (day_bids && day_bids.length) { tot_day_res = day_bids.split(',').length; } // populate loading skeletons for month-day bookings var monthday_loading = vboWidgetBooksCalMdaySkeleton(); if (tot_day_res > 1) { // double up the loading skeletons monthday_loading = monthday_loading + monthday_loading; } widget_instance.find('.vbo-widget-booskcal-mday-list').html(monthday_loading); // toggle elements widget_instance.find('.vbo-widget-booskcal-calendar-table-wrap').hide(); widget_instance.find('.vbo-widget-booskcal-newbook-wrap').hide(); widget_instance.find('.vbo-widget-booskcal-mday-wrap').show(); // check for room rates management let cell_rates_restr = cell.find('.vbo-widget-booskcal-mday-ratesrestr'); if (cell_rates_restr.length) { // build day-room pricing details let room_rates_restr = { cost: cell_rates_restr.find('.vbo-widget-booskcal-mday-roomrate').attr('data-cost'), min_los: 0, is_cta: 0, is_ctd: 0, }; // check for room restrictions for this day let cell_restrictions = cell_rates_restr.find('.vbo-widget-booskcal-mday-restrictions'); if (cell_restrictions.length) { room_rates_restr['min_los'] = parseInt(cell_restrictions.attr('data-minlos')); room_rates_restr['is_cta'] = parseInt(cell_restrictions.attr('data-cta')); room_rates_restr['is_ctd'] = parseInt(cell_restrictions.attr('data-ctd')); } // populate room pricing details let mday_pricing_cell = widget_instance.find('.vbo-widget-booskcal-mday-pricing'); mday_pricing_cell.find('.vbo-widget-booskcal-mday-pricing-cost').text(room_rates_restr['cost']); mday_pricing_cell.find('.vbo-widget-booskcal-mday-pricing-minlos').text(room_rates_restr['min_los']); let ctad_data = []; if (room_rates_restr['is_cta']) { ctad_data.push(Joomla.JText._('VBO_CTA_SHORT')); } if (room_rates_restr['is_ctd']) { ctad_data.push(Joomla.JText._('VBO_CTD_SHORT')); } mday_pricing_cell.find('.vbo-widget-booskcal-mday-pricing-ctad').text(ctad_data.join(', ')); // display room pricing details mday_pricing_cell.show(); // show loading 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>'); // the current room ID var room_id = widget_instance.find('.vbo-booskcal-roomid').val(); // the widget method to call var call_method = 'loadRoomDayRatePlans'; // make a request to load the bookings calendar VBOCore.doAjax( "<?php echo $this->getExecWidgetAjaxUri(); ?>", { widget_id: "<?php echo $this->getIdentifier(); ?>", call: call_method, return: 1, ymd: day_ymd, room_id: room_id, wrapper: wrapper, tmpl: "component" }, (response) => { // stop loading mday_pricing_cell.find('.vbo-widget-bookscal-mday-mngrates-loading').remove(); try { var obj_res = typeof response === 'string' ? JSON.parse(response) : response; if (!obj_res.hasOwnProperty(call_method) || !obj_res[call_method]['room_rates'].hasOwnProperty(day_ymd)) { console.error('Unexpected JSON response', obj_res); return false; } // remove the pricing node that will be rebuilt mday_pricing_cell.find('.vbo-widget-booskcal-mday-pricing-data-cost').remove(); // update room-ota relations object vboWidgetBooksCalRoomOtaRels = obj_res[call_method]['ota_relations']; // display the cost for each rate plan and related name obj_res[call_method]['room_rates'][day_ymd].forEach((tariff, index) => { // build rate plan HTML 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 + '">'; // check for derived rate data let derived_info = ''; if (obj_res[call_method]['derived_rates'].hasOwnProperty(tariff['idprice'])) { // build derived information HTML string derived_info = '<span class="badge badge-warning" title="' + obj_res[call_method]['derived_rates'][tariff['idprice']]['derived_info'] + '"><?php VikBookingIcons::e('link'); ?></span> '; } // keep building day_rplan_html += '<span class="vbo-widget-booskcal-mday-pricing-rplan">' + derived_info + tariff['name'] + '</span> '; day_rplan_html += '<span class="vbo-widget-booskcal-mday-pricing-currency"><?php echo VikBooking::getCurrencySymb(); ?></span> '; day_rplan_html += '<span class="vbo-widget-booskcal-mday-pricing-cost">' + tariff['formatted_cost'] + '</span> '; day_rplan_html += '<a class="vbo-widget-booskcal-mday-pricing-edit" href="JavaScript: void(0);" onclick="vboWidgetBooksCalToggleEditSetRate(this, \'' + wrapper + '\');">' + Joomla.JText._('VBMAINPAYMENTSEDIT') + '</a>'; // edit container day_rplan_html += '<div class="vbo-widget-booskcal-mday-pricing-edit-wrap" style="display: none;">'; day_rplan_html += ' <div class="vbo-widget-booskcal-mday-pricing-edit-block vbo-widget-booskcal-mday-pricing-edit-cost">'; day_rplan_html += ' <label>' + Joomla.JText._('VBRATESOVWSETNEWRATE') + '</label>'; day_rplan_html += ' <div class="vbo-widget-booskcal-mday-pricing-edit-input vbo-input-currency-wrap">'; day_rplan_html += ' <span class="vbo-widget-booskcal-mday-pricing-edit-currency"><?php echo VikBooking::getCurrencySymb(); ?></span> '; day_rplan_html += ' </div>'; day_rplan_html += ' </div>'; day_rplan_html += ' <div class="vbo-widget-booskcal-mday-pricing-edit-block vbo-widget-booskcal-mday-pricing-edit-minlos">'; day_rplan_html += ' <label>' + Joomla.JText._('VBOMINIMUMSTAYSET') + '</label>'; day_rplan_html += ' <div class="vbo-widget-booskcal-mday-pricing-edit-input">'; day_rplan_html += ' <input class="vbo-widget-booskcal-mday-pricing-edit-newminlos" type="number" value="' + tariff['restrictions']['minlos'] + '" min="0" max="99" />'; day_rplan_html += ' </div>'; day_rplan_html += ' </div>'; day_rplan_html += ' <div class="vbo-widget-booskcal-mday-pricing-edit-block vbo-widget-booskcal-mday-pricing-edit-otas">'; day_rplan_html += ' <label for="vbo-widget-booskcal-mday-pricing-edit-updotas-' + index + '">' + Joomla.JText._('VBOUPDRATESONCHANNELS') + '</label>'; day_rplan_html += ' <div class="vbo-widget-booskcal-mday-pricing-edit-input">'; 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 />'; day_rplan_html += ' </div>'; day_rplan_html += ' </div>'; day_rplan_html += ' <div class="vbo-widget-booskcal-mday-pricing-edit-block vbo-widget-booskcal-mday-pricing-edit-save">'; day_rplan_html += ' <button type="button" class="btn vbo-btn-black" onclick="vboWidgetBooksCalSetRateRestriction(this, \'' + wrapper + '\');">' + Joomla.JText._('VBAPPLY') + '</button>'; day_rplan_html += ' </div>'; day_rplan_html += ' <div class="vbo-widget-booskcal-mday-otapricing-wrap"></div>'; day_rplan_html += '</div>'; // close block day_rplan_html += '</div>'; // update restrictions from the AJAX response data mday_pricing_cell.find('.vbo-widget-booskcal-mday-pricing-minlos').text(tariff['restrictions']['minlos']); let ctad_data = []; if (tariff['restrictions']['cta'] && tariff['restrictions']['cta'].includes('-' + obj_res[call_method]['wday'] + '-')) { ctad_data.push(Joomla.JText._('VBO_CTA_SHORT')); } if (tariff['restrictions']['ctd'] && tariff['restrictions']['ctd'].includes('-' + obj_res[call_method]['wday'] + '-')) { ctad_data.push(Joomla.JText._('VBO_CTD_SHORT')); } mday_pricing_cell.find('.vbo-widget-booskcal-mday-pricing-ctad').text(ctad_data.join(', ')); // prepend pricing information block mday_pricing_cell.find('.vbo-widget-booskcal-mday-pricing-data').prepend(day_rplan_html); // build set-new-rate input field and related listener let new_rate_input = jQuery('<input/>'); new_rate_input.addClass('vbo-widget-booskcal-mday-pricing-edit-newcost'); new_rate_input.attr('type', 'number'); new_rate_input.attr('min', 0); new_rate_input.val(tariff['cost']); new_rate_input.on('input', VBOCore.debounceEvent((e) => { // dispatch the event to calculate the new OTA pricing value VBOCore.emitEvent('vbo-wbookscal-setnewrate-calc-ota-pricing-' + wrapper, { wrapper: wrapper, rate: e.target.value, room_id: room_id, rate_id: tariff['idprice'], }); }, 200)); // append input field mday_pricing_cell .find('.vbo-widget-booskcal-mday-pricing-data') .find('.vbo-widget-booskcal-mday-pricing-data-cost[data-rplan-id="' + tariff['idprice'] + '"]') .find('.vbo-widget-booskcal-mday-pricing-edit-cost') .find('.vbo-widget-booskcal-mday-pricing-edit-input') .append(new_rate_input); // populate room-ota relations vboWidgetBooksCalSetRoomOtaRelations(wrapper, room_id, tariff['idprice']); }); // replace raw checkbox with the hidden toggle button setTimeout(() => { widget_instance.find('.vbo-widget-booskcal-mday-pricing-data-cost').each(function(index, block) { // obtain the proper index to use for this rate plan let block_index = jQuery(this).attr('data-index'); // build the identifier string value for this toggle button let toggle_id = 'vbo-widget-booskcal-mday-pricing-edit-updotas-' + block_index; // get toggle button element let nice_toggle_el = widget_instance.find('.vbo-widget-booskcal-mday-hidden-helper-togglebtn').first().clone(); // adjust attributes to make them unique nice_toggle_el.find('input').addClass('vbo-widget-booskcal-mday-pricing-edit-updotas').attr('id', toggle_id); nice_toggle_el.find('label').attr('for', toggle_id); // replace checkbox element with toggle button element jQuery(this) .find('.vbo-widget-booskcal-mday-pricing-edit-otas') .find('input.vbo-widget-booskcal-mday-pricing-edit-updotas') .replaceWith(nice_toggle_el); }); }, 100); } catch(err) { console.error('could not parse JSON response', err, response); } }, (error) => { // stop loading mday_pricing_cell.find('.vbo-widget-bookscal-mday-mngrates-loading').remove(); // display error alert(error.responseText || 'An error occurred while loading the rate plan details.'); } ); } else { // make sure to hide the room pricing details widget_instance.find('.vbo-widget-booskcal-mday-pricing').hide(); } // launch month-day bookings retrieval vboWidgetBooksCalGetMdayRes(wrapper, day_ymd); } /** * Populates the room-ota pricing information. */ function vboWidgetBooksCalSetRoomOtaRelations(wrapper, room_id, rplan_id) { var widget_instance = jQuery('#' + wrapper); if (!widget_instance.length) { return false; } // the room-rate wrapper let rrate_wrapper = widget_instance.find('.vbo-widget-booskcal-mday-pricing-data-cost[data-rplan-id="' + rplan_id + '"]'); // the room-ota relations wrapper let rota_wrapper = rrate_wrapper.find('.vbo-widget-booskcal-mday-otapricing-wrap'); // always empty the wrapper rota_wrapper.html(''); if (!room_id || !rplan_id || !vboWidgetBooksCalRoomOtaRels.hasOwnProperty(room_id) || !vboWidgetBooksCalRoomOtaRels[room_id].hasOwnProperty('channels')) { // nothing to render return; } // start counter let ota_ch_counter = 0; // build and append room-OTA relations for (const ota_name in vboWidgetBooksCalRoomOtaRels[room_id]['channels']) { // build ota readable name let ota_read_name = ota_name; ota_read_name = ota_read_name.replace(/api$/, ''); ota_read_name = ota_read_name.replace(/^(google)(hotel|vr)$/i, '$1 $2'); // build room-ota relation block and elements let ota_block = jQuery('<div></div>'); ota_block.addClass('vbo-roverw-setnewrate-vcm-ota-relation'); let ota_block_inner = jQuery('<div></div>'); ota_block_inner .addClass('vbo-roverw-setnewrate-vcm-ota-relation-pricing') .attr('data-ota', (ota_name + '').toLowerCase()); let ota_block_channel = jQuery('<div></div>'); ota_block_channel .addClass('vbo-roverw-setnewrate-vcm-ota-relation-channel') .attr('data-otaid', vboWidgetBooksCalRoomOtaRels[room_id]['accounts'][ota_ch_counter]['idchannel']) .append('<img src="' + vboWidgetBooksCalRoomOtaRels[room_id]['channels'][ota_name] + '" />') .append('<span>' + ota_read_name + '</span>'); let ota_pricing_value = jQuery('<span></span>'); ota_pricing_value .addClass('vbo-roverw-setnewrate-vcm-ota-pricing-startvalue') .html('<?php VikBookingIcons::e('circle-notch', 'fa-spin fa-fw'); ?>') .on('click', function() { jQuery(this) .closest('.vbo-roverw-setnewrate-vcm-ota-relation-pricing') .find('.vbo-roverw-setnewrate-vcm-ota-channel-pricing') .toggle(); }); let ota_block_pricing = jQuery('<div></div>'); ota_block_pricing .addClass('vbo-roverw-setnewrate-vcm-ota-channel-pricing') .css('display', 'none') .append(jQuery('.vbo-roverw-setnewrate-vcm-ota-pricing-alteration').first().clone()); // register "input" event for select/input elements to control the channel alteration rule overrides ota_block_pricing.find('select, input').on('input', function() { let input_elem = jQuery(this); // get the current channel alteration command let ota_alteration_command = input_elem .closest('.vbo-roverw-setnewrate-vcm-ota-relation-pricing') .find('.vbo-roverw-setnewrate-ota-pricing-currentvalue[data-alteration]') .attr('data-alteration'); // access alteration rule and input value let rmod_type = input_elem.attr('data-alter-rule'); let rmod_value = input_elem.val(); if (!ota_alteration_command || !rmod_type || !(rmod_value + '').length) { return; } // check what pricing factor was changed if (rmod_type == 'rmodsop') { // increase or decrease rate let command_old_val = ota_alteration_command.substr(0, 1); let command_new_val = parseInt(rmod_value) == 1 ? '+' : '-'; ota_alteration_command = ota_alteration_command.replace(command_old_val, command_new_val); } else if (rmod_type == 'rmodsamount') { // amount let command_op = ota_alteration_command.substr(0, 1); let command_val = ota_alteration_command.substr(-1, 1); let command_old_val = ota_alteration_command.replace(command_op, '').replace(command_val, ''); let command_new_val = parseFloat(rmod_value); ota_alteration_command = ota_alteration_command.replace(command_old_val, command_new_val); } else if (rmod_type == 'rmodsval') { // percent or absolute let command_old_val = ota_alteration_command.substr(-1, 1); let command_new_val = parseInt(rmod_value) == 1 ? '%' : '*'; ota_alteration_command = ota_alteration_command.replace(command_old_val, command_new_val); } // get current currency options let currencyObj = VBOCore.getCurrency(); let orig_currency_options = currencyObj.getOptions(); // check if the channel requires a specific currency let ota_currency_data = input_elem .closest('.vbo-roverw-setnewrate-vcm-ota-relation-pricing') .find('.vbo-roverw-setnewrate-ota-pricing-willvalue') .attr('data-currency'); if (ota_currency_data) { // decode currency data instructions try { ota_currency_data = JSON.parse(ota_currency_data); } catch (e) { ota_currency_data = {}; } } // define the current channel alteration string (readable) let ota_alteration_string = ota_alteration_command; // finalize the current channel alteration string (readable) let ota_alteration_val = ota_alteration_string.substr(-1, 1); if (ota_alteration_val != '%') { ota_alteration_string = ota_alteration_string.replace(ota_alteration_val, '') + ((ota_currency_data && ota_currency_data?.symbol ? ota_currency_data.symbol : '') || vbo_currency_symbol); } // update the alteration rule command attribute input_elem .closest('.vbo-roverw-setnewrate-vcm-ota-relation-pricing') .find('.vbo-roverw-setnewrate-ota-pricing-currentvalue[data-alteration]') .attr('data-alteration', ota_alteration_command); // update the alteration rule string tag text input_elem .closest('.vbo-roverw-setnewrate-vcm-ota-relation-pricing') .find('.vbo-roverw-setnewrate-ota-pricing-currentvalue[data-alteration]') .html(ota_alteration_string); // get the current rate to set let current_room_rate = rrate_wrapper.find('.vbo-widget-booskcal-mday-pricing-edit-newcost').val(); if (current_room_rate) { // dispatch the event to trigger the re-calculation of the OTA rates VBOCore.emitEvent('vbo-wbookscal-setnewrate-calc-ota-pricing-' + wrapper, { wrapper: wrapper, rate: current_room_rate, room_id: room_id, rate_id: rplan_id, }); } }); // append elements to wrapper ota_block_channel.append(ota_pricing_value); ota_block_inner.append(ota_block_channel); ota_block_inner.append(ota_block_pricing); ota_block.append(ota_block_inner); rota_wrapper.append(ota_block); // increase OTA channel counter ota_ch_counter++; } // trigger an AJAX request to load the current alteration rules, if any VBOCore.doAjax( "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=pricing.loadOtaAlterationRules'); ?>", { room_id: room_id, rate_id: rplan_id, }, (res) => { var obj_res = typeof res === 'string' ? JSON.parse(res) : res; let alter_room_rates = obj_res['rmod'] == '1' || obj_res['rmod'] == 1; // scan all room OTAs rota_wrapper.find('.vbo-roverw-setnewrate-vcm-ota-relation').each(function(key, elem) { // get the current OTA identifier and whether pricing is altered let ota_wrap = jQuery(elem); let ota_id = ota_wrap.find('.vbo-roverw-setnewrate-vcm-ota-relation-channel').attr('data-otaid'); let alter_ota_rates = alter_room_rates && obj_res['channels'] && (obj_res['channels'].includes(ota_id) || obj_res['channels'].includes(parseInt(ota_id))); if (!alter_ota_rates && alter_room_rates && obj_res.hasOwnProperty('rmod_channels') && obj_res['rmod_channels'].hasOwnProperty(ota_id)) { alter_ota_rates = true; } // check if the current channel is using a different currency let ota_currency_data = {}; if (obj_res.hasOwnProperty('cur_rplans') && obj_res['cur_rplans'].hasOwnProperty(ota_id)) { let ota_check_currency = obj_res['cur_rplans'][ota_id]; if (obj_res.hasOwnProperty('currency_data_options') && obj_res['currency_data_options'].hasOwnProperty(ota_check_currency)) { // set custom currency data returned ota_currency_data = obj_res['currency_data_options'][ota_check_currency]; } } // build pricing alteration strings let alteration_command = ''; let alteration_string = ''; // default alteration factors (no pricing alteration rules) let alter_op = '+'; let alter_amount = '0'; let alter_val = '%'; if (alter_ota_rates) { // check how rates are altered for this channel if (obj_res.hasOwnProperty('rmod_channels') && obj_res['rmod_channels'].hasOwnProperty(ota_id)) { // ota-level pricing alteration rule if (parseInt(obj_res['rmod_channels'][ota_id]['rmod']) == 1) { alter_op = parseInt(obj_res['rmod_channels'][ota_id]['rmodop']) == 1 ? '+' : '-'; alter_amount = obj_res['rmod_channels'][ota_id]['rmodamount']; alter_val = parseInt(obj_res['rmod_channels'][ota_id]['rmodval']) == 1 ? '%' : '*'; } } else { // room-level pricing alteration rule alter_op = parseInt(obj_res['rmodop']) == 1 ? '+' : '-'; alter_amount = obj_res['rmodamount'] || '0'; alter_val = parseInt(obj_res['rmodval']) == 1 ? '%' : '*'; } } // construct alteration strings alteration_command = alter_op + (alter_amount + '') + (alter_val + ''); alteration_string = alter_op + (alter_amount + '') + (alter_val == '%' ? '%' : (ota_currency_data?.symbol || vbo_currency_symbol)); // stop room-ota loading and set alteration string let alteration_elem = jQuery('<span></span>'); alteration_elem .addClass('vbo-roverw-setnewrate-ota-pricing-currentvalue') .attr('data-alteration', alteration_command) .html(alteration_string); let will_alter_elem = jQuery('<span></span>').addClass('vbo-roverw-setnewrate-ota-pricing-willvalue'); if (ota_currency_data.symbol) { // set currency data object will_alter_elem.attr('data-currency', JSON.stringify(ota_currency_data)); } // set elements ota_wrap .find('.vbo-roverw-setnewrate-vcm-ota-pricing-startvalue') .html('') .append(will_alter_elem) .append(alteration_elem) .append('<?php VikBookingIcons::e('edit', 'edit-ota-pricing'); ?>'); // populate default values for input element overrides ota_wrap.find('select[data-alter-rule="rmodsop"]').val(alter_op == '+' ? 1 : 0); ota_wrap.find('input[data-alter-rule="rmodsamount"]').val(parseInt(alter_amount) > 0 ? alter_amount : ''); ota_wrap.find('select[data-alter-rule="rmodsval"]').val(alter_val == '%' ? 1 : 0); }); // check the current rate value let current_room_rate = rrate_wrapper.find('.vbo-widget-booskcal-mday-pricing-edit-newcost').val(); if (current_room_rate) { // dispatch the event to allow the actual calculation of the OTA rate VBOCore.emitEvent('vbo-wbookscal-setnewrate-calc-ota-pricing-' + wrapper, { wrapper: wrapper, rate: current_room_rate, room_id: room_id, rate_id: rplan_id, }); } }, (err) => { alert(err.responseText || 'Request Failed'); } ); } /** * Calculates the OTA pricing information when a new rate is input. */ function vboWidgetBooksCalSetNewRateCalcOtaPricing(e) { if (!e || !e.detail || !e.detail.wrapper || !e.detail.rate || !e.detail.room_id || !e.detail.rate_id) { // invalid event data return; } let widget_instance = document.querySelector('#' + e.detail.wrapper); if (!widget_instance) { // could not find widget instance return; } // the room-rate wrapper let rrate_wrapper = widget_instance.querySelector('.vbo-widget-booskcal-mday-pricing-data-cost[data-rplan-id="' + e.detail.rate_id + '"]'); // get the new PMS rate let rate_amount = parseFloat(e.detail.rate); // access the currency object let currencyObj = VBOCore.getCurrency({ symbol: vbo_currency_symbol, digits: vbo_currency_digits, decimals: vbo_currency_decimals, thousands: vbo_currency_thousands, noDecimals: 1, }); // scan all OTA alteration rules, if any rrate_wrapper.querySelectorAll('.vbo-roverw-setnewrate-ota-pricing-currentvalue[data-alteration]').forEach((elem) => { // channel alteration string let alter_string = elem.getAttribute('data-alteration'); if (!alter_string) { alter_string = '+0%'; } // default alteration factors (no pricing alteration rules) let alter_op = alter_string.substr(0, 1); let alter_val = alter_string.substr(-1, 1); let alter_amount = parseFloat(alter_string.replace(alter_op, '').replace(alter_val, '')); // calculate what the rate will be on the OTA let ota_rate_amount = rate_amount; if (!isNaN(alter_amount) && Math.abs(alter_amount) > 0) { if (alter_op == '+') { // increase rate if (alter_val == '%') { // percent let amount_inc = currencyObj.multiply(alter_amount, 0.01); amount_inc = currencyObj.multiply(rate_amount, amount_inc); ota_rate_amount = currencyObj.sum(rate_amount, amount_inc); } else { // absolute ota_rate_amount = currencyObj.sum(rate_amount, alter_amount); } } else { // discount rate if (alter_val == '%') { // percent let amount_inc = currencyObj.multiply(alter_amount, 0.01); amount_inc = currencyObj.multiply(rate_amount, amount_inc); ota_rate_amount = currencyObj.diff(rate_amount, amount_inc); } else { // absolute ota_rate_amount = currencyObj.diff(rate_amount, alter_amount); } } } // get the element containing the calculated ota pricing let will_alter_elem = elem .closest('.vbo-roverw-setnewrate-vcm-ota-pricing-startvalue') .querySelector('.vbo-roverw-setnewrate-ota-pricing-willvalue'); // define the currency options let ota_currency_options = {}; // check if the channel requires a specific currency let ota_currency_data = will_alter_elem.getAttribute('data-currency'); if (ota_currency_data) { // decode currency data instructions try { ota_currency_data = JSON.parse(ota_currency_data); } catch (e) { ota_currency_data = {}; } // set custom currency options if (ota_currency_data['symbol']) { ota_currency_options['symbol'] = ota_currency_data['symbol']; } if (ota_currency_data['decimals']) { ota_currency_options['digits'] = ota_currency_data['decimals']; } if (ota_currency_data['decimals_sep']) { ota_currency_options['decimals'] = ota_currency_data['decimals_sep']; } if (ota_currency_data['thousands_sep']) { ota_currency_options['thousands'] = ota_currency_data['thousands_sep']; } } // set calculated OTA rate value will_alter_elem.innerHTML = currencyObj.format(ota_rate_amount, ota_currency_options); }); } /** * Applies the new rate and restriction set for a room rate. */ function vboWidgetBooksCalSetRateRestriction(elem, wrapper) { var widget_instance = jQuery('#' + wrapper); if (!widget_instance.length) { return false; } // the clicked element let btn = jQuery(elem); // the rate plan block let rplan_block = btn.closest('.vbo-widget-booskcal-mday-pricing-data-cost'); // gather the rate plan ID let rplan_id = rplan_block.attr('data-rplan-id'); // disable button to prevent duplicate triggers btn.prop('disabled', true); // start loading btn.append(' <?php VikBookingIcons::e('circle-notch', 'fa-spin fa-fw'); ?>'); // whether to update OTAs let updotas = rplan_block.find('.vbo-widget-booskcal-mday-pricing-edit-updotas').prop('checked') ? 1 : 0; // check the OTA pricing alteration rules, if any let ota_pricing = {}; if (updotas) { // access the room-rate pricing block through plain JS let rrate_pricing_block = document.querySelector('#' + wrapper).querySelector('.vbo-widget-booskcal-mday-pricing-data-cost[data-rplan-id="' + rplan_id + '"]'); // scan all OTA alteration rules, if any rrate_pricing_block.querySelectorAll('.vbo-roverw-setnewrate-ota-pricing-currentvalue[data-alteration]').forEach((elem) => { // channel alteration string let alter_string = elem.getAttribute('data-alteration'); if (!alter_string) { alter_string = ''; } // access the parent node to get the OTA channel identifier let ota_id = elem .closest('.vbo-roverw-setnewrate-vcm-ota-relation-channel[data-otaid]') .getAttribute('data-otaid'); if (!ota_id || !alter_string || alter_string == '+0%' || alter_string == '+0*') { // avoid pushing an empty alteration command return; } // push OTA pricing alteration command ota_pricing[ota_id] = alter_string; }); } if (!Object.keys(ota_pricing).length) { // unset the object for the request ota_pricing = null; } // the widget method to call var call_method = 'setRoomDayRateRestiction'; // make a request to load the bookings calendar VBOCore.doAjax( "<?php echo $this->getExecWidgetAjaxUri(); ?>", { widget_id: "<?php echo $this->getIdentifier(); ?>", call: call_method, return: 1, ymd: rplan_block.attr('data-ymd'), rplan_id: rplan_id, room_id: widget_instance.find('.vbo-booskcal-roomid').val(), rate: rplan_block.find('.vbo-widget-booskcal-mday-pricing-edit-newcost').val(), minlos: rplan_block.find('.vbo-widget-booskcal-mday-pricing-edit-newminlos').val(), updotas: updotas, ota_pricing: ota_pricing, wrapper: wrapper, tmpl: "component" }, (response) => { // restore initial values btn.prop('disabled', false); btn.find('i').remove(); try { var obj_res = typeof response === 'string' ? JSON.parse(response) : response; if (!obj_res.hasOwnProperty(call_method)) { console.error('Unexpected JSON response', obj_res); return false; } // set new price rplan_block.find('.vbo-widget-booskcal-mday-pricing-cost').text( rplan_block.find('.vbo-widget-booskcal-mday-pricing-edit-newcost').val() ); // set success icon btn.append(' <?php VikBookingIcons::e('check-circle'); ?>'); // remove any rate/restriction data from the monthly view widget_instance.find('.vbo-widget-booskcal-mday-ratesrestr').remove(); // check if we should render the Channel Manager update result if (obj_res[call_method].hasOwnProperty('new_rates') && obj_res[call_method]['new_rates'].hasOwnProperty('vcm')) { // render results vboWidgetBooksCalRenderChannelManagerResult(obj_res[call_method]['new_rates']['vcm']); } // finalize the operation by going back to the monthly view with a little timeout setTimeout(() => { // reload rates and restrictions vboWidgetBooksCalManageRatesRestr(wrapper); // navigate back to the monthly view vboWidgetBooksCalMonth(wrapper); }, 200); } catch(err) { console.error('could not parse JSON response', err, response); } }, (error) => { // restore initial values btn.prop('disabled', false); btn.find('i').remove(); // display error alert(error.responseText || 'An error occurred while applying the room rate and restriction.'); } ); } /** * Renders the results of the Channel Manager update request for rates/restrictions. * Supports multiple rate plans due to derived/linkage rules. */ function vboWidgetBooksCalRenderChannelManagerResult(vcm_response) { if (!Array.isArray(vcm_response)) { // make sure the result is a list of result objects vcm_response = [vcm_response]; } // compose modal body var htmlres = '<div class="vbo-vcm-rates-res-container">'; vcm_response.forEach((obj) => { htmlres += '<div class="vbo-vcm-rates-res-rplan-wrap">'; if (obj.hasOwnProperty('rplan_name')) { htmlres += '<div class="vbo-vcm-rates-res-rplan-data">'; htmlres += '<strong>' + obj['rplan_name'] + '</strong>'; if (obj.hasOwnProperty('is_derived') && obj['is_derived']) { htmlres += ' <span class="label label-info">' + Joomla.JText._('VBO_IS_DERIVED_RATE') + '</span>'; } htmlres += '</div>'; } if (obj.hasOwnProperty('channels_success')) { htmlres += '<div class="vbo-vcm-rates-res-success">'; for (var ch_id in obj['channels_success']) { htmlres += '<div class="vbo-vcm-rates-res-channel">'; htmlres += ' <div class="vbo-vcm-rates-res-channel-esit">'; htmlres += ' <i class="<?php echo VikBookingIcons::i('check'); ?>"></i>'; htmlres += ' </div>'; htmlres += ' <div class="vbo-vcm-rates-res-channel-logo">'; if (obj['channels_updated'].hasOwnProperty(ch_id) && obj['channels_updated'][ch_id]['logo'].length) { htmlres += '<img src="'+obj['channels_updated'][ch_id]['logo']+'" />'; } else { htmlres += '<span>'+obj['channels_success'][ch_id]+'</span>'; } htmlres += ' </div>'; htmlres += '</div>'; } if (obj.hasOwnProperty('channels_bkdown')) { htmlres += '<div class="vbo-vcm-rates-res-bkdown">'; htmlres += ' <div><pre>'+obj['channels_bkdown']+'</pre></div>'; htmlres += '</div>'; } htmlres += '</div>'; } if (obj.hasOwnProperty('channels_warnings')) { htmlres += '<div class="vbo-vcm-rates-res-warning">'; for (var ch_id in obj['channels_warnings']) { htmlres += '<div class="vbo-vcm-rates-res-channel">'; htmlres += ' <div class="vbo-vcm-rates-res-channel-esit">'; htmlres += ' <i class="<?php echo VikBookingIcons::i('exclamation-triangle'); ?>"></i>'; htmlres += ' </div>'; htmlres += ' <div class="vbo-vcm-rates-res-channel-logo">'; if (obj['channels_updated'].hasOwnProperty(ch_id) && obj['channels_updated'][ch_id]['logo'].length) { htmlres += '<img src="'+obj['channels_updated'][ch_id]['logo']+'" />'; } else if (obj['channels_updated'].hasOwnProperty(ch_id)) { htmlres += '<span>'+obj['channels_updated'][ch_id]['name']+'</span>'; } htmlres += ' </div>'; htmlres += ' <div class="vbo-vcm-rates-res-channel-det">'; htmlres += ' <pre>'+obj['channels_warnings'][ch_id]+'</pre>'; htmlres += ' </div>'; htmlres += '</div>'; } htmlres += '</div>'; } if (obj.hasOwnProperty('channels_errors')) { htmlres += '<div class="vbo-vcm-rates-res-error">'; for (var ch_id in obj['channels_errors']) { htmlres += '<div class="vbo-vcm-rates-res-channel">'; htmlres += ' <div class="vbo-vcm-rates-res-channel-esit">'; htmlres += ' <i class="<?php echo VikBookingIcons::i('times'); ?>"></i>'; htmlres += ' </div>'; htmlres += ' <div class="vbo-vcm-rates-res-channel-logo">'; if (obj['channels_updated'].hasOwnProperty(ch_id) && obj['channels_updated'][ch_id]['logo'].length) { htmlres += ' <img src="'+obj['channels_updated'][ch_id]['logo']+'" />'; } else if (obj['channels_updated'].hasOwnProperty(ch_id)) { htmlres += ' <span>'+obj['channels_updated'][ch_id]['name']+'</span>'; } htmlres += ' </div>'; htmlres += ' <div class="vbo-vcm-rates-res-channel-det">'; htmlres += ' <pre>'+obj['channels_errors'][ch_id]+'</pre>'; htmlres += ' </div>'; htmlres += '</div>'; } htmlres += '</div>'; } htmlres += '</div>'; }); // close container htmlres += '</div>'; // display results within a modal window VBOCore.displayModal({ suffix: 'vbo-vcm-rates-res', extra_class: 'vbo-modal-rounded vbo-modal-dialog', title: Joomla.JText._('VBOVCMRATESRES'), body: htmlres, draggable: true, }); } /** * Toggles the block for setting a new rate/restriction. */ function vboWidgetBooksCalToggleEditSetRate(elem, wrapper) { let set_rate_block = jQuery(elem).closest('.vbo-widget-booskcal-mday-pricing-data-cost').find('.vbo-widget-booskcal-mday-pricing-edit-wrap'); if (set_rate_block.is(':visible')) { // hide clicked block set_rate_block.hide(); } else { // hide any other rate plan block jQuery('#' + wrapper).find('.vbo-widget-booskcal-mday-pricing-edit-wrap').hide(); // show clicked block set_rate_block.show(); } } /** * Navigate between the various pages of the month-day bookings. */ function vboWidgetBooksCalMdayNavigate(wrapper, direction) { var widget_instance = jQuery('#' + wrapper); if (!widget_instance.length) { return false; } // get bookings container var bookings_list = widget_instance.find('.vbo-widget-booskcal-mday-list'); // show loading skeletons var monthday_loading = vboWidgetBooksCalMdaySkeleton(); bookings_list.html(monthday_loading + monthday_loading); // get current offset and length (MUST be numbers, not strings) var current_offset = parseInt(bookings_list.attr('data-offset')); var current_length = parseInt(bookings_list.attr('data-length')); var day_ymd = bookings_list.attr('data-ymd'); // check direction and update offsets for nav if (direction > 0) { // navigate forward bookings_list.attr('data-offset', (current_offset + current_length)); } else { // navigate backward var new_offset = current_offset - current_length; new_offset = new_offset >= 0 ? new_offset : 0; bookings_list.attr('data-offset', new_offset); } // launch month-day bookings retrieval vboWidgetBooksCalGetMdayRes(wrapper, day_ymd); } /** * Go back to the montly/month-day view from the month-day/new-booking view. */ function vboWidgetBooksCalMonth(wrapper) { var widget_instance = jQuery('#' + wrapper); if (!widget_instance.length) { return false; } // remove listener for the set new rate event for calculating the ota pricing information document.removeEventListener('vbo-wbookscal-setnewrate-calc-ota-pricing-' + wrapper, vboWidgetBooksCalSetNewRateCalcOtaPricing); // check current day and room var current_ymd = ''; var current_rid = ''; if (widget_instance.find('.vbo-widget-booskcal-newbook-wrap').is(':visible')) { current_ymd = widget_instance.find('.vbo-widget-booskcal-newbook-wrap').attr('data-ymd'); current_rid = widget_instance.find('.vbo-widget-booskcal-newbook-wrap').attr('data-roomid'); if (current_rid != widget_instance.find('.vbo-booskcal-roomid').val()) { // going back from the new-booking view detected a change of the room filter, so we do a reset // show loading skeletons vboWidgetBooksCalSkeletons(wrapper); // let the records be loaded for this new room filter vboWidgetBooksCalLoad(wrapper, 0); // do not proceed return; } } // toggle elements depending on the current view widget_instance.find('.vbo-widget-booskcal-newbook-wrap').hide().attr('data-ymd', ''); if (current_ymd) { // back to the month-day view widget_instance.find('.vbo-widget-booskcal-calendar-table-wrap').hide(); widget_instance.find('.vbo-widget-booskcal-mday-wrap').show(); } else { // back to the monthly view widget_instance.find('.vbo-widget-booskcal-mday-wrap').hide(); widget_instance.find('.vbo-widget-booskcal-mday-pricing').hide(); widget_instance.find('.vbo-widget-booskcal-mday-pricing-edit-wrap').remove(); widget_instance.find('.vbo-widget-booskcal-mday-list').html('').attr('data-ymd', '').attr('data-offset', '0'); widget_instance.find('.vbo-widget-booskcal-calendar-table-wrap').show(); } } /** * Display the new booking interface by hiding the other elements. */ function vboWidgetBookCalsNewBooking(wrapper) { var widget_instance = jQuery('#' + wrapper); if (!widget_instance.length) { return false; } var current_ymd = ''; var current_rid = widget_instance.find('.vbo-booskcal-roomid').val(); if (widget_instance.find('.vbo-widget-booskcal-mday-wrap').is(':visible')) { current_ymd = widget_instance.find('.vbo-widget-booskcal-mday-list').attr('data-ymd'); } // toggle elements widget_instance.find('.vbo-widget-booskcal-calendar-table-wrap').hide(); widget_instance.find('.vbo-widget-booskcal-mday-wrap').hide(); widget_instance.find('.vbo-widget-booskcal-newbook-wrap').show().attr('data-ymd', current_ymd).attr('data-roomid', current_rid); // empty selected nights counter widget_instance.find('.vbo-widget-bookscal-nights-counter').hide().text(''); if (current_ymd) { // set the check-in date var ymd_parts = current_ymd.split('-'); 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))); // trigger the onSelect function in datepicker jQuery('.ui-datepicker-current-day').click(); } } /** * Toggle the room-closure status when creating a new booking. */ function vboWidgetBooksCalClosure(wrapper) { var widget_instance = jQuery('#' + wrapper); if (!widget_instance.length) { return false; } if (widget_instance.find('input[name="closeroom"]').prop('checked')) { widget_instance.find('.vbo-param-container[data-noclosure="1"]').hide(); } else { widget_instance.find('.vbo-param-container[data-noclosure="1"]').show(); // check selected room var vbo_bookscal_roomfilt = widget_instance.find('.vbo-booskcal-roomid'); if (vbo_bookscal_roomfilt.val()) { var vbo_bookscal_room_units = vbo_bookscal_roomfilt.find('option:selected').attr('data-units'); if (vbo_bookscal_room_units < 2) { widget_instance.find('.vbo-widget-bookscal-units').closest('[data-noclosure="1"]').hide(); } } else { widget_instance.find('.vbo-widget-bookscal-units').closest('[data-noclosure="1"]').hide(); } // hide website rates if not available widget_instance.find('.vbo-param-container[data-unavailable="1"]').hide(); } } /** * Open the modal to assign an existing or a new customer to the booking. */ function vboWidgetBooksCalAssignCustomer(wrapper) { var widget_instance = jQuery('#' + wrapper); if (!widget_instance.length) { return false; } var modal_body = VBOCore.displayModal({ extra_class: 'vbo-modal-rounded vbo-modal-tall', title: Joomla.JText._('VBFILLCUSTFIELDS'), footer_left: '<button type="button" class="btn" onclick="VBOCore.emitEvent(\'vbo-widget-booskcal-assigncustomer-dismiss\');">' + Joomla.JText._('VBANNULLA') + '</button>', footer_right: '<button type="button" class="btn btn-success" onclick="vboWidgetBooksCalSetCustomer(\'' + wrapper + '\');">' + Joomla.JText._('VBAPPLY') + '</button>', dismiss_event: 'vbo-widget-booskcal-assigncustomer-dismiss', loading_event: 'vbo-widget-booskcal-assigncustomer-loading', onDismiss: (e) => { if (!e || !e.detail) { // no event data received, maybe the modal was simply dismissed return; } // parse data received within the dismiss event try { let customer_data = JSON.parse(e.detail); // set values received widget_instance.find('.vbo-widget-bookscal-custid').val((customer_data['id'] || '')); widget_instance.find('.vbo-widget-bookscal-custmail').val((customer_data['email'] || '')); widget_instance.find('.vbo-widget-bookscal-custdata').val((customer_data['data'] || '')); widget_instance.find('.vbo-widget-bookscal-country').val((customer_data['country'] || '')); widget_instance.find('.vbo-widget-bookscal-state').val((customer_data['state'] || '')); widget_instance.find('.vbo-widget-bookscal-phone').val((customer_data['phone'] || '')); let tot_names = customer_data['nominatives'] ? customer_data['nominatives'].length : 0; if (tot_names > 1) { widget_instance.find('.vbo-widget-bookscal-tfname').val(customer_data['nominatives'][0]); widget_instance.find('.vbo-widget-bookscal-tlname').val(customer_data['nominatives'][1]); } if (tot_names) { widget_instance.find('.vbo-assign-customer').find('span').text(customer_data['nominatives'].join(' ')); } else { widget_instance.find('.vbo-assign-customer').find('span').text(Joomla.JText._('VBFILLCUSTFIELDS')); } } catch(e) { // log the error and abort console.error(e); return; } }, }); // start loading VBOCore.emitEvent('vbo-widget-booskcal-assigncustomer-loading'); // the widget method to call var call_method = 'displayCustomerFilling'; // make a request to load the bookings calendar VBOCore.doAjax( "<?php echo $this->getExecWidgetAjaxUri(); ?>", { widget_id: "<?php echo $this->getIdentifier(); ?>", call: call_method, return: 1, wrapper: wrapper, tmpl: "component" }, (response) => { // stop loading VBOCore.emitEvent('vbo-widget-booskcal-assigncustomer-loading'); try { var obj_res = typeof response === 'string' ? JSON.parse(response) : response; if (!obj_res.hasOwnProperty(call_method)) { // dismiss modal and abort VBOCore.emitEvent('vbo-widget-booskcal-assigncustomer-dismiss'); console.error('Unexpected JSON response', obj_res); return false; } // append modal content modal_body.append(obj_res[call_method]['html']); } catch(err) { console.error('could not parse JSON response', err, response); } }, (error) => { // stop loading VBOCore.emitEvent('vbo-widget-booskcal-assigncustomer-loading'); // display error alert(error.responseText); // dismiss modal VBOCore.emitEvent('vbo-widget-booskcal-assigncustomer-dismiss'); } ); } /** * Applies the provided customer information when creating a new booking. */ function vboWidgetBooksCalSetCustomer(wrapper) { var widget_instance = jQuery('#' + wrapper); if (!widget_instance.length) { return false; } var cfields_cont = ''; var cfield_vals = { id: jQuery('#vbo-widget-bookscal-cfield-custid' + wrapper).val(), email: '', phone: '', nominatives: [], country: '', state: '', data: '', }; jQuery('.vbo-calendar-cfields-filler[data-wrapper="' + wrapper + '"]').find('.vbo-calendar-cfield-entry').each(function() { var cfield_entry = jQuery(this); var cfield_name = cfield_entry.find('label').text(); var cfield_input = cfield_entry.find('span').find('input'); var cfield_textarea = cfield_entry.find('span').find('textarea'); var cfield_select = cfield_entry.find('span').find('select.vbo-calendar-cfield-country'); var cfield_state = cfield_entry.find('span').find('select.vbo-calendar-cfield-state'); var cfield_cont = ''; if (cfield_input.length) { cfield_cont = cfield_input.val(); if (cfield_input.attr('data-isemail') == '1' && cfield_cont && cfield_cont.length) { cfield_vals['email'] = cfield_cont; } if (cfield_input.attr('data-isphone') == '1') { cfield_vals['phone'] = cfield_cont; } if (cfield_input.attr('data-isnominative') == '1') { cfield_vals['nominatives'].push(cfield_cont); } } else if (cfield_textarea.length) { cfield_cont = cfield_textarea.val(); } else if (cfield_select.length) { cfield_cont = cfield_select.val(); if (cfield_cont && cfield_cont.length) { var country_code = jQuery('option:selected', cfield_select).attr('data-ccode'); if (country_code && country_code.length) { cfield_vals['country'] = country_code; } } } else if (cfield_state.length) { cfield_cont = cfield_state.val(); cfield_vals['state'] = cfield_cont; } if (cfield_cont && cfield_cont.length) { cfields_cont += cfield_name + ": " + cfield_cont + "\r\n"; } }); if (!cfields_cont.length) { // empty information alert(Joomla.JText._('VBO_PLEASE_FILL_FIELDS')); // do not proceed return false; } // clean up last new lines cfields_cont = cfields_cont.replace(/\r\n+$/, ""); // set raw customer data string cfield_vals['data'] = cfields_cont; // dimiss the modal by injecting the customer object information VBOCore.emitEvent('vbo-widget-booskcal-assigncustomer-dismiss', JSON.stringify(cfield_vals)); } /** * Reloads a list of states according to the given country. */ function vboWidgetBooksCalReloadStates(country_3_code, wrapper) { var states_elem = jQuery('.vbo-calendar-cfields-filler[data-wrapper="' + wrapper + '"]').find('select.vbo-calendar-cfield-state'); if (!states_elem.length || !country_3_code || !country_3_code.length) { return; } // unset the current states/provinces states_elem.html(''); // make a request to load the states/provinces of the selected country VBOCore.doAjax( "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=states.load_from_country'); ?>", { country_3_code: country_3_code, tmpl: "component" }, (response) => { try { var obj_res = typeof response === 'string' ? JSON.parse(response) : response; if (!obj_res) { console.error('Unexpected JSON response', obj_res); return false; } // append empty value states_elem.append('<option value="">-----</option>'); for (var i = 0; i < obj_res.length; i++) { // append state states_elem.append('<option value="' + obj_res[i]['state_2_code'] + '">' + obj_res[i]['state_name'] + '</option>'); } } catch(err) { console.error('could not parse JSON response', err, response); } }, (error) => { console.error(error); } ); } /** * Triggers when a custom field of type country is changed. */ function vboWidgetBooksCalChangeCountry(elem, wrapper) { var sel_country = jQuery(elem).find('option:selected'); if (!sel_country.length) { return false; } // trigger event for phone number jQuery('.vbo-calendar-cfield-phone').trigger('vboupdatephonenumber', sel_country.attr('data-c2code')); // reload state/province vboWidgetBooksCalReloadStates(sel_country.attr('data-ccode'), wrapper); } /** * Counts the nights of stay from the given stay dates. */ function vboWidgetBooksCalCalcNights(checkindate, checkoutdate) { let vbo_df = '<?php echo $this->getDateFormat('date'); ?>'; let checkin_parts = checkindate.split('/'); let checkout_parts = checkoutdate.split('/'); let vbcheckind = new Date(); let vbcheckoutd = new Date(); if (vbo_df == "d/m/Y") { let vbinmonth = parseInt(checkin_parts[1]); vbinmonth = vbinmonth - 1; let vbinday = parseInt(checkin_parts[0], 10); vbcheckind = new Date(checkin_parts[2], vbinmonth, vbinday); let vboutmonth = parseInt(checkout_parts[1]); vboutmonth = vboutmonth - 1; let vboutday = parseInt(checkout_parts[0], 10); vbcheckoutd = new Date(checkout_parts[2], vboutmonth, vboutday); } else if (vbo_df == "m/d/Y") { let vbinmonth = parseInt(checkin_parts[0]); vbinmonth = vbinmonth - 1; let vbinday = parseInt(checkin_parts[1], 10); vbcheckind = new Date(checkin_parts[2], vbinmonth, vbinday); let vboutmonth = parseInt(checkout_parts[0]); vboutmonth = vboutmonth - 1; let vboutday = parseInt(checkout_parts[1], 10); vbcheckoutd = new Date(checkout_parts[2], vboutmonth, vboutday); } else { let vbinmonth = parseInt(checkin_parts[1]); vbinmonth = vbinmonth - 1; let vbinday = parseInt(checkin_parts[2], 10); vbcheckind = new Date(checkin_parts[0], vbinmonth, vbinday); let vboutmonth = parseInt(checkout_parts[1]); vboutmonth = vboutmonth - 1; let vboutday = parseInt(checkout_parts[2], 10); vbcheckoutd = new Date(checkout_parts[0], vboutmonth, vboutday); } let vbdivider = 1000 * 60 * 60 * 24; let vbints = vbcheckind.getTime(); let vboutts = vbcheckoutd.getTime(); if (vboutts > vbints) { let utc1 = Date.UTC(vbcheckind.getFullYear(), vbcheckind.getMonth(), vbcheckind.getDate()); let utc2 = Date.UTC(vbcheckoutd.getFullYear(), vbcheckoutd.getMonth(), vbcheckoutd.getDate()); let vbnights = Math.ceil((utc2 - utc1) / vbdivider); if (!isNaN(vbnights) && vbnights > 0) { // return the properly calculated nights of stay return vbnights; } } // return 0 when the nights of stay cannot be calculated return 0; } /** * Calculates the website rates according to the input values. */ function vboWidgetBooksCalGetWebsiteRates(wrapper) { var widget_instance = jQuery('#' + wrapper); if (!widget_instance.length) { return false; } // unset previously selected rates, if any widget_instance.find('.vbo-widget-bookscal-roomcost').val(''); widget_instance.find('.vbo-widget-bookscal-idprice').val(''); widget_instance.find('.vbo-widget-bookscal-custcost').val('').attr('readonly', false); widget_instance.find('.vbo-cal-wbrate-wrap').removeClass('vbo-cal-wbrate-wrap-selected'); // gather values var is_closing = widget_instance.find('input[name="closeroom"]').prop('checked'); var room_id = widget_instance.find('.vbo-booskcal-roomid').val(); var checkinfdate = widget_instance.find('.vbo-widget-bookscal-checkindt').val(); var checkoutfdate = widget_instance.find('.vbo-widget-bookscal-checkoutdt').val(); var adults = widget_instance.find('.vbo-widget-bookscal-adults').val(); var children = widget_instance.find('.vbo-widget-bookscal-children').val(); var units = widget_instance.find('.vbo-widget-bookscal-units').val(); // empty selected nights counter widget_instance.find('.vbo-widget-bookscal-nights-counter').hide().text(''); if (checkinfdate && checkoutfdate) { let tot_sel_nights = vboWidgetBooksCalCalcNights(checkinfdate, checkoutfdate); if (tot_sel_nights) { widget_instance.find('.vbo-widget-bookscal-nights-counter').text(Joomla.JText._('VBDAYS') + ': ' + tot_sel_nights).show(); } } if (is_closing || !room_id || !checkinfdate || !checkoutfdate) { // do not proceed widget_instance.find('.vbo-website-rates-row').hide().attr('data-unavailable', '1'); return; } // tax settings var vbo_tax_included = <?php echo $prices_vat_included; ?>; // make the request VBOCore.doAjax( "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=calc_rates'); ?>", { id_room: room_id, checkinfdate: checkinfdate, checkoutfdate: checkoutfdate, num_nights: 0, num_adults: adults, num_children: children, units: units, only_rates: 1, tmpl: "component" }, (resp) => { try { obj_res = typeof resp === 'string' ? JSON.parse(resp) : resp; if (!obj_res[0].hasOwnProperty('idprice')) { widget_instance.find('.vbo-website-rates-row').hide().attr('data-unavailable', '1'); return false; } // display the rates obtained var wrhtml = ""; for (var i in obj_res) { if (!obj_res.hasOwnProperty(i)) { continue; } if (!vbo_tax_included && obj_res[i].hasOwnProperty('net') && obj_res[i].hasOwnProperty('fnet')) { obj_res[i]['tot'] = obj_res[i]['net']; obj_res[i]['ftot'] = obj_res[i]['fnet']; } wrhtml += "<div class=\"vbo-cal-wbrate-wrap\" onclick=\"vboWidgetBooksCalSelWebsiteRate(this, '" + wrapper + "');\">"; wrhtml += " <div class=\"vbo-cal-wbrate-inner\">"; wrhtml += " <span class=\"vbo-cal-wbrate-name\" data-idprice=\"" + obj_res[i]['idprice'] + "\">" + obj_res[i]['name'] + "</span>"; wrhtml += " <span class=\"vbo-cal-wbrate-cost\" data-cost=\"" + obj_res[i]['tot'] + "\">" + obj_res[i]['ftot'] + "</span>"; wrhtml += " </div>"; wrhtml += "</div>"; } widget_instance.find('.vbo-website-rates-cont').html(wrhtml); widget_instance.find('.vbo-website-rates-row').fadeIn().attr('data-unavailable', '0'); } catch(err) { widget_instance.find('.vbo-website-rates-row').hide().attr('data-unavailable', '1'); console.error("could not parse JSON response", resp); return false; } }, (error) => { widget_instance.find('.vbo-website-rates-row').hide().attr('data-unavailable', '1'); console.error("Error calculating the rates", error); } ); } /** * Attempts to display the tax rates drop down for the custom rate. */ function vboWidgetBooksCalFocusTaxes(wrapper) { var widget_instance = jQuery('#' + wrapper); if (!widget_instance.length) { return false; } var tax_rates_sel = widget_instance.find('.vbo-widget-bookscal-taxid'); if (tax_rates_sel.length) { tax_rates_sel.show(); } } /** * Select a website rate plan. */ function vboWidgetBooksCalSelWebsiteRate(elem, wrapper) { var widget_instance = jQuery('#' + wrapper); if (!widget_instance.length) { return false; } var rate = jQuery(elem); var idprice = rate.find('.vbo-cal-wbrate-name').attr('data-idprice'); var cost = rate.find('.vbo-cal-wbrate-cost').attr('data-cost'); var prev_idprice = widget_instance.find('.vbo-widget-bookscal-idprice').val(); // reset all selected classes widget_instance.find('.vbo-cal-wbrate-wrap').removeClass('vbo-cal-wbrate-wrap-selected'); if (prev_idprice && prev_idprice == idprice) { // rate plan has been de-selected widget_instance.find('.vbo-widget-bookscal-idprice').val(""); widget_instance.find('.vbo-widget-bookscal-roomcost').val(""); widget_instance.find('.vbo-widget-bookscal-custcost').attr('readonly', false); } else { // rate plan has been selected rate.addClass('vbo-cal-wbrate-wrap-selected'); widget_instance.find('.vbo-widget-bookscal-idprice').val(idprice); widget_instance.find('.vbo-widget-bookscal-roomcost').val(cost); widget_instance.find('.vbo-widget-bookscal-custcost').attr('readonly', true); } } /** * Save a new booking. */ function vboWidgetBooksCalSaveBooking(wrapper) { var widget_instance = jQuery('#' + wrapper); if (!widget_instance.length) { return false; } // gather all the necessary information var room_id = widget_instance.find('.vbo-booskcal-roomid').val(); var checkin = widget_instance.find('.vbo-widget-bookscal-checkindt').val(); var checkout = widget_instance.find('.vbo-widget-bookscal-checkoutdt').val(); var closure = widget_instance.find('input[name="closeroom"]').prop('checked') ? 1 : 0; var units = widget_instance.find('.vbo-widget-bookscal-units').val(); var adults = widget_instance.find('.vbo-widget-bookscal-adults').val(); var children = widget_instance.find('.vbo-widget-bookscal-children').val(); var cust_id = widget_instance.find('.vbo-widget-bookscal-custid').val(); var cust_email = widget_instance.find('.vbo-widget-bookscal-custmail').val(); var cust_data = widget_instance.find('.vbo-widget-bookscal-custdata').val(); var cust_country = widget_instance.find('.vbo-widget-bookscal-country').val(); var cust_state = widget_instance.find('.vbo-widget-bookscal-state').val(); var cust_phone = widget_instance.find('.vbo-widget-bookscal-phone').val(); var cust_tfname = widget_instance.find('.vbo-widget-bookscal-tfname').val(); var cust_tlname = widget_instance.find('.vbo-widget-bookscal-tlname').val(); var roomcost = widget_instance.find('.vbo-widget-bookscal-roomcost').val(); var idprice = widget_instance.find('.vbo-widget-bookscal-idprice').val(); var cust_roomcost = widget_instance.find('.vbo-widget-bookscal-custcost').val(); var taxid = widget_instance.find('.vbo-widget-bookscal-taxid').val(); if (!room_id || !checkin || !checkout) { // missing information alert(Joomla.JText._('VBO_PLEASE_FILL_FIELDS')); // abort return false; } // check if suggesting to mark the rooms as closed is necessary var set_units_closed = 0; var current_room_units = 0; var vbo_bookscal_roomfilt = widget_instance.find('.vbo-booskcal-roomid'); if (vbo_bookscal_roomfilt.val()) { var current_room_units = vbo_bookscal_roomfilt.find('option:selected').attr('data-units'); } if (!cust_id && !cust_data && !closure && current_room_units > 1) { // no closure, no customer email, multiple units, ask if the units should be marked as closed if (confirm(Joomla.JText._('VBO_MARK_UNITS_CLOSED'))) { set_units_closed = 1; cust_data = Joomla.JText._('VBDBTEXTROOMCLOSED'); } } if (!set_units_closed && !cust_id && !cust_data && !closure) { // missing information alert(Joomla.JText._('VBO_PLEASE_FILL_FIELDS')); // abort return false; } // loading content 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>'; // show loading widget_instance.find('.vbo-widget-booskcal-newbook-cont').prepend(loading_content); // the widget method to call var call_method = 'saveBooking'; // make a request to save the booking VBOCore.doAjax( "<?php echo $this->getExecWidgetAjaxUri(); ?>", { widget_id: "<?php echo $this->getIdentifier(); ?>", call: call_method, return: 1, room_id: room_id, checkin: checkin, checkout: checkout, closure: closure, units_closed: set_units_closed, units: units, adults: adults, children: children, cust_id: cust_id, cust_email: cust_email, cust_data: cust_data, cust_country: cust_country, cust_state: cust_state, cust_phone: cust_phone, cust_tfname: cust_tfname, cust_tlname: cust_tlname, roomcost: roomcost, idprice: idprice, cust_roomcost: cust_roomcost, taxid: taxid, wrapper: wrapper, tmpl: "component" }, (response) => { // stop loading widget_instance.find('.vbo-modal-overlay-content-backdrop').remove(); try { var obj_res = typeof response === 'string' ? JSON.parse(response) : response; if (!obj_res.hasOwnProperty(call_method)) { console.error('Unexpected JSON response', obj_res); return false; } // show last booking created widget_instance.find('.vbo-widget-booskcal-newbook-start').hide(); 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']); // check if should suggest to run VCM if (obj_res[call_method]['vcm_action']) { widget_instance.append('<p class="info" onclick="jQuery(this).remove();">' + obj_res[call_method]['vcm_action'] + '</p>'); } // register the last booking ID created vbo_widget_books_cal_last_new_bid = obj_res[call_method]['new_booking_id']; // reload bookings calendar // show loading skeletons vboWidgetBooksCalSkeletons(wrapper); // let the records be loaded for this new room filter vboWidgetBooksCalLoad(wrapper, 0); } catch(err) { console.error('could not parse JSON response', err, response); } }, (error) => { // stop loading widget_instance.find('.vbo-modal-overlay-content-backdrop').remove(); // display error console.error(error); alert(error.responseText); } ); } /** * Triggers when the newly created booking button is clicked. */ function vboWidgetBookCalsOpenNewBooking(wrapper) { var widget_instance = jQuery('#' + wrapper); if (!widget_instance.length) { return false; } var bid = widget_instance.find('.vbo-widget-booskcal-newbook-id').attr('data-bookingid'); // open the booking vboWidgetBooksCalOpenBooking(bid); // switch back to the regular button to create a new booking widget_instance.find('.vbo-widget-booskcal-newbook-id').hide(); widget_instance.find('.vbo-widget-booskcal-newbook-start').show(); } /** * Toggles the cancelled reservations. */ function vboWidgetBooksCalCancToggle(wrapper) { var widget_instance = jQuery('#' + wrapper); if (!widget_instance.length) { return false; } widget_instance.find('[data-type="cancelled"]').toggle(); } /** * Triggers when the multitask panel opens. */ function vboWidgetBooksCalMultitaskOpen(wrapper) { var widget_instance = jQuery('#' + wrapper); if (!widget_instance.length) { return false; } // check if a booking ID was set for this page var page_bid = widget_instance.attr('data-pagebid'); if (!page_bid || page_bid < 1) { return false; } // show loading skeletons vboWidgetBooksCalSkeletons(wrapper); // load data by injecting the current booking ID vboWidgetBooksCalLoad(wrapper, 0, page_bid); } /** * Handles the selection of a customer to assign to a new booking. */ function vboWidgetBooksCalHandleCustomerSelection(e) { if (!e?.detail?.element?.id) { return; } let custid = e.detail.element.id; let custemail = e.detail.element?.email; let custphone = e.detail.element?.phone; let custcountry = e.detail.element?.country; let custfirstname = e.detail.element?.first_name; let custlastname = e.detail.element?.last_name; // set customer ID jQuery('#vbo-widget-bookscal-cfield-custid<?php echo $wrapper_id; ?>').val(custid); // check previous custom fields if (e.detail.element?.cfields && typeof e.detail.element.cfields === 'object') { for (const [cfid, cfval] of Object.entries(e.detail.element.cfields)) { let fill_field = document.querySelector('#cfield' + cfid + '<?php echo $wrapper_id; ?>'); if (fill_field && cfval) { // set previous value fill_field.value = cfval; } } } // always populate basic information on custom fields let fields_wrap = jQuery('.vbo-calendar-cfields-filler[data-wrapper="<?php echo $wrapper_id; ?>"]'); if (custcountry) { if (custcountry.length > 3) { fields_wrap.find('select.vbo-calendar-cfield-country').val(custcountry); } else { let country_opt = fields_wrap.find('select.vbo-calendar-cfield-country').find('option[data-ccode="' + custcountry + '"]'); if (country_opt.length) { fields_wrap.find('select.vbo-calendar-cfield-country').val(country_opt.attr('value')); } } } fields_wrap.find('input[data-isnominative="1"]').each(function(k, v) { if (k == 0) { jQuery(this).val(custfirstname); return true; } if (k == 1) { jQuery(this).val(custlastname); return true; } return false; }); fields_wrap.find('input[data-isemail="1"]').val(custemail); fields_wrap.find('input[data-isphone="1"]').val(custphone); // do NOT set customer upon the selection just made, because data may need to be manually adjusted // vboWidgetBooksCalSetCustomer('<?php echo $wrapper_id; ?>'); } </script> <?php } ?> <script type="text/javascript"> // store the lastly created booking ID var vbo_widget_books_cal_last_new_bid = null; // store widget options, if any var vbo_widget_books_cal_options_oo = <?php echo json_encode($this->getOptions()); ?>; jQuery(function() { // when document is ready, load bookings calendar for this widget's instance vboWidgetBooksCalLoad('<?php echo $wrapper_id; ?>', 0, '<?php echo $modal_load_bid; ?>', <?php echo $load_room_rates ? 'true' : 'false'; ?>); // convert the select to a Select2 element if (typeof jQuery.fn.select2 !== 'undefined' && <?php echo $use_nice_select; ?>) { jQuery('#<?php echo $wrapper_id; ?>').find('select.vbo-booskcal-roomid').select2({ width: "100%", placeholder: "<?php echo htmlspecialchars(VikBooking::strTrimLiteral(JText::translate('VBOREPORTSROOMFILT'))); ?>", allowClear: true, templateResult: (element) => { if (typeof vbo_widget_books_cal_mini_thumbs !== 'undefined' && vbo_widget_books_cal_mini_thumbs.hasOwnProperty((element.id || 0))) { return jQuery('<span class="vbo-sel2-element-img"><img src="' + vbo_widget_books_cal_mini_thumbs[element.id] + '" /> <span>' + element.text + '</span></span>'); } else { return element.text; } }, }); } // subscribe to the multitask-panel-open event document.addEventListener(VBOCore.multitask_open_event, function() { vboWidgetBooksCalMultitaskOpen('<?php echo $wrapper_id; ?>'); }); // subscribe to the multitask-panel-close event to emit the event for the lastly created booking ID document.addEventListener(VBOCore.multitask_close_event, function() { if (vbo_widget_books_cal_last_new_bid) { // emit the event with data for anyone who is listening to it VBOCore.emitEvent('vbo_new_booking_created', { bid: vbo_widget_books_cal_last_new_bid }); } }); // subscribe to the event for choosing a customer to assign to a new booking document.addEventListener('vbo-widget-books-cal-choose-customer-<?php echo $wrapper_id; ?>', vboWidgetBooksCalHandleCustomerSelection); <?php if ($js_modal_id) { // widget can be dismissed through the modal ?> // subscribe to the modal-dismissed event to emit the event for the lastly created booking ID document.addEventListener(VBOCore.widget_modal_dismissed + '<?php echo $js_modal_id; ?>', function() { if (vbo_widget_books_cal_last_new_bid) { // emit the event with data for anyone who is listening to it VBOCore.emitEvent('vbo_new_booking_created', { bid: vbo_widget_books_cal_last_new_bid }); } // remove the event for handling the selection of a customer document.removeEventListener('vbo-widget-books-cal-choose-customer-<?php echo $wrapper_id; ?>', vboWidgetBooksCalHandleCustomerSelection); }); <?php } ?> }); </script> <?php } /** * Helper method to load the booking cancellations for the given room and date. * * @param int $room_id the Vik Booking room ID. * @param string $ymd the current calendar date in Y-m-d format. * * @return array list of involved booking cancellations. * * @since 1.16.1 (J) - 1.6.1 (WP) */ protected function loadCancellations($room_id, $ymd) { $dbo = JFactory::getDbo(); if (empty($room_id) || empty($ymd)) { return []; } $stay_date_info = getdate(strtotime($ymd)); $lim_ts_to = mktime(23, 59, 59, $stay_date_info['mon'], $stay_date_info['mday'], $stay_date_info['year']); $q = $dbo->getQuery(true); $q->select($dbo->qn('o') . '.*'); $q->from($dbo->qn('#__vikbooking_orders', 'o')); $q->leftjoin($dbo->qn('#__vikbooking_ordersrooms', 'or') . ' ON ' . $dbo->qn('o.id') . ' = ' . $dbo->qn('or.idorder')); $q->where($dbo->qn('o.status') . ' = ' . $dbo->q('cancelled')); $q->where($dbo->qn('o.checkin') . ' <= ' . $lim_ts_to); $q->where($dbo->qn('o.checkout') . ' > ' . $lim_ts_to); $q->where($dbo->qn('or.idroom') . ' = ' . (int)$room_id); $q->group($dbo->qn('o.id')); $q->order($dbo->qn('o.id') . ' ASC'); $dbo->setQuery($q); $cancellations = $dbo->loadAssocList(); // join the customer information with a separate query as this is faster than joining two more tables at once foreach ($cancellations as &$canc_book) { $q = $dbo->getQuery(true); $q->select($dbo->qn('co.idcustomer')); $q->select('CONCAT_WS(" ", ' . $dbo->qn('c.first_name') . ', ' . $dbo->qn('c.last_name') . ') AS ' . $dbo->qn('customer_fullname')); $q->select($dbo->qn('c.country', 'customer_country')); $q->select($dbo->qn('c.pic')); $q->from($dbo->qn('#__vikbooking_customers_orders', 'co')); $q->leftjoin($dbo->qn('#__vikbooking_customers', 'c') . ' ON ' . $dbo->qn('c.id') . ' = ' . $dbo->qn('co.idcustomer')); $q->where($dbo->qn('co.idorder') . ' = ' . (int)$canc_book['id']); $dbo->setQuery($q); $customer_data = $dbo->loadAssoc(); if ($customer_data) { // merge properties $canc_book = array_merge($canc_book, $customer_data); } } // unset last reference unset($canc_book); // return the list of cancellation bookings return $cancellations; } /** * When Multitask Data Options are injected with an overbooking flag, this * method loads the overbooking details and sets the proper option values. * * @param int $bid the overbooking reservation ID. * * @return void * * @since 1.16.8 (J) - 1.6.8 (WP) */ protected function prepareOverbookingOptions($bid) { // get the booking details $booking_info = VikBooking::getBookingInfoFromID($bid); if (!$booking_info || strcasecmp((string) $booking_info['type'], 'overbooking')) { return; } $booking_checkin_dt = date('Y-m-d', $booking_info['checkin']); $booking_checkout_dt = date('Y-m-d', $booking_info['checkout']); // access the availability helper object $av_helper = VikBooking::getAvailabilityInstance(true) ->setStayDates($booking_checkin_dt, $booking_checkout_dt); // get stay date timestamps list($checkin_ts, $checkout_ts) = $av_helper->getStayDates(true); // count length of stay and nights involved $tot_nights = $av_helper->countNightsOfStay(); $groupdays = VikBooking::getGroupDays($checkin_ts, $checkout_ts, $tot_nights); // get all rooms involved $booking_rooms = VikBooking::loadOrdersRoomsData($booking_info['id']); $room_ids = array_unique(array_column($booking_rooms, 'idroom')); // load all the occupied records for the involved rooms $busy_records = VikBooking::loadBusyRecords($room_ids, $checkin_ts, strtotime('+1 day', $checkout_ts)); // scan all rooms to find the first date in overbooking state foreach ($booking_rooms as $booking_room) { $room_id = $booking_room['idroom']; $room_data = VikBooking::getRoomInfo($room_id, ['units']); if (empty($room_data['units']) || !isset($busy_records[$room_id])) { // room is no longer on db or has got no occupied records continue; } // scan all nights involved foreach ($groupdays as $gday) { // start counter $bfound = 0; // scan all the occupied records foreach ($busy_records[$room_id] as $bu) { $busy_info_in = getdate($bu['checkin']); $busy_info_out = getdate($bu['checkout']); $busy_in_ts = mktime(0, 0, 0, $busy_info_in['mon'], $busy_info_in['mday'], $busy_info_in['year']); $busy_out_ts = mktime(0, 0, 0, $busy_info_out['mon'], $busy_info_out['mday'], $busy_info_out['year']); if ($gday >= $busy_in_ts && $gday <= $busy_out_ts) { // room is occupied $bfound++; } // check if the room is overbooked if ($bfound > $room_data['units']) { // overbooking date found, inject data and abort $overbooking_dt = date('Y-m-d', $gday); // set "offset" and "day" widget options $this->setOption('offset', $overbooking_dt); $this->setOption('day', $overbooking_dt); // set room ID widget option $this->setOption('id_room', $room_id); // abort for information found return; } } } } // fallback onto setting the first values available // set "offset" and "day" widget options $this->setOption('offset', $booking_checkin_dt); $this->setOption('day', $booking_checkin_dt); $booking_rooms = VikBooking::loadOrdersRoomsData($booking_info['id']); foreach ($booking_rooms as $booking_room) { // set room ID widget option $this->setOption('id_room', $booking_room['idroom']); break; } } }