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