PluginProbe ʕ •ᴥ•ʔ
VikAppointments Services Booking Calendar / trunk
VikAppointments Services Booking Calendar vtrunk
trunk 1.2.17 1.2.18 1.2.19
vikappointments / admin / models / waitinglist.php
vikappointments / admin / models Last commit date
apiban.php 4 years ago apilog.php 2 years ago apiplugin.php 4 years ago apiuser.php 2 years ago apiuseroptions.php 2 years ago backup.php 5 months ago caldays.php 1 month ago calendar.php 1 month ago city.php 4 years ago closure.php 1 month ago configapp.php 4 years ago configcldays.php 4 years ago configcron.php 4 years ago configemp.php 4 years ago configsmsapi.php 4 years ago configuration.php 5 months ago conversion.php 4 years ago country.php 2 years ago coupon.php 2 years ago couponemployee.php 4 years ago coupongroup.php 2 years ago couponservice.php 4 years ago cronjob.php 2 years ago cronjoblog.php 4 years ago customer.php 5 months ago customf.php 2 years ago customfservice.php 4 years ago customizer.php 4 years ago empgroup.php 2 years ago employee.php 2 years ago empsettings.php 4 years ago file.php 4 years ago findreservation.php 1 month ago group.php 2 years ago import.php 4 years ago index.html 4 years ago invoice.php 1 month ago langcustomf.php 4 years ago langempgroup.php 4 years ago langemployee.php 4 years ago langgroup.php 4 years ago langmedia.php 4 years ago langoption.php 2 years ago langoptiongroup.php 4 years ago langoptionvar.php 4 years ago langpackage.php 4 years ago langpackgroup.php 4 years ago langpayment.php 4 years ago langservice.php 4 years ago langstatuscode.php 4 years ago langsubscr.php 4 years ago langtax.php 2 years ago langtaxrule.php 4 years ago location.php 2 years ago mailtext.php 2 years ago makerecurrence.php 1 month ago media.php 2 years ago multiorder.php 1 month ago option.php 1 year ago optiongroup.php 2 years ago optionvar.php 1 year ago orderstatus.php 2 years ago package.php 2 years ago packageservice.php 4 years ago packgroup.php 2 years ago packorder.php 2 years ago packorderitem.php 1 month ago payment.php 2 years ago rate.php 2 years ago reportsemp.php 5 months ago reportsser.php 5 months ago reservation.php 1 month ago resoptassoc.php 2 years ago restriction.php 2 years ago review.php 3 years ago serempassoc.php 1 year ago seroptassoc.php 4 years ago serrateassoc.php 4 years ago serrestrassoc.php 4 years ago service.php 2 years ago state.php 2 years ago statswidget.php 2 years ago statuscode.php 2 years ago subscription.php 1 month ago subscrorder.php 1 month ago tag.php 4 years ago tax.php 2 years ago taxrule.php 2 years ago updateprogram.php 4 years ago usernote.php 2 years ago waitinglist.php 1 month ago webhook.php 1 year ago worktime.php 1 month ago
waitinglist.php
659 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 waiting list model.
18 *
19 * @since 1.7
20 */
21 class VikAppointmentsModelWaitinglist extends JModelVAP
22 {
23 /**
24 * Save implementation performed from the front-end customer.
25 *
26 * @return mixed The ID on success, false otherwise.
27 */
28 public function subscribe($data)
29 {
30 $data = (array) $data;
31
32 $user = JFactory::getUser();
33
34 // use current user ID if not specified
35 if (empty($data['jid']))
36 {
37 $data['jid'] = $user->id;
38 }
39
40 // try to use the current user e-mail if not specified
41 if (empty($data['email']))
42 {
43 $data['email'] = $user->email;
44 }
45
46 // validate required fields
47 if (empty($data['email']) || empty($data['phone_number']) || empty($data['id_service']) || empty($data['timestamp']))
48 {
49 // missing required fields
50 $this->setError(JText::translate('VAPERRINSUFFCUSTF'));
51 return false;
52 }
53
54 // sanitize employee ID
55 $data['id_employee'] = isset($data['id_employee']) && $data['id_employee'] > 1 ? $data['id_employee'] : 0;
56
57 // validate service and employee
58 $assocModel = JModelVAP::getInstance('serempassoc');
59 $service = $assocModel->getOverrides($data['id_service'], $data['id_employee']);
60
61 if (!$service)
62 {
63 // one between the service and the relation with the employee does not exist
64 $this->setError(JText::translate('VAPERRINSUFFCUSTF'));
65 return false;
66 }
67
68 $dbo = JFactory::getDbo();
69
70 // make sure the user is not yet subscribed to this waiting list
71
72 $q = $dbo->getQuery(true)
73 ->select(1)
74 ->from($dbo->qn('#__vikappointments_waitinglist'))
75 ->where(array(
76 $dbo->qn('id_service') . ' = ' . (int) $data['id_service'],
77 $dbo->qn('id_employee') . ' = ' . (int) $data['id_employee'],
78 $dbo->qn('timestamp') . ' = ' . $dbo->q($data['timestamp']),
79 ));
80
81 if ($data['jid'] > 0)
82 {
83 // check by user ID
84 $q->where($dbo->qn('jid') . ' = ' . $data['jid']);
85 }
86 else
87 {
88 // check email or phone number (it doesn't need to have both them identical)
89 $q->andWhere(array(
90 $dbo->qn('email') . ' = ' . $dbo->q($data['email']),
91 $dbo->qn('phone_number') . ' = ' . $dbo->q($data['phone_number']),
92 ), 'OR');
93 }
94
95 $dbo->setQuery($q, 0, 1);
96 $dbo->execute();
97
98 if ($dbo->getNumRows())
99 {
100 // already in waiting list
101 $this->setError(JText::translate('VAPWAITLISTALREADYIN'));
102 return false;
103 }
104
105 // save in waiting list
106 return $this->save($data);
107 }
108
109 /**
110 * Delete implementation performed from the front-end customer.
111 *
112 * @return mixed The number of deleted records, false on failure.
113 */
114 public function unsubscribe($data)
115 {
116 $data = (array) $data;
117
118 $user = JFactory::getUser();
119
120 // use current user ID if not specified
121 if (empty($data['jid']))
122 {
123 $data['jid'] = $user->id;
124 }
125
126 // try to use the current user e-mail if not specified
127 if (empty($data['email']))
128 {
129 $data['email'] = $user->email;
130 }
131
132 // validate required fields
133 if (empty($data['email']) && empty($data['phone_number']) && empty($data['jid']))
134 {
135 // missing required fields
136 $this->setError(JText::translate('VAPERRINSUFFCUSTF'));
137 return false;
138 }
139
140 $dbo = JFactory::getDbo();
141
142 $q = $dbo->getQuery(true)
143 ->select($dbo->qn('id'))
144 ->from($dbo->qn('#__vikappointments_waitinglist'))
145 ->where(1);
146
147 if ((int) $data['jid'])
148 {
149 // get all records belonging to this user
150 $q->where($dbo->qn('jid') . ' = ' . (int) $data['jid']);
151 }
152
153 $where = array();
154
155 if (!empty($data['email']))
156 {
157 // filter by e-mail
158 $where[] = $dbo->qn('email') . ' = ' . $dbo->q($data['email']);
159 }
160
161 if (!empty($data['phone_number']))
162 {
163 // filter by phone number
164 $where[] = $dbo->qn('phone_number') . ' = ' . $dbo->q($data['phone_number']);
165 }
166
167 if ($where)
168 {
169 $q->orWhere($where);
170 }
171
172 $filters = array();
173
174 if (isset($data['timestamp']))
175 {
176 $date = JFactory::getDate($data['timestamp']);
177
178 $date->modify('00:00:00');
179 $start = $date->toSql();
180
181 $date->modify('23:59:59');
182 $end = $date->toSql();
183
184 // filter by check-in date
185 $filters[] = $dbo->qn('timestamp') . ' BETWEEN ' . $dbo->q($start) . ' AND ' . $dbo->q($end);
186 }
187
188 if (isset($data['id_service']))
189 {
190 // filter by service ID
191 $filters[] = $dbo->qn('id_service') . ' = ' . (int) $data['id_service'];
192 }
193
194 if ($filters)
195 {
196 // filter the waiting list by date/service
197 $q->andWhere($filters, 'AND');
198 }
199
200 $dbo->setQuery($q);
201
202 if ($columns = $dbo->loadColumn())
203 {
204 // delete all fetched records
205 $this->delete($columns);
206 }
207
208 return count($columns);
209 }
210
211 /**
212 * Processes the waiting list queue for the users registered for
213 * the service and check-in of the cancelled appointment.
214 *
215 * @param integer $id The appointment ID.
216 *
217 * @return boolean True on success, false otherwise.
218 */
219 public function notify($id)
220 {
221 if (!VAPFactory::getConfig()->getBool('enablewaitlist'))
222 {
223 // waiting list not enabled
224 return false;
225 }
226
227 try
228 {
229 // load order details
230 VAPLoader::import('libraries.order.factory');
231 $order = VAPOrderFactory::getAppointments($id);
232 }
233 catch (Exception $e)
234 {
235 // propagate error
236 $this->setError($e);
237
238 return false;
239 }
240
241 $services = array();
242
243 // fetch common services
244 foreach ($order->appointments as $appointment)
245 {
246 $sid = $appointment->service->id;
247 $eid = $appointment->employee->id;
248
249 // adjust check-in to the employee timezone
250 $date = JFactory::getDate($appointment->checkin->utc);
251 $date->setTimezone(new DateTimeZone($appointment->employee->checkin->timezone));
252
253 // format day and time
254 $day = $date->format('Y-m-d', $local = true);
255 $time = $date->format('H:i', $local = true);
256 // convert time in minutes
257 $time = JHtml::fetch('vikappointments.time2min', $time);
258
259 if (empty($services[$sid]))
260 {
261 // init service pool
262 $services[$sid] = array();
263 }
264
265 if (empty($services[$sid][$eid]))
266 {
267 // init employee pool
268 $services[$sid][$eid] = array();
269 }
270
271 if (empty($services[$sid][$eid][$day]))
272 {
273 // init date pool
274 $services[$sid][$eid][$day] = array();
275 }
276
277 // register time
278 $services[$sid][$eid][$day][] = $time;
279 }
280
281 /**
282 * Here we should have a tree structure built as follows:
283 *
284 * - ID service (20)
285 * - ID employee (2)
286 * - checkin day (Y-m-d)
287 * - time (630)
288 * - time (730)
289 * - checkin day (Y-m-d)
290 * - time (960)
291 */
292
293 $notified = false;
294
295 // get compatible customers in waiting list
296 foreach ($services as $id_service => $employees)
297 {
298 foreach ($employees as $id_employee => $dates)
299 {
300 foreach ($dates as $day => $times)
301 {
302 // get matching customers
303 $list = $this->getMatches($id_service, $id_employee, $day);
304
305 // send notifications
306 $notified = $this->sendNotifications($order, $list, $id_service, $id_employee, $day, $times) || $notified;
307 }
308 }
309 }
310
311 return $notified;
312 }
313
314 /**
315 * Finds a list of customers registered in the waiting list that
316 * match the specified search arguments.
317 *
318 * @param integer id_service The service ID.
319 * @param integer id_employee The employee ID.
320 * @param string date The date in military format.
321 *
322 * @return array A list of matching records.
323 */
324 protected function getMatches($id_service, $id_employee, $date)
325 {
326 $dbo = JFactory::getDbo();
327
328 $q = $dbo->getQuery(true);
329
330 $q->select('*');
331 $q->from($dbo->qn('#__vikappointments_waitinglist'));
332
333 // Check if the employee is set and matches the specified one.
334 // In this case, the slot has been emptied even if the order
335 // was referring to a different service.
336 $q->where(array(
337 $dbo->qn('id_employee') . ' = ' . (int) $id_employee,
338 $dbo->qn('id_employee') . ' > 0',
339 ));
340
341 // Otherwise make sure the ID of the service matches the specified one.
342 // In addition, the employee must be not set or equals to the specified one.
343 $q->orWhere(array(
344 $dbo->qn('id_service') . ' = ' . (int) $id_service,
345 '(' . $dbo->qn('id_employee') . ' = ' . (int) $id_employee . ' OR ' . $dbo->qn('id_employee') . ' <= 0)',
346 ), 'AND');
347
348 // extend the previous statement and make sure the subscription is for the specified day
349 $q->andWhere($dbo->qn('timestamp') . ' = ' . $dbo->q($date));
350
351 // older registrations come first
352 $q->order($dbo->qn('created_on') . ' ASC');
353
354 $dbo->setQuery($q);
355 return $dbo->loadObjectList();
356 }
357
358 /**
359 * Sends the notifications to the specified customers.
360 *
361 * @param object $order The order details.
362 * @param array $list The waiting list to notify.
363 * @param integer $id_service The service ID.
364 * @param integer $id_employee The employee ID.
365 * @param string $date The check-in date.
366 * @param array $times A list of check-in times.
367 *
368 * @return boolean True if notified, false otherwise.
369 */
370 protected function sendNotifications($order, $list, $id_service, $id_employee, $date, $times)
371 {
372 if (!$list)
373 {
374 // empty list, none to notify
375 return false;
376 }
377
378 $index = null;
379
380 // iterate the order details until we find the appointment
381 // that matches the given parameters
382 foreach ($order->appointments as $i => $appointment)
383 {
384 if ($appointment->service->id == $id_service && $appointment->employee->id == $id_employee)
385 {
386 // record found
387 $index = $i;
388 break;
389 }
390 }
391
392 if (is_null($index))
393 {
394 // no matches...
395 return false;
396 }
397
398 // send e-mail notification
399 $mail = $this->sendEmailNotification($order->appointments[$index], $list, $date, $times);
400 // send SMS notification
401 $sms = $this->sendSmsNotification($order->appointments[$index], $list, $date, $times);
402
403 return $mail || $sms;
404 }
405
406 /**
407 * Sends an e-mail notification to the specified customers.
408 *
409 * @param object $appointment The appointment details.
410 * @param array $list The waiting list to notify.
411 * @param string $date The check-in date.
412 * @param array $times A list of check-in times.
413 *
414 * @return boolean True if notified, false otherwise.
415 */
416 protected function sendEmailNotification($appointment, $list, $date, $times)
417 {
418 $notified = false;
419
420 VAPLoader::import('libraries.mail.factory');
421
422 // iterate subscribed customers one by one
423 foreach ($list as $customer)
424 {
425 // create mail instance
426 $mail = VAPMailFactory::getInstance('waitlist', $appointment, $customer, $date, $times);
427
428 // make sure we should send the e-mail
429 if ($mail->shouldSend())
430 {
431 // try to dispatch the notification
432 $notified = $mail->send() || $notified;
433 }
434 }
435
436 return $notified;
437 }
438
439 /**
440 * Sends a SMS notification to the specified customers.
441 *
442 * @param object $appointment The appointment details.
443 * @param array $list The waiting list to notify.
444 * @param string $date The check-in date.
445 * @param array $times A list of check-in times.
446 *
447 * @return boolean True if notified, false otherwise.
448 */
449 protected function sendSmsNotification($appointment, $list, $date, $times)
450 {
451 try
452 {
453 // get current SMS instance
454 $smsapi = VAPApplication::getInstance()->getSmsInstance();
455 }
456 catch (Exception $e)
457 {
458 // SMS API not configured
459 $this->setError(JText::translate('VAPSMSESTIMATEERR1'));
460
461 return false;
462 }
463
464 $notified = 0;
465 $errors = array();
466
467 // iterate subscribed customers one by one
468 foreach ($list as $customer)
469 {
470 // get phone number
471 $phone = $customer->phone_number;
472
473 if (!$phone)
474 {
475 // missing phone number, go ahead
476 continue;
477 }
478
479 try
480 {
481 /**
482 * Preload SMS template tags.
483 *
484 * @since 1.7.8
485 */
486 $tags = $this->getSmsTags($appointment, $customer, $date, $times);
487 }
488 catch (Exception $error)
489 {
490 // move to the next customer
491 continue;
492 }
493
494 // generate and parse SMS template
495 $tmpl = $this->getSmsTemplate($tags, $times);
496
497 if (!$tmpl)
498 {
499 // missing template, go ahead
500 continue;
501 }
502
503 // check if we have a dial code and the phone doesn't specify it
504 if ($customer->phone_prefix && !preg_match("/^\+/", $phone))
505 {
506 // prepend dial code
507 $phone = $customer->phone_prefix . $phone;
508 }
509
510 /**
511 * Inject tags within the API provider as well.
512 *
513 * @since 1.7.8
514 */
515 if (method_exists($smsapi, 'setOrder'))
516 {
517 $smsapi->setOrder($tags);
518 }
519
520 // send message
521 $response = $smsapi->sendMessage($phone, $tmpl);
522
523 // validate response
524 if ($smsapi->validateResponse($response))
525 {
526 // increase number of notified customers
527 $notified++;
528 }
529 else
530 {
531 // register log found
532 $errors[] = $smsapi->getLog();
533 }
534 }
535
536 if ($errors)
537 {
538 // notify administrator
539 VikAppointments::sendAdminMailSmsFailed($errors);
540 }
541
542 return (bool) $notified;
543 }
544
545 /**
546 * Parses SMS template used for the notifications of the waiting list.
547 * The placeholders contained in the template will be replaced with real values.
548 *
549 * The SMS template message to use depends on the language and the number
550 * of times that are available again (1 or more).
551 *
552 * @param array $tags A lookup of available tags.
553 * @param array $times All the times that have been emptied for the given day.
554 *
555 * @return string The SMS plain message to send.
556 */
557 protected function getSmsTemplate($tags, $times)
558 {
559 // get SMS map templates from configuration
560 $sms_map = VAPFactory::getConfig()->getArray('waitlistsmscont');
561
562 // NOTE: the system sends a notification into the current language.
563 // This means that, if we have an Italian customer that cancels an
564 // appointment from the front-end, any other user will be notified
565 // it Italian. We should consider to always use the default site
566 // language or to register the langtag of the subscribed customers.
567
568 $lang = JFactory::getLanguage()->getTag();
569 $sms = $sms_map[(count($times) == 1 ? 0 : 1)][$lang];
570
571 // parse placeholders
572 $sms = str_replace('{checkin_day}' , $tags['checkin_day'], $sms);
573 $sms = str_replace('{checkin_time}' , $tags['checkin_time'], $sms);
574 $sms = str_replace('{service}' , $tags['service'], $sms);
575 $sms = str_replace('{company}' , $tags['company'], $sms);
576 $sms = str_replace('{details_url}' , $tags['details_url'], $sms);
577
578 return $sms;
579 }
580
581 /**
582 * Prepares the tags available for the SMS notification template.
583 *
584 * @param object $appointment The appointment details object.
585 * @param object $record The waiting list record.
586 * @param string $date The check-in date (military format).
587 * @param array $times All the times that have been emptied for the given day.
588 *
589 * @return array The tags lookup.
590 *
591 * @throws Exception
592 *
593 * @since 1.7.8
594 */
595 protected function getSmsTags($appointment, $record, $date, $times)
596 {
597 $config = VAPFactory::getConfig();
598
599 // Format check-in date. Force UTC timezone because the date should be
600 // displayed as it is.
601 $formatted_date = JHtml::fetch('date', $date, JText::translate('DATE_FORMAT_LC4'), 'UTC');
602
603 $formatted_times = array();
604
605 foreach ($times as $time)
606 {
607 // convert minutes in time
608 $formatted_times[] = JHtml::fetch('vikappointments.min2time', $time, true);
609 }
610
611 if (count($times) > 1)
612 {
613 // use list of available times
614 $formatted_time = implode(', ', $formatted_times);
615 }
616 else
617 {
618 // use only the first available time
619 $formatted_time = $formatted_times[0];
620 }
621
622 if ($appointment->service->id == $record->id_service)
623 {
624 // the customer did a cancellation for the same service
625 $service = $appointment->service;
626 }
627 else
628 {
629 // the customer did the cancellation for a different service,
630 // we need to fetch the details of the service for which this
631 // user subscribed into the waiting list
632 $service = JModelVAP::getInstance('service')->getItem($record->id_service);
633
634 if (!$service)
635 {
636 throw new Exception('Service not found.', 404);
637 }
638 }
639
640 // create link to access the service details page
641 $dt = JHtml::fetch('date', $date, $config->get('dateformat'), 'UTC');
642 $url = "index.php?option=com_vikappointments&view=servicesearch&id_service={$service->id}&date={$dt}";
643
644 if ($appointment->viewEmp > 0)
645 {
646 // include employee ID too, if selectable
647 $url .= '&id_emp=' . $appointment->employee->id;
648 }
649
650 return [
651 'checkin_day' => $formatted_date,
652 'checkin_time' => $formatted_time,
653 'service' => $service->name,
654 'company' => $config->get('agencyname'),
655 'details_url' => VAPApplication::getInstance()->routeForExternalUse($url),
656 ];
657 }
658 }
659