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, ]; } }