578 lines
22 KiB
PHP
Executable File
578 lines
22 KiB
PHP
Executable File
<?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\UserModel;
|
|
use App\Models\ConfigurationModel;
|
|
use App\Controllers\View\GradingController;
|
|
use RuntimeException;
|
|
use App\Services\SemesterScoreService;
|
|
use Config\Services;
|
|
use App\Models\GradingLockModel;
|
|
use App\Models\MissingScoreOverrideModel;
|
|
|
|
class QuizController extends Controller
|
|
{
|
|
protected $db;
|
|
protected $semesterScoreService;
|
|
protected $quizModel;
|
|
protected $configModel;
|
|
protected $homeworkModel;
|
|
protected $userModel;
|
|
protected $schoolYear;
|
|
protected $semester;
|
|
protected $studentClassModel;
|
|
protected $studentModel;
|
|
protected $teacherClassModel;
|
|
protected $gradingLockModel;
|
|
protected $missingScoreOverrideModel;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->db = \Config\Database::connect();
|
|
$this->teacherClassModel = new TeacherClassModel();
|
|
$this->homeworkModel = new HomeworkModel();
|
|
$this->userModel = new UserModel();
|
|
$this->studentClassModel = new StudentClassModel();
|
|
$this->studentModel = new StudentModel();
|
|
$this->configModel = new ConfigurationModel();
|
|
$this->quizModel = new QuizModel();
|
|
$this->semesterScoreService = service('semesterScoreService');
|
|
$this->schoolYear = $this->configModel->getConfig('school_year');
|
|
$this->semester = $this->configModel->getConfig('semester');
|
|
$this->gradingLockModel = new GradingLockModel();
|
|
$this->missingScoreOverrideModel = new MissingScoreOverrideModel();
|
|
}
|
|
|
|
public function updateQuizScores(array $scores = null, int $updatedBy = null, int $classSectionId = null)
|
|
{
|
|
$scores = $this->request->getPost('scores');
|
|
log_message('error', '✅ Raw Scores: ' . print_r($scores, true));
|
|
|
|
if ($updatedBy === null) {
|
|
$updatedBy = session()->get('user_id');
|
|
}
|
|
$classSectionId = $this->request->getPost('class_section_id') ?? session()->get('class_section_id');
|
|
$semester = $this->request->getPost('semester') ?? $this->getTeacherSelectedSemester();
|
|
$schoolYear = $this->request->getPost('school_year') ?? $this->schoolYear;
|
|
$classSectionId = (int) ($classSectionId ?? 0);
|
|
if ($classSectionId > 0 && $this->isScoresLocked($classSectionId, $semester, $schoolYear)) {
|
|
return redirect()->back()->with('error', 'Scores are locked for this class. Unlock to edit.');
|
|
}
|
|
|
|
$missingOk = $this->request->getPost('missing_ok') ?? [];
|
|
$studentIds = is_array($scores) ? array_keys($scores) : array_keys((array) $missingOk);
|
|
$indexSet = [];
|
|
if (is_array($scores)) {
|
|
foreach ($scores as $studentId => $quizData) {
|
|
if (!is_array($quizData)) {
|
|
continue;
|
|
}
|
|
foreach ($quizData as $index => $score) {
|
|
if (is_numeric($index)) {
|
|
$indexSet[(int)$index] = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
$checkedItems = [];
|
|
if (is_array($missingOk)) {
|
|
foreach ($missingOk as $studentId => $indexes) {
|
|
if (!is_array($indexes)) {
|
|
continue;
|
|
}
|
|
foreach ($indexes as $index => $flag) {
|
|
if (!is_numeric($index)) {
|
|
continue;
|
|
}
|
|
$checkedItems[] = [
|
|
'student_id' => (int) $studentId,
|
|
'item_index' => (int) $index,
|
|
];
|
|
}
|
|
}
|
|
}
|
|
$this->missingScoreOverrideModel->replaceOverrides(
|
|
$classSectionId,
|
|
(string) $semester,
|
|
(string) $schoolYear,
|
|
'quiz',
|
|
$studentIds,
|
|
array_keys($indexSet),
|
|
$checkedItems,
|
|
$updatedBy
|
|
);
|
|
|
|
if (is_array($scores)) {
|
|
foreach ($scores as $studentId => $quizData) {
|
|
if (!is_numeric($studentId) || $studentId <= 0) continue;
|
|
|
|
// Normalize: if single input submitted
|
|
if (!is_array($quizData)) {
|
|
$quizData = [1 => $quizData];
|
|
}
|
|
|
|
foreach ($quizData as $quizNumber => $score) {
|
|
if (!is_numeric($quizNumber)) continue;
|
|
|
|
$isBlank = $score === null || (is_string($score) && trim($score) === '');
|
|
$normalizedScore = (!$isBlank && is_numeric($score)) ? (float) $score : null;
|
|
|
|
$existing = $this->quizModel->where([
|
|
'student_id' => $studentId,
|
|
'quiz_index' => $quizNumber,
|
|
'class_section_id' => $classSectionId,
|
|
'semester' => $semester,
|
|
'school_year' => $schoolYear,
|
|
])->first();
|
|
|
|
if ($isBlank) {
|
|
if ($existing) {
|
|
$this->quizModel->update($existing['id'], [
|
|
'score' => null,
|
|
'updated_at' => utc_now(),
|
|
]);
|
|
} else {
|
|
$this->quizModel->insert([
|
|
'student_id' => $studentId,
|
|
'school_id' => '', // Optional
|
|
'class_section_id' => $classSectionId,
|
|
'updated_by' => $updatedBy,
|
|
'quiz_index' => $quizNumber,
|
|
'score' => null,
|
|
'semester' => $semester,
|
|
'school_year' => $schoolYear,
|
|
'created_at' => utc_now(),
|
|
'updated_at' => utc_now()
|
|
]);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if ($existing) {
|
|
$this->quizModel->update($existing['id'], [
|
|
'score' => $normalizedScore,
|
|
'updated_at' => utc_now()
|
|
]);
|
|
log_message('debug', "✅ Updated quiz ID {$existing['id']} with score {$normalizedScore}");
|
|
} else {
|
|
$this->quizModel->insert([
|
|
'student_id' => $studentId,
|
|
'school_id' => '', // Optional
|
|
'class_section_id' => $classSectionId,
|
|
'updated_by' => $updatedBy,
|
|
'quiz_index' => $quizNumber,
|
|
'score' => $normalizedScore,
|
|
'semester' => $semester,
|
|
'school_year' => $schoolYear,
|
|
'created_at' => utc_now(),
|
|
'updated_at' => utc_now()
|
|
]);
|
|
log_message('debug', "✅ Inserted quiz for student $studentId quiz $quizNumber with score $score");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$studentTeacherInfo = $this->studentModel->getStudentInfoByClassSectionId($classSectionId, $semester, $schoolYear);
|
|
// Call the updateScoresForStudents method
|
|
try {
|
|
|
|
$this->semesterScoreService->updateScoresForStudents($studentTeacherInfo, $semester, $schoolYear);
|
|
} catch (RuntimeException $e) {
|
|
// Handle error
|
|
}
|
|
return redirect()->to(base_url('/teacher/addQuiz'))->with('status', 'Quiz scores updated successfully.');
|
|
}
|
|
|
|
public function addQuiz()
|
|
{
|
|
// 1) class_section_id from view (POST/GET) with session fallback
|
|
$incoming = $this->request->getPost('class_section_id')
|
|
?? $this->request->getGet('class_section_id')
|
|
?? session()->get('class_section_id');
|
|
|
|
$classSectionId = (int) ($incoming ?: 0);
|
|
if ($classSectionId <= 0) {
|
|
return redirect()->back()->with('status', 'Missing class section.');
|
|
}
|
|
session()->set('class_section_id', $classSectionId);
|
|
|
|
// 3) Headers: distinct quiz_index for this class/term
|
|
$semester = $this->getTeacherSelectedSemester();
|
|
$schoolYear = $this->schoolYear;
|
|
$quizHeaderRows = $this->quizModel->select('quiz_index')
|
|
->where('class_section_id', $classSectionId)
|
|
->where('school_year', $schoolYear)
|
|
->where('semester', $semester)
|
|
->groupBy('quiz_index')
|
|
->orderBy('quiz_index', 'ASC')
|
|
->get()->getResultArray();
|
|
|
|
$quizHeaders = array_map(static fn($r) => (int)$r['quiz_index'], $quizHeaderRows); // [1,2,3,...]
|
|
|
|
// 4) Roster (distinct students for this section/term)
|
|
$roster = $this->db->table('student_class sc')
|
|
->select('s.id AS student_id, s.firstname, s.lastname, s.school_id')
|
|
->distinct() // ✅ apply DISTINCT correctly
|
|
->join('students s', 's.id = sc.student_id', 'inner')
|
|
->where('sc.class_section_id', $classSectionId)
|
|
->where('sc.school_year', $schoolYear)
|
|
->orderBy('s.lastname', 'ASC')
|
|
->orderBy('s.firstname', 'ASC')
|
|
->get()->getResultArray();
|
|
|
|
|
|
// 5) All existing quiz scores (single query) -> pivot
|
|
$scoreRows = $this->quizModel->select('id, student_id, quiz_index, score')
|
|
->where('class_section_id', $classSectionId)
|
|
->where('school_year', $schoolYear)
|
|
->where('semester', $semester)
|
|
->get()->getResultArray();
|
|
|
|
$scoresMap = []; // [student_id][quiz_index] => score
|
|
$idMap = []; // [student_id][quiz_index] => id (if you need to update existing rows)
|
|
|
|
foreach ($scoreRows as $r) {
|
|
$sid = (int)$r['student_id'];
|
|
$qi = (int)$r['quiz_index'];
|
|
$scoresMap[$sid][$qi] = $r['score'];
|
|
$idMap[$sid][$qi] = (int)$r['id'];
|
|
}
|
|
|
|
// 6) Build students array with per-quiz scores
|
|
$students = [];
|
|
foreach ($roster as $st) {
|
|
$sid = (int)$st['student_id'];
|
|
|
|
$quizScores = [];
|
|
foreach ($quizHeaders as $qi) {
|
|
$quizScores[$qi] = $scoresMap[$sid][$qi] ?? ''; // empty if none yet
|
|
}
|
|
|
|
$students[] = [
|
|
'student_id' => $sid,
|
|
'firstname' => $st['firstname'],
|
|
'lastname' => $st['lastname'],
|
|
'school_id' => $st['school_id'] ?? null,
|
|
'scores' => $quizScores,
|
|
// If your view needs existing row ids to decide update vs insert:
|
|
'existingIds' => $idMap[$sid] ?? [], // optional
|
|
];
|
|
}
|
|
|
|
// 7) Render
|
|
return view('teacher/add_quiz', [
|
|
'students' => $students,
|
|
'quizHeaders' => $quizHeaders, // e.g., [1,2,3]
|
|
'semester' => $semester,
|
|
'schoolYear' => $this->schoolYear,
|
|
'classSectionId' => $classSectionId,
|
|
'missingOkMap' => $this->missingScoreOverrideModel->getOverridesMap($classSectionId, $semester, $this->schoolYear, 'quiz'),
|
|
]);
|
|
}
|
|
|
|
|
|
public function addNextQuizColumn()
|
|
{
|
|
$updatedBy = session()->get('user_id');
|
|
$classSectionId = session()->get('class_section_id');
|
|
|
|
if (!$updatedBy || !$classSectionId) {
|
|
return $this->response->setJSON([
|
|
'status' => 'error',
|
|
'message' => 'Missing teacher or class section data.'
|
|
]);
|
|
}
|
|
|
|
try {
|
|
|
|
$semester = $this->getTeacherSelectedSemester();
|
|
$schoolYear = $this->schoolYear;
|
|
$existingQuizNumbers = $this->quizModel
|
|
->select('quiz_index')
|
|
->where('class_section_id', $classSectionId)
|
|
->where('semester', $semester)
|
|
->where('school_year', $schoolYear)
|
|
->groupBy('quiz_index')
|
|
->orderBy('quiz_index', 'DESC')
|
|
->findAll();
|
|
|
|
$maxQuizNumber = 0;
|
|
foreach ($existingQuizNumbers as $row) {
|
|
if (is_numeric($row['quiz_index'])) {
|
|
$maxQuizNumber = max($maxQuizNumber, (int)$row['quiz_index']);
|
|
}
|
|
}
|
|
|
|
$nextQuizNumber = $maxQuizNumber + 1;
|
|
|
|
if ($nextQuizNumber <= 0) {
|
|
return $this->response->setJSON([
|
|
'status' => 'error',
|
|
'message' => '🚫 Invalid quiz number',
|
|
'calculated' => $nextQuizNumber
|
|
]);
|
|
}
|
|
|
|
$students = $this->studentClassModel
|
|
->where('class_section_id', $classSectionId)
|
|
->findAll();
|
|
|
|
if (empty($students)) {
|
|
return $this->response->setJSON([
|
|
'status' => 'error',
|
|
'message' => 'No students found in this class section.'
|
|
]);
|
|
}
|
|
|
|
$firstInsertedId = null;
|
|
|
|
foreach ($students as $i => $student) {
|
|
$exists = $this->quizModel
|
|
->where([
|
|
'student_id' => $student['student_id'],
|
|
'quiz_index' => $nextQuizNumber,
|
|
'class_section_id' => $classSectionId,
|
|
'updated_by' => $updatedBy,
|
|
'semester' => $this->semester,
|
|
'school_year' => $this->schoolYear,
|
|
])
|
|
->first();
|
|
|
|
if ($exists) continue;
|
|
|
|
$insertData = [
|
|
'student_id' => $student['student_id'],
|
|
'class_section_id' => $classSectionId,
|
|
'updated_by' => $updatedBy,
|
|
'quiz_index' => $nextQuizNumber,
|
|
'score' => null,
|
|
'semester' => $semester,
|
|
'school_year' => $schoolYear,
|
|
'created_at' => utc_now(),
|
|
'updated_at' => utc_now(),
|
|
];
|
|
|
|
log_message('error', '✅ Inserting quiz row: ' . json_encode($insertData));
|
|
|
|
try {
|
|
$id = $this->quizModel->insert($insertData, true);
|
|
if ($i === 0) $firstInsertedId = $id;
|
|
} catch (\Throwable $e) {
|
|
return $this->response->setJSON([
|
|
'status' => 'error',
|
|
'message' => '❌ Insert failed: ' . $e->getMessage(),
|
|
'debug' => $insertData
|
|
]);
|
|
}
|
|
}
|
|
|
|
return $this->response->setJSON([
|
|
'status' => 'success',
|
|
'quiz_index' => $nextQuizNumber,
|
|
'new_quiz_id' => $firstInsertedId
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
return $this->response->setJSON([
|
|
'status' => 'error',
|
|
'message' => '❌ Unexpected error: ' . $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
|
|
public function showQuizMngt()
|
|
{
|
|
// Accept POST first, then GET (query string), then session fallback
|
|
$incomingId = $this->request->getPost('class_section_id');
|
|
if (!$incomingId) {
|
|
$incomingId = $this->request->getGet('class_section_id');
|
|
}
|
|
|
|
$classSectionId = (int) ($incomingId ?: (session()->get('class_section_id') ?? 0));
|
|
|
|
if ($classSectionId <= 0) {
|
|
return redirect()->back()->with('status', 'No class section found for the current teacher or selection.');
|
|
}
|
|
session()->set('class_section_id', $classSectionId);
|
|
$semester = $this->getTeacherSelectedSemester();
|
|
$schoolYear = $this->schoolYear;
|
|
$quizScores = $this->getquizScoresByStudent($classSectionId, $semester, $schoolYear);
|
|
$students = $this->getStudentsByClassSectionAndYear($classSectionId, $schoolYear);
|
|
$quizHeaders = $this->getquizHeaders($classSectionId, $semester, $schoolYear);
|
|
$scoresLocked = $this->isScoresLocked($classSectionId, $semester, $schoolYear);
|
|
|
|
return view('grading/quiz', [
|
|
'quizScores' => $quizScores,
|
|
'students' => $students,
|
|
'semester' => $semester,
|
|
'schoolYear' => $schoolYear,
|
|
'quizHeaders' => $quizHeaders,
|
|
'classSectionId' => $classSectionId,
|
|
'scoresLocked' => $scoresLocked,
|
|
]);
|
|
}
|
|
|
|
public function updateQuiz()
|
|
{
|
|
$scores = $this->request->getPost('scores');
|
|
$studentIds = $this->request->getPost('student_ids');
|
|
$semester = $this->request->getPost('semester') ?? $this->getTeacherSelectedSemester();
|
|
$schoolYear = $this->request->getPost('school_year') ?? $this->schoolYear;
|
|
|
|
$updatedBy = $this->request->getPost('teacher_id') ?? session()->get('user_id');
|
|
$classSectionId = $this->request->getPost('class_section_id') ?? session()->get('class_section_id'); // or query it
|
|
$classSectionId = (int) ($classSectionId ?? 0);
|
|
if ($classSectionId > 0 && $this->isScoresLocked($classSectionId, $semester, $schoolYear)) {
|
|
return redirect()->back()->with('error', 'Scores are locked for this class. Unlock to edit.');
|
|
}
|
|
|
|
if ($scores && $studentIds && $semester && $schoolYear && $classSectionId) {
|
|
$this->updateQuizScores($scores, $updatedBy, $classSectionId);
|
|
session()->setFlashdata('status', 'quiz scores updated successfully.');
|
|
|
|
// ⬇️ Directly call the method and return its view
|
|
return $this->showQuizMngt();
|
|
}
|
|
|
|
$studentTeacherInfo = $this->studentModel->getStudentInfoByClassSectionId($classSectionId, $semester, $schoolYear);
|
|
// Call the updateScoresForStudents method
|
|
try {
|
|
|
|
$this->semesterScoreService->updateScoresForStudents($studentTeacherInfo, $semester, $schoolYear);
|
|
} catch (RuntimeException $e) {
|
|
// Handle error
|
|
}
|
|
return redirect()->back()->with('status', 'Failed to update scores.');
|
|
}
|
|
|
|
private function isScoresLocked(int $classSectionId, string $semester, string $schoolYear): bool
|
|
{
|
|
return $this->gradingLockModel->isLocked($classSectionId, $semester, $schoolYear);
|
|
}
|
|
|
|
private function getStudentsByClassSectionAndYear($classSectionId, $schoolYear)
|
|
{
|
|
// Step 1: Get student IDs from student_class table
|
|
$studentClassRows = $this->studentClassModel
|
|
->select('student_id')
|
|
->where('class_section_id', $classSectionId)
|
|
->where('school_year', $schoolYear)
|
|
->findAll();
|
|
|
|
$studentIds = array_column($studentClassRows, 'student_id');
|
|
|
|
if (empty($studentIds)) {
|
|
return []; // No students found
|
|
}
|
|
|
|
// Step 2: Get student data from students table
|
|
$students = $this->studentModel
|
|
->whereIn('id', $studentIds)
|
|
->where('is_active', 1)
|
|
->orderBy('lastname', 'ASC')
|
|
->findAll();
|
|
|
|
return $students;
|
|
}
|
|
|
|
private function getquizHeaders($classSectionId, $semester, $schoolYear)
|
|
{
|
|
$rows = $this->quizModel
|
|
->select('quiz_index')
|
|
->where('class_section_id', $classSectionId)
|
|
->where('semester', $semester)
|
|
->where('school_year', $schoolYear)
|
|
->orderBy('quiz_index', 'ASC')
|
|
->findAll();
|
|
|
|
$headers = [];
|
|
|
|
foreach ($rows as $row) {
|
|
$index = $row['quiz_index'];
|
|
if (!is_array($index)) {
|
|
$headers[$index] = true;
|
|
}
|
|
}
|
|
|
|
ksort($headers);
|
|
return array_keys($headers);
|
|
}
|
|
|
|
private function getClassSectionIdForTeacher($updatedBy)
|
|
{
|
|
$teacherClassModel = new TeacherClassModel();
|
|
$class = $teacherClassModel->where('teacher_id', $updatedBy)->first();
|
|
return $class['class_section_id'] ?? null;
|
|
}
|
|
|
|
|
|
private function getquizScoresByStudent($classSectionId, $semester, $schoolYear)
|
|
{
|
|
$rows = $this->quizModel
|
|
->select('student_id, quiz_index, score')
|
|
->where('class_section_id', $classSectionId)
|
|
->where('semester', $semester)
|
|
->where('school_year', $schoolYear)
|
|
->findAll();
|
|
|
|
$studentScores = [];
|
|
foreach ($rows as $row) {
|
|
$studentId = $row['student_id'];
|
|
$index = $row['quiz_index'];
|
|
$score = $row['score'];
|
|
|
|
$studentScores[$studentId]['scores'][$index] = $score;
|
|
}
|
|
|
|
return $studentScores;
|
|
}
|
|
|
|
private function getStudentsWithquizScores($classSectionId, $quizHeaders)
|
|
{
|
|
$studentClasses = $this->studentClassModel
|
|
->active()
|
|
->where('student_class.class_section_id', $classSectionId)
|
|
->findAll();
|
|
$students = [];
|
|
|
|
foreach ($studentClasses as $sc) {
|
|
$student = $this->studentModel
|
|
->where('id', $sc['student_id'])
|
|
->where('is_active', 1)
|
|
->first();
|
|
if (!$student) continue;
|
|
|
|
$scores = [];
|
|
foreach ($quizHeaders as $index => $ids) {
|
|
$entry = $this->quizModel->where('quiz_index', $ids)
|
|
->where('student_id', $sc['student_id'])
|
|
->first();
|
|
$scores[$index] = $entry['score'] ?? '';
|
|
}
|
|
|
|
$students[] = [
|
|
'student_id' => $sc['student_id'],
|
|
'firstname' => $student['firstname'],
|
|
'lastname' => $student['lastname'],
|
|
'scores' => $scores
|
|
];
|
|
}
|
|
|
|
usort($students, fn($a, $b) => strcmp($a['lastname'], $b['lastname']) ?: strcmp($a['firstname'], $b['firstname']));
|
|
return $students;
|
|
}
|
|
|
|
private function getTeacherSelectedSemester(): string
|
|
{
|
|
helper('semester_selection_helper');
|
|
return selected_teacher_semester();
|
|
}
|
|
}
|