440 lines
17 KiB
PHP
440 lines
17 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Grading;
|
|
|
|
use App\Models\AttendanceRecord;
|
|
use App\Models\CalendarEvent;
|
|
use App\Models\Configuration;
|
|
use App\Models\ClassSection;
|
|
use App\Models\GradingLock;
|
|
use App\Models\Student;
|
|
use App\Services\Scores\AttendanceCalculator;
|
|
use App\Services\Scores\SemesterScoreService;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class GradingOverviewService
|
|
{
|
|
private AttendanceCalculator $attendanceCalculator;
|
|
|
|
public function __construct(private SemesterScoreService $semesterScoreService)
|
|
{
|
|
$this->attendanceCalculator = new AttendanceCalculator(
|
|
new AttendanceRecord(),
|
|
new Configuration(),
|
|
new CalendarEvent()
|
|
);
|
|
}
|
|
|
|
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');
|
|
|
|
if ($classId && $this->semesterScoreService) {
|
|
$sectionIds = ClassSection::query()
|
|
->select('class_section_id')
|
|
->where('class_id', $classId)
|
|
->pluck('class_section_id')
|
|
->map(fn ($v) => (int) $v)
|
|
->all();
|
|
foreach ($sectionIds as $sectionId) {
|
|
$studentTeacherInfo = Student::getStudentInfoByClassSectionId(
|
|
$sectionId,
|
|
$semester,
|
|
$schoolYear
|
|
);
|
|
if (empty($studentTeacherInfo)) {
|
|
continue;
|
|
}
|
|
try {
|
|
$this->semesterScoreService->updateScoresForStudents($studentTeacherInfo, $semester, $schoolYear);
|
|
} catch (\Throwable $e) {
|
|
}
|
|
}
|
|
}
|
|
|
|
$rows = $this->buildGradingRows($semester, $schoolYear);
|
|
|
|
$counts = $this->loadScoreCounts($rows, $semester, $schoolYear);
|
|
|
|
$sectionsNeedingRefresh = [];
|
|
foreach ($rows as $row) {
|
|
$sectionId = (int) ($row['section_id'] ?? 0);
|
|
if ($sectionId <= 0) {
|
|
continue;
|
|
}
|
|
if ($row['ss_ptap_score'] === null || $row['ss_semester_score'] === null) {
|
|
$sectionsNeedingRefresh[$sectionId] = true;
|
|
}
|
|
}
|
|
|
|
if (!empty($sectionsNeedingRefresh)) {
|
|
foreach (array_keys($sectionsNeedingRefresh) as $sectionId) {
|
|
$studentTeacherInfo = Student::getStudentInfoByClassSectionId(
|
|
$sectionId,
|
|
$semester,
|
|
$schoolYear
|
|
);
|
|
if (empty($studentTeacherInfo)) {
|
|
continue;
|
|
}
|
|
try {
|
|
$this->semesterScoreService->updateScoresForStudents($studentTeacherInfo, $semester, $schoolYear);
|
|
} catch (\Throwable $e) {
|
|
}
|
|
}
|
|
$rows = $this->buildGradingRows($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->calculateAttendanceScoreForStudent(
|
|
$studentId,
|
|
$semester,
|
|
$schoolYear,
|
|
$sectionId
|
|
);
|
|
if ($attendanceScore === null) {
|
|
$rawAttendance = $row['ss_attendance_score'] ?? null;
|
|
if ($rawAttendance !== null && $rawAttendance !== '') {
|
|
$attendanceScore = round((float) $rawAttendance, 2);
|
|
}
|
|
}
|
|
|
|
$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 calculateAttendanceScoreForStudent(int $studentId, string $semester, string $schoolYear, ?int $classSectionId = null): ?float
|
|
{
|
|
try {
|
|
$result = $this->attendanceCalculator->calculate($studentId, $semester, $schoolYear, $classSectionId);
|
|
$score = $result['attendance_score'] ?? null;
|
|
if ($score === null || $score === '') {
|
|
return null;
|
|
}
|
|
return round((float) $score, 2);
|
|
} catch (\Throwable $e) {
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
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');
|
|
}
|
|
}
|
|
}
|