PluginProbe ʕ •ᴥ•ʔ
VikAppointments Services Booking Calendar / 1.2.21
VikAppointments Services Booking Calendar v1.2.21
1.2.21 1.2.20 trunk 1.2.17 1.2.18 1.2.19
vikappointments / site / helpers / libraries / statistics / widgets / packages / items / chart.php
vikappointments / site / helpers / libraries / statistics / widgets / packages / items Last commit date
chart.php 6 days ago count.php 6 days ago index.html 6 days ago
chart.php
512 lines
1 <?php
2 /**
3 * @package VikAppointments
4 * @subpackage core
5 * @author E4J s.r.l.
6 * @copyright Copyright (C) 2021 E4J s.r.l. All Rights Reserved.
7 * @license http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL
8 * @link https://vikwp.com
9 */
10
11 // No direct access
12 defined('ABSPATH') or die('No script kiddies please!');
13
14 /**
15 * Widget used to draw a trend chart of each supported package.
16 *
17 * @since 1.7
18 */
19 class VAPStatisticsWidgetPackagesItemsChart extends VAPStatisticsWidget
20 {
21 /**
22 * Internal flag used to check whether the current user
23 * owns enough permissions to access the financial data.
24 *
25 * Use true by default in case the widget is displayed
26 * without checking the permissions.
27 *
28 * @var boolean
29 */
30 protected $hasFinanceAccess = true;
31
32 /**
33 * Checks whether the specified group is supported by the widget.
34 *
35 * @param string $group The group to check.
36 *
37 * @return boolean True if supported, false otherwise.
38 */
39 public function isSupported($group)
40 {
41 return empty($group) || $group == '*' || $group == 'dashboard' || $group == 'packages';
42 }
43
44 /**
45 * Checks whether the specified user is capable to access this widget.
46 *
47 * @param JUser $user The user instance.
48 *
49 * @return boolean True if capable, false otherwise.
50 */
51 public function checkPermissions($user)
52 {
53 // detected finance visibility
54 $this->hasFinanceAccess = $user->authorise('core.access.analytics.finance', 'com_vikappointments');
55
56 return $user->authorise('core.access.analytics.packages', 'com_vikappointments');
57 }
58
59 /**
60 * Returns to the caller the financial rights of the user.
61 *
62 * @return boolean True if capable, false otherwise.
63 */
64 public function hasFinanceAccess()
65 {
66 return $this->hasFinanceAccess;
67 }
68
69 /**
70 * Override this method to return a configuration form of the widget.
71 *
72 * @return array
73 */
74 public function getForm()
75 {
76 $form = array(
77 /**
78 * The initial date to take when a new session starts.
79 *
80 * @var select
81 */
82 'range' => array(
83 'type' => 'select',
84 'label' => JText::translate('VAP_CHART_INITIAL_RANGE_FIELD'),
85 'help' => JText::translate('VAP_CHART_INITIAL_RANGE_FIELD_HELP'),
86 'default' => '-6 months',
87 'options' => array(
88 '-1 week' => JText::plural('VAP_LAST_N_WEEKS', 1),
89 '-2 weeks' => JText::plural('VAP_LAST_N_WEEKS', 2),
90 '-1 month' => JText::plural('VAP_LAST_N_MONTHS', 1),
91 '-3 months' => JText::plural('VAP_LAST_N_MONTHS', 3),
92 '-6 months' => JText::plural('VAP_LAST_N_MONTHS', 6),
93 '-9 months' => JText::plural('VAP_LAST_N_MONTHS', 9),
94 '-12 months' => JText::plural('VAP_LAST_N_MONTHS', 12),
95 ),
96 ),
97
98 /**
99 * The initial date of the range.
100 *
101 * The parameter is VOLATILE because, every time the session
102 * ends, we need to restore the field to an empty value, just
103 * to obtain the current date.
104 *
105 * @var date
106 */
107 'datefrom' => array(
108 'type' => 'date',
109 'label' => JText::translate('VAPEXPORTRES3'),
110 'volatile' => true,
111 ),
112
113 /**
114 * The ending date of the range.
115 *
116 * The parameter is VOLATILE because, every time the session
117 * ends, we need to restore the field to an empty value, just
118 * to obtain the current date.
119 *
120 * @var date
121 */
122 'dateto' => array(
123 'type' => 'date',
124 'label' => JText::translate('VAPEXPORTRES4'),
125 'volatile' => true,
126 ),
127
128 /**
129 * The type of chart to display (line or bar).
130 *
131 * @var select
132 */
133 'chart' => array(
134 'type' => 'select',
135 'label' => JText::translate('VAP_CHART_TYPE'),
136 'default' => 'line',
137 'options' => array(
138 'line' => JText::translate('VAP_CHART_TYPE_LINE'),
139 'bar' => JText::translate('VAP_CHART_TYPE_BAR'),
140 ),
141 ),
142
143 /**
144 * The entity of the chart (reservations count or total earning).
145 *
146 * @var select
147 */
148 'valuetype' => array(
149 'type' => 'select',
150 'label' => JText::translate('VAP_CHART_VALUE_TYPE_FIELD'),
151 'help' => JText::translate('VAP_CHART_VALUE_TYPE_FIELD_HELP'),
152 'default' => 'total',
153 'options' => array(
154 'total' => JText::translate('VAPREPORTSVALUETYPEOPT1'),
155 'count' => JText::translate('VAPREPORTSVALUETYPEOPT2'),
156 ),
157 ),
158
159 /**
160 * Select a group to automatically track all the assigned packages.
161 *
162 * @var select
163 */
164 'group' => array(
165 'type' => 'select',
166 'label' => JText::translate('VAPMANAGESERVICE10'),
167 'help' => JText::translate('VAP_STATS_WIDGET_PACKAGES_ITEMS_CHART_GROUP_FIELD_HELP'),
168 'options' => JHtml::fetch('vaphtml.admin.packgroups', $blank = true),
169 ),
170
171 /**
172 * A list of packages to track.
173 *
174 * @var select
175 */
176 'packages' => array(
177 'type' => 'select',
178 'label' => JText::translate('VAPMENUPACKAGES'),
179 'multiple' => true,
180 'options' => JHtml::fetch('vaphtml.admin.packages', $strict = false, $blank = false, $group = true),
181 ),
182 );
183
184 // check whether the user owns enough permissions to see financial data
185 if (!$this->hasFinanceAccess)
186 {
187 // change type to hidden to avoid its selection
188 $form['valuetype']['type'] = 'hidden';
189 // display by default the total number of packages
190 $form['valuetype']['default'] = 'count';
191 }
192
193 $packages = array();
194
195 // ungroup packages and merge them all at the same level
196 foreach ($form['packages']['options'] as $group)
197 {
198 $packages = array_merge($packages, $group);
199 }
200
201 // update options attribute
202 $form['packages']['options'] = $packages;
203
204 // make sure we have a list of groups
205 if (count($form['group']['options']) == 1)
206 {
207 // nope, the list contains only the placeholder, unset parameter
208 unset($form['group']);
209 }
210
211 return $form;
212 }
213
214 /**
215 * Checks whether the widget is able to export the fetched data.
216 *
217 * @return array A list of supported exportable functions.
218 */
219 public function isExportable()
220 {
221 return ['export', 'print'];
222 }
223
224 /**
225 * Loads the dataset(s) that will be recovered asynchronously
226 * for being displayed within the widget.
227 *
228 * It is possible to return an array of records to be passed
229 * to a chart or directly the HTML to replace.
230 *
231 * @return mixed
232 */
233 public function getData()
234 {
235 $filters = array();
236 $filters['group'] = $this->getOption('group');
237 $filters['packages'] = array_values(array_filter($this->getOption('packages', [])));
238 $filters['valuetype'] = $this->getOption('valuetype', 'total');
239 $filters['datefrom'] = $this->getOption('datefrom');
240 $filters['dateto'] = $this->getOption('dateto');
241 $filters['range'] = $this->getOption('range', '-6 months');
242
243 if ($filters['group'])
244 {
245 $dbo = JFactory::getDbo();
246
247 // filter specified, take all packages under this group
248 $q = $dbo->getQuery(true)
249 ->select($dbo->qn('id'))
250 ->from($dbo->qn('#__vikappointments_package'))
251 ->where($dbo->qn('id_group') . ' = ' . (int) $filters['group']);
252
253 $dbo->setQuery($q);
254
255 if ($packages = $dbo->loadColumn())
256 {
257 // merge with selected packages
258 $filters['packages'] = array_merge($filters['packages'], $packages);
259 // get rid of duplicates
260 $filters['packages'] = array_values(array_unique($filters['packages']));
261 }
262 }
263
264 if (!$this->hasFinanceAccess)
265 {
266 // always display the total number of packages in case
267 // the user cannot access financial data
268 $filters['valuetype'] = 'count';
269 }
270
271 $tz = JFactory::getUser()->getTimezone();
272
273 // use default range in case one of the specified dates is empty
274 if (VAPDateHelper::isNull($filters['datefrom']) || VAPDateHelper::isNull($filters['dateto']))
275 {
276 // create current date by using the timezone of the logged-in user
277 $filters['dateto'] = JFactory::getDate('now', $tz);
278 // set ending date to current one, 1 second before tomorrow
279 $filters['dateto']->modify('23:59:59');
280
281 if (strpos($filters['range'], 'months') !== false)
282 {
283 // set ending date to the end of the month
284 $filters['dateto']->modify($filters['dateto']->format('Y-m-t'));
285 }
286
287 // set starting date to current one at midnight
288 $filters['datefrom'] = clone $filters['dateto'];
289 // go to next day to properly calculate the starting date
290 $filters['datefrom']->modify('tomorrow 00:00:00');
291 // subtract given range
292 $filters['datefrom']->modify($filters['range']);
293 }
294 else
295 {
296 // convert specified dates to SQL format
297 $filters['datefrom'] = JFactory::getDate(VAPDateHelper::getDate($filters['datefrom'], 0, 0, 0), $tz);
298 $filters['dateto'] = JFactory::getDate(VAPDateHelper::getDate($filters['dateto'], 23, 59, 59), $tz);
299 }
300
301 // temporary option that can be used to extend the date format at runtime
302 $extended = $this->getOption('extended', false);
303
304 // import packages helper
305 VAPLoader::import('libraries.statistics.helpers.packages');
306 // fetch items trend
307 $data = VAPStatisticsHelperPackages::getItemsTrend($filters['datefrom'], $filters['dateto'], $filters['valuetype'], $filters['packages'], $extended);
308
309 return $data;
310 }
311
312 /**
313 * Create adapter for export method.
314 * This widget only supports "print" export function.
315 *
316 * @param mixed $rule The requested export type.
317 *
318 * @return void
319 */
320 public function export($rule = null)
321 {
322 if ($rule == 'print')
323 {
324 // auto-print the document
325 JHtml::fetch('vaphtml.sitescripts.winprint', 256);
326
327 // auto-fetch the widget data
328 $data = array(
329 'data' => $this->getData(),
330 'legend' => true,
331 );
332
333 // display widget with fetched data
334 echo $this->display($data);
335 }
336 else
337 {
338 // temporarily obtain all the columns and take an extended date format
339 $this->setOption('valuetype', ['total', 'tax', 'net', 'count']);
340 $this->setOption('extended', true);
341
342 // auto-fetch the widget data
343 $data = $this->getData();
344
345 $app = JFactory::getApplication();
346
347 $path = null;
348
349 // create file name
350 $filename = JFilterOutput::stringURLSafe('packages-reports');
351
352 if ($rule != 'file')
353 {
354 // send headers for CSV download
355 $app->setHeader('Cache-Control', 'no-store, no-cache');
356 $app->setHeader('Content-Type', 'text/csv; charset=UTF-8');
357 $app->setHeader('Content-Disposition', 'attachment; filename="' . htmlspecialchars($filename) . '.csv"');
358 $app->sendHeaders();
359
360 // send bytes through PHP output
361 $handle = fopen('php://output', 'w');
362 }
363 else
364 {
365 // create file path
366 $path = rtrim($app->get('tmp_path'), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
367
368 $count = 1;
369
370 // append a counter next to the file name as long as the file already exists
371 while (is_file($path . $filename . ($count > 1 ? '-' . $count : '') . '.csv'))
372 {
373 $count++;
374 }
375
376 // create path
377 $path = $path . $filename . ($count > 1 ? '-' . $count : '') . '.csv';
378
379 // send bytes through file
380 $handle = fopen($path, 'w');
381 }
382
383 // create table heading
384 $head = array(
385 // date
386 JText::translate('VAPMANAGEREVIEW4'),
387 // package
388 JText::translate('VAPMANAGEPACKORDER18'),
389 // total gross
390 JText::translate('VAPTOTALGROSS'),
391 // total tax
392 JText::translate('VAPTOTALTAX'),
393 // total net
394 JText::translate('VAPTOTALNET'),
395 // count
396 JText::translate('VAPQUANTITY'),
397 );
398
399 // include table heading
400 fputcsv($handle, $head, ',', '"', $escape = '');
401
402 $currency = VAPFactory::getCurrency();
403
404 $sumTotal = $sumTax = $sumNet = $sumCount = 0;
405
406 // iterate all records
407 foreach ($data as $date => $packages)
408 {
409 $row = array(
410 // date
411 $date,
412 // package
413 '',
414 // total gross
415 '',
416 // total tax
417 '',
418 // total net
419 '',
420 // count
421 '',
422 );
423
424 // include table row
425 fputcsv($handle, $row, ',', '"', $escape = '');
426
427 $subTotal = $subTax = $subNet = $subCount = 0;
428
429 foreach ($packages as $package)
430 {
431 $row = array(
432 // date
433 '',
434 // package
435 $package['name'],
436 // total gross
437 $currency->format($package['total']),
438 // total tax
439 $currency->format($package['tax']),
440 // net
441 $currency->format($package['net']),
442 // count
443 $package['count'],
444 );
445
446 // sum totals
447 $subTotal += $package['total'];
448 $subTax += $package['tax'];
449 $subNet += $package['net'];
450 $subCount += $package['count'];
451
452 // include table row
453 fputcsv($handle, $row, ',', '"', $escape = '');
454 }
455
456 // sum grand totals
457 $sumTotal += $subTotal;
458 $sumTax += $subTax;
459 $sumNet += $subNet;
460 $sumCount += $subCount;
461
462 $row = array(
463 // date
464 '',
465 // package
466 '',
467 // total gross
468 $currency->format($subTotal),
469 // total tax
470 $currency->format($subTax),
471 // total net
472 $currency->format($subNet),
473 // count
474 $subCount,
475 );
476
477 // include table row
478 fputcsv($handle, $row, ',', '"', $escape = '');
479 }
480
481 $row = array(
482 // date
483 '',
484 // package
485 '',
486 // total gross
487 $currency->format($sumTotal),
488 // total tax
489 $currency->format($sumTax),
490 // total net
491 $currency->format($sumNet),
492 // count
493 $sumCount,
494 );
495
496 // include table row
497 fputcsv($handle, $row, ',', '"', $escape = '');
498
499 fclose($handle);
500
501 if ($rule != 'file')
502 {
503 // terminate session in case of download
504 $app->close();
505 }
506
507 // return file path otherwise
508 return $path;
509 }
510 }
511 }
512