Files
alrahma_sunday_school_api/app/Services/Students/StudentAutoDistributionService.php
T
2026-06-09 00:03:03 -04:00

257 lines
9.0 KiB
PHP

<?php
namespace App\Services\Students;
use App\Models\ClassSection;
use App\Models\PromotionQueue;
use App\Models\StudentClass;
use Illuminate\Support\Facades\DB;
class StudentAutoDistributionService
{
public function __construct(private StudentConfigService $configService) {}
public function promotionTotals(?string $schoolYear = null): array
{
$context = $this->configService->context();
$year = $schoolYear ?: (string) ($context['school_year'] ?? '');
$bases = ClassSection::query()
->whereRaw("class_section_name NOT LIKE '%-%'")
->orderBy('class_id')
->get()
->toArray();
$wanted = [];
foreach ($bases as $row) {
$nameRaw = (string) ($row['class_section_name'] ?? '');
$name = strtolower($nameRaw);
if ($name === 'kg' || $name === 'youth') {
$wanted[] = $row;
continue;
}
if (ctype_digit($name)) {
$num = (int) $name;
if ($num >= 1 && $num <= 9) {
$wanted[] = $row;
}
}
}
$rows = [];
foreach ($wanted as $row) {
$classId = (int) ($row['class_id'] ?? 0);
if ($classId <= 0) {
continue;
}
$cands = DB::table('promotion_queue as pq')
->select('pq.student_id')
->join('enrollments as e', function ($join) use ($year) {
$join->on('e.student_id', '=', 'pq.student_id')
->where('e.school_year', '=', $year);
})
->where('pq.to_class_id', $classId)
->where('pq.school_year_to', $year)
->whereIn('pq.status', ['queued', 'assigned', 'applied'])
->whereIn('e.enrollment_status', ['payment pending', 'enrolled'])
->groupBy('pq.student_id')
->get();
$rows[] = [
'class_id' => $classId,
'class_section_id' => (int) ($row['class_section_id'] ?? 0),
'class_section_name' => (string) ($row['class_section_name'] ?? ''),
'total' => $cands->count(),
];
}
return [
'year' => $year,
'rows' => $rows,
];
}
public function autoDistribute(int $classId, int $studentsPerSection, ?string $schoolYear = null, ?int $classSectionId = null, ?int $userId = null): array
{
$context = $this->configService->context();
$year = $schoolYear ?: (string) ($context['school_year'] ?? '');
$semester = (string) ($context['semester'] ?? '');
if ($classId <= 0 && $classSectionId > 0) {
$classId = (int) (ClassSection::getClassId($classSectionId) ?? 0);
}
if ($classId <= 0 || $studentsPerSection <= 0) {
return ['ok' => false, 'message' => 'Invalid class_id or students_per_section.'];
}
$cands = DB::table('promotion_queue as pq')
->select('pq.*', 'students.gender')
->join('students', 'students.id', '=', 'pq.student_id')
->where('pq.to_class_id', $classId)
->where('pq.school_year_to', $year)
->whereIn('pq.status', ['queued', 'assigned'])
->get()
->map(fn ($r) => (array) $r)
->all();
if (empty($cands)) {
return ['ok' => false, 'message' => 'No students found in promotion queue for selected class/year.'];
}
$studentIds = array_map(static fn ($r) => (int) $r['student_id'], $cands);
$enrolledIds = DB::table('enrollments')
->select('student_id')
->whereIn('student_id', $studentIds)
->where('school_year', $year)
->whereIn('enrollment_status', ['payment pending', 'enrolled'])
->groupBy('student_id')
->pluck('student_id')
->map(fn ($v) => (int) $v)
->all();
$cands = array_values(array_filter($cands, static function ($row) use ($enrolledIds) {
return in_array((int) $row['student_id'], $enrolledIds, true);
}));
if (empty($cands)) {
return ['ok' => false, 'message' => 'No eligible enrolled students found to distribute.'];
}
$sectionsNeeded = (int) ceil(count($cands) / $studentsPerSection);
$letters = ClassSection::getLetterSectionsByClassId($classId);
if (empty($letters)) {
return ['ok' => false, 'message' => 'No lettered sections found for the selected class.'];
}
if (count($letters) < $sectionsNeeded) {
return ['ok' => false, 'message' => 'Not enough sections available.'];
}
$letters = array_slice($letters, 0, $sectionsNeeded);
$buckets = [];
foreach ($letters as $idx => $section) {
$buckets[$idx] = [
'class_section_id' => (int) ($section['class_section_id'] ?? 0),
'assigned' => [],
'male' => 0,
'female' => 0,
];
}
$males = [];
$females = [];
foreach ($cands as $row) {
$gender = (string) ($row['gender'] ?? '');
if (strcasecmp($gender, 'Female') === 0) {
$females[] = $row;
} else {
$males[] = $row;
}
}
$pickBucket = function (string $gender) use (&$buckets, $studentsPerSection): ?int {
$bestIdx = null;
$bestCnt = PHP_INT_MAX;
foreach ($buckets as $idx => $bucket) {
if (count($bucket['assigned']) >= $studentsPerSection) {
continue;
}
$cnt = $gender === 'female' ? $bucket['female'] : $bucket['male'];
if ($cnt < $bestCnt) {
$bestCnt = $cnt;
$bestIdx = $idx;
}
}
return $bestIdx;
};
foreach ($males as $row) {
$idx = $pickBucket('male');
if ($idx === null) {
break;
}
$buckets[$idx]['assigned'][] = (int) $row['student_id'];
$buckets[$idx]['male']++;
}
foreach ($females as $row) {
$idx = $pickBucket('female');
if ($idx === null) {
break;
}
$buckets[$idx]['assigned'][] = (int) $row['student_id'];
$buckets[$idx]['female']++;
}
$promoIdsByStudent = [];
foreach ($cands as $row) {
$promoIdsByStudent[(int) $row['student_id']] = (int) $row['id'];
}
DB::transaction(function () use ($buckets, $promoIdsByStudent, $year, $semester, $userId): void {
foreach ($buckets as $bucket) {
$sectionId = (int) $bucket['class_section_id'];
foreach ($bucket['assigned'] as $studentId) {
if (isset($promoIdsByStudent[$studentId])) {
PromotionQueue::query()->where('id', $promoIdsByStudent[$studentId])->update([
'to_class_section_id' => $sectionId,
'status' => 'assigned',
'updated_by' => $userId,
'updated_at' => now(),
]);
}
$exists = StudentClass::query()
->where('student_id', $studentId)
->where('school_year', $year)
->when($semester !== '', fn ($q) => $q->where('semester', $semester))
->first();
$payload = [
'student_id' => $studentId,
'class_section_id' => $sectionId,
'school_year' => $year,
'semester' => $semester,
'updated_by' => $userId,
'updated_at' => now(),
];
if ($exists) {
$exists->update($payload);
} else {
StudentClass::query()->create($payload + ['created_at' => now()]);
}
}
}
});
$nameById = [];
foreach ($letters as $row) {
$nameById[(int) $row['class_section_id']] = (string) ($row['class_section_name'] ?? '');
}
$summary = [];
foreach ($buckets as $bucket) {
$sectionId = (int) $bucket['class_section_id'];
$summary[] = [
'class_section_id' => $sectionId,
'class_section_name' => $nameById[$sectionId] ?? (string) $sectionId,
'total' => count($bucket['assigned']),
'male' => $bucket['male'],
'female' => $bucket['female'],
];
}
return [
'ok' => true,
'message' => 'Auto distribution completed.',
'sections' => $summary,
];
}
}