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 / report / rates_flow.php

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

1,052 lines 34.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 * 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(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 ($decoded_data) {
757 $uname = '';
758 if (is_object($decoded_data) && !empty($decoded_data->User)) {
759 $uname = ' (' . $decoded_data->User . ')';
760 }
761 return empty($val) ? trim($uname) : $val . $uname;
762 },
763 'attr' => array(
764 'class="center"'
765 ),
766 'value' => $say_created_by,
767 ),
768 array(
769 'key' => 'data',
770 'attr' => array(
771 'class="center vbo-report-col-hideable"'
772 ),
773 'callback' => function($val) use ($vbo_rplan_id) {
774 if (!is_object($val)) {
775 return '';
776 }
777 $data_parts = array();
778 if (isset($val->RatePlan)) {
779 $ota_rplan_name = !empty($val->RatePlan->name) ? $val->RatePlan->name : '';
780 $ota_rplan_name .= !empty($val->RatePlan->id) && $val->RatePlan->id != '-1' && $val->RatePlan->id != $vbo_rplan_id ? (' (' . $val->RatePlan->id . ')') : '';
781 array_push($data_parts, $ota_rplan_name);
782 }
783 if (isset($val->RatesLOS)) {
784 array_push($data_parts, 'LOS Model');
785 }
786 if (isset($val->Restrictions)) {
787 if (isset($val->Restrictions->minLOS)) {
788 array_push($data_parts, 'Min LOS ' . $val->Restrictions->minLOS);
789 }
790 if (isset($val->Restrictions->cta)) {
791 if ((is_bool($val->Restrictions->cta) && $val->Restrictions->cta === true) || (is_string($val->Restrictions->cta) && !strcasecmp($val->Restrictions->cta, 'true'))) {
792 array_push($data_parts, 'CTA');
793 }
794 }
795 if (isset($val->Restrictions->ctd)) {
796 if ((is_bool($val->Restrictions->ctd) && $val->Restrictions->ctd === true) || (is_string($val->Restrictions->ctd) && !strcasecmp($val->Restrictions->ctd, 'true'))) {
797 array_push($data_parts, 'CTD');
798 }
799 }
800 }
801 return implode(', ', $data_parts);
802 },
803 'value' => $decoded_data,
804 ),
805 ));
806
807 if (!empty($found_rows) && $fetch_alterations) {
808 // unshift the row just pushed and prepend the ID of the record just added
809 $rows_last_key = count($this->rows) - 1;
810 array_unshift($this->rows[$rows_last_key], array(
811 'key' => 'id',
812 'value' => $record['id'],
813 ));
814 }
815 }
816
817 // sort rows
818 $this->sortRows($pkrsort, $pkrorder);
819
820 // update sorting and ordering key
821 $this->defaultKeySort = $pkrsort;
822 $this->defaultKeyOrder = $pkrorder;
823
824 return true;
825 }
826
827 /**
828 * Returns an array with the minimum and maximum dates updated.
829 * We keep the visibility as public so that who invokes this class can use it.
830 *
831 * @return array to be used with list() to get the min/max date timestamps.
832 */
833 public function getMinDatesRatesFlow()
834 {
835 $mindate = null;
836 $maxdate = null;
837
838 $rflow_handler = VikBooking::getRatesFlowInstance();
839 if (!$rflow_handler) {
840 // make sure VCM is installed, or the query below will raise an error
841 return array($mindate, $maxdate);
842 }
843
844 $q = "SELECT MIN(`day_from`) AS `mindate`, MAX(`day_to`) AS `maxdate`, MIN(`created_on`) AS `mincreatedate` FROM `#__vikchannelmanager_rates_flow`;";
845 $this->dbo->setQuery($q);
846 $this->dbo->execute();
847 if ($this->dbo->getNumRows()) {
848 $data = $this->dbo->loadAssoc();
849 if (!empty($data['mindate']) && !empty($data['maxdate'])) {
850 $mindate = strtotime($data['mindate']);
851 $maxdate = strtotime($data['maxdate']);
852 $mincreatedate = strtotime($data['mincreatedate']);
853 if ($mincreatedate < $mindate) {
854 $mindate = $mincreatedate;
855 }
856 }
857 }
858
859 return array($mindate, $maxdate);
860 }
861
862 /**
863 * Returns the total number of unique channel identifiers updated at least once.
864 * We keep the visibility as public so that who invokes this class can use it.
865 *
866 * @return int total number of unique channels updated at least once.
867 */
868 public function countRatesFlowChannels()
869 {
870 $rflow_handler = VikBooking::getRatesFlowInstance();
871 if (!$rflow_handler) {
872 // make sure VCM is installed, or the query below will raise an error
873 return 0;
874 }
875
876 $q = "SELECT `channel_id` FROM `#__vikchannelmanager_rates_flow` WHERE 1 GROUP BY `channel_id`;";
877 $this->dbo->setQuery($q);
878 $this->dbo->execute();
879
880 return (int)$this->dbo->getNumRows();
881 }
882
883 /**
884 * Registers the name to give to the CSV file being exported.
885 *
886 * @return void
887 *
888 * @since 1.16.1 (J) - 1.6.1 (WP)
889 */
890 private function registerExportCSVFileName()
891 {
892 $pfromdate = VikRequest::getString('fromdate', '', 'request');
893 $ptodate = VikRequest::getString('todate', '', 'request');
894
895 $report_extraname = '';
896 $pchannel = VikRequest::getInt('channel', 0, 'request');
897 if (!empty($pchannel)) {
898 // set channel name for exported file
899 $report_extraname = $this->sayChannelName($pchannel);
900 }
901
902 $this->setExportCSVFileName($this->reportName . (!empty($report_extraname) ? '-' . $report_extraname : '') . '-' . str_replace('/', '_', $pfromdate) . '-' . str_replace('/', '_', $ptodate) . '.csv');
903 }
904
905 /**
906 * Given a channel identifier number, returns a proper name for it.
907 *
908 * @param int $ch_key the channel identifier number.
909 * @param array $av_channels optional list of AV-enabled channels.
910 *
911 * @return string the proper channel name.
912 */
913 private function sayChannelName($ch_key, $av_channels = array())
914 {
915 $channel_name = '';
916
917 if ((int)$ch_key == -1) {
918 // website
919 return JText::translate('VBORDFROMSITE');
920 }
921
922 try {
923 $all_av_channels = count($av_channels) ? $av_channels : VikChannelManager::getAllAvChannels();
924 foreach ($all_av_channels as $ch_id => $ch_name) {
925 if ($ch_key != $ch_id) {
926 continue;
927 }
928 $channel_name = $ch_id == VikChannelManagerConfig::GOOGLEHOTEL ? 'Google Hotel' : ucwords($ch_name);
929 $channel_name = $ch_id == VikChannelManagerConfig::AIRBNBAPI ? 'Airbnb' : $channel_name;
930 $channel_name = defined('VikChannelManagerConfig::VRBOAPI') && $ch_id == VikChannelManagerConfig::VRBOAPI ? 'Vrbo' : $channel_name;
931 }
932 } catch (Exception $e) {
933 // do nothing
934 }
935
936 return $channel_name;
937 }
938
939 /**
940 * Given a channel identifier number, returns the raw name of it.
941 *
942 * @param int $ch_key the channel identifier number.
943 * @param array $av_channels optional list of AV-enabled channels.
944 *
945 * @return string the raw channel name (provenience).
946 */
947 private function getRawChannelName($ch_key, $av_channels = array())
948 {
949 $channel_name = '';
950
951 if ((int)$ch_key == -1) {
952 // website
953 return JText::translate('VBORDFROMSITE');
954 }
955
956 try {
957 $all_av_channels = count($av_channels) ? $av_channels : VikChannelManager::getAllAvChannels();
958 foreach ($all_av_channels as $ch_id => $ch_name) {
959 if ($ch_key == $ch_id) {
960 // channel found
961 $channel_name = $ch_name;
962 break;
963 }
964 }
965 } catch (Exception $e) {
966 // do nothing
967 }
968
969 return $channel_name;
970 }
971
972 /**
973 * Attempts to match a channel name (provenience) to its logo URI.
974 *
975 * @param string $ch_name the raw channel name.
976 *
977 * @return string the channel logo URI or an empty string.
978 */
979 private function getChannelLogoURI($ch_name)
980 {
981 $channel_logo = '';
982
983 if (empty($ch_name)) {
984 return $channel_logo;
985 }
986
987 try {
988 $channel_logo = VikChannelManager::getLogosInstance($ch_name)->getSmallLogoURL();
989 } catch (Exception $e) {
990 // do nothing
991 }
992
993 return $channel_logo;
994 }
995
996 /**
997 * Given a created by string identifier, returns a readable name for it.
998 *
999 * @param string $created_by the raw created by string.
1000 *
1001 * @return string the readable created by string.
1002 */
1003 private function sayCreatedBy($created_by)
1004 {
1005 if (empty($created_by)) {
1006 return '';
1007 }
1008
1009 if (!strcasecmp($created_by, 'VBO') || !strcasecmp($created_by, 'VikBooking')) {
1010 // website
1011 return JText::translate('VBORDFROMSITE');
1012 }
1013
1014 if (!strcasecmp($created_by, 'setNewRate') || !strcasecmp($created_by, 'VCM')) {
1015 // VCM Custom Rates
1016 return JText::translate('VBMENUCHANNELMANAGER');
1017 }
1018
1019 if (!strcasecmp($created_by, 'channelsRatesPush') || !strcasecmp(str_replace(' ', '', $created_by), 'SmartBalancer')) {
1020 // VCM Bulk Action - Rates Upload
1021 return JText::translate('VBMENUCHANNELMANAGER');
1022 }
1023
1024 if (!strcasecmp($created_by, 'App')) {
1025 // e4jConnect Mobile App
1026 return JText::translate('VBO_MOBILE_APP');
1027 }
1028
1029 return $created_by;
1030 }
1031
1032 /**
1033 * Returns the total number of channels supporting updates of rates.
1034 *
1035 * @return int the total number of channels for the rates flow.
1036 */
1037 private function countChannels()
1038 {
1039 // we start from 1 to include the website
1040 $tot_channels = 1;
1041
1042 try {
1043 $all_av_channels = VikChannelManager::getAllAvChannels();
1044 $tot_channels += count($all_av_channels);
1045 } catch (Exception $e) {
1046 // do nothing
1047 }
1048
1049 return $tot_channels;
1050 }
1051 }
1052