PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.16
VikBooking Hotel Booking Engine & PMS v1.8.16
1.8.16 1.8.15 1.8.14 1.8.13 1.8.12 1.8.11 1.8.10 1.8.9 1.8.6 1.8.7 1.8.8 trunk 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 All 37 releases
← All changes | admin/helpers/widgets/finance.php +1768 -0 1.6.3 → 1.8.16 View file →
@@ -1,0 +1,1768 @@
1 +<?php
2 +/**
3 + * @package VikBooking
4 + * @subpackage com_vikbooking
5 + * @author Alessio Gaggii - e4j - Extensionsforjoomla.com
6 + * @copyright Copyright (C) 2018 e4j - Extensionsforjoomla.com. All rights reserved.
7 + * @license GNU General Public License version 2 or later; see LICENSE
8 + * @link https://vikwp.com
9 + */
10 +
11 +defined('ABSPATH') or die('No script kiddies please!');
12 +
13 +/**
14 + * Class handler for admin widget "finance".
15 + *
16 + * @since 1.16.0 (J) - 1.6.0 (WP)
17 + */
18 +class VikBookingAdminWidgetFinance extends VikBookingAdminWidget
19 +{
20 + /**
21 + * The instance counter of this widget. Since we do not load individual parameters
22 + * for each widget's instance, we use a static counter to determine its settings.
23 + *
24 + * @var int
25 + */
26 + protected static $instance_counter = -1;
27 +
28 + /**
29 + * Class constructor will define the widget name and identifier.
30 + */
31 + public function __construct()
32 + {
33 + // call parent constructor
34 + parent::__construct();
35 +
36 + $this->widgetName = JText::translate('VBO_W_FINANCE_TITLE');
37 + $this->widgetDescr = JText::translate('VBO_W_FINANCE_DESCR');
38 + $this->widgetId = basename(__FILE__, '.php');
39 +
40 + // define widget and icon and style name
41 + $this->widgetIcon = '<i class="' . VikBookingIcons::i('piggy-bank') . '"></i>';
42 + $this->widgetStyleName = 'red';
43 + }
44 +
45 + /**
46 + * Preload the necessary CSS/JS assets.
47 + *
48 + * @return void
49 + */
50 + public function preload()
51 + {
52 + // load assets for datepicker
53 + $this->vbo_app->loadDatePicker();
54 +
55 + // load assets for contextual menu
56 + $this->vbo_app->loadContextMenuAssets();
57 +
58 + // check for daily welcome message with stats
59 + $this->setupWelcomeStats();
60 +
61 + // lang vars for JS
62 + JText::script('VBO_COMPARE_WITH_LAST_Y');
63 + JText::script('VBO_COMPARE_WITH_PREV_Q');
64 + JText::script('VBO_COMPARE_WITH_PREV_M');
65 + JText::script('VBO_COMPARE_WITH_PREV_W');
66 + JText::script('VBO_COMPARE_WITH_PREV_D');
67 + JText::script('VBNOTRACKINGS');
68 + JText::script('VBO_ERR_LOAD_RESULTS');
69 + JText::script('VBNOROOMSFOUND');
70 + JText::script('VBO_SEARCHING');
71 + JText::script('VBROOMFILTER');
72 + }
73 +
74 + /**
75 + * Custom method for this widget only to load the financial statistics.
76 + * The method is called by the admin controller through an AJAX request.
77 + * The visibility should be public, it should not exit the process, and
78 + * any content sent to output will be returned to the AJAX response.
79 + * In this case we return an array because this method requires "return":1.
80 + *
81 + * It's the actual rendering of the widget which also allows navigation.
82 + */
83 + public function loadFinancialStats()
84 + {
85 + $app = JFactory::getApplication();
86 +
87 + $wrapper = $app->input->getString('wrapper', '');
88 +
89 + $fromdate = $app->input->getString('fromdate', '');
90 + $todate = $app->input->getString('todate', '');
91 + $step = $app->input->getString('step', 'quarter');
92 + $date_dir = $app->input->getInt('date_dir', 0);
93 + $room_ids = array_filter((array) $app->input->getInt('room_ids', []));
94 +
95 + if (empty($fromdate)) {
96 + // default to current quarter (3 full months)
97 + $now_info = getdate();
98 + $end_info = getdate(mktime(0, 0, 0, ($now_info['mon'] + 2), 1, $now_info['year']));
99 + $to_ts = mktime(23, 59, 59, $end_info['mon'], date('t', $end_info[0]), $end_info['year']);
100 + $fromdate = date('Y-m-d', mktime(0, 0, 0, $now_info['mon'], 1, $now_info['year']));
101 + $todate = date('Y-m-d', $to_ts);
102 + }
103 +
104 + // convert dates to timestamps to support custom date formats
105 + $from_ts = VikBooking::getDateTimestamp($fromdate);
106 + $to_ts = VikBooking::getDateTimestamp($todate, 23, 59, 59);
107 +
108 + // check dates navigation and step
109 + $step_name = '';
110 + if ($date_dir > 0 || $date_dir < 0) {
111 + // calculate forward or backward dates for navigation
112 + list($from_ts, $to_ts, $step_name) = $this->calcDatesNavigation($from_ts, $to_ts, $step, $date_dir);
113 + }
114 +
115 + // convert final dates to Y-m-d
116 + $fromdate = date('Y-m-d', $from_ts);
117 + $todate = date('Y-m-d', $to_ts);
118 +
119 + // stats calculation type
120 + $calc_type = !strcasecmp($step, 'booking_dates') ? 'booking_dates' : 'stay_dates';
121 +
122 + // access the finance helper object
123 + $finance = VBOTaxonomyFinance::getInstance();
124 +
125 + // currency symbol
126 + $currencysymb = VikBooking::getCurrencySymb();
127 +
128 + // get the financial stats for the requested dates
129 + try {
130 + $stats = $finance->getStats($fromdate, $todate, $room_ids, $calc_type);
131 + } catch (Exception $e) {
132 + // make the AJAX request fail nicely
133 + VBOHttpDocument::getInstance()->close($e->getCode(), $e->getMessage());
134 + }
135 +
136 + // language string identifier for step-comparison
137 + $js_step_comp_str = 'VBO_COMPARE_WITH_PREV_Q';
138 + if ($step == 'month') {
139 + $js_step_comp_str = 'VBO_COMPARE_WITH_PREV_M';
140 + } elseif ($step == 'week') {
141 + $js_step_comp_str = 'VBO_COMPARE_WITH_PREV_W';
142 + } elseif (!strcasecmp($step, 'booking_dates')) {
143 + $js_step_comp_str = 'VBO_COMPARE_WITH_PREV_D';
144 + }
145 +
146 + // start output buffering
147 + ob_start();
148 +
149 + if ($room_ids) {
150 + // get the details of the rooms filtered
151 + $rooms_filtered = VikBooking::getAvailabilityInstance()->loadRooms($room_ids);
152 +
153 + if ($rooms_filtered) {
154 + ?>
155 + <div class="vbo-widget-finance-rooms-filtered-cont">
156 + <?php echo implode(', ', array_column($rooms_filtered, 'name')); ?>
157 + </div>
158 + <?php
159 + }
160 + }
161 +
162 + ?>
163 + <div class="vbo-widget-finance-data-blocks">
164 +
165 + <div class="vbo-widget-finance-data-block" data-typestat="gross_revenue">
166 + <div class="vbo-widget-finance-stat">
167 + <div class="vbo-widget-finance-stat-info">
168 + <span class="vbo-widget-finance-stat-name"><?php echo JText::translate('VBO_GROSS_BOOKING_VALUE'); ?></span>
169 + <span class="vbo-widget-finance-stat-cmd"><?php VikBookingIcons::e('ellipsis-v'); ?></span>
170 + </div>
171 + <div class="vbo-widget-finance-stat-amount">
172 + <span class="vbo-widget-finance-stat-amount-value vbo-tooltip vbo-tooltip-top" data-tooltiptext="<?php echo VikBooking::formatCurrencyNumber(VikBooking::numberFormat($stats['gross_revenue']), $currencysymb); ?>">
173 + <?php echo VikBooking::formatCurrencyNumber($finance->numberFormatShort($stats['gross_revenue']), $currencysymb, ['<span class="vbo-currency">%s</span>', '<span class="vbo-price">%s</span>']); ?>
174 + </span>
175 + </div>
176 + </div>
177 + </div>
178 +
179 + <div class="vbo-widget-finance-data-block" data-typestat="taxes">
180 + <div class="vbo-widget-finance-stat">
181 + <div class="vbo-widget-finance-stat-info">
182 + <span class="vbo-widget-finance-stat-name"><?php echo JText::translate('VBOINVCOLTAX'); ?></span>
183 + <span class="vbo-widget-finance-stat-cmd"><?php VikBookingIcons::e('ellipsis-v'); ?></span>
184 + </div>
185 + <div class="vbo-widget-finance-stat-amount">
186 + <span class="vbo-widget-finance-stat-amount-value vbo-tooltip vbo-tooltip-top" data-tooltiptext="<?php echo VikBooking::formatCurrencyNumber(VikBooking::numberFormat($stats['taxes']), $currencysymb); ?>">
187 + <?php echo VikBooking::formatCurrencyNumber($finance->numberFormatShort($stats['taxes']), $currencysymb, ['<span class="vbo-currency">%s</span>', '<span class="vbo-price">%s</span>']); ?>
188 + </span>
189 + </div>
190 + </div>
191 + </div>
192 +
193 + <div class="vbo-widget-finance-data-block" data-typestat="cmms">
194 + <div class="vbo-widget-finance-stat">
195 + <div class="vbo-widget-finance-stat-info">
196 + <span class="vbo-widget-finance-stat-name"><?php echo JText::translate('VBTOTALCOMMISSIONS'); ?></span>
197 + <span class="vbo-widget-finance-stat-cmd"><?php VikBookingIcons::e('ellipsis-v'); ?></span>
198 + </div>
199 + <div class="vbo-widget-finance-stat-amount">
200 + <span class="vbo-widget-finance-stat-amount-value vbo-tooltip vbo-tooltip-top" data-tooltiptext="<?php echo VikBooking::formatCurrencyNumber(VikBooking::numberFormat($stats['cmms']), $currencysymb); ?>">
201 + <?php echo VikBooking::formatCurrencyNumber($finance->numberFormatShort($stats['cmms']), $currencysymb, ['<span class="vbo-currency">%s</span>', '<span class="vbo-price">%s</span>']); ?>
202 + </span>
203 + </div>
204 + </div>
205 + </div>
206 +
207 + <div class="vbo-widget-finance-data-block" data-typestat="revenue">
208 + <div class="vbo-widget-finance-stat">
209 + <div class="vbo-widget-finance-stat-info">
210 + <span class="vbo-widget-finance-stat-name"><?php echo JText::translate('VBOREPORTREVENUE'); ?></span>
211 + <span class="vbo-widget-finance-stat-cmd"><?php VikBookingIcons::e('ellipsis-v'); ?></span>
212 + </div>
213 + <div class="vbo-widget-finance-stat-amount">
214 + <span class="vbo-widget-finance-stat-amount-value vbo-tooltip vbo-tooltip-top" data-tooltiptext="<?php echo VikBooking::formatCurrencyNumber(VikBooking::numberFormat($stats['revenue']), $currencysymb); ?>">
215 + <?php echo VikBooking::formatCurrencyNumber($finance->numberFormatShort($stats['revenue']), $currencysymb, ['<span class="vbo-currency">%s</span>', '<span class="vbo-price">%s</span>']); ?>
216 + </span>
217 + </div>
218 + </div>
219 + </div>
220 +
221 + <div class="vbo-widget-finance-data-block" data-typestat="ibe_revenue">
222 + <div class="vbo-widget-finance-stat">
223 + <div class="vbo-widget-finance-stat-info">
224 + <span class="vbo-widget-finance-stat-name"><?php echo JText::translate('VBOREPORTREVENUEREVWEB'); ?></span>
225 + <span class="vbo-widget-finance-stat-cmd"><?php VikBookingIcons::e('ellipsis-v'); ?></span>
226 + </div>
227 + <div class="vbo-widget-finance-stat-amount">
228 + <span class="vbo-widget-finance-stat-amount-value vbo-tooltip vbo-tooltip-top" data-tooltiptext="<?php echo VikBooking::formatCurrencyNumber(VikBooking::numberFormat($stats['ibe_revenue']), $currencysymb); ?>">
229 + <?php echo VikBooking::formatCurrencyNumber($finance->numberFormatShort($stats['ibe_revenue']), $currencysymb, ['<span class="vbo-currency">%s</span>', '<span class="vbo-price">%s</span>']); ?>
230 + </span>
231 + </div>
232 + </div>
233 + </div>
234 +
235 + <div class="vbo-widget-finance-data-block" data-typestat="ota_revenue">
236 + <div class="vbo-widget-finance-stat">
237 + <div class="vbo-widget-finance-stat-info">
238 + <span class="vbo-widget-finance-stat-name"><?php echo JText::translate('VBOREPORTREVENUEREVOTA'); ?></span>
239 + <span class="vbo-widget-finance-stat-cmd"><?php VikBookingIcons::e('ellipsis-v'); ?></span>
240 + </div>
241 + <div class="vbo-widget-finance-stat-amount">
242 + <span class="vbo-widget-finance-stat-amount-value vbo-tooltip vbo-tooltip-top" data-tooltiptext="<?php echo VikBooking::formatCurrencyNumber(VikBooking::numberFormat($stats['ota_revenue']), $currencysymb); ?>">
243 + <?php echo VikBooking::formatCurrencyNumber($finance->numberFormatShort($stats['ota_revenue']), $currencysymb, ['<span class="vbo-currency">%s</span>', '<span class="vbo-price">%s</span>']); ?>
244 + </span>
245 + </div>
246 + </div>
247 + </div>
248 +
249 + <?php
250 + if ($stats['tot_vat'] > 0) {
251 + ?>
252 + <div class="vbo-widget-finance-data-block" data-typestat="tot_vat">
253 + <div class="vbo-widget-finance-stat">
254 + <div class="vbo-widget-finance-stat-info">
255 + <span class="vbo-widget-finance-stat-name"><?php echo JText::translate('VBO_INV_VATGST'); ?></span>
256 + <span class="vbo-widget-finance-stat-cmd"><?php VikBookingIcons::e('ellipsis-v'); ?></span>
257 + </div>
258 + <div class="vbo-widget-finance-stat-amount">
259 + <span class="vbo-widget-finance-stat-amount-value vbo-tooltip vbo-tooltip-top" data-tooltiptext="<?php echo VikBooking::formatCurrencyNumber(VikBooking::numberFormat($stats['tot_vat']), $currencysymb); ?>">
260 + <?php echo VikBooking::formatCurrencyNumber($finance->numberFormatShort($stats['tot_vat']), $currencysymb, ['<span class="vbo-currency">%s</span>', '<span class="vbo-price">%s</span>']); ?>
261 + </span>
262 + </div>
263 + </div>
264 + </div>
265 + <?php
266 + }
267 + if ($stats['city_taxes'] > 0) {
268 + ?>
269 + <div class="vbo-widget-finance-data-block" data-typestat="city_taxes">
270 + <div class="vbo-widget-finance-stat">
271 + <div class="vbo-widget-finance-stat-info">
272 + <span class="vbo-widget-finance-stat-name"><?php echo JText::translate('VBTOTALCITYTAX'); ?></span>
273 + <span class="vbo-widget-finance-stat-cmd"><?php VikBookingIcons::e('ellipsis-v'); ?></span>
274 + </div>
275 + <div class="vbo-widget-finance-stat-amount">
276 + <span class="vbo-widget-finance-stat-amount-value vbo-tooltip vbo-tooltip-top" data-tooltiptext="<?php echo VikBooking::formatCurrencyNumber(VikBooking::numberFormat($stats['city_taxes']), $currencysymb); ?>">
277 + <?php echo VikBooking::formatCurrencyNumber($finance->numberFormatShort($stats['city_taxes']), $currencysymb, ['<span class="vbo-currency">%s</span>', '<span class="vbo-price">%s</span>']); ?>
278 + </span>
279 + </div>
280 + </div>
281 + </div>
282 + <?php
283 + }
284 + if ($stats['cmm_savings'] > 0) {
285 + ?>
286 + <div class="vbo-widget-finance-data-block" data-typestat="ota_avg_cmms">
287 + <div class="vbo-widget-finance-stat">
288 + <div class="vbo-widget-finance-stat-info">
289 + <span class="vbo-widget-finance-stat-name"><?php echo JText::translate('VBO_AVG_COMMISSIONS'); ?></span>
290 + <span class="vbo-widget-finance-stat-cmd"><?php VikBookingIcons::e('ellipsis-v'); ?></span>
291 + </div>
292 + <div class="vbo-widget-finance-stat-amount">
293 + <span class="vbo-widget-finance-stat-amount-value">
294 + <span><?php echo $stats['ota_avg_cmms']; ?>%</span>
295 + </span>
296 + </div>
297 + </div>
298 + </div>
299 +
300 + <div class="vbo-widget-finance-data-block" data-typestat="cmm_savings">
301 + <div class="vbo-widget-finance-stat">
302 + <div class="vbo-widget-finance-stat-info">
303 + <span class="vbo-widget-finance-stat-name"><?php echo JText::translate('VBO_COMMISSION_SAVINGS'); ?></span>
304 + <span class="vbo-widget-finance-stat-cmd"><?php VikBookingIcons::e('ellipsis-v'); ?></span>
305 + </div>
306 + <div class="vbo-widget-finance-stat-amount">
307 + <span class="vbo-widget-finance-stat-amount-value vbo-tooltip vbo-tooltip-top" data-tooltiptext="<?php echo VikBooking::formatCurrencyNumber(VikBooking::numberFormat($stats['cmm_savings']), $currencysymb); ?>">
308 + <?php echo VikBooking::formatCurrencyNumber($finance->numberFormatShort($stats['cmm_savings']), $currencysymb, ['<span class="vbo-currency">%s</span>', '<span class="vbo-price">%s</span>']); ?>
309 + </span>
310 + </div>
311 + </div>
312 + </div>
313 + <?php
314 + }
315 +
316 + /**
317 + * Return the information about the damage deposits collected/held.
318 + *
319 + * @since 1.18.0 (J) - 1.8.0 (WP)
320 + */
321 + if ($stats['damage_deposits'] > 0) {
322 + ?>
323 + <div class="vbo-widget-finance-data-block" data-typestat="damage_deposits">
324 + <div class="vbo-widget-finance-stat">
325 + <div class="vbo-widget-finance-stat-info">
326 + <span class="vbo-widget-finance-stat-name"><?php echo JText::translate('VBO_TOT_DAMAGE_DEPOSITS'); ?></span>
327 + <span class="vbo-widget-finance-stat-cmd"><?php VikBookingIcons::e('ellipsis-v'); ?></span>
328 + </div>
329 + <div class="vbo-widget-finance-stat-amount">
330 + <span class="vbo-widget-finance-stat-amount-value vbo-tooltip vbo-tooltip-top" data-tooltiptext="<?php echo VikBooking::formatCurrencyNumber(VikBooking::numberFormat($stats['damage_deposits']), $currencysymb); ?>">
331 + <?php echo VikBooking::formatCurrencyNumber($finance->numberFormatShort($stats['damage_deposits']), $currencysymb, ['<span class="vbo-currency">%s</span>', '<span class="vbo-price">%s</span>']); ?>
332 + </span>
333 + </div>
334 + </div>
335 + </div>
336 + <?php
337 + }
338 + ?>
339 +
340 + <div class="vbo-widget-finance-data-block" data-typestat="tot_bookings">
341 + <div class="vbo-widget-finance-stat">
342 + <div class="vbo-widget-finance-stat-info">
343 + <span class="vbo-widget-finance-stat-name"><?php echo JText::translate('VBCUSTOMERTOTBOOKINGS'); ?></span>
344 + <span class="vbo-widget-finance-stat-cmd"><?php VikBookingIcons::e('ellipsis-v'); ?></span>
345 + </div>
346 + <div class="vbo-widget-finance-stat-amount">
347 + <span class="vbo-widget-finance-stat-amount-value">
348 + <span><?php echo $stats['tot_bookings']; ?></span>
349 + </span>
350 + </div>
351 + </div>
352 + </div>
353 +
354 + <div class="vbo-widget-finance-data-block" data-typestat="nights_booked">
355 + <div class="vbo-widget-finance-stat">
356 + <div class="vbo-widget-finance-stat-info">
357 + <span class="vbo-widget-finance-stat-name"><?php echo JText::translate('VBOGRAPHTOTNIGHTSLBL'); ?></span>
358 + <span class="vbo-widget-finance-stat-cmd"><?php VikBookingIcons::e('ellipsis-v'); ?></span>
359 + </div>
360 + <div class="vbo-widget-finance-stat-amount">
361 + <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'] . '%'); ?>">
362 + <span><?php echo $stats['nights_booked'] . '/' . $stats['tot_inventory']; ?></span>
363 + </span>
364 + </div>
365 + </div>
366 + </div>
367 +
368 + <div class="vbo-widget-finance-data-block" data-typestat="avg_los">
369 + <div class="vbo-widget-finance-stat">
370 + <div class="vbo-widget-finance-stat-info">
371 + <span class="vbo-widget-finance-stat-name"><?php echo JText::translate('VBTRKAVGLOS'); ?></span>
372 + <span class="vbo-widget-finance-stat-cmd"><?php VikBookingIcons::e('ellipsis-v'); ?></span>
373 + </div>
374 + <div class="vbo-widget-finance-stat-amount">
375 + <span class="vbo-widget-finance-stat-amount-value">
376 + <span><?php echo $stats['avg_los']; ?></span>
377 + </span>
378 + </div>
379 + </div>
380 + </div>
381 +
382 + <div class="vbo-widget-finance-data-block" data-typestat="abw">
383 + <div class="vbo-widget-finance-stat">
384 + <div class="vbo-widget-finance-stat-info">
385 + <span class="vbo-widget-finance-stat-name"><?php echo JText::translate('VBO_AVG_BOOK_WINDOW'); ?></span>
386 + <span class="vbo-widget-finance-stat-cmd"><?php VikBookingIcons::e('ellipsis-v'); ?></span>
387 + </div>
388 + <div class="vbo-widget-finance-stat-amount">
389 + <span class="vbo-widget-finance-stat-amount-value vbo-tooltip vbo-tooltip-top" data-tooltiptext="<?php echo htmlspecialchars(JText::translate('VBOCRONSMSREMPARAMBEFD')); ?>">
390 + <span><?php echo round($stats['abw'], 1); ?></span>
391 + </span>
392 + </div>
393 + </div>
394 + </div>
395 +
396 + <div class="vbo-widget-finance-data-block" data-typestat="rooms_booked">
397 + <div class="vbo-widget-finance-stat">
398 + <div class="vbo-widget-finance-stat-info">
399 + <span class="vbo-widget-finance-stat-name"><?php echo JText::translate('VBLIBTEN'); ?></span>
400 + <span class="vbo-widget-finance-stat-cmd"><?php VikBookingIcons::e('ellipsis-v'); ?></span>
401 + </div>
402 + <div class="vbo-widget-finance-stat-amount">
403 + <span class="vbo-widget-finance-stat-amount-value vbo-tooltip vbo-tooltip-top" data-tooltiptext="<?php echo htmlspecialchars(JText::translate('VBOGRAPHTOTUNITSLBL') . ' ' . $stats['room_units']); ?>">
404 + <span><?php echo $stats['rooms_booked']; ?></span>
405 + </span>
406 + </div>
407 + </div>
408 + </div>
409 +
410 + <div class="vbo-widget-finance-data-block" data-typestat="cancellations_amt">
411 + <div class="vbo-widget-finance-stat">
412 + <div class="vbo-widget-finance-stat-info">
413 + <span class="vbo-widget-finance-stat-name"><?php echo JText::translate('VBO_CANCELLATIONS'); ?></span>
414 + <span class="vbo-widget-finance-stat-cmd"><?php VikBookingIcons::e('ellipsis-v'); ?></span>
415 + </div>
416 + <div class="vbo-widget-finance-stat-amount">
417 + <span class="vbo-widget-finance-stat-amount-value vbo-tooltip vbo-tooltip-top" data-tooltiptext="<?php echo VikBooking::formatCurrencyNumber(VikBooking::numberFormat($stats['cancellations_amt']), $currencysymb) . ($stats['tot_cancellations'] ? ' (' . $stats['tot_cancellations'] . ')' : ''); ?>">
418 + <?php echo VikBooking::formatCurrencyNumber($finance->numberFormatShort($stats['cancellations_amt']), $currencysymb, ['<span class="vbo-currency">%s</span>', '<span class="vbo-price">%s</span>']); ?>
419 + </span>
420 + </div>
421 + </div>
422 + </div>
423 +
424 + </div>
425 +
426 + <div class="vbo-widget-finance-data-block-rankings">
427 +
428 + <div class="vbo-widget-finance-data-block-rank" data-typestat="country_ranks">
429 + <div class="vbo-widget-finance-stat">
430 + <div class="vbo-widget-finance-stat-info">
431 + <span class="vbo-widget-finance-stat-name"><?php echo JText::translate('VBOSTATSTOPCOUNTRIES'); ?></span>
432 + </div>
433 + <div class="vbo-widget-finance-stat-ranks">
434 + <?php
435 + foreach ($stats['country_ranks'] as $country_rank) {
436 + ?>
437 + <div class="vbo-widget-finance-stat-rank">
438 + <div class="vbo-widget-finance-stat-rank-logo">
439 + <?php
440 + if (is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . 'countries' . DIRECTORY_SEPARATOR . $country_rank['code'] . '.png')) {
441 + ?>
442 + <img src="<?php echo VBO_ADMIN_URI . 'resources/countries/' . $country_rank['code'] . '.png'; ?>" />
443 + <?php
444 + } else {
445 + VikBookingIcons::e('globe');
446 + }
447 + ?>
448 + </div>
449 + <div class="vbo-widget-finance-stat-rank-score">
450 + <div class="vbo-widget-finance-stat-rank-name">
451 + <span><?php echo $country_rank['name']; ?></span>
452 + </div>
453 + <div class="vbo-widget-finance-stat-rank-amount">
454 + <?php
455 + echo VikBooking::formatCurrencyNumber(
456 + $finance->numberFormatShort($country_rank['revenue']),
457 + $currencysymb,
458 + [
459 + '<span class="vbo-currency">%s</span>',
460 + '<span class="vbo-price vbo-tooltip vbo-tooltip-top" data-tooltiptext="' . VikBooking::formatCurrencyNumber(VikBooking::numberFormat($country_rank['revenue']), $currencysymb) . '">%s</span>',
461 + ]
462 + );
463 + ?>
464 + </div>
465 + <div class="vbo-widget-finance-stat-rank-pcent">
466 + <progress class="vbo-widget-finance-progress" value="<?php echo $country_rank['pcent']; ?>" max="100"><?php echo $country_rank['pcent']; ?>%</progress>
467 + </div>
468 + </div>
469 + </div>
470 + <?php
471 + }
472 + ?>
473 + </div>
474 + </div>
475 + </div>
476 +
477 + <div class="vbo-widget-finance-data-block-rank" data-typestat="pos_revenue">
478 + <div class="vbo-widget-finance-stat">
479 + <div class="vbo-widget-finance-stat-info">
480 + <span class="vbo-widget-finance-stat-name"><?php echo JText::translate('VBOCHANNELS'); ?></span>
481 + </div>
482 + <div class="vbo-widget-finance-stat-ranks">
483 + <?php
484 + foreach ($stats['pos_revenue'] as $pos_revenue) {
485 + $say_pos_name = ucfirst($pos_revenue['name']);
486 + ?>
487 + <div class="vbo-widget-finance-stat-rank">
488 + <div class="vbo-widget-finance-stat-rank-logo">
489 + <?php
490 + if (!empty($pos_revenue['logo'])) {
491 + ?>
492 + <img src="<?php echo $pos_revenue['logo']; ?>" />
493 + <?php
494 + // adjust channel name
495 + $say_pos_name = strtolower($pos_revenue['name']) == 'airbnbapi' ? 'Airbnb' : $say_pos_name;
496 + $say_pos_name = strtolower($pos_revenue['name']) == 'googlehotel' ? 'Google Hotel' : $say_pos_name;
497 + } elseif (!strcasecmp($pos_revenue['name'], 'website')) {
498 + VikBookingIcons::e('hotel');
499 + // adjust channel name
500 + $say_pos_name = JText::translate('VBORDFROMSITE');
501 + } else {
502 + VikBookingIcons::e('globe');
503 + }
504 + ?>
505 + </div>
506 + <div class="vbo-widget-finance-stat-rank-score">
507 + <div class="vbo-widget-finance-stat-rank-name">
508 + <span><?php echo $say_pos_name; ?></span>
509 + </div>
510 + <div class="vbo-widget-finance-stat-rank-amount">
511 + <?php
512 + echo VikBooking::formatCurrencyNumber(
513 + $finance->numberFormatShort($pos_revenue['revenue']),
514 + $currencysymb,
515 + [
516 + '<span class="vbo-currency">%s</span>',
517 + '<span class="vbo-price vbo-tooltip vbo-tooltip-top" data-tooltiptext="' . VikBooking::formatCurrencyNumber(VikBooking::numberFormat($pos_revenue['revenue']), $currencysymb) . '">%s</span>',
518 + ]
519 + );
520 + ?>
521 + </div>
522 + <div class="vbo-widget-finance-stat-rank-pcent">
523 + <progress class="vbo-widget-finance-progress" value="<?php echo $pos_revenue['pcent']; ?>" max="100"><?php echo $pos_revenue['pcent']; ?>%</progress>
524 + </div>
525 + </div>
526 + </div>
527 + <?php
528 + }
529 + ?>
530 + </div>
531 + </div>
532 + </div>
533 +
534 + </div>
535 +
536 + <input type="hidden" class="vbo-widget-finance-json-stats" value="" />
537 + <input type="hidden" class="vbo-widget-finance-stats-from" value="" />
538 + <input type="hidden" class="vbo-widget-finance-stats-to" value="" />
539 +
540 + <script type="text/javascript">
541 + jQuery(function() {
542 +
543 + let vbo_wfinance_compare_auto = jQuery('#<?php echo $wrapper; ?>').find('.vbo-finance-compare-auto').val();
544 +
545 + let vbo_wfinance_ctx_btns = [
546 + {
547 + icon: '<?php echo VikBookingIcons::i('balance-scale-left'); ?>',
548 + text: Joomla.JText._('<?php echo $js_step_comp_str; ?>'),
549 + separator: false,
550 + action: (root, config) => {
551 + vboWidgetFinanceCompare('<?php echo $wrapper; ?>', '<?php echo $step; ?>');
552 + },
553 + },
554 + {
555 + icon: '<?php echo VikBookingIcons::i('balance-scale-right'); ?>',
556 + text: Joomla.JText._('VBO_COMPARE_WITH_LAST_Y'),
557 + separator: false,
558 + action: (root, config) => {
559 + vboWidgetFinanceCompare('<?php echo $wrapper; ?>', 'year');
560 + },
561 + },
562 + ];
563 +
564 + if (vbo_wfinance_compare_auto.length) {
565 + vbo_wfinance_ctx_btns[1].separator = true;
566 + vbo_wfinance_ctx_btns.push({
567 + icon: '<?php echo VikBookingIcons::i('ban'); ?>',
568 + text: Joomla.JText._((vbo_wfinance_compare_auto == 'year' ? 'VBO_COMPARE_WITH_LAST_Y' : '<?php echo $js_step_comp_str; ?>')),
569 + class: 'vbo-context-menu-entry-danger',
570 + separator: false,
571 + action: (root, config) => {
572 + // turn off the flag to automatically compare data
573 + jQuery('#<?php echo $wrapper; ?>').find('.vbo-finance-compare-auto').val('');
574 + // reload data
575 + vboWidgetFinanceLoad('<?php echo $wrapper; ?>');
576 + },
577 + });
578 + }
579 +
580 + jQuery('#<?php echo $wrapper; ?>').find('.vbo-widget-finance-stat-cmd').vboContextMenu({
581 + placement: 'bottom-right',
582 + buttons: vbo_wfinance_ctx_btns,
583 + });
584 + });
585 + </script>
586 +
587 + <?php
588 +
589 + // get the HTML buffer
590 + $html_content = ob_get_contents();
591 + ob_end_clean();
592 +
593 + // take care of the readable dates interval
594 + list($format_from_date, $format_to_date) = $this->parseReadableDateInterval($from_ts, $to_ts, $this->df, $step);
595 +
596 + // return an associative array of values
597 + return [
598 + 'html' => $html_content,
599 + 'fromdate' => $fromdate,
600 + 'f_fromdate' => $format_from_date,
601 + 'todate' => $todate,
602 + 'f_todate' => $format_to_date,
603 + 'step' => $step,
604 + 'step_name' => $step_name,
605 + 'stats' => $stats,
606 + ];
607 + }
608 +
609 + /**
610 + * Custom method for this widget only to load the comparison values.
611 + * The method is called by the admin controller through an AJAX request.
612 + * The visibility should be public, it should not exit the process, and
613 + * any content sent to output will be returned to the AJAX response.
614 + * In this case we return an array because this method requires "return":1.
615 + *
616 + * Returns the necessary HTML to display comparison values with past dates.
617 + */
618 + public function loadComparisonStats()
619 + {
620 + $app = JFactory::getApplication();
621 +
622 + $wrapper = $app->input->getString('wrapper', '');
623 +
624 + $fromdate = $app->input->getString('fromdate', '');
625 + $todate = $app->input->getString('todate', '');
626 + $step = $app->input->getString('step', 'quarter');
627 + $prev_stats = $app->input->get('stats', [], 'array');
628 + $date_dir = -1;
629 +
630 + if (empty($fromdate) || empty($todate) || empty($prev_stats)) {
631 + // make the AJAX request fail nicely
632 + VBOHttpDocument::getInstance()->close(500, 'Missing data to calculate comparison values');
633 + }
634 +
635 + // convert dates to timestamps to support custom date formats
636 + $from_ts = VikBooking::getDateTimestamp($fromdate);
637 + $to_ts = VikBooking::getDateTimestamp($todate, 23, 59, 59);
638 +
639 + // stats calculation type
640 + $calc_type = 'stay_dates';
641 + if (!strcasecmp($step, 'booking_dates')) {
642 + $calc_type = 'booking_dates';
643 + }
644 +
645 + // calculate backward dates for comparison
646 + list($from_ts, $to_ts, $step_name) = $this->calcDatesNavigation($from_ts, $to_ts, $step, $date_dir);
647 +
648 + // convert final dates to Y-m-d
649 + $fromdate = date('Y-m-d', $from_ts);
650 + $todate = date('Y-m-d', $to_ts);
651 +
652 + // access the finance helper object
653 + $finance = VBOTaxonomyFinance::getInstance();
654 +
655 + // currency symbol
656 + $currencysymb = VikBooking::getCurrencySymb();
657 +
658 + // get the financial stats for the requested dates and use them for comparison
659 + try {
660 + $compare_stats = $finance->getStats($fromdate, $todate, [], $calc_type);
661 + } catch (Exception $e) {
662 + // make the AJAX request fail nicely
663 + VBOHttpDocument::getInstance()->close($e->getCode(), $e->getMessage());
664 + }
665 +
666 + // define the "compare vs" string
667 + switch ($step) {
668 + case 'year':
669 + $compare_vs_str = JText::translate('VBO_VS_LAST_Y');
670 + break;
671 + case 'quarter':
672 + $compare_vs_str = JText::translate('VBO_VS_PREV_Q');
673 + break;
674 + case 'month':
675 + $compare_vs_str = JText::translate('VBO_VS_PREV_M');
676 + break;
677 + case 'week':
678 + $compare_vs_str = JText::translate('VBO_VS_PREV_W');
679 + break;
680 + case 'booking_dates':
681 + $compare_vs_str = JText::translate('VBO_VS_PREV_D');
682 + break;
683 + default:
684 + $compare_vs_str = '';
685 + break;
686 + }
687 +
688 + // build comparison values
689 + $comparison = [];
690 +
691 + // stats that require percent values for comparison
692 + $pcent_stats = [
693 + 'gross_revenue' => [],
694 + 'taxes' => [],
695 + 'tot_vat' => [],
696 + 'city_taxes' => [],
697 + 'cmms' => [
698 + 'reverse' => 1,
699 + ],
700 + 'revenue' => [],
701 + 'ibe_revenue' => [],
702 + 'ota_revenue' => [],
703 + 'ota_avg_cmms' => [
704 + 'reverse' => 1,
705 + 'no_pcent' => 1,
706 + 'fixednum' => 1,
707 + ],
708 + 'cmm_savings' => [],
709 + 'damage_deposits' => [],
710 + 'tot_bookings' => [
711 + 'fixednum' => 1,
712 + ],
713 + 'nights_booked' => [
714 + 'fixednum' => 1,
715 + ],
716 + 'avg_los' => [
717 + 'fixednum' => 1,
718 + ],
719 + 'abw' => [
720 + 'fixednum' => 1,
721 + ],
722 + 'rooms_booked' => [
723 + 'fixednum' => 1,
724 + ],
725 + 'cancellations_amt' => [
726 + 'reverse' => 1,
727 + ],
728 + ];
729 +
730 + foreach ($pcent_stats as $stat_name => $stat_opt) {
731 + if (!isset($compare_stats[$stat_name])) {
732 + continue;
733 + }
734 +
735 + // make sure the previous value is set
736 + $prev_stats[$stat_name] = !isset($prev_stats[$stat_name]) ? 0 : $prev_stats[$stat_name];
737 +
738 + // difference
739 + $diff = isset($stat_opt['reverse']) ? ($compare_stats[$stat_name] - $prev_stats[$stat_name]) : ($prev_stats[$stat_name] - $compare_stats[$stat_name]);
740 +
741 + // calculate values
742 + $comparison[$stat_name] = [
743 + 'amount' => $compare_stats[$stat_name],
744 + 'amount_f' => VikBooking::numberFormat($compare_stats[$stat_name]),
745 + 'amount_s' => $finance->numberFormatShort($compare_stats[$stat_name]),
746 + 'diff' => $diff,
747 + 'diff_f' => VikBooking::numberFormat($diff),
748 + 'diff_s' => $finance->numberFormatShort($diff),
749 + 'pcent' => isset($stat_opt['no_pcent']) ? null : $finance->calcAbsPercent($prev_stats[$stat_name], $compare_stats[$stat_name]),
750 + 'reverse' => $stat_opt['reverse'] ?? false,
751 + ];
752 +
753 + // inject property for fixed number with no currency
754 + if (isset($stat_opt['fixednum']) && $stat_opt['fixednum']) {
755 + $comparison[$stat_name]['fixednum'] = $stat_opt['fixednum'];
756 + }
757 + }
758 +
759 + // return an associative array of values
760 + return [
761 + 'no_data' => ($compare_stats['tot_bookings'] < 1 ? 1 : 0),
762 + 'compare' => $comparison,
763 + 'vs_str' => $compare_vs_str,
764 + 'currency' => $currencysymb,
765 + 'stats' => $compare_stats,
766 + ];
767 + }
768 +
769 + /**
770 + * Custom method for this widget only to retrieve the daily
771 + * welcome statistics about the bookings of the day before.
772 + *
773 + * @return array the associative list of statistics.
774 + */
775 + public function getWelcomeStats()
776 + {
777 + // get yesterday's date
778 + $yesterday = date('Y-m-d', strtotime('yesterday'));
779 +
780 + // access the finance helper object
781 + $finance = VBOTaxonomyFinance::getInstance();
782 +
783 + // get the financial stats for the requested dates
784 + try {
785 + $stats = $finance->getStats($yesterday, $yesterday, $rooms = [], $type = 'booking_dates');
786 + } catch (Exception $e) {
787 + // make the AJAX request fail nicely
788 + VBOHttpDocument::getInstance()->close($e->getCode(), $e->getMessage());
789 + }
790 +
791 + return $stats;
792 + }
793 +
794 +
795 + /**
796 + * Main method to invoke the widget. Contents will be loaded
797 + * through AJAX requests, not via PHP when the page loads.
798 + *
799 + * @param ?VBOMultitaskData $data
800 + *
801 + * @return void
802 + */
803 + public function render(?VBOMultitaskData $data = null)
804 + {
805 + // increase widget's instance counter
806 + static::$instance_counter++;
807 +
808 + // check whether the widget is being rendered via AJAX when adding it through the customizer
809 + $is_ajax = $this->isAjaxRendering();
810 +
811 + // generate a unique ID for the sticky notes wrapper instance
812 + $wrapper_instance = !$is_ajax ? static::$instance_counter : rand();
813 + $wrapper_id = 'vbo-widget-finance-' . $wrapper_instance;
814 +
815 + // check permissions
816 + $vbo_auth_pricing = JFactory::getUser()->authorise('core.vbo.pricing', 'com_vikbooking');
817 + $vbo_auth_bookings = JFactory::getUser()->authorise('core.vbo.bookings', 'com_vikbooking');
818 + if (!$vbo_auth_pricing || !$vbo_auth_bookings) {
819 + // base permissions are not met
820 + return;
821 + }
822 +
823 + // we make use of the rates flow and occupancy ranking report helper classes
824 + $report = VikBooking::getReportInstance('rates_flow');
825 + if (!$report) {
826 + // display nothing
827 + return;
828 + }
829 +
830 + // get minimum and maximum nights updated for dates filters
831 + list($mindate, $maxdate) = $this->getMinDatesFinance();
832 + $mindate = empty($mindate) ? time() : $mindate;
833 + $maxdate = empty($maxdate) ? $mindate : $maxdate;
834 +
835 + // dates navigation preference
836 + $cookie = JFactory::getApplication()->input->cookie;
837 + $cookie_step = $cookie->get('vbo_widget_finance_step', 'quarter', 'string');
838 +
839 + // determine default period name and dates
840 + $df = $report->getDateFormat();
841 + $dtpicker_df = $report->getDateFormat('jui');
842 + $now_info = getdate();
843 + if ($cookie_step == 'week') {
844 + // next week
845 + $from_ts = mktime(0, 0, 0, $now_info['mon'], $now_info['mday'], $now_info['year']);
846 + $to_ts = mktime(0, 0, 0, $now_info['mon'], ($now_info['mday'] + 7), $now_info['year']);
847 + $fromdate = date($df, $from_ts);
848 + $todate = date($df, $to_ts);
849 + $period_name = JText::translate('VBOWEEK');
850 + } elseif ($cookie_step == 'month') {
851 + // current full month
852 + $from_ts = mktime(0, 0, 0, $now_info['mon'], 1, $now_info['year']);
853 + $fromdate = date($df, $from_ts);
854 + $to_ts = mktime(23, 59, 59, $now_info['mon'], date('t', $now_info[0]), $now_info['year']);
855 + $todate = date($df, $to_ts);
856 + $period_name = JText::translate('VBPVIEWRESTRICTIONSTWO');
857 + } else {
858 + // default to next 3 (total) months from today's month
859 + $from_ts = mktime(0, 0, 0, $now_info['mon'], 1, $now_info['year']);
860 + $fromdate = date($df, $from_ts);
861 + $end_info = getdate(mktime(0, 0, 0, ($now_info['mon'] + 2), 1, $now_info['year']));
862 + $to_ts = mktime(23, 59, 59, $end_info['mon'], date('t', $end_info[0]), $end_info['year']);
863 + $todate = date($df, $to_ts);
864 + $period_name = JText::translate('VBO_QUARTER');
865 + }
866 +
867 + // check multitask data
868 + $rooms_filtered = [];
869 + if ($data) {
870 + $inj_fromdate = $data->get('fromdate', $this->options()->get('fromdate', ''));
871 + $inj_todate = $data->get('todate', $this->options()->get('todate', ''));
872 + $inj_type = $data->get('type', $this->options()->get('type', ''));
873 +
874 + // check if some room IDs were filtered
875 + $rooms_filtered = (array) $data->get('room_ids', $this->options()->get('room_ids', []));
876 +
877 + if (preg_match("/^\d{4}-\d{2}-\d{2}$/", $inj_fromdate) && preg_match("/^\d{4}-\d{2}-\d{2}$/", $inj_todate)) {
878 + // valid dates injected in Y-m-d format
879 + $fromdate = $inj_fromdate;
880 + $todate = $inj_todate;
881 + $to_info = getdate(strtotime($todate));
882 + $from_ts = strtotime($fromdate);
883 + $to_ts = mktime(23, 59, 59, $to_info['mon'], $to_info['mday'], $to_info['year']);
884 + if (!empty($inj_type)) {
885 + $cookie_step = $inj_type;
886 + if (!strcasecmp($inj_type, 'booking_dates')) {
887 + $period_name = JText::translate('VBOSTATSMODETS');
888 + }
889 + }
890 + }
891 + }
892 +
893 + // take care of the readable dates interval
894 + list($format_from_date, $format_to_date) = $this->parseReadableDateInterval($from_ts, $to_ts, $df, $cookie_step);
895 + $interval_name = $format_from_date . ' - ' . $format_to_date;
896 + if ($format_from_date == $format_to_date) {
897 + $interval_name = $format_from_date;
898 + }
899 +
900 + ?>
901 + <div id="<?php echo $wrapper_id; ?>" class="vbo-admin-widget-wrapper" data-instance="<?php echo $wrapper_instance; ?>">
902 + <div class="vbo-admin-widget-head">
903 + <div class="vbo-admin-widget-head-inline">
904 + <h4><?php echo $this->widgetIcon; ?> <span><?php echo $this->widgetName; ?></span></h4>
905 + <div class="vbo-admin-widget-head-commands">
906 +
907 + <div class="vbo-reportwidget-commands">
908 + <div class="vbo-reportwidget-commands-main">
909 + <div class="vbo-reportwidget-command-dates">
910 + <div class="vbo-reportwidget-period-name"><?php echo $period_name; ?></div>
911 + <div class="vbo-reportwidget-period-date"><?php echo $interval_name; ?></div>
912 + </div>
913 + <div class="vbo-reportwidget-command-chevron vbo-reportwidget-command-prev">
914 + <span class="vbo-widget-finance-dt-prev" onclick="vboWidgetFinanceDatesNav('<?php echo $wrapper_id; ?>', -1);"><?php VikBookingIcons::e('chevron-left'); ?></span>
915 + </div>
916 + <div class="vbo-reportwidget-command-chevron vbo-reportwidget-command-next">
917 + <span class="vbo-widget-finance-dt-next" onclick="vboWidgetFinanceDatesNav('<?php echo $wrapper_id; ?>', 1);"><?php VikBookingIcons::e('chevron-right'); ?></span>
918 + </div>
919 + </div>
920 + <div class="vbo-reportwidget-command-dots">
921 + <span class="vbo-widget-command-togglefilters vbo-widget-finance-togglefilters" onclick="vboWidgetFinanceToggleFilters('<?php echo $wrapper_id; ?>');"><?php VikBookingIcons::e('ellipsis-v'); ?></span>
922 + </div>
923 + </div>
924 + <div class="vbo-reportwidget-filters">
925 + <div class="vbo-reportwidget-filter">
926 + <span class="vbo-reportwidget-datepicker">
927 + <?php VikBookingIcons::e('calendar', 'vbo-widget-finance-caltrigger'); ?>
928 + <input type="text" class="vbo-finance-dtpicker-from" value="<?php echo $fromdate; ?>" />
929 + </span>
930 + </div>
931 + <div class="vbo-reportwidget-filter">
932 + <span class="vbo-reportwidget-datepicker">
933 + <?php VikBookingIcons::e('calendar', 'vbo-widget-finance-caltrigger'); ?>
934 + <input type="text" class="vbo-finance-dtpicker-to" value="<?php echo $todate; ?>" />
935 + </span>
936 + </div>
937 + <div class="vbo-reportwidget-filter">
938 + <select class="vbo-finance-period-nav" onchange="vboWidgetFinanceChangePeriod(this.value);">
939 + <option value="week"<?php echo $cookie_step == 'week' ? ' selected="selected"' : ''; ?>><?php echo JText::translate('VBOWEEK'); ?></option>
940 + <option value="month"<?php echo $cookie_step == 'month' ? ' selected="selected"' : ''; ?>><?php echo JText::translate('VBPVIEWRESTRICTIONSTWO'); ?></option>
941 + <option value="quarter"<?php echo $cookie_step == 'quarter' ? ' selected="selected"' : ''; ?>><?php echo JText::translate('VBO_QUARTER'); ?></option>
942 + <option value="booking_dates"<?php echo !strcasecmp($cookie_step, 'booking_dates') ? ' selected="selected"' : ''; ?>><?php echo JText::translate('VBPCHOOSEBUSYORDATE'); ?></option>
943 + </select>
944 + </div>
945 + <div class="vbo-reportwidget-filter vbo-widget-finance-rooms-filter-wrap">
946 + <select class="vbo-widget-finance-rooms-filter" multiple="multiple">
947 + <?php
948 + foreach ($rooms_filtered as $room_filtered) {
949 + if ($room_info = VikBooking::getRoomInfo($room_filtered, ['id', 'name'], true)) {
950 + ?>
951 + <option value="<?php echo $room_info['id']; ?>" selected="selected"><?php echo $room_info['name']; ?></option>
952 + <?php
953 + }
954 + }
955 + ?>
956 + </select>
957 + </div>
958 + <div class="vbo-reportwidget-filter vbo-reportwidget-filter-confirm">
959 + <input type="hidden" class="vbo-finance-compare-auto" value="" />
960 + <button type="button" class="btn vbo-config-btn" onclick="vboWidgetFinanceChangeDates('<?php echo $wrapper_id; ?>');"><?php echo JText::translate('VBADMINNOTESUPD'); ?></button>
961 + </div>
962 + </div>
963 +
964 + </div>
965 + </div>
966 + </div>
967 + <div class="vbo-widget-finance-wrap">
968 + <div class="vbo-widget-finance-inner">
969 + <div class="vbo-widget-finance-list">
970 + <div class="vbo-widget-finance-skeleton-blocks">
971 + <?php
972 + // display a few loading skeletons
973 + for ($i = 0; $i < 10; $i++) {
974 + ?>
975 + <div class="vbo-widget-finance-skeleton-block">
976 + <div class="vbo-widget-finance-skeleton-top">
977 + <div class="vbo-skeleton-loading vbo-skeleton-loading-finance-top"></div>
978 + </div>
979 + <div class="vbo-widget-finance-skeleton-bottom">
980 + <div class="vbo-skeleton-loading vbo-skeleton-loading-finance-bottom"></div>
981 + </div>
982 + </div>
983 + <?php
984 + }
985 + ?>
986 + </div>
987 + </div>
988 + </div>
989 + </div>
990 + </div>
991 + <?php
992 +
993 + if (static::$instance_counter === 0 || $is_ajax) {
994 + /**
995 + * Print the JS code only once for all instances of this widget.
996 + * The real rendering is made through AJAX, not when the page loads.
997 + */
998 + ?>
999 + <script type="text/javascript">
1000 +
1001 + /**
1002 + * Display the loading skeletons.
1003 + */
1004 + function vboWidgetFinanceSkeletons(wrapper) {
1005 + var widget_instance = jQuery('#' + wrapper);
1006 + if (!widget_instance.length) {
1007 + return false;
1008 + }
1009 + widget_instance.find('.vbo-widget-finance-list').html('');
1010 + var skeleton = '';
1011 + skeleton += '<div class="vbo-widget-finance-skeleton-blocks">' + "\n";
1012 + for (var i = 0; i < 10; i++) {
1013 + skeleton += '<div class="vbo-widget-finance-skeleton-block">';
1014 + skeleton += ' <div class="vbo-widget-finance-skeleton-top">';
1015 + skeleton += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-finance-top"></div>';
1016 + skeleton += ' </div>';
1017 + skeleton += ' <div class="vbo-widget-finance-skeleton-bottom">';
1018 + skeleton += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-finance-bottom"></div>';
1019 + skeleton += ' </div>';
1020 + skeleton += '</div>' + "\n";
1021 + }
1022 + skeleton += '</div>' + "\n";
1023 + // append skeletons
1024 + jQuery(skeleton).appendTo(widget_instance.find('.vbo-widget-finance-list'));
1025 + }
1026 +
1027 + /**
1028 + * Perform the request to load the financial stats.
1029 + */
1030 + function vboWidgetFinanceLoad(wrapper, dates_direction) {
1031 + var widget_instance = jQuery('#' + wrapper);
1032 + if (!widget_instance.length) {
1033 + return false;
1034 + }
1035 +
1036 + // check if a navigation of dates was requested (0 = no dates nav)
1037 + if (typeof dates_direction === 'undefined') {
1038 + dates_direction = 0;
1039 + }
1040 +
1041 + // get vars for making the request
1042 + var from_date = widget_instance.find('.vbo-finance-dtpicker-from').val();
1043 + var to_date = widget_instance.find('.vbo-finance-dtpicker-to').val();
1044 + var dates_step = widget_instance.find('.vbo-finance-period-nav').val();
1045 + var room_ids = widget_instance.find('select.vbo-widget-finance-rooms-filter').val();
1046 + var auto_compare = widget_instance.find('.vbo-finance-compare-auto').val();
1047 +
1048 + // the widget method to call
1049 + var call_method = 'loadFinancialStats';
1050 +
1051 + // make a request to load the financial stats
1052 + VBOCore.doAjax(
1053 + "<?php echo $this->getExecWidgetAjaxUri(); ?>",
1054 + {
1055 + widget_id: "<?php echo $this->getIdentifier(); ?>",
1056 + call: call_method,
1057 + return: 1,
1058 + fromdate: from_date,
1059 + todate: to_date,
1060 + step: dates_step,
1061 + room_ids: room_ids,
1062 + date_dir: dates_direction,
1063 + wrapper: wrapper,
1064 + tmpl: "component"
1065 + },
1066 + (response) => {
1067 + try {
1068 + var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
1069 + if (!obj_res.hasOwnProperty(call_method)) {
1070 + console.error('Unexpected JSON response', obj_res);
1071 + return false;
1072 + }
1073 +
1074 + // replace HTML with new stats
1075 + widget_instance.find('.vbo-widget-finance-list').html(obj_res[call_method]['html']);
1076 +
1077 + // populate JSON data for comparison
1078 + widget_instance.find('input.vbo-widget-finance-json-stats').val(JSON.stringify(obj_res[call_method]['stats']));
1079 + widget_instance.find('input.vbo-widget-finance-stats-from').val(obj_res[call_method]['fromdate']);
1080 + widget_instance.find('input.vbo-widget-finance-stats-to').val(obj_res[call_method]['todate']);
1081 +
1082 + if (dates_direction > 0 || dates_direction < 0) {
1083 + // set new dates calculated - note: if maxDate or minDate is exceeded, the datepicker calendars won't update!
1084 + try {
1085 + // construct a solid date object without using the raw Y-m-d string as only argument
1086 + var dfrom_parts = obj_res[call_method]['fromdate'].split('-');
1087 + var dfrom_obj = new Date(parseInt(dfrom_parts[0]), (parseInt(dfrom_parts[1]) - 1), parseInt(dfrom_parts[2]), 0, 0, 0);
1088 + widget_instance.find('.vbo-finance-dtpicker-from').datepicker('setDate', dfrom_obj);
1089 +
1090 + // restore the original minimum and maximum dates for the "to date" calendar to avoid issues setting a date
1091 + widget_instance.find('.vbo-finance-dtpicker-to').datepicker('option', {
1092 + minDate: "<?php echo date($df, $mindate); ?>",
1093 + maxDate: "<?php echo date($df, $maxdate); ?>",
1094 + });
1095 +
1096 + // do the same for the to date by using a precise date object instance
1097 + var dto_parts = obj_res[call_method]['todate'].split('-');
1098 + var dto_obj = new Date(parseInt(dto_parts[0]), (parseInt(dto_parts[1]) - 1), parseInt(dto_parts[2]), 0, 0, 0);
1099 + widget_instance.find('.vbo-finance-dtpicker-to').datepicker('setDate', dto_obj);
1100 + } catch(e) {
1101 + // just log the error
1102 + console.error(e);
1103 + }
1104 + }
1105 +
1106 + // update step name
1107 + if (obj_res[call_method]['step_name'] && obj_res[call_method]['step_name'].length) {
1108 + widget_instance.find('.vbo-reportwidget-period-name').text(obj_res[call_method]['step_name']);
1109 + }
1110 +
1111 + // update readable dates interval
1112 + var readable_period = obj_res[call_method]['f_fromdate'] + ' - ' + obj_res[call_method]['f_todate'];
1113 + if (obj_res[call_method]['f_fromdate'] == obj_res[call_method]['f_todate']) {
1114 + // a full month does not need an interval
1115 + readable_period = obj_res[call_method]['f_fromdate'];
1116 + }
1117 + widget_instance.find('.vbo-reportwidget-period-date').text(readable_period);
1118 +
1119 + if (auto_compare.length) {
1120 + // trigger the stats comparison
1121 + setTimeout(() => {
1122 + vboWidgetFinanceCompare(wrapper, (auto_compare == 'year' ? 'year' : dates_step));
1123 + }, 400);
1124 + }
1125 + } catch(err) {
1126 + console.error('could not parse JSON response', err, response);
1127 + }
1128 + },
1129 + (error) => {
1130 + // remove the skeleton loading
1131 + widget_instance.find('.vbo-widget-finance-list').find('.vbo-widget-finance-skeleton-blocks').remove();
1132 + console.error(error);
1133 + alert(error.responseText);
1134 + }
1135 + );
1136 + }
1137 +
1138 + /**
1139 + * Load stats comparison values.
1140 + */
1141 + function vboWidgetFinanceCompare(wrapper, period) {
1142 + var widget_instance = jQuery('#' + wrapper);
1143 + if (!widget_instance.length) {
1144 + return false;
1145 + }
1146 +
1147 + // append loading elements
1148 + widget_instance.find('.vbo-widget-finance-data-blocks').find('.vbo-widget-finance-stat').each(function(k, v) {
1149 + if (jQuery(this).find('.vbo-widget-finance-stat-compare').length) {
1150 + jQuery(this).find('.vbo-widget-finance-stat-compare').remove();
1151 + }
1152 + var comparison_el = jQuery('<div></div>').addClass('vbo-widget-finance-stat-compare');
1153 + var loading_el = jQuery('<div></div>').addClass('vbo-widget-finance-compare-amount');
1154 + loading_el.append('<i class="<?php echo VikBookingIcons::i('circle-notch', 'fa-spin fa-fw'); ?>"></i>');
1155 + comparison_el.append(loading_el);
1156 + jQuery(this).append(comparison_el);
1157 + });
1158 +
1159 + // get vars for making the request
1160 + var prev_stats = JSON.parse(widget_instance.find('.vbo-widget-finance-json-stats').val());
1161 + var from_date = widget_instance.find('.vbo-widget-finance-stats-from').val();
1162 + var to_date = widget_instance.find('.vbo-widget-finance-stats-to').val();
1163 +
1164 + // the widget method to call
1165 + var call_method = 'loadComparisonStats';
1166 +
1167 + // make a request to load the comparison values
1168 + VBOCore.doAjax(
1169 + "<?php echo $this->getExecWidgetAjaxUri(); ?>",
1170 + {
1171 + widget_id: "<?php echo $this->getIdentifier(); ?>",
1172 + call: call_method,
1173 + return: 1,
1174 + fromdate: from_date,
1175 + todate: to_date,
1176 + step: period,
1177 + stats: prev_stats,
1178 + wrapper: wrapper,
1179 + tmpl: "component"
1180 + },
1181 + (response) => {
1182 + try {
1183 + var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
1184 + if (!obj_res.hasOwnProperty(call_method)) {
1185 + console.error('Unexpected JSON response', obj_res);
1186 + return false;
1187 + }
1188 +
1189 + // turn on the flag to automatically compare data
1190 + widget_instance.find('.vbo-finance-compare-auto').val(period);
1191 +
1192 + if (obj_res[call_method]['no_data']) {
1193 + // no data available for comparison
1194 + widget_instance.find('.vbo-widget-finance-data-blocks').find('.vbo-widget-finance-stat').each(function(k, v) {
1195 + if (jQuery(this).find('.vbo-widget-finance-stat-compare').length) {
1196 + jQuery(this).find('.vbo-widget-finance-stat-compare').remove();
1197 + }
1198 + let comparison_el = jQuery('<div></div>').addClass('vbo-widget-finance-stat-compare');
1199 + let amount_el = jQuery('<div></div>').addClass('vbo-widget-finance-compare-amount');
1200 + comparison_el.append(amount_el);
1201 + let pcent_el = jQuery('<div></div>').addClass('vbo-widget-finance-compare-pcent');
1202 + pcent_el.append('<span></span>').addClass('vbo-widget-finance-compare-txt').text(Joomla.JText._('VBNOTRACKINGS'));
1203 + comparison_el.append(pcent_el);
1204 + jQuery(this).append(comparison_el);
1205 + });
1206 +
1207 + // do not proceed
1208 + return;
1209 + }
1210 +
1211 + // parse all comparison values
1212 + for (let stat_name in obj_res[call_method]['compare']) {
1213 + if (!obj_res[call_method]['compare'].hasOwnProperty(stat_name)) {
1214 + continue;
1215 + }
1216 +
1217 + let stat_elem = widget_instance.find('.vbo-widget-finance-data-block[data-typestat="' + stat_name + '"]');
1218 + if (!stat_elem || !stat_elem.length) {
1219 + continue;
1220 + }
1221 +
1222 + // clean up loading element
1223 + if (stat_elem.find('.vbo-widget-finance-stat-compare').length) {
1224 + stat_elem.find('.vbo-widget-finance-stat-compare').remove();
1225 + }
1226 +
1227 + // check if currency is needed
1228 + let use_currency = (!obj_res[call_method]['compare'][stat_name].hasOwnProperty('fixednum') || !obj_res[call_method]['compare'][stat_name]['fixednum']);
1229 +
1230 + // the new comparison element
1231 + let comparison_el = jQuery('<div></div>').addClass('vbo-widget-finance-stat-compare');
1232 +
1233 + // amount block
1234 + let amount_block = jQuery('<div></div>').addClass('vbo-widget-finance-compare-amount');
1235 + let amount_value = jQuery('<span></span>').addClass('vbo-widget-finance-compare-amount-value');
1236 + if (use_currency) {
1237 + 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']);
1238 + amount_value.append('<span class="vbo-currency">' + obj_res[call_method]['currency'] + '</span>');
1239 + }
1240 + if (obj_res[call_method]['compare'][stat_name]['pcent'] != null) {
1241 + amount_value.append('<span class="vbo-price">' + obj_res[call_method]['compare'][stat_name]['amount_s'] + '</span>');
1242 + } else {
1243 + amount_value.append('<span class="vbo-price">' + obj_res[call_method]['compare'][stat_name]['amount'] + '%</span>');
1244 + }
1245 + amount_block.append(amount_value);
1246 +
1247 + // percent block
1248 + let pcent_block = jQuery('<div></div>').addClass('vbo-widget-finance-compare-pcent');
1249 + let pcent_icon = '';
1250 + if (obj_res[call_method]['compare'][stat_name]['diff'] == 0) {
1251 + pcent_block.addClass('vbo-widget-finance-compare-pcent-equal');
1252 + pcent_icon = '<?php VikBookingIcons::e('equals') ?>';
1253 + } else if (obj_res[call_method]['compare'][stat_name]['diff'] > 0) {
1254 + pcent_block.addClass('vbo-widget-finance-compare-pcent-up');
1255 + pcent_icon = obj_res[call_method]['compare'][stat_name]['reverse'] ? '<?php VikBookingIcons::e('arrow-down') ?>' : '<?php VikBookingIcons::e('arrow-up') ?>';
1256 + } else if (obj_res[call_method]['compare'][stat_name]['diff'] < 0) {
1257 + pcent_block.addClass('vbo-widget-finance-compare-pcent-down');
1258 + pcent_icon = obj_res[call_method]['compare'][stat_name]['reverse'] ? '<?php VikBookingIcons::e('arrow-up') ?>' : '<?php VikBookingIcons::e('arrow-down') ?>';
1259 + }
1260 + if (obj_res[call_method]['compare'][stat_name]['pcent'] != null) {
1261 + pcent_block.append('<span class="vbo-widget-finance-compare-val">' + pcent_icon + ' ' + obj_res[call_method]['compare'][stat_name]['pcent'] + '%</span>');
1262 + }
1263 + pcent_block.append('<span class="vbo-widget-finance-compare-txt">' + obj_res[call_method]['vs_str'] + '</span>');
1264 +
1265 + // append amount block
1266 + comparison_el.append(amount_block);
1267 + // append percent block
1268 + comparison_el.append(pcent_block);
1269 +
1270 + // append comparison element
1271 + stat_elem.find('.vbo-widget-finance-stat').append(comparison_el);
1272 + }
1273 + } catch(err) {
1274 + console.error('could not parse JSON response', err, response);
1275 + }
1276 + },
1277 + (error) => {
1278 + // remove loading icons
1279 + widget_instance.find('.vbo-widget-finance-data-blocks').find('.vbo-widget-finance-stat-compare').remove();
1280 + console.error(error);
1281 + alert(error.responseText);
1282 + }
1283 + );
1284 +
1285 + }
1286 +
1287 + /**
1288 + * Navigate between the date steps and load the stats.
1289 + */
1290 + function vboWidgetFinanceDatesNav(wrapper, direction) {
1291 + var widget_instance = jQuery('#' + wrapper);
1292 + if (!widget_instance.length) {
1293 + return false;
1294 + }
1295 +
1296 + // show loading skeletons
1297 + vboWidgetFinanceSkeletons(wrapper);
1298 +
1299 + // launch dates navigation and load records
1300 + vboWidgetFinanceLoad(wrapper, direction);
1301 + }
1302 +
1303 + /**
1304 + * Load stats for the selected dates.
1305 + */
1306 + function vboWidgetFinanceChangeDates(wrapper) {
1307 + var widget_instance = jQuery('#' + wrapper);
1308 + if (!widget_instance.length) {
1309 + return false;
1310 + }
1311 +
1312 + // hide filters when making a new request
1313 + widget_instance.find('.vbo-reportwidget-filters').hide();
1314 +
1315 + // show loading skeletons
1316 + vboWidgetFinanceSkeletons(wrapper);
1317 +
1318 + // load data
1319 + vboWidgetFinanceLoad(wrapper);
1320 + }
1321 +
1322 + /**
1323 + * Toggle dates and step filters.
1324 + */
1325 + function vboWidgetFinanceToggleFilters(wrapper) {
1326 + var widget_instance = jQuery('#' + wrapper);
1327 + if (!widget_instance.length) {
1328 + return false;
1329 + }
1330 +
1331 + widget_instance.find('.vbo-reportwidget-filters').toggle();
1332 + }
1333 +
1334 + /**
1335 + * Datepicker dates selection.
1336 + */
1337 + function vboWidgetFinanceCheckDates(selectedDate, inst) {
1338 + if (selectedDate === null || inst === null) {
1339 + return;
1340 + }
1341 + var cur_from_date = jQuery(this).val();
1342 + if (jQuery(this).hasClass("vbo-finance-dtpicker-from") && cur_from_date.length) {
1343 + var nowstart = jQuery(this).datepicker("getDate");
1344 + var nowstartdate = new Date(nowstart.getTime());
1345 + jQuery(".vbo-finance-dtpicker-to").datepicker("option", {minDate: nowstartdate});
1346 + }
1347 + }
1348 +
1349 + /**
1350 + * Navigation period cookie onchange.
1351 + */
1352 + function vboWidgetFinanceChangePeriod(period) {
1353 + // always exclude "booking_dates"
1354 + if (period && period === 'booking_dates') {
1355 + return;
1356 + }
1357 + // update cookie for the step selected
1358 + var nd = new Date();
1359 + nd.setTime(nd.getTime() + (365*24*60*60*1000));
1360 + document.cookie = "vbo_widget_finance_step=" + period + "; expires=" + nd.toUTCString() + "; path=/; SameSite=Lax";
1361 + }
1362 +
1363 + </script>
1364 + <?php
1365 + }
1366 + ?>
1367 +
1368 + <script type="text/javascript">
1369 +
1370 + jQuery(function() {
1371 +
1372 + // render datepicker calendars for dates navigation
1373 + jQuery('#<?php echo $wrapper_id; ?>').find('.vbo-finance-dtpicker-from, .vbo-finance-dtpicker-to').datepicker({
1374 + minDate: "<?php echo date($df, $mindate); ?>",
1375 + maxDate: "<?php echo date($df, $maxdate); ?>",
1376 + yearRange: "<?php echo date('Y', $mindate); ?>:<?php echo date('Y', $maxdate); ?>",
1377 + changeMonth: true,
1378 + changeYear: true,
1379 + dateFormat: "<?php echo $dtpicker_df; ?>",
1380 + onSelect: vboWidgetFinanceCheckDates
1381 + });
1382 +
1383 + // triggering for datepicker calendar icons
1384 + jQuery('i.vbo-widget-finance-caltrigger').click(function() {
1385 + var jdp = jQuery(this).parent().find('input.hasDatepicker');
1386 + if (jdp.length) {
1387 + jdp.focus();
1388 + }
1389 + });
1390 +
1391 + // register room filters search via AJAX
1392 + jQuery('#<?php echo $wrapper_id; ?>').find('select.vbo-widget-finance-rooms-filter').select2({
1393 + placeholder: Joomla.JText._('VBROOMFILTER'),
1394 + language: {
1395 + errorLoading: () => {
1396 + return Joomla.JText._('VBO_ERR_LOAD_RESULTS');
1397 + },
1398 + noResults: () => {
1399 + return Joomla.JText._('VBNOROOMSFOUND');
1400 + },
1401 + searching: () => {
1402 + return Joomla.JText._('VBO_SEARCHING');
1403 + },
1404 + },
1405 + ajax: {
1406 + delay: 350,
1407 + url: "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=bookings.rooms_search'); ?>",
1408 + dataType: 'json',
1409 + },
1410 + templateResult: (element) => {
1411 + if (!element.img) {
1412 + return element.text;
1413 + }
1414 + return jQuery('<span class="vbo-sel2-element-img"><img src="' + element.img + '" /> <span>' + element.text + '</span></span>');
1415 + },
1416 + });
1417 +
1418 + // when document is ready, load stats for this widget's instance
1419 + vboWidgetFinanceLoad('<?php echo $wrapper_id; ?>');
1420 +
1421 + });
1422 +
1423 + </script>
1424 +
1425 + <?php
1426 + }
1427 +
1428 + /**
1429 + * Returns an array with the minimum and maximum dates booked.
1430 + *
1431 + * @return array to be used with list() to get the min/max stay date timestamps.
1432 + */
1433 + protected function getMinDatesFinance()
1434 + {
1435 + $dbo = JFactory::getDbo();
1436 +
1437 + $mindate = null;
1438 + $maxdate = null;
1439 +
1440 + $q = "SELECT MIN(`checkin`) AS `mindate`, MAX(`checkout`) AS `maxdate` FROM `#__vikbooking_orders`
1441 + WHERE `status` = 'confirmed' AND `closure` = 0";
1442 + $dbo->setQuery($q);
1443 + $data = $dbo->loadAssoc();
1444 + if ($data) {
1445 + if (!empty($data['mindate']) && !empty($data['maxdate'])) {
1446 + $mindate = $data['mindate'];
1447 + $maxdate = $data['maxdate'];
1448 + }
1449 + }
1450 +
1451 + return [$mindate, $maxdate];
1452 + }
1453 +
1454 + /**
1455 + * Calculates the new dates interval for navigation according to step.
1456 + * Internal method for this widget only.
1457 + *
1458 + * @param int $from_ts current from date timestamp.
1459 + * @param int $to_ts current end date timestamp.
1460 + * @param string $step the navigation step to take (week, month, quarter or booking_dates).
1461 + * @param int $date_dir less than 0 backward nav, more than 0 forward.
1462 + *
1463 + * @return array the new from and end date timestamps, plus the step name.
1464 + */
1465 + protected function calcDatesNavigation($from_ts, $to_ts, $step, $date_dir)
1466 + {
1467 + if (empty($from_ts) || (!($date_dir > 0) && !($date_dir < 0))) {
1468 + return [$from_ts, $to_ts, ''];
1469 + }
1470 +
1471 + // start/end date information
1472 + $from_info = getdate($from_ts);
1473 + $to_info = getdate($to_ts);
1474 +
1475 + // next or prev day (booking_dates)
1476 + if (!strcasecmp($step, 'booking_dates')) {
1477 + if ($date_dir > 0) {
1478 + // forward dates navigation
1479 + $from_ts = mktime(0, 0, 0, $to_info['mon'], ($to_info['mday'] + 1), $to_info['year']);
1480 + $to_ts = mktime(23, 59, 59, $to_info['mon'], ($to_info['mday'] + 1), $to_info['year']);
1481 + } elseif ($date_dir < 0) {
1482 + // backward dates navigation
1483 + $from_ts = mktime(0, 0, 0, $from_info['mon'], ($from_info['mday'] - 1), $from_info['year']);
1484 + $to_ts = mktime(23, 59, 59, $from_info['mon'], ($from_info['mday'] - 1), $from_info['year']);
1485 + }
1486 +
1487 + return [$from_ts, $to_ts, JText::translate('VBPCHOOSEBUSYORDATE')];
1488 + }
1489 +
1490 + // next or prev week
1491 + if ($step == 'week') {
1492 + if ($date_dir > 0) {
1493 + // forward dates navigation
1494 + $from_ts = $to_ts;
1495 + $to_ts = strtotime("+1 week", $to_ts);
1496 + } elseif ($date_dir < 0) {
1497 + // backward dates navigation
1498 + $to_ts = $from_ts;
1499 + $from_ts = strtotime("-1 week", $from_ts);
1500 + }
1501 +
1502 + return [$from_ts, $to_ts, JText::translate('VBOWEEK')];
1503 + }
1504 +
1505 + // next or prev month
1506 + if ($step == 'month') {
1507 + if ($date_dir > 0) {
1508 + // forward dates navigation
1509 + $from_ts = mktime(0, 0, 0, ($from_info['mon'] + 1), 1, $from_info['year']);
1510 + $to_ts = mktime(0, 0, 0, ($from_info['mon'] + 1), date('t', $from_ts), $from_info['year']);
1511 + } elseif ($date_dir < 0) {
1512 + // backward dates navigation
1513 + $from_ts = mktime(0, 0, 0, ($from_info['mon'] - 1), 1, $from_info['year']);
1514 + $to_ts = mktime(0, 0, 0, ($from_info['mon'] - 1), date('t', $from_ts), $from_info['year']);
1515 + }
1516 +
1517 + return [$from_ts, $to_ts, JText::translate('VBPVIEWRESTRICTIONSTWO')];
1518 + }
1519 +
1520 + // next or prev year (keep the same exact dates)
1521 + if ($step == 'year') {
1522 + if ($date_dir > 0) {
1523 + // forward dates navigation
1524 + $from_ts = mktime(0, 0, 0, $from_info['mon'], $from_info['mday'], ($from_info['year'] + 1));
1525 + $to_ts = mktime(0, 0, 0, $to_info['mon'], $to_info['mday'], ($to_info['year'] + 1));
1526 + } elseif ($date_dir < 0) {
1527 + // backward dates navigation
1528 + $from_ts = mktime(0, 0, 0, $from_info['mon'], $from_info['mday'], ($from_info['year'] - 1));
1529 + $to_ts = mktime(0, 0, 0, $to_info['mon'], $to_info['mday'], ($to_info['year'] - 1));
1530 + }
1531 +
1532 + return [$from_ts, $to_ts, JText::translate('VBCONFIGSEARCHPMAXDATEYEARS')];
1533 + }
1534 +
1535 + // next or prev quarter by default
1536 + if ($date_dir > 0) {
1537 + // forward dates navigation
1538 + $from_ts = mktime(0, 0, 0, ($from_info['mon'] + 3), 1, $from_info['year']);
1539 + $end_ts = mktime(0, 0, 0, ($from_info['mon'] + 5), 1, $from_info['year']);
1540 + $to_ts = mktime(0, 0, 0, ($from_info['mon'] + 5), date('t', $end_ts), $from_info['year']);
1541 + } elseif ($date_dir < 0) {
1542 + // backward dates navigation
1543 + $from_ts = mktime(0, 0, 0, ($from_info['mon'] - 3), 1, $from_info['year']);
1544 + $end_ts = mktime(0, 0, 0, ($from_info['mon'] - 1), 1, $from_info['year']);
1545 + $to_ts = mktime(0, 0, 0, ($from_info['mon'] - 1), date('t', $end_ts), $from_info['year']);
1546 + }
1547 +
1548 + return [$from_ts, $to_ts, JText::translate('VBO_QUARTER')];
1549 + }
1550 +
1551 + /**
1552 + * Parse an interval of date timestamps into a readable interval of dates according to step.
1553 + * Internal method for this widget only.
1554 + *
1555 + * @param int $from_ts current from date timestamp.
1556 + * @param int $to_ts current end date timestamp.
1557 + * @param string $vbo_df the date format in VBO.
1558 + * @param string $step the navigation step to take (week, month or quarter).
1559 + *
1560 + * @return array the formatted from and to date interval strings.
1561 + */
1562 + protected function parseReadableDateInterval($from_ts, $to_ts, $vbo_df, $step)
1563 + {
1564 + $from_info = getdate($from_ts);
1565 + $to_info = getdate($to_ts);
1566 + $format_from_date = date($vbo_df, $from_ts);
1567 + $format_to_date = date($vbo_df, $to_ts);
1568 +
1569 + if ($step == 'week') {
1570 + // include day of week
1571 + $format_from_date = VikBooking::sayWeekDay($from_info['wday']) . ', ' . $format_from_date;
1572 + $format_to_date = VikBooking::sayWeekDay($to_info['wday']) . ', ' . $format_to_date;
1573 + } elseif ($step == 'month') {
1574 + // check if it's a full month to say its name
1575 + if ($from_info['mon'] == $to_info['mon'] && $from_info['year'] == $to_info['year']) {
1576 + if ($from_info['mday'] == 1 && $to_info['mday'] == date('t', $to_ts)) {
1577 + $format_from_date = VikBooking::sayMonth($from_info['mon']) . ' ' . $from_info['year'];
1578 + $format_to_date = VikBooking::sayMonth($to_info['mon']) . ' ' . $to_info['year'];
1579 + }
1580 + }
1581 + } elseif ($step == 'quarter') {
1582 + // check if it's a full quarter
1583 + if ($from_info['mday'] == 1 && $to_info['mday'] == date('t', $to_ts)) {
1584 + $format_from_date = VikBooking::sayMonth($from_info['mon']) . ' ' . $from_info['year'];
1585 + $format_to_date = VikBooking::sayMonth($to_info['mon']) . ' ' . $to_info['year'];
1586 + }
1587 + }
1588 +
1589 + return [$format_from_date, $format_to_date];
1590 + }
1591 +
1592 + /**
1593 + * Sets up the necessary script to load the welcome message for the user.
1594 + * On WordPress this method may run outside Vik Booking, and so Javascript,
1595 + * may not be able to access language definitions from the DOM.
1596 + *
1597 + * @return bool true if welcome was set up for the user, or false.
1598 + */
1599 + protected function setupWelcomeStats()
1600 + {
1601 + // avoid welcome stats on certain pages
1602 + if (VBOPlatformDetection::isWordPress()) {
1603 + global $pagenow;
1604 + $skip_pages = ['update.php', 'plugins.php', 'plugin-install.php'];
1605 + if (isset($pagenow) && in_array($pagenow, $skip_pages)) {
1606 + return false;
1607 + }
1608 + }
1609 +
1610 + $admin_user = JFactory::getUser();
1611 + $admin_user_name = $admin_user->name;
1612 +
1613 + if (!$admin_user_name || !$admin_user->authorise('core.vbo.bookings', 'com_vikbooking')) {
1614 + // user not authorized
1615 + return false;
1616 + }
1617 +
1618 + // get yesterday's date
1619 + $yesterday_info = getdate(strtotime('yesterday'));
1620 + $yesterday_read = implode(' ', [VikBooking::sayWeekDay($yesterday_info['wday'], true), $yesterday_info['mday'], VikBooking::sayMonth($yesterday_info['mon'], true)]);
1621 + $yesterday_read = htmlspecialchars($yesterday_read);
1622 + $yesterday = date('Y-m-d', $yesterday_info[0]);
1623 +
1624 + // the payload for getting the welcome stats for yesterday
1625 + $payload = [
1626 + 'fromdate' => $yesterday,
1627 + 'todate' => $yesterday,
1628 + 'type' => 'booking_dates',
1629 + ];
1630 +
1631 + // check if we are inside Vik Booking
1632 + $in_vbo = (int)(JFactory::getApplication()->input->get('option', '') === 'com_vikbooking');
1633 + $vbo_uri = VBOPlatformDetection::isWordPress() ? 'admin.php?page=vikbooking' : 'index.php?option=com_vikbooking';
1634 + if (!$in_vbo && VBOPlatformDetection::isWordPress()) {
1635 + // append query string value to render the admin widget
1636 + $q_cmds = [
1637 + 'load_widget' => $this->widgetId,
1638 + 'multitask_data' => $payload,
1639 + ];
1640 + $vbo_uri .= '&' . http_build_query($q_cmds);
1641 + }
1642 +
1643 + // build data (the JS code may not be able to access script language definitions if running outside Vik Booking)
1644 + $aj_endpoint = $this->getExecWidgetAjaxUri();
1645 + $widget_id = $this->getIdentifier();
1646 + $website_logo = $this->getIconUrl();
1647 + $website_logo = $website_logo ? $website_logo : '';
1648 + $greetings = VikBooking::strTrimLiteral(htmlspecialchars(JText::sprintf('VBO_WELCOME_ADMIN_USER', $admin_user_name)));
1649 + $greetings_ms = VikBooking::strTrimLiteral(htmlspecialchars(JText::translate('VBO_BOOKINGS_YESTERDAY')));
1650 + $modal_title = VikBooking::strTrimLiteral(htmlspecialchars(JText::translate('VBOYESTERDAY') . ', ' . $yesterday_read));
1651 + $payload_json = json_encode($payload);
1652 + $sugg_notifs_btn = '<button class="vbo-suggest-notifications-btn" type="button"><i class="' . VikBookingIcons::i('bell', 'can-shake') . '"></i></button>';
1653 +
1654 + JFactory::getDocument()->addScriptDeclaration(
1655 +<<<JS
1656 +(function($) {
1657 + 'use strict';
1658 +
1659 + $(function() {
1660 + try {
1661 + if (!VBOCore.storageSupported() || VBOCore.storageGetItem('vbo_finance_last_dt') == '$yesterday') {
1662 + return false;
1663 + }
1664 + } catch(e) {
1665 + return false;
1666 + }
1667 +
1668 + let call_method = 'getWelcomeStats';
1669 + VBOCore.doAjax(
1670 + "$aj_endpoint",
1671 + {
1672 + widget_id: "$widget_id",
1673 + call: call_method,
1674 + return: 1,
1675 + tmpl: "component"
1676 + },
1677 + (response) => {
1678 + try {
1679 + var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
1680 + if (!obj_res.hasOwnProperty(call_method)) {
1681 + throw new Error('Unexpected JSON response');
1682 + }
1683 +
1684 + VBOCore.storageSetItem('vbo_finance_last_dt', '$yesterday');
1685 +
1686 + if (!obj_res[call_method].hasOwnProperty('tot_bookings') || !obj_res[call_method]['tot_bookings']) {
1687 + return;
1688 + }
1689 +
1690 + let vbo_notif_click_handler = 'VBOCore.handleGoto';
1691 + if ($in_vbo) {
1692 + vbo_notif_click_handler = () => {
1693 + try {
1694 + let yesterday_modal = VBOCore.displayModal({
1695 + suffix: 'finance-welcome',
1696 + extra_class: 'vbo-modal-rounded vbo-modal-tall vbo-modal-nofooter',
1697 + title: '<span class="vbo-suggest-notifications-wrap"><span>$modal_title</span>' + (VBOCore.notificationsEnabled() === false ? '$sugg_notifs_btn' : '') + '</span>',
1698 + dismiss_event: 'vbo-dismiss-modal-finance-welcome',
1699 + loading_event: 'vbo-loading-modal-finance-welcome',
1700 + });
1701 +
1702 + VBOCore.suggestNotifications('.vbo-suggest-notifications-btn');
1703 + VBOCore.emitEvent('vbo-loading-modal-finance-welcome');
1704 +
1705 + VBOCore.renderAdminWidget('$this->widgetId', $payload_json).then((content) => {
1706 + VBOCore.emitEvent('vbo-loading-modal-finance-welcome');
1707 + yesterday_modal.append(content);
1708 + }).catch((error) => {
1709 + VBOCore.emitEvent('vbo-dismiss-modal-finance-welcome');
1710 + alert(error);
1711 + });
1712 + } catch(e) {
1713 + document.location.href = '$vbo_uri';
1714 + }
1715 + };
1716 + }
1717 +
1718 + setTimeout(() => {
1719 + VBOCore.dispatchNotification({
1720 + title: '$greetings',
1721 + message: ('$greetings_ms').replace('%d', obj_res[call_method]['tot_bookings']),
1722 + icon: '$website_logo',
1723 + onclick: vbo_notif_click_handler,
1724 + gotourl: '$vbo_uri',
1725 + });
1726 + }, 500);
1727 + } catch(e) {
1728 + console.error(e);
1729 + }
1730 + },
1731 + (error) => {
1732 + console.error(error.responseText);
1733 + }
1734 + );
1735 + });
1736 +})(jQuery);
1737 +JS
1738 + );
1739 +
1740 + return true;
1741 + }
1742 +
1743 + /**
1744 + * Returns the URL to the default site logo.
1745 + *
1746 + * @return string|null
1747 + */
1748 + protected function getIconUrl()
1749 + {
1750 + $config = VBOFactory::getConfig();
1751 +
1752 + // back-end custom logo
1753 + $use_logo = $config->get('backlogo');
1754 + if (empty($use_logo) || !strcasecmp($use_logo, 'vikbooking.png')) {
1755 + // fallback to company (site) logo
1756 + $use_logo = $config->get('sitelogo');
1757 + }
1758 +
1759 + if (!empty($use_logo) && strcasecmp($use_logo, 'vikbooking.png')) {
1760 + // uploaded logo found
1761 + $use_logo = VBO_ADMIN_URI . 'resources/' . $use_logo;
1762 + } else {
1763 + $use_logo = null;
1764 + }
1765 +
1766 + return $use_logo;
1767 + }
1768 +}