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 / occupancy_ranking.php

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

1,387 lines 44.9 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 * Occupancy Ranking child Class of VikBookingReport
15 */
16 class VikBookingReportOccupancyRanking extends VikBookingReport
17 {
18 /**
19 * Property 'defaultKeySort' is used by the View that renders the report.
20 */
21 public $defaultKeySort = 'occupancy';
22
23 /**
24 * Property 'defaultKeyOrder' is used by the View that renders the report.
25 */
26 public $defaultKeyOrder = 'DESC';
27
28 /**
29 * Property 'exportAllowed' is used by the View to display the export button.
30 */
31 public $exportAllowed = 1;
32
33 /**
34 * The script to render the Chart
35 */
36 protected $chartScript;
37
38 /**
39 * The current Chart title
40 */
41 protected $chartTitle;
42
43 /**
44 * The Chart meta data
45 */
46 protected $chartMetaData;
47
48 /**
49 * The Chart labels
50 */
51 protected $chartJsLabels;
52
53 /**
54 * The Chart Dataset label(s)
55 */
56 protected $chartJsDataSetLabel;
57
58 /**
59 * The Chart colors
60 */
61 protected $chartJsColors;
62
63 /**
64 * The Chart Data
65 */
66 protected $chartJsData;
67
68 /**
69 * Debug mode is activated by passing the value 'e4j_debug' > 0
70 */
71 private $debug;
72
73 /**
74 * Class constructor should define the name of the report and
75 * other vars. Call the parent constructor to define the DB object.
76 */
77 public function __construct()
78 {
79 $this->reportFile = basename(__FILE__, '.php');
80 $this->reportName = JText::translate('VBOREPORT'.strtoupper(str_replace('_', '', $this->reportFile)));
81 $this->reportFilters = array();
82
83 $this->cols = array();
84 $this->rows = array();
85 $this->footerRow = array();
86
87 $this->chartScript = '';
88 $this->chartTitle = '';
89 $this->chartMetaData = array();
90 $this->chartJsLabels = array();
91 $this->chartJsDataSetLabel = '';
92 $this->chartJsColors = array();
93 $this->chartJsData = array();
94
95 $this->debug = (VikRequest::getInt('e4j_debug', 0, 'request') > 0);
96
97 $this->registerExportCSVFileName();
98
99 parent::__construct();
100 }
101
102 /**
103 * Returns the name of this report.
104 *
105 * @return string
106 */
107 public function getName()
108 {
109 return $this->reportName;
110 }
111
112 /**
113 * Returns the name of this file without .php.
114 *
115 * @return string
116 */
117 public function getFileName()
118 {
119 return $this->reportFile;
120 }
121
122 /**
123 * Returns the filters of this report.
124 *
125 * @return array
126 */
127 public function getFilters()
128 {
129 if (count($this->reportFilters)) {
130 // do not run this method twice, as it could load JS and CSS files.
131 return $this->reportFilters;
132 }
133
134 // get VBO Application Object
135 $vbo_app = VikBooking::getVboApplication();
136
137 // load the jQuery UI Datepicker
138 $this->loadDatePicker();
139
140 // load Charts assets
141 $this->loadChartsAssets();
142
143 // from Date Filter
144 $filter_opt = array(
145 'label' => '<label for="fromdate">'.JText::translate('VBOREPORTSDATEFROM').'</label>',
146 'html' => '<input type="text" id="fromdate" name="fromdate" value="" class="vbo-report-datepicker vbo-report-datepicker-from" />',
147 'type' => 'calendar',
148 'name' => 'fromdate'
149 );
150 array_push($this->reportFilters, $filter_opt);
151
152 // to Date Filter
153 $filter_opt = array(
154 'label' => '<label for="todate">'.JText::translate('VBOREPORTSDATETO').'</label>',
155 'html' => '<input type="text" id="todate" name="todate" value="" class="vbo-report-datepicker vbo-report-datepicker-to" />',
156 'type' => 'calendar',
157 'name' => 'todate'
158 );
159 array_push($this->reportFilters, $filter_opt);
160
161 // period type filter
162 $pperiod = VikRequest::getString('period', 'month', 'request');
163 $periods = array(
164 'month' => JText::translate('VBPVIEWRESTRICTIONSTWO'),
165 'week' => JText::translate('VBOWEEK'),
166 'day' => JText::translate('VBODAY'),
167 );
168 $periods_sel_html = $vbo_app->getNiceSelect($periods, $pperiod, 'period', '', '', '', '', 'period');
169 $filter_opt = array(
170 'label' => '<label for="period">'.JText::translate('VBOGROUPBY').'</label>',
171 'html' => $periods_sel_html,
172 'type' => 'select',
173 'name' => 'period'
174 );
175 array_push($this->reportFilters, $filter_opt);
176
177 // room ID filter
178 $pidroom = VikRequest::getInt('idroom', '', 'request');
179 $all_rooms = $this->getRooms();
180 $rooms = array();
181 foreach ($all_rooms as $room) {
182 $rooms[$room['id']] = $room['name'];
183 }
184 if (count($rooms)) {
185 $rooms_sel_html = $vbo_app->getNiceSelect($rooms, $pidroom, 'idroom', JText::translate('VBOSTATSALLROOMS'), JText::translate('VBOSTATSALLROOMS'), '', '', 'idroom');
186 $filter_opt = array(
187 'label' => '<label for="idroom">'.JText::translate('VBOREPORTSROOMFILT').'</label>',
188 'html' => $rooms_sel_html,
189 'type' => 'select',
190 'name' => 'idroom'
191 );
192 array_push($this->reportFilters, $filter_opt);
193 }
194
195 // channel filter
196 $all_channels = array();
197 $pchannel = VikRequest::getString('channel', '', 'request');
198 $q = "SELECT `channel` FROM `#__vikbooking_orders` WHERE `channel` IS NOT NULL GROUP BY `channel`;";
199 $this->dbo->setQuery($q);
200 $this->dbo->execute();
201 if ($this->dbo->getNumRows()) {
202 $ord_channels = $this->dbo->loadAssocList();
203 // push website as first option
204 $all_channels['-1'] = JText::translate('VBORDFROMSITE');
205 // push all channel names
206 foreach ($ord_channels as $o_channel) {
207 $channel_parts = explode('_', $o_channel['channel']);
208 $channel_name = count($channel_parts) > 1 ? trim($channel_parts[1]) : trim($channel_parts[0]);
209 if (isset($all_channels[$channel_name])) {
210 continue;
211 }
212 $say_channel_name = $channel_name == 'googlehotel' ? 'Google Hotel' : ucwords($channel_name);
213 $all_channels[$channel_name] = $say_channel_name;
214 }
215 // push filter
216 $channels_sel_html = $vbo_app->getNiceSelect($all_channels, $pchannel, 'channel', '- - - -', '- - - -', '', '', 'channel');
217 $filter_opt = array(
218 'label' => '<label for="channel">'.JText::translate('VBCHANNELFILTER').'</label>',
219 'html' => $channels_sel_html,
220 'type' => 'select',
221 'name' => 'channel'
222 );
223 array_push($this->reportFilters, $filter_opt);
224 }
225
226 // get minimum check-in and maximum check-out for dates filters
227 $df = $this->getDateFormat();
228 $mincheckin = 0;
229 $maxcheckout = 0;
230 $q = "SELECT MIN(`checkin`) AS `mincheckin`, MAX(`checkout`) AS `maxcheckout` FROM `#__vikbooking_orders` WHERE `status`='confirmed' AND `closure`=0;";
231 $this->dbo->setQuery($q);
232 $this->dbo->execute();
233 if ($this->dbo->getNumRows()) {
234 $data = $this->dbo->loadAssoc();
235 if (!empty($data['mincheckin']) && !empty($data['maxcheckout'])) {
236 $mincheckin = $data['mincheckin'];
237 $maxcheckout = $data['maxcheckout'];
238 }
239 }
240 //
241
242 // jQuery code for the datepicker calendars and select2
243 $pfromdate = VikRequest::getString('fromdate', '', 'request');
244 $pfromdate = empty($pfromdate) && !empty($mincheckin) ? date($df, $mincheckin) : $pfromdate;
245 $ptodate = VikRequest::getString('todate', '', 'request');
246 $ptodate = empty($ptodate) && !empty($maxcheckout) ? date($df, $maxcheckout) : $ptodate;
247 $js = 'jQuery(function() {
248 jQuery(".vbo-report-datepicker:input").datepicker({
249 '.(!empty($mincheckin) ? 'minDate: "'.date($df, $mincheckin).'", ' : '').'
250 '.(!empty($maxcheckout) ? 'maxDate: "'.date($df, $maxcheckout).'", ' : '').'
251 '.(!empty($mincheckin) && !empty($maxcheckout) ? 'yearRange: "'.(date('Y', $mincheckin)).':'.date('Y', $maxcheckout).'", changeMonth: true, changeYear: true, ' : '').'
252 dateFormat: "'.$this->getDateFormat('jui').'",
253 onSelect: vboReportCheckDates
254 });
255 '.(!empty($pfromdate) ? 'jQuery(".vbo-report-datepicker-from").datepicker("setDate", "'.$pfromdate.'");' : '').'
256 '.(!empty($ptodate) ? 'jQuery(".vbo-report-datepicker-to").datepicker("setDate", "'.$ptodate.'");' : '').'
257 });
258 function vboReportCheckDates(selectedDate, inst) {
259 if (selectedDate === null || inst === null) {
260 return;
261 }
262 var cur_from_date = jQuery(this).val();
263 if (jQuery(this).hasClass("vbo-report-datepicker-from") && cur_from_date.length) {
264 var nowstart = jQuery(this).datepicker("getDate");
265 var nowstartdate = new Date(nowstart.getTime());
266 jQuery(".vbo-report-datepicker-to").datepicker("option", {minDate: nowstartdate});
267 }
268 }';
269 $this->setScript($js);
270
271 return $this->reportFilters;
272 }
273
274 /**
275 * Loads the report data from the DB.
276 * Returns true in case of success, false otherwise.
277 * Sets the columns and rows for the report to be displayed.
278 *
279 * @return boolean
280 */
281 public function getReportData()
282 {
283 if (strlen($this->getError())) {
284 // export functions may set errors rather than exiting the process, and the View may continue the execution to attempt to render the report.
285 return false;
286 }
287 // input fields and other vars
288 $pfromdate = VikRequest::getString('fromdate', '', 'request');
289 $ptodate = VikRequest::getString('todate', '', 'request');
290 $pperiod = VikRequest::getString('period', 'month', 'request');
291 // idroom can be an array of IDs or just one ID as int/string
292 $pidroom = VikRequest::getVar('idroom', null, 'request');
293 //
294 $pchannel = VikRequest::getString('channel', '', 'request');
295 $pkrsort = VikRequest::getString('krsort', $this->defaultKeySort, 'request');
296 $pkrsort = empty($pkrsort) ? $this->defaultKeySort : $pkrsort;
297 $pkrorder = VikRequest::getString('krorder', $this->defaultKeyOrder, 'request');
298 $pkrorder = empty($pkrorder) ? $this->defaultKeyOrder : $pkrorder;
299 $pkrorder = $pkrorder == 'DESC' ? 'DESC' : 'ASC';
300 // bookings max creation date
301 $pmaxdate = VikRequest::getString('maxdate', '', 'request');
302 $pmaxdate = !empty($pmaxdate) ? VikBooking::getDateTimestamp($pmaxdate, 23, 59, 59) : $pmaxdate;
303 //
304 $currency_symb = VikBooking::getCurrencySymb();
305 $df = $this->getDateFormat();
306 $datesep = VikBooking::getDateSeparator();
307 if (empty($ptodate)) {
308 $ptodate = $pfromdate;
309 }
310 // get dates timestamps
311 $from_ts = VikBooking::getDateTimestamp($pfromdate, 0, 0);
312 $to_ts = VikBooking::getDateTimestamp($ptodate, 23, 59, 59);
313 if (empty($pfromdate) || empty($from_ts) || empty($to_ts)) {
314 $this->setError(JText::translate('VBOREPORTSERRNODATES'));
315 return false;
316 }
317
318 // months map
319 $months_map = array(
320 JText::translate('VBMONTHONE'),
321 JText::translate('VBMONTHTWO'),
322 JText::translate('VBMONTHTHREE'),
323 JText::translate('VBMONTHFOUR'),
324 JText::translate('VBMONTHFIVE'),
325 JText::translate('VBMONTHSIX'),
326 JText::translate('VBMONTHSEVEN'),
327 JText::translate('VBMONTHEIGHT'),
328 JText::translate('VBMONTHNINE'),
329 JText::translate('VBMONTHTEN'),
330 JText::translate('VBMONTHELEVEN'),
331 JText::translate('VBMONTHTWELVE'),
332 );
333
334 // query to obtain the records
335 $q = "SELECT `o`.`id`,`o`.`ts`,`o`.`days`,`o`.`checkin`,`o`.`checkout`,`o`.`totpaid`,`o`.`roomsnum`,`o`.`total`,`o`.`idorderota`,`o`.`channel`,`o`.`country`,`o`.`tot_taxes`," .
336 "`o`.`tot_city_taxes`,`o`.`tot_fees`,`o`.`cmms`,`or`.`idorder`,`or`.`idroom`,`or`.`optionals`,`or`.`cust_cost`,`or`.`cust_idiva`,`or`.`extracosts`,`or`.`room_cost`,`r`.`name` AS `room_name` " .
337 "FROM `#__vikbooking_orders` AS `o` LEFT JOIN `#__vikbooking_ordersrooms` AS `or` ON `or`.`idorder`=`o`.`id` " .
338 "LEFT JOIN `#__vikbooking_rooms` AS `r` ON `or`.`idroom`=`r`.`id` " .
339 "WHERE ".(!empty($pmaxdate) ? "`o`.`ts`<={$pmaxdate} AND " : "")."`r`.`name` IS NOT NULL AND `o`.`status`='confirmed' AND `o`.`closure`=0 AND `o`.`checkout`>={$from_ts} AND `o`.`checkin`<={$to_ts} " .
340 (!empty($pidroom) && !is_array($pidroom) ? "AND `or`.`idroom`=" . (int)$pidroom . " " : (is_array($pidroom) && count($pidroom) ? "AND `or`.`idroom` IN (" . implode(', ', $pidroom) . ") " : '')) .
341 (strlen($pchannel) ? "AND `o`.`channel` " . ($pchannel == '-1' ? 'IS NULL' : "LIKE " . $this->dbo->quote("%{$pchannel}%")) . ' ' : '') .
342 "ORDER BY `o`.`checkin` ASC, `o`.`id` ASC, `or`.`id` ASC;";
343 $this->dbo->setQuery($q);
344 $records = $this->dbo->loadAssocList();
345
346 $dummy_values = false;
347 if (!count($records)) {
348 if ($pperiod != 'full') {
349 // when using the regular report interface, we display an error in case of no bookings found
350 $this->setError(JText::translate('VBOREPORTSERRNORESERV'));
351 return false;
352 }
353
354 // layout file building the chart prefers empty values rather than an error
355 $dummy_values = true;
356 // populate array with one dummy booking with empty values
357 $records = array(
358 array(
359 'id' => -1,
360 'ts' => time(),
361 'days' => 1,
362 'checkin' => $from_ts,
363 'checkout' => strtotime("+1 day", $from_ts),
364 'totpaid' => 0,
365 'roomsnum' => 1,
366 'total' => 0,
367 'idorderota' => null,
368 'channel' => null,
369 'country' => null,
370 'tot_taxes' => 0,
371 'tot_city_taxes' => 0,
372 'tot_fees' => 0,
373 'cmms' => 0,
374 'idorder' => -1,
375 'idroom' => -1,
376 'optionals' => null,
377 'cust_cost' => null,
378 'cust_idiva' => null,
379 'extracosts' => null,
380 'room_cost' => 0,
381 ),
382 );
383 }
384
385 // nest records with multiple rooms booked inside sub-array
386 $bookings = array();
387 foreach ($records as $v) {
388 if (!isset($bookings[$v['id']])) {
389 $bookings[$v['id']] = array();
390 }
391 // calculate the from_ts and to_ts values for later comparison
392 $in_info = getdate($v['checkin']);
393 $out_info = getdate($v['checkout']);
394 // these two properties are necessary for many other controls below
395 $v['from_ts'] = mktime(0, 0, 0, $in_info['mon'], $in_info['mday'], $in_info['year']);
396 $v['to_ts'] = mktime(23, 59, 59, $out_info['mon'], ($out_info['mday'] - 1), $out_info['year']);
397 //
398 array_push($bookings[$v['id']], $v);
399 }
400
401 // first day of the week for weekly periods (0 for Sunday till 6 for Saturday)
402 $firstwday = (int)VikBooking::getFirstWeekDay();
403 // we make it end to the day before as weeks should start on this weekday
404 $firstwday -= 1;
405 $firstwday = $firstwday < 0 ? 6 : $firstwday;
406
407 // build ranges of periods by looping over the dates of the report
408 $ranges = array();
409 $from_info = getdate($from_ts);
410 $to_info = getdate($to_ts);
411 $cur_month = array('from_ts' => $from_info[0]);
412 $cur_week = array('from_ts' => $from_info[0]);
413 while ($from_info[0] <= $to_info[0]) {
414 if ($pperiod == 'month') {
415 if (date('n', $from_info[0]) != date('n', $cur_month['from_ts'])) {
416 // month has changed, set to_ts to previous day at midnight
417 $cur_month['to_ts'] = mktime(23, 59, 59, $from_info['mon'], ($from_info['mday'] - 1), $from_info['year']);
418 // push month delimiter to ranges
419 array_push($ranges, array(
420 'from_ts' => $cur_month['from_ts'],
421 'to_ts' => $cur_month['to_ts'],
422 ));
423 // reset current month handler to current day (1st of the new month)
424 $cur_month = array(
425 'from_ts' => $from_info[0]
426 );
427 }
428 } elseif ($pperiod == 'week') {
429 if (!isset($cur_week['from_ts'])) {
430 // 1st day of the new week
431 $cur_week['from_ts'] = $from_info[0];
432 }
433 if ($from_info[0] != $cur_week['from_ts'] && (int)$from_info['wday'] == $firstwday) {
434 // not the first day of the loop, but same weekday, so it's the week after
435 $cur_week['to_ts'] = mktime(23, 59, 59, $from_info['mon'], $from_info['mday'], $from_info['year']);
436 // push week delimiter to ranges
437 array_push($ranges, array(
438 'from_ts' => $cur_week['from_ts'],
439 'to_ts' => $cur_week['to_ts'],
440 ));
441 // reset current week handler
442 $cur_week = array();
443 }
444 } elseif ($pperiod == 'day') {
445 // push the range until the end of the current day
446 array_push($ranges, array(
447 'from_ts' => $from_info[0],
448 'to_ts' => mktime(23, 59, 59, $from_info['mon'], $from_info['mday'], $from_info['year']),
449 ));
450 } else {
451 // (full) push the range until the "to date"
452 array_push($ranges, array(
453 'from_ts' => $from_info[0],
454 'to_ts' => $to_info[0],
455 ));
456 // do not loop any further date as we need the entire range requested
457 break;
458 }
459
460 // next day iteration
461 $from_info = getdate(mktime(0, 0, 0, $from_info['mon'], ($from_info['mday'] + 1), $from_info['year']));
462 }
463
464 // finalize ranges of period delimiters in case the loop ended on a non-precise date
465 if ($pperiod == 'month' && date('Y-m-d', $cur_month['from_ts']) != date('Y-m-d', $to_info[0])) {
466 // push last month delimiter to ranges
467 array_push($ranges, array(
468 'from_ts' => $cur_month['from_ts'],
469 'to_ts' => $to_info[0],
470 ));
471 } elseif ($pperiod == 'week' && isset($cur_week['from_ts'])) {
472 // push last week delimiter to ranges
473 array_push($ranges, array(
474 'from_ts' => $cur_week['from_ts'],
475 'to_ts' => $to_info[0],
476 ));
477 }
478
479 // total number of rooms
480 $total_rooms_units = $this->countRooms($pidroom) ?: 1;
481
482 // define the columns of the report
483 $this->cols = array(
484 // date
485 array(
486 'key' => 'day',
487 'sortable' => 1,
488 'label' => JText::translate('VBOREPORTREVENUEDAY')
489 ),
490 // rooms sold
491 array(
492 'key' => 'rooms_sold',
493 'attr' => array(
494 'class="center"'
495 ),
496 'sortable' => 1,
497 'label' => JText::translate('VBOREPORTREVENUERSOLD'),
498 'tip' => JText::sprintf('VBOREPORTTOTROOMSHELP', $total_rooms_units)
499 ),
500 // nights booked
501 array(
502 'key' => 'nights_booked',
503 'attr' => array(
504 'class="center"'
505 ),
506 'sortable' => 1,
507 'label' => JText::translate('VBOGRAPHTOTNIGHTSLBL')
508 ),
509 // total bookings
510 array(
511 'key' => 'tot_bookings',
512 'attr' => array(
513 'class="center"'
514 ),
515 'sortable' => 1,
516 'label' => JText::translate('VBOREPORTREVENUETOTB')
517 ),
518 // % occupancy
519 array(
520 'key' => 'occupancy',
521 'attr' => array(
522 'class="center"'
523 ),
524 'sortable' => 1,
525 'label' => JText::translate('VBOREPORTREVENUEPOCC')
526 ),
527 // IBE revenue
528 array(
529 'key' => 'ibe_revenue',
530 'attr' => array(
531 'class="center vbo-report-col-hideable"'
532 ),
533 'sortable' => 1,
534 'label' => JText::translate('VBOREPORTREVENUEREVWEB')
535 ),
536 // OTAs revenue
537 array(
538 'key' => 'ota_revenue',
539 'attr' => array(
540 'class="center vbo-report-col-hideable"'
541 ),
542 'sortable' => 1,
543 'label' => JText::translate('VBOREPORTREVENUEREVOTA')
544 ),
545 // ADR
546 array(
547 'key' => 'adr',
548 'attr' => array(
549 'class="center vbo-report-col-hideable"'
550 ),
551 'sortable' => 1,
552 'label' => JText::translate('VBOREPORTREVENUEADR'),
553 'tip' => JText::translate('VBOREPORTREVENUEADRHELP')
554 ),
555 // RevPAR
556 array(
557 'key' => 'revpar',
558 'attr' => array(
559 'class="center vbo-report-col-hideable"'
560 ),
561 'sortable' => 1,
562 'label' => JText::translate('VBOREPORTREVENUEREVPAR'),
563 'tip' => JText::translate('VBOREPORTREVENUEREVPARH')
564 ),
565 // Taxes
566 array(
567 'key' => 'taxes',
568 'attr' => array(
569 'class="center vbo-report-col-hideable"'
570 ),
571 'sortable' => 1,
572 'label' => JText::translate('VBOREPORTREVENUETAX')
573 ),
574 // Revenue
575 array(
576 'key' => 'revenue',
577 'attr' => array(
578 'class="center"'
579 ),
580 'sortable' => 1,
581 'label' => JText::translate('VBOREPORTREVENUEREV')
582 )
583 );
584
585 // loop over the ranges to build the rows
586 foreach ($ranges as $ind => $range) {
587 // prepare default fields for this row
588 $range_ts_from = $range['from_ts'];
589 $info_ts_from = getdate($range['from_ts']);
590 $range_ts_to = $range['to_ts'];
591 $info_ts_to = getdate($range['to_ts']);
592 $curwday_from = $this->getWdayString($info_ts_from['wday'], 'short');
593 $curwday_to = $this->getWdayString($info_ts_to['wday'], 'short');
594 $range_same_day = (date('Y-m-d', $range_ts_from) == date('Y-m-d', $range_ts_to));
595 $rooms_sold = 0;
596 $nights_booked = 0;
597 $tot_bookings = 0;
598 $occupancy = 0;
599 $ibe_revenue = 0;
600 $ota_revenue = 0;
601 $adr = 0;
602 $revpar = 0;
603 $taxes = 0;
604 $revenue = 0;
605 // count the days in this range
606 if (!isset($ranges[$ind]['days'])) {
607 $ranges[$ind]['days'] = $this->countDaysInRange($info_ts_from, $info_ts_to);
608 }
609 // maximum occupancy of this range is given by the days in the range times the total rooms units
610 $range_max_occupancy = $ranges[$ind]['days'] * $total_rooms_units;
611 $range_max_occupancy = $range_max_occupancy < 1 ? 1 : $range_max_occupancy;
612 // calculate the report details for this day
613 foreach ($bookings as $gbook) {
614 if ($dummy_values) {
615 // we need all values to be left as 0
616 break;
617 }
618 if ( // range start date is between the check-in and check-out of this booking
619 $range['from_ts'] >= $gbook[0]['from_ts'] && $range['from_ts'] <= $gbook[0]['to_ts'] ||
620 // range end date is between the check-in and check-out of this booking
621 $range['to_ts'] >= $gbook[0]['from_ts'] && $range['to_ts'] <= $gbook[0]['to_ts'] ||
622 // range start and end dates include this booking (probably a long period or a short booking)
623 $range['from_ts'] <= $gbook[0]['from_ts'] && $range['to_ts'] >= $gbook[0]['to_ts']
624 ) {
625 // this booking affects the current range of dates
626 if (!isset($ranges[$ind]['bookings'])) {
627 $ranges[$ind]['bookings'] = array();
628 }
629 array_push($ranges[$ind]['bookings'], $gbook[0]['id']);
630 // increase values
631 $rooms_sold += $gbook[0]['roomsnum'];
632 // nights booked is per rooms booked, but $booking_nights is the total nights booked per booking, not per room
633 $booking_nights = $this->countNightsBookedRange($info_ts_from, $info_ts_to, $gbook[0]);
634 $nights_booked += $booking_nights * $gbook[0]['roomsnum'];
635 $tot_bookings++;
636 // calculate net revenue and taxes
637 $tot_net = $gbook[0]['total'] - (float)$gbook[0]['tot_taxes'] - (float)$gbook[0]['tot_city_taxes'] - (float)$gbook[0]['tot_fees'] - (float)$gbook[0]['cmms'];
638 $tot_net = $tot_net / $gbook[0]['days'] * $booking_nights;
639 $revenue += $tot_net;
640 if (!empty($gbook[0]['idorderota']) && !empty($gbook[0]['channel'])) {
641 $ota_revenue += $tot_net;
642 } else {
643 $ibe_revenue += $tot_net;
644 }
645 $tot_taxes = ((float)$gbook[0]['tot_taxes'] + (float)$gbook[0]['tot_city_taxes'] + (float)$gbook[0]['tot_fees'] + (float)$gbook[0]['cmms']) / $gbook[0]['days'] * $booking_nights;
646 $taxes += $tot_taxes;
647 }
648 }
649 $occupancy = round(($nights_booked * 100 / $range_max_occupancy), 2);
650 $adr = $rooms_sold > 0 ? $revenue / $rooms_sold : 0;
651 $revpar = $total_rooms_units > 0 ? ($revenue / $total_rooms_units) : 0;
652 // push fields in the rows array as a new row
653 array_push($this->rows, array(
654 array(
655 'key' => 'day',
656 'callback' => function ($val) use ($range_ts_to, $df, $datesep, $curwday_from, $curwday_to, $pperiod, $months_map, $range_same_day) {
657 if ($pperiod == 'day' || $range_same_day) {
658 return $curwday_from . ', ' . date(str_replace("/", $datesep, $df), $val);
659 }
660 if (($pperiod == 'month' || $pperiod == 'full') && date('d', $val) == '1' && date('t', $range_ts_to) == date('d', $range_ts_to) && date('m', $val) == date('m', $range_ts_to)) {
661 // full month
662 return $months_map[((int)date('m', $val) - 1)] . ' ' . date('Y', $val);
663 }
664 return $curwday_from . ', ' . date(str_replace("/", $datesep, $df), $val) . ' - ' . $curwday_to . ', ' . date(str_replace("/", $datesep, $df), $range_ts_to);
665 },
666 'value' => $range_ts_from
667 ),
668 array(
669 'key' => 'rooms_sold',
670 'attr' => array(
671 'class="center"'
672 ),
673 'value' => $rooms_sold
674 ),
675 array(
676 'key' => 'nights_booked',
677 'attr' => array(
678 'class="center"'
679 ),
680 'callback' => function ($val) use ($range_max_occupancy) {
681 return $val . ' / ' . $range_max_occupancy;
682 },
683 'value' => $nights_booked
684 ),
685 array(
686 'key' => 'tot_bookings',
687 'attr' => array(
688 'class="center"'
689 ),
690 'value' => $tot_bookings
691 ),
692 array(
693 'key' => 'occupancy',
694 'attr' => array(
695 'class="center"'
696 ),
697 'value' => $occupancy
698 ),
699 array(
700 'key' => 'ibe_revenue',
701 'attr' => array(
702 'class="center vbo-report-col-hideable"'
703 ),
704 'callback' => function ($val) use ($currency_symb) {
705 return $currency_symb.' '.VikBooking::numberFormat($val);
706 },
707 'value' => $ibe_revenue
708 ),
709 array(
710 'key' => 'ota_revenue',
711 'attr' => array(
712 'class="center vbo-report-col-hideable"'
713 ),
714 'callback' => function ($val) use ($currency_symb) {
715 return $currency_symb.' '.VikBooking::numberFormat($val);
716 },
717 'value' => $ota_revenue
718 ),
719 array(
720 'key' => 'adr',
721 'attr' => array(
722 'class="center vbo-report-col-hideable"'
723 ),
724 'callback' => function ($val) use ($currency_symb) {
725 return $currency_symb.' '.VikBooking::numberFormat($val);
726 },
727 'value' => $adr
728 ),
729 array(
730 'key' => 'revpar',
731 'attr' => array(
732 'class="center vbo-report-col-hideable"'
733 ),
734 'callback' => function ($val) use ($currency_symb) {
735 return $currency_symb.' '.VikBooking::numberFormat($val);
736 },
737 'value' => $revpar
738 ),
739 array(
740 'key' => 'taxes',
741 'attr' => array(
742 'class="center vbo-report-col-hideable"'
743 ),
744 'callback' => function ($val) use ($currency_symb) {
745 return $currency_symb.' '.VikBooking::numberFormat($val);
746 },
747 'value' => $taxes
748 ),
749 array(
750 'key' => 'revenue',
751 'attr' => array(
752 'class="center"'
753 ),
754 'callback' => function ($val) use ($currency_symb) {
755 return $currency_symb.' '.VikBooking::numberFormat($val);
756 },
757 'value' => $revenue
758 )
759 ));
760 }
761
762 // sort rows
763 $this->sortRows($pkrsort, $pkrorder);
764
765 // update sorting and ordering key
766 $this->defaultKeySort = $pkrsort;
767 $this->defaultKeyOrder = $pkrorder;
768
769 // loop over the rows to build the footer row with the totals
770 $foot_rooms_sold = 0;
771 $foot_nights_booked = 0;
772 $foot_tot_bookings = 0;
773 $foot_ibe_revenue = 0;
774 $foot_ota_revenue = 0;
775 $foot_taxes = 0;
776 $foot_revenue = 0;
777 foreach ($this->rows as $row) {
778 $foot_rooms_sold += $row[1]['value'];
779 $foot_nights_booked += $row[2]['value'];
780 $foot_tot_bookings += $row[3]['value'];
781 $foot_ibe_revenue += $row[5]['value'];
782 $foot_ota_revenue += $row[6]['value'];
783 $foot_taxes += $row[9]['value'];
784 $foot_revenue += $row[10]['value'];
785 }
786 array_push($this->footerRow, array(
787 array(
788 'attr' => array(
789 'class="vbo-report-total"'
790 ),
791 'value' => '<h3>'.JText::translate('VBOREPORTSTOTALROW').'</h3>'
792 ),
793 array(
794 'attr' => array(
795 'class="center"'
796 ),
797 'value' => $foot_rooms_sold
798 ),
799 array(
800 'attr' => array(
801 'class="center"'
802 ),
803 'value' => $foot_nights_booked
804 ),
805 array(
806 'attr' => array(
807 'class="center"'
808 ),
809 'value' => $foot_tot_bookings
810 ),
811 array(
812 'value' => ''
813 ),
814 array(
815 'attr' => array(
816 'class="center vbo-report-col-hideable"'
817 ),
818 'callback' => function ($val) use ($currency_symb) {
819 return $currency_symb.' '.VikBooking::numberFormat($val);
820 },
821 'value' => $foot_ibe_revenue
822 ),
823 array(
824 'attr' => array(
825 'class="center vbo-report-col-hideable"'
826 ),
827 'callback' => function ($val) use ($currency_symb) {
828 return $currency_symb.' '.VikBooking::numberFormat($val);
829 },
830 'value' => $foot_ota_revenue
831 ),
832 array(
833 'attr' => array(
834 'class="center vbo-report-col-hideable"'
835 ),
836 'value' => ''
837 ),
838 array(
839 'attr' => array(
840 'class="center vbo-report-col-hideable"'
841 ),
842 'value' => ''
843 ),
844 array(
845 'attr' => array(
846 'class="center vbo-report-col-hideable"'
847 ),
848 'callback' => function ($val) use ($currency_symb) {
849 return $currency_symb.' '.VikBooking::numberFormat($val);
850 },
851 'value' => $foot_taxes
852 ),
853 array(
854 'attr' => array(
855 'class="center"'
856 ),
857 'callback' => function ($val) use ($currency_symb) {
858 return $currency_symb.' '.VikBooking::numberFormat($val);
859 },
860 'value' => $foot_revenue
861 )
862 ));
863
864 // debug data
865 $debug_str = 'Periods COUNT = ' . count($ranges) . '<br/>';
866 foreach ($ranges as $range) {
867 $debug_str .= date('D, Y-m-d', $range['from_ts']) . ' - ' . date('D, Y-m-d', $range['to_ts']) . (isset($range['days']) ? ' ('.$range['days'].'d)' : '') . (isset($range['bookings']) ? ' - ' . implode(', ', $range['bookings']) : '') . '<br/>';
868 }
869 $debug_str .= '<br/><pre>' . print_r($bookings, true) . '</pre><br/>';
870 $debug_str .= 'Total Room Units = ' . $total_rooms_units . '<br/>';
871 if ($this->debug) {
872 $this->setWarning($debug_str);
873 $this->setWarning('path to report file = '.urlencode(dirname(__FILE__)).'<br/>');
874 $this->setWarning('$total_rooms_units = '.$total_rooms_units.'<br/>');
875 $this->setWarning('$bookings:<pre>'.print_r($bookings, true).'</pre><br/>');
876 }
877
878 return true;
879 }
880
881 /**
882 * Counts the number of nights booked by the
883 * given booking in the given range of dates.
884 *
885 * @param array $from_info the getdate() info of the range start date timestamp.
886 * @param array $to_info the getdate() info of the range end date timestamp.
887 * @param array $booking the booking array (one room record).
888 *
889 * @return int the total number of nights booked in the given range.
890 */
891 private function countNightsBookedRange($from_info, $to_info, $booking)
892 {
893 $tot_nights = 0;
894 if (!is_array($from_info) || !is_array($to_info) || $from_info[0] > $to_info[0]) {
895 return $tot_nights;
896 }
897
898 $checkout_ymd = date('Y-m-d', $booking['checkout']);
899
900 while ($from_info[0] <= $to_info[0]) {
901 if ($from_info[0] >= $booking['from_ts'] && $from_info[0] <= $booking['to_ts']) {
902 // range day is inside booking dates
903 if (date('Y-m-d', $from_info[0]) == $checkout_ymd) {
904 // this is the check-out day, so it is not a night booked
905 return $tot_nights;
906 }
907 $tot_nights++;
908 }
909 // next date
910 $from_info = getdate(mktime(0, 0, 0, $from_info['mon'], ($from_info['mday'] + 1), $from_info['year']));
911 }
912
913 return $tot_nights;
914 }
915
916 /**
917 * Counts the number of nights in the given range of dates.
918 * End date of range is always inclusive for the bookings.
919 *
920 * @param array $from_info the getdate() info of the range start date timestamp.
921 * @param array $to_info the getdate() info of the range end date timestamp.
922 *
923 * @return int the total number of days in the given range.
924 */
925 private function countDaysInRange($from_info, $to_info)
926 {
927 $tot_days = 0;
928 if (!is_array($from_info) || !is_array($to_info) || $from_info[0] > $to_info[0]) {
929 return $tot_days;
930 }
931
932 if (date('Y-m-d', $from_info[0]) == date('Y-m-d', $to_info[0])) {
933 return 1;
934 }
935
936 while ($from_info[0] <= $to_info[0]) {
937 $tot_days++;
938
939 // next date
940 $from_info = getdate(mktime(0, 0, 0, $from_info['mon'], ($from_info['mday'] + 1), $from_info['year']));
941 }
942
943 return $tot_days;
944
945 }
946
947 /**
948 * Registers the name to give to the CSV file being exported.
949 *
950 * @return void
951 *
952 * @since 1.16.1 (J) - 1.6.1 (WP)
953 */
954 private function registerExportCSVFileName()
955 {
956 $pfromdate = VikRequest::getString('fromdate', '', 'request');
957 $ptodate = VikRequest::getString('todate', '', 'request');
958
959 $report_extraname = '';
960 $pchannel = VikRequest::getString('channel', '', 'request');
961 if (strlen($pchannel)) {
962 // set channel name for exported file
963 if ($pchannel == '-1') {
964 $report_extraname = JText::translate('VBORDFROMSITE');
965 } else {
966 $report_extraname = $pchannel;
967 }
968 }
969
970 $this->setExportCSVFileName($this->reportName . (!empty($report_extraname) ? '-' . $report_extraname : '') . '-' . str_replace('/', '_', $pfromdate) . '-' . str_replace('/', '_', $ptodate) . '.csv');
971 }
972
973 /**
974 * Returns the Chart title.
975 *
976 * @return string the title of the Chart
977 */
978 public function getChartTitle()
979 {
980 $name_parts = array($this->reportName, $this->chartTitle);
981
982 $pchannel = VikRequest::getString('channel', '', 'request');
983 if (strlen($pchannel)) {
984 // push channel name in report
985 if ($pchannel == '-1') {
986 array_push($name_parts, JText::translate('VBORDFROMSITE'));
987 } else {
988 array_push($name_parts, $pchannel);
989 }
990 }
991
992 return implode(' - ', $name_parts);
993 }
994
995 /**
996 * This report can render a Chart. Main method called to whoever
997 * needs to access the Chart data rendered by this report.
998 * Defines some properties and accepts instructions through the arg.
999 *
1000 * @param ?array $data null or mixed for requested Chart data.
1001 *
1002 * @return string the HTML of the canvas element.
1003 */
1004 public function getChart(?array $data = null)
1005 {
1006 if (!count($this->rows) && !$this->getReportData()) {
1007 return '';
1008 }
1009
1010 // find the dataset label depending on the active sorting (i.e. "occupancy")
1011 $dataset_label = '';
1012 foreach ($this->cols as $col) {
1013 if ($col['key'] == $this->defaultKeySort) {
1014 $dataset_label = $col['label'];
1015 break;
1016 }
1017 }
1018 if (empty($dataset_label)) {
1019 // ordering column label not found
1020 return '';
1021 }
1022 // set Chart title
1023 $this->chartTitle = $dataset_label;
1024
1025 // push data values for the requested key depending on the active sorting
1026 $chart_labels = array();
1027 $chart_data = array();
1028 $chart_indexes = array();
1029 $counter = 0;
1030 $max_points = 10;
1031 foreach ($this->rows as $row) {
1032 foreach ($row as $ind => $field) {
1033 if ($counter >= $max_points) {
1034 break 2;
1035 }
1036 if ($ind === 0) {
1037 // make sure this field is not the same as the active sorting
1038 if ($field['key'] == $this->defaultKeySort) {
1039 // we cannot build the Chart, or the X and Y would have the same values
1040 return '';
1041 }
1042 // the first column is the label
1043 array_push($chart_labels, (isset($field['callback']) && is_callable($field['callback']) ? $field['callback']($field['value']) : $field['value']));
1044 // save the raw value for this index
1045 array_push($chart_indexes, $field['value']);
1046 }
1047 if ($field['key'] != $this->defaultKeySort) {
1048 // we do not care about this column
1049 continue;
1050 }
1051 array_push($chart_data, $field['value']);
1052 $counter++;
1053 }
1054 }
1055 if (!count($chart_labels) || count($chart_labels) != count($chart_data)) {
1056 // missing or invalid chart values
1057 return '';
1058 }
1059
1060 // check whether a slice of the data was requested through the depth property
1061 if (is_array($data) && isset($data['depth']) && $data['depth'] > 0 && count($chart_labels) >= $data['depth']) {
1062 // get a slice of the labels and data
1063 $chart_labels = array_slice($chart_labels, 0, $data['depth']);
1064 $chart_data = array_slice($chart_data, 0, $data['depth']);
1065 } else {
1066 // sort labels and data by time ascending, to obtain a readable line chart
1067 asort($chart_indexes);
1068 $sorted_labels = array();
1069 $sorted_data = array();
1070 foreach ($chart_indexes as $k => $v) {
1071 array_push($sorted_labels, $chart_labels[$k]);
1072 array_push($sorted_data, $chart_data[$k]);
1073 }
1074 $chart_labels = $sorted_labels;
1075 $chart_data = $sorted_data;
1076 }
1077
1078 // the canvas element ID and tag
1079 $canvas_id = 'vbo-report-chart-canvas';
1080 $canvas_html = '<canvas id="' . $canvas_id . '" class="vbo-report-chart-canvas"></canvas>';
1081
1082 // additional Chart properties
1083 $chart_type = is_array($data) && !empty($data['type']) ? $data['type'] : 'line';
1084 $chart_colors = array(
1085 'backgroundColor' => 'rgba(34,72,93,0.2)',
1086 'borderColor' => 'rgba(34,72,93,1)',
1087 'pieBackgroundColor' => '["rgba(34,72,93,1)"]',
1088 'pieHoverBorderColor' => '["rgba(34,72,93,0.9)"]',
1089 );
1090
1091 // add a new label for the "free rooms" if pie, count = 1 and occupancy filtering
1092 $pie_tooltip_format = '';
1093 if ($chart_type != 'line' && count($chart_data) === 1 && is_array($data) && isset($data['keys'])) {
1094 if (((is_array($data['keys']) && in_array('occupancy', $data['keys'])) || $data['keys'] == 'occupancy')) {
1095 // push "free rooms" label and data to complete the doughnut chart
1096 $chart_labels = array(
1097 JText::translate('VBOROOMSOCCUPANCY'),
1098 JText::translate('VBOROOMSUNSOLD'),
1099 );
1100 array_push($chart_data, round((100 - $chart_data[0]), 2));
1101 $chart_colors['pieBackgroundColor'] = json_encode(array(
1102 'rgba(34,72,93,1)',
1103 'rgba(77,152,198,1)',
1104 ));
1105 $chart_colors['pieHoverBorderColor'] = json_encode(array(
1106 'rgba(34,72,93,0.9)',
1107 'rgba(77,152,198,0.9)',
1108 ));
1109 $pie_tooltip_format = ' + " %"';
1110 }
1111 }
1112
1113 /**
1114 * Set some Chart properties that can be accessed through getProperty()
1115 *
1116 * @see getProperty()
1117 */
1118 $this->chartJsLabels = $chart_labels;
1119 $this->chartJsDataSetLabel = $dataset_label;
1120 $this->chartJsColors = $chart_colors;
1121 $this->chartJsData = $chart_data;
1122 //
1123
1124 if (!empty($this->chartScript)) {
1125 // the script has already been set, return just the HTML
1126 return $canvas_html;
1127 }
1128
1129 // prepare the necessary script to render the Chart
1130 $this->chartScript .= 'jQuery(function() {' . "\n";
1131 $this->chartScript .= 'var vbo_report_ctx = document.getElementById("' . $canvas_id . '").getContext("2d");' . "\n";
1132 $this->chartScript .= '
1133 var vboReportType = "' . $chart_type . '";
1134 var vboReportLineData = {
1135 labels: ' . json_encode($this->chartJsLabels) . ',
1136 datasets: [{
1137 label: "' . addslashes($this->chartJsDataSetLabel) . '",
1138 backgroundColor: "' . $this->chartJsColors['backgroundColor'] . '",
1139 borderColor: "' . $this->chartJsColors['borderColor'] . '",
1140 data: ' . json_encode($this->chartJsData) . ',
1141 }],
1142 };
1143 // since we are inside a callback, the var vboReportPieData must declared globally
1144 window[\'vboReportPieData\'] = {
1145 labels: ' . json_encode($this->chartJsLabels) . ',
1146 datasets: [{
1147 label: "' . addslashes($this->chartJsDataSetLabel) . '",
1148 backgroundColor: ' . $this->chartJsColors['pieBackgroundColor'] . ',
1149 hoverBorderColor: ' . $this->chartJsColors['pieHoverBorderColor'] . ',
1150 data: ' . json_encode($this->chartJsData) . ',
1151 }],
1152 };
1153 var vboReportLineOptions = {
1154 responsive: true,
1155 legend: {
1156 display: false,
1157 },
1158 legendCallback: function (chart) {
1159 // Return the HTML string here.
1160 var text = [];
1161 text.push("<ul class=\"chart-line-legend\">");
1162 for (var i = 0; i < chart.data.datasets.length; i++) {
1163 text.push("<li>");
1164 text.push("<span class=\"legend-entry\" style=\"background-color: " + chart.data.datasets[i].backgroundColor + "\"></span>");
1165 text.push("<span class=\"legend-label\">" + chart.data.datasets[i].label + "</span>");
1166 text.push("</li>");
1167 }
1168 text.push("</ul>");
1169 return text.join("");
1170 },
1171 };
1172 var vboReportPieOptions = {
1173 responsive: true,
1174 legend: {
1175 display: false,
1176 },
1177 legendCallback: function (chart) {
1178 // Return the HTML string here.
1179 var text = [];
1180 text.push("<ul class=\"chart-line-legend chart-pie-legend\">");
1181 for (var i = 0; i < chart.data.labels.length; i++) {
1182 text.push("<li>");
1183 text.push("<span class=\"legend-entry\" style=\"background-color: " + chart.data.datasets[0].backgroundColor[i] + "\"></span>");
1184 text.push("<span class=\"legend-label\">" + chart.data.labels[i] + "</span>");
1185 text.push("</li>");
1186 }
1187 text.push("</ul>");
1188 return text.join("");
1189 },
1190 tooltips: {
1191 callbacks: {
1192 // format the tooltip text displayed when hovering a point
1193 label: function(tooltipItem, data) {
1194 // keep default label
1195 var label = data.labels[tooltipItem.index] || "";
1196 if (label) {
1197 label += ": ";
1198 }
1199 label += data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index]' . $pie_tooltip_format . ';
1200 return " " + label;
1201 },
1202 },
1203 },
1204 };
1205 var vboReportChart = new Chart(vbo_report_ctx, {
1206 type: vboReportType,
1207 data: (vboReportType == "line" ? vboReportLineData : vboReportPieData),
1208 options: (vboReportType == "line" ? vboReportLineOptions : vboReportPieOptions),
1209 });
1210 jQuery("#' . $canvas_id . '").parent().append(vboReportChart.generateLegend());
1211 jQuery("#' . $canvas_id . '").on("vbo_update_report_chart", function() {
1212 jQuery(".chart-line-legend").remove();
1213 vboReportChart.update();
1214 jQuery("#' . $canvas_id . '").parent().append(vboReportChart.generateLegend());
1215 });';
1216 $this->chartScript .= '});' . "\n";
1217
1218 // set the necessary script
1219 $this->setScript($this->chartScript);
1220
1221 // return the HTML to render the chart
1222 return $canvas_html;
1223 }
1224
1225 /**
1226 * Returns an array of information (meta boxes) about the Chart.
1227 * Information can be filtered by different positions.
1228 *
1229 * @param mixed $position null, top, right or bottom.
1230 * @param mixed $data null or an associative array of values.
1231 *
1232 * @return array the list of meta data for the position.
1233 */
1234 public function getChartMetaData($position = null, $data = null)
1235 {
1236 if (!count($this->rows) && !$this->getReportData()) {
1237 return array();
1238 }
1239
1240 if (!count($this->chartMetaData)) {
1241 // prepare the meta data only once
1242 $this->generateChartMetaData($data);
1243 }
1244
1245 if (!empty($position)) {
1246 return isset($this->chartMetaData[$position]) ? $this->chartMetaData[$position] : array();
1247 }
1248
1249 if (!count($this->chartMetaData['top']) && !count($this->chartMetaData['right']) && !count($this->chartMetaData['bottom'])) {
1250 // no positions requested and no count, return an empty array
1251 return array();
1252 }
1253
1254 return $this->chartMetaData;
1255 }
1256
1257 /**
1258 * Prepares the Chart meta data.
1259 *
1260 * @param mixed $data null or an associative array of values.
1261 *
1262 * @return void
1263 */
1264 private function generateChartMetaData($data = null)
1265 {
1266 // reset container
1267 $this->chartMetaData = array();
1268
1269 if (!count($this->rows)) {
1270 return;
1271 }
1272
1273 // whether custom data has been requested
1274 $is_custom_data = (is_array($data) && isset($data['keys']) && is_array($data['keys']));
1275 $good_threshold = is_array($data) && isset($data['threshold']) ? (int)$data['threshold'] : 60;
1276
1277 // currency symbol to format some data
1278 $currency_symb = VikBooking::getCurrencySymb();
1279
1280 // all meta box containers
1281 $meta_top = array();
1282 $meta_right = array();
1283 $meta_bottom = array();
1284
1285 // collect data
1286 $dates_pool = array();
1287 $revenue_pool = array();
1288 $tbookings_pool = array();
1289 $occupancy_pool = array();
1290 $nightsbkd_pool = array();
1291 $nightsbkd_totl = array();
1292 foreach ($this->rows as $ind => $row) {
1293 foreach ($row as $field) {
1294 if ($field['key'] == 'day') {
1295 $dates_pool[$ind] = (isset($field['callback']) && is_callable($field['callback']) ? $field['callback']($field['value']) : $field['value']);
1296 } elseif ($field['key'] == 'revenue') {
1297 $revenue_pool[$ind] = $field['value'];
1298 } elseif ($field['key'] == 'tot_bookings') {
1299 $tbookings_pool[$ind] = $field['value'];
1300 } elseif ($field['key'] == 'occupancy') {
1301 $occupancy_pool[$ind] = $field['value'];
1302 } elseif ($field['key'] == 'nights_booked') {
1303 $nightsbkd_pool[$ind] = $field['value'];
1304 $nightsbkd_totl[$ind] = (isset($field['callback']) && is_callable($field['callback']) ? $field['callback']($field['value']) : $field['value']);
1305 }
1306 }
1307 }
1308
1309 // get min/max data
1310 $min_occ = min($occupancy_pool);
1311 $max_occ = max($occupancy_pool);
1312 $min_occ_dt = $dates_pool[array_search($min_occ, $occupancy_pool)];
1313 $max_occ_dt = $dates_pool[array_search($max_occ, $occupancy_pool)];
1314 $min_tbook = min($tbookings_pool);
1315 $max_tbook = max($tbookings_pool);
1316 $min_tbook_dt = $dates_pool[array_search($min_tbook, $tbookings_pool)];
1317 $max_tbook_dt = $dates_pool[array_search($max_tbook, $tbookings_pool)];
1318 $max_revenue = max($revenue_pool);
1319 $max_revenue_dt = $dates_pool[array_search($max_revenue, $revenue_pool)];
1320 $max_nightsbk = max($nightsbkd_pool);
1321 $max_nightsbk_dt = $nightsbkd_pool[array_search($max_nightsbk, $nightsbkd_pool)];
1322 // override max nights booked with value formatted by the callback
1323 $max_nightsbk = $nightsbkd_totl[array_search($max_nightsbk, $nightsbkd_pool)];
1324 //
1325
1326 // populate Chart meta boxes
1327 $occ_lbl = trim(str_replace('%', '', JText::translate('VBOREPORTREVENUEPOCC')));
1328 array_push($meta_top, array(
1329 'key' => 'occupancy',
1330 'label' => $occ_lbl,
1331 'value' => $max_occ . ' %',
1332 'class' => ($is_custom_data && $max_occ < $good_threshold ? 'vbo-report-chart-meta-min' : 'vbo-report-chart-meta-max'),
1333 'descr' => ($is_custom_data ? '' : $max_occ_dt),
1334 ));
1335 if ($min_occ != $max_occ) {
1336 array_push($meta_top, array(
1337 'key' => 'occupancy',
1338 'label' => $occ_lbl,
1339 'value' => $min_occ . ' %',
1340 'class' => 'vbo-report-chart-meta-min',
1341 'descr' => ($is_custom_data ? '' : $min_occ_dt),
1342 ));
1343 }
1344 array_push($meta_right, array(
1345 'key' => 'tot_bookings',
1346 'label' => JText::translate('VBOREPORTREVENUETOTB'),
1347 'value' => $max_tbook,
1348 'class' => ($is_custom_data && $max_occ < $good_threshold ? 'vbo-report-chart-meta-min' : 'vbo-report-chart-meta-max'),
1349 'descr' => ($is_custom_data ? '' : $max_tbook_dt),
1350 ));
1351 if ($min_tbook != $max_tbook) {
1352 array_push($meta_right, array(
1353 'key' => 'tot_bookings',
1354 'label' => JText::translate('VBOREPORTREVENUETOTB'),
1355 'value' => $min_tbook,
1356 'class' => 'vbo-report-chart-meta-min',
1357 'descr' => ($is_custom_data ? '' : $min_tbook_dt),
1358 ));
1359 }
1360 if (!is_array($data) || ($is_custom_data && in_array('revenue', $data['keys']))) {
1361 array_push($meta_bottom, array(
1362 'key' => 'revenue',
1363 'label' => JText::translate('VBOREPORTREVENUEREV'),
1364 'value' => $currency_symb . ' ' . VikBooking::numberFormat($max_revenue),
1365 'class' => 'vbo-report-chart-meta-max',
1366 'descr' => ($is_custom_data ? '' : $max_revenue_dt),
1367 ));
1368 }
1369 if ($is_custom_data && in_array('nights_booked', $data['keys'])) {
1370 array_push($meta_bottom, array(
1371 'key' => 'nights_booked',
1372 'label' => JText::translate('VBOGRAPHTOTNIGHTSLBL'),
1373 'value' => $max_nightsbk,
1374 'class' => ($is_custom_data && $max_occ < $good_threshold ? 'vbo-report-chart-meta-min' : 'vbo-report-chart-meta-max'),
1375 'descr' => ($is_custom_data ? '' : $max_nightsbk_dt),
1376 ));
1377 }
1378
1379 // build container for all positions
1380 $this->chartMetaData = array(
1381 'top' => $meta_top,
1382 'right' => $meta_right,
1383 'bottom' => $meta_bottom,
1384 );
1385 }
1386 }
1387