exam draft notification and submission
This commit is contained in:
@@ -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;
|
||||
|
||||
if (!empty($classSectionIds)) {
|
||||
$legacyExams = $this->examDraftModel
|
||||
$legacyQuery = $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)
|
||||
->where('exam_drafts.final_file IS NOT NULL', null, false);
|
||||
|
||||
if (!empty($classSectionIds)) {
|
||||
$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;
|
||||
@@ -285,6 +296,7 @@ class ExamDraftController extends BaseController
|
||||
$this->applyAuthorFilePayload($updateSubmit, $teacherFile, $teacherFilename);
|
||||
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.');
|
||||
}
|
||||
@@ -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 = '<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
|
||||
{
|
||||
$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);
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ class ExamDraftModel extends Model
|
||||
'admin_id',
|
||||
'is_legacy',
|
||||
'reviewer_comment',
|
||||
'reviewer_comments',
|
||||
'admin_comments',
|
||||
'reviewed_at',
|
||||
'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>
|
||||
</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">
|
||||
<span class="text-muted small js-review-status"></span>
|
||||
</div>
|
||||
@@ -392,19 +392,27 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
};
|
||||
|
||||
const saveRow = (row) => {
|
||||
const saveRow = (row, options = {}) => {
|
||||
const form = row.querySelector('form');
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
const statusSelect = form.querySelector('select[name="review_status"]');
|
||||
if (!statusSelect || statusSelect.value === '') {
|
||||
if (!statusSelect) {
|
||||
return;
|
||||
}
|
||||
const statusLabel = form.querySelector('.js-review-status');
|
||||
const data = new FormData(form);
|
||||
data.delete('final_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);
|
||||
if (csrfToken) {
|
||||
data.set(csrfTokenName, csrfToken);
|
||||
@@ -480,10 +488,29 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
return;
|
||||
}
|
||||
const row = target.closest('tr');
|
||||
if (row) {
|
||||
debouncedSave(row);
|
||||
if (row && target.value.endsWith('\n')) {
|
||||
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>
|
||||
<?= $this->endSection() ?>
|
||||
@@ -496,13 +523,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
word-break: break-word;
|
||||
}
|
||||
.exam-drafts-table th {
|
||||
min-width: 120px;
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.exam-drafts-table td {
|
||||
max-width: 220px;
|
||||
max-width: none;
|
||||
}
|
||||
.exam-drafts-table {
|
||||
table-layout: auto;
|
||||
width: max-content;
|
||||
}
|
||||
.exam-drafts-table thead th {
|
||||
background-color: #f8f9fa;
|
||||
|
||||
@@ -11,9 +11,36 @@ $schoolYear = $schoolYear ?? '';
|
||||
$semester = $semester ?? '';
|
||||
$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">
|
||||
<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')): ?>
|
||||
<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-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() ?>
|
||||
<div class="col-md-6">
|
||||
<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>
|
||||
<?php if (!empty($drafts)): ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped align-middle">
|
||||
<table class="table table-striped align-middle teacher-drafts-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Class</th>
|
||||
@@ -85,7 +112,7 @@ $maxUploadBytes = $maxUploadBytes ?? (12 * 1024 * 1024);
|
||||
?>
|
||||
<tr data-draft-id="<?= (int) ($d['id'] ?? 0) ?>">
|
||||
<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><?= $badgeHtml ?></td>
|
||||
<td>
|
||||
@@ -124,7 +151,7 @@ $maxUploadBytes = $maxUploadBytes ?? (12 * 1024 * 1024);
|
||||
—
|
||||
<?php endif; ?>
|
||||
</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>
|
||||
|
||||
</tr>
|
||||
@@ -137,15 +164,18 @@ $maxUploadBytes = $maxUploadBytes ?? (12 * 1024 * 1024);
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($legacyExams)): ?>
|
||||
<h2 class="h5 mt-5 mb-3">Previous exams (legacy)</h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-striped align-middle">
|
||||
<div id="legacySection" class="table-responsive d-none mt-4">
|
||||
<table class="table table-sm table-striped align-middle teacher-drafts-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Class</th>
|
||||
<th>Title</th>
|
||||
<th>Ver.</th>
|
||||
<th>Status</th>
|
||||
<th>School year</th>
|
||||
<th>Semester</th>
|
||||
<th>Exam type</th>
|
||||
<th>Updated</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -154,12 +184,16 @@ $maxUploadBytes = $maxUploadBytes ?? (12 * 1024 * 1024);
|
||||
<?php $st = strtolower((string) ($d['status'] ?? '')); ?>
|
||||
<tr>
|
||||
<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><?= $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>
|
||||
<?php if (!empty($d['is_printable'])): ?>
|
||||
<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>
|
||||
<?php if (!empty($d['final_file'])): ?>
|
||||
<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; ?>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -172,8 +206,41 @@ $maxUploadBytes = $maxUploadBytes ?? (12 * 1024 * 1024);
|
||||
</div>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('scripts') ?>
|
||||
<script>
|
||||
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 rows = new Map();
|
||||
document.querySelectorAll('tr[data-draft-id]').forEach((row) => {
|
||||
@@ -227,5 +294,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
poll();
|
||||
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>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
Reference in New Issue
Block a user