Files
alrahma_sunday_school/app/Services/PlacementGradingService.php
T
root 2b0206e7f2
Deploy to Shared Hosting / Shared hosting deploy (push) Failing after 47s
Tests / PHPUnit (push) Successful in 1m16s
move service logic from grading and administrator controller
2026-08-20 16:59:51 -04:00

438 lines
15 KiB
PHP

<?php
namespace App\Services;
use App\Models\PlacementBatchModel;
use App\Models\PlacementLevelModel;
use App\Models\PlacementScoreModel;
use App\Models\StudentModel;
class PlacementGradingService
{
protected $db;
protected $studentModel;
protected $placementLevelModel;
protected $placementBatchModel;
protected $placementScoreModel;
protected string $schoolYear = '';
public function __construct(
\CodeIgniter\Database\BaseConnection $db,
StudentModel $studentModel,
PlacementLevelModel $placementLevelModel,
PlacementBatchModel $placementBatchModel,
PlacementScoreModel $placementScoreModel,
string $schoolYear = ''
) {
$this->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;
}
}