Files
alrahma_sunday_school_api/app/Services/Grading/GradingBelowSixtyService.php
T
2026-03-09 02:52:13 -04:00

404 lines
15 KiB
PHP

<?php
namespace App\Services\Grading;
use App\Models\CurrentFlag;
use App\Models\ParentMeetingSchedule;
use App\Models\ScoreComment;
use App\Models\Student;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Event;
use RuntimeException;
class GradingBelowSixtyService
{
public function listRows(string $schoolYear, string $semester): array
{
$semesterKey = strtolower(trim($semester));
$rows = DB::table('semester_scores as ss')
->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'] ?? ''),
];
}
}