Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
44.26% covered (danger)
44.26%
54 / 122
33.33% covered (danger)
33.33%
5 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
SlotList
44.26% covered (danger)
44.26%
54 / 122
33.33% covered (danger)
33.33%
5 / 15
274.06
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 getQuery
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getParametersMonth
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
2
 getParametersDay
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
2
 setSlotData
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
5.02
 addQueryData
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
5
 getCalculatedSlot
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 addToCalendar
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 addFreeProcessesToCalendar
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
30
 getFreeProcesses
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 createSlots
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 isSameAvailability
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 toReducedBySlots
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 postProcess
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 __toString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace BO\Zmsdb\Query;
4
5use BO\Zmsentities\Helper\DateTime;
6use BO\Zmsentities\Slot;
7
8/**
9 *
10 * @SuppressWarnings(CouplingBetweenObjects)
11 * Calculate Slots for available booking times
12 */
13class SlotList extends Base
14{
15    const QUERY = 'SELECT
16
17            -- collect some important settings, especially from the scope, use the appointment key
18            CONCAT(b.Datum, " ", b.Uhrzeit) AS appointment__date,
19            s.StandortID AS appointment__scope__id,
20            s.mehrfachtermine AS appointment__scope__preferences__appointment__multipleSlotsEnabled,
21
22            -- results are used slots, collect some information to match calculated open slots
23            DAYOFMONTH(b.Datum) AS `day`,
24            MONTH(b.Datum) AS `month`,
25            YEAR(b.Datum) AS `year`,
26            b.Uhrzeit AS slottime,
27            b.Datum AS slotdate,
28
29            -- as grouped by slot, we can calculate available free appointments
30            GREATEST(0, o.Anzahlterminarbeitsplaetze - o.reduktionTermineImInternet - COUNT(b.Datum))
31                AS `freeAppointments__public`,
32            GREATEST(0, o.Anzahlterminarbeitsplaetze - o.reduktionTermineCallcenter - COUNT(b.Datum))
33                AS `freeAppointments__callcenter`,
34            o.Anzahlterminarbeitsplaetze - COUNT(b.Datum)
35                AS `freeAppointments__intern`,
36
37            -- calculate the incrementing slotnr for the availability
38            FLOOR(((TIME_TO_SEC(b.Uhrzeit) - TIME_TO_SEC(o.Terminanfangszeit)) / TIME_TO_SEC(o.Timeslot))) AS `slotnr`,
39
40            -- collect settings for the availability to calculate missing slots
41            o.OeffnungszeitID AS availability__id,
42            o.erlaubemehrfachslots AS availability__multipleSlotsAllowed,
43            o.allexWochen AS availability__repeat__afterWeeks,
44            o.jedexteWoche AS availability__repeat__weekOfMonth,
45            FLOOR(TIME_TO_SEC(o.Timeslot) / 60) AS availability__slotTimeInMinutes,
46            o.Startdatum AS availability__startDate,
47            o.Endedatum AS availability__endDate,
48            o.Terminanfangszeit     AS availability__startTime,
49            o.Terminendzeit     AS availability__endTime,
50
51            -- weekday is saved bitwise
52            o.Wochentag & 2 AS availability__weekday__monday,
53            o.Wochentag & 4 AS availability__weekday__tuesday,
54            o.Wochentag & 8 AS availability__weekday__wednesday,
55            o.Wochentag & 16 AS availability__weekday__thursday,
56            o.Wochentag & 32 AS availability__weekday__friday,
57            o.Wochentag & 64 AS availability__weekday__saturday,
58            o.Wochentag & 1 AS availability__weekday__sunday,
59
60            -- calculate available slots, do not use reduction values
61            o.Anzahlterminarbeitsplaetze - o.reduktionTermineImInternet AS availability__workstationCount__public,
62            o.Anzahlterminarbeitsplaetze - o.reduktionTermineCallcenter AS availability__workstationCount__callcenter,
63            o.Anzahlterminarbeitsplaetze AS availability__workstationCount__intern,
64
65            -- availability overwrites scope settings if greater zero
66            IF(o.Offen_ab, o.Offen_ab, s.Termine_ab) AS availability__bookable__startInDays,
67            IF(o.Offen_bis, o.Offen_bis, s.Termine_bis) AS availability__bookable__endInDays
68        FROM
69            standort s
70            LEFT JOIN oeffnungszeit o USING(StandortID)
71            LEFT JOIN buerger b ON
72                (
73                    b.StandortID = o.StandortID
74
75                    -- match weekday
76                    AND o.Wochentag & POW(2, DAYOFWEEK(b.Datum) - 1)
77
78                    -- match week
79                    AND (
80                        (
81                            o.allexWochen
82                            -- The following line would be correct by logic, but does not work :-/
83                                AND FLOOR(
84                                    (FLOOR(UNIX_TIMESTAMP(b.Datum))
85                                    - FLOOR(UNIX_TIMESTAMP(o.Startdatum)))
86                                    / 86400
87                                    / 7
88                                ) % o.allexWochen = 0
89                        )
90                        OR (
91                            o.jedexteWoche
92                            AND (
93                                CEIL(DAYOFMONTH(b.Datum) / 7) = o.jedexteWoche
94                                OR (
95                                    o.jedexteWoche = 5
96                                    AND CEIL(LAST_DAY(b.Datum) / 7) = CEIL(DAYOFMONTH(b.Datum) / 7)
97                                )
98                            )
99                        )
100                        OR (o.allexWochen = 0 AND o.jedexteWoche = 0)
101                    )
102
103                    -- ignore slots out of date range
104                    AND b.Datum BETWEEN :start_process AND :end_process
105
106                    -- match time and date
107                    AND b.Uhrzeit >= o.Terminanfangszeit
108                    AND b.Uhrzeit < o.Terminendzeit
109                    AND b.Datum >= o.Startdatum
110                    AND b.Datum <= o.Endedatum
111
112                    -- match day off
113                    AND (
114                        b.Datum NOT IN (
115                            SELECT Datum FROM feiertage f WHERE f.BehoerdenID = s.BehoerdenID OR f.BehoerdenID = 0
116                        )
117                        -- ignore day off if availabilty is valid for two or less days
118                        OR UNIX_TIMESTAMP(o.Endedatum) - UNIX_TIMESTAMP(o.Startdatum) < 172800
119                    )
120                )
121        WHERE
122            s.StandortID = :scope_id
123            AND o.OeffnungszeitID IS NOT NULL
124
125            -- ignore availability out of date range
126            AND o.Endedatum >= :start_availability
127            AND o.Startdatum <= :end_availability
128
129            -- ignore availability on midnight
130            AND o.Terminanfangszeit != "00:00:00"
131            AND o.Terminendzeit != "00:00:00"
132
133            -- ignore availability without appointment slots
134            AND o.Anzahlterminarbeitsplaetze != 0
135        GROUP BY o.OeffnungszeitID, b.Datum, `slotnr`
136        HAVING
137            -- reduce results cause processing them costs time even with query cache
138            (
139                appointment__date BETWEEN
140                    DATE_ADD(:nowStart, INTERVAL availability__bookable__startInDays DAY)
141                    -- appointment__date includes midnight time, so take the following day to include the last day
142                    AND DATE_ADD(:nowEnd, INTERVAL availability__bookable__endInDays + 1 DAY)
143                AND
144                (
145                    slotdate !=  DATE_ADD(:nowCompare, INTERVAL availability__bookable__endInDays DAY)
146                    OR availability__startTime < :nowTime
147                )
148            )
149            OR appointment__date IS NULL
150
151        -- ordering is important for processing later on (slot reduction)
152        ORDER BY o.OeffnungszeitID, b.Datum, `slotnr`
153        ';
154
155    /**
156     *
157     * @var array $slotData Single result row from the query
158     */
159    protected $slotData = null;
160
161    /**
162     *
163     * @var \BO\Zmsentities\Scope $scope
164     */
165    protected $scope = null;
166
167    /**
168     *
169     * @var \BO\Zmsentities\Availability $availability
170     */
171    protected $availability = null;
172
173    /**
174     *
175     * @var Array $slots
176     */
177    protected $slots = array();
178
179    public function __construct(
180        array $slotData = ['availability__id' => null],
181        \DateTimeImmutable $start = null,
182        \DateTimeImmutable $stop = null,
183        \DateTimeInterface $now = null,
184        \BO\Zmsentities\Availability $availability = null,
185        \BO\Zmsentities\Scope $scope = null
186    ) {
187        $this->availability = $availability;
188        $this->scope = $scope;
189        $this->setSlotData($slotData);
190        if ($this->availability && isset($this->availability['id'])) {
191            $this->createSlots($start, $stop, $now);
192            $this->addQueryData($slotData);
193        }
194    }
195
196    public static function getQuery()
197    {
198        return self::QUERY;
199    }
200
201    public static function getParametersMonth($scopeId, \DateTimeInterface $monthDateTime, \DateTimeInterface $now)
202    {
203        $now = DateTime::create($now);
204        $monthDateTime = DateTime::create($monthDateTime);
205        $parameters = [
206            'scope_id' => $scopeId,
207            'start_process' => $monthDateTime->format('Y-m-1'),
208            'end_process' => $monthDateTime->format('Y-m-t'),
209            'start_availability' => $monthDateTime->format('Y-m-1'),
210            'end_availability' => $monthDateTime->format('Y-m-t'),
211            'nowStart' => $now->format('Y-m-d'),
212            'nowEnd' => $now->format('Y-m-d'),
213            'nowCompare' => $now->format('Y-m-d'),
214            'nowTime' => $now->format('H:i:s'),
215        ];
216        return $parameters;
217    }
218
219    public static function getParametersDay($scopeId, \DateTimeInterface $dateTime, \DateTimeInterface $now)
220    {
221        $now = DateTime::create($now);
222        $dateTime = DateTime::create($dateTime);
223        //\App::$log->error("FreeProcess", [$dateTime->format('c')]);
224        $parameters = [
225            'scope_id' => $scopeId,
226            'start_process' => $dateTime->format('Y-m-d'),
227            'end_process' => $dateTime->format('Y-m-d'),
228            'start_availability' => $dateTime->format('Y-m-d'),
229            'end_availability' => $dateTime->format('Y-m-d'),
230            'nowStart' => $now->format('Y-m-d'),
231            'nowEnd' => $now->format('Y-m-d'),
232            'nowCompare' => $now->format('Y-m-d'),
233            'nowTime' => $now->format('H:i:s'),
234        ];
235        return $parameters;
236    }
237
238    /**
239     * To avoid a db query for availability,
240     * we use the scope data to add missing values
241     * and try to use availability data in query result
242     */
243    public function setSlotData(array $slotData)
244    {
245        $this->slotData = $slotData;
246        if (null === $this->availability) {
247            $availability = [ ];
248            foreach ($slotData as $key => $value) {
249                if (0 === strpos($key, 'availability__')) {
250                    $newkey = str_replace('availability__', '', $key);
251                    $availability[$newkey] = $value;
252                }
253            }
254            $this->availability = new \BO\Zmsentities\Availability($availability);
255        }
256        if (null !== $this->scope) {
257            $this->availability['scope'] = $this->scope;
258        }
259        return $this;
260    }
261
262    /**
263     * add data from a mysql result set
264     * @see self::QUERY
265     *
266     */
267    public function addQueryData(array $slotData)
268    {
269        if (isset($slotData['slotnr'])) {
270            $slotnumber = $slotData['slotnr'];
271            $slotdate = $slotData['slotdate'];
272            if (!isset($this->slots[$slotdate])) {
273                $slotDebug = "$slotdate #$slotnumber @" . $slotData['slottime'] . " on " . $this->availability;
274                throw new \BO\Zmsdb\Exception\SlotDataWithoutPreGeneratedSlot(
275                    "Found database entry without a generated date for $slotDebug"
276                );
277            }
278            $slotList = $this->slots[$slotdate];
279            $slot = $slotList->getSlot($slotnumber);
280            if (null === $slot) {
281                $slotDebug = "$slotdate #$slotnumber @" . $slotData['slottime'] . " on " . $this->availability;
282                // error_log("Debugdata: Found database entry without a pre-generated slot $slotDebug");
283                throw new \BO\Zmsdb\Exception\SlotDataWithoutPreGeneratedSlot(
284                    "Found database entry without a pre-generated slot $slotDebug"
285                );
286            }
287            //if ($slot->type !== Slot::FREE) {
288                // We do not throw an exception, cause availability slotTime might have changed
289            //}
290            $slotList[$slotnumber] = $this->getCalculatedSlot($slot, $slotData);
291        } elseif (isset($slotData['availability__id'])) {
292            // Only availability data for available slots, do nothing
293        } else {
294            throw new \BO\Zmsdb\Exception\SlotDataEmpty("Found empty slot: " . var_export($slotData, true));
295        }
296        return $this;
297    }
298
299    protected function getCalculatedSlot(Slot $slot, $slotData)
300    {
301        $slot->public += $slotData['freeAppointments__public'] -
302            $slotData['availability__workstationCount__public'];
303        $slot->callcenter += $slotData['freeAppointments__callcenter'] -
304            $slotData['availability__workstationCount__callcenter'];
305        $slot->intern += $slotData['freeAppointments__intern'] -
306            $slotData['availability__workstationCount__intern'];
307        $slot->time = (new DateTime($slotData['slottime']))->format('H:i');
308        $slot->type = Slot::TIMESLICE;
309        return $slot;
310    }
311
312    public function addToCalendar(
313        \BO\Zmsentities\Calendar $calendar,
314        \DateTimeInterface $now,
315        $freeProcessesDate,
316        $slotType = 'public',
317        $slotsRequired = 1
318    ) {
319        $nowDate = $now->format('Y-m-d');
320        foreach ($this->slots as $date => $slotList) {
321            if ($nowDate == $date) {
322                $slotList = ('intern' != $slotType) ? $slotList->withTimeGreaterThan($now, $slotType) : $slotList;
323                $this->slots[$date] = $slotList;
324            }
325            $this->addFreeProcessesToCalendar($calendar, $freeProcessesDate, $date, $slotType, $slotsRequired);
326            $datetime = new \DateTimeImmutable($date);
327            $day = $calendar->getDayByDateTime($datetime);
328            $day['freeAppointments'] = $slotList->getSummerizedSlot($day['freeAppointments']);
329            $day->getWithStatus($slotType, $now);
330        }
331        return $calendar;
332    }
333
334    protected function addFreeProcessesToCalendar(
335        \BO\Zmsentities\Calendar $calendar,
336        $freeProcessesDate,
337        $date,
338        $slotType = 'public',
339        $slotsRequired = 1
340    ) {
341        if (null !== $freeProcessesDate && $date == $freeProcessesDate->format('Y-m-d')) {
342            $freeProcesses = $this->getFreeProcesses($calendar, $freeProcessesDate, $slotType, $slotsRequired);
343            foreach ($freeProcesses as $process) {
344                if ($process instanceof \BO\Zmsentities\Process) {
345                    $calendar['freeProcesses']->addEntity($process);
346                }
347            }
348        }
349    }
350
351    /**
352     * TODO Unterscheidung nach intern/callcenter/public sollte erst nach der API erfolgen!
353     */
354    public function getFreeProcesses(
355        \BO\Zmsentities\Calendar $calendar,
356        \DateTimeImmutable $freeProcessesDate = null,
357        $slotType = 'public',
358        $slotsRequired = 1
359    ) {
360        $selectedDate = $freeProcessesDate->format('Y-m-d');
361        $slotList = $this->slots[$selectedDate];
362        return $slotList->getFreeProcesses(
363            $selectedDate,
364            $this->scope,
365            $this->availability,
366            $slotType,
367            $calendar['requests'],
368            $slotsRequired
369        );
370    }
371
372    /**
373     * Create slots based on availability
374     */
375    public function createSlots(\DateTimeInterface $startDate, \DateTimeInterface $stopDate, \DateTimeInterface $now)
376    {
377        $startDate = ($startDate < $now) ? $now->modify('00:00:00') : $startDate;
378        $stopDate = $stopDate->modify('00:00:00');
379        $time = DateTime::create($startDate);
380        $slotlist = $this->availability->getSlotList();
381        do {
382            $date = $time->format('Y-m-d');
383            if ($this->availability->hasDate($time, $now)) {
384                $this->slots[$date] = clone $slotlist;
385            }
386            $time = $time->modify('+1day');
387        } while ($time->getTimestamp() <= $stopDate->getTimestamp());
388    }
389
390    public function isSameAvailability(array $slotData)
391    {
392        return $this->slotData['availability__id'] == $slotData['availability__id'];
393    }
394
395    /**
396     * Reduce available slots
397     * On given amount of required slots reduce the amount of available slots by comparing continous slots available
398     *
399     * @param Int $slotsRequired
400     * @return self
401     */
402    public function toReducedBySlots($slotsRequired)
403    {
404        if (count($this->slots) && $slotsRequired > 1) {
405            foreach ($this->slots as $date => $slotList) {
406                $reduced = $slotList->withReducedSlots($slotsRequired);
407                $this->slots[$date] = $reduced;
408            }
409        }
410        return $this;
411    }
412
413    public function postProcess($data)
414    {
415        $data[$this->getPrefixed("appointment__date")] = strtotime($data[$this->getPrefixed("appointment__date")]);
416        $data[$this->getPrefixed("availability__startDate")] =
417            strtotime($data[$this->getPrefixed("availability__startDate")]);
418        $data[$this->getPrefixed("availability__endDate")] =
419            strtotime($data[$this->getPrefixed("availability__endDate")]);
420        return $data;
421    }
422
423    public function __toString()
424    {
425        return "Query_SlotList: {$this->availability} {$this->scope}";
426    }
427}