Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
99.47% covered (success)
99.47%
753 / 757
88.00% covered (warning)
88.00%
22 / 25
CRAP
0.00% covered (danger)
0.00%
0 / 1
AcademiaSeeder
99.47% covered (success)
99.47%
753 / 757
88.00% covered (warning)
88.00%
22 / 25
75
0.00% covered (danger)
0.00%
0 / 1
 run
94.74% covered (success)
94.74%
18 / 19
0.00% covered (danger)
0.00%
0 / 1
2.00
 seedRoles
94.12% covered (success)
94.12%
32 / 34
0.00% covered (danger)
0.00%
0 / 1
4.00
 seedSettings
100.00% covered (success)
100.00%
56 / 56
100.00% covered (success)
100.00%
1 / 1
1
 seedEvaluationRules
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
1
 seedLevels
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
2
 seedEvaluationCatalog
100.00% covered (success)
100.00%
73 / 73
100.00% covered (success)
100.00%
1 / 1
5
 seedUsers
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 seedCoaches
100.00% covered (success)
100.00%
36 / 36
100.00% covered (success)
100.00%
1 / 1
2
 seedCoachAvailability
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
3
 seedGroups
100.00% covered (success)
100.00%
38 / 38
100.00% covered (success)
100.00%
1 / 1
2
 seedParents
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
3
 seedChildren
100.00% covered (success)
100.00%
60 / 60
100.00% covered (success)
100.00%
1 / 1
2
 seedAttendance
100.00% covered (success)
100.00%
64 / 64
100.00% covered (success)
100.00%
1 / 1
23
 seedEvaluations
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 seedNotifications
100.00% covered (success)
100.00%
48 / 48
100.00% covered (success)
100.00%
1 / 1
1
 createUser
94.12% covered (success)
94.12%
16 / 17
0.00% covered (danger)
0.00%
0 / 1
2.00
 createEvaluation
100.00% covered (success)
100.00%
57 / 57
100.00% covered (success)
100.00%
1 / 1
6
 childDefinitions
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
1
 childDefinition
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
1
 attendancePatterns
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
1
 evaluationPlan
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
1
 standardPlan
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
1
 scoreTemplates
100.00% covered (success)
100.00%
68 / 68
100.00% covered (success)
100.00%
1 / 1
1
 groupCoachKey
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 childAttendsSession
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
5
1<?php
2
3namespace App\Database\Seeds;
4
5use App\Models\AcademyGroupModel;
6use App\Models\AcademyLevelModel;
7use App\Models\AttendanceRecordModel;
8use App\Models\AttendanceSessionModel;
9use App\Models\ChildCoachModel;
10use App\Models\ChildGuardianModel;
11use App\Models\ChildModel;
12use App\Models\CoachAvailabilityModel;
13use App\Models\CoachModel;
14use App\Models\EvaluationCriteriaModel;
15use App\Models\EvaluationModel;
16use App\Models\EvaluationRuleModel;
17use App\Models\EvaluationScoreModel;
18use App\Models\EvaluationStatusHistoryModel;
19use App\Models\EvaluationTypeModel;
20use App\Models\GuardianModel;
21use App\Models\NotificationModel;
22use App\Models\SettingModel;
23use App\Models\UserRoleModel;
24use CodeIgniter\Database\Seeder;
25use CodeIgniter\Shield\Entities\User;
26use CodeIgniter\Shield\Models\UserModel;
27use RuntimeException;
28
29class AcademiaSeeder extends Seeder
30{
31    private string $defaultPassword = 'Academia123!';
32
33    private UserModel $userModel;
34
35    /** @var array<string, int> */
36    private array $roleIds = [];
37
38    /** @var array<string, int> */
39    private array $userIds = [];
40
41    /** @var array<string, int> */
42    private array $coachIds = [];
43
44    /** @var array<string, int> */
45    private array $groupIds = [];
46
47    /** @var array<string, int> */
48    private array $levelIds = [];
49
50    /** @var array<string, int> */
51    private array $guardianIds = [];
52
53    /** @var array<string, array<string, mixed>> */
54    private array $children = [];
55
56    /** @var array<string, int> */
57    private array $evaluationTypeIds = [];
58
59    /** @var array<string, array<string, mixed>> */
60    private array $criteria = [];
61
62    /** @var array<string, list<string>> */
63    private array $criteriaByType = [];
64
65    /** @var array<string, float|int|string|bool|null> */
66    private array $rule = [];
67
68    public function run(): void
69    {
70        $this->userModel = model(UserModel::class);
71
72        $this->db->transStart();
73
74        $this->seedRoles();
75        $this->seedSettings();
76        $this->seedEvaluationRules();
77        $this->seedLevels();
78        $this->seedEvaluationCatalog();
79        $this->seedUsers();
80        $this->seedCoaches();
81        $this->seedCoachAvailability();
82        $this->seedGroups();
83        $this->seedParents();
84        $this->seedChildren();
85        $this->seedAttendance();
86        $this->seedEvaluations();
87        $this->seedNotifications();
88
89        $this->db->transComplete();
90
91        if (! $this->db->transStatus()) {
92            throw new RuntimeException('Academia seed failed: ' . json_encode($this->db->error()));
93        }
94    }
95
96    private function seedRoles(): void
97    {
98        $now = date('Y-m-d H:i:s');
99
100        $inserted = $this->db->table('roles')->insertBatch([
101            [
102                'slug' => 'admin',
103                'name' => 'Admin',
104                'description' => 'Acces complet in platforma.',
105                'priority' => 1,
106                'created_at' => $now,
107                'updated_at' => $now,
108            ],
109            [
110                'slug' => 'coach',
111                'name' => 'Trainer',
112                'description' => 'Acces la copii, evaluari si prezenta pentru grupele proprii de inot.',
113                'priority' => 2,
114                'created_at' => $now,
115                'updated_at' => $now,
116            ],
117            [
118                'slug' => 'parent',
119                'name' => 'Parent',
120                'description' => 'Rol limitat, pregatit pentru fazele viitoare.',
121                'priority' => 3,
122                'created_at' => $now,
123                'updated_at' => $now,
124            ],
125        ]);
126
127        if ($inserted === false) {
128            throw new RuntimeException('Unable to seed roles: ' . json_encode($this->db->error()));
129        }
130
131        $query = $this->db->table('roles')->get();
132        if ($query === false) {
133            throw new RuntimeException('Unable to read roles after seed: ' . json_encode($this->db->error()));
134        }
135
136        foreach ($query->getResultArray() as $row) {
137            $this->roleIds[$row['slug']] = (int) $row['id'];
138        }
139    }
140
141    private function seedSettings(): void
142    {
143        $model = model(SettingModel::class);
144        $now   = date('Y-m-d H:i:s');
145
146        $model->insertBatch([
147            [
148                'class' => 'Academia',
149                'key' => 'academy_name',
150                'value' => 'Splash Academy',
151                'type' => 'string',
152                'context' => 'general',
153                'label' => 'Academy Name',
154                'group_name' => 'general',
155                'description' => 'Numele academiei afisat in layout.',
156                'is_public' => 1,
157                'created_at' => $now,
158                'updated_at' => $now,
159            ],
160            [
161                'class' => 'Academia',
162                'key' => 'default_dashboard_period',
163                'value' => '30',
164                'type' => 'integer',
165                'context' => 'dashboard',
166                'label' => 'Default Dashboard Period',
167                'group_name' => 'dashboard',
168                'description' => 'Perioada implicita pentru filtrele dashboardului.',
169                'is_public' => 0,
170                'created_at' => $now,
171                'updated_at' => $now,
172            ],
173            [
174                'class' => 'Academia',
175                'key' => 'no_evaluation_days',
176                'value' => '45',
177                'type' => 'integer',
178                'context' => 'dashboard',
179                'label' => 'No Evaluation Days Threshold',
180                'group_name' => 'dashboard',
181                'description' => 'Numarul de zile dupa care copilul intra in alerta fara evaluare.',
182                'is_public' => 0,
183                'created_at' => $now,
184                'updated_at' => $now,
185            ],
186            [
187                'class' => 'Academia',
188                'key' => 'absence_alert_threshold',
189                'value' => '25',
190                'type' => 'integer',
191                'context' => 'dashboard',
192                'label' => 'Absence Alert Threshold',
193                'group_name' => 'dashboard',
194                'description' => 'Procent de absenta peste care este generata alerta.',
195                'is_public' => 0,
196                'created_at' => $now,
197                'updated_at' => $now,
198            ],
199        ]);
200    }
201
202    private function seedEvaluationRules(): void
203    {
204        $model = model(EvaluationRuleModel::class);
205        $now   = date('Y-m-d H:i:s');
206
207        $this->rule = [
208            'name' => 'Global Default Rule',
209            'scope_type' => 'global',
210            'scope_id' => null,
211            'almost_ready_threshold' => 3.5,
212            'ready_threshold' => 4.0,
213            'minimum_critical_threshold' => 3.0,
214            'is_active' => 1,
215            'notes' => 'Pragurile implicite pentru scorare si semnal de promovare.',
216            'created_at' => $now,
217            'updated_at' => $now,
218        ];
219
220        $model->insert($this->rule);
221    }
222
223    private function seedLevels(): void
224    {
225        $model = model(AcademyLevelModel::class);
226        $now   = date('Y-m-d H:i:s');
227
228        $levels = [
229            ['key' => 'delfinas', 'name' => 'DelfinaÈ™ (IniÈ›iere)', 'slug' => 'delfinas-initiere', 'sort_order' => 1, 'color_hex' => '#0ea5e9'],
230            ['key' => 'rechin', 'name' => 'Rechin (Avansat)', 'slug' => 'rechin-avansat', 'sort_order' => 2, 'color_hex' => '#0284c7'],
231            ['key' => 'pre_performanta', 'name' => 'Pre-Performanță', 'slug' => 'pre-performanta', 'sort_order' => 3, 'color_hex' => '#14b8a6'],
232            ['key' => 'sportiv_performanta', 'name' => 'Sportiv de Performanță', 'slug' => 'sportiv-de-performanta', 'sort_order' => 4, 'color_hex' => '#f59e0b'],
233        ];
234
235        foreach ($levels as $level) {
236            $model->insert([
237                'name' => $level['name'],
238                'slug' => $level['slug'],
239                'sort_order' => $level['sort_order'],
240                'color_hex' => $level['color_hex'],
241                'internal_description' => 'Nivel intern pentru cursurile de inot: ' . $level['name'] . '.',
242                'parent_description' => 'Nivelul ' . $level['name'] . ' marcheaza progresul copilului in programul de inot.',
243                'benchmark_minimum' => 3.0,
244                'benchmark_target' => 4.0,
245                'promotion_rules' => 'Necesita evaluare full cu status READY si criterii critice peste prag.',
246                'created_at' => $now,
247                'updated_at' => $now,
248            ]);
249
250            $this->levelIds[$level['key']] = (int) $model->getInsertID();
251        }
252    }
253
254    private function seedEvaluationCatalog(): void
255    {
256        $typeModel     = model(EvaluationTypeModel::class);
257        $criteriaModel = model(EvaluationCriteriaModel::class);
258        $now           = date('Y-m-d H:i:s');
259
260        $types = [
261            [
262                'key' => 'quick-check',
263                'name' => 'Quick Check',
264                'description' => 'Monitorizare rapida pentru progresul saptamanal la inot.',
265                'is_quick_check' => 1,
266                'affects_promotion' => 0,
267            ],
268            [
269                'key' => 'full-evaluation',
270                'name' => 'Full Evaluation',
271                'description' => 'Evaluare completa folosita pentru decizii de promovare in programul de inot.',
272                'is_quick_check' => 0,
273                'affects_promotion' => 1,
274            ],
275        ];
276
277        foreach ($types as $type) {
278            $typeModel->insert([
279                'name' => $type['name'],
280                'slug' => $type['key'],
281                'description' => $type['description'],
282                'is_quick_check' => $type['is_quick_check'],
283                'affects_promotion' => $type['affects_promotion'],
284                'status' => 'active',
285                'created_at' => $now,
286                'updated_at' => $now,
287            ]);
288
289            $this->evaluationTypeIds[$type['key']] = (int) $typeModel->getInsertID();
290        }
291
292        $criteria = [
293            ['slug' => 'water-confidence', 'name' => 'Water Confidence', 'weight' => 1.2, 'critical' => 1, 'sort_order' => 1],
294            ['slug' => 'breathing-control', 'name' => 'Breathing Control', 'weight' => 1.2, 'critical' => 1, 'sort_order' => 2],
295            ['slug' => 'stroke-technique', 'name' => 'Stroke Technique', 'weight' => 1.4, 'critical' => 1, 'sort_order' => 3],
296            ['slug' => 'start-turn-basics', 'name' => 'Start & Turn Basics', 'weight' => 0.9, 'critical' => 0, 'sort_order' => 4],
297            ['slug' => 'race-awareness', 'name' => 'Race Awareness', 'weight' => 0.7, 'critical' => 0, 'sort_order' => 5],
298            ['slug' => 'focus-effort', 'name' => 'Focus & Effort', 'weight' => 0.8, 'critical' => 0, 'sort_order' => 6],
299        ];
300
301        foreach ($criteria as $criterion) {
302            $criteriaModel->insert([
303                'name' => $criterion['name'],
304                'slug' => $criterion['slug'],
305                'description' => $criterion['name'] . ' in contextul evaluarii academiei.',
306                'is_active' => 1,
307                'default_weight' => $criterion['weight'],
308                'is_critical' => $criterion['critical'],
309                'sort_order' => $criterion['sort_order'],
310                'min_score' => 1,
311                'max_score' => 5,
312                'created_at' => $now,
313                'updated_at' => $now,
314            ]);
315
316            $this->criteria[$criterion['slug']] = [
317                'id' => (int) $criteriaModel->getInsertID(),
318                'weight' => (float) $criterion['weight'],
319                'is_critical' => (bool) $criterion['critical'],
320            ];
321        }
322
323        $this->criteriaByType = [
324            'quick-check' => ['water-confidence', 'breathing-control', 'stroke-technique', 'focus-effort'],
325            'full-evaluation' => ['water-confidence', 'breathing-control', 'stroke-technique', 'start-turn-basics', 'race-awareness', 'focus-effort'],
326        ];
327
328        $pivotRows = [];
329        foreach ($this->criteriaByType as $typeSlug => $criteriaSlugs) {
330            foreach ($criteriaSlugs as $index => $criterionSlug) {
331                $pivotRows[] = [
332                    'evaluation_type_id' => $this->evaluationTypeIds[$typeSlug],
333                    'evaluation_criteria_id' => $this->criteria[$criterionSlug]['id'],
334                    'weight' => $this->criteria[$criterionSlug]['weight'],
335                    'is_required' => 1,
336                    'sort_order' => $index + 1,
337                ];
338            }
339        }
340
341        $this->db->table('evaluation_type_criteria')->insertBatch($pivotRows);
342    }
343
344    private function seedUsers(): void
345    {
346        $this->userIds['admin'] = $this->createUser('admin', 'admin@academia.local', 'admin', 'admin');
347        $this->userIds['coach_andrei'] = $this->createUser('andrei.popescu', 'andrei@academia.local', 'coach', 'coach');
348        $this->userIds['coach_ioana'] = $this->createUser('ioana.dumitrescu', 'ioana@academia.local', 'coach', 'coach');
349        $this->userIds['coach_mihai'] = $this->createUser('mihai.ionescu', 'mihai@academia.local', 'coach', 'coach');
350        $this->userIds['parent_cristina'] = $this->createUser('cristina.marin', 'cristina@academia.local', 'parent', 'parent');
351    }
352
353    private function seedCoaches(): void
354    {
355        $model = model(CoachModel::class);
356        $now   = date('Y-m-d H:i:s');
357
358        $coaches = [
359            'coach_andrei' => [
360                'full_name' => 'Andrei Popescu',
361                'display_role' => 'Head Trainer',
362                'phone' => '0711 111 111',
363                'email' => 'andrei@academia.local',
364            ],
365            'coach_ioana' => [
366                'full_name' => 'Ioana Dumitrescu',
367                'display_role' => 'Trainer',
368                'phone' => '0722 222 222',
369                'email' => 'ioana@academia.local',
370            ],
371            'coach_mihai' => [
372                'full_name' => 'Mihai Ionescu',
373                'display_role' => 'Assistant Trainer',
374                'phone' => '0733 333 333',
375                'email' => 'mihai@academia.local',
376            ],
377        ];
378
379        foreach ($coaches as $key => $coach) {
380            $model->insert([
381                'user_id' => $this->userIds[$key],
382                'full_name' => $coach['full_name'],
383                'display_role' => $coach['display_role'],
384                'phone' => $coach['phone'],
385                'email' => $coach['email'],
386                'avatar_path' => null,
387                'is_active' => 1,
388                'notes' => 'Profil initial seed-uit pentru Faza 1.',
389                'created_at' => $now,
390                'updated_at' => $now,
391            ]);
392
393            $this->coachIds[$key] = (int) $model->getInsertID();
394        }
395    }
396
397    private function seedCoachAvailability(): void
398    {
399        $model = model(CoachAvailabilityModel::class);
400        $now   = date('Y-m-d H:i:s');
401
402        $slots = [
403            'coach_andrei' => [[1, '16:00:00', '19:00:00'], [3, '16:00:00', '19:00:00'], [5, '16:00:00', '18:30:00']],
404            'coach_ioana' => [[2, '16:00:00', '19:00:00'], [4, '16:00:00', '19:00:00'], [6, '10:00:00', '13:00:00']],
405            'coach_mihai' => [[1, '17:00:00', '20:00:00'], [4, '17:00:00', '20:00:00'], [6, '09:00:00', '12:00:00']],
406        ];
407
408        foreach ($slots as $coachKey => $entries) {
409            foreach ($entries as [$dayOfWeek, $start, $end]) {
410                $model->insert([
411                    'coach_id' => $this->coachIds[$coachKey],
412                    'day_of_week' => $dayOfWeek,
413                    'start_time' => $start,
414                    'end_time' => $end,
415                    'is_available' => 1,
416                    'created_at' => $now,
417                ]);
418            }
419        }
420    }
421
422    private function seedGroups(): void
423    {
424        $model = model(AcademyGroupModel::class);
425        $now   = date('Y-m-d H:i:s');
426
427        $groups = [
428            'red_sparks' => [
429                'name' => 'Delfinas Initiere',
430                'code' => 'DFN',
431                'academy_level_id' => $this->levelIds['delfinas'],
432                'primary_coach_id' => $this->coachIds['coach_andrei'],
433                'schedule_summary' => 'Mon & Wed, 17:00',
434            ],
435            'orange_aces' => [
436                'name' => 'Rechin Avansat',
437                'code' => 'RCH',
438                'academy_level_id' => $this->levelIds['rechin'],
439                'primary_coach_id' => $this->coachIds['coach_ioana'],
440                'schedule_summary' => 'Tue & Thu, 17:00',
441            ],
442            'green_smash' => [
443                'name' => 'Performanta Bazin',
444                'code' => 'PRF',
445                'academy_level_id' => $this->levelIds['sportiv_performanta'],
446                'primary_coach_id' => $this->coachIds['coach_mihai'],
447                'schedule_summary' => 'Mon & Thu, 18:00',
448            ],
449        ];
450
451        foreach ($groups as $key => $group) {
452            $model->insert([
453                'name' => $group['name'],
454                'code' => $group['code'],
455                'academy_level_id' => $group['academy_level_id'],
456                'primary_coach_id' => $group['primary_coach_id'],
457                'schedule_summary' => $group['schedule_summary'],
458                'status' => 'active',
459                'capacity' => 12,
460                'created_at' => $now,
461                'updated_at' => $now,
462            ]);
463
464            $this->groupIds[$key] = (int) $model->getInsertID();
465        }
466    }
467
468    private function seedParents(): void
469    {
470        $model = model(GuardianModel::class);
471        $now   = date('Y-m-d H:i:s');
472
473        foreach ($this->childDefinitions() as $childKey => $child) {
474            $guardian = $child['guardian'];
475            $model->insert([
476                'user_id' => $guardian['user_key'] ? $this->userIds[$guardian['user_key']] : null,
477                'full_name' => $guardian['full_name'],
478                'phone' => $guardian['phone'],
479                'email' => $guardian['email'],
480                'relationship_type' => 'parent',
481                'status' => 'active',
482                'notes' => 'Contact seed-uit pentru profilul copilului.',
483                'created_at' => $now,
484                'updated_at' => $now,
485            ]);
486
487            $this->guardianIds[$childKey] = (int) $model->getInsertID();
488        }
489    }
490
491    private function seedChildren(): void
492    {
493        $childModel          = model(ChildModel::class);
494        $childGuardianModel  = model(ChildGuardianModel::class);
495        $childCoachModel     = model(ChildCoachModel::class);
496        $journeyService      = service('journey');
497        $now                 = date('Y-m-d H:i:s');
498
499        foreach ($this->childDefinitions() as $childKey => $definition) {
500            $fullName = $definition['first_name'] . ' ' . $definition['last_name'];
501            $childModel->insert([
502                'first_name' => $definition['first_name'],
503                'last_name' => $definition['last_name'],
504                'full_name' => $fullName,
505                'birth_date' => $definition['birth_date'],
506                'gender' => $definition['gender'],
507                'academy_level_id' => $this->levelIds[$definition['level_key']],
508                'academy_group_id' => $this->groupIds[$definition['group_key']],
509                'primary_coach_id' => $this->coachIds[$definition['coach_key']],
510                'primary_guardian_id' => $this->guardianIds[$childKey],
511                'first_class_at' => $definition['first_class_at'],
512                'status' => $definition['status'],
513                'has_siblings' => $definition['has_siblings'],
514                'notes' => $definition['notes'],
515                'created_at' => $now,
516                'updated_at' => $now,
517            ]);
518
519            $childId = (int) $childModel->getInsertID();
520
521            $childGuardianModel->insert([
522                'child_id' => $childId,
523                'parent_id' => $this->guardianIds[$childKey],
524                'relationship_type' => 'parent',
525                'is_primary' => 1,
526                'created_at' => $now,
527            ]);
528
529            $childCoachModel->insert([
530                'child_id' => $childId,
531                'coach_id' => $this->coachIds[$definition['coach_key']],
532                'is_primary' => 1,
533                'assigned_at' => $definition['first_class_at'] . ' 09:00:00',
534                'unassigned_at' => null,
535            ]);
536
537            $this->children[$childKey] = [
538                'id' => $childId,
539                'full_name' => $fullName,
540                'status' => $definition['status'],
541                'group_key' => $definition['group_key'],
542                'coach_key' => $definition['coach_key'],
543                'level_key' => $definition['level_key'],
544            ];
545
546            $journeyService->recordEvent([
547                'child_id' => $childId,
548                'event_type' => 'child_created',
549                'title' => 'Copil adaugat in academie',
550                'description' => 'Profilul copilului a fost creat si asignat la grupa si trainerul principal.',
551                'metadata' => json_encode([
552                    'group' => $definition['group_key'],
553                    'coach' => $definition['coach_key'],
554                    'level' => $definition['level_key'],
555                ], JSON_UNESCAPED_UNICODE),
556                'created_by' => $this->userIds['admin'],
557                'created_at' => $now,
558            ]);
559        }
560    }
561
562    private function seedAttendance(): void
563    {
564        $sessionModel = model(AttendanceSessionModel::class);
565        $recordModel  = model(AttendanceRecordModel::class);
566        $journey      = service('journey');
567        $today        = new \DateTimeImmutable('today');
568        $offsets      = [-27, -24, -20, -17, -13, -10, -6, -3];
569        $patterns     = $this->attendancePatterns();
570
571        foreach ($this->groupIds as $groupKey => $groupId) {
572            $cancelledSessionId = null;
573
574            foreach ($offsets as $index => $offset) {
575                $sessionDate = $today->modify($offset . ' days')->format('Y-m-d');
576                $status      = 'completed';
577                $isCancelled = 0;
578                $recoveryFor = null;
579
580                if ($groupKey === 'orange_aces' && $index === 2) {
581                    $status      = 'cancelled';
582                    $isCancelled = 1;
583                }
584
585                if ($groupKey === 'orange_aces' && $index === 3) {
586                    $status      = 'recovery';
587                    $recoveryFor = $cancelledSessionId;
588                }
589
590                $sessionModel->insert([
591                    'academy_group_id' => $groupId,
592                    'coach_id' => $this->coachIds[$this->groupCoachKey($groupKey)],
593                    'session_date' => $sessionDate,
594                    'start_time' => $groupKey === 'green_smash' ? '18:00:00' : '17:00:00',
595                    'end_time' => $groupKey === 'green_smash' ? '19:30:00' : '18:30:00',
596                    'status' => $status,
597                    'is_cancelled' => $isCancelled,
598                    'cancelled_reason' => $isCancelled ? 'Trainer indisponibil - sesiune mutata pe recovery.' : null,
599                    'recovery_for_session_id' => $recoveryFor,
600                    'notes' => $status === 'recovery' ? 'Sesiune de recuperare programata automat.' : null,
601                    'created_at' => $sessionDate . ' 08:00:00',
602                    'updated_at' => $sessionDate . ' 08:00:00',
603                ]);
604
605                $sessionId = (int) $sessionModel->getInsertID();
606
607                if ($isCancelled) {
608                    $cancelledSessionId = $sessionId;
609                    continue;
610                }
611
612                foreach ($this->children as $childKey => $child) {
613                    if ($child['group_key'] !== $groupKey || ! $this->childAttendsSession($child['status'], $offset)) {
614                        continue;
615                    }
616
617                    $pattern = $patterns[$childKey] ?? array_fill(0, count($offsets), 'present');
618                    $status  = $pattern[$index] ?? 'present';
619
620                    if ($status === 'recovery' && $groupKey !== 'orange_aces') {
621                        $status = 'present';
622                    }
623
624                    if ($groupKey === 'orange_aces' && $index === 3 && $status === 'present') {
625                        $status = 'recovery';
626                    }
627
628                    $recordModel->insert([
629                        'attendance_session_id' => $sessionId,
630                        'child_id' => $child['id'],
631                        'status' => $status,
632                        'checked_in_at' => in_array($status, ['present', 'recovery'], true) ? $sessionDate . ' 16:55:00' : null,
633                        'notes' => $status === 'absent_unexcused' ? 'Absenta nemotivata.' : null,
634                        'created_at' => $sessionDate . ' 20:00:00',
635                        'updated_at' => $sessionDate . ' 20:00:00',
636                    ]);
637                }
638            }
639        }
640
641        foreach (['maria_enache', 'teodor_stan'] as $childKey) {
642            $journey->recordEvent([
643                'child_id' => $this->children[$childKey]['id'],
644                'event_type' => 'attendance_issue',
645                'title' => 'Avertizare de prezenta',
646                'description' => 'Copilul a depasit pragul de absente pentru perioada recenta.',
647                'metadata' => json_encode(['source' => 'seed'], JSON_UNESCAPED_UNICODE),
648                'created_by' => $this->userIds['admin'],
649            ]);
650        }
651    }
652
653    private function seedEvaluations(): void
654    {
655        $plans = $this->evaluationPlan();
656
657        foreach ($plans as $childKey => $entries) {
658            foreach ($entries as $entry) {
659                $this->createEvaluation(
660                    $childKey,
661                    $entry['type'],
662                    $entry['days_ago'],
663                    $entry['template'],
664                    $entry['note'],
665                );
666            }
667        }
668    }
669
670    private function seedNotifications(): void
671    {
672        $model = model(NotificationModel::class);
673        $now   = date('Y-m-d H:i:s');
674
675        $model->insertBatch([
676            [
677                'user_id' => $this->userIds['admin'],
678                'child_id' => $this->children['luca_marin']['id'],
679                'type' => 'promotion_ready',
680                'title' => 'Copil pregatit pentru avansare',
681                'message' => 'Luca Marin are ultima evaluare Full cu status READY.',
682                'status' => 'unread',
683                'action_url' => '/dashboard',
684                'created_at' => $now,
685                'read_at' => null,
686            ],
687            [
688                'user_id' => $this->userIds['coach_andrei'],
689                'child_id' => $this->children['luca_marin']['id'],
690                'type' => 'promotion_ready',
691                'title' => 'Ready in grupa ta',
692                'message' => 'Luca Marin poate intra in discutia de promovare.',
693                'status' => 'unread',
694                'action_url' => '/dashboard',
695                'created_at' => $now,
696                'read_at' => null,
697            ],
698            [
699                'user_id' => $this->userIds['admin'],
700                'child_id' => $this->children['maria_enache']['id'],
701                'type' => 'attendance_alert',
702                'title' => 'Prezenta scazuta',
703                'message' => 'Maria Enache are absente ridicate in ultimele sedinte.',
704                'status' => 'unread',
705                'action_url' => '/dashboard',
706                'created_at' => $now,
707                'read_at' => null,
708            ],
709            [
710                'user_id' => $this->userIds['admin'],
711                'child_id' => $this->children['daria_neagu']['id'],
712                'type' => 'evaluation_overdue',
713                'title' => 'Fara evaluare recenta',
714                'message' => 'Daria Neagu nu are evaluare in ultimele 45 de zile.',
715                'status' => 'unread',
716                'action_url' => '/dashboard',
717                'created_at' => $now,
718                'read_at' => null,
719            ],
720        ]);
721    }
722
723    private function createUser(string $username, string $email, string $shieldGroup, string $appRole): int
724    {
725        $user = new User([
726            'username' => $username,
727            'email' => $email,
728            'password' => $this->defaultPassword,
729            'active' => 1,
730        ]);
731
732        $this->userModel->save($user);
733
734        $created = $this->userModel->findByCredentials(['email' => $email]);
735        if (! $created) {
736            throw new RuntimeException('Unable to create user ' . $email);
737        }
738
739        $created->addGroup($shieldGroup);
740
741        model(UserRoleModel::class)->insert([
742            'user_id' => (int) $created->id,
743            'role_id' => $this->roleIds[$appRole],
744            'created_at' => date('Y-m-d H:i:s'),
745        ]);
746
747        return (int) $created->id;
748    }
749
750    private function createEvaluation(string $childKey, string $typeKey, int $daysAgo, string $templateKey, string $note): void
751    {
752        $child        = $this->children[$childKey];
753        $typeId       = $this->evaluationTypeIds[$typeKey];
754        $date         = (new \DateTimeImmutable('today'))->modify('-' . $daysAgo . ' days')->format('Y-m-d');
755        $coachKey     = $child['coach_key'];
756        $template     = $this->scoreTemplates()[$templateKey];
757        $criteriaRows = [];
758
759        foreach ($this->criteriaByType[$typeKey] as $criterionSlug) {
760            $criteriaRows[] = [
761                'evaluation_criteria_id' => $this->criteria[$criterionSlug]['id'],
762                'weight' => $this->criteria[$criterionSlug]['weight'],
763                'score' => $template[$criterionSlug],
764                'is_critical' => $this->criteria[$criterionSlug]['is_critical'],
765                'notes' => null,
766            ];
767        }
768
769        $result = service('evaluationEngine')->evaluate($criteriaRows, $this->rule);
770        $promotionSignal = $typeKey === 'full-evaluation' ? (int) $result['promotion_signal'] : 0;
771
772        $evaluationModel = model(EvaluationModel::class);
773        $evaluationModel->insert([
774            'child_id' => $child['id'],
775            'evaluation_type_id' => $typeId,
776            'evaluation_date' => $date,
777            'evaluator_user_id' => $this->userIds[$coachKey],
778            'evaluator_coach_id' => $this->coachIds[$coachKey],
779            'level_at_time_id' => $this->levelIds[$child['level_key']],
780            'final_score' => $result['final_score'],
781            'final_status' => $result['final_status'],
782            'promotion_signal' => $promotionSignal,
783            'notes' => $note,
784            'created_at' => $date . ' 12:00:00',
785            'updated_at' => $date . ' 12:00:00',
786        ]);
787
788        $evaluationId = (int) $evaluationModel->getInsertID();
789
790        foreach ($criteriaRows as $criteriaRow) {
791            model(EvaluationScoreModel::class)->insert(array_merge($criteriaRow, [
792                'evaluation_id' => $evaluationId,
793            ]));
794        }
795
796        model(EvaluationStatusHistoryModel::class)->insert([
797            'evaluation_id' => $evaluationId,
798            'previous_status' => null,
799            'new_status' => $result['final_status'],
800            'reason' => 'Seeded initial evaluation status.',
801            'changed_by' => $this->userIds[$coachKey],
802            'created_at' => $date . ' 12:05:00',
803        ]);
804
805        service('journey')->recordEvent([
806            'child_id' => $child['id'],
807            'event_type' => $typeKey === 'quick-check' ? 'quick_check_added' : 'evaluation_added',
808            'title' => $typeKey === 'quick-check' ? 'Quick Check adaugat' : 'Full Evaluation adaugata',
809            'description' => $note,
810            'metadata' => json_encode([
811                'type' => $typeKey,
812                'score' => $result['final_score'],
813                'status' => $result['final_status'],
814            ], JSON_UNESCAPED_UNICODE),
815            'created_by' => $this->userIds[$coachKey],
816            'created_at' => $date . ' 12:10:00',
817        ]);
818    }
819
820    /**
821     * @return array<string, array<string, mixed>>
822     */
823    private function childDefinitions(): array
824    {
825        return [
826            'luca_marin' => $this->childDefinition('Luca', 'Marin', '2016-04-12', 'male', 'rechin', 'red_sparks', 'coach_andrei', 'active', '2025-09-02', 1, 'Adaptare buna in apa si ritm foarte bun.', 'Cristina Marin', '0740 100 100', 'cristina@academia.local', 'parent_cristina'),
827            'sofia_radu' => $this->childDefinition('Sofia', 'Radu', '2017-01-20', 'female', 'delfinas', 'red_sparks', 'coach_andrei', 'active', '2025-10-10', 0, 'Foarte receptiva la feedback.', 'Elena Radu', '0740 100 101', 'elena.radu@academia.local'),
828            'matei_dinu' => $this->childDefinition('Matei', 'Dinu', '2016-08-05', 'male', 'rechin', 'red_sparks', 'coach_andrei', 'active', '2025-09-18', 0, 'Constanta in crestere, are nevoie de mai multa incredere.', 'Roxana Dinu', '0740 100 102', 'roxana.dinu@academia.local'),
829            'emma_pavel' => $this->childDefinition('Emma', 'Pavel', '2015-07-11', 'female', 'pre_performanta', 'orange_aces', 'coach_ioana', 'active', '2025-08-15', 0, 'Se misca bine in bazin, coordonare in progres.', 'Adrian Pavel', '0740 100 103', 'adrian.pavel@academia.local'),
830            'david_ilie' => $this->childDefinition('David', 'Ilie', '2015-11-09', 'male', 'pre_performanta', 'orange_aces', 'coach_ioana', 'active', '2025-08-22', 1, 'Competitiv, raspunde bine la obiective clare.', 'Corina Ilie', '0740 100 104', 'corina.ilie@academia.local'),
831            'ana_toma' => $this->childDefinition('Ana', 'Toma', '2014-09-03', 'female', 'sportiv_performanta', 'orange_aces', 'coach_ioana', 'active', '2025-07-30', 0, 'Stabila in sesiuni, aproape de pragul urmator.', 'Andreea Toma', '0740 100 105', 'andreea.toma@academia.local'),
832            'teodor_stan' => $this->childDefinition('Teodor', 'Stan', '2014-12-14', 'male', 'sportiv_performanta', 'orange_aces', 'coach_ioana', 'active', '2025-07-18', 0, 'Prezenta inconstanta in ultima luna.', 'Mihnea Stan', '0740 100 106', 'mihnea.stan@academia.local'),
833            'ilinca_pop' => $this->childDefinition('Ilinca', 'Pop', '2013-06-01', 'female', 'sportiv_performanta', 'green_smash', 'coach_mihai', 'active', '2025-06-20', 0, 'Control bun pe serii lungi.', 'Camelia Pop', '0740 100 107', 'camelia.pop@academia.local'),
834            'victor_oprea' => $this->childDefinition('Victor', 'Oprea', '2013-10-27', 'male', 'sportiv_performanta', 'green_smash', 'coach_mihai', 'active', '2025-06-28', 0, 'Nivel bun, inca lucreaza la strategie de cursa.', 'Laura Oprea', '0740 100 108', 'laura.oprea@academia.local'),
835            'maria_enache' => $this->childDefinition('Maria', 'Enache', '2012-03-16', 'female', 'sportiv_performanta', 'green_smash', 'coach_mihai', 'active', '2025-05-12', 0, 'Potential bun, dar absentele incetinesc progresul.', 'George Enache', '0740 100 109', 'george.enache@academia.local'),
836            'alex_tudor' => $this->childDefinition('Alex', 'Tudor', '2017-09-21', 'male', 'delfinas', 'red_sparks', 'coach_andrei', 'active', '2025-10-18', 0, 'Energetic, are nevoie de mai multa consistenta.', 'Bianca Tudor', '0740 100 110', 'bianca.tudor@academia.local'),
837            'eva_nistor' => $this->childDefinition('Eva', 'Nistor', '2015-02-07', 'female', 'pre_performanta', 'orange_aces', 'coach_ioana', 'pause', '2025-08-05', 0, 'Pauza temporara, revenire planificata luna viitoare.', 'Sorin Nistor', '0740 100 111', 'sorin.nistor@academia.local'),
838            'rares_mocanu' => $this->childDefinition('Rares', 'Mocanu', '2013-01-30', 'male', 'sportiv_performanta', 'green_smash', 'coach_mihai', 'active', '2025-05-28', 0, 'Stagnare usoara pe partea de cursa.', 'Daniela Mocanu', '0740 100 112', 'daniela.mocanu@academia.local'),
839            'daria_neagu' => $this->childDefinition('Daria', 'Neagu', '2012-11-11', 'female', 'sportiv_performanta', 'green_smash', 'coach_mihai', 'active', '2025-05-02', 0, 'Necesita reevaluare completa dupa revenirea in ritm.', 'Silviu Neagu', '0740 100 113', 'silviu.neagu@academia.local'),
840            'tudor_barbu' => $this->childDefinition('Tudor', 'Barbu', '2016-06-18', 'male', 'rechin', 'red_sparks', 'coach_andrei', 'left', '2025-09-10', 0, 'A parasit temporar programul.', 'Anca Barbu', '0740 100 114', 'anca.barbu@academia.local'),
841        ];
842    }
843
844    /**
845     * @return array<string, mixed>
846     */
847    private function childDefinition(
848        string $firstName,
849        string $lastName,
850        string $birthDate,
851        string $gender,
852        string $levelKey,
853        string $groupKey,
854        string $coachKey,
855        string $status,
856        string $firstClassAt,
857        int $hasSiblings,
858        string $notes,
859        string $guardianName,
860        string $guardianPhone,
861        string $guardianEmail,
862        ?string $guardianUserKey = null,
863    ): array {
864        return [
865            'first_name' => $firstName,
866            'last_name' => $lastName,
867            'birth_date' => $birthDate,
868            'gender' => $gender,
869            'level_key' => $levelKey,
870            'group_key' => $groupKey,
871            'coach_key' => $coachKey,
872            'status' => $status,
873            'first_class_at' => $firstClassAt,
874            'has_siblings' => $hasSiblings,
875            'notes' => $notes,
876            'guardian' => [
877                'full_name' => $guardianName,
878                'phone' => $guardianPhone,
879                'email' => $guardianEmail,
880                'user_key' => $guardianUserKey,
881            ],
882        ];
883    }
884
885    /**
886     * @return array<string, list<string>>
887     */
888    private function attendancePatterns(): array
889    {
890        return [
891            'luca_marin' => ['present', 'present', 'present', 'present', 'recovery', 'present', 'present', 'present'],
892            'sofia_radu' => ['present', 'present', 'absent_excused', 'present', 'present', 'present', 'present', 'present'],
893            'matei_dinu' => ['present', 'absent_unexcused', 'present', 'present', 'absent_excused', 'present', 'present', 'present'],
894            'emma_pavel' => ['present', 'present', 'present', 'present', 'present', 'present', 'present', 'present'],
895            'david_ilie' => ['present', 'present', 'present', 'present', 'recovery', 'present', 'present', 'present'],
896            'ana_toma' => ['present', 'present', 'absent_excused', 'recovery', 'present', 'present', 'present', 'present'],
897            'teodor_stan' => ['present', 'absent_unexcused', 'absent_unexcused', 'present', 'present', 'absent_excused', 'present', 'present'],
898            'ilinca_pop' => ['present', 'present', 'present', 'present', 'present', 'present', 'present', 'present'],
899            'victor_oprea' => ['present', 'present', 'present', 'present', 'present', 'absent_excused', 'present', 'present'],
900            'maria_enache' => ['present', 'absent_unexcused', 'absent_unexcused', 'present', 'absent_excused', 'absent_unexcused', 'recovery', 'absent_unexcused'],
901            'alex_tudor' => ['present', 'present', 'present', 'present', 'present', 'present', 'absent_excused', 'present'],
902            'eva_nistor' => ['present', 'present', 'absent_excused', 'present', 'present', 'present', 'absent_excused', 'absent_excused'],
903            'rares_mocanu' => ['present', 'present', 'present', 'present', 'absent_unexcused', 'present', 'present', 'present'],
904            'daria_neagu' => ['present', 'present', 'present', 'present', 'present', 'present', 'present', 'present'],
905            'tudor_barbu' => ['present', 'present', 'absent_unexcused', 'present', 'present', 'present', 'present', 'present'],
906        ];
907    }
908
909    /**
910     * @return array<string, list<array<string, int|string>>>
911     */
912    private function evaluationPlan(): array
913    {
914        return [
915            'luca_marin' => $this->standardPlan('ready', 6),
916            'sofia_radu' => $this->standardPlan('ready', 9),
917            'matei_dinu' => $this->standardPlan('almost', 5),
918            'emma_pavel' => $this->standardPlan('almost', 4),
919            'david_ilie' => $this->standardPlan('ready', 8),
920            'ana_toma' => $this->standardPlan('almost', 11),
921            'teodor_stan' => $this->standardPlan('hold', 7),
922            'ilinca_pop' => $this->standardPlan('ready', 10),
923            'victor_oprea' => $this->standardPlan('almost', 12),
924            'maria_enache' => $this->standardPlan('hold', 3),
925            'alex_tudor' => $this->standardPlan('hold', 13),
926            'eva_nistor' => $this->standardPlan('almost', 20),
927            'rares_mocanu' => $this->standardPlan('hold', 14),
928            'daria_neagu' => [
929                ['type' => 'full-evaluation', 'days_ago' => 58, 'template' => 'full_hold_old', 'note' => 'Ultima evaluare completa este depasita si necesita refresh.'],
930            ],
931            'tudor_barbu' => [
932                ['type' => 'full-evaluation', 'days_ago' => 75, 'template' => 'full_hold_old', 'note' => 'Evaluare istorica pastrata pentru traseul copilului.'],
933            ],
934        ];
935    }
936
937    /**
938     * @return list<array<string, int|string>>
939     */
940    private function standardPlan(string $status, int $latestFullDaysAgo): array
941    {
942        return match ($status) {
943            'ready' => [
944                ['type' => 'full-evaluation', 'days_ago' => 52, 'template' => 'full_ready_old', 'note' => 'Evaluare completa de referinta, aproape de READY.'],
945                ['type' => 'quick-check', 'days_ago' => 18, 'template' => 'quick_ready', 'note' => 'Quick Check pozitiv, progres confirmat intre evaluari.'],
946                ['type' => 'full-evaluation', 'days_ago' => $latestFullDaysAgo, 'template' => 'full_ready', 'note' => 'Evaluare completa recenta, copil pregatit pentru discutie de promovare.'],
947            ],
948            'almost' => [
949                ['type' => 'full-evaluation', 'days_ago' => 49, 'template' => 'full_almost_old', 'note' => 'Evaluare anterioara cu progres constant, dar sub pragul final.'],
950                ['type' => 'quick-check', 'days_ago' => 16, 'template' => 'quick_almost', 'note' => 'Quick Check de monitorizare, copil aproape de READY.'],
951                ['type' => 'full-evaluation', 'days_ago' => $latestFullDaysAgo, 'template' => 'full_almost', 'note' => 'Evaluare completa recenta, copil aproape gata pentru nivelul urmator.'],
952            ],
953            default => [
954                ['type' => 'full-evaluation', 'days_ago' => 47, 'template' => 'full_hold_old', 'note' => 'Evaluare anterioara cu blocaj pe criteriile critice.'],
955                ['type' => 'quick-check', 'days_ago' => 15, 'template' => 'quick_hold', 'note' => 'Quick Check folosit pentru monitorizare interna si alerta.'],
956                ['type' => 'full-evaluation', 'days_ago' => $latestFullDaysAgo, 'template' => 'full_hold', 'note' => 'Evaluare completa recenta, copilul ramane in HOLD.'],
957            ],
958        };
959    }
960
961    /**
962     * @return array<string, array<string, float>>
963     */
964    private function scoreTemplates(): array
965    {
966        return [
967            'full_ready_old' => [
968                'water-confidence' => 3.8,
969                'breathing-control' => 3.9,
970                'stroke-technique' => 4.0,
971                'start-turn-basics' => 3.6,
972                'race-awareness' => 3.7,
973                'focus-effort' => 3.9,
974            ],
975            'full_ready' => [
976                'water-confidence' => 4.4,
977                'breathing-control' => 4.2,
978                'stroke-technique' => 4.5,
979                'start-turn-basics' => 4.0,
980                'race-awareness' => 4.1,
981                'focus-effort' => 4.4,
982            ],
983            'full_almost_old' => [
984                'water-confidence' => 3.2,
985                'breathing-control' => 3.3,
986                'stroke-technique' => 3.4,
987                'start-turn-basics' => 3.0,
988                'race-awareness' => 3.1,
989                'focus-effort' => 3.3,
990            ],
991            'full_almost' => [
992                'water-confidence' => 3.6,
993                'breathing-control' => 3.7,
994                'stroke-technique' => 3.8,
995                'start-turn-basics' => 3.4,
996                'race-awareness' => 3.5,
997                'focus-effort' => 3.7,
998            ],
999            'full_hold_old' => [
1000                'water-confidence' => 2.9,
1001                'breathing-control' => 3.0,
1002                'stroke-technique' => 2.8,
1003                'start-turn-basics' => 3.1,
1004                'race-awareness' => 3.0,
1005                'focus-effort' => 3.2,
1006            ],
1007            'full_hold' => [
1008                'water-confidence' => 3.0,
1009                'breathing-control' => 3.1,
1010                'stroke-technique' => 2.9,
1011                'start-turn-basics' => 3.2,
1012                'race-awareness' => 3.0,
1013                'focus-effort' => 3.3,
1014            ],
1015            'quick_ready' => [
1016                'water-confidence' => 4.3,
1017                'breathing-control' => 4.1,
1018                'stroke-technique' => 4.3,
1019                'focus-effort' => 4.4,
1020            ],
1021            'quick_almost' => [
1022                'water-confidence' => 3.7,
1023                'breathing-control' => 3.6,
1024                'stroke-technique' => 3.7,
1025                'focus-effort' => 3.8,
1026            ],
1027            'quick_hold' => [
1028                'water-confidence' => 3.0,
1029                'breathing-control' => 3.0,
1030                'stroke-technique' => 2.8,
1031                'focus-effort' => 3.2,
1032            ],
1033        ];
1034    }
1035
1036    private function groupCoachKey(string $groupKey): string
1037    {
1038        return match ($groupKey) {
1039            'red_sparks' => 'coach_andrei',
1040            'orange_aces' => 'coach_ioana',
1041            default => 'coach_mihai',
1042        };
1043    }
1044
1045    private function childAttendsSession(string $status, int $offset): bool
1046    {
1047        if ($status === 'left' && $offset > -18) {
1048            return false;
1049        }
1050
1051        if ($status === 'pause' && $offset > -8) {
1052            return false;
1053        }
1054
1055        return true;
1056    }
1057}