db = db_connect(); $this->poolModel = new QuestionPoolModel(); $this->questionModel = new AssessmentQuestionModel(); $this->formModel = new AssessmentFormModel(); $this->assessmentModel = new StudentAssessmentModel(); $this->studentModel = new StudentModel(); } public function pools() { $pool = $this->primaryPool(); if (! $pool) { throw new PageNotFoundException('The New Student Assessment pool is not installed. Run the database migrations first.'); } $questions = $this->questionModel->where('pool_id', $pool['id'])->orderBy('order_index', 'ASC')->orderBy('id', 'ASC')->findAll(); return view('assessments/pool', ['pool' => $pool, 'questions' => $questions]); } public function pool(int $id) { $pool = $this->primaryPool(); if (! $pool || (int) $pool['id'] !== $id) { return redirect()->to('administrator/assessments/pools'); } $questions = $this->questionModel->where('pool_id', $id)->orderBy('order_index', 'ASC')->orderBy('id', 'ASC')->findAll(); return view('assessments/pool', ['pool' => $pool, 'questions' => $questions]); } public function storeQuestion(int $poolId) { $this->requirePool($poolId); if (! $this->validateQuestion()) { return redirect()->back()->withInput()->with('errors', $this->validator->getErrors()); } $max = $this->db->table('assessment_questions')->selectMax('order_index')->where('pool_id', $poolId)->get()->getRowArray(); $this->questionModel->insert($this->questionPayload($poolId, ((int) ($max['order_index'] ?? 0)) + 1)); return redirect()->to('administrator/assessments/pools')->with('success', 'Question added.'); } public function updateQuestion(int $id) { $question = $this->requireQuestion($id); if (! $this->validateQuestion()) { return redirect()->back()->withInput()->with('errors', $this->validator->getErrors()); } $this->questionModel->update($id, $this->questionPayload((int) $question['pool_id'], (int) $question['order_index'])); return redirect()->to('administrator/assessments/pools')->with('success', 'Question updated.'); } public function deleteQuestion(int $id) { $question = $this->requireQuestion($id); if ($this->db->table('assessment_form_questions')->where('question_id', $id)->countAllResults() > 0) { return redirect()->back()->with('error', 'This question is used by a form and cannot be deleted.'); } $this->questionModel->delete($id); $this->normalizeQuestionOrder((int) $question['pool_id']); return redirect()->to('administrator/assessments/pools')->with('success', 'Question deleted.'); } public function moveQuestion(int $id) { $question = $this->requireQuestion($id); $direction = (string) $this->request->getPost('direction'); $operator = $direction === 'up' ? '<' : '>'; $sort = $direction === 'up' ? 'DESC' : 'ASC'; $other = $this->db->table('assessment_questions') ->where('pool_id', $question['pool_id'])->where('order_index ' . $operator, $question['order_index']) ->orderBy('order_index', $sort)->limit(1)->get()->getRowArray(); if ($other) { $this->db->transStart(); $this->questionModel->update($id, ['order_index' => $other['order_index']]); $this->questionModel->update((int) $other['id'], ['order_index' => $question['order_index']]); $this->db->transComplete(); } return redirect()->to('administrator/assessments/pools'); } public function forms() { $forms = $this->db->table('assessment_forms f') ->select('f.*, p.name AS pool_name, COUNT(DISTINCT fq.question_id) AS question_count, COUNT(DISTINCT sa.id) AS assignment_count') ->join('question_pools p', 'p.id = f.pool_id') ->join('assessment_form_questions fq', 'fq.form_id = f.id', 'left') ->join('student_assessments sa', 'sa.form_id = f.id', 'left') ->groupBy('f.id')->orderBy('f.created_at', 'DESC')->get()->getResultArray(); return view('assessments/forms', ['forms' => $forms]); } public function newForm() { $pool = $this->primaryPool(); $selectedSchoolYear = $this->currentSchoolYearName((string) session()->get('school_year')); return view('assessments/form_builder', [ 'form' => null, 'pools' => $pool ? [$pool] : [], 'questions' => $pool ? $this->questionModel->where('pool_id', $pool['id'])->orderBy('order_index', 'ASC')->findAll() : [], 'selectedIds' => [], 'locked' => false, 'schoolYears' => $this->schoolYearOptions(), 'selectedSchoolYear' => $selectedSchoolYear, ]); } public function storeForm() { return $this->saveForm(null); } public function editForm(int $id) { $form = $this->requireForm($id); $questions = $this->questionModel->where('pool_id', $form['pool_id'])->orderBy('order_index', 'ASC')->findAll(); $selectedRows = $this->db->table('assessment_form_questions')->where('form_id', $id)->orderBy('order_index', 'ASC')->get()->getResultArray(); return view('assessments/form_builder', [ 'form' => $form, 'pools' => $this->poolModel->orderBy('name', 'ASC')->findAll(), 'questions' => $questions, 'selectedIds' => array_map('intval', array_column($selectedRows, 'question_id')), 'locked' => $this->db->table('student_assessments')->where('form_id', $id)->countAllResults() > 0, 'schoolYears' => $this->schoolYearOptions(), 'selectedSchoolYear' => (string) ($form['school_year'] ?? ''), ]); } public function updateForm(int $id) { $this->requireForm($id); return $this->saveForm($id); } public function publishForm(int $id) { $form = $this->requireForm($id); $questionCount = $this->db->table('assessment_form_questions')->where('form_id', $id)->countAllResults(); if ($questionCount === 0) { return redirect()->to('administrator/assessments/forms')->with('error', 'Add at least one question before publishing this form.'); } if (! preg_match('/^\d{4}-\d{4}$/', (string) ($form['school_year'] ?? ''))) { return redirect()->to('administrator/assessments/forms/' . $id . '/edit') ->with('error', 'Select a school year before publishing this form.'); } if ($form['status'] !== 'published') { $this->formModel->update($id, ['status' => 'published']); } return redirect()->to('administrator/assessments/forms')->with('success', 'Assessment form published.'); } public function preview(int $id) { $data = $this->formWithQuestions($id); $data['preview'] = true; return view('assessments/take', $data); } public function newStudentRoster() { $schoolYear = $this->currentSchoolYearName((string) session()->get('school_year')); $data = service('enrollmentWithdrawal')->newStudents($schoolYear); $data['schoolYear'] = $schoolYear; $data['assessmentByStudent'] = $this->assessmentActionsForStudents(array_column($data['new_students'] ?? [], 'id'), $schoolYear); return view('assessments/new_students', $data); } public function studentAssignments(int $studentId) { $student = $this->studentModel->find($studentId); if (! $student) { throw new PageNotFoundException('Student not found.'); } $schoolYear = $this->currentSchoolYearName((string) session()->get('school_year')); $active = $this->activeAssessmentForStudent($studentId, $schoolYear); if ($active) { return view('assessments/admin_take', $this->assessmentDetails((int) $active['id'])); } $forms = $this->db->table('assessment_forms f') ->select('f.*, p.name AS pool_name, COUNT(fq.question_id) AS question_count') ->join('question_pools p', 'p.id = f.pool_id') ->join('assessment_form_questions fq', 'fq.form_id = f.id', 'left') ->where('f.status', 'published')->where('f.school_year', $schoolYear) ->groupBy('f.id')->orderBy('f.name', 'ASC')->get()->getResultArray(); $assignments = $this->db->table('student_assessments sa') ->select('sa.*, f.name AS form_name, f.school_year')->join('assessment_forms f', 'f.id = sa.form_id') ->where('sa.student_id', $studentId)->orderBy('sa.assigned_at', 'DESC')->get()->getResultArray(); return view('assessments/student_assignments', ['student' => $student, 'forms' => $forms, 'assignments' => $assignments]); } public function startInterview(int $studentId) { if (! $this->studentModel->find($studentId)) { throw new PageNotFoundException('Student not found.'); } $schoolYear = $this->currentSchoolYearName((string) session()->get('school_year')); $active = $this->activeAssessmentForStudent($studentId, $schoolYear); if ($active) return redirect()->to("administrator/assessments/students/{$studentId}"); $assignedFormIds = array_map('intval', array_column( $this->assessmentModel->select('form_id')->where('student_id', $studentId)->findAll(), 'form_id' )); $formBuilder = $this->formModel->where('status', 'published')->where('school_year', $schoolYear) ->orderBy('created_at', 'DESC')->orderBy('id', 'DESC'); if ($assignedFormIds !== []) $formBuilder->whereNotIn('id', $assignedFormIds); $form = $formBuilder->first(); if (! $form) { return redirect()->to('administrator/assessments/students/' . $studentId) ->with('error', 'No unassigned published assessment form is available for this student.'); } $this->assessmentModel->insert([ 'form_id' => (int) $form['id'], 'student_id' => $studentId, 'status' => 'not_started', 'assigned_at' => date('Y-m-d H:i:s'), ]); return redirect()->to("administrator/assessments/students/{$studentId}"); } public function assign(int $studentId) { if (! $this->studentModel->find($studentId)) { throw new PageNotFoundException('Student not found.'); } $formId = (int) $this->request->getPost('form_id'); $schoolYear = $this->currentSchoolYearName((string) session()->get('school_year')); $form = $this->formModel->where('id', $formId)->where('status', 'published')->where('school_year', $schoolYear)->first(); if (! $form) { return redirect()->back()->with('error', 'Choose a published assessment form.'); } if ($this->assessmentModel->where(['form_id' => $formId, 'student_id' => $studentId])->first()) { return redirect()->back()->with('error', 'This form is already assigned to this student.'); } try { $inserted = $this->assessmentModel->insert([ 'form_id' => $formId, 'student_id' => $studentId, 'status' => 'not_started', 'assigned_at' => date('Y-m-d H:i:s'), ]); } catch (\Throwable $e) { log_message('warning', 'Assessment assignment rejected: ' . $e->getMessage()); $inserted = false; } if ($inserted === false) { return redirect()->back()->with('error', 'This form is already assigned to this student.'); } return redirect()->to("administrator/assessments/students/{$studentId}")->with('success', 'Assessment assigned.'); } public function grade(int $assessmentId) { $data = $this->assessmentDetails($assessmentId); if (! in_array($data['assessment']['status'], ['completed', 'graded'], true)) { return redirect()->back()->with('error', 'The student must complete the assessment before it can be reviewed.'); } return view('assessments/grade', $data); } public function saveGrade(int $assessmentId) { $data = $this->assessmentDetails($assessmentId); if (! in_array($data['assessment']['status'], ['completed', 'graded'], true)) { return redirect()->back()->with('error', 'The assessment is not ready for review.'); } $this->persistEducationCommitteeNote($assessmentId); $this->db->transStart(); $this->db->table('student_answers')->where('student_assessment_id', $assessmentId)->update([ 'is_correct' => null, 'points_awarded' => null, 'updated_at' => date('Y-m-d H:i:s'), ]); $this->assessmentModel->update($assessmentId, [ 'status' => 'graded', 'score' => null, 'graded_at' => date('Y-m-d H:i:s'), 'graded_by' => (int) session()->get('user_id') ?: null, ]); $this->db->transComplete(); return redirect()->to("administrator/assessments/results/{$assessmentId}")->with('success', 'Assessment review completed.'); } public function results(int $assessmentId) { $data = $this->assessmentDetails($assessmentId); $data['adminView'] = true; return view('assessments/results', $data); } public function saveAdminProgress(int $assessmentId) { $assessment = $this->assessmentModel->find($assessmentId); if (! $assessment) throw new PageNotFoundException('Assessment not found.'); if (! in_array($assessment['status'], ['not_started', 'in_progress'], true)) { return $this->response->setStatusCode(409)->setJSON(['status' => false, 'message' => 'This assessment can no longer be changed.']); } $this->persistEducationCommitteeNote($assessmentId); $this->persistAnswers($assessment, (array) $this->request->getPost('answers')); return $this->response->setJSON([ 'status' => true, 'message' => 'Saved', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function saveAdminNote(int $assessmentId) { if (! $this->assessmentModel->find($assessmentId)) { throw new PageNotFoundException('Assessment not found.'); } $this->persistEducationCommitteeNote($assessmentId); return $this->response->setJSON([ 'status' => true, 'message' => 'Note saved', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function completeAdminInterview(int $assessmentId) { $assessment = $this->assessmentModel->find($assessmentId); if (! $assessment) throw new PageNotFoundException('Assessment not found.'); if (! in_array($assessment['status'], ['not_started', 'in_progress'], true)) { return redirect()->to('administrator/assessments/students/' . $assessment['student_id']) ->with('error', 'This assessment has already been completed.'); } $this->persistEducationCommitteeNote($assessmentId); $this->persistAnswers($assessment, (array) $this->request->getPost('answers')); $this->assessmentModel->update($assessmentId, ['status' => 'completed', 'submitted_at' => date('Y-m-d H:i:s')]); return redirect()->to("administrator/assessments/review/{$assessmentId}") ->with('success', 'Assessment completed. Review the responses below.'); } public function myAssessments() { $studentIds = $this->accessibleStudentIds(); $assignments = []; if ($studentIds) { $assignments = $this->db->table('student_assessments sa') ->select('sa.*, f.name AS form_name, f.school_year, s.firstname, s.lastname') ->join('assessment_forms f', 'f.id = sa.form_id') ->join('students s', 's.id = sa.student_id') ->whereIn('sa.student_id', $studentIds)->orderBy('sa.assigned_at', 'DESC')->get()->getResultArray(); } return view('assessments/my_assessments', ['assignments' => $assignments]); } public function take(int $assessmentId) { $assessment = $this->requireAccessibleAssessment($assessmentId); if ($assessment['status'] === 'graded') { return redirect()->to("student/assessments/{$assessmentId}/results"); } if ($assessment['status'] === 'completed') { return redirect()->to('student/assessments')->with('success', 'This assessment has already been submitted and is awaiting review.'); } $data = $this->formWithQuestions((int) $assessment['form_id']); $answerRows = $this->db->table('student_answers')->where('student_assessment_id', $assessmentId)->get()->getResultArray(); $data['answers'] = array_column($answerRows, 'answer_value', 'question_id'); $data['assessment'] = $assessment; $data['preview'] = false; return view('assessments/take', $data); } public function saveProgress(int $assessmentId) { $assessment = $this->requireAccessibleAssessment($assessmentId); if (! in_array($assessment['status'], ['not_started', 'in_progress'], true)) { return $this->response->setStatusCode(409)->setJSON(['status' => false, 'message' => 'This assessment can no longer be changed.']); } $answers = (array) $this->request->getPost('answers'); $this->persistAnswers($assessment, $answers); if ($this->request->isAJAX()) { return $this->response->setJSON([ 'status' => true, 'message' => 'Progress saved.', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } return redirect()->back()->with('success', 'Progress saved.'); } public function submit(int $assessmentId) { $assessment = $this->requireAccessibleAssessment($assessmentId); if (! in_array($assessment['status'], ['not_started', 'in_progress'], true)) { return redirect()->to('student/assessments')->with('error', 'This assessment was already submitted.'); } $this->persistAnswers($assessment, (array) $this->request->getPost('answers')); $this->assessmentModel->update($assessmentId, ['status' => 'completed', 'submitted_at' => date('Y-m-d H:i:s')]); return redirect()->to('student/assessments')->with('success', 'Assessment submitted. Your responses will appear after review.'); } public function myResults(int $assessmentId) { $assessment = $this->requireAccessibleAssessment($assessmentId); if ($assessment['status'] !== 'graded') { return redirect()->to('student/assessments')->with('error', 'Results are not available yet.'); } $data = $this->assessmentDetails($assessmentId); $data['adminView'] = false; return view('assessments/results', $data); } private function saveForm(?int $id) { $rules = [ 'name' => 'required|string|max_length[150]', 'pool_id' => 'required|is_natural_no_zero', 'school_year' => 'required|regex_match[/^\d{4}-\d{4}$/]', 'status' => 'required|in_list[draft,published,archived]', 'education_committee_note' => 'permit_empty|string|max_length[10000]', ]; if (! $this->validate($rules)) { return redirect()->back()->withInput()->with('errors', $this->validator->getErrors()); } $existingForm = $id !== null ? $this->requireForm($id) : null; $submittedSchoolYear = trim((string) $this->request->getPost('school_year')); if ($this->db->tableExists('school_years') && $this->db->table('school_years')->where('name', $submittedSchoolYear)->countAllResults() === 0) { return redirect()->back()->withInput()->with('error', 'Select a valid school year.'); } $poolId = $existingForm ? (int) $existingForm['pool_id'] : (int) $this->request->getPost('pool_id'); if (! $this->poolModel->find($poolId)) { return redirect()->back()->withInput()->with('error', 'Question pool not found.'); } $locked = $id !== null && $this->db->table('student_assessments')->where('form_id', $id)->countAllResults() > 0; $selected = array_values(array_unique(array_map('intval', (array) $this->request->getPost('question_ids')))); if ($this->request->getPost('use_all')) { $selected = array_map('intval', array_column($this->questionModel->where('pool_id', $poolId)->orderBy('order_index', 'ASC')->findAll(), 'id')); } if (! $locked) { $validRows = empty($selected) ? [] : $this->questionModel->where('pool_id', $poolId)->whereIn('id', $selected)->findAll(); $validIds = array_map('intval', array_column($validRows, 'id')); $selected = array_values(array_filter($selected, static fn ($qid) => in_array($qid, $validIds, true))); usort($selected, function ($a, $b) { return ((int) $this->request->getPost('order_' . $a)) <=> ((int) $this->request->getPost('order_' . $b)); }); if (empty($selected)) { return redirect()->back()->withInput()->with('error', 'Select at least one question.'); } } $payload = [ 'name' => trim((string) $this->request->getPost('name')), 'school_year' => $locked ? (string) $existingForm['school_year'] : $submittedSchoolYear, 'status' => $this->request->getPost('status'), 'education_committee_note' => $this->nullableString($this->request->getPost('education_committee_note')), ]; if ($id === null) $payload['pool_id'] = $poolId; $this->db->transStart(); if ($id === null) { $payload['created_by'] = (int) session()->get('user_id') ?: null; $id = (int) $this->formModel->insert($payload, true); } else { $this->formModel->update($id, $payload); } if (! $locked) { $this->db->table('assessment_form_questions')->where('form_id', $id)->delete(); foreach ($selected as $index => $questionId) { $this->db->table('assessment_form_questions')->insert(['form_id' => $id, 'question_id' => $questionId, 'order_index' => $index + 1]); } } $this->db->transComplete(); return redirect()->to('administrator/assessments/forms')->with('success', 'Assessment form saved.'); } private function validateQuestion(): bool { $valid = $this->validate([ 'type' => 'required|in_list[multiple_choice,short_answer,true_false,essay]', 'text' => 'required|string|max_length[10000]', 'options_text' => 'permit_empty|string|max_length[10000]', 'correct_answer' => 'permit_empty|string|max_length[10000]', ]); if ($valid && $this->request->getPost('type') === 'multiple_choice') { $choices = preg_split('/\r\n|\r|\n/', trim((string) $this->request->getPost('options_text'))) ?: []; $choices = array_filter(array_map('trim', $choices), static fn ($choice) => $choice !== ''); if (count($choices) < 2) { $this->validator->setError('options_text', 'Multiple-choice questions need at least two choices.'); return false; } } return $valid; } private function questionPayload(int $poolId, int $order): array { $type = (string) $this->request->getPost('type'); $options = null; if ($type === 'true_false') { $options = json_encode(['True', 'False']); } elseif ($type === 'multiple_choice') { $values = preg_split('/\r\n|\r|\n/', trim((string) $this->request->getPost('options_text'))); $values = array_values(array_filter(array_map('trim', $values ?: []), static fn ($v) => $v !== '')); $options = $values ? json_encode($values, JSON_UNESCAPED_UNICODE) : null; } return [ 'pool_id' => $poolId, 'type' => $type, 'text' => trim((string) $this->request->getPost('text')), 'options' => $options, 'correct_answer' => $this->nullableString($this->request->getPost('correct_answer')), 'points' => 0, 'order_index' => $order, ]; } private function formWithQuestions(int $id): array { $form = $this->requireForm($id); $questions = $this->db->table('assessment_form_questions fq') ->select('q.*, fq.order_index AS form_order')->join('assessment_questions q', 'q.id = fq.question_id') ->where('fq.form_id', $id)->orderBy('fq.order_index', 'ASC')->get()->getResultArray(); return ['form' => $form, 'questions' => $questions, 'answers' => []]; } private function assessmentDetails(int $id): array { $assessment = $this->db->table('student_assessments sa') ->select('sa.*, f.name AS form_name, f.id AS assessment_form_id, f.school_year, f.education_committee_note AS form_education_committee_note, s.firstname, s.lastname, s.school_id') ->join('assessment_forms f', 'f.id = sa.form_id')->join('students s', 's.id = sa.student_id') ->where('sa.id', $id)->get()->getRowArray(); if (! $assessment) { throw new PageNotFoundException('Assessment not found.'); } $questions = $this->db->table('assessment_form_questions fq') ->select('q.*, fq.order_index AS form_order, a.answer_value') ->join('assessment_questions q', 'q.id = fq.question_id') ->join('student_answers a', 'a.question_id = q.id AND a.student_assessment_id = ' . (int) $id, 'left') ->where('fq.form_id', $assessment['form_id'])->orderBy('fq.order_index', 'ASC')->get()->getResultArray(); return ['assessment' => $assessment, 'questions' => $questions]; } private function requirePool(int $id): array { $row = $this->poolModel->find($id); if (! $row) throw new PageNotFoundException('Question pool not found.'); return $row; } private function primaryPool(): ?array { $pool = $this->poolModel->where('name', 'New Student Assessment')->first(); if ($pool) return $pool; return $this->poolModel->orderBy('id', 'ASC')->first() ?: null; } private function schoolYearOptions(): array { if (! $this->db->tableExists('school_years')) return []; return $this->db->table('school_years')->select('name, status')->orderBy('name', 'DESC')->get()->getResultArray(); } private function requireQuestion(int $id): array { $row = $this->questionModel->find($id); if (! $row) throw new PageNotFoundException('Question not found.'); return $row; } private function requireForm(int $id): array { $row = $this->formModel->find($id); if (! $row) throw new PageNotFoundException('Assessment form not found.'); return $row; } private function normalizeQuestionOrder(int $poolId): void { $rows = $this->questionModel->where('pool_id', $poolId)->orderBy('order_index', 'ASC')->orderBy('id', 'ASC')->findAll(); foreach ($rows as $index => $row) $this->questionModel->update($row['id'], ['order_index' => $index + 1]); } private function nullableString($value): ?string { $value = trim((string) $value); return $value === '' ? null : $value; } private function assessmentActionsForStudents(array $studentIds, string $schoolYear): array { $studentIds = array_values(array_unique(array_filter(array_map('intval', $studentIds)))); if ($studentIds === []) return []; $rows = $this->db->table('student_assessments sa') ->select('sa.*, f.name AS form_name')->join('assessment_forms f', 'f.id = sa.form_id') ->where('f.school_year', $schoolYear) ->whereIn('sa.student_id', $studentIds)->orderBy('sa.assigned_at', 'DESC')->orderBy('sa.id', 'DESC') ->get()->getResultArray(); $map = []; foreach ($rows as $row) { $studentId = (int) $row['student_id']; $priority = match ((string) $row['status']) { 'completed' => 3, 'not_started', 'in_progress' => 2, 'graded' => 1, default => 0, }; $currentStatus = $map[$studentId]['status'] ?? null; $currentPriority = match ($currentStatus) { 'completed' => 3, 'not_started', 'in_progress' => 2, 'graded' => 1, default => -1, }; if ($priority > $currentPriority) $map[$studentId] = $row; } return $map; } private function activeAssessmentForStudent(int $studentId, string $schoolYear): ?array { return $this->db->table('student_assessments sa') ->select('sa.*')->join('assessment_forms f', 'f.id = sa.form_id') ->where('sa.student_id', $studentId)->where('f.school_year', $schoolYear) ->whereIn('sa.status', ['not_started', 'in_progress']) ->orderBy('sa.assigned_at', 'DESC')->orderBy('sa.id', 'DESC')->get()->getRowArray() ?: null; } private function persistAnswers(array $assessment, array $answers): void { $questionIds = array_map('intval', array_column( $this->db->table('assessment_form_questions')->select('question_id')->where('form_id', $assessment['form_id'])->get()->getResultArray(), 'question_id' )); $now = date('Y-m-d H:i:s'); $this->db->transStart(); foreach ($questionIds as $questionId) { if (! array_key_exists($questionId, $answers)) continue; $value = is_array($answers[$questionId]) ? implode(', ', $answers[$questionId]) : trim((string) $answers[$questionId]); $existing = $this->db->table('student_answers')->where([ 'student_assessment_id' => $assessment['id'], 'question_id' => $questionId, ])->get()->getRowArray(); $payload = ['answer_value' => $value, 'updated_at' => $now]; if ($existing) { $this->db->table('student_answers')->where('id', $existing['id'])->update($payload); } else { $payload += ['student_assessment_id' => $assessment['id'], 'question_id' => $questionId, 'created_at' => $now]; $this->db->table('student_answers')->insert($payload); } } if ($assessment['status'] === 'not_started') { $this->assessmentModel->update($assessment['id'], ['status' => 'in_progress', 'started_at' => $now]); } $this->db->transComplete(); } private function persistEducationCommitteeNote(int $assessmentId): void { $note = trim((string) $this->request->getPost('education_committee_note')); if (mb_strlen($note) > 10000) $note = mb_substr($note, 0, 10000); $this->assessmentModel->update($assessmentId, [ 'education_committee_note' => $note === '' ? null : $note, ]); } private function requireAccessibleAssessment(int $id): array { $assessment = $this->assessmentModel->find($id); if (! $assessment || ! in_array((int) $assessment['student_id'], $this->accessibleStudentIds(), true)) { throw new PageNotFoundException('Assessment not found.'); } return $assessment; } private function accessibleStudentIds(): array { $userId = (int) session()->get('user_id'); if ($userId <= 0) return []; $ids = []; $user = $this->db->table('users')->select('school_id')->where('id', $userId)->get()->getRowArray(); $schoolId = trim((string) ($user['school_id'] ?? '')); if ($schoolId !== '') { $row = $this->db->table('students')->select('id')->where('school_id', $schoolId)->get()->getRowArray(); if ($row) $ids[] = (int) $row['id']; } $parentRows = $this->db->table('students')->select('id')->where('parent_id', $userId)->get()->getResultArray(); $ids = array_merge($ids, array_map('intval', array_column($parentRows, 'id'))); if ($this->db->tableExists('family_guardians') && $this->db->tableExists('family_students')) { $familyRows = $this->db->table('family_guardians fg')->select('fs.student_id') ->join('family_students fs', 'fs.family_id = fg.family_id')->where('fg.user_id', $userId)->get()->getResultArray(); $ids = array_merge($ids, array_map('intval', array_column($familyRows, 'student_id'))); } return array_values(array_unique(array_filter($ids))); } }