Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 429
0.00% covered (danger)
0.00%
0 / 19
CRAP
0.00% covered (danger)
0.00%
0 / 1
AcademyCatalogService
0.00% covered (danger)
0.00%
0 / 429
0.00% covered (danger)
0.00%
0 / 19
14042
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getGroupsIndexData
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
30
 getLevelsIndexData
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
12
 getGroupFormData
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 1
110
 getLevelFormData
0.00% covered (danger)
0.00%
0 / 40
0.00% covered (danger)
0.00%
0 / 1
110
 saveGroup
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
72
 saveLevel
0.00% covered (danger)
0.00%
0 / 46
0.00% covered (danger)
0.00%
0 / 1
156
 groupChildCounts
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 levelStats
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 levelOptions
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 coachOptions
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 evaluationTypeOptions
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 criteriaCatalogRows
0.00% covered (danger)
0.00%
0 / 37
0.00% covered (danger)
0.00%
0 / 1
42
 normalizeCriteriaInput
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
156
 emptyCriteriaRow
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
2
 saveCriteriaCatalog
0.00% covered (danger)
0.00%
0 / 87
0.00% covered (danger)
0.00%
0 / 1
1122
 ensureLevelCriteriaInitialized
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
20
 slugify
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 db
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace App\Services;
4
5use App\Models\AcademyGroupModel;
6use App\Models\AcademyLevelModel;
7use CodeIgniter\Shield\Entities\User;
8
9class AcademyCatalogService
10{
11    private AuthorizationService $authorizationService;
12
13    public function __construct()
14    {
15        helper('academy');
16
17        $this->authorizationService = service('authorization');
18    }
19
20    /**
21     * @param array<string, mixed> $input
22     * @return array<string, mixed>
23     */
24    public function getGroupsIndexData(array $input, User $user): array
25    {
26        $role = $this->authorizationService->getPrimaryRole((int) $user->id);
27        $lockedTrainerId = $role === 'coach' ? $this->authorizationService->getCoachIdForUser((int) $user->id) : null;
28        $query = trim((string) ($input['q'] ?? ''));
29
30        $builder = $this->db()
31            ->table('academy_groups g')
32            ->select('g.*, levels.name as level_name, levels.color_hex, coaches.full_name as coach_name')
33            ->join('academy_levels levels', 'levels.id = g.academy_level_id', 'left')
34            ->join('coaches', 'coaches.id = g.primary_coach_id', 'left')
35            ->orderBy('g.name', 'ASC');
36
37        if ($lockedTrainerId !== null) {
38            $builder->where('g.primary_coach_id', $lockedTrainerId);
39        }
40
41        if ($query !== '') {
42            $builder
43                ->groupStart()
44                ->like('g.name', $query)
45                ->orLike('g.code', $query)
46                ->orLike('coaches.full_name', $query)
47                ->groupEnd();
48        }
49
50        $groups = $builder->get()->getResultArray();
51        $counts = $this->groupChildCounts(array_map(static fn (array $row): int => (int) $row['id'], $groups));
52
53        foreach ($groups as &$group) {
54            $group['active_children'] = $counts[(int) $group['id']] ?? 0;
55        }
56        unset($group);
57
58        return [
59            'pageTitle' => 'Grupe',
60            'pageDescription' => 'Structura operationala a grupelor, trainerii principali si incarcare curenta.',
61            'groups' => $groups,
62            'query' => $query,
63            'canManage' => $role === 'admin',
64        ];
65    }
66
67    /**
68     * @param array<string, mixed> $input
69     * @return array<string, mixed>
70     */
71    public function getLevelsIndexData(array $input, User $user): array
72    {
73        $query = trim((string) ($input['q'] ?? ''));
74
75        $builder = $this->db()
76            ->table('academy_levels')
77            ->orderBy('sort_order', 'ASC');
78
79        if ($query !== '') {
80            $builder
81                ->groupStart()
82                ->like('name', $query)
83                ->orLike('slug', $query)
84                ->groupEnd();
85        }
86
87        $levels = $builder->get()->getResultArray();
88        $counts = $this->levelStats(array_map(static fn (array $row): int => (int) $row['id'], $levels));
89
90        foreach ($levels as &$level) {
91            $level['children_count'] = $counts['children'][(int) $level['id']] ?? 0;
92            $level['groups_count'] = $counts['groups'][(int) $level['id']] ?? 0;
93        }
94        unset($level);
95
96        return [
97            'pageTitle' => 'Nivele academie',
98            'pageDescription' => 'Benchmarks, descrieri si distributia curenta a copiilor pe nivele.',
99            'levels' => $levels,
100            'query' => $query,
101            'canManage' => $this->authorizationService->userHasRole((int) $user->id, 'admin'),
102        ];
103    }
104
105    /**
106     * @param array<string, mixed> $input
107     * @param array<string, string> $errors
108     * @return array<string, mixed>|null
109     */
110    public function getGroupFormData(?int $groupId = null, array $input = [], array $errors = []): ?array
111    {
112        $group = $groupId !== null ? model(AcademyGroupModel::class)->find($groupId) : null;
113        if ($groupId !== null && $group === null) {
114            return null;
115        }
116
117        $values = [
118            'name' => '',
119            'code' => '',
120            'academy_level_id' => '',
121            'primary_coach_id' => '',
122            'schedule_summary' => '',
123            'status' => 'active',
124            'capacity' => '12',
125        ];
126
127        if ($group !== null) {
128            $values = [
129                'name' => $group['name'],
130                'code' => $group['code'],
131                'academy_level_id' => (string) ($group['academy_level_id'] ?? ''),
132                'primary_coach_id' => (string) ($group['primary_coach_id'] ?? ''),
133                'schedule_summary' => $group['schedule_summary'],
134                'status' => $group['status'],
135                'capacity' => (string) $group['capacity'],
136            ];
137        }
138
139        foreach ($input as $key => $value) {
140            if (array_key_exists($key, $values) && is_string($value)) {
141                $values[$key] = trim($value);
142            }
143        }
144
145        return [
146            'pageTitle' => $groupId === null ? 'Grupa noua' : 'Editare grupa',
147            'pageDescription' => 'Configureaza grupa, nivelul tinta si trainerul principal.',
148            'group' => $group,
149            'values' => $values,
150            'errors' => $errors,
151            'levelOptions' => $this->levelOptions(),
152            'coachOptions' => $this->coachOptions(),
153            'submitUrl' => $groupId === null ? url_to('groups.create') : url_to('groups.update', $groupId),
154            'cancelUrl' => url_to('groups.index'),
155        ];
156    }
157
158    /**
159     * @param array<string, mixed> $input
160     * @param array<string, string> $errors
161     * @return array<string, mixed>|null
162     */
163    public function getLevelFormData(?int $levelId = null, array $input = [], array $errors = []): ?array
164    {
165        $level = $levelId !== null ? model(AcademyLevelModel::class)->find($levelId) : null;
166        if ($levelId !== null && $level === null) {
167            return null;
168        }
169
170        $values = [
171            'name' => '',
172            'slug' => '',
173            'sort_order' => '1',
174            'color_hex' => '#4f46e5',
175            'internal_description' => '',
176            'parent_description' => '',
177            'benchmark_minimum' => '3.0',
178            'benchmark_target' => '4.0',
179            'promotion_rules' => '',
180        ];
181
182        if ($level !== null) {
183            $values = [
184                'name' => $level['name'],
185                'slug' => $level['slug'],
186                'sort_order' => (string) $level['sort_order'],
187                'color_hex' => $level['color_hex'],
188                'internal_description' => $level['internal_description'],
189                'parent_description' => $level['parent_description'],
190                'benchmark_minimum' => (string) $level['benchmark_minimum'],
191                'benchmark_target' => (string) $level['benchmark_target'],
192                'promotion_rules' => $level['promotion_rules'],
193            ];
194        }
195
196        foreach ($input as $key => $value) {
197            if (array_key_exists($key, $values) && is_string($value)) {
198                $values[$key] = trim($value);
199            }
200        }
201
202        return [
203            'pageTitle' => $levelId === null ? 'Nivel nou' : 'Editare nivel',
204            'pageDescription' => 'Ajusteaza benchmark-urile, textele si criteriile folosite in evaluari.',
205            'level' => $level,
206            'values' => $values,
207            'errors' => $errors,
208            'evaluationTypes' => $this->evaluationTypeOptions(),
209            'criteriaRows' => $this->criteriaCatalogRows($levelId, $input),
210            'submitUrl' => $levelId === null ? url_to('levels.create') : url_to('levels.update', $levelId),
211            'cancelUrl' => url_to('levels.index'),
212        ];
213    }
214
215    /**
216     * @param array<string, mixed> $input
217     * @return array{success: bool, group_id?: int, errors?: array<string, string>}
218     */
219    public function saveGroup(array $input, ?int $groupId = null): array
220    {
221        $validation = service('validation');
222        $validation->setRules(config('Validation')->academyGroup);
223
224        $payload = [
225            'name' => trim((string) ($input['name'] ?? '')),
226            'code' => trim((string) ($input['code'] ?? '')),
227            'academy_level_id' => trim((string) ($input['academy_level_id'] ?? '')),
228            'primary_coach_id' => trim((string) ($input['primary_coach_id'] ?? '')),
229            'schedule_summary' => trim((string) ($input['schedule_summary'] ?? '')),
230            'status' => trim((string) ($input['status'] ?? 'active')),
231            'capacity' => trim((string) ($input['capacity'] ?? '12')),
232        ];
233
234        if (! $validation->run($payload)) {
235            return ['success' => false, 'errors' => $validation->getErrors()];
236        }
237
238        $duplicate = $this->db()->table('academy_groups')->select('id')->where('LOWER(code)', strtolower($payload['code']))->get()->getRowArray();
239        if ($duplicate !== null && (int) $duplicate['id'] !== (int) $groupId) {
240            return ['success' => false, 'errors' => ['code' => 'Codul grupei este deja folosit.']];
241        }
242
243        $data = [
244            'name' => $payload['name'],
245            'code' => $payload['code'],
246            'academy_level_id' => $payload['academy_level_id'] !== '' ? (int) $payload['academy_level_id'] : null,
247            'primary_coach_id' => $payload['primary_coach_id'] !== '' ? (int) $payload['primary_coach_id'] : null,
248            'schedule_summary' => $payload['schedule_summary'] !== '' ? $payload['schedule_summary'] : null,
249            'status' => $payload['status'],
250            'capacity' => (int) $payload['capacity'],
251            'updated_at' => date('Y-m-d H:i:s'),
252        ];
253
254        $model = model(AcademyGroupModel::class);
255
256        if ($groupId === null) {
257            $data['created_at'] = $data['updated_at'];
258            $model->insert($data);
259            $groupId = (int) $model->getInsertID();
260        } else {
261            $model->update($groupId, $data);
262        }
263
264        return ['success' => true, 'group_id' => $groupId];
265    }
266
267    /**
268     * @param array<string, mixed> $input
269     * @return array{success: bool, level_id?: int, errors?: array<string, string>}
270     */
271    public function saveLevel(array $input, ?int $levelId = null): array
272    {
273        $validation = service('validation');
274        $validation->setRules(config('Validation')->academyLevel);
275
276        $payload = [
277            'name' => trim((string) ($input['name'] ?? '')),
278            'slug' => trim((string) ($input['slug'] ?? '')),
279            'sort_order' => trim((string) ($input['sort_order'] ?? '1')),
280            'color_hex' => trim((string) ($input['color_hex'] ?? '#4f46e5')),
281            'internal_description' => trim((string) ($input['internal_description'] ?? '')),
282            'parent_description' => trim((string) ($input['parent_description'] ?? '')),
283            'benchmark_minimum' => trim((string) ($input['benchmark_minimum'] ?? '3.0')),
284            'benchmark_target' => trim((string) ($input['benchmark_target'] ?? '4.0')),
285            'promotion_rules' => trim((string) ($input['promotion_rules'] ?? '')),
286        ];
287
288        if (! $validation->run($payload)) {
289            return ['success' => false, 'errors' => $validation->getErrors()];
290        }
291
292        $duplicate = $this->db()->table('academy_levels')->select('id')->where('LOWER(slug)', strtolower($payload['slug']))->get()->getRowArray();
293        if ($duplicate !== null && (int) $duplicate['id'] !== (int) $levelId) {
294            return ['success' => false, 'errors' => ['slug' => 'Slug-ul nivelului este deja folosit.']];
295        }
296
297        $data = [
298            'name' => $payload['name'],
299            'slug' => $payload['slug'],
300            'sort_order' => (int) $payload['sort_order'],
301            'color_hex' => $payload['color_hex'] !== '' ? $payload['color_hex'] : null,
302            'internal_description' => $payload['internal_description'] !== '' ? $payload['internal_description'] : null,
303            'parent_description' => $payload['parent_description'] !== '' ? $payload['parent_description'] : null,
304            'benchmark_minimum' => $payload['benchmark_minimum'] !== '' ? $payload['benchmark_minimum'] : null,
305            'benchmark_target' => $payload['benchmark_target'] !== '' ? $payload['benchmark_target'] : null,
306            'promotion_rules' => $payload['promotion_rules'] !== '' ? $payload['promotion_rules'] : null,
307            'updated_at' => date('Y-m-d H:i:s'),
308        ];
309
310        $model = model(AcademyLevelModel::class);
311
312        $this->db()->transBegin();
313
314        if ($levelId === null) {
315            $data['created_at'] = $data['updated_at'];
316            $model->insert($data);
317            $levelId = (int) $model->getInsertID();
318        } else {
319            $model->update($levelId, $data);
320        }
321
322        $criteriaResult = $this->saveCriteriaCatalog($levelId, (array) ($input['criteria'] ?? []));
323        if (! $criteriaResult['success']) {
324            $this->db()->transRollback();
325
326            return [
327                'success' => false,
328                'errors' => $criteriaResult['errors'],
329            ];
330        }
331
332        $this->db()->transCommit();
333
334        return ['success' => true, 'level_id' => $levelId];
335    }
336
337    /**
338     * @param list<int> $groupIds
339     * @return array<int, int>
340     */
341    private function groupChildCounts(array $groupIds): array
342    {
343        $counts = [];
344        if ($groupIds === []) {
345            return $counts;
346        }
347
348        foreach ($this->db()->table('children')->select('academy_group_id, COUNT(*) as total')->where('status', 'active')->whereIn('academy_group_id', $groupIds)->groupBy('academy_group_id')->get()->getResultArray() as $row) {
349            $counts[(int) $row['academy_group_id']] = (int) $row['total'];
350        }
351
352        return $counts;
353    }
354
355    /**
356     * @param list<int> $levelIds
357     * @return array<string, array<int, int>>
358     */
359    private function levelStats(array $levelIds): array
360    {
361        $children = [];
362        $groups   = [];
363
364        if ($levelIds === []) {
365            return compact('children', 'groups');
366        }
367
368        foreach ($this->db()->table('children')->select('academy_level_id, COUNT(*) as total')->where('status !=', 'left')->whereIn('academy_level_id', $levelIds)->groupBy('academy_level_id')->get()->getResultArray() as $row) {
369            $children[(int) $row['academy_level_id']] = (int) $row['total'];
370        }
371
372        foreach ($this->db()->table('academy_groups')->select('academy_level_id, COUNT(*) as total')->whereIn('academy_level_id', $levelIds)->groupBy('academy_level_id')->get()->getResultArray() as $row) {
373            $groups[(int) $row['academy_level_id']] = (int) $row['total'];
374        }
375
376        return compact('children', 'groups');
377    }
378
379    /**
380     * @return list<array<string, mixed>>
381     */
382    private function levelOptions(): array
383    {
384        return $this->db()->table('academy_levels')->select('id, name')->orderBy('sort_order', 'ASC')->get()->getResultArray();
385    }
386
387    /**
388     * @return list<array<string, mixed>>
389     */
390    private function coachOptions(): array
391    {
392        return $this->db()->table('coaches')->select('id, full_name')->where('is_active', 1)->orderBy('full_name', 'ASC')->get()->getResultArray();
393    }
394
395    /**
396     * @return list<array<string, mixed>>
397     */
398    private function evaluationTypeOptions(): array
399    {
400        return $this->db()
401            ->table('evaluation_types')
402            ->select('id, name, slug')
403            ->where('status', 'active')
404            ->orderBy('is_quick_check', 'DESC')
405            ->orderBy('id', 'ASC')
406            ->get()
407            ->getResultArray();
408    }
409
410    /**
411     * @param array<string, mixed> $input
412     * @return list<array<string, mixed>>
413     */
414    private function criteriaCatalogRows(?int $levelId, array $input = []): array
415    {
416        if (isset($input['criteria']) && is_array($input['criteria'])) {
417            return $this->normalizeCriteriaInput($input['criteria'], true);
418        }
419
420        if ($levelId === null) {
421            return [$this->emptyCriteriaRow()];
422        }
423
424        $this->ensureLevelCriteriaInitialized($levelId);
425
426        $criteria = $this->db()
427            ->table('academy_level_criteria level_criteria')
428            ->select('criteria.*, level_criteria.weight as level_weight, level_criteria.sort_order as level_sort_order, types.slug as type_slug')
429            ->join('evaluation_criteria criteria', 'criteria.id = level_criteria.evaluation_criteria_id')
430            ->join('evaluation_types types', 'types.id = level_criteria.evaluation_type_id')
431            ->where('level_criteria.academy_level_id', $levelId)
432            ->orderBy('level_criteria.sort_order', 'ASC')
433            ->orderBy('criteria.id', 'ASC')
434            ->get()
435            ->getResultArray();
436
437        $rowsByCriterion = [];
438        foreach ($criteria as $row) {
439            $criterionId = (int) $row['id'];
440            if (! isset($rowsByCriterion[$criterionId])) {
441                $rowsByCriterion[$criterionId] = [
442                    'id' => (string) $criterionId,
443                    'name' => (string) $row['name'],
444                    'slug' => (string) $row['slug'],
445                    'description' => (string) ($row['description'] ?? ''),
446                    'default_weight' => (string) $row['level_weight'],
447                    'sort_order' => (string) $row['level_sort_order'],
448                    'min_score' => (string) $row['min_score'],
449                    'max_score' => (string) $row['max_score'],
450                    'is_active' => (string) ((int) $row['is_active']),
451                    'is_critical' => (string) ((int) $row['is_critical']),
452                    'delete' => '0',
453                    'types' => [],
454                ];
455            }
456
457            $rowsByCriterion[$criterionId]['types'][] = (string) $row['type_slug'];
458        }
459
460        $rows = array_values($rowsByCriterion);
461        $rows[] = $this->emptyCriteriaRow();
462
463        return $rows;
464    }
465
466    /**
467     * @param array<mixed> $input
468     * @return list<array<string, mixed>>
469     */
470    private function normalizeCriteriaInput(array $input, bool $includeBlank = false): array
471    {
472        $rows = [];
473
474        foreach ($input as $row) {
475            if (! is_array($row)) {
476                continue;
477            }
478
479            $normalized = [
480                'id' => trim((string) ($row['id'] ?? '')),
481                'name' => trim((string) ($row['name'] ?? '')),
482                'slug' => trim((string) ($row['slug'] ?? '')),
483                'description' => trim((string) ($row['description'] ?? '')),
484                'default_weight' => trim((string) ($row['default_weight'] ?? '1.0')),
485                'sort_order' => trim((string) ($row['sort_order'] ?? '1')),
486                'min_score' => trim((string) ($row['min_score'] ?? '1')),
487                'max_score' => trim((string) ($row['max_score'] ?? '5')),
488                'is_active' => isset($row['is_active']) ? '1' : '0',
489                'is_critical' => isset($row['is_critical']) ? '1' : '0',
490                'delete' => isset($row['delete']) ? '1' : '0',
491                'types' => array_values(array_filter((array) ($row['types'] ?? []), static fn ($type): bool => is_string($type) && $type !== '')),
492            ];
493
494            if ($normalized['id'] === '' && $normalized['name'] === '' && $normalized['slug'] === '' && ! $includeBlank) {
495                continue;
496            }
497
498            $rows[] = $normalized;
499        }
500
501        if ($includeBlank) {
502            $rows[] = $this->emptyCriteriaRow();
503        }
504
505        return $rows;
506    }
507
508    /**
509     * @return array<string, mixed>
510     */
511    private function emptyCriteriaRow(): array
512    {
513        return [
514            'id' => '',
515            'name' => '',
516            'slug' => '',
517            'description' => '',
518            'default_weight' => '1.0',
519            'sort_order' => '10',
520            'min_score' => '1',
521            'max_score' => '5',
522            'is_active' => '1',
523            'is_critical' => '0',
524            'delete' => '0',
525            'types' => ['quick-check', 'full-evaluation'],
526        ];
527    }
528
529    /**
530     * @param array<mixed> $input
531     * @return array{success: bool, errors?: array<string, string>}
532     */
533    private function saveCriteriaCatalog(int $levelId, array $input): array
534    {
535        if ($input === []) {
536            return ['success' => true];
537        }
538
539        $rows = $this->normalizeCriteriaInput($input);
540        $errors = [];
541        $now = date('Y-m-d H:i:s');
542        $typeRows = $this->evaluationTypeOptions();
543        $typeIdsBySlug = [];
544        foreach ($typeRows as $type) {
545            $typeIdsBySlug[(string) $type['slug']] = (int) $type['id'];
546        }
547
548        foreach ($rows as $index => $row) {
549            $fieldPrefix = 'criteria_' . $index . '_';
550            $criterionId = $row['id'] !== '' ? (int) $row['id'] : null;
551
552            if ($row['delete'] === '1' && $criterionId !== null) {
553                $this->db()
554                    ->table('academy_level_criteria')
555                    ->where('academy_level_id', $levelId)
556                    ->where('evaluation_criteria_id', $criterionId)
557                    ->delete();
558                continue;
559            }
560
561            if ($criterionId === null && $row['name'] === '' && $row['slug'] === '') {
562                continue;
563            }
564
565            if ($row['name'] === '') {
566                $errors[$fieldPrefix . 'name'] = 'Numele intrebarii este obligatoriu.';
567            }
568
569            $slug = $row['slug'] !== '' ? strtolower($row['slug']) : $this->slugify($row['name']);
570            if ($slug === '') {
571                $errors[$fieldPrefix . 'slug'] = 'Slug-ul este obligatoriu.';
572            }
573
574            if (! preg_match('/^[a-z0-9-]+$/', $slug)) {
575                $errors[$fieldPrefix . 'slug'] = 'Slug-ul poate contine doar litere mici, cifre si cratime.';
576            }
577
578            $sortOrder = filter_var($row['sort_order'], FILTER_VALIDATE_INT);
579            if ($sortOrder === false || $sortOrder < 1) {
580                $errors[$fieldPrefix . 'sort_order'] = 'Ordinea trebuie sa fie un numar pozitiv.';
581            }
582
583            $weight = filter_var($row['default_weight'], FILTER_VALIDATE_FLOAT);
584            if ($weight === false || $weight <= 0) {
585                $errors[$fieldPrefix . 'default_weight'] = 'Ponderea trebuie sa fie mai mare decat 0.';
586            }
587
588            $minScore = filter_var($row['min_score'], FILTER_VALIDATE_FLOAT);
589            $maxScore = filter_var($row['max_score'], FILTER_VALIDATE_FLOAT);
590            if ($minScore === false || $maxScore === false || $minScore < 1 || $maxScore > 5 || $minScore >= $maxScore) {
591                $errors[$fieldPrefix . 'score_range'] = 'Scara trebuie sa fie intre 1 si 5, cu minimul mai mic decat maximul.';
592            }
593
594            if ($row['types'] === []) {
595                $errors[$fieldPrefix . 'types'] = 'Alege cel putin un tip de evaluare.';
596            }
597
598            foreach ($row['types'] as $typeSlug) {
599                if (! isset($typeIdsBySlug[$typeSlug])) {
600                    $errors[$fieldPrefix . 'types'] = 'Tip de evaluare invalid.';
601                }
602            }
603
604            $duplicate = $this->db()
605                ->table('evaluation_criteria')
606                ->select('id')
607                ->where('LOWER(slug)', strtolower($slug))
608                ->get()
609                ->getRowArray();
610
611            if ($duplicate !== null && (int) $duplicate['id'] !== (int) ($criterionId ?? 0)) {
612                $errors[$fieldPrefix . 'slug'] = 'Slug-ul criteriului este deja folosit.';
613            }
614
615            if ($errors !== []) {
616                continue;
617            }
618
619            $data = [
620                'name' => $row['name'],
621                'slug' => $slug,
622                'description' => $row['description'] !== '' ? $row['description'] : null,
623                'default_weight' => (float) $weight,
624                'is_critical' => (int) $row['is_critical'],
625                'sort_order' => (int) $sortOrder,
626                'min_score' => (float) $minScore,
627                'max_score' => (float) $maxScore,
628                'is_active' => (int) $row['is_active'],
629                'updated_at' => $now,
630            ];
631
632            if ($criterionId === null) {
633                $data['created_at'] = $now;
634                $this->db()->table('evaluation_criteria')->insert($data);
635                $criterionId = (int) $this->db()->insertID();
636            } else {
637                $this->db()->table('evaluation_criteria')->where('id', $criterionId)->update($data);
638            }
639
640            $this->db()
641                ->table('academy_level_criteria')
642                ->where('academy_level_id', $levelId)
643                ->where('evaluation_criteria_id', $criterionId)
644                ->delete();
645            foreach ($row['types'] as $typeSlug) {
646                $this->db()->table('academy_level_criteria')->insert([
647                    'academy_level_id' => $levelId,
648                    'evaluation_type_id' => $typeIdsBySlug[$typeSlug],
649                    'evaluation_criteria_id' => $criterionId,
650                    'weight' => (float) $weight,
651                    'is_required' => 1,
652                    'sort_order' => (int) $sortOrder,
653                ]);
654            }
655        }
656
657        if ($errors !== []) {
658            return ['success' => false, 'errors' => $errors];
659        }
660
661        return ['success' => true];
662    }
663
664    private function ensureLevelCriteriaInitialized(int $levelId): void
665    {
666        $existing = $this->db()
667            ->table('academy_level_criteria')
668            ->select('id')
669            ->where('academy_level_id', $levelId)
670            ->limit(1)
671            ->get()
672            ->getRowArray();
673
674        if ($existing !== null) {
675            return;
676        }
677
678        $globalRows = $this->db()
679            ->table('evaluation_type_criteria')
680            ->get()
681            ->getResultArray();
682
683        if ($globalRows === []) {
684            return;
685        }
686
687        $rows = [];
688        foreach ($globalRows as $row) {
689            $rows[] = [
690                'academy_level_id' => $levelId,
691                'evaluation_type_id' => (int) $row['evaluation_type_id'],
692                'evaluation_criteria_id' => (int) $row['evaluation_criteria_id'],
693                'weight' => $row['weight'],
694                'is_required' => (int) $row['is_required'],
695                'sort_order' => (int) $row['sort_order'],
696            ];
697        }
698
699        $this->db()->table('academy_level_criteria')->insertBatch($rows);
700    }
701
702    private function slugify(string $value): string
703    {
704        $value = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $value) ?: $value;
705        $value = strtolower($value);
706        $value = preg_replace('/[^a-z0-9]+/', '-', $value) ?? '';
707        $value = trim($value, '-');
708
709        return $value;
710    }
711
712    private function db()
713    {
714        return db_connect();
715    }
716}