Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
82.58% covered (warning)
82.58%
109 / 132
50.00% covered (danger)
50.00%
3 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
OverallCalendarRead
82.58% covered (warning)
82.58%
109 / 132
50.00% covered (danger)
50.00%
3 / 6
36.08
0.00% covered (danger)
0.00%
0 / 1
 readResponse
97.73% covered (success)
97.73%
43 / 44
0.00% covered (danger)
0.00%
0 / 1
4
 buildAvailabilityMap
40.62% covered (danger)
40.62%
13 / 32
0.00% covered (danger)
0.00%
0 / 1
30.93
 readScopeMeta
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 buildDaysPayload
92.50% covered (success)
92.50%
37 / 40
0.00% covered (danger)
0.00%
0 / 1
9.03
 minHHMM
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 maxHHMM
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3namespace BO\Zmsbackend\Calendar\Api;
4
5use BO\Slim\Render;
6use BO\Mellon\Validator;
7use BO\Zmsbackend\OverviewCalendar\Service\OverviewCalendar as BookingQuery;
8use BO\Zmsbackend\Availability\Service\Availability as AvailabilityQuery;
9use BO\Zmsbackend\Scope\Service\Scope as ScopeQuery;
10use DateTimeImmutable;
11
12class OverallCalendarRead extends \BO\Zmsbackend\Api\BaseController
13{
14    /**
15     * @return \Psr\Http\Message\ResponseInterface
16     */
17    #[\Override]
18    public function readResponse(
19        \Psr\Http\Message\RequestInterface $request,
20        \Psr\Http\Message\ResponseInterface $response,
21        array $args
22    ) {
23        (new \BO\Zmsbackend\Helper\User($request))->checkPermissions('overviewcalendar');
24
25        $scopeIdCsv = Validator::param('scopeIds')->isString()->isMatchOf('/^\d+(,\d+)*$/')->assertValid()->getValue();
26        $scopeIds = array_map('intval', explode(',', $scopeIdCsv));
27        $dateFrom = Validator::param('dateFrom')->isDate('Y-m-d')->assertValid()->getValue();
28        $dateUntil = Validator::param('dateUntil')->isDate('Y-m-d')->assertValid()->getValue();
29        $updateAfter = Validator::param('updateAfter')->isDatetime()->setDefault(null)->getValue();
30
31        if (empty($scopeIds)) {
32            return Render::withJson($response, \BO\Zmsbackend\Api\Response\Message::create($request)->setUpdatedMetaData(), 200);
33        }
34        $untilExclusive = (new DateTimeImmutable($dateUntil))->modify('+1 day')->format('Y-m-d');
35        $bookingDb = new BookingQuery();
36        $bookings = $updateAfter === null
37            ? $bookingDb->readRange($scopeIds, $dateFrom, $untilExclusive)
38            : $bookingDb->readRangeUpdated($scopeIds, $dateFrom, $untilExclusive, $updateAfter);
39
40        $deletedProcessIds = [];
41        if ($updateAfter !== null) {
42            $changedPids = $bookingDb->readChangedProcessIdsSince($scopeIds, $updateAfter);
43            $processIdsInWindow = array_unique(array_map(fn($r) => (int)$r['process_id'], $bookings));
44            $deletedProcessIds   = array_values(array_diff($changedPids, $processIdsInWindow));
45        }
46
47        $availByDayAndScope = $this->buildAvailabilityMap($scopeIds, $dateFrom, $dateUntil);
48
49        $scopeMeta = $this->readScopeMeta($scopeIds);
50
51        [$days, $globalMin, $globalMax] = $this->buildDaysPayload(
52            $dateFrom,
53            $dateUntil,
54            $scopeIds,
55            $availByDayAndScope,
56            $bookings
57        );
58
59        $maxUpdatedWindow = $bookingDb->readMaxUpdated($scopeIds, $dateFrom, $untilExclusive);
60        $maxUpdatedGlobal = $bookingDb->readMaxUpdatedGlobal($scopeIds);
61        $maxUpdated = $maxUpdatedGlobal ?? $maxUpdatedWindow ?? (new DateTimeImmutable())->format('Y-m-d H:i:s');
62
63        $payload = [
64            'meta' => [
65                'axis'   => ['start' => $globalMin, 'end' => $globalMax],
66                'scopes' => $scopeMeta,
67            ],
68            'days'         => array_values($days),
69            'delta'        => $updateAfter !== null,
70            'maxUpdatedAt' => $maxUpdated,
71            'deletedProcessIds'   => $deletedProcessIds,
72        ];
73
74        $msg       = \BO\Zmsbackend\Api\Response\Message::create($request);
75        $msg->data = $payload;
76
77        $response = Render::withLastModified($response, (new DateTimeImmutable($maxUpdated))->getTimestamp(), '0');
78        return Render::withJson($response, $msg->setUpdatedMetaData(), 200);
79    }
80
81    private function buildAvailabilityMap(array $scopeIds, string $dateFrom, string $dateUntil): array
82    {
83        $map = [];
84        $from = new \DateTimeImmutable($dateFrom);
85        $until = new \DateTimeImmutable($dateUntil);
86        $avail = new \BO\Zmsbackend\Availability\Service\Availability();
87
88        foreach ($scopeIds as $scopeId) {
89            $scope = new \BO\Zmsentities\Scope(['id' => $scopeId]);
90            $list = $avail->readAvailabilityListByScope($scope, 0, $from, $until);
91
92            for ($day = $from; $day <= $until; $day = $day->modify('+1 day')) {
93                $dateKey = $day->format('Y-m-d');
94
95                $availForDay = $list->withDateTime($day);
96                if (!$availForDay->count()) {
97                    continue;
98                }
99
100                $intervals = [];
101
102                foreach ($availForDay as $availabilityDay) {
103                    $start = substr((string)$availabilityDay->startTime, 0, 5);
104                    $end = substr((string)$availabilityDay->endTime, 0, 5);
105                    if (!$start || !$end || $start >= $end) {
106                        continue;
107                    }
108
109                    $capacity = array_key_exists('intern', $availabilityDay->workstationCount)
110                        ? (int)$availabilityDay->workstationCount['intern']
111                        : null;
112
113                    $intervals[] = [
114                        'start' => $start,
115                        'end' => $end,
116                        'capacity' => $capacity,
117                    ];
118                }
119
120                if ($intervals) {
121                    usort($intervals, fn($x, $y) => strcmp($x['start'], $y['start']));
122
123                    $map[$dateKey][$scopeId] = [
124                        'intervals' => $intervals
125                    ];
126                }
127            }
128        }
129
130        return $map;
131    }
132
133    private function readScopeMeta(array $scopeIds): array
134    {
135        $scopeDb = new ScopeQuery();
136        $byId = $scopeDb->readEntitiesByIds($scopeIds, 0);
137        $meta = [];
138        foreach ($scopeIds as $id) {
139            $scope = $byId[$id] ?? null;
140            $meta[$id] = [
141                'name'      => $scope?->getName() ?? '',
142                'shortName' => $scope?->getShortName() ?? '',
143            ];
144        }
145        return $meta;
146    }
147
148    private function buildDaysPayload(
149        string $dateFrom,
150        string $dateUntil,
151        array $scopeIds,
152        array $availByDayAndScope,
153        array $bookingRows
154    ): array {
155        $days = [];
156        $globalMin = null;
157        $globalMax = null;
158
159        for (
160            $cursor = new \DateTimeImmutable($dateFrom);
161             $cursor <= new \DateTimeImmutable($dateUntil);
162             $cursor = $cursor->modify('+1 day')
163        ) {
164            $ymd = $cursor->format('Y-m-d');
165            $days[$ymd] = ['date' => $ymd, 'scopes' => []];
166
167            foreach ($scopeIds as $scopeId) {
168                $intervals = $availByDayAndScope[$ymd][$scopeId]['intervals'] ?? [];
169
170                if ($intervals) {
171                    foreach ($intervals as $interval) {
172                        $globalMin = $this->minHHMM($globalMin, $interval['start']);
173                        $globalMax = $this->maxHHMM($globalMax, $interval['end']);
174                    }
175                }
176
177                $days[$ymd]['scopes'][$scopeId] = [
178                    'id' => $scopeId,
179                    'intervals' => $intervals,
180                    'events' => [],
181                ];
182            }
183        }
184
185        foreach ($bookingRows as $bookingRow) {
186            $dKey = (new \DateTimeImmutable($bookingRow['starts_at']))->format('Y-m-d');
187
188            $sid = (int)$bookingRow['scope_id'];
189            $start = substr($bookingRow['starts_at'], 11, 5);
190            $end = substr($bookingRow['ends_at'], 11, 5);
191
192            $displayNumber = isset($bookingRow['display_number'])
193                ? trim((string)$bookingRow['display_number'])
194                : '';
195            $days[$dKey]['scopes'][$sid]['events'][] = [
196                'processId' => (int)$bookingRow['process_id'],
197                'displayNumber' => $displayNumber !== '' ? $displayNumber : null,
198                'start' => $start,
199                'end' => $end,
200                'status' => $bookingRow['status'],
201                'updatedAt' => (string)$bookingRow['updated_at'],
202            ];
203
204            $globalMin = $this->minHHMM($globalMin, $start);
205            $globalMax = $this->maxHHMM($globalMax, $end);
206        }
207
208        foreach ($days as &$day) {
209            $day['scopes'] = array_values($day['scopes']);
210        }
211
212        return [$days, $globalMin, $globalMax];
213    }
214
215    private function minHHMM(?string $a, string $b): string
216    {
217        if ($a === null) {
218            return $b;
219        }
220        return ($a <= $b) ? $a : $b;
221    }
222
223    private function maxHHMM(?string $a, string $b): string
224    {
225        if ($a === null) {
226            return $b;
227        }
228        return ($a >= $b) ? $a : $b;
229    }
230}