1438 lines
58 KiB
PHP
1438 lines
58 KiB
PHP
<?php
|
|
|
|
namespace App\Controllers\View;
|
|
|
|
use CodeIgniter\Controller;
|
|
use App\Models\TeacherClassModel;
|
|
use App\Models\StudentClassModel;
|
|
use App\Models\StudentModel;
|
|
use App\Models\QuizModel;
|
|
use App\Models\HomeworkModel;
|
|
use App\Models\FinalExamModel;
|
|
use App\Models\MidtermExamModel;
|
|
use App\Models\ProjectModel;
|
|
use App\Models\SemesterScoreModel;
|
|
use App\Models\ConfigurationModel;
|
|
use App\Models\ScoreCommentModel;
|
|
use App\Models\ParticipationModel;
|
|
use App\Models\AttendanceRecordModel;
|
|
use App\Models\AttendanceDataModel;
|
|
use App\Models\ClassSectionModel;
|
|
use App\Models\UserModel;
|
|
use CodeIgniter\Events\Events;
|
|
use Config\Services;
|
|
use App\Models\CalendarModel;
|
|
use App\Services\Calculators\AttendanceCalculator;
|
|
use App\Models\GradingLockModel;
|
|
use App\Models\MissingScoreOverrideModel;
|
|
|
|
|
|
|
|
class ScoreController extends Controller
|
|
{
|
|
protected $semesterScoreService;
|
|
protected $db;
|
|
protected $configModel;
|
|
protected $semester;
|
|
protected $schoolYear;
|
|
protected $classSectionModel;
|
|
protected $studentClassModel;
|
|
protected $teacherClassModel;
|
|
protected $userModel;
|
|
protected $targetHigh;
|
|
protected $targetLow;
|
|
protected $semesterScoreModel;
|
|
protected $studentModel;
|
|
protected $calendarModel;
|
|
protected $attendanceCalculator;
|
|
protected $gradingLockModel;
|
|
protected $missingScoreOverrideModel;
|
|
|
|
public function __construct()
|
|
{
|
|
// Load the database service
|
|
$this->db = \Config\Database::connect();
|
|
// Check if the database connection is established
|
|
if (!$this->db->connect()) {
|
|
log_message('error', 'Database connection failed.');
|
|
throw new \Exception('Database connection failed.');
|
|
} else {
|
|
log_message('info', 'Database connection successful.');
|
|
}
|
|
|
|
// Initialize the config model
|
|
$this->configModel = new ConfigurationModel();
|
|
$this->classSectionModel = new ClassSectionModel();
|
|
$this->teacherClassModel = new TeacherClassModel();
|
|
$this->semesterScoreModel = new SemesterScoreModel();
|
|
$this->studentModel = new StudentModel();
|
|
$this->studentClassModel = new StudentClassModel();
|
|
$this->userModel = new UserModel();
|
|
$this->calendarModel = new CalendarModel();
|
|
$this->attendanceCalculator = new AttendanceCalculator(
|
|
new AttendanceRecordModel(),
|
|
$this->configModel,
|
|
$this->calendarModel
|
|
);
|
|
$this->gradingLockModel = new GradingLockModel();
|
|
$this->missingScoreOverrideModel = new MissingScoreOverrideModel();
|
|
// Retrieve the configuration values
|
|
$this->semester = $this->configModel->getConfig('semester');
|
|
$this->schoolYear = $this->configModel->getConfig('school_year');
|
|
$this->targetHigh = $this->configModel->getConfig('trophy_score');
|
|
$this->targetLow = $this->configModel->getConfig('pass_score');
|
|
$this->semesterScoreService = Services::semesterScoreService();
|
|
}
|
|
|
|
|
|
public function index()
|
|
{
|
|
log_message('debug', 'ScoreController::index invoked');
|
|
$semesterChoiceParam = $this->request->getGet('semester_choice');
|
|
$session = session();
|
|
if ($this->request->getGet('choose_semester') !== null) {
|
|
$session->remove('teacher_scores_selected_semester');
|
|
$session->remove('semester');
|
|
$semesterChoiceParam = null;
|
|
}
|
|
$normalizedChoice = $this->normalizeSemester($semesterChoiceParam);
|
|
$sessionSelected = $session->get('teacher_scores_selected_semester');
|
|
$selectedSemesterLabel = '';
|
|
if ($normalizedChoice !== '') {
|
|
$selectedSemesterLabel = ucfirst($normalizedChoice);
|
|
$session->set('teacher_scores_selected_semester', $selectedSemesterLabel);
|
|
$session->set('semester', $selectedSemesterLabel);
|
|
} elseif (!empty($sessionSelected)) {
|
|
$selectedSemesterLabel = $sessionSelected;
|
|
} else {
|
|
$invalidChoice = null;
|
|
if ($semesterChoiceParam !== null && $semesterChoiceParam !== '') {
|
|
$invalidChoice = $semesterChoiceParam;
|
|
}
|
|
return view('teacher/select_semester', [
|
|
'fallUrl' => base_url('/teacher/scores?semester_choice=Fall'),
|
|
'springUrl' => base_url('/teacher/scores?semester_choice=Spring'),
|
|
'invalidChoice' => $invalidChoice,
|
|
]);
|
|
}
|
|
$focusTarget = $this->request->getGet('focus');
|
|
$focusTarget = in_array($focusTarget, ['scores', 'comments'], true) ? $focusTarget : null;
|
|
|
|
// Step 1: Get the teacher_id from the session
|
|
$teacherId = session()->get('user_id');
|
|
$teacher = $this->userModel->find($teacherId);
|
|
$teacherFirst = $teacher['firstname'] ?? $teacher['first_name'] ?? '';
|
|
$teacherLast = $teacher['lastname'] ?? $teacher['last_name'] ?? '';
|
|
$teacherName = ($teacherFirst !== '' || $teacherLast !== '')
|
|
? trim($teacherFirst . ' ' . $teacherLast)
|
|
: 'Unknown';
|
|
|
|
$activeSemester = $selectedSemesterLabel;
|
|
$effectiveSemester = $activeSemester;
|
|
$effectiveSchoolYear = $this->schoolYear;
|
|
|
|
// ✅ Get class section ID for the active term (respect active selection)
|
|
$assignments = $this->teacherClassModel->getClassAssignmentsByUserId(
|
|
(int)$teacherId,
|
|
(string)$effectiveSchoolYear,
|
|
(string)$effectiveSemester
|
|
);
|
|
|
|
if (empty($assignments)) {
|
|
// Fallback: any class regardless of term
|
|
$fallback = $this->teacherClassModel
|
|
->where('teacher_id', $teacherId)
|
|
->first();
|
|
if ($fallback) {
|
|
$assignments = [[
|
|
'class_section_id' => $fallback['class_section_id'],
|
|
'class_section_name' => null,
|
|
'school_year' => $fallback['school_year'] ?? $effectiveSchoolYear,
|
|
'semester' => $fallback['semester'] ?? $effectiveSemester,
|
|
]];
|
|
$effectiveSemester = $fallback['semester'] ?? $effectiveSemester;
|
|
$effectiveSchoolYear = $fallback['school_year'] ?? $effectiveSchoolYear;
|
|
} else {
|
|
log_message('info', 'No class section assigned to teacher ID: ' . $teacherId);
|
|
return redirect()
|
|
->to('no-classes')
|
|
->with('message', 'You do not have an assigned class yet. Please contact the administration.');
|
|
}
|
|
}
|
|
|
|
$allowedIds = array_map(static fn($a) => (int)($a['class_section_id'] ?? 0), $assignments);
|
|
$allowedIds = array_values(array_filter(array_unique($allowedIds)));
|
|
|
|
$requestedId = (int)($this->request->getGet('class_section_id') ?? 0);
|
|
$sessionId = (int)(session()->get('class_section_id') ?? 0);
|
|
$chosenAssignment = null;
|
|
if ($requestedId && in_array($requestedId, $allowedIds, true)) {
|
|
$classSectionId = $requestedId;
|
|
foreach ($assignments as $a) {
|
|
if ((int)$a['class_section_id'] === $requestedId) { $chosenAssignment = $a; break; }
|
|
}
|
|
} elseif ($sessionId && in_array($sessionId, $allowedIds, true)) {
|
|
$classSectionId = $sessionId;
|
|
foreach ($assignments as $a) {
|
|
if ((int)$a['class_section_id'] === $sessionId) { $chosenAssignment = $a; break; }
|
|
}
|
|
} else {
|
|
$classSectionId = $allowedIds[0];
|
|
foreach ($assignments as $a) {
|
|
if ((int)$a['class_section_id'] === $classSectionId) { $chosenAssignment = $a; break; }
|
|
}
|
|
}
|
|
|
|
session()->set('class_section_id', $classSectionId);
|
|
|
|
if ($chosenAssignment) {
|
|
$effectiveSemester = $chosenAssignment['semester'] ?? $effectiveSemester;
|
|
$effectiveSchoolYear = $chosenAssignment['school_year'] ?? $effectiveSchoolYear;
|
|
}
|
|
|
|
// Build assigned teacher/TA names for this section (current term)
|
|
$rows = $this->db->table('teacher_class tc')
|
|
->select('tc.position, u.firstname, u.lastname')
|
|
->join('users u', 'u.id = tc.teacher_id', 'inner')
|
|
->where('tc.class_section_id', $classSectionId)
|
|
->where('tc.school_year', $effectiveSchoolYear)
|
|
->orderBy("FIELD(tc.position,'main','ta')", '', false)
|
|
->orderBy('u.firstname', 'ASC')
|
|
->get()->getResultArray();
|
|
|
|
$mains = [];
|
|
$tas = [];
|
|
foreach ($rows as $r) {
|
|
$name = trim(($r['firstname'] ?? '') . ' ' . ($r['lastname'] ?? ''));
|
|
if ($name === '') continue;
|
|
$pos = strtolower((string)($r['position'] ?? ''));
|
|
if (in_array($pos, ['main', 'teacher'], true)) {
|
|
$mains[] = $name;
|
|
} elseif (in_array($pos, ['ta', 'assistant', 'teacher_assistant'], true)) {
|
|
$tas[] = $name;
|
|
}
|
|
}
|
|
if (empty($mains) && !empty($teacherName)) {
|
|
$mains = [$teacherName];
|
|
}
|
|
$mainsText = implode(', ', $mains);
|
|
$tasText = implode(', ', $tas);
|
|
|
|
// Resolve class section name (by section code)
|
|
$csRow = $this->db->table('classSection')
|
|
->select('class_section_name')
|
|
->where('class_section_id', $classSectionId)
|
|
->get()->getRowArray();
|
|
$classSectionName = $csRow['class_section_name'] ?? '';
|
|
log_message('debug', "ScoreController::index teacher {$teacherId} classSection {$classSectionId} semester {$effectiveSemester}");
|
|
|
|
$studentTeacherInfo = $this->studentModel->getStudentInfoByClassSectionId(
|
|
$classSectionId,
|
|
$effectiveSemester,
|
|
$effectiveSchoolYear
|
|
);
|
|
if (!empty($studentTeacherInfo) && $this->semesterScoreService !== null) {
|
|
try {
|
|
$this->semesterScoreService->updateScoresForStudents($studentTeacherInfo);
|
|
} catch (\Throwable $e) {
|
|
log_message('error', 'ScoreController::index semester score refresh failed: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
// Step 2: Retrieve the student scores data
|
|
$students = $this->getStudentScores($classSectionId, $effectiveSemester, $effectiveSchoolYear, $teacherId);
|
|
|
|
// Step 3: Sort the students array by last name and then by first name
|
|
usort($students, function ($a, $b) {
|
|
$lastNameComparison = strcmp($a['lastname'], $b['lastname']);
|
|
return $lastNameComparison === 0
|
|
? strcmp($a['firstname'], $b['firstname'])
|
|
: $lastNameComparison;
|
|
});
|
|
|
|
if (ENVIRONMENT !== 'production') {
|
|
$sampleRows = array_map(static function ($student) {
|
|
return [
|
|
'id' => $student['student_id'],
|
|
'attendance_score' => $student['attendance_score'] ?? null,
|
|
'ptap_score' => $student['ptap_score'] ?? null,
|
|
'final_exam' => $student['final_exam'][0] ?? null,
|
|
];
|
|
}, array_slice($students, 0, 5));
|
|
log_message('debug', 'ScoreController::index sample rows: ' . json_encode($sampleRows));
|
|
}
|
|
|
|
$scoresLocked = $this->gradingLockModel->isLocked(
|
|
(int) $classSectionId,
|
|
(string) $effectiveSemester,
|
|
(string) $effectiveSchoolYear
|
|
);
|
|
|
|
// Step 4: Pass everything to the view
|
|
$data = [
|
|
'students' => $students,
|
|
'teacher_name' => $teacherName,
|
|
'class_section_id' => $classSectionId,
|
|
'class_section_name' => $classSectionName,
|
|
'mainsText' => $mainsText,
|
|
'tasText' => $tasText,
|
|
'activeSemester' => $activeSemester,
|
|
'selectedSemester' => $activeSemester,
|
|
'focusTarget' => $focusTarget,
|
|
'schoolYear' => $effectiveSchoolYear,
|
|
'scoresLocked' => $scoresLocked,
|
|
];
|
|
|
|
return view('/teacher/scores', $data);
|
|
}
|
|
|
|
public function submitScoresLock()
|
|
{
|
|
$classSectionId = (int) ($this->request->getPost('class_section_id') ?? session()->get('class_section_id') ?? 0);
|
|
$semesterRaw = (string) ($this->request->getPost('semester') ?? $this->request->getPost('selected_semester') ?? '');
|
|
$semesterNormalized = $this->normalizeSemester($semesterRaw);
|
|
$semester = $semesterNormalized !== '' ? ucfirst($semesterNormalized) : $this->semester;
|
|
$schoolYear = (string) ($this->request->getPost('school_year') ?? $this->schoolYear);
|
|
|
|
if ($classSectionId <= 0 || $semester === '' || $schoolYear === '') {
|
|
return redirect()->back()->with('error', 'Missing class section or term.');
|
|
}
|
|
|
|
$missingOk = $this->request->getPost('missing_ok') ?? [];
|
|
$studentIds = $this->request->getPost('student_ids') ?? array_keys((array) $missingOk);
|
|
$studentIds = array_map('intval', (array) $studentIds);
|
|
$teacherId = (int) (session()->get('user_id') ?? 0);
|
|
$isFall = strtolower($semester) === 'fall';
|
|
$isSpring = strtolower($semester) === 'spring';
|
|
|
|
$commentTypes = ['ptap_comment'];
|
|
if ($isFall) {
|
|
$commentTypes[] = 'midterm_comment';
|
|
}
|
|
if ($isSpring) {
|
|
$commentTypes[] = 'final_comment';
|
|
}
|
|
|
|
foreach ($commentTypes as $type) {
|
|
$checkedItems = [];
|
|
if (is_array($missingOk)) {
|
|
foreach ($missingOk as $studentId => $types) {
|
|
if (!is_array($types) || empty($types[$type])) {
|
|
continue;
|
|
}
|
|
$checkedItems[] = [
|
|
'student_id' => (int) $studentId,
|
|
'item_index' => null,
|
|
];
|
|
}
|
|
}
|
|
$this->missingScoreOverrideModel->replaceOverrides(
|
|
$classSectionId,
|
|
(string) $semester,
|
|
(string) $schoolYear,
|
|
$type,
|
|
$studentIds,
|
|
null,
|
|
$checkedItems,
|
|
$teacherId,
|
|
true
|
|
);
|
|
}
|
|
|
|
$existing = $this->gradingLockModel->getLock($classSectionId, $semester, $schoolYear);
|
|
if (!empty($existing) && !empty($existing['is_locked'])) {
|
|
return redirect()->back()->with('status', 'Scores already submitted and locked.');
|
|
}
|
|
|
|
$userId = $teacherId;
|
|
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 redirect()->back()->with('status', 'Scores submitted and locked for this semester.');
|
|
}
|
|
|
|
public function noClasses()
|
|
{
|
|
return view('/teacher/no_classes');
|
|
}
|
|
|
|
private function getStudentScores(int $classSectionId, $semester, $schoolYear, int $teacherId)
|
|
{
|
|
if (!$semester || !$schoolYear) {
|
|
throw new \RuntimeException('Semester or school year configuration is missing.');
|
|
}
|
|
|
|
$totalSemesterDays = null;
|
|
$normSemester = $this->normalizeSemester($semester);
|
|
if ($schoolYear !== '' && $normSemester !== '') {
|
|
$semRange = $this->getSemesterRange($schoolYear, $normSemester);
|
|
if ($semRange) {
|
|
try {
|
|
$events = $this->calendarModel->getEventsBySchoolYearAndSemester($schoolYear, $normSemester);
|
|
$noSchoolSundays = 0;
|
|
foreach ($events as $event) {
|
|
if (!empty($event['no_school'])) {
|
|
$date = new \DateTime($event['date']);
|
|
if ($date->format('N') == 7) {
|
|
if ($date->format('Y-m-d') >= $semRange[0] && $date->format('Y-m-d') <= $semRange[1]) {
|
|
$noSchoolSundays++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
$totalSemesterDays = $this->countSundays($semRange[0], $semRange[1]) - $noSchoolSundays;
|
|
} catch (\Throwable $e) {
|
|
log_message('error', 'Failed to count sundays for score calculation: ' . $e->getMessage());
|
|
$totalSemesterDays = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($totalSemesterDays === null) {
|
|
$totalSemesterDays = strtolower($semester) === 'fall'
|
|
? $this->configModel->getConfig('total_semester1_days')
|
|
: $this->configModel->getConfig('total_semester2_days');
|
|
}
|
|
|
|
|
|
if (!$totalSemesterDays) {
|
|
throw new \RuntimeException('Total semester days configuration is missing.');
|
|
}
|
|
|
|
if ($classSectionId <= 0) {
|
|
throw new \RuntimeException('Invalid class section for the current teacher.');
|
|
}
|
|
$students = $this->prepareStudents($classSectionId, $teacherId, $schoolYear);
|
|
$studentIds = array_keys($students);
|
|
|
|
// Load regular scores
|
|
$this->loadScoresByType(new HomeworkModel(), 'homework', $students, $classSectionId, $semester, $schoolYear);
|
|
$this->loadScoresByType(new QuizModel(), 'quiz', $students, $classSectionId, $semester, $schoolYear);
|
|
$this->loadScoresByType(new ProjectModel(), 'project', $students, $classSectionId, $semester, $schoolYear);
|
|
$this->loadScoresByType(new MidtermExamModel(), 'midterm', $students, $classSectionId, $semester, $schoolYear);
|
|
$this->loadScoresByType(new FinalExamModel(), 'final_exam', $students, $classSectionId, $semester, $schoolYear);
|
|
$this->loadScoresByType(new ParticipationModel(), 'participation', $students, $classSectionId, $semester, $schoolYear);
|
|
|
|
$this->calculateStudentScores($students, $classSectionId, $semester, $totalSemesterDays);
|
|
|
|
// Load all semester scores in one query
|
|
$this->loadSemesterScores($students, $classSectionId, $semester, $schoolYear);
|
|
|
|
$this->loadComments(new ScoreCommentModel(), $students, $studentIds, $semester, $schoolYear);
|
|
|
|
$homeworkIndexes = $this->loadIndexedScores(new HomeworkModel(), 'homework_index', $students, $classSectionId, $semester, $schoolYear, 'homework_scores_by_index');
|
|
$quizIndexes = $this->loadIndexedScores(new QuizModel(), 'quiz_index', $students, $classSectionId, $semester, $schoolYear, 'quiz_scores_by_index');
|
|
$projectIndexes = $this->loadIndexedScores(new ProjectModel(), 'project_index', $students, $classSectionId, $semester, $schoolYear, 'project_scores_by_index');
|
|
$this->loadMissingOverrides($students, $classSectionId, $semester, $schoolYear);
|
|
$this->buildMissingItems($students, $homeworkIndexes, $quizIndexes, $projectIndexes, $semester);
|
|
|
|
return $students;
|
|
}
|
|
|
|
private function loadSemesterScores(&$students, $classSectionId, $semester, $schoolYear)
|
|
{
|
|
// Get all scores in one query
|
|
$results = $this->semesterScoreModel->select('student_id, attendance_score, ptap_score, semester_score, midterm_exam_score, final_exam_score')
|
|
->where('class_section_id', $classSectionId)
|
|
->where('semester', $semester)
|
|
->where('school_year', $schoolYear)
|
|
->findAll();
|
|
|
|
foreach ($results as $row) {
|
|
$studentId = $row['student_id'];
|
|
|
|
if (isset($students[$studentId])) {
|
|
$attendanceScore = $this->roundScore($row['attendance_score'] ?? null);
|
|
if ($attendanceScore !== null && !isset($students[$studentId]['attendance_score'])) {
|
|
$students[$studentId]['attendance_score'] = $attendanceScore;
|
|
}
|
|
$ptapScore = $this->roundScore($row['ptap_score'] ?? null);
|
|
if ($ptapScore !== null && !isset($students[$studentId]['ptap_score'])) {
|
|
$students[$studentId]['ptap_score'] = $ptapScore;
|
|
}
|
|
$semesterScore = $this->roundScore($row['semester_score'] ?? null);
|
|
if ($semesterScore !== null && !isset($students[$studentId]['semester_score'])) {
|
|
$students[$studentId]['semester_score'] = $semesterScore;
|
|
}
|
|
$midtermScore = $this->roundScore($row['midterm_exam_score'] ?? null);
|
|
if ($midtermScore !== null && empty($students[$studentId]['midterm'])) {
|
|
$students[$studentId]['midterm'][] = $midtermScore;
|
|
}
|
|
$finalExamScore = $this->roundScore($row['final_exam_score'] ?? null);
|
|
if ($finalExamScore !== null && empty($students[$studentId]['final_exam'])) {
|
|
$students[$studentId]['final_exam'][] = $finalExamScore;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private function roundScore($value): ?float
|
|
{
|
|
if ($value === null) {
|
|
return null;
|
|
}
|
|
|
|
// Ensure value is numeric before rounding
|
|
return is_numeric($value) ? round((float)$value, 1) : null;
|
|
}
|
|
|
|
private function prepareStudents($classSectionId, $teacherId, $schoolYear)
|
|
{
|
|
$students = [];
|
|
$studentClasses = $this->studentClassModel
|
|
->active()
|
|
->where('student_class.class_section_id', $classSectionId)
|
|
->findAll();
|
|
|
|
foreach ($studentClasses as $studentClass) {
|
|
$student = $this->studentModel
|
|
->select('id, firstname, lastname, age, school_id')
|
|
->where('id', $studentClass['student_id'])
|
|
->where('is_active', 1)
|
|
->first();
|
|
if ($student) {
|
|
$students[$student['id']] = [
|
|
'student_id' => $student['id'],
|
|
'firstname' => $student['firstname'],
|
|
'lastname' => $student['lastname'],
|
|
'age' => $student['age'],
|
|
'school_id' => $student['school_id'],
|
|
'class_section_id' => $classSectionId,
|
|
'school_year' => $schoolYear,
|
|
'teacher_id' => $teacherId,
|
|
'homework' => [],
|
|
'quiz' => [],
|
|
'midterm' => [],
|
|
'final_exam' => [],
|
|
'project' => [],
|
|
'participation' => [],
|
|
'ptap' => [],
|
|
'ptap_comment' => '',
|
|
'midterm_comment' => '',
|
|
'final_comment' => ''
|
|
];
|
|
}
|
|
}
|
|
return $students;
|
|
}
|
|
|
|
private function loadScoresByType($model, $type, &$students, $classSectionId, $semester, $schoolYear)
|
|
{
|
|
$records = $model->where('class_section_id', $classSectionId)
|
|
->where('semester', $semester)
|
|
->where('school_year', $schoolYear)
|
|
->findAll();
|
|
|
|
foreach ($records as $rec) {
|
|
if (!isset($students[$rec['student_id']])) {
|
|
continue;
|
|
}
|
|
|
|
// Only include real numeric scores; leave null/blank out entirely
|
|
if ($rec['score'] === null || $rec['score'] === '') {
|
|
continue;
|
|
}
|
|
|
|
if (!is_numeric($rec['score'])) {
|
|
continue;
|
|
}
|
|
|
|
$students[$rec['student_id']][$type][] = (float) $rec['score'];
|
|
}
|
|
}
|
|
|
|
private function loadIndexedScores($model, string $indexField, array &$students, int $classSectionId, $semester, $schoolYear, string $storeKey): array
|
|
{
|
|
$records = $model->select("student_id, {$indexField} as idx, score")
|
|
->where('class_section_id', $classSectionId)
|
|
->where('semester', $semester)
|
|
->where('school_year', $schoolYear)
|
|
->findAll();
|
|
|
|
$indexes = [];
|
|
foreach ($records as $rec) {
|
|
$sid = (int) ($rec['student_id'] ?? 0);
|
|
$idx = (int) ($rec['idx'] ?? 0);
|
|
if ($idx <= 0) {
|
|
continue;
|
|
}
|
|
$indexes[$idx] = true;
|
|
if (isset($students[$sid])) {
|
|
if (!isset($students[$sid][$storeKey]) || !is_array($students[$sid][$storeKey])) {
|
|
$students[$sid][$storeKey] = [];
|
|
}
|
|
$students[$sid][$storeKey][$idx] = $rec['score'] ?? null;
|
|
}
|
|
}
|
|
|
|
$indexList = array_keys($indexes);
|
|
sort($indexList, SORT_NUMERIC);
|
|
return $indexList;
|
|
}
|
|
|
|
private function loadComments($commentModel, &$students, $studentIds, $semester, $schoolYear)
|
|
{
|
|
if (empty($studentIds)) return;
|
|
|
|
$semVariants = array_values(array_unique(array_filter([
|
|
$semester,
|
|
strtolower((string)$semester),
|
|
ucfirst(strtolower((string)$semester)),
|
|
], static fn($v) => $v !== '')));
|
|
|
|
$comments = $commentModel->whereIn('student_id', $studentIds)
|
|
->whereIn('semester', $semVariants)
|
|
->where('school_year', $schoolYear)
|
|
->findAll();
|
|
|
|
foreach ($comments as $comment) {
|
|
$sid = $comment['student_id'];
|
|
if (!isset($students[$sid])) continue;
|
|
|
|
$type = strtolower($comment['score_type']);
|
|
|
|
// Set both comment and comment_review for each type
|
|
if ($type === 'ptap') {
|
|
$students[$sid]['ptap_comment'] = $comment['comment'];
|
|
$students[$sid]['ptap_comment_review'] = $comment['comment_review'] ?? null;
|
|
}
|
|
elseif ($type === 'midterm') {
|
|
$students[$sid]['midterm_comment'] = $comment['comment'];
|
|
$students[$sid]['midterm_comment_review'] = $comment['comment_review'] ?? null;
|
|
}
|
|
elseif ($type === 'final') {
|
|
$students[$sid]['final_comment'] = $comment['comment'];
|
|
$students[$sid]['final_comment_review'] = $comment['comment_review'] ?? null;
|
|
}
|
|
elseif ($type === 'attendance' || $type === 'attendance_comment') {
|
|
$students[$sid]['attendance_comment'] = $comment['comment'];
|
|
$students[$sid]['attendance_comment_review'] = $comment['comment_review'] ?? null;
|
|
}
|
|
}
|
|
|
|
// Fallback: if no attendance comment saved, derive one from score for display
|
|
helper('attendance_comment');
|
|
foreach ($students as $sid => &$student) {
|
|
if (!isset($student['attendance_comment']) && isset($student['attendance_score'])) {
|
|
$student['attendance_comment'] = attendance_comment_from_score(
|
|
$student['attendance_score'],
|
|
$student['firstname'] ?? ''
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
private function loadMissingOverrides(array &$students, int $classSectionId, $semester, $schoolYear): void
|
|
{
|
|
if ($classSectionId <= 0 || empty($students)) {
|
|
return;
|
|
}
|
|
|
|
$rows = $this->missingScoreOverrideModel
|
|
->select('student_id, item_type, item_index')
|
|
->where('class_section_id', $classSectionId)
|
|
->where('semester', $semester)
|
|
->where('school_year', $schoolYear)
|
|
->where('is_allowed', 1)
|
|
->findAll();
|
|
|
|
foreach ($rows as $row) {
|
|
$sid = (int) ($row['student_id'] ?? 0);
|
|
if (!isset($students[$sid])) {
|
|
continue;
|
|
}
|
|
$type = (string) ($row['item_type'] ?? '');
|
|
if ($type === '') {
|
|
continue;
|
|
}
|
|
$idx = isset($row['item_index']) ? (int) $row['item_index'] : 0;
|
|
if (!isset($students[$sid]['missing_ok'])) {
|
|
$students[$sid]['missing_ok'] = [];
|
|
}
|
|
if (!isset($students[$sid]['missing_ok'][$type])) {
|
|
$students[$sid]['missing_ok'][$type] = [];
|
|
}
|
|
$students[$sid]['missing_ok'][$type][$idx] = true;
|
|
}
|
|
}
|
|
|
|
private function buildMissingItems(array &$students, array $homeworkIndexes, array $quizIndexes, array $projectIndexes, $semester): void
|
|
{
|
|
$isFall = strtolower((string) $semester) === 'fall';
|
|
$isSpring = strtolower((string) $semester) === 'spring';
|
|
|
|
foreach ($students as $sid => &$student) {
|
|
$missing = [];
|
|
$missingOk = $student['missing_ok'] ?? [];
|
|
$isAllowed = static function (array $missingOk, string $type, ?int $idx = null): bool {
|
|
$key = $idx === null ? 0 : $idx;
|
|
return !empty($missingOk[$type][$key]);
|
|
};
|
|
|
|
foreach ($homeworkIndexes as $idx) {
|
|
$val = $student['homework_scores_by_index'][$idx] ?? null;
|
|
if (($val === null || $val === '' || !is_numeric($val)) && !$isAllowed($missingOk, 'homework', $idx)) {
|
|
$missing[] = 'Homework ' . $idx;
|
|
}
|
|
}
|
|
|
|
foreach ($quizIndexes as $idx) {
|
|
$val = $student['quiz_scores_by_index'][$idx] ?? null;
|
|
if (($val === null || $val === '' || !is_numeric($val)) && !$isAllowed($missingOk, 'quiz', $idx)) {
|
|
$missing[] = 'Quiz ' . $idx;
|
|
}
|
|
}
|
|
|
|
foreach ($projectIndexes as $idx) {
|
|
$val = $student['project_scores_by_index'][$idx] ?? null;
|
|
if (($val === null || $val === '' || !is_numeric($val)) && !$isAllowed($missingOk, 'project', $idx)) {
|
|
$missing[] = 'Project ' . $idx;
|
|
}
|
|
}
|
|
|
|
if (empty($student['participation']) && !$isAllowed($missingOk, 'participation', 0)) {
|
|
$missing[] = 'Participation';
|
|
}
|
|
|
|
if ($isFall && empty($student['midterm']) && !$isAllowed($missingOk, 'midterm_exam', 0)) {
|
|
$missing[] = 'Midterm Exam';
|
|
}
|
|
|
|
if ($isSpring && empty($student['final_exam']) && !$isAllowed($missingOk, 'final_exam', 0)) {
|
|
$missing[] = 'Final Exam';
|
|
}
|
|
|
|
$ptapComment = trim((string) ($student['ptap_comment'] ?? ''));
|
|
if ($ptapComment === '' && !$isAllowed($missingOk, 'ptap_comment', 0)) {
|
|
$missing[] = 'PTAP Comment';
|
|
}
|
|
|
|
if ($isFall) {
|
|
$midtermComment = trim((string) ($student['midterm_comment'] ?? ''));
|
|
if ($midtermComment === '' && !$isAllowed($missingOk, 'midterm_comment', 0)) {
|
|
$missing[] = 'Midterm Comment';
|
|
}
|
|
}
|
|
|
|
if ($isSpring) {
|
|
$finalComment = trim((string) ($student['final_comment'] ?? ''));
|
|
if ($finalComment === '' && !$isAllowed($missingOk, 'final_comment', 0)) {
|
|
$missing[] = 'Final Comment';
|
|
}
|
|
}
|
|
|
|
$student['missing_items'] = $missing;
|
|
}
|
|
}
|
|
|
|
private function calculateStudentScores(&$students, int $classSectionId, $semester, $totalDays)
|
|
{
|
|
foreach ($students as $sid => &$s) {
|
|
$attendanceScore = $this->calculateAttendanceScoreForStudent(
|
|
$sid,
|
|
$classSectionId,
|
|
$semester,
|
|
$s['school_year'] ?? '',
|
|
$totalDays
|
|
);
|
|
if ($attendanceScore === null) {
|
|
$attendanceScore = 100.0;
|
|
}
|
|
$attendance = $attendanceScore;
|
|
|
|
$homeworkScores = array_values(array_filter(
|
|
$s['homework'],
|
|
static fn($score) => $score !== null && $score !== '' && is_numeric($score)
|
|
));
|
|
$avgHomework = !empty($homeworkScores) ? array_sum($homeworkScores) / count($homeworkScores) : null;
|
|
|
|
$quizScores = array_values(array_filter(
|
|
$s['quiz'],
|
|
static fn($score) => $score !== null && $score !== '' && is_numeric($score)
|
|
));
|
|
$avgQuiz = !empty($quizScores) ? array_sum($quizScores) / count($quizScores) : null;
|
|
|
|
$projectScores = array_values(array_filter(
|
|
$s['project'],
|
|
static fn($score) => $score !== null && $score !== '' && is_numeric($score)
|
|
));
|
|
$avgProject = !empty($projectScores) ? array_sum($projectScores) / count($projectScores) : null;
|
|
|
|
$participationScores = array_values(array_filter(
|
|
$s['participation'],
|
|
static fn($score) => $score !== null && $score !== '' && is_numeric($score)
|
|
));
|
|
$avgParticipation = !empty($participationScores) ? array_sum($participationScores) / count($participationScores) : null;
|
|
|
|
$ptap = $this->calculatePtapScore($avgHomework, $avgQuiz, $avgProject, $avgParticipation);
|
|
|
|
$midterm = (!empty($s['midterm']) && $s['midterm'][0] !== null && $s['midterm'][0] !== '')
|
|
? (float) $s['midterm'][0]
|
|
: null;
|
|
$final = (!empty($s['final_exam']) && $s['final_exam'][0] !== null && $s['final_exam'][0] !== '')
|
|
? (float) $s['final_exam'][0]
|
|
: null;
|
|
|
|
$score = null;
|
|
if ($ptap !== null) {
|
|
$score = 0.2 * $ptap + 0.2 * $attendance;
|
|
if (strtolower($semester) === 'fall') {
|
|
if ($midterm !== null) {
|
|
$score += 0.6 * $midterm;
|
|
} else {
|
|
$score = null;
|
|
}
|
|
} else {
|
|
if ($final !== null) {
|
|
$score += 0.6 * $final;
|
|
} else {
|
|
$score = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
$s['average_homework'] = $avgHomework !== null ? round($avgHomework, 1) : null;
|
|
$s['average_quiz'] = $avgQuiz !== null ? round($avgQuiz, 1) : null;
|
|
$s['average_project'] = $avgProject !== null ? round($avgProject, 1) : null;
|
|
$s['average_participation'] = $avgParticipation !== null ? round($avgParticipation, 1) : null;
|
|
$s['ptap_score'] = $ptap !== null ? round($ptap, 1) : null;
|
|
$s['midterm_score'] = $midterm !== null ? round($midterm, 1) : null;
|
|
$s['final_exam_score'] = $final !== null ? round($final, 1) : null;
|
|
$s['attendance_score'] = round($attendanceScore, 1);
|
|
$s['semester_score'] = $score !== null ? round(min($score, 100), 1) : null;
|
|
}
|
|
}
|
|
|
|
private function calculateAttendanceScoreForStudent(
|
|
int $studentId,
|
|
int $classSectionId,
|
|
string $semester,
|
|
string $schoolYear,
|
|
?int $totalDays
|
|
): ?float {
|
|
$score = $this->getAttendanceScoreFromCalculator(
|
|
$studentId,
|
|
$semester,
|
|
$schoolYear,
|
|
$classSectionId
|
|
);
|
|
if ($score !== null) {
|
|
return $score;
|
|
}
|
|
return $this->calculateAttendanceScoreFallback(
|
|
$studentId,
|
|
$semester,
|
|
$schoolYear,
|
|
$totalDays
|
|
);
|
|
}
|
|
|
|
private function getAttendanceScoreFromCalculator(
|
|
int $studentId,
|
|
string $semester,
|
|
string $schoolYear,
|
|
int $classSectionId
|
|
): ?float {
|
|
try {
|
|
$result = $this->attendanceCalculator->calculate($studentId, $semester, $schoolYear, $classSectionId);
|
|
$score = $result['attendance_score'] ?? null;
|
|
if ($score === null || $score === '') {
|
|
return null;
|
|
}
|
|
return (float) $score;
|
|
} catch (\Throwable $e) {
|
|
log_message(
|
|
'error',
|
|
'ScoreController::getAttendanceScoreFromCalculator failed for '
|
|
. "student {$studentId}: " . $e->getMessage()
|
|
);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private function calculateAttendanceScoreFallback(
|
|
int $studentId,
|
|
string $semester,
|
|
string $schoolYear,
|
|
?int $totalDays
|
|
): ?float {
|
|
if ($totalDays === null || $totalDays <= 0) {
|
|
return 100.0;
|
|
}
|
|
|
|
$attendanceDataModel = new AttendanceDataModel();
|
|
$absences = $attendanceDataModel
|
|
->where('student_id', $studentId)
|
|
->where('semester', $semester)
|
|
->where('school_year', $schoolYear)
|
|
->where('status', 'absent')
|
|
->countAllResults();
|
|
|
|
return max(0, ((($totalDays - $absences) / $totalDays) * 100));
|
|
}
|
|
|
|
/**
|
|
* PTAP weighting rules (tests are represented by quiz averages):
|
|
* - No tests and no projects: 50% homework, 50% participation.
|
|
* - No projects: 40% homework, 40% participation, 20% tests.
|
|
* - No tests: 34% homework, 33% participation, 33% projects.
|
|
* - All available: 30% homework, 30% participation, 30% projects, 10% tests.
|
|
*/
|
|
private function calculatePtapScore(?float $avgHomework, ?float $avgQuiz, ?float $avgProject, ?float $avgParticipation): ?float
|
|
{
|
|
$hasTests = $avgQuiz !== null;
|
|
$hasProjects = $avgProject !== null;
|
|
|
|
if (!$hasTests && !$hasProjects) {
|
|
if ($avgHomework === null || $avgParticipation === null) {
|
|
return null;
|
|
}
|
|
return round(($avgHomework * 0.5) + ($avgParticipation * 0.5), 1);
|
|
}
|
|
|
|
if (!$hasProjects) {
|
|
if ($avgHomework === null || $avgParticipation === null || $avgQuiz === null) {
|
|
return null;
|
|
}
|
|
return round(($avgHomework * 0.4) + ($avgParticipation * 0.4) + ($avgQuiz * 0.2), 1);
|
|
}
|
|
|
|
if (!$hasTests) {
|
|
if ($avgHomework === null || $avgParticipation === null || $avgProject === null) {
|
|
return null;
|
|
}
|
|
return round(($avgHomework * 0.34) + ($avgParticipation * 0.33) + ($avgProject * 0.33), 1);
|
|
}
|
|
|
|
if ($avgHomework === null || $avgParticipation === null || $avgProject === null || $avgQuiz === null) {
|
|
return null;
|
|
}
|
|
|
|
return round(
|
|
($avgHomework * 0.3) + ($avgParticipation * 0.3) + ($avgProject * 0.3) + ($avgQuiz * 0.1),
|
|
1
|
|
);
|
|
}
|
|
|
|
private function getHomeworkCount(int $classSectionId, string $semester, string $schoolYear): int
|
|
{
|
|
if ($classSectionId <= 0 || $semester === '' || $schoolYear === '') {
|
|
return 0;
|
|
}
|
|
|
|
$builder = $this->db->table('homework')
|
|
->select('COUNT(DISTINCT homework_index) AS hw_count')
|
|
->where('class_section_id', $classSectionId)
|
|
->where('semester', $semester);
|
|
|
|
if ($schoolYear !== '') {
|
|
$builder->where('school_year', $schoolYear);
|
|
}
|
|
|
|
$row = $builder->get()->getRow();
|
|
|
|
// Fallback: if none found (school_year not populated), drop school_year filter
|
|
if ((!$row || $row->hw_count === null) && $schoolYear !== '') {
|
|
$row = $this->db->table('homework')
|
|
->select('COUNT(DISTINCT homework_index) AS hw_count')
|
|
->where('class_section_id', $classSectionId)
|
|
->where('semester', $semester)
|
|
->get()
|
|
->getRow();
|
|
}
|
|
|
|
return ($row && $row->hw_count !== null) ? (int) $row->hw_count : 0;
|
|
}
|
|
|
|
private function saveFinalScores($students, $model, $teacherId, $classSectionId, $semester, $schoolYear)
|
|
{
|
|
foreach ($students as $s) {
|
|
$existing = $model->where('student_id', $s['student_id'])
|
|
->where('class_section_id', $classSectionId)
|
|
->where('semester', $semester)
|
|
->where('school_year', $schoolYear)
|
|
->first();
|
|
|
|
$data = [
|
|
'student_id' => $s['student_id'],
|
|
'class_section_id' => $classSectionId,
|
|
'updated_by' => $teacherId,
|
|
'score' => $s['semester_score'],
|
|
'score_letter' => $this->convertScoreToLetter($s['semester_score']),
|
|
'semester' => $semester,
|
|
'school_year' => $schoolYear,
|
|
'updated_at' => utc_now(),
|
|
];
|
|
|
|
if ($existing) $model->update($existing['id'], $data);
|
|
else {
|
|
$data['created_at'] = utc_now();
|
|
$model->save($data);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
private function convertScoreToLetter($score)
|
|
{
|
|
// Example conversion function, adjust logic as needed
|
|
if ($score >= 90) {
|
|
return 'A';
|
|
} elseif ($score >= 80) {
|
|
return 'B';
|
|
} elseif ($score >= 70) {
|
|
return 'C';
|
|
} elseif ($score >= 60) {
|
|
return 'D';
|
|
} else {
|
|
return 'F';
|
|
}
|
|
}
|
|
|
|
private function resolveActiveSemester(): string
|
|
{
|
|
$session = session();
|
|
$selected = $session->get('teacher_scores_selected_semester');
|
|
if (!empty($selected)) {
|
|
return (string) $selected;
|
|
}
|
|
return $session->get('semester') ?? $this->semester;
|
|
}
|
|
|
|
protected function normalizeSemester(?string $input): string
|
|
{
|
|
$s = strtolower(trim((string)$input));
|
|
if ($s === 'fall' || str_contains($s, '1')) return 'fall';
|
|
if ($s === 'spring' || str_contains($s, '2')) return 'spring';
|
|
return '';
|
|
}
|
|
|
|
protected function getSemesterRange(string $schoolYear, string $semester): ?array
|
|
{
|
|
$norm = $this->normalizeSemester($semester);
|
|
if ($norm === '' || !preg_match('/^(\\d{4})/', trim($schoolYear), $m)) {
|
|
return null;
|
|
}
|
|
$y1 = (int)$m[1];
|
|
$y2 = $y1 + 1;
|
|
|
|
$fallStartCfg = (string)($this->configModel->getConfig('fall_semester_start') ?? '');
|
|
$fallEndCfg = (string)($this->configModel->getConfig('fall_end_date') ?? ''); // e.g., 'YYYY-01-15'
|
|
$springStartCfg = (string)($this->configModel->getConfig('spring_semester_start') ?? ''); // e.g., 'YYYY-01-16'
|
|
$springEndCfg = (string)($this->configModel->getConfig('last_school_day') ?? '');
|
|
|
|
if ($norm === 'fall') {
|
|
$start = ($fallStartCfg !== '') ? date($y1 . '-m-d', strtotime($fallStartCfg)) : "{$y1}-09-01";
|
|
$end = ($fallEndCfg !== '') ? date($y2 . '-m-d', strtotime($fallEndCfg)) : "{$y2}-01-15";
|
|
return [$start, $end];
|
|
}
|
|
|
|
if ($norm === 'spring') {
|
|
$start = ($springStartCfg !== '') ? date($y2 . '-m-d', strtotime($springStartCfg)) : "{$y2}-01-16";
|
|
$end = ($springEndCfg !== '') ? date($y2 . '-m-d', strtotime($springEndCfg)) : "{$y2}-06-30";
|
|
return [$start, $end];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function countSundays(string $startDate, string $endDate): int
|
|
{
|
|
$start = new \DateTime($startDate);
|
|
$end = new \DateTime($endDate);
|
|
$sundays = 0;
|
|
$interval = new \DateInterval('P1D');
|
|
$period = new \DatePeriod($start, $interval, $end->modify('+1 day')); // include end date
|
|
|
|
foreach ($period as $day) {
|
|
if ($day->format('N') == 7) {
|
|
$sundays++;
|
|
}
|
|
}
|
|
return $sundays;
|
|
}
|
|
|
|
|
|
public function updateFinalExamScores()
|
|
{
|
|
return $this->updateScores('final_exam');
|
|
}
|
|
|
|
public function updateFinalExamScoresForGrading()
|
|
{
|
|
return $this->updateScores('final_exam', base_url('grading'));
|
|
}
|
|
|
|
public function updateMidtermScores()
|
|
{
|
|
return $this->updateScores('midterm_exam');
|
|
}
|
|
|
|
public function updateScores(string $table, ?string $redirectUrl = null)
|
|
{
|
|
$builder = $this->db->table($table);
|
|
|
|
$scores = $this->request->getPost('final_score'); // name must match input form
|
|
$teacherId = session()->get('user_id');
|
|
$classSectionId = session()->get('class_section_id');
|
|
$semester = $this->resolveActiveSemester();
|
|
$affectedStudents = [];
|
|
|
|
$classSectionId = (int) ($classSectionId ?? 0);
|
|
if ($classSectionId > 0 && $this->gradingLockModel->isLocked($classSectionId, $semester, $this->schoolYear)) {
|
|
return redirect()->back()->with('error', 'Scores are locked for this class. Unlock to edit.');
|
|
}
|
|
|
|
if (is_array($scores) && in_array($table, ['midterm_exam', 'final_exam'], true)) {
|
|
$missingOk = $this->request->getPost('missing_ok') ?? [];
|
|
$studentIds = array_keys($scores);
|
|
$checkedItems = [];
|
|
if (is_array($missingOk)) {
|
|
foreach ($missingOk as $studentId => $flags) {
|
|
$checkedItems[] = [
|
|
'student_id' => (int) $studentId,
|
|
'item_index' => null,
|
|
];
|
|
}
|
|
}
|
|
$this->missingScoreOverrideModel->replaceOverrides(
|
|
$classSectionId,
|
|
(string) $semester,
|
|
(string) $this->schoolYear,
|
|
$table === 'midterm_exam' ? 'midterm_exam' : 'final_exam',
|
|
$studentIds,
|
|
null,
|
|
$checkedItems,
|
|
$teacherId,
|
|
true
|
|
);
|
|
}
|
|
|
|
if (is_array($scores)) {
|
|
foreach ($scores as $studentId => $data) {
|
|
if (!is_numeric($studentId)) {
|
|
continue;
|
|
}
|
|
|
|
$rawScore = $data['score'] ?? null;
|
|
$normalizedScore = is_numeric($rawScore) ? (float) $rawScore : null;
|
|
|
|
// Check if a record already exists for this student
|
|
$existing = $builder
|
|
->where('student_id', $studentId)
|
|
->where('class_section_id', $classSectionId)
|
|
->where('semester', $semester)
|
|
->where('school_year', $this->schoolYear)
|
|
->get()
|
|
->getRow();
|
|
|
|
$insertData = [
|
|
'student_id' => $studentId,
|
|
'class_section_id' => $classSectionId,
|
|
'updated_by' => $teacherId,
|
|
'score' => $normalizedScore,
|
|
//'comment' => $data['comment'] ?? null,
|
|
'semester' => $semester,
|
|
'school_year' => $this->schoolYear,
|
|
'updated_at' => utc_now()
|
|
];
|
|
|
|
if (strcasecmp($table, 'quiz') === 0) {
|
|
$insertData['quiz_index'] = $data['quiz_index'] ?? null;
|
|
}
|
|
|
|
if ($existing) {
|
|
// Update existing
|
|
$builder->where('id', $existing->id)->update($insertData);
|
|
} else {
|
|
// Insert new
|
|
$insertData['created_at'] = utc_now();
|
|
$builder->insert($insertData);
|
|
}
|
|
|
|
// Track student for events (avoid duplicates)
|
|
$affectedStudents[(int)$studentId] = true;
|
|
}
|
|
}
|
|
|
|
// If final exam scores updated during Spring, trigger finalScoreReleased for these students
|
|
if (strcasecmp($table, 'final_exam') === 0 && strcasecmp((string)$this->semester, 'Spring') === 0 && !empty($affectedStudents)) {
|
|
$ids = array_keys($affectedStudents);
|
|
$payload = ['students' => array_map(static fn($sid) => ['id' => (int)$sid], $ids)];
|
|
//Events::trigger('finalScoreReleased', $payload);
|
|
}
|
|
|
|
// Ensure semester_scores are re-calculated when midterm/final exams change.
|
|
if (in_array(strtolower($table), ['final_exam', 'midterm_exam'], true) && $this->semesterScoreService !== null) {
|
|
try {
|
|
$studentTeacherInfo = $this->studentModel->getStudentInfoByClassSectionId(
|
|
$classSectionId,
|
|
$semester,
|
|
$this->schoolYear
|
|
);
|
|
if (!empty($studentTeacherInfo)) {
|
|
$this->semesterScoreService->updateScoresForStudents($studentTeacherInfo, $semester, $this->schoolYear);
|
|
}
|
|
} catch (\Throwable $e) {
|
|
log_message('error', 'semester score resync failed after exam update: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
$statusMessage = ucfirst($table) . ' scores updated successfully.';
|
|
if ($redirectUrl !== null) {
|
|
return redirect()->to($redirectUrl)->with('status', $statusMessage);
|
|
}
|
|
return redirect()->back()->with('status', $statusMessage);
|
|
}
|
|
|
|
|
|
private function fetchAndOrganizeScores($db, &$organizedScores, $tableName, $parentId, $selectedYear, $selectedSemester)
|
|
{
|
|
// Only supports semester_scores table in this context
|
|
if ($tableName !== 'semester_scores') {
|
|
return;
|
|
}
|
|
|
|
// Define the columns to select
|
|
$columns = [
|
|
'students.id as student_id',
|
|
'students.school_id',
|
|
'students.firstname as student_firstname',
|
|
'students.lastname as student_lastname',
|
|
'classSection.class_section_name',
|
|
"$tableName.homework_avg",
|
|
"$tableName.quiz_avg",
|
|
"$tableName.project_avg",
|
|
"$tableName.midterm_exam_score",
|
|
"$tableName.final_exam_score",
|
|
"$tableName.attendance_score",
|
|
"$tableName.test_avg",
|
|
"$tableName.semester_score",
|
|
"$tableName.school_year",
|
|
"$tableName.semester",
|
|
"$tableName.created_at",
|
|
"$tableName.updated_at"
|
|
// "$tableName.comment"
|
|
];
|
|
|
|
|
|
// Build the query
|
|
$builder = $db->table($tableName);
|
|
$builder->select(implode(', ', $columns));
|
|
$builder->join('students', 'students.id = ' . $tableName . '.student_id');
|
|
// Join on the correct foreign key: classSection.class_section_id
|
|
$builder->join('classSection', 'classSection.class_section_id = ' . $tableName . '.class_section_id');
|
|
$builder->where('students.parent_id', $parentId)
|
|
->where("$tableName.school_year", $selectedYear)
|
|
->where("$tableName.semester", $selectedSemester);
|
|
|
|
$scores = $builder->get()->getResultArray();
|
|
|
|
foreach ($scores as $score) {
|
|
if (!isset($score['school_id'])) {
|
|
log_message('error', "Missing school_id for student ID: " . ($score['student_id'] ?? 'unknown'));
|
|
continue;
|
|
}
|
|
|
|
$schoolYear = $score['school_year'];
|
|
$semester = $score['semester'];
|
|
$studentSchoolId = $score['school_id'];
|
|
|
|
if (!isset($organizedScores[$schoolYear][$semester][$studentSchoolId])) {
|
|
$organizedScores[$schoolYear][$semester][$studentSchoolId] = [
|
|
'student_firstname' => $score['student_firstname'],
|
|
'student_lastname' => $score['student_lastname'],
|
|
'class_section_name' => $score['class_section_name'],
|
|
'school_id' => $studentSchoolId, // ✅ Add this line
|
|
'scores' => [],
|
|
// 'comment' => $score['comment'] ?? ''
|
|
];
|
|
}
|
|
|
|
|
|
// Organize each category
|
|
$organizedScores[$schoolYear][$semester][$studentSchoolId]['scores'] = [
|
|
'homework' => ['score' => $score['homework_avg']],
|
|
'quiz' => ['score' => $score['quiz_avg']],
|
|
'project' => ['score' => $score['project_avg']],
|
|
'midterm_exam' => ['score' => $score['midterm_exam_score']],
|
|
'final_exam' => ['score' => $score['final_exam_score']],
|
|
'attendance' => ['score' => $score['attendance_score']],
|
|
'test_avg' => ['score' => $score['test_avg']],
|
|
'semester' => ['score' => $score['semester_score']],
|
|
];
|
|
|
|
// Optionally include comment
|
|
$organizedScores[$schoolYear][$semester][$studentSchoolId]['comment'] = $score['comment'] ?? '';
|
|
}
|
|
}
|
|
|
|
|
|
public function viewStudentScore()
|
|
{
|
|
$parentId = session()->get('user_id');
|
|
$userType = $_SESSION['user_type'];
|
|
$firstParentId = null;
|
|
$releaseFall = $this->getParentScoresReleasedForSemester('Fall');
|
|
$releaseSpring = $this->getParentScoresReleasedForSemester('Spring');
|
|
$releaseAny = $releaseFall || $releaseSpring;
|
|
|
|
// Identify the firstparent based on user type
|
|
if ($userType === 'primary') {
|
|
$firstParentId = $parentId;
|
|
} elseif ($userType === 'secondary') {
|
|
$parentData = $this->db->table('parents')
|
|
->select('parent_id')
|
|
->where('secondparent_user_id', $parentId)
|
|
->get()
|
|
->getRowArray();
|
|
|
|
if ($parentData) {
|
|
$firstParentId = $parentData['parent_id'];
|
|
}
|
|
} elseif ($userType === 'tertiary') {
|
|
$authUserData = $this->db->table('authorized_users')
|
|
->select('user_id as parent_id')
|
|
->where('authorized_user_id', $parentId)
|
|
->get()
|
|
->getRowArray();
|
|
|
|
if ($authUserData) {
|
|
$firstParentId = $authUserData['parent_id'];
|
|
}
|
|
}
|
|
|
|
if (!$firstParentId) {
|
|
return redirect()->back()->with('error', 'Unable to retrieve student data. Please contact support.');
|
|
}
|
|
|
|
// Handle selected school year (parents can view scores for any available year)
|
|
$selectedYear = $this->request->getVar('school_year') ?? $this->schoolYear;
|
|
|
|
// Fetch distinct school years only from semester_scores
|
|
$query = $this->db->table('semester_scores')
|
|
->select('school_year')
|
|
->distinct()
|
|
->get();
|
|
|
|
$schoolYears = array_column($query->getResultArray(), 'school_year');
|
|
$schoolYears = array_unique($schoolYears);
|
|
rsort($schoolYears);
|
|
|
|
// Initialize scores array
|
|
$scores = [];
|
|
|
|
// First get all students with their class section info for this parent
|
|
$students = $this->db->table('students')
|
|
->select('students.id, students.school_id, students.firstname, students.lastname, classSection.class_section_name AS class_section_name')
|
|
->join('student_class', 'student_class.student_id = students.id', 'left')
|
|
// Join using class_section_id FK stored in student_class
|
|
->join('classSection', 'classSection.class_section_id = student_class.class_section_id', 'left')
|
|
->where('students.parent_id', $firstParentId)
|
|
->get()
|
|
->getResultArray();
|
|
|
|
if (!empty($students)) {
|
|
$studentIdList = array_column($students, 'id');
|
|
|
|
// Fetch data from semester_scores for these students
|
|
$semesterScores = $this->db->table('semester_scores')
|
|
->whereIn('student_id', $studentIdList)
|
|
->where('school_year', $selectedYear)
|
|
->get()
|
|
->getResultArray();
|
|
|
|
$scoresByStudent = [];
|
|
foreach ($semesterScores as $score) {
|
|
$studentIdKey = (int)($score['student_id'] ?? 0);
|
|
$semesterName = trim((string)($score['semester'] ?? ''));
|
|
if ($studentIdKey === 0 || $semesterName === '') {
|
|
continue;
|
|
}
|
|
$normalizedSemester = ucfirst(strtolower($semesterName));
|
|
$scoresByStudent[$studentIdKey][$normalizedSemester] = $score;
|
|
}
|
|
|
|
foreach ($students as $student) {
|
|
$studentId = (int)$student['id'];
|
|
if (empty($scoresByStudent[$studentId])) {
|
|
continue;
|
|
}
|
|
foreach ($scoresByStudent[$studentId] as $semester => $score) {
|
|
$releaseScores = (strcasecmp($semester, 'Fall') === 0) ? $releaseFall
|
|
: ((strcasecmp($semester, 'Spring') === 0) ? $releaseSpring : $releaseAny);
|
|
$scores[$selectedYear][$semester][$studentId] = [
|
|
'school_id' => $student['school_id'],
|
|
'student_firstname' => $student['firstname'],
|
|
'student_lastname' => $student['lastname'],
|
|
'class_section_name' => $student['class_section_name'] ?? 'Unknown Class',
|
|
'comment' => $score['comment'] ?? null,
|
|
'scores' => [
|
|
'homework' => ['score' => $score['homework_avg'] ?? '-'],
|
|
'quiz' => ['score' => $score['quiz_avg'] ?? '-'],
|
|
'project' => ['score' => $score['project_avg'] ?? '-'],
|
|
'attendance' => ['score' => $score['attendance_score'] ?? '-'],
|
|
'participation_score' => ['score' => $score['participation_score'] ?? '-'],
|
|
'ptap' => ['score' => $releaseScores ? ($score['ptap_score'] ?? '-') : '-'],
|
|
'midterm_exam' => ['score' => $releaseScores ? ($score['midterm_exam_score'] ?? '-') : '-'],
|
|
'final_exam' => ['score' => $releaseScores ? ($score['final_exam_score'] ?? '-') : '-'],
|
|
'semester' => ['score' => $releaseScores ? ($score['semester_score'] ?? '-') : '-']
|
|
]
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
return view('/parent/scores', [
|
|
'organizedScores' => $scores,
|
|
'schoolYears' => $schoolYears,
|
|
'selectedYear' => $selectedYear,
|
|
'semesterOrder' => ['Fall', 'Spring'],
|
|
'showExamScores' => $releaseAny,
|
|
'showExamScoresBySemester' => [
|
|
'Fall' => $releaseFall,
|
|
'Spring' => $releaseSpring,
|
|
],
|
|
]);
|
|
}
|
|
|
|
private 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);
|
|
}
|
|
|
|
private function getParentReleaseKey(string $semester): ?string
|
|
{
|
|
$s = strtolower(trim($semester));
|
|
if ($s === 'fall') {
|
|
return 'parent_scores_released_fall';
|
|
}
|
|
if ($s === 'spring') {
|
|
return 'parent_scores_released_spring';
|
|
}
|
|
return null;
|
|
}
|
|
|
|
|
|
|
|
}
|