895 lines
36 KiB
PHP
895 lines
36 KiB
PHP
<?php
|
|
|
|
namespace App\Controllers\View;
|
|
use App\Controllers\BaseController;
|
|
use App\Models\StudentClassModel;
|
|
use App\Models\UserModel;
|
|
use App\Models\TeacherClassModel;
|
|
use App\Models\StudentModel;
|
|
use App\Models\TeacherModel;
|
|
use App\Models\ClassSectionModel;
|
|
use App\Models\ConfigurationModel;
|
|
use App\Support\Enrollment\EnrollmentEligibility;
|
|
use Config\Database;
|
|
|
|
class AssignmentController extends BaseController
|
|
{
|
|
protected $userModel;
|
|
protected $configModel;
|
|
protected $semester;
|
|
protected $schoolYear;
|
|
protected $studentModel;
|
|
protected $teacherClassModel;
|
|
protected $studentClassModel;
|
|
protected $classSectionModel;
|
|
|
|
public function __construct()
|
|
{
|
|
helper('auth');
|
|
// Load models
|
|
$this->userModel = new UserModel();
|
|
$this->configModel = new ConfigurationModel();
|
|
$this->studentModel = new StudentModel();
|
|
$this->classSectionModel = new ClassSectionModel();
|
|
$this->teacherClassModel = new TeacherClassModel();
|
|
$this->studentClassModel = new StudentClassModel();
|
|
|
|
$this->semester = getSemester();
|
|
$this->schoolYear = $this->configModel->getConfig('school_year');
|
|
|
|
}
|
|
|
|
public function index()
|
|
{
|
|
$data = [
|
|
'classSections' => []
|
|
];
|
|
|
|
// Apply school year filter (default to current config) but avoid semester filtering so the full year is visible
|
|
$selectedSemester = (string)($this->request->getGet('semester') ?? $this->semester ?? '');
|
|
$year = trim((string)($this->request->getGet('schoolYear') ?? $this->request->getGet('school_year') ?? ''));
|
|
if ($year === '') {
|
|
$year = $this->currentSchoolYearName((string)($this->schoolYear ?? ''));
|
|
}
|
|
|
|
$this->applyPendingDistributionDraftsForEnrolledStudents($year);
|
|
$this->repairMissingEnrollmentClassAssignments($year);
|
|
$distributedSectionIds = array_values(array_unique(array_merge(
|
|
$this->distributedClassSectionIds($year),
|
|
$this->enrollmentAssignedClassSectionIds($year)
|
|
)));
|
|
|
|
$classSectionsAll = [];
|
|
if (!empty($distributedSectionIds)) {
|
|
$classSectionsAll = $this->classSectionModel
|
|
->select('id, class_section_id, class_section_name, school_year')
|
|
->where('school_year', $year)
|
|
->whereIn('class_section_id', $distributedSectionIds)
|
|
->orderBy('class_section_name', 'ASC')
|
|
->findAll();
|
|
}
|
|
|
|
$classSectionById = [];
|
|
foreach ($classSectionsAll as $section) {
|
|
$sectionId = (int)($section['class_section_id'] ?? 0);
|
|
if ($sectionId > 0) {
|
|
$classSectionById[$sectionId] = $section;
|
|
}
|
|
}
|
|
|
|
$tcQ = $this->teacherClassModel;
|
|
if ($year !== '') {
|
|
$tcQ = $tcQ->where('school_year', $year);
|
|
}
|
|
$teacherClassesAll = $tcQ->findAll();
|
|
|
|
$scQ = $this->studentClassModel->active();
|
|
if ($year !== '') {
|
|
$scQ = $scQ->where('student_class.school_year', $year);
|
|
}
|
|
$studentClassesAll = $scQ->findAll();
|
|
|
|
// Group teacher and student classes by section
|
|
$teacherBySection = [];
|
|
foreach ($teacherClassesAll as $tc) {
|
|
$teacherBySection[$tc['class_section_id']][] = $tc;
|
|
}
|
|
|
|
$studentsBySection = [];
|
|
foreach ($studentClassesAll as $sc) {
|
|
$studentsBySection[$sc['class_section_id']][] = $sc;
|
|
}
|
|
|
|
$allSectionIds = array_unique(array_merge(array_keys($classSectionById), array_keys($teacherBySection), array_keys($studentsBySection)));
|
|
$distributedSectionSet = array_fill_keys($distributedSectionIds, true);
|
|
|
|
foreach ($allSectionIds as $classSectionId) {
|
|
if (!isset($distributedSectionSet[(int)$classSectionId])) {
|
|
continue;
|
|
}
|
|
|
|
$teacherClasses = $teacherBySection[$classSectionId] ?? [];
|
|
$studentClasses = $studentsBySection[$classSectionId] ?? [];
|
|
|
|
$hasTeacher = !empty($teacherClasses);
|
|
$hasStudents = !empty($studentClasses);
|
|
$hasClassSection = isset($classSectionById[(int)$classSectionId]);
|
|
|
|
if (!$hasClassSection && !$hasTeacher && !$hasStudents) {
|
|
continue;
|
|
}
|
|
|
|
$classSectionName = (string) ($classSectionById[(int)$classSectionId]['class_section_name'] ?? $this->classSectionModel->getClassSectionNameBySectionId($classSectionId) ?? '');
|
|
|
|
$mainTeachers = [];
|
|
$teacherAssistants = [];
|
|
$sectionSemester = '';
|
|
$sectionSchoolYear = '';
|
|
$description = '';
|
|
|
|
if ($hasTeacher) {
|
|
foreach ($teacherClasses as $teacherClass) {
|
|
$teacher = $this->userModel->find($teacherClass['teacher_id']);
|
|
if (!$teacher) continue;
|
|
|
|
$teacherName = trim(($teacher['firstname'] ?? '') . ' ' . ($teacher['lastname'] ?? ''));
|
|
if ($teacherName === '') continue;
|
|
|
|
if (($teacherClass['position'] ?? '') === 'main') {
|
|
$mainTeachers[] = $teacherName;
|
|
} elseif (($teacherClass['position'] ?? '') === 'ta') {
|
|
$teacherAssistants[] = $teacherName;
|
|
}
|
|
|
|
if ($sectionSemester === '' && !empty($teacherClass['semester'])) {
|
|
$sectionSemester = (string)$teacherClass['semester'];
|
|
}
|
|
if ($sectionSchoolYear === '' && !empty($teacherClass['school_year'])) {
|
|
$sectionSchoolYear = (string)$teacherClass['school_year'];
|
|
}
|
|
if ($description === '' && !empty($teacherClass['description'])) {
|
|
$description = (string)$teacherClass['description'];
|
|
}
|
|
}
|
|
}
|
|
|
|
$students = [];
|
|
$seenStudentIds = [];
|
|
foreach ($studentClasses as $studentClass) {
|
|
$sid = (int)($studentClass['student_id'] ?? 0);
|
|
if ($sid <= 0 || isset($seenStudentIds[$sid])) {
|
|
continue;
|
|
}
|
|
if ($sectionSemester === '' && !empty($studentClass['semester'])) {
|
|
$sectionSemester = (string)$studentClass['semester'];
|
|
}
|
|
if ($sectionSchoolYear === '' && !empty($studentClass['school_year'])) {
|
|
$sectionSchoolYear = (string)$studentClass['school_year'];
|
|
}
|
|
if ($description === '' && !empty($studentClass['description'])) {
|
|
$description = (string)$studentClass['description'];
|
|
}
|
|
|
|
$student = $this->studentModel
|
|
->where('id', $studentClass['student_id'])
|
|
->where('is_active', 1)
|
|
->first();
|
|
if (!$student) {
|
|
continue;
|
|
}
|
|
|
|
$calculatedAge = EnrollmentEligibility::ageOnSeptemberFirst($student['dob'] ?? null, $year);
|
|
|
|
$students[] = [
|
|
'id' => (int)$student['id'],
|
|
'firstname' => esc($student['firstname']),
|
|
'lastname' => esc($student['lastname']),
|
|
'age' => esc((string)($calculatedAge ?? ($student['age'] ?? ''))),
|
|
'gender' => esc($student['gender']),
|
|
'registration_grade' => esc($student['registration_grade']),
|
|
'photo_consent' => esc($student['photo_consent'] ? 'Yes' : 'No'),
|
|
'tuition_paid' => esc($student['tuition_paid'] ? 'Yes' : 'No'),
|
|
'school_id' => esc($student['school_id']),
|
|
];
|
|
$seenStudentIds[$sid] = true;
|
|
}
|
|
|
|
$sectionSemesterDisplay = $sectionSemester !== '' ? $sectionSemester : ((string)($this->semester ?? ''));
|
|
$sectionSchoolYearDisplay = $sectionSchoolYear !== '' ? $sectionSchoolYear : ((string)($this->schoolYear ?? ''));
|
|
$data['classSections'][] = [
|
|
'class_section_id' => $classSectionId,
|
|
'class_section_name' => $classSectionName,
|
|
'main_teachers' => $mainTeachers,
|
|
'teacher_assistants' => $teacherAssistants,
|
|
'students' => $students,
|
|
'semester' => $sectionSemesterDisplay,
|
|
'school_year' => $sectionSchoolYearDisplay,
|
|
'description' => $description,
|
|
];
|
|
}
|
|
|
|
$schoolYearsList = [];
|
|
try {
|
|
$db = Database::connect();
|
|
$yearsQuery = $db->table('classSection')
|
|
->select('DISTINCT school_year', false)
|
|
->where('school_year IS NOT NULL', null, false)
|
|
->orderBy('school_year', 'DESC')
|
|
->get()
|
|
->getResultArray();
|
|
$studentYearsQuery = $db->table('student_class')
|
|
->select('DISTINCT school_year', false)
|
|
->where('school_year IS NOT NULL', null, false)
|
|
->orderBy('school_year', 'DESC')
|
|
->get()
|
|
->getResultArray();
|
|
$yearsQuery = array_merge($yearsQuery, $studentYearsQuery);
|
|
foreach ($yearsQuery as $row) {
|
|
$val = (string)($row['school_year'] ?? '');
|
|
if ($val !== '' && !in_array($val, $schoolYearsList, true)) {
|
|
$schoolYearsList[] = $val;
|
|
}
|
|
}
|
|
} catch (\Throwable $e) {
|
|
// ignore fallback below
|
|
}
|
|
if ($this->schoolYear !== null && $this->schoolYear !== '' && !in_array((string)$this->schoolYear, $schoolYearsList, true)) {
|
|
array_unshift($schoolYearsList, (string)$this->schoolYear);
|
|
}
|
|
|
|
// Sort sections
|
|
usort($data['classSections'], fn($a, $b) => strnatcasecmp((string) $a['class_section_name'], (string) $b['class_section_name']));
|
|
|
|
$data['schoolYears'] = $schoolYearsList;
|
|
$data['schoolYear'] = $year;
|
|
$data['selectedYear'] = $year;
|
|
$data['selectedSemester'] = $selectedSemester;
|
|
|
|
return view('administrator/class_assignment', $data);
|
|
}
|
|
|
|
private function distributedClassSectionIds(string $year): array
|
|
{
|
|
$year = trim($year);
|
|
if ($year === '') {
|
|
return [];
|
|
}
|
|
|
|
try {
|
|
$db = Database::connect();
|
|
if (! $db->tableExists('student_section_distribution_drafts')) {
|
|
return [];
|
|
}
|
|
|
|
$baseRows = $db->table('classSection')
|
|
->select('class_id, class_section_id, class_section_name')
|
|
->where('school_year', $year)
|
|
->where("class_section_name NOT LIKE '%-%'", null, false)
|
|
->orderBy('class_id', 'ASC')
|
|
->get()
|
|
->getResultArray();
|
|
|
|
$draftRows = $db->table('student_section_distribution_drafts d')
|
|
->select('d.class_id, d.class_section_id, COALESCE(cs.class_section_name, d.class_section_id) AS class_section_name', false)
|
|
->join(
|
|
'classSection cs',
|
|
'cs.class_section_id = d.class_section_id AND cs.school_year = d.school_year',
|
|
'left',
|
|
false
|
|
)
|
|
->where('d.school_year', $year)
|
|
->where('d.class_section_id >', 0)
|
|
->whereIn('status', ['pending', 'applied'])
|
|
->get()
|
|
->getResultArray();
|
|
|
|
$draftsByClassId = [];
|
|
foreach ($draftRows as $row) {
|
|
$classId = (int)($row['class_id'] ?? 0);
|
|
$sectionId = (int)($row['class_section_id'] ?? 0);
|
|
if ($classId <= 0 || $sectionId <= 0) {
|
|
continue;
|
|
}
|
|
|
|
$draftsByClassId[$classId][$sectionId] = [
|
|
'class_section_id' => $sectionId,
|
|
'class_section_name' => (string)($row['class_section_name'] ?? ''),
|
|
];
|
|
}
|
|
|
|
$allowed = [];
|
|
$seenClassIds = [];
|
|
foreach ($baseRows as $row) {
|
|
$classId = (int)($row['class_id'] ?? 0);
|
|
$baseSectionId = (int)($row['class_section_id'] ?? 0);
|
|
$baseName = trim((string)($row['class_section_name'] ?? ''));
|
|
if ($classId <= 0 || $baseSectionId <= 0 || $baseName === '') {
|
|
continue;
|
|
}
|
|
|
|
$normalized = strtolower($baseName);
|
|
$isAutoDistributeBase = $normalized === 'youth'
|
|
|| (ctype_digit($normalized) && (int)$normalized >= 1 && (int)$normalized <= 10);
|
|
if (!$isAutoDistributeBase) {
|
|
continue;
|
|
}
|
|
|
|
$seenClassIds[$classId] = true;
|
|
$classDrafts = $draftsByClassId[$classId] ?? [];
|
|
$hasBaseDraft = false;
|
|
$letteredDraftIds = [];
|
|
foreach ($classDrafts as $draft) {
|
|
$draftSectionId = (int)($draft['class_section_id'] ?? 0);
|
|
$draftName = trim((string)($draft['class_section_name'] ?? ''));
|
|
if ($draftSectionId === $baseSectionId || $draftName === '' || strpos($draftName, '-') === false) {
|
|
$hasBaseDraft = true;
|
|
continue;
|
|
}
|
|
|
|
$letteredDraftIds[] = $draftSectionId;
|
|
}
|
|
|
|
if ($hasBaseDraft || empty($letteredDraftIds)) {
|
|
$allowed[] = $baseSectionId;
|
|
continue;
|
|
}
|
|
|
|
foreach ($letteredDraftIds as $draftSectionId) {
|
|
$allowed[] = $draftSectionId;
|
|
}
|
|
}
|
|
|
|
foreach ($draftsByClassId as $classId => $classDrafts) {
|
|
if (isset($seenClassIds[$classId])) {
|
|
continue;
|
|
}
|
|
|
|
foreach ($classDrafts as $draft) {
|
|
$sectionId = (int)($draft['class_section_id'] ?? 0);
|
|
if ($sectionId > 0) {
|
|
$allowed[] = $sectionId;
|
|
}
|
|
}
|
|
}
|
|
|
|
return array_values(array_unique(array_filter(
|
|
$allowed,
|
|
static fn(int $sectionId): bool => $sectionId > 0
|
|
)));
|
|
} catch (\Throwable $e) {
|
|
log_message('error', 'distributedClassSectionIds failed: ' . $e->getMessage());
|
|
return [];
|
|
}
|
|
}
|
|
|
|
private function enrollmentAssignedClassSectionIds(string $year): array
|
|
{
|
|
$year = trim($year);
|
|
if ($year === '') {
|
|
return [];
|
|
}
|
|
|
|
try {
|
|
$db = Database::connect();
|
|
$allowedStatuses = ['admission under review', 'review & decision', 'payment pending', 'enrolled'];
|
|
$ids = [];
|
|
|
|
if ($db->tableExists('enrollments')) {
|
|
$builder = $db->table('enrollments e')
|
|
->select('e.class_section_id')
|
|
->join('students s', 's.id = e.student_id', 'inner')
|
|
->where('e.school_year', $year)
|
|
->whereIn('e.enrollment_status', $allowedStatuses)
|
|
->where('e.class_section_id IS NOT NULL', null, false)
|
|
->where('e.class_section_id >', 0);
|
|
|
|
if ($db->fieldExists('is_active', 'students')) {
|
|
$builder->where('s.is_active', 1);
|
|
}
|
|
if ($db->fieldExists('is_withdrawn', 'enrollments')) {
|
|
$builder->groupStart()
|
|
->where('e.is_withdrawn', 0)
|
|
->orWhere('e.is_withdrawn', null)
|
|
->groupEnd();
|
|
}
|
|
|
|
foreach ($builder->groupBy('e.class_section_id')->get()->getResultArray() as $row) {
|
|
$sectionId = (int)($row['class_section_id'] ?? 0);
|
|
if ($sectionId > 0) {
|
|
$ids[] = $sectionId;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($db->tableExists('student_class') && $db->tableExists('enrollments')) {
|
|
$builder = $db->table('student_class sc')
|
|
->select('sc.class_section_id')
|
|
->join(
|
|
'enrollments e',
|
|
'e.student_id = sc.student_id AND e.school_year = sc.school_year',
|
|
'inner',
|
|
false
|
|
)
|
|
->join('students s', 's.id = sc.student_id', 'inner')
|
|
->where('sc.school_year', $year)
|
|
->whereIn('e.enrollment_status', $allowedStatuses)
|
|
->where('sc.class_section_id IS NOT NULL', null, false)
|
|
->where('sc.class_section_id >', 0);
|
|
|
|
if ($db->fieldExists('is_active', 'students')) {
|
|
$builder->where('s.is_active', 1);
|
|
}
|
|
if ($db->fieldExists('is_event_only', 'student_class')) {
|
|
$builder->groupStart()
|
|
->where('sc.is_event_only', 0)
|
|
->orWhere('sc.is_event_only', null)
|
|
->groupEnd();
|
|
}
|
|
if ($db->fieldExists('is_withdrawn', 'enrollments')) {
|
|
$builder->groupStart()
|
|
->where('e.is_withdrawn', 0)
|
|
->orWhere('e.is_withdrawn', null)
|
|
->groupEnd();
|
|
}
|
|
|
|
foreach ($builder->groupBy('sc.class_section_id')->get()->getResultArray() as $row) {
|
|
$sectionId = (int)($row['class_section_id'] ?? 0);
|
|
if ($sectionId > 0) {
|
|
$ids[] = $sectionId;
|
|
}
|
|
}
|
|
}
|
|
|
|
return array_values(array_unique($ids));
|
|
} catch (\Throwable $e) {
|
|
log_message('error', 'enrollmentAssignedClassSectionIds failed: ' . $e->getMessage());
|
|
return [];
|
|
}
|
|
}
|
|
|
|
private function applyPendingDistributionDraftsForEnrolledStudents(string $year): void
|
|
{
|
|
if ($year === '') {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
$db = Database::connect();
|
|
if (! $db->tableExists('student_section_distribution_drafts')) {
|
|
return;
|
|
}
|
|
|
|
$rows = $db->table('student_section_distribution_drafts d')
|
|
->select('d.id, d.student_id, d.class_section_id')
|
|
->join(
|
|
'enrollments e',
|
|
'e.student_id = d.student_id AND e.school_year = d.school_year',
|
|
'inner'
|
|
)
|
|
->where('d.school_year', $year)
|
|
->where('d.status', 'pending')
|
|
->whereIn('e.enrollment_status', ['admission under review', 'payment pending', 'enrolled'])
|
|
->groupBy('d.id, d.student_id, d.class_section_id')
|
|
->get()
|
|
->getResultArray();
|
|
|
|
if (empty($rows)) {
|
|
return;
|
|
}
|
|
|
|
$now = utc_now();
|
|
$updatedBy = (int)(session()->get('user_id') ?? 0) ?: null;
|
|
|
|
$db->transStart();
|
|
foreach ($rows as $row) {
|
|
$studentId = (int)($row['student_id'] ?? 0);
|
|
$sectionId = (int)($row['class_section_id'] ?? 0);
|
|
if ($studentId <= 0 || $sectionId <= 0) {
|
|
continue;
|
|
}
|
|
|
|
$existing = $db->table('student_class')
|
|
->select('id')
|
|
->where('student_id', $studentId)
|
|
->where('school_year', $year)
|
|
->get()
|
|
->getRowArray();
|
|
|
|
$payload = [
|
|
'student_id' => $studentId,
|
|
'class_section_id' => $sectionId,
|
|
'school_year' => $year,
|
|
'updated_by' => $updatedBy,
|
|
'updated_at' => $now,
|
|
];
|
|
|
|
if ($existing) {
|
|
$db->table('student_class')
|
|
->where('id', (int)$existing['id'])
|
|
->update($payload);
|
|
} else {
|
|
$payload['created_at'] = $now;
|
|
$db->table('student_class')->insert($payload);
|
|
}
|
|
|
|
$db->table('enrollments')
|
|
->where('student_id', $studentId)
|
|
->where('school_year', $year)
|
|
->whereIn('enrollment_status', ['admission under review', 'payment pending', 'enrolled'])
|
|
->update([
|
|
'class_section_id' => $sectionId,
|
|
'assigned_class_section_id' => $sectionId,
|
|
'placement_status' => 'automatic_distribution_applied',
|
|
'updated_at' => $now,
|
|
]);
|
|
|
|
$db->table('student_section_distribution_drafts')
|
|
->where('id', (int)$row['id'])
|
|
->update([
|
|
'status' => 'applied',
|
|
'applied_at' => $now,
|
|
'updated_at' => $now,
|
|
]);
|
|
}
|
|
$db->transComplete();
|
|
} catch (\Throwable $e) {
|
|
log_message('error', 'applyPendingDistributionDraftsForEnrolledStudents failed: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
private function repairMissingEnrollmentClassAssignments(string $year): void
|
|
{
|
|
if ($year === '') {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
$db = Database::connect();
|
|
$this->ensureClassSectionsForYear($db, $year);
|
|
if (! $db->tableExists('enrollments') || ! $db->tableExists('student_class')) {
|
|
return;
|
|
}
|
|
|
|
$previousYear = $this->previousSchoolYearName($year);
|
|
if ($previousYear === null) {
|
|
return;
|
|
}
|
|
|
|
$rows = $db->table('enrollments e')
|
|
->select('e.student_id, e.school_year, e.enrollment_status, e.class_section_id, sc.id AS student_class_id')
|
|
->join('student_class sc', 'sc.student_id = e.student_id AND sc.school_year = e.school_year', 'left')
|
|
->where('e.school_year', $year)
|
|
->whereIn('e.enrollment_status', ['admission under review', 'payment pending', 'enrolled'])
|
|
->groupStart()
|
|
->where('e.class_section_id', null)
|
|
->orWhere('e.class_section_id', 0)
|
|
->orWhere('sc.id', null)
|
|
->groupEnd()
|
|
->groupBy('e.student_id, e.school_year, e.enrollment_status, e.class_section_id, sc.id')
|
|
->get()
|
|
->getResultArray();
|
|
|
|
foreach ($rows as $row) {
|
|
$studentId = (int)($row['student_id'] ?? 0);
|
|
if ($studentId <= 0) {
|
|
continue;
|
|
}
|
|
|
|
$evaluation = service('enrollmentTransition')->evaluate($studentId, $previousYear, $year, 'admin');
|
|
if (!($evaluation['academic_eligible'] ?? false) || ($evaluation['blockers'] ?? []) !== []) {
|
|
continue;
|
|
}
|
|
|
|
$targetSectionId = (int)($evaluation['assigned_class_section_id'] ?? 0);
|
|
if ($targetSectionId <= 0 && (int)($evaluation['assigned_grade_id'] ?? 0) > 0) {
|
|
$base = $this->baseSectionForClassYear((int)$evaluation['assigned_grade_id'], $year);
|
|
$targetSectionId = (int)($base['class_section_id'] ?? 0);
|
|
}
|
|
|
|
if ($targetSectionId <= 0) {
|
|
continue;
|
|
}
|
|
|
|
$placementStatus = match ((string)($evaluation['placement_status'] ?? '')) {
|
|
'automatic_distribution_pending' => 'base_section_pending_distribution',
|
|
'same_class_assigned', 'temporary_same_grade' => (string)$evaluation['placement_status'],
|
|
default => 'manual_class_assigned',
|
|
};
|
|
|
|
$existing = $db->table('student_class')
|
|
->select('id')
|
|
->where('student_id', $studentId)
|
|
->where('school_year', $year)
|
|
->orderBy('id', 'DESC')
|
|
->limit(1)
|
|
->get()
|
|
->getRowArray();
|
|
|
|
$studentClassPayload = [
|
|
'student_id' => $studentId,
|
|
'class_section_id' => $targetSectionId,
|
|
'school_year' => $year,
|
|
'updated_by' => (int)(session()->get('user_id') ?? 0) ?: null,
|
|
'updated_at' => utc_now(),
|
|
];
|
|
|
|
if ($existing !== null) {
|
|
$db->table('student_class')->where('id', (int)$existing['id'])->update($studentClassPayload);
|
|
} else {
|
|
$studentClassPayload['created_at'] = utc_now();
|
|
$db->table('student_class')->insert($studentClassPayload);
|
|
}
|
|
|
|
$db->table('enrollments')
|
|
->where('student_id', $studentId)
|
|
->where('school_year', $year)
|
|
->whereIn('enrollment_status', ['admission under review', 'payment pending', 'enrolled'])
|
|
->update([
|
|
'class_section_id' => $targetSectionId,
|
|
'assigned_class_section_id' => $targetSectionId,
|
|
'placement_status' => $placementStatus,
|
|
'updated_at' => utc_now(),
|
|
]);
|
|
}
|
|
} catch (\Throwable $e) {
|
|
log_message('error', 'repairMissingEnrollmentClassAssignments failed: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
private function baseSectionForClassYear(int $classId, string $schoolYear): ?array
|
|
{
|
|
$db = Database::connect();
|
|
$this->ensureClassSectionsForYear($db, $schoolYear);
|
|
if ($classId <= 0 || $schoolYear === '' || ! $db->tableExists('classSection')) {
|
|
return null;
|
|
}
|
|
|
|
$builder = $db->table('classSection')
|
|
->select('class_section_id, class_section_name, class_id')
|
|
->where('class_id', $classId)
|
|
->where("class_section_name NOT LIKE '%-%'", null, false)
|
|
->orderBy('id', 'ASC')
|
|
->limit(1);
|
|
|
|
if ($db->fieldExists('school_year', 'classSection')) {
|
|
$builder->where('school_year', $schoolYear);
|
|
}
|
|
|
|
$row = $builder->get()->getRowArray();
|
|
|
|
return $row !== null && (int)($row['class_section_id'] ?? 0) > 0 ? $row : null;
|
|
}
|
|
|
|
private function ensureClassSectionsForYear($db, string $targetSchoolYear): void
|
|
{
|
|
$targetSchoolYear = trim($targetSchoolYear);
|
|
if ($targetSchoolYear === '' || ! $db->tableExists('classSection') || ! $db->fieldExists('school_year', 'classSection')) {
|
|
return;
|
|
}
|
|
|
|
if ($db->table('classSection')->where('school_year', $targetSchoolYear)->countAllResults() > 0) {
|
|
return;
|
|
}
|
|
|
|
$sourceSchoolYear = $this->previousSchoolYearName($targetSchoolYear);
|
|
if ($sourceSchoolYear === null) {
|
|
return;
|
|
}
|
|
|
|
$sourceRows = $db->table('classSection')
|
|
->select('class_id, class_section_id, class_section_name')
|
|
->where('school_year', $sourceSchoolYear)
|
|
->orderBy('id', 'ASC')
|
|
->get()
|
|
->getResultArray();
|
|
|
|
$now = utc_now();
|
|
foreach ($sourceRows as $row) {
|
|
$classSectionId = (int)($row['class_section_id'] ?? 0);
|
|
if ($classSectionId <= 0) {
|
|
continue;
|
|
}
|
|
|
|
$exists = $db->table('classSection')
|
|
->where('school_year', $targetSchoolYear)
|
|
->where('class_section_id', $classSectionId)
|
|
->countAllResults();
|
|
if ($exists > 0) {
|
|
continue;
|
|
}
|
|
|
|
$db->table('classSection')->insert([
|
|
'class_id' => (int)($row['class_id'] ?? 0),
|
|
'class_section_id' => $classSectionId,
|
|
'class_section_name' => (string)($row['class_section_name'] ?? ''),
|
|
'school_year' => $targetSchoolYear,
|
|
'created_at' => $now,
|
|
'updated_at' => $now,
|
|
]);
|
|
}
|
|
}
|
|
|
|
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 save()
|
|
{
|
|
|
|
$data = [
|
|
'student_id' => $this->request->getPost('student_id'),
|
|
'class_section_id' => $this->request->getPost('class_section_id'),
|
|
'semester' => $this->request->getPost('semester'),
|
|
'school_year' => $this->request->getPost('school_year'),
|
|
'description' => $this->request->getPost('description'),
|
|
'updated_by' => session()->get('user_id'),
|
|
];
|
|
|
|
$this->studentClassModel->save($data);
|
|
|
|
return redirect()->to('/assignments')->with('message', 'Assignment saved successfully');
|
|
}
|
|
|
|
// API: JSON payload for Classes List page
|
|
public function classAssignmentData()
|
|
{
|
|
$year = trim((string)($this->request->getGet('schoolYear') ?? $this->request->getGet('school_year') ?? ''));
|
|
if ($year === '') {
|
|
$year = $this->currentSchoolYearName((string)($this->schoolYear ?? ''));
|
|
}
|
|
$this->applyPendingDistributionDraftsForEnrolledStudents($year);
|
|
$this->repairMissingEnrollmentClassAssignments($year);
|
|
$distributedSectionIds = array_values(array_unique(array_merge(
|
|
$this->distributedClassSectionIds($year),
|
|
$this->enrollmentAssignedClassSectionIds($year)
|
|
)));
|
|
|
|
$classSectionsAll = [];
|
|
if (!empty($distributedSectionIds)) {
|
|
$classSectionsAll = $this->classSectionModel
|
|
->select('id, class_section_id, class_section_name, school_year')
|
|
->where('school_year', $year)
|
|
->whereIn('class_section_id', $distributedSectionIds)
|
|
->orderBy('class_section_name', 'ASC')
|
|
->findAll();
|
|
}
|
|
|
|
$classSectionById = [];
|
|
foreach ($classSectionsAll as $section) {
|
|
$sectionId = (int)($section['class_section_id'] ?? 0);
|
|
if ($sectionId > 0) {
|
|
$classSectionById[$sectionId] = $section;
|
|
}
|
|
}
|
|
|
|
$tcQ = $this->teacherClassModel;
|
|
if ($year !== '') {
|
|
$tcQ = $tcQ->where('school_year', $year);
|
|
}
|
|
$teacherClassesAll = $tcQ->findAll();
|
|
|
|
$scQ = $this->studentClassModel->active();
|
|
if ($year !== '') {
|
|
$scQ = $scQ->where('student_class.school_year', $year);
|
|
}
|
|
$studentClassesAll = $scQ->findAll();
|
|
|
|
// Group by section
|
|
$teacherBySection = [];
|
|
foreach ($teacherClassesAll as $tc) {
|
|
$secId = (int)($tc['class_section_id'] ?? 0);
|
|
if ($secId) $teacherBySection[$secId][] = $tc;
|
|
}
|
|
$studentsBySection = [];
|
|
foreach ($studentClassesAll as $sc) {
|
|
$secId = (int)($sc['class_section_id'] ?? 0);
|
|
if ($secId) $studentsBySection[$secId][] = $sc;
|
|
}
|
|
|
|
$allSectionIds = array_values(array_unique(array_merge(array_keys($classSectionById), array_keys($teacherBySection), array_keys($studentsBySection))));
|
|
$distributedSectionSet = array_fill_keys($distributedSectionIds, true);
|
|
|
|
$classSections = [];
|
|
|
|
foreach ($allSectionIds as $classSectionId) {
|
|
if (!isset($distributedSectionSet[(int)$classSectionId])) {
|
|
continue;
|
|
}
|
|
|
|
$hasTeacher = !empty($teacherBySection[$classSectionId]);
|
|
$hasStudents = !empty($studentsBySection[$classSectionId]);
|
|
$hasClassSection = isset($classSectionById[(int)$classSectionId]);
|
|
if (!$hasClassSection && !$hasTeacher && !$hasStudents) continue;
|
|
|
|
$classSectionName = (string) ($classSectionById[(int)$classSectionId]['class_section_name'] ?? $this->classSectionModel->getClassSectionNameBySectionId($classSectionId) ?? '');
|
|
|
|
$mainTeachers = [];
|
|
$teacherAssistants = [];
|
|
$semesterMeta = '';
|
|
$schoolYearMeta = '';
|
|
$descriptionMeta = '';
|
|
|
|
if ($hasTeacher) {
|
|
foreach ($teacherBySection[$classSectionId] as $teacherClass) {
|
|
$teacher = $this->userModel->find((int)$teacherClass['teacher_id']);
|
|
if ($teacher) {
|
|
$tname = trim(($teacher['firstname'] ?? '') . ' ' . ($teacher['lastname'] ?? ''));
|
|
if (($teacherClass['position'] ?? '') === 'main') {
|
|
$mainTeachers[] = $tname;
|
|
} elseif (($teacherClass['position'] ?? '') === 'ta') {
|
|
$teacherAssistants[] = $tname;
|
|
}
|
|
}
|
|
// assign meta (same for all rows in a section)
|
|
if ($semesterMeta === '' && !empty($teacherClass['semester'])) {
|
|
$semesterMeta = (string)$teacherClass['semester'];
|
|
}
|
|
if ($schoolYearMeta === '' && !empty($teacherClass['school_year'])) {
|
|
$schoolYearMeta = (string)$teacherClass['school_year'];
|
|
}
|
|
if ($descriptionMeta === '' && !empty($teacherClass['description'])) {
|
|
$descriptionMeta = (string)$teacherClass['description'];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Load students for the section
|
|
$students = [];
|
|
$seenStudentIds = [];
|
|
foreach ($studentsBySection[$classSectionId] ?? [] as $studentClass) {
|
|
$sid = (int)($studentClass['student_id'] ?? 0);
|
|
if ($sid <= 0 || isset($seenStudentIds[$sid])) {
|
|
continue;
|
|
}
|
|
$stu = $this->studentModel
|
|
->where('id', $sid)
|
|
->where('is_active', 1)
|
|
->first();
|
|
if (!$stu) continue;
|
|
|
|
$students[] = [
|
|
'id' => (int)$stu['id'],
|
|
'firstname' => (string)($stu['firstname'] ?? ''),
|
|
'lastname' => (string)($stu['lastname'] ?? ''),
|
|
'age' => $stu['age'] ?? null,
|
|
'gender' => (string)($stu['gender'] ?? ''),
|
|
'registration_grade' => (string)($stu['registration_grade'] ?? ''),
|
|
'photo_consent' => (bool)($stu['photo_consent'] ?? false),
|
|
'tuition_paid' => (bool)($stu['tuition_paid'] ?? false),
|
|
'school_id' => (string)($stu['school_id'] ?? ''),
|
|
];
|
|
$seenStudentIds[$sid] = true;
|
|
}
|
|
|
|
$classSections[] = [
|
|
'class_section_id' => (int)$classSectionId,
|
|
'class_section_name' => $classSectionName,
|
|
'main_teachers' => array_values(array_unique($mainTeachers)),
|
|
'teacher_assistants' => array_values(array_unique($teacherAssistants)),
|
|
'students' => $students,
|
|
'semester' => $semesterMeta ?: (string)$this->semester,
|
|
'school_year' => $schoolYearMeta ?: (string)$this->schoolYear,
|
|
'description' => $descriptionMeta,
|
|
];
|
|
}
|
|
|
|
// Sort by class_section_name
|
|
usort($classSections, fn($a, $b) => strnatcasecmp((string)($a['class_section_name'] ?? ''), (string)($b['class_section_name'] ?? '')));
|
|
|
|
return $this->response->setJSON([
|
|
'classSections' => $classSections,
|
|
'csrfHash' => csrf_hash(),
|
|
'semester' => (string)$this->semester,
|
|
'school_year' => (string)$this->schoolYear,
|
|
]);
|
|
}
|
|
}
|