@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Concerns;
|
||||
|
||||
use App\Support\SchoolYear\SchoolYearContext;
|
||||
use CodeIgniter\Model;
|
||||
|
||||
trait SchoolYearScopedModelTrait
|
||||
{
|
||||
public function forSchoolYear(SchoolYearContext|string|int $schoolYear): Model
|
||||
{
|
||||
if ($schoolYear instanceof SchoolYearContext) {
|
||||
if ($this->fieldExists('school_year_id')) {
|
||||
return $this->where($this->table . '.school_year_id', $schoolYear->id());
|
||||
}
|
||||
|
||||
return $this->where($this->table . '.school_year', $schoolYear->yearName());
|
||||
}
|
||||
|
||||
if (is_int($schoolYear)) {
|
||||
return $this->where($this->table . '.school_year_id', $schoolYear);
|
||||
}
|
||||
|
||||
return $this->where($this->table . '.school_year', $schoolYear);
|
||||
}
|
||||
|
||||
private function fieldExists(string $field): bool
|
||||
{
|
||||
return in_array($field, $this->db->getFieldNames($this->table), true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class SchoolYearClosingBatchModel extends Model
|
||||
{
|
||||
protected $table = 'school_year_closing_batches';
|
||||
protected $primaryKey = 'id';
|
||||
protected $returnType = 'array';
|
||||
protected $useTimestamps = true;
|
||||
|
||||
protected $allowedFields = [
|
||||
'source_school_year_id',
|
||||
'target_school_year_id',
|
||||
'status',
|
||||
'preview_hash',
|
||||
'total_families',
|
||||
'total_positive_balance',
|
||||
'total_credit_balance',
|
||||
'started_by',
|
||||
'completed_by',
|
||||
'started_at',
|
||||
'completed_at',
|
||||
'failed_at',
|
||||
'failure_message',
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class SchoolYearClosingItemModel extends Model
|
||||
{
|
||||
protected $table = 'school_year_closing_items';
|
||||
protected $primaryKey = 'id';
|
||||
protected $returnType = 'array';
|
||||
protected $useTimestamps = true;
|
||||
|
||||
protected $allowedFields = [
|
||||
'closing_batch_id',
|
||||
'family_id',
|
||||
'source_balance',
|
||||
'credit_amount',
|
||||
'adjustment_amount',
|
||||
'carry_forward_amount',
|
||||
'target_invoice_id',
|
||||
'target_adjustment_id',
|
||||
'status',
|
||||
'error_message',
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class SchoolYearModel extends Model
|
||||
{
|
||||
protected $table = 'school_years';
|
||||
protected $primaryKey = 'id';
|
||||
protected $returnType = 'array';
|
||||
protected $useTimestamps = true;
|
||||
|
||||
protected $allowedFields = [
|
||||
'name',
|
||||
'status',
|
||||
'starts_on',
|
||||
'ends_on',
|
||||
'description',
|
||||
'registration_starts_on',
|
||||
'registration_ends_on',
|
||||
'previous_school_year_id',
|
||||
'next_school_year_id',
|
||||
'activated_at',
|
||||
'closing_started_at',
|
||||
'closed_at',
|
||||
'archived_at',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
];
|
||||
|
||||
protected $validationRules = [
|
||||
'name' => 'required|regex_match[/^\d{4}-\d{4}$/]|max_length[9]',
|
||||
'status' => 'required|in_list[draft,active,closing,closed,archived]',
|
||||
'starts_on' => 'permit_empty|valid_date[Y-m-d]',
|
||||
'ends_on' => 'permit_empty|valid_date[Y-m-d]',
|
||||
'registration_starts_on' => 'permit_empty|valid_date[Y-m-d]',
|
||||
'registration_ends_on' => 'permit_empty|valid_date[Y-m-d]',
|
||||
];
|
||||
|
||||
public function active(): ?array
|
||||
{
|
||||
return $this->where('status', 'active')
|
||||
->orderBy('id', 'DESC')
|
||||
->first();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class SchoolYearTransitionLogModel extends Model
|
||||
{
|
||||
protected $table = 'school_year_transition_logs';
|
||||
protected $primaryKey = 'id';
|
||||
protected $returnType = 'array';
|
||||
protected $useTimestamps = false;
|
||||
|
||||
protected $allowedFields = [
|
||||
'school_year_id',
|
||||
'from_status',
|
||||
'to_status',
|
||||
'action',
|
||||
'performed_by',
|
||||
'metadata_json',
|
||||
'created_at',
|
||||
];
|
||||
}
|
||||
+434
-133
@@ -2,12 +2,17 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Database\BaseBuilder;
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class StudentClassModel extends Model
|
||||
{
|
||||
protected $table = 'student_class';
|
||||
protected $primaryKey = 'id';
|
||||
protected $returnType = 'array';
|
||||
|
||||
protected $useAutoIncrement = true;
|
||||
protected $protectFields = true;
|
||||
|
||||
protected $allowedFields = [
|
||||
'student_id',
|
||||
@@ -16,214 +21,510 @@ class StudentClassModel extends Model
|
||||
'is_event_only',
|
||||
'description',
|
||||
'updated_by',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'created_at'
|
||||
];
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
|
||||
protected $validationRules = [
|
||||
'student_id' => 'required|integer',
|
||||
'class_section_id'=> 'permit_empty|integer',
|
||||
'school_year' => 'required|max_length[20]',
|
||||
'is_event_only' => 'permit_empty|in_list[0,1]',
|
||||
'updated_by' => 'permit_empty|integer',
|
||||
];
|
||||
|
||||
protected $validationMessages = [];
|
||||
protected $skipValidation = false;
|
||||
protected $cleanValidationRules = true;
|
||||
|
||||
/**
|
||||
* Scope: only students active for classes/attendance.
|
||||
* Create a fresh builder for student_class.
|
||||
*
|
||||
* Using a fresh builder prevents WHERE conditions from a previous model
|
||||
* query from leaking into another query.
|
||||
*/
|
||||
public function active(): self
|
||||
private function freshBuilder(): BaseBuilder
|
||||
{
|
||||
return $this->select('student_class.*')
|
||||
->join('students', 'students.id = student_class.student_id', 'inner')
|
||||
return $this->db->table($this->table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a fresh builder scoped to active students.
|
||||
*/
|
||||
private function activeStudentsBuilder(): BaseBuilder
|
||||
{
|
||||
return $this->freshBuilder()
|
||||
->select('student_class.*')
|
||||
->join(
|
||||
'students',
|
||||
'students.id = student_class.student_id',
|
||||
'inner'
|
||||
)
|
||||
->where('students.is_active', 1);
|
||||
}
|
||||
|
||||
public function getClassSectionNameByStudentId(int $studentId): ?string
|
||||
/**
|
||||
* Apply the active-student scope to the model.
|
||||
*
|
||||
* Prefer the dedicated retrieval methods below for new code because they
|
||||
* use fresh builders and cannot inherit stale model query state.
|
||||
*/
|
||||
public function active(): self
|
||||
{
|
||||
return $this->db->table($this->table)
|
||||
->select('cs.class_section_name')
|
||||
->join('classSection cs', 'cs.class_section_id = student_class.class_section_id')
|
||||
->where('student_class.student_id', $studentId)
|
||||
->get()
|
||||
->getRow('class_section_name');
|
||||
}
|
||||
|
||||
|
||||
// Custom findAll() method
|
||||
public function findAll($limit = 0, $offset = 0)
|
||||
{
|
||||
// Optional: Add custom logic before fetching all records
|
||||
|
||||
// Then call the parent findAll method to fetch the records
|
||||
return parent::findAll($limit, $offset);
|
||||
}
|
||||
|
||||
// Method to get all students in a specific class section (optionally scoped to term)
|
||||
public function getClassStudents($classSectionId, ?string $schoolYear = null)
|
||||
{
|
||||
$qb = $this->active()->where('student_class.class_section_id', $classSectionId);
|
||||
if ($schoolYear !== null && $schoolYear !== '') {
|
||||
$qb->where('student_class.school_year', $schoolYear);
|
||||
}
|
||||
return $qb->findAll();
|
||||
return $this
|
||||
->select('student_class.*')
|
||||
->join(
|
||||
'students',
|
||||
'students.id = student_class.student_id',
|
||||
'inner'
|
||||
)
|
||||
->where('students.is_active', 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return class section names for a student in a given school year.
|
||||
* Return the most recent class section name assigned to a student.
|
||||
*/
|
||||
public function getClassSectionNameByStudentId(
|
||||
int $studentId,
|
||||
?string $schoolYear = null
|
||||
): ?string {
|
||||
$builder = $this->freshBuilder()
|
||||
->select('cs.class_section_name')
|
||||
->join(
|
||||
'classSection cs',
|
||||
'cs.class_section_id = student_class.class_section_id',
|
||||
'inner'
|
||||
)
|
||||
->where('student_class.student_id', $studentId)
|
||||
->where(
|
||||
'student_class.class_section_id IS NOT NULL',
|
||||
null,
|
||||
false
|
||||
);
|
||||
|
||||
if ($schoolYear !== null && trim($schoolYear) !== '') {
|
||||
$builder->where(
|
||||
'student_class.school_year',
|
||||
trim($schoolYear)
|
||||
);
|
||||
}
|
||||
|
||||
$row = $builder
|
||||
->orderBy('student_class.created_at', 'DESC')
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
if (!$row) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$name = trim((string) ($row['class_section_name'] ?? ''));
|
||||
|
||||
return $name !== '' ? $name : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get active students assigned to a class section.
|
||||
*
|
||||
* student_class is scoped by school year only. It has no semester column.
|
||||
*/
|
||||
public function getClassStudents(
|
||||
int $classSectionId,
|
||||
?string $schoolYear = null
|
||||
): array {
|
||||
$builder = $this->activeStudentsBuilder()
|
||||
->where(
|
||||
'student_class.class_section_id',
|
||||
$classSectionId
|
||||
)
|
||||
->where(
|
||||
'student_class.class_section_id IS NOT NULL',
|
||||
null,
|
||||
false
|
||||
);
|
||||
|
||||
if ($schoolYear !== null && trim($schoolYear) !== '') {
|
||||
$builder->where(
|
||||
'student_class.school_year',
|
||||
trim($schoolYear)
|
||||
);
|
||||
}
|
||||
|
||||
return $builder
|
||||
->orderBy('students.lastname', 'ASC')
|
||||
->orderBy('students.firstname', 'ASC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return student IDs assigned to a class section for a school year.
|
||||
*/
|
||||
public function getStudentIdsByClassSection(
|
||||
int $classSectionId,
|
||||
string $schoolYear
|
||||
): array {
|
||||
$schoolYear = trim($schoolYear);
|
||||
|
||||
$builder = $this->freshBuilder()
|
||||
->select('student_class.student_id')
|
||||
->join(
|
||||
'students',
|
||||
'students.id = student_class.student_id',
|
||||
'inner'
|
||||
)
|
||||
->where(
|
||||
'student_class.class_section_id',
|
||||
$classSectionId
|
||||
)
|
||||
->where('students.is_active', 1)
|
||||
->where(
|
||||
'student_class.class_section_id IS NOT NULL',
|
||||
null,
|
||||
false
|
||||
);
|
||||
|
||||
if ($schoolYear !== '') {
|
||||
$builder->where(
|
||||
'student_class.school_year',
|
||||
$schoolYear
|
||||
);
|
||||
}
|
||||
|
||||
$rows = $builder
|
||||
->groupBy('student_class.student_id')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
$studentIds = array_map(
|
||||
static fn(array $row): int =>
|
||||
(int) ($row['student_id'] ?? 0),
|
||||
$rows
|
||||
);
|
||||
|
||||
return array_values(array_unique(array_filter(
|
||||
$studentIds,
|
||||
static fn(int $studentId): bool => $studentId > 0
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return class section names assigned to a student for a school year.
|
||||
*
|
||||
* @param int|string $studentId
|
||||
* @param string $schoolYear
|
||||
* @param bool $asArray When true, return an array of names; otherwise a comma-separated string.
|
||||
* @return array|string
|
||||
*/
|
||||
public function getClassSectionsByStudentId($studentId, string $schoolYear, bool $asArray = false)
|
||||
{
|
||||
$rows = $this->db->table('student_class')
|
||||
public function getClassSectionsByStudentId(
|
||||
int|string $studentId,
|
||||
string $schoolYear,
|
||||
bool $asArray = false
|
||||
): array|string {
|
||||
$rows = $this->freshBuilder()
|
||||
->select('cs.class_section_name')
|
||||
->join('classSection cs', 'student_class.class_section_id = cs.class_section_id')
|
||||
->join(
|
||||
'classSection cs',
|
||||
'student_class.class_section_id = cs.class_section_id',
|
||||
'inner'
|
||||
)
|
||||
->where('student_class.student_id', $studentId)
|
||||
->where('student_class.class_section_id IS NOT NULL', null, false)
|
||||
->where('student_class.school_year', $schoolYear)
|
||||
->where(
|
||||
'student_class.class_section_id IS NOT NULL',
|
||||
null,
|
||||
false
|
||||
)
|
||||
->where(
|
||||
'student_class.school_year',
|
||||
trim($schoolYear)
|
||||
)
|
||||
->orderBy('cs.class_section_name', 'ASC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
$names = array_values(array_unique(array_filter(array_map(static function ($row) {
|
||||
return $row['class_section_name'] ?? null;
|
||||
}, $rows))));
|
||||
$names = array_map(
|
||||
static fn(array $row): string =>
|
||||
trim((string) ($row['class_section_name'] ?? '')),
|
||||
$rows
|
||||
);
|
||||
|
||||
if ($asArray) {
|
||||
return $names;
|
||||
}
|
||||
$names = array_values(array_unique(array_filter(
|
||||
$names,
|
||||
static fn(string $name): bool => $name !== ''
|
||||
)));
|
||||
|
||||
return !empty($names) ? implode(', ', $names) : '';
|
||||
return $asArray ? $names : implode(', ', $names);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return class section names for a student in a given school year, with optional event flag.
|
||||
* Return class section names and indicate event-only assignments.
|
||||
*
|
||||
* @param int|string $studentId
|
||||
* @param string $schoolYear
|
||||
* @param bool $asArray When true, return an array of names; otherwise a comma-separated string.
|
||||
* @return array|string
|
||||
*/
|
||||
public function getClassSectionsByStudentIdWithFlags($studentId, string $schoolYear, bool $asArray = false)
|
||||
{
|
||||
$rows = $this->db->table('student_class')
|
||||
->select('cs.class_section_name, student_class.is_event_only')
|
||||
->join('classSection cs', 'student_class.class_section_id = cs.class_section_id')
|
||||
public function getClassSectionsByStudentIdWithFlags(
|
||||
int|string $studentId,
|
||||
string $schoolYear,
|
||||
bool $asArray = false
|
||||
): array|string {
|
||||
$rows = $this->freshBuilder()
|
||||
->select(
|
||||
'cs.class_section_name, student_class.is_event_only'
|
||||
)
|
||||
->join(
|
||||
'classSection cs',
|
||||
'student_class.class_section_id = cs.class_section_id',
|
||||
'inner'
|
||||
)
|
||||
->where('student_class.student_id', $studentId)
|
||||
->where('student_class.class_section_id IS NOT NULL', null, false)
|
||||
->where('student_class.school_year', $schoolYear)
|
||||
->where(
|
||||
'student_class.class_section_id IS NOT NULL',
|
||||
null,
|
||||
false
|
||||
)
|
||||
->where(
|
||||
'student_class.school_year',
|
||||
trim($schoolYear)
|
||||
)
|
||||
->orderBy('cs.class_section_name', 'ASC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
$names = array_values(array_unique(array_filter(array_map(static function ($row) {
|
||||
$name = $row['class_section_name'] ?? null;
|
||||
if (!$name) return null;
|
||||
$isEvent = (int)($row['is_event_only'] ?? 0) === 1;
|
||||
return $isEvent ? ($name . ' (Event)') : $name;
|
||||
}, $rows))));
|
||||
$names = [];
|
||||
|
||||
if ($asArray) {
|
||||
return $names;
|
||||
foreach ($rows as $row) {
|
||||
$name = trim(
|
||||
(string) ($row['class_section_name'] ?? '')
|
||||
);
|
||||
|
||||
if ($name === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((int) ($row['is_event_only'] ?? 0) === 1) {
|
||||
$name .= ' (Event)';
|
||||
}
|
||||
|
||||
$names[] = $name;
|
||||
}
|
||||
|
||||
return !empty($names) ? implode(', ', $names) : '';
|
||||
$names = array_values(array_unique($names));
|
||||
|
||||
return $asArray ? $names : implode(', ', $names);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return class_section_id values for a student/year.
|
||||
* Return class-section IDs assigned to a student for a school year.
|
||||
*
|
||||
* @param int|string $studentId
|
||||
* @param string $schoolYear
|
||||
* @return int[]
|
||||
*/
|
||||
public function getClassSectionIdsByStudentId($studentId, string $schoolYear): array
|
||||
{
|
||||
$rows = $this->db->table('student_class')
|
||||
->select('class_section_id')
|
||||
->where('student_id', $studentId)
|
||||
->where('class_section_id IS NOT NULL', null, false)
|
||||
->where('school_year', $schoolYear)
|
||||
public function getClassSectionIdsByStudentId(
|
||||
int|string $studentId,
|
||||
string $schoolYear
|
||||
): array {
|
||||
$rows = $this->freshBuilder()
|
||||
->select('student_class.class_section_id')
|
||||
->where('student_class.student_id', $studentId)
|
||||
->where(
|
||||
'student_class.class_section_id IS NOT NULL',
|
||||
null,
|
||||
false
|
||||
)
|
||||
->where(
|
||||
'student_class.school_year',
|
||||
trim($schoolYear)
|
||||
)
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
$ids = array_map(static fn($r) => (int)($r['class_section_id'] ?? 0), $rows);
|
||||
return array_values(array_filter(array_unique($ids), static fn($v) => $v > 0));
|
||||
$ids = array_map(
|
||||
static fn(array $row): int =>
|
||||
(int) ($row['class_section_id'] ?? 0),
|
||||
$rows
|
||||
);
|
||||
|
||||
return array_values(array_unique(array_filter(
|
||||
$ids,
|
||||
static fn(int $id): bool => $id > 0
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return active students for a set of class-section IDs.
|
||||
*/
|
||||
public function getStudentsByClassSectionIds(
|
||||
array $classSectionIds,
|
||||
?string $schoolYear = null
|
||||
): array {
|
||||
$classSectionIds = array_values(array_unique(array_filter(
|
||||
array_map('intval', $classSectionIds),
|
||||
static fn(int $id): bool => $id > 0
|
||||
)));
|
||||
|
||||
public function getStudentsByClassSectionIds(array $classSectionIds)
|
||||
{
|
||||
return $this->select('students.id as student_id, students.firstname, students.lastname, students.school_id, students.is_new,students.photo_consent ,students.age, student_class.school_year, student_class.class_section_id')
|
||||
->join('students', 'students.id = student_class.student_id')
|
||||
->whereIn('student_class.class_section_id', $classSectionIds)
|
||||
->where('students.is_active', 1)
|
||||
if ($classSectionIds === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$builder = $this->freshBuilder()
|
||||
->select([
|
||||
'students.id AS student_id',
|
||||
'students.firstname',
|
||||
'students.lastname',
|
||||
'students.school_id',
|
||||
'students.is_new',
|
||||
'students.photo_consent',
|
||||
'students.age',
|
||||
'student_class.school_year',
|
||||
'student_class.class_section_id',
|
||||
'student_class.is_event_only',
|
||||
])
|
||||
->join(
|
||||
'students',
|
||||
'students.id = student_class.student_id',
|
||||
'inner'
|
||||
)
|
||||
->whereIn(
|
||||
'student_class.class_section_id',
|
||||
$classSectionIds
|
||||
)
|
||||
->where('students.is_active', 1);
|
||||
|
||||
if ($schoolYear !== null && trim($schoolYear) !== '') {
|
||||
$builder->where(
|
||||
'student_class.school_year',
|
||||
trim($schoolYear)
|
||||
);
|
||||
}
|
||||
|
||||
return $builder
|
||||
->orderBy('students.lastname', 'ASC')
|
||||
->orderBy('students.firstname', 'ASC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the class ID (grade) for a specific student.
|
||||
*
|
||||
* @param int $studentId
|
||||
* @return string class_id or 'N/A'
|
||||
* Return the student's most recent non-event class grade.
|
||||
*/
|
||||
public function getStudentGrade(int $studentId): string
|
||||
{
|
||||
$studentClass = $this->where('student_id', $studentId)
|
||||
->where('is_event_only', 0)
|
||||
->orderBy('created_at', 'DESC') // in case of multiple entries, get latest
|
||||
->first();
|
||||
public function getStudentGrade(
|
||||
int $studentId,
|
||||
?string $schoolYear = null
|
||||
): string {
|
||||
$builder = $this->freshBuilder()
|
||||
->select('classSection.class_id')
|
||||
->join(
|
||||
'classSection',
|
||||
'classSection.class_section_id = student_class.class_section_id',
|
||||
'inner'
|
||||
)
|
||||
->where('student_class.student_id', $studentId)
|
||||
->where('student_class.is_event_only', 0)
|
||||
->where(
|
||||
'student_class.class_section_id IS NOT NULL',
|
||||
null,
|
||||
false
|
||||
);
|
||||
|
||||
if ($studentClass && isset($studentClass['class_section_id'])) {
|
||||
$classSection = $this->db->table('classSection')
|
||||
->where('class_section_id', $studentClass['class_section_id'])
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
return $classSection['class_id'] ?? 'N/A';
|
||||
if ($schoolYear !== null && trim($schoolYear) !== '') {
|
||||
$builder->where(
|
||||
'student_class.school_year',
|
||||
trim($schoolYear)
|
||||
);
|
||||
}
|
||||
|
||||
log_message('error', "Student class or section not found for student ID: $studentId");
|
||||
$row = $builder
|
||||
->orderBy('student_class.created_at', 'DESC')
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
$classId = trim((string) ($row['class_id'] ?? ''));
|
||||
|
||||
if ($classId !== '') {
|
||||
return $classId;
|
||||
}
|
||||
|
||||
log_message(
|
||||
'warning',
|
||||
'Student class or section not found for student ID: {studentId}',
|
||||
['studentId' => $studentId]
|
||||
);
|
||||
|
||||
return 'N/A';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the student has at least one non-event class assignment for the year.
|
||||
* Determine whether a student has a non-event assignment for a year.
|
||||
*/
|
||||
public function hasNonEventAssignment(int $studentId, string $schoolYear): bool
|
||||
{
|
||||
return (bool) $this->where('student_id', $studentId)
|
||||
->where('school_year', $schoolYear)
|
||||
->where('is_event_only', 0)
|
||||
->first();
|
||||
public function hasNonEventAssignment(
|
||||
int $studentId,
|
||||
string $schoolYear
|
||||
): bool {
|
||||
return $this->freshBuilder()
|
||||
->select('student_class.id')
|
||||
->where('student_class.student_id', $studentId)
|
||||
->where(
|
||||
'student_class.school_year',
|
||||
trim($schoolYear)
|
||||
)
|
||||
->where('student_class.is_event_only', 0)
|
||||
->limit(1)
|
||||
->get()
|
||||
->getRowArray() !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return student counts per class_section_id, optionally scoped to a school year.
|
||||
* Return active student counts by class section.
|
||||
*
|
||||
* @param string|null $schoolYear
|
||||
* @return array<int|string,int> Map of class_section_id => count
|
||||
* @return array<int, int>
|
||||
*/
|
||||
public function getStudentCountsBySection(?string $schoolYear = null): array
|
||||
{
|
||||
$qb = $this->db->table($this->table)
|
||||
->select('class_section_id, COUNT(*) AS total')
|
||||
->join('students', 'students.id = student_class.student_id', 'inner')
|
||||
public function getStudentCountsBySection(
|
||||
?string $schoolYear = null
|
||||
): array {
|
||||
$builder = $this->freshBuilder()
|
||||
->select(
|
||||
'student_class.class_section_id, ' .
|
||||
'COUNT(DISTINCT student_class.student_id) AS total'
|
||||
)
|
||||
->join(
|
||||
'students',
|
||||
'students.id = student_class.student_id',
|
||||
'inner'
|
||||
)
|
||||
->where('students.is_active', 1)
|
||||
->where('student_class.class_section_id IS NOT NULL', null, false)
|
||||
->where(
|
||||
'student_class.class_section_id IS NOT NULL',
|
||||
null,
|
||||
false
|
||||
)
|
||||
->groupBy('student_class.class_section_id');
|
||||
|
||||
if ($schoolYear !== null && $schoolYear !== '') {
|
||||
$qb->where('student_class.school_year', $schoolYear);
|
||||
if ($schoolYear !== null && trim($schoolYear) !== '') {
|
||||
$builder->where(
|
||||
'student_class.school_year',
|
||||
trim($schoolYear)
|
||||
);
|
||||
}
|
||||
|
||||
$rows = $qb->get()->getResultArray();
|
||||
$out = [];
|
||||
foreach ($rows as $r) {
|
||||
$cid = $r['class_section_id'] ?? null;
|
||||
if ($cid === null || $cid === '') continue;
|
||||
$out[$cid] = (int)($r['total'] ?? 0);
|
||||
$rows = $builder
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
$counts = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$classSectionId = (int) (
|
||||
$row['class_section_id'] ?? 0
|
||||
);
|
||||
|
||||
if ($classSectionId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$counts[$classSectionId] = (int) (
|
||||
$row['total'] ?? 0
|
||||
);
|
||||
}
|
||||
return $out;
|
||||
|
||||
return $counts;
|
||||
}
|
||||
}
|
||||
}
|
||||
+26
-54
@@ -242,9 +242,7 @@ class UserModel extends Model
|
||||
->where('roles.name', $roleName)
|
||||
->where('user_roles.deleted_at', null);
|
||||
|
||||
if ($this->userRolesHaveSchoolYear()) {
|
||||
$builder->where('user_roles.school_year', $schoolYear);
|
||||
} elseif ($this->roleUsesTeacherAssignments($roleName)) {
|
||||
if ($this->roleUsesTeacherAssignments($roleName)) {
|
||||
$builder->join(
|
||||
'teacher_class tc_year',
|
||||
'tc_year.teacher_id = users.id AND tc_year.school_year = ' . $this->db->escape($schoolYear),
|
||||
@@ -366,49 +364,32 @@ class UserModel extends Model
|
||||
}
|
||||
|
||||
if (!empty($schoolYear)) {
|
||||
// Prefer user_roles.school_year if present (true year-scoped roles)
|
||||
$userRolesFields = [];
|
||||
try {
|
||||
$userRolesFields = $this->db->getFieldNames('user_roles');
|
||||
} catch (\Throwable $e) {
|
||||
// ignore
|
||||
}
|
||||
$escYear = $this->db->escape($schoolYear);
|
||||
|
||||
if (in_array('school_year', $userRolesFields, true)) {
|
||||
$builder->where('ur.school_year', $schoolYear);
|
||||
} else {
|
||||
// Fallback: role-aware filter
|
||||
$escYear = $this->db->escape($schoolYear);
|
||||
|
||||
// Include user if:
|
||||
// (A) They are assigned as teacher/TA in teacher_class for that year
|
||||
// OR
|
||||
// (B) They have at least one non-teacher-ish (and non-parent) role (admin/staff/etc.)
|
||||
//
|
||||
// Teacher-ish detection: name contains 'teacher' OR equals 'ta'
|
||||
$builder->groupStart()
|
||||
// A) has teacher assignment that year
|
||||
->where("
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM teacher_class tc
|
||||
WHERE tc.teacher_id = users.id
|
||||
AND tc.school_year = {$escYear}
|
||||
)
|
||||
", null, false)
|
||||
// B) OR has any non-teacher-ish, non-parent role
|
||||
->orWhere("
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM user_roles ur2
|
||||
JOIN roles r2 ON r2.id = ur2.role_id
|
||||
WHERE ur2.user_id = users.id
|
||||
AND LOWER(r2.name) NOT IN ('parent','ta')
|
||||
AND LOWER(r2.name) NOT LIKE '%teacher%'
|
||||
)
|
||||
", null, false)
|
||||
->groupEnd();
|
||||
}
|
||||
// Include user if:
|
||||
// (A) They are assigned as teacher/TA in teacher_class for that year
|
||||
// OR
|
||||
// (B) They have at least one non-teacher-ish (and non-parent) global role.
|
||||
$builder->groupStart()
|
||||
->where("
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM teacher_class tc
|
||||
WHERE tc.teacher_id = users.id
|
||||
AND tc.school_year = {$escYear}
|
||||
)
|
||||
", null, false)
|
||||
->orWhere("
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM user_roles ur2
|
||||
JOIN roles r2 ON r2.id = ur2.role_id
|
||||
WHERE ur2.user_id = users.id
|
||||
AND LOWER(r2.name) NOT IN ('parent','ta')
|
||||
AND LOWER(r2.name) NOT LIKE '%teacher%'
|
||||
)
|
||||
", null, false)
|
||||
->groupEnd();
|
||||
}
|
||||
|
||||
return $builder
|
||||
@@ -417,15 +398,6 @@ class UserModel extends Model
|
||||
->findAll();
|
||||
}
|
||||
|
||||
private function userRolesHaveSchoolYear(): bool
|
||||
{
|
||||
try {
|
||||
return in_array('school_year', $this->db->getFieldNames('user_roles'), true);
|
||||
} catch (\Throwable $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function roleUsesTeacherAssignments(string $roleName): bool
|
||||
{
|
||||
$role = strtolower(trim($roleName));
|
||||
|
||||
Reference in New Issue
Block a user