| Code Coverage
| Lines | Functions and Methods | Classes and Traits
Total | 52.29% covered (warning) | 52.29% | 171 / 327 | 28.57% covered (danger) | 28.57% | 4 / 14 | CRAP | 0.00% covered (danger) | 0.00% | 0 / 1
EvaluationsService | 52.29% covered (warning) | 52.29% | 171 / 327 | 28.57% covered (danger) | 28.57% | 4 / 14 | 570.05 | 0.00% covered (danger) | 0.00% | 0 / 1
__construct | 100.00% covered (success) | 100.00% | 4 / 4 | 100.00% covered (success) | 100.00% | 1 / 1 | 1 |
getIndexData | 0.00% covered (danger) | 0.00% | 0 / 16 | 0.00% covered (danger) | 0.00% | 0 / 1 | 20 |
getFormData | 4.65% covered (danger) | 4.65% | 2 / 43 | 0.00% covered (danger) | 0.00% | 0 / 1 | 159.50 |
save | 89.90% covered (warning) | 89.90% | 89 / 99 | 0.00% covered (danger) | 0.00% | 0 / 1 | 22.50 |
getDetailData | 97.44% covered (success) | 97.44% | 38 / 39 | 0.00% covered (danger) | 0.00% | 0 / 1 | 5 |
buildIndexFilters | 0.00% covered (danger) | 0.00% | 0 / 37 | 0.00% covered (danger) | 0.00% | 0 / 1 | 56 |
getEvaluationRows | 0.00% covered (danger) | 0.00% | 0 / 21 | 0.00% covered (danger) | 0.00% | 0 / 1 | 42 |
buildStats | 0.00% covered (danger) | 0.00% | 0 / 11 | 0.00% covered (danger) | 0.00% | 0 / 1 | 6 |
getChildRow | 100.00% covered (success) | 100.00% | 9 / 9 | 100.00% covered (success) | 100.00% | 1 / 1 | 1 |
getEvaluationType | 100.00% covered (success) | 100.00% | 6 / 6 | 100.00% covered (success) | 100.00% | 1 / 1 | 1 |
getCriteriaForType | 95.65% covered (success) | 95.65% | 22 / 23 | 0.00% covered (danger) | 0.00% | 0 / 1 | 3 |
getLatestEvaluationForChildAndType | 0.00% covered (danger) | 0.00% | 0 / 11 | 0.00% covered (danger) | 0.00% | 0 / 1 | 2 |
coachOptions | 0.00% covered (danger) | 0.00% | 0 / 7 | 0.00% covered (danger) | 0.00% | 0 / 1 | 2 |
db | 100.00% covered (success) | 100.00% | 1 / 1 | 100.00% covered (success) | 100.00% | 1 / 1 | 1 |

1 | <?php
2 |
3 | namespace App\Services;
4 |
5 | use CodeIgniter\Shield\Entities\User;
6 | use DateTimeImmutable;
7 |
8 | class EvaluationsService
9 | {
10 | private AuthorizationService $authorizationService;
11 | private EvaluationEngineService $evaluationEngine;
12 | private JourneyService $journeyService;
13 |
14 | public function __construct()
15 | {
16 | helper('academy');
17 |
18 | $this->authorizationService = service('authorization');
19 | $this->evaluationEngine = service('evaluationEngine');
20 | $this->journeyService = service('journey');
21 | }
22 |
23 | /**
24 | * @param array<string, mixed> $input
25 | * @return array<string, mixed>|null
26 | */
27 | public function getIndexData(array $input, User $user, string $typeSlug): ?array
28 | {
29 | $type = $this->getEvaluationType($typeSlug);
30 | if ($type === null) {
31 | return null;
32 | }
33 |
34 | $filters = $this->buildIndexFilters($input, (int) $user->id, $typeSlug);
35 | $evaluations = $this->getEvaluationRows((int) $type['id'], $filters);
36 | $stats = $this->buildStats($evaluations);
37 |
38 | return [
39 | 'pageTitle' => $typeSlug === 'quick-check' ? 'Quick Check' : 'Evaluari',
40 | 'pageDescription' => $typeSlug === 'quick-check'
41 | ? 'Monitorizare rapida a progresului, separata de evaluarile complete care pot sustine promovarea.'
42 | : 'Evaluari complete cu scorare ponderata, status calculat automat si semnal de promovare.',
43 | 'scopeType' => $type,
44 | 'filters' => $filters,
45 | 'stats' => $stats,
46 | 'evaluations' => $evaluations,
47 | ];
48 | }
49 |
50 | /**
51 | * @param array<string, mixed> $input
52 | * @param array<string, string> $errors
53 | * @return array<string, mixed>|null
54 | */
55 | public function getFormData(int $childId, string $typeSlug, User $user, array $input = [], array $errors = []): ?array
56 | {
57 | if (! $this->authorizationService->userCanAccessChild((int) $user->id, $childId)) {
58 | return null;
59 | }
60 |
61 | $child = $this->getChildRow($childId);
62 | $type = $this->getEvaluationType($typeSlug);
63 | if ($child === null || $type === null) {
64 | return null;
65 | }
66 |
67 | $criteria = $this->getCriteriaForType((int) $type['id'], $child['academy_level_id'] !== null ? (int) $child['academy_level_id'] : null);
68 | $rules = $this->evaluationEngine->getActiveRuleSet();
69 | $coachId = $this->authorizationService->getCoachIdForUser((int) $user->id);
70 | $defaultEvaluatorTrainerId = $this->authorizationService->userHasRole((int) $user->id, 'coach')
71 | ? $coachId
72 | : ((int) ($child['primary_coach_id'] ?? 0) > 0 ? (int) $child['primary_coach_id'] : null);
73 |
74 | $values = [
75 | 'evaluation_date' => date('Y-m-d'),
76 | 'evaluator_coach_id' => $defaultEvaluatorTrainerId !== null ? (string) $defaultEvaluatorTrainerId : '',
77 | 'notes' => '',
78 | 'scores' => [],
79 | ];
80 |
81 | foreach ($criteria as $criterion) {
82 | $values['scores'][(int) $criterion['id']] = '';
83 | }
84 |
85 | if ($input !== []) {
86 | $values['evaluation_date'] = trim((string) ($input['evaluation_date'] ?? $values['evaluation_date']));
87 | $values['evaluator_coach_id'] = trim((string) ($input['evaluator_coach_id'] ?? $values['evaluator_coach_id']));
88 | $values['notes'] = trim((string) ($input['notes'] ?? ''));
89 |
90 | foreach ((array) ($input['scores'] ?? []) as $criterionId => $score) {
91 | $criterionId = (int) $criterionId;
92 | if (array_key_exists($criterionId, $values['scores'])) {
93 | $values['scores'][$criterionId] = trim((string) $score);
94 | }
95 | }
96 | }
97 |
98 | return [
99 | 'pageTitle' => $type['name'] . ' nou',
100 | 'pageDescription' => 'Evaluarea foloseste criteriile active asociate tipului selectat si calculeaza scorul final in backend.',
101 | 'child' => $child,
102 | 'evaluationType' => $type,
103 | 'criteria' => $criteria,
104 | 'rules' => $rules,
105 | 'values' => $values,
106 | 'errors' => $errors,
107 | 'canChooseEvaluator' => $this->authorizationService->userHasRole((int) $user->id, 'admin'),
108 | 'evaluatorOptions' => $this->coachOptions(),
109 | 'latestEvaluation' => $this->getLatestEvaluationForChildAndType($childId, $typeSlug),
110 | 'submitUrl' => url_to('evaluations.create', $childId, $typeSlug),
111 | 'cancelUrl' => url_to('children.show', $childId) . '?tab=' . ($typeSlug === 'quick-check' ? 'quick-checks' : 'evaluations'),
112 | ];
113 | }
114 |
115 | /**
116 | * @param array<string, mixed> $input
117 | * @return array{success: bool, evaluation_id?: int, errors?: array<string, string>, forbidden?: bool}
118 | */
119 | public function save(int $childId, string $typeSlug, array $input, User $user): array
120 | {
121 | if (! $this->authorizationService->userCanAccessChild((int) $user->id, $childId)) {
122 | return ['success' => false, 'forbidden' => true];
123 | }
124 |
125 | $child = $this->getChildRow($childId);
126 | $type = $this->getEvaluationType($typeSlug);
127 | if ($child === null || $type === null) {
128 | return ['success' => false, 'forbidden' => true];
129 | }
130 |
131 | $criteria = $this->getCriteriaForType((int) $type['id'], $child['academy_level_id'] !== null ? (int) $child['academy_level_id'] : null);
132 | $payload = [
133 | 'evaluation_date' => trim((string) ($input['evaluation_date'] ?? '')),
134 | 'evaluator_coach_id' => trim((string) ($input['evaluator_coach_id'] ?? '')),
135 | 'notes' => trim((string) ($input['notes'] ?? '')),
136 | 'scores' => (array) ($input['scores'] ?? []),
137 | ];
138 |
139 | $validation = service('validation');
140 | $validation->setRules(config('Validation')->evaluation);
141 |
142 | $errors = [];
143 |
144 | if (! $validation->run($payload)) {
145 | $errors = $validation->getErrors();
146 | }
147 |
148 | $criteriaRows = [];
149 | foreach ($criteria as $criterion) {
150 | $criterionId = (int) $criterion['id'];
151 | $value = trim((string) ($payload['scores'][$criterionId] ?? ''));
152 |
153 | if ($value === '') {
154 | $errors['score_' . $criterionId] = 'Scorul este obligatoriu.';
155 | continue;
156 | }
157 |
158 | if (! is_numeric($value)) {
159 | $errors['score_' . $criterionId] = 'Scorul trebuie sa fie numeric.';
160 | continue;
161 | }
162 |
163 | $score = (float) $value;
164 | $min = (float) $criterion['min_score'];
165 | $max = (float) $criterion['max_score'];
166 |
167 | if ($score < $min || $score > $max) {
168 | $errors['score_' . $criterionId] = 'Scorul trebuie sa fie intre ' . format_score($min, 0) . ' si ' . format_score($max, 0) . '.';
169 | continue;
170 | }
171 |
172 | $criteriaRows[] = [
173 | 'evaluation_criteria_id' => $criterionId,
174 | 'weight' => (float) $criterion['weight'],
175 | 'score' => $score,
176 | 'is_critical' => (bool) $criterion['is_critical'],
177 | 'notes' => null,
178 | ];
179 | }
180 |
181 | if ($errors !== []) {
182 | return ['success' => false, 'errors' => $errors];
183 | }
184 |
185 | $currentTrainerId = $this->authorizationService->getCoachIdForUser((int) $user->id);
186 | $evaluatorTrainerId = $this->authorizationService->userHasRole((int) $user->id, 'coach')
187 | ? $currentTrainerId
188 | : ($payload['evaluator_coach_id'] !== '' ? (int) $payload['evaluator_coach_id'] : null);
189 |
190 | $rules = $this->evaluationEngine->getActiveRuleSet();
191 | $result = $this->evaluationEngine->evaluate($criteriaRows, $rules);
192 | $signal = (int) ($type['affects_promotion'] ? $result['promotion_signal'] : false);
193 | $timestamp = date('Y-m-d H:i:s');
194 |
195 | $db = $this->db();
196 | $db->transStart();
197 |
198 | $db->table('evaluations')->insert([
199 | 'child_id' => $childId,
200 | 'evaluation_type_id' => (int) $type['id'],
201 | 'evaluation_date' => $payload['evaluation_date'],
202 | 'evaluator_user_id' => (int) $user->id,
203 | 'evaluator_coach_id' => $evaluatorTrainerId,
204 | 'level_at_time_id' => $child['academy_level_id'] !== null ? (int) $child['academy_level_id'] : null,
205 | 'final_score' => $result['final_score'],
206 | 'final_status' => $result['final_status'],
207 | 'promotion_signal' => $signal,
208 | 'notes' => $payload['notes'] !== '' ? $payload['notes'] : null,
209 | 'created_at' => $timestamp,
210 | 'updated_at' => $timestamp,
211 | ]);
212 |
213 | $evaluationId = (int) $db->insertID();
214 |
215 | foreach ($criteriaRows as $criteriaRow) {
216 | $db->table('evaluation_scores')->insert(array_merge($criteriaRow, [
217 | 'evaluation_id' => $evaluationId,
218 | ]));
219 | }
220 |
221 | $db->table('evaluation_status_history')->insert([
222 | 'evaluation_id' => $evaluationId,
223 | 'previous_status' => null,
224 | 'new_status' => $result['final_status'],
225 | 'reason' => 'Status initial calculat automat la creare.',
226 | 'changed_by' => (int) $user->id,
227 | 'created_at' => $timestamp,
228 | ]);
229 |
230 | $this->journeyService->recordEvent([
231 | 'child_id' => $childId,
232 | 'event_type' => $typeSlug === 'quick-check' ? 'quick_check_added' : 'evaluation_added',
233 | 'title' => $typeSlug === 'quick-check' ? 'Quick Check adaugat' : 'Full Evaluation adaugata',
234 | 'description' => $payload['notes'] !== '' ? $payload['notes'] : 'Evaluare noua adaugata din aplicatie.',
235 | 'metadata' => json_encode([
236 | 'evaluation_id' => $evaluationId,
237 | 'type' => $typeSlug,
238 | 'score' => $result['final_score'],
239 | 'status' => $result['final_status'],
240 | 'promotion_signal' => $signal,
241 | ], JSON_UNESCAPED_UNICODE),
242 | 'created_by' => (int) $user->id,
243 | 'created_at' => $timestamp,
244 | ]);
245 |
246 | $db->transComplete();
247 |
248 | if (! $db->transStatus()) {
249 | return ['success' => false, 'errors' => ['general' => 'Nu am putut salva evaluarea.']];
250 | }
251 |
252 | return ['success' => true, 'evaluation_id' => $evaluationId];
253 | }
254 |
255 | /**
256 | * @return array<string, mixed>|null
257 | */
258 | public function getDetailData(int $evaluationId, User $user): ?array
259 | {
260 | $evaluation = $this->db()
261 | ->table('evaluations e')
262 | ->select('e.*, children.full_name as child_name, children.id as child_id, children.status as child_status, children.primary_coach_id as child_primary_coach_id, types.name as type_name, types.slug as type_slug, levels.name as level_name, coaches.full_name as evaluator_coach_name, users.username as evaluator_username')
263 | ->join('children', 'children.id = e.child_id')
264 | ->join('evaluation_types types', 'types.id = e.evaluation_type_id')
265 | ->join('academy_levels levels', 'levels.id = e.level_at_time_id', 'left')
266 | ->join('coaches', 'coaches.id = e.evaluator_coach_id', 'left')
267 | ->join('users', 'users.id = e.evaluator_user_id', 'left')
268 | ->where('e.id', $evaluationId)
269 | ->get()
270 | ->getRowArray();
271 |
272 | if ($evaluation === null || ! $this->authorizationService->userCanAccessChild((int) $user->id, (int) $evaluation['child_id'])) {
273 | return null;
274 | }
275 |
276 | $criteriaScores = $this->db()
277 | ->table('evaluation_scores scores')
278 | ->select('scores.score, scores.weight, scores.is_critical, scores.notes, criteria.name, criteria.min_score, criteria.max_score')
279 | ->join('evaluation_criteria criteria', 'criteria.id = scores.evaluation_criteria_id')
280 | ->where('scores.evaluation_id', $evaluationId)
281 | ->orderBy('criteria.sort_order', 'ASC')
282 | ->get()
283 | ->getResultArray();
284 |
285 | $statusHistory = $this->db()
286 | ->table('evaluation_status_history history')
287 | ->select('history.*, users.username')
288 | ->join('users', 'users.id = history.changed_by', 'left')
289 | ->where('history.evaluation_id', $evaluationId)
290 | ->orderBy('history.created_at', 'DESC')
291 | ->get()
292 | ->getResultArray();
293 |
294 | return [
295 | 'pageTitle' => $evaluation['type_name'],
296 | 'pageDescription' => 'Detaliul complet al evaluarii, inclusiv criteriile punctate si istoricul statusului rezultat.',
297 | 'evaluation' => $evaluation,
298 | 'criteriaScores' => $criteriaScores,
299 | 'statusHistory' => $statusHistory,
300 | 'rules' => $this->evaluationEngine->getActiveRuleSet(),
301 | 'childProfileUrl' => url_to('children.show', $evaluation['child_id']) . '?tab=' . ($evaluation['type_slug'] === 'quick-check' ? 'quick-checks' : 'evaluations'),
302 | 'moduleUrl' => $evaluation['type_slug'] === 'quick-check' ? url_to('quickChecks.index') : url_to('evaluations.index'),
303 | ];
304 | }
305 |
306 | /**
307 | * @param array<string, mixed> $input
308 | * @return array<string, mixed>
309 | */
310 | private function buildIndexFilters(array $input, int $userId, string $typeSlug): array
311 | {
312 | $primaryRole = $this->authorizationService->getPrimaryRole($userId);
313 | $coachId = $primaryRole === 'coach' ? $this->authorizationService->getCoachIdForUser($userId) : null;
314 | $selectedPeriod = (string) ($input['period'] ?? '30');
315 | $today = new DateTimeImmutable('today');
316 | $dateStart = null;
317 | $dateEnd = null;
318 |
319 | if ($selectedPeriod === '30' || $selectedPeriod === '90' || $selectedPeriod === '7') {
320 | $days = (int) $selectedPeriod;
321 | $dateStart = $today->modify('-' . ($days - 1) . ' days')->format('Y-m-d');
322 | $dateEnd = $today->format('Y-m-d');
323 | } else {
324 | $selectedPeriod = 'all';
325 | }
326 |
327 | $selectedTrainer = $coachId ?: (int) ($input['coach_id'] ?? 0);
328 | $selectedStatus = (string) ($input['status'] ?? 'all');
329 |
330 | return [
331 | 'type_slug' => $typeSlug,
332 | 'current_role' => $primaryRole,
333 | 'coach_locked' => $primaryRole === 'coach',
334 | 'selected_coach_id' => $selectedTrainer,
335 | 'selected_status' => in_array($selectedStatus, ['all', 'READY', 'ALMOST READY', 'HOLD'], true) ? $selectedStatus : 'all',
336 | 'query' => trim((string) ($input['q'] ?? '')),
337 | 'selected_period' => $selectedPeriod,
338 | 'date_start' => $dateStart,
339 | 'date_end' => $dateEnd,
340 | 'period_options' => [
341 | '7' => 'Ultimele 7 zile',
342 | '30' => 'Ultimele 30 zile',
343 | '90' => 'Ultimele 90 zile',
344 | 'all' => 'Toata perioada',
345 | ],
346 | 'status_options' => [
347 | 'all' => 'Toate statusurile',
348 | 'READY' => 'Ready',
349 | 'ALMOST READY' => 'Almost Ready',
350 | 'HOLD' => 'Hold',
351 | ],
352 | 'coach_options' => $this->coachOptions(),
353 | ];
354 | }
355 |
356 | /**
357 | * @param array<string, mixed> $filters
358 | * @return list<array<string, mixed>>
359 | */
360 | private function getEvaluationRows(int $typeId, array $filters): array
361 | {
362 | $builder = $this->db()
363 | ->table('evaluations e')
364 | ->select('e.id, e.evaluation_date, e.final_score, e.final_status, e.promotion_signal, e.notes, children.id as child_id, children.full_name as child_name, children.status as child_status, coaches.full_name as evaluator_coach_name')
365 | ->join('children', 'children.id = e.child_id')
366 | ->join('coaches', 'coaches.id = e.evaluator_coach_id', 'left')
367 | ->where('e.evaluation_type_id', $typeId)
368 | ->orderBy('e.evaluation_date', 'DESC')
369 | ->orderBy('e.id', 'DESC');
370 |
371 | if ($filters['selected_coach_id'] > 0) {
372 | $builder->where('children.primary_coach_id', $filters['selected_coach_id']);
373 | }
374 |
375 | if ($filters['selected_status'] !== 'all') {
376 | $builder->where('e.final_status', $filters['selected_status']);
377 | }
378 |
379 | if ($filters['query'] !== '') {
380 | $builder->groupStart()
381 | ->like('children.full_name', $filters['query'])
382 | ->orLike('coaches.full_name', $filters['query'])
383 | ->groupEnd();
384 | }
385 |
386 | if ($filters['date_start'] !== null && $filters['date_end'] !== null) {
387 | $builder->where('e.evaluation_date >=', $filters['date_start'])
388 | ->where('e.evaluation_date <=', $filters['date_end']);
389 | }
390 |
391 | return $builder->get()->getResultArray();
392 | }
393 |
394 | /**
395 | * @param list<array<string, mixed>> $evaluations
396 | * @return list<array<string, mixed>>
397 | */
398 | private function buildStats(array $evaluations): array
399 | {
400 | $ready = count(array_filter($evaluations, static fn (array $row): bool => $row['final_status'] === 'READY'));
401 | $almost = count(array_filter($evaluations, static fn (array $row): bool => $row['final_status'] === 'ALMOST READY'));
402 | $avgScore = $evaluations === []
403 | ? null
404 | : round(array_sum(array_map(static fn (array $row): float => (float) $row['final_score'], $evaluations)) / count($evaluations), 2);
405 |
406 | return [
407 | ['label' => 'Total evaluari', 'value' => count($evaluations), 'meta' => 'Rezultate in filtrul curent', 'icon' => 'eval'],
408 | ['label' => 'Ready', 'value' => $ready, 'meta' => 'Status READY calculat automat', 'icon' => 'rdy'],
409 | ['label' => 'Almost Ready', 'value' => $almost, 'meta' => 'Sub pragul final, peste pragul de alerta', 'icon' => 'alm'],
410 | ['label' => 'Scor mediu', 'value' => format_score($avgScore), 'meta' => 'Media scorurilor finale in selectie', 'icon' => 'avg'],
411 | ];
412 | }
413 |
414 | /**
415 | * @return array<string, mixed>|null
416 | */
417 | private function getChildRow(int $childId): ?array
418 | {
419 | return $this->db()
420 | ->table('children c')
421 | ->select('c.id, c.full_name, c.status, c.academy_level_id, c.primary_coach_id, levels.name as level_name, groups.name as group_name, coaches.full_name as coach_name')
422 | ->join('academy_levels levels', 'levels.id = c.academy_level_id', 'left')
423 | ->join('academy_groups groups', 'groups.id = c.academy_group_id', 'left')
424 | ->join('coaches', 'coaches.id = c.primary_coach_id', 'left')
425 | ->where('c.id', $childId)
426 | ->get()
427 | ->getRowArray();
428 | }
429 |
430 | /**
431 | * @return array<string, mixed>|null
432 | */
433 | private function getEvaluationType(string $typeSlug): ?array
434 | {
435 | return $this->db()
436 | ->table('evaluation_types')
437 | ->where('slug', $typeSlug)
438 | ->where('status', 'active')
439 | ->get()
440 | ->getRowArray();
441 | }
442 |
443 | /**
444 | * @return list<array<string, mixed>>
445 | */
446 | private function getCriteriaForType(int $typeId, ?int $levelId = null): array
447 | {
448 | if ($levelId !== null) {
449 | $levelCriteria = $this->db()
450 | ->table('academy_level_criteria level_criteria')
451 | ->select('criteria.id, criteria.name, criteria.description, criteria.min_score, criteria.max_score, criteria.is_critical, level_criteria.weight, level_criteria.is_required')
452 | ->join('evaluation_criteria criteria', 'criteria.id = level_criteria.evaluation_criteria_id')
453 | ->where('level_criteria.academy_level_id', $levelId)
454 | ->where('level_criteria.evaluation_type_id', $typeId)
455 | ->where('criteria.is_active', 1)
456 | ->orderBy('level_criteria.sort_order', 'ASC')
457 | ->orderBy('criteria.id', 'ASC')
458 | ->get()
459 | ->getResultArray();
460 |
461 | if ($levelCriteria !== []) {
462 | return $levelCriteria;
463 | }
464 | }
465 |
466 | return $this->db()
467 | ->table('evaluation_type_criteria pivot')
468 | ->select('criteria.id, criteria.name, criteria.description, criteria.min_score, criteria.max_score, criteria.is_critical, pivot.weight, pivot.is_required')
469 | ->join('evaluation_criteria criteria', 'criteria.id = pivot.evaluation_criteria_id')
470 | ->where('pivot.evaluation_type_id', $typeId)
471 | ->where('criteria.is_active', 1)
472 | ->orderBy('pivot.sort_order', 'ASC')
473 | ->get()
474 | ->getResultArray();
475 | }
476 |
477 | /**
478 | * @return array<string, mixed>|null
479 | */
480 | private function getLatestEvaluationForChildAndType(int $childId, string $typeSlug): ?array
481 | {
482 | return $this->db()
483 | ->table('evaluations e')
484 | ->select('e.evaluation_date, e.final_score, e.final_status, e.notes, coaches.full_name as evaluator_name')
485 | ->join('evaluation_types types', 'types.id = e.evaluation_type_id')
486 | ->join('coaches', 'coaches.id = e.evaluator_coach_id', 'left')
487 | ->where('e.child_id', $childId)
488 | ->where('types.slug', $typeSlug)
489 | ->orderBy('e.evaluation_date', 'DESC')
490 | ->orderBy('e.id', 'DESC')
491 | ->get()
492 | ->getRowArray();
493 | }
494 |
495 | /**
496 | * @return list<array<string, mixed>>
497 | */
498 | private function coachOptions(): array
499 | {
500 | return $this->db()
501 | ->table('coaches')
502 | ->select('id, full_name')
503 | ->where('is_active', 1)
504 | ->orderBy('full_name', 'ASC')
505 | ->get()
506 | ->getResultArray();
507 | }
508 |
509 | private function db()
510 | {
511 | return db_connect();
512 | }
513 | }