257 lines
9.0 KiB
PHP
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,
|
|
];
|
|
}
|
|
}
|