PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.6.7
VikBooking Hotel Booking Engine & PMS v1.6.7
1.8.16 1.8.15 1.8.14 1.8.13 1.8.12 1.8.11 1.8.10 1.8.9 1.8.6 1.8.7 1.8.8 trunk 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 All 37 releases
vikbooking / admin / helpers / widgets / finance.php

finance.php in VikBooking Hotel Booking Engine & PMS 1.6.7, at admin/helpers/widgets/finance.php

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