Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
86.44% covered (warning)
86.44%
102 / 118
44.44% covered (danger)
44.44%
4 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
JourneyService
86.44% covered (warning)
86.44%
102 / 118
44.44% covered (danger)
44.44%
4 / 9
26.56
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 recordEvent
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 getIndexData
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 buildFilters
96.30% covered (success)
96.30%
26 / 27
0.00% covered (danger)
0.00%
0 / 1
4
 resolveDates
58.33% covered (warning)
58.33%
7 / 12
0.00% covered (danger)
0.00%
0 / 1
3.65
 getEvents
65.22% covered (warning)
65.22%
15 / 23
0.00% covered (danger)
0.00%
0 / 1
6.05
 buildSummary
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
4
 eventTypeOptions
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
4.00
 allowedChildIds
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
2.00
1<?php
2
3namespace App\Services;
4
5use App\Models\JourneyEventModel;
6use CodeIgniter\Shield\Entities\User;
7use DateTimeImmutable;
8
9class JourneyService
10{
11    private AuthorizationService $authorizationService;
12
13    public function __construct()
14    {
15        helper(['academy', 'url']);
16
17        $this->authorizationService = service('authorization');
18    }
19
20    /**
21     * @param array<string, mixed> $event
22     */
23    public function recordEvent(array $event): int
24    {
25        $model = model(JourneyEventModel::class);
26
27        $payload = array_merge([
28            'metadata' => null,
29            'created_at' => date('Y-m-d H:i:s'),
30        ], $event);
31
32        $model->insert($payload);
33
34        return (int) $model->getInsertID();
35    }
36
37    /**
38     * @param array<string, mixed> $input
39     * @return array<string, mixed>
40     */
41    public function getIndexData(array $input, User $user): array
42    {
43        $filters = $this->buildFilters($input, (int) $user->id);
44        $events  = $this->getEvents($filters);
45
46        return [
47            'pageTitle' => 'Journey',
48            'pageDescription' => 'Timeline operational pentru copii, cu evenimente generate automat din evaluari, profil si prezenta.',
49            'filters' => $filters,
50            'summary' => $this->buildSummary($events),
51            'events' => $events,
52        ];
53    }
54
55    /**
56     * @param array<string, mixed> $input
57     * @return array<string, mixed>
58     */
59    private function buildFilters(array $input, int $userId): array
60    {
61        $role           = $this->authorizationService->getPrimaryRole($userId);
62        $lockedTrainerId  = $role === 'coach' ? $this->authorizationService->getCoachIdForUser($userId) : null;
63        $selectedPeriod = (string) ($input['period'] ?? '30');
64        $today          = new DateTimeImmutable('today');
65        [$dateStart, $dateEnd] = $this->resolveDates($selectedPeriod, $today);
66        $query          = trim((string) ($input['q'] ?? ''));
67        $selectedType   = trim((string) ($input['event_type'] ?? 'all'));
68        $eventOptions   = $this->eventTypeOptions($lockedTrainerId);
69
70        if ($selectedType !== 'all' && ! array_key_exists($selectedType, $eventOptions)) {
71            $selectedType = 'all';
72        }
73
74        return [
75            'current_role' => $role,
76            'coach_locked' => $lockedTrainerId !== null,
77            'selected_period' => $selectedPeriod,
78            'selected_event_type' => $selectedType,
79            'query' => $query,
80            'date_start' => $dateStart,
81            'date_end' => $dateEnd,
82            'period_options' => [
83                '7' => 'Ultimele 7 zile',
84                '30' => 'Ultimele 30 zile',
85                '90' => 'Ultimele 90 zile',
86                'month' => 'Luna curenta',
87            ],
88            'event_type_options' => ['all' => 'Toate evenimentele'] + $eventOptions,
89            'allowed_child_ids' => $this->allowedChildIds($lockedTrainerId),
90        ];
91    }
92
93    /**
94     * @return array{0: string, 1: string}
95     */
96    private function resolveDates(string $selectedPeriod, DateTimeImmutable $today): array
97    {
98        if ($selectedPeriod === 'month') {
99            return [
100                $today->modify('first day of this month')->format('Y-m-d'),
101                $today->format('Y-m-d'),
102            ];
103        }
104
105        $days = (int) $selectedPeriod;
106        if (! in_array($days, [7, 30, 90], true)) {
107            $days = 30;
108        }
109
110        return [
111            $today->modify('-' . ($days - 1) . ' days')->format('Y-m-d'),
112            $today->format('Y-m-d'),
113        ];
114    }
115
116    /**
117     * @param array<string, mixed> $filters
118     * @return list<array<string, mixed>>
119     */
120    private function getEvents(array $filters): array
121    {
122        $builder = db_connect()
123            ->table('journey_events events')
124            ->select('events.*, children.full_name as child_name, users.username as actor_name')
125            ->join('children', 'children.id = events.child_id')
126            ->join('users', 'users.id = events.created_by', 'left')
127            ->where('events.created_at >=', $filters['date_start'] . ' 00:00:00')
128            ->where('events.created_at <=', $filters['date_end'] . ' 23:59:59')
129            ->orderBy('events.created_at', 'DESC')
130            ->orderBy('events.id', 'DESC');
131
132        if ($filters['allowed_child_ids'] !== null) {
133            if ($filters['allowed_child_ids'] === []) {
134                return [];
135            }
136
137            $builder->whereIn('events.child_id', $filters['allowed_child_ids']);
138        }
139
140        if ($filters['selected_event_type'] !== 'all') {
141            $builder->where('events.event_type', $filters['selected_event_type']);
142        }
143
144        if ($filters['query'] !== '') {
145            $builder
146                ->groupStart()
147                ->like('children.full_name', $filters['query'])
148                ->orLike('events.title', $filters['query'])
149                ->orLike('events.description', $filters['query'])
150                ->groupEnd();
151        }
152
153        return $builder->get()->getResultArray();
154    }
155
156    /**
157     * @param list<array<string, mixed>> $events
158     * @return list<array<string, mixed>>
159     */
160    private function buildSummary(array $events): array
161    {
162        $counts = [];
163        foreach ($events as $event) {
164            $type = (string) $event['event_type'];
165            if (! isset($counts[$type])) {
166                $counts[$type] = 0;
167            }
168
169            $counts[$type]++;
170        }
171
172        arsort($counts);
173        $topType = $counts === [] ? ['label' => '-', 'value' => 0] : ['label' => status_label((string) array_key_first($counts)), 'value' => (int) current($counts)];
174
175        return [
176            ['label' => 'Evenimente in selectie', 'value' => count($events), 'meta' => 'Timeline filtrat dupa perioada'],
177            ['label' => 'Tip dominant', 'value' => $topType['label'], 'meta' => 'Aparitii: ' . $topType['value']],
178        ];
179    }
180
181    /**
182     * @return array<string, string>
183     */
184    private function eventTypeOptions(?int $lockedTrainerId): array
185    {
186        $builder = db_connect()
187            ->table('journey_events events')
188            ->distinct()
189            ->select('events.event_type')
190            ->orderBy('events.event_type', 'ASC');
191
192        $allowedChildIds = $this->allowedChildIds($lockedTrainerId);
193        if ($allowedChildIds !== null) {
194            if ($allowedChildIds === []) {
195                return [];
196            }
197
198            $builder->whereIn('events.child_id', $allowedChildIds);
199        }
200
201        $rows    = $builder->get()->getResultArray();
202        $options = [];
203        foreach ($rows as $row) {
204            $options[$row['event_type']] = status_label($row['event_type']);
205        }
206
207        return $options;
208    }
209
210    /**
211     * @return list<int>|null
212     */
213    private function allowedChildIds(?int $lockedTrainerId): ?array
214    {
215        if ($lockedTrainerId === null) {
216            return null;
217        }
218
219        return array_values(array_map(
220            static fn (array $row): int => (int) $row['id'],
221            db_connect()
222                ->table('children')
223                ->select('id')
224                ->where('primary_coach_id', $lockedTrainerId)
225                ->get()
226                ->getResultArray(),
227        ));
228    }
229}