db = $db; $this->studentModel = $studentModel; $this->placementLevelModel = $placementLevelModel; $this->placementBatchModel = $placementBatchModel; $this->placementScoreModel = $placementScoreModel; $this->schoolYear = $schoolYear; } public function setSchoolYear(string $schoolYear): void { $this->schoolYear = $schoolYear; } public function updatePlacementLevel(array $params = []) { $get = $params['get'] ?? []; $post = $params['post'] ?? []; $studentId = (int) (($post['student_id'] ?? null) ?? 0); $levelRaw = trim((string) (($post['placement_level'] ?? null) ?? '')); $schoolYear = (string) (($post['school_year'] ?? null) ?? $this->schoolYear); if ($studentId <= 0 || $schoolYear === '') { return ['kind' => 'flash', 'redirect' => 'back', 'type' => 'error', 'message' => 'Missing student or school year.']; } $level = $levelRaw === '' ? null : (int) $levelRaw; if ($level !== null && !in_array($level, [1, 2, 3], true)) { return ['kind' => 'flash', 'redirect' => 'back', 'type' => 'error', 'message' => 'Invalid placement level.']; } $existing = $this->placementLevelModel ->where('student_id', $studentId) ->first(); if ($level === null) { if ($existing) { $this->placementLevelModel->delete($existing['id']); } return ['kind' => 'flash', 'redirect' => 'back', 'type' => 'status', 'message' => 'Placement level cleared.']; } $payload = [ 'student_id' => $studentId, 'level' => $level, 'updated_by' => session()->get('user_id'), ]; if ($existing) { $this->placementLevelModel->update($existing['id'], $payload); } else { $payload['created_by'] = session()->get('user_id'); $this->placementLevelModel->insert($payload); } return ['kind' => 'flash', 'redirect' => 'back', 'type' => 'status', 'message' => 'Placement level updated.']; } public function placementPage(array $params = []) { $get = $params['get'] ?? []; $post = $params['post'] ?? []; $classSectionId = (int) (($get['class_section_id'] ?? null) ?? 0); $schoolYear = (string) (($get['school_year'] ?? null) ?? $this->schoolYear); $placementTest = (string) (($get['placement_test'] ?? null) ?? ''); $openFlag = (string) (($get['open'] ?? null) ?? ''); if ($classSectionId <= 0 || $schoolYear === '') { $showStudents = ($placementTest !== '' && $openFlag === '1'); $students = $showStudents ? $this->fetchActiveStudentsWithSection($schoolYear) : []; $batches = $this->fetchPlacementBatches($schoolYear); $batchDetails = $this->fetchPlacementBatchDetails($batches, $schoolYear); return ['kind' => 'view', 'view' => 'grading/placement_index', 'data' => [ 'schoolYear' => $schoolYear, 'students' => $students, 'batches' => $batches, 'batchDetails' => $batchDetails, 'placementTest' => $placementTest, 'showStudents' => $showStudents, ]]; } $sectionName = $this->classSection->getClassSectionNameBySectionId($classSectionId) ?? ''; $classId = $this->classSection->getClassId($classSectionId); $students = $this->studentModel->getStudentInfoByClassSectionId($classSectionId, null, $schoolYear); $studentIds = array_values(array_filter(array_map( static fn($row) => (int) ($row['student_id'] ?? 0), $students ), static fn($id) => $id > 0)); $levels = []; if (!empty($studentIds)) { $rows = $this->placementLevelModel ->whereIn('student_id', $studentIds) ->findAll(); foreach ($rows as $row) { $levels[(int) $row['student_id']] = $row['level'] ?? null; } } return ['kind' => 'view', 'view' => 'grading/placement', 'data' => [ 'classSectionId' => $classSectionId, 'classSectionName' => $sectionName, 'classId' => $classId, 'schoolYear' => $schoolYear, 'students' => $students, 'levels' => $levels, ]]; } public function updatePlacementLevels(array $params = []) { $get = $params['get'] ?? []; $post = $params['post'] ?? []; $classSectionId = (int) (($post['class_section_id'] ?? null) ?? 0); $schoolYear = (string) (($post['school_year'] ?? null) ?? $this->schoolYear); $levels = ($post['placement_level'] ?? null) ?? []; if ($classSectionId <= 0 || $schoolYear === '') { return ['kind' => 'flash', 'redirect' => 'back', 'type' => 'error', 'message' => 'Missing class section or school year.']; } $students = $this->studentModel->getStudentInfoByClassSectionId($classSectionId, null, $schoolYear); $validIds = array_values(array_filter(array_map( static fn($row) => (int) ($row['student_id'] ?? 0), $students ), static fn($id) => $id > 0)); $validSet = array_flip($validIds); $existingRows = []; if (!empty($validIds)) { $rows = $this->placementLevelModel ->whereIn('student_id', $validIds) ->findAll(); foreach ($rows as $row) { $existingRows[(int) $row['student_id']] = $row; } } $userId = session()->get('user_id'); foreach ($levels as $studentIdRaw => $levelRaw) { $studentId = (int) $studentIdRaw; if (!isset($validSet[$studentId])) { continue; } $levelRaw = trim((string) $levelRaw); $level = $levelRaw === '' ? null : (int) $levelRaw; if ($level !== null && !in_array($level, [1, 2, 3], true)) { continue; } if ($level === null) { if (isset($existingRows[$studentId])) { $this->placementLevelModel->delete($existingRows[$studentId]['id']); } continue; } $payload = [ 'student_id' => $studentId, 'level' => $level, 'updated_by' => $userId, ]; if (isset($existingRows[$studentId])) { $this->placementLevelModel->update($existingRows[$studentId]['id'], $payload); } else { $payload['created_by'] = $userId; $this->placementLevelModel->insert($payload); } } return ['kind' => 'flash', 'redirect' => 'back', 'type' => 'status', 'message' => 'Placement levels updated.']; } public function updatePlacementLevelsAll(array $params = []) { $get = $params['get'] ?? []; $post = $params['post'] ?? []; $schoolYear = (string) (($post['school_year'] ?? null) ?? $this->schoolYear); $placementTest = (string) (($post['placement_test'] ?? null) ?? ''); $levels = ($post['placement_level'] ?? null) ?? []; if ($schoolYear === '' || $placementTest === '') { return ['kind' => 'flash', 'redirect' => 'back', 'type' => 'error', 'message' => 'Missing placement test or school year.']; } $students = $this->fetchActiveStudentsWithSection($schoolYear); $validIds = array_values(array_filter(array_map( static fn($row) => (int) ($row['student_id'] ?? 0), $students ), static fn($id) => $id > 0)); $validSet = array_flip($validIds); $userId = session()->get('user_id'); $batchId = $this->placementBatchModel->insert([ 'placement_test' => $placementTest, 'school_year' => $schoolYear, 'created_by' => $userId, 'updated_by' => $userId, ]); if (!$batchId) { return ['kind' => 'flash', 'redirect' => 'back', 'type' => 'error', 'message' => 'Unable to create placement batch.']; } $savedCount = 0; foreach ($levels as $studentIdRaw => $levelRaw) { $studentId = (int) $studentIdRaw; if (!isset($validSet[$studentId])) { continue; } $levelRaw = trim((string) $levelRaw); if ($levelRaw === '') { continue; } $level = (int) $levelRaw; if ($level < 0 || $level > 100) { continue; } $this->placementScoreModel->insert([ 'batch_id' => (int) $batchId, 'student_id' => $studentId, 'score' => $level, 'created_by' => $userId, 'updated_by' => $userId, ]); $savedCount++; } if ($savedCount === 0) { $this->placementBatchModel->delete((int) $batchId); return ['kind' => 'flash', 'redirect' => 'back', 'type' => 'error', 'message' => 'No scores entered. Batch not saved.']; } return ['kind' => 'flash', 'redirect' => base_url('grading/placement'), 'type' => 'status', 'message' => 'Placement batch saved.']; } public function editPlacementBatch(int $batchId, array $params = []) { $get = $params['get'] ?? []; $post = $params['post'] ?? []; $batch = $this->placementBatchModel->find($batchId); if (!$batch) { return ['kind' => 'flash', 'redirect' => base_url('grading/placement'), 'type' => 'error', 'message' => 'Placement batch not found.']; } $schoolYear = (string) ($batch['school_year'] ?? $this->schoolYear); $students = $this->fetchActiveStudentsWithSection($schoolYear); $scores = $this->fetchPlacementScoresForBatch($batchId); return ['kind' => 'view', 'view' => 'grading/placement_batch', 'data' => [ 'batch' => $batch, 'students' => $students, 'scores' => $scores, ]]; } public function updatePlacementBatch(int $batchId, array $params = []) { $get = $params['get'] ?? []; $post = $params['post'] ?? []; $batch = $this->placementBatchModel->find($batchId); if (!$batch) { return ['kind' => 'flash', 'redirect' => base_url('grading/placement'), 'type' => 'error', 'message' => 'Placement batch not found.']; } $schoolYear = (string) ($batch['school_year'] ?? $this->schoolYear); $levels = ($post['placement_level'] ?? null) ?? []; $students = $this->fetchActiveStudentsWithSection($schoolYear); $validIds = array_values(array_filter(array_map( static fn($row) => (int) ($row['student_id'] ?? 0), $students ), static fn($id) => $id > 0)); $validSet = array_flip($validIds); $existing = $this->fetchPlacementScoresForBatch($batchId); $userId = session()->get('user_id'); foreach ($levels as $studentIdRaw => $scoreRaw) { $studentId = (int) $studentIdRaw; if (!isset($validSet[$studentId])) { continue; } $scoreRaw = trim((string) $scoreRaw); if ($scoreRaw === '') { if (isset($existing[$studentId])) { $this->placementScoreModel->delete($existing[$studentId]['id']); } continue; } $score = (int) $scoreRaw; if ($score < 0 || $score > 100) { continue; } if (isset($existing[$studentId])) { $this->placementScoreModel->update($existing[$studentId]['id'], [ 'score' => $score, 'updated_by' => $userId, ]); } else { $this->placementScoreModel->insert([ 'batch_id' => $batchId, 'student_id' => $studentId, 'score' => $score, 'created_by' => $userId, 'updated_by' => $userId, ]); } } $this->placementBatchModel->update($batchId, [ 'updated_by' => $userId, ]); return ['kind' => 'flash', 'redirect' => base_url('grading/placement'), 'type' => 'status', 'message' => 'Placement batch updated.']; } public function fetchActiveStudentsWithSection(string $schoolYear): array { return $this->db->table('students s') ->select('s.id AS student_id, s.school_id, s.firstname, s.lastname, s.is_active, sc.class_section_id, cs.class_section_name, c.class_name, e.enrollment_status, e.is_withdrawn') ->join('student_class sc', 'sc.student_id = s.id AND sc.school_year = ' . $this->db->escape($schoolYear), 'left') ->join('enrollments e', 'e.student_id = s.id AND e.school_year = ' . $this->db->escape($schoolYear), 'left') ->join('`classSection` cs', 'cs.class_section_id = sc.class_section_id', 'left') ->join('classes c', 'c.id = cs.class_id', 'left') ->groupStart() ->where('s.is_active', 1) ->orWhere("LOWER(TRIM(e.enrollment_status)) IN ('denied','withdrawn','waitlist')", null, false) ->orWhere('e.is_withdrawn', 1) ->groupEnd() ->orderBy('s.lastname', 'ASC') ->orderBy('s.firstname', 'ASC') ->get() ->getResultArray(); } public function fetchPlacementBatches(string $schoolYear): array { return $this->placementBatchModel ->where('school_year', $schoolYear) ->orderBy('created_at', 'DESC') ->findAll(); } public function fetchPlacementBatchDetails(array $batches, string $schoolYear): array { if (empty($batches)) { return []; } $batchIds = array_values(array_filter(array_map( static fn($row) => (int) ($row['id'] ?? 0), $batches ), static fn($id) => $id > 0)); if (empty($batchIds)) { return []; } $rows = $this->db->table('placement_scores ps') ->select('ps.batch_id, ps.student_id, ps.score, s.school_id, s.firstname, s.lastname, s.is_active, e.enrollment_status, e.is_withdrawn, cs.class_section_name, c.class_name') ->join('students s', 's.id = ps.student_id', 'inner') ->join('student_class sc', 'sc.student_id = s.id AND sc.school_year = ' . $this->db->escape($schoolYear), 'left') ->join('enrollments e', 'e.student_id = s.id AND e.school_year = ' . $this->db->escape($schoolYear), 'left') ->join('`classSection` cs', 'cs.class_section_id = sc.class_section_id', 'left') ->join('classes c', 'c.id = cs.class_id', 'left') ->whereIn('ps.batch_id', $batchIds) ->groupStart() ->where('s.is_active', 1) ->orWhere("LOWER(TRIM(e.enrollment_status)) IN ('denied','withdrawn','waitlist')", null, false) ->orWhere('e.is_withdrawn', 1) ->groupEnd() ->orderBy('ps.batch_id', 'ASC') ->orderBy('s.lastname', 'ASC') ->orderBy('s.firstname', 'ASC') ->get() ->getResultArray(); $details = []; foreach ($rows as $row) { $bid = (int) ($row['batch_id'] ?? 0); if ($bid <= 0) continue; $details[$bid][] = $row; } return $details; } public function fetchPlacementScoresForBatch(int $batchId): array { $rows = $this->placementScoreModel ->where('batch_id', $batchId) ->findAll(); $scores = []; foreach ($rows as $row) { $scores[(int) $row['student_id']] = $row; } return $scores; } }