fix is_active student flag logic and make moving with enrollment status
Deploy to Shared Hosting / Shared hosting deploy (push) Failing after 47s
Tests / PHPUnit (push) Successful in 1m16s

This commit is contained in:
root
2026-08-20 03:11:37 -04:00
parent 6f722795c5
commit 0a31aa1393
20 changed files with 588 additions and 489 deletions
+11 -247
View File
@@ -15,7 +15,6 @@ use App\Models\StudentAllergyModel;
use App\Models\StudentMedicalConditionModel;
use App\Support\Enrollment\DeliberationDecision;
use CodeIgniter\Database\Exceptions\DataException;
use Config\Services;
use Throwable;
class StudentController extends BaseController
@@ -225,14 +224,20 @@ class StudentController extends BaseController
if ($enroll) {
$enPk = $this->enrollmentModel->primaryKey ?? 'id';
if (!$this->enrollmentModel->update($enroll[$enPk], [
$result = \Config\Services::enrollmentStatus(false)->upsertStatus([
'id' => (int) $enroll[$enPk],
'student_id' => $studentId,
'parent_id' => (int) ($enroll['parent_id'] ?? 0),
'school_year' => (string) $this->schoolYear,
'semester' => (string) $this->semester,
'class_section_id' => $primarySectionId,
'enrollment_status' => 'payment pending',
// Ensure admission is marked accepted once moved out of review
'admission_status' => 'accepted',
'updated_at' => $now,
])) {
throw new \RuntimeException('Failed to update enrollment: ' . json_encode($this->enrollmentModel->errors()));
], $userId ?: null, 'student_class_assignment');
if ((int) ($result['id'] ?? 0) <= 0) {
throw new \RuntimeException('Failed to update enrollment.');
}
}
@@ -387,242 +392,6 @@ class StudentController extends BaseController
}
}
public function removedStudents()
{
$schoolYear = (string)($this->schoolYear ?? '');
$classMap = [];
$activeYearStudentIds = [];
$classQuery = $this->db->table('student_class sc')
->select('sc.student_id, cs.class_section_name')
->join('classSection cs', 'cs.class_section_id = sc.class_section_id', 'left');
if ($schoolYear !== '') {
$classQuery->where('sc.school_year', $schoolYear);
}
$classRows = $classQuery->get()->getResultArray();
foreach ($classRows as $row) {
$sid = (int)($row['student_id'] ?? 0);
if ($sid > 0) {
$activeYearStudentIds[$sid] = true;
}
$name = trim((string)($row['class_section_name'] ?? ''));
if ($sid <= 0 || $name === '') continue;
$classMap[$sid][] = $name;
}
$students = $this->studentModel
->select('id, school_id, firstname, lastname, gender, age, is_active')
->orderBy('lastname', 'ASC')
->orderBy('firstname', 'ASC')
->findAll();
$activeStudents = [];
$removedStudents = [];
foreach ($students as $student) {
$studentId = (int)($student['id'] ?? 0);
$isGloballyActive = (int)($student['is_active'] ?? 0) === 1;
$hasSelectedYearClass = $studentId > 0 && isset($activeYearStudentIds[$studentId]);
if ($isGloballyActive && $hasSelectedYearClass) {
$activeStudents[] = $student;
} else {
$removedStudents[] = $student;
}
}
$attachClassNames = static function (array $students) use ($classMap): array {
foreach ($students as &$student) {
$sid = (int)($student['id'] ?? 0);
$names = $classMap[$sid] ?? [];
$names = array_values(array_unique(array_filter($names)));
$student['class_sections'] = $names;
$student['class_section_name'] = !empty($names) ? implode(', ', $names) : 'No class assigned';
}
unset($student);
return $students;
};
return view('administrator/removed_students', [
'active_students' => $attachClassNames($activeStudents),
'removed_students' => $attachClassNames($removedStudents),
'school_year' => $schoolYear,
'active_school_year' => (string)($this->schoolYear ?? ''),
]);
}
public function setStudentActive()
{
$studentId = (int) $this->request->getPost('student_id');
$isActiveRaw = (string) $this->request->getPost('is_active');
$isActive = $isActiveRaw === '1' ? 1 : 0;
$now = utc_now();
$userId = (int) (session()->get('user_id') ?? 0);
if ($studentId <= 0) {
return redirect()->back()->with('error', 'Invalid student ID.');
}
$student = $this->studentModel->find($studentId);
if (!$student) {
return redirect()->back()->with('error', 'Student not found.');
}
if (!$this->studentModel->update($studentId, ['is_active' => $isActive])) {
return redirect()->back()->with('error', 'Unable to update student status.');
}
$message = $isActive ? 'Student restored successfully.' : 'Student removed successfully.';
if ($isActive === 1) {
$hasCurrentClass = $this->studentClassModel
->where('student_id', $studentId)
->where('school_year', (string)$this->schoolYear)
->where('semester', (string)$this->semester)
->first();
if (!$hasCurrentClass) {
$lastClass = $this->studentClassModel
->where('student_id', $studentId)
->orderBy('updated_at', 'DESC')
->orderBy('created_at', 'DESC')
->orderBy('id', 'DESC')
->first();
$restoreClassId = (int)($lastClass['class_section_id'] ?? 0);
if ($restoreClassId > 0) {
$inserted = $this->studentClassModel->insert([
'student_id' => $studentId,
'class_section_id' => $restoreClassId,
'semester' => (string)$this->semester,
'school_year' => (string)$this->schoolYear,
'description' => $lastClass['description'] ?? null,
'updated_by' => $userId ?: null,
'updated_at' => $now,
'created_at' => $now,
]);
if ($inserted) {
$classLabel = (string)($this->classSectionModel->getClassSectionNameBySectionId($restoreClassId) ?? '');
if ($classLabel !== '') {
$message .= ' Class assignment restored to ' . $classLabel . '.';
}
} else {
$message .= ' No class assignment found for the current term.';
return redirect()->to(base_url('administrator/removed_students'))->with('warning', $message);
}
} else {
$message .= ' No class assignment found for the current term.';
return redirect()->to(base_url('administrator/removed_students'))->with('warning', $message);
}
}
}
if ($isActive === 0) {
if (!$this->notifyParentOfStudentRemoval($studentId)) {
$message .= ' Parent notification email could not be sent.';
}
}
return redirect()->to(base_url('administrator/removed_students'))->with('success', $message);
}
private function notifyParentOfStudentRemoval(int $studentId): bool
{
try {
$studentRow = $this->studentModel
->select([
'students.firstname AS student_firstname',
'students.lastname AS student_lastname',
'students.school_id',
'users.id AS parent_id',
'users.firstname AS parent_firstname',
'users.lastname AS parent_lastname',
'users.email AS parent_email',
])
->join('users', 'users.id = students.parent_id', 'left')
->where('students.id', $studentId)
->first();
if (empty($studentRow)) {
log_message('warning', "Student removal email skipped: student {$studentId} not found.");
return false;
}
$parentEmail = trim((string)($studentRow['parent_email'] ?? ''));
if ($parentEmail === '') {
log_message('warning', "Student removal email skipped: missing parent email for student {$studentId}.");
return false;
}
$studentName = trim(trim((string)($studentRow['student_firstname'] ?? '')) . ' ' . trim((string)($studentRow['student_lastname'] ?? '')));
$parentName = trim(trim((string)($studentRow['parent_firstname'] ?? '')) . ' ' . trim((string)($studentRow['parent_lastname'] ?? '')));
if ($parentName === '') {
$parentName = 'Parent/Guardian';
}
$subject = 'Withdrawal Notice for ' . ($studentName !== '' ? $studentName : 'Your Student');
$html = view('emails/student_removed', [
'student_name' => $studentName,
'school_id' => $studentRow['school_id'] ?? '',
'parent_name' => $parentName,
'signature' => 'AlRahma School Administration',
], ['saveData' => true]);
$sent = $this->sendHtmlEmail($parentEmail, $subject, $html);
$logLevel = $sent ? 'info' : 'warning';
log_message($logLevel, "Student removal email " . ($sent ? '' : 'not ') . "sent (studentId: {$studentId}, parent: {$parentEmail}).");
return $sent;
} catch (\Throwable $e) {
log_message('error', 'Student removal notification failed: ' . $e->getMessage());
return false;
}
}
private function sendHtmlEmail(string $to, string $subject, string $html): bool
{
try {
$service = function_exists('service') ? service('emailService') : null;
if (!$service && method_exists(Services::class, 'emailService')) {
$service = Services::emailService();
}
if ($service && method_exists($service, 'send')) {
$result = $service->send($to, $subject, $html, 'student-removal');
if ($result) {
return true;
}
log_message('debug', 'Custom emailService failed to send student removal notice.');
}
$email = Services::email();
$cfg = config('Email');
$fromEmail = $cfg->fromEmail ?? $cfg->SMTPUser ?? 'no-reply@example.com';
$fromName = $cfg->fromName ?? 'Al Rahma Sunday School';
$email->setTo($to);
$email->setFrom($fromEmail, $fromName);
$email->setSubject($subject);
$email->setMessage($html);
$email->setMailType('html');
$ok = $email->send();
if (!$ok) {
$debug = method_exists($email, 'printDebugger') ? $email->printDebugger(['headers', 'subject']) : 'no debugger';
log_message('debug', 'CI Email send failed for student removal: ' . print_r($debug, true));
}
return $ok;
} catch (\Throwable $e) {
log_message('debug', 'sendHtmlEmail exception for student removal: ' . $e->getMessage());
return false;
}
}
private function updateStudentAttendanceSection(
int $studentId,
int $newClassSectionId,
@@ -2742,7 +2511,6 @@ class StudentController extends BaseController
'rfid_tag' => 'permit_empty|max_length[100]',
'semester' => 'permit_empty|in_list[Fall,Spring,Summer]',
'is_new' => 'required|in_list[0,1]',
'is_active' => 'permit_empty|in_list[0,1]',
// Free-text lists; parsed later
'medical_conditions' => 'permit_empty',
@@ -2777,7 +2545,6 @@ class StudentController extends BaseController
'rfid_tag' => trim((string) $request->getPost('rfid_tag')),
'semester' => trim((string) $request->getPost('semester')),
'is_new' => (string) $request->getPost('is_new', FILTER_SANITIZE_NUMBER_INT),
'is_active' => (string) ($rawPost['is_active'] ?? ''),
// raw lists (may be missing from POST entirely)
'medical_conditions' => (string) ($rawPost['medical_conditions'] ?? ''),
@@ -2841,11 +2608,8 @@ class StudentController extends BaseController
'school_year' => $in['school_year'] ?: null,
'rfid_tag' => $in['rfid_tag'] ?: null,
'semester' => $in['semester'] ?: null,
'is_new' => (int) ($in['is_new'] === '1'),
];
if (array_key_exists('is_active', $rawPost)) {
$studentData['is_active'] = (int) ($in['is_active'] === '1');
}
'is_new' => (int) ($in['is_new'] === '1'),
];
// Normalize health lists (server-side safety)
$normConditions = $this->normalizeHealthList($in['medical_conditions'], 100); // -> condition_name