diff --git a/app/Controllers/View/ReportCardsController.php b/app/Controllers/View/ReportCardsController.php index cdba8d2..b3b128f 100644 --- a/app/Controllers/View/ReportCardsController.php +++ b/app/Controllers/View/ReportCardsController.php @@ -317,18 +317,27 @@ class ReportCardsController extends PrintablesBaseController $examCommentTypes = $isSecond ? ['final'] : ($isFirst ? ['midterm'] : ['midterm', 'final']); - $commentTypes = array_merge($examCommentTypes, ['ptap', 'attendance', 'attendance_comment']); + $wantedCommentTypes = array_merge($examCommentTypes, ['ptap', 'attendance']); $hasCommentReview = $this->db->fieldExists('comment_review', 'score_comments'); - $commentSelect = $hasCommentReview - ? 'student_id, score_type, comment, comment_review' - : 'student_id, score_type, comment'; + $hasCommentSemester = $this->db->fieldExists('semester', 'score_comments'); + $hasCommentUpdatedAt = $this->db->fieldExists('updated_at', 'score_comments'); + $commentSelectParts = ['student_id', 'score_type', 'comment']; + if ($hasCommentReview) { + $commentSelectParts[] = 'comment_review'; + } + if ($hasCommentSemester) { + $commentSelectParts[] = 'semester'; + } + + // Do NOT filter comments by score_type or semester here. + // A single bad row like "Attendance Comment", "attendance-comment", or a blank semester + // should not make one student look incomplete while the PDF can still print the comment. $commentBuilder = $this->scoreCommentModel - ->select($commentSelect) + ->select(implode(', ', $commentSelectParts)) ->whereIn('student_id', $studentIds) - ->where('school_year', $year) - ->whereIn('score_type', $commentTypes); - if ($sem !== '') { - $this->applySemesterFilter($commentBuilder, $sem, 'semester'); + ->where('school_year', $year); + if ($hasCommentUpdatedAt) { + $commentBuilder->orderBy('updated_at', 'DESC'); } $commentRows = []; try { @@ -336,25 +345,92 @@ class ReportCardsController extends PrintablesBaseController } catch (\Throwable $e) { } + $cleanComment = static function ($value): string { + $text = (string)($value ?? ''); + $text = str_replace("\xc2\xa0", ' ', $text); // normalize non-breaking spaces + return trim($text); + }; + + $normalizeCommentType = static function ($value) use ($cleanComment): string { + $type = strtolower($cleanComment($value)); + $type = preg_replace('/[^a-z0-9]+/', '_', $type); + $type = trim((string)$type, '_'); + + $map = [ + 'attendance_comment' => 'attendance', + 'attendance_comments' => 'attendance', + 'attendance' => 'attendance', + 'attendence' => 'attendance', + 'attendence_comment' => 'attendance', + 'attendence_comments' => 'attendance', + 'ptap_comment' => 'ptap', + 'ptap_comments' => 'ptap', + 'ptap' => 'ptap', + 'midterm_comment' => 'midterm', + 'midterm_comments' => 'midterm', + 'midterm' => 'midterm', + 'final_comment' => 'final', + 'final_comments' => 'final', + 'final' => 'final', + ]; + + return $map[$type] ?? $type; + }; + + $normalizeSemester = static function ($value) use ($cleanComment): string { + $v = strtolower($cleanComment($value)); + $v = preg_replace('/[^a-z0-9]+/', ' ', $v); + $v = trim((string)$v); + if (in_array($v, ['fall', 'first', 'first semester', 'semester 1', '1'], true)) { + return 'fall'; + } + if (in_array($v, ['spring', 'second', 'second semester', 'semester 2', '2'], true)) { + return 'spring'; + } + return $v; + }; + $wantedSemester = $normalizeSemester($sem); + $commentsByStudent = []; + $commentPriorityByStudent = []; foreach ($commentRows as $row) { $sid = (int)($row['student_id'] ?? 0); if ($sid <= 0) { continue; } - $typeRaw = strtolower(trim((string)($row['score_type'] ?? ''))); - if ($typeRaw === '') { + + $typeRaw = $normalizeCommentType($row['score_type'] ?? ''); + if ($typeRaw === '' || !in_array($typeRaw, $wantedCommentTypes, true)) { continue; } - if ($typeRaw === 'attendance_comment') { - $typeRaw = 'attendance'; + + $rawVal = $cleanComment($row['comment'] ?? ''); + if ($typeRaw === 'attendance') { + // Attendance comments are stored in score_comments.comment. + // Do not use comment_review here, because it can be blank while the PDF comment exists. + $commentVal = $rawVal; + } else { + // For non-attendance comments, prefer reviewed text but fall back to the saved comment. + $reviewVal = $hasCommentReview ? $cleanComment($row['comment_review'] ?? '') : ''; + $commentVal = $reviewVal !== '' ? $reviewVal : $rawVal; } - $reviewVal = $hasCommentReview ? trim((string)($row['comment_review'] ?? '')) : ''; - $commentVal = $hasCommentReview ? $reviewVal : trim((string)($row['comment'] ?? '')); if ($commentVal === '') { continue; } - $commentsByStudent[$sid][$typeRaw] = $commentVal; + + $rowSemester = $hasCommentSemester ? $normalizeSemester($row['semester'] ?? '') : ''; + $priority = 1; + if ($wantedSemester !== '' && $rowSemester === $wantedSemester) { + $priority = 3; + } elseif ($rowSemester === '') { + $priority = 2; + } + + $existingPriority = $commentPriorityByStudent[$sid][$typeRaw] ?? 0; + if (!isset($commentsByStudent[$sid][$typeRaw]) || $priority >= $existingPriority) { + $commentsByStudent[$sid][$typeRaw] = $commentVal; + $commentPriorityByStudent[$sid][$typeRaw] = $priority; + } } $isNumeric = static fn($v) => $v !== null && $v !== '' && is_numeric($v); @@ -467,6 +543,20 @@ class ReportCardsController extends PrintablesBaseController if (trim((string)($commentSet['ptap'] ?? '')) === '') { $missing[] = 'PTAP comment'; } + + // Keep completeness consistent with the PDF report generation: + // the report can auto-generate an attendance comment from attendance_score. + if (trim((string)($commentSet['attendance'] ?? '')) === '' && $isNumeric($attendanceScore)) { + $autoAttendance = attendance_comment_from_score( + (float)$attendanceScore, + trim((string)($student['firstname'] ?? '')) + ); + if ($autoAttendance !== null && trim((string)$autoAttendance) !== '') { + $commentSet['attendance'] = $autoAttendance; + $warnings[] = 'Attendance comment computed from attendance score'; + } + } + if (trim((string)($commentSet['attendance'] ?? '')) === '') { $missing[] = 'Attendance comment'; } @@ -1531,10 +1621,16 @@ $scoresEndY = $pdf->GetY(); if ($typeRaw === 'attendance_comment') { $typeRaw = 'attendance'; } - $reviewVal = trim((string)($row['comment_review'] ?? '')); - $commentVal = $this->db->fieldExists('comment_review', 'score_comments') - ? $reviewVal - : trim((string)($row['comment'] ?? '')); + $rawComment = trim((string)($row['comment'] ?? '')); + if ($typeRaw === 'attendance') { + // Attendance comments must come from score_comments.comment. + $commentVal = $rawComment; + } else { + $reviewVal = trim((string)($row['comment_review'] ?? '')); + $commentVal = $this->db->fieldExists('comment_review', 'score_comments') && $reviewVal !== '' + ? $reviewVal + : $rawComment; + } if ($commentVal === '') { continue; }