| 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 |
3 | namespace App\Services;
4 |
5 | use App\Models\JourneyEventModel;
6 | use CodeIgniter\Shield\Entities\User;
7 | use DateTimeImmutable;
8 |
9 | class 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 | }