Files
alrahma_sunday_school_api/app/Http/Controllers/Api/GradingController.php
T
2026-03-05 12:29:37 -05:00

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,
};
}
}