483 lines
18 KiB
PHP
Executable File
483 lines
18 KiB
PHP
Executable File
<?php
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
use App\Models\AttendanceRecord;
|
|
use App\Models\ClassSection;
|
|
use App\Models\Configuration;
|
|
use App\Models\FinalExam;
|
|
use App\Models\Homework;
|
|
use App\Models\MidtermExam;
|
|
use App\Models\Project;
|
|
use App\Models\Quiz;
|
|
use App\Models\ScoreComment;
|
|
use App\Models\SemesterScore;
|
|
use App\Models\StudentClass;
|
|
use App\Models\Student;
|
|
use App\Models\TeacherClass;
|
|
use App\Models\User;
|
|
use Illuminate\Support\Facades\DB;
|
|
use RuntimeException;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
|
|
class GradingController extends BaseApiController
|
|
{
|
|
protected Configuration $config;
|
|
protected Homework $homework;
|
|
protected User $user;
|
|
protected StudentClass $studentClass;
|
|
protected Student $student;
|
|
protected TeacherClass $teacherClass;
|
|
protected ClassSection $classSection;
|
|
protected string $schoolYear;
|
|
protected string $semester;
|
|
protected $semesterScoreService;
|
|
|
|
public function __construct()
|
|
{
|
|
parent::__construct();
|
|
$this->config = model(Configuration::class);
|
|
$this->homework = model(Homework::class);
|
|
$this->user = model(User::class);
|
|
$this->studentClass = model(StudentClass::class);
|
|
$this->student = model(Student::class);
|
|
$this->teacherClass = model(TeacherClass::class);
|
|
$this->classSection = model(ClassSection::class);
|
|
$this->schoolYear = (string) ($this->config->getConfig('school_year') ?? '');
|
|
$this->semester = (string) ($this->config->getConfig('semester') ?? '');
|
|
$this->semesterScoreService = \Config\Services::semesterScoreService();
|
|
}
|
|
|
|
/**
|
|
* Show scores for a specific type, class section, and student
|
|
*/
|
|
public function show($type, $classSectionId = null, $studentId = null)
|
|
{
|
|
$model = $this->getModelByType($type);
|
|
if (!$model) {
|
|
return $this->respondError('Invalid type', Response::HTTP_BAD_REQUEST);
|
|
}
|
|
|
|
$student = model(Student::class);
|
|
$student = null;
|
|
if ($studentId) {
|
|
$student = $student->find($studentId);
|
|
if (!$student) {
|
|
return $this->respondError('Student not found', Response::HTTP_NOT_FOUND);
|
|
}
|
|
}
|
|
|
|
try {
|
|
$scores = $model->where([
|
|
'student_id' => $studentId,
|
|
'semester' => $this->semester,
|
|
'school_year' => $this->schoolYear
|
|
])->findAll();
|
|
|
|
return $this->success([
|
|
'student' => $student,
|
|
'scores' => $scores,
|
|
'type' => $type,
|
|
'class_section_id' => $classSectionId,
|
|
'semester' => $this->semester,
|
|
'school_year' => $this->schoolYear,
|
|
], 'Scores retrieved successfully');
|
|
} catch (\Throwable $e) {
|
|
log_message('error', 'Grading show error: ' . $e->getMessage());
|
|
return $this->respondError('Failed to retrieve scores', Response::HTTP_INTERNAL_SERVER_ERROR);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update scores for various types
|
|
*/
|
|
public function update()
|
|
{
|
|
$data = $this->payloadData();
|
|
$type = $data['type'] ?? null;
|
|
$studentId = (int) ($data['student_id'] ?? 0);
|
|
$classSectionId = (int) ($data['class_section_id'] ?? 0);
|
|
|
|
if (!$type) {
|
|
return $this->respondError('Type is required', Response::HTTP_BAD_REQUEST);
|
|
}
|
|
|
|
$model = $this->getModelByType($type);
|
|
if (!$model) {
|
|
return $this->respondError('Invalid type', Response::HTTP_BAD_REQUEST);
|
|
}
|
|
|
|
try {
|
|
if (in_array($type, ['homework', 'quiz', 'project'])) {
|
|
$scoreIds = $data['score_ids'] ?? [];
|
|
$scores = $data['scores'] ?? [];
|
|
$comments = $data['comments'] ?? [];
|
|
|
|
foreach ($scoreIds as $i => $id) {
|
|
$updateData = [
|
|
'score' => $scores[$i] ?? null,
|
|
'updated_at' => utc_now()
|
|
];
|
|
if (isset($comments[$i])) {
|
|
$updateData['comment'] = $comments[$i];
|
|
}
|
|
$model->update($id, $updateData);
|
|
}
|
|
} elseif (in_array($type, ['midterm', 'final', 'test'])) {
|
|
$score = $data['score'] ?? null;
|
|
$updateData = [
|
|
'score' => $score,
|
|
'updated_at' => utc_now()
|
|
];
|
|
|
|
$existing = $model->where([
|
|
'student_id' => $studentId,
|
|
'class_section_id' => $classSectionId,
|
|
'semester' => $this->semester,
|
|
'school_year' => $this->schoolYear
|
|
])->first();
|
|
|
|
if ($existing) {
|
|
$model->update($existing['id'], $updateData);
|
|
} else {
|
|
$insertData = array_merge($updateData, [
|
|
'student_id' => $studentId,
|
|
'class_section_id' => $classSectionId,
|
|
'semester' => $this->semester,
|
|
'school_year' => $this->schoolYear,
|
|
'created_at' => utc_now()
|
|
]);
|
|
$model->insert($insertData);
|
|
}
|
|
} elseif ($type === 'comments') {
|
|
$comment = $data['comment'] ?? null;
|
|
|
|
// Remove existing comments for this student/semester/year
|
|
$model->where([
|
|
'student_id' => $studentId,
|
|
'semester' => $this->semester,
|
|
'school_year' => $this->schoolYear
|
|
])->delete();
|
|
|
|
// Insert new comment
|
|
$model->insert([
|
|
'student_id' => $studentId,
|
|
'score_type' => 'general',
|
|
'semester' => $this->semester,
|
|
'school_year' => $this->schoolYear,
|
|
'comment' => $comment,
|
|
'commented_by' => $this->getCurrentUserId(),
|
|
'created_at' => utc_now()
|
|
]);
|
|
}
|
|
|
|
// Update semester scores for students in the class section
|
|
if ($classSectionId > 0) {
|
|
$studentTeacherInfo = $this->student->getStudentInfoByClassSectionId(
|
|
$classSectionId,
|
|
$this->semester,
|
|
$this->schoolYear
|
|
);
|
|
|
|
try {
|
|
foreach ($studentTeacherInfo as $studentInfo) {
|
|
$this->semesterScoreService->updateStudentScores([
|
|
'student_id' => $studentInfo['student_id'],
|
|
'school_id' => $studentInfo['school_id'] ?? null,
|
|
'class_section_id' => $studentInfo['class_section_id'],
|
|
'updated_by' => $studentInfo['updated_by'] ?? $this->getCurrentUserId(),
|
|
], $this->semester, $this->schoolYear);
|
|
}
|
|
} catch (RuntimeException $e) {
|
|
log_message('error', 'Failed to update semester scores: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
return $this->success(null, 'Scores updated successfully');
|
|
} catch (\Throwable $e) {
|
|
log_message('error', 'Grading update error: ' . $e->getMessage());
|
|
return $this->respondError('Failed to update scores: ' . $e->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get grading main view data - all students grouped by class sections
|
|
*/
|
|
public function grading()
|
|
{
|
|
$schoolYear = (string) $this->schoolYear;
|
|
$semester = (string) $this->semester;
|
|
|
|
// Roster + dual-join to semester_scores:
|
|
// ss_b: match when semester_scores.class_section_id = BUSINESS ID (student_class.class_section_id)
|
|
// ss_p: match when semester_scores.class_section_id = PK (classSection.id)
|
|
$rows = DB::table('student_class as sc')
|
|
->select([
|
|
'cs.id as section_pk',
|
|
'cs.class_section_id as section_id',
|
|
'cs.class_id',
|
|
'cs.class_section_name',
|
|
's.id as student_id',
|
|
's.school_id',
|
|
's.firstname',
|
|
's.lastname',
|
|
DB::raw('COALESCE(ss_b.ptap_score, ss_p.ptap_score) as ss_ptap_score'),
|
|
DB::raw('COALESCE(ss_b.semester_score, ss_p.semester_score) as ss_semester_score'),
|
|
'ss_b.class_section_id as matched_biz_csid',
|
|
'ss_p.class_section_id as matched_pk_csid'
|
|
])
|
|
->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'sc.class_section_id')
|
|
->join('students as s', 's.id', '=', 'sc.student_id')
|
|
->leftJoin('semester_scores as ss_b', function($join) use ($semester, $schoolYear) {
|
|
$join->on('ss_b.student_id', '=', 's.id')
|
|
->on('ss_b.class_section_id', '=', 'sc.class_section_id')
|
|
->whereRaw('LOWER(ss_b.semester) = LOWER(?)', [$semester])
|
|
->where('ss_b.school_year', '=', $schoolYear);
|
|
})
|
|
->leftJoin('semester_scores as ss_p', function($join) use ($semester, $schoolYear) {
|
|
$join->on('ss_p.student_id', '=', 's.id')
|
|
->on('ss_p.class_section_id', '=', 'cs.id')
|
|
->whereRaw('LOWER(ss_p.semester) = LOWER(?)', [$semester])
|
|
->where('ss_p.school_year', '=', $schoolYear);
|
|
})
|
|
->where('sc.school_year', $schoolYear)
|
|
->orderBy('cs.class_id', 'ASC')
|
|
->orderBy('cs.class_section_name', 'ASC')
|
|
->orderBy('s.lastname', 'ASC')
|
|
->orderBy('s.firstname', 'ASC')
|
|
->get()
|
|
->toArray();
|
|
|
|
// Build structures keyed by BUSINESS section id
|
|
$grades = []; // class_id => [ ['class_section_id','class_section_name'], ... ]
|
|
$studentsBySection = []; // section_id => [ students... ]
|
|
|
|
foreach ($rows as $r) {
|
|
$sectionId = (int) ($r->section_id ?? 0);
|
|
$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;
|
|
|
|
$ptapScore = $r->ss_ptap_score ?? null;
|
|
$semesterScore = $r->ss_semester_score ?? null;
|
|
|
|
if (!isset($studentsBySection[$sectionId])) {
|
|
$studentsBySection[$sectionId] = [];
|
|
}
|
|
|
|
$studentsBySection[$sectionId][] = [
|
|
'id' => $sid,
|
|
'school_id' => $r->school_id ?? null,
|
|
'firstname' => $r->firstname ?? null,
|
|
'lastname' => $r->lastname ?? null,
|
|
'ptap' => is_null($ptapScore) ? null : (float) $ptapScore,
|
|
'semester_score' => is_null($semesterScore) ? null : (float) $semesterScore,
|
|
];
|
|
}
|
|
|
|
return $this->success([
|
|
'grades' => $grades,
|
|
'students_by_section' => $studentsBySection,
|
|
'semester' => $semester,
|
|
'school_year' => $schoolYear,
|
|
], 'Grading data retrieved successfully');
|
|
}
|
|
|
|
/**
|
|
* Get all scores and comments for students
|
|
*/
|
|
public function getScoreComment()
|
|
{
|
|
// Get all students for the current semester and school year
|
|
$studentClassEntries = $this->studentClass
|
|
->where('semester', $this->semester)
|
|
->where('school_year', $this->schoolYear)
|
|
->findAll();
|
|
|
|
// Group student IDs
|
|
$studentIds = array_map(fn($entry) => $entry['student_id'], $studentClassEntries);
|
|
|
|
if (empty($studentIds)) {
|
|
return $this->success([], 'No students found');
|
|
}
|
|
|
|
// Fetch all scores and comments for the students
|
|
$scoresAndComments = $this->getAllScoresAndComments($studentIds, $this->semester, $this->schoolYear);
|
|
|
|
return $this->success($scoresAndComments, 'Scores and comments retrieved successfully');
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
private function getAllScoresAndComments($studentIds, $semester, $schoolYear)
|
|
{
|
|
// Validate input parameters
|
|
if (empty($studentIds) || !is_array($studentIds)) {
|
|
return [];
|
|
}
|
|
|
|
if (empty($semester) || empty($schoolYear)) {
|
|
throw new \InvalidArgumentException('Semester and school year must be provided');
|
|
}
|
|
|
|
// Initialize models
|
|
$models = [
|
|
'final_exam' => model(FinalExam::class),
|
|
'homework' => model(Homework::class),
|
|
'midterm' => model(MidtermExam::class),
|
|
'project' => model(Project::class),
|
|
'quiz' => model(Quiz::class),
|
|
'comments' => model(ScoreComment::class),
|
|
'semester_scores' => model(SemesterScore::class),
|
|
'student' => model(Student::class),
|
|
'student_class' => model(StudentClass::class),
|
|
'teacher_class' => model(TeacherClass::class),
|
|
'config' => model(Configuration::class),
|
|
'attendance' => model(AttendanceRecord::class)
|
|
];
|
|
|
|
// 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' => $semester,
|
|
'school_year' => $schoolYear
|
|
];
|
|
|
|
// Fetch all student data first
|
|
$students = $models['student']->whereIn('id', $studentIds)
|
|
->where('school_year', $schoolYear)
|
|
->findAll();
|
|
|
|
if (empty($students)) {
|
|
return [];
|
|
}
|
|
|
|
// Initialize result array with student data
|
|
$allScores = [];
|
|
|
|
foreach ($students as $student) {
|
|
$className = $models['student_class']->getClassSectionsByStudentId($student['id'], $schoolYear);
|
|
$updatedBy = $models['teacher_class']->getTeacherIdByClassSection($className, $semester, $schoolYear);
|
|
|
|
$allScores[$student['id']] = [
|
|
'school_id' => $student['school_id'],
|
|
'firstname' => $student['firstname'],
|
|
'lastname' => $student['lastname'],
|
|
'class_name' => $className,
|
|
'comments' => []
|
|
];
|
|
}
|
|
|
|
// Fetch and process attendance data
|
|
foreach ($studentIds as $studentId) {
|
|
if (!isset($allScores[$studentId])) continue;
|
|
|
|
$absences = $models['attendance']->getTotalAbsences($studentId, $semester, $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;
|
|
}
|
|
|
|
/**
|
|
* Get model instance by type
|
|
*/
|
|
private function getModelByType(?string $type)
|
|
{
|
|
return match ($type) {
|
|
'homework' => model(Homework::class),
|
|
'quiz' => model(Quiz::class),
|
|
'project' => model(Project::class),
|
|
'midterm' => model(MidtermExam::class),
|
|
'final' => model(FinalExam::class),
|
|
'test' => model(SemesterScore::class),
|
|
'comments' => model(ScoreComment::class),
|
|
default => null,
|
|
};
|
|
}
|
|
}
|