update code

This commit is contained in:
root
2026-02-26 23:27:50 -05:00
parent 663c0cdbda
commit a6880ea14f
10 changed files with 807 additions and 138 deletions
+249 -7
View File
@@ -5,7 +5,10 @@ namespace App\Controllers;
use App\Models\ClassProgressReportModel;
use App\Models\ClassProgressAttachmentModel;
use App\Models\ClassSectionModel;
use App\Models\CalendarModel;
use App\Models\ConfigurationModel;
use App\Models\StudentClassModel;
use App\Services\SemesterRangeService;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\ResponseInterface;
@@ -15,6 +18,9 @@ class AdminProgressController extends BaseController
protected ClassProgressAttachmentModel $attachmentModel;
protected ClassSectionModel $classSectionModel;
protected StudentClassModel $studentClassModel;
protected CalendarModel $calendarModel;
protected ConfigurationModel $configModel;
protected SemesterRangeService $semesterRangeService;
public function __construct()
{
@@ -23,6 +29,9 @@ class AdminProgressController extends BaseController
$this->attachmentModel = new ClassProgressAttachmentModel();
$this->classSectionModel = new ClassSectionModel();
$this->studentClassModel = new StudentClassModel();
$this->calendarModel = new CalendarModel();
$this->configModel = new ConfigurationModel();
$this->semesterRangeService = new SemesterRangeService($this->configModel);
}
public function index()
@@ -53,22 +62,30 @@ class AdminProgressController extends BaseController
}
$rows = $builder->orderBy('week_start', 'DESC')->get()->getResultArray();
$reportGroups = [];
$reportGroupsBySection = [];
foreach ($rows as $row) {
$row['status_label'] = ClassProgressController::STATUS_OPTIONS[$row['status']] ?? 'Unknown';
$key = ($row['week_start'] ?? '') . '_' . ($row['class_section_id'] ?? '');
if ($key === '_') {
$sectionId = (int) ($row['class_section_id'] ?? 0);
$weekKey = (string) ($row['week_start'] ?? '');
if ($sectionId === 0 || $weekKey === '') {
continue;
}
if (! isset($reportGroups[$key])) {
$reportGroups[$key] = [
if (! isset($reportGroupsBySection[$sectionId])) {
$reportGroupsBySection[$sectionId] = [];
}
if (! isset($reportGroupsBySection[$sectionId][$weekKey])) {
$reportGroupsBySection[$sectionId][$weekKey] = [
'week_start' => $row['week_start'],
'week_end' => $row['week_end'],
'class_section_name' => $row['class_section_name'] ?? '',
'reports' => [],
];
}
$reportGroups[$key]['reports'][$row['subject']] = $row;
$reportGroupsBySection[$sectionId][$weekKey]['reports'][$row['subject']] = $row;
}
foreach ($reportGroupsBySection as $sectionId => $groups) {
krsort($groups);
$reportGroupsBySection[$sectionId] = $groups;
}
$classSections = $this->classSectionModel->getClassSections();
@@ -78,12 +95,27 @@ class AdminProgressController extends BaseController
return isset($studentCounts[$sectionId]) && $studentCounts[$sectionId] > 0;
}));
$filterStart = $this->normalizeDate($filters['from'] ?? '');
$filterEnd = $this->normalizeDate($filters['to'] ?? '');
[$dateList, $noSchoolDays, $totalPassedDays, $passedDatesSet] = $this->buildSemesterDates();
[$expectedDays, $activeDatesSet] = $this->resolveExpectedDays(
$dateList,
$noSchoolDays,
$totalPassedDays,
$passedDatesSet,
$filterStart,
$filterEnd
);
$sectionStats = $this->buildSectionSubmissionStats($rows, $activeDatesSet, $expectedDays);
return view('admin/class_progress_list', [
'reportGroups' => $reportGroups,
'reportGroupsBySection' => $reportGroupsBySection,
'filters' => $filters,
'classSections' => $filteredSections,
'statusOptions' => ClassProgressController::STATUS_OPTIONS,
'subjectSections' => ClassProgressController::SUBJECT_SECTIONS,
'sectionStats' => $sectionStats,
'expectedDays' => $expectedDays,
]);
}
@@ -211,4 +243,214 @@ class AdminProgressController extends BaseController
$flags = json_decode($json, true);
return is_array($flags) ? $flags : [];
}
protected function normalizeDate(string $value): string
{
$value = trim($value);
if (! preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) {
return '';
}
[$y, $m, $d] = array_map('intval', explode('-', $value));
return checkdate($m, $d, $y) ? $value : '';
}
protected function buildSemesterDates(): array
{
$schoolYear = (string) ($this->configModel->getConfig('school_year') ?? '');
$semester = (string) ($this->configModel->getConfig('semester') ?? '');
$schoolYearForRange = $schoolYear !== '' ? $schoolYear : (string) ($this->configModel->getConfig('school_year') ?? '');
[$rangeStart, $rangeEnd] = $this->semesterRangeService->getSchoolYearRange($schoolYearForRange);
$semesterNorm = $this->semesterRangeService->normalizeSemester($semester);
if ($semesterNorm !== '' && $schoolYearForRange !== '') {
$semRange = $this->semesterRangeService->getSemesterRange($schoolYearForRange, $semesterNorm);
if ($semRange) {
[$rangeStart, $rangeEnd] = $semRange;
}
}
$dateList = [];
try {
$start = new \DateTimeImmutable($rangeStart);
$end = new \DateTimeImmutable($rangeEnd);
$cursor = $start;
$w = (int) $cursor->format('w');
if ($w !== 0) {
$cursor = $cursor->modify('next sunday');
}
while ($cursor <= $end) {
$dateList[] = $cursor->format('Y-m-d');
$cursor = $cursor->modify('+7 days');
}
} catch (\Throwable $e) {
$dateList = [];
}
$noSchoolDays = [];
$events = [];
try {
$events = $this->calendarModel->getEvents();
} catch (\Throwable $e) {
$events = [];
}
foreach ($events as $event) {
$d = substr((string) ($event['date'] ?? ''), 0, 10);
if ($d === '' || empty($event['no_school'])) {
continue;
}
if ($d < $rangeStart || $d > $rangeEnd) {
continue;
}
$eventYear = trim((string) ($event['school_year'] ?? ''));
if ($schoolYearForRange !== '' && $eventYear !== '' && $eventYear !== $schoolYearForRange) {
continue;
}
$noSchoolDays[$d] = true;
}
$anchorSundayYmd = '';
try {
$tzName = (string) (config('School')->attendance['timezone'] ?? user_timezone());
$tzObj = new \DateTimeZone($tzName ?: 'UTC');
} catch (\Throwable $e) {
try {
$tzObj = new \DateTimeZone(user_timezone() ?: 'UTC');
} catch (\Throwable $e2) {
$tzObj = new \DateTimeZone('UTC');
}
}
try {
$nowDate = new \DateTime('now', $tzObj);
} catch (\Throwable $e) {
$nowDate = new \DateTime('now');
}
$weekday = (int) $nowDate->format('w');
$anchorSundayYmd = $weekday === 0
? $nowDate->format('Y-m-d')
: $nowDate->modify('next sunday')->format('Y-m-d');
$passedDatesSet = [];
if (! empty($dateList) && $anchorSundayYmd !== '') {
foreach ($dateList as $d) {
if ($d <= $anchorSundayYmd && empty($noSchoolDays[$d])) {
$passedDatesSet[$d] = true;
}
}
}
$totalPassedDays = count($passedDatesSet);
return [$dateList, $noSchoolDays, $totalPassedDays, $passedDatesSet];
}
protected function resolveExpectedDays(
array $dateList,
array $noSchoolDays,
int $totalPassedDays,
array $passedDatesSet,
string $filterStart,
string $filterEnd
): array {
$activeDatesSet = [];
if ($filterStart === '' && $filterEnd === '') {
$expectedDays = $totalPassedDays;
if ($expectedDays > 0) {
$activeDatesSet = $passedDatesSet;
}
return [$expectedDays, $activeDatesSet];
}
$expectedDays = 0;
foreach ($dateList as $d) {
if ($d === '') {
continue;
}
if ($filterStart !== '' && $d < $filterStart) {
continue;
}
if ($filterEnd !== '' && $d > $filterEnd) {
continue;
}
if (! empty($noSchoolDays[$d])) {
continue;
}
$activeDatesSet[$d] = true;
$expectedDays++;
}
return [$expectedDays, $activeDatesSet];
}
protected function buildSectionSubmissionStats(array $rows, array $activeDatesSet, int $expectedDays): array
{
$requiredSubjects = [];
foreach (ClassProgressController::SUBJECT_SECTIONS as $section) {
$requiredSubjects[] = $section['db_subject'] ?? $section['label'] ?? '';
}
$requiredSubjects = array_values(array_filter($requiredSubjects));
$submittedBySection = [];
foreach ($rows as $row) {
$sectionId = (int) ($row['class_section_id'] ?? 0);
$weekStart = (string) ($row['week_start'] ?? '');
$subject = (string) ($row['subject'] ?? '');
if ($sectionId === 0 || $weekStart === '' || $subject === '') {
continue;
}
if (! empty($activeDatesSet) && empty($activeDatesSet[$weekStart])) {
continue;
}
if (! in_array($subject, $requiredSubjects, true)) {
continue;
}
$submittedBySection[$sectionId][$weekStart][$subject] = true;
}
$stats = [];
foreach ($submittedBySection as $sectionId => $weeks) {
$submitted = 0;
foreach ($weeks as $subjects) {
$all = true;
foreach ($requiredSubjects as $subject) {
if (empty($subjects[$subject])) {
$all = false;
break;
}
}
if ($all) {
$submitted++;
}
}
$stats[$sectionId] = $this->buildSectionStat($submitted, $expectedDays);
}
return $stats;
}
protected function buildSectionStat(int $submitted, int $expectedDays): array
{
$percent = $expectedDays > 0 ? round(($submitted * 100) / $expectedDays, 1) : 0.0;
if ($expectedDays === 0) {
$badgeClass = 'bg-secondary';
$labelClass = 'text-muted';
} elseif ($percent >= 100) {
$badgeClass = 'bg-success';
$labelClass = 'text-success';
} elseif ($percent >= 60) {
$badgeClass = 'bg-warning text-dark';
$labelClass = 'text-warning';
} elseif ($percent >= 40) {
$badgeClass = 'bg-orange text-dark';
$labelClass = 'text-warning';
} else {
$badgeClass = 'bg-danger';
$labelClass = 'text-danger';
}
return [
'submitted' => $submitted,
'expected' => $expectedDays,
'percent' => $percent,
'badgeClass' => $badgeClass,
'labelClass' => $labelClass,
];
}
}
+127 -8
View File
@@ -7,6 +7,7 @@ use App\Models\ClassProgressAttachmentModel;
use App\Models\ConfigurationModel;
use App\Models\SubjectCurriculumModel;
use App\Models\TeacherClassModel;
use App\Services\SemesterRangeService;
use CodeIgniter\Exceptions\PageNotFoundException;
class ClassProgressController extends BaseController
@@ -56,6 +57,7 @@ class ClassProgressController extends BaseController
$classSectionName = $first['class_section_name'] ?? null;
$classId = $first['class_id'] ?? null;
$sundayOptions = $this->buildSundayOptions();
$defaultWeekStart = $this->pickDefaultWeekStart($sundayOptions);
$subjectCurriculum = [];
if ($classId) {
foreach (self::SUBJECT_SECTIONS as $slug => $section) {
@@ -69,7 +71,7 @@ class ClassProgressController extends BaseController
'classSectionName' => $classSectionName,
'classId' => $classId,
'sundayOptions' => $sundayOptions,
'defaultWeekStart' => $sundayOptions[0] ?? '',
'defaultWeekStart' => $defaultWeekStart,
];
return view('teacher/class_progress_submit', $data);
}
@@ -171,10 +173,16 @@ class ClassProgressController extends BaseController
if ($selectedSectionId && ! in_array($selectedSectionId, $validSectionIds, true)) {
$selectedSectionId = $validSectionIds[0] ?? null;
}
[$semester, $schoolYear] = $this->resolveCurrentTerm();
$allowedTeacherIds = $this->resolveAssignedTeacherIds($selectedSectionId, $semester, $schoolYear);
if (empty($allowedTeacherIds)) {
$allowedTeacherIds = [$teacherId];
}
$builder = $this->reportModel
->select('class_progress_reports.*, cs.class_section_name')
->select('class_progress_reports.*, cs.class_section_name, CONCAT(IFNULL(u.firstname, ""), " ", IFNULL(u.lastname, "")) AS teacher_name')
->join('classSection cs', 'cs.class_section_id = class_progress_reports.class_section_id', 'left')
->where('teacher_id', $teacherId);
->join('users u', 'u.id = class_progress_reports.teacher_id', 'left')
->whereIn('teacher_id', $allowedTeacherIds);
if ($selectedSectionId) {
$builder->where('class_progress_reports.class_section_id', $selectedSectionId);
}
@@ -216,20 +224,33 @@ class ClassProgressController extends BaseController
{
$teacherId = (int) session()->get('user_id');
$row = $this->reportModel
->select('class_progress_reports.*, cs.class_section_name')
->select('class_progress_reports.*, cs.class_section_name, CONCAT(IFNULL(u.firstname, ""), " ", IFNULL(u.lastname, "")) AS teacher_name')
->join('classSection cs', 'cs.class_section_id = class_progress_reports.class_section_id', 'left')
->where('teacher_id', $teacherId)
->find((int) $id);
->join('users u', 'u.id = class_progress_reports.teacher_id', 'left')
->where('class_progress_reports.id', (int) $id)
->first();
if (! $row) {
throw new PageNotFoundException('Progress report not found.');
}
[$semester, $schoolYear] = $this->resolveCurrentTerm();
$allowedTeacherIds = $this->resolveAssignedTeacherIds((int) $row['class_section_id'], $semester, $schoolYear);
if (empty($allowedTeacherIds)) {
if ($teacherId !== (int) $row['teacher_id']) {
throw new PageNotFoundException('Progress report not found.');
}
$allowedTeacherIds = [(int) $row['teacher_id']];
} elseif (! in_array($teacherId, $allowedTeacherIds, true)) {
throw new PageNotFoundException('Progress report not found.');
}
$row['status_label'] = self::STATUS_OPTIONS[$row['status']] ?? 'Unknown';
$weeklyReports = $this->reportModel
->select('class_progress_reports.*, cs.class_section_name')
->select('class_progress_reports.*, cs.class_section_name, CONCAT(IFNULL(u.firstname, ""), " ", IFNULL(u.lastname, "")) AS teacher_name')
->join('classSection cs', 'cs.class_section_id = class_progress_reports.class_section_id', 'left')
->where('teacher_id', $teacherId)
->join('users u', 'u.id = class_progress_reports.teacher_id', 'left')
->whereIn('teacher_id', $allowedTeacherIds)
->where('class_progress_reports.class_section_id', $row['class_section_id'])
->where('week_start', $row['week_start'])
->orderBy('subject', 'ASC')
@@ -427,6 +448,39 @@ class ClassProgressController extends BaseController
}
protected function buildSundayOptions(int $count = 12): array
{
$range = $this->resolveProgressDateRange();
if ($range === null) {
return $this->buildUpcomingSundayOptions($count);
}
[$rangeStart, $rangeEnd] = $range;
try {
$start = new \DateTime($rangeStart);
$end = new \DateTime($rangeEnd);
} catch (\Exception $e) {
return $this->buildUpcomingSundayOptions($count);
}
if ($end < $start) {
[$start, $end] = [$end, $start];
}
$weekday = (int) $start->format('w');
if ($weekday !== 0) {
$start->modify('next sunday');
}
$options = [];
while ($start <= $end) {
$options[] = $start->format('Y-m-d');
$start->modify('+7 days');
}
return $options;
}
protected function buildUpcomingSundayOptions(int $count): array
{
$start = new \DateTime('today');
$weekday = (int) $start->format('w');
@@ -443,6 +497,48 @@ class ClassProgressController extends BaseController
return $options;
}
protected function resolveProgressDateRange(): ?array
{
$schoolYear = (string) ($this->configModel->getConfig('school_year') ?? '');
if ($schoolYear === '') {
return null;
}
$semesterResolver = new SemesterRangeService($this->configModel);
$semester = $semesterResolver->normalizeSemester((string) ($this->configModel->getConfig('semester') ?? ''));
if ($semester === '') {
$semester = $semesterResolver->getSemesterForDate();
}
if ($semester !== '') {
$range = $semesterResolver->getSemesterRange($schoolYear, $semester);
if ($range !== null) {
return $range;
}
}
return $semesterResolver->getSchoolYearRange($schoolYear);
}
protected function pickDefaultWeekStart(array $options): string
{
if (empty($options)) {
return '';
}
$today = date('Y-m-d');
$default = '';
foreach ($options as $option) {
if ($option <= $today) {
$default = $option;
continue;
}
break;
}
return $default !== '' ? $default : $options[0];
}
protected function buildWeekEndFromStart(string $weekStart): string
{
try {
@@ -468,4 +564,27 @@ class ClassProgressController extends BaseController
mkdir($this->attachmentStoragePath, 0755, true);
}
}
protected function resolveCurrentTerm(): array
{
$schoolYear = (string) ($this->configModel->getConfig('school_year') ?? '');
$semester = (string) ($this->configModel->getConfig('semester') ?? '');
return [$semester, $schoolYear];
}
protected function resolveAssignedTeacherIds(?int $classSectionId, string $semester, string $schoolYear): array
{
if (! $classSectionId || $schoolYear === '') {
return [];
}
$rows = $this->teacherClassModel->assignedForSectionTerm($classSectionId, $semester, $schoolYear);
$ids = [];
foreach ($rows as $row) {
$id = (int) ($row['teacher_id'] ?? 0);
if ($id > 0) {
$ids[$id] = true;
}
}
return array_keys($ids);
}
}
+16 -17
View File
@@ -797,26 +797,25 @@ public function showUpdateAttendanceForm()
}
$noSchoolDays = [];
if ($schoolYearForRange !== '') {
$events = [];
try {
$events = $this->calendarModel->getEvents();
} catch (\Throwable $e) {
$events = [];
try {
$events = $this->calendarModel->getEventsBySchoolYearAndSemester(
$schoolYearForRange,
$semesterNorm !== '' ? $semesterNorm : null
);
} catch (\Throwable $e) {
$events = [];
}
foreach ($events as $event) {
$d = substr((string)($event['date'] ?? ''), 0, 10);
if ($d === '' || empty($event['no_school'])) {
continue;
}
foreach ($events as $event) {
$d = substr((string)($event['date'] ?? ''), 0, 10);
if ($d === '' || empty($event['no_school'])) {
continue;
}
if ($d < $rangeStart || $d > $rangeEnd) {
continue;
}
$noSchoolDays[$d] = true;
if ($d < $rangeStart || $d > $rangeEnd) {
continue;
}
$eventYear = trim((string)($event['school_year'] ?? ''));
if ($schoolYearForRange !== '' && $eventYear !== '' && $eventYear !== $schoolYearForRange) {
continue;
}
$noSchoolDays[$d] = true;
}
// Total passed attendance days up to this Sunday (inclusive), excluding no-school days.
@@ -60,13 +60,15 @@ class ClassPreparationController extends BaseController
$limitToSemester = $this->hasRosterForSemester($schoolYear, $semester);
// 1) Get student count per class-section (distinct students, correct semester)
$scQ = $this->studentClassModel
->select('class_section_id, COUNT(DISTINCT student_id) AS student_count')
->where('school_year', $schoolYear);
$scQ = $this->db->table('student_class sc')
->select('sc.class_section_id, COUNT(DISTINCT sc.student_id) AS student_count', false)
->join('students s', 's.id = sc.student_id', 'inner')
->where('s.is_active', 1)
->where('sc.school_year', $schoolYear);
if ($limitToSemester && $semester !== '') {
$scQ->where('semester', $semester);
$scQ->where('sc.semester', $semester);
}
$classSections = $scQ->groupBy('class_section_id')->findAll();
$classSections = $scQ->groupBy('sc.class_section_id')->get()->getResultArray();
// 2) Inventory availability — prefer good_qty when present, else condition='good' quantity.
$inventoryMap = $this->buildInventoryAvailability($schoolYear, $semester, $limitToSemester, $allowed);
@@ -298,14 +300,16 @@ class ClassPreparationController extends BaseController
$className = $this->classSectionModel->getClassSectionNameBySectionId($classSectionId) ?? $classSectionId;
// Distinct student count for this term
$studentQ = $this->studentClassModel
->select('COUNT(DISTINCT student_id) AS cnt')
->where('class_section_id', $classSectionId)
->where('school_year', $schoolYear);
$studentQ = $this->db->table('student_class sc')
->select('COUNT(DISTINCT sc.student_id) AS cnt', false)
->join('students s', 's.id = sc.student_id', 'inner')
->where('s.is_active', 1)
->where('sc.class_section_id', $classSectionId)
->where('sc.school_year', $schoolYear);
if ($limitToSemester && $semester !== '') {
$studentQ->where('semester', $semester);
$studentQ->where('sc.semester', $semester);
}
$studentRow = $studentQ->first();
$studentRow = $studentQ->get()->getRowArray();
$studentCount = (int)($studentRow['cnt'] ?? 0);
// Live calc + adjustments
@@ -355,13 +359,15 @@ class ClassPreparationController extends BaseController
$limitToSemester = $this->hasRosterForSemester($schoolYear, $semester);
// Student counts
$scQ = $this->studentClassModel
->select('class_section_id, COUNT(DISTINCT student_id) AS student_count')
->where('school_year', $schoolYear);
$scQ = $this->db->table('student_class sc')
->select('sc.class_section_id, COUNT(DISTINCT sc.student_id) AS student_count', false)
->join('students s', 's.id = sc.student_id', 'inner')
->where('s.is_active', 1)
->where('sc.school_year', $schoolYear);
if ($limitToSemester && $semester !== '') {
$scQ->where('semester', $semester);
$scQ->where('sc.semester', $semester);
}
$classSections = $scQ->groupBy('class_section_id')->findAll();
$classSections = $scQ->groupBy('sc.class_section_id')->get()->getResultArray();
// Build inventory availability maps
$inventoryMap = $this->buildInventoryAvailability($schoolYear, $semester, $limitToSemester, $allowed);
@@ -448,14 +454,16 @@ class ClassPreparationController extends BaseController
$now = utc_now();
$count = 0;
foreach ($ids as $classSectionId) {
$studentQ = $this->studentClassModel
->select('COUNT(DISTINCT student_id) AS cnt')
->where('class_section_id', $classSectionId)
->where('school_year', $schoolYear);
$studentQ = $this->db->table('student_class sc')
->select('COUNT(DISTINCT sc.student_id) AS cnt', false)
->join('students s', 's.id = sc.student_id', 'inner')
->where('s.is_active', 1)
->where('sc.class_section_id', $classSectionId)
->where('sc.school_year', $schoolYear);
if ($limitToSemester && $semester !== '') {
$studentQ->where('semester', $semester);
$studentQ->where('sc.semester', $semester);
}
$studentRow = $studentQ->first();
$studentRow = $studentQ->get()->getRowArray();
$studentCount = (int)($studentRow['cnt'] ?? 0);
$classLevel = $this->getClassLevelBySection((string)$classSectionId);
$className = $this->classSectionModel->getClassSectionNameBySectionId($classSectionId) ?? $classSectionId;
@@ -71,12 +71,15 @@ class ParentAttendanceReportController extends BaseController
->orderBy('s.lastname', 'ASC')
->get()->getResultArray();
$cutoffThreshold = $this->normalizeCutoffTime((string) $this->configModel->getConfig('parent_report_cutoff'));
return view('parent/report_attendance', [
'students' => $students,
'today' => local_date(utc_now(), 'Y-m-d'),
'sundays' => $sundays, // array of ['value'=>Y-m-d, 'label'=>...] items
'defaultDate' => $defaultDate, // preselected date (upcoming Sunday)
'myReports' => $previewRows,
'cutoffThreshold' => $cutoffThreshold,
]);
}
@@ -141,6 +144,17 @@ class ParentAttendanceReportController extends BaseController
// Enforce Sunday-only and future-or-today selection
$todayCheck = new \DateTime('today');
$cutoffThreshold = $this->normalizeCutoffTime((string) $this->configModel->getConfig('parent_report_cutoff'));
$tzName = (string) (config('School')->attendance['timezone'] ?? user_timezone());
$tz = null;
$nowTz = null;
try {
$tz = new \DateTimeZone($tzName ?: 'UTC');
$nowTz = new \DateTime('now', $tz);
} catch (\Throwable $e) {
$tz = null;
$nowTz = null;
}
$validDates = [];
$cap = $this->firstSundayOfJune((string) $this->configModel->getConfig('school_year'));
@@ -150,9 +164,11 @@ class ParentAttendanceReportController extends BaseController
$lateTodayStr = $todayCheck->format('Y-m-d');
if ($type === 'late') {
try {
$tzName = (string) (config('School')->attendance['timezone'] ?? user_timezone());
$tz = new \DateTimeZone($tzName ?: 'UTC');
$lateNow = new \DateTime('now', $tz);
if (!$tz) {
$tzName = (string) (config('School')->attendance['timezone'] ?? user_timezone());
$tz = new \DateTimeZone($tzName ?: 'UTC');
}
$lateNow = $nowTz ?: new \DateTime('now', $tz);
$cm = $this->configModel;
$cutoffStr = (string) ($cm->getConfig('late_report_cutoff')
?: $cm->getConfig('late_cutoff_time')
@@ -183,6 +199,17 @@ class ParentAttendanceReportController extends BaseController
if ($dt < $todayCheck) {
return redirect()->back()->withInput()->with('error', 'Past dates are not allowed. Please select today or a future Sunday.');
}
if ($nowTz && $tz) {
try {
$cutoff = new \DateTime($entry . ' ' . $cutoffThreshold . ':00', $tz);
if ($nowTz >= $cutoff) {
$abbr = $cutoff->format('T');
return redirect()->back()->withInput()->with('error', 'Reporting closes at ' . $cutoffThreshold . ' ' . $abbr . ' on the report date.');
}
} catch (\Throwable $e) {
// ignore and continue
}
}
if ($dt > $cap) {
return redirect()->back()->withInput()->with('error', 'The last available date is the first Sunday of June.');
}
@@ -800,6 +827,36 @@ class ParentAttendanceReportController extends BaseController
$update['reported'] = 1;
}
$this->attendanceDataModel->update((int) $existing['id'], $update);
return;
}
if ($existingStatus === '' || $existingStatus === 'present') {
$update = [
'status' => $status,
'reason' => $existing['reason'] ?: $reasonText,
'updated_at' => utc_now(),
];
if ($hasIsReported) {
$update['is_reported'] = 'yes';
}
if ($hasLegacyReported) {
$update['reported'] = 1;
}
$this->attendanceDataModel->update((int) $existing['id'], $update);
$classSectionId = (int) ($existing['class_section_id'] ?? $classSectionId ?? 0);
$schoolId = (string) ($existing['school_id'] ?? '');
if ($classSectionId > 0 && $schoolId !== '') {
$this->adjustAttendanceRecordForStatusChange(
studentId: $studentId,
classSectionId: $classSectionId,
schoolId: $schoolId,
semester: $semester,
schoolYear: $schoolYear,
oldStatus: $existingStatus,
newStatus: $status
);
}
}
}
@@ -856,6 +913,61 @@ class ParentAttendanceReportController extends BaseController
$this->attendanceRecordModel->insert($insert);
}
private function adjustAttendanceRecordForStatusChange(
int $studentId,
int $classSectionId,
string $schoolId,
string $semester,
string $schoolYear,
string $oldStatus,
string $newStatus
): void {
$oldStatus = strtolower($oldStatus);
$newStatus = strtolower($newStatus);
if ($oldStatus === $newStatus) {
return;
}
$record = $this->attendanceRecordModel->where([
'student_id' => $studentId,
'semester' => $semester,
'school_year' => $schoolYear,
])->first();
if (!$record) {
$this->bumpAttendanceRecordFromParent($studentId, $classSectionId, $schoolId, $newStatus, $semester, $schoolYear);
return;
}
$updates = [
'total_presence' => (int) ($record['total_presence'] ?? 0),
'total_absence' => (int) ($record['total_absence'] ?? 0),
'total_late' => (int) ($record['total_late'] ?? 0),
'total_attendance' => (int) ($record['total_attendance'] ?? 0),
'updated_at' => utc_now(),
'modified_by' => (int) (session()->get('user_id') ?? 0),
];
if ($oldStatus === 'present') {
$updates['total_presence'] = max(0, $updates['total_presence'] - 1);
} elseif ($oldStatus === 'absent') {
$updates['total_absence'] = max(0, $updates['total_absence'] - 1);
} elseif ($oldStatus === 'late') {
$updates['total_late'] = max(0, $updates['total_late'] - 1);
}
if ($newStatus === 'present') {
$updates['total_presence']++;
} elseif ($newStatus === 'absent') {
$updates['total_absence']++;
} elseif ($newStatus === 'late') {
$updates['total_late']++;
}
$this->attendanceRecordModel->update((int) $record['id'], $updates);
}
// GET: staff view to list reports (today & upcoming by default)
public function list()
{
@@ -1503,6 +1615,20 @@ class ParentAttendanceReportController extends BaseController
return $userId; // fallback
}
private function normalizeCutoffTime(string $raw, string $default = '09:00'): string
{
$raw = trim($raw);
if ($raw === '') {
return $default;
}
if (preg_match('/^(\d{1,2}):(\d{2})$/', $raw, $m)) {
$hh = str_pad((string) ((int) $m[1]), 2, '0', STR_PAD_LEFT);
$mm = $m[2];
return $hh . ':' . $mm;
}
return $default;
}
/**
* Send confirmation/notification emails for parent attendance submissions.
*/