exam draft notification and submission
This commit is contained in:
@@ -26,6 +26,9 @@ class ExamDraftController extends BaseController
|
|||||||
protected bool $hasAcceptanceTypeColumn = false;
|
protected bool $hasAcceptanceTypeColumn = false;
|
||||||
protected bool $hasAdminIdColumn = false;
|
protected bool $hasAdminIdColumn = false;
|
||||||
protected bool $hasReviewRevisionColumn = false;
|
protected bool $hasReviewRevisionColumn = false;
|
||||||
|
protected bool $hasReviewerCommentColumn = false;
|
||||||
|
protected bool $hasReviewerCommentsColumn = false;
|
||||||
|
protected bool $hasAdminCommentsColumn = false;
|
||||||
protected string $authorIdColumn = 'teacher_id';
|
protected string $authorIdColumn = 'teacher_id';
|
||||||
protected string $authorFileColumn = 'teacher_file';
|
protected string $authorFileColumn = 'teacher_file';
|
||||||
protected string $authorFilenameColumn = 'teacher_filename';
|
protected string $authorFilenameColumn = 'teacher_filename';
|
||||||
@@ -65,6 +68,9 @@ class ExamDraftController extends BaseController
|
|||||||
$this->hasAcceptanceTypeColumn = $this->schemaHasColumn('exam_drafts', 'acceptance_type');
|
$this->hasAcceptanceTypeColumn = $this->schemaHasColumn('exam_drafts', 'acceptance_type');
|
||||||
$this->hasAdminIdColumn = $this->schemaHasColumn('exam_drafts', 'admin_id');
|
$this->hasAdminIdColumn = $this->schemaHasColumn('exam_drafts', 'admin_id');
|
||||||
$this->hasReviewRevisionColumn = $this->schemaHasColumn('exam_drafts', 'review_revision');
|
$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->authorIdColumn = $this->schemaHasColumn('exam_drafts', 'teacher_id') ? 'teacher_id' : 'author_id';
|
||||||
$this->authorFileColumn = $this->schemaHasColumn('exam_drafts', 'teacher_file') ? 'teacher_file' : 'author_file';
|
$this->authorFileColumn = $this->schemaHasColumn('exam_drafts', 'teacher_file') ? 'teacher_file' : 'author_file';
|
||||||
$this->authorFilenameColumn = $this->schemaHasColumn('exam_drafts', 'teacher_filename') ? 'teacher_filename' : 'author_filename';
|
$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->schemaHasColumn('exam_drafts', 'reviewer_id') ? 'reviewer_id' : '');
|
||||||
$this->reviewerCommentColumn = $this->schemaHasColumn('exam_drafts', 'reviewer_comment')
|
$this->reviewerCommentColumn = $this->schemaHasColumn('exam_drafts', 'reviewer_comment')
|
||||||
? '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']);
|
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
|
// Keep all submissions visible; legacy ones are also surfaced in a separate tab
|
||||||
$drafts = $allDrafts;
|
$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)) {
|
if (!empty($classSectionIds)) {
|
||||||
$legacyExams = $this->examDraftModel
|
$legacyQuery = $legacyQuery->whereIn('exam_drafts.class_section_id', $classSectionIds);
|
||||||
->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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$legacyExams = $legacyQuery
|
||||||
|
->orderBy('cs.class_section_name', 'ASC')
|
||||||
|
->orderBy('exam_drafts.created_at', 'DESC')
|
||||||
|
->findAll();
|
||||||
} else {
|
} else {
|
||||||
// Legacy column absent, show all drafts and skip legacy tab query
|
// Legacy column absent, show all drafts and skip legacy tab query
|
||||||
$drafts = $allDrafts;
|
$drafts = $allDrafts;
|
||||||
@@ -283,11 +294,12 @@ class ExamDraftController extends BaseController
|
|||||||
'status' => 'submitted',
|
'status' => 'submitted',
|
||||||
];
|
];
|
||||||
$this->applyAuthorFilePayload($updateSubmit, $teacherFile, $teacherFilename);
|
$this->applyAuthorFilePayload($updateSubmit, $teacherFile, $teacherFilename);
|
||||||
if ($this->examDraftModel->update($draftRowId, $updateSubmit)) {
|
if ($this->examDraftModel->update($draftRowId, $updateSubmit)) {
|
||||||
$saved = $this->ensureDraftStatus($draftRowId, 'submitted');
|
$saved = $this->ensureDraftStatus($draftRowId, 'submitted');
|
||||||
$this->notifyExamDraftEvent($saved, 'submitted');
|
$this->notifyPrincipalExamDraftSubmitted($saved, $teacherId, $classSectionId);
|
||||||
return redirect()->to('/teacher/exam-drafts')->with('success', 'Exam draft submitted for review.');
|
$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.');
|
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)) {
|
if ($this->examDraftModel->insert($payload)) {
|
||||||
$newId = (int) $this->examDraftModel->getInsertID();
|
$newId = (int) $this->examDraftModel->getInsertID();
|
||||||
$saved = $newId > 0 ? $this->ensureDraftStatus($newId, 'submitted') : null;
|
$saved = $newId > 0 ? $this->ensureDraftStatus($newId, 'submitted') : null;
|
||||||
|
$this->notifyPrincipalExamDraftSubmitted($saved, $teacherId, $classSectionId);
|
||||||
$this->notifyExamDraftEvent($saved, 'submitted');
|
$this->notifyExamDraftEvent($saved, 'submitted');
|
||||||
return redirect()->to('/teacher/exam-drafts')->with('success', 'Exam draft submitted for review.');
|
return redirect()->to('/teacher/exam-drafts')->with('success', 'Exam draft submitted for review.');
|
||||||
}
|
}
|
||||||
@@ -556,8 +569,16 @@ class ExamDraftController extends BaseController
|
|||||||
'status' => $status,
|
'status' => $status,
|
||||||
'reviewed_at' => utc_now(),
|
'reviewed_at' => utc_now(),
|
||||||
];
|
];
|
||||||
if ($this->reviewerCommentColumn !== '') {
|
$existingComment = (string) ($draft['reviewer_comment'] ?? $draft['reviewer_comments'] ?? $draft['admin_comments'] ?? '');
|
||||||
$update[$this->reviewerCommentColumn] = $reviewerComment === '' ? null : $reviewerComment;
|
$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 !== '') {
|
if ($this->reviewerIdColumn !== '') {
|
||||||
$update[$this->reviewerIdColumn] = (int) (session()->get('user_id') ?? 0);
|
$update[$this->reviewerIdColumn] = (int) (session()->get('user_id') ?? 0);
|
||||||
@@ -589,8 +610,14 @@ class ExamDraftController extends BaseController
|
|||||||
if ($this->hasReviewRevisionColumn) {
|
if ($this->hasReviewRevisionColumn) {
|
||||||
$newRow['review_revision'] = $reviewRevision;
|
$newRow['review_revision'] = $reviewRevision;
|
||||||
}
|
}
|
||||||
if ($this->reviewerCommentColumn !== '') {
|
if ($this->hasReviewerCommentColumn) {
|
||||||
$newRow[$this->reviewerCommentColumn] = $update[$this->reviewerCommentColumn] ?? null;
|
$newRow['reviewer_comment'] = $commentValue;
|
||||||
|
}
|
||||||
|
if ($this->hasReviewerCommentsColumn) {
|
||||||
|
$newRow['reviewer_comments'] = $commentValue;
|
||||||
|
}
|
||||||
|
if ($this->hasAdminCommentsColumn) {
|
||||||
|
$newRow['admin_comments'] = $commentValue;
|
||||||
}
|
}
|
||||||
if ($this->reviewerIdColumn !== '') {
|
if ($this->reviewerIdColumn !== '') {
|
||||||
$newRow[$this->reviewerIdColumn] = $update[$this->reviewerIdColumn] ?? null;
|
$newRow[$this->reviewerIdColumn] = $update[$this->reviewerIdColumn] ?? null;
|
||||||
@@ -831,6 +858,46 @@ class ExamDraftController extends BaseController
|
|||||||
return $saved;
|
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 = '<p>A teacher has submitted a new exam draft.</p>'
|
||||||
|
. '<table style="border-collapse:collapse;">'
|
||||||
|
. '<tr><td style="padding:4px 8px;"><strong>Teacher</strong></td><td style="padding:4px 8px;">' . esc($teacherName) . '</td></tr>'
|
||||||
|
. '<tr><td style="padding:4px 8px;"><strong>Class</strong></td><td style="padding:4px 8px;">' . esc($className) . '</td></tr>'
|
||||||
|
. '<tr><td style="padding:4px 8px;"><strong>Exam type</strong></td><td style="padding:4px 8px;">' . esc($draft['exam_type'] ?? 'N/A') . '</td></tr>'
|
||||||
|
. '<tr><td style="padding:4px 8px;"><strong>Version</strong></td><td style="padding:4px 8px;">' . esc((string) ($draft['version'] ?? '')) . '</td></tr>'
|
||||||
|
. '<tr><td style="padding:4px 8px;"><strong>Status</strong></td><td style="padding:4px 8px;">' . esc((string) ($draft['status'] ?? 'submitted')) . '</td></tr>'
|
||||||
|
. '<tr><td style="padding:4px 8px;"><strong>Submitted at</strong></td><td style="padding:4px 8px;">' . esc((string) $submittedAt) . '</td></tr>'
|
||||||
|
. '</table>'
|
||||||
|
. '<p>Review drafts: <a href="' . esc(base_url('administrator/exam-drafts')) . '">Admin exam drafts</a></p>';
|
||||||
|
|
||||||
|
$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
|
private function nextDraftVersion(array $draft): int
|
||||||
{
|
{
|
||||||
$query = $this->examDraftModel
|
$query = $this->examDraftModel
|
||||||
@@ -887,6 +954,36 @@ class ExamDraftController extends BaseController
|
|||||||
return $current + 1;
|
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
|
private function statusOptions(): array
|
||||||
{
|
{
|
||||||
return ['submitted', 'accepted', 'review needed', 'rejected', 'canceled', 'under review', 'legacy'];
|
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]['_is_review_row'] = true;
|
||||||
$grouped[$key]['review_files'] = [];
|
$grouped[$key]['review_files'] = [];
|
||||||
$grouped[$key]['final_version'] = null;
|
$grouped[$key]['final_version'] = null;
|
||||||
|
$grouped[$key]['_latest_review_comment'] = null;
|
||||||
}
|
}
|
||||||
$grouped[$key]['review_files'][] = [
|
$grouped[$key]['review_files'][] = [
|
||||||
'review_revision' => $reviewRev,
|
'review_revision' => $reviewRev,
|
||||||
@@ -1008,6 +1106,16 @@ class ExamDraftController extends BaseController
|
|||||||
'final_filename' => $row['final_filename'] ?? null,
|
'final_filename' => $row['final_filename'] ?? null,
|
||||||
'status' => $row['status'] ?? 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') {
|
if (strtolower((string) ($row['status'] ?? '')) === 'accepted') {
|
||||||
$currentFinal = $grouped[$key]['final_version'] ?? null;
|
$currentFinal = $grouped[$key]['final_version'] ?? null;
|
||||||
if ($currentFinal === null || $reviewRev > (int) ($currentFinal['review_revision'] ?? 0)) {
|
if ($currentFinal === null || $reviewRev > (int) ($currentFinal['review_revision'] ?? 0)) {
|
||||||
@@ -1029,8 +1137,12 @@ class ExamDraftController extends BaseController
|
|||||||
|
|
||||||
if (!empty($grouped[$key]['_is_review_row'])) {
|
if (!empty($grouped[$key]['_is_review_row'])) {
|
||||||
$reviewFiles = $grouped[$key]['review_files'] ?? [];
|
$reviewFiles = $grouped[$key]['review_files'] ?? [];
|
||||||
|
$latestReviewComment = $grouped[$key]['_latest_review_comment'] ?? null;
|
||||||
$grouped[$key] = $row;
|
$grouped[$key] = $row;
|
||||||
$grouped[$key]['review_files'] = $reviewFiles;
|
$grouped[$key]['review_files'] = $reviewFiles;
|
||||||
|
if (!empty($latestReviewComment['comment'])) {
|
||||||
|
$grouped[$key]['reviewer_comment'] = $latestReviewComment['comment'];
|
||||||
|
}
|
||||||
unset($grouped[$key]['_is_review_row']);
|
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_file'] = $row['final_version']['final_file'] ?? $row['final_file'] ?? null;
|
||||||
$row['final_filename'] = $row['final_version']['final_filename'] ?? $row['final_filename'] ?? 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['_is_review_row']);
|
||||||
|
unset($row['_latest_review_comment']);
|
||||||
}
|
}
|
||||||
unset($row);
|
unset($row);
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ class ExamDraftModel extends Model
|
|||||||
'admin_id',
|
'admin_id',
|
||||||
'is_legacy',
|
'is_legacy',
|
||||||
'reviewer_comment',
|
'reviewer_comment',
|
||||||
|
'reviewer_comments',
|
||||||
'admin_comments',
|
'admin_comments',
|
||||||
'reviewed_at',
|
'reviewed_at',
|
||||||
'final_file',
|
'final_file',
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ $fileAccept = implode(',', array_map(static fn ($x) => '.' . $x, $allowedExtensi
|
|||||||
<label class="form-check-label" for="minor_<?= (int) ($draft['id'] ?? 0) ?>">Minor edits</label>
|
<label class="form-check-label" for="minor_<?= (int) ($draft['id'] ?? 0) ?>">Minor edits</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<textarea name="reviewer_comment" class="form-control form-control-sm js-review-comment" rows="3" placeholder="Feedback for the teacher"><?= esc($draft['reviewer_comment'] ?? $draft['admin_comments'] ?? '') ?></textarea>
|
<textarea name="reviewer_comment" class="form-control form-control-sm js-review-comment" rows="3" placeholder="Feedback for the teacher"><?= esc($draft['reviewer_comment'] ?? $draft['reviewer_comments'] ?? $draft['admin_comments'] ?? '') ?></textarea>
|
||||||
<div class="d-flex flex-wrap gap-2">
|
<div class="d-flex flex-wrap gap-2">
|
||||||
<span class="text-muted small js-review-status"></span>
|
<span class="text-muted small js-review-status"></span>
|
||||||
</div>
|
</div>
|
||||||
@@ -392,19 +392,27 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveRow = (row) => {
|
const saveRow = (row, options = {}) => {
|
||||||
const form = row.querySelector('form');
|
const form = row.querySelector('form');
|
||||||
if (!form) {
|
if (!form) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const statusSelect = form.querySelector('select[name="review_status"]');
|
const statusSelect = form.querySelector('select[name="review_status"]');
|
||||||
if (!statusSelect || statusSelect.value === '') {
|
if (!statusSelect) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const statusLabel = form.querySelector('.js-review-status');
|
const statusLabel = form.querySelector('.js-review-status');
|
||||||
const data = new FormData(form);
|
const data = new FormData(form);
|
||||||
data.delete('final_file');
|
data.delete('final_file');
|
||||||
data.delete('old_exam_file');
|
data.delete('old_exam_file');
|
||||||
|
if (options.commitComment) {
|
||||||
|
const commentInput = form.querySelector('textarea[name="reviewer_comment"]');
|
||||||
|
if (commentInput) {
|
||||||
|
const raw = commentInput.value || '';
|
||||||
|
const committed = raw.endsWith('\n') ? raw : raw + '\n';
|
||||||
|
data.set('reviewer_comment', committed);
|
||||||
|
}
|
||||||
|
}
|
||||||
const csrfToken = syncCsrfToken(form);
|
const csrfToken = syncCsrfToken(form);
|
||||||
if (csrfToken) {
|
if (csrfToken) {
|
||||||
data.set(csrfTokenName, csrfToken);
|
data.set(csrfTokenName, csrfToken);
|
||||||
@@ -480,10 +488,29 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const row = target.closest('tr');
|
const row = target.closest('tr');
|
||||||
if (row) {
|
if (row && target.value.endsWith('\n')) {
|
||||||
debouncedSave(row);
|
const lastCommitted = target.dataset.lastCommitted || '';
|
||||||
|
if (target.value !== lastCommitted) {
|
||||||
|
target.dataset.lastCommitted = target.value;
|
||||||
|
debouncedSave(row, { commitComment: true });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.addEventListener('blur', (event) => {
|
||||||
|
const target = event.target;
|
||||||
|
if (!(target instanceof HTMLTextAreaElement) || target.name !== 'reviewer_comment') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const row = target.closest('tr');
|
||||||
|
if (row) {
|
||||||
|
const lastCommitted = target.dataset.lastCommitted || '';
|
||||||
|
if (target.value !== lastCommitted) {
|
||||||
|
target.dataset.lastCommitted = target.value;
|
||||||
|
debouncedSave(row, { commitComment: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<?= $this->endSection() ?>
|
<?= $this->endSection() ?>
|
||||||
@@ -496,13 +523,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
.exam-drafts-table th {
|
.exam-drafts-table th {
|
||||||
min-width: 120px;
|
min-width: 0;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.exam-drafts-table td {
|
.exam-drafts-table td {
|
||||||
max-width: 220px;
|
max-width: none;
|
||||||
}
|
}
|
||||||
.exam-drafts-table {
|
.exam-drafts-table {
|
||||||
table-layout: auto;
|
table-layout: auto;
|
||||||
|
width: max-content;
|
||||||
}
|
}
|
||||||
.exam-drafts-table thead th {
|
.exam-drafts-table thead th {
|
||||||
background-color: #f8f9fa;
|
background-color: #f8f9fa;
|
||||||
|
|||||||
@@ -11,9 +11,36 @@ $schoolYear = $schoolYear ?? '';
|
|||||||
$semester = $semester ?? '';
|
$semester = $semester ?? '';
|
||||||
$maxUploadBytes = $maxUploadBytes ?? (12 * 1024 * 1024);
|
$maxUploadBytes = $maxUploadBytes ?? (12 * 1024 * 1024);
|
||||||
?>
|
?>
|
||||||
<div class="container-xxl py-5">
|
<style>
|
||||||
|
.teacher-drafts-page,
|
||||||
|
.teacher-drafts-page * {
|
||||||
|
font-family: Arial, sans-serif !important;
|
||||||
|
}
|
||||||
|
.teacher-drafts-table {
|
||||||
|
table-layout: auto;
|
||||||
|
width: max-content;
|
||||||
|
}
|
||||||
|
.teacher-drafts-table th {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.teacher-drafts-table td {
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
.teacher-drafts-table th {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.teacher-drafts-table td.title-cell {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="container-xxl py-5 teacher-drafts-page">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1 class="mb-4">Exam drafts</h1>
|
<div class="d-flex flex-wrap justify-content-between align-items-center mb-4">
|
||||||
|
<h1 class="mb-0">Exam drafts</h1>
|
||||||
|
<?php if (!empty($legacyExams)): ?>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary" id="legacyToggle">Legacy Exams</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
<?php if (session()->getFlashdata('success')): ?>
|
<?php if (session()->getFlashdata('success')): ?>
|
||||||
<div class="alert alert-success"><?= esc(session()->getFlashdata('success')) ?></div>
|
<div class="alert alert-success"><?= esc(session()->getFlashdata('success')) ?></div>
|
||||||
@@ -24,7 +51,7 @@ $maxUploadBytes = $maxUploadBytes ?? (12 * 1024 * 1024);
|
|||||||
|
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<?= form_open_multipart(base_url('teacher/exam-drafts'), ['class' => 'row g-3']) ?>
|
<?= form_open_multipart(base_url('teacher/exam-drafts'), ['class' => 'row g-3', 'id' => 'examDraftForm']) ?>
|
||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label" for="exam_type">Exam type <span class="text-danger">*</span></label>
|
<label class="form-label" for="exam_type">Exam type <span class="text-danger">*</span></label>
|
||||||
@@ -65,7 +92,7 @@ $maxUploadBytes = $maxUploadBytes ?? (12 * 1024 * 1024);
|
|||||||
<h2 class="h5 mb-3">Your submissions</h2>
|
<h2 class="h5 mb-3">Your submissions</h2>
|
||||||
<?php if (!empty($drafts)): ?>
|
<?php if (!empty($drafts)): ?>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-striped align-middle">
|
<table class="table table-striped align-middle teacher-drafts-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Class</th>
|
<th>Class</th>
|
||||||
@@ -85,7 +112,7 @@ $maxUploadBytes = $maxUploadBytes ?? (12 * 1024 * 1024);
|
|||||||
?>
|
?>
|
||||||
<tr data-draft-id="<?= (int) ($d['id'] ?? 0) ?>">
|
<tr data-draft-id="<?= (int) ($d['id'] ?? 0) ?>">
|
||||||
<td><?= esc($d['class_section_name'] ?? '') ?></td>
|
<td><?= esc($d['class_section_name'] ?? '') ?></td>
|
||||||
<td><?= esc($d['draft_title'] ?? $d['exam_type'] ?? '') ?></td>
|
<td class="title-cell"><?= esc($d['draft_title'] ?? $d['exam_type'] ?? '') ?></td>
|
||||||
<td><?= esc((string) ($d['version'] ?? '')) ?></td>
|
<td><?= esc((string) ($d['version'] ?? '')) ?></td>
|
||||||
<td><?= $badgeHtml ?></td>
|
<td><?= $badgeHtml ?></td>
|
||||||
<td>
|
<td>
|
||||||
@@ -124,7 +151,7 @@ $maxUploadBytes = $maxUploadBytes ?? (12 * 1024 * 1024);
|
|||||||
—
|
—
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-nowrap"><?= esc($d['reviewer_comment'] ?? $d['admin_comments'] ?? '—') ?></td>
|
<td><?= nl2br(esc($d['reviewer_comment'] ?? $d['reviewer_comments'] ?? $d['admin_comments'] ?? '—')) ?></td>
|
||||||
<td><?= esc(!empty($d['updated_at']) ? local_datetime($d['updated_at'], 'm-d-Y H:i') : '') ?></td>
|
<td><?= esc(!empty($d['updated_at']) ? local_datetime($d['updated_at'], 'm-d-Y H:i') : '') ?></td>
|
||||||
|
|
||||||
</tr>
|
</tr>
|
||||||
@@ -137,15 +164,18 @@ $maxUploadBytes = $maxUploadBytes ?? (12 * 1024 * 1024);
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if (!empty($legacyExams)): ?>
|
<?php if (!empty($legacyExams)): ?>
|
||||||
<h2 class="h5 mt-5 mb-3">Previous exams (legacy)</h2>
|
<div id="legacySection" class="table-responsive d-none mt-4">
|
||||||
<div class="table-responsive">
|
<table class="table table-sm table-striped align-middle teacher-drafts-table">
|
||||||
<table class="table table-sm table-striped align-middle">
|
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Class</th>
|
<th>Class</th>
|
||||||
<th>Title</th>
|
<th>Title</th>
|
||||||
<th>Ver.</th>
|
<th>Ver.</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
|
<th>School year</th>
|
||||||
|
<th>Semester</th>
|
||||||
|
<th>Exam type</th>
|
||||||
|
<th>Updated</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -154,12 +184,16 @@ $maxUploadBytes = $maxUploadBytes ?? (12 * 1024 * 1024);
|
|||||||
<?php $st = strtolower((string) ($d['status'] ?? '')); ?>
|
<?php $st = strtolower((string) ($d['status'] ?? '')); ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td><?= esc($d['class_section_name'] ?? '') ?></td>
|
<td><?= esc($d['class_section_name'] ?? '') ?></td>
|
||||||
<td><?= esc($d['draft_title'] ?? $d['exam_type'] ?? '') ?></td>
|
<td class="title-cell"><?= esc($d['draft_title'] ?? $d['exam_type'] ?? '') ?></td>
|
||||||
<td><?= esc((string) ($d['version'] ?? '')) ?></td>
|
<td><?= esc((string) ($d['version'] ?? '')) ?></td>
|
||||||
<td><?= $renderBadge($st, $statusBadges) ?></td>
|
<td><?= $renderBadge($st, $statusBadges) ?></td>
|
||||||
|
<td><?= esc($d['school_year'] ?? '') ?></td>
|
||||||
|
<td><?= esc($d['semester'] ?? '') ?></td>
|
||||||
|
<td><?= esc($d['exam_type'] ?? '') ?></td>
|
||||||
|
<td><?= esc(!empty($d['updated_at']) ? local_datetime($d['updated_at'], 'm-d-Y H:i') : '') ?></td>
|
||||||
<td>
|
<td>
|
||||||
<?php if (!empty($d['is_printable'])): ?>
|
<?php if (!empty($d['final_file'])): ?>
|
||||||
<a class="btn btn-sm btn-outline-primary" href="<?= base_url('teacher/exam-drafts/print/' . (int) ($d['id'] ?? 0)) ?>" target="_blank" rel="noopener">Print</a>
|
<a class="btn btn-sm btn-outline-primary" href="<?= base_url('exam-drafts/files/final/' . $d['final_file']) ?>" target="_blank" rel="noopener">View</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -172,8 +206,41 @@ $maxUploadBytes = $maxUploadBytes ?? (12 * 1024 * 1024);
|
|||||||
</div>
|
</div>
|
||||||
<?= $this->endSection() ?>
|
<?= $this->endSection() ?>
|
||||||
|
|
||||||
|
<?= $this->section('scripts') ?>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const csrfTokenName = <?= json_encode(csrf_token(), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT) ?>;
|
||||||
|
const csrfCookieNames = <?= json_encode(array_values(array_filter([
|
||||||
|
config('Security')->csrfCookieName ?? null,
|
||||||
|
config('Security')->cookieName ?? null,
|
||||||
|
'csrf_cookie_name',
|
||||||
|
])), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT) ?>;
|
||||||
|
|
||||||
|
const readCookie = (name) => {
|
||||||
|
const match = document.cookie.match(new RegExp('(?:^|; )' + name.replace(/[$()*+.?[\\\]^{|}-]/g, '\\$&') + '=([^;]*)'));
|
||||||
|
return match ? decodeURIComponent(match[1]) : '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const syncCsrfToken = (form) => {
|
||||||
|
let tokenValue = '';
|
||||||
|
csrfCookieNames.some((name) => {
|
||||||
|
tokenValue = readCookie(name);
|
||||||
|
return tokenValue !== '';
|
||||||
|
});
|
||||||
|
if (!tokenValue || !form) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const tokenInput = form.querySelector(`input[name="${csrfTokenName}"]`);
|
||||||
|
if (tokenInput) {
|
||||||
|
tokenInput.value = tokenValue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const draftForm = document.getElementById('examDraftForm');
|
||||||
|
if (draftForm) {
|
||||||
|
draftForm.addEventListener('submit', () => syncCsrfToken(draftForm));
|
||||||
|
}
|
||||||
|
|
||||||
const statusBadges = <?= json_encode($statusBadges, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT) ?>;
|
const statusBadges = <?= json_encode($statusBadges, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT) ?>;
|
||||||
const rows = new Map();
|
const rows = new Map();
|
||||||
document.querySelectorAll('tr[data-draft-id]').forEach((row) => {
|
document.querySelectorAll('tr[data-draft-id]').forEach((row) => {
|
||||||
@@ -227,5 +294,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
poll();
|
poll();
|
||||||
setInterval(poll, 15000);
|
setInterval(poll, 15000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const legacyToggle = document.getElementById('legacyToggle');
|
||||||
|
const legacySection = document.getElementById('legacySection');
|
||||||
|
if (legacyToggle && legacySection) {
|
||||||
|
legacyToggle.addEventListener('click', () => {
|
||||||
|
legacySection.classList.toggle('d-none');
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
<?= $this->endSection() ?>
|
||||||
|
|||||||
Reference in New Issue
Block a user