File "finance.php"

Full Path: /home/romayxjt/public_html/wp-content/plugins/vikbooking/admin/helpers/widgets/finance.php
File size: 65.6 KB
MIME-type: text/x-php
Charset: utf-8

<?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 "finance".
 * 
 * @since 	1.16.0 (J) - 1.6.0 (WP)
 */
class VikBookingAdminWidgetFinance 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;

	/**
	 * Class constructor will define the widget name and identifier.
	 */
	public function __construct()
	{
		// call parent constructor
		parent::__construct();

		$this->widgetName = JText::translate('VBO_W_FINANCE_TITLE');
		$this->widgetDescr = JText::translate('VBO_W_FINANCE_DESCR');
		$this->widgetId = basename(__FILE__, '.php');

		// define widget and icon and style name
		$this->widgetIcon = '<i class="' . VikBookingIcons::i('piggy-bank') . '"></i>';
		$this->widgetStyleName = 'red';
	}

	/**
	 * Preload the necessary CSS/JS assets.
	 * 
	 * @return 	void
	 */
	public function preload()
	{
		// load assets for datepicker
		$this->vbo_app->loadDatePicker();

		// load assets for contextual menu
		$this->vbo_app->loadContextMenuAssets();

		// check for daily welcome message with stats
		$this->setupWelcomeStats();

		// lang vars for JS
		JText::script('VBO_COMPARE_WITH_LAST_Y');
		JText::script('VBO_COMPARE_WITH_PREV_Q');
		JText::script('VBO_COMPARE_WITH_PREV_M');
		JText::script('VBO_COMPARE_WITH_PREV_W');
		JText::script('VBO_COMPARE_WITH_PREV_D');
		JText::script('VBNOTRACKINGS');
		JText::script('VBO_ERR_LOAD_RESULTS');
		JText::script('VBNOROOMSFOUND');
		JText::script('VBO_SEARCHING');
		JText::script('VBROOMFILTER');
	}

	/**
	 * Custom method for this widget only to load the financial statistics.
	 * 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.
	 * 
	 * It's the actual rendering of the widget which also allows navigation.
	 */
	public function loadFinancialStats()
	{
		$app = JFactory::getApplication();

		$wrapper = $app->input->getString('wrapper', '');

		$fromdate = $app->input->getString('fromdate', '');
		$todate   = $app->input->getString('todate', '');
		$step 	  = $app->input->getString('step', 'quarter');
		$date_dir = $app->input->getInt('date_dir', 0);
		$room_ids = array_filter((array) $app->input->getInt('room_ids', []));

		if (empty($fromdate)) {
			// default to current quarter (3 full months)
			$now_info = getdate();
			$end_info = getdate(mktime(0, 0, 0, ($now_info['mon'] + 2), 1, $now_info['year']));
			$to_ts    = mktime(23, 59, 59, $end_info['mon'], date('t', $end_info[0]), $end_info['year']);
			$fromdate = date('Y-m-d', mktime(0, 0, 0, $now_info['mon'], 1, $now_info['year']));
			$todate   = date('Y-m-d', $to_ts);
		}

		// convert dates to timestamps to support custom date formats
		$from_ts = VikBooking::getDateTimestamp($fromdate);
		$to_ts 	 = VikBooking::getDateTimestamp($todate, 23, 59, 59);

		// check dates navigation and step
		$step_name = '';
		if ($date_dir > 0 || $date_dir < 0) {
			// calculate forward or backward dates for navigation
			list($from_ts, $to_ts, $step_name) = $this->calcDatesNavigation($from_ts, $to_ts, $step, $date_dir);
		}

		// convert final dates to Y-m-d
		$fromdate = date('Y-m-d', $from_ts);
		$todate   = date('Y-m-d', $to_ts);

		// stats calculation type
		$calc_type = !strcasecmp($step, 'booking_dates') ? 'booking_dates' : 'stay_dates';

		// access the finance helper object
		$finance = VBOTaxonomyFinance::getInstance();

		// currency symbol
		$currencysymb = VikBooking::getCurrencySymb();

		// get the financial stats for the requested dates
		try {
			$stats = $finance->getStats($fromdate, $todate, $room_ids, $calc_type);
		} catch (Exception $e) {
			// make the AJAX request fail nicely
			VBOHttpDocument::getInstance()->close($e->getCode(), $e->getMessage());
		}

		// language string identifier for step-comparison
		$js_step_comp_str = 'VBO_COMPARE_WITH_PREV_Q';
		if ($step == 'month') {
			$js_step_comp_str = 'VBO_COMPARE_WITH_PREV_M';
		} elseif ($step == 'week') {
			$js_step_comp_str = 'VBO_COMPARE_WITH_PREV_W';
		} elseif (!strcasecmp($step, 'booking_dates')) {
			$js_step_comp_str = 'VBO_COMPARE_WITH_PREV_D';
		}

		// start output buffering
		ob_start();

		if ($room_ids) {
			// get the details of the rooms filtered
			$rooms_filtered = VikBooking::getAvailabilityInstance()->loadRooms($room_ids);

			if ($rooms_filtered) {
				?>
		<div class="vbo-widget-finance-rooms-filtered-cont">
			<?php echo implode(', ', array_column($rooms_filtered, 'name')); ?>
		</div>
				<?php
			}
		}

		?>
		<div class="vbo-widget-finance-data-blocks">

			<div class="vbo-widget-finance-data-block" data-typestat="gross_revenue">
				<div class="vbo-widget-finance-stat">
					<div class="vbo-widget-finance-stat-info">
						<span class="vbo-widget-finance-stat-name"><?php echo JText::translate('VBO_GROSS_BOOKING_VALUE'); ?></span>
						<span class="vbo-widget-finance-stat-cmd"><?php VikBookingIcons::e('ellipsis-v'); ?></span>
					</div>
					<div class="vbo-widget-finance-stat-amount">
						<span class="vbo-widget-finance-stat-amount-value vbo-tooltip vbo-tooltip-top" data-tooltiptext="<?php echo $currencysymb . ' ' . VikBooking::numberFormat($stats['gross_revenue']); ?>">
							<span class="vbo-currency"><?php echo $currencysymb; ?></span>
							<span class="vbo-price"><?php echo $finance->numberFormatShort($stats['gross_revenue']); ?></span>
						</span>
					</div>
				</div>
			</div>

			<div class="vbo-widget-finance-data-block" data-typestat="taxes">
				<div class="vbo-widget-finance-stat">
					<div class="vbo-widget-finance-stat-info">
						<span class="vbo-widget-finance-stat-name"><?php echo JText::translate('VBOINVCOLTAX'); ?></span>
						<span class="vbo-widget-finance-stat-cmd"><?php VikBookingIcons::e('ellipsis-v'); ?></span>
					</div>
					<div class="vbo-widget-finance-stat-amount">
						<span class="vbo-widget-finance-stat-amount-value vbo-tooltip vbo-tooltip-top" data-tooltiptext="<?php echo $currencysymb . ' ' . VikBooking::numberFormat($stats['taxes']); ?>">
							<span class="vbo-currency"><?php echo $currencysymb; ?></span>
							<span class="vbo-price"><?php echo $finance->numberFormatShort($stats['taxes']); ?></span>
						</span>
					</div>
				</div>
			</div>

			<div class="vbo-widget-finance-data-block" data-typestat="cmms">
				<div class="vbo-widget-finance-stat">
					<div class="vbo-widget-finance-stat-info">
						<span class="vbo-widget-finance-stat-name"><?php echo JText::translate('VBTOTALCOMMISSIONS'); ?></span>
						<span class="vbo-widget-finance-stat-cmd"><?php VikBookingIcons::e('ellipsis-v'); ?></span>
					</div>
					<div class="vbo-widget-finance-stat-amount">
						<span class="vbo-widget-finance-stat-amount-value vbo-tooltip vbo-tooltip-top" data-tooltiptext="<?php echo $currencysymb . ' ' . VikBooking::numberFormat($stats['cmms']); ?>">
							<span class="vbo-currency"><?php echo $currencysymb; ?></span>
							<span class="vbo-price"><?php echo $finance->numberFormatShort($stats['cmms']); ?></span>
						</span>
					</div>
				</div>
			</div>

			<div class="vbo-widget-finance-data-block" data-typestat="revenue">
				<div class="vbo-widget-finance-stat">
					<div class="vbo-widget-finance-stat-info">
						<span class="vbo-widget-finance-stat-name"><?php echo JText::translate('VBOREPORTREVENUE'); ?></span>
						<span class="vbo-widget-finance-stat-cmd"><?php VikBookingIcons::e('ellipsis-v'); ?></span>
					</div>
					<div class="vbo-widget-finance-stat-amount">
						<span class="vbo-widget-finance-stat-amount-value vbo-tooltip vbo-tooltip-top" data-tooltiptext="<?php echo $currencysymb . ' ' . VikBooking::numberFormat($stats['revenue']); ?>">
							<span class="vbo-currency"><?php echo $currencysymb; ?></span>
							<span class="vbo-price"><?php echo $finance->numberFormatShort($stats['revenue']); ?></span>
						</span>
					</div>
				</div>
			</div>

			<div class="vbo-widget-finance-data-block" data-typestat="ibe_revenue">
				<div class="vbo-widget-finance-stat">
					<div class="vbo-widget-finance-stat-info">
						<span class="vbo-widget-finance-stat-name"><?php echo JText::translate('VBOREPORTREVENUEREVWEB'); ?></span>
						<span class="vbo-widget-finance-stat-cmd"><?php VikBookingIcons::e('ellipsis-v'); ?></span>
					</div>
					<div class="vbo-widget-finance-stat-amount">
						<span class="vbo-widget-finance-stat-amount-value vbo-tooltip vbo-tooltip-top" data-tooltiptext="<?php echo $currencysymb . ' ' . VikBooking::numberFormat($stats['ibe_revenue']); ?>">
							<span class="vbo-currency"><?php echo $currencysymb; ?></span>
							<span class="vbo-price"><?php echo $finance->numberFormatShort($stats['ibe_revenue']); ?></span>
						</span>
					</div>
				</div>
			</div>

			<div class="vbo-widget-finance-data-block" data-typestat="ota_revenue">
				<div class="vbo-widget-finance-stat">
					<div class="vbo-widget-finance-stat-info">
						<span class="vbo-widget-finance-stat-name"><?php echo JText::translate('VBOREPORTREVENUEREVOTA'); ?></span>
						<span class="vbo-widget-finance-stat-cmd"><?php VikBookingIcons::e('ellipsis-v'); ?></span>
					</div>
					<div class="vbo-widget-finance-stat-amount">
						<span class="vbo-widget-finance-stat-amount-value vbo-tooltip vbo-tooltip-top" data-tooltiptext="<?php echo $currencysymb . ' ' . VikBooking::numberFormat($stats['ota_revenue']); ?>">
							<span class="vbo-currency"><?php echo $currencysymb; ?></span>
							<span class="vbo-price"><?php echo $finance->numberFormatShort($stats['ota_revenue']); ?></span>
						</span>
					</div>
				</div>
			</div>

		<?php
		if ($stats['cmm_savings'] > 0) {
			?>
			<div class="vbo-widget-finance-data-block" data-typestat="ota_avg_cmms">
				<div class="vbo-widget-finance-stat">
					<div class="vbo-widget-finance-stat-info">
						<span class="vbo-widget-finance-stat-name"><?php echo JText::translate('VBO_AVG_COMMISSIONS'); ?></span>
						<span class="vbo-widget-finance-stat-cmd"><?php VikBookingIcons::e('ellipsis-v'); ?></span>
					</div>
					<div class="vbo-widget-finance-stat-amount">
						<span class="vbo-widget-finance-stat-amount-value">
							<span><?php echo $stats['ota_avg_cmms']; ?>%</span>
						</span>
					</div>
				</div>
			</div>

			<div class="vbo-widget-finance-data-block" data-typestat="cmm_savings">
				<div class="vbo-widget-finance-stat">
					<div class="vbo-widget-finance-stat-info">
						<span class="vbo-widget-finance-stat-name"><?php echo JText::translate('VBO_COMMISSION_SAVINGS'); ?></span>
						<span class="vbo-widget-finance-stat-cmd"><?php VikBookingIcons::e('ellipsis-v'); ?></span>
					</div>
					<div class="vbo-widget-finance-stat-amount">
						<span class="vbo-widget-finance-stat-amount-value vbo-tooltip vbo-tooltip-top" data-tooltiptext="<?php echo $currencysymb . ' ' . VikBooking::numberFormat($stats['cmm_savings']); ?>">
							<span class="vbo-currency"><?php echo $currencysymb; ?></span>
							<span class="vbo-price"><?php echo $finance->numberFormatShort($stats['cmm_savings']); ?></span>
						</span>
					</div>
				</div>
			</div>
			<?php
		}

		/**
		 * Return the information about the damage deposits collected/held.
		 * 
		 * @since 	1.18.0 (J) - 1.8.0 (WP)
		 */
		if ($stats['damage_deposits'] > 0) {
			?>
			<div class="vbo-widget-finance-data-block" data-typestat="damage_deposits">
				<div class="vbo-widget-finance-stat">
					<div class="vbo-widget-finance-stat-info">
						<span class="vbo-widget-finance-stat-name"><?php echo JText::translate('VBO_TOT_DAMAGE_DEPOSITS'); ?></span>
						<span class="vbo-widget-finance-stat-cmd"><?php VikBookingIcons::e('ellipsis-v'); ?></span>
					</div>
					<div class="vbo-widget-finance-stat-amount">
						<span class="vbo-widget-finance-stat-amount-value vbo-tooltip vbo-tooltip-top" data-tooltiptext="<?php echo $currencysymb . ' ' . VikBooking::numberFormat($stats['damage_deposits']); ?>">
							<span class="vbo-currency"><?php echo $currencysymb; ?></span>
							<span class="vbo-price"><?php echo $finance->numberFormatShort($stats['damage_deposits']); ?></span>
						</span>
					</div>
				</div>
			</div>
			<?php
		}
		?>

			<div class="vbo-widget-finance-data-block" data-typestat="tot_bookings">
				<div class="vbo-widget-finance-stat">
					<div class="vbo-widget-finance-stat-info">
						<span class="vbo-widget-finance-stat-name"><?php echo JText::translate('VBCUSTOMERTOTBOOKINGS'); ?></span>
						<span class="vbo-widget-finance-stat-cmd"><?php VikBookingIcons::e('ellipsis-v'); ?></span>
					</div>
					<div class="vbo-widget-finance-stat-amount">
						<span class="vbo-widget-finance-stat-amount-value">
							<span><?php echo $stats['tot_bookings']; ?></span>
						</span>
					</div>
				</div>
			</div>

			<div class="vbo-widget-finance-data-block" data-typestat="nights_booked">
				<div class="vbo-widget-finance-stat">
					<div class="vbo-widget-finance-stat-info">
						<span class="vbo-widget-finance-stat-name"><?php echo JText::translate('VBOGRAPHTOTNIGHTSLBL'); ?></span>
						<span class="vbo-widget-finance-stat-cmd"><?php VikBookingIcons::e('ellipsis-v'); ?></span>
					</div>
					<div class="vbo-widget-finance-stat-amount">
						<span class="vbo-widget-finance-stat-amount-value vbo-tooltip vbo-tooltip-top" data-tooltiptext="<?php echo htmlspecialchars(trim(str_replace('%', '', JText::translate('VBOREPORTREVENUEPOCC'))) . ' ' . $stats['occupancy'] . '%'); ?>">
							<span><?php echo $stats['nights_booked'] . '/' . $stats['tot_inventory']; ?></span>
						</span>
					</div>
				</div>
			</div>

			<div class="vbo-widget-finance-data-block" data-typestat="avg_los">
				<div class="vbo-widget-finance-stat">
					<div class="vbo-widget-finance-stat-info">
						<span class="vbo-widget-finance-stat-name"><?php echo JText::translate('VBTRKAVGLOS'); ?></span>
						<span class="vbo-widget-finance-stat-cmd"><?php VikBookingIcons::e('ellipsis-v'); ?></span>
					</div>
					<div class="vbo-widget-finance-stat-amount">
						<span class="vbo-widget-finance-stat-amount-value">
							<span><?php echo $stats['avg_los']; ?></span>
						</span>
					</div>
				</div>
			</div>

			<div class="vbo-widget-finance-data-block" data-typestat="abw">
				<div class="vbo-widget-finance-stat">
					<div class="vbo-widget-finance-stat-info">
						<span class="vbo-widget-finance-stat-name"><?php echo JText::translate('VBO_AVG_BOOK_WINDOW'); ?></span>
						<span class="vbo-widget-finance-stat-cmd"><?php VikBookingIcons::e('ellipsis-v'); ?></span>
					</div>
					<div class="vbo-widget-finance-stat-amount">
						<span class="vbo-widget-finance-stat-amount-value vbo-tooltip vbo-tooltip-top" data-tooltiptext="<?php echo htmlspecialchars(JText::translate('VBOCRONSMSREMPARAMBEFD')); ?>">
							<span><?php echo round($stats['abw'], 1); ?></span>
						</span>
					</div>
				</div>
			</div>

			<div class="vbo-widget-finance-data-block" data-typestat="rooms_booked">
				<div class="vbo-widget-finance-stat">
					<div class="vbo-widget-finance-stat-info">
						<span class="vbo-widget-finance-stat-name"><?php echo JText::translate('VBLIBTEN'); ?></span>
						<span class="vbo-widget-finance-stat-cmd"><?php VikBookingIcons::e('ellipsis-v'); ?></span>
					</div>
					<div class="vbo-widget-finance-stat-amount">
						<span class="vbo-widget-finance-stat-amount-value vbo-tooltip vbo-tooltip-top" data-tooltiptext="<?php echo htmlspecialchars(JText::translate('VBOGRAPHTOTUNITSLBL') . ' ' . $stats['room_units']); ?>">
							<span><?php echo $stats['rooms_booked']; ?></span>
						</span>
					</div>
				</div>
			</div>

			<div class="vbo-widget-finance-data-block" data-typestat="cancellations_amt">
				<div class="vbo-widget-finance-stat">
					<div class="vbo-widget-finance-stat-info">
						<span class="vbo-widget-finance-stat-name"><?php echo JText::translate('VBO_CANCELLATIONS'); ?></span>
						<span class="vbo-widget-finance-stat-cmd"><?php VikBookingIcons::e('ellipsis-v'); ?></span>
					</div>
					<div class="vbo-widget-finance-stat-amount">
						<span class="vbo-widget-finance-stat-amount-value vbo-tooltip vbo-tooltip-top" data-tooltiptext="<?php echo $currencysymb . ' ' . VikBooking::numberFormat($stats['cancellations_amt']) . ($stats['tot_cancellations'] ? ' (' . $stats['tot_cancellations'] . ')' : ''); ?>">
							<span class="vbo-currency"><?php echo $currencysymb; ?></span>
							<span class="vbo-price"><?php echo $finance->numberFormatShort($stats['cancellations_amt']); ?></span>
						</span>
					</div>
				</div>
			</div>

		</div>

		<div class="vbo-widget-finance-data-block-rankings">

			<div class="vbo-widget-finance-data-block-rank" data-typestat="country_ranks">
				<div class="vbo-widget-finance-stat">
					<div class="vbo-widget-finance-stat-info">
						<span class="vbo-widget-finance-stat-name"><?php echo JText::translate('VBOSTATSTOPCOUNTRIES'); ?></span>
					</div>
					<div class="vbo-widget-finance-stat-ranks">
					<?php
					foreach ($stats['country_ranks'] as $country_rank) {
						?>
						<div class="vbo-widget-finance-stat-rank">
							<div class="vbo-widget-finance-stat-rank-logo">
							<?php
							if (is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . 'countries' . DIRECTORY_SEPARATOR . $country_rank['code'] . '.png')) {
								?>
								<img src="<?php echo VBO_ADMIN_URI . 'resources/countries/' . $country_rank['code'] . '.png'; ?>" />
								<?php
							} else {
								VikBookingIcons::e('globe');
							}
							?>
							</div>
							<div class="vbo-widget-finance-stat-rank-score">
								<div class="vbo-widget-finance-stat-rank-name">
									<span><?php echo $country_rank['name']; ?></span>
								</div>
								<div class="vbo-widget-finance-stat-rank-amount">
									<span class="vbo-currency"><?php echo $currencysymb; ?></span>
									<span class="vbo-price vbo-tooltip vbo-tooltip-top" data-tooltiptext="<?php echo $currencysymb . ' ' . VikBooking::numberFormat($country_rank['revenue']); ?>"><?php echo $finance->numberFormatShort($country_rank['revenue']); ?></span>
								</div>
								<div class="vbo-widget-finance-stat-rank-pcent">
									<progress class="vbo-widget-finance-progress" value="<?php echo $country_rank['pcent']; ?>" max="100"><?php echo $country_rank['pcent']; ?>%</progress>
								</div>
							</div>
						</div>
						<?php
					}
					?>
					</div>
				</div>
			</div>

			<div class="vbo-widget-finance-data-block-rank" data-typestat="pos_revenue">
				<div class="vbo-widget-finance-stat">
					<div class="vbo-widget-finance-stat-info">
						<span class="vbo-widget-finance-stat-name"><?php echo JText::translate('VBOCHANNELS'); ?></span>
					</div>
					<div class="vbo-widget-finance-stat-ranks">
					<?php
					foreach ($stats['pos_revenue'] as $pos_revenue) {
						$say_pos_name = ucfirst($pos_revenue['name']);
						?>
						<div class="vbo-widget-finance-stat-rank">
							<div class="vbo-widget-finance-stat-rank-logo">
							<?php
							if (!empty($pos_revenue['logo'])) {
								?>
								<img src="<?php echo $pos_revenue['logo']; ?>" />
								<?php
								// adjust channel name
								$say_pos_name = strtolower($pos_revenue['name']) == 'airbnbapi' ? 'Airbnb' : $say_pos_name;
								$say_pos_name = strtolower($pos_revenue['name']) == 'googlehotel' ? 'Google Hotel' : $say_pos_name;
							} elseif (!strcasecmp($pos_revenue['name'], 'website')) {
								VikBookingIcons::e('hotel');
								// adjust channel name
								$say_pos_name = JText::translate('VBORDFROMSITE');
							} else {
								VikBookingIcons::e('globe');
							}
							?>
							</div>
							<div class="vbo-widget-finance-stat-rank-score">
								<div class="vbo-widget-finance-stat-rank-name">
									<span><?php echo $say_pos_name; ?></span>
								</div>
								<div class="vbo-widget-finance-stat-rank-amount">
									<span class="vbo-currency"><?php echo $currencysymb; ?></span>
									<span class="vbo-price vbo-tooltip vbo-tooltip-top" data-tooltiptext="<?php echo $currencysymb . ' ' . VikBooking::numberFormat($pos_revenue['revenue']); ?>"><?php echo $finance->numberFormatShort($pos_revenue['revenue']); ?></span>
								</div>
								<div class="vbo-widget-finance-stat-rank-pcent">
									<progress class="vbo-widget-finance-progress" value="<?php echo $pos_revenue['pcent']; ?>" max="100"><?php echo $pos_revenue['pcent']; ?>%</progress>
								</div>
							</div>
						</div>
						<?php
					}
					?>
					</div>
				</div>
			</div>

		</div>

		<input type="hidden" class="vbo-widget-finance-json-stats" value="" />
		<input type="hidden" class="vbo-widget-finance-stats-from" value="" />
		<input type="hidden" class="vbo-widget-finance-stats-to" value="" />

		<script type="text/javascript">
			jQuery(function() {

				let vbo_wfinance_compare_auto = jQuery('#<?php echo $wrapper; ?>').find('.vbo-finance-compare-auto').val();

				let vbo_wfinance_ctx_btns = [
					{
						icon: '<?php echo VikBookingIcons::i('balance-scale-left'); ?>',
						text: Joomla.JText._('<?php echo $js_step_comp_str; ?>'),
						separator: false,
						action: (root, config) => {
							vboWidgetFinanceCompare('<?php echo $wrapper; ?>', '<?php echo $step; ?>');
						},
					},
					{
						icon: '<?php echo VikBookingIcons::i('balance-scale-right'); ?>',
						text: Joomla.JText._('VBO_COMPARE_WITH_LAST_Y'),
						separator: false,
						action: (root, config) => {
							vboWidgetFinanceCompare('<?php echo $wrapper; ?>', 'year');
						},
					},
				];

				if (vbo_wfinance_compare_auto.length) {
					vbo_wfinance_ctx_btns[1].separator = true;
					vbo_wfinance_ctx_btns.push({
						icon: '<?php echo VikBookingIcons::i('ban'); ?>',
						text: Joomla.JText._((vbo_wfinance_compare_auto == 'year' ? 'VBO_COMPARE_WITH_LAST_Y' : '<?php echo $js_step_comp_str; ?>')),
						class: 'vbo-context-menu-entry-danger',
						separator: false,
						action: (root, config) => {
							// turn off the flag to automatically compare data
							jQuery('#<?php echo $wrapper; ?>').find('.vbo-finance-compare-auto').val('');
							// reload data
							vboWidgetFinanceLoad('<?php echo $wrapper; ?>');
						},
					});
				}

				jQuery('#<?php echo $wrapper; ?>').find('.vbo-widget-finance-stat-cmd').vboContextMenu({
					placement: 'bottom-right',
					buttons: vbo_wfinance_ctx_btns,
				});
			});
		</script>

		<?php

		// get the HTML buffer
		$html_content = ob_get_contents();
		ob_end_clean();

		// take care of the readable dates interval
		list($format_from_date, $format_to_date) = $this->parseReadableDateInterval($from_ts, $to_ts, $this->df, $step);

		// return an associative array of values
		return [
			'html' 		  => $html_content,
			'fromdate' 	  => $fromdate,
			'f_fromdate'  => $format_from_date,
			'todate' 	  => $todate,
			'f_todate' 	  => $format_to_date,
			'step' 		  => $step,
			'step_name'   => $step_name,
			'stats' 	  => $stats,
		];
	}

	/**
	 * Custom method for this widget only to load the comparison values.
	 * 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.
	 * 
	 * Returns the necessary HTML to display comparison values with past dates.
	 */
	public function loadComparisonStats()
	{
		$app = JFactory::getApplication();

		$wrapper = $app->input->getString('wrapper', '');

		$fromdate 	= $app->input->getString('fromdate', '');
		$todate   	= $app->input->getString('todate', '');
		$step 	  	= $app->input->getString('step', 'quarter');
		$prev_stats = $app->input->get('stats', [], 'array');
		$date_dir 	= -1;

		if (empty($fromdate) || empty($todate) || empty($prev_stats)) {
			// make the AJAX request fail nicely
			VBOHttpDocument::getInstance()->close(500, 'Missing data to calculate comparison values');
		}

		// convert dates to timestamps to support custom date formats
		$from_ts = VikBooking::getDateTimestamp($fromdate);
		$to_ts 	 = VikBooking::getDateTimestamp($todate, 23, 59, 59);

		// stats calculation type
		$calc_type = 'stay_dates';
		if (!strcasecmp($step, 'booking_dates')) {
			$calc_type = 'booking_dates';
		}

		// calculate backward dates for comparison
		list($from_ts, $to_ts, $step_name) = $this->calcDatesNavigation($from_ts, $to_ts, $step, $date_dir);

		// convert final dates to Y-m-d
		$fromdate = date('Y-m-d', $from_ts);
		$todate   = date('Y-m-d', $to_ts);

		// access the finance helper object
		$finance = VBOTaxonomyFinance::getInstance();

		// currency symbol
		$currencysymb = VikBooking::getCurrencySymb();

		// get the financial stats for the requested dates and use them for comparison
		try {
			$compare_stats = $finance->getStats($fromdate, $todate, [], $calc_type);
		} catch (Exception $e) {
			// make the AJAX request fail nicely
			VBOHttpDocument::getInstance()->close($e->getCode(), $e->getMessage());
		}

		// define the "compare vs" string
		switch ($step) {
			case 'year':
				$compare_vs_str = JText::translate('VBO_VS_LAST_Y');
				break;
			case 'quarter':
				$compare_vs_str = JText::translate('VBO_VS_PREV_Q');
				break;
			case 'month':
				$compare_vs_str = JText::translate('VBO_VS_PREV_M');
				break;
			case 'week':
				$compare_vs_str = JText::translate('VBO_VS_PREV_W');
				break;
			case 'booking_dates':
				$compare_vs_str = JText::translate('VBO_VS_PREV_D');
				break;
			default:
				$compare_vs_str = '';
				break;
		}

		// build comparison values
		$comparison = [];

		// stats that require percent values for comparison
		$pcent_stats = [
			'gross_revenue'   => [],
			'taxes'           => [],
			'cmms'            => [
				'reverse' => 1,
			],
			'revenue'         => [],
			'ibe_revenue'     => [],
			'ota_revenue'     => [],
			'ota_avg_cmms'    => [
				'reverse'  => 1,
				'no_pcent' => 1,
				'fixednum' => 1,
			],
			'cmm_savings'     => [],
			'damage_deposits' => [],
			'tot_bookings'    => [
				'fixednum' => 1,
			],
			'nights_booked'   => [
				'fixednum' => 1,
			],
			'avg_los'         => [
				'fixednum' => 1,
			],
			'abw'             => [
				'fixednum' => 1,
			],
			'rooms_booked'    => [
				'fixednum' => 1,
			],
			'cancellations_amt' => [
				'reverse' => 1,
			],
		];

		foreach ($pcent_stats as $stat_name => $stat_opt) {
			if (!isset($compare_stats[$stat_name])) {
				continue;
			}

			// make sure the previous value is set
			$prev_stats[$stat_name] = !isset($prev_stats[$stat_name]) ? 0 : $prev_stats[$stat_name];

			// difference
			$diff = isset($stat_opt['reverse']) ? ($compare_stats[$stat_name] - $prev_stats[$stat_name]) : ($prev_stats[$stat_name] - $compare_stats[$stat_name]);

			// calculate values
			$comparison[$stat_name] = [
				'amount'   => $compare_stats[$stat_name],
				'amount_f' => VikBooking::numberFormat($compare_stats[$stat_name]),
				'amount_s' => $finance->numberFormatShort($compare_stats[$stat_name]),
				'diff' 	   => $diff,
				'diff_f'   => VikBooking::numberFormat($diff),
				'diff_s'   => $finance->numberFormatShort($diff),
				'pcent'    => isset($stat_opt['no_pcent']) ? null : $finance->calcAbsPercent($prev_stats[$stat_name], $compare_stats[$stat_name]),
				'reverse'  => $stat_opt['reverse'] ?? false,
			];

			// inject property for fixed number with no currency
			if (isset($stat_opt['fixednum']) && $stat_opt['fixednum']) {
				$comparison[$stat_name]['fixednum'] = $stat_opt['fixednum'];
			}
		}

		// return an associative array of values
		return [
			'no_data'  => ($compare_stats['tot_bookings'] < 1 ? 1 : 0),
			'compare'  => $comparison,
			'vs_str'   => $compare_vs_str,
			'currency' => $currencysymb,
			'stats'    => $compare_stats,
		];
	}

	/**
	 * Custom method for this widget only to retrieve the daily
	 * welcome statistics about the bookings of the day before.
	 * 
	 * @return 	array 	the associative list of statistics.
	 */
	public function getWelcomeStats()
	{
		// get yesterday's date
		$yesterday = date('Y-m-d', strtotime('yesterday'));

		// access the finance helper object
		$finance = VBOTaxonomyFinance::getInstance();

		// get the financial stats for the requested dates
		try {
			$stats = $finance->getStats($yesterday, $yesterday, $rooms = [], $type = 'booking_dates');
		} catch (Exception $e) {
			// make the AJAX request fail nicely
			VBOHttpDocument::getInstance()->close($e->getCode(), $e->getMessage());
		}

		return $stats;
	}


	/**
	 * 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-finance-' . $wrapper_instance;

		// check permissions
		$vbo_auth_pricing = JFactory::getUser()->authorise('core.vbo.pricing', 'com_vikbooking');
		$vbo_auth_bookings = JFactory::getUser()->authorise('core.vbo.bookings', 'com_vikbooking');
		if (!$vbo_auth_pricing || !$vbo_auth_bookings) {
			// base permissions are not met
			return;
		}

		// we make use of the rates flow and occupancy ranking report helper classes
		$report = VikBooking::getReportInstance('rates_flow');
		if (!$report) {
			// display nothing
			return;
		}

		// get minimum and maximum nights updated for dates filters
		list($mindate, $maxdate) = $this->getMinDatesFinance();
		$mindate = empty($mindate) ? time() : $mindate;
		$maxdate = empty($maxdate) ? $mindate : $maxdate;

		// dates navigation preference
		$cookie = JFactory::getApplication()->input->cookie;
		$cookie_step = $cookie->get('vbo_widget_finance_step', 'quarter', 'string');

		// determine default period name and dates
		$df = $report->getDateFormat();
		$dtpicker_df = $report->getDateFormat('jui');
		$now_info = getdate();
		if ($cookie_step == 'week') {
			// next week
			$from_ts = mktime(0, 0, 0, $now_info['mon'], $now_info['mday'], $now_info['year']);
			$to_ts   = mktime(0, 0, 0, $now_info['mon'], ($now_info['mday'] + 7), $now_info['year']);
			$fromdate = date($df, $from_ts);
			$todate   = date($df, $to_ts);
			$period_name = JText::translate('VBOWEEK');
		} elseif ($cookie_step == 'month') {
			// current full month
			$from_ts  = mktime(0, 0, 0, $now_info['mon'], 1, $now_info['year']);
			$fromdate = date($df, $from_ts);
			$to_ts 	  = mktime(23, 59, 59, $now_info['mon'], date('t', $now_info[0]), $now_info['year']);
			$todate   = date($df, $to_ts);
			$period_name = JText::translate('VBPVIEWRESTRICTIONSTWO');
		} else {
			// default to next 3 (total) months from today's month
			$from_ts  = mktime(0, 0, 0, $now_info['mon'], 1, $now_info['year']);
			$fromdate = date($df, $from_ts);
			$end_info = getdate(mktime(0, 0, 0, ($now_info['mon'] + 2), 1, $now_info['year']));
			$to_ts    = mktime(23, 59, 59, $end_info['mon'], date('t', $end_info[0]), $end_info['year']);
			$todate   = date($df, $to_ts);
			$period_name = JText::translate('VBO_QUARTER');
		}

		// check multitask data
		$rooms_filtered = [];
		if ($data) {
			$inj_fromdate = $data->get('fromdate', $this->options()->get('fromdate', ''));
			$inj_todate   = $data->get('todate', $this->options()->get('todate', ''));
			$inj_type 	  = $data->get('type', $this->options()->get('type', ''));

			// check if some room IDs were filtered
			$rooms_filtered = (array) $data->get('room_ids', $this->options()->get('room_ids', []));

			if (preg_match("/^\d{4}-\d{2}-\d{2}$/", $inj_fromdate) && preg_match("/^\d{4}-\d{2}-\d{2}$/", $inj_todate)) {
				// valid dates injected in Y-m-d format
				$fromdate = $inj_fromdate;
				$todate   = $inj_todate;
				$to_info = getdate(strtotime($todate));
				$from_ts = strtotime($fromdate);
				$to_ts = mktime(23, 59, 59, $to_info['mon'], $to_info['mday'], $to_info['year']);
				if (!empty($inj_type)) {
					$cookie_step = $inj_type;
					if (!strcasecmp($inj_type, 'booking_dates')) {
						$period_name = JText::translate('VBOSTATSMODETS');
					}
				}
			}
		}

		// take care of the readable dates interval
		list($format_from_date, $format_to_date) = $this->parseReadableDateInterval($from_ts, $to_ts, $df, $cookie_step);
		$interval_name = $format_from_date . ' - ' . $format_to_date;
		if ($format_from_date == $format_to_date) {
			$interval_name = $format_from_date;
		}

		?>
		<div id="<?php echo $wrapper_id; ?>" class="vbo-admin-widget-wrapper" data-instance="<?php echo $wrapper_instance; ?>">
			<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 $period_name; ?></div>
									<div class="vbo-reportwidget-period-date"><?php echo $interval_name; ?></div>
								</div>
								<div class="vbo-reportwidget-command-chevron vbo-reportwidget-command-prev">
									<span class="vbo-widget-finance-dt-prev" onclick="vboWidgetFinanceDatesNav('<?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-finance-dt-next" onclick="vboWidgetFinanceDatesNav('<?php echo $wrapper_id; ?>', 1);"><?php VikBookingIcons::e('chevron-right'); ?></span>
								</div>
							</div>
							<div class="vbo-reportwidget-command-dots">
								<span class="vbo-widget-command-togglefilters vbo-widget-finance-togglefilters" onclick="vboWidgetFinanceToggleFilters('<?php echo $wrapper_id; ?>');"><?php VikBookingIcons::e('ellipsis-v'); ?></span>
							</div>
						</div>
						<div class="vbo-reportwidget-filters">
							<div class="vbo-reportwidget-filter">
								<span class="vbo-reportwidget-datepicker">
									<?php VikBookingIcons::e('calendar', 'vbo-widget-finance-caltrigger'); ?>
									<input type="text" class="vbo-finance-dtpicker-from" value="<?php echo $fromdate; ?>" />
								</span>
							</div>
							<div class="vbo-reportwidget-filter">
								<span class="vbo-reportwidget-datepicker">
									<?php VikBookingIcons::e('calendar', 'vbo-widget-finance-caltrigger'); ?>
									<input type="text" class="vbo-finance-dtpicker-to" value="<?php echo $todate; ?>" />
								</span>
							</div>
							<div class="vbo-reportwidget-filter">
								<select class="vbo-finance-period-nav" onchange="vboWidgetFinanceChangePeriod(this.value);">
									<option value="week"<?php echo $cookie_step == 'week' ? ' selected="selected"' : ''; ?>><?php echo JText::translate('VBOWEEK'); ?></option>
									<option value="month"<?php echo $cookie_step == 'month' ? ' selected="selected"' : ''; ?>><?php echo JText::translate('VBPVIEWRESTRICTIONSTWO'); ?></option>
									<option value="quarter"<?php echo $cookie_step == 'quarter' ? ' selected="selected"' : ''; ?>><?php echo JText::translate('VBO_QUARTER'); ?></option>
									<option value="booking_dates"<?php echo !strcasecmp($cookie_step, 'booking_dates') ? ' selected="selected"' : ''; ?>><?php echo JText::translate('VBPCHOOSEBUSYORDATE'); ?></option>
								</select>
							</div>
							<div class="vbo-reportwidget-filter vbo-widget-finance-rooms-filter-wrap">
								<select class="vbo-widget-finance-rooms-filter" multiple="multiple">
								<?php
								foreach ($rooms_filtered as $room_filtered) {
									if ($room_info = VikBooking::getRoomInfo($room_filtered, ['id', 'name'], true)) {
										?>
									<option value="<?php echo $room_info['id']; ?>" selected="selected"><?php echo $room_info['name']; ?></option>
										<?php
									}
								}
								?>
								</select>
							</div>
							<div class="vbo-reportwidget-filter vbo-reportwidget-filter-confirm">
								<input type="hidden" class="vbo-finance-compare-auto" value="" />
								<button type="button" class="btn vbo-config-btn" onclick="vboWidgetFinanceChangeDates('<?php echo $wrapper_id; ?>');"><?php echo JText::translate('VBADMINNOTESUPD'); ?></button>
							</div>
						</div>

					</div>
				</div>
			</div>
			<div class="vbo-widget-finance-wrap">
				<div class="vbo-widget-finance-inner">
					<div class="vbo-widget-finance-list">
						<div class="vbo-widget-finance-skeleton-blocks">
						<?php
						// display a few loading skeletons
						for ($i = 0; $i < 10; $i++) {
							?>
							<div class="vbo-widget-finance-skeleton-block">
								<div class="vbo-widget-finance-skeleton-top">
									<div class="vbo-skeleton-loading vbo-skeleton-loading-finance-top"></div>
								</div>
								<div class="vbo-widget-finance-skeleton-bottom">
									<div class="vbo-skeleton-loading vbo-skeleton-loading-finance-bottom"></div>
								</div>
							</div>
							<?php
						}
						?>
						</div>
					</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.
			 */
			?>
		<script type="text/javascript">

			/**
			 * Display the loading skeletons.
			 */
			function vboWidgetFinanceSkeletons(wrapper) {
				var widget_instance = jQuery('#' + wrapper);
				if (!widget_instance.length) {
					return false;
				}
				widget_instance.find('.vbo-widget-finance-list').html('');
				var skeleton = '';
				skeleton += '<div class="vbo-widget-finance-skeleton-blocks">' + "\n";
				for (var i = 0; i < 10; i++) {
					skeleton += '<div class="vbo-widget-finance-skeleton-block">';
					skeleton += '	<div class="vbo-widget-finance-skeleton-top">';
					skeleton += '		<div class="vbo-skeleton-loading vbo-skeleton-loading-finance-top"></div>';
					skeleton += '	</div>';
					skeleton += '	<div class="vbo-widget-finance-skeleton-bottom">';
					skeleton += '		<div class="vbo-skeleton-loading vbo-skeleton-loading-finance-bottom"></div>';
					skeleton += '	</div>';
					skeleton += '</div>' + "\n";
				}
				skeleton += '</div>' + "\n";
				// append skeletons
				jQuery(skeleton).appendTo(widget_instance.find('.vbo-widget-finance-list'));
			}

			/**
			 * Perform the request to load the financial stats.
			 */
			function vboWidgetFinanceLoad(wrapper, dates_direction) {
				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;
				}

				// get vars for making the request
				var from_date 		= widget_instance.find('.vbo-finance-dtpicker-from').val();
				var to_date 		= widget_instance.find('.vbo-finance-dtpicker-to').val();
				var dates_step 		= widget_instance.find('.vbo-finance-period-nav').val();
				var room_ids        = widget_instance.find('select.vbo-widget-finance-rooms-filter').val();
				var auto_compare 	= widget_instance.find('.vbo-finance-compare-auto').val();

				// the widget method to call
				var call_method = 'loadFinancialStats';

				// make a request to load the financial stats
				VBOCore.doAjax(
					"<?php echo $this->getExecWidgetAjaxUri(); ?>",
					{
						widget_id: "<?php echo $this->getIdentifier(); ?>",
						call: call_method,
						return: 1,
						fromdate: from_date,
						todate: to_date,
						step: dates_step,
						room_ids: room_ids,
						date_dir: dates_direction,
						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 new stats
							widget_instance.find('.vbo-widget-finance-list').html(obj_res[call_method]['html']);

							// populate JSON data for comparison
							widget_instance.find('input.vbo-widget-finance-json-stats').val(JSON.stringify(obj_res[call_method]['stats']));
							widget_instance.find('input.vbo-widget-finance-stats-from').val(obj_res[call_method]['fromdate']);
							widget_instance.find('input.vbo-widget-finance-stats-to').val(obj_res[call_method]['todate']);

							if (dates_direction > 0 || dates_direction < 0) {
								// set new dates calculated - note: if maxDate or minDate is exceeded, the datepicker calendars won't update!
								try {
									// construct a solid date object without using the raw Y-m-d string as only argument
									var dfrom_parts = obj_res[call_method]['fromdate'].split('-');
									var dfrom_obj 	= new Date(parseInt(dfrom_parts[0]), (parseInt(dfrom_parts[1]) - 1), parseInt(dfrom_parts[2]), 0, 0, 0);
									widget_instance.find('.vbo-finance-dtpicker-from').datepicker('setDate', dfrom_obj);

									// restore the original minimum and maximum dates for the "to date" calendar to avoid issues setting a date
									widget_instance.find('.vbo-finance-dtpicker-to').datepicker('option', {
										minDate: "<?php echo date($df, $mindate); ?>",
										maxDate: "<?php echo date($df, $maxdate); ?>",
									});

									// do the same for the to date by using a precise date object instance
									var dto_parts 	= obj_res[call_method]['todate'].split('-');
									var dto_obj 	= new Date(parseInt(dto_parts[0]), (parseInt(dto_parts[1]) - 1), parseInt(dto_parts[2]), 0, 0, 0);
									widget_instance.find('.vbo-finance-dtpicker-to').datepicker('setDate', dto_obj);
								} catch(e) {
									// just log the error
									console.error(e);
								}
							}

							// update step name
							if (obj_res[call_method]['step_name'] && obj_res[call_method]['step_name'].length) {
								widget_instance.find('.vbo-reportwidget-period-name').text(obj_res[call_method]['step_name']);
							}

							// update readable dates interval
							var readable_period = obj_res[call_method]['f_fromdate'] + ' - ' + obj_res[call_method]['f_todate'];
							if (obj_res[call_method]['f_fromdate'] == obj_res[call_method]['f_todate']) {
								// a full month does not need an interval
								readable_period = obj_res[call_method]['f_fromdate'];
							}
							widget_instance.find('.vbo-reportwidget-period-date').text(readable_period);

							if (auto_compare.length) {
								// trigger the stats comparison
								setTimeout(() => {
									vboWidgetFinanceCompare(wrapper, (auto_compare == 'year' ? 'year' : dates_step));
								}, 400);
							}
						} catch(err) {
							console.error('could not parse JSON response', err, response);
						}
					},
					(error) => {
						// remove the skeleton loading
						widget_instance.find('.vbo-widget-finance-list').find('.vbo-widget-finance-skeleton-blocks').remove();
						console.error(error);
						alert(error.responseText);
					}
				);
			}

			/**
			 * Load stats comparison values.
			 */
			function vboWidgetFinanceCompare(wrapper, period) {
				var widget_instance = jQuery('#' + wrapper);
				if (!widget_instance.length) {
					return false;
				}

				// append loading elements
				widget_instance.find('.vbo-widget-finance-data-blocks').find('.vbo-widget-finance-stat').each(function(k, v) {
					if (jQuery(this).find('.vbo-widget-finance-stat-compare').length) {
						jQuery(this).find('.vbo-widget-finance-stat-compare').remove();
					}
					var comparison_el = jQuery('<div></div>').addClass('vbo-widget-finance-stat-compare');
					var loading_el = jQuery('<div></div>').addClass('vbo-widget-finance-compare-amount');
					loading_el.append('<i class="<?php echo VikBookingIcons::i('circle-notch', 'fa-spin fa-fw'); ?>"></i>');
					comparison_el.append(loading_el);
					jQuery(this).append(comparison_el);
				});

				// get vars for making the request
				var prev_stats = JSON.parse(widget_instance.find('.vbo-widget-finance-json-stats').val());
				var from_date  = widget_instance.find('.vbo-widget-finance-stats-from').val();
				var to_date    = widget_instance.find('.vbo-widget-finance-stats-to').val();

				// the widget method to call
				var call_method = 'loadComparisonStats';

				// make a request to load the comparison values
				VBOCore.doAjax(
					"<?php echo $this->getExecWidgetAjaxUri(); ?>",
					{
						widget_id: "<?php echo $this->getIdentifier(); ?>",
						call: call_method,
						return: 1,
						fromdate: from_date,
						todate: to_date,
						step: period,
						stats: prev_stats,
						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;
							}

							// turn on the flag to automatically compare data
							widget_instance.find('.vbo-finance-compare-auto').val(period);

							if (obj_res[call_method]['no_data']) {
								// no data available for comparison
								widget_instance.find('.vbo-widget-finance-data-blocks').find('.vbo-widget-finance-stat').each(function(k, v) {
									if (jQuery(this).find('.vbo-widget-finance-stat-compare').length) {
										jQuery(this).find('.vbo-widget-finance-stat-compare').remove();
									}
									let comparison_el = jQuery('<div></div>').addClass('vbo-widget-finance-stat-compare');
									let amount_el = jQuery('<div></div>').addClass('vbo-widget-finance-compare-amount');
									comparison_el.append(amount_el);
									let pcent_el = jQuery('<div></div>').addClass('vbo-widget-finance-compare-pcent');
									pcent_el.append('<span></span>').addClass('vbo-widget-finance-compare-txt').text(Joomla.JText._('VBNOTRACKINGS'));
									comparison_el.append(pcent_el);
									jQuery(this).append(comparison_el);
								});

								// do not proceed
								return;
							}

							// parse all comparison values
							for (let stat_name in obj_res[call_method]['compare']) {
								if (!obj_res[call_method]['compare'].hasOwnProperty(stat_name)) {
									continue;
								}

								let stat_elem = widget_instance.find('.vbo-widget-finance-data-block[data-typestat="' + stat_name + '"]');
								if (!stat_elem || !stat_elem.length) {
									continue;
								}

								// clean up loading element
								if (stat_elem.find('.vbo-widget-finance-stat-compare').length) {
									stat_elem.find('.vbo-widget-finance-stat-compare').remove();
								}

								// check if currency is needed
								let use_currency = (!obj_res[call_method]['compare'][stat_name].hasOwnProperty('fixednum') || !obj_res[call_method]['compare'][stat_name]['fixednum']);

								// the new comparison element
								let comparison_el = jQuery('<div></div>').addClass('vbo-widget-finance-stat-compare');

								// amount block
								let amount_block = jQuery('<div></div>').addClass('vbo-widget-finance-compare-amount');
								let amount_value = jQuery('<span></span>').addClass('vbo-widget-finance-compare-amount-value');
								if (use_currency) {
									amount_value.addClass('vbo-tooltip vbo-tooltip-top').attr('data-tooltiptext', obj_res[call_method]['currency'] + ' ' + obj_res[call_method]['compare'][stat_name]['amount_f']);
									amount_value.append('<span class="vbo-currency">' + obj_res[call_method]['currency'] + '</span>');
								}
								if (obj_res[call_method]['compare'][stat_name]['pcent'] != null) {
									amount_value.append('<span class="vbo-price">' + obj_res[call_method]['compare'][stat_name]['amount_s'] + '</span>');
								} else {
									amount_value.append('<span class="vbo-price">' + obj_res[call_method]['compare'][stat_name]['amount'] + '%</span>');
								}
								amount_block.append(amount_value);

								// percent block
								let pcent_block = jQuery('<div></div>').addClass('vbo-widget-finance-compare-pcent');
								let pcent_icon  = '';
								if (obj_res[call_method]['compare'][stat_name]['diff'] == 0) {
									pcent_block.addClass('vbo-widget-finance-compare-pcent-equal');
									pcent_icon = '<?php VikBookingIcons::e('equals') ?>';
								} else if (obj_res[call_method]['compare'][stat_name]['diff'] > 0) {
									pcent_block.addClass('vbo-widget-finance-compare-pcent-up');
									pcent_icon = obj_res[call_method]['compare'][stat_name]['reverse'] ? '<?php VikBookingIcons::e('arrow-down') ?>' : '<?php VikBookingIcons::e('arrow-up') ?>';
								} else if (obj_res[call_method]['compare'][stat_name]['diff'] < 0) {
									pcent_block.addClass('vbo-widget-finance-compare-pcent-down');
									pcent_icon = obj_res[call_method]['compare'][stat_name]['reverse'] ? '<?php VikBookingIcons::e('arrow-up') ?>' : '<?php VikBookingIcons::e('arrow-down') ?>';
								}
								if (obj_res[call_method]['compare'][stat_name]['pcent'] != null) {
									pcent_block.append('<span class="vbo-widget-finance-compare-val">' + pcent_icon + ' ' + obj_res[call_method]['compare'][stat_name]['pcent'] + '%</span>');
								}
								pcent_block.append('<span class="vbo-widget-finance-compare-txt">' + obj_res[call_method]['vs_str'] + '</span>');
								
								// append amount block
								comparison_el.append(amount_block);
								// append percent block
								comparison_el.append(pcent_block);

								// append comparison element
								stat_elem.find('.vbo-widget-finance-stat').append(comparison_el);
							}
						} catch(err) {
							console.error('could not parse JSON response', err, response);
						}
					},
					(error) => {
						// remove loading icons
						widget_instance.find('.vbo-widget-finance-data-blocks').find('.vbo-widget-finance-stat-compare').remove();
						console.error(error);
						alert(error.responseText);
					}
				);

			}

			/**
			 * Navigate between the date steps and load the stats.
			 */
			function vboWidgetFinanceDatesNav(wrapper, direction) {
				var widget_instance = jQuery('#' + wrapper);
				if (!widget_instance.length) {
					return false;
				}

				// show loading skeletons
				vboWidgetFinanceSkeletons(wrapper);

				// launch dates navigation and load records
				vboWidgetFinanceLoad(wrapper, direction);
			}

			/**
			 * Load stats for the selected dates.
			 */
			function vboWidgetFinanceChangeDates(wrapper) {
				var widget_instance = jQuery('#' + wrapper);
				if (!widget_instance.length) {
					return false;
				}

				// hide filters when making a new request
				widget_instance.find('.vbo-reportwidget-filters').hide();

				// show loading skeletons
				vboWidgetFinanceSkeletons(wrapper);

				// load data
				vboWidgetFinanceLoad(wrapper);
			}

			/**
			 * Toggle dates and step filters.
			 */
			function vboWidgetFinanceToggleFilters(wrapper) {
				var widget_instance = jQuery('#' + wrapper);
				if (!widget_instance.length) {
					return false;
				}

				widget_instance.find('.vbo-reportwidget-filters').toggle();
			}

			/**
			 * Datepicker dates selection.
			 */
			function vboWidgetFinanceCheckDates(selectedDate, inst) {
				if (selectedDate === null || inst === null) {
					return;
				}
				var cur_from_date = jQuery(this).val();
				if (jQuery(this).hasClass("vbo-finance-dtpicker-from") && cur_from_date.length) {
					var nowstart = jQuery(this).datepicker("getDate");
					var nowstartdate = new Date(nowstart.getTime());
					jQuery(".vbo-finance-dtpicker-to").datepicker("option", {minDate: nowstartdate});
				}
			}

			/**
			 * Navigation period cookie onchange.
			 */
			function vboWidgetFinanceChangePeriod(period) {
				// always exclude "booking_dates"
				if (period && period === 'booking_dates') {
					return;
				}
				// update cookie for the step selected
				var nd = new Date();
				nd.setTime(nd.getTime() + (365*24*60*60*1000));
				document.cookie = "vbo_widget_finance_step=" + period + "; expires=" + nd.toUTCString() + "; path=/; SameSite=Lax";
			}

		</script>
			<?php
		}
		?>

		<script type="text/javascript">

			jQuery(function() {

				// render datepicker calendars for dates navigation
				jQuery('#<?php echo $wrapper_id; ?>').find('.vbo-finance-dtpicker-from, .vbo-finance-dtpicker-to').datepicker({
					minDate: "<?php echo date($df, $mindate); ?>",
					maxDate: "<?php echo date($df, $maxdate); ?>",
					yearRange: "<?php echo date('Y', $mindate); ?>:<?php echo date('Y', $maxdate); ?>",
					changeMonth: true,
					changeYear: true,
					dateFormat: "<?php echo $dtpicker_df; ?>",
					onSelect: vboWidgetFinanceCheckDates
				});

				// triggering for datepicker calendar icons
				jQuery('i.vbo-widget-finance-caltrigger').click(function() {
					var jdp = jQuery(this).parent().find('input.hasDatepicker');
					if (jdp.length) {
						jdp.focus();
					}
				});

				// register room filters search via AJAX
				jQuery('#<?php echo $wrapper_id; ?>').find('select.vbo-widget-finance-rooms-filter').select2({
					placeholder: Joomla.JText._('VBROOMFILTER'),
					language: {
						errorLoading: () => {
							return Joomla.JText._('VBO_ERR_LOAD_RESULTS');
						},
						noResults: () => {
							return Joomla.JText._('VBNOROOMSFOUND');
						},
						searching: () => {
							return Joomla.JText._('VBO_SEARCHING');
						},
					},
					ajax: {
						delay: 350,
						url: "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=bookings.rooms_search'); ?>",
						dataType: 'json',
					},
					templateResult: (element) => {
						if (!element.img) {
							return element.text;
						}
						return jQuery('<span class="vbo-sel2-element-img"><img src="' + element.img + '" /> <span>' + element.text + '</span></span>');
					},
				});

				// when document is ready, load stats for this widget's instance
				vboWidgetFinanceLoad('<?php echo $wrapper_id; ?>');

			});

		</script>

		<?php
	}

	/**
	 * Returns an array with the minimum and maximum dates booked.
	 * 
	 * @return 	array 	to be used with list() to get the min/max stay date timestamps.
	 */
	protected function getMinDatesFinance()
	{
		$dbo = JFactory::getDbo();

		$mindate = null;
		$maxdate = null;

		$q = "SELECT MIN(`checkin`) AS `mindate`, MAX(`checkout`) AS `maxdate` FROM `#__vikbooking_orders` 
			WHERE `status` = 'confirmed' AND `closure` = 0";
		$dbo->setQuery($q);
		$data = $dbo->loadAssoc();
		if ($data) {
			if (!empty($data['mindate']) && !empty($data['maxdate'])) {
				$mindate = $data['mindate'];
				$maxdate = $data['maxdate'];
			}
		}

		return [$mindate, $maxdate];
	}

	/**
	 * Calculates the new dates interval for navigation according to step.
	 * Internal method for this widget only.
	 * 
	 * @param 	int 	$from_ts 	current from date timestamp.
	 * @param 	int 	$to_ts 		current end date timestamp.
	 * @param 	string 	$step		the navigation step to take (week, month, quarter or booking_dates).
	 * @param 	int 	$date_dir 	less than 0 backward nav, more than 0 forward.
	 * 
	 * @return 	array 				the new from and end date timestamps, plus the step name.
	 */
	protected function calcDatesNavigation($from_ts, $to_ts, $step, $date_dir)
	{
		if (empty($from_ts) || (!($date_dir > 0) && !($date_dir < 0))) {
			return [$from_ts, $to_ts, ''];
		}

		// start/end date information
		$from_info = getdate($from_ts);
		$to_info   = getdate($to_ts);

		// next or prev day (booking_dates)
		if (!strcasecmp($step, 'booking_dates')) {
			if ($date_dir > 0) {
				// forward dates navigation
				$from_ts = mktime(0, 0, 0, $to_info['mon'], ($to_info['mday'] + 1), $to_info['year']);
				$to_ts 	 = mktime(23, 59, 59, $to_info['mon'], ($to_info['mday'] + 1), $to_info['year']);
			} elseif ($date_dir < 0) {
				// backward dates navigation
				$from_ts = mktime(0, 0, 0, $from_info['mon'], ($from_info['mday'] - 1), $from_info['year']);
				$to_ts 	 = mktime(23, 59, 59, $from_info['mon'], ($from_info['mday'] - 1), $from_info['year']);
			}

			return [$from_ts, $to_ts, JText::translate('VBPCHOOSEBUSYORDATE')];
		}

		// next or prev week
		if ($step == 'week') {
			if ($date_dir > 0) {
				// forward dates navigation
				$from_ts = $to_ts;
				$to_ts   = strtotime("+1 week", $to_ts);
			} elseif ($date_dir < 0) {
				// backward dates navigation
				$to_ts   = $from_ts;
				$from_ts = strtotime("-1 week", $from_ts);
			}

			return [$from_ts, $to_ts, JText::translate('VBOWEEK')];
		}

		// next or prev month
		if ($step == 'month') {
			if ($date_dir > 0) {
				// forward dates navigation
				$from_ts = mktime(0, 0, 0, ($from_info['mon'] + 1), 1, $from_info['year']);
				$to_ts 	 = mktime(0, 0, 0, ($from_info['mon'] + 1), date('t', $from_ts), $from_info['year']);
			} elseif ($date_dir < 0) {
				// backward dates navigation
				$from_ts = mktime(0, 0, 0, ($from_info['mon'] - 1), 1, $from_info['year']);
				$to_ts 	 = mktime(0, 0, 0, ($from_info['mon'] - 1), date('t', $from_ts), $from_info['year']);
			}

			return [$from_ts, $to_ts, JText::translate('VBPVIEWRESTRICTIONSTWO')];
		}

		// next or prev year (keep the same exact dates)
		if ($step == 'year') {
			if ($date_dir > 0) {
				// forward dates navigation
				$from_ts = mktime(0, 0, 0, $from_info['mon'], $from_info['mday'], ($from_info['year'] + 1));
				$to_ts 	 = mktime(0, 0, 0, $to_info['mon'], $to_info['mday'], ($to_info['year'] + 1));
			} elseif ($date_dir < 0) {
				// backward dates navigation
				$from_ts = mktime(0, 0, 0, $from_info['mon'], $from_info['mday'], ($from_info['year'] - 1));
				$to_ts 	 = mktime(0, 0, 0, $to_info['mon'], $to_info['mday'], ($to_info['year'] - 1));
			}

			return [$from_ts, $to_ts, JText::translate('VBCONFIGSEARCHPMAXDATEYEARS')];
		}

		// next or prev quarter by default
		if ($date_dir > 0) {
			// forward dates navigation
			$from_ts = mktime(0, 0, 0, ($from_info['mon'] + 3), 1, $from_info['year']);
			$end_ts  = mktime(0, 0, 0, ($from_info['mon'] + 5), 1, $from_info['year']);
			$to_ts 	 = mktime(0, 0, 0, ($from_info['mon'] + 5), date('t', $end_ts), $from_info['year']);
		} elseif ($date_dir < 0) {
			// backward dates navigation
			$from_ts = mktime(0, 0, 0, ($from_info['mon'] - 3), 1, $from_info['year']);
			$end_ts  = mktime(0, 0, 0, ($from_info['mon'] - 1), 1, $from_info['year']);
			$to_ts 	 = mktime(0, 0, 0, ($from_info['mon'] - 1), date('t', $end_ts), $from_info['year']);
		}

		return [$from_ts, $to_ts, JText::translate('VBO_QUARTER')];
	}

	/**
	 * Parse an interval of date timestamps into a readable interval of dates according to step.
	 * Internal method for this widget only.
	 * 
	 * @param 	int 	$from_ts 	current from date timestamp.
	 * @param 	int 	$to_ts 		current end date timestamp.
	 * @param 	string 	$vbo_df 	the date format in VBO.
	 * @param 	string 	$step		the navigation step to take (week, month or quarter).
	 * 
	 * @return 	array 				the formatted from and to date interval strings.
	 */
	protected function parseReadableDateInterval($from_ts, $to_ts, $vbo_df, $step)
	{
		$from_info = getdate($from_ts);
		$to_info   = getdate($to_ts);
		$format_from_date = date($vbo_df, $from_ts);
		$format_to_date   = date($vbo_df, $to_ts);

		if ($step == 'week') {
			// include day of week
			$format_from_date = VikBooking::sayWeekDay($from_info['wday']) . ', ' . $format_from_date;
			$format_to_date   = VikBooking::sayWeekDay($to_info['wday']) . ', ' . $format_to_date;
		} elseif ($step == 'month') {
			// check if it's a full month to say its name
			if ($from_info['mon'] == $to_info['mon'] && $from_info['year'] == $to_info['year']) {
				if ($from_info['mday'] == 1 && $to_info['mday'] == date('t', $to_ts)) {
					$format_from_date = VikBooking::sayMonth($from_info['mon']) . ' ' . $from_info['year'];
					$format_to_date   = VikBooking::sayMonth($to_info['mon']) . ' ' . $to_info['year'];
				}
			}
		} elseif ($step == 'quarter') {
			// check if it's a full quarter
			if ($from_info['mday'] == 1 && $to_info['mday'] == date('t', $to_ts)) {
				$format_from_date = VikBooking::sayMonth($from_info['mon']) . ' ' . $from_info['year'];
				$format_to_date   = VikBooking::sayMonth($to_info['mon']) . ' ' . $to_info['year'];
			}
		}

		return [$format_from_date, $format_to_date];
	}

	/**
	 * Sets up the necessary script to load the welcome message for the user.
	 * On WordPress this method may run outside Vik Booking, and so Javascript,
	 * may not be able to access language definitions from the DOM.
	 * 
	 * @return 	bool 	true if welcome was set up for the user, or false.
	 */
	protected function setupWelcomeStats()
	{
		// avoid welcome stats on certain pages
		if (VBOPlatformDetection::isWordPress()) {
			global $pagenow;
			$skip_pages = ['update.php', 'plugins.php', 'plugin-install.php'];
			if (isset($pagenow) && in_array($pagenow, $skip_pages)) {
				return false;
			}
		}

		$admin_user 	 = JFactory::getUser();
		$admin_user_name = $admin_user->name;

		if (!$admin_user_name || !$admin_user->authorise('core.vbo.bookings', 'com_vikbooking')) {
			// user not authorized
			return false;
		}

		// get yesterday's date
		$yesterday_info = getdate(strtotime('yesterday'));
		$yesterday_read = implode(' ', [VikBooking::sayWeekDay($yesterday_info['wday'], true), $yesterday_info['mday'], VikBooking::sayMonth($yesterday_info['mon'], true)]);
		$yesterday_read = htmlspecialchars($yesterday_read);
		$yesterday = date('Y-m-d', $yesterday_info[0]);

		// the payload for getting the welcome stats for yesterday
		$payload = [
			'fromdate' => $yesterday,
			'todate'   => $yesterday,
			'type' 	   => 'booking_dates',
		];

		// check if we are inside Vik Booking
		$in_vbo  = (int)(JFactory::getApplication()->input->get('option', '') === 'com_vikbooking');
		$vbo_uri = VBOPlatformDetection::isWordPress() ? 'admin.php?page=vikbooking' : 'index.php?option=com_vikbooking';
		if (!$in_vbo && VBOPlatformDetection::isWordPress()) {
			// append query string value to render the admin widget
			$q_cmds = [
				'load_widget' 	 => $this->widgetId,
				'multitask_data' => $payload,
			];
			$vbo_uri .= '&' . http_build_query($q_cmds);
		}

		// build data (the JS code may not be able to access script language definitions if running outside Vik Booking)
		$aj_endpoint  = $this->getExecWidgetAjaxUri();
		$widget_id 	  = $this->getIdentifier();
		$website_logo = $this->getIconUrl();
		$website_logo = $website_logo ? $website_logo : '';
		$greetings 	  = VikBooking::strTrimLiteral(htmlspecialchars(JText::sprintf('VBO_WELCOME_ADMIN_USER', $admin_user_name)));
		$greetings_ms = VikBooking::strTrimLiteral(htmlspecialchars(JText::translate('VBO_BOOKINGS_YESTERDAY')));
		$modal_title  = VikBooking::strTrimLiteral(htmlspecialchars(JText::translate('VBOYESTERDAY') . ', ' . $yesterday_read));
		$payload_json = json_encode($payload);
		$sugg_notifs_btn = '<button class="vbo-suggest-notifications-btn" type="button"><i class="' . VikBookingIcons::i('bell', 'can-shake') . '"></i></button>';

		JFactory::getDocument()->addScriptDeclaration(
<<<JS
(function($) {
	'use strict';

	$(function() {
		try {
			if (!VBOCore.storageSupported() || VBOCore.storageGetItem('vbo_finance_last_dt') == '$yesterday') {
				return false;
			}
		} catch(e) {
			return false;
		}

		let call_method = 'getWelcomeStats';
		VBOCore.doAjax(
			"$aj_endpoint",
			{
				widget_id: "$widget_id",
				call: call_method,
				return: 1,
				tmpl: "component"
			},
			(response) => {
				try {
					var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
					if (!obj_res.hasOwnProperty(call_method)) {
						throw new Error('Unexpected JSON response');
					}

					VBOCore.storageSetItem('vbo_finance_last_dt', '$yesterday');

					if (!obj_res[call_method].hasOwnProperty('tot_bookings') || !obj_res[call_method]['tot_bookings']) {
						return;
					}

					let vbo_notif_click_handler = 'VBOCore.handleGoto';
					if ($in_vbo) {
						vbo_notif_click_handler = () => {
							try {
								let yesterday_modal = VBOCore.displayModal({
									suffix: 	   'finance-welcome',
									extra_class:   'vbo-modal-rounded vbo-modal-tall vbo-modal-nofooter',
									title: 		   '<span class="vbo-suggest-notifications-wrap"><span>$modal_title</span>' + (VBOCore.notificationsEnabled() === false ? '$sugg_notifs_btn' : '') + '</span>',
									dismiss_event: 'vbo-dismiss-modal-finance-welcome',
									loading_event: 'vbo-loading-modal-finance-welcome',
								});

								VBOCore.suggestNotifications('.vbo-suggest-notifications-btn');
								VBOCore.emitEvent('vbo-loading-modal-finance-welcome');

								VBOCore.renderAdminWidget('$this->widgetId', $payload_json).then((content) => {
									VBOCore.emitEvent('vbo-loading-modal-finance-welcome');
									yesterday_modal.append(content);
								}).catch((error) => {
									VBOCore.emitEvent('vbo-dismiss-modal-finance-welcome');
									alert(error);
								});
							} catch(e) {
								document.location.href = '$vbo_uri';
							}
						};
					}

					setTimeout(() => {
						VBOCore.dispatchNotification({
							title: 	 '$greetings',
							message: ('$greetings_ms').replace('%d', obj_res[call_method]['tot_bookings']),
							icon: 	 '$website_logo',
							onclick: vbo_notif_click_handler,
							gotourl: '$vbo_uri',
						});
					}, 500);
				} catch(e) {
					console.error(e);
				}
			},
			(error) => {
				console.error(error.responseText);
			}
		);
	});
})(jQuery);
JS
		);

		return true;
	}

	/**
	 * Returns the URL to the default site logo.
	 * 
	 * @return 	string|null
	 */
	protected function getIconUrl()
	{
		$config = VBOFactory::getConfig();

		// back-end custom logo
		$use_logo = $config->get('backlogo');
		if (empty($use_logo) || !strcasecmp($use_logo, 'vikbooking.png')) {
			// fallback to company (site) logo
			$use_logo = $config->get('sitelogo');
		}

		if (!empty($use_logo) && strcasecmp($use_logo, 'vikbooking.png')) {
			// uploaded logo found
			$use_logo = VBO_ADMIN_URI . 'resources/' . $use_logo;
		} else {
			$use_logo = null;
		}

		return $use_logo;
	}
}