PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.6.4
VikBooking Hotel Booking Engine & PMS v1.6.4
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 / report / rates_flow.php

rates_flow.php in VikBooking Hotel Booking Engine & PMS 1.6.4, at admin/helpers/report/rates_flow.php

1,048 lines 34.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 * Rates Flow child Class of VikBookingReport
15 *
16 * @since 1.15.0 (J) - 1.5.0 (WP)
17 */
18 class VikBookingReportRatesFlow extends VikBookingReport
19 {
20 /**
21 * Property 'defaultKeySort' is used by the View that renders the report.
22 *
23 * @var string
24 */
25 public $defaultKeySort = 'created_on';
26
27 /**
28 * Property 'defaultKeyOrder' is used by the View that renders the report.
29 *
30 * @var string
31 */
32 public $defaultKeyOrder = 'DESC';
33
34 /**
35 * Property 'exportAllowed' is used by the View to display the export button.
36 *
37 * @var int
38 */
39 public $exportAllowed = 1;
40
41 /**
42 * Debug mode is activated by passing the value 'e4j_debug' > 0
43 *
44 * @var bool
45 */
46 private $debug;
47
48 /**
49 * Class constructor should define the name of the report and
50 * other vars. Call the parent constructor to define the DB object.
51 */
52 public function __construct()
53 {
54 $this->reportFile = basename(__FILE__, '.php');
55 $this->reportName = JText::translate('VBOREPORT'.strtoupper(str_replace('_', '', $this->reportFile)));
56 $this->reportFilters = array();
57
58 $this->cols = array();
59 $this->rows = array();
60 $this->footerRow = array();
61
62 $this->debug = (VikRequest::getInt('e4j_debug', 0, 'request') > 0);
63
64 $this->registerExportCSVFileName();
65
66 parent::__construct();
67 }
68
69 /**
70 * Returns the name of this report.
71 *
72 * @return string
73 */
74 public function getName()
75 {
76 return $this->reportName;
77 }
78
79 /**
80 * Returns the name of this file without .php.
81 *
82 * @return string
83 */
84 public function getFileName()
85 {
86 return $this->reportFile;
87 }
88
89 /**
90 * Returns the filters of this report.
91 *
92 * @return array
93 */
94 public function getFilters()
95 {
96 if (count($this->reportFilters)) {
97 // do not run this method twice, as it could load JS and CSS files.
98 return $this->reportFilters;
99 }
100
101 // get VBO Application Object
102 $vbo_app = VikBooking::getVboApplication();
103
104 // date format
105 $df = $this->getDateFormat();
106
107 // load the jQuery UI Datepicker
108 $this->loadDatePicker();
109
110 /**
111 * Get the rates flow handler from VCM, which is mandatory for this report.
112 * This will also load the VCM dependencies in case of success.
113 */
114 $rflow_handler = VikBooking::getRatesFlowInstance();
115 if (!$rflow_handler) {
116 // VCM is not installed or is outdated: do not proceed and set an error
117 $this->setError(JText::translate('VBCONFIGVCMAUTOUPDMISS'));
118 return $this->reportFilters;
119 }
120
121 // from Date Filter
122 $filter_opt = array(
123 'label' => '<label for="fromdate">'.JText::translate('VBOREPORTSDATEFROM').'</label>',
124 'html' => '<input type="text" id="fromdate" name="fromdate" value="" class="vbo-report-datepicker vbo-report-datepicker-from" />',
125 'type' => 'calendar',
126 'name' => 'fromdate'
127 );
128 array_push($this->reportFilters, $filter_opt);
129
130 // to Date Filter
131 $filter_opt = array(
132 'label' => '<label for="todate">'.JText::translate('VBOREPORTSDATETO').'</label>',
133 'html' => '<input type="text" id="todate" name="todate" value="" class="vbo-report-datepicker vbo-report-datepicker-to" />',
134 'type' => 'calendar',
135 'name' => 'todate'
136 );
137 array_push($this->reportFilters, $filter_opt);
138
139 // date type filter
140 $pdt_type = VikRequest::getString('dt_type', 'night', 'request');
141 $dt_types = array(
142 'night' => JText::translate('VBDAY'),
143 'creation' => JText::translate('VBOINVCREATIONDATE'),
144 );
145 $dt_type_sel_html = $vbo_app->getNiceSelect($dt_types, $pdt_type, 'dt_type', '', '', '', '', 'dt_type');
146 $filter_opt = array(
147 'label' => '<label for="dt_type">' . JText::translate('VBODASHSEARCHKEYS') . '</label>',
148 'html' => $dt_type_sel_html,
149 'type' => 'select',
150 'name' => 'dt_type'
151 );
152 array_push($this->reportFilters, $filter_opt);
153
154
155 // room ID filter
156 $pidroom = VikRequest::getInt('idroom', 0, 'request');
157 $all_rooms = $this->getRooms();
158 $rooms = array();
159 foreach ($all_rooms as $room) {
160 $rooms[$room['id']] = $room['name'];
161 }
162 if (count($rooms)) {
163 $rooms_sel_html = $vbo_app->getNiceSelect($rooms, $pidroom, 'idroom', JText::translate('VBOSTATSALLROOMS'), JText::translate('VBOSTATSALLROOMS'), '', '', 'idroom');
164 $filter_opt = array(
165 'label' => '<label for="idroom">'.JText::translate('VBOREPORTSROOMFILT').'</label>',
166 'html' => $rooms_sel_html,
167 'type' => 'select',
168 'name' => 'idroom'
169 );
170 array_push($this->reportFilters, $filter_opt);
171 }
172
173 // rate plan id filter
174 $pidprice = VikRequest::getInt('idprice', 0, 'request');
175 $all_prices = $this->getRatePlans();
176 $prices = array();
177 foreach ($all_prices as $price) {
178 $prices[$price['id']] = $price['name'];
179 }
180 if (count($prices)) {
181 $prices_sel_html = $vbo_app->getNiceSelect($prices, $pidprice, 'idprice', JText::translate('VBAFFANYPRICE'), JText::translate('VBAFFANYPRICE'), '', '', 'idprice');
182 $filter_opt = array(
183 'label' => '<label for="idprice">'.JText::translate('VBOROVWSELRPLAN').'</label>',
184 'html' => $prices_sel_html,
185 'type' => 'select',
186 'name' => 'idprice'
187 );
188 array_push($this->reportFilters, $filter_opt);
189 }
190
191 // channel filter
192 $all_channels = array();
193 // push website channel identifier (-1) as the first option
194 $all_channels['-1'] = JText::translate('VBORDFROMSITE');
195 try {
196 $all_av_channels = VikChannelManager::getAllAvChannels();
197 foreach ($all_av_channels as $ch_key => $ch_name) {
198 // push VCM channel
199 $all_channels[$ch_key] = $this->sayChannelName($ch_key, $all_av_channels);
200 }
201 } catch (Exception $e) {
202 // do nothing
203 }
204 $pchannel = VikRequest::getString('channel', '', 'request');
205 // push filter
206 $channels_sel_html = $vbo_app->getNiceSelect($all_channels, $pchannel, 'channel', '- - - -', '- - - -', '', '', 'channel');
207 $filter_opt = array(
208 'label' => '<label for="channel">'.JText::translate('VBCHANNELFILTER').'</label>',
209 'html' => $channels_sel_html,
210 'type' => 'select',
211 'name' => 'channel'
212 );
213 array_push($this->reportFilters, $filter_opt);
214
215 // get minimum and maximum nights updated for dates filters
216 list($mindate, $maxdate) = $this->getMinDatesRatesFlow();
217
218 // jQuery code for the datepicker calendars and select2
219 $now = time();
220 $pfromdate = VikRequest::getString('fromdate', '', 'request');
221 $ptodate = VikRequest::getString('todate', '', 'request');
222 // try to build the default dates
223 if (!empty($pfromdate) && empty($ptodate)) {
224 $ptodate = $pfromdate;
225 } elseif (empty($pfromdate) && !empty($ptodate)) {
226 $pfromdate = $ptodate;
227 } elseif (empty($pfromdate) && empty($ptodate) && !empty($mindate)) {
228 // filter dates are empty
229 if ($now < $maxdate) {
230 // populate default filter dates to today and one month ahead
231 $pfromdate = date($df);
232 $next_mon_ts = mktime(0, 0, 0, (date("n") + 1), date("j"), date("Y"));
233 $next_mon_ts = $next_mon_ts > $maxdate ? $maxdate : $next_mon_ts;
234 $ptodate = date($df, $next_mon_ts);
235 }
236 }
237
238 $js = 'jQuery(document).ready(function() {
239 jQuery(".vbo-report-datepicker:input").datepicker({
240 '.(!empty($mindate) ? 'minDate: "'.date($df, $mindate).'", ' : '').'
241 '.(!empty($maxdate) ? 'maxDate: "'.date($df, $maxdate).'", ' : '').'
242 '.(!empty($mindate) && !empty($maxdate) ? 'yearRange: "'.(date('Y', $mindate)).':'.date('Y', $maxdate).'", changeMonth: true, changeYear: true, ' : '').'
243 dateFormat: "'.$this->getDateFormat('jui').'",
244 onSelect: vboReportCheckDates
245 });
246 '.(!empty($pfromdate) ? 'jQuery(".vbo-report-datepicker-from").datepicker("setDate", "'.$pfromdate.'");' : '').'
247 '.(!empty($ptodate) ? 'jQuery(".vbo-report-datepicker-to").datepicker("setDate", "'.$ptodate.'");' : '').'
248 });
249 function vboReportCheckDates(selectedDate, inst) {
250 if (selectedDate === null || inst === null) {
251 return;
252 }
253 var cur_from_date = jQuery(this).val();
254 if (jQuery(this).hasClass("vbo-report-datepicker-from") && cur_from_date.length) {
255 var nowstart = jQuery(this).datepicker("getDate");
256 var nowstartdate = new Date(nowstart.getTime());
257 jQuery(".vbo-report-datepicker-to").datepicker("option", {minDate: nowstartdate});
258 }
259 }';
260 $this->setScript($js);
261
262 return $this->reportFilters;
263 }
264
265 /**
266 * Loads the report data from the DB.
267 * Returns true in case of success, false otherwise.
268 * Sets the columns and rows for the report to be displayed.
269 *
270 * @return boolean
271 */
272 public function getReportData()
273 {
274 if (strlen($this->getError())) {
275 // export functions may set errors rather than exiting the process, and the View may continue the execution to attempt to render the report.
276 return false;
277 }
278
279 /**
280 * Get the rates flow handler from VCM, which is mandatory for this report.
281 * This will also load the VCM dependencies in case of success.
282 */
283 $rflow_handler = VikBooking::getRatesFlowInstance();
284 if (!$rflow_handler) {
285 // VCM is not installed or is outdated: do not proceed and set an error
286 $this->setError(JText::translate('VBCONFIGVCMAUTOUPDMISS'));
287 return false;
288 }
289
290 // load all AV-enabled channels from VCM
291 $all_av_channels = VikChannelManager::getAllAvChannels();
292
293 /**
294 * This report makes use of the options that could be injected by those who
295 * invoke this report. Rather than injecting request vars, this report supports
296 * custom options to change the behavior of the report data calculated.
297 */
298 $options = $this->getReportOptions();
299
300 // injected options will replace request variables, if any
301 $opt_fromdate = $options->get('fromdate', '');
302 $opt_todate = $options->get('todate', '');
303 $opt_dt_type = $options->get('dt_type', '');
304 $opt_prices = $options->get('idprice');
305 $opt_rooms = $options->get('idroom');
306 $opt_channel = $options->get('channel', 0);
307 $opt_sort = $options->get('krsort');
308 $opt_order = $options->get('krorder');
309
310 // input (request) vars
311 $pfromdate = !empty($opt_fromdate) ? $opt_fromdate : VikRequest::getString('fromdate', '', 'request');
312 $ptodate = !empty($opt_todate) ? $opt_todate : VikRequest::getString('todate', '', 'request');
313 $dt_type = !empty($opt_dt_type) ? $opt_dt_type : VikRequest::getString('dt_type', 'night', 'request');
314
315 // adjust dates, if necessary
316 if (!empty($pfromdate) && empty($ptodate)) {
317 $ptodate = $pfromdate;
318 } elseif (empty($pfromdate) && !empty($ptodate)) {
319 $pfromdate = $ptodate;
320 }
321
322 // idroom can be an array of IDs or just one ID as int/string
323 $pidroom = VikRequest::getVar('idroom', null, 'request');
324 $pidroom = empty($pidroom) && !empty($opt_rooms) ? $opt_rooms : $pidroom;
325 // idprice can be an array of IDs or just one ID as int/string
326 $pidprice = VikRequest::getVar('idprice', null, 'request');
327 $pidprice = empty($pidprice) && !empty($opt_prices) ? $opt_prices : $pidprice;
328 // channel filter is an integer, can be signed (-1 = website), and it's taken from VCM
329 $pchannel = !empty($opt_channel) ? $opt_channel : VikRequest::getInt('channel', 0, 'request');
330
331 // sorting and ordering
332 $pkrsort = VikRequest::getString('krsort', $this->defaultKeySort, 'request');
333 $pkrsort = empty($pkrsort) ? $this->defaultKeySort : $pkrsort;
334 $pkrsort = !empty($opt_sort) ? $opt_sort : $pkrsort;
335 $pkrorder = VikRequest::getString('krorder', $this->defaultKeyOrder, 'request');
336 $pkrorder = empty($pkrorder) ? $this->defaultKeyOrder : $pkrorder;
337 $pkrorder = !empty($opt_order) ? $opt_order : $pkrorder;
338 $pkrorder = $pkrorder == 'DESC' ? 'DESC' : 'ASC';
339
340 // currency symbol and date params
341 $currency_symb = VikBooking::getCurrencySymb();
342 $df = $this->getDateFormat();
343 $datesep = VikBooking::getDateSeparator();
344
345 // get dates timestamps and SQL datetime strings
346 $from_ts = VikBooking::getDateTimestamp($pfromdate, 0, 0);
347 $to_ts = VikBooking::getDateTimestamp($ptodate, 23, 59, 59);
348 if (empty($pfromdate) || empty($from_ts) || empty($to_ts)) {
349 // filtering by dates is mandatory
350 $this->setError(JText::translate('VBOREPORTSERRNODATES'));
351 return false;
352 }
353 $from_sql_date = date('Y-m-d', $from_ts);
354 $to_sql_date = date('Y-m-d', $to_ts);
355
356 // months map
357 $months_map = array(
358 JText::translate('VBMONTHONE'),
359 JText::translate('VBMONTHTWO'),
360 JText::translate('VBMONTHTHREE'),
361 JText::translate('VBMONTHFOUR'),
362 JText::translate('VBMONTHFIVE'),
363 JText::translate('VBMONTHSIX'),
364 JText::translate('VBMONTHSEVEN'),
365 JText::translate('VBMONTHEIGHT'),
366 JText::translate('VBMONTHNINE'),
367 JText::translate('VBMONTHTEN'),
368 JText::translate('VBMONTHELEVEN'),
369 JText::translate('VBMONTHTWELVE'),
370 );
371
372 // query to obtain the records
373 $records = array();
374 $clauses = array();
375 // date type and date filters
376 if ($dt_type == 'night') {
377 // filter nights updated
378 $sub_clauses = array();
379 // build sub-clauses
380 $sub_clause_one = array();
381 array_push($sub_clause_one, "`rf`.`day_from` <= " . $this->dbo->quote($from_sql_date));
382 array_push($sub_clause_one, "`rf`.`day_from` <= " . $this->dbo->quote($to_sql_date));
383 array_push($sub_clause_one, "`rf`.`day_to` >= " . $this->dbo->quote($from_sql_date));
384 array_push($sub_clause_one, "`rf`.`day_to` >= " . $this->dbo->quote($to_sql_date));
385 $sub_clause_two = array();
386 array_push($sub_clause_two, "`rf`.`day_from` >= " . $this->dbo->quote($from_sql_date));
387 array_push($sub_clause_two, "`rf`.`day_from` <= " . $this->dbo->quote($to_sql_date));
388 array_push($sub_clause_two, "`rf`.`day_to` >= " . $this->dbo->quote($from_sql_date));
389 array_push($sub_clause_two, "`rf`.`day_to` <= " . $this->dbo->quote($to_sql_date));
390 $sub_clause_three = array();
391 array_push($sub_clause_three, "`rf`.`day_from` >= " . $this->dbo->quote($from_sql_date));
392 array_push($sub_clause_three, "`rf`.`day_from` <= " . $this->dbo->quote($to_sql_date));
393 array_push($sub_clause_three, "`rf`.`day_to` >= " . $this->dbo->quote($from_sql_date));
394 array_push($sub_clause_three, "`rf`.`day_to` >= " . $this->dbo->quote($to_sql_date));
395 $sub_clause_four = array();
396 array_push($sub_clause_four, "`rf`.`day_from` <= " . $this->dbo->quote($from_sql_date));
397 array_push($sub_clause_four, "`rf`.`day_from` <= " . $this->dbo->quote($to_sql_date));
398 array_push($sub_clause_four, "`rf`.`day_to` >= " . $this->dbo->quote($from_sql_date));
399 array_push($sub_clause_four, "`rf`.`day_to` <= " . $this->dbo->quote($to_sql_date));
400 // push all sub-clauses
401 array_push($sub_clauses, "(" . implode(' AND ', $sub_clause_one) . ")");
402 array_push($sub_clauses, "(" . implode(' AND ', $sub_clause_two) . ")");
403 array_push($sub_clauses, "(" . implode(' AND ', $sub_clause_three) . ")");
404 array_push($sub_clauses, "(" . implode(' AND ', $sub_clause_four) . ")");
405 // push full clause
406 array_push($clauses, "(" . implode(' OR ', $sub_clauses) . ")");
407 } else {
408 // filter dates for creation date
409 array_push($clauses, "`rf`.`created_on` >= " . $this->dbo->quote($from_sql_date));
410 array_push($clauses, "`rf`.`created_on` <= " . $this->dbo->quote($to_sql_date));
411 }
412 // room ID or room IDs
413 if (!empty($pidroom) && !is_array($pidroom)) {
414 array_push($clauses, "`rf`.`vbo_room_id` = " . (int)$pidroom);
415 } elseif (is_array($pidroom) && count($pidroom)) {
416 array_push($clauses, "`rf`.`vbo_room_id` IN (" . implode(', ', $pidroom) . ")");
417 }
418 // rate plan ID or rate plan IDs
419 if (!empty($pidprice) && !is_array($pidprice)) {
420 array_push($clauses, "`rf`.`vbo_price_id` = " . (int)$pidprice);
421 } elseif (is_array($pidprice) && count($pidprice)) {
422 array_push($clauses, "`rf`.`vbo_price_id` IN (" . implode(', ', $pidprice) . ")");
423 }
424 // channel filter
425 if (!empty($pchannel)) {
426 array_push($clauses, "`rf`.`channel_id` = " . $pchannel);
427 }
428 // additional filters set through custom options
429 $fetch_alterations = (!strcasecmp($options->get('fetch', ''), 'alterations'));
430 if ($fetch_alterations) {
431 // exclude rates flow records generated by the Bulk Actions in VCM (i.e. rates flow admin widget)
432 array_push($clauses, "`rf`.`created_by` != " . $this->dbo->quote('channelsRatesPush'));
433 }
434 // query limits
435 $limfirst = $options->get('lim', 0);
436 $limstart = $options->get('limstart', 0);
437 $lim = $limfirst;
438 $found_rows = '';
439 $tot_rows = 0;
440 $t_records = 0;
441 $multiplim = 1;
442 if ($lim > 0 && $fetch_alterations) {
443 /**
444 * We need to multiply the limit by the number of channels to fetch, so
445 * that the results will include all rate modifications for any channel.
446 */
447 $multiplim = $this->countChannels();
448 $lim *= $multiplim;
449 $found_rows = "SQL_CALC_FOUND_ROWS ";
450 }
451
452 // query the database (do not change the default ordering columns!)
453 $q = "SELECT {$found_rows}`rf`.*, `r`.`name` AS `room_name`, `p`.`name` AS `rplan_name` " .
454 "FROM `#__vikchannelmanager_rates_flow` AS `rf` " .
455 "LEFT JOIN `#__vikbooking_rooms` AS `r` ON `r`.`id`=`rf`.`vbo_room_id` " .
456 "LEFT JOIN `#__vikbooking_prices` AS `p` ON `p`.`id`=`rf`.`vbo_price_id` " .
457 "WHERE " . implode(' AND ', $clauses) . " " .
458 "ORDER BY `rf`.`created_on` ASC, `rf`.`channel_id` ASC";
459 $this->dbo->setQuery($q, $limstart, $lim);
460 $this->dbo->execute();
461 if ($this->dbo->getNumRows()) {
462 $records = $this->dbo->loadAssocList();
463 if (!empty($found_rows)) {
464 // grab total rows count without limits for pagination
465 $this->dbo->setQuery('SELECT FOUND_ROWS();');
466 $tot_rows = $this->dbo->loadResult();
467 }
468 }
469
470 // count total records fetched from query before any manipulation
471 $t_records = count($records);
472
473 // define the columns of the report
474 $this->cols = array(
475 // creation date
476 array(
477 'key' => 'created_on',
478 'sortable' => 1,
479 'label' => JText::translate('VBOINVCREATIONDATE'),
480 ),
481 // channel
482 array(
483 'key' => 'channel_id',
484 'attr' => array(
485 'class="center"'
486 ),
487 'sortable' => 1,
488 'label' => JText::translate('VBOCHANNEL'),
489 ),
490 // from night (date) updated
491 array(
492 'key' => 'day_from',
493 'sortable' => 1,
494 'label' => JText::translate('VBNEWRESTRICTIONDFROMRANGE'),
495 ),
496 // to night (date) updated
497 array(
498 'key' => 'day_to',
499 'sortable' => 1,
500 'label' => JText::translate('VBNEWRESTRICTIONDTORANGE'),
501 ),
502 // VBO room id
503 array(
504 'key' => 'vbo_room_id',
505 'attr' => array(
506 'class="center"'
507 ),
508 'sortable' => 1,
509 'label' => JText::translate('VBNEWROOMFIVE'),
510 ),
511 // VBO price id
512 array(
513 'key' => 'vbo_price_id',
514 'attr' => array(
515 'class="center"'
516 ),
517 'sortable' => 1,
518 'label' => JText::translate('VBOROVWSELRPLAN'),
519 ),
520 // base price per night
521 array(
522 'key' => 'base_fee',
523 'attr' => array(
524 'class="center"'
525 ),
526 'sortable' => 1,
527 'label' => JText::translate('VBO_BASE_RATE'),
528 ),
529 // price per night set
530 array(
531 'key' => 'nightly_fee',
532 'attr' => array(
533 'class="center"'
534 ),
535 'sortable' => 1,
536 'label' => JText::translate('VBNEWOPTFIVE'),
537 ),
538 // channel alteration
539 array(
540 'key' => 'channel_alter',
541 'attr' => array(
542 'class="center"'
543 ),
544 'sortable' => 1,
545 'label' => JText::translate('VBNEWSEASONSIX'),
546 ),
547 // created by (through)
548 array(
549 'key' => 'created_by',
550 'attr' => array(
551 'class="center"'
552 ),
553 'sortable' => 1,
554 'label' => JText::translate('VBCSVCREATEDBY'),
555 ),
556 // extra data
557 array(
558 'key' => 'data',
559 'attr' => array(
560 'class="center"'
561 ),
562 'label' => JText::translate('VBPSHOWPAYMENTSTHREE'),
563 ),
564 );
565
566 // check if paging should be added or if records should be adjusted
567 if (!empty($found_rows) && $fetch_alterations) {
568 // adjust records according to limit multiplied by number of AV channels
569 $records_intvals_keys = array();
570 $consequent_key = -1;
571 $unexpected_records = 0;
572 foreach ($records as $k => $record) {
573 if ($consequent_key >= $k) {
574 continue;
575 }
576 $consequent_key = $k;
577 for ($i = 1; $i < $multiplim; $i++) {
578 $check_key = ($k + $i);
579 if (!isset($records[$check_key])) {
580 break;
581 }
582 if ($record['day_from'] == $records[$check_key]['day_from'] && $record['day_to'] == $records[$check_key]['day_to']) {
583 // expected record found
584 $consequent_key = $check_key;
585 continue;
586 }
587 // unexpected record found according to limit multiplied by number of AV channels
588 $unexpected_records++;
589 }
590 // push interval of consequent keys
591 array_push($records_intvals_keys, array($k, $consequent_key));
592 }
593
594 // check if the offset for the next request needs to be adjusted
595 $offset_removed = 0;
596 if (count($records_intvals_keys) > $limfirst) {
597 // let's split up the records found to respect the limit requested
598 $max_key = $records_intvals_keys[($limfirst - 1)][1];
599 foreach ($records as $k => $v) {
600 if ($k > $max_key) {
601 // remove this record that would exceed the limit requested
602 unset($records[$k]);
603 // increase the offset for removed records
604 $offset_removed++;
605 }
606 }
607 }
608
609 // check if there is a next page and calculate next limit and offset
610 $has_next_page = false;
611 $page_number = 1;
612 $next_lim = null;
613 $next_offset = null;
614 if ($lim > 0 && $tot_rows > 0 && $t_records >= $limfirst) {
615 // limit requested satisfied, so we may have a next page
616 $has_next_page = (($limstart + $t_records - $offset_removed) < $tot_rows);
617 if ($has_next_page) {
618 // calculate the actual next offset
619 $next_offset = $limstart + $t_records - $offset_removed;
620 // keep the original limit
621 $next_lim = $limfirst;
622 }
623 }
624 // count (approx) current page number
625 if ($lim > 0 && $tot_rows > 0 && $limstart > 0) {
626 // we must be at a page after the #1
627 $page_number = floor($tot_rows / $limstart);
628 $page_number = $page_number < 2 ? 2 : $page_number;
629 }
630
631 // add paging details as a special column (if fetch "alterations")
632 array_push($this->cols, array(
633 'key' => 'paging',
634 'has_next_page' => (int)$has_next_page,
635 'page_num' => (int)$page_number,
636 'lim' => $next_lim,
637 'limstart' => $next_offset,
638 'rm_offset' => $offset_removed,
639 ));
640 }
641
642 // loop over the records to build the rows
643 foreach ($records as $record) {
644 // get rates flow record object
645 $rflow_record = $rflow_handler->getRecord($record);
646
647 $created_on = $rflow_record->getCreatedOn();
648 list($day_from, $day_to) = $rflow_record->getDates();
649
650 $ts_created = strtotime($created_on);
651 $info_created = getdate($ts_created);
652 $wday_created = $this->getWdayString($info_created['wday'], 'short');
653 $mon_created = $months_map[($info_created['mon'] - 1)];
654
655 $say_channel_name = $this->sayChannelName($rflow_record->getChannelID(), $all_av_channels);
656 $vbo_room_name = $record['room_name'];
657 $vbo_rplan_name = $record['rplan_name'];
658 $vbo_rplan_id = $rflow_record->getVBORatePlanID();
659 $channel_alteration_str = $rflow_record->getChannelAlteration();
660 $channel_alteration_num = !empty($channel_alteration_str) ? (float)preg_replace("/[^0-9.,-]/", '', $channel_alteration_str) : $channel_alteration_str;
661 $say_created_by = $this->sayCreatedBy($rflow_record->getCreatedBy());
662 $decoded_data = $rflow_record->getExtraData();
663
664 // attempt to get the channel logo, if any
665 $channel_raw_name = $this->getRawChannelName($rflow_record->getChannelID(), $all_av_channels);
666 $channel_logo = $this->getChannelLogoURI($channel_raw_name);
667
668 // push fields in the rows array as a new row
669 array_push($this->rows, array(
670 array(
671 'key' => 'created_on',
672 'callback' => function($val) use ($df, $datesep, $wday_created, $mon_created, $ts_created) {
673 return $wday_created . ', ' . date('j', $ts_created) . ' ' . $mon_created . ' ' . date('Y', $ts_created) . ' ' . date('H:i', $ts_created);
674 },
675 'value' => $created_on,
676 ),
677 array(
678 'key' => 'channel_id',
679 'callback' => function($val) use ($say_channel_name) {
680 return empty($val) ? '' : $say_channel_name;
681 },
682 'attr' => array(
683 'class="center"'
684 ),
685 'value' => $rflow_record->getChannelID(),
686 // set a special (reserved) key for the channel logo
687 '_logo' => $channel_logo,
688 ),
689 array(
690 'key' => 'day_from',
691 'callback' => function($val) use ($df, $datesep) {
692 return date(str_replace("/", $datesep, $df), strtotime($val));
693 },
694 'value' => $day_from,
695 ),
696 array(
697 'key' => 'day_to',
698 'callback' => function($val) use ($df, $datesep) {
699 return date(str_replace("/", $datesep, $df), strtotime($val));
700 },
701 'value' => $day_to,
702 ),
703 array(
704 'key' => 'vbo_room_id',
705 'callback' => function($val) use ($vbo_room_name) {
706 return empty($vbo_room_name) ? $val : $vbo_room_name;
707 },
708 'attr' => array(
709 'class="center"'
710 ),
711 'title' => $rflow_record->getOTARoomID(),
712 'value' => $rflow_record->getVBORoomID(),
713 ),
714 array(
715 'key' => 'vbo_price_id',
716 'callback' => function($val) use ($vbo_rplan_name) {
717 return empty($vbo_rplan_name) ? $val : $vbo_rplan_name;
718 },
719 'attr' => array(
720 'class="center"'
721 ),
722 'value' => $vbo_rplan_id,
723 ),
724 array(
725 'key' => 'base_fee',
726 'attr' => array(
727 'class="center vbo-report-col-hideable"'
728 ),
729 'callback' => function($val) use ($currency_symb) {
730 return $currency_symb . ' ' . VikBooking::numberFormat($val);
731 },
732 'value' => $rflow_record->getBaseFee(),
733 ),
734 array(
735 'key' => 'nightly_fee',
736 'attr' => array(
737 'class="center"'
738 ),
739 'callback' => function($val) use ($currency_symb) {
740 return $currency_symb . ' ' . VikBooking::numberFormat($val);
741 },
742 'value' => $rflow_record->getNightlyFee(),
743 ),
744 array(
745 'key' => 'channel_alter',
746 'attr' => array(
747 'class="center"'
748 ),
749 'callback' => function($val) use ($channel_alteration_str) {
750 return !empty($channel_alteration_str) ? $channel_alteration_str : '';
751 },
752 'value' => $channel_alteration_num,
753 ),
754 array(
755 'key' => 'created_by',
756 'callback' => function($val) use ($say_created_by) {
757 return empty($val) ? '' : $say_created_by;
758 },
759 'attr' => array(
760 'class="center"'
761 ),
762 'value' => $say_created_by,
763 ),
764 array(
765 'key' => 'data',
766 'attr' => array(
767 'class="center vbo-report-col-hideable"'
768 ),
769 'callback' => function($val) use ($vbo_rplan_id) {
770 if (!is_object($val)) {
771 return '';
772 }
773 $data_parts = array();
774 if (isset($val->RatePlan)) {
775 $ota_rplan_name = !empty($val->RatePlan->name) ? $val->RatePlan->name : '';
776 $ota_rplan_name .= !empty($val->RatePlan->id) && $val->RatePlan->id != '-1' && $val->RatePlan->id != $vbo_rplan_id ? (' (' . $val->RatePlan->id . ')') : '';
777 array_push($data_parts, $ota_rplan_name);
778 }
779 if (isset($val->RatesLOS)) {
780 array_push($data_parts, 'LOS Model');
781 }
782 if (isset($val->Restrictions)) {
783 if (isset($val->Restrictions->minLOS)) {
784 array_push($data_parts, 'Min LOS ' . $val->Restrictions->minLOS);
785 }
786 if (isset($val->Restrictions->cta)) {
787 if ((is_bool($val->Restrictions->cta) && $val->Restrictions->cta === true) || (is_string($val->Restrictions->cta) && !strcasecmp($val->Restrictions->cta, 'true'))) {
788 array_push($data_parts, 'CTA');
789 }
790 }
791 if (isset($val->Restrictions->ctd)) {
792 if ((is_bool($val->Restrictions->ctd) && $val->Restrictions->ctd === true) || (is_string($val->Restrictions->ctd) && !strcasecmp($val->Restrictions->ctd, 'true'))) {
793 array_push($data_parts, 'CTD');
794 }
795 }
796 }
797 return implode(', ', $data_parts);
798 },
799 'value' => $decoded_data,
800 ),
801 ));
802
803 if (!empty($found_rows) && $fetch_alterations) {
804 // unshift the row just pushed and prepend the ID of the record just added
805 $rows_last_key = count($this->rows) - 1;
806 array_unshift($this->rows[$rows_last_key], array(
807 'key' => 'id',
808 'value' => $record['id'],
809 ));
810 }
811 }
812
813 // sort rows
814 $this->sortRows($pkrsort, $pkrorder);
815
816 // update sorting and ordering key
817 $this->defaultKeySort = $pkrsort;
818 $this->defaultKeyOrder = $pkrorder;
819
820 return true;
821 }
822
823 /**
824 * Returns an array with the minimum and maximum dates updated.
825 * We keep the visibility as public so that who invokes this class can use it.
826 *
827 * @return array to be used with list() to get the min/max date timestamps.
828 */
829 public function getMinDatesRatesFlow()
830 {
831 $mindate = null;
832 $maxdate = null;
833
834 $rflow_handler = VikBooking::getRatesFlowInstance();
835 if (!$rflow_handler) {
836 // make sure VCM is installed, or the query below will raise an error
837 return array($mindate, $maxdate);
838 }
839
840 $q = "SELECT MIN(`day_from`) AS `mindate`, MAX(`day_to`) AS `maxdate`, MIN(`created_on`) AS `mincreatedate` FROM `#__vikchannelmanager_rates_flow`;";
841 $this->dbo->setQuery($q);
842 $this->dbo->execute();
843 if ($this->dbo->getNumRows()) {
844 $data = $this->dbo->loadAssoc();
845 if (!empty($data['mindate']) && !empty($data['maxdate'])) {
846 $mindate = strtotime($data['mindate']);
847 $maxdate = strtotime($data['maxdate']);
848 $mincreatedate = strtotime($data['mincreatedate']);
849 if ($mincreatedate < $mindate) {
850 $mindate = $mincreatedate;
851 }
852 }
853 }
854
855 return array($mindate, $maxdate);
856 }
857
858 /**
859 * Returns the total number of unique channel identifiers updated at least once.
860 * We keep the visibility as public so that who invokes this class can use it.
861 *
862 * @return int total number of unique channels updated at least once.
863 */
864 public function countRatesFlowChannels()
865 {
866 $rflow_handler = VikBooking::getRatesFlowInstance();
867 if (!$rflow_handler) {
868 // make sure VCM is installed, or the query below will raise an error
869 return 0;
870 }
871
872 $q = "SELECT DISTINCT `channel_id` FROM `#__vikchannelmanager_rates_flow`;";
873 $this->dbo->setQuery($q);
874 $this->dbo->execute();
875
876 return (int)$this->dbo->getNumRows();
877 }
878
879 /**
880 * Registers the name to give to the CSV file being exported.
881 *
882 * @return void
883 *
884 * @since 1.16.1 (J) - 1.6.1 (WP)
885 */
886 private function registerExportCSVFileName()
887 {
888 $pfromdate = VikRequest::getString('fromdate', '', 'request');
889 $ptodate = VikRequest::getString('todate', '', 'request');
890
891 $report_extraname = '';
892 $pchannel = VikRequest::getInt('channel', 0, 'request');
893 if (!empty($pchannel)) {
894 // set channel name for exported file
895 $report_extraname = $this->sayChannelName($pchannel);
896 }
897
898 $this->setExportCSVFileName($this->reportName . (!empty($report_extraname) ? '-' . $report_extraname : '') . '-' . str_replace('/', '_', $pfromdate) . '-' . str_replace('/', '_', $ptodate) . '.csv');
899 }
900
901 /**
902 * Given a channel identifier number, returns a proper name for it.
903 *
904 * @param int $ch_key the channel identifier number.
905 * @param array $av_channels optional list of AV-enabled channels.
906 *
907 * @return string the proper channel name.
908 */
909 private function sayChannelName($ch_key, $av_channels = array())
910 {
911 $channel_name = '';
912
913 if ((int)$ch_key == -1) {
914 // website
915 return JText::translate('VBORDFROMSITE');
916 }
917
918 try {
919 $all_av_channels = count($av_channels) ? $av_channels : VikChannelManager::getAllAvChannels();
920 foreach ($all_av_channels as $ch_id => $ch_name) {
921 if ($ch_key != $ch_id) {
922 continue;
923 }
924 $channel_name = $ch_id == VikChannelManagerConfig::GOOGLEHOTEL ? 'Google Hotel' : ucwords($ch_name);
925 $channel_name = $ch_id == VikChannelManagerConfig::AIRBNBAPI ? 'Airbnb' : $channel_name;
926 $channel_name = defined('VikChannelManagerConfig::VRBOAPI') && $ch_id == VikChannelManagerConfig::VRBOAPI ? 'Vrbo' : $channel_name;
927 }
928 } catch (Exception $e) {
929 // do nothing
930 }
931
932 return $channel_name;
933 }
934
935 /**
936 * Given a channel identifier number, returns the raw name of it.
937 *
938 * @param int $ch_key the channel identifier number.
939 * @param array $av_channels optional list of AV-enabled channels.
940 *
941 * @return string the raw channel name (provenience).
942 */
943 private function getRawChannelName($ch_key, $av_channels = array())
944 {
945 $channel_name = '';
946
947 if ((int)$ch_key == -1) {
948 // website
949 return JText::translate('VBORDFROMSITE');
950 }
951
952 try {
953 $all_av_channels = count($av_channels) ? $av_channels : VikChannelManager::getAllAvChannels();
954 foreach ($all_av_channels as $ch_id => $ch_name) {
955 if ($ch_key == $ch_id) {
956 // channel found
957 $channel_name = $ch_name;
958 break;
959 }
960 }
961 } catch (Exception $e) {
962 // do nothing
963 }
964
965 return $channel_name;
966 }
967
968 /**
969 * Attempts to match a channel name (provenience) to its logo URI.
970 *
971 * @param string $ch_name the raw channel name.
972 *
973 * @return string the channel logo URI or an empty string.
974 */
975 private function getChannelLogoURI($ch_name)
976 {
977 $channel_logo = '';
978
979 if (empty($ch_name)) {
980 return $channel_logo;
981 }
982
983 try {
984 $channel_logo = VikChannelManager::getLogosInstance($ch_name)->getSmallLogoURL();
985 } catch (Exception $e) {
986 // do nothing
987 }
988
989 return $channel_logo;
990 }
991
992 /**
993 * Given a created by string identifier, returns a readable name for it.
994 *
995 * @param string $created_by the raw created by string.
996 *
997 * @return string the readable created by string.
998 */
999 private function sayCreatedBy($created_by)
1000 {
1001 if (empty($created_by)) {
1002 return '';
1003 }
1004
1005 if (!strcasecmp($created_by, 'VBO') || !strcasecmp($created_by, 'VikBooking')) {
1006 // website
1007 return JText::translate('VBORDFROMSITE');
1008 }
1009
1010 if (!strcasecmp($created_by, 'setNewRate') || !strcasecmp($created_by, 'VCM')) {
1011 // VCM Custom Rates
1012 return JText::translate('VBMENUCHANNELMANAGER');
1013 }
1014
1015 if (!strcasecmp($created_by, 'channelsRatesPush') || !strcasecmp(str_replace(' ', '', $created_by), 'SmartBalancer')) {
1016 // VCM Bulk Action - Rates Upload
1017 return JText::translate('VBMENUCHANNELMANAGER');
1018 }
1019
1020 if (!strcasecmp($created_by, 'App')) {
1021 // e4jConnect Mobile App
1022 return JText::translate('VBO_MOBILE_APP');
1023 }
1024
1025 return $created_by;
1026 }
1027
1028 /**
1029 * Returns the total number of channels supporting updates of rates.
1030 *
1031 * @return int the total number of channels for the rates flow.
1032 */
1033 private function countChannels()
1034 {
1035 // we start from 1 to include the website
1036 $tot_channels = 1;
1037
1038 try {
1039 $all_av_channels = VikChannelManager::getAllAvChannels();
1040 $tot_channels += count($all_av_channels);
1041 } catch (Exception $e) {
1042 // do nothing
1043 }
1044
1045 return $tot_channels;
1046 }
1047 }
1048