Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
0.00% |
0 / 429 |
|
0.00% |
0 / 19 |
CRAP | |
0.00% |
0 / 1 |
| AcademyCatalogService | |
0.00% |
0 / 429 |
|
0.00% |
0 / 19 |
14042 | |
0.00% |
0 / 1 |
| __construct | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
| getGroupsIndexData | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
30 | |||
| getLevelsIndexData | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
12 | |||
| getGroupFormData | |
0.00% |
0 / 36 |
|
0.00% |
0 / 1 |
110 | |||
| getLevelFormData | |
0.00% |
0 / 40 |
|
0.00% |
0 / 1 |
110 | |||
| saveGroup | |
0.00% |
0 / 33 |
|
0.00% |
0 / 1 |
72 | |||
| saveLevel | |
0.00% |
0 / 46 |
|
0.00% |
0 / 1 |
156 | |||
| groupChildCounts | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
| levelStats | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
20 | |||
| levelOptions | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| coachOptions | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| evaluationTypeOptions | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
| criteriaCatalogRows | |
0.00% |
0 / 37 |
|
0.00% |
0 / 1 |
42 | |||
| normalizeCriteriaInput | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
156 | |||
| emptyCriteriaRow | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
2 | |||
| saveCriteriaCatalog | |
0.00% |
0 / 87 |
|
0.00% |
0 / 1 |
1122 | |||
| ensureLevelCriteriaInitialized | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
20 | |||
| slugify | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
| db | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Services; |
| 4 | |
| 5 | use App\Models\AcademyGroupModel; |
| 6 | use App\Models\AcademyLevelModel; |
| 7 | use CodeIgniter\Shield\Entities\User; |
| 8 | |
| 9 | class 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 | } |