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 / availability / timeline / parser / group.php
vikappointments / site / helpers / libraries / availability / timeline / parser Last commit date
allemployees.php 4 days ago employee.php 4 days ago group.php 4 days ago groupext.php 4 days ago index.html 4 days ago service.php 4 days ago
group.php
411 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.availability.timeline.parser');
15
16 /**
17 * Group timeline parser.
18 *
19 * @since 1.7
20 */
21 class VAPAvailabilityTimelineParserGroup extends VAPAvailabilityTimelineParser
22 {
23 /**
24 * Internal method used by children classes to implement their own logic
25 * to fetch the availability timeline.
26 *
27 * @param string $date The UTC date in military format.
28 * @param integer $people The number of participants.
29 * @param integer $id The selected appointment ID.
30 *
31 * @return mixed The resulting timeline on success, false otherwise.
32 */
33 protected function buildTimeline($date, $people = 1, $id = 0)
34 {
35 // look for a closing day/period
36 if ($this->search->isClosingDay($date))
37 {
38 // the day is closed
39 $this->setError(JText::translate('VAPFINDRESCLOSINGDAY'));
40
41 return false;
42 }
43
44 // check whether the service is published for the given date
45 if (!$this->search->isServicePublished($date))
46 {
47 // service not published
48 $this->setError(JText::translate('VAPFINDRESNODAYEMPLOYEE'));
49
50 return false;
51 }
52
53 // get employee working times
54 $worktimes = $this->search->getWorkingTimes($date);
55
56 if (!$worktimes)
57 {
58 // the employee doesn't work on the specified date
59 $this->setError(JText::translate('VAPFINDRESNODAYEMPLOYEE'));
60
61 return false;
62 }
63
64 // extract service and employee IDs from search object
65 $id_service = (int) $this->search->get('id_service');
66 $id_employee = (int) $this->search->get('id_employee');
67
68 // get service-employee association model
69 $model = JModelVAP::getInstance('serempassoc');
70 // get service details
71 $service = $model->getOverrides($id_service, $id_employee);
72
73 // validate selected number of people (front-end only)
74 if (!$this->search->isAdmin() && $people > $service->max_capacity)
75 {
76 // number of people not supported
77 $this->setError(JText::translate('VAPFINDRESPEOPLENOTVALID'));
78
79 return false;
80 }
81
82 /**
83 * We are looking for the appointments for the current day.
84 * Extend this bounds in order to support midnight reservations.
85 *
86 * Instead having:
87 * 2021-07-09 @ 00:00:00 - 2021-07-09 23:59:59
88 * we need to have:
89 * 2021-07-08 @ 00:00:00 - 2021-07-10 23:59:59
90 *
91 * @since 1.6
92 */
93 $start = JFactory::getDate($date);
94 $start->modify('-1 day 00:00:00');
95
96 $end = JFactory::getDate($date);
97 $end->modify('+1 day 23:59:59');
98
99 // load existing bookings
100 $bookings = $this->search->getReservations($start->toSql(), $end->toSql(), $id);
101
102 // register seats trace
103 $seats = array();
104
105 // use helper method to elaborate the timeline
106 $arr = $this->elaborateTimeLine($date, $people, $worktimes, $bookings, $service, $seats);
107
108 // create timeline
109 $timeline = new VAPAvailabilityTimeline($date, $this->search);
110
111 // create new level
112 $timeline->addLevel();
113
114 foreach ($arr as $k => $v)
115 {
116 // create check-in date time
117 $checkin = JFactory::getDate($date);
118 $checkin->modify(JHtml::fetch('vikappointments.min2time', $k, $string = true, $format = 'H:i:s'));
119
120 // check whether the time is in the past (preserve them in case of admin)
121 if ($arr[$k] && $this->search->isPastTime($checkin))
122 {
123 // unset block if it is in the past
124 unset($arr[$k]);
125 }
126 else if ($k >= 1440)
127 {
128 /**
129 * Unset the time slots that exceed the midnight.
130 *
131 * @since 1.6
132 */
133 if (!$service->checkout_selection)
134 {
135 /**
136 * Time-slots that exceed the midnight are mandatory for
137 * the checkout selection. Unset them only whether the
138 * "checkout_selection" parameter is turned off.
139 *
140 * @since 1.6.2
141 */
142 unset($arr[$k]);
143 }
144 }
145
146 if (isset($arr[$k]))
147 {
148 // add time
149 $timeBlock = $timeline->addTime($k, $arr[$k], $service->duration);
150
151 $trace = array();
152
153 // calculate rate for the current time block
154 $price = VAPSpecialRates::getRate($id_service, $id_employee, $timeBlock->checkin(), $people, $trace);
155
156 // multiply by the number of guests (if enabled)
157 if ($service->priceperpeople)
158 {
159 $price *= $people;
160 }
161
162 // set time block price
163 $timeBlock->setPrice($price);
164
165 if ($trace && !empty($trace['rates']))
166 {
167 // register rate trace
168 $timeBlock->setRatesTrace($trace['rates']);
169 }
170
171 // set occupancy
172 $timeBlock->setOccupancy(isset($seats[$k]) ? $service->max_capacity - $seats[$k] : 0, $service->max_capacity);
173 }
174 }
175
176 // make sure all the times are not in the past
177 if (!$timeline->hasTimes())
178 {
179 // all times are in the past
180 $this->setError(JText::translate('VAPFINDRESNOLONGERAVAILABLE'));
181
182 return false;
183 }
184
185 return $timeline;
186 }
187
188 /**
189 * Helper method used to elaborate the resulting timeline.
190 *
191 * @param string $date The UTC date in military format.
192 * @param integer $people The number of participants.
193 * @param array $worktime A list of working times.
194 * @param array $bookings A list of booked appointments.
195 * @param object $service The service details.
196 * @param mixed &$seats An array containing the remaining seats for each time.
197 *
198 * @return array The resulting timeline.
199 */
200 protected function elaborateTimeLine($date, $people, $worktime, $bookings, $service, &$seats = null)
201 {
202 $arr = array();
203
204 if ($service->interval == 1)
205 {
206 // same as service duration
207 $min_int = $service->duration + $service->sleep;
208 }
209 else
210 {
211 // default one specified by the configuration
212 $min_int = VAPFactory::getConfig()->getUint('minuteintervals');
213 }
214
215 for ($i = 0; $i < count($worktime); $i++)
216 {
217 $arr[$i] = [];
218
219 for ($j = $worktime[$i]->fromts; ($j + $min_int) <= $worktime[$i]->endts; $j += $min_int)
220 {
221 $arr[$i][$j] = 1;
222 }
223 }
224
225 // fetch default system timezone
226 $tz = JFactory::getApplication()->get('offset', 'UTC');
227
228 // re-format check-in date for correct comparison
229 $ymd = JFactory::getDate($date)->format('Y-m-d');
230
231 $cont_people = 0;
232
233 foreach ($bookings as $k => $b)
234 {
235 if (!$b->timezone)
236 {
237 // use default system timezone
238 $b->timezone = $tz;
239 }
240
241 // increase people count
242 $cont_people += $b->people_count;
243
244 if ($k == count($bookings) - 1 || $bookings[$k + 1]->checkin_ts != $b->checkin_ts || $bookings[$k + 1]->timezone != $b->timezone)
245 {
246 // create check-in and adjust it to the employee timezone
247 $checkin = JFactory::getDate($b->checkin_ts);
248 $checkin->setTimezone(new DateTimeZone($b->timezone));
249
250 // get check-in time adjusted to local timezone
251 $from = $checkin->format('H:i', $local = true);
252 // convert time string in minutes
253 $from = JHtml::fetch('vikappointments.time2min', $from);
254
255 /**
256 * Check the day factor of a reservation to check if it is referring
257 * to the current day or if it close to the bounds of this working time.
258 * Used to support midnight reservations.
259 *
260 * @since 1.6
261 */
262
263 if ($checkin->format('Y-m-d', $local = true) > $ymd)
264 {
265 // we are evaluating a reservation for the next day, so we need to increase the
266 // initial time by 1440 minutes (24 hours * 60).
267 $from += 1440;
268 }
269 else if ($checkin->format('Y-m-d', $local = true) < $ymd)
270 {
271 // we are evaluating a reservation for the previous day, so we need to decrease the
272 // initial time by 1440 minutes (24 hours * 60).
273 $from -= 1440;
274 }
275
276 // calculate end time by adding total duration to start time
277 $to = $from + $b->duration + $b->sleep;
278
279 for ($i = $from; $i < $to; $i += $min_int)
280 {
281 $found = false;
282
283 for ($j = 0; $j < count($arr) && !$found; $j++)
284 {
285 /**
286 * Try to block appointments that come from a different service or
287 * if the number of people exceeds the total capacity.
288 *
289 * @since 1.6 check if the services are different only if $arr[$j][$i] is set
290 */
291 if (!empty($arr[$j][$i]) && ($cont_people + $people > $service->max_capacity || $b->id_service != $service->id || $b->closure))
292 {
293 $found = true;
294 $arr[$j][$i] = 0;
295 }
296
297 /**
298 * If $seats argument is an array, push the remaining seats.
299 *
300 * @since 1.6
301 */
302 if (is_array($seats))
303 {
304 if ($b->id_service == $service->id && !$b->closure)
305 {
306 // same service, we can display the remaining seats
307 $seats[$i] = $service->max_capacity - $cont_people;
308 }
309 else
310 {
311 // booked for a different service, unset the remaining seats
312 $seats[$i] = 0;
313 }
314 }
315 }
316
317 /**
318 * We may have different services that display shifted
319 * timelines. This would cause an issue as previous check
320 * ignores the times that don't match the evaluated slots.
321 *
322 * We need to unset here all the times that intersect with
323 * an existing reservation, which might have been created for
324 * a different service.
325 *
326 * @since 1.6.2
327 */
328 if (!$found)
329 {
330 // find all slots that intersect this one
331 for ($j = 0; $j < count($arr); $j++)
332 {
333 foreach ($arr[$j] as $arr_hm => &$v)
334 {
335 if (($from < $arr_hm && $arr_hm < $to)
336 || ($arr_hm < $from && $from < $arr_hm + $service->duration + $service->sleep))
337 {
338 /**
339 * @todo evaluate to enhance this statement, because in case
340 * an appointment uses an increased duration, the exceeding
341 * slot are entirely turned off
342 */
343 $v = 0;
344 }
345 }
346 }
347 }
348 }
349
350 $cont_people = 0;
351 }
352 }
353
354 $n_step = $service->duration + $service->sleep;
355
356 // array deep : elaborate each timeline
357 for ($level = 0; $level < count($arr); $level++)
358 {
359 // get all the times in the current timeline
360 $keys = array_keys($arr[$level] ?? []);
361 // insert the end working time to evaluate properly the last available time
362 $keys[] = $worktime[$level]->endts;
363
364 for ($i = 0; $i < count($keys) - 1; $i++)
365 {
366 $last_index = -1;
367
368 for ($j = $i + 1; $j < count($keys) && $last_index == -1; $j++)
369 {
370 /**
371 * If index is last or if current time is not available.
372 *
373 * @since 1.6 Use empty($arr[$level][$keys[$j]]) to avoid "Undefined Index" notices.
374 * These notices may be raised when the reservations were stored for certain
375 * times that don't exist anymore.
376 */
377 // if ($keys[$j] == count($keys) -1 || $arr[$level][$keys[$j]] == 0)
378 if ($keys[$j] == count($keys) -1 || empty($arr[$level][$keys[$j]]))
379 {
380 // store last index found and stop for statement
381 $last_index = $j;
382 }
383 }
384
385 // if subtraction of last index found with current index is not enough
386 if ($keys[$last_index] - $keys[$i] < $n_step)
387 {
388 // if current time is still available
389 if ($arr[$level][$keys[$i]] == 1)
390 {
391 // mark current time as no more available
392 $arr[$level][$keys[$i]] = 2;
393 }
394 }
395 }
396 }
397
398 $timeline = array();
399
400 foreach ($arr as $a)
401 {
402 foreach ($a as $hour => $val)
403 {
404 $timeline[$hour] = $val;
405 }
406 }
407
408 return $timeline;
409 }
410 }
411