Files
alrahma_sunday_school_api/app/Services/Frontend/LandingPageTeacherSummaryService.php
T
2026-03-11 17:53:15 -04:00

596 lines
22 KiB
PHP
Raw 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\Services\Frontend;
use App\Models\AttendanceDay;
use App\Models\AttendanceRecord;
use App\Models\Calendar;
use App\Models\ClassSection;
use App\Models\ScoreComment;
use App\Models\SemesterScore;
use App\Models\StudentClass;
use App\Models\TeacherClass;
use DateTimeImmutable;
use DateTimeZone;
use Illuminate\Support\Facades\DB;
class LandingPageTeacherSummaryService
{
public function __construct(private LandingPageContextService $context)
{
}
public function summary(int $teacherId, ?int $requestedClassSectionId = null): array
{
$context = $this->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 dont 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),
];
}
}