Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
160 / 160 |
|
100.00% |
36 / 36 |
CRAP | |
100.00% |
1 / 1 |
Stats | |
100.00% |
160 / 160 |
|
100.00% |
36 / 36 |
58 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
usersState | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
3 | |||
usersInfo | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
forceActiveUsers | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
usersPronouns | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
|
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | ||||
currentlyWorkingUsers | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
3 | |||
vouchersQuery | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
vouchers | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
vouchersBuckets | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
goodies | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
tshirtSizes | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
languages | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
themes | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
licenses | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
2 | |||
workSecondsQuery | n/a |
0 / 0 |
n/a |
0 / 0 |
5 | |||||
workSeconds | n/a |
0 / 0 |
n/a |
0 / 0 |
1 | |||||
workBuckets | n/a |
0 / 0 |
n/a |
0 / 0 |
1 | |||||
getBuckets | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
worklogSeconds | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
worklogBuckets | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
locations | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
shiftTypes | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
angelTypesSum | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
angelTypes | |
100.00% |
21 / 21 |
|
100.00% |
1 / 1 |
3 | |||
shifts | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
announcements | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
comments | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
questions | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
faq | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
messages | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
sessions | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
oauth | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
databaseRead | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
databaseWrite | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
logEntries | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
passwordResets | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getQuery | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
raw | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | declare(strict_types=1); |
4 | |
5 | namespace Engelsystem\Controllers\Metrics; |
6 | |
7 | use Carbon\Carbon; |
8 | use Engelsystem\Database\Database; |
9 | use Engelsystem\Models\AngelType; |
10 | use Engelsystem\Models\EventConfig; |
11 | use Engelsystem\Models\Faq; |
12 | use Engelsystem\Models\Location; |
13 | use Engelsystem\Models\LogEntry; |
14 | use Engelsystem\Models\Message; |
15 | use Engelsystem\Models\News; |
16 | use Engelsystem\Models\NewsComment; |
17 | use Engelsystem\Models\OAuth; |
18 | use Engelsystem\Models\Question; |
19 | use Engelsystem\Models\Shifts\Shift; |
20 | use Engelsystem\Models\Shifts\ShiftType; |
21 | use Engelsystem\Models\User\License; |
22 | use Engelsystem\Models\User\PasswordReset; |
23 | use Engelsystem\Models\User\PersonalData; |
24 | use Engelsystem\Models\User\Settings; |
25 | use Engelsystem\Models\User\State; |
26 | use Engelsystem\Models\User\User; |
27 | use Engelsystem\Models\UserAngelType; |
28 | use Engelsystem\Models\Worklog; |
29 | use Illuminate\Contracts\Database\Query\Builder as BuilderContract; |
30 | use Illuminate\Database\Eloquent\Builder; |
31 | use Illuminate\Database\Query\Builder as QueryBuilder; |
32 | use Illuminate\Database\Query\Expression as QueryExpression; |
33 | use Illuminate\Support\Collection; |
34 | |
35 | class Stats |
36 | { |
37 | public function __construct(protected Database $db) |
38 | { |
39 | } |
40 | |
41 | /** |
42 | * The number of users that arrived/not arrived and/or did some work |
43 | * |
44 | */ |
45 | public function usersState(?bool $working = null, bool $arrived = true): int |
46 | { |
47 | $query = State::whereArrived($arrived); |
48 | |
49 | if (!is_null($working)) { |
50 | $query |
51 | ->leftJoin('worklogs', 'worklogs.user_id', '=', 'users_state.user_id') |
52 | ->leftJoin('shift_entries', 'shift_entries.user_id', '=', 'users_state.user_id') |
53 | ->distinct(); |
54 | |
55 | $query->where(function ($query) use ($working): void { |
56 | /** @var QueryBuilder $query */ |
57 | if ($working) { |
58 | $query |
59 | ->whereNotNull('shift_entries.shift_id') |
60 | ->orWhereNotNull('worklogs.hours'); |
61 | |
62 | return; |
63 | } |
64 | |
65 | $query |
66 | ->whereNull('shift_entries.shift_id') |
67 | ->whereNull('worklogs.hours'); |
68 | }); |
69 | } |
70 | |
71 | return $query->count('users_state.user_id'); |
72 | } |
73 | |
74 | public function usersInfo(): int |
75 | { |
76 | return State::query() |
77 | ->whereNotNull('user_info') |
78 | ->whereNot('user_info', '') |
79 | ->count(); |
80 | } |
81 | |
82 | public function forceActiveUsers(): int |
83 | { |
84 | return State::whereForceActive(true)->count(); |
85 | } |
86 | |
87 | public function usersPronouns(): int |
88 | { |
89 | return PersonalData::query()->where('pronoun', '!=', '')->count(); |
90 | } |
91 | |
92 | public function email(string $type): int |
93 | { |
94 | return match ($type) { |
95 | 'system' => Settings::whereEmailShiftinfo(true)->count(), |
96 | 'humans' => Settings::whereEmailHuman(true)->count(), |
97 | 'goodie' => Settings::whereEmailGoodie(true)->count(), |
98 | 'news' => Settings::whereEmailNews(true)->count(), |
99 | default => 0, |
100 | }; |
101 | } |
102 | |
103 | /** |
104 | * The number of currently working users |
105 | * |
106 | */ |
107 | public function currentlyWorkingUsers(?bool $freeloaded = null): int |
108 | { |
109 | $query = User::query() |
110 | ->join('shift_entries', 'shift_entries.user_id', '=', 'users.id') |
111 | ->join('shifts', 'shifts.id', '=', 'shift_entries.shift_id') |
112 | ->where('shifts.start', '<=', Carbon::now()) |
113 | ->where('shifts.end', '>', Carbon::now()); |
114 | |
115 | if (!is_null($freeloaded)) { |
116 | $freeloaded |
117 | ? $query->whereNotNull('shift_entries.freeloaded_by') |
118 | : $query->whereNull('shift_entries.freeloaded_by'); |
119 | } |
120 | |
121 | return $query->count(); |
122 | } |
123 | |
124 | protected function vouchersQuery(): Builder |
125 | { |
126 | return State::query(); |
127 | } |
128 | |
129 | public function vouchers(): int |
130 | { |
131 | return (int) $this->vouchersQuery()->sum('got_voucher'); |
132 | } |
133 | |
134 | public function vouchersBuckets(array $buckets): array |
135 | { |
136 | $return = []; |
137 | foreach ($buckets as $bucket) { |
138 | $query = $this->vouchersQuery(); |
139 | |
140 | if ($bucket !== '+Inf') { |
141 | $query->where('got_voucher', '<=', $bucket); |
142 | } |
143 | |
144 | $return[$bucket] = $query->count('got_voucher'); |
145 | } |
146 | |
147 | return $return; |
148 | } |
149 | |
150 | public function goodies(): int |
151 | { |
152 | return State::whereGotGoodie(true)->count(); |
153 | } |
154 | |
155 | public function tshirtSizes(): Collection |
156 | { |
157 | return PersonalData::query() |
158 | ->select(['shirt_size', $this->raw('COUNT(shirt_size) AS count')]) |
159 | ->whereNotNull('shirt_size') |
160 | ->groupBy(['shirt_size']) |
161 | ->get(); |
162 | } |
163 | |
164 | public function languages(): Collection |
165 | { |
166 | return Settings::query() |
167 | ->select(['language', $this->raw('COUNT(language) AS count')]) |
168 | ->groupBy(['language']) |
169 | ->get(); |
170 | } |
171 | |
172 | public function themes(): Collection |
173 | { |
174 | return Settings::query() |
175 | ->select(['theme', $this->raw('COUNT(theme) AS count')]) |
176 | ->groupBy(['theme']) |
177 | ->get(); |
178 | } |
179 | |
180 | public function licenses(string $license, bool $confirmed = false): int |
181 | { |
182 | $mapping = [ |
183 | 'has_car' => ['has_car', null], |
184 | 'forklift' => ['drive_forklift', 'drive_confirmed'], |
185 | 'car' => ['drive_car', 'drive_confirmed'], |
186 | '3.5t' => ['drive_3_5t', 'drive_confirmed'], |
187 | '7.5t' => ['drive_7_5t', 'drive_confirmed'], |
188 | '12t' => ['drive_12t', 'drive_confirmed'], |
189 | 'ifsg_light' => ['ifsg_certificate_light', 'ifsg_confirmed'], |
190 | 'ifsg' => ['ifsg_certificate', 'ifsg_confirmed'], |
191 | ]; |
192 | |
193 | $query = (new License()) |
194 | ->getQuery() |
195 | ->where($mapping[$license][0], true); |
196 | if (!is_null($mapping[$license][1])) { |
197 | $query->where($mapping[$license][1], $confirmed); |
198 | } |
199 | |
200 | return $query->count(); |
201 | } |
202 | |
203 | /** |
204 | * |
205 | * @codeCoverageIgnore because it is only used in functions that use TIMESTAMPDIFF |
206 | */ |
207 | protected function workSecondsQuery(?bool $done = null, ?bool $freeloaded = null): QueryBuilder |
208 | { |
209 | $query = $this |
210 | ->getQuery('shift_entries') |
211 | ->join('shifts', 'shifts.id', '=', 'shift_entries.shift_id'); |
212 | |
213 | if (!is_null($freeloaded)) { |
214 | $freeloaded |
215 | ? $query->whereNotNull('freeloaded_by') |
216 | : $query->whereNull('freeloaded_by'); |
217 | } |
218 | |
219 | if (!is_null($done)) { |
220 | $query->where('end', ($done ? '<' : '>='), Carbon::now()); |
221 | } |
222 | |
223 | return $query; |
224 | } |
225 | |
226 | /** |
227 | * The amount of worked seconds |
228 | * |
229 | * |
230 | * @codeCoverageIgnore as TIMESTAMPDIFF is not implemented in SQLite |
231 | */ |
232 | public function workSeconds(?bool $done = null, ?bool $freeloaded = null): int |
233 | { |
234 | $query = $this->workSecondsQuery($done, $freeloaded); |
235 | |
236 | return (int) $query->sum($this->raw('TIMESTAMPDIFF(MINUTE, start, end) * 60')); |
237 | } |
238 | |
239 | /** |
240 | * The number of worked shifts |
241 | * |
242 | * |
243 | * @codeCoverageIgnore as TIMESTAMPDIFF is not implemented in SQLite |
244 | */ |
245 | public function workBuckets(array $buckets, ?bool $done = null, ?bool $freeloaded = null): array |
246 | { |
247 | return $this->getBuckets( |
248 | $buckets, |
249 | $this->workSecondsQuery($done, $freeloaded), |
250 | 'user_id', |
251 | 'SUM(TIMESTAMPDIFF(MINUTE, start, end) * 60)', |
252 | 'SUM(TIMESTAMPDIFF(MINUTE, start, end) * 60)' |
253 | ); |
254 | } |
255 | |
256 | protected function getBuckets( |
257 | array $buckets, |
258 | BuilderContract $basicQuery, |
259 | string $groupBy, |
260 | string $having, |
261 | string $count |
262 | ): array { |
263 | $return = []; |
264 | |
265 | foreach ($buckets as $bucket) { |
266 | $query = clone $basicQuery; |
267 | $query->groupBy($groupBy); |
268 | |
269 | if ($bucket !== '+Inf') { |
270 | $query->having($this->raw($having), '<=', $bucket); |
271 | } |
272 | |
273 | $return[$bucket] = count($query->get($this->raw($count))); |
274 | } |
275 | |
276 | return $return; |
277 | } |
278 | |
279 | public function worklogSeconds(): int |
280 | { |
281 | return (int) Worklog::query() |
282 | ->sum($this->raw('hours * 60 * 60')); |
283 | } |
284 | |
285 | public function worklogBuckets(array $buckets): array |
286 | { |
287 | return $this->getBuckets( |
288 | $buckets, |
289 | Worklog::query(), |
290 | 'user_id', |
291 | 'SUM(hours * 60 * 60)', |
292 | 'SUM(hours * 60 * 60)' |
293 | ); |
294 | } |
295 | |
296 | public function locations(): int |
297 | { |
298 | return Location::query() |
299 | ->count(); |
300 | } |
301 | |
302 | public function shiftTypes(): int |
303 | { |
304 | return ShiftType::query() |
305 | ->count(); |
306 | } |
307 | |
308 | public function angelTypesSum(): int |
309 | { |
310 | return AngelType::query()->count(); |
311 | } |
312 | |
313 | public function angelTypes(): array |
314 | { |
315 | $angelTypes = []; |
316 | $rawAngelTypes = AngelType::query()->select(['id', 'name', 'restricted'])->orderBy('name')->get(); |
317 | foreach ($rawAngelTypes as $angelType) { |
318 | $restricted = $angelType->restricted; |
319 | $userAngelTypeQuery = UserAngelType::query() |
320 | ->where('angel_type_id', $angelType->id); |
321 | |
322 | $members = $userAngelTypeQuery->count(); |
323 | $supporters = (clone $userAngelTypeQuery)->where('supporter', true)->count(); |
324 | $confirmed = $members - $supporters; |
325 | $unconfirmed = 0; |
326 | if ($restricted) { |
327 | $confirmed = (clone $userAngelTypeQuery)->whereNotNull('confirm_user_id')->count() - $supporters; |
328 | $unconfirmed = $members - ($supporters + $confirmed); |
329 | } |
330 | |
331 | $angelTypes[] = [ |
332 | 'name' => $angelType->name, |
333 | 'restricted' => $restricted, |
334 | 'unconfirmed' => $unconfirmed, |
335 | 'supporters' => $supporters, |
336 | 'confirmed' => $confirmed, |
337 | ]; |
338 | } |
339 | return $angelTypes; |
340 | } |
341 | |
342 | public function shifts(): int |
343 | { |
344 | return Shift::query()->count(); |
345 | } |
346 | |
347 | public function announcements(?bool $meeting = null): int |
348 | { |
349 | $query = is_null($meeting) ? News::query() : News::whereIsMeeting($meeting); |
350 | |
351 | return $query->count(); |
352 | } |
353 | |
354 | public function comments(): int |
355 | { |
356 | return NewsComment::query() |
357 | ->count(); |
358 | } |
359 | |
360 | public function questions(?bool $answered = null): int |
361 | { |
362 | $query = Question::query(); |
363 | if (!is_null($answered)) { |
364 | if ($answered) { |
365 | $query->whereNotNull('answerer_id'); |
366 | } else { |
367 | $query->whereNull('answerer_id'); |
368 | } |
369 | } |
370 | |
371 | return $query->count(); |
372 | } |
373 | |
374 | public function faq(): int |
375 | { |
376 | return Faq::query()->count(); |
377 | } |
378 | |
379 | public function messages(): int |
380 | { |
381 | return Message::query()->count(); |
382 | } |
383 | |
384 | public function sessions(): int |
385 | { |
386 | return $this |
387 | ->getQuery('sessions') |
388 | ->count(); |
389 | } |
390 | |
391 | public function oauth(): Collection |
392 | { |
393 | return OAuth::query() |
394 | ->select(['provider', $this->raw('COUNT(provider) AS count')]) |
395 | ->groupBy(['provider']) |
396 | ->get(); |
397 | } |
398 | |
399 | public function databaseRead(): float |
400 | { |
401 | $start = microtime(true); |
402 | |
403 | (new EventConfig())->findOrNew('last_metrics'); |
404 | |
405 | return microtime(true) - $start; |
406 | } |
407 | |
408 | public function databaseWrite(): float |
409 | { |
410 | $config = (new EventConfig())->findOrNew('last_metrics'); |
411 | $config |
412 | ->setAttribute('name', 'last_metrics') |
413 | ->setAttribute('value', new Carbon()); |
414 | |
415 | $start = microtime(true); |
416 | |
417 | $config->save(); |
418 | |
419 | return microtime(true) - $start; |
420 | } |
421 | |
422 | public function logEntries(?string $level = null): int |
423 | { |
424 | $query = is_null($level) ? LogEntry::query() : LogEntry::whereLevel($level); |
425 | |
426 | return $query->count(); |
427 | } |
428 | |
429 | public function passwordResets(): int |
430 | { |
431 | return PasswordReset::query()->count(); |
432 | } |
433 | |
434 | protected function getQuery(string $table): QueryBuilder |
435 | { |
436 | return $this->db |
437 | ->getConnection() |
438 | ->table($table); |
439 | } |
440 | |
441 | protected function raw(mixed $value): QueryExpression |
442 | { |
443 | return $this->db->getConnection()->raw($value); |
444 | } |
445 | } |