exam draft notification and submission

This commit is contained in:
root
2026-04-09 01:50:24 -04:00
parent 112d073235
commit 2543df3d33
4 changed files with 262 additions and 40 deletions
+36 -7
View 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;
+88 -12
View File
@@ -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() ?>