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 / models / employeeslist.php
vikappointments / site / models Last commit date
allorders.php 1 day ago calendarweek.php 1 day ago cart.php 1 day ago confirmapp.php 1 day ago empaccountstat.php 1 day ago empattachser.php 1 day ago empcoupons.php 1 day ago empcustfields.php 1 day ago empeditcoupon.php 1 day ago empeditcustfield.php 1 day ago empeditlocation.php 1 day ago empeditpay.php 1 day ago empeditprofile.php 1 day ago empeditservice.php 1 day ago empeditwdays.php 1 day ago emplocations.php 1 day ago emplocwdays.php 1 day ago emplogin.php 1 day ago employeesearch.php 1 day ago employeeslist.php 1 day ago empmanres.php 1 day ago emppaylist.php 1 day ago empserviceslist.php 1 day ago empsettingsman.php 1 day ago empsubscrcart.php 1 day ago empsubscrhistory.php 1 day ago empsubscrorder.php 1 day ago empwdays.php 1 day ago index.html 1 day ago packages.php 1 day ago packagescart.php 1 day ago packagesconfirm.php 1 day ago packorders.php 1 day ago servicesearch.php 1 day ago serviceslist.php 1 day ago subscrcart.php 1 day ago subscrhistory.php 1 day ago subscrpayment.php 1 day ago
employeeslist.php
1080 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 employees list view model.
18 *
19 * @since 1.7
20 */
21 class VikAppointmentsModelEmployeeslist extends JModelVAP
22 {
23 /**
24 * The list view pagination object.
25 *
26 * @var JPagination
27 */
28 protected $pagination = null;
29
30 /**
31 * The total number of fetched rows.
32 *
33 * @var integer
34 */
35 protected $total = 0;
36
37 /**
38 * Retrieves the filters set in request and sanitizes them to prevent XSS.
39 *
40 * @return array The query filters.
41 *
42 * @since 1.7.4
43 */
44 public function getActiveFilters()
45 {
46 // obtain all filters
47 $filters = JFactory::getApplication()->input->get('filters', [], 'array');
48
49 $inputFilter = JFilterInput::getInstance();
50
51 foreach ($filters as $k => $v)
52 {
53 // make string safe
54 $filters[$k] = $inputFilter->clean($v, 'string');
55 }
56
57 return $filters;
58 }
59
60 /**
61 * Loads a list of employees to be displayed within the
62 * employees list site view.
63 *
64 * @param array &$filters An array of filters.
65 * @param array &$options An array of options, such as the ordering mode.
66 *
67 * @return array A list of employees.
68 */
69 public function getItems(array &$filters = array(), array &$options = array())
70 {
71 // always reset pagination and total count
72 $this->pagination = null;
73 $this->total = 0;
74
75 // validate filters and options
76 $this->validateRequest($filters, $options);
77
78 $dispatcher = VAPFactory::getEventDispatcher();
79
80 $dbo = JFactory::getDbo();
81
82 $employees = array();
83
84 $q = $dbo->getQuery(true);
85
86 if (empty($options['locations']))
87 {
88 // extended select
89 $q->select('SQL_CALC_FOUND_ROWS e.*');
90 $q->select($dbo->qn('eg.name', 'group_name'));
91 $q->select($dbo->qn('eg.description', 'group_description'));
92 $q->select('(' . $this->getRatingQuery($dbo) . ') AS ' . $dbo->qn('ratingAVG'));
93 $q->select('(' . $this->getReviewsQuery($dbo) . ') AS ' . $dbo->qn('reviewsCount'));
94
95 // group by employee
96 $q->group($dbo->qn('e.id'));
97 }
98 else
99 {
100 // minified select
101 $q->select($dbo->qn(array('e.id', 'e.nickname')));
102 $q->select(array(
103 $dbo->qn('l.id', 'id_location'),
104 $dbo->qn('l.latitude'),
105 $dbo->qn('l.longitude'),
106 $dbo->qn('l.name', 'locname'),
107 $dbo->qn('l.address', 'locaddress'),
108 $dbo->qn('l.zip', 'loczip'),
109 $dbo->qn('l.id_employee'),
110 ));
111
112 // group by location
113 $q->group($dbo->qn('l.id'));
114 }
115
116 $q->from($dbo->qn('#__vikappointments_employee', 'e'));
117 $q->leftjoin($dbo->qn('#__vikappointments_employee_group', 'eg') . ' ON ' . $dbo->qn('e.id_group') . ' = ' . $dbo->qn('eg.id'));
118
119 if (!empty($filters['employee_group']))
120 {
121 $q->where($dbo->qn('e.id_group') . ' = ' . (int) $filters['employee_group']);
122 }
123
124 // take only listable employees
125 $q->where($dbo->qn('e.listable') . ' = 1');
126 // take only those employees with lifetime license or that are not expired
127 $q->andWhere(array(
128 $dbo->qn('e.active_to') . ' = -1',
129 $dbo->qn('e.active_to_date') . ' >= ' . $dbo->q(JFactory::getDate()->toSql()),
130 ), 'OR');
131
132 // apply filters to query
133 $this->buildQueryFilters($q, $filters, $options, $dbo);
134
135 // apply the given ordering
136 switch ($options['ordering'])
137 {
138 case 1:
139 // alphabetically a..Z
140 $q->order(array(
141 $dbo->qn('e.lastname') . ' ASC',
142 $dbo->qn('e.firstname') . ' ASC',
143 ));
144 break;
145
146 case 2:
147 // alphabetically Z..a
148 $q->order(array(
149 $dbo->qn('e.lastname') . ' DESC',
150 $dbo->qn('e.firstname') . ' DESC',
151 ));
152 break;
153
154 case 3:
155 // newest
156 $q->order($dbo->qn('e.id') . ' DESC');
157 break;
158
159 case 4:
160 // oldest
161 $q->order($dbo->qn('e.id') . ' ASC');
162 break;
163
164 case 5:
165 // most popular
166 $q->leftjoin($dbo->qn('#__vikappointments_reservation', 'r') . ' ON ' . $dbo->qn('e.id') . ' = ' . $dbo->qn('r.id_employee'));
167 $q->order('COUNT(' . $dbo->qn('r.id') . ') DESC');
168 break;
169
170 case 6:
171 // highest rating
172 $q->order(array(
173 $dbo->qn('ratingAVG') . ' DESC',
174 $dbo->qn('reviewsCount') . ' DESC',
175 ));
176 break;
177
178 case 7:
179 // lowest price
180 $q->order($dbo->qn('a.rate') . ' ASC');
181 break;
182
183 case 8:
184 // highest price
185 $q->order($dbo->qn('a.rate') . ' DESC');
186 break;
187 }
188
189 /**
190 * Trigger hook to manipulate the query at runtime. Third party plugins
191 * can extend the query by applying further conditions or selecting
192 * additional data.
193 *
194 * @param mixed &$query Either a query builder or a query string.
195 * @param array $filters An array of filters.
196 * @param array &$options An array of options.
197 *
198 * @return void
199 *
200 * @since 1.7
201 */
202 $dispatcher->trigger('onBuildEmployeesListQuery', array(&$q, $filters, &$options));
203
204 $dbo->setQuery($q, $options['start'], $options['limit']);
205
206 if ($rows = $dbo->loadObjectList())
207 {
208 // fetch pagination
209 $this->getPagination($filters, $options);
210
211 $employees = $this->buildEmployeesData($rows, $filters, $options);
212 }
213
214 if (empty($options['locations']))
215 {
216 // translate employees
217 $this->translate($employees);
218 }
219
220 /**
221 * Trigger hook to manipulate the query response at runtime. Third party
222 * plugins can alter the resulting list of employees.
223 *
224 * @param array &$employees An array of fetched employees.
225 * @param JModel $model The current model.
226 *
227 * @return void
228 *
229 * @since 1.7
230 */
231 $dispatcher->trigger('onBuildEmployeesListData', array(&$employees, $this));
232
233 return $employees;
234 }
235
236 /**
237 * Returns the list pagination.
238 *
239 * @param array $filters An array of filters.
240 * @param array $options An array of options.
241 *
242 * @return JPagination
243 */
244 public function getPagination(array $filters = array(), array $options = array())
245 {
246 if (!$this->pagination)
247 {
248 jimport('joomla.html.pagination');
249 $dbo = JFactory::getDbo();
250 $dbo->setQuery('SELECT FOUND_ROWS();');
251 $this->total = (int) $dbo->loadResult();
252
253 $this->pagination = new JPagination($this->total, $options['start'], $options['limit']);
254
255 foreach ($filters as $k => $v)
256 {
257 /**
258 * Appends only filters that own a value as it doesn't
259 * make sense to populate the URL using empty variables.
260 *
261 * @since 1.6.2
262 */
263 if ($v !== null && $v !== '')
264 {
265 if (is_array($v))
266 {
267 /**
268 * Added support to multi-selection filters.
269 *
270 * @since 1.7.6
271 */
272 foreach ($v as $iv)
273 {
274 $this->pagination->setAdditionalUrlParam("filters[$k][]", $iv);
275 }
276 }
277 else
278 {
279 $this->pagination->setAdditionalUrlParam("filters[$k]", $v);
280 }
281 }
282 }
283 }
284
285 return $this->pagination;
286 }
287
288 /**
289 * Returns the total number of employees matching the search query.
290 *
291 * @return integer
292 */
293 public function getTotal()
294 {
295 return $this->total;
296 }
297
298 /**
299 * Loads the groups that contain at least an employee.
300 *
301 * @return array The groups.
302 */
303 public function getGroups()
304 {
305 $dbo = JFactory::getDbo();
306
307 $q = $dbo->getQuery(true)
308 ->select($dbo->qn(array('g.id', 'g.name')))
309 ->from($dbo->qn('#__vikappointments_employee_group', 'g'))
310 ->order($dbo->qn('g.ordering') . ' ASC');
311
312 // create a new inner query to count
313 // the employees assigned to the groups
314 $inner = $dbo->getQuery(true)
315 ->select('COUNT(1)')
316 ->from($dbo->qn('#__vikappointments_employee', 'e'))
317 ->where(array(
318 $dbo->qn('e.id_group') . ' = ' . $dbo->qn('g.id'),
319 ));
320
321 // add inner query to obtain the number of employees assigned to this group
322 $q->select('(' . $inner . ') AS ' . $dbo->qn('count'));
323 // get only the groups that own at least an employee
324 $q->having($dbo->qn('count') . ' > 0');
325
326 $dbo->setQuery($q);
327
328 // load groups
329 $groups = $dbo->loadObjectList();
330
331 if (!$groups)
332 {
333 return array();
334 }
335
336 /**
337 * Ignore translation in case the multilingual feature is disabled.
338 *
339 * DO NOT move the groups translations within the `translate` method provided by this
340 * model because other classes might want to retrieve the groups. That's why the
341 * translation process has been merged within the method used to fetch them.
342 *
343 * @since 1.7.4
344 */
345 if (VAPFactory::getConfig()->getBool('ismultilang'))
346 {
347 $langtag = JFactory::getLanguage()->getTag();
348
349 // get translator
350 $translator = VAPFactory::getTranslator();
351
352 // get all group IDs
353 $ids = array_map(function($g)
354 {
355 return $g->id;
356 }, $groups);
357
358 // pre-load employees groups translations
359 $groupLang = $translator->load('empgroup', array_unique($ids), $langtag);
360
361 foreach ($groups as $k => $g)
362 {
363 // translate group for the given language
364 $grp_tx = $groupLang->getTranslation($g->id, $langtag);
365
366 if ($grp_tx)
367 {
368 $groups[$k]->name = $grp_tx->name;
369 $groups[$k]->description = $grp_tx->description;
370 }
371 }
372 }
373
374 return $groups;
375 }
376
377 /**
378 * Calculates the resulting availability timeline according
379 * to the specified search options.
380 *
381 * @param array $options An array of options.
382 *
383 * @return mixed The resulting renderer.
384 */
385 public function getTimeline($options)
386 {
387 VAPLoader::import('libraries.availability.manager');
388 VAPLoader::import('libraries.availability.timeline.factory');
389
390 if (VAPDateHelper::isNull($options['date']))
391 {
392 // use current date if not specified
393 $options['date'] = JFactory::getDate()->format('Y-m-d');
394 }
395
396 if ($options['id_ser'] <= 0)
397 {
398 // get the first available service assigned to the specified employee
399 $service = $this->getFirstAvailableService($options['id_emp'], $options['date']);
400
401 if ($service)
402 {
403 // update the service ID with the one found
404 $options['id_ser'] = $service->id;
405
406 if (empty($options['people']) && $service->min_per_res > 1)
407 {
408 // update also the number of guests to be compliant with the
409 // configuration of the service
410 $options['people'] = $service->min_per_res;
411 }
412 }
413 }
414
415 if (empty($options['people']))
416 {
417 // use default number of participants
418 $options['people'] = 1;
419 }
420
421 // create availability search instance
422 $search = VAPAvailabilityManager::getInstance($options['id_ser'], $options['id_emp'], $options);
423
424 try
425 {
426 // create timeline parser instance
427 $parser = VAPAvailabilityTimelineFactory::getParser($search);
428 }
429 catch (Exception $e)
430 {
431 // register exception as error
432 $this->setError($e);
433
434 return false;
435 }
436
437 $table = array();
438
439 /**
440 * This value determines the number of columns
441 * to show within the table. The value should be
442 * in the range of [2-5].
443 *
444 * @var integer
445 */
446 $num_iter = 4;
447
448 /**
449 * Trigger hook to alter the number of columns to display within the timeline displayed by the employees list page.
450 *
451 * @param integer $columns The default number of columns.
452 * @param array $options An array of search options.
453 *
454 * @return integer The number of columns to display. It is suggested to use a value between 2 and 5.
455 *
456 * @since 1.7
457 */
458 $result = VAPFactory::getEventDispatcher()->numbers('onCountTimesTableColumns', array($num_iter, $options));
459
460 if ($result)
461 {
462 // override default amount (take the first returned value)
463 $num_iter = (int) abs($result[0]);
464 }
465
466 // create date instance
467 $date = JFactory::getDate($options['date']);
468
469 for ($i = 1; $i <= $num_iter; $i++)
470 {
471 // elaborate timeline
472 $timeline = $parser->getTimeline($date->format('Y-m-d'), $options['people']);
473
474 // register day timeline
475 $table[$date->format('Y-m-d')] = $timeline;
476
477 // go to next day
478 $date->modify('+1 day');
479 }
480
481 try
482 {
483 // create timeline renderer instance
484 $renderer = VAPAvailabilityTimelineFactory::getRenderer($table, 'table');
485 }
486 catch (Exception $e)
487 {
488 // register exception as error
489 $this->setError($e);
490
491 return false;
492 }
493
494 return $renderer;
495 }
496
497 /**
498 * Returns the first available service assigned to the
499 * given employee.
500 *
501 * @param integer $id_emp The employee ID.
502 * @param string $date The check-in date.
503 *
504 * @return mixed The service object on success, null otherwise.
505 */
506 public function getFirstAvailableService($id_emp, $date)
507 {
508 // get all published services assigned to this employee
509 $services = JModelVAP::getInstance('employee')->getServices($id_emp, $strict = true);
510
511 if (!$services)
512 {
513 // no available services
514 return null;
515 }
516
517 VAPLoader::import('libraries.availability.manager');
518 // create search instance
519 $search = VAPAvailabilityManager::getInstance(0, $id_emp);
520
521 foreach ($services as $service)
522 {
523 // update service ID
524 $search->set('id_service', $service->id);
525
526 // make sure the service is published
527 if ($search->isServicePublished($date))
528 {
529 // get service details
530 return JModelVAP::getInstance('serempassoc')->getOverrides($service->id, $id_emp);
531 }
532 }
533
534 return null;
535 }
536
537 ////////////////////////////////////////
538 //////////// HELPER METHODS ////////////
539 ////////////////////////////////////////
540
541 /**
542 * Validates the selected ordering against the supported listing modes
543 * and filters set in request.
544 *
545 * @param array $filters An array of filters.
546 * @param array &$options An array of options, such as the ordering mode.
547 *
548 * @return self This object to support chaining.
549 */
550 protected function validateRequest(array &$filters = array(), array &$options = array())
551 {
552 // load all supported types of ordering
553 $available_orderings = VikAppointments::getEmployeesAvailableOrderings();
554 $default_ordering = VikAppointments::getEmployeesListingMode();
555
556 // make sure there's a sorting more and it is supported
557 if (empty($options['ordering']) || !in_array($options['ordering'], $available_orderings))
558 {
559 // empty or not supported, use the default one
560 $options['ordering'] = $default_ordering;
561 }
562
563 // check if the service is set in the request
564 if (!isset($filters['service']))
565 {
566 // since the service is not specified, we cannot sort
567 // the employees depending on the given rate
568 if ($options['ordering'] == 7 || $options['ordering'] == 8)
569 {
570 // use the default one
571 $options['ordering'] = $default_ordering;
572 }
573
574 // if the listing mode is still set to "price",
575 // it means that the default mode cannot be used
576 if ($options['ordering'] == 7 || $options['ordering'] == 8)
577 {
578 // fallback to a..Z
579 $options['ordering'] = 1;
580 }
581 }
582
583 if (!array_key_exists('start', $options))
584 {
585 // start from the beginning
586 $options['start'] = 0;
587 }
588
589 if (!array_key_exists('limit', $options))
590 {
591 // use the default configuration limit
592 $options['limit'] = VAPFactory::getConfig()->getUint('emplistlim');
593 }
594
595 return $this;
596 }
597
598 /**
599 * Returns the inner query that should be used to calculate the
600 * average rating of the employees.
601 *
602 * @param mixed $dbo The database object.
603 *
604 * @return mixed The database query.
605 */
606 protected function getRatingQuery($dbo)
607 {
608 return $dbo->getQuery(true)
609 ->select('AVG(' . $dbo->qn('re.rating') . ')')
610 ->from($dbo->qn('#__vikappointments_reviews', 're'))
611 ->where(array(
612 $dbo->qn('e.id') . ' = ' . $dbo->qn('re.id_employee'),
613 $dbo->qn('re.published') . ' = 1',
614 ));
615 }
616
617 /**
618 * Returns the inner query that should be used to calculate the
619 * number of reviews of the employees.
620 *
621 * @param mixed $dbo The database object.
622 *
623 * @return mixed The database query.
624 */
625 protected function getReviewsQuery($dbo)
626 {
627 return $dbo->getQuery(true)
628 ->select('COUNT(' . $dbo->qn('re.rating') . ')')
629 ->from($dbo->qn('#__vikappointments_reviews', 're'))
630 ->where(array(
631 $dbo->qn('e.id') . ' = ' . $dbo->qn('re.id_employee'),
632 $dbo->qn('re.published') . ' = 1',
633 ));
634 }
635
636 /**
637 * Extends the search query by applying the filters set.
638 *
639 * @param mixed &$q The query builder object.
640 * @param array &$filters The associative array of filters.
641 * @param array $options The associative array of options.
642 * @param mixed $dbo The database object.
643 *
644 * @return self This object to support chaining.
645 */
646 protected function buildQueryFilters(&$q, array &$filters, $options, $dbo)
647 {
648 $locations_table_loaded = false;
649
650 if (!empty($options['locations']))
651 {
652 // extend query by loading the employee working times
653 $q->leftjoin($dbo->qn('#__vikappointments_emp_worktime', 'w') . ' ON ' . $dbo->qn('w.id_employee') . ' = ' . $dbo->qn('e.id'));
654 // extend query by loading the employee locations
655 $q->leftjoin($dbo->qn('#__vikappointments_employee_location', 'l') . ' ON ' . $dbo->qn('w.id_location') . ' = ' . $dbo->qn('l.id'));
656
657 // take only locations with specified coordinates
658 $q->where($dbo->qn('l.latitude') . ' IS NOT NULL');
659
660 $locations_table_loaded = true;
661 }
662
663 if (!empty($filters['group']) || !empty($filters['service']) || !empty($filters['price']))
664 {
665 // extend query by fetching the assigned services
666 $q->leftjoin($dbo->qn('#__vikappointments_ser_emp_assoc', 'a') . ' ON ' . $dbo->qn('a.id_employee') . ' = ' . $dbo->qn('e.id'));
667 $q->leftjoin($dbo->qn('#__vikappointments_service', 's') . ' ON ' . $dbo->qn('a.id_service') . ' = ' . $dbo->qn('s.id'));
668
669 if (!empty($filters['group']))
670 {
671 // filter by service group
672 $q->where($dbo->qn('s.id_group') . ' = ' . (int) $filters['group']);
673 }
674
675 if (!empty($filters['service']))
676 {
677 // filter by service and select the rate
678 $q->select($dbo->qn('a.rate'));
679 $q->where($dbo->qn('s.id') . ' = ' . (int) $filters['service']);
680
681 if (!empty($options['locations']))
682 {
683 /**
684 * Filter the working days by service to ignore the locations assigned
685 * to the working days that don't match the specified service.
686 *
687 * @since 1.7
688 */
689 $q->where($dbo->qn('w.id_service') . ' = ' . (int) $filters['service']);
690 }
691
692 /**
693 * Retrieve only the services that belong to the view
694 * access level of the current user.
695 *
696 * @since 1.6
697 */
698 $levels = JFactory::getUser()->getAuthorisedViewLevels();
699
700 if ($levels)
701 {
702 $q->where($dbo->qn('s.level') . ' IN (' . implode(', ', $levels) . ')');
703 }
704 }
705
706 if (!empty($filters['price']))
707 {
708 // price set in request, extract the given range
709 $range = explode(':', $filters['price']);
710
711 if (count($range) != 2)
712 {
713 $range = array(0, 0);
714 }
715 else
716 {
717 $range = array_map('intval', $range);
718 }
719
720 // filter by price range
721 $q->where($dbo->qn('a.rate') . ' BETWEEN ' . implode(' AND ', $range));
722 }
723 }
724
725 if (!empty($filters['country']) || !empty($filters['state']) || !empty($filters['city'])
726 || !empty($filters['zip']) || !empty($filters['nearby']) || !empty($filters['id_location']))
727 {
728 if (!$locations_table_loaded)
729 {
730 // extend query by loading the employee locations
731 $q->leftjoin($dbo->qn('#__vikappointments_emp_worktime', 'w') . ' ON ' . $dbo->qn('w.id_employee') . ' = ' . $dbo->qn('e.id'));
732 $q->leftjoin($dbo->qn('#__vikappointments_employee_location', 'l') . ' ON ' . $dbo->qn('w.id_location') . ' = ' . $dbo->qn('l.id'));
733 }
734
735 if (empty($filters['nearby']))
736 {
737 if (!empty($filters['country']))
738 {
739 // filter by country ID
740 $q->where($dbo->qn('l.id_country') . ' = ' . (int) $filters['country']);
741 }
742
743 if (!empty($filters['state']))
744 {
745 // filter by state/province ID
746 $q->where($dbo->qn('l.id_state') . ' = ' . (int) $filters['state']);
747 }
748
749 if (!empty($filters['city']))
750 {
751 // filter by city ID
752 $q->where($dbo->qn('l.id_city') . ' = ' . (int) $filters['city']);
753 }
754
755 if (!empty($filters['zip']))
756 {
757 // filter by ZIP code
758 $q->where($dbo->qn('l.zip') . ' = ' . $dbo->q($filters['zip']));
759 }
760 }
761 // make sure the browser has been authorised to obtain the coordinates
762 else if (!empty($filters['base_coord']))
763 {
764 // get query for geodetica
765 $distance = (int) $filters['distance'];
766 $coord = explode(',', $filters['base_coord']);
767
768 if (count($coord) < 2)
769 {
770 // Invalid coordinates... Use dummy data to
771 // avoid breaking the query.
772 $coord = array(0, 0);
773 }
774
775 list($lat, $lng) = array_map('floatval', $coord);
776
777 /**
778 * Convert distance to km for query as the Earth radius
779 * is specified in kilometers.
780 *
781 * @since 1.6
782 */
783 $distance = VikAppointments::convertDistanceToKilometers($distance, $filters);
784
785 // apply nearby query
786 $q->where($this->getNearbyWhereQuery($lat, $lng, $distance));
787
788 // copy coordinates within filters array
789 $filters['latitude'] = $lat;
790 $filters['longitude'] = $lng;
791 }
792
793 /**
794 * Filter the employees also by location ID.
795 *
796 * @since 1.6
797 */
798 if (!empty($filters['id_location']))
799 {
800 $q->where($dbo->qn('w.id_location') . ' = ' . (int) $filters['id_location']);
801 }
802 }
803
804 /**
805 * When specified, take only the given employee.
806 *
807 * @since 1.7
808 */
809 if (!empty($filters['id_employee']))
810 {
811 $q->where($dbo->qn('e.id') . ' = ' . (int) $filters['id_employee']);
812 }
813
814 /**
815 * Extend query by using the employees custom fields.
816 *
817 * @since 1.6
818 */
819 return $this->extendWithCustomFilters($q, $filters, $dbo);
820 }
821
822 /**
823 * Returns the WHERE statement used to filter the employees
824 * in the nearby of the specified coordinates.
825 *
826 * @param float $lat The center latitude.
827 * @param float $lng The center longitude.
828 * @param integer $distance The circle radius (in km).
829 *
830 * @return string The query WHERE statement.
831 */
832 protected function getNearbyWhereQuery($lat, $lng, $distance)
833 {
834 $lat = $lat * pi() / 180.0;
835 $lng = $lng * pi() / 180.0;
836
837 /**
838 * Distance between 2 coordinates.
839 *
840 * R = 6371 (Earth radius ~6371 km)
841 *
842 * Coordinates in radians
843 * lat1, lng1, lat2, lng2
844 *
845 * Calculate the included angle fi
846 * fi = abs( lng1 - lng2 );
847 *
848 * Calculate the third side of the spherical triangle
849 * p = acos(
850 * sin(lat2) * sin(lat1) +
851 * cos(lat2) * cos(lat1) *
852 * cos( fi )
853 * )
854 *
855 * Multiply the third side per the Earth radius (distance in km)
856 * D = p * R;
857 *
858 * MINIFIED EXPRESSION
859 *
860 * acos(
861 * sin(lat2) * sin(lat1) +
862 * cos(lat2) * cos(lat1) *
863 * cos( abs(lng1 - lng2) )
864 * ) * R
865 */
866
867 return "`l`.`latitude` IS NOT NULL AND (ACOS(
868 SIN(RADIANS(`l`.`latitude`)) * SIN($lat) +
869 COS(RADIANS(`l`.`latitude`)) * COS($lat) *
870 COS(ABS($lng - RADIANS(`l`.`longitude`)))
871 ) * 6371) < $distance";
872 }
873
874 /**
875 * Extends the search query using the custom filters.
876 *
877 * @param mixed &$q The query builder object.
878 * @param array $filters The associative array of filters.
879 * @param mixed $dbo The database object.
880 *
881 * @return self This object to support chaining.
882 */
883 protected function extendWithCustomFilters(&$q, array $filters, $dbo)
884 {
885 $lookup = array();
886
887 foreach ($filters as $k => $v)
888 {
889 if ($v !== null && $v !== '' && strpos($k, 'field_') === 0)
890 {
891 $lookup[] = substr($k, 6);
892 }
893 }
894
895 if (!$lookup)
896 {
897 // no custom filters
898 return $this;
899 }
900
901 $lookup = array_map(array($dbo, 'q'), $lookup);
902
903 $q2 = $dbo->getQuery(true)
904 ->select($dbo->qn('formname'))
905 ->from($dbo->qn('#__vikappointments_custfields'))
906 ->where(array(
907 $dbo->qn('group') . ' = 1',
908 $dbo->qn('formname') . ' IN (' . implode(',', $lookup) . ')',
909 ));
910
911 $dbo->setQuery($q2);
912 $fields = $dbo->loadColumn();
913
914 if (!$fields)
915 {
916 // no custom fields, possible hack attempt
917 return $this;
918 }
919
920 foreach ($fields as $field)
921 {
922 $key = 'field_' . $field;
923
924 if (is_array($filters[$key]))
925 {
926 /**
927 * Search in JSON array to support multi-selection fields.
928 *
929 * @since 1.7.6
930 */
931 foreach ($filters[$key] as $v)
932 {
933 $q->where($dbo->qn('e.' . $key) . ' REGEXP ' . $dbo->q('(\[|,)"?' . $v . '"?(,|\])'));
934 }
935 }
936 else
937 {
938 $q->where($dbo->qn('e.' . $key) . ' = ' . $dbo->q($filters[$key]));
939 }
940 }
941
942 return $this;
943 }
944
945 /**
946 * Applies additional queries to fill the employees list
947 * with other data, such as the supported locations.
948 *
949 * @param array $list The employees list.
950 * @param array $filters An array of filters.
951 * @param array $options An array of options.
952 *
953 * @return array The resulting employees list.
954 */
955 protected function buildEmployeesData($list, $filters, $options)
956 {
957 if (!empty($options['locations']))
958 {
959 // do not need to manipulate the fetched rows
960 return $list;
961 }
962
963 $employees = array();
964
965 // get employee search model
966 $empSearchModel = JModelVAP::getInstance('employeesearch');
967
968 // check if we have a service within the filters
969 if (!empty($filters['service']))
970 {
971 $id_service = (int) $filters['service'];
972 }
973 else
974 {
975 $id_service = 0;
976 }
977
978 foreach ($list as $e)
979 {
980 // round rating to the closest .0 or .5
981 $e->rating = VikAppointments::roundHalfClosest($e->ratingAVG);
982
983 // fetch employee locations (and filter by service if needed)
984 $e->locations = $empSearchModel->getLocations($e->id, $id_service);
985
986 if ($e->id_group)
987 {
988 // register group data in a different object
989 $group = new stdClass;
990 $group->id = $e->id_group;
991 $group->name = $e->group_name;
992 $group->description = $e->group_description;
993
994 $e->group = $group;
995 }
996 else
997 {
998 // no assigned group
999 $e->group = null;
1000 }
1001
1002 // get rid of duplicates
1003 unset($e->id_group);
1004 unset($e->group_name);
1005 unset($e->group_description);
1006
1007 $employees[] = $e;
1008 }
1009
1010 return $employees;
1011 }
1012
1013 /**
1014 * Translates the groups and the employees.
1015 *
1016 * @param array &$rows The rows to translate.
1017 *
1018 * @return void
1019 */
1020 protected function translate(&$rows)
1021 {
1022 /**
1023 * Ignore translation in case the multilingual feature is disabled.
1024 *
1025 * @since 1.7.4
1026 */
1027 if (VAPFactory::getConfig()->getBool('ismultilang') == false)
1028 {
1029 return;
1030 }
1031
1032 $langtag = JFactory::getLanguage()->getTag();
1033
1034 // get translator
1035 $translator = VAPFactory::getTranslator();
1036
1037 $employee_ids = array();
1038 $group_ids = array();
1039
1040 foreach ($rows as $employee)
1041 {
1042 $employee_ids[] = $employee->id;
1043
1044 if ($employee->group)
1045 {
1046 $group_ids[] = $employee->group->id;
1047 }
1048 }
1049
1050 // pre-load employees translations
1051 $empLang = $translator->load('employee', array_unique($employee_ids), $langtag);
1052 // pre-load employees groups translations
1053 $groupLang = $translator->load('empgroup', array_unique($group_ids), $langtag);
1054
1055 foreach ($rows as $k => $employee)
1056 {
1057 // translate employee for the given language
1058 $emp_tx = $empLang->getTranslation($employee->id, $langtag);
1059
1060 if ($emp_tx)
1061 {
1062 $rows[$k]->nickname = $emp_tx->nickname;
1063 $rows[$k]->note = $emp_tx->note;
1064 }
1065
1066 if ($employee->group)
1067 {
1068 // translate group for the given language
1069 $grp_tx = $groupLang->getTranslation($employee->group->id, $langtag);
1070
1071 if ($grp_tx)
1072 {
1073 $rows[$k]->group->name = $grp_tx->name;
1074 $rows[$k]->group->description = $grp_tx->description;
1075 }
1076 }
1077 }
1078 }
1079 }
1080