PluginProbe ʕ •ᴥ•ʔ
VikAppointments Services Booking Calendar / trunk
VikAppointments Services Booking Calendar vtrunk
trunk 1.2.17 1.2.18 1.2.19
vikappointments / site / models / servicesearch.php
vikappointments / site / models Last commit date
allorders.php 2 years ago calendarweek.php 1 month ago cart.php 1 month ago confirmapp.php 1 month ago empaccountstat.php 2 years ago empattachser.php 2 years ago empcoupons.php 2 years ago empcustfields.php 2 years ago empeditcoupon.php 3 years ago empeditcustfield.php 3 years ago empeditlocation.php 3 years ago empeditpay.php 3 years ago empeditprofile.php 5 months ago empeditservice.php 3 years ago empeditwdays.php 3 years ago emplocations.php 2 years ago emplocwdays.php 4 years ago emplogin.php 1 month ago employeesearch.php 1 month ago employeeslist.php 1 month ago empmanres.php 2 years ago emppaylist.php 2 years ago empserviceslist.php 2 years ago empsettingsman.php 4 years ago empsubscrcart.php 4 years ago empsubscrhistory.php 2 years ago empsubscrorder.php 5 months ago empwdays.php 2 years ago index.html 4 years ago packages.php 2 years ago packagescart.php 4 years ago packagesconfirm.php 5 months ago packorders.php 2 years ago servicesearch.php 2 years ago serviceslist.php 2 years ago subscrcart.php 2 years ago subscrhistory.php 2 years ago subscrpayment.php 5 months ago
servicesearch.php
731 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.mvc.model');
15
16 /**
17 * VikAppointments service search view model.
18 *
19 * @since 1.7
20 */
21 class VikAppointmentsModelServicesearch extends JModelVAP
22 {
23 /**
24 * Returns the service details.
25 *
26 * @param integer $id The service ID.
27 * @param array $options An array of options.
28 *
29 * @return object The service details.
30 */
31 public function getService($id, array $options = array())
32 {
33 $dispatcher = VAPFactory::getEventDispatcher();
34
35 // inject service ID within options array, which will be
36 // passed to the hooks dispatcher
37 $options['id_service'] = (int) $id;
38
39 $dbo = JFactory::getDbo();
40
41 // inner query to calculate the average rating of the service
42 $rating = $dbo->getQuery(true)
43 ->select('AVG(' . $dbo->qn('re.rating') . ')')
44 ->from($dbo->qn('#__vikappointments_reviews', 're'))
45 ->where(array(
46 $dbo->qn('s.id') . ' = ' . $dbo->qn('re.id_service'),
47 $dbo->qn('re.published') . ' = 1',
48 ));
49
50 $q = $dbo->getQuery(true);
51
52 $q->select('s.*');
53 $q->select('(' . $rating . ') AS ' . $dbo->qn('ratingAVG'));
54 $q->select($dbo->qn('sg.name', 'group_name'));
55 $q->select($dbo->qn('sg.description', 'group_description'));
56
57 $q->from($dbo->qn('#__vikappointments_service', 's'));
58 $q->leftjoin($dbo->qn('#__vikappointments_group', 'sg') . ' ON ' . $dbo->qn('s.id_group') . ' = ' . $dbo->qn('sg.id'));
59
60 $q->where($dbo->qn('s.id') . ' = ' . (int) $id);
61
62 // make sure the service is not yet expired
63 $q->andWhere(array(
64 $dbo->qn('s.end_publishing') . ' IS NULL',
65 $dbo->qn('s.end_publishing') . ' = ' . $dbo->q($dbo->getNullDate()),
66 $dbo->qn('s.end_publishing') . ' > ' . $dbo->q(JFactory::getDate()->toSql()),
67 ), 'OR');
68
69 /**
70 * Retrieve only the services that belong to the view
71 * access level of the current user.
72 *
73 * @since 1.6
74 */
75 $levels = JFactory::getUser()->getAuthorisedViewLevels();
76
77 if ($levels)
78 {
79 $q->where($dbo->qn('s.level') . ' IN (' . implode(', ', $levels) . ')');
80 }
81
82 /**
83 * Trigger hook to manipulate the query at runtime. Third party plugins
84 * can extend the query by applying further conditions or selecting
85 * additional data.
86 *
87 * @param mixed &$query Either a query builder or a query string.
88 * @param array &$options An array of options.
89 *
90 * @return void
91 *
92 * @since 1.7
93 */
94 $dispatcher->trigger('onBuildServiceSearchQuery', array(&$q, &$options));
95
96 $dbo->setQuery($q, 0, 1);
97 $service = $dbo->loadObject();
98
99 if (!$service)
100 {
101 // service not found
102 $this->setError(JText::translate('VAPSERNOTFOUNDERROR'));
103 return false;
104 }
105
106 // build service data
107 $service = $this->buildServiceData($service, $options);
108
109 // translate service details
110 $this->translate($service);
111
112 /**
113 * Trigger hook to manipulate the query response at runtime. Third party
114 * plugins can include further details into the service object.
115 *
116 * @param object $service An object holding the service details.
117 * @param JModel $model The current model.
118 *
119 * @return void
120 *
121 * @since 1.7
122 */
123 $dispatcher->trigger('onBuildServiceSearchData', array($service, $this));
124
125 return $service;
126 }
127
128 /**
129 * Loads all the options assigned to the specified service.
130 *
131 * @param integer $id The service ID.
132 *
133 * @return array An array of extra options.
134 */
135 public function getOptions($id)
136 {
137 $dbo = JFactory::getDbo();
138
139 $options = array();
140
141 $q = $dbo->getQuery(true)
142 ->select('o.*')
143 ->select(array(
144 $dbo->qn('v.id', 'id_var'),
145 $dbo->qn('v.name', 'var_name'),
146 $dbo->qn('v.inc_price'),
147 $dbo->qn('v.inc_duration'),
148 $dbo->qn('g.name', 'group_name'),
149 $dbo->qn('g.description', 'group_description'),
150 ))
151 ->from($dbo->qn('#__vikappointments_option', 'o'))
152 ->leftjoin($dbo->qn('#__vikappointments_ser_opt_assoc', 'a') . ' ON ' . $dbo->qn('o.id') . ' = ' . $dbo->qn('a.id_option'))
153 ->leftjoin($dbo->qn('#__vikappointments_option_value', 'v') . ' ON ' . $dbo->qn('o.id') . ' = ' . $dbo->qn('v.id_option'))
154 ->leftjoin($dbo->qn('#__vikappointments_option_group', 'g') . ' ON ' . $dbo->qn('g.id') . ' = ' . $dbo->qn('o.id_group'))
155 ->where(array(
156 $dbo->qn('a.id_service') . ' = ' . (int) $id,
157 $dbo->qn('o.published') . ' = 1',
158 ))
159 ->order(array(
160 $dbo->qn('g.ordering') . ' ASC',
161 $dbo->qn('o.ordering') . ' ASC',
162 $dbo->qn('v.ordering') . ' ASC',
163 ));
164
165 /**
166 * Retrieve only the options that belong to the view
167 * access level of the current user.
168 *
169 * @since 1.7.3
170 */
171 $levels = JFactory::getUser()->getAuthorisedViewLevels();
172
173 if ($levels)
174 {
175 $q->where($dbo->qn('o.level') . ' IN (' . implode(', ', $levels) . ')');
176 }
177
178 /**
179 * Trigger hook to manipulate the query at runtime. Third party plugins
180 * can extend the query by applying further conditions or selecting
181 * additional data.
182 *
183 * @param mixed &$query Either a query builder or a query string.
184 * @param integer $id The service ID.
185 *
186 * @return void
187 *
188 * @since 1.7.3
189 */
190 VAPFactory::getEventDispatcher()->trigger('onBuildServiceSearchOptionsQuery', array(&$q, $id));
191
192 $dbo->setQuery($q);
193 $rows = $dbo->loadObjectList();
194
195 if (!$rows)
196 {
197 // no assigned options
198 return array();
199 }
200
201 $options = array();
202
203 $grp_ids = array();
204 $opt_ids = array();
205 $var_ids = array();
206
207 foreach ($rows as $opt)
208 {
209 if (!isset($options[$opt->id_group]))
210 {
211 $group = new stdClass;
212 $group->id = (int) $opt->id_group;
213 $group->name = $opt->group_name;
214 $group->description = $opt->group_description;
215 $group->options = array();
216
217 $options[$opt->id_group] = $group;
218
219 if ($group->id)
220 {
221 $grp_ids[] = $group->id;
222 }
223 }
224
225 if (!isset($options[$opt->id_group]->options[$opt->id]))
226 {
227 // create option
228 $opt->variations = array();
229 $options[$opt->id_group]->options[$opt->id] = $opt;
230
231 $opt_ids[] = $opt->id;
232 }
233
234 if ($opt->id_var)
235 {
236 // create option variation
237 $var = new stdClass;
238 $var->id = $opt->id_var;
239 $var->name = $opt->var_name;
240 $var->price = $opt->inc_price;
241 $var->duration = $opt->inc_duration;
242
243 $options[$opt->id_group]->options[$opt->id]->variations[] = $var;
244
245 $var_ids[] = $var->id;
246 }
247 }
248
249 /**
250 * Ignore translation in case the multilingual feature is disabled.
251 *
252 * DO NOT move the options translations within the `translate` method provided by this
253 * model because other classes might want to retrieve the options. That's why the
254 * translation process has been merged within the method used to fetch them.
255 *
256 * @since 1.7.4
257 */
258 if (VAPFactory::getConfig()->getBool('ismultilang'))
259 {
260 $translator = VAPFactory::getTranslator();
261 $langtag = JFactory::getLanguage()->getTag();
262
263 // preload groups translations
264 $grpLang = $translator->load('optiongroup', array_unique($grp_ids), $langtag);
265 // preload options translations
266 $optLang = $translator->load('option', array_unique($opt_ids), $langtag);
267 // preload variations translations
268 $varLang = $translator->load('optionvar', array_unique($var_ids), $langtag);
269
270 foreach ($options as $group)
271 {
272 if ($group->id)
273 {
274 // get group translation
275 $grp_tx = $grpLang->getTranslation($group->id, $langtag);
276
277 if ($grp_tx)
278 {
279 // apply option translation
280 $group->name = $grp_tx->name;
281 $group->description = $grp_tx->description;
282 }
283 }
284
285 foreach ($group->options as $opt)
286 {
287 // get option translation
288 $opt_tx = $optLang->getTranslation($opt->id, $langtag);
289
290 if ($opt_tx)
291 {
292 // apply option translation
293 $opt->name = $opt_tx->name;
294 $opt->description = $opt_tx->description;
295 }
296
297 foreach ($opt->variations as $var)
298 {
299 // get variation translation
300 $var_tx = $varLang->getTranslation($var->id, $langtag);
301
302 if ($var_tx)
303 {
304 // apply option translation
305 $var->name = $var_tx->name;
306 }
307 }
308 }
309 }
310 }
311
312 $options = array_values($options);
313
314 if (count($options) && !$options[0]->id)
315 {
316 // always move options without group at the end of the list
317 $options[] = array_shift($options);
318 }
319
320 return $options;
321 }
322
323 /**
324 * Returns an object holding the details of the calendar.
325 *
326 * @param array &$options An array of options.
327 *
328 * @return object
329 */
330 public function getCalendar(&$options)
331 {
332 // fetch calendar availability through the employee search model,
333 // since the behavior is exactly the same
334 return JModelVAP::getInstance('employeesearch')->getCalendar($options);
335 }
336
337 /**
338 * Sends an e-mail to the specified service.
339 *
340 * @param integer $id_service The service to notify.
341 * @param string $name The customer name (sender name).
342 * @param string $email The customer e-mail (sender mail).
343 * @param string $content The e-mail content (plain text).
344 *
345 * @return boolean True on success, false otherwise.
346 */
347 public function askQuestion($id_service, $name, $email, $content)
348 {
349 // validate user data
350 if (!$name || !$email)
351 {
352 // register missing fields error
353 $this->setError(JText::translate('VAPCONFAPPREQUIREDERROR'));
354 return false;
355 }
356
357 // make sure we have a valid e-mail
358 if (!VikAppointments::validateUserEmail($email))
359 {
360 // invalid e-mail
361 $this->setError(JText::translate('VAPCONFAPPREQUIREDMAILERROR'));
362 return false;
363 }
364
365 // strip HTML tags from content
366 $content = strip_tags($content);
367
368 if (!$content)
369 {
370 // missing content
371 $this->setError(JText::translate('VAPQUICKCONTACTNOCONTENT'));
372 return false;
373 }
374
375 // use model to access the service details
376 $service = JModelVAP::getInstance('service')->getItem((int) $id_service);
377
378 if (!$service)
379 {
380 // service not found
381 $this->setError(JText::translate('VAPSERNOTFOUNDERROR'));
382 return false;
383 }
384
385 if (!$service->quick_contact)
386 {
387 // the service doesn't allow quick contact messages
388 $this->setError(JText::translate('VAPSERNOTREACHABLE'));
389 return false;
390 }
391
392 // fetch e-mail subject
393 $subject = JText::sprintf('VAPSERQUICKCONTACTSUBJECT', $service->name);
394 $is_html = false;
395
396 // define sender details
397 $sender = [
398 'name' => $name,
399 'email' => VikAppointments::getSenderMail(),
400 ];
401
402 // use all administrator e-mails as recipient.
403 $recipients = VikAppointments::getAdminMailList();
404
405 // prepare hook arguments
406 $args = array($id_service, &$subject, &$content, &$is_html, &$sender, &$recipients, $this);
407
408 try
409 {
410 /**
411 * Trigger event to allow the plugins to manipulate quick contact messages
412 * for the administrators. It is possible to throw an exception to avoid
413 * sending the message. The exception text will be used as error message.
414 *
415 * @param string $id_ser The service ID.
416 * @param string &$subject The e-mail subject.
417 * @param string &$content The e-mail content (the customer message).
418 * @param boolean &$is_html True if the e-mail should support HTML tags.
419 * @param string &$sender The e-mail sender details (@since 1.7).
420 * @param array &$recipients A list of recipient e-mails (@since 1.7).
421 * @param JModel $model The current model (@since 1.7).
422 *
423 * @return boolean False to prevent e-mail sending (@since 1.7).
424 *
425 * @throws Exception
426 *
427 * @since 1.6
428 */
429 if (VAPFactory::getEventDispatcher()->false('onBeforeQuickContactServiceSend', $args))
430 {
431 // someone prevented the e-mail sending
432 return false;
433 }
434 }
435 catch (Exception $e)
436 {
437 // catch the error and register the message set
438 $this->setError($e->getMessage());
439 return false;
440 }
441
442 // By default e-mails are always sent by using the customer e-mail/name
443 // as sender details. In case a server rejects certain e-mails, it is
444 // possible to use the hook previously described to change the sender
445 // details with other ones (such as the global sender e-mail).
446
447 $sent = false;
448
449 // iterate all recipients
450 foreach ($recipients as $recipient)
451 {
452 // try to send the e-mail
453 $sent = VAPApplication::getInstance()->sendMail(
454 $sender['email'], // sender e-mail
455 $sender['name'], // sender name
456 $recipient, // recipient e-mail
457 $email, // reply-to e-mail
458 $subject, // e-mail subject
459 $content, // e-mail content
460 null, // attachments
461 $is_html // is HTML flag
462 ) || $sent;
463 }
464
465 if (!$sent)
466 {
467 /**
468 * Register generic error to inform the user that the website
469 * is currently unable to send messages.
470 *
471 * @since 1.7
472 */
473 $this->setError(JText::translate('VAPMAILERR'));
474 return false;
475 }
476
477 return true;
478 }
479
480 ////////////////////////////////////////
481 //////////// HELPER METHODS ////////////
482 ////////////////////////////////////////
483
484 /**
485 * Applies additional queries to fill the service object
486 * with other data, such as the supported locations.
487 *
488 * @param object $service The service details object.
489 * @param array $options An array of options.
490 *
491 * @return object The resulting service object.
492 */
493 protected function buildServiceData($service, $options)
494 {
495 $dispatcher = VAPFactory::getEventDispatcher();
496
497 $dbo = JFactory::getDbo();
498
499 if ($service->id_group)
500 {
501 // register group data in a different object
502 $group = new stdClass;
503 $group->id = $service->id_group;
504 $group->name = $service->group_name;
505 $group->description = $service->group_description;
506
507 $service->group = $group;
508 }
509 else
510 {
511 // no assigned group
512 $service->group = null;
513 }
514
515 // round rating to the closest .0 or .5
516 $service->rating = VikAppointments::roundHalfClosest($service->ratingAVG);
517
518 // load service reviews
519 $service->reviews = VikAppointments::loadReviews('service', $service->id);
520
521 // get rid of duplicates
522 unset($service->id_group);
523 unset($service->group_name);
524 unset($service->group_description);
525
526 // load all employees supported by the service
527 $q = $dbo->getQuery(true)
528 ->select('e.*')
529 ->from($dbo->qn('#__vikappointments_ser_emp_assoc', 'a'))
530 ->leftjoin($dbo->qn('#__vikappointments_employee', 'e') . ' ON ' . $dbo->qn('e.id') . ' = ' . $dbo->qn('a.id_employee'))
531 ->where($dbo->qn('a.id_service') . ' = ' . (int) $service->id)
532 ->order($dbo->qn('a.ordering') . ' ASC');
533
534 if ($service->choose_emp)
535 {
536 // take only listable employees
537 $q->where($dbo->qn('e.listable') . ' = 1');
538 }
539
540 // take only those employees with lifetime license or that are not expired
541 $q->andWhere(array(
542 $dbo->qn('e.active_to') . ' = -1',
543 $dbo->qn('e.active_to_date') . ' >= ' . $dbo->q(JFactory::getDate()->toSql()),
544 ), 'OR');
545
546 /**
547 * Trigger hook to manipulate the query at runtime. Third party plugins
548 * can extend the query by applying further conditions or selecting
549 * additional data.
550 *
551 * @param mixed &$query Either a query builder or a query string.
552 * @param object $service The service details.
553 * @param array $options An array of options.
554 *
555 * @return void
556 *
557 * @since 1.7.4
558 */
559 $dispatcher->trigger('onBuildServiceSearchEmployeesQuery', array(&$q, $service, $options));
560
561 $dbo->setQuery($q);
562 $service->employees = $dbo->loadObjectList();
563
564 /**
565 * Trigger hook to manipulate the query response at runtime. Third party
566 * plugins can include further details into the service object.
567 *
568 * @param array &$employees A list of fetched employees.
569 * @param object $service An object holding the service details.
570 * @param array $options An array of options.
571 *
572 * @return void
573 *
574 * @since 1.7.4
575 */
576 $dispatcher->trigger('onBuildServiceSearchEmployeesData', array(&$service->employees, $service, $options));
577
578 /**
579 * Pick the first available employee only in case the random selection is disabled.
580 *
581 * @since 1.7
582 */
583 if ((empty($options['id_employee']) || $options['id_employee'] <= 0) && !$service->random_emp)
584 {
585 $options['id_employee'] = null;
586
587 if ($service->choose_emp && count($service->employees))
588 {
589 // use the first available service
590 $options['id_employee'] = $service->employees[0]->id;
591 }
592 }
593
594 if ($options['id_employee'])
595 {
596 // get service-employee overrides
597 $ov = JModelVAP::getInstance('serempassoc')->getOverrides($service->id, $options['id_employee']);
598
599 if ($ov)
600 {
601 // overwrite original service data with the overrides found
602 foreach ($ov as $k => $v)
603 {
604 $service->{$k} = $v;
605 }
606 }
607 }
608
609 $service->locations = array();
610
611 /**
612 * The locations should be retrieved even if the employee is not choosable.
613 * In this case, we should obtain the locations of all the employees
614 * assigned to the current service.
615 *
616 * @since 1.6
617 */
618 if ($options['id_employee'] > 0 || count($service->employees))
619 {
620 if ($options['id_employee'])
621 {
622 // use selected employee
623 $tmp = $options['id_employee'];
624 }
625 else
626 {
627 // use all employees
628 $tmp = array_map(function($e)
629 {
630 return (int) $e->id;
631 }, $service->employees);
632 }
633
634 // fetch employee locations and filter by service
635 $service->locations = JModelVAP::getInstance('employeesearch')->getLocations($tmp, $service->id);
636 }
637
638 // load options
639 $service->options = $this->getOptions($options['id_service']);
640
641 return $service;
642 }
643
644 /**
645 * Translates the service details.
646 *
647 * @param object &$service The row to translate.
648 *
649 * @return void
650 */
651 protected function translate(&$service)
652 {
653 /**
654 * Ignore translation in case the multilingual feature is disabled.
655 *
656 * @since 1.7.4
657 */
658 if (VAPFactory::getConfig()->getBool('ismultilang') == false)
659 {
660 return;
661 }
662
663 $langtag = JFactory::getLanguage()->getTag();
664
665 // get translator
666 $translator = VAPFactory::getTranslator();
667
668 // translate service for the given language
669 $ser_tx = $translator->translate('service', $service->id, $langtag);
670
671 if ($ser_tx)
672 {
673 $service->name = $ser_tx->name;
674 $service->description = $ser_tx->description;
675
676 /**
677 * The description might have overwritten the override
678 * for this specific employee/service combination. For
679 * this reason we need to re-apply it, if existing.
680 *
681 * @since 1.7
682 */
683 if (!empty($service->overrideDescription))
684 {
685 $service->description .= "\n" . $service->overrideDescription;
686 }
687 }
688
689 if ($service->group)
690 {
691 // translate group for the given language
692 $grp_tx = $translator->translate('group', $service->group->id, $langtag);
693
694 if ($grp_tx)
695 {
696 $service->group->name = $grp_tx->name;
697 $service->group->description = $grp_tx->description;
698 }
699 }
700
701 // translate employees only whether their selection is allowed
702 if ($service->choose_emp)
703 {
704 $employee_ids = array();
705
706 // map all existing employees
707 foreach ($service->employees as $employee)
708 {
709 $employee_ids[] = $employee->id;
710 }
711
712 // preload employees translations
713 $empLang = $translator->load('employee', array_unique($employee_ids), $langtag);
714
715 // apply translations found
716 foreach ($service->employees as $employee)
717 {
718 // get employee translation
719 $emp_tx = $empLang->getTranslation($employee->id, $langtag);
720
721 if ($emp_tx)
722 {
723 // use employee name and description translations
724 $employee->nickname = $emp_tx->nickname;
725 $employee->note = $emp_tx->note;
726 }
727 }
728 }
729 }
730 }
731