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 / classes / appointment.php
vikappointments / site / helpers / libraries / order / classes Last commit date
appointment.php 3 days ago empsubscr.php 3 days ago index.html 3 days ago package.php 3 days ago subscr.php 3 days ago
appointment.php
1189 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 VAPLoader::import('libraries.order.wrapper');
15
16 /**
17 * Appointments order class wrapper.
18 *
19 * @since 1.7
20 */
21 class VAPOrderAppointment extends VAPOrderWrapper
22 {
23 /**
24 * Class constructor.
25 *
26 * @param integer $id The order ID.
27 * @param mixed $langtag The language tag. If null, the default one will be used.
28 * @param array $options An array of options to be passed to the order instance.
29 */
30 public function __construct($id, $langtag = null, array $options = array())
31 {
32 // always force translation
33 $options['translate'] = true;
34
35 // get current language tag
36 $default_lang = JFactory::getLanguage()->getTag();
37
38 // construct with parent
39 parent::__construct($id, $langtag, $options);
40
41 // restore previous language according to the current cllient
42 VikAppointments::loadLanguage($default_lang);
43 }
44
45 /**
46 * Helper method used to access all the user notes that belong to the
47 * specified appointment/order.
48 *
49 * @return array
50 */
51 public function getUserNotes($id = null)
52 {
53 $notes = array();
54
55 foreach ($this->notes as $note)
56 {
57 // take only public notes
58 if ($note->status && (!$id || $id == $note->id_parent))
59 {
60 $notes[] = $note;
61 }
62 }
63
64 return $notes;
65 }
66
67 /**
68 * Returns an array containing the requested information of the attendees.
69 *
70 * @param string $column The information to access (name, mail, phone).
71 *
72 * @return array An array of values.
73 */
74 public function getAttendeesInfo($column = 'mail')
75 {
76 // fetch property name
77 switch ($column)
78 {
79 case 'name':
80 case 'nominative':
81 $column = 'purchaser_nominative';
82 break;
83
84 case 'mail':
85 case 'email':
86 $column = 'purchaser_mail';
87 break;
88
89 case 'tel':
90 case 'phone':
91 $column = 'purchaser_phone';
92 break;
93 }
94
95 $list = array();
96
97 // include author info, if existing
98 $list[] = $this->get($column, null);
99
100 // iterate attendees
101 foreach ($this->attendees as $attendee)
102 {
103 // make sure the requested property exists
104 if (isset($attendee[$column]))
105 {
106 $list[] = $attendee[$column];
107 }
108 }
109
110 // get rid of empty and duplicate values
111 return array_values(array_unique(array_filter($list)));
112 }
113
114 /**
115 * @override
116 * Returns the appointments order object.
117 *
118 * @param integer $id The order ID.
119 * @param mixed $langtag The language tag. If null, the default one will be used.
120 * @param array $options An array of options to be passed to the order instance.
121 *
122 * @return mixed The array/object to load.
123 *
124 * @throws Exception
125 */
126 protected function load($id, $langtag = null, array $options = array())
127 {
128 $dbo = JFactory::getDbo();
129 $config = VAPFactory::getConfig();
130 $dispatcher = VAPFactory::getEventDispatcher();
131
132 // create query
133 $q = $dbo->getQuery(true);
134
135 // select all reservation columns
136 $q->select('r.*');
137 $q->from($dbo->qn('#__vikappointments_reservation', 'r'));
138
139 // select employee details
140 $q->select($dbo->qn('e.nickname', 'employee_name'));
141 $q->select($dbo->qn('e.email', 'employee_email'));
142 $q->select($dbo->qn('e.phone', 'employee_phone'));
143 $q->select($dbo->qn('e.notify', 'employee_notify'));
144 $q->select($dbo->qn('e.showemail', 'employee_replyto'));
145 $q->select($dbo->qn('e.image', 'employee_image'));
146 $q->select($dbo->qn('e.timezone'));
147 $q->leftjoin($dbo->qn('#__vikappointments_employee', 'e') . ' ON ' . $dbo->qn('r.id_employee') . ' = ' . $dbo->qn('e.id'));
148
149 // select employee group details
150 $q->select($dbo->qn('ge.id', 'employee_group_id'));
151 $q->select($dbo->qn('ge.name', 'employee_group_name'));
152 $q->leftjoin($dbo->qn('#__vikappointments_employee_group', 'ge') . ' ON ' . $dbo->qn('e.id_group') . ' = ' . $dbo->qn('ge.id'));
153
154 // select service details
155 $q->select($dbo->qn('s.name', 'service_name'));
156 $q->select($dbo->qn('s.description', 'service_description'));
157 $q->select($dbo->qn('s.image', 'service_image'));
158 $q->select($dbo->qn('s.max_capacity', 'service_max_capacity'));
159 $q->select($dbo->qn('s.choose_emp', 'service_choose_emp'));
160 $q->select($dbo->qn('s.attachments', 'service_attachments'));
161 $q->leftjoin($dbo->qn('#__vikappointments_service', 's') . ' ON ' . $dbo->qn('r.id_service') . ' = ' . $dbo->qn('s.id'));
162
163 // select service group details
164 $q->select($dbo->qn('gs.id', 'service_group_id'));
165 $q->select($dbo->qn('gs.name', 'service_group_name'));
166 $q->leftjoin($dbo->qn('#__vikappointments_group', 'gs') . ' ON ' . $dbo->qn('s.id_group') . ' = ' . $dbo->qn('gs.id'));
167
168 // select payment details
169 $q->select($dbo->qn('gp.name', 'payment_name'));
170 $q->select($dbo->qn('gp.file', 'payment_file'));
171 $q->select($dbo->qn('gp.note', 'payment_note'));
172 $q->select($dbo->qn('gp.prenote', 'payment_prenote'));
173 $q->select($dbo->qn('gp.icontype', 'payment_icontype'));
174 $q->select($dbo->qn('gp.icon', 'payment_icon'));
175 $q->leftjoin($dbo->qn('#__vikappointments_gpayments', 'gp') . ' ON ' . $dbo->qn('r.id_payment') . ' = ' . $dbo->qn('gp.id'));
176
177 // select purchased options
178 $q->select($dbo->qn('oi.id', 'option_id'));
179 $q->select($dbo->qn('oi.id_option', 'option_id_option'));
180 $q->select($dbo->qn('oi.id_variation', 'option_id_variation'));
181 $q->select($dbo->qn('oi.quantity', 'option_quantity'));
182 $q->select($dbo->qn('oi.inc_price', 'option_price'));
183 $q->select($dbo->qn('oi.net', 'option_net'));
184 $q->select($dbo->qn('oi.tax', 'option_tax'));
185 $q->select($dbo->qn('oi.gross', 'option_gross'));
186 $q->select($dbo->qn('oi.discount', 'option_discount'));
187 $q->select($dbo->qn('oi.tax_breakdown', 'option_tax_breakdown'));
188 $q->select($dbo->qn('o.name', 'option_name'));
189 $q->select($dbo->qn('o.description', 'option_description'));
190 $q->select($dbo->qn('o.image', 'option_image'));
191 $q->select($dbo->qn('o.single', 'option_single'));
192 $q->select($dbo->qn('o.duration', 'option_duration'));
193 $q->select($dbo->qn('ov.inc_duration', 'option_var_duration'));
194 $q->leftjoin($dbo->qn('#__vikappointments_res_opt_assoc', 'oi') . ' ON ' . $dbo->qn('oi.id_reservation') . ' = ' . $dbo->qn('r.id'));
195 $q->leftjoin($dbo->qn('#__vikappointments_option', 'o') . ' ON ' . $dbo->qn('oi.id_option') . ' = ' . $dbo->qn('o.id'));
196 $q->leftjoin($dbo->qn('#__vikappointments_option_value', 'ov') . ' ON ' . $dbo->qn('oi.id_variation') . ' = ' . $dbo->qn('ov.id'));
197
198 // exclude closures
199 $q->where($dbo->qn('r.closure') . ' = 0');
200
201 // filter by order key, if specified
202 if (isset($options['sid']))
203 {
204 $q->where($dbo->qn('r.sid') . ' = ' . $dbo->q($options['sid']));
205 }
206
207 // load appointments matching the specified ID/parent
208 $q->andWhere(array(
209 $dbo->qn('r.id') . ' = ' . (int) $id,
210 $dbo->qn('r.id_parent') . ' = ' . (int) $id,
211 ), 'OR');
212
213 // filter by confirmation key, if specified
214 if (isset($options['conf_key']))
215 {
216 $q->where($dbo->qn('r.conf_key') . ' = ' . $dbo->q($options['conf_key']));
217 }
218
219 $q->order($dbo->qn('r.id') . ' ASC');
220 $q->order($dbo->qn('oi.id') . ' ASC');
221
222 /**
223 * External plugins can attach to this hook in order to manipulate
224 * the query at runtime, in example to alter the default ordering.
225 *
226 * @param mixed &$query A query builder instance.
227 * @param integer $id The ID of the order.
228 * @param mixed $langtag The language tag. If null, the default one will be used.
229 * @param array $options An array of options to be passed to the order instance.
230 *
231 * @return void
232 *
233 * @since 1.7
234 */
235 $dispatcher->trigger('onLoadAppointmentsOrderDetails', array(&$q, $id, $langtag, $options));
236
237 $dbo->setQuery($q);
238 $list = $dbo->loadObjectList();
239
240 if (!$list)
241 {
242 // order not found raise error
243 throw new Exception(sprintf('Order [%d] not found', $id), 404);
244 }
245
246 if (!$langtag)
247 {
248 // use order lang tag in case it was not specified
249 $langtag = $list[0]->langtag;
250
251 if (!$langtag)
252 {
253 // the order is not assigned to any lang tag, use the current one
254 $langtag = JFactory::getLanguage()->getTag();
255 }
256 }
257
258
259 // Load the language adjusted to the specified one, because this method
260 // uses JText to translate some contents, such as the date formats.
261 // Otherwise, when sending a notification from the back-end, in case the
262 // language differs, the dates will be reported in a wrong locale.
263 VikAppointments::loadLanguage($langtag, JPATH_SITE);
264
265 // get current user timezone
266 $tz = JFactory::getUser()->getTimezone()->getName();
267
268 // get system timezone
269 $system_tz = JFactory::getApplication()->get('offset', 'UTC');
270
271 if ($list[0]->user_timezone)
272 {
273 // use the timezone saved within the appointment record
274 $customer_tz = $list[0]->user_timezone;
275 }
276 else if ($list[0]->createdby > 0)
277 {
278 // Get customer timezone.
279 // Notice that it relies on the used ID of the author. This means that
280 // the timezone might not be correct in case the reservation has been
281 // created from the back-end.
282 $customer_tz = JFactory::getUser($list[0]->createdby)->getTimezone()->getName();
283 }
284 else
285 {
286 // use system timezone
287 $customer_tz = $system_tz;
288 }
289
290 // get working time model
291 $wdModel = JModelVAP::getInstance('worktime');
292 // get location model
293 $locModel = JModelVAP::getInstance('location');
294
295 // create parent order details
296 $order = $list[0];
297
298 // register customer timezone
299 $order->customerTimezone = $customer_tz;
300
301 $order->appointments = array();
302 $order->sameEmp = array();
303
304 foreach ($list as $row)
305 {
306 if ($row->id_employee <= 0)
307 {
308 // we have a parent ID, skip it
309 continue;
310 }
311
312 // check if we have already registered the appointment
313 if (!isset($order->appointments[$row->id]))
314 {
315 // create appointment object
316 $app = new stdClass;
317 $app->id = $row->id;
318 $app->sid = $row->sid;
319
320 // create employee object
321 $app->employee = new stdClass;
322 $app->employee->id = $this->detach($row, 'id_employee');
323 $app->employee->name = $this->detach($row, 'employee_name');
324 $app->employee->email = $this->detach($row, 'employee_email');
325 $app->employee->phone = $this->detach($row, 'employee_phone');
326 $app->employee->image = $this->detach($row, 'employee_image');
327 $app->employee->notify = $this->detach($row, 'employee_notify');
328 $app->employee->replyto = (bool) $this->detach($row, 'employee_replyto');
329
330 $order->sameEmp[] = $app->employee->id;
331
332 // create employee group object
333 if ($row->employee_group_id)
334 {
335 $app->employee->group = new stdClass;
336 $app->employee->group->id = $this->detach($row, 'employee_group_id');
337 $app->employee->group->name = $this->detach($row, 'employee_group_name');
338 }
339 else
340 {
341 $app->employee->group = null;
342 }
343
344 // create service object
345 $app->service = new stdClass;
346 $app->service->id = $this->detach($row, 'id_service');
347 $app->service->name = $this->detach($row, 'service_name');
348 $app->service->description = $this->detach($row, 'service_description');
349 $app->service->price = $this->detach($row, 'service_price');
350 $app->service->image = $this->detach($row, 'service_image');
351 $app->service->maxCapacity = $this->detach($row, 'service_max_capacity');
352 $app->service->chooseEmployee = $this->detach($row, 'service_choose_emp');
353 $app->service->attachments = $this->detach($row, 'service_attachments');
354 // decode attachments
355 $app->service->attachments = $app->service->attachments ? (array) json_decode($app->service->attachments, true) : array();
356
357 // create service group object
358 if ($row->service_group_id)
359 {
360 $app->service->group = new stdClass;
361 $app->service->group->id = $this->detach($row, 'service_group_id');
362 $app->service->group->name = $this->detach($row, 'service_group_name');
363 }
364 else
365 {
366 $app->service->group = null;
367 }
368
369 // create check-in object relative to current user timezone
370 $app->checkin = new stdClass;
371 $app->checkin->lc = JHtml::fetch('date', $row->checkin_ts, JText::translate('DATE_FORMAT_LC1') . ' ' . $config->get('timeformat'));
372 $app->checkin->lc2 = JHtml::fetch('date', $row->checkin_ts, JText::translate('DATE_FORMAT_LC2'));
373 $app->checkin->lc3 = JHtml::fetch('date', $row->checkin_ts, JText::translate('DATE_FORMAT_LC3') . ' ' . $config->get('timeformat'));
374 $app->checkin->date = JHtml::fetch('date', $row->checkin_ts, JText::translate('DATE_FORMAT_LC5'));
375 $app->checkin->utc = $row->checkin_ts;
376 // register ISO 8601 date format (Y-m-dTH:i:sZ)
377 $app->checkin->iso8601 = JFactory::getDate($row->checkin_ts)->toISO8601();
378 // register check-in UNIX timestamp (UTC)
379 $app->checkin->timestamp = JFactory::getDate($row->checkin_ts)->getTimestamp();
380 // register check-in timezone
381 $app->checkin->timezone = $tz;
382
383 $checkout = VikAppointments::getCheckout($row->checkin_ts, $row->duration);
384
385 // create check-out object relative to current user timezone
386 $app->checkout = new stdClass;
387 $app->checkout->lc = JHtml::fetch('date', $checkout, JText::translate('DATE_FORMAT_LC1') . ' ' . $config->get('timeformat'));
388 $app->checkout->lc2 = JHtml::fetch('date', $checkout, JText::translate('DATE_FORMAT_LC2'));
389 $app->checkout->lc3 = JHtml::fetch('date', $checkout, JText::translate('DATE_FORMAT_LC3') . ' ' . $config->get('timeformat'));
390 $app->checkout->date = JHtml::fetch('date', $checkout, JText::translate('DATE_FORMAT_LC5'));
391 $app->checkout->utc = $checkout;
392 // register ISO 8601 date format (Y-m-dTH:i:sZ)
393 $app->checkout->iso8601 = JFactory::getDate($checkout)->toISO8601();
394 // register check-out timezone
395 $app->checkout->timezone = $tz;
396
397 if (!$row->timezone)
398 {
399 // use system timezone for employees that do not specify it
400 $row->timezone = $system_tz;
401 }
402
403 // create check-in object relative to employee timezone
404 $app->employee->checkin = new stdClass;
405 $app->employee->checkin->lc = JHtml::fetch('date', $row->checkin_ts, JText::translate('DATE_FORMAT_LC1') . ' ' . $config->get('timeformat'), $row->timezone);
406 $app->employee->checkin->lc2 = JHtml::fetch('date', $row->checkin_ts, JText::translate('DATE_FORMAT_LC2'), $row->timezone);
407 $app->employee->checkin->lc3 = JHtml::fetch('date', $row->checkin_ts, JText::translate('DATE_FORMAT_LC3') . ' ' . $config->get('timeformat'), $row->timezone);
408 $app->employee->checkin->date = JHtml::fetch('date', $row->checkin_ts, JText::translate('DATE_FORMAT_LC5'), $row->timezone);
409 // register check-in timezone
410 $app->employee->checkin->timezone = $row->timezone;
411
412 // create check-out object relative to employee timezone
413 $app->employee->checkout = new stdClass;
414 $app->employee->checkout->lc = JHtml::fetch('date', $checkout, JText::translate('DATE_FORMAT_LC1') . ' ' . $config->get('timeformat'), $row->timezone);
415 $app->employee->checkout->lc2 = JHtml::fetch('date', $checkout, JText::translate('DATE_FORMAT_LC2'), $row->timezone);
416 $app->employee->checkout->lc3 = JHtml::fetch('date', $checkout, JText::translate('DATE_FORMAT_LC3') . ' ' . $config->get('timeformat'), $row->timezone);
417 $app->employee->checkout->date = JHtml::fetch('date', $checkout, JText::translate('DATE_FORMAT_LC5'), $row->timezone);
418 // register check-out timezone
419 $app->employee->checkout->timezone = $row->timezone;
420
421 // create check-in object relative to customer timezone
422 $app->customerCheckin = new stdClass;
423 $app->customerCheckin->lc = JHtml::fetch('date', $row->checkin_ts, JText::translate('DATE_FORMAT_LC1') . ' ' . $config->get('timeformat'), $customer_tz);
424 $app->customerCheckin->lc2 = JHtml::fetch('date', $row->checkin_ts, JText::translate('DATE_FORMAT_LC2'), $customer_tz);
425 $app->customerCheckin->lc3 = JHtml::fetch('date', $row->checkin_ts, JText::translate('DATE_FORMAT_LC3') . ' ' . $config->get('timeformat'), $customer_tz);
426 $app->customerCheckin->date = JHtml::fetch('date', $row->checkin_ts, JText::translate('DATE_FORMAT_LC5'), $customer_tz);
427 // register check-in timezone
428 $app->customerCheckin->timezone = $customer_tz;
429
430 // create check-out object relative to customer timezone
431 $app->customerCheckout = new stdClass;
432 $app->customerCheckout->lc = JHtml::fetch('date', $checkout, JText::translate('DATE_FORMAT_LC1') . ' ' . $config->get('timeformat'), $customer_tz);
433 $app->customerCheckout->lc2 = JHtml::fetch('date', $checkout, JText::translate('DATE_FORMAT_LC2'), $customer_tz);
434 $app->customerCheckout->lc3 = JHtml::fetch('date', $checkout, JText::translate('DATE_FORMAT_LC3') . ' ' . $config->get('timeformat'), $customer_tz);
435 $app->customerCheckout->date = JHtml::fetch('date', $checkout, JText::translate('DATE_FORMAT_LC5'), $customer_tz);
436 // register check-out timezone
437 $app->customerCheckout->timezone = $customer_tz;
438
439 // create check-in object relative to system timezone
440 $app->systemCheckin = new stdClass;
441 $app->systemCheckin->lc = JHtml::fetch('date', $row->checkin_ts, JText::translate('DATE_FORMAT_LC1') . ' ' . $config->get('timeformat'), $system_tz);
442 $app->systemCheckin->lc2 = JHtml::fetch('date', $row->checkin_ts, JText::translate('DATE_FORMAT_LC2'), $system_tz);
443 $app->systemCheckin->lc3 = JHtml::fetch('date', $row->checkin_ts, JText::translate('DATE_FORMAT_LC3') . ' ' . $config->get('timeformat'), $system_tz);
444 $app->systemCheckin->date = JHtml::fetch('date', $row->checkin_ts, JText::translate('DATE_FORMAT_LC5'), $system_tz);
445 // register check-in timezone
446 $app->systemCheckin->timezone = $system_tz;
447
448 // create check-out object relative to system timezone
449 $app->systemCheckout = new stdClass;
450 $app->systemCheckout->lc = JHtml::fetch('date', $checkout, JText::translate('DATE_FORMAT_LC1') . ' ' . $config->get('timeformat'), $system_tz);
451 $app->systemCheckout->lc2 = JHtml::fetch('date', $checkout, JText::translate('DATE_FORMAT_LC2'), $system_tz);
452 $app->systemCheckout->lc3 = JHtml::fetch('date', $checkout, JText::translate('DATE_FORMAT_LC3') . ' ' . $config->get('timeformat'), $system_tz);
453 $app->systemCheckout->date = JHtml::fetch('date', $checkout, JText::translate('DATE_FORMAT_LC5'), $system_tz);
454 // register check-out timezone
455 $app->systemCheckout->timezone = $system_tz;
456
457 // inject appointment details
458 $app->duration = $this->detach($row, 'duration');
459 $app->sleep = $this->detach($row, 'sleep');
460 $app->people = $this->detach($row, 'people');
461 $app->viewEmp = $this->detach($row, 'view_emp');
462 $app->status = $row->status;
463
464 $app->statusRole = null;
465
466 // fetch status role
467 if (JHtml::fetch('vaphtml.status.ispending', 'appointments', $app->status))
468 {
469 $app->statusRole = 'PENDING';
470 }
471 else if (JHtml::fetch('vaphtml.status.isapproved', 'appointments', $app->status))
472 {
473 $app->statusRole = 'APPROVED';
474 }
475 else if (JHtml::fetch('vaphtml.status.isremoved', 'appointments', $app->status))
476 {
477 $app->statusRole = 'EXPIRED';
478 }
479 else if (JHtml::fetch('vaphtml.status.iscancelled', 'appointments', $app->status))
480 {
481 $app->statusRole = 'CANCELLED';
482 }
483
484 // create appointment sub-totals
485 $app->totals = new stdClass;
486 $app->totals->net = $this->detach($row, 'service_net');
487 $app->totals->tax = $this->detach($row, 'service_tax');
488 $app->totals->gross = $this->detach($row, 'service_gross');
489 $app->totals->grossOpt = $app->totals->gross;
490 $app->totals->discount = $this->detach($row, 'service_discount');
491 $app->totals->breakdown = $row->tax_breakdown ? json_decode($this->detach($row, 'tax_breakdown')) : null;
492
493 // find location of the appointment
494 $id_location = $wdModel->getLocation($app->checkin->utc, $app->service->id, $app->employee->id);
495 // get location details
496 $app->location = $locModel->getInfo($id_location);
497
498 // init options array
499 $app->options = array();
500
501 // register appointment
502 $order->appointments[$row->id] = $app;
503 }
504
505 // check if we have an option to register
506 if (!empty($row->option_id))
507 {
508 // create option
509 $option = new stdClass;
510 $option->id_assoc = $this->detach($row, 'option_id');
511 $option->id = $this->detach($row, 'option_id_option');
512 $option->id_variation = $this->detach($row, 'option_id_variation');
513 $option->name = $this->detach($row, 'option_name');
514 $option->varName = $this->detach($row, 'option_var_name');
515 $option->fullName = $option->name . ($option->varName ? ' - ' . $option->varName : '');
516 $option->description = $this->detach($row, 'option_description');
517 $option->image = $this->detach($row, 'option_image');
518 $option->multiple = $this->detach($row, 'option_single');
519 $option->quantity = $this->detach($row, 'option_quantity');
520 $option->price = $this->detach($row, 'option_price');
521
522 // create option totals
523 $option->totals = new stdClass;
524 $option->totals->net = $this->detach($row, 'option_net');
525 $option->totals->tax = $this->detach($row, 'option_tax');
526 $option->totals->gross = $this->detach($row, 'option_gross');
527 $option->totals->discount = $this->detach($row, 'option_discount');
528 $option->totals->breakdown = $row->option_tax_breakdown ? json_decode($this->detach($row, 'option_tax_breakdown')) : null;
529
530 /**
531 * Include information about the extra duration provided by the option, if configured.
532 *
533 * @since 1.7.7
534 */
535 if ($duration = (int) $this->detach($row, 'option_duration'))
536 {
537 $option->duration = new stdClass;
538 $option->duration->extraUnit = $duration + ($this->detach($row, 'option_var_duration') ?: 0);
539 $option->duration->extraTotal = $option->duration->extraUnit * $option->quantity;
540 }
541 else
542 {
543 $option->duration = null;
544 }
545
546 // register option within appointment record
547 $order->appointments[$row->id]->options[] = $option;
548
549 // increase appointment gross by the option cost
550 $order->appointments[$row->id]->totals->grossOpt += $option->totals->gross;
551 }
552 }
553
554 // reset array keys
555 $order->appointments = array_values($order->appointments);
556
557 // fetch coupon
558 if ($order->coupon_str)
559 {
560 list($code, $type, $amount) = explode(';;', $this->detach($order, 'coupon_str'));
561
562 $order->coupon = new stdClass;
563 $order->coupon->code = $code;
564 $order->coupon->amount = $amount;
565 $order->coupon->type = $type;
566 }
567 else
568 {
569 $order->coupon = null;
570 $this->detach($order, 'coupon_str');
571 }
572
573 // decode stored CF data
574 $order->custom_f = $order->custom_f ? (array) json_decode($order->custom_f, true) : [];
575 $order->fields = $order->custom_f;
576 $order->displayFields = $order->fields;
577
578 $vars = array_values($order->fields);
579 $vars = array_filter($vars, function($elem)
580 {
581 if (is_array($elem))
582 {
583 return (bool) $elem;
584 }
585
586 return strlen($elem);
587 });
588
589 $order->hasFields = (bool) $vars;
590
591 // decode uploads
592 $order->uploads = json_decode($order->uploads);
593
594 // decode attendees
595 $order->attendees = $order->attendees ? (array) json_decode($order->attendees, true) : array();
596
597 /**
598 * Get rid of records that do not specify any details for the attendee.
599 *
600 * @since 1.7.1
601 */
602 $order->attendees = array_filter($order->attendees, function($attendee)
603 {
604 if (!empty($attendee['fields']))
605 {
606 foreach ((array) $attendee['fields'] as $v)
607 {
608 if (strlen($v))
609 {
610 return true;
611 }
612 }
613 }
614
615 if (!empty($attendee['uploads']))
616 {
617 foreach ((array) $attendee['uploads'] as $v)
618 {
619 if (strlen($v))
620 {
621 return true;
622 }
623 }
624 }
625
626 return false;
627 });
628
629 // fetch payment data
630 if ($order->payment_file)
631 {
632 $order->payment = new stdClass;
633 $order->payment->id = $this->detach($order, 'id_payment');
634 $order->payment->name = $this->detach($order, 'payment_name');
635 $order->payment->driver = $this->detach($order, 'payment_file');
636 $order->payment->iconType = $this->detach($order, 'payment_icontype');
637 $order->payment->icon = $this->detach($order, 'payment_icon');
638
639 if ($order->payment->iconType == 1)
640 {
641 // Font Icon
642 $order->payment->fontIcon = $order->payment->icon;
643 }
644 else
645 {
646 // Image Icon
647 $order->payment->iconURI = JUri::root() . $order->payment->icon;
648
649 // fetch Font Icon based on payment driver
650 switch ($order->payment->driver)
651 {
652 case 'bank_transfer.php':
653 $order->payment->fontIcon = 'fas fa-money-bill';
654 break;
655
656 case 'paypal.php':
657 $order->payment->fontIcon = 'fab fa-paypal';
658 break;
659
660 default:
661 $order->payment->fontIcon = 'fas fa-credit-card';
662 }
663 }
664
665 $order->payment->notes = new stdClass;
666 $order->payment->notes->beforePurchase = $this->detach($order, 'payment_prenote');
667 $order->payment->notes->afterPurchase = $this->detach($order, 'payment_note');
668 }
669 else
670 {
671 $order->payment = null;
672 }
673
674 // setup totals
675 $order->totals = new stdClass;
676 $order->totals->net = $this->detach($order, 'total_net');
677 $order->totals->tax = $this->detach($order, 'total_tax');
678 $order->totals->gross = $this->detach($order, 'total_cost');
679 $order->totals->deposit = $this->detach($order, 'deposit');
680 $order->totals->discount = $this->detach($order, 'discount');
681 $order->totals->paid = $this->detach($order, 'tot_paid');
682 $order->totals->payCharge = $this->detach($order, 'payment_charge');
683 $order->totals->payTax = $this->detach($order, 'payment_tax');
684 $order->totals->due = $order->totals->gross - $order->totals->paid;
685
686 if (!$order->paid)
687 {
688 // fetch paid flag based on current order status
689 $order->paid = JHtml::fetch('vaphtml.status.ispaid', 'appointments', $order->status);
690 }
691
692 if ($order->paid)
693 {
694 // amount paid, no remaining balance
695 $order->totals->due = 0;
696 }
697
698 $order->statusRole = null;
699
700 // fetch status role
701 if (JHtml::fetch('vaphtml.status.ispending', 'appointments', $order->status))
702 {
703 $order->statusRole = 'PENDING';
704 }
705 else if (JHtml::fetch('vaphtml.status.isapproved', 'appointments', $order->status))
706 {
707 $order->statusRole = 'APPROVED';
708 }
709 else if (JHtml::fetch('vaphtml.status.isremoved', 'appointments', $order->status))
710 {
711 $order->statusRole = 'EXPIRED';
712 }
713 else if (JHtml::fetch('vaphtml.status.iscancelled', 'appointments', $order->status))
714 {
715 $order->statusRole = 'CANCELLED';
716 }
717
718 // flag used to check whether all the appointments have been
719 // booked for the same employee
720 $order->sameEmp = count(array_unique($order->sameEmp)) == 1;
721
722 /**
723 * External plugins can use this event to manipulate the object holding
724 * the details of the order. Useful to inject all the additional data
725 * fetched with the manipulation of the query.
726 *
727 * @param mixed $order The order details object.
728 * @param array $list The query resulting array.
729 *
730 * @return void
731 */
732 $dispatcher->trigger('onSetupAppointmentsOrderDetails', array($order, $list));
733
734 $unsetList = array(
735 'id_service',
736 'id_employee',
737 'checkin_ts',
738 'duration',
739 'sleep',
740 'people',
741 'tax_breakdown',
742 'timezone',
743 'view_emp',
744 // unset notes because they are loaded through the getNotes() method
745 'notes',
746 );
747
748 // get rid of not needed properties
749 foreach (get_object_vars($order) as $k => $v)
750 {
751 if (preg_match("/^(employee|service|option|payment)_/", $k))
752 {
753 // the property is probably related to the appointment
754 // or it results blank (so it was not parsed)
755 unset($order->{$k});
756 }
757 else if (preg_match("/^__/", $k))
758 {
759 // remove deprecated (back-up) property
760 unset($order->{$k});
761 }
762 else if (in_array($k, $unsetList))
763 {
764 // remove property if contained in the list
765 unset($order->{$k});
766 }
767 }
768
769 return $order;
770 }
771
772 /**
773 * @override
774 * Translates the internal properties.
775 *
776 * @param mixed $langtag The language tag. If null, the default one will be used.
777 *
778 * @return void
779 */
780 protected function translate($langtag = null)
781 {
782 $dispatcher = VAPFactory::getEventDispatcher();
783
784 if (!$langtag)
785 {
786 // use order lang tag in case it was not specified
787 $langtag = $this->get('langtag', null);
788
789 if (!$langtag)
790 {
791 // the order is not assigned to any lang tag, use the current one
792 $langtag = JFactory::getLanguage()->getTag();
793 }
794 }
795
796 // load front-end language
797 VikAppointments::loadLanguage($langtag, JPATH_SITE);
798
799 // get translator
800 $translator = VAPFactory::getTranslator();
801
802 $service_ids = array();
803 $ser_group_ids = array();
804 $emp_group_ids = array();
805 $employee_ids = array();
806 $option_ids = array();
807 $optvar_ids = array();
808
809 foreach ($this->appointments as $app)
810 {
811 $service_ids[] = $app->service->id;
812 $employee_ids[] = $app->employee->id;
813
814 if ($app->service->group)
815 {
816 $ser_group_ids[] = $app->service->group->id;
817 }
818
819 if ($app->employee->group)
820 {
821 $emp_group_ids[] = $app->employee->group->id;
822 }
823
824 foreach ($app->options as $opt)
825 {
826 $option_ids[] = $opt->id;
827
828 if ($opt->id_variation > 0)
829 {
830 $optvar_ids[] = $opt->id_variation;
831 }
832 }
833 }
834
835 // pre-load services translations
836 $serLang = $translator->load('service', array_unique($service_ids), $langtag);
837 // pre-load services groups translations
838 $serGroupLang = $translator->load('group', array_unique($ser_group_ids), $langtag);
839 // pre-load employees translations
840 $empLang = $translator->load('employee', array_unique($employee_ids), $langtag);
841 // pre-load employees groups translations
842 $empGroupLang = $translator->load('empgroup', array_unique($emp_group_ids), $langtag);
843 // pre-load options translations
844 $optLang = $translator->load('option', array_unique($option_ids), $langtag);
845 // pre-load options variations translations
846 $varLang = $translator->load('optionvar', array_unique($optvar_ids), $langtag);
847
848 // iterate appointments and apply translationss
849 foreach ($this->appointments as $k => $app)
850 {
851 // translate service name for the given language
852 $ser_tx = $serLang->getTranslation($app->service->id, $langtag);
853
854 if ($ser_tx)
855 {
856 $app->service->name = $ser_tx->name;
857 $app->service->description = $ser_tx->description;
858 }
859
860 // translate employee name for the given language
861 $emp_tx = $empLang->getTranslation($app->employee->id, $langtag);
862
863 if ($emp_tx)
864 {
865 $app->employee->name = $emp_tx->nickname;
866 }
867
868 if ($app->service->group)
869 {
870 // translate service group for the given language
871 $grp_tx = $serGroupLang->getTranslation($app->service->group->id, $langtag);
872
873 if ($grp_tx)
874 {
875 $app->service->group->name = $grp_tx->name;
876 }
877 }
878
879 if ($app->employee->group)
880 {
881 // translate employee group for the given language
882 $grp_tx = $empGroupLang->getTranslation($app->employee->group->id, $langtag);
883
884 if ($grp_tx)
885 {
886 $app->employee->group->name = $grp_tx->name;
887 }
888 }
889
890 foreach ($app->options as $j => $opt)
891 {
892 // translate option name for the given language
893 $opt_tx = $optLang->getTranslation($opt->id, $langtag);
894
895 if ($opt_tx)
896 {
897 $opt->name = $opt_tx->name;
898 }
899
900 if ($opt->id_variation > 0)
901 {
902 // translate variation name for the given language
903 $var_tx = $varLang->getTranslation($opt->id_variation, $langtag);
904
905 if ($var_tx)
906 {
907 $opt->varName = $var_tx->name;
908 }
909 }
910
911 $opt->fullName = $opt->name . ($opt->varName ? ' - ' . $opt->varName : '');
912
913 // update option
914 $app->options[$j] = $opt;
915 }
916
917 // update appointment
918 $this->appointments[$k] = $app;
919 }
920
921 // translate payment if specified
922 if ($this->payment)
923 {
924 // get payment translation
925 $pay_tx = $translator->translate('payment', $this->payment->id, $langtag);
926
927 if ($pay_tx)
928 {
929 // inject translation within order details
930 $this->payment->name = $pay_tx->name;
931 $this->payment->notes->beforePurchase = $pay_tx->prenote;
932 $this->payment->notes->afterPurchase = $pay_tx->note;
933 }
934 }
935
936 // import custom fields loader
937 VAPLoader::import('libraries.customfields.loader');
938
939 // get relevant custom fields only
940 $fields = VAPCustomFieldsLoader::getInstance()
941 ->translate($langtag)
942 ->noRequiredCheckbox()
943 ->noSeparator()
944 ->forService($service_ids);
945
946 if (count(array_unique($employee_ids)) == 1)
947 {
948 $fields->ofEmployee($employee_ids[0]);
949 }
950
951 $cf = $fields->fetch();
952
953 // translate CF data object
954 $this->fields = VAPCustomFieldsLoader::translateObject($this->fields, $cf, $langtag);
955
956 // reset display fields
957 $this->displayFields = array();
958
959 foreach ($cf as $field)
960 {
961 $k = $field['name'];
962
963 if (!array_key_exists($k, $this->fields))
964 {
965 // field not found inside the given object, go to next one
966 continue;
967 }
968
969 $v = $this->fields[$k];
970
971 // take only if the value is not empty
972 if ((is_scalar($v) && strlen($v)) || !empty($v))
973 {
974 // get a more readable label/text of the saved value
975 $this->displayFields[$field['langname']] = $v;
976 }
977 }
978
979 // translate custom fields of the attendees
980 foreach ($this->attendees as $i => $attendee)
981 {
982 // translate CF data object
983 $attFields = VAPCustomFieldsLoader::translateObject($attendee['fields'], $cf, $langtag);
984
985 // reset display fields
986 $attendee['display'] = array();
987
988 foreach ($cf as $field)
989 {
990 $k = $field['name'];
991
992 if (!array_key_exists($k, $attFields) || !$field['repeat'])
993 {
994 // field not found inside the given object, go to next one
995 continue;
996 }
997
998 $v = $attFields[$k];
999
1000 // take only if the value is not empty
1001 if ((is_scalar($v) && strlen($v)) || !empty($v))
1002 {
1003 // get a more readable label/text of the saved value
1004 $attendee['display'][$field['langname']] = $v;
1005 }
1006 }
1007
1008 $this->attendees[$i] = $attendee;
1009 }
1010
1011 /**
1012 * External plugins can use this event to apply the translations to
1013 * additional details manually included within the order object.
1014 *
1015 * @param mixed $order The order details object.
1016 * @param string $langtag The requested language tag.
1017 *
1018 * @return void
1019 *
1020 * @since 1.7
1021 */
1022 $dispatcher->trigger('onTranslateAppointmentsOrderDetails', array($this, $langtag));
1023 }
1024
1025 /**
1026 * @override
1027 * Returns the billing details of the user that made the order.
1028 *
1029 * @return object
1030 */
1031 protected function getBilling()
1032 {
1033 $dbo = JFactory::getDbo();
1034
1035 $q = $dbo->getQuery(true)
1036 ->select('*')
1037 ->from($dbo->qn('#__vikappointments_users'))
1038 ->where($dbo->qn('id') . ' = ' . (int) $this->id_user)
1039 ->orWhere(array(
1040 $dbo->qn('billing_mail') . ' <> ' . $dbo->q(''),
1041 $dbo->qn('billing_mail') . ' IS NOT NULL',
1042 $dbo->qn('billing_mail') . ' = ' . $dbo->q($this->purchaser_mail),
1043 ), 'AND');
1044
1045 $dbo->setQuery($q, 0, 1);
1046 return $dbo->loadObject() ?? false;
1047 }
1048
1049 /**
1050 * @override
1051 * Returns the account details of the order author.
1052 *
1053 * @return object
1054 */
1055 protected function getAuthor()
1056 {
1057 if ($this->createdby <= 0)
1058 {
1059 // no registered author, do not go ahead
1060 return false;
1061 }
1062
1063 $dbo = JFactory::getDbo();
1064
1065 $q = $dbo->getQuery(true)
1066 ->select($dbo->qn('name'))
1067 ->select($dbo->qn('username'))
1068 ->select($dbo->qn('email'))
1069 ->from($dbo->qn('#__users'))
1070 ->where($dbo->qn('id') . ' = ' . (int) $this->createdby);
1071
1072 $dbo->setQuery($q, 0, 1);
1073 return $dbo->loadObject() ?? false;
1074 }
1075
1076 /**
1077 * @override
1078 * Returns the invoice details of the order.
1079 *
1080 * @return mixed The invoice object if exists, false otherwise.
1081 */
1082 protected function getInvoice()
1083 {
1084 $id_order = $this->id;
1085
1086 if ($this->id_parent != -1 && $this->id != $this->id_parent)
1087 {
1088 $id_order = $this->id_parent;
1089 }
1090
1091 // load invoice details
1092 return JModelVAP::getInstance('invoice')->getInvoice($id_order, 'appointments');
1093 }
1094
1095 /**
1096 * @override
1097 * Returns the history of the status codes set for the order.
1098 *
1099 * @return array
1100 */
1101 protected function getHistory()
1102 {
1103 return VAPOrderStatus::getInstance()->getOrderTrack($this->id, $locale = true);
1104 }
1105
1106 /**
1107 * @override
1108 * Returns a list of notes assigned to this order.
1109 *
1110 * @return array
1111 */
1112 protected function getNotes()
1113 {
1114 $dbo = JFactory::getDbo();
1115
1116 $q = $dbo->getQuery(true)
1117 ->select('n.*')
1118 ->select($dbo->qn('u.name', 'authorName'))
1119 ->from($dbo->qn('#__vikappointments_user_notes', 'n'))
1120 ->leftjoin($dbo->qn('#__users', 'u') . ' ON ' . $dbo->qn('u.id') . ' = ' . $dbo->qn('n.author'))
1121 ->where($dbo->qn('n.group') . ' = ' . $dbo->q('appointments'));
1122
1123 if (count($this->appointments) > 1)
1124 {
1125 $ids = array((int) $this->id);
1126
1127 foreach ($this->appointments as $app)
1128 {
1129 $ids[] = (int) $app->id;
1130 }
1131
1132 // load all notes assigned to the parent order and its children
1133 $q->where($dbo->qn('n.id_parent') . ' IN (' . implode(',', $ids) . ')');
1134 }
1135 else
1136 {
1137 // load all notes assigned to this appointment
1138 $q->where($dbo->qn('n.id_parent') . ' = ' . (int) $this->id);
1139 }
1140
1141 // sort by descending modify/create date
1142 $q->order(sprintf(
1143 'IFNULL(%s, %s) %s',
1144 $dbo->qn('n.modifiedon'),
1145 $dbo->qn('n.createdon'),
1146 'DESC'
1147 ));
1148
1149 $dbo->setQuery($q);
1150
1151 // load all assigned notes
1152 $notes = $dbo->loadObjectList();
1153
1154 if (!$notes)
1155 {
1156 // no assigned notes
1157 return [];
1158 }
1159
1160 // get tags model
1161 $tagModel = JModelVAP::getInstance('tag');
1162
1163 foreach ($notes as $note)
1164 {
1165 // JSON decode attachments
1166 $note->attachments = $note->attachments ? (array) json_decode($note->attachments, true) : array();
1167
1168 // get file details of the attachments
1169 $note->attachments = array_map(function($attachment)
1170 {
1171 $file = new stdClass;
1172 $file->name = basename($attachment);
1173 $file->path = VAPCUSTOMERS_DOCUMENTS . DIRECTORY_SEPARATOR . $attachment;
1174 $file->uri = VAPCUSTOMERS_DOCUMENTS_URI . str_replace(DIRECTORY_SEPARATOR, '/', $attachment);
1175
1176 return is_file($file->path) ? $file : null;
1177 }, $note->attachments);
1178
1179 // get rid of missing files
1180 $note->attachments = array_values(array_filter($note->attachments));
1181
1182 // get details of the specified tags
1183 $note->tags = $tagModel->readTags($note->tags, '*');
1184 }
1185
1186 return $notes;
1187 }
1188 }
1189