1458 lines
52 KiB
PHP
1458 lines
52 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\AttendanceRecordModel;
|
|
use App\Models\CalendarModel;
|
|
use App\Models\ClassSectionModel;
|
|
use App\Models\ConfigurationModel;
|
|
use App\Models\CurrentFlagModel;
|
|
use App\Models\FinalExamModel;
|
|
use App\Models\GradingLockModel;
|
|
use App\Models\HomeworkModel;
|
|
use App\Models\MidtermExamModel;
|
|
use App\Models\ProjectModel;
|
|
use App\Models\QuizModel;
|
|
use App\Models\ScoreCommentModel;
|
|
use App\Models\SemesterScoreModel;
|
|
use App\Models\StudentClassModel;
|
|
use App\Models\StudentModel;
|
|
use App\Models\TeacherClassModel;
|
|
use App\Models\UserModel;
|
|
use App\Services\Calculators\AttendanceCalculator;
|
|
use App\Services\NavbarService;
|
|
use CodeIgniter\Events\Events;
|
|
|
|
class GradingScoreService
|
|
{
|
|
protected $db;
|
|
protected $configModel;
|
|
protected $homeworkModel;
|
|
protected $userModel;
|
|
protected $studentClassModel;
|
|
protected $studentModel;
|
|
protected $teacherClassModel;
|
|
protected $classSection;
|
|
protected $attendanceCalculator;
|
|
protected $gradingLockModel;
|
|
protected $semesterScoreService;
|
|
protected string $schoolYear = '';
|
|
protected string $semester = '';
|
|
|
|
public function __construct(
|
|
\CodeIgniter\Database\BaseConnection $db,
|
|
ConfigurationModel $configModel,
|
|
HomeworkModel $homeworkModel,
|
|
UserModel $userModel,
|
|
StudentClassModel $studentClassModel,
|
|
StudentModel $studentModel,
|
|
TeacherClassModel $teacherClassModel,
|
|
ClassSectionModel $classSection,
|
|
AttendanceCalculator $attendanceCalculator,
|
|
GradingLockModel $gradingLockModel,
|
|
$semesterScoreService,
|
|
string $schoolYear = '',
|
|
string $semester = ''
|
|
) {
|
|
$this->db = $db;
|
|
$this->configModel = $configModel;
|
|
$this->homeworkModel = $homeworkModel;
|
|
$this->userModel = $userModel;
|
|
$this->studentClassModel = $studentClassModel;
|
|
$this->studentModel = $studentModel;
|
|
$this->teacherClassModel = $teacherClassModel;
|
|
$this->classSection = $classSection;
|
|
$this->attendanceCalculator = $attendanceCalculator;
|
|
$this->gradingLockModel = $gradingLockModel;
|
|
$this->semesterScoreService = $semesterScoreService;
|
|
$this->schoolYear = $schoolYear;
|
|
$this->semester = $semester;
|
|
}
|
|
|
|
public function setTerm(string $schoolYear, string $semester): void
|
|
{
|
|
$this->schoolYear = $schoolYear;
|
|
$this->semester = $semester;
|
|
}
|
|
|
|
public function showType($type, $classSectionId, $studentId, array $params = [])
|
|
{
|
|
$get = $params['get'] ?? [];
|
|
$post = $params['post'] ?? [];
|
|
|
|
$scoreModel = $this->getModelByType($type);
|
|
$studentModel = new StudentModel();
|
|
$configModel = new ConfigurationModel();
|
|
|
|
$schoolYear = $configModel->getConfig('school_year');
|
|
$semester = getSemester();
|
|
|
|
$student = $studentModel->find($studentId);
|
|
$scores = $scoreModel->where([
|
|
'student_id' => $studentId,
|
|
'semester' => $semester,
|
|
'school_year' => $schoolYear
|
|
])->findAll();
|
|
$scoresLocked = false;
|
|
$classSectionIdInt = (int) ($classSectionId ?? 0);
|
|
if ($classSectionIdInt > 0) {
|
|
$scoresLocked = $this->gradingLockModel->isLocked($classSectionIdInt, $semester, $schoolYear);
|
|
}
|
|
|
|
return ['kind' => 'view', 'view' => "grading/{$type}", 'data' => [
|
|
'student' => $student,
|
|
'scores' => $scores,
|
|
'type' => $type,
|
|
'classSectionId' => $classSectionId, // ✅ pass it manually
|
|
'semester' => $semester, // ✅ Pass semester to the view
|
|
'scoresLocked' => $scoresLocked,
|
|
]];
|
|
}
|
|
|
|
public function updateScores(array $params = [])
|
|
{
|
|
$get = $params['get'] ?? [];
|
|
$post = $params['post'] ?? [];
|
|
|
|
$type = ($post['type'] ?? null);
|
|
$studentId = ($post['student_id'] ?? null);
|
|
$classSectionId = ($post['class_section_id'] ?? null);
|
|
$configModel = new ConfigurationModel();
|
|
$studentModel = new StudentModel();
|
|
|
|
$schoolYear = $configModel->getConfig('school_year');
|
|
$semester = getSemester();
|
|
|
|
$model = $this->getModelByType($type);
|
|
$classSectionIdInt = (int) ($classSectionId ?? 0);
|
|
if ($classSectionIdInt > 0 && $this->isScoresLocked($classSectionIdInt, $semester, $schoolYear)) {
|
|
return ['kind' => 'flash', 'redirect' => 'back', 'type' => 'error', 'message' => 'Scores are locked for this class. Unlock to edit.'];
|
|
}
|
|
|
|
if (in_array($type, ['homework', 'quiz', 'project'])) {
|
|
$scoreIds = ($post['score_ids'] ?? null);
|
|
$scores = ($post['scores'] ?? null);
|
|
$comments = ($post['comments'] ?? null);
|
|
|
|
foreach ($scoreIds as $i => $id) {
|
|
$model->update($id, [
|
|
'score' => $scores[$i],
|
|
'comment' => $comments[$i] ?? null,
|
|
'updated_at' => utc_now()
|
|
]);
|
|
}
|
|
} elseif (in_array($type, ['midterm', 'final', 'test'])) {
|
|
$score = ($post['score'] ?? null);
|
|
|
|
$data = [
|
|
'score' => $score,
|
|
'updated_at' => utc_now()
|
|
];
|
|
|
|
$existing = $model->where([
|
|
'student_id' => $studentId,
|
|
'class_section_id' => $classSectionId,
|
|
'semester' => $semester,
|
|
'school_year' => $schoolYear
|
|
])->first();
|
|
|
|
if ($existing) {
|
|
$model->update($existing['id'], $data);
|
|
} else {
|
|
$data += [
|
|
'student_id' => $studentId,
|
|
'class_section_id' => $classSectionId,
|
|
'semester' => $semester,
|
|
'school_year' => $schoolYear,
|
|
'created_at' => utc_now()
|
|
];
|
|
$model->insert($data);
|
|
}
|
|
} elseif ($type === 'comments') {
|
|
$comment = ($post['comment'] ?? null);
|
|
|
|
$model->where([
|
|
'student_id' => $studentId,
|
|
'semester' => $semester,
|
|
'school_year' => $schoolYear
|
|
])->delete(); // Remove existing comments of this type (optional)
|
|
|
|
$model->insert([
|
|
'student_id' => $studentId,
|
|
'score_type' => 'general',
|
|
'semester' => $semester,
|
|
'school_year' => $schoolYear,
|
|
'comment' => $comment,
|
|
'commented_by' => session()->get('user_id'),
|
|
'created_at' => utc_now()
|
|
]);
|
|
}
|
|
|
|
$studentTeacherInfo = $studentModel->getStudentInfoByClassSectionId($classSectionId, $semester, $schoolYear);
|
|
// Call the updateScoresForStudents method
|
|
try {
|
|
|
|
$this->semesterScoreService->updateScoresForStudents($studentTeacherInfo, $semester, $schoolYear);
|
|
} catch (RuntimeException $e) {
|
|
// Handle error
|
|
}
|
|
return ['kind' => 'flash', 'redirect' => 'back', 'type' => 'status', 'message' => 'Scores updated successfully.'];
|
|
}
|
|
|
|
public function gradingPage(array $params = [])
|
|
{
|
|
$get = $params['get'] ?? [];
|
|
$post = $params['post'] ?? [];
|
|
|
|
$schoolYear = (string) $this->schoolYear;
|
|
$configuredSemester = (string) $this->semester;
|
|
$requestedClassId = (int) (($get['class_id'] ?? null) ?? 0);
|
|
$semesterOptions = $this->getSemestersForSchoolYear($schoolYear, $configuredSemester);
|
|
$session = session();
|
|
$requestedSemester = $this->normalizeSemesterInput(($get['semester'] ?? null));
|
|
$sessionSemester = $this->normalizeSemesterInput($session->get('grading_selected_semester'));
|
|
$effectiveRequested = $requestedSemester ?? $sessionSemester;
|
|
$semester = trim($this->resolveSemesterSelection($effectiveRequested, $semesterOptions, $configuredSemester));
|
|
if ($semester === '') {
|
|
$semester = $configuredSemester !== '' ? $configuredSemester : ($semesterOptions[0] ?? 'Fall');
|
|
}
|
|
if (!in_array($semester, $semesterOptions, true)) {
|
|
$semesterOptions[] = $semester;
|
|
}
|
|
$semesterOptions = array_values(array_unique($semesterOptions));
|
|
$session->set('grading_selected_semester', $semester);
|
|
|
|
$this->ensureParentReleaseKeyExists('Fall');
|
|
$this->ensureParentReleaseKeyExists('Spring');
|
|
$scoresReleased = $this->getParentScoresReleasedForSemester($semester);
|
|
$scoresReleasedFall = $this->getParentScoresReleasedForSemester('Fall');
|
|
$scoresReleasedSpring = $this->getParentScoresReleasedForSemester('Spring');
|
|
|
|
// Refresh PTAP/semester scores for the requested class (if provided) so values are present.
|
|
if ($requestedClassId > 0 && $this->semesterScoreService !== null) {
|
|
$sectionIds = $this->classSection
|
|
->select('class_section_id')
|
|
->where('class_id', $requestedClassId)
|
|
->findAll();
|
|
|
|
$sectionIds = array_values(array_filter(array_map(
|
|
static fn($row) => (int)($row['class_section_id'] ?? 0),
|
|
$sectionIds
|
|
), static fn($id) => $id > 0));
|
|
|
|
foreach ($sectionIds as $sectionId) {
|
|
$studentTeacherInfo = $this->studentModel->getStudentInfoByClassSectionId(
|
|
$sectionId,
|
|
$semester,
|
|
$schoolYear
|
|
);
|
|
if (empty($studentTeacherInfo)) {
|
|
continue;
|
|
}
|
|
try {
|
|
$this->semesterScoreService->updateScoresForStudents($studentTeacherInfo, $semester, $schoolYear);
|
|
} catch (\Throwable $e) {
|
|
log_message(
|
|
'error',
|
|
'GradingController::grading score refresh failed for section '
|
|
. $sectionId . ': ' . $e->getMessage()
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Normalize the semester text for safe comparison
|
|
$semEsc = $this->db->escape($semester);
|
|
$yrEsc = $this->db->escape($schoolYear);
|
|
|
|
$rows = $this->buildGradingRows($semEsc, $yrEsc, $schoolYear, $semester);
|
|
|
|
// Preload quiz/homework/project/participation/midterm score counts to distinguish true zeros from empty scores
|
|
$quizCounts = [];
|
|
$homeworkCounts = [];
|
|
$projectCounts = [];
|
|
$participationCounts = [];
|
|
$midtermCounts = [];
|
|
if (!empty($rows)) {
|
|
$sectionIds = [];
|
|
$studentIds = [];
|
|
foreach ($rows as $r) {
|
|
$sid = (int) ($r['student_id'] ?? 0);
|
|
$sec = (int) ($r['section_id'] ?? 0);
|
|
if ($sid > 0) $studentIds[$sid] = true;
|
|
if ($sec > 0) $sectionIds[$sec] = true;
|
|
}
|
|
$sectionIds = array_keys($sectionIds);
|
|
$studentIds = array_keys($studentIds);
|
|
|
|
if (!empty($sectionIds) && !empty($studentIds)) {
|
|
$quizRows = $this->db->table('quiz')
|
|
->select('student_id, class_section_id, COUNT(*) AS cnt')
|
|
->where('semester', $semester)
|
|
->where('school_year', $schoolYear)
|
|
->whereIn('class_section_id', $sectionIds)
|
|
->whereIn('student_id', $studentIds)
|
|
->where('score IS NOT NULL', null, false)
|
|
->groupBy('student_id, class_section_id')
|
|
->get()->getResultArray();
|
|
|
|
foreach ($quizRows as $qr) {
|
|
$sec = (int) ($qr['class_section_id'] ?? 0);
|
|
$sid = (int) ($qr['student_id'] ?? 0);
|
|
if ($sec > 0 && $sid > 0) {
|
|
$quizCounts[$sec][$sid] = (int) ($qr['cnt'] ?? 0);
|
|
}
|
|
}
|
|
|
|
$hwRows = $this->db->table('homework')
|
|
->select('student_id, class_section_id, COUNT(*) AS cnt')
|
|
->where('semester', $semester)
|
|
->where('school_year', $schoolYear)
|
|
->whereIn('class_section_id', $sectionIds)
|
|
->whereIn('student_id', $studentIds)
|
|
->where('score IS NOT NULL', null, false)
|
|
->groupBy('student_id, class_section_id')
|
|
->get()->getResultArray();
|
|
|
|
foreach ($hwRows as $hr) {
|
|
$sec = (int) ($hr['class_section_id'] ?? 0);
|
|
$sid = (int) ($hr['student_id'] ?? 0);
|
|
if ($sec > 0 && $sid > 0) {
|
|
$homeworkCounts[$sec][$sid] = (int) ($hr['cnt'] ?? 0);
|
|
}
|
|
}
|
|
|
|
$projectRows = $this->db->table('project')
|
|
->select('student_id, class_section_id, COUNT(*) AS cnt')
|
|
->where('semester', $semester)
|
|
->where('school_year', $schoolYear)
|
|
->whereIn('class_section_id', $sectionIds)
|
|
->whereIn('student_id', $studentIds)
|
|
->where('score IS NOT NULL', null, false)
|
|
->groupBy('student_id, class_section_id')
|
|
->get()->getResultArray();
|
|
|
|
foreach ($projectRows as $pr) {
|
|
$sec = (int) ($pr['class_section_id'] ?? 0);
|
|
$sid = (int) ($pr['student_id'] ?? 0);
|
|
if ($sec > 0 && $sid > 0) {
|
|
$projectCounts[$sec][$sid] = (int) ($pr['cnt'] ?? 0);
|
|
}
|
|
}
|
|
|
|
$participationRows = $this->db->table('participation')
|
|
->select('student_id, class_section_id, COUNT(*) AS cnt')
|
|
->where('semester', $semester)
|
|
->where('school_year', $schoolYear)
|
|
->whereIn('class_section_id', $sectionIds)
|
|
->whereIn('student_id', $studentIds)
|
|
->where('score IS NOT NULL', null, false)
|
|
->groupBy('student_id, class_section_id')
|
|
->get()->getResultArray();
|
|
|
|
foreach ($participationRows as $par) {
|
|
$sec = (int) ($par['class_section_id'] ?? 0);
|
|
$sid = (int) ($par['student_id'] ?? 0);
|
|
if ($sec > 0 && $sid > 0) {
|
|
$participationCounts[$sec][$sid] = (int) ($par['cnt'] ?? 0);
|
|
}
|
|
}
|
|
|
|
$midtermRows = $this->db->table('midterm_exam')
|
|
->select('student_id, class_section_id, COUNT(*) AS cnt')
|
|
->where('semester', $semester)
|
|
->where('school_year', $schoolYear)
|
|
->whereIn('class_section_id', $sectionIds)
|
|
->whereIn('student_id', $studentIds)
|
|
->where('score IS NOT NULL', null, false)
|
|
->groupBy('student_id, class_section_id')
|
|
->get()->getResultArray();
|
|
|
|
foreach ($midtermRows as $mr) {
|
|
$sec = (int) ($mr['class_section_id'] ?? 0);
|
|
$sid = (int) ($mr['student_id'] ?? 0);
|
|
if ($sec > 0 && $sid > 0) {
|
|
$midtermCounts[$sec][$sid] = (int) ($mr['cnt'] ?? 0);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If any section is missing PTAP or semester score, refresh and reload once
|
|
$sectionsNeedingRefresh = [];
|
|
foreach ($rows as $r) {
|
|
$sectionId = (int) ($r['section_id'] ?? 0);
|
|
if ($sectionId <= 0) continue;
|
|
$ptapMissing = $r['ss_ptap_score'] === null;
|
|
$semMissing = $r['ss_semester_score'] === null;
|
|
if ($ptapMissing || $semMissing) {
|
|
$sectionsNeedingRefresh[$sectionId] = true;
|
|
}
|
|
}
|
|
|
|
if (!empty($sectionsNeedingRefresh) && $this->semesterScoreService !== null) {
|
|
foreach (array_keys($sectionsNeedingRefresh) as $sectionId) {
|
|
$studentTeacherInfo = $this->studentModel->getStudentInfoByClassSectionId(
|
|
$sectionId,
|
|
$semester,
|
|
$schoolYear
|
|
);
|
|
if (empty($studentTeacherInfo)) {
|
|
continue;
|
|
}
|
|
try {
|
|
$this->semesterScoreService->updateScoresForStudents($studentTeacherInfo, $semester, $schoolYear);
|
|
} catch (\Throwable $e) {
|
|
log_message(
|
|
'error',
|
|
'GradingController::grading refresh missing scores for section '
|
|
. $sectionId . ': ' . $e->getMessage()
|
|
);
|
|
}
|
|
}
|
|
// Reload rows after refresh
|
|
$rows = $this->buildGradingRows($semEsc, $yrEsc, $schoolYear, $semester);
|
|
}
|
|
|
|
// Build structures keyed by BUSINESS section id
|
|
$grades = []; // class_id => [ ['class_section_id','class_section_name'], ... ]
|
|
$studentsBySection = []; // section_id => [ students... ]
|
|
$seenStudentsBySection = [];
|
|
|
|
foreach ($rows as $r) {
|
|
$sectionId = (int) ($r['section_id'] ?? 0); // BUSINESS id
|
|
$classId = (int) ($r['class_id'] ?? 0);
|
|
$sectionName = (string) ($r['class_section_name'] ?? '');
|
|
if ($sectionId <= 0 || $classId <= 0) continue;
|
|
|
|
if (!isset($grades[$classId])) $grades[$classId] = [];
|
|
$exists = false;
|
|
foreach ($grades[$classId] as $s) {
|
|
if ((int)$s['class_section_id'] === $sectionId) {
|
|
$exists = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!$exists) {
|
|
$grades[$classId][] = [
|
|
'class_section_id' => $sectionId,
|
|
'class_section_name' => $sectionName,
|
|
];
|
|
}
|
|
|
|
$sid = (int) ($r['student_id'] ?? 0);
|
|
if ($sid <= 0) continue;
|
|
if (isset($seenStudentsBySection[$sectionId][$sid])) continue;
|
|
$seenStudentsBySection[$sectionId][$sid] = true;
|
|
|
|
$ptapScore = $r['ss_ptap_score'] ?? null;
|
|
$semesterScore = $r['ss_semester_score'] ?? null;
|
|
|
|
$attendanceScore = $this->calculateAttendanceScoreForStudent(
|
|
$sid,
|
|
$semester,
|
|
$schoolYear,
|
|
$sectionId
|
|
);
|
|
if ($attendanceScore === null) {
|
|
$rawAttendance = $r['ss_attendance_score'] ?? null;
|
|
if ($rawAttendance !== null && $rawAttendance !== '') {
|
|
$attendanceScore = round((float) $rawAttendance, 2);
|
|
}
|
|
}
|
|
$homeworkAvg = isset($r['ss_homework_avg']) && $r['ss_homework_avg'] !== '' ? round((float) $r['ss_homework_avg'], 2) : null;
|
|
if ($homeworkAvg !== null && (float) $homeworkAvg === 0.0) {
|
|
$hwCount = (int) ($homeworkCounts[$sectionId][$sid] ?? 0);
|
|
if ($hwCount === 0) {
|
|
$homeworkAvg = null;
|
|
}
|
|
}
|
|
|
|
$projectAvg = isset($r['ss_project_avg']) && $r['ss_project_avg'] !== '' ? round((float) $r['ss_project_avg'], 2) : null;
|
|
if ($projectAvg !== null && (float) $projectAvg === 0.0) {
|
|
$prCount = (int) ($projectCounts[$sectionId][$sid] ?? 0);
|
|
if ($prCount === 0) {
|
|
$projectAvg = null;
|
|
}
|
|
}
|
|
|
|
$participationScore = isset($r['ss_participation_score']) && $r['ss_participation_score'] !== '' ? round((float) $r['ss_participation_score'], 2) : null;
|
|
if ($participationScore !== null && (float) $participationScore === 0.0) {
|
|
$pCount = (int) ($participationCounts[$sectionId][$sid] ?? 0);
|
|
if ($pCount === 0) {
|
|
$participationScore = null;
|
|
}
|
|
}
|
|
|
|
$midtermExam = isset($r['ss_midterm_exam_score']) && $r['ss_midterm_exam_score'] !== '' ? round((float) $r['ss_midterm_exam_score'], 2) : null;
|
|
if ($midtermExam !== null && (float) $midtermExam === 0.0) {
|
|
$mCount = (int) ($midtermCounts[$sectionId][$sid] ?? 0);
|
|
if ($mCount === 0) {
|
|
$midtermExam = null;
|
|
}
|
|
}
|
|
|
|
$quizAvg = isset($r['ss_quiz_avg']) && $r['ss_quiz_avg'] !== '' ? round((float) $r['ss_quiz_avg'], 2) : null;
|
|
if ($quizAvg !== null && (float) $quizAvg === 0.0) {
|
|
$quizCount = (int) ($quizCounts[$sectionId][$sid] ?? 0);
|
|
if ($quizCount === 0) {
|
|
$quizAvg = null;
|
|
}
|
|
}
|
|
|
|
$studentsBySection[$sectionId][] = [
|
|
'id' => $sid,
|
|
'school_id' => $r['school_id'] ?? null,
|
|
'firstname' => $r['firstname'] ?? null,
|
|
'lastname' => $r['lastname'] ?? null,
|
|
'is_active' => (int)($r['is_active'] ?? 1),
|
|
'enrollment_status' => $r['enrollment_status'] ?? '',
|
|
'is_withdrawn' => (int)($r['is_withdrawn'] ?? 0),
|
|
'class_id' => $classId,
|
|
'ptap' => is_null($ptapScore) ? null : round((float) $ptapScore, 2),
|
|
'semester_score' => is_null($semesterScore) ? null : round((float) $semesterScore, 2),
|
|
'attendance' => $attendanceScore,
|
|
'homework_avg' => $homeworkAvg,
|
|
'project_avg' => $projectAvg,
|
|
'quiz_avg' => $quizAvg,
|
|
'participation' => $participationScore,
|
|
'midterm_exam' => $midtermExam,
|
|
'final_exam' => isset($r['ss_final_exam_score']) && $r['ss_final_exam_score'] !== '' ? round((float) $r['ss_final_exam_score'], 2) : null,
|
|
'matched_biz_csid' => $r['matched_biz_csid'] ?? null,
|
|
'matched_pk_csid' => $r['matched_pk_csid'] ?? null,
|
|
'placement_level' => $r['placement_level'] ?? null,
|
|
];
|
|
}
|
|
|
|
$scoreLocks = [];
|
|
$lockSectionIds = [];
|
|
foreach ($grades as $sections) {
|
|
foreach ($sections as $section) {
|
|
$sid = (int) ($section['class_section_id'] ?? 0);
|
|
if ($sid > 0) {
|
|
$lockSectionIds[$sid] = true;
|
|
}
|
|
}
|
|
}
|
|
$lockSectionIds = array_keys($lockSectionIds);
|
|
if (!empty($lockSectionIds)) {
|
|
$lockRows = $this->gradingLockModel
|
|
->whereIn('class_section_id', $lockSectionIds)
|
|
->where('semester', $semester)
|
|
->where('school_year', $schoolYear)
|
|
->findAll();
|
|
foreach ($lockRows as $row) {
|
|
$scoreLocks[(int) ($row['class_section_id'] ?? 0)] = !empty($row['is_locked']);
|
|
}
|
|
}
|
|
|
|
return ['kind' => 'view', 'view' => 'grading/grading_main', 'data' => [
|
|
'grades' => $grades,
|
|
'studentsBySection' => $studentsBySection,
|
|
'semester' => $semester,
|
|
'schoolYear' => $schoolYear,
|
|
'requestedClassId' => $requestedClassId,
|
|
'semesterOptions' => $semesterOptions,
|
|
'scoresReleased' => $scoresReleased,
|
|
'scoresReleasedFall' => $scoresReleasedFall,
|
|
'scoresReleasedSpring' => $scoresReleasedSpring,
|
|
'scoreLocks' => $scoreLocks,
|
|
]];
|
|
}
|
|
|
|
public function toggleScoreLock(array $params = [])
|
|
{
|
|
$get = $params['get'] ?? [];
|
|
$post = $params['post'] ?? [];
|
|
|
|
$classSectionId = (int) (($post['class_section_id'] ?? null) ?? 0);
|
|
$semester = trim((string) (($post['semester'] ?? null) ?? $this->semester));
|
|
$schoolYear = trim((string) (($post['school_year'] ?? null) ?? $this->schoolYear));
|
|
|
|
if ($classSectionId <= 0 || $semester === '' || $schoolYear === '') {
|
|
return ['kind' => 'flash', 'redirect' => 'back', 'type' => 'error', 'message' => 'Missing class section or term.'];
|
|
}
|
|
|
|
$existing = $this->gradingLockModel->getLock($classSectionId, $semester, $schoolYear);
|
|
$userId = (int) (session()->get('user_id') ?? 0);
|
|
|
|
if (!empty($existing) && !empty($existing['is_locked'])) {
|
|
$this->gradingLockModel->update($existing['id'], [
|
|
'is_locked' => 0,
|
|
'locked_by' => null,
|
|
'locked_at' => null,
|
|
]);
|
|
return ['kind' => 'flash', 'redirect' => 'back', 'type' => 'status', 'message' => 'Scores unlocked for this class.'];
|
|
}
|
|
|
|
if (!empty($existing)) {
|
|
$this->gradingLockModel->update($existing['id'], [
|
|
'is_locked' => 1,
|
|
'locked_by' => $userId > 0 ? $userId : null,
|
|
'locked_at' => utc_now(),
|
|
]);
|
|
} else {
|
|
$this->gradingLockModel->insert([
|
|
'class_section_id' => $classSectionId,
|
|
'semester' => $semester,
|
|
'school_year' => $schoolYear,
|
|
'is_locked' => 1,
|
|
'locked_by' => $userId > 0 ? $userId : null,
|
|
'locked_at' => utc_now(),
|
|
]);
|
|
}
|
|
|
|
return ['kind' => 'flash', 'redirect' => 'back', 'type' => 'status', 'message' => 'Scores locked for this class.'];
|
|
}
|
|
|
|
public function lockAllScores(array $params = [])
|
|
{
|
|
$get = $params['get'] ?? [];
|
|
$post = $params['post'] ?? [];
|
|
|
|
$semester = trim((string) (($post['semester'] ?? null) ?? $this->semester));
|
|
$schoolYear = trim((string) (($post['school_year'] ?? null) ?? $this->schoolYear));
|
|
|
|
if ($semester === '' || $schoolYear === '') {
|
|
return ['kind' => 'flash', 'redirect' => 'back', 'type' => 'error', 'message' => 'Missing semester or school year.'];
|
|
}
|
|
|
|
$sectionRows = $this->classSection
|
|
->select('class_section_id')
|
|
->groupBy('class_section_id')
|
|
->findAll();
|
|
|
|
$sectionIds = array_values(array_filter(array_map(
|
|
static fn($row) => (int) ($row['class_section_id'] ?? 0),
|
|
$sectionRows
|
|
), static fn($id) => $id > 0));
|
|
|
|
if (empty($sectionIds)) {
|
|
return ['kind' => 'flash', 'redirect' => 'back', 'type' => 'error', 'message' => 'No class sections found to lock.'];
|
|
}
|
|
|
|
$existingLocks = $this->gradingLockModel
|
|
->whereIn('class_section_id', $sectionIds)
|
|
->where('semester', $semester)
|
|
->where('school_year', $schoolYear)
|
|
->findAll();
|
|
|
|
$existingBySection = [];
|
|
foreach ($existingLocks as $row) {
|
|
$sid = (int) ($row['class_section_id'] ?? 0);
|
|
if ($sid > 0) {
|
|
$existingBySection[$sid] = $row;
|
|
}
|
|
}
|
|
|
|
$userId = (int) (session()->get('user_id') ?? 0);
|
|
$now = utc_now();
|
|
$insertRows = [];
|
|
|
|
foreach ($sectionIds as $sid) {
|
|
if (!empty($existingBySection[$sid])) {
|
|
if (!empty($existingBySection[$sid]['is_locked'])) {
|
|
continue;
|
|
}
|
|
$this->gradingLockModel->update($existingBySection[$sid]['id'], [
|
|
'is_locked' => 1,
|
|
'locked_by' => $userId > 0 ? $userId : null,
|
|
'locked_at' => $now,
|
|
]);
|
|
continue;
|
|
}
|
|
|
|
$insertRows[] = [
|
|
'class_section_id' => $sid,
|
|
'semester' => $semester,
|
|
'school_year' => $schoolYear,
|
|
'is_locked' => 1,
|
|
'locked_by' => $userId > 0 ? $userId : null,
|
|
'locked_at' => $now,
|
|
'created_at' => $now,
|
|
'updated_at' => $now,
|
|
];
|
|
}
|
|
|
|
if (!empty($insertRows)) {
|
|
$this->gradingLockModel->insertBatch($insertRows);
|
|
}
|
|
|
|
return ['kind' => 'flash', 'redirect' => 'back', 'type' => 'status', 'message' => 'Scores locked for all classes.'];
|
|
}
|
|
|
|
public function toggleParentScoresRelease(array $params = [])
|
|
{
|
|
$get = $params['get'] ?? [];
|
|
$post = $params['post'] ?? [];
|
|
|
|
$semester = (string) (($post['semester'] ?? null) ?? '');
|
|
if ($semester === '') {
|
|
$semester = (string) (session()->get('grading_selected_semester') ?? $this->semester);
|
|
}
|
|
$configKey = $this->getParentReleaseKey($semester) ?? 'parent_scores_released';
|
|
|
|
$releaseScoresRaw = (string) ($this->configModel->getConfig($configKey) ?? '');
|
|
$scoresReleased = in_array(strtolower(trim($releaseScoresRaw)), ['1', 'true', 'yes', 'y', 'on'], true);
|
|
$nextValue = $scoresReleased ? '0' : '1';
|
|
|
|
$ok = $this->configModel->setConfigValueByKey($configKey, $nextValue);
|
|
log_message('info', 'toggleParentScoresRelease', [
|
|
'semester' => $semester,
|
|
'config_key' => $configKey,
|
|
'prev' => $releaseScoresRaw,
|
|
'next' => $nextValue,
|
|
'ok' => $ok,
|
|
]);
|
|
|
|
if ($ok) {
|
|
$msg = $scoresReleased
|
|
? 'Parent exam/semester scores are now hidden.'
|
|
: 'Parent exam/semester scores are now released.';
|
|
$msg .= ' (' . $configKey . '=' . $nextValue . ')';
|
|
return ['kind' => 'flash', 'redirect' => 'back', 'type' => 'status', 'message' => $msg];
|
|
}
|
|
|
|
return ['kind' => 'flash', 'redirect' => 'back', 'type' => 'error', 'message' => 'Unable to update the scores release flag.'];
|
|
}
|
|
|
|
public function refreshSemesterScores(array $params = [])
|
|
{
|
|
$get = $params['get'] ?? [];
|
|
$post = $params['post'] ?? [];
|
|
|
|
$classSectionId = (int) (($post['class_section_id'] ?? null) ?? 0);
|
|
if ($classSectionId <= 0) {
|
|
return ['kind' => 'flash', 'redirect' => 'back', 'type' => 'error', 'message' => 'Missing class section.'];
|
|
}
|
|
|
|
$requestedSemester = (string) (($post['semester'] ?? null) ?? '');
|
|
$requestedYear = (string) (($post['school_year'] ?? null) ?? '');
|
|
$semester = $this->normalizeSemesterInput($requestedSemester) ?? $requestedSemester;
|
|
if ($semester === '') {
|
|
$semester = (string) $this->semester;
|
|
}
|
|
$schoolYear = trim($requestedYear) !== '' ? trim($requestedYear) : (string) $this->schoolYear;
|
|
|
|
$studentTeacherInfo = $this->studentModel->getStudentInfoByClassSectionId(
|
|
$classSectionId,
|
|
$semester,
|
|
$schoolYear
|
|
);
|
|
if (empty($studentTeacherInfo)) {
|
|
return ['kind' => 'flash', 'redirect' => 'back', 'type' => 'error', 'message' => 'No students found for this class/term.'];
|
|
}
|
|
|
|
try {
|
|
$this->semesterScoreService->updateScoresForStudents($studentTeacherInfo, $semester, $schoolYear);
|
|
$this->refreshAttendanceComments($classSectionId, $semester, $schoolYear);
|
|
} catch (\Throwable $e) {
|
|
log_message('error', 'refreshSemesterScores failed: ' . $e->getMessage());
|
|
return ['kind' => 'flash', 'redirect' => 'back', 'type' => 'error', 'message' => 'Refresh failed. Check logs.'];
|
|
}
|
|
|
|
return ['kind' => 'flash', 'redirect' => 'back', 'type' => 'status', 'message' => 'Semester scores refreshed for this class/term.'];
|
|
}
|
|
|
|
public function getScoreComment(array $params = [])
|
|
{
|
|
$get = $params['get'] ?? [];
|
|
$post = $params['post'] ?? [];
|
|
|
|
// Get all students for the current semester and school year
|
|
$studentClassEntries = $this->studentClassModel
|
|
->where('semester', $this->semester)
|
|
->where('school_year', $this->schoolYear)
|
|
->findAll();
|
|
|
|
// Group student IDs
|
|
$studentIds = array_map(fn($entry) => $entry['student_id'], $studentClassEntries);
|
|
|
|
// Fetch all scores and comments for the students
|
|
$scoresAndComments = $this->getAllScoresAndComments($studentIds, $this->semester, $this->schoolYear);
|
|
|
|
return $scoresAndComments;
|
|
}
|
|
|
|
public function getModelByType($type)
|
|
{
|
|
return match ($type) {
|
|
'homework' => new HomeworkModel(),
|
|
'quiz' => new QuizModel(),
|
|
'project' => new ProjectModel(),
|
|
'midterm' => new MidtermExamModel(),
|
|
'final' => new FinalExamModel(),
|
|
'test' => new SemesterScoreModel(), // Assuming you use test_avg here
|
|
'comments' => new ScoreCommentModel(),
|
|
default => throw new \InvalidArgumentException("Invalid type: $type"),
|
|
};
|
|
}
|
|
|
|
public function isScoresLocked(int $classSectionId, string $semester, string $schoolYear): bool
|
|
{
|
|
return $this->gradingLockModel->isLocked($classSectionId, $semester, $schoolYear);
|
|
}
|
|
|
|
public function getParentReleaseKey(string $semester): ?string
|
|
{
|
|
$norm = strtolower(trim((string) $semester));
|
|
if ($norm === 'fall') {
|
|
return 'parent_scores_released_fall';
|
|
}
|
|
if ($norm === 'spring') {
|
|
return 'parent_scores_released_spring';
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public function getParentScoresReleasedForSemester(string $semester): bool
|
|
{
|
|
$key = $this->getParentReleaseKey($semester);
|
|
$raw = $key ? $this->configModel->getConfig($key) : null;
|
|
$raw = (string) ($raw ?? '');
|
|
return in_array(strtolower(trim($raw)), ['1', 'true', 'yes', 'y', 'on'], true);
|
|
}
|
|
|
|
public function ensureParentReleaseKeyExists(string $semester): void
|
|
{
|
|
$key = $this->getParentReleaseKey($semester);
|
|
if (!$key) {
|
|
return;
|
|
}
|
|
if ($this->configModel->getConfigValueByKey($key) === null) {
|
|
$this->configModel->setConfigValueByKey($key, '0');
|
|
}
|
|
}
|
|
|
|
public function refreshAttendanceComments(int $classSectionId, string $semester, string $schoolYear): void
|
|
{
|
|
helper('attendance_comment');
|
|
|
|
$scoreModel = new SemesterScoreModel();
|
|
$commentModel = new ScoreCommentModel();
|
|
|
|
$scoreRows = $scoreModel
|
|
->select(['student_id', 'attendance_score'])
|
|
->where('class_section_id', $classSectionId)
|
|
->where('semester', $semester)
|
|
->where('school_year', $schoolYear)
|
|
->findAll();
|
|
|
|
if (empty($scoreRows)) {
|
|
return;
|
|
}
|
|
|
|
$studentIds = array_values(array_unique(array_map(
|
|
static fn($row) => (int) ($row['student_id'] ?? 0),
|
|
$scoreRows
|
|
)));
|
|
$studentIds = array_values(array_filter($studentIds, static fn($id) => $id > 0));
|
|
if (empty($studentIds)) {
|
|
return;
|
|
}
|
|
|
|
$students = $this->studentModel
|
|
->select(['id', 'firstname'])
|
|
->whereIn('id', $studentIds)
|
|
->findAll();
|
|
$nameMap = [];
|
|
foreach ($students as $st) {
|
|
$nameMap[(int)$st['id']] = (string) ($st['firstname'] ?? '');
|
|
}
|
|
|
|
$existing = $commentModel
|
|
->where('score_type', 'attendance')
|
|
->where('class_section_id', $classSectionId)
|
|
->where('semester', $semester)
|
|
->where('school_year', $schoolYear)
|
|
->whereIn('student_id', $studentIds)
|
|
->findAll();
|
|
$existingByStudent = [];
|
|
foreach ($existing as $row) {
|
|
$existingByStudent[(int)$row['student_id']] = $row;
|
|
}
|
|
|
|
foreach ($scoreRows as $row) {
|
|
$sid = (int) ($row['student_id'] ?? 0);
|
|
if ($sid <= 0) {
|
|
continue;
|
|
}
|
|
$score = isset($row['attendance_score']) ? (float) $row['attendance_score'] : null;
|
|
if ($score === null) {
|
|
continue;
|
|
}
|
|
$auto = attendance_comment_from_score($score, $nameMap[$sid] ?? '');
|
|
if ($auto === null) {
|
|
continue;
|
|
}
|
|
|
|
if (isset($existingByStudent[$sid])) {
|
|
$commentModel->update($existingByStudent[$sid]['id'], [
|
|
'comment' => $auto,
|
|
]);
|
|
} else {
|
|
$commentModel->insert([
|
|
'student_id' => $sid,
|
|
'class_section_id' => $classSectionId,
|
|
'score_type' => 'attendance',
|
|
'semester' => $semester,
|
|
'school_year' => $schoolYear,
|
|
'comment' => $auto,
|
|
'commented_by' => null,
|
|
'created_at' => utc_now(),
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Build grading rows with PTAP and semester scores (business + pk join).
|
|
*
|
|
* @param string $semEsc Escaped semester string for SQL
|
|
* @param string $yrEsc Escaped school year string for SQL
|
|
* @param string $schoolYear Raw school year value for filtering student_class
|
|
* @return array
|
|
*/
|
|
public function buildGradingRows(string $semEsc, string $yrEsc, string $schoolYear, string $semesterRaw): array
|
|
{
|
|
$builder = $this->db->table('student_class sc')
|
|
->select([
|
|
'cs.id AS section_pk',
|
|
'cs.class_section_id AS section_id', // BUSINESS id used in URLs
|
|
'cs.class_id AS class_id', // 13=KG, 1..11=Grade N, 12=Youth
|
|
'cs.class_section_name',
|
|
's.id AS student_id',
|
|
's.school_id',
|
|
's.firstname',
|
|
's.lastname',
|
|
's.is_active',
|
|
'e.enrollment_status',
|
|
'e.is_withdrawn',
|
|
'pl.level AS placement_level',
|
|
|
|
// Prefer business-id match; fall back to pk match
|
|
'COALESCE(ss_b.ptap_score, ss_p.ptap_score) AS ss_ptap_score',
|
|
'COALESCE(ss_b.semester_score, ss_p.semester_score) AS ss_semester_score',
|
|
'COALESCE(ss_b.attendance_score, ss_p.attendance_score) AS ss_attendance_score',
|
|
'COALESCE(ss_b.homework_avg, ss_p.homework_avg) AS ss_homework_avg',
|
|
'COALESCE(ss_b.project_avg, ss_p.project_avg) AS ss_project_avg',
|
|
'COALESCE(ss_b.quiz_avg, ss_p.quiz_avg) AS ss_quiz_avg',
|
|
'COALESCE(ss_b.participation_score, ss_p.participation_score) AS ss_participation_score',
|
|
'COALESCE(ss_b.midterm_exam_score, ss_p.midterm_exam_score) AS ss_midterm_exam_score',
|
|
'COALESCE(ss_b.final_exam_score, ss_p.final_exam_score) AS ss_final_exam_score',
|
|
// helpful to debug what matched:
|
|
'ss_b.class_section_id AS matched_biz_csid',
|
|
'ss_p.class_section_id AS matched_pk_csid'
|
|
])
|
|
->distinct()
|
|
->join('`classSection` cs', 'cs.class_section_id = sc.class_section_id', 'left')
|
|
->join('students s', 's.id = sc.student_id', 'inner')
|
|
->join(
|
|
'enrollments e',
|
|
"e.student_id = s.id AND e.school_year = {$yrEsc}",
|
|
'left'
|
|
)
|
|
->join(
|
|
'placement_levels pl',
|
|
'pl.student_id = s.id',
|
|
'left'
|
|
)
|
|
|
|
// business-id join
|
|
->join(
|
|
'semester_scores ss_b',
|
|
"ss_b.student_id = s.id
|
|
AND ss_b.class_section_id = sc.class_section_id
|
|
AND LOWER(ss_b.semester) = LOWER(TRIM({$semEsc}))
|
|
AND ss_b.school_year = {$yrEsc}",
|
|
'left'
|
|
)
|
|
// pk join
|
|
->join(
|
|
'semester_scores ss_p',
|
|
"ss_p.student_id = s.id
|
|
AND ss_p.class_section_id = cs.id
|
|
AND LOWER(ss_p.semester) = LOWER(TRIM({$semEsc}))
|
|
AND ss_p.school_year = {$yrEsc}",
|
|
'left'
|
|
)
|
|
->where('sc.school_year', $schoolYear)
|
|
->groupStart()
|
|
->where('s.is_active', 1)
|
|
->orWhere("LOWER(TRIM(e.enrollment_status)) IN ('denied','withdrawn','waitlist')", null, false)
|
|
->orWhere('e.is_withdrawn', 1)
|
|
->groupEnd();
|
|
|
|
return $builder
|
|
->orderBy('cs.class_id', 'ASC')
|
|
->orderBy('cs.class_section_name', 'ASC')
|
|
->orderBy('s.lastname', 'ASC')
|
|
->orderBy('s.firstname', 'ASC')
|
|
->get()->getResultArray();
|
|
}
|
|
|
|
public 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) {
|
|
log_message(
|
|
'error',
|
|
'GradingController::calculateAttendanceScoreForStudent failed for '
|
|
. "student {$studentId}: " . $e->getMessage()
|
|
);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public function calculateTrophyThreshold(array $scores, float $percentile = 75.0): array
|
|
{
|
|
$scores = array_values(array_filter(
|
|
$scores,
|
|
static fn ($value): bool => is_numeric($value) && $value !== null
|
|
));
|
|
$scores = array_map('floatval', $scores);
|
|
sort($scores);
|
|
|
|
$count = count($scores);
|
|
|
|
if ($count === 0) {
|
|
return ['threshold' => null, 'winners' => 0, 'method' => 'empty'];
|
|
}
|
|
|
|
$minWinners = 3;
|
|
$maxWinners = max($minWinners, (int) floor($count * (1 - $percentile / 100)));
|
|
|
|
$threshold = $this->empiricalTrophyPercentile($scores, $percentile);
|
|
$winners = $this->countScoresAtOrAbove($scores, $threshold);
|
|
|
|
if ($winners < $minWinners) {
|
|
$target = min($minWinners, $count);
|
|
$descending = array_reverse($scores);
|
|
$threshold = $descending[$target - 1];
|
|
$winners = $this->countScoresAtOrAbove($scores, $threshold);
|
|
|
|
return ['threshold' => $threshold, 'winners' => $winners, 'method' => 'min3_reduced'];
|
|
}
|
|
|
|
if ($winners <= $maxWinners) {
|
|
return ['threshold' => $threshold, 'winners' => $winners, 'method' => 'empirical_percentile'];
|
|
}
|
|
|
|
$result = $this->capTrophyThresholdByRank($scores, $maxWinners);
|
|
|
|
if ($result['winners'] < $minWinners) {
|
|
$target = min($minWinners, $count);
|
|
$descending = array_reverse($scores);
|
|
$threshold = $descending[$target - 1];
|
|
$winners = $this->countScoresAtOrAbove($scores, $threshold);
|
|
|
|
return ['threshold' => $threshold, 'winners' => $winners, 'method' => 'min3_after_cap'];
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
public function capTrophyThresholdByRank(array $sortedScores, int $max): array
|
|
{
|
|
$descending = array_reverse($sortedScores);
|
|
$threshold = $descending[$max - 1];
|
|
$winners = $this->countScoresAtOrAbove($sortedScores, $threshold);
|
|
|
|
if ($winners <= $max) {
|
|
return ['threshold' => $threshold, 'winners' => $winners, 'method' => 'capped_25pct'];
|
|
}
|
|
|
|
$uniqueHigherScores = array_values(array_unique(array_filter(
|
|
$sortedScores,
|
|
static fn ($score): bool => $score > $threshold
|
|
)));
|
|
sort($uniqueHigherScores);
|
|
|
|
foreach ($uniqueHigherScores as $candidate) {
|
|
$winnerCount = $this->countScoresAtOrAbove($sortedScores, $candidate);
|
|
|
|
if ($winnerCount <= $max) {
|
|
return ['threshold' => $candidate, 'winners' => $winnerCount, 'method' => 'capped_25pct'];
|
|
}
|
|
}
|
|
|
|
return ['threshold' => $sortedScores[0], 'winners' => count($sortedScores), 'method' => 'all_equal'];
|
|
}
|
|
|
|
public function empiricalTrophyPercentile(array $sortedScores, float $percentile): float
|
|
{
|
|
$count = count($sortedScores);
|
|
|
|
if ($count === 0) {
|
|
return 0.0;
|
|
}
|
|
|
|
$index = ($percentile / 100.0) * ($count - 1);
|
|
$lower = (int) floor($index);
|
|
$upper = (int) ceil($index);
|
|
|
|
if ($lower === $upper) {
|
|
return $sortedScores[$lower];
|
|
}
|
|
|
|
return $sortedScores[$lower] + ($index - $lower) * ($sortedScores[$upper] - $sortedScores[$lower]);
|
|
}
|
|
|
|
public function countScoresAtOrAbove(array $scores, float $threshold): int
|
|
{
|
|
return count(array_filter(
|
|
$scores,
|
|
static fn ($score): bool => $score >= $threshold
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Fetch all scores and comments for a list of students based on semester and school year.
|
|
*
|
|
* @param array $studentIds List of student IDs.
|
|
* @param string $semester Current semester (e.g., 'fall', 'spring').
|
|
* @param string $schoolYear Current school year (e.g., '2025-2026').
|
|
* @return array
|
|
*/
|
|
public function getAllScoresAndComments($studentIds, $semester, $schoolYear)
|
|
{
|
|
// Validate input parameters
|
|
if (empty($studentIds) || !is_array($studentIds)) {
|
|
return [];
|
|
}
|
|
|
|
if (empty($this->semester) || empty($this->schoolYear)) {
|
|
throw new \InvalidArgumentException('Semester and school year must be provided');
|
|
}
|
|
|
|
// Initialize models
|
|
$models = [
|
|
'final_exam' => new FinalExamModel(),
|
|
'homework' => new HomeworkModel(),
|
|
'midterm' => new MidtermExamModel(),
|
|
'project' => new ProjectModel(),
|
|
'quiz' => new QuizModel(),
|
|
'comments' => new ScoreCommentModel(),
|
|
'semester_scores' => new SemesterScoreModel(),
|
|
'student' => new StudentModel(),
|
|
'student_class' => new StudentClassModel(),
|
|
'teacher_class' => new TeacherClassModel(),
|
|
'config' => new ConfigurationModel(),
|
|
'attendance' => new AttendanceRecordModel()
|
|
];
|
|
|
|
// Get semester days configuration
|
|
$semesterKey = strtolower($semester) === 'fall' ? 'total_semester1_days' : 'total_semester2_days';
|
|
$totalSemesterDays = $models['config']->getConfig($semesterKey) ?? 0;
|
|
|
|
// Common query conditions
|
|
$conditions = [
|
|
'semester' => $this->semester,
|
|
'school_year' => $this->schoolYear
|
|
];
|
|
|
|
// Fetch all student data first
|
|
$students = $models['student']->whereIn('id', $studentIds)
|
|
->where('school_year', $this->schoolYear)
|
|
->findAll();
|
|
|
|
if (empty($students)) {
|
|
return [];
|
|
}
|
|
|
|
// Initialize result array with student data
|
|
$allScores = [];
|
|
foreach ($students as $student) {
|
|
$className = $models['student_class']->getClassSectionsByStudentId($student['id'], $this->schoolYear);
|
|
$updatedBy = $models['teacher_class']->getTeacherIdByClassSection($className, $this->semester, $this->schoolYear);
|
|
|
|
$allScores[$student['id']] = [
|
|
'school_id' => $student['school_id'],
|
|
'firstname' => $student['firstname'],
|
|
'lastname' => $student['lastname'],
|
|
'class_name' => $className,
|
|
//'teacherId' => $updatedBy,
|
|
'comments' => [] // Initialize empty comments array
|
|
];
|
|
}
|
|
|
|
// Fetch and process attendance data
|
|
foreach ($studentIds as $studentId) {
|
|
if (!isset($allScores[$studentId])) continue;
|
|
|
|
$absences = $models['attendance']->getTotalAbsences($studentId, $this->semester, $this->schoolYear);
|
|
$attendance = min((($totalSemesterDays - $absences + 1) / $totalSemesterDays) * 100, 100);
|
|
|
|
$allScores[$studentId]['attendance'] = [
|
|
'score' => round($attendance, 2),
|
|
'absences' => $absences,
|
|
'total_days' => $totalSemesterDays
|
|
];
|
|
}
|
|
|
|
// Fetch and process all score types
|
|
$scoreTypes = [
|
|
'final_exam' => $models['final_exam'],
|
|
'homework' => $models['homework'],
|
|
'midterm' => $models['midterm'],
|
|
'project' => $models['project'],
|
|
'quiz' => $models['quiz'],
|
|
'semester_score' => $models['semester_scores']
|
|
];
|
|
|
|
foreach ($scoreTypes as $type => $model) {
|
|
$scores = $model->whereIn('student_id', $studentIds)
|
|
->where($conditions)
|
|
->findAll();
|
|
|
|
foreach ($scores as $score) {
|
|
if ($type === 'semester_score') {
|
|
$allScores[$score['student_id']][$type] = [
|
|
'homework_avg' => $score['homework_avg'],
|
|
'quiz_avg' => $score['quiz_avg'],
|
|
'project_avg' => $score['project_avg'],
|
|
'midterm_exam_score' => $score['midterm_exam_score'],
|
|
'final_exam_score' => $score['final_exam_score'],
|
|
'attendance_score' => $score['attendance_score'],
|
|
'participation_score' => $score['participation_score'],
|
|
'ptap_score' => $score['ptap_score'],
|
|
'test_avg' => $score['test_avg'],
|
|
'semester_score' => $score['semester_score'],
|
|
'semester' => $score['semester'],
|
|
'school_year' => $score['school_year']
|
|
];
|
|
} else {
|
|
$allScores[$score['student_id']][$type] = $score;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fetch and process comments
|
|
$comments = $models['comments']->whereIn('student_id', $studentIds)
|
|
->where($conditions)
|
|
->findAll();
|
|
|
|
foreach ($comments as $comment) {
|
|
$allScores[$comment['student_id']]['comments'][] = $comment;
|
|
}
|
|
|
|
return $allScores;
|
|
}
|
|
|
|
public function normalizeSemesterInput(?string $semester): ?string
|
|
{
|
|
if (!is_string($semester)) {
|
|
return null;
|
|
}
|
|
$trimmed = trim($semester);
|
|
return $trimmed === '' ? null : $trimmed;
|
|
}
|
|
|
|
public function resolveSemesterSelection(?string $requestedSemester, array $semesterOptions, ?string $fallbackSemester): string
|
|
{
|
|
if ($requestedSemester !== null) {
|
|
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] ?? '';
|
|
}
|
|
|
|
public function getSemestersForSchoolYear(string $schoolYear, ?string $fallbackSemester = null): array
|
|
{
|
|
$rows = $this->db->table('semester_scores')
|
|
->select('DISTINCT semester', false)
|
|
->where('semester IS NOT NULL', null, false)
|
|
->where('semester != ""', null, false)
|
|
->where('school_year', $schoolYear)
|
|
->orderBy('semester', 'ASC')
|
|
->get()
|
|
->getResultArray();
|
|
|
|
$semesters = [];
|
|
foreach ($rows as $row) {
|
|
$value = trim((string) ($row['semester'] ?? ''));
|
|
if ($value === '') continue;
|
|
$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));
|
|
}
|
|
|
|
public function getSchoolYearsForScores(?string $fallback = null): array
|
|
{
|
|
$schoolYears = [];
|
|
try {
|
|
$rows = $this->db->table('semester_scores')
|
|
->select('DISTINCT school_year', false)
|
|
->where('school_year IS NOT NULL', null, false)
|
|
->where('school_year != ""', null, false)
|
|
->orderBy('school_year', 'DESC')
|
|
->get()
|
|
->getResultArray();
|
|
foreach ($rows as $row) {
|
|
$val = (string)($row['school_year'] ?? '');
|
|
if ($val !== '') $schoolYears[] = $val;
|
|
}
|
|
} catch (\Throwable $e) {
|
|
}
|
|
try {
|
|
$rows2 = $this->db->table('student_class')
|
|
->select('DISTINCT school_year', false)
|
|
->where('school_year IS NOT NULL', null, false)
|
|
->where('school_year != ""', null, false)
|
|
->orderBy('school_year', 'DESC')
|
|
->get()
|
|
->getResultArray();
|
|
foreach ($rows2 as $row) {
|
|
$val = (string)($row['school_year'] ?? '');
|
|
if ($val !== '' && !in_array($val, $schoolYears, true)) $schoolYears[] = $val;
|
|
}
|
|
} catch (\Throwable $e) {
|
|
}
|
|
if ($fallback && !in_array($fallback, $schoolYears, true)) {
|
|
array_unshift($schoolYears, $fallback);
|
|
}
|
|
return array_values(array_unique($schoolYears));
|
|
}
|
|
|
|
public function userHasMenuUrl(string $needle): bool
|
|
{
|
|
$needle = strtolower(trim($needle));
|
|
if ($needle === '') {
|
|
return false;
|
|
}
|
|
|
|
$rawRole = session()->get('role');
|
|
$roles = is_array($rawRole) ? $rawRole : [$rawRole ?? 'guest'];
|
|
$roles = array_values(array_filter(array_map('strval', $roles)));
|
|
if (empty($roles)) {
|
|
return false;
|
|
}
|
|
|
|
$service = new NavbarService();
|
|
$menu = $service->getMenuForRoles($roles);
|
|
if (empty($menu)) {
|
|
return false;
|
|
}
|
|
|
|
$normalize = static function (string $url) use ($needle): string {
|
|
$url = strtolower(trim($url));
|
|
if ($url === '') return '';
|
|
$url = preg_replace('#^https?://[^/]+/#i', '', $url);
|
|
$url = ltrim($url, '/');
|
|
return $url;
|
|
};
|
|
|
|
$target = $normalize($needle);
|
|
$stack = $menu;
|
|
while (!empty($stack)) {
|
|
$node = array_shift($stack);
|
|
if (!empty($node['url'])) {
|
|
$url = $normalize((string)$node['url']);
|
|
if ($url !== '' && $url === $target) {
|
|
return true;
|
|
}
|
|
}
|
|
if (!empty($node['children']) && is_array($node['children'])) {
|
|
foreach ($node['children'] as $child) {
|
|
$stack[] = $child;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Parses a classSection name into a (classId, label) pair compatible with your Attendance view.
|
|
* - KG -> (0, 'KG')
|
|
* - Youth-> (13, 'Youth')
|
|
* - Grade N -> (N, 'Grade N')
|
|
* Fallback: tries to extract first number; else returns (99, original).
|
|
*/
|
|
public function parseClassIdFromSectionName(string $name): array
|
|
{
|
|
$n = trim($name);
|
|
|
|
// Common patterns used in your data
|
|
if (preg_match('/\bKG\b/i', $n)) {
|
|
return [0, 'KG'];
|
|
}
|
|
if (preg_match('/\bYouth\b/i', $n)) {
|
|
return [13, 'Youth'];
|
|
}
|
|
if (preg_match('/Grade\s*(\d+)/i', $n, $m)) {
|
|
$g = (int)$m[1];
|
|
return [$g, 'Grade ' . $g];
|
|
}
|
|
|
|
// Fallback: any number present becomes the grade id
|
|
if (preg_match('/(\d+)/', $n, $m2)) {
|
|
$g = (int)$m2[1];
|
|
return [$g, 'Grade ' . $g];
|
|
}
|
|
|
|
// Last fallback
|
|
return [99, $n];
|
|
}
|
|
|
|
/**
|
|
* Resolve class_section_id for a student in the current term.
|
|
*/
|
|
public function resolveClassSectionIdForStudent(int $studentId): int
|
|
{
|
|
$row = $this->db->table('student_class')
|
|
->select('class_section_id')
|
|
->where('student_id', $studentId)
|
|
->where('school_year', $this->schoolYear)
|
|
->where('semester', $this->semester)
|
|
->get()->getRow();
|
|
|
|
return $row ? (int)$row->class_section_id : 0;
|
|
}
|
|
|
|
/** Simple slugifier used as array keys for tabs */
|
|
public function slugify(string $s): string
|
|
{
|
|
$s = strtolower($s);
|
|
$s = preg_replace('/[^a-z0-9]+/i', '-', $s);
|
|
return trim($s, '-');
|
|
}
|
|
|
|
/** Rank: KG first, numbers ascending, Youth last */
|
|
public function rankOf(string $name): int
|
|
{
|
|
$k = strtolower(trim($name));
|
|
if ($k === 'kg' || $k === 'kindergarten') return -100;
|
|
if ($k === 'youth') return 100000;
|
|
if (preg_match('/\d+/', $k, $m)) return (int)$m[0];
|
|
return 50000;
|
|
}
|
|
}
|