Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
160 / 160
100.00% covered (success)
100.00%
36 / 36
CRAP
100.00% covered (success)
100.00%
1 / 1
Stats
100.00% covered (success)
100.00%
160 / 160
100.00% covered (success)
100.00%
36 / 36
58
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
 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 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}