context->currentConfig(); $semester = (string) ($cfg['semester'] ?? ''); $schoolYear = (string) ($cfg['school_year'] ?? ''); $assignmentRows = DB::table('teacher_class as tc') ->select([ 'tc.class_section_id', 'cs.class_section_name', 'tc.teacher_id', 'u.firstname', 'u.lastname', 'tc.position', ]) ->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'tc.class_section_id') ->leftJoin('users as u', 'u.id', '=', 'tc.teacher_id') ->where('tc.school_year', $schoolYear) ->where('tc.semester', $semester) ->orderBy('cs.class_section_name') ->get(); $teachersBySection = []; foreach ($assignmentRows as $assignment) { $sectionId = (int) ($assignment->class_section_id ?? 0); if ($sectionId <= 0) { continue; } $positionKey = strtolower(trim((string) ($assignment->position ?? ''))); $roleKey = $positionKey !== '' ? $positionKey : 'teacher'; $positionLabel = match ($roleKey) { 'ta' => 'TA', 'main' => 'Main', default => $roleKey !== '' ? ucfirst($roleKey) : 'Teacher', }; $teacherFullName = trim( (string)($assignment->firstname ?? '') . ' ' . (string)($assignment->lastname ?? '') ); $teacherId = (int) ($assignment->teacher_id ?? 0); if ($teacherId <= 0 || $teacherFullName === '') { continue; } if (!isset($teachersBySection[$sectionId])) { $teachersBySection[$sectionId] = [ 'class_section' => (string) ($assignment->class_section_name ?? "Section {$sectionId}"), 'teachers' => [], ]; } $teachersBySection[$sectionId]['teachers'][] = [ 'id' => $teacherId, 'label' => "{$positionLabel}: {$teacherFullName}", 'role_key' => $roleKey, ]; } $today = Carbon::now()->format('Y-m-d'); $rows = []; $totalStatuses = 0; $missingItemCount = 0; $allTeacherIds = []; $allClassSectionIds = []; foreach ($teachersBySection as $classSectionId => $section) { $classSectionId = (int) $classSectionId; if ($classSectionId <= 0) { continue; } $studentIds = DB::table('student_class') ->where('class_section_id', $classSectionId) ->where('semester', $semester) ->where('school_year', $schoolYear) ->pluck('student_id') ->map(fn ($id) => (int) $id) ->filter(fn ($id) => $id > 0) ->values() ->all(); $expected = count($studentIds); // semester_scores $midtermStudents = []; $participationStudents = []; $scoreRecords = DB::table('semester_scores') ->select('student_id', 'midterm_exam_score', 'participation_score') ->where('class_section_id', $classSectionId) ->where('semester', $semester) ->where('school_year', $schoolYear) ->get(); foreach ($scoreRecords as $score) { $sid = (int) ($score->student_id ?? 0); if ($sid <= 0 || ($expected > 0 && !in_array($sid, $studentIds, true))) { continue; } $midtermValue = trim((string) ($score->midterm_exam_score ?? '')); if ($midtermValue !== '') { $midtermStudents[$sid] = true; } $participationValue = trim((string) ($score->participation_score ?? '')); if ($participationValue !== '') { $participationStudents[$sid] = true; } } // score_comments $midtermCommentStudents = []; $ptapCommentStudents = []; if (!empty($studentIds)) { $comments = DB::table('score_comments') ->select('student_id', 'score_type', 'comment') ->whereIn('student_id', $studentIds) ->where('semester', $semester) ->where('school_year', $schoolYear) ->whereIn('score_type', ['midterm', 'ptap']) ->get(); foreach ($comments as $comment) { $sid = (int) ($comment->student_id ?? 0); if ($sid <= 0) { continue; } $text = trim((string) ($comment->comment ?? '')); if ($text === '') { continue; } $type = strtolower(trim((string) ($comment->score_type ?? ''))); if ($type === 'midterm') { $midtermCommentStudents[$sid] = true; } if ($type === 'ptap') { $ptapCommentStudents[$sid] = true; } } } // attendance_days (today) $attendanceRow = DB::table('attendance_days') ->where('class_section_id', $classSectionId) ->where('semester', $semester) ->where('school_year', $schoolYear) ->where('date', $today) ->first(); $attendanceSubmitted = $attendanceRow && in_array( strtolower((string) ($attendanceRow->status ?? '')), ['submitted', 'published', 'finalized'], true ); $teacherList = $section['teachers'] ?? []; if (!empty($teacherList)) { usort($teacherList, function (array $a, array $b) { return $this->teacherRolePriority($a['role_key'] ?? 'teacher') <=> $this->teacherRolePriority($b['role_key'] ?? 'teacher'); }); $teacherList = array_values($teacherList); } foreach ($teacherList as $teacherEntry) { if (!empty($teacherEntry['id'])) { $allTeacherIds[] = (int) $teacherEntry['id']; } } $allClassSectionIds[] = $classSectionId; $midtermScoreStatus = $this->submissionStatus(count($midtermStudents), $expected); $midtermCommentStatus = $this->submissionStatus(count($midtermCommentStudents), $expected); $participationStatus = $this->submissionStatus(count($participationStudents), $expected); $ptapCommentStatus = $this->submissionStatus(count($ptapCommentStudents), $expected); $attendanceStatus = $this->attendanceStatus($attendanceSubmitted); // CI original intentionally only counted score/comment statuses in summary (not attendance) $statusDetails = [ 'midterm_score_status' => $midtermScoreStatus, 'midterm_comment_status' => $midtermCommentStatus, 'participation_status' => $participationStatus, 'ptap_comment_status' => $ptapCommentStatus, ]; $missingItemsForSection = $this->buildMissingItems($statusDetails); $missingItemCount += count($missingItemsForSection); $totalStatuses += count($statusDetails); $rows[] = [ 'class_section' => $section['class_section'] ?? "Section {$classSectionId}", 'class_section_id' => $classSectionId, 'teachers' => $teacherList, 'midterm_score_status' => $midtermScoreStatus, 'midterm_comment_status' => $midtermCommentStatus, 'participation_status' => $participationStatus, 'ptap_comment_status' => $ptapCommentStatus, 'attendance_status' => $attendanceStatus, 'missing_items' => $missingItemsForSection, 'student_count' => $expected, ]; } $historyMap = []; $teacherIds = array_values(array_unique($allTeacherIds)); $classSectionIds = array_values(array_unique($allClassSectionIds)); if (!empty($teacherIds) && !empty($classSectionIds) && $this->hasTable('teacher_submission_notification_history')) { $historyRecords = DB::table('teacher_submission_notification_history as h') ->select([ 'h.*', 'u.firstname', 'u.lastname', ]) ->leftJoin('users as u', 'u.id', '=', 'h.admin_id') ->where('h.notification_category', 'teacher_submissions') ->whereIn('h.teacher_id', $teacherIds) ->whereIn('h.class_section_id', $classSectionIds) ->orderByDesc('h.sent_at') ->get(); foreach ($historyRecords as $record) { $sectionId = (int) ($record->class_section_id ?? 0); $teacherId = (int) ($record->teacher_id ?? 0); if ($sectionId <= 0 || $teacherId <= 0) { continue; } $sentAt = $record->sent_at ?? null; $sentAtText = $sentAt ? Carbon::parse($sentAt)->format('M j, Y g:i A') : ''; $adminName = trim((string)($record->firstname ?? '') . ' ' . (string)($record->lastname ?? '')); if ($adminName === '') { $adminName = 'Administrator'; } $historyMap[$sectionId][$teacherId][] = [ 'sent_at_text' => $sentAtText, 'admin_name' => $adminName, 'status' => strtolower((string) ($record->status ?? 'sent')), ]; } // keep latest 3 per section/teacher, same as CI foreach ($historyMap as &$teachersHistory) { foreach ($teachersHistory as &$entries) { $entries = array_slice($entries, 0, 3); } unset($entries); } unset($teachersHistory); } $summary = [ 'total_items' => $totalStatuses, 'missing_items' => $missingItemCount, 'submitted_items' => max(0, $totalStatuses - $missingItemCount), 'submission_percentage' => $totalStatuses > 0 ? (int) round((($totalStatuses - $missingItemCount) / $totalStatuses) * 100) : 100, ]; return [ 'semester' => $semester, 'school_year' => $schoolYear, 'rows' => $rows, 'notification_history' => $historyMap, 'summary' => $summary, ]; } /** * API payload format expected: * [ * 'notify' => [sectionId => [teacherId => 1, ...], ...], * 'missing_items' => [sectionId => [teacherId => base64(json(items))]] * ] */ public function sendNotifications(int $adminId, array $payload): array { $notify = $payload['notify'] ?? null; if (!is_array($notify)) { return [ 'message' => 'Select at least one teacher to notify.', 'sent' => 0, 'failed' => 0, 'results' => [], ]; } $missingItemsPayload = is_array($payload['missing_items'] ?? null) ? $payload['missing_items'] : []; $targets = []; foreach ($notify as $sectionIdRaw => $teachers) { $sectionId = (int) $sectionIdRaw; if ($sectionId <= 0 || !is_array($teachers)) { continue; } foreach ($teachers as $teacherIdRaw => $value) { $teacherId = (int) $teacherIdRaw; if ($teacherId <= 0 || $value === null || $value === '') { continue; } $targets["{$sectionId}_{$teacherId}"] = [ 'class_section_id' => $sectionId, 'teacher_id' => $teacherId, ]; } } if (empty($targets)) { return [ 'message' => 'Select at least one teacher to notify.', 'sent' => 0, 'failed' => 0, 'results' => [], ]; } $targets = array_values($targets); $teacherIds = array_values(array_unique(array_map( fn ($t) => (int) $t['teacher_id'], $targets ))); $classSectionIds = array_values(array_unique(array_map( fn ($t) => (int) $t['class_section_id'], $targets ))); $classSectionMap = DB::table('classSection') ->select('class_section_id', 'class_section_name') ->whereIn('class_section_id', $classSectionIds) ->get() ->mapWithKeys(fn ($row) => [(int)$row->class_section_id => (string)($row->class_section_name ?? '')]) ->all(); $teacherLookup = DB::table('users') ->select('id', 'firstname', 'lastname', 'email') ->whereIn('id', $teacherIds) ->get() ->keyBy(fn ($row) => (int) $row->id); $adminUser = DB::table('users')->select('id', 'firstname', 'lastname')->where('id', $adminId)->first(); $adminName = $adminUser ? trim((string)($adminUser->firstname ?? '') . ' ' . (string)($adminUser->lastname ?? '')) : ''; if ($adminName === '') { $adminName = 'Administrator'; } $cfg = $this->context->currentConfig(); $scoreUrl = rtrim((string) config('app.url'), '/') . '/'; $sentCount = 0; $failCount = 0; $results = []; foreach ($targets as $target) { $classSectionId = (int) $target['class_section_id']; $teacherId = (int) $target['teacher_id']; $teacher = $teacherLookup->get($teacherId); $sectionName = $classSectionMap[$classSectionId] ?? "Section {$classSectionId}"; $teacherName = $teacher ? trim((string)($teacher->firstname ?? '') . ' ' . (string)($teacher->lastname ?? '')) : ''; if ($teacherName === '') { $teacherName = 'Teacher'; } $subject = "Reminder: Complete submissions for {$sectionName}"; $missingPayload = $missingItemsPayload[$classSectionId][$teacherId] ?? ''; $missingItems = $this->parseMissingItemsPayload((string) $missingPayload); if (!empty($missingItems)) { $missingText = e($this->formatMissingItemsText($missingItems)); $missingNote = "

Outstanding items: {$missingText}.

"; } else { $missingNote = "

Our records show no outstanding submissions for this section, but please verify if anything still needs attention.

"; } $body = "

Dear {$teacherName},

" . "

Administration is gently reminding you to wrap up any remaining score submissions and/or comments for {$sectionName}.

" . $missingNote . "

Visit Teacher Score Submission to address any remaining items.

" . "

Thank you,
Al Rahma Administration

"; $email = (string) ($teacher->email ?? ''); $status = 'failed'; $error = null; if ($email !== '' && filter_var($email, FILTER_VALIDATE_EMAIL)) { try { Mail::html($body, function ($message) use ($email, $subject) { $message->to($email)->subject($subject); }); $status = 'sent'; } catch (Throwable $e) { $status = 'failed'; $error = $e->getMessage(); } } else { $error = 'Missing or invalid email'; } if ($status === 'sent') { $sentCount++; } else { $failCount++; } if ($this->hasTable('teacher_submission_notification_history')) { DB::table('teacher_submission_notification_history')->insert([ 'teacher_id' => $teacherId, 'class_section_id' => $classSectionId, 'admin_id' => $adminId, 'notification_category' => 'teacher_submissions', 'message' => $this->truncateNotificationMessage($body), 'status' => $status, 'school_year' => $cfg['school_year'] ?? null, 'semester' => $cfg['semester'] ?? null, 'sent_at' => Carbon::now('UTC'), ]); } $results[] = [ 'teacher_id' => $teacherId, 'class_section_id' => $classSectionId, 'teacher_name' => $teacherName, 'section_name' => $sectionName, 'email' => $email, 'status' => $status, 'missing_items' => $missingItems, 'error' => $error, ]; } $parts = []; if ($sentCount > 0) { $parts[] = $sentCount . ' reminder' . ($sentCount === 1 ? '' : 's') . ' sent'; } if ($failCount > 0) { $parts[] = $failCount . ' reminder' . ($failCount === 1 ? '' : 's') . ' failed'; } return [ 'message' => !empty($parts) ? implode(' and ', $parts) : 'No notifications were sent.', 'sent' => $sentCount, 'failed' => $failCount, 'results' => $results, ]; } private function submissionStatus(int $filled, int $expected): array { if ($expected <= 0) { return [ 'label' => 'No students', 'badge' => 'bg-secondary', 'detail' => '', 'completed' => true, ]; } $completed = $filled >= $expected; return [ 'label' => $completed ? 'Submitted' : 'Missing', 'badge' => $completed ? 'bg-success' : 'bg-danger', 'detail' => "{$filled}/{$expected}", 'completed' => $completed, ]; } private function attendanceStatus(bool $submitted): array { return [ 'label' => $submitted ? 'Submitted' : 'Missing', 'badge' => $submitted ? 'bg-success' : 'bg-danger', 'completed' => $submitted, ]; } private function buildMissingItems(array $statusMap): array { $labels = [ 'midterm_score_status' => 'midterm scores', 'midterm_comment_status' => 'midterm comments', 'participation_status' => 'participation', 'ptap_comment_status' => 'PTAP comments', 'attendance_status' => 'attendance', ]; $items = []; foreach ($statusMap as $key => $status) { $completed = (bool) ($status['completed'] ?? true); if (!$completed && isset($labels[$key])) { $items[] = $labels[$key]; } } return array_values($items); } private function formatMissingItemsText(array $items): string { $items = array_values(array_unique(array_filter(array_map( fn ($v) => trim((string)$v), $items )))); $count = count($items); if ($count === 0) return ''; if ($count === 1) return $items[0]; if ($count === 2) return $items[0] . ' and ' . $items[1]; $last = array_pop($items); return implode(', ', $items) . ' and ' . $last; } private function teacherRolePriority(string $roleKey): int { return match (strtolower($roleKey)) { 'main' => 1, 'ta' => 2, default => 3, }; } private function truncateNotificationMessage(string $html, int $limit = 1000): string { $text = trim(strip_tags($html)); if ($text === '') { return ''; } return mb_strlen($text) <= $limit ? $text : mb_substr($text, 0, $limit) . '…'; } private function parseMissingItemsPayload(string $payload): array { if ($payload === '') { return []; } $decodedRaw = base64_decode($payload, true); if ($decodedRaw === false) { return []; } $decoded = json_decode($decodedRaw, true); if (!is_array($decoded)) { return []; } $items = []; foreach ($decoded as $item) { $item = trim((string) $item); if ($item !== '') { $items[] = $item; } } return array_values(array_unique($items)); } private function hasTable(string $table): bool { try { return DB::getSchemaBuilder()->hasTable($table); } catch (Throwable) { return false; } } }