224 lines
8.9 KiB
PHP
224 lines
8.9 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Promotions;
|
|
|
|
use App\Models\Student;
|
|
use App\Models\StudentPromotionRecord;
|
|
use App\Models\User;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
|
use Illuminate\Pagination\LengthAwarePaginator;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
/**
|
|
* Admin-side reads against `student_promotion_records` (plan sections 13
|
|
* and 16). Always returns hydrated student / parent context so the
|
|
* admin UI can render rows without N+1 queries.
|
|
*/
|
|
class PromotionQueryService
|
|
{
|
|
/**
|
|
* Filterable list of promotion records for admin views.
|
|
*
|
|
* Filters supported:
|
|
* - status: string|array
|
|
* - next_school_year: string
|
|
* - current_school_year: string
|
|
* - parent_id: int
|
|
* - student_id: int
|
|
* - search: string (matches student first/last name)
|
|
* - parent_action_required: bool (subset of parentActionableStatuses)
|
|
*
|
|
* Returns array<int, array<string, mixed>> when paginate=false,
|
|
* otherwise a Laravel paginator instance.
|
|
*
|
|
* @param array{
|
|
* status?: string|array<int,string>,
|
|
* 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<int,array<string,mixed>>,
|
|
* total: int,
|
|
* page: int,
|
|
* per_page: int,
|
|
* total_pages: int,
|
|
* counts_by_status: array<string,int>
|
|
* }
|
|
*/
|
|
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<int,StudentPromotionRecord> $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<string,int>
|
|
*/
|
|
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<StudentPromotionRecord> $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;
|
|
}
|
|
}
|