PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.9
VikBooking Hotel Booking Engine & PMS v1.8.9
1.8.16 1.8.15 1.8.14 1.8.13 1.8.12 1.8.11 1.8.10 1.8.9 1.8.6 1.8.7 1.8.8 trunk 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 All 37 releases
vikbooking / admin / helpers / report / rms_booking_pace.php

rms_booking_pace.php in VikBooking Hotel Booking Engine & PMS 1.8.9, at admin/helpers/report/rms_booking_pace.php

860 lines 33.3 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 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