| Code Coverage
| Lines | Functions and Methods | Classes and Traits
Total | 48.64% covered (danger) | 48.64% | 161 / 331 | 38.89% covered (danger) | 38.89% | 7 / 18 | CRAP | 0.00% covered (danger) | 0.00% | 0 / 1
CoachesService | 48.64% covered (danger) | 48.64% | 161 / 331 | 38.89% covered (danger) | 38.89% | 7 / 18 | 992.94 | 0.00% covered (danger) | 0.00% | 0 / 1
__construct | 100.00% covered (success) | 100.00% | 2 / 2 | 100.00% covered (success) | 100.00% | 1 / 1 | 1 |
getIndexData | 0.00% covered (danger) | 0.00% | 0 / 19 | 0.00% covered (danger) | 0.00% | 0 / 1 | 6 |
getFormData | 0.00% covered (danger) | 0.00% | 0 / 49 | 0.00% covered (danger) | 0.00% | 0 / 1 | 210 |
save | 68.66% covered (warning) | 68.66% | 46 / 67 | 0.00% covered (danger) | 0.00% | 0 / 1 | 23.88 |
getProfileData | 88.89% covered (warning) | 88.89% | 32 / 36 | 0.00% covered (danger) | 0.00% | 0 / 1 | 5.03 |
buildFilters | 0.00% covered (danger) | 0.00% | 0 / 14 | 0.00% covered (danger) | 0.00% | 0 / 1 | 6 |
getCoachRows | 0.00% covered (danger) | 0.00% | 0 / 19 | 0.00% covered (danger) | 0.00% | 0 / 1 | 30 |
buildCoachStats | 0.00% covered (danger) | 0.00% | 0 / 18 | 0.00% covered (danger) | 0.00% | 0 / 1 | 56 |
normalizePayload | 100.00% covered (success) | 100.00% | 13 / 13 | 100.00% covered (success) | 100.00% | 1 / 1 | 1 |
validateCredentials | 84.00% covered (warning) | 84.00% | 21 / 25 | 0.00% covered (danger) | 0.00% | 0 / 1 | 10.41 |
normalizeAvailabilityInput | 90.91% covered (success) | 90.91% | 10 / 11 | 0.00% covered (danger) | 0.00% | 0 / 1 | 5.02 |
defaultAvailability | 100.00% covered (success) | 100.00% | 8 / 8 | 100.00% covered (success) | 100.00% | 1 / 1 | 2 |
availabilityMap | 0.00% covered (danger) | 0.00% | 0 / 9 | 0.00% covered (danger) | 0.00% | 0 / 1 | 6 |
syncAvailability | 100.00% covered (success) | 100.00% | 15 / 15 | 100.00% covered (success) | 100.00% | 1 / 1 | 4 |
getCoachWithUser | 100.00% covered (success) | 100.00% | 7 / 7 | 100.00% covered (success) | 100.00% | 1 / 1 | 1 |
availabilityRows | 100.00% covered (success) | 100.00% | 6 / 6 | 100.00% covered (success) | 100.00% | 1 / 1 | 1 |
availabilityRowsForMany | 0.00% covered (danger) | 0.00% | 0 / 12 | 0.00% covered (danger) | 0.00% | 0 / 1 | 12 |
db | 100.00% covered (success) | 100.00% | 1 / 1 | 100.00% covered (success) | 100.00% | 1 / 1 | 1 |

1 | <?php
2 |
3 | namespace App\Services;
4 |
5 | use App\Models\CoachAvailabilityModel;
6 | use App\Models\CoachModel;
7 | use App\Models\RoleModel;
8 | use App\Models\UserRoleModel;
9 | use CodeIgniter\Shield\Entities\User;
10 | use CodeIgniter\Shield\Models\UserModel;
11 |
12 | class CoachesService
13 | {
14 | private AuthorizationService $authorizationService;
15 |
16 | public function __construct()
17 | {
18 | helper('academy');
19 |
20 | $this->authorizationService = service('authorization');
21 | }
22 |
23 | /**
24 | * @param array<string, mixed> $input
25 | * @return array<string, mixed>
26 | */
27 | public function getIndexData(array $input, User $user): array
28 | {
29 | $filters = $this->buildFilters($input, (int) $user->id);
30 | $rows = $this->getCoachRows($filters);
31 | $coachIds = array_map(static fn (array $row): int => (int) $row['id'], $rows);
32 | $stats = $this->buildCoachStats($coachIds);
33 |
34 | foreach ($rows as &$row) {
35 | $coachId = (int) $row['id'];
36 | $row['active_children'] = $stats['children'][$coachId] ?? 0;
37 | $row['evaluations_count'] = $stats['evaluations'][$coachId] ?? 0;
38 | $row['avg_child_score'] = $stats['scores'][$coachId] ?? null;
39 | $row['avg_child_score_label'] = format_score($row['avg_child_score']);
40 | $row['availability_summary'] = $stats['availability'][$coachId] ?? 'Program neconfigurat';
41 | }
42 | unset($row);
43 |
44 | return [
45 | 'pageTitle' => 'Traineri',
46 | 'pageDescription' => 'Echipa academiei, capacitatea curenta si incarcarea operationala pe trainer.',
47 | 'filters' => $filters,
48 | 'coaches' => $rows,
49 | 'canManage' => $filters['current_role'] === 'admin',
50 | ];
51 | }
52 |
53 | /**
54 | * @param array<string, mixed> $input
55 | * @param array<string, string> $errors
56 | * @return array<string, mixed>|null
57 | */
58 | public function getFormData(User $user, ?int $coachId = null, array $input = [], array $errors = []): ?array
59 | {
60 | $coach = $coachId !== null ? $this->getCoachWithUser($coachId) : null;
61 | if ($coachId !== null && $coach === null) {
62 | return null;
63 | }
64 |
65 | $values = [
66 | 'full_name' => '',
67 | 'display_role' => 'Trainer',
68 | 'phone' => '',
69 | 'email' => '',
70 | 'avatar_path' => '',
71 | 'is_active' => '1',
72 | 'notes' => '',
73 | 'username' => '',
74 | 'password' => '',
75 | 'password_confirmation' => '',
76 | 'availability' => $this->defaultAvailability(),
77 | ];
78 |
79 | if ($coach !== null) {
80 | $values = [
81 | 'full_name' => $coach['full_name'],
82 | 'display_role' => $coach['display_role'],
83 | 'phone' => $coach['phone'],
84 | 'email' => $coach['email'],
85 | 'avatar_path' => $coach['avatar_path'],
86 | 'is_active' => (string) ((int) $coach['is_active']),
87 | 'notes' => $coach['notes'],
88 | 'username' => $coach['username'],
89 | 'password' => '',
90 | 'password_confirmation' => '',
91 | 'availability' => $this->availabilityMap($coachId),
92 | ];
93 | }
94 |
95 | foreach ($input as $key => $value) {
96 | if ($key === 'availability' && is_array($value)) {
97 | $values['availability'] = $this->normalizeAvailabilityInput($value);
98 | continue;
99 | }
100 |
101 | if (array_key_exists($key, $values) && is_string($value)) {
102 | $values[$key] = trim($value);
103 | }
104 | }
105 |
106 | return [
107 | 'pageTitle' => $coachId === null ? 'Trainer nou' : 'Editare trainer',
108 | 'pageDescription' => $coachId === null
109 | ? 'Creeaza profilul de trainer, contul de acces si disponibilitatea saptamanala.'
110 | : 'Actualizeaza datele trainerului fara sa pierzi istoricul evaluarilor efectuate.',
111 | 'coach' => $coach,
112 | 'values' => $values,
113 | 'errors' => $errors,
114 | 'roleOptions' => ['Head Trainer', 'Trainer', 'Assistant Trainer'],
115 | 'submitUrl' => $coachId === null ? url_to('coaches.create') : url_to('coaches.update', $coachId),
116 | 'cancelUrl' => $coachId === null ? url_to('coaches.index') : url_to('coaches.show', $coachId),
117 | 'canManage' => $this->authorizationService->userHasRole((int) $user->id, 'admin'),
118 | ];
119 | }
120 |
121 | /**
122 | * @param array<string, mixed> $input
123 | * @return array{success: bool, coach_id?: int, errors?: array<string, string>}
124 | */
125 | public function save(array $input, User $user, ?int $coachId = null): array
126 | {
127 | $validation = service('validation');
128 | $validation->setRules(config('Validation')->coach);
129 |
130 | $payload = $this->normalizePayload($input);
131 |
132 | if (! $validation->run($payload)) {
133 | return ['success' => false, 'errors' => $validation->getErrors()];
134 | }
135 |
136 | $extraErrors = $this->validateCredentials($payload, $coachId);
137 | if ($extraErrors !== []) {
138 | return ['success' => false, 'errors' => $extraErrors];
139 | }
140 |
141 | $existing = $coachId !== null ? $this->getCoachWithUser($coachId) : null;
142 | $userModel = model(UserModel::class);
143 | $coachModel = model(CoachModel::class);
144 | $roleId = (int) (model(RoleModel::class)->where('slug', 'coach')->first()['id'] ?? 0);
145 | $now = date('Y-m-d H:i:s');
146 |
147 | $db = $this->db();
148 | $db->transStart();
149 |
150 | if ($existing === null) {
151 | $account = new \CodeIgniter\Shield\Entities\User([
152 | 'username' => $payload['username'],
153 | 'email' => $payload['email'],
154 | 'password' => $payload['password'],
155 | 'active' => (int) $payload['is_active'],
156 | ]);
157 |
158 | $userModel->save($account);
159 |
160 | $createdUser = $userModel->findByCredentials(['email' => $payload['email']]);
161 | $createdUser?->addGroup('coach');
162 |
163 | if ($createdUser !== null && $roleId > 0) {
164 | model(UserRoleModel::class)->insert([
165 | 'user_id' => (int) $createdUser->id,
166 | 'role_id' => $roleId,
167 | 'created_at' => $now,
168 | ]);
169 | }
170 |
171 | $coachModel->insert([
172 | 'user_id' => (int) $createdUser?->id,
173 | 'full_name' => $payload['full_name'],
174 | 'display_role' => $payload['display_role'],
175 | 'phone' => $payload['phone'] !== '' ? $payload['phone'] : null,
176 | 'email' => $payload['email'],
177 | 'avatar_path' => $payload['avatar_path'] !== '' ? $payload['avatar_path'] : null,
178 | 'is_active' => (int) $payload['is_active'],
179 | 'notes' => $payload['notes'] !== '' ? $payload['notes'] : null,
180 | 'created_at' => $now,
181 | 'updated_at' => $now,
182 | ]);
183 |
184 | $coachId = (int) $coachModel->getInsertID();
185 | } else {
186 | $account = $userModel->withGroups()->find($existing['user_id']);
187 | if ($account !== null) {
188 | $account->username = $payload['username'];
189 | $account->email = $payload['email'];
190 | $account->active = (int) $payload['is_active'];
191 | if ($payload['password'] !== '') {
192 | $account->password = $payload['password'];
193 | }
194 | $userModel->save($account);
195 | }
196 |
197 | $coachModel->update($coachId, [
198 | 'full_name' => $payload['full_name'],
199 | 'display_role' => $payload['display_role'],
200 | 'phone' => $payload['phone'] !== '' ? $payload['phone'] : null,
201 | 'email' => $payload['email'],
202 | 'avatar_path' => $payload['avatar_path'] !== '' ? $payload['avatar_path'] : null,
203 | 'is_active' => (int) $payload['is_active'],
204 | 'notes' => $payload['notes'] !== '' ? $payload['notes'] : null,
205 | 'updated_at' => $now,
206 | ]);
207 | }
208 |
209 | $this->syncAvailability((int) $coachId, $payload['availability'], $now);
210 |
211 | $db->transComplete();
212 |
213 | if (! $db->transStatus()) {
214 | return ['success' => false, 'errors' => ['general' => 'Nu am putut salva trainerul.']];
215 | }
216 |
217 | return ['success' => true, 'coach_id' => (int) $coachId];
218 | }
219 |
220 | /**
221 | * @return array<string, mixed>|null
222 | */
223 | public function getProfileData(int $coachId, User $user): ?array
224 | {
225 | $coach = $this->getCoachWithUser($coachId);
226 | if ($coach === null) {
227 | return null;
228 | }
229 |
230 | if ($this->authorizationService->userHasRole((int) $user->id, 'coach') && ! $this->authorizationService->userHasRole((int) $user->id, 'admin')) {
231 | $currentTrainerId = $this->authorizationService->getCoachIdForUser((int) $user->id);
232 | if ($currentTrainerId !== $coachId) {
233 | return null;
234 | }
235 | }
236 |
237 | $children = $this->db()
238 | ->table('children')
239 | ->select('id, full_name, status, birth_date')
240 | ->where('primary_coach_id', $coachId)
241 | ->orderBy('full_name', 'ASC')
242 | ->get()
243 | ->getResultArray();
244 |
245 | $recentEvaluations = $this->db()
246 | ->table('evaluations e')
247 | ->select('e.evaluation_date, e.final_score, e.final_status, children.full_name as child_name, types.name as type_name, types.slug as type_slug')
248 | ->join('children', 'children.id = e.child_id')
249 | ->join('evaluation_types types', 'types.id = e.evaluation_type_id')
250 | ->where('e.evaluator_coach_id', $coachId)
251 | ->orderBy('e.evaluation_date', 'DESC')
252 | ->orderBy('e.id', 'DESC')
253 | ->limit(8)
254 | ->get()
255 | ->getResultArray();
256 |
257 | $quickChecks = array_values(array_filter($recentEvaluations, static fn (array $row): bool => $row['type_slug'] === 'quick-check'));
258 |
259 | return [
260 | 'pageTitle' => $coach['full_name'],
261 | 'pageDescription' => 'Profil operational cu copii asignati, evaluari recente si disponibilitate.',
262 | 'coach' => $coach,
263 | 'children' => $children,
264 | 'recentEvaluations' => $recentEvaluations,
265 | 'recentQuickChecks' => $quickChecks,
266 | 'availability' => $this->availabilityRows($coachId),
267 | 'canManage' => $this->authorizationService->userHasRole((int) $user->id, 'admin'),
268 | ];
269 | }
270 |
271 | /**
272 | * @param array<string, mixed> $input
273 | * @return array<string, mixed>
274 | */
275 | private function buildFilters(array $input, int $userId): array
276 | {
277 | $role = $this->authorizationService->getPrimaryRole($userId);
278 | $lockedTrainerId = $role === 'coach' ? $this->authorizationService->getCoachIdForUser($userId) : null;
279 |
280 | return [
281 | 'current_role' => $role,
282 | 'coach_locked' => $lockedTrainerId !== null,
283 | 'selected_status' => (string) ($input['status'] ?? 'all'),
284 | 'query' => trim((string) ($input['q'] ?? '')),
285 | 'locked_coach_id' => $lockedTrainerId,
286 | 'status_options' => [
287 | 'all' => 'Toate statusurile',
288 | 'active' => 'Activ',
289 | 'inactive' => 'Inactiv',
290 | ],
291 | ];
292 | }
293 |
294 | /**
295 | * @param array<string, mixed> $filters
296 | * @return list<array<string, mixed>>
297 | */
298 | private function getCoachRows(array $filters): array
299 | {
300 | $builder = $this->db()
301 | ->table('coaches c')
302 | ->select('c.*, users.username')
303 | ->join('users', 'users.id = c.user_id', 'left')
304 | ->orderBy('c.full_name', 'ASC');
305 |
306 | if ($filters['locked_coach_id'] !== null) {
307 | $builder->where('c.id', $filters['locked_coach_id']);
308 | }
309 |
310 | if ($filters['selected_status'] === 'active') {
311 | $builder->where('c.is_active', 1);
312 | } elseif ($filters['selected_status'] === 'inactive') {
313 | $builder->where('c.is_active', 0);
314 | }
315 |
316 | if ($filters['query'] !== '') {
317 | $builder
318 | ->groupStart()
319 | ->like('c.full_name', $filters['query'])
320 | ->orLike('c.email', $filters['query'])
321 | ->orLike('c.display_role', $filters['query'])
322 | ->groupEnd();
323 | }
324 |
325 | return $builder->get()->getResultArray();
326 | }
327 |
328 | /**
329 | * @param list<int> $coachIds
330 | * @return array<string, array<int, mixed>>
331 | */
332 | private function buildCoachStats(array $coachIds): array
333 | {
334 | if ($coachIds === []) {
335 | return ['children' => [], 'evaluations' => [], 'scores' => [], 'availability' => []];
336 | }
337 |
338 | $children = [];
339 | foreach ($this->db()->table('children')->select('primary_coach_id, COUNT(*) as total')->where('status', 'active')->whereIn('primary_coach_id', $coachIds)->groupBy('primary_coach_id')->get()->getResultArray() as $row) {
340 | $children[(int) $row['primary_coach_id']] = (int) $row['total'];
341 | }
342 |
343 | $evaluations = [];
344 | foreach ($this->db()->table('evaluations')->select('evaluator_coach_id, COUNT(*) as total')->whereIn('evaluator_coach_id', $coachIds)->groupBy('evaluator_coach_id')->get()->getResultArray() as $row) {
345 | $evaluations[(int) $row['evaluator_coach_id']] = (int) $row['total'];
346 | }
347 |
348 | $scores = [];
349 | foreach ($this->db()->table('children c')->select('c.primary_coach_id, ROUND(AVG(e.final_score), 2) as avg_score')->join('evaluations e', 'e.child_id = c.id', 'left')->whereIn('c.primary_coach_id', $coachIds)->where('c.status', 'active')->groupBy('c.primary_coach_id')->get()->getResultArray() as $row) {
350 | $scores[(int) $row['primary_coach_id']] = $row['avg_score'] !== null ? (float) $row['avg_score'] : null;
351 | }
352 |
353 | $availability = [];
354 | foreach ($this->availabilityRowsForMany($coachIds) as $coachId => $rows) {
355 | $availability[$coachId] = implode(' • ', array_map(
356 | static fn (array $row): string => day_of_week_label((int) $row['day_of_week']) . ' ' . substr((string) $row['start_time'], 0, 5) . '-' . substr((string) $row['end_time'], 0, 5),
357 | $rows,
358 | ));
359 | }
360 |
361 | return compact('children', 'evaluations', 'scores', 'availability');
362 | }
363 |
364 | /**
365 | * @param array<string, mixed> $input
366 | * @return array<string, mixed>
367 | */
368 | private function normalizePayload(array $input): array
369 | {
370 | return [
371 | 'full_name' => trim((string) ($input['full_name'] ?? '')),
372 | 'display_role' => trim((string) ($input['display_role'] ?? 'Trainer')),
373 | 'phone' => trim((string) ($input['phone'] ?? '')),
374 | 'email' => trim((string) ($input['email'] ?? '')),
375 | 'avatar_path' => trim((string) ($input['avatar_path'] ?? '')),
376 | 'is_active' => (string) ((int) ($input['is_active'] ?? 1)),
377 | 'notes' => trim((string) ($input['notes'] ?? '')),
378 | 'username' => trim((string) ($input['username'] ?? '')),
379 | 'password' => (string) ($input['password'] ?? ''),
380 | 'password_confirmation' => (string) ($input['password_confirmation'] ?? ''),
381 | 'availability' => $this->normalizeAvailabilityInput((array) ($input['availability'] ?? [])),
382 | ];
383 | }
384 |
385 | /**
386 | * @return array<string, string>
387 | */
388 | private function validateCredentials(array $payload, ?int $coachId): array
389 | {
390 | $errors = [];
391 | $existing = $coachId !== null ? $this->getCoachWithUser($coachId) : null;
392 | $ignoreUserId = $existing['user_id'] ?? null;
393 |
394 | if ($coachId === null && trim($payload['password']) === '') {
395 | $errors['password'] = 'Parola este obligatorie pentru contul de trainer.';
396 | }
397 |
398 | if ($payload['password'] !== '' && $payload['password'] !== $payload['password_confirmation']) {
399 | $errors['password_confirmation'] = 'Confirmarea parolei nu corespunde.';
400 | }
401 |
402 | $usernameTaken = $this->db()
403 | ->table('users')
404 | ->select('id')
405 | ->where('LOWER(username)', strtolower($payload['username']))
406 | ->get()
407 | ->getRowArray();
408 |
409 | if ($usernameTaken !== null && (int) $usernameTaken['id'] !== (int) $ignoreUserId) {
410 | $errors['username'] = 'Username-ul este deja folosit.';
411 | }
412 |
413 | $emailTaken = $this->db()
414 | ->table('auth_identities')
415 | ->select('user_id')
416 | ->where('type', 'email_password')
417 | ->where('LOWER(secret)', strtolower($payload['email']))
418 | ->get()
419 | ->getRowArray();
420 |
421 | if ($emailTaken !== null && (int) $emailTaken['user_id'] !== (int) $ignoreUserId) {
422 | $errors['email'] = 'Exista deja un cont cu acest email.';
423 | }
424 |
425 | return $errors;
426 | }
427 |
428 | /**
429 | * @param array<string, mixed> $availability
430 | * @return array<int, array<string, string|bool>>
431 | */
432 | private function normalizeAvailabilityInput(array $availability): array
433 | {
434 | $normalized = $this->defaultAvailability();
435 |
436 | foreach ($availability as $day => $row) {
437 | $day = (int) $day;
438 | if (! isset($normalized[$day]) || ! is_array($row)) {
439 | continue;
440 | }
441 |
442 | $normalized[$day] = [
443 | 'enabled' => isset($row['enabled']) && (string) $row['enabled'] === '1',
444 | 'start' => trim((string) ($row['start'] ?? $normalized[$day]['start'])),
445 | 'end' => trim((string) ($row['end'] ?? $normalized[$day]['end'])),
446 | ];
447 | }
448 |
449 | return $normalized;
450 | }
451 |
452 | /**
453 | * @return array<int, array<string, string|bool>>
454 | */
455 | private function defaultAvailability(): array
456 | {
457 | $days = [];
458 |
459 | for ($day = 1; $day <= 7; $day++) {
460 | $days[$day] = [
461 | 'enabled' => false,
462 | 'start' => '16:00',
463 | 'end' => '19:00',
464 | ];
465 | }
466 |
467 | return $days;
468 | }
469 |
470 | /**
471 | * @return array<int, array<string, string|bool>>
472 | */
473 | private function availabilityMap(int $coachId): array
474 | {
475 | $map = $this->defaultAvailability();
476 |
477 | foreach ($this->availabilityRows($coachId) as $row) {
478 | $day = (int) $row['day_of_week'];
479 | $map[$day] = [
480 | 'enabled' => true,
481 | 'start' => substr((string) $row['start_time'], 0, 5),
482 | 'end' => substr((string) $row['end_time'], 0, 5),
483 | ];
484 | }
485 |
486 | return $map;
487 | }
488 |
489 | private function syncAvailability(int $coachId, array $availability, string $now): void
490 | {
491 | $this->db()->table('coach_availability')->where('coach_id', $coachId)->delete();
492 |
493 | $rows = [];
494 | foreach ($availability as $day => $slot) {
495 | if (! ($slot['enabled'] ?? false)) {
496 | continue;
497 | }
498 |
499 | $rows[] = [
500 | 'coach_id' => $coachId,
501 | 'day_of_week' => (int) $day,
502 | 'start_time' => ($slot['start'] ?? '16:00') . ':00',
503 | 'end_time' => ($slot['end'] ?? '19:00') . ':00',
504 | 'is_available' => 1,
505 | 'created_at' => $now,
506 | ];
507 | }
508 |
509 | if ($rows !== []) {
510 | model(CoachAvailabilityModel::class)->insertBatch($rows);
511 | }
512 | }
513 |
514 | /**
515 | * @return array<string, mixed>|null
516 | */
517 | private function getCoachWithUser(int $coachId): ?array
518 | {
519 | return $this->db()
520 | ->table('coaches c')
521 | ->select('c.*, users.username')
522 | ->join('users', 'users.id = c.user_id', 'left')
523 | ->where('c.id', $coachId)
524 | ->get()
525 | ->getRowArray();
526 | }
527 |
528 | /**
529 | * @return list<array<string, mixed>>
530 | */
531 | private function availabilityRows(int $coachId): array
532 | {
533 | return $this->db()
534 | ->table('coach_availability')
535 | ->where('coach_id', $coachId)
536 | ->orderBy('day_of_week', 'ASC')
537 | ->get()
538 | ->getResultArray();
539 | }
540 |
541 | /**
542 | * @param list<int> $coachIds
543 | * @return array<int, list<array<string, mixed>>>
544 | */
545 | private function availabilityRowsForMany(array $coachIds): array
546 | {
547 | $grouped = [];
548 | if ($coachIds === []) {
549 | return $grouped;
550 | }
551 |
552 | $rows = $this->db()
553 | ->table('coach_availability')
554 | ->whereIn('coach_id', $coachIds)
555 | ->orderBy('day_of_week', 'ASC')
556 | ->get()
557 | ->getResultArray();
558 |
559 | foreach ($rows as $row) {
560 | $grouped[(int) $row['coach_id']][] = $row;
561 | }
562 |
563 | return $grouped;
564 | }
565 |
566 | private function db()
567 | {
568 | return db_connect();
569 | }
570 | }