> when paginate=false, * otherwise a Laravel paginator instance. * * @param array{ * status?: string|array, * next_school_year?: string|null, * current_school_year?: string|null, * parent_id?: int|null, * student_id?: int|null, * search?: string|null, * parent_action_required?: bool, * per_page?: int|null, * } $filters * * @return array{ * data: array>, * total: int, * page: int, * per_page: int, * total_pages: int, * counts_by_status: array * } */ public function list(array $filters): array { $query = StudentPromotionRecord::query(); $this->applyFilters($query, $filters); $perPage = isset($filters['per_page']) ? max(1, min(200, (int) $filters['per_page'])) : 50; $page = isset($filters['page']) ? max(1, (int) $filters['page']) : 1; /** @var LengthAwarePaginator $paginator */ $paginator = $query->orderByDesc('promotion_id')->paginate($perPage, ['*'], 'page', $page); /** @var EloquentCollection $records */ $records = $paginator->getCollection(); $studentIds = $records->pluck('student_id')->unique()->values()->all(); $parentIds = $records->pluck('parent_id')->filter()->unique()->values()->all(); $studentsById = !empty($studentIds) ? Student::query()->whereIn('id', $studentIds)->get()->keyBy('id') : new EloquentCollection(); $parentsById = !empty($parentIds) ? User::query()->whereIn('id', $parentIds)->get()->keyBy('id') : new EloquentCollection(); $rows = $records->map(function (StudentPromotionRecord $record) use ($studentsById, $parentsById) { return $this->presentAdminRow( $record, $studentsById->get($record->student_id), $record->parent_id ? $parentsById->get($record->parent_id) : null ); })->all(); return [ 'data' => array_values($rows), 'total' => (int) $paginator->total(), 'page' => (int) $paginator->currentPage(), 'per_page' => (int) $paginator->perPage(), 'total_pages' => (int) $paginator->lastPage(), 'counts_by_status' => $this->countsByStatus($filters), ]; } /** * Returns the per-status count of promotion records that match the * given filter set (ignoring `status` and pagination filters). Plan * section 16's reports rely on these counts. * * @return array */ public function countsByStatus(array $filters): array { $query = StudentPromotionRecord::query(); // Apply non-status filters only. $filtersSansStatus = $filters; unset($filtersSansStatus['status'], $filtersSansStatus['parent_action_required']); $this->applyFilters($query, $filtersSansStatus); $rows = $query ->select('promotion_status', DB::raw('count(*) as total')) ->groupBy('promotion_status') ->get(); $counts = array_fill_keys(StudentPromotionRecord::ALL_STATUSES, 0); foreach ($rows as $row) { $counts[(string) $row->promotion_status] = (int) $row->total; } return $counts; } /** * Hydrates a single record with student + parent context for the * admin detail view. */ public function detail(StudentPromotionRecord $record): array { $student = Student::query()->find($record->student_id); $parent = $record->parent_id ? User::query()->find($record->parent_id) : null; return $this->presentAdminRow($record, $student, $parent, true); } /** * @param Builder $query */ private function applyFilters(Builder $query, array $filters): void { if (!empty($filters['status'])) { $statuses = is_array($filters['status']) ? $filters['status'] : [$filters['status']]; $statuses = array_values(array_filter($statuses, static fn ($s) => is_string($s) && $s !== '')); if (!empty($statuses)) { $query->whereIn('promotion_status', $statuses); } } if (!empty($filters['parent_action_required'])) { $query->whereIn('promotion_status', StudentPromotionRecord::parentActionableStatuses()); } if (!empty($filters['next_school_year'])) { $query->where('next_school_year', $filters['next_school_year']); } if (!empty($filters['current_school_year'])) { $query->where('current_school_year', $filters['current_school_year']); } if (!empty($filters['parent_id'])) { $query->where('parent_id', (int) $filters['parent_id']); } if (!empty($filters['student_id'])) { $query->where('student_id', (int) $filters['student_id']); } if (!empty($filters['search'])) { $search = '%' . strtolower((string) $filters['search']) . '%'; $query->whereIn('student_id', function ($sub) use ($search) { $sub->select('id') ->from('students') ->where(function ($w) use ($search) { $w->whereRaw('LOWER(firstname) LIKE ?', [$search]) ->orWhereRaw('LOWER(lastname) LIKE ?', [$search]); }); }); } } private function presentAdminRow( StudentPromotionRecord $record, ?Student $student, ?User $parent, bool $detail = false ): array { $row = [ 'promotion_id' => (int) $record->getKey(), 'student_id' => (int) $record->student_id, 'student_name' => $student ? trim((string) $student->firstname . ' ' . (string) $student->lastname) : null, 'school_id' => $student->school_id ?? null, 'parent_id' => $record->parent_id ? (int) $record->parent_id : null, 'parent_name' => $parent ? trim((string) $parent->firstname . ' ' . (string) $parent->lastname) : null, 'parent_email' => $parent->email ?? null, 'current_school_year' => $record->current_school_year, 'next_school_year' => $record->next_school_year, 'current_level' => $record->current_level_name, 'promoted_level' => $record->promoted_level_name, 'promotion_status' => $record->promotion_status, 'enrollment_status' => $record->enrollment_status, 'enrollment_deadline' => $record->enrollment_deadline?->toDateString(), 'parent_notified_at' => $record->parent_notified_at?->toDateTimeString(), 'enrollment_started_at' => $record->enrollment_started_at?->toDateTimeString(), 'enrollment_completed_at' => $record->enrollment_completed_at?->toDateTimeString(), 'promotion_finalized_at' => $record->promotion_finalized_at?->toDateTimeString(), 'final_average' => $record->final_average !== null ? (float) $record->final_average : null, 'passed_current_level' => $record->passed_current_level, 'updated_by' => $record->updated_by ? (int) $record->updated_by : null, 'updated_at' => $record->updated_at?->toDateTimeString(), ]; if ($detail) { $row['checklist'] = [ 'info_confirmed' => (bool) $record->info_confirmed, 'documents_uploaded' => (bool) $record->documents_uploaded, 'agreement_accepted' => (bool) $record->agreement_accepted, 'payment_completed' => (bool) $record->payment_completed, ]; $row['eligibility_notes'] = $record->eligibility_notes; $row['enrollment_id'] = $record->enrollment_id ? (int) $record->enrollment_id : null; } return $row; } }