@@ -305,6 +305,7 @@ class ParentController extends BaseController
|
|||||||
// Attach enrollment and class section data to each student
|
// Attach enrollment and class section data to each student
|
||||||
foreach ($students as &$student) {
|
foreach ($students as &$student) {
|
||||||
$studentId = $student['id'];
|
$studentId = $student['id'];
|
||||||
|
$student['age'] = $this->calculateAgeAsOfSchoolYearStartYear($student['dob'] ?? null, $selectedYear);
|
||||||
|
|
||||||
// Get class section info (can be multiple sections like Grade + Arabic)
|
// Get class section info (can be multiple sections like Grade + Arabic)
|
||||||
$classSections = $this->studentClassModel->getClassSectionsByStudentId($studentId, $selectedYear, true);
|
$classSections = $this->studentClassModel->getClassSectionsByStudentId($studentId, $selectedYear, true);
|
||||||
@@ -428,11 +429,14 @@ class ParentController extends BaseController
|
|||||||
}
|
}
|
||||||
if ($existingEnrollment) {
|
if ($existingEnrollment) {
|
||||||
if ($existingEnrollment['is_withdrawn'] == 1) {
|
if ($existingEnrollment['is_withdrawn'] == 1) {
|
||||||
|
$passedPreviousYear = $this->studentPassedPreviousYear((int)$studentId, (string)$this->schoolYear);
|
||||||
|
|
||||||
// Reactivate the enrollment if the student was previously withdrawn
|
// Reactivate the enrollment if the student was previously withdrawn
|
||||||
$this->enrollmentModel->where('id', $existingEnrollment['id'])->update([
|
$this->enrollmentModel->where('id', $existingEnrollment['id'])->update([
|
||||||
'is_withdrawn' => 0,
|
'is_withdrawn' => 0,
|
||||||
'withdrawal_date' => null,
|
'withdrawal_date' => null,
|
||||||
'enrollment_status' => 'payment pending',
|
'enrollment_status' => $passedPreviousYear ? 'payment pending' : 'admission under review',
|
||||||
|
'admission_status' => $passedPreviousYear ? 'accepted' : 'pending',
|
||||||
'updated_at' => utc_now()
|
'updated_at' => utc_now()
|
||||||
]);
|
]);
|
||||||
log_message('info', "{$studentData[$studentId]['firstname']} {$studentData[$studentId]['lastname']} (ID $studentId) has been re-enrolled in enrollment ID {$existingEnrollment['id']}.");
|
log_message('info', "{$studentData[$studentId]['firstname']} {$studentData[$studentId]['lastname']} (ID $studentId) has been re-enrolled in enrollment ID {$existingEnrollment['id']}.");
|
||||||
@@ -442,6 +446,8 @@ class ParentController extends BaseController
|
|||||||
log_message('info', "{$studentData[$studentId]['firstname']} {$studentData[$studentId]['lastname']} (ID $studentId) is already actively enrolled.");
|
log_message('info', "{$studentData[$studentId]['firstname']} {$studentData[$studentId]['lastname']} (ID $studentId) is already actively enrolled.");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
$passedPreviousYear = $this->studentPassedPreviousYear((int)$studentId, (string)$this->schoolYear);
|
||||||
|
|
||||||
// If no enrollment record exists, insert a new enrollment record
|
// If no enrollment record exists, insert a new enrollment record
|
||||||
$result = $this->enrollmentModel->insert([
|
$result = $this->enrollmentModel->insert([
|
||||||
'student_id' => $studentId,
|
'student_id' => $studentId,
|
||||||
@@ -450,8 +456,8 @@ class ParentController extends BaseController
|
|||||||
'semester' => $this->semester,
|
'semester' => $this->semester,
|
||||||
'enrollment_date' => local_date(utc_now(), 'Y-m-d'),
|
'enrollment_date' => local_date(utc_now(), 'Y-m-d'),
|
||||||
'is_withdrawn' => 0,
|
'is_withdrawn' => 0,
|
||||||
'enrollment_status' => 'admission under review',
|
'enrollment_status' => $passedPreviousYear ? 'payment pending' : 'admission under review',
|
||||||
'admission_status' => 'pending',
|
'admission_status' => $passedPreviousYear ? 'accepted' : 'pending',
|
||||||
'created_at' => utc_now()
|
'created_at' => utc_now()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -625,6 +631,61 @@ class ParentController extends BaseController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function studentPassedPreviousYear(int $studentId, string $targetSchoolYear): bool
|
||||||
|
{
|
||||||
|
$previousSchoolYear = $this->previousSchoolYearName($targetSchoolYear);
|
||||||
|
|
||||||
|
if ($studentId <= 0 || $previousSchoolYear === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ($this->db->tableExists('promotion_queue')) {
|
||||||
|
$queuedPromotion = $this->db->table('promotion_queue')
|
||||||
|
->select('id')
|
||||||
|
->where('student_id', $studentId)
|
||||||
|
->where('school_year_from', $previousSchoolYear)
|
||||||
|
->where('school_year_to', $targetSchoolYear)
|
||||||
|
->whereIn('status', ['queued', 'assigned', 'applied'])
|
||||||
|
->limit(1)
|
||||||
|
->get()
|
||||||
|
->getRowArray();
|
||||||
|
|
||||||
|
if ($queuedPromotion !== null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->db->tableExists('student_decisions')) {
|
||||||
|
$passedDecision = $this->db->table('student_decisions')
|
||||||
|
->select('id')
|
||||||
|
->where('student_id', $studentId)
|
||||||
|
->where('school_year', $previousSchoolYear)
|
||||||
|
->where('LOWER(TRIM(decision)) = ' . $this->db->escape('pass'), null, false)
|
||||||
|
->limit(1)
|
||||||
|
->get()
|
||||||
|
->getRowArray();
|
||||||
|
|
||||||
|
return $passedDecision !== null;
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
log_message('error', 'studentPassedPreviousYear failed: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function previousSchoolYearName(string $schoolYear): ?string
|
||||||
|
{
|
||||||
|
$schoolYear = trim($schoolYear);
|
||||||
|
|
||||||
|
if (! preg_match('/^(\d{4})-(\d{4})$/', $schoolYear, $matches)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ((int) $matches[1] - 1) . '-' . ((int) $matches[2] - 1);
|
||||||
|
}
|
||||||
|
|
||||||
public function enrollFailure()
|
public function enrollFailure()
|
||||||
{
|
{
|
||||||
echo view('/parent/enroll_failure');
|
echo view('/parent/enroll_failure');
|
||||||
@@ -1754,6 +1815,77 @@ $existing = $this->studentModel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function calculateAgeAsOfToday(?string $dob): ?int
|
||||||
|
{
|
||||||
|
$dob = trim((string) $dob);
|
||||||
|
|
||||||
|
if ($dob === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$timezone = new \DateTimeZone((string) (config('School')->attendance['timezone'] ?? user_timezone()));
|
||||||
|
$birthDate = \DateTimeImmutable::createFromFormat('!Y-m-d', $dob, $timezone);
|
||||||
|
$errors = \DateTimeImmutable::getLastErrors();
|
||||||
|
$hasParseErrors = is_array($errors)
|
||||||
|
&& (($errors['warning_count'] ?? 0) > 0 || ($errors['error_count'] ?? 0) > 0);
|
||||||
|
|
||||||
|
if ($birthDate === false || $hasParseErrors) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$today = new \DateTimeImmutable('today', $timezone);
|
||||||
|
|
||||||
|
if ($birthDate > $today) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $birthDate->diff($today)->y;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
log_message('warning', 'Unable to calculate current age from DOB: {message}', [
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function calculateAgeAsOfSchoolYearStartYear(?string $dob, string $schoolYear): ?int
|
||||||
|
{
|
||||||
|
$dob = trim((string) $dob);
|
||||||
|
$schoolYear = trim($schoolYear);
|
||||||
|
|
||||||
|
if ($dob === '' || ! preg_match('/^(\d{4})/', $schoolYear, $matches)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$timezone = new \DateTimeZone((string) (config('School')->attendance['timezone'] ?? user_timezone()));
|
||||||
|
$birthDate = \DateTimeImmutable::createFromFormat('!Y-m-d', $dob, $timezone);
|
||||||
|
$errors = \DateTimeImmutable::getLastErrors();
|
||||||
|
$hasParseErrors = is_array($errors)
|
||||||
|
&& (($errors['warning_count'] ?? 0) > 0 || ($errors['error_count'] ?? 0) > 0);
|
||||||
|
|
||||||
|
if ($birthDate === false || $hasParseErrors) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$schoolYearStartYearCutoff = new \DateTimeImmutable($matches[1] . '-12-31', $timezone);
|
||||||
|
|
||||||
|
if ($birthDate > $schoolYearStartYearCutoff) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $birthDate->diff($schoolYearStartYearCutoff)->y;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
log_message('warning', 'Unable to calculate school-year age from DOB: {message}', [
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function parentEventPage()
|
public function parentEventPage()
|
||||||
{
|
{
|
||||||
$schoolYear = $this->currentSchoolYearName((string) ($this->schoolYear ?? ''));
|
$schoolYear = $this->currentSchoolYearName((string) ($this->schoolYear ?? ''));
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Database\Migrations;
|
||||||
|
|
||||||
|
use CodeIgniter\Database\Migration;
|
||||||
|
|
||||||
|
class CreateStudentSectionDistributionDrafts extends Migration
|
||||||
|
{
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
if ($this->db->tableExists('student_section_distribution_drafts')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->forge->addField([
|
||||||
|
'id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'auto_increment' => true],
|
||||||
|
'student_id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true],
|
||||||
|
'class_id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true],
|
||||||
|
'class_section_id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true],
|
||||||
|
'school_year' => ['type' => 'VARCHAR', 'constraint' => 20],
|
||||||
|
'previous_school_year' => ['type' => 'VARCHAR', 'constraint' => 20, 'null' => true],
|
||||||
|
'previous_final_score' => ['type' => 'DECIMAL', 'constraint' => '6,2', 'null' => true],
|
||||||
|
'score_group' => ['type' => 'VARCHAR', 'constraint' => 20],
|
||||||
|
'status' => ['type' => 'VARCHAR', 'constraint' => 20, 'default' => 'pending'],
|
||||||
|
'batch_key' => ['type' => 'VARCHAR', 'constraint' => 64],
|
||||||
|
'created_by' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'null' => true],
|
||||||
|
'applied_at' => ['type' => 'DATETIME', 'null' => true],
|
||||||
|
'created_at' => ['type' => 'DATETIME', 'null' => true],
|
||||||
|
'updated_at' => ['type' => 'DATETIME', 'null' => true],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->forge->addKey('id', true);
|
||||||
|
$this->forge->addUniqueKey(['student_id', 'school_year'], 'unique_distribution_draft_student_year');
|
||||||
|
$this->forge->addKey(['class_id', 'school_year', 'status'], false, 'distribution_draft_class_year_status');
|
||||||
|
$this->forge->addKey(['class_section_id', 'school_year'], false, 'distribution_draft_section_year');
|
||||||
|
$this->forge->createTable('student_section_distribution_drafts');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
$this->forge->dropTable('student_section_distribution_drafts', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use CodeIgniter\Model;
|
||||||
|
|
||||||
|
class StudentSectionDistributionDraftModel extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'student_section_distribution_drafts';
|
||||||
|
protected $primaryKey = 'id';
|
||||||
|
protected $returnType = 'array';
|
||||||
|
protected $useAutoIncrement = true;
|
||||||
|
protected $protectFields = true;
|
||||||
|
|
||||||
|
protected $allowedFields = [
|
||||||
|
'student_id',
|
||||||
|
'class_id',
|
||||||
|
'class_section_id',
|
||||||
|
'school_year',
|
||||||
|
'previous_school_year',
|
||||||
|
'previous_final_score',
|
||||||
|
'score_group',
|
||||||
|
'status',
|
||||||
|
'batch_key',
|
||||||
|
'created_by',
|
||||||
|
'applied_at',
|
||||||
|
'created_at',
|
||||||
|
'updated_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $useTimestamps = true;
|
||||||
|
protected $createdField = 'created_at';
|
||||||
|
protected $updatedField = 'updated_at';
|
||||||
|
}
|
||||||
@@ -27,7 +27,6 @@ class UserModel extends Model
|
|||||||
'school_id',
|
'school_id',
|
||||||
'failed_attempts',
|
'failed_attempts',
|
||||||
'last_failed_at',
|
'last_failed_at',
|
||||||
'semester',
|
|
||||||
'status',
|
'status',
|
||||||
'is_suspended',
|
'is_suspended',
|
||||||
'is_verified',
|
'is_verified',
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\App\Controllers\View;
|
||||||
|
|
||||||
|
use App\Controllers\View\ParentController;
|
||||||
|
use CodeIgniter\Test\CIUnitTestCase;
|
||||||
|
use ReflectionMethod;
|
||||||
|
|
||||||
|
final class ParentControllerAgeTest extends CIUnitTestCase
|
||||||
|
{
|
||||||
|
public function testEnrollmentAgeUsesSchoolYearStartYearCutoff(): void
|
||||||
|
{
|
||||||
|
$controller = new class extends ParentController {
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$method = new ReflectionMethod(ParentController::class, 'calculateAgeAsOfSchoolYearStartYear');
|
||||||
|
$method->setAccessible(true);
|
||||||
|
|
||||||
|
$this->assertSame(6, $method->invoke($controller, '2019-09-15', '2025-2026'));
|
||||||
|
$this->assertSame(5, $method->invoke($controller, '2020-01-01', '2025-2026'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEnrollmentAgeRejectsInvalidInputs(): void
|
||||||
|
{
|
||||||
|
$controller = new class extends ParentController {
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$method = new ReflectionMethod(ParentController::class, 'calculateAgeAsOfSchoolYearStartYear');
|
||||||
|
$method->setAccessible(true);
|
||||||
|
|
||||||
|
$this->assertNull($method->invoke($controller, '', '2025-2026'));
|
||||||
|
$this->assertNull($method->invoke($controller, 'not-a-date', '2025-2026'));
|
||||||
|
$this->assertNull($method->invoke($controller, '2026-01-01', '2025-2026'));
|
||||||
|
$this->assertNull($method->invoke($controller, '2019-09-15', ''));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user