Files
alrahma_sunday_school_api/app/Services/Grading/GradingOverviewService.php
T
2026-06-09 01:03:53 -04:00

348 lines
14 KiB
PHP

<?php
namespace App\Services\Grading;
use App\Models\Configuration;
use App\Models\GradingLock;
use Illuminate\Support\Facades\DB;
class GradingOverviewService
{
public function __construct()
{
}
public function overview(?int $classId, ?string $semester, ?string $schoolYear): array
{
$schoolYear = $schoolYear ?: (string) (Configuration::getConfig('school_year') ?? '');
$configuredSemester = (string) (Configuration::getConfig('semester') ?? '');
$semesterOptions = $this->getSemestersForSchoolYear($schoolYear, $configuredSemester);
$semester = $this->resolveSemesterSelection($semester, $semesterOptions, $configuredSemester);
$this->ensureParentReleaseKeyExists('Fall');
$this->ensureParentReleaseKeyExists('Spring');
$scoresReleased = $this->getParentScoresReleasedForSemester($semester);
$scoresReleasedFall = $this->getParentScoresReleasedForSemester('Fall');
$scoresReleasedSpring = $this->getParentScoresReleasedForSemester('Spring');
$rows = $this->buildGradingRows($semester, $schoolYear);
$counts = $this->loadScoreCounts($rows, $semester, $schoolYear);
$grades = [];
$studentsBySection = [];
foreach ($rows as $row) {
$sectionId = (int) ($row['section_id'] ?? 0);
$classIdValue = (int) ($row['class_id'] ?? 0);
$sectionName = (string) ($row['class_section_name'] ?? '');
if ($sectionId <= 0 || $classIdValue <= 0) {
continue;
}
if (!isset($grades[$classIdValue])) {
$grades[$classIdValue] = [];
}
$exists = false;
foreach ($grades[$classIdValue] as $section) {
if ((int) $section['class_section_id'] === $sectionId) {
$exists = true;
break;
}
}
if (!$exists) {
$grades[$classIdValue][] = [
'class_section_id' => $sectionId,
'class_section_name' => $sectionName,
];
}
$studentId = (int) ($row['student_id'] ?? 0);
if ($studentId <= 0) {
continue;
}
$attendanceScore = $this->normalizeScore($row['ss_attendance_score'] ?? null);
$studentRow = [
'id' => $studentId,
'school_id' => $row['school_id'] ?? null,
'firstname' => $row['firstname'] ?? null,
'lastname' => $row['lastname'] ?? null,
'class_id' => $classIdValue,
'ptap' => $this->normalizeScore($row['ss_ptap_score'] ?? null),
'semester_score' => $this->normalizeScore($row['ss_semester_score'] ?? null),
'attendance' => $attendanceScore,
'homework_avg' => $this->normalizeCountedScore($row['ss_homework_avg'] ?? null, $counts['homework'] ?? [], $sectionId, $studentId),
'project_avg' => $this->normalizeCountedScore($row['ss_project_avg'] ?? null, $counts['project'] ?? [], $sectionId, $studentId),
'quiz_avg' => $this->normalizeCountedScore($row['ss_quiz_avg'] ?? null, $counts['quiz'] ?? [], $sectionId, $studentId),
'participation' => $this->normalizeCountedScore($row['ss_participation_score'] ?? null, $counts['participation'] ?? [], $sectionId, $studentId),
'midterm_exam' => $this->normalizeCountedScore($row['ss_midterm_exam_score'] ?? null, $counts['midterm'] ?? [], $sectionId, $studentId),
'final_exam' => $this->normalizeScore($row['ss_final_exam_score'] ?? null),
'matched_biz_csid' => $row['matched_biz_csid'] ?? null,
'matched_pk_csid' => $row['matched_pk_csid'] ?? null,
'placement_level' => $row['placement_level'] ?? null,
];
$studentsBySection[$sectionId][] = $studentRow;
}
$scoreLocks = $this->loadScoreLocks($grades, $semester, $schoolYear);
return [
'grades' => $grades,
'students_by_section' => $studentsBySection,
'semester' => $semester,
'school_year' => $schoolYear,
'requested_class_id' => $classId,
'semester_options' => $semesterOptions,
'scores_released' => $scoresReleased,
'scores_released_fall' => $scoresReleasedFall,
'scores_released_spring' => $scoresReleasedSpring,
'score_locks' => $scoreLocks,
];
}
private function buildGradingRows(string $semester, string $schoolYear): array
{
$semLower = strtolower(trim($semester));
return DB::table('student_class as sc')
->select([
'cs.id as section_pk',
'cs.class_section_id as section_id',
'cs.class_id as class_id',
'cs.class_section_name',
's.id as student_id',
's.school_id',
's.firstname',
's.lastname',
'pl.level as placement_level',
DB::raw('COALESCE(ss_b.ptap_score, ss_p.ptap_score) as ss_ptap_score'),
DB::raw('COALESCE(ss_b.semester_score, ss_p.semester_score) as ss_semester_score'),
DB::raw('COALESCE(ss_b.attendance_score, ss_p.attendance_score) as ss_attendance_score'),
DB::raw('COALESCE(ss_b.homework_avg, ss_p.homework_avg) as ss_homework_avg'),
DB::raw('COALESCE(ss_b.project_avg, ss_p.project_avg) as ss_project_avg'),
DB::raw('COALESCE(ss_b.quiz_avg, ss_p.quiz_avg) as ss_quiz_avg'),
DB::raw('COALESCE(ss_b.participation_score, ss_p.participation_score) as ss_participation_score'),
DB::raw('COALESCE(ss_b.midterm_exam_score, ss_p.midterm_exam_score) as ss_midterm_exam_score'),
DB::raw('COALESCE(ss_b.final_exam_score, ss_p.final_exam_score) as ss_final_exam_score'),
'ss_b.class_section_id as matched_biz_csid',
'ss_p.class_section_id as matched_pk_csid',
])
->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'sc.class_section_id')
->join('students as s', 's.id', '=', 'sc.student_id')
->leftJoin('placement_levels as pl', function ($join) use ($schoolYear) {
$join->on('pl.student_id', '=', 's.id')
->where('pl.school_year', '=', $schoolYear);
})
->leftJoin('semester_scores as ss_b', function ($join) use ($semLower, $schoolYear) {
$join->on('ss_b.student_id', '=', 's.id')
->on('ss_b.class_section_id', '=', 'sc.class_section_id')
->whereRaw('LOWER(TRIM(ss_b.semester)) = ?', [$semLower])
->where('ss_b.school_year', '=', $schoolYear);
})
->leftJoin('semester_scores as ss_p', function ($join) use ($semLower, $schoolYear) {
$join->on('ss_p.student_id', '=', 's.id')
->on('ss_p.class_section_id', '=', 'cs.id')
->whereRaw('LOWER(TRIM(ss_p.semester)) = ?', [$semLower])
->where('ss_p.school_year', '=', $schoolYear);
})
->where('sc.school_year', $schoolYear)
->where('s.is_active', 1)
->orderBy('cs.class_id', 'ASC')
->orderBy('cs.class_section_name', 'ASC')
->orderBy('s.lastname', 'ASC')
->orderBy('s.firstname', 'ASC')
->get()
->map(fn ($row) => (array) $row)
->all();
}
private function loadScoreCounts(array $rows, string $semester, string $schoolYear): array
{
$sectionIds = [];
$studentIds = [];
foreach ($rows as $row) {
$studentId = (int) ($row['student_id'] ?? 0);
$sectionId = (int) ($row['section_id'] ?? 0);
if ($studentId > 0) {
$studentIds[$studentId] = true;
}
if ($sectionId > 0) {
$sectionIds[$sectionId] = true;
}
}
$sectionIds = array_keys($sectionIds);
$studentIds = array_keys($studentIds);
if (empty($sectionIds) || empty($studentIds)) {
return [
'quiz' => [],
'homework' => [],
'project' => [],
'participation' => [],
'midterm' => [],
];
}
return [
'quiz' => $this->countScores('quiz', $sectionIds, $studentIds, $semester, $schoolYear),
'homework' => $this->countScores('homework', $sectionIds, $studentIds, $semester, $schoolYear),
'project' => $this->countScores('project', $sectionIds, $studentIds, $semester, $schoolYear),
'participation' => $this->countScores('participation', $sectionIds, $studentIds, $semester, $schoolYear),
'midterm' => $this->countScores('midterm_exam', $sectionIds, $studentIds, $semester, $schoolYear),
];
}
private function countScores(string $table, array $sectionIds, array $studentIds, string $semester, string $schoolYear): array
{
$rows = DB::table($table)
->select('student_id', 'class_section_id', DB::raw('COUNT(*) as cnt'))
->where('semester', $semester)
->where('school_year', $schoolYear)
->whereIn('class_section_id', $sectionIds)
->whereIn('student_id', $studentIds)
->whereNotNull('score')
->groupBy('student_id', 'class_section_id')
->get();
$counts = [];
foreach ($rows as $row) {
$counts[(int) $row->class_section_id][(int) $row->student_id] = (int) $row->cnt;
}
return $counts;
}
private function loadScoreLocks(array $grades, string $semester, string $schoolYear): array
{
$sectionIds = [];
foreach ($grades as $sections) {
foreach ($sections as $section) {
$sid = (int) ($section['class_section_id'] ?? 0);
if ($sid > 0) {
$sectionIds[$sid] = true;
}
}
}
$sectionIds = array_keys($sectionIds);
if (empty($sectionIds)) {
return [];
}
$locks = GradingLock::query()
->whereIn('class_section_id', $sectionIds)
->where('semester', $semester)
->where('school_year', $schoolYear)
->get();
$map = [];
foreach ($locks as $lock) {
$map[(int) $lock->class_section_id] = (bool) $lock->is_locked;
}
return $map;
}
private function normalizeScore($value): ?float
{
if ($value === null || $value === '') {
return null;
}
return round((float) $value, 2);
}
private function normalizeCountedScore($value, array $countMap, int $sectionId, int $studentId): ?float
{
$score = $this->normalizeScore($value);
if ($score !== null && (float) $score === 0.0) {
$count = (int) ($countMap[$sectionId][$studentId] ?? 0);
if ($count === 0) {
return null;
}
}
return $score;
}
private function resolveSemesterSelection(?string $requestedSemester, array $semesterOptions, ?string $fallbackSemester): string
{
if ($requestedSemester !== null && $requestedSemester !== '') {
foreach ($semesterOptions as $option) {
if (strcasecmp($option, $requestedSemester) === 0) {
return $option;
}
}
return $requestedSemester;
}
if ($fallbackSemester !== null && $fallbackSemester !== '') {
foreach ($semesterOptions as $option) {
if (strcasecmp($option, $fallbackSemester) === 0) {
return $option;
}
}
return $fallbackSemester;
}
return $semesterOptions[0] ?? '';
}
private function getSemestersForSchoolYear(string $schoolYear, ?string $fallbackSemester = null): array
{
$rows = DB::table('semester_scores')
->select(DB::raw('DISTINCT semester'))
->whereNotNull('semester')
->where('semester', '!=', '')
->where('school_year', $schoolYear)
->orderBy('semester', 'ASC')
->get();
$semesters = [];
foreach ($rows as $row) {
$value = trim((string) $row->semester);
if ($value !== '') {
$semesters[] = $value;
}
}
if (empty($semesters)) {
$semesters = ['Fall', 'Spring'];
}
if ($fallbackSemester !== null && $fallbackSemester !== '' && !in_array($fallbackSemester, $semesters, true)) {
array_unshift($semesters, $fallbackSemester);
}
return array_values(array_unique($semesters));
}
private function getParentReleaseKey(string $semester): ?string
{
$norm = strtolower(trim($semester));
if ($norm === 'fall') {
return 'parent_scores_released_fall';
}
if ($norm === 'spring') {
return 'parent_scores_released_spring';
}
return null;
}
private function getParentScoresReleasedForSemester(string $semester): bool
{
$key = $this->getParentReleaseKey($semester);
$raw = $key ? Configuration::getConfig($key) : null;
$raw = (string) ($raw ?? '');
return in_array(strtolower(trim($raw)), ['1', 'true', 'yes', 'y', 'on'], true);
}
private function ensureParentReleaseKeyExists(string $semester): void
{
$key = $this->getParentReleaseKey($semester);
if (!$key) {
return;
}
if (Configuration::getConfigValueByKey($key) === null) {
Configuration::setConfigValueByKey($key, '0');
}
}
}