diff --git a/app/Controllers/View/ExamDraftController.php b/app/Controllers/View/ExamDraftController.php index a32524b..78879e6 100644 --- a/app/Controllers/View/ExamDraftController.php +++ b/app/Controllers/View/ExamDraftController.php @@ -26,6 +26,9 @@ class ExamDraftController extends BaseController protected bool $hasAcceptanceTypeColumn = false; protected bool $hasAdminIdColumn = false; protected bool $hasReviewRevisionColumn = false; + protected bool $hasReviewerCommentColumn = false; + protected bool $hasReviewerCommentsColumn = false; + protected bool $hasAdminCommentsColumn = false; protected string $authorIdColumn = 'teacher_id'; protected string $authorFileColumn = 'teacher_file'; protected string $authorFilenameColumn = 'teacher_filename'; @@ -65,6 +68,9 @@ class ExamDraftController extends BaseController $this->hasAcceptanceTypeColumn = $this->schemaHasColumn('exam_drafts', 'acceptance_type'); $this->hasAdminIdColumn = $this->schemaHasColumn('exam_drafts', 'admin_id'); $this->hasReviewRevisionColumn = $this->schemaHasColumn('exam_drafts', 'review_revision'); + $this->hasReviewerCommentColumn = $this->schemaHasColumn('exam_drafts', 'reviewer_comment'); + $this->hasReviewerCommentsColumn = $this->schemaHasColumn('exam_drafts', 'reviewer_comments'); + $this->hasAdminCommentsColumn = $this->schemaHasColumn('exam_drafts', 'admin_comments'); $this->authorIdColumn = $this->schemaHasColumn('exam_drafts', 'teacher_id') ? 'teacher_id' : 'author_id'; $this->authorFileColumn = $this->schemaHasColumn('exam_drafts', 'teacher_file') ? 'teacher_file' : 'author_file'; $this->authorFilenameColumn = $this->schemaHasColumn('exam_drafts', 'teacher_filename') ? 'teacher_filename' : 'author_filename'; @@ -73,7 +79,9 @@ class ExamDraftController extends BaseController : ($this->schemaHasColumn('exam_drafts', 'reviewer_id') ? 'reviewer_id' : ''); $this->reviewerCommentColumn = $this->schemaHasColumn('exam_drafts', 'reviewer_comment') ? 'reviewer_comment' - : ($this->schemaHasColumn('exam_drafts', 'admin_comments') ? 'admin_comments' : ''); + : ($this->schemaHasColumn('exam_drafts', 'reviewer_comments') + ? 'reviewer_comments' + : ($this->schemaHasColumn('exam_drafts', 'admin_comments') ? 'admin_comments' : '')); helper(['form', 'url', 'date']); } @@ -116,19 +124,22 @@ class ExamDraftController extends BaseController // Keep all submissions visible; legacy ones are also surfaced in a separate tab $drafts = $allDrafts; + $legacyQuery = $this->examDraftModel + ->select($this->draftSelectColumns()) + ->select('cs.class_section_name') + ->join('classSection cs', 'cs.class_section_id = exam_drafts.class_section_id', 'left') + ->where('exam_drafts.is_legacy', 1) + ->where('exam_drafts.status', 'legacy') + ->where('exam_drafts.final_file IS NOT NULL', null, false); + if (!empty($classSectionIds)) { - $legacyExams = $this->examDraftModel - ->select($this->draftSelectColumns()) - ->select('cs.class_section_name') - ->join('classSection cs', 'cs.class_section_id = exam_drafts.class_section_id', 'left') - ->whereIn('exam_drafts.class_section_id', $classSectionIds) - ->where('exam_drafts.is_legacy', 1) - ->where('exam_drafts.status', 'legacy') - ->where('exam_drafts.final_file IS NOT NULL', null, false) - ->orderBy('cs.class_section_name', 'ASC') - ->orderBy('exam_drafts.created_at', 'DESC') - ->findAll(); + $legacyQuery = $legacyQuery->whereIn('exam_drafts.class_section_id', $classSectionIds); } + + $legacyExams = $legacyQuery + ->orderBy('cs.class_section_name', 'ASC') + ->orderBy('exam_drafts.created_at', 'DESC') + ->findAll(); } else { // Legacy column absent, show all drafts and skip legacy tab query $drafts = $allDrafts; @@ -283,11 +294,12 @@ class ExamDraftController extends BaseController 'status' => 'submitted', ]; $this->applyAuthorFilePayload($updateSubmit, $teacherFile, $teacherFilename); - if ($this->examDraftModel->update($draftRowId, $updateSubmit)) { - $saved = $this->ensureDraftStatus($draftRowId, 'submitted'); - $this->notifyExamDraftEvent($saved, 'submitted'); - return redirect()->to('/teacher/exam-drafts')->with('success', 'Exam draft submitted for review.'); - } + if ($this->examDraftModel->update($draftRowId, $updateSubmit)) { + $saved = $this->ensureDraftStatus($draftRowId, 'submitted'); + $this->notifyPrincipalExamDraftSubmitted($saved, $teacherId, $classSectionId); + $this->notifyExamDraftEvent($saved, 'submitted'); + return redirect()->to('/teacher/exam-drafts')->with('success', 'Exam draft submitted for review.'); + } return redirect()->back()->withInput()->with('error', 'Unable to submit the exam draft.'); } @@ -319,6 +331,7 @@ class ExamDraftController extends BaseController if ($this->examDraftModel->insert($payload)) { $newId = (int) $this->examDraftModel->getInsertID(); $saved = $newId > 0 ? $this->ensureDraftStatus($newId, 'submitted') : null; + $this->notifyPrincipalExamDraftSubmitted($saved, $teacherId, $classSectionId); $this->notifyExamDraftEvent($saved, 'submitted'); return redirect()->to('/teacher/exam-drafts')->with('success', 'Exam draft submitted for review.'); } @@ -556,8 +569,16 @@ class ExamDraftController extends BaseController 'status' => $status, 'reviewed_at' => utc_now(), ]; - if ($this->reviewerCommentColumn !== '') { - $update[$this->reviewerCommentColumn] = $reviewerComment === '' ? null : $reviewerComment; + $existingComment = (string) ($draft['reviewer_comment'] ?? $draft['reviewer_comments'] ?? $draft['admin_comments'] ?? ''); + $commentValue = $reviewerComment === '' ? null : $this->appendReviewerComment($existingComment, $reviewerComment); + if ($this->hasReviewerCommentColumn) { + $update['reviewer_comment'] = $commentValue; + } + if ($this->hasReviewerCommentsColumn) { + $update['reviewer_comments'] = $commentValue; + } + if ($this->hasAdminCommentsColumn) { + $update['admin_comments'] = $commentValue; } if ($this->reviewerIdColumn !== '') { $update[$this->reviewerIdColumn] = (int) (session()->get('user_id') ?? 0); @@ -589,8 +610,14 @@ class ExamDraftController extends BaseController if ($this->hasReviewRevisionColumn) { $newRow['review_revision'] = $reviewRevision; } - if ($this->reviewerCommentColumn !== '') { - $newRow[$this->reviewerCommentColumn] = $update[$this->reviewerCommentColumn] ?? null; + if ($this->hasReviewerCommentColumn) { + $newRow['reviewer_comment'] = $commentValue; + } + if ($this->hasReviewerCommentsColumn) { + $newRow['reviewer_comments'] = $commentValue; + } + if ($this->hasAdminCommentsColumn) { + $newRow['admin_comments'] = $commentValue; } if ($this->reviewerIdColumn !== '') { $newRow[$this->reviewerIdColumn] = $update[$this->reviewerIdColumn] ?? null; @@ -831,6 +858,46 @@ class ExamDraftController extends BaseController return $saved; } + private function notifyPrincipalExamDraftSubmitted(?array $draft, int $teacherId, int $classSectionId): void + { + $principalEmail = trim((string) (env('PRINCIPAL_EMAIL') ?: '')); + if ($principalEmail === '' || !filter_var($principalEmail, FILTER_VALIDATE_EMAIL)) { + return; + } + if (empty($draft)) { + return; + } + + try { + $teacher = $this->userModel->find($teacherId) ?? []; + $teacherName = trim(($teacher['firstname'] ?? '') . ' ' . ($teacher['lastname'] ?? '')); + if ($teacherName === '') { + $teacherName = 'Teacher #' . $teacherId; + } + + $class = $this->classSectionModel->find($classSectionId) ?? []; + $className = $class['class_section_name'] ?? ('Class ' . $classSectionId); + + $subject = 'Exam draft submitted: ' . $className; + $submittedAt = $draft['created_at'] ?? $draft['updated_at'] ?? ''; + $body = '
A teacher has submitted a new exam draft.
' + . '| Teacher | ' . esc($teacherName) . ' |
| Class | ' . esc($className) . ' |
| Exam type | ' . esc($draft['exam_type'] ?? 'N/A') . ' |
| Version | ' . esc((string) ($draft['version'] ?? '')) . ' |
| Status | ' . esc((string) ($draft['status'] ?? 'submitted')) . ' |
| Submitted at | ' . esc((string) $submittedAt) . ' |
Review drafts: Admin exam drafts
'; + + $mailer = \Config\Services::emailService(); + $mailer->send($principalEmail, $subject, $body, 'notifications'); + } catch (\Throwable $e) { + log_message('error', 'Failed to send exam draft notification: ' . $e->getMessage()); + } + } + private function nextDraftVersion(array $draft): int { $query = $this->examDraftModel @@ -887,6 +954,36 @@ class ExamDraftController extends BaseController return $current + 1; } + private function appendReviewerComment(string $existing, string $incoming): string + { + $existing = trim($existing); + $incoming = trim($incoming); + if ($incoming === '') { + return $existing; + } + + if ($existing !== '' && str_starts_with($incoming, $existing)) { + $remainder = trim(substr($incoming, strlen($existing))); + if ($remainder === '') { + return $existing; + } + $incoming = $remainder; + } + + $lines = $existing === '' ? [] : preg_split('/\R/', $existing); + $lastLine = $lines ? trim((string) end($lines)) : ''; + if ($lastLine !== '') { + $lastBody = preg_replace('/^\d+\-\s*/', '', $lastLine); + if ($lastBody !== null && trim($lastBody) === $incoming) { + return $existing; + } + } + $nextIndex = count($lines) + 1; + $incoming = trim(preg_replace('/\R+/', ' ', $incoming) ?? $incoming); + $lines[] = $nextIndex . '- ' . $incoming; + return implode("\n", $lines); + } + private function statusOptions(): array { return ['submitted', 'accepted', 'review needed', 'rejected', 'canceled', 'under review', 'legacy']; @@ -1001,6 +1098,7 @@ class ExamDraftController extends BaseController $grouped[$key]['_is_review_row'] = true; $grouped[$key]['review_files'] = []; $grouped[$key]['final_version'] = null; + $grouped[$key]['_latest_review_comment'] = null; } $grouped[$key]['review_files'][] = [ 'review_revision' => $reviewRev, @@ -1008,6 +1106,16 @@ class ExamDraftController extends BaseController 'final_filename' => $row['final_filename'] ?? null, 'status' => $row['status'] ?? null, ]; + $reviewComment = $row['reviewer_comment'] ?? $row['admin_comments'] ?? null; + if ($reviewComment !== null && $reviewComment !== '') { + $latestComment = $grouped[$key]['_latest_review_comment'] ?? null; + if ($latestComment === null || $reviewRev > (int) ($latestComment['review_revision'] ?? 0)) { + $grouped[$key]['_latest_review_comment'] = [ + 'review_revision' => $reviewRev, + 'comment' => $reviewComment, + ]; + } + } if (strtolower((string) ($row['status'] ?? '')) === 'accepted') { $currentFinal = $grouped[$key]['final_version'] ?? null; if ($currentFinal === null || $reviewRev > (int) ($currentFinal['review_revision'] ?? 0)) { @@ -1029,8 +1137,12 @@ class ExamDraftController extends BaseController if (!empty($grouped[$key]['_is_review_row'])) { $reviewFiles = $grouped[$key]['review_files'] ?? []; + $latestReviewComment = $grouped[$key]['_latest_review_comment'] ?? null; $grouped[$key] = $row; $grouped[$key]['review_files'] = $reviewFiles; + if (!empty($latestReviewComment['comment'])) { + $grouped[$key]['reviewer_comment'] = $latestReviewComment['comment']; + } unset($grouped[$key]['_is_review_row']); } } @@ -1043,7 +1155,11 @@ class ExamDraftController extends BaseController $row['final_file'] = $row['final_version']['final_file'] ?? $row['final_file'] ?? null; $row['final_filename'] = $row['final_version']['final_filename'] ?? $row['final_filename'] ?? null; } + if (empty($row['reviewer_comment']) && !empty($row['_latest_review_comment']['comment'])) { + $row['reviewer_comment'] = $row['_latest_review_comment']['comment']; + } unset($row['_is_review_row']); + unset($row['_latest_review_comment']); } unset($row); diff --git a/app/Models/ExamDraftModel.php b/app/Models/ExamDraftModel.php index 80244f8..bf96c84 100644 --- a/app/Models/ExamDraftModel.php +++ b/app/Models/ExamDraftModel.php @@ -52,6 +52,7 @@ class ExamDraftModel extends Model 'admin_id', 'is_legacy', 'reviewer_comment', + 'reviewer_comments', 'admin_comments', 'reviewed_at', 'final_file', diff --git a/app/Views/administrator/exam_drafts.php b/app/Views/administrator/exam_drafts.php index c7f6b37..70dab41 100644 --- a/app/Views/administrator/exam_drafts.php +++ b/app/Views/administrator/exam_drafts.php @@ -198,7 +198,7 @@ $fileAccept = implode(',', array_map(static fn ($x) => '.' . $x, $allowedExtensi - +| Class | @@ -85,7 +112,7 @@ $maxUploadBytes = $maxUploadBytes ?? (12 * 1024 * 1024); ?>|||||||||
|---|---|---|---|---|---|---|---|---|---|
| = esc($d['class_section_name'] ?? '') ?> | -= esc($d['draft_title'] ?? $d['exam_type'] ?? '') ?> | += esc($d['draft_title'] ?? $d['exam_type'] ?? '') ?> | = esc((string) ($d['version'] ?? '')) ?> | = $badgeHtml ?> | @@ -124,7 +151,7 @@ $maxUploadBytes = $maxUploadBytes ?? (12 * 1024 * 1024); — | -= esc($d['reviewer_comment'] ?? $d['admin_comments'] ?? '—') ?> | += nl2br(esc($d['reviewer_comment'] ?? $d['reviewer_comments'] ?? $d['admin_comments'] ?? '—')) ?> | = esc(!empty($d['updated_at']) ? local_datetime($d['updated_at'], 'm-d-Y H:i') : '') ?> |
| Class | Title | Ver. | Status | +School year | +Semester | +Exam type | +Updated | Actions | |
|---|---|---|---|---|---|---|---|---|---|
| = esc($d['class_section_name'] ?? '') ?> | -= esc($d['draft_title'] ?? $d['exam_type'] ?? '') ?> | += esc($d['draft_title'] ?? $d['exam_type'] ?? '') ?> | = esc((string) ($d['version'] ?? '')) ?> | = $renderBadge($st, $statusBadges) ?> | += esc($d['school_year'] ?? '') ?> | += esc($d['semester'] ?? '') ?> | += esc($d['exam_type'] ?? '') ?> | += esc(!empty($d['updated_at']) ? local_datetime($d['updated_at'], 'm-d-Y H:i') : '') ?> | - - Print + + View |