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