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 / order / export / drivers / csv.php
vikappointments / site / helpers / libraries / order / export / drivers Last commit date
csv.php 3 days ago excel.php 3 days ago ics.php 3 days ago index.html 3 days ago
csv.php
719 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 * Driver class used to export the orders/appointments in CSV format.
16 *
17 * @since 1.7
18 */
19 class VAPOrderExportDriverCsv extends VAPOrderExportDriver
20 {
21 /**
22 * A list of custom fields.
23 *
24 * @var array
25 */
26 private $customFields;
27
28 /**
29 * The timezone to use according to the fetched entity.
30 *
31 * @var DateTimeZone
32 */
33 private $timezone;
34
35 /**
36 * @override
37 * Builds the form parameters required to the CSV driver.
38 *
39 * @return array
40 */
41 protected function buildForm()
42 {
43 return array(
44 /**
45 * Choose whether only the confirmed orders will be retrieved.
46 *
47 * @var checkbox
48 */
49 'confirmed' => array(
50 'type' => 'checkbox',
51 'label' => JText::translate('VAP_EXPORT_DRIVER_CSV_CONFIRMED_STATUS_FIELD'),
52 'help' => JText::translate('VAP_EXPORT_DRIVER_CSV_CONFIRMED_STATUS_FIELD_HELP'),
53 'default' => 1,
54 ),
55
56 /**
57 * Choose whether the order items should be retrieved and included
58 * within the CSV.
59 *
60 * @var checkbox
61 */
62 'useitems' => array(
63 'type' => 'checkbox',
64 'label' => JText::translate('VAP_EXPORT_DRIVER_CSV_USE_ITEMS_FIELD'),
65 'help' => JText::translate('VAP_EXPORT_DRIVER_CSV_USE_ITEMS_FIELD_HELP'),
66 'default' => 0,
67 ),
68
69 /**
70 * The separator character that will be used to separate the value
71 * of the columns.
72 *
73 * @var select
74 */
75 'delimiter' => array(
76 'type' => 'select',
77 'label' => JText::translate('VAP_EXPORT_DRIVER_CSV_DELIMITER_FIELD'),
78 'help' => JText::translate('VAP_EXPORT_DRIVER_CSV_DELIMITER_FIELD_HELP'),
79 'default' => ',',
80 'options' => array(
81 ',' => JText::translate('VAP_EXPORT_DRIVER_CSV_DELIMITER_FIELD_OPT_COMMA'),
82 ';' => JText::translate('VAP_EXPORT_DRIVER_CSV_DELIMITER_FIELD_OPT_SEMICOLON'),
83 ),
84 ),
85
86 /**
87 * The enclosure character that will be used to wrap, and escape,
88 * the value of the columns.
89 *
90 * @var select
91 */
92 'enclosure' => array(
93 'type' => 'select',
94 'label' => JText::translate('VAP_EXPORT_DRIVER_CSV_ENCLOSURE_FIELD'),
95 'help' => JText::translate('VAP_EXPORT_DRIVER_CSV_ENCLOSURE_FIELD_HELP'),
96 'default' => '"',
97 'options' => array(
98 '"' => JText::translate('VAP_EXPORT_DRIVER_CSV_ENCLOSURE_FIELD_OPT_DOUBLE_QUOTE'),
99 '\'' => JText::translate('VAP_EXPORT_DRIVER_CSV_ENCLOSURE_FIELD_OPT_SINGLE_QUOTE'),
100 ),
101 ),
102 );
103 }
104
105 /**
106 * @override
107 * Exports the reservations in the given format.
108 *
109 * @return string The resulting export string.
110 */
111 public function export()
112 {
113 // start catching output buffer
114 ob_start();
115
116 // open file resource pointing to PHP OUTPUT
117 $handle = fopen('php://output', 'w');
118
119 // output CSV to the given resource
120 $this->output($handle);
121
122 // catch buffer
123 $buffer = ob_get_contents();
124
125 // close resource
126 fclose($handle);
127
128 // close output buffer
129 ob_end_clean();
130
131 // strip trailing new line and return CSV string
132 return trim($buffer, "\n");
133 }
134
135 /**
136 * @override
137 * Downloads the reservations in a file compatible with the given format.
138 *
139 * @param string $filename The name of the file that will be downloaded.
140 *
141 * @return void
142 *
143 * @uses export()
144 */
145 public function download($filename = null)
146 {
147 if ($filename)
148 {
149 // strip file extension
150 $filename = preg_replace("/\.csv$/i", '', $filename);
151 }
152 else
153 {
154 // use current date time as name
155 $filename = JHtml::fetch('date', 'now', 'Y-m-d H_i_s');
156 }
157
158 $app = JFactory::getApplication();
159
160 // prepare headers
161 $this->prepareDownload($app, $filename);
162
163 // send headers
164 $app->sendHeaders();
165
166 // open file resource pointing to PHP OUTPUT
167 $handle = fopen('php://output', 'w');
168
169 // output CSV to the given resource
170 $this->output($handle);
171
172 // close resource
173 fclose($handle);
174 }
175
176 /**
177 * Prepares the application headers to start the download.
178 *
179 * @param mixed $app The client application.
180 * @param string $filename The name of the file that will be downloaded.
181 *
182 * @return void
183 */
184 protected function prepareDownload($app, $filename)
185 {
186 // prepare headers
187 $app->setHeader('Cache-Control', 'no-store, no-cache');
188 $app->setHeader('Content-Type', 'text/csv; charset=UTF-8');
189 $app->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '.csv"');
190 }
191
192 /**
193 * Generates the CSV structure by putting the fetched
194 * bytes into the specified resource.
195 *
196 * @param mixed $handle The resource pointer created with fopen().
197 *
198 * @return void
199 */
200 protected function output($handle)
201 {
202 if (!$handle)
203 {
204 throw new RuntimeException('Invalid resource for CSV generation');
205 }
206
207 $dispatcher = VAPFactory::getEventDispatcher();
208
209 // fetch timezone
210 if ($this->getOption('admin'))
211 {
212 $id_emp = (int) $this->getOption('id_employee');
213
214 if ($id_emp > 0)
215 {
216 // use employee timezone
217 $this->timezone = JModelVAP::getInstance('employee')->getTimezone($id_emp);
218 }
219 else
220 {
221 // use system timezone for admin
222 $this->timezone = JFactory::getApplication()->get('offset', 'UTC');
223 }
224
225 $this->timezone = new DateTimeZone($this->timezone);
226 }
227 else
228 {
229 // use timezone of currently logged-in user
230 $this->timezone = JFactory::getUser()->getTimezone();
231 }
232
233 // retrieve settings
234 $delimiter = $this->getOption('delimiter', ',');
235 $enclosure = $this->getOption('enclosure', '"');
236
237 $records = $this->getRecords();
238
239 /**
240 * Take all the exported services to make sure we are properly
241 * obtaining all the custom fields.
242 *
243 * @since 1.7.4
244 */
245 $all_services = array_map(function($record)
246 {
247 return (int) $record->id_service;
248 }, $records);
249
250 /**
251 * Load custom fields.
252 *
253 * Include the required checkboxes too to give an idea to the administrator about
254 * the reservations that accepted the terms of service.
255 *
256 * @since 1.7.5
257 */
258 VAPLoader::import('libraries.customfields.loader');
259 $this->customFields = VAPCustomFieldsLoader::getInstance()
260 // ->noRequiredCheckbox()
261 ->noInputFile()
262 ->noSeparator()
263 ->forService($all_services)
264 ->translate()
265 ->fetch();
266
267 // creates the CSV header
268 $head = $this->createHead();
269
270 // put head within the CSV
271 $this->putRow($handle, $head, $delimiter, $enclosure);
272
273 // iterate records and create arrays CSV-compatible
274 foreach ($records as $data)
275 {
276 // create CSV row
277 $row = $this->createRow($data);
278
279 // put records within the CSV
280 $this->putRow($handle, $row, $delimiter, $enclosure);
281 }
282
283 /**
284 * Trigger event to allow the plugins to append additional rows
285 * within the CSV file.
286 *
287 * @param array $records An array of database records.
288 * @param mixed $handler The current handler instance.
289 *
290 * @return array The rows to include. Must be an array of arrays.
291 *
292 * @since 1.7
293 */
294 $results = $dispatcher->trigger('onAfterBuildRowsCSV', array($records, $this));
295
296 // iterate plugin results
297 foreach ($results as $res)
298 {
299 // iterate result rows
300 foreach ($res as $row)
301 {
302 if (is_array($row))
303 {
304 // put record within the CSV
305 $this->putRow($handle, $row, $delimiter, $enclosure);
306 }
307 }
308 }
309 }
310
311 /**
312 * Inserts the row within the CSV file.
313 *
314 * @param mixed $handle The resource pointer created with fopen().
315 * @param array $row The row to include.
316 * @param mixed $delimiter The delimiter used to separate the columns
317 * @param mixed $enclosure The enclosure used to wrap the values.
318 *
319 * @return void
320 */
321 protected function putRow($handle, $row, $delimiter = null, $enclosure = null)
322 {
323 fputcsv($handle, $row, $delimiter, $enclosure, $escape = '');
324 }
325
326 /**
327 * Creates the CSV table header.
328 *
329 * @return array
330 */
331 protected function createHead()
332 {
333 $dispatcher = VAPFactory::getEventDispatcher();
334
335 $head = array();
336
337 // order number
338 $head['id'] = JText::translate('VAPMANAGERESERVATION0');
339 // order key
340 $head['sid'] = JText::translate('VAPMANAGERESERVATION2');
341 // created on
342 $head['date'] = JText::translate('VAPMANAGEMEDIA14');
343
344 if ($this->isGroup('appointment'))
345 {
346 // service
347 $head['service'] = JText::translate('VAPMANAGERESERVATION4');
348 // employee
349 $head['employee'] = JText::translate('VAPMANAGERESERVATION3');
350 // check-in
351 $head['checkin'] = JText::translate('VAPMANAGERESERVATION26');
352 // duration
353 $head['duration'] = JText::translate('VAPMANAGERESERVATION10');
354 // people
355 $head['people'] = JText::translate('VAPMANAGERESERVATION25');
356 }
357
358 // total net
359 $head['net'] = JText::translate('VAPINVTOTAL');
360 // total tax
361 $head['tax'] = JText::translate('VAPINVTAXES');
362 // total cost
363 $head['gross'] = JText::translate('VAPMANAGERESERVATION9');
364 // discount
365 $head['discount'] = JText::translate('VAPMANAGEPACKAGE13');
366 // payment
367 $head['payment'] = JText::translate('VAPMANAGERESERVATION13');
368 // coupon
369 $head['coupon'] = JText::translate('VAPMANAGERESERVATION21');
370 // status
371 $head['status'] = JText::translate('VAPMANAGERESERVATION19');
372 // purchaser nominative
373 $head['customer'] = JText::translate('VAPMANAGERESERVATION32');
374 // purchaser e-mail
375 $head['email'] = JText::translate('VAPMANAGERESERVATION8');
376 // purchaser phone
377 $head['phone'] = JText::translate('VAPMANAGERESERVATION27');
378
379 // iterate fields and push them within the head
380 foreach ($this->customFields as $field)
381 {
382 // exclude custom fields that are already displayed by
383 // using the purchaser information
384 if (!in_array($field['rule'], array('nominative', 'email', 'phone')))
385 {
386 $head['cf' . $field['id']] = $field['langname'];
387 }
388 }
389
390 // check if the items should be included
391 if ($this->getOption('useitems'))
392 {
393 if ($this->isGroup('appointment'))
394 {
395 // extra options
396 $head['items'] = JText::translate('VAPMANAGERESERVATION14');
397 }
398 }
399
400 /**
401 * Trigger event to allow the plugins to manipulate the heading
402 * row of the CSV file. Here it is possible to attach new columns,
403 * detach existing columns and reorder them. Notice that the same
404 * changes must be applied to the body of the CSV, otherwise the
405 * columns might result shifted.
406 *
407 * @param array &$head The CSV head array.
408 * @param mixed $handler The current handler instance.
409 *
410 * @return void
411 *
412 * @since 1.7
413 */
414 $dispatcher->trigger('onBuildHeadCSV', array(&$head, $this));
415
416 // reset keys
417 return array_values($head);
418 }
419
420 /**
421 * Creates a CSV table row.
422 *
423 * @param object $data The database record.
424 *
425 * @return array The resulting row.
426 */
427 protected function createRow($data)
428 {
429 $dispatcher = VAPFactory::getEventDispatcher();
430
431 $currency = VAPFactory::getCurrency();
432
433 $row = array();
434
435 if (VAPFactory::getConfig()->getBool('multitimezone'))
436 {
437 // include timezone name next to dates
438 $tz_str = ' (' . $this->timezone->getName() . ')';
439 }
440 else
441 {
442 $tz_str = '';
443 }
444
445 // order number
446 $row['id'] = $data->id;
447 // order key
448 $row['sid'] = $data->sid;
449 // creation date
450 $row['date'] = JHtml::fetch('date', $data->createdon, JText::translate('DATE_FORMAT_LC6'), $this->timezone->getName()) . $tz_str;
451
452 if ($this->isGroup('appointment'))
453 {
454 // service
455 $row['service'] = $data->service_name;
456 // employee
457 $row['employee'] = $data->employee_name;
458 // check-in
459 $row['checkin'] = JHtml::fetch('date', $data->checkin_ts, JText::translate('DATE_FORMAT_LC6'), $this->timezone->getName()) . $tz_str;
460 // duration
461 $row['duration'] = VikAppointments::formatMinutesToTime($data->duration);
462 // people
463 $row['people'] = $data->people;
464 }
465
466 // total net
467 $row['net'] = $currency->format($data->total_net);
468 // total tax
469 $row['tax'] = $currency->format($data->total_tax);
470 // total cost
471 $row['gross'] = $currency->format($data->total_cost);
472 // discount
473 $row['discount'] = $currency->format($data->discount);
474
475 $coupon = '';
476
477 if ($this->isGroup('appointment'))
478 {
479 if ($data->coupon_str)
480 {
481 list($coupon_code, $coupon_type, $coupon_amount) = explode(';;', $data->coupon_str);
482
483 $coupon = $coupon_code . ' : ' . ($coupon_type == 1 ? $coupon_amount . '%' : $currency->format($coupon_amount));
484 }
485 }
486
487 // payments
488 $row['payment'] = $data->payment_name;
489 // coupon
490 $row['coupon'] = $coupon;
491 // status
492 $row['status'] = JHtml::fetch('vaphtml.status.display', $data->status, 'plain');
493 // purchaser nominative
494 $row['customer'] = $data->purchaser_nominative;
495 // purchaser e-mail
496 $row['email'] = $data->purchaser_mail;
497 // purchaser phone
498 $row['phone'] = $data->purchaser_phone;
499
500 // decode custom fields and translate values
501 $cf = $data->custom_f ? (array) json_decode($data->custom_f, true) : [];
502 $cf = VAPCustomFieldsLoader::translateObject($cf, $this->customFields);
503
504 // iterate fields and push them within the head
505 foreach ($this->customFields as $field)
506 {
507 // exclude custom fields that are already displayed by
508 // using the purchaser information
509 if (!in_array($field['rule'], array('nominative', 'email', 'phone')))
510 {
511 $row['cf' . $field['id']] = isset($cf[$field['name']]) ? $cf[$field['name']] : '';
512 }
513 }
514
515 // check if the items should be included
516 if ($this->getOption('useitems'))
517 {
518 // items
519 $row['items'] = implode("\r\n", $data->items);
520 }
521
522 /**
523 * Trigger event to allow the plugins to manipulate the row that
524 * is going to be added into the CSV body. Here it is possible to
525 * attach new columns, detach existing columns and reorder them.
526 * Notice that the same changes must be applied to the head of the
527 * CSV, otherwise the columns might result shifted.
528 *
529 * @param array &$row The CSV body row.
530 * @param object $data The row fetched from the database.
531 * @param mixed $handler The current handler instance.
532 *
533 * @return void
534 *
535 * @since 1.7
536 */
537 $dispatcher->trigger('onBuildRowCSV', array(&$row, $data, $this));
538
539 // reset keys
540 return array_values($row);
541 }
542
543 /**
544 * Returns the list of records to export.
545 *
546 * @return array A list of records.
547 */
548 protected function getRecords()
549 {
550 $dispatcher = VAPFactory::getEventDispatcher();
551
552 $dbo = JFactory::getDbo();
553
554 $currency = VAPFactory::getCurrency();
555
556 $q = $dbo->getQuery(true);
557
558 if ($this->isGroup('appointment'))
559 {
560 // select all reservation columns
561 $q->select('r.*');
562 $q->from($dbo->qn('#__vikappointments_reservation', 'r'));
563
564 // get employee details
565 $q->select($dbo->qn('e.nickname', 'employee_name'));
566 $q->select($dbo->qn('e.timezone', 'employee_tz'));
567 $q->leftjoin($dbo->qn('#__vikappointments_employee', 'e') . ' ON ' . $dbo->qn('r.id_employee') . ' = ' . $dbo->qn('e.id'));
568
569 // get service details
570 $q->select($dbo->qn('s.name', 'service_name'));
571 $q->leftjoin($dbo->qn('#__vikappointments_service', 's') . ' ON ' . $dbo->qn('r.id_service') . ' = ' . $dbo->qn('s.id'));
572
573 // get selected payment method
574 $q->select($dbo->qn('gp.name', 'payment_name'));
575 $q->leftjoin($dbo->qn('#__vikappointments_gpayments', 'gp') . ' ON ' . $dbo->qn('r.id_payment') . ' = ' . $dbo->qn('gp.id'));
576
577 // check if the items should be loaded
578 if ($this->getOption('useitems'))
579 {
580 // get item details
581 $q->select(sprintf(
582 'IF(%2$s IS NOT NULL, CONCAT_WS(\' - \', %1$s, %2$s), %1$s) AS %3$s',
583 $dbo->qn('o.name'),
584 $dbo->qn('v.name'),
585 $dbo->qn('item_name')
586 ));
587 $q->select($dbo->qn('i.quantity', 'item_quantity'));
588 $q->select($dbo->qn('i.gross', 'item_total'));
589 $q->leftjoin($dbo->qn('#__vikappointments_res_opt_assoc', 'i') . ' ON ' . $dbo->qn('i.id_reservation') . ' = ' . $dbo->qn('r.id'));
590 $q->leftjoin($dbo->qn('#__vikappointments_option', 'o') . ' ON ' . $dbo->qn('i.id_option') . ' = ' . $dbo->qn('o.id'));
591 $q->leftjoin($dbo->qn('#__vikappointments_option_value', 'v') . ' ON ' . $dbo->qn('i.id_variation') . ' = ' . $dbo->qn('v.id'));
592 }
593
594 // DO NOT take closures
595 $q->where($dbo->qn('r.closure') . ' = 0');
596
597 // DO NOT take parent orders
598 $q->where($dbo->qn('r.id_parent') . ' > 0');
599
600 if ($this->getOption('confirmed'))
601 {
602 // get approved statuses
603 $approved = JHtml::fetch('vaphtml.status.find', 'code', array('appointments' => 1, 'approved' => 1));
604
605 if ($approved)
606 {
607 // filter by approved status
608 $q->where($dbo->qn('r.status') . ' IN (' . implode(',', array_map(array($dbo, 'q'), $approved)) . ')');
609 }
610 }
611
612 // include records with check-in equals or higher than
613 // the specified starting date
614 $from = $this->getOption('fromdate');
615
616 if (!VAPDateHelper::isNull($from))
617 {
618 $q->where($dbo->qn('r.checkin_ts') . ' >= ' . $dbo->q($from));
619 }
620
621 // include records with check-in equals or lower than
622 // the specified ending date
623 $to = $this->getOption('todate');
624
625 if (!VAPDateHelper::isNull($to))
626 {
627 $q->where($dbo->qn('r.checkin_ts') . ' <= ' . $dbo->q($to));
628 }
629
630 // retrieve only the selected records, if any
631 $ids = $this->getOption('cid');
632
633 if ($ids)
634 {
635 /**
636 * The export system is now able to fetch also the appointments assigned to a parent order.
637 *
638 * @since 1.7.4
639 */
640 $q->andWhere([
641 $dbo->qn('r.id') . ' IN (' . implode(',', array_map('intval', $ids)) . ')',
642 $dbo->qn('r.id_parent') . ' IN (' . implode(',', array_map('intval', $ids)) . ')',
643 ], 'OR');
644 }
645
646 // retrieve employee filter, if any
647 $id_emp = $this->getOption('id_employee');
648
649 if ($id_emp)
650 {
651 $q->where($dbo->qn('r.id_employee') . ' = ' . (int) $id_emp);
652 }
653
654 // order by ascending checkin
655 $q->order($dbo->qn('r.checkin_ts') . ' ASC');
656 }
657
658 /**
659 * Trigger event to allow the plugins to manipulate the query used to retrieve
660 * a standard list of records.
661 *
662 * @param mixed &$query The query string or a query builder object.
663 * @param mixed $options A configuration registry.
664 *
665 * @return void
666 *
667 * @since 1.7
668 */
669 $dispatcher->trigger('onBeforeListQueryExportCSV', array(&$q, $this->options));
670
671 $dbo->setQuery($q);
672 $list = $dbo->loadObjectList();
673
674 if (!$list)
675 {
676 // no rows found
677 return array();
678 }
679
680 $rows = array();
681
682 foreach ($list as $obj)
683 {
684 if (!isset($rows[$obj->id]))
685 {
686 $rows[$obj->id] = $obj;
687
688 $rows[$obj->id]->items = array();
689 }
690
691 // group reservation items
692 if (!empty($obj->item_name))
693 {
694 $rows[$obj->id]->items[] = sprintf(
695 "%dx %s\t(%s)",
696 $obj->item_quantity,
697 $obj->item_name,
698 $currency->format($obj->item_total)
699 );
700 }
701 }
702
703 /**
704 * Trigger event to allow the plugins to manipulate response fetched by
705 * the query used to retrieve a standard list of records.
706 *
707 * @param mixed &$rows An array of results (objects).
708 * @param mixed $options A configuration registry.
709 *
710 * @return void
711 *
712 * @since 1.7
713 */
714 $dispatcher->trigger('onAfterListQueryExportCSV', array(&$rows, $this->options));
715
716 return array_values($rows);
717 }
718 }
719