PluginProbe
VikBooking Hotel Booking Engine & PMS / trunk
VikBooking Hotel Booking Engine & PMS vtrunk
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 1.7.3 All 36 releases
vikbooking / admin / helpers / widgets / rates_flow.php

rates_flow.php in VikBooking Hotel Booking Engine & PMS trunk, at admin/helpers/widgets/rates_flow.php

1,040 lines 38.0 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 "rates flow".
15 *
16 * @since 1.15.0 (J) - 1.5.0 (WP)
17 */
18 class VikBookingAdminWidgetRatesFlow extends VikBookingAdminWidget
19 {
20 /**
21 * The instance counter of this widget. Since we do not load individual parameters
22 * for each widget's instance, we use a static counter to determine its settings.
23 *
24 * @var int
25 */
26 protected static $instance_counter = -1;
27
28 /**
29 * Default number of rates flow alterations per page.
30 *
31 * @var int
32 */
33 protected $alterations_per_page = 2;
34
35 /**
36 * Suggested limit of records per page.
37 *
38 * @var int
39 */
40 protected $lim_page_records = 8;
41
42 /**
43 * Class constructor will define the widget name and identifier.
44 */
45 public function __construct()
46 {
47 // call parent constructor
48 parent::__construct();
49
50 $this->widgetName = JText::translate('VBOREPORTRATESFLOW');
51 $this->widgetDescr = JText::translate('VBO_W_RATESFLOW_DESCR');
52 $this->widgetId = basename(__FILE__, '.php');
53
54 // define widget and icon and style name
55 $this->widgetIcon = '<i class="' . VikBookingIcons::i('chart-line') . '"></i>';
56 $this->widgetStyleName = 'violet';
57 }
58
59 /**
60 * Custom method for this widget only to load the rates flow records.
61 * The method is called by the admin controller through an AJAX request.
62 * The visibility should be public, it should not exit the process, and
63 * any content sent to output will be returned to the AJAX response.
64 * In this case we return an array because this method requires "return":1.
65 *
66 * It's the actual rendering of the widget which also allows navigation.
67 */
68 public function loadRatesFlowRecords()
69 {
70 $offset = VikRequest::getInt('offset', 0, 'request');
71 $length = VikRequest::getInt('length', $this->alterations_per_page, 'request');
72 $wrapper = VikRequest::getString('wrapper', '', 'request');
73
74 $fromdate = VikRequest::getString('fromdate', '', 'request');
75 $todate = VikRequest::getString('todate', '', 'request');
76 $step = VikRequest::getString('step', 'quarter', 'request');
77 $date_dir = VikRequest::getInt('date_dir', 0, 'request');
78 if (empty($fromdate)) {
79 // default to current quarter (3 full months)
80 $now_info = getdate();
81 $end_info = getdate(mktime(0, 0, 0, ($now_info['mon'] + 2), 1, $now_info['year']));
82 $to_ts = mktime(23, 59, 59, $end_info['mon'], date('t', $end_info[0]), $end_info['year']);
83 $fromdate = date('Y-m-d', mktime(0, 0, 0, $now_info['mon'], 1, $now_info['year']));
84 $todate = date('Y-m-d', $to_ts);
85 }
86
87 // convert dates to timestamps to support custom date formats
88 $from_ts = VikBooking::getDateTimestamp($fromdate);
89 $to_ts = VikBooking::getDateTimestamp($todate, 23, 59, 59);
90
91 // check dates navigation and step
92 $step_name = '';
93 if ($date_dir > 0 || $date_dir < 0) {
94 // calculate forward or backward dates for navigation
95 list($from_ts, $to_ts, $step_name) = $this->calcDatesNavigation($from_ts, $to_ts, $step, $date_dir);
96 }
97
98 // convert final dates to Y-m-d
99 $fromdate = date('Y-m-d', $from_ts);
100 $todate = date('Y-m-d', $to_ts);
101
102 // get rates flow report instance
103 $report = VikBooking::getReportInstance('rates_flow');
104 if (!$report) {
105 // do nothing
106 return;
107 }
108
109 // get VBO date format
110 $vbo_df = $report->getDateFormat();
111
112 // set report options
113 $report->setReportOptions(array(
114 'fetch' => 'alterations',
115 'krsort' => 'created_on',
116 'krorder' => 'DESC',
117 'fromdate' => $fromdate,
118 'todate' => $todate,
119 'lim' => $length,
120 'limstart' => $offset,
121 ));
122
123 // generate report data
124 $alterations = $report->getReportValues();
125 $alterations = !is_array($alterations) ? array() : $alterations;
126
127 // grab report columns for paging
128 $columns = $report->getColumnsValues();
129
130 // check next page and next offset
131 $has_next_page = (isset($columns['paging']) && !empty($columns['paging']['has_next_page']));
132 $next_offset = isset($columns['paging']) && !empty($columns['paging']['limstart']) ? $columns['paging']['limstart'] : 0;
133 $current_page = isset($columns['paging']) && !empty($columns['paging']['page_num']) ? $columns['paging']['page_num'] : 1;
134
135 // check festivities, by getting the next ones
136 $fests = VikBooking::getFestivitiesInstance();
137 $next_fests = $fests->loadFestDates($fromdate, $todate);
138
139 // start output buffering
140 ob_start();
141
142 // error, warning or info messages should go on top
143 if (strlen($report->getError())) {
144 ?>
145 <p class="err"><?php echo $report->getError(); ?></p>
146 <?php
147 }
148 if (strlen($report->getWarning())) {
149 ?>
150 <p class="warn"><?php echo $report->getWarning(); ?></p>
151 <?php
152 }
153 if (!count($alterations)) {
154 ?>
155 <p class="info"><?php echo JText::translate('VBONORESULTS'); ?></p>
156 <?php
157 }
158
159 // loop through all rates flow records
160 foreach ($alterations as $rflow) {
161 // attempt to get the festivity name for the updated dates
162 $day_from_ts = strtotime($rflow['day_from']['value']);
163 $day_to_ts = strtotime($rflow['day_to']['value']);
164 $fest_name = $this->findDatesFestName($next_fests, $day_from_ts, $day_to_ts);
165
166 $restr_infos = array();
167 if (is_object($rflow['data']['value']) && isset($rflow['data']['value']->Restrictions)) {
168 $restr_obj = $rflow['data']['value']->Restrictions;
169 if (isset($restr_obj->minLOS) && (int)$restr_obj->minLOS > 1) {
170 array_push($restr_infos, JText::translate('VBOMINIMUMSTAY') . ': ' . $restr_obj->minLOS);
171 }
172 if (isset($restr_obj->cta)) {
173 if ((is_bool($restr_obj->cta) && $restr_obj->cta === true) || (is_string($restr_obj->cta) && !strcasecmp($restr_obj->cta, 'true'))) {
174 array_push($restr_infos, 'CTA');
175 }
176 }
177 if (isset($restr_obj->ctd)) {
178 if ((is_bool($restr_obj->ctd) && $restr_obj->ctd === true) || (is_string($restr_obj->ctd) && !strcasecmp($restr_obj->ctd, 'true'))) {
179 array_push($restr_infos, 'CTD');
180 }
181 }
182 }
183
184 ?>
185 <div class="vbo-widget-ratesflow-record" data-recordid="<?php echo $rflow['id']['value']; ?>">
186 <div class="vbo-widget-ratesflow-avatar">
187 <?php
188 if (!empty($rflow['channel_id']['_logo'])) {
189 // channel logo has got the highest priority
190 ?>
191 <img class="vbo-widget-ratesflow-avatar-profile" src="<?php echo $rflow['channel_id']['_logo']; ?>" />
192 <?php
193 } else {
194 // we use an icon as fallback
195 VikBookingIcons::e('hotel', 'vbo-widget-ratesflow-avatar-icon');
196 }
197 ?>
198 </div>
199 <div class="vbo-widget-ratesflow-content">
200 <div class="vbo-widget-ratesflow-content-head">
201 <div class="vbo-widget-ratesflow-content-info-details">
202 <h4><?php echo $rflow['channel_id']['display_value']; ?></h4>
203 <div class="vbo-widget-ratesflow-content-info-dates">
204 <span class="vbo-widget-ratesflow-date-from"><?php echo $rflow['day_from']['display_value']; ?></span>
205 <?php
206 if ($rflow['day_from']['value'] != $rflow['day_to']['value']) {
207 ?>
208 <span class="vbo-widget-ratesflow-date-sep">-</span>
209 <span class="vbo-widget-ratesflow-date-to"><?php echo $rflow['day_to']['display_value']; ?></span>
210 <?php
211 }
212 if (!empty($fest_name)) {
213 ?>
214 <span class="vbo-widget-ratesflow-date-fest"><?php echo $fest_name; ?></span>
215 <?php
216 }
217 ?>
218 </div>
219 </div>
220 <div class="vbo-widget-ratesflow-content-info-rates">
221 <?php
222 $higher_rates = null;
223 $higher_class = '';
224 $higher_icon = 'equals';
225 $higher_pcfee = 0;
226 if (!empty($rflow['base_fee']) && !empty($rflow['base_fee']['value'])) {
227 if ($rflow['base_fee']['value'] < $rflow['nightly_fee']['value']) {
228 $higher_rates = true;
229 $higher_class = ' vbo-widget-ratesflow-rate-changes-higher';
230 $higher_icon = 'sort-up';
231 $higher_pcfee = round((($rflow['nightly_fee']['value'] * 100 / $rflow['base_fee']['value']) - 100), 2);
232 } elseif ($rflow['base_fee']['value'] > $rflow['nightly_fee']['value']) {
233 $higher_rates = false;
234 $higher_class = ' vbo-widget-ratesflow-rate-changes-lower';
235 $higher_icon = 'sort-down';
236 $higher_pcfee = round((($rflow['base_fee']['value'] * 100 / $rflow['nightly_fee']['value']) - 100), 2);
237 }
238 }
239 ?>
240 <div class="vbo-widget-ratesflow-nightly-rate">
241 <span><?php echo $rflow['nightly_fee']['display_value']; ?></span>
242 </div>
243 <div class="vbo-widget-ratesflow-rate-changes<?php echo $higher_class; ?>">
244 <?php
245 if (!empty($rflow['channel_alter']['value'])) {
246 ?>
247 <div class="vbo-widget-ratesflow-rate-alter">
248 <span><?php echo $rflow['channel_alter']['display_value']; ?></span>
249 </div>
250 <?php
251 }
252 ?>
253 <div class="vbo-widget-ratesflow-rate-growth">
254 <span><?php VikBookingIcons::e($higher_icon); ?> <?php echo $higher_pcfee; ?>%</span>
255 </div>
256 </div>
257 </div>
258 </div>
259 <div class="vbo-widget-ratesflow-content-info-msg">
260 <span class="vbo-widget-ratesflow-content-info-msg-room"><?php echo $rflow['vbo_room_id']['display_value']; ?></span>
261 <?php
262 if (count($restr_infos)) {
263 ?>
264 <span class="vbo-widget-ratesflow-content-info-msg-restr"><?php echo implode(', ', $restr_infos); ?></span>
265 <?php
266 }
267 ?>
268 <span class="vbo-widget-ratesflow-content-info-msg-cdate"><?php echo $rflow['created_on']['display_value']; ?></span>
269 </div>
270 </div>
271 </div>
272 <?php
273 }
274
275 // append navigation
276 ?>
277 <div class="vbo-widget-commands vbo-widget-commands-right">
278 <div class="vbo-widget-commands-main">
279 <?php
280 if ($offset > 0) {
281 // show backward navigation button
282 ?>
283 <div class="vbo-widget-command-chevron vbo-widget-command-prev">
284 <span class="vbo-widget-command-chevron-prev" onclick="vboWidgetRatesFlowNavigate('<?php echo $wrapper; ?>', -1);"><?php VikBookingIcons::e('chevron-left'); ?></span>
285 </div>
286 <?php
287 }
288 if ($has_next_page) {
289 // show forward navigation button
290 ?>
291 <div class="vbo-widget-command-chevron vbo-widget-command-next">
292 <span class="vbo-widget-command-chevron-next" onclick="vboWidgetRatesFlowNavigate('<?php echo $wrapper; ?>', 1);"><?php VikBookingIcons::e('chevron-right'); ?></span>
293 </div>
294 <?php
295 }
296 ?>
297 </div>
298 </div>
299 <?php
300
301 // get the HTML buffer
302 $html_content = ob_get_contents();
303 ob_end_clean();
304
305 // take care of the readable dates interval
306 list($format_from_date, $format_to_date) = $this->parseReadableDateInterval($from_ts, $to_ts, $vbo_df, $step);
307
308 // return an associative array of values
309 return array(
310 'html' => $html_content,
311 'fromdate' => $fromdate,
312 'f_fromdate' => $format_from_date,
313 'todate' => $todate,
314 'f_todate' => $format_to_date,
315 'step' => $step,
316 'step_name' => $step_name,
317 'tot_records' => count($alterations),
318 'next_page' => (int)$has_next_page,
319 'page_num' => (int)$current_page,
320 'offset' => $next_offset,
321 );
322 }
323
324 /**
325 * Preload the necessary CSS/JS assets.
326 *
327 * @return void
328 */
329 public function preload()
330 {
331 // load assets
332 $this->vbo_app->loadDatePicker();
333 }
334
335 /**
336 * Main method to invoke the widget. Contents will be loaded
337 * through AJAX requests, not via PHP when the page loads.
338 *
339 * @return void
340 */
341 public function render(?VBOMultitaskData $data = null)
342 {
343 // increase widget's instance counter
344 static::$instance_counter++;
345
346 // check whether the widget is being rendered via AJAX when adding it through the customizer
347 $is_ajax = $this->isAjaxRendering();
348
349 // generate a unique ID for the sticky notes wrapper instance
350 $wrapper_instance = !$is_ajax ? static::$instance_counter : rand();
351 $wrapper_id = 'vbo-widget-ratesflow-' . $wrapper_instance;
352
353 // get permissions
354 $vbo_auth_pricing = JFactory::getUser()->authorise('core.vbo.pricing', 'com_vikbooking');
355
356 // get rates flow report instance
357 $report = VikBooking::getReportInstance('rates_flow');
358 if (!$report || !$vbo_auth_pricing) {
359 // display nothing
360 return;
361 }
362
363 // get the total number of unique channels updated
364 $tot_channels_updated = $report->countRatesFlowChannels();
365 if ($tot_channels_updated > 0 && ($this->alterations_per_page * $tot_channels_updated) < $this->lim_page_records) {
366 $best_lim_page = floor($this->lim_page_records / $tot_channels_updated);
367 $this->alterations_per_page = $best_lim_page > $this->alterations_per_page ? $best_lim_page : $this->alterations_per_page;
368 }
369
370 // count average records per page
371 $avg_per_page = floor(($this->alterations_per_page + $this->lim_page_records) / 2);
372 $avg_per_page = $avg_per_page < $this->alterations_per_page ? $this->alterations_per_page : $avg_per_page;
373
374 // get minimum and maximum nights updated for dates filters
375 list($mindate, $maxdate) = $report->getMinDatesRatesFlow();
376 $mindate = empty($mindate) ? time() : $mindate;
377 $maxdate = empty($maxdate) ? $mindate : $maxdate;
378
379 // dates navigation preference
380 $cookie = JFactory::getApplication()->input->cookie;
381 $cookie_step = $cookie->get('vbo_widget_ratesflow_step', 'quarter', 'string');
382
383 // determine default period name and dates
384 $df = $report->getDateFormat();
385 $dtpicker_df = $report->getDateFormat('jui');
386 $now_info = getdate();
387 if ($cookie_step == 'weekend') {
388 // next weekend
389 $from_ts = strtotime("next Friday");
390 $to_ts = strtotime("next Sunday");
391 if ($to_ts < $from_ts) {
392 // this weekend
393 $from_ts = strtotime("last Friday");
394 }
395 $fromdate = date($df, $from_ts);
396 $todate = date($df, $to_ts);
397 $period_name = JText::translate('VBOWEEKND');
398 } elseif ($cookie_step == 'month') {
399 // current full month
400 $from_ts = mktime(0, 0, 0, $now_info['mon'], 1, $now_info['year']);
401 $fromdate = date($df, $from_ts);
402 $to_ts = mktime(23, 59, 59, $now_info['mon'], date('t', $now_info[0]), $now_info['year']);
403 $todate = date($df, $to_ts);
404 $period_name = JText::translate('VBPVIEWRESTRICTIONSTWO');
405 } else {
406 // default to next 3 (total) months from today's month
407 $from_ts = mktime(0, 0, 0, $now_info['mon'], 1, $now_info['year']);
408 $fromdate = date($df, $from_ts);
409 $end_info = getdate(mktime(0, 0, 0, ($now_info['mon'] + 2), 1, $now_info['year']));
410 $to_ts = mktime(23, 59, 59, $end_info['mon'], date('t', $end_info[0]), $end_info['year']);
411 $todate = date($df, $to_ts);
412 $period_name = JText::translate('VBO_QUARTER');
413 }
414
415 // take care of the readable dates interval
416 list($format_from_date, $format_to_date) = $this->parseReadableDateInterval($from_ts, $to_ts, $df, $cookie_step);
417 $interval_name = $format_from_date . ' - ' . $format_to_date;
418 if ($format_from_date == $format_to_date) {
419 $interval_name = $format_from_date;
420 }
421
422 ?>
423 <div id="<?php echo $wrapper_id; ?>" class="vbo-admin-widget-wrapper" data-instance="<?php echo $wrapper_instance; ?>" data-offset="0" data-nowoffset="0" data-prevoffset="-1" data-length="<?php echo $this->alterations_per_page; ?>">
424 <div class="vbo-admin-widget-head">
425 <div class="vbo-admin-widget-head-inline">
426 <h4><?php echo $this->widgetIcon; ?> <span><?php echo $this->widgetName; ?></span></h4>
427 <div class="vbo-admin-widget-head-commands">
428
429 <div class="vbo-reportwidget-commands">
430 <div class="vbo-reportwidget-commands-main">
431 <div class="vbo-reportwidget-command-dates">
432 <div class="vbo-reportwidget-period-name"><?php echo $period_name; ?></div>
433 <div class="vbo-reportwidget-period-date"><?php echo $interval_name; ?></div>
434 </div>
435 <div class="vbo-reportwidget-command-chevron vbo-reportwidget-command-prev">
436 <span class="vbo-widget-ratesflow-dt-prev" onclick="vboWidgetRatesFlowDatesNav('<?php echo $wrapper_id; ?>', -1);"><?php VikBookingIcons::e('chevron-left'); ?></span>
437 </div>
438 <div class="vbo-reportwidget-command-chevron vbo-reportwidget-command-next">
439 <span class="vbo-widget-ratesflow-dt-next" onclick="vboWidgetRatesFlowDatesNav('<?php echo $wrapper_id; ?>', 1);"><?php VikBookingIcons::e('chevron-right'); ?></span>
440 </div>
441 </div>
442 <div class="vbo-reportwidget-command-dots">
443 <span class="vbo-widget-command-togglefilters vbo-widget-ratesflow-togglefilters" onclick="vboWidgetRatesFlowToggleFilters('<?php echo $wrapper_id; ?>');"><?php VikBookingIcons::e('ellipsis-v'); ?></span>
444 </div>
445 </div>
446 <div class="vbo-reportwidget-filters">
447 <div class="vbo-reportwidget-filter">
448 <span class="vbo-reportwidget-datepicker">
449 <?php VikBookingIcons::e('calendar', 'vbo-widget-ratesflow-caltrigger'); ?>
450 <input type="text" class="vbo-ratesflow-dtpicker-from" value="<?php echo $fromdate; ?>" />
451 </span>
452 </div>
453 <div class="vbo-reportwidget-filter">
454 <span class="vbo-reportwidget-datepicker">
455 <?php VikBookingIcons::e('calendar', 'vbo-widget-ratesflow-caltrigger'); ?>
456 <input type="text" class="vbo-ratesflow-dtpicker-to" value="<?php echo $todate; ?>" />
457 </span>
458 </div>
459 <div class="vbo-reportwidget-filter">
460 <select class="vbo-ratesflow-period-nav" onchange="vboWidgetRatesFlowChangePeriod(this.value);">
461 <option value="weekend"<?php echo $cookie_step == 'weekend' ? ' selected="selected"' : ''; ?>><?php echo JText::translate('VBOWEEKND'); ?></option>
462 <option value="month"<?php echo $cookie_step == 'month' ? ' selected="selected"' : ''; ?>><?php echo JText::translate('VBPVIEWRESTRICTIONSTWO'); ?></option>
463 <option value="quarter"<?php echo $cookie_step == 'quarter' ? ' selected="selected"' : ''; ?>><?php echo JText::translate('VBO_QUARTER'); ?></option>
464 </select>
465 </div>
466 <div class="vbo-reportwidget-filter vbo-reportwidget-filter-confirm">
467 <button type="button" class="btn vbo-config-btn" onclick="vboWidgetRatesFlowChangeDates('<?php echo $wrapper_id; ?>');"><?php echo JText::translate('VBADMINNOTESUPD'); ?></button>
468 </div>
469 </div>
470
471 </div>
472 </div>
473 </div>
474 <div class="vbo-widget-ratesflow-wrap">
475 <div class="vbo-widget-ratesflow-inner">
476 <div class="vbo-widget-ratesflow-list">
477 <?php
478 for ($i = 0; $i < $avg_per_page; $i++) {
479 ?>
480 <div class="vbo-dashboard-guest-activity vbo-dashboard-guest-activity-skeleton">
481 <div class="vbo-dashboard-guest-activity-avatar">
482 <div class="vbo-skeleton-loading vbo-skeleton-loading-avatar"></div>
483 </div>
484 <div class="vbo-dashboard-guest-activity-content">
485 <div class="vbo-dashboard-guest-activity-content-head">
486 <div class="vbo-skeleton-loading vbo-skeleton-loading-title"></div>
487 </div>
488 <div class="vbo-dashboard-guest-activity-content-info-msg">
489 <div class="vbo-skeleton-loading vbo-skeleton-loading-content"></div>
490 </div>
491 </div>
492 </div>
493 <?php
494 }
495 ?>
496 </div>
497 </div>
498 </div>
499 </div>
500 <?php
501
502 if (static::$instance_counter === 0 || $is_ajax) {
503 /**
504 * Print the JS code only once for all instances of this widget.
505 * The real rendering is made through AJAX, not when the page loads.
506 */
507 ?>
508 <script type="text/javascript">
509
510 /**
511 * Display the loading skeletons.
512 */
513 function vboWidgetRatesFlowSkeletons(wrapper) {
514 var widget_instance = jQuery('#' + wrapper);
515 if (!widget_instance.length) {
516 return false;
517 }
518 widget_instance.find('.vbo-widget-ratesflow-list').html('');
519 for (var i = 0; i < <?php echo $avg_per_page; ?>; i++) {
520 var skeleton = '';
521 skeleton += '<div class="vbo-dashboard-guest-activity vbo-dashboard-guest-activity-skeleton">';
522 skeleton += ' <div class="vbo-dashboard-guest-activity-avatar">';
523 skeleton += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-avatar"></div>';
524 skeleton += ' </div>';
525 skeleton += ' <div class="vbo-dashboard-guest-activity-content">';
526 skeleton += ' <div class="vbo-dashboard-guest-activity-content-head">';
527 skeleton += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-title"></div>';
528 skeleton += ' </div>';
529 skeleton += ' <div class="vbo-dashboard-guest-activity-content-info-msg">';
530 skeleton += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-content"></div>';
531 skeleton += ' </div>';
532 skeleton += ' </div>';
533 skeleton += '</div>';
534 // append skeleton
535 jQuery(skeleton).appendTo(widget_instance.find('.vbo-widget-ratesflow-list'));
536 }
537 }
538
539 /**
540 * Perform the request to load the rates flow records.
541 */
542 function vboWidgetRatesFlowLoad(wrapper, dates_direction) {
543 var widget_instance = jQuery('#' + wrapper);
544 if (!widget_instance.length) {
545 return false;
546 }
547
548 // check if a navigation of dates was requested (0 = no dates nav)
549 if (typeof dates_direction === 'undefined') {
550 dates_direction = 0;
551 }
552
553 // get vars for making the request
554 var current_offset = parseInt(widget_instance.attr('data-offset'));
555 var length_per_page = parseInt(widget_instance.attr('data-length'));
556 var from_date = widget_instance.find('.vbo-ratesflow-dtpicker-from').val();
557 var to_date = widget_instance.find('.vbo-ratesflow-dtpicker-to').val();
558 var dates_step = widget_instance.find('.vbo-ratesflow-period-nav').val();
559
560 // the widget method to call
561 var call_method = 'loadRatesFlowRecords';
562
563 // make a request to load the rates flow records
564 VBOCore.doAjax(
565 "<?php echo $this->getExecWidgetAjaxUri(); ?>",
566 {
567 widget_id: "<?php echo $this->getIdentifier(); ?>",
568 call: call_method,
569 return: 1,
570 offset: current_offset,
571 length: length_per_page,
572 fromdate: from_date,
573 todate: to_date,
574 step: dates_step,
575 date_dir: dates_direction,
576 wrapper: wrapper,
577 tmpl: "component"
578 },
579 function(response) {
580 try {
581 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
582 if (!obj_res.hasOwnProperty(call_method)) {
583 console.error('Unexpected JSON response', obj_res);
584 return false;
585 }
586
587 // adjust current offset for backward navigation
588 if (obj_res[call_method]['page_num'] && obj_res[call_method]['page_num'] > 1) {
589 widget_instance.attr('data-nowoffset', current_offset);
590 } else {
591 // first page
592 widget_instance.attr('data-nowoffset', '0');
593 }
594
595 // replace HTML with new rates flow records
596 widget_instance.find('.vbo-widget-ratesflow-list').html(obj_res[call_method]['html']);
597
598 // check results
599 var tot_records = obj_res[call_method]['tot_records'] || 0;
600 var new_offset = obj_res[call_method]['offset'] || 0;
601 if (tot_records) {
602 // set new offset for pagination next nav
603 widget_instance.attr('data-offset', new_offset);
604 }
605
606 if (dates_direction > 0 || dates_direction < 0) {
607 // set new dates calculated
608 try {
609 // construct a solid date object without using the raw Y-m-d string as only argument
610 var dfrom_parts = obj_res[call_method]['fromdate'].split('-');
611 var dfrom_obj = new Date(parseInt(dfrom_parts[0]), (parseInt(dfrom_parts[1]) - 1), parseInt(dfrom_parts[2]), 0, 0, 0);
612 widget_instance.find('.vbo-ratesflow-dtpicker-from').datepicker('setDate', dfrom_obj);
613
614 // restore the original minimum and maximum dates for the "to date" calendar to avoid issues setting a date
615 widget_instance.find('.vbo-ratesflow-dtpicker-to').datepicker('option', {
616 minDate: "<?php echo date($df, $mindate); ?>",
617 maxDate: "<?php echo date($df, $maxdate); ?>",
618 });
619
620 // do the same for the to date by using a precise date object instance
621 var dto_parts = obj_res[call_method]['todate'].split('-');
622 var dto_obj = new Date(parseInt(dto_parts[0]), (parseInt(dto_parts[1]) - 1), parseInt(dto_parts[2]), 0, 0, 0);
623 widget_instance.find('.vbo-ratesflow-dtpicker-to').datepicker('setDate', dto_obj);
624 } catch(e) {
625 // just log the error
626 console.error(e);
627 }
628 }
629
630 // update step name
631 if (obj_res[call_method]['step_name'] && obj_res[call_method]['step_name'].length) {
632 widget_instance.find('.vbo-reportwidget-period-name').text(obj_res[call_method]['step_name']);
633 }
634
635 // update readable dates interval
636 var readable_period = obj_res[call_method]['f_fromdate'] + ' - ' + obj_res[call_method]['f_todate'];
637 if (obj_res[call_method]['f_fromdate'] == obj_res[call_method]['f_todate']) {
638 // a full month does not need an interval
639 readable_period = obj_res[call_method]['f_fromdate'];
640 }
641 widget_instance.find('.vbo-reportwidget-period-date').text(readable_period);
642
643 if (!isNaN(tot_records) && parseInt(tot_records) < 1) {
644 // no results can indicate the offset is invalid or too high
645 if (!isNaN(new_offset) && parseInt(new_offset) > 0) {
646 // reset offset to 0
647 widget_instance.attr('data-offset', 0);
648 // show loading skeletons
649 vboWidgetRatesFlowSkeletons(wrapper);
650 // reload the first page
651 vboWidgetRatesFlowLoad(wrapper);
652 }
653 }
654 } catch(err) {
655 console.error('could not parse JSON response', err, response);
656 }
657 },
658 function(error) {
659 // remove the skeleton loading
660 widget_instance.find('.vbo-widget-ratesflow-list').find('.vbo-dashboard-guest-activity-skeleton').remove();
661 console.error(error);
662 }
663 );
664 }
665
666 /**
667 * Navigate between the various pages of the rates flow records.
668 */
669 function vboWidgetRatesFlowNavigate(wrapper, direction) {
670 var widget_instance = jQuery('#' + wrapper);
671 if (!widget_instance.length) {
672 return false;
673 }
674
675 // show loading skeletons
676 vboWidgetRatesFlowSkeletons(wrapper);
677
678 var instance_val = widget_instance.attr('data-instance');
679
680 // check direction and update offsets for nav
681 if (direction > 0) {
682 // navigate forward
683 var current_offset = widget_instance.attr('data-nowoffset');
684 widget_instance.attr('data-prevoffset', current_offset);
685 // push history
686 try {
687 vbo_w_ratesflow_history[instance_val].unshift(current_offset);
688 } catch(e) {
689 // do nothing
690 }
691 } else {
692 // navigate backward
693 var prev_offset = widget_instance.attr('data-prevoffset');
694 // check and update history
695 try {
696 var last_offset = vbo_w_ratesflow_history[instance_val].shift();
697 if (typeof last_offset !== 'undefined') {
698 widget_instance.attr('data-prevoffset', last_offset);
699 prev_offset = last_offset;
700 }
701 } catch(e) {
702 // do nothing
703 }
704 prev_offset = prev_offset >= 0 ? prev_offset : 0;
705 widget_instance.attr('data-offset', prev_offset);
706 }
707
708 // launch navigation
709 vboWidgetRatesFlowLoad(wrapper);
710 }
711
712 /**
713 * Navigate between the date steps and load the rates flow records.
714 */
715 function vboWidgetRatesFlowDatesNav(wrapper, direction) {
716 var widget_instance = jQuery('#' + wrapper);
717 if (!widget_instance.length) {
718 return false;
719 }
720
721 // show loading skeletons
722 vboWidgetRatesFlowSkeletons(wrapper);
723
724 // reset offsets to the first state
725 widget_instance.attr('data-offset', '0');
726 widget_instance.attr('data-nowoffset', '0');
727 widget_instance.attr('data-prevoffset', '-1');
728
729 // launch dates navigation and load records
730 vboWidgetRatesFlowLoad(wrapper, direction);
731 }
732
733 /**
734 * Load rates flow records for the selected dates.
735 */
736 function vboWidgetRatesFlowChangeDates(wrapper) {
737 var widget_instance = jQuery('#' + wrapper);
738 if (!widget_instance.length) {
739 return false;
740 }
741
742 // hide filters when making a new request
743 widget_instance.find('.vbo-reportwidget-filters').hide();
744
745 // show loading skeletons
746 vboWidgetRatesFlowSkeletons(wrapper);
747
748 // reset offsets to the first state
749 widget_instance.attr('data-offset', '0');
750 widget_instance.attr('data-nowoffset', '0');
751 widget_instance.attr('data-prevoffset', '-1');
752
753 // load data
754 vboWidgetRatesFlowLoad(wrapper);
755 }
756
757 /**
758 * Toggle dates and step filters.
759 */
760 function vboWidgetRatesFlowToggleFilters(wrapper) {
761 var widget_instance = jQuery('#' + wrapper);
762 if (!widget_instance.length) {
763 return false;
764 }
765
766 widget_instance.find('.vbo-reportwidget-filters').toggle();
767 }
768
769 /**
770 * Datepicker dates selection.
771 */
772 function vboWidgetRatesFlowCheckDates(selectedDate, inst) {
773 if (selectedDate === null || inst === null) {
774 return;
775 }
776 var cur_from_date = jQuery(this).val();
777 if (jQuery(this).hasClass("vbo-ratesflow-dtpicker-from") && cur_from_date.length) {
778 var nowstart = jQuery(this).datepicker("getDate");
779 var nowstartdate = new Date(nowstart.getTime());
780 jQuery(".vbo-ratesflow-dtpicker-to").datepicker("option", {minDate: nowstartdate});
781 }
782 }
783
784 /**
785 * Navigation period cookie onchange.
786 */
787 function vboWidgetRatesFlowChangePeriod(period) {
788 // update cookie for the step selected
789 var nd = new Date();
790 nd.setTime(nd.getTime() + (365*24*60*60*1000));
791 document.cookie = "vbo_widget_ratesflow_step=" + period + "; expires=" + nd.toUTCString() + "; path=/; SameSite=Lax";
792 }
793
794 </script>
795 <?php
796 }
797 ?>
798
799 <script type="text/javascript">
800
801 // declare widgets object history array
802 if (typeof vbo_w_ratesflow_history === 'undefined') {
803 vbo_w_ratesflow_history = {
804 "<?php echo $wrapper_instance; ?>": []
805 };
806 }
807
808 jQuery(function() {
809
810 // render datepicker calendars for dates navigation
811 jQuery('#<?php echo $wrapper_id; ?>').find('.vbo-ratesflow-dtpicker-from, .vbo-ratesflow-dtpicker-to').datepicker({
812 minDate: "<?php echo date($df, $mindate); ?>",
813 maxDate: "<?php echo date($df, $maxdate); ?>",
814 yearRange: "<?php echo date('Y', $mindate); ?>:<?php echo date('Y', $maxdate); ?>",
815 changeMonth: true,
816 changeYear: true,
817 dateFormat: "<?php echo $dtpicker_df; ?>",
818 onSelect: vboWidgetRatesFlowCheckDates
819 });
820
821 // triggering for datepicker calendar icons
822 jQuery('i.vbo-widget-ratesflow-caltrigger').click(function() {
823 var jdp = jQuery(this).parent().find('input.hasDatepicker');
824 if (jdp.length) {
825 jdp.focus();
826 }
827 });
828
829 // when document is ready, load rates flow records for this widget's instance
830 vboWidgetRatesFlowLoad('<?php echo $wrapper_id; ?>');
831
832 });
833
834 </script>
835
836 <?php
837 }
838
839 /**
840 * Checks if the given range of dates belong to a festivity.
841 * Internal method for this widget only.
842 *
843 * @param array $next_fests the list of festivities to parse.
844 * @param int $day_from_ts from date timestamp.
845 * @param int $day_to_ts to date timestamp.
846 *
847 * @return mixed string fest name on success, or false.
848 */
849 protected function findDatesFestName($next_fests, $day_from_ts, $day_to_ts)
850 {
851 if (!is_array($next_fests) || empty($day_from_ts) || empty($day_to_ts)) {
852 return false;
853 }
854
855 // we also support closest matches
856 $closest_matches = array();
857
858 foreach ($next_fests as $fest_data) {
859 if (empty($fest_data['festinfo'])) {
860 continue;
861 }
862 $fest = $fest_data['festinfo'][0];
863 $earliest_ts = 0;
864 $latest_ts = 0;
865
866 if ($fest->from_ts <= $day_from_ts && $fest->to_ts >= $day_to_ts) {
867 // fest found wrapping all dates updated
868 return $fest->trans_name;
869 }
870
871 if (isset($fest->all_timestamps) && count($fest->all_timestamps) > 1) {
872 // check bridge-dates
873 $earliest_ts = $fest->all_timestamps[0];
874 $latest_ts = $fest->all_timestamps[(count($fest->all_timestamps) - 1)] + 86399;
875 if ($earliest_ts <= $day_from_ts && $latest_ts >= $day_to_ts) {
876 // fest found with bridge-dates wrapping all dates updated
877 return $fest->trans_name;
878 }
879 }
880
881 // check if at least one date updated in the interval is matching
882 $days_diff = floor(($day_to_ts - $day_from_ts) / 86400);
883 $days_to_start = null;
884 if ($fest->from_ts <= $day_from_ts && $fest->to_ts >= $day_from_ts) {
885 // this is a closest match, because the first interval day updated is a fest
886 $days_to_start = floor(($day_to_ts - $fest->to_ts) / 86400);
887 } elseif ($fest->from_ts <= $day_to_ts && $fest->to_ts >= $day_to_ts) {
888 // this is a closest match, because the last interval day updated is a fest
889 $days_to_start = floor(($fest->from_ts - $day_from_ts) / 86400);
890 } elseif ($earliest_ts > 0 && $earliest_ts <= $day_from_ts && $latest_ts >= $day_from_ts) {
891 // this is a closest match, because the first interval day updated is a fest-bridge
892 $days_to_start = floor(($day_to_ts - $latest_ts) / 86400);
893 } elseif ($earliest_ts > 0 && $earliest_ts <= $day_to_ts && $latest_ts >= $day_to_ts) {
894 // this is a closest match, because the last interval day updated is a fest-bridge
895 $days_to_start = floor(($earliest_ts - $day_from_ts) / 86400);
896 }
897 if ($days_to_start !== null && abs($days_to_start) <= 4 && $days_diff < 15) {
898 // push close match
899 array_push($closest_matches, array(
900 'fest_name' => $fest->trans_name,
901 'days_diff' => $days_diff,
902 'days_start' => abs($days_to_start),
903 ));
904 }
905 }
906
907 // count closest matches, if any
908 $tot_matches = count($closest_matches);
909 if ($tot_matches === 1) {
910 // return the first closest match
911 return $closest_matches[0]['fest_name'];
912 }
913 if ($tot_matches > 1) {
914 // sort closest matches
915 usort($closest_matches, function($a, $b) {
916 // sort by days diff ASC
917 $sort_diff = ($a['days_diff'] < $b['days_diff']) ? -1 : 1;
918 // sort by days to start ASC
919 $sort_diff += ($a['days_start'] < $b['days_start']) ? -1 : 1;
920 return $sort_diff;
921 });
922 // return the best close match
923 return $closest_matches[0]['fest_name'];
924 }
925
926 return false;
927 }
928
929 /**
930 * Calculates the new dates interval for navigation according to step.
931 * Internal method for this widget only.
932 *
933 * @param int $from_ts current from date timestamp.
934 * @param int $to_ts current end date timestamp.
935 * @param string $step the navigation step to take (weekend, month or quarter).
936 * @param int $date_dir less than 0 backward nav, more than 0 forward.
937 *
938 * @return array the new from and end date timestamps, plus the step name.
939 */
940 protected function calcDatesNavigation($from_ts, $to_ts, $step, $date_dir)
941 {
942 if (empty($from_ts) || (!($date_dir > 0) && !($date_dir < 0))) {
943 return array($from_ts, $to_ts, '');
944 }
945
946 // start date information
947 $from_info = getdate($from_ts);
948
949 // next or prev weekend
950 if ($step == 'weekend') {
951 if ($date_dir > 0) {
952 // forward dates navigation
953 $from_ts = strtotime("next Friday", $from_info[0]);
954 $to_ts = strtotime("next Sunday", $from_info[0]);
955 if ($to_ts < $from_ts) {
956 // week after
957 $to_ts = strtotime("+1 week", $to_ts);
958 }
959 } elseif ($date_dir < 0) {
960 // backward dates navigation
961 $from_ts = strtotime("last Friday", $from_info[0]);
962 $to_ts = strtotime("last Sunday", $from_info[0]);
963 if ($to_ts < $from_ts) {
964 // week before
965 $from_ts = strtotime("-1 week", $from_ts);
966 }
967 }
968 return array($from_ts, $to_ts, JText::translate('VBOWEEKND'));
969 }
970
971 // next or prev month
972 if ($step == 'month') {
973 if ($date_dir > 0) {
974 // forward dates navigation
975 $from_ts = mktime(0, 0, 0, ($from_info['mon'] + 1), 1, $from_info['year']);
976 $to_ts = mktime(0, 0, 0, ($from_info['mon'] + 1), date('t', $from_ts), $from_info['year']);
977 } elseif ($date_dir < 0) {
978 // backward dates navigation
979 $from_ts = mktime(0, 0, 0, ($from_info['mon'] - 1), 1, $from_info['year']);
980 $to_ts = mktime(0, 0, 0, ($from_info['mon'] - 1), date('t', $from_ts), $from_info['year']);
981 }
982 return array($from_ts, $to_ts, JText::translate('VBPVIEWRESTRICTIONSTWO'));
983 }
984
985 // next or prev quarter by default
986 if ($date_dir > 0) {
987 // forward dates navigation
988 $from_ts = mktime(0, 0, 0, ($from_info['mon'] + 3), 1, $from_info['year']);
989 $end_ts = mktime(0, 0, 0, ($from_info['mon'] + 5), 1, $from_info['year']);
990 $to_ts = mktime(0, 0, 0, ($from_info['mon'] + 5), date('t', $end_ts), $from_info['year']);
991 } elseif ($date_dir < 0) {
992 // backward dates navigation
993 $from_ts = mktime(0, 0, 0, ($from_info['mon'] - 3), 1, $from_info['year']);
994 $end_ts = mktime(0, 0, 0, ($from_info['mon'] - 1), 1, $from_info['year']);
995 $to_ts = mktime(0, 0, 0, ($from_info['mon'] - 1), date('t', $end_ts), $from_info['year']);
996 }
997 return array($from_ts, $to_ts, JText::translate('VBO_QUARTER'));
998 }
999
1000 /**
1001 * Parse an interval of date timestamps into a readable interval of dates according to step.
1002 * Internal method for this widget only.
1003 *
1004 * @param int $from_ts current from date timestamp.
1005 * @param int $to_ts current end date timestamp.
1006 * @param string $vbo_df the date format in VBO.
1007 * @param string $step the navigation step to take (weekend, month or quarter).
1008 *
1009 * @return array the formatted from and to date interval strings.
1010 */
1011 protected function parseReadableDateInterval($from_ts, $to_ts, $vbo_df, $step)
1012 {
1013 $from_info = getdate($from_ts);
1014 $to_info = getdate($to_ts);
1015 $format_from_date = date($vbo_df, $from_ts);
1016 $format_to_date = date($vbo_df, $to_ts);
1017 if ($step == 'weekend') {
1018 // include day of week
1019 $format_from_date = VikBooking::sayWeekDay($from_info['wday']) . ', ' . $format_from_date;
1020 $format_to_date = VikBooking::sayWeekDay($to_info['wday']) . ', ' . $format_to_date;
1021 } elseif ($step == 'month') {
1022 // check if it's a full month to say its name
1023 if ($from_info['mon'] == $to_info['mon'] && $from_info['year'] == $to_info['year']) {
1024 if ($from_info['mday'] == 1 && $to_info['mday'] == date('t', $to_ts)) {
1025 $format_from_date = VikBooking::sayMonth($from_info['mon']) . ' ' . $from_info['year'];
1026 $format_to_date = VikBooking::sayMonth($to_info['mon']) . ' ' . $to_info['year'];
1027 }
1028 }
1029 } elseif ($step == 'quarter') {
1030 // check if it's a full quarter
1031 if ($from_info['mday'] == 1 && $to_info['mday'] == date('t', $to_ts)) {
1032 $format_from_date = VikBooking::sayMonth($from_info['mon']) . ' ' . $from_info['year'];
1033 $format_to_date = VikBooking::sayMonth($to_info['mon']) . ' ' . $to_info['year'];
1034 }
1035 }
1036
1037 return array($format_from_date, $format_to_date);
1038 }
1039 }
1040