Files
root 09ea144bb2
Tests / PHPUnit (push) Failing after 34s
fix issues related to school year
2026-07-31 18:52:25 -04:00

496 lines
18 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Models;
use CodeIgniter\Model;
use App\Models\Concerns\SchoolYearAutoFillTrait;
class TeacherClassModel extends Model
{
use SchoolYearAutoFillTrait;
protected $table = 'teacher_class';
protected $primaryKey = 'id';
protected $allowedFields = [
'teacher_id',
'class_section_id',
'school_year',
'position', // NEW
'updated_by',
'updated_at',
'created_at'
];
protected $useTimestamps = true;
protected $createdField = 'created_at';
protected $updatedField = 'updated_at';
protected $returnType = 'array';
protected $useSoftDeletes = false;
protected $skipValidation = false;
protected $afterInsert = ['syncStaffDirectoryAfterWrite'];
protected $afterUpdate = ['syncStaffDirectoryAfterWrite'];
/** Request-lifetime memo cache */
protected array $assignedCache = [];
protected array $assignedBySectionCache = [];
protected $validationRules = [
'teacher_id' => 'required|integer',
'class_section_id' => 'required|integer',
'school_year' => 'required|string|max_length[9]',
'position' => 'required|in_list[main,ta]',
];
protected $validationMessages = [
'teacher_id' => [
'required' => 'The teacher ID is required.',
'integer' => 'The teacher ID must be an integer.',
],
'class_section_id' => [
'required' => 'The class section ID is required.',
'integer' => 'The class section ID must be an integer.',
],
'school_year' => [
'required' => 'The school year is required.',
'max_length' => 'The school year cannot exceed 9 characters.',
],
'position' => [
'required' => 'The position (main or ta) is required.',
'in_list' => 'The position must be either "main" or "ta".',
],
];
protected function syncStaffDirectoryAfterWrite(array $data): array
{
try {
$userIds = [];
$teacherId = (int) ($data['data']['teacher_id'] ?? 0);
if ($teacherId > 0) {
$userIds[] = $teacherId;
}
if ($userIds === [] && ! empty($data['id'])) {
$ids = is_array($data['id']) ? $data['id'] : [$data['id']];
$ids = array_values(array_filter(array_map('intval', $ids), static fn (int $id): bool => $id > 0));
if ($ids !== []) {
$rows = $this->db->table($this->table)
->select('teacher_id')
->whereIn($this->primaryKey, $ids)
->get()
->getResultArray();
foreach ($rows as $row) {
$rowTeacherId = (int) ($row['teacher_id'] ?? 0);
if ($rowTeacherId > 0) {
$userIds[] = $rowTeacherId;
}
}
}
}
foreach (array_unique($userIds) as $userId) {
service('staffDirectorySync')->syncUser((int) $userId);
}
} catch (\Throwable $e) {
log_message('error', 'TeacherClassModel staff directory sync failed: ' . $e->getMessage());
}
return $data;
}
public function getClassSectionIdByUserId($user_id)
{
$result = $this->where('teacher_id', $user_id)->first();
if (!$result) {
log_message('error', "No class section found for user ID: $user_id");
return null;
}
return $result;
}
public function getClassAssignmentsByUserId(int $userId, ?string $schoolYear = null, ?string $semester = null): array
{
$hasSectionSchoolYear = $this->db->fieldExists('school_year', 'classSection');
$sectionJoin = 'tc.class_section_id = cs.class_section_id';
if ($schoolYear && $hasSectionSchoolYear) {
$sectionJoin .= ' AND cs.school_year = ' . $this->db->escape($schoolYear);
}
$builder = $this->db->table('teacher_class tc')
->select([
'tc.class_section_id AS teacher_class_section_id',
'cs.class_section_name',
'cs.class_section_id AS class_section_pk',
'cs.class_section_id',
'c.id AS class_id',
'c.class_name',
'tc.teacher_id',
'tc.position',
'tc.school_year',
])
->join('classSection cs', $sectionJoin, 'left', false)
->join('classes c', 'cs.class_id = c.id', 'left')
->where('tc.teacher_id', $userId)
->where('tc.class_section_id IS NOT NULL', null, false)
->orderBy('COALESCE(cs.class_section_name, tc.class_section_id)', 'ASC', false);
if ($schoolYear) {
$builder->where('tc.school_year', $schoolYear);
}
$rows = $builder->get()->getResultArray();
$out = [];
foreach ($rows as $r) {
$sectionId = (int) ($r['teacher_class_section_id'] ?? $r['class_section_id'] ?? 0);
$sectionName = $r['class_section_name']
?? $this->fallbackClassSectionName($sectionId, $schoolYear);
$out[] = [
'class_section_pk' => (int)($r['class_section_pk'] ?? $sectionId),
'class_section_id' => $sectionId,
'class_section_name' => $sectionName,
'class_id' => isset($r['class_id']) ? (int)$r['class_id'] : null,
'class_name' => $r['class_name'] ?? null,
'teacher_id' => (int)$r['teacher_id'],
'teacher_role' => ucfirst($r['position']), // "Main" or "Ta"
'school_year' => $r['school_year'],
'semester' => $semester,
];
}
return $out;
}
private function fallbackClassSectionName(int $classSectionId, ?string $schoolYear = null): string
{
if ($classSectionId <= 0) {
return 'Class';
}
if ($schoolYear && $this->db->fieldExists('school_year', 'classSection')) {
$row = $this->db->table('classSection')
->select('class_section_name')
->where('class_section_id', $classSectionId)
->where('school_year', $schoolYear)
->get()
->getRowArray();
if (!empty($row['class_section_name'])) {
return (string) $row['class_section_name'];
}
}
$row = $this->db->table('classSection')
->select('class_section_name')
->where('class_section_id', $classSectionId)
->orderBy('id', 'DESC')
->get()
->getRowArray();
return !empty($row['class_section_name'])
? (string) $row['class_section_name']
: ('Class ' . $classSectionId);
}
public function getClassSectionsByTeacherId($teacherId)
{
$builder = $this->db->table('teacher_class')
->select('classSection.class_section_name, teacher_class.teacher_id, teacher_class.position')
->join('classSection', 'teacher_class.class_section_id = classSection.class_section_id')
->where('teacher_class.teacher_id', $teacherId)
->where('teacher_class.class_section_id IS NOT NULL', null, false);
$result = $builder->get()->getResultArray();
if ($result) {
$sectionNames = array_map(fn($row) => $row['class_section_name'], $result);
$position = $result[0]['position'] ?? '';
return [
'role' => ucfirst($position),
'class_section_name' => implode(', ', $sectionNames)
];
} else {
return ['role' => '', 'class_section_name' => 'No class assigned'];
}
}
public function getClassByTeacherId($teacherId)
{
$builder = $this->db->table('teacher_class')
->select('classSection.class_section_id, classSection.class_section_name')
->join('classSection', 'teacher_class.class_section_id = classSection.class_section_id')
->where('teacher_class.teacher_id', $teacherId)
->where('teacher_class.class_section_id IS NOT NULL', null, false);
return $builder->get()->getResultArray() ?? [];
}
public function getTeacherIdByClassSection(string $classSectionName, string $semester, string $schoolYear)
{
$db = \Config\Database::connect();
$classSectionName = trim($classSectionName);
if (strpos($classSectionName, ',') !== false) {
$parts = array_filter(array_map('trim', explode(',', $classSectionName)));
if (!empty($parts)) {
$classSectionName = reset($parts); // pick the first section as primary
}
}
if ($classSectionName === '') {
return null;
}
$section = $db->table('classSection')
->select('class_section_id')
->where('class_section_name', $classSectionName)
->get()
->getRowArray();
if (!$section) return null;
$result = $db->table('teacher_class')
->select('teacher_id')
->where([
'class_section_id' => $section['class_section_id'],
'school_year' => $schoolYear,
'position' => 'main'
])
->get()
->getRowArray();
return $result['teacher_id'] ?? null;
}
public function getMainTeacherBySection(string $classSectionName, string $semester, string $schoolYear): ?array
{
$db = \Config\Database::connect();
// Adjust table/column names if yours differ (e.g., cs.class_section_id vs cs.id)
$row = $db->table('classSection cs')
->select('u.id AS teacher_id, u.firstname, u.lastname')
->join('teacher_class tc', 'tc.class_section_id = cs.class_section_id', 'inner')
->join('users u', 'u.id = tc.teacher_id', 'inner')
->where('cs.class_section_name', $classSectionName)
->where('tc.school_year', $schoolYear)
->where('tc.position', 'main')
->get()
->getRowArray();
return $row ?: null; // ['teacher_id' => ..., 'firstname' => ..., 'lastname' => ...] or null
}
/**
* Return teacher + TAs for a class section in the current term.
* Output:
* [
* 'teacher_id' => int|null,
* 'teacher_name' => string, // 'N/A' if none found
* 'tas' => [ ['id'=>int,'name'=>string], ... ],
* ]
*/
public function getTeacherTABySection(int|string $classSectionId, string $semester, string $schoolYear): array
{
$db = \Config\Database::connect();
// We can read everything straight from teacher_class; no need to join classSection.
$rows = $db->table('teacher_class tc')
->select('u.id AS user_id, u.firstname, u.lastname, u.gender, tc.position')
->join('users u', 'u.id = tc.teacher_id', 'inner')
->where('tc.class_section_id', $classSectionId)
->where('tc.school_year', $schoolYear)
// Prefer the most recent assignment and keep main/co-teachers ahead of assistants
->orderBy("CASE WHEN LOWER(tc.position) IN ('main','teacher','main teacher') THEN 0 ELSE 1 END", 'ASC', false)
->orderBy('COALESCE(tc.updated_at, tc.id)', 'DESC', false)
->orderBy('tc.id', 'DESC')
->get()
->getResultArray();
$teacherId = null;
$teacherName = 'N/A';
$teacherNames = [];
$tas = [];
$firstSeen = null; // fallback if no explicit "main"/"teacher"
$taSeen = [];
$mainSeen = [];
$withPrefix = static function (array $row): string {
$first = trim((string)($row['firstname'] ?? ''));
$last = trim((string)($row['lastname'] ?? ''));
$gender = strtolower(trim((string)($row['gender'] ?? '')));
$name = trim($first . ' ' . $last);
if ($name === '') return '';
$prefix = ($gender === 'female') ? 'Sr ' : (($gender === 'male') ? 'Br ' : '');
return trim($prefix . $name);
};
foreach ($rows as $r) {
$full = $withPrefix($r);
if ($full === '') continue;
if ($firstSeen === null) {
$firstSeen = ['id' => (int)$r['user_id'], 'name' => $full];
}
$pos = strtolower((string)($r['position'] ?? ''));
// Treat either 'main' or legacy 'teacher' as the main teacher role.
if (in_array($pos, ['main', 'teacher', 'main teacher'], true)) {
$uid = (int)$r['user_id'];
if (!isset($mainSeen[$uid])) {
$teacherNames[] = $full;
$mainSeen[$uid] = true;
}
if ($teacherId === null) {
$teacherId = $uid;
$teacherName = $full;
}
continue;
}
// Treat 'ta', 'assistant', 'teacher_assistant' (and similar) as TA roles.
if (in_array($pos, ['ta', 'assistant', 'teacher_assistant'], true)) {
$tid = (int)$r['user_id'];
if (!isset($taSeen[$tid])) {
$tas[] = ['id' => $tid, 'name' => $full];
$taSeen[$tid] = true;
}
continue;
}
// Unknown/missing position: don't discard; well use firstSeen as a fallback later.
}
// Fallback: if no explicit main teacher found, use the first record seen.
if ($teacherId === null && $firstSeen !== null) {
$teacherId = $firstSeen['id'];
$teacherName = $firstSeen['name'];
$teacherNames[] = $firstSeen['name'];
}
// Ensure uniqueness/cleaning
$teacherNames = array_values(array_unique(array_filter(array_map('trim', $teacherNames))));
return [
'teacher_id' => $teacherId,
'teacher_name' => $teacherName,
'teacher_names' => $teacherNames,
'tas' => $tas,
];
}
public function assignTeacherToClass($teacherId, $classSectionId, $semester, $schoolYear, $position = 'main')
{
if (!in_array($position, ['main', 'ta'])) {
throw new \InvalidArgumentException('Position must be either "main" or "ta"');
}
$existing = $this->where([
'teacher_id' => $teacherId,
'class_section_id' => $classSectionId,
'school_year' => $schoolYear,
'position' => $position
])->first();
if ($existing) {
return $this->update($existing['id'], ['updated_at' => utc_now()]);
}
return $this->insert([
'teacher_id' => $teacherId,
'class_section_id' => $classSectionId,
'school_year' => $schoolYear,
'position' => $position,
'created_at' => utc_now(),
'updated_at' => utc_now()
]);
}
public function findAll($limit = 0, $offset = 0)
{
return parent::findAll($limit, $offset);
}
public function getAssignedClassSections(int $teacherId, string $schoolYear, string $semester)
{
return $this->select('teacher_class.class_section_id, teacher_class.position, classSection.class_section_name')
->join('classSection', 'classSection.id = teacher_class.class_section_id')
->where('teacher_class.teacher_id', $teacherId)
->where('teacher_class.school_year', $schoolYear)
->findAll();
}
/**
* Get assigned teachers (with names) for ONE section code + term.
* Returns rows: class_section_id, teacher_id, position, firstname, lastname
*/
public function assignedForSectionTerm(int $sectionCode, string $semester, string $schoolYear): array
{
$key = $sectionCode . '|' . $semester . '|' . $schoolYear;
if (isset($this->assignedCache[$key])) {
return $this->assignedCache[$key];
}
$rows = $this->db->table('teacher_class tc')
->select('tc.class_section_id, tc.teacher_id, tc.position, u.firstname, u.lastname')
->join('users u', 'u.id = tc.teacher_id', 'left')
->where('tc.class_section_id', $sectionCode) // SECTION CODE
->where('tc.school_year', $schoolYear)
->orderBy("FIELD(tc.position, 'main','ta')", '', false) // main → ta
->orderBy('u.firstname', 'ASC')
->get()->getResultArray();
return $this->assignedCache[$key] = $rows;
}
/**
* Convenience: same as above but keyed by teacher_id.
* Returns: [teacher_id] => row
*/
public function assignedMapForSectionTerm(int $sectionCode, string $semester, string $schoolYear): array
{
$out = [];
foreach ($this->assignedForSectionTerm($sectionCode, $semester, $schoolYear) as $r) {
$out[(int)$r['teacher_id']] = $r;
}
return $out;
}
/**
* Batch helper for a whole term (optionally filtered to some sections).
* Returns: [sectionCode] => [rows...]
*/
public function assignedBySectionForTerm(string $semester, string $schoolYear, ?array $onlySectionCodes = null): array
{
$cacheKey = ($onlySectionCodes ? implode(',', array_map('intval', $onlySectionCodes)) : '*') . '|' . $semester . '|' . $schoolYear;
if (isset($this->assignedBySectionCache[$cacheKey])) {
return $this->assignedBySectionCache[$cacheKey];
}
$qb = $this->db->table('teacher_class tc')
->select('tc.class_section_id, tc.teacher_id, tc.position, u.firstname, u.lastname')
->join('users u', 'u.id = tc.teacher_id', 'left')
->where('tc.school_year', $schoolYear)
->orderBy('tc.class_section_id', 'ASC')
->orderBy("FIELD(tc.position, 'main','ta')", '', false)
->orderBy('u.firstname', 'ASC');
if (!empty($onlySectionCodes)) {
$qb->whereIn('tc.class_section_id', array_map('intval', $onlySectionCodes));
}
$rows = $qb->get()->getResultArray();
$out = [];
foreach ($rows as $r) {
$code = (int)$r['class_section_id'];
$out[$code][] = $r;
}
return $this->assignedBySectionCache[$cacheKey] = $out;
}
}