0ac3a8375e
- load global semester helpers consistently and use date-based semester defaults - fix grading and daily attendance duplicate student/section rows - keep attendance violations scoped to the current semester by default - update invoice, refund, discount, payment, and financial aid flows - add configuration cleanup migrations for duplicate calendar/semester keys - refresh parent registration/report-card and print request handling - update related models, services, views, cron notes, and test coverage
285 lines
11 KiB
PHP
285 lines
11 KiB
PHP
<?php
|
|
|
|
namespace App\Controllers\View;
|
|
|
|
use App\Controllers\BaseController;
|
|
use App\Models\ConfigurationModel;
|
|
use App\Models\CalendarModel;
|
|
use App\Models\HomeworkModel;
|
|
|
|
class HomeworkTrackingController extends BaseController
|
|
{
|
|
protected $configModel;
|
|
protected $calendarModel;
|
|
protected $homeworkModel;
|
|
protected $db;
|
|
|
|
protected string $semester;
|
|
protected string $schoolYear;
|
|
/** cache for teacher assignment presence per term */
|
|
private array $teacherAssignmentCache = [];
|
|
|
|
public function __construct()
|
|
{
|
|
$this->configModel = new ConfigurationModel();
|
|
$this->calendarModel = new CalendarModel();
|
|
$this->homeworkModel = new HomeworkModel();
|
|
$this->db = \Config\Database::connect();
|
|
|
|
$this->semester = (string) (getSemester() ?? '');
|
|
$this->schoolYear = (string) ($this->configModel->getConfig('school_year') ?? '');
|
|
}
|
|
|
|
public function index()
|
|
{
|
|
// Semester-based date range (Fall or Spring) derived from school year start.
|
|
[$start, $end] = $this->getDateRangeForSemester($this->schoolYear, $this->semester);
|
|
|
|
// Move start to the first Sunday on/after start
|
|
if ((int)$start->format('w') !== 0) {
|
|
$start = (clone $start)->modify('next sunday');
|
|
}
|
|
|
|
// Collect all Sundays within range
|
|
$sundays = [];
|
|
$d = clone $start;
|
|
while ($d <= $end) {
|
|
$sundays[] = $d->format('Y-m-d');
|
|
$d->modify('+7 days');
|
|
}
|
|
|
|
// Map "no school" events by date (Y-m-d) using explicit DB flag
|
|
$events = $this->calendarModel->getEventsBySchoolYear($this->schoolYear) ?? [];
|
|
$eventDays = [];
|
|
foreach ($events as $ev) {
|
|
$ymd = substr((string)($ev['date'] ?? ''), 0, 10);
|
|
$no = (int)($ev['no_school'] ?? 0);
|
|
if ($ymd && $no === 1) {
|
|
$eventDays[$ymd] = true; // yellow highlight for no-school day
|
|
}
|
|
}
|
|
|
|
// Map Sundays to ordinal homework_index, skipping event Sundays
|
|
$dateToIndex = [];
|
|
$idx = 0;
|
|
foreach ($sundays as $ymd) {
|
|
if (!isset($eventDays[$ymd])) {
|
|
$idx++;
|
|
$dateToIndex[$ymd] = $idx;
|
|
} else {
|
|
$dateToIndex[$ymd] = null; // event day (no homework expected)
|
|
}
|
|
}
|
|
|
|
$limitToSemester = $this->hasTeacherAssignments($this->schoolYear, $this->semester);
|
|
$semesterVariants = $this->getSemesterVariants($this->semester);
|
|
|
|
// Aggregate submitted homework per class_section_id + homework_index.
|
|
// Adding a homework column creates blank rows for every student, so only
|
|
// indexes with at least one non-null score count as submitted.
|
|
$hwQ = $this->db->table('homework')
|
|
->select('class_section_id, homework_index')
|
|
->select('MIN(CASE WHEN score IS NOT NULL THEN updated_at ELSE NULL END) AS first_submitted', false)
|
|
->select('COUNT(score) AS scored_count', false)
|
|
->where('school_year', $this->schoolYear);
|
|
if ($limitToSemester && $semesterVariants !== []) {
|
|
$hwQ->whereIn('semester', $semesterVariants);
|
|
}
|
|
$rows = $hwQ->groupBy('class_section_id, homework_index')
|
|
->get()
|
|
->getResultArray();
|
|
|
|
$hasHomework = [];
|
|
$hwEnteredAt = [];
|
|
$homeworkSubmissionCounts = [];
|
|
foreach ($rows as $r) {
|
|
$csid = (int)($r['class_section_id'] ?? 0);
|
|
$hi = (int)($r['homework_index'] ?? 0);
|
|
$cnt = (int)($r['scored_count'] ?? 0);
|
|
if ($csid > 0 && $hi > 0 && $cnt > 0) {
|
|
$hasHomework[$csid][$hi] = true;
|
|
$dateStr = substr((string)($r['first_submitted'] ?? ''), 0, 10);
|
|
$hwEnteredAt[$csid][$hi] = $dateStr ?: null;
|
|
$homeworkSubmissionCounts[$csid] = ($homeworkSubmissionCounts[$csid] ?? 0) + 1;
|
|
}
|
|
}
|
|
|
|
$indexToDate = [];
|
|
foreach ($dateToIndex as $ymd => $homeworkIndex) {
|
|
if ($homeworkIndex !== null) {
|
|
$indexToDate[(int) $homeworkIndex] = $ymd;
|
|
}
|
|
}
|
|
$hasHomeworkByDate = [];
|
|
$hwEnteredAtByDate = [];
|
|
foreach ($hasHomework as $csid => $indexes) {
|
|
foreach ($indexes as $homeworkIndex => $_submitted) {
|
|
$ymd = $indexToDate[(int) $homeworkIndex] ?? null;
|
|
if ($ymd === null) {
|
|
continue;
|
|
}
|
|
|
|
$hasHomeworkByDate[(int) $csid][$ymd] = true;
|
|
$hwEnteredAtByDate[(int) $csid][$ymd] = $hwEnteredAt[(int) $csid][(int) $homeworkIndex] ?? null;
|
|
}
|
|
}
|
|
|
|
// Fetch teachers and TAs per section for this term
|
|
$tb = $this->db->table('teacher_class tc')
|
|
->select('tc.class_section_id, tc.position, cs.class_section_name, cs.class_id, u.firstname, u.lastname')
|
|
->join('classSection cs', 'cs.class_section_id = tc.class_section_id', 'left')
|
|
->join('users u', 'u.id = tc.teacher_id', 'left')
|
|
->where('tc.school_year', $this->schoolYear);
|
|
$teacherRows = $tb->where('tc.class_section_id IS NOT NULL', null, false)
|
|
->whereIn('tc.position', ['main','ta'])
|
|
->orderBy('cs.class_id', 'ASC')
|
|
->orderBy('cs.class_section_name', 'ASC')
|
|
->orderBy("FIELD(tc.position, 'main','ta')", '', false)
|
|
->orderBy('u.firstname', 'ASC')
|
|
->get()
|
|
->getResultArray();
|
|
|
|
$bySection = [];
|
|
foreach ($teacherRows as $r) {
|
|
$sid = (int)($r['class_section_id'] ?? 0);
|
|
if ($sid <= 0) continue;
|
|
if (!isset($bySection[$sid])) {
|
|
$bySection[$sid] = [
|
|
'class_section_id' => $sid,
|
|
'class_id' => (int)($r['class_id'] ?? 0),
|
|
'class_section_name' => (string)($r['class_section_name'] ?? ''),
|
|
'teachers' => [],
|
|
'tas' => [],
|
|
];
|
|
}
|
|
$name = trim(($r['firstname'] ?? '') . ' ' . ($r['lastname'] ?? ''));
|
|
$pos = strtolower((string)($r['position'] ?? ''));
|
|
if ($name !== '') {
|
|
if ($pos === 'main') { $bySection[$sid]['teachers'][] = $name; }
|
|
elseif ($pos === 'ta') { $bySection[$sid]['tas'][] = $name; }
|
|
}
|
|
}
|
|
|
|
$teachers = array_values($bySection);
|
|
|
|
// Sort by grade order: KG(13) → Grade 1..11 → Youth(12) → others
|
|
usort($teachers, function ($a, $b) {
|
|
$ai = (int)($a['class_id'] ?? 0);
|
|
$bi = (int)($b['class_id'] ?? 0);
|
|
$ord = function ($cid) {
|
|
if ($cid === 13) return 0; // KG first
|
|
if ($cid >= 1 && $cid <= 11) return $cid; // Grades 1..11
|
|
if ($cid === 12) return 99; // Youth last of known
|
|
return 200 + $cid; // any others after
|
|
};
|
|
$cmp = $ord($ai) <=> $ord($bi);
|
|
if ($cmp !== 0) return $cmp;
|
|
return strnatcasecmp($a['class_section_name'] ?? '', $b['class_section_name'] ?? '');
|
|
});
|
|
|
|
// Simple server-side pagination for rows (8 lines per page)
|
|
$perPage = 8;
|
|
$totalRows = count($teachers);
|
|
$totalPages = max(1, (int)ceil($totalRows / $perPage));
|
|
$page = (int) ($this->request->getGet('page') ?? 1);
|
|
if ($page < 1) $page = 1;
|
|
if ($page > $totalPages) $page = $totalPages;
|
|
$offset = ($page - 1) * $perPage;
|
|
$teachersPage = array_slice($teachers, $offset, $perPage);
|
|
|
|
return view('grading/homework_tracking', [
|
|
'semester' => $this->semester,
|
|
'schoolYear' => $this->schoolYear,
|
|
'sundays' => $sundays,
|
|
'eventDays' => $eventDays,
|
|
'dateToIndex' => $dateToIndex,
|
|
'teachers' => $teachersPage,
|
|
'hasHomework' => $hasHomework,
|
|
'hwEnteredAt' => $hwEnteredAt,
|
|
'homeworkSubmissionCounts' => $homeworkSubmissionCounts,
|
|
'hasHomeworkByDate' => $hasHomeworkByDate,
|
|
'hwEnteredAtByDate' => $hwEnteredAtByDate,
|
|
'page' => $page,
|
|
'totalPages' => $totalPages,
|
|
'perPage' => $perPage,
|
|
'totalRows' => $totalRows,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Returns true if teacher_class has any rows for the given school year.
|
|
* The semester argument is ignored because assignments are tracked per year.
|
|
*/
|
|
private function hasTeacherAssignments(string $schoolYear, string $semester): bool
|
|
{
|
|
$year = trim((string)$schoolYear);
|
|
if ($year === '') {
|
|
return false;
|
|
}
|
|
|
|
if (array_key_exists($year, $this->teacherAssignmentCache)) {
|
|
return $this->teacherAssignmentCache[$year];
|
|
}
|
|
|
|
$cnt = $this->db->table('teacher_class')
|
|
->where('school_year', $year)
|
|
->countAllResults();
|
|
|
|
$this->teacherAssignmentCache[$year] = $cnt > 0;
|
|
return $this->teacherAssignmentCache[$year];
|
|
}
|
|
|
|
private function getSemesterVariants(string $semester): array
|
|
{
|
|
$trimmed = trim($semester);
|
|
if ($trimmed === '') {
|
|
return [];
|
|
}
|
|
|
|
return array_values(array_unique([
|
|
$trimmed,
|
|
ucfirst(strtolower($trimmed)),
|
|
strtolower($trimmed),
|
|
strtoupper($trimmed),
|
|
]));
|
|
}
|
|
|
|
/**
|
|
* Compute start/end dates for the given semester within the school year.
|
|
* Fall: 09/21/{startYear} to 01/18/{startYear+1}
|
|
* Spring: 01/25/{startYear+1} to 05/31/{startYear+1}
|
|
* Defaults to today if parsing fails.
|
|
*/
|
|
private function getDateRangeForSemester(string $schoolYear, string $semester): array
|
|
{
|
|
$startYear = null;
|
|
if (preg_match('/\b(20\d{2})\b/', $schoolYear, $m)) {
|
|
$startYear = (int)$m[1];
|
|
}
|
|
if ($startYear === null) {
|
|
$today = new \DateTime('today');
|
|
return [$today, $today];
|
|
}
|
|
|
|
$nextYear = $startYear + 1;
|
|
$sem = strtolower(trim($semester));
|
|
|
|
$start = $end = null;
|
|
try {
|
|
if ($sem === 'spring') {
|
|
$start = new \DateTime("{$nextYear}-01-25");
|
|
$end = new \DateTime("{$nextYear}-05-31");
|
|
} else {
|
|
// default/fall
|
|
$start = new \DateTime("{$startYear}-09-21");
|
|
$end = new \DateTime("{$nextYear}-01-18");
|
|
}
|
|
} catch (\Throwable $e) {
|
|
$today = new \DateTime('today');
|
|
return [$today, $today];
|
|
}
|
|
|
|
return [$start, $end];
|
|
}
|
|
}
|