select([ 's.id as student_id', 's.school_id', 's.firstname', 's.lastname', 'cs.class_section_name', 'ss.homework_avg', 'ss.project_avg', 'ss.participation_score', DB::raw('COALESCE(ss.test_avg, ss.quiz_avg) as test_avg'), 'ss.ptap_score', 'ss.attendance_score', 'ss.midterm_exam_score', 'ss.final_exam_score', 'ss.semester_score', ]) ->join('students as s', 's.id', '=', 'ss.student_id') ->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'ss.class_section_id') ->where('s.is_active', 1) ->where('ss.school_year', $schoolYear) ->whereRaw('LOWER(TRIM(ss.semester)) = ?', [$semesterKey]) ->whereNotNull('ss.semester_score') ->where('ss.semester_score', '<', 60) ->orderBy('s.lastname', 'ASC') ->orderBy('s.firstname', 'ASC') ->get() ->map(fn ($row) => (array) $row) ->all(); if (empty($rows)) { return []; } $studentIds = array_values(array_unique(array_map( static fn ($r) => (int) ($r['student_id'] ?? 0), $rows ))); $studentIds = array_values(array_filter($studentIds, static fn ($id) => $id > 0)); $commentMap = []; if (!empty($studentIds)) { $commentRows = ScoreComment::query() ->select('student_id', 'comment', 'created_at') ->where('score_type', 'general') ->where('school_year', $schoolYear) ->whereRaw('LOWER(TRIM(semester)) = ?', [$semesterKey]) ->whereIn('student_id', $studentIds) ->orderBy('created_at', 'DESC') ->get(); foreach ($commentRows as $row) { $sid = (int) $row->student_id; if ($sid > 0 && !isset($commentMap[$sid])) { $commentMap[$sid] = (string) ($row->comment ?? ''); } } } $statusMap = []; if (!empty($studentIds)) { $flagRows = CurrentFlag::query() ->select('student_id', 'flag_state') ->where('flag', 'grade') ->where('school_year', $schoolYear) ->whereRaw('LOWER(TRIM(semester)) = ?', [$semesterKey]) ->whereIn('student_id', $studentIds) ->get(); foreach ($flagRows as $row) { $sid = (int) $row->student_id; if ($sid > 0) { $statusMap[$sid] = (string) ($row->flag_state ?? ''); } } } foreach ($rows as &$row) { $sid = (int) ($row['student_id'] ?? 0); $row['comment'] = $commentMap[$sid] ?? ''; $flagState = strtolower(trim((string) ($statusMap[$sid] ?? ''))); $row['status'] = ($flagState === 'closed' || $flagState === 'canceled') ? 'Closed' : 'Open'; } unset($row); return $rows; } public function emailContext(int $studentId, string $schoolYear, string $semester): array { $row = $this->fetchBelowSixtyEmailRow($studentId, $schoolYear, $semester); if (empty($row)) { throw new RuntimeException('Student record not found for the selected term.'); } $studentName = trim((string) ($row['firstname'] ?? '') . ' ' . (string) ($row['lastname'] ?? '')); $subject = $this->buildBelowSixtySubject($studentName, $semester, $schoolYear); $parentName = $this->fetchBelowSixtyParentName($studentId); $scores = [ 'homework_avg' => $row['homework_avg'] ?? null, 'project_avg' => $row['project_avg'] ?? null, 'participation_score' => $row['participation_score'] ?? null, 'test_avg' => $row['test_avg'] ?? null, 'ptap_score' => $row['ptap_score'] ?? null, 'attendance_score' => $row['attendance_score'] ?? null, 'midterm_exam_score' => $row['midterm_exam_score'] ?? null, 'semester_score' => $row['semester_score'] ?? null, ]; return [ 'student_id' => $studentId, 'student_name' => $studentName !== '' ? $studentName : 'Student', 'parent_name' => $parentName, 'class_section_name' => $row['class_section_name'] ?? '', 'semester' => $semester, 'school_year' => $schoolYear, 'scores' => $scores, 'comment' => $row['comment'] ?? '', 'subject' => $subject, ]; } public function sendEmail(array $payload): void { Event::dispatch('below60.email', [$payload]); } public function updateStatus(int $studentId, string $semester, string $schoolYear, string $status, string $note, ?int $userId): void { $status = ucfirst(strtolower($status)); if (!in_array($status, ['Open', 'Closed'], true)) { throw new RuntimeException('Invalid status.'); } $semesterKey = strtolower(trim($semester)); $existing = CurrentFlag::query() ->where('student_id', $studentId) ->where('flag', 'grade') ->where('school_year', $schoolYear) ->whereRaw('LOWER(TRIM(semester)) = ?', [$semesterKey]) ->first(); $now = now(); if ($existing) { $data = [ 'flag_state' => $status, 'flag_datetime' => $now, 'updated_at' => $now, ]; if ($status === 'Open') { $data['updated_by_open'] = $userId; if ($note !== '') { $prev = (string) ($existing->open_description ?? ''); $data['open_description'] = trim($prev . PHP_EOL . $note); } } else { $data['updated_by_closed'] = $userId; if ($note !== '') { $prev = (string) ($existing->close_description ?? ''); $data['close_description'] = trim($prev . PHP_EOL . $note); } } $existing->update($data); return; } $row = $this->fetchBelowSixtyEmailRow($studentId, $schoolYear, $semester); $studentName = trim((string) ($row['firstname'] ?? '') . ' ' . (string) ($row['lastname'] ?? '')); $grade = (string) ($row['class_section_name'] ?? ''); $data = [ 'student_id' => $studentId, 'student_name' => $studentName !== '' ? $studentName : 'Student', 'grade' => $grade, 'flag' => 'grade', 'flag_datetime' => $now, 'flag_state' => $status, 'semester' => $semester, 'school_year' => $schoolYear, 'updated_at' => $now, ]; if ($status === 'Open') { $data['updated_by_open'] = $userId; if ($note !== '') { $data['open_description'] = $note; } } else { $data['updated_by_closed'] = $userId; if ($note !== '') { $data['close_description'] = $note; } } CurrentFlag::query()->create($data); } public function meetingContext(int $studentId, string $schoolYear, string $semester): array { $context = $this->fetchBelowSixtyMeetingContext($studentId, $schoolYear, $semester); if (empty($context)) { throw new RuntimeException('Student record not found for the selected term.'); } return $context + [ 'student_id' => $studentId, 'semester' => $semester, 'school_year' => $schoolYear, ]; } public function saveMeeting(array $payload, ?int $userId): void { $studentId = (int) ($payload['student_id'] ?? 0); $semester = (string) ($payload['semester'] ?? ''); $schoolYear = (string) ($payload['school_year'] ?? ''); $date = trim((string) ($payload['date'] ?? '')); $time = trim((string) ($payload['time'] ?? '')); $notes = trim((string) ($payload['notes'] ?? '')); if ($studentId <= 0 || $semester === '' || $schoolYear === '' || $date === '') { throw new RuntimeException('Missing required fields.'); } $context = $this->fetchBelowSixtyMeetingContext($studentId, $schoolYear, $semester); if (empty($context)) { throw new RuntimeException('Student record not found for the selected term.'); } ParentMeetingSchedule::query()->create([ 'student_id' => $studentId, 'parent_user_id' => $context['parent_user_id'] ?? null, 'parent_name' => $context['parent_name'], 'student_name' => $context['student_name'], 'class_section_name' => $context['class_section_name'], 'date' => $date, 'time' => $time !== '' ? $time : null, 'notes' => $notes !== '' ? $notes : null, 'semester' => $semester, 'school_year' => $schoolYear, 'status' => 'scheduled', 'created_by' => $userId, ]); } private function fetchBelowSixtyEmailRow(int $studentId, string $schoolYear, string $semester): array { $semesterKey = strtolower(trim($semester)); $row = DB::table('semester_scores as ss') ->select([ 's.id as student_id', 's.firstname', 's.lastname', 'cs.class_section_name', 'ss.homework_avg', 'ss.project_avg', 'ss.participation_score', DB::raw('COALESCE(ss.test_avg, ss.quiz_avg) as test_avg'), 'ss.ptap_score', 'ss.attendance_score', 'ss.midterm_exam_score', 'ss.semester_score', ]) ->join('students as s', 's.id', '=', 'ss.student_id') ->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'ss.class_section_id') ->where('ss.school_year', $schoolYear) ->where('ss.student_id', $studentId) ->where('s.is_active', 1) ->whereRaw('LOWER(TRIM(ss.semester)) = ?', [$semesterKey]) ->first(); if (!$row) { return []; } $commentRow = ScoreComment::query() ->select('comment') ->where('score_type', 'general') ->where('school_year', $schoolYear) ->whereRaw('LOWER(TRIM(semester)) = ?', [$semesterKey]) ->where('student_id', $studentId) ->orderBy('created_at', 'DESC') ->first(); $row = (array) $row; $row['comment'] = (string) ($commentRow?->comment ?? ''); return $row; } private function fetchBelowSixtyParentName(int $studentId): string { $parentName = 'Parent/Guardian'; try { $rows = DB::select( "SELECT u.firstname, u.lastname FROM family_students fs JOIN family_guardians fg ON fg.family_id = fs.family_id JOIN users u ON u.id = fg.user_id WHERE fs.student_id = ? ORDER BY fg.is_primary DESC, u.lastname, u.firstname LIMIT 1", [$studentId] ); if (!empty($rows[0])) { $candidate = trim((string) ($rows[0]->firstname ?? '') . ' ' . (string) ($rows[0]->lastname ?? '')); if ($candidate !== '') { $parentName = $candidate; } } } catch (\Throwable $e) { } return $parentName; } private function buildBelowSixtySubject(string $studentName, string $semester, string $schoolYear): string { $subject = 'Student Performance Alert'; if ($studentName !== '') { $subject .= ' — ' . $studentName; } if ($semester !== '' || $schoolYear !== '') { $subject .= ' (' . trim($semester . ' ' . $schoolYear) . ')'; } return $subject; } private function fetchBelowSixtyMeetingContext(int $studentId, string $schoolYear, string $semester): array { $row = $this->fetchBelowSixtyEmailRow($studentId, $schoolYear, $semester); if (empty($row)) { return []; } $parentName = 'Parent/Guardian'; $parentUserId = null; try { $pRows = DB::select( "SELECT u.id AS user_id, u.firstname, u.lastname FROM family_students fs JOIN family_guardians fg ON fg.family_id = fs.family_id JOIN users u ON u.id = fg.user_id WHERE fs.student_id = ? ORDER BY fg.is_primary DESC, u.lastname, u.firstname LIMIT 1", [$studentId] ); if (!empty($pRows[0])) { $parentUserId = (int) ($pRows[0]->user_id ?? 0) ?: null; $parentName = trim((string) ($pRows[0]->firstname ?? '') . ' ' . (string) ($pRows[0]->lastname ?? '')); if ($parentName === '') { $parentName = 'Parent/Guardian'; } } } catch (\Throwable $e) { } if ($parentUserId === null) { try { $srow = DB::selectOne( "SELECT s.parent_id, u.firstname, u.lastname FROM students s LEFT JOIN users u ON u.id = s.parent_id WHERE s.id = ? LIMIT 1", [$studentId] ); if (!empty($srow)) { $parentUserId = (int) ($srow->parent_id ?? 0) ?: null; $fallbackName = trim((string) ($srow->firstname ?? '') . ' ' . (string) ($srow->lastname ?? '')); if ($fallbackName !== '') { $parentName = $fallbackName; } } } catch (\Throwable $e) { } } $studentName = trim((string) ($row['firstname'] ?? '') . ' ' . (string) ($row['lastname'] ?? '')); return [ 'student_name' => $studentName !== '' ? $studentName : 'Student', 'parent_name' => $parentName, 'parent_user_id' => $parentUserId, 'class_section_name' => (string) ($row['class_section_name'] ?? ''), ]; } }