fix school year and related issues
Tests / PHPUnit (push) Failing after 34s

This commit is contained in:
root
2026-07-30 00:48:54 -04:00
parent f8f00c70c8
commit 81a72a4c59
27 changed files with 1512 additions and 52 deletions
+206 -5
View File
@@ -9,6 +9,7 @@ use App\Models\ClassSectionModel;
use App\Models\StudentClassModel;
use App\Models\StudentSectionDistributionDraftModel;
use App\Models\AuthorizedUserModel;
use App\Models\ParentPolicyAcceptanceModel;
use App\Models\EmergencyContactModel;
use App\Models\ConfigurationModel;
use App\Models\StudentMedicalConditionModel;
@@ -54,6 +55,7 @@ class ParentController extends BaseController
protected $medicalConditionModel;
protected $allergyModel;
protected $authorizedUsersModel;
protected ParentPolicyAcceptanceModel $policyAcceptanceModel;
protected $schoolStartDate;
protected $ageDateRefernce;
@@ -75,6 +77,7 @@ class ParentController extends BaseController
$this->medicalConditionModel = new StudentMedicalConditionModel();
$this->allergyModel = new StudentAllergyModel();
$this->authorizedUsersModel = new AuthorizedUserModel();
$this->policyAcceptanceModel = new ParentPolicyAcceptanceModel();
$this->ageDateRefernce = $this->configModel->getConfig('date_age_reference');
$this->lastDayOfRegistration = $this->configModel->getConfig('enrollment_deadline');
@@ -270,7 +273,7 @@ class ParentController extends BaseController
// Verify user type is "parent" from the `users` table
$userData = $this->db->table('users')
->select('user_type')
->select('user_type, accept_school_policy')
->where('id', $parentId)
->get()
->getRowArray();
@@ -290,6 +293,9 @@ class ParentController extends BaseController
return redirect()->to('/no-kids')->with('error', 'No students found. Please register your child first.');
}
$previousSchoolYear = $this->previousSchoolYearName($selectedYear);
$fallMakeupExamOn = $this->fallMakeupExamDateForYear($selectedYear);
// Map enrollment statuses
$statusMap = [
'admission under review' => 'admission under review',
@@ -356,16 +362,23 @@ class ParentController extends BaseController
$student['enrollment_status'],
['admission under review', 'payment pending', 'enrolled', 'withdraw under review', 'denied']
);
$student['previous_year_decision'] = $previousSchoolYear !== null
? $this->studentDecisionForYear((int) $studentId, $previousSchoolYear)
: null;
}
// Render view
return view('/parent/enroll_classes', [
'students' => $students,
'selectedYear' => $selectedYear,
'previousSchoolYear' => $previousSchoolYear,
'fallMakeupExamOn' => $fallMakeupExamOn,
'isEditable' => $isEditable,
'withdrawalDeadline' => $this->withdrawalDeadline,
'lastDayOfRegistration' => $this->lastDayOfRegistration,
'schoolStartDate' => $this->schoolStartDate
'schoolStartDate' => $this->schoolStartDate,
'hasAcceptedSchoolPolicy' => $this->hasAcceptedPolicyForYear((int) $parentId, $selectedYear),
]);
} catch (Exception $e) {
log_message('error', 'An error occurred in enrollClasses: ' . $e->getMessage());
@@ -389,6 +402,25 @@ class ParentController extends BaseController
$withdraw = $this->request->getPost('withdraw'); // Selected students for withdrawal
$parentId = session()->get('user_id'); // Parent ID from session
if (!empty($enroll)) {
$parent = $this->userModel->find((int) $parentId);
$selectedYear = $this->currentSchoolYearName((string) ($this->schoolYear ?? ''));
$hasAcceptedPolicy = $this->hasAcceptedPolicyForYear((int) $parentId, $selectedYear);
$acceptedOnSubmit = (string) ($this->request->getPost('accept_school_policy') ?? '') === '1';
if (!$hasAcceptedPolicy && !$acceptedOnSubmit) {
return redirect()->back()->withInput()->with('error', 'You must read and accept the school policy before enrolling students.');
}
if (!$hasAcceptedPolicy && $acceptedOnSubmit) {
$this->recordPolicyAcceptance((int) $parentId, $selectedYear, 'returning_parent_enrollment');
$this->userModel->update((int) $parentId, [
'accept_school_policy' => 1,
'updated_at' => utc_now(),
]);
}
}
if ($this->lastDayOfRegistration && local_date(utc_now(), 'Y-m-d') > date('Y-m-d', strtotime($this->lastDayOfRegistration))) {
if (!empty($enroll)) {
return redirect()->back()->with('error', 'Enrollment deadline has passed. New enrollments are not allowed.');
@@ -404,6 +436,12 @@ class ParentController extends BaseController
$studentData = [];
if (!empty($enroll)) {
$selectedYear = $this->currentSchoolYearName((string) ($this->schoolYear ?? ''));
$blockingDecisionMessages = $this->blockedEnrollmentDecisionMessages(array_map('intval', (array) $enroll), $selectedYear);
if ($blockingDecisionMessages !== []) {
return redirect()->back()->withInput()->with('error', implode(' ', $blockingDecisionMessages));
}
foreach ($enroll as $studentId) {
// Get student full name (supports both string return or array with firstname/lastname)
$studentInfo = $this->studentModel->getFullNameById($studentId);
@@ -571,6 +609,40 @@ class ParentController extends BaseController
}
}
private function hasAcceptedPolicyForYear(int $parentId, string $schoolYear): bool
{
if ($parentId <= 0 || $schoolYear === '') {
return false;
}
try {
return $this->policyAcceptanceModel->hasAccepted($parentId, $schoolYear);
} catch (Throwable $e) {
log_message('error', 'Failed to read parent policy acceptance: {message}', [
'message' => $e->getMessage(),
]);
return false;
}
}
private function recordPolicyAcceptance(int $parentId, string $schoolYear, string $source): void
{
if ($parentId <= 0 || $schoolYear === '') {
throw new \RuntimeException('Unable to record school policy acceptance.');
}
if (! $this->policyAcceptanceModel->recordAcceptance(
$parentId,
$schoolYear,
$source,
$this->request->getIPAddress(),
$this->request->getUserAgent()->getAgentString()
)) {
throw new \RuntimeException('Unable to record school policy acceptance.');
}
}
/**
* If a promotion_queue record exists for this student and the given school year,
* create/update student_class row accordingly (base section until distribution),
@@ -705,6 +777,84 @@ class ParentController extends BaseController
return false;
}
private function studentDecisionForYear(int $studentId, string $schoolYear): ?array
{
if ($studentId <= 0 || $schoolYear === '' || ! $this->db->tableExists('student_decisions')) {
return null;
}
try {
return $this->db->table('student_decisions')
->select('decision, source, notes, class_section_name')
->where('student_id', $studentId)
->where('school_year', $schoolYear)
->orderBy('updated_at', 'DESC')
->orderBy('id', 'DESC')
->limit(1)
->get()
->getRowArray() ?: null;
} catch (\Throwable $e) {
log_message('error', 'studentDecisionForYear failed: ' . $e->getMessage());
return null;
}
}
private function fallMakeupExamDateForYear(string $schoolYear): ?string
{
if ($schoolYear === '' || ! $this->db->tableExists('school_years') || ! $this->db->fieldExists('fall_makeup_exam_on', 'school_years')) {
return null;
}
try {
$row = $this->db->table('school_years')
->select('fall_makeup_exam_on')
->where('name', $schoolYear)
->limit(1)
->get()
->getRowArray();
$date = trim((string) ($row['fall_makeup_exam_on'] ?? ''));
return $date !== '' ? $date : null;
} catch (\Throwable $e) {
log_message('error', 'fallMakeupExamDateForYear failed: ' . $e->getMessage());
return null;
}
}
private function blockedEnrollmentDecisionMessages(array $studentIds, string $targetSchoolYear): array
{
$previousSchoolYear = $this->previousSchoolYearName($targetSchoolYear);
if ($previousSchoolYear === null) {
return [];
}
$messages = [];
foreach (array_unique($studentIds) as $studentId) {
$studentId = (int) $studentId;
$decisionRow = $this->studentDecisionForYear($studentId, $previousSchoolYear);
$decision = strtolower(trim((string) ($decisionRow['decision'] ?? '')));
$source = strtolower(trim((string) ($decisionRow['source'] ?? '')));
if ($decision === 'pass' || $decision === 'repeat class') {
continue;
}
if ($decision === '' && $source !== 'pending') {
continue;
}
$studentInfo = $this->studentModel->getFullNameById($studentId);
$studentName = is_array($studentInfo)
? trim((string) ($studentInfo['firstname'] ?? '') . ' ' . (string) ($studentInfo['lastname'] ?? ''))
: trim((string) $studentInfo);
$studentName = $studentName !== '' ? $studentName : 'Student ID ' . $studentId;
$messages[] = $studentName . ': enrollment cannot be submitted until the prior-year decision is resolved with administration.';
}
return $messages;
}
private function previousSchoolYearName(string $schoolYear): ?string
{
$schoolYear = trim($schoolYear);
@@ -1292,15 +1442,17 @@ class ParentController extends BaseController
$kids = $this->studentModel->where('parent_id', $parentId)->findAll();
foreach ($kids as &$kid) {
$studentId = (int) ($kid['id'] ?? 0);
$kid['allergies'] = $this->allergyModel
->where('student_id', $kid['id'])
->where('student_id', $studentId)
->findColumn('allergy') ?? [];
$kid['medical_conditions'] = $this->medicalConditionModel
->where('student_id', $kid['id'])
->where('student_id', $studentId)
->findColumn('condition_name') ?? [];
$kid['enrollment'] = isset($enrollmentMap[$kid['id']]['id']) && !empty($enrollmentMap[$kid['id']]['id']) ? 1 : 0;
$kid['enrollment'] = isset($enrollmentMap[$studentId]['id']) && !empty($enrollmentMap[$studentId]['id']) ? 1 : 0;
$kid['can_delete'] = $this->canParentDeleteStudent($kid, $parentId);
}
$emergencies = $this->emergencyContactModel->where('parent_id', $parentId)->findAll();
@@ -1784,6 +1936,10 @@ $existing = $this->studentModel
return redirect()->back()->with('error', 'Student not found or unauthorized.');
}
if (! $this->canParentDeleteStudent($student, (int) $parentId)) {
return redirect()->back()->with('error', 'This student has enrollment history and cannot be deleted. Please contact administration if a change is needed.');
}
// Perform deletion
if (!$this->studentModel->delete($id)) {
return redirect()->back()->with('error', 'Failed to delete student.');
@@ -1803,6 +1959,51 @@ $existing = $this->studentModel
return redirect()->to('/parent/child_register')->with('success', 'Student deleted successfully.');
}
private function canParentDeleteStudent(array $student, int $parentId): bool
{
$studentId = (int) ($student['id'] ?? 0);
if ($studentId <= 0) {
return false;
}
if ((string) ($student['is_new'] ?? '1') === '0') {
return false;
}
if ($this->studentHasEnrollmentHistory($studentId, $parentId)) {
return false;
}
if ($this->studentHasClassAssignmentHistory($studentId)) {
return false;
}
return true;
}
private function studentHasEnrollmentHistory(int $studentId, int $parentId): bool
{
if (! $this->db->tableExists('enrollments')) {
return false;
}
return $this->db->table('enrollments')
->where('student_id', $studentId)
->where('parent_id', $parentId)
->countAllResults() > 0;
}
private function studentHasClassAssignmentHistory(int $studentId): bool
{
if (! $this->db->tableExists('student_class')) {
return false;
}
return $this->db->table('student_class')
->where('student_id', $studentId)
->countAllResults() > 0;
}
private function formatName(string $name): string