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