context->context(); $schoolYear = (string) ($context['school_year'] ?? ''); $semester = (string) ($context['semester'] ?? ''); $assignments = TeacherClass::getClassAssignmentsByUserId($teacherId, $schoolYear, $semester); $availableIds = array_values(array_filter(array_unique(array_map( static fn (array $row) => (int) ($row['class_section_id'] ?? 0), $assignments )))); $activeId = null; if ($requestedClassSectionId && in_array($requestedClassSectionId, $availableIds, true)) { $activeId = $requestedClassSectionId; } elseif (! empty($availableIds)) { $activeId = $availableIds[0]; } $activeName = null; foreach ($assignments as $row) { if ((int) ($row['class_section_id'] ?? 0) === (int) $activeId) { $activeName = $row['class_section_name'] ?? null; break; } } $normalizedSemester = ucfirst(strtolower(trim((string) $semester))); if (! in_array($normalizedSemester, ['Fall', 'Spring'], true)) { $normalizedSemester = 'Fall'; } $classSectionIds = $availableIds; $dashboardSummary = $this->buildTeacherDashboardSummary($classSectionIds, $normalizedSemester, $schoolYear); return [ 'class_section_id' => $activeId, 'active_class_name' => $activeName, 'classes' => $assignments, 'jobCount' => count($assignments), 'scoreSummary' => $dashboardSummary['scoreSummary'], 'commentSummary' => $dashboardSummary['commentSummary'], 'attendanceSummary' => $dashboardSummary['attendanceSummary'], 'deadlineEvents' => $dashboardSummary['deadlineEvents'], 'participationSummary' => $dashboardSummary['participationSummary'], 'attendanceStatus' => $dashboardSummary['attendanceStatus'], 'dashboardNotifications' => $dashboardSummary['notifications'], 'school_year' => $schoolYear, 'semester' => $semester, ]; } private function buildTeacherDashboardSummary(array $classSectionIds, string $semester, string $schoolYear): array { $scoreField = $semester === 'Spring' ? 'final_exam_score' : 'midterm_exam_score'; $scoreLabel = $semester === 'Spring' ? 'Final Exam' : 'Midterm'; $summary = [ 'scoreSummary' => [ 'completionPct' => 0, 'missing' => 0, 'expected' => 0, 'filled' => 0, 'fieldLabel' => $scoreLabel, ], 'commentSummary' => [ 'total' => 0, 'pendingReview' => 0, 'reviewed' => 0, 'byType' => [], ], 'attendanceSummary' => [ 'recorded' => 0, 'students' => 0, 'completionPct' => 0, 'avgAbsences' => 0, ], 'participationSummary' => [ 'filled' => 0, 'missing' => 0, 'completionPct' => 0, 'expected' => 0, ], 'deadlineEvents' => [], 'notifications' => [], 'attendanceStatus' => [ 'date' => '', 'missingSections' => [], 'missingNames' => [], 'submitted' => true, ], ]; $uniqueStudentIds = []; if (! empty($classSectionIds)) { $studentRows = DB::table('student_class as sc') ->select('sc.student_id', 'sc.class_section_id') ->join('students as s', 's.id', '=', 'sc.student_id') ->whereIn('sc.class_section_id', $classSectionIds) ->where('s.is_active', 1) ->where('sc.school_year', $schoolYear) ->get(); foreach ($studentRows as $row) { $studentId = (int) ($row->student_id ?? 0); if ($studentId > 0) { $uniqueStudentIds[] = $studentId; } } } $uniqueStudentIds = array_values(array_unique($uniqueStudentIds)); $totalStudents = count($uniqueStudentIds); $summary['attendanceSummary']['students'] = $totalStudents; $scoreRows = []; if (! empty($classSectionIds)) { $scoreRows = SemesterScore::query() ->select(['student_id', 'class_section_id', 'midterm_exam_score', 'final_exam_score', 'participation_score']) ->whereIn('class_section_id', $classSectionIds) ->where('semester', $semester) ->where('school_year', $schoolYear) ->get() ->toArray(); } $scoreTypesConfig = [ 'participation' => ['label' => 'Participation', 'field' => 'participation_score'], ]; if ($semester === 'Fall') { $scoreTypesConfig = array_merge([ 'midterm' => ['label' => 'Midterm', 'field' => 'midterm_exam_score'], ], $scoreTypesConfig); } if ($semester === 'Spring') { $scoreTypesConfig = array_merge([ 'final' => ['label' => 'Final exam', 'field' => 'final_exam_score'], ], $scoreTypesConfig); } $filledCounts = array_fill_keys(array_keys($scoreTypesConfig), 0); foreach ($scoreRows as $row) { foreach ($scoreTypesConfig as $key => $cfg) { $value = trim((string) ($row[$cfg['field']] ?? '')); if ($value !== '') { $filledCounts[$key]++; } } } $expectedScoreCount = $totalStudents > 0 ? $totalStudents : count($scoreRows); $scoreDetails = []; foreach ($scoreTypesConfig as $key => $cfg) { $filled = $filledCounts[$key] ?? 0; $missing = max(0, $expectedScoreCount - $filled); $completionPct = $expectedScoreCount > 0 ? (int) round(min(100, ($filled / $expectedScoreCount) * 100)) : 0; $scoreDetails[$key] = [ 'label' => $cfg['label'], 'field' => $cfg['field'], 'filled' => $filled, 'missing' => $missing, 'expected' => $expectedScoreCount, 'completionPct' => $completionPct, ]; } $scoreTypeByField = []; if ($semester === 'Fall') { $scoreTypeByField['midterm_exam_score'] = 'midterm'; } if ($semester === 'Spring') { $scoreTypeByField['final_exam_score'] = 'final'; } $activeScoreType = $scoreTypeByField[$scoreField] ?? array_key_first($scoreDetails); $activeScoreDetails = $scoreDetails[$activeScoreType] ?? array_values($scoreDetails)[0]; $summary['scoreSummary'] = [ 'completionPct' => $activeScoreDetails['completionPct'], 'missing' => $activeScoreDetails['missing'], 'expected' => $activeScoreDetails['expected'], 'filled' => $activeScoreDetails['filled'], 'fieldLabel' => $scoreLabel, 'types' => $scoreDetails, ]; $participationMetrics = $scoreDetails['participation']; $summary['participationSummary'] = [ 'filled' => $participationMetrics['filled'], 'missing' => $participationMetrics['missing'], 'completionPct' => $participationMetrics['completionPct'], 'expected' => $participationMetrics['expected'], ]; $commentRows = []; if (! empty($uniqueStudentIds)) { $commentRows = ScoreComment::query() ->select(['student_id', 'score_type', 'comment', 'comment_review']) ->whereIn('student_id', $uniqueStudentIds) ->where('semester', $semester) ->where('school_year', $schoolYear) ->get() ->toArray(); } $expectedCommentTypes = array_values(array_filter([ 'ptap', $semester === 'Fall' ? 'midterm' : null, $semester === 'Spring' ? 'final' : null, ])); $commentStats = []; $commentsByStudent = []; foreach ($commentRows as $row) { $sid = (int) ($row['student_id'] ?? 0); $type = strtolower(trim((string) ($row['score_type'] ?? ''))); if ($type === '') { $type = 'general'; } if (! isset($commentStats[$type])) { $commentStats[$type] = ['pending' => 0, 'reviewed' => 0]; } $commentText = trim((string) ($row['comment'] ?? '')); $reviewValue = trim((string) ($row['comment_review'] ?? '')); if ($sid > 0 && $commentText !== '') { $commentsByStudent[$sid][$type] = $commentText; } if ($commentText === '') { continue; } if ($reviewValue === '') { $commentStats[$type]['pending']++; } else { $commentStats[$type]['reviewed']++; } } foreach ($expectedCommentTypes as $type) { if (! isset($commentStats[$type])) { $commentStats[$type] = ['pending' => 0, 'reviewed' => 0]; } } $missingComments = 0; $missingDetail = []; foreach ($uniqueStudentIds as $sid) { foreach ($expectedCommentTypes as $type) { $text = $commentsByStudent[$sid][$type] ?? ''; if ($text === '') { $missingDetail[$type] = ($missingDetail[$type] ?? 0) + 1; $missingComments++; } } } $commentTypes = []; foreach ($expectedCommentTypes as $type) { $stats = $commentStats[$type]; $filled = $stats['pending'] + $stats['reviewed']; $completionPct = $totalStudents > 0 ? (int) round(min(100, ($filled / $totalStudents) * 100)) : 0; $commentTypes[$type] = [ 'label' => ucfirst($type).' comments', 'pending' => $stats['pending'], 'reviewed' => $stats['reviewed'], 'filled' => $filled, 'missing' => $missingDetail[$type] ?? 0, 'expected' => $totalStudents, 'completionPct' => $completionPct, ]; } $totalPending = array_sum(array_column($commentTypes, 'pending')); $totalReviewed = array_sum(array_column($commentTypes, 'reviewed')); $totalFilled = array_sum(array_column($commentTypes, 'filled')); $totalExpected = array_sum(array_column($commentTypes, 'expected')); $commentCompletionPct = $totalExpected > 0 ? (int) round(min(100, ($totalFilled / $totalExpected) * 100)) : 0; $summary['commentSummary'] = [ 'total' => $totalFilled, 'pendingReview' => $totalPending, 'reviewed' => $totalReviewed, 'missing' => $missingComments, 'missingDetail' => $missingDetail, 'completionPct' => $commentCompletionPct, 'types' => $commentTypes, ]; $attendanceRows = []; if (! empty($classSectionIds)) { $attendanceRows = AttendanceRecord::query() ->select(['student_id', 'class_section_id', 'total_absence']) ->whereIn('class_section_id', $classSectionIds) ->where('semester', $semester) ->where('school_year', $schoolYear) ->get() ->toArray(); } $attendanceSet = []; $totalAbsences = 0; foreach ($attendanceRows as $record) { $studentId = (int) ($record['student_id'] ?? 0); $sectionId = (int) ($record['class_section_id'] ?? 0); if ($studentId <= 0 || $sectionId <= 0) { continue; } $key = "{$sectionId}:{$studentId}"; $attendanceSet[$key] = true; $totalAbsences += (int) ($record['total_absence'] ?? 0); } $attendanceRecorded = count($attendanceSet); $attendancePct = $totalStudents > 0 ? (int) round(min(100, ($attendanceRecorded / $totalStudents) * 100)) : 0; $avgAbsence = $attendanceRecorded > 0 ? round($totalAbsences / $attendanceRecorded, 1) : 0; $summary['attendanceSummary'] = [ 'recorded' => $attendanceRecorded, 'students' => $totalStudents, 'completionPct' => $attendancePct, 'avgAbsences' => $avgAbsence, ]; $timezone = new DateTimeZone(date_default_timezone_get() ?: 'UTC'); $today = (new DateTimeImmutable('now', $timezone))->format('Y-m-d'); $attendanceStatus = $this->buildAttendanceSubmissionStatus($classSectionIds, $semester, $schoolYear, $today); $summary['attendanceStatus'] = $attendanceStatus; $summary['deadlineEvents'] = Calendar::query() ->where('notify_teacher', 1) ->where('school_year', $schoolYear) ->where('semester', $semester) ->where('date', '>=', $today) ->orderBy('date', 'ASC') ->limit(5) ->get() ->toArray(); if ($attendanceStatus['submitted'] === false && ! empty($classSectionIds)) { $summary['attendanceSummary']['completionPct'] = 0; $summary['attendanceSummary']['recorded'] = 0; } $summary['notifications'] = $this->buildDashboardNotifications($summary); return $summary; } private function buildDashboardNotifications(array $summary): array { $notifications = []; $buildScoreLink = static function (?string $focus): string { $base = '/teacher/scores'; return $focus ? $base.'?focus='.urlencode($focus) : $base; }; $determineFocus = static function (array $event): string { $eventTypeText = strtolower(trim((string) ($event['event_type'] ?? ''))); if ($eventTypeText !== '' && str_contains($eventTypeText, 'draft')) { return 'comments'; } $text = strtolower(trim(($event['title'] ?? '').' '.($event['description'] ?? ''))); if ($text === '') { return 'scores'; } if (str_contains($text, 'comment') || str_contains($text, 'ptap')) { return 'comments'; } return 'scores'; }; $calendarLink = '/teacher/calendar'; $attendanceLink = '/teacher/showupdate_attendance'; $timezone = new DateTimeZone(date_default_timezone_get() ?: 'UTC'); $todayDate = (new DateTimeImmutable('now', $timezone))->setTime(0, 0); $calendarNotificationConfig = [ '1st Semester Scores' => 14, '2nd Semester Scores' => 14, 'Final' => 14, 'Final Exam Draft' => 42, 'Midterm' => 14, 'Midterm Exam Draft' => 42, ]; $activeDeadline = false; foreach ($summary['deadlineEvents'] ?? [] as $event) { $eventTypeRaw = trim((string) ($event['event_type'] ?? '')); if ($eventTypeRaw === '') { continue; } $matchedType = null; $thresholdDays = null; foreach ($calendarNotificationConfig as $type => $days) { if (strcasecmp($eventTypeRaw, $type) === 0) { $matchedType = $type; $thresholdDays = $days; break; } } if ($matchedType === null || $thresholdDays === null) { continue; } $rawDate = $event['date'] ?? ''; if ($rawDate === '') { continue; } try { $eventDateTime = new DateTimeImmutable($rawDate, $timezone); } catch (\Exception $e) { continue; } $eventDateTime = $eventDateTime->setTime(0, 0); $daysUntil = (int) $todayDate->diff($eventDateTime)->format('%r%a'); if ($daysUntil <= 0) { $activeDeadline = true; $eventFocus = $determineFocus($event); $notifications[] = [ 'type' => 'danger', 'message' => sprintf( 'Deadline today: %s is due. Submit the remaining entries before the day ends.', $matchedType ), 'link' => $buildScoreLink($eventFocus), 'linkText' => 'Submit now', ]; continue; } if ($daysUntil > $thresholdDays) { continue; } $relativeText = $daysUntil === 1 ? ' (tomorrow)' : " (in {$daysUntil} days)"; $notifications[] = [ 'type' => 'warning', 'message' => sprintf( '%s is due on %s%s. Review it on the calendar so you don’t miss it.', $matchedType, $eventDateTime->format('M j, Y'), $relativeText ), 'link' => $calendarLink, 'linkText' => 'View deadline', ]; } if (! $activeDeadline) { $scoreLabel = $summary['scoreSummary']['fieldLabel'] ?? 'Score'; if (! empty($summary['scoreSummary']['missing'] ?? 0)) { $notifications[] = [ 'type' => 'warning', 'message' => sprintf( 'You have %s %s entries missing on the scores page. Please finish submitting them.', (int) $summary['scoreSummary']['missing'], $scoreLabel ), 'link' => $buildScoreLink('scores'), 'linkText' => 'Score sheet', ]; } foreach (['midterm', 'final', 'ptap'] as $type) { $count = (int) ($summary['commentSummary']['missingDetail'][$type] ?? 0); if ($count <= 0) { continue; } $notifications[] = [ 'type' => 'warning', 'message' => sprintf( '%s comment missing for %s student(s). Leave comments on the scores page.', ucfirst($type), $count ), 'link' => $buildScoreLink('comments'), 'linkText' => ucfirst($type).' comments', ]; } if (! empty($summary['participationSummary']['missing'] ?? 0)) { $notifications[] = [ 'type' => 'warning', 'message' => sprintf( 'Participation entries are still missing for %s student(s). Please add them from the scores page.', (int) $summary['participationSummary']['missing'] ), 'link' => $buildScoreLink('scores'), 'linkText' => 'Participation', ]; } } if (! empty($summary['attendanceStatus']['missingSections'] ?? [])) { $missingNames = $summary['attendanceStatus']['missingNames'] ?? []; $label = ''; if (! empty($missingNames)) { $label = ' ('.implode(', ', array_slice($missingNames, 0, 3)).(count($missingNames) > 3 ? '…' : '').')'; } $notifications[] = [ 'type' => 'danger', 'message' => "Today's attendance is still unsubmitted for {$summary['attendanceStatus']['date']}{$label}. Please submit it now.", 'link' => $attendanceLink, 'linkText' => 'Record attendance', ]; } return $notifications; } private function buildAttendanceSubmissionStatus(array $classSectionIds, string $semester, string $schoolYear, string $date): array { if (empty($classSectionIds)) { return [ 'date' => $date, 'missingSections' => [], 'missingNames' => [], 'submitted' => true, ]; } $rows = AttendanceDay::query() ->select(['class_section_id', 'status']) ->whereIn('class_section_id', $classSectionIds) ->where('date', $date) ->where('semester', $semester) ->where('school_year', $schoolYear) ->get() ->toArray(); $statusMap = []; foreach ($rows as $row) { $csId = (int) ($row['class_section_id'] ?? 0); if ($csId <= 0) { continue; } $statusMap[$csId] = strtolower(trim((string) ($row['status'] ?? ''))); } $missingSections = []; foreach ($classSectionIds as $classSectionId) { $status = $statusMap[$classSectionId] ?? ''; if (! in_array($status, ['submitted', 'published', 'finalized'], true)) { $missingSections[] = $classSectionId; } } $missingNames = []; if (! empty($missingSections)) { $sectionRows = ClassSection::query() ->select(['class_section_id', 'class_section_name']) ->whereIn('class_section_id', $missingSections) ->get() ->toArray(); foreach ($sectionRows as $row) { $name = trim((string) ($row['class_section_name'] ?? '')); if ($name === '') { $name = (string) ($row['class_section_id'] ?? ''); } $missingNames[] = $name; } } return [ 'date' => $date, 'missingSections' => $missingSections, 'missingNames' => $missingNames, 'submitted' => empty($missingSections), ]; } }