update api and add more features
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user