| 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 |
|