| 1 |
<?php |
| 2 |
/** |
| 3 |
* @package VikBooking |
| 4 |
* @subpackage com_vikbooking |
| 5 |
* @author Alessio Gaggii - E4J srl |
| 6 |
* @copyright Copyright (C) 2026 E4J srl. 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 |
* RMS Booking Pace report implementation. |
| 15 |
* |
| 16 |
* @since 1.18.6 (J) - 1.8.6 (WP) |
| 17 |
*/ |
| 18 |
class VikBookingReportRmsBookingPace extends VikBookingReport |
| 19 |
{ |
| 20 |
/** |
| 21 |
* Property 'defaultKeySort' is used by the View that renders the report. |
| 22 |
*/ |
| 23 |
public $defaultKeySort = 'line'; |
| 24 |
|
| 25 |
/** |
| 26 |
* Property 'defaultKeyOrder' is used by the View that renders the report. |
| 27 |
*/ |
| 28 |
public $defaultKeyOrder = 'ASC'; |
| 29 |
|
| 30 |
/** |
| 31 |
* Property 'exportAllowed' is used by the View to display the export button. |
| 32 |
*/ |
| 33 |
public $exportAllowed = 1; |
| 34 |
|
| 35 |
/** |
| 36 |
* @var array |
| 37 |
*/ |
| 38 |
protected array $paceOptions = []; |
| 39 |
|
| 40 |
/** |
| 41 |
* Class constructor will define some default properties. |
| 42 |
*/ |
| 43 |
public function __construct() |
| 44 |
{ |
| 45 |
$this->reportFile = basename(__FILE__, '.php'); |
| 46 |
$this->reportName = JText::translate('VBO_RMS_BOOKING_PACE'); |
| 47 |
$this->reportFilters = []; |
| 48 |
|
| 49 |
$this->cols = []; |
| 50 |
$this->rows = []; |
| 51 |
$this->footerRow = []; |
| 52 |
|
| 53 |
$this->registerExportCSVFileName(); |
| 54 |
} |
| 55 |
|
| 56 |
/** |
| 57 |
* @inheritDoc |
| 58 |
*/ |
| 59 |
public function preflight() |
| 60 |
{ |
| 61 |
return class_exists('VikChannelManagerConfig'); |
| 62 |
} |
| 63 |
|
| 64 |
/** |
| 65 |
* Returns the name of this report. |
| 66 |
* |
| 67 |
* @return string |
| 68 |
*/ |
| 69 |
public function getName() |
| 70 |
{ |
| 71 |
return $this->reportName; |
| 72 |
} |
| 73 |
|
| 74 |
/** |
| 75 |
* Returns the name of this file without .php. |
| 76 |
* |
| 77 |
* @return string |
| 78 |
*/ |
| 79 |
public function getFileName() |
| 80 |
{ |
| 81 |
return $this->reportFile; |
| 82 |
} |
| 83 |
|
| 84 |
/** |
| 85 |
* Returns the filters of this report. |
| 86 |
* |
| 87 |
* @return array |
| 88 |
*/ |
| 89 |
public function getFilters() |
| 90 |
{ |
| 91 |
if ($this->reportFilters) { |
| 92 |
// do not run this method twice, as it could load JS and CSS files. |
| 93 |
return $this->reportFilters; |
| 94 |
} |
| 95 |
|
| 96 |
$app = JFactory::getApplication(); |
| 97 |
$dbo = JFactory::getDbo(); |
| 98 |
|
| 99 |
// get VBO Application Object |
| 100 |
$vbo_app = VikBooking::getVboApplication(); |
| 101 |
|
| 102 |
// load the jQuery UI Datepicker |
| 103 |
$this->loadDatePicker(); |
| 104 |
|
| 105 |
// load Charts assets |
| 106 |
$this->loadChartsAssets(); |
| 107 |
|
| 108 |
// default date format |
| 109 |
$df = $this->getDateFormat(); |
| 110 |
|
| 111 |
// obtain min check-in and max check-out timestamps |
| 112 |
$dbo->setQuery( |
| 113 |
$dbo->getQuery(true) |
| 114 |
->select([ |
| 115 |
'MIN(' . $dbo->qn('ts') . ') AS ' . $dbo->qn('mints'), |
| 116 |
'MIN(' . $dbo->qn('checkin') . ') AS ' . $dbo->qn('mincheckin'), |
| 117 |
'MAX(' . $dbo->qn('checkout') . ') AS ' . $dbo->qn('maxcheckout'), |
| 118 |
]) |
| 119 |
->from($dbo->qn('#__vikbooking_orders')) |
| 120 |
->where($dbo->qn('status') . ' = ' . $dbo->q('confirmed')) |
| 121 |
->where($dbo->qn('closure') . ' = 0') |
| 122 |
); |
| 123 |
$data = $dbo->loadAssoc(); |
| 124 |
$mintsdt = date($df, $data['mints'] ?? time()); |
| 125 |
$mincheckindt = date($df, $data['mincheckin'] ?? time()); |
| 126 |
$maxcheckoutdt = date($df, $data['maxcheckout'] ?? time()); |
| 127 |
$minyearts = date('Y', $data['mints'] ?? time()); |
| 128 |
$maxyearts = date('Y'); |
| 129 |
$minyear = date('Y', $data['mincheckin'] ?? time()); |
| 130 |
$maxyear = date('Y', $data['maxcheckout'] ?? time()); |
| 131 |
|
| 132 |
// start building filters |
| 133 |
$this->reportFilters = [ |
| 134 |
// pickup date from |
| 135 |
[ |
| 136 |
'label' => '<label for="pickup_date_from">' . JText::translate('VBO_PICKUP_DATE_START') . '</label>', |
| 137 |
'html' => '<input type="text" id="pickup_date_from" name="pickup_date_from" value="' . date(str_replace('d', '01', $df), strtotime('-1 month')) . '" class="vbo-report-datepicker vbo-report-datepicker-pickupdt vbo-report-datepicker-pickup-date-from" />', |
| 138 |
'type' => 'calendar', |
| 139 |
'name' => 'pickup_date_from', |
| 140 |
'default' => date(str_replace('d', '01', $df), strtotime('-1 month')), |
| 141 |
], |
| 142 |
// pickup date to |
| 143 |
[ |
| 144 |
'label' => '<label for="pickup_date_to">' . JText::translate('VBO_PICKUP_DATE_END') . '</label>', |
| 145 |
'html' => '<input type="text" id="pickup_date_to" name="pickup_date_to" value="' . date($df) . '" class="vbo-report-datepicker vbo-report-datepicker-pickupdt vbo-report-datepicker-pickup-date-to" />', |
| 146 |
'type' => 'calendar', |
| 147 |
'name' => 'pickup_date_to', |
| 148 |
'default' => date($df), |
| 149 |
], |
| 150 |
// target date from |
| 151 |
[ |
| 152 |
'label' => '<label for="target_date_from">' . JText::translate('VBO_TARGET_DATE_START') . '</label>', |
| 153 |
'html' => '<input type="text" id="target_date_from" name="target_date_from" value="' . date(str_replace('d', '01', $df), strtotime('+3 months')) . '" class="vbo-report-datepicker vbo-report-datepicker-targetdt vbo-report-datepicker-target-date-from" />', |
| 154 |
'type' => 'calendar', |
| 155 |
'name' => 'target_date_from', |
| 156 |
'default' => date(str_replace('d', '01', $df), strtotime('+3 months')), |
| 157 |
], |
| 158 |
// target date to |
| 159 |
[ |
| 160 |
'label' => '<label for="target_date_to">' . JText::translate('VBO_TARGET_DATE_END') . '</label>', |
| 161 |
'html' => '<input type="text" id="target_date_to" name="target_date_to" value="' . date(str_replace('d', 't', $df), strtotime('+3 months')) . '" class="vbo-report-datepicker vbo-report-datepicker-targetdt vbo-report-datepicker-target-date-to" />', |
| 162 |
'type' => 'calendar', |
| 163 |
'name' => 'target_date_to', |
| 164 |
'default' => date(str_replace('d', 't', $df), strtotime('+3 months')), |
| 165 |
], |
| 166 |
// listing IDs |
| 167 |
[ |
| 168 |
'label' => '<label for="listingsfilt">' . ucfirst(strtolower(JText::translate('VBROOMFILTER'))) . '</label>', |
| 169 |
'html' => '<span class="vbo-toolbar-multiselect-wrap">' . $vbo_app->renderElementsDropDown([ |
| 170 |
'id' => 'listingsfilt', |
| 171 |
'elements' => 'listings', |
| 172 |
'load_categories' => 1, |
| 173 |
'categories_lbl' => JText::translate('VBOCATEGORYFILTER'), |
| 174 |
'placeholder' => JText::translate('VBO_LISTINGS'), |
| 175 |
'allow_clear' => 1, |
| 176 |
'attributes' => [ |
| 177 |
'name' => 'listings[]', |
| 178 |
'multiple' => 'multiple', |
| 179 |
], |
| 180 |
'selected_values' => (array) $app->input->get('listings', [], 'array'), |
| 181 |
]) . '</span>', |
| 182 |
'type' => 'select', |
| 183 |
'multiple' => true, |
| 184 |
'name' => 'listings', |
| 185 |
], |
| 186 |
]; |
| 187 |
|
| 188 |
// set script for rendering the datepicker calendars |
| 189 |
$juidf = $this->getDateFormat('jui'); |
| 190 |
$defPickupFromDt = $app->input->getString('pickup_date_from') ?: date(str_replace('d', '01', $df), strtotime('-1 month')); |
| 191 |
$defPickupToDt = $app->input->getString('pickup_date_to') ?: date($df); |
| 192 |
$defTargetFromDt = $app->input->getString('target_date_from') ?: date(str_replace('d', '01', $df), strtotime('+3 months')); |
| 193 |
$defTargetToDt = $app->input->getString('target_date_to') ?: date(str_replace('d', 't', $df), strtotime('+3 months')); |
| 194 |
$js = <<<JAVASCRIPT |
| 195 |
jQuery(() => { |
| 196 |
jQuery('.vbo-report-datepicker-pickupdt:input').datepicker({ |
| 197 |
minDate: '$mintsdt', |
| 198 |
maxDate: '+0D', |
| 199 |
yearRange: '$minyearts:$maxyearts', |
| 200 |
changeMonth: true, |
| 201 |
changeYear: true, |
| 202 |
dateFormat: '$juidf', |
| 203 |
onSelect: vboReportCheckPickupDates, |
| 204 |
}); |
| 205 |
jQuery('.vbo-report-datepicker-targetdt:input').datepicker({ |
| 206 |
minDate: '$mincheckindt', |
| 207 |
maxDate: '$maxcheckoutdt', |
| 208 |
yearRange: '$minyear:$maxyear', |
| 209 |
changeMonth: true, |
| 210 |
changeYear: true, |
| 211 |
dateFormat: '$juidf', |
| 212 |
onSelect: vboReportCheckTargetDates, |
| 213 |
}); |
| 214 |
jQuery('.vbo-report-datepicker-pickup-date-from').datepicker('setDate', '$defPickupFromDt'); |
| 215 |
jQuery('.vbo-report-datepicker-pickup-date-to').datepicker('setDate', '$defPickupToDt'); |
| 216 |
jQuery('.vbo-report-datepicker-target-date-from').datepicker('setDate', '$defTargetFromDt'); |
| 217 |
jQuery('.vbo-report-datepicker-target-date-to').datepicker('setDate', '$defTargetToDt'); |
| 218 |
function vboReportCheckPickupDates(selectedDate, inst) { |
| 219 |
if (selectedDate === null || inst === null) { |
| 220 |
return; |
| 221 |
} |
| 222 |
let cur_from_date = jQuery(this).val(); |
| 223 |
if (jQuery(this).hasClass('vbo-report-datepicker-pickup-date-from') && cur_from_date.length) { |
| 224 |
let nowstart = jQuery(this).datepicker('getDate'); |
| 225 |
let nowstartdate = new Date(nowstart.getTime()); |
| 226 |
jQuery('.vbo-report-datepicker-pickup-date-to').datepicker('option', {minDate: nowstartdate}); |
| 227 |
} |
| 228 |
} |
| 229 |
function vboReportCheckTargetDates(selectedDate, inst) { |
| 230 |
if (selectedDate === null || inst === null) { |
| 231 |
return; |
| 232 |
} |
| 233 |
let cur_from_date = jQuery(this).val(); |
| 234 |
if (jQuery(this).hasClass('vbo-report-datepicker-target-date-from') && cur_from_date.length) { |
| 235 |
let nowstart = jQuery(this).datepicker('getDate'); |
| 236 |
let nowstartdate = new Date(nowstart.getTime()); |
| 237 |
jQuery('.vbo-report-datepicker-target-date-to').datepicker('option', {minDate: nowstartdate}); |
| 238 |
} |
| 239 |
} |
| 240 |
}); |
| 241 |
JAVASCRIPT; |
| 242 |
|
| 243 |
$this->setScript($js); |
| 244 |
|
| 245 |
// return all filters |
| 246 |
return $this->reportFilters; |
| 247 |
} |
| 248 |
|
| 249 |
/** |
| 250 |
* Allows the report to define the sub-filters template. |
| 251 |
* |
| 252 |
* @return ?string |
| 253 |
*/ |
| 254 |
public function getSubFiltersTpl() |
| 255 |
{ |
| 256 |
$app = JFactory::getApplication(); |
| 257 |
|
| 258 |
// current target dates |
| 259 |
$currentTargetFrom = $app->input->getString('target_date_from', ''); |
| 260 |
$currentTargetTo = $app->input->getString('target_date_to', ''); |
| 261 |
|
| 262 |
// default date format |
| 263 |
$df = $this->getDateFormat(); |
| 264 |
|
| 265 |
// months map |
| 266 |
$monthsMap = [ |
| 267 |
JText::translate('VBSHORTMONTHONE'), |
| 268 |
JText::translate('VBSHORTMONTHTWO'), |
| 269 |
JText::translate('VBSHORTMONTHTHREE'), |
| 270 |
JText::translate('VBSHORTMONTHFOUR'), |
| 271 |
JText::translate('VBSHORTMONTHFIVE'), |
| 272 |
JText::translate('VBSHORTMONTHSIX'), |
| 273 |
JText::translate('VBSHORTMONTHSEVEN'), |
| 274 |
JText::translate('VBSHORTMONTHEIGHT'), |
| 275 |
JText::translate('VBSHORTMONTHNINE'), |
| 276 |
JText::translate('VBSHORTMONTHTEN'), |
| 277 |
JText::translate('VBSHORTMONTHELEVEN'), |
| 278 |
JText::translate('VBSHORTMONTHTWELVE'), |
| 279 |
]; |
| 280 |
|
| 281 |
// starting month |
| 282 |
$monthStart = new DateTime(date('Y-m-01'), new DateTimeZone(date_default_timezone_get())); |
| 283 |
|
| 284 |
// build selectable months template |
| 285 |
$selMonths = ''; |
| 286 |
for ($i = 0; $i < 12; $i++) { |
| 287 |
$monthIndex = (int) $monthStart->format('n') - 1; |
| 288 |
$monthName = $monthsMap[$monthIndex] . ' ' . $monthStart->format('y'); |
| 289 |
$setTargetFrom = $monthStart->format(str_replace('d', '01', $df)); |
| 290 |
$setTargetTo = $monthStart->format(str_replace('d', 't', $df)); |
| 291 |
$activeClass = $currentTargetFrom == $setTargetFrom && $currentTargetTo == $setTargetTo ? ' vbo-report-subfilter-month-active' : ''; |
| 292 |
$selMonths .= <<<HTML |
| 293 |
<span class="vbo-report-subfilter-month$activeClass" data-target-from="$setTargetFrom" data-target-to="$setTargetTo">$monthName</span> |
| 294 |
HTML; |
| 295 |
$monthStart->modify('+1 month'); |
| 296 |
} |
| 297 |
|
| 298 |
// build the sub-filters template string |
| 299 |
$tpl = <<<HTML |
| 300 |
<div class="vbo-report-subfilters-rms"> |
| 301 |
<div class="vbo-report-subfilter-months">$selMonths</div> |
| 302 |
</div> |
| 303 |
<script> |
| 304 |
VBOCore.DOMLoaded(() => { |
| 305 |
document.querySelectorAll('.vbo-report-subfilter-month').forEach((selMonth) => { |
| 306 |
selMonth.addEventListener('click', () => { |
| 307 |
// gather month dates |
| 308 |
let fromDt = selMonth.getAttribute('data-target-from'); |
| 309 |
let toDt = selMonth.getAttribute('data-target-to'); |
| 310 |
// update values in datepicker calendars |
| 311 |
jQuery('.vbo-report-datepicker-target-date-from').datepicker('setDate', fromDt); |
| 312 |
jQuery('.vbo-report-datepicker-target-date-to').datepicker('setDate', toDt); |
| 313 |
// remove active month class from all buttons |
| 314 |
document.querySelectorAll('.vbo-report-subfilter-month.vbo-report-subfilter-month-active').forEach((activeMonth) => { |
| 315 |
activeMonth.classList.remove('vbo-report-subfilter-month-active'); |
| 316 |
}); |
| 317 |
// add active month class to current button |
| 318 |
selMonth.classList.add('vbo-report-subfilter-month-active'); |
| 319 |
// trigger event to (re)load report data |
| 320 |
VBOCore.emitEvent('vbo_report_reload'); |
| 321 |
}); |
| 322 |
}); |
| 323 |
}); |
| 324 |
</script> |
| 325 |
HTML; |
| 326 |
|
| 327 |
return $tpl; |
| 328 |
} |
| 329 |
|
| 330 |
/** |
| 331 |
* Loads report data by setting the columns and rows to be displayed. |
| 332 |
* |
| 333 |
* @return bool |
| 334 |
*/ |
| 335 |
public function getReportData() |
| 336 |
{ |
| 337 |
if ($this->getError()) { |
| 338 |
// export functions may set errors rather than exiting the process, and |
| 339 |
// the View may continue the execution to attempt to render the report. |
| 340 |
return false; |
| 341 |
} |
| 342 |
|
| 343 |
if ($this->rows) { |
| 344 |
// method must have run already |
| 345 |
return true; |
| 346 |
} |
| 347 |
|
| 348 |
$app = JFactory::getApplication(); |
| 349 |
|
| 350 |
// default environment values |
| 351 |
$df = $this->getDateFormat(); |
| 352 |
$datesep = VikBooking::getDateSeparator(); |
| 353 |
$currency_symb = VikBooking::getCurrencySymb(); |
| 354 |
|
| 355 |
// week days short names map |
| 356 |
$wdays_map = [ |
| 357 |
JText::translate('VBSUN'), |
| 358 |
JText::translate('VBMON'), |
| 359 |
JText::translate('VBTUE'), |
| 360 |
JText::translate('VBWED'), |
| 361 |
JText::translate('VBTHU'), |
| 362 |
JText::translate('VBFRI'), |
| 363 |
JText::translate('VBSAT'), |
| 364 |
]; |
| 365 |
|
| 366 |
// get the possibly injected report options |
| 367 |
$options = $this->getReportOptions(); |
| 368 |
|
| 369 |
// collect report filters |
| 370 |
$pickup_date_from = $options->get('pickup_date_from') ?: $app->input->getString('pickup_date_from') ?: date(str_replace('d', '01', $df), strtotime('-1 month')); |
| 371 |
$pickup_date_to = $options->get('pickup_date_to') ?: $app->input->getString('pickup_date_to') ?: date($df); |
| 372 |
$target_date_from = $options->get('target_date_from') ?: $app->input->getString('target_date_from') ?: date(str_replace('d', '01', $df), strtotime('+3 months')); |
| 373 |
$target_date_to = $options->get('target_date_to') ?: $app->input->getString('target_date_to') ?: date(str_replace('d', 't', $df), strtotime('+3 months')); |
| 374 |
$interval = $options->get('interval') ?: $app->input->getString('interval') ?: 'DAY'; |
| 375 |
$listing_ids = (array) ($options->get('listings') ?: $app->input->getString('listings') ?: []); |
| 376 |
|
| 377 |
// determine sorting and ordering |
| 378 |
$krsort = $app->input->getString('krsort') ?: $this->defaultKeySort; |
| 379 |
$krorder = $app->input->getString('krorder') ?: $this->defaultKeyOrder; |
| 380 |
$krorder = $krorder == 'DESC' ? 'DESC' : 'ASC'; |
| 381 |
|
| 382 |
// tell whether sorting is allowed |
| 383 |
$sortingAllowed = 1; |
| 384 |
|
| 385 |
// build pace options |
| 386 |
$this->paceOptions = [ |
| 387 |
'pickup' => [ |
| 388 |
'from' => date('Y-m-d', VikBooking::getDateTimestamp($pickup_date_from)), |
| 389 |
'to' => date('Y-m-d', VikBooking::getDateTimestamp($pickup_date_to)), |
| 390 |
], |
| 391 |
'target' => [ |
| 392 |
'from' => date('Y-m-d', VikBooking::getDateTimestamp($target_date_from)), |
| 393 |
'to' => date('Y-m-d', VikBooking::getDateTimestamp($target_date_to)), |
| 394 |
], |
| 395 |
'interval' => $interval, |
| 396 |
'listings' => $listing_ids, |
| 397 |
'sort_rooms' => 'occupancy', |
| 398 |
]; |
| 399 |
|
| 400 |
try { |
| 401 |
// obtain booking pace data |
| 402 |
$paceData = VBORmsPace::getInstance()->getBookingData($this->paceOptions); |
| 403 |
} catch (Exception $e) { |
| 404 |
$this->setError($e->getMessage()); |
| 405 |
return false; |
| 406 |
} |
| 407 |
|
| 408 |
// define the report columns |
| 409 |
$this->cols = [ |
| 410 |
// period start date |
| 411 |
[ |
| 412 |
'key' => 'date', |
| 413 |
'label' => JText::translate('VBPVIEWORDERSONE'), |
| 414 |
'sortable' => $sortingAllowed, |
| 415 |
], |
| 416 |
// new bookings |
| 417 |
[ |
| 418 |
'key' => 'nb', |
| 419 |
'label' => JText::translate('VBO_NEW_BOOKINGS'), |
| 420 |
'center' => 1, |
| 421 |
'sortable' => $sortingAllowed, |
| 422 |
], |
| 423 |
// cancellations |
| 424 |
[ |
| 425 |
'key' => 'cb', |
| 426 |
'label' => JText::translate('VBO_CANCELLATIONS'), |
| 427 |
'center' => 1, |
| 428 |
'sortable' => $sortingAllowed, |
| 429 |
], |
| 430 |
// on the books |
| 431 |
[ |
| 432 |
'key' => 'otb', |
| 433 |
'label' => JText::translate('VBCUSTOMERTOTBOOKINGS'), |
| 434 |
'sortable' => $sortingAllowed, |
| 435 |
'center' => 1, |
| 436 |
], |
| 437 |
// adr |
| 438 |
[ |
| 439 |
'key' => 'adr', |
| 440 |
'label' => JText::translate('VBOREPORTREVENUEADR'), |
| 441 |
'sortable' => $sortingAllowed, |
| 442 |
'center' => 1, |
| 443 |
], |
| 444 |
// Room Revenue |
| 445 |
[ |
| 446 |
'key' => 'roomrev', |
| 447 |
'label' => JText::translate('VBO_GROSS_BOOKING_VALUE'), |
| 448 |
'sortable' => $sortingAllowed, |
| 449 |
'center' => 1, |
| 450 |
], |
| 451 |
]; |
| 452 |
|
| 453 |
// start global line counters |
| 454 |
$lineCounter = 0; |
| 455 |
|
| 456 |
// start metric indexes and values for the various column keys |
| 457 |
$metricIndexes = []; |
| 458 |
$metricValues = []; |
| 459 |
|
| 460 |
// iterate booking pace data pickup dates |
| 461 |
foreach ($paceData['pace'] as $parseIndex => $metrics) { |
| 462 |
// increase line counter |
| 463 |
$lineCounter++; |
| 464 |
|
| 465 |
// build row metric columns |
| 466 |
$metricRowCols = [ |
| 467 |
// period start date |
| 468 |
[ |
| 469 |
'key' => 'date', |
| 470 |
'callback' => function($dt) use ($df, $datesep, $wdays_map) { |
| 471 |
return sprintf('%s, %s', $wdays_map[$dt->format('w')] ?? '', $dt->format(str_replace("/", $datesep, $df))); |
| 472 |
}, |
| 473 |
'value' => $metrics['date'], |
| 474 |
], |
| 475 |
// new bookings |
| 476 |
[ |
| 477 |
'key' => 'nb', |
| 478 |
'callback' => function($value) { |
| 479 |
if ($value > 0) { |
| 480 |
return '<span class="label label-success">' . $value . '</span>'; |
| 481 |
} |
| 482 |
return '<span class="label">' . $value . '</span>'; |
| 483 |
}, |
| 484 |
'callback_export' => function($value) { |
| 485 |
return $value; |
| 486 |
}, |
| 487 |
'value' => $metrics['nb'], |
| 488 |
'center' => 1, |
| 489 |
], |
| 490 |
// cancellations |
| 491 |
[ |
| 492 |
'key' => 'cb', |
| 493 |
'callback' => function($value) { |
| 494 |
if ($value > 0) { |
| 495 |
return '<span class="label label-error">' . $value . '</span>'; |
| 496 |
} |
| 497 |
return '<span class="label">' . $value . '</span>'; |
| 498 |
}, |
| 499 |
'callback_export' => function($value) { |
| 500 |
return $value; |
| 501 |
}, |
| 502 |
'value' => $metrics['cb'], |
| 503 |
'center' => 1, |
| 504 |
], |
| 505 |
// on the books |
| 506 |
[ |
| 507 |
'key' => 'otb', |
| 508 |
'callback' => function($value) { |
| 509 |
if ($value > 0) { |
| 510 |
return '<span class="label label-info">' . $value . '</span>'; |
| 511 |
} |
| 512 |
return '<span class="label">' . $value . '</span>'; |
| 513 |
}, |
| 514 |
'callback_export' => function($value) { |
| 515 |
return $value; |
| 516 |
}, |
| 517 |
'value' => $metrics['otb'], |
| 518 |
'center' => 1, |
| 519 |
], |
| 520 |
// adr |
| 521 |
[ |
| 522 |
'key' => 'adr', |
| 523 |
'value' => $metrics['adr'], |
| 524 |
'center' => 1, |
| 525 |
'callback' => function($metric) { |
| 526 |
return VikBooking::formatCurrencyNumber($metric); |
| 527 |
}, |
| 528 |
], |
| 529 |
// Room Revenue |
| 530 |
[ |
| 531 |
'key' => 'roomrev', |
| 532 |
'value' => $metrics['roomrev'], |
| 533 |
'center' => 1, |
| 534 |
'callback' => function($metric) { |
| 535 |
return VikBooking::formatCurrencyNumber($metric); |
| 536 |
}, |
| 537 |
], |
| 538 |
]; |
| 539 |
|
| 540 |
if (!$metricIndexes) { |
| 541 |
// store the indexes for each metric key |
| 542 |
$metricIndexes = array_values(array_column(array_filter($metricRowCols, function($colData) { |
| 543 |
return empty($colData['ignore_view']); |
| 544 |
}), 'key')); |
| 545 |
} |
| 546 |
|
| 547 |
// update metric values for current target to be used for the footer calculations |
| 548 |
$metricValues['nb'] = ($metricValues['nb'] ?? 0) + $metrics['nb']; |
| 549 |
$metricValues['cb'] = ($metricValues['cb'] ?? 0) + $metrics['cb']; |
| 550 |
$metricValues['otb'] = max(($metricValues['otb'] ?? 0), $metrics['otb']); |
| 551 |
|
| 552 |
// push metric row with calculated columns |
| 553 |
$this->rows[] = $metricRowCols; |
| 554 |
} |
| 555 |
|
| 556 |
if ($sortingAllowed) { |
| 557 |
// sort rows |
| 558 |
$this->sortRows($krsort, $krorder); |
| 559 |
} |
| 560 |
|
| 561 |
// count total cells per row and total metric values for the footer |
| 562 |
$cellsPerRow = count($metricIndexes); |
| 563 |
$footerColsCount = count($metricValues) ?: 1; |
| 564 |
$footerColspan = floor($cellsPerRow / $footerColsCount); |
| 565 |
|
| 566 |
// build report footer row |
| 567 |
$this->footerRow[] = [ |
| 568 |
// new bookings |
| 569 |
[ |
| 570 |
'callback' => function($metricValue) { |
| 571 |
// prepare template variables |
| 572 |
$blockTitle = JText::translate('VBO_NEW_BOOKINGS'); |
| 573 |
$metricCls = 'vbo-report-footer-rms-metric-target'; |
| 574 |
|
| 575 |
// return the HTML string to build this footer cell |
| 576 |
return <<<HTML |
| 577 |
<div class="vbo-report-footer-rms-wrap"> |
| 578 |
<div class="vbo-report-footer-rms-title">$blockTitle</div> |
| 579 |
<div class="vbo-report-footer-rms-metrics"> |
| 580 |
<div class="vbo-report-footer-rms-metric $metricCls"> |
| 581 |
<div class="vbo-report-footer-rms-metric-value">$metricValue</div> |
| 582 |
</div> |
| 583 |
</div> |
| 584 |
</div> |
| 585 |
HTML; |
| 586 |
}, |
| 587 |
'callback_export' => function($metricValue) { |
| 588 |
return $metricValue; |
| 589 |
}, |
| 590 |
'value' => $metricValues['nb'], |
| 591 |
'html' => 1, |
| 592 |
'center' => 1, |
| 593 |
'colspan' => $footerColspan, |
| 594 |
], |
| 595 |
// cancellations |
| 596 |
[ |
| 597 |
'callback' => function($metricValue) { |
| 598 |
// prepare template variables |
| 599 |
$blockTitle = JText::translate('VBO_CANCELLATIONS'); |
| 600 |
$metricCls = 'vbo-report-footer-rms-metric-target'; |
| 601 |
|
| 602 |
// return the HTML string to build this footer cell |
| 603 |
return <<<HTML |
| 604 |
<div class="vbo-report-footer-rms-wrap"> |
| 605 |
<div class="vbo-report-footer-rms-title">$blockTitle</div> |
| 606 |
<div class="vbo-report-footer-rms-metrics"> |
| 607 |
<div class="vbo-report-footer-rms-metric $metricCls"> |
| 608 |
<div class="vbo-report-footer-rms-metric-value">$metricValue</div> |
| 609 |
</div> |
| 610 |
</div> |
| 611 |
</div> |
| 612 |
HTML; |
| 613 |
}, |
| 614 |
'callback_export' => function($metricValue) { |
| 615 |
return $metricValue; |
| 616 |
}, |
| 617 |
'value' => $metricValues['cb'], |
| 618 |
'html' => 1, |
| 619 |
'center' => 1, |
| 620 |
'colspan' => $footerColspan, |
| 621 |
], |
| 622 |
// on the books |
| 623 |
[ |
| 624 |
'callback' => function($metricValue) { |
| 625 |
// prepare template variables |
| 626 |
$blockTitle = JText::translate('VBCUSTOMERTOTBOOKINGS'); |
| 627 |
$metricCls = 'vbo-report-footer-rms-metric-target'; |
| 628 |
|
| 629 |
// return the HTML string to build this footer cell |
| 630 |
return <<<HTML |
| 631 |
<div class="vbo-report-footer-rms-wrap"> |
| 632 |
<div class="vbo-report-footer-rms-title">$blockTitle</div> |
| 633 |
<div class="vbo-report-footer-rms-metrics"> |
| 634 |
<div class="vbo-report-footer-rms-metric $metricCls"> |
| 635 |
<div class="vbo-report-footer-rms-metric-value">$metricValue</div> |
| 636 |
</div> |
| 637 |
</div> |
| 638 |
</div> |
| 639 |
HTML; |
| 640 |
}, |
| 641 |
'callback_export' => function($metricValue) { |
| 642 |
return $metricValue; |
| 643 |
}, |
| 644 |
'value' => $metricValues['otb'], |
| 645 |
'html' => 1, |
| 646 |
'center' => 1, |
| 647 |
'colspan' => $footerColspan, |
| 648 |
], |
| 649 |
]; |
| 650 |
|
| 651 |
if ($cellsPerRow > ($footerColspan * $footerColsCount)) { |
| 652 |
// add empty cells to footer rows |
| 653 |
for ($i = 1; $i <= ($cellsPerRow - ($footerColspan * $footerColsCount)); $i++) { |
| 654 |
// push empty row |
| 655 |
$this->footerRow[0][] = [ |
| 656 |
'value' => '', |
| 657 |
]; |
| 658 |
} |
| 659 |
} |
| 660 |
|
| 661 |
// process completed with success |
| 662 |
return true; |
| 663 |
} |
| 664 |
|
| 665 |
/** |
| 666 |
* @inheritDoc |
| 667 |
*/ |
| 668 |
public function getChartTitle() |
| 669 |
{ |
| 670 |
$df = $this->getDateFormat(); |
| 671 |
$datesep = VikBooking::getDateSeparator(); |
| 672 |
|
| 673 |
$pickup_title_parts = [ |
| 674 |
$this->rows[0][0]['value'] ?? null, |
| 675 |
$this->rows[count($this->rows) - 1][0]['value'] ?? null, |
| 676 |
]; |
| 677 |
|
| 678 |
$target_title_parts = [ |
| 679 |
$this->paceOptions['target']['from'] ?? null, |
| 680 |
$this->paceOptions['target']['to'] ?? null, |
| 681 |
]; |
| 682 |
|
| 683 |
return JText::translate('VBO_RMS_BOOKING_PACE') . ' ' . implode(' - ', array_map(function($dt) use ($df, $datesep) { |
| 684 |
return $dt->format(str_replace('/', $datesep, $df)); |
| 685 |
}, array_filter($pickup_title_parts))) . ', ' . implode(' - ', array_map(function($dt) use ($df, $datesep) { |
| 686 |
return date(str_replace('/', $datesep, $df), strtotime($dt)); |
| 687 |
}, array_filter($target_title_parts))); |
| 688 |
} |
| 689 |
|
| 690 |
/** |
| 691 |
* @inheritDoc |
| 692 |
*/ |
| 693 |
public function getChart(?array $data = null) |
| 694 |
{ |
| 695 |
if (!$this->getReportRows() && !$this->getReportData()) { |
| 696 |
return ''; |
| 697 |
} |
| 698 |
|
| 699 |
// build chart labels and dataset values |
| 700 |
$chartLabels = []; |
| 701 |
$chartDatasetValues = [ |
| 702 |
'nb' => [], |
| 703 |
'cb' => [], |
| 704 |
'otb' => [], |
| 705 |
]; |
| 706 |
|
| 707 |
// iterate all report rows |
| 708 |
foreach ($this->getReportRows() as $row) { |
| 709 |
// push date to labels (first key of each row) |
| 710 |
$chartLabels[] = $row[0]['callback']($row[0]['value']); |
| 711 |
// push dataset values |
| 712 |
$chartDatasetValues['nb'][] = $row[1]['value']; |
| 713 |
$chartDatasetValues['cb'][] = $row[2]['value']; |
| 714 |
$chartDatasetValues['otb'][] = $row[3]['value']; |
| 715 |
} |
| 716 |
|
| 717 |
// the canvas element ID and tag |
| 718 |
$canvas_id = 'vbo-report-booking-pace-chart-canvas-' . uniqid(); |
| 719 |
$canvas_html = '<canvas id="' . $canvas_id . '" class="vbo-report-chart-canvas"></canvas>'; |
| 720 |
|
| 721 |
if (empty($this->chartScript)) { |
| 722 |
// prepare JSON variables |
| 723 |
$chartLabelsJSON = json_encode($chartLabels); |
| 724 |
$nbDatasetJSON = json_encode($chartDatasetValues['nb']); |
| 725 |
$cbDatasetJSON = json_encode($chartDatasetValues['cb']); |
| 726 |
$otbDatasetJSON = json_encode($chartDatasetValues['otb']); |
| 727 |
|
| 728 |
$tnNb = json_encode(JText::translate('VBO_NEW_BOOKINGS')); |
| 729 |
$tnCb = json_encode(JText::translate('VBO_CANCELLATIONS')); |
| 730 |
$tnOtb = json_encode(JText::translate('VBCUSTOMERTOTBOOKINGS')); |
| 731 |
|
| 732 |
// prepare the necessary script to render the Chart |
| 733 |
$this->chartScript = <<<JAVASCRIPT |
| 734 |
VBOCore.DOMLoaded(() => { |
| 735 |
const vboReportCanvas = document.getElementById('$canvas_id'); |
| 736 |
const vboReportCtx = vboReportCanvas.getContext('2d'); |
| 737 |
const vboReportData = { |
| 738 |
labels: $chartLabelsJSON, |
| 739 |
datasets: [ |
| 740 |
{ |
| 741 |
label: $tnNb, |
| 742 |
data: $nbDatasetJSON, |
| 743 |
borderColor: 'rgba(0, 100, 0, 1)', |
| 744 |
backgroundColor: 'rgba(0, 100, 0, 0.2)', |
| 745 |
}, |
| 746 |
{ |
| 747 |
label: $tnCb, |
| 748 |
data: $cbDatasetJSON, |
| 749 |
borderColor: 'rgba(220, 20, 60, 1)', |
| 750 |
backgroundColor: 'rgba(220, 20, 60, 0.2)', |
| 751 |
}, |
| 752 |
{ |
| 753 |
label: $tnOtb, |
| 754 |
data: $otbDatasetJSON, |
| 755 |
borderColor: 'rgba(30, 144, 255, 1)', |
| 756 |
backgroundColor: 'rgba(30, 144, 255, 0.2)', |
| 757 |
}, |
| 758 |
], |
| 759 |
}; |
| 760 |
const vboReportChart = new Chart(vboReportCtx, { |
| 761 |
type: 'line', |
| 762 |
data: vboReportData, |
| 763 |
options: { |
| 764 |
responsive: true, |
| 765 |
legend: { |
| 766 |
display: true, |
| 767 |
}, |
| 768 |
// axes handling |
| 769 |
scales: { |
| 770 |
// Y Axis properties |
| 771 |
yAxes: [{ |
| 772 |
// make sure the chart starts at 0 |
| 773 |
ticks: { |
| 774 |
// format value as currency |
| 775 |
callback: (value) => parseInt(value), |
| 776 |
beginAtZero: true, |
| 777 |
stepSize: 1, |
| 778 |
}, |
| 779 |
}], |
| 780 |
}, |
| 781 |
}, |
| 782 |
}); |
| 783 |
}); |
| 784 |
JAVASCRIPT; |
| 785 |
|
| 786 |
// set the necessary script |
| 787 |
$this->setScript($this->chartScript); |
| 788 |
} |
| 789 |
|
| 790 |
// return the canvas HTML |
| 791 |
return $canvas_html; |
| 792 |
} |
| 793 |
|
| 794 |
/** |
| 795 |
* @inheritDoc |
| 796 |
*/ |
| 797 |
public function getChartMetaData($position = null, $data = null) |
| 798 |
{ |
| 799 |
if ((!$this->getReportRows() && !$this->getReportData()) || !$position) { |
| 800 |
return []; |
| 801 |
} |
| 802 |
|
| 803 |
$metrics = [ |
| 804 |
'nb' => 0, |
| 805 |
'cb' => 0, |
| 806 |
'otb' => [], |
| 807 |
]; |
| 808 |
|
| 809 |
// iterate all report rows |
| 810 |
foreach ($this->getReportRows() as $row) { |
| 811 |
// sum new bookings |
| 812 |
$metrics['nb'] += $row[1]['value']; |
| 813 |
// sum cancellations |
| 814 |
$metrics['cb'] += $row[2]['value']; |
| 815 |
// push "on the books" stat |
| 816 |
$metrics['otb'][] = $row[3]['value']; |
| 817 |
} |
| 818 |
|
| 819 |
$metaData = [ |
| 820 |
'bottom' => [ |
| 821 |
[ |
| 822 |
'key' => 'nb', |
| 823 |
'label' => JText::translate('VBO_NEW_BOOKINGS'), |
| 824 |
'value' => $metrics['nb'], |
| 825 |
'class' => 'vbo-report-chart-meta-max', |
| 826 |
], |
| 827 |
[ |
| 828 |
'key' => 'cb', |
| 829 |
'label' => JText::translate('VBO_CANCELLATIONS'), |
| 830 |
'value' => $metrics['cb'], |
| 831 |
'class' => 'vbo-report-chart-meta-min', |
| 832 |
], |
| 833 |
], |
| 834 |
]; |
| 835 |
|
| 836 |
return $metaData[$position] ?? []; |
| 837 |
} |
| 838 |
|
| 839 |
/** |
| 840 |
* Registers the name to give to the CSV file being exported. |
| 841 |
* |
| 842 |
* @return void |
| 843 |
*/ |
| 844 |
protected function registerExportCSVFileName() |
| 845 |
{ |
| 846 |
$app = JFactory::getApplication(); |
| 847 |
|
| 848 |
$nameValues = array_map(function($dtString) { |
| 849 |
return date('Y-m-d', VikBooking::getDateTimestamp($dtString)); |
| 850 |
}, array_filter([ |
| 851 |
$app->input->getString('pickup_date_from', ''), |
| 852 |
$app->input->getString('pickup_date_to', ''), |
| 853 |
$app->input->getString('target_date_from', ''), |
| 854 |
$app->input->getString('target_date_to', ''), |
| 855 |
])); |
| 856 |
|
| 857 |
$this->setExportCSVFileName($this->reportName . '-' . implode('_', array_filter($nameValues)) . '.csv'); |
| 858 |
} |
| 859 |
} |
| 860 |
|