593 lines
39 KiB
PHP
Executable File
593 lines
39 KiB
PHP
Executable File
<?= $this->extend('layout/management_layout') ?>
|
|
<?= $this->section('content') ?>
|
|
<?php
|
|
$statusBadges = $statusBadges ?? [];
|
|
$drafts = $drafts ?? [];
|
|
$legacyByClass = $legacyByClass ?? [];
|
|
$classSections = $classSections ?? [];
|
|
$examTypes = $examTypes ?? [];
|
|
$visibleClasses = $visibleClasses ?? [];
|
|
$classDraftGroups = $classDraftGroups ?? [];
|
|
$newSubmissionClasses = $newSubmissionClasses ?? [];
|
|
$schoolYear = $schoolYear ?? '';
|
|
$semester = $semester ?? '';
|
|
$maxUploadBytes = $maxUploadBytes ?? (12 * 1024 * 1024);
|
|
$allowedExtensions = $allowedExtensions ?? ['doc', 'docx', 'pdf'];
|
|
$reviewActionUrl = $reviewActionUrl ?? base_url('administrator/exam-drafts/review');
|
|
$legacyUploadUrl = $legacyUploadUrl ?? base_url('administrator/exam-drafts/upload-legacy');
|
|
$reviewPortalLabel = $reviewPortalLabel ?? 'Administrator';
|
|
|
|
$renderBadge = static function (string $status, array $badges): string {
|
|
$b = $badges[$status] ?? ['label' => $status, 'class' => 'bg-secondary text-white'];
|
|
$style = !empty($b['style']) ? ' style="' . esc($b['style']) . '"' : '';
|
|
return '<span class="badge ' . esc($b['class']) . '"' . $style . '>' . esc($b['label']) . '</span>';
|
|
};
|
|
|
|
$fileAccept = implode(',', array_map(static fn ($x) => '.' . $x, $allowedExtensions));
|
|
?>
|
|
<div class="container-fluid px-4 py-4">
|
|
<div class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-center gap-3 mb-4">
|
|
<div>
|
|
<h1 class="h3 mb-1">Exam draft submissions</h1>
|
|
<p class="text-muted mb-0 small">
|
|
<?= esc($reviewPortalLabel) ?> review portal · <?= esc($semester ?: 'Semester') ?> <?= esc($schoolYear ?: '') ?>
|
|
</p>
|
|
</div>
|
|
<div class="text-muted small d-flex flex-column align-items-lg-end">
|
|
<span>Allowed files: <?= esc(implode(', ', $allowedExtensions)) ?></span>
|
|
<span>Max size: <?= number_format($maxUploadBytes / 1024 / 1024, 0) ?> MB</span>
|
|
</div>
|
|
</div>
|
|
|
|
<?php if (session()->getFlashdata('success')): ?>
|
|
<div class="alert alert-success"><?= esc(session()->getFlashdata('success')) ?></div>
|
|
<?php elseif (session()->getFlashdata('error')): ?>
|
|
<div class="alert alert-danger"><?= esc(session()->getFlashdata('error')) ?></div>
|
|
<?php endif; ?>
|
|
|
|
<ul class="nav nav-pills mb-3" id="examDraftTabs" role="tablist">
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link active" id="submissions-tab" data-bs-toggle="pill" data-bs-target="#submissions" type="button" role="tab" aria-controls="submissions" aria-selected="true">
|
|
Submissions
|
|
</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" id="legacy-tab" data-bs-toggle="pill" data-bs-target="#legacy" type="button" role="tab" aria-controls="legacy" aria-selected="false">
|
|
Legacy exams
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
|
|
<div class="tab-content" id="examDraftTabsContent">
|
|
<div class="tab-pane fade show active" id="submissions" role="tabpanel" aria-labelledby="submissions-tab">
|
|
<div class="card mb-4">
|
|
<div class="card-body">
|
|
<?php if (empty($visibleClasses)): ?>
|
|
<p class="text-muted mb-0">No classes with enrolled students are available for this term.</p>
|
|
<?php else: ?>
|
|
<div class="accordion exam-drafts-accordion" id="examDraftsAccordion">
|
|
<?php foreach ($visibleClasses as $classSectionId => $classInfo): ?>
|
|
<?php $classDraftsForSection = $classDraftGroups[$classSectionId] ?? []; ?>
|
|
<?php $collapseId = 'classDraftGroup_' . $classSectionId; ?>
|
|
<div class="accordion-item mb-3">
|
|
<h2 class="accordion-header" id="heading_<?= esc($collapseId) ?>">
|
|
<button class="accordion-button collapsed px-3" type="button" data-bs-toggle="collapse" data-bs-target="#<?= esc($collapseId) ?>" aria-expanded="false" aria-controls="<?= esc($collapseId) ?>">
|
|
<div class="d-flex flex-column flex-lg-row w-100 justify-content-between gap-2">
|
|
<div>
|
|
<strong><?= esc($classInfo['class_section_name'] ?? ('Class ' . $classSectionId)) ?></strong>
|
|
<div class="small text-muted">
|
|
<?= esc($classInfo['student_count'] ?? 0) ?> students · <?= esc(count($classDraftsForSection)) ?> submissions
|
|
<?php if (!empty($newSubmissionClasses[$classSectionId])): ?>
|
|
<span class="badge bg-info text-dark ms-2">New submission</span>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
<span class="badge bg-secondary align-self-start">
|
|
<?= esc(count($classDraftsForSection)) ?>
|
|
<?= count($classDraftsForSection) === 1 ? 'submission' : 'submissions' ?>
|
|
</span>
|
|
</div>
|
|
</button>
|
|
</h2>
|
|
<div id="<?= esc($collapseId) ?>" class="accordion-collapse collapse" aria-labelledby="heading_<?= esc($collapseId) ?>">
|
|
<div class="accordion-body pt-0 px-3">
|
|
<?php if (empty($classDraftsForSection)): ?>
|
|
<p class="text-muted mb-0">No submissions yet for this class.</p>
|
|
<?php else: ?>
|
|
<div class="table-responsive">
|
|
<table class="table table-striped table-bordered align-middle mb-0 exam-drafts-table no-mgmt-sticky">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>Teacher</th>
|
|
<th>Class</th>
|
|
<th>Title & AuthorNote</th>
|
|
<th>Version</th>
|
|
<th>Date-Time</th>
|
|
<th>Status</th>
|
|
<th>Files</th>
|
|
<th>Reviewer Action</th>
|
|
<th>Final version</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($classDraftsForSection as $draft): ?>
|
|
<?php
|
|
$teacherName = trim(($draft['teacher_first'] ?? '') . ' ' . ($draft['teacher_last'] ?? ''));
|
|
if ($teacherName === '') {
|
|
$teacherName = ($draft['admin_id'] ?? null) === ($draft['teacher_id'] ?? null)
|
|
? 'Admin upload'
|
|
: 'User #' . ($draft['teacher_id'] ?? 'N/A');
|
|
}
|
|
$st = strtolower((string) ($draft['status'] ?? ''));
|
|
$adminName = trim(($draft['admin_first'] ?? '') . ' ' . ($draft['admin_last'] ?? ''));
|
|
$authorNote = $draft['author_comment'] ?? $draft['description'] ?? '';
|
|
$reviewerNote = $draft['reviewer_comment'] ?? $draft['admin_comments'] ?? '';
|
|
?>
|
|
<tr>
|
|
<td><?= esc($teacherName) ?></td>
|
|
<td><?= esc($draft['class_section_name'] ?? 'Unknown') ?></td>
|
|
<td>
|
|
<strong><?= esc($draft['draft_title'] ?? 'Untitled') ?></strong>
|
|
<div class="small text-muted"><?= esc($draft['exam_type'] ?? 'N/A') ?></div>
|
|
<?php if ($authorNote !== ''): ?>
|
|
<div class="small mt-1">
|
|
<span class="text-muted fw-semibold">Author:</span>
|
|
<?= esc($authorNote) ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
<?php if ($reviewerNote !== ''): ?>
|
|
<div class="small mt-1 border-top pt-1">
|
|
<span class="text-muted fw-semibold">Reviewer:</span>
|
|
<?= esc($reviewerNote) ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td class="text-nowrap">v<?= esc((string) ($draft['version'] ?? 1)) ?></td>
|
|
<td class="text-nowrap"><?= esc(!empty($draft['created_at']) ? local_datetime($draft['created_at'], 'M j, Y g:i A') : '—') ?></td>
|
|
<td>
|
|
<?php
|
|
$badgeData = $statusBadges[$st] ?? ['label' => $st, 'class' => 'bg-secondary text-white'];
|
|
$badgeStyle = !empty($badgeData['style']) ? ' style="' . esc($badgeData['style']) . '"' : '';
|
|
?>
|
|
<span class="badge <?= esc($badgeData['class']) ?> js-status-badge" data-status="<?= esc($st) ?>"<?= $badgeStyle ?>>
|
|
<?= esc($badgeData['label']) ?>
|
|
</span>
|
|
<div class="small text-muted mt-1 js-acceptance-note">
|
|
<?php if ($st === 'accepted' && !empty($draft['acceptance_type'])): ?>
|
|
<?= $draft['acceptance_type'] === 'minor_edits' ? 'Accepted w/ minor edits' : 'Accepted as is' ?>
|
|
<?php endif; ?>
|
|
</div>
|
|
<?php if (!empty($draft['reviewed_at'])): ?>
|
|
<div class="small text-muted mt-1">
|
|
Reviewed <?= esc(local_datetime($draft['reviewed_at'], 'M j, Y g:i A')) ?>
|
|
<?php if ($adminName !== ''): ?>
|
|
by <?= esc($adminName) ?>
|
|
<?php endif; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td>
|
|
<?php $formId = 'review_form_' . (int) ($draft['id'] ?? 0); ?>
|
|
<?php $revNumber = max(1, (int) ($draft['version'] ?? 1)); ?>
|
|
<?php if (!empty($draft['teacher_file'])): ?>
|
|
<div>
|
|
<a href="<?= base_url('exam-drafts/files/teacher/' . $draft['teacher_file']) ?>" target="_blank" rel="noopener">
|
|
<?= esc('Ver' . $revNumber . ' ' . ($draft['teacher_filename'] ?? 'Submitted draft')) ?>
|
|
</a>
|
|
</div>
|
|
<?php else: ?>
|
|
<span class="text-muted small">No teacher file</span>
|
|
<?php endif; ?>
|
|
<?php if (!empty($draft['final_file']) && strtolower((string) ($draft['status'] ?? '')) === 'accepted'): ?>
|
|
<div class="mt-2">
|
|
<a href="<?= base_url('exam-drafts/files/final/' . $draft['final_file']) ?>" target="_blank" rel="noopener" class="link-success small">
|
|
<?= esc('Final ' . ($draft['final_filename'] ?? 'Final draft')) ?>
|
|
</a>
|
|
</div>
|
|
<?php endif; ?>
|
|
<?php if (!empty($draft['review_files'])): ?>
|
|
<div class="mt-2">
|
|
<?php
|
|
$reviewLinks = [];
|
|
foreach ($draft['review_files'] as $rf) {
|
|
$rev = max(1, (int) ($rf['review_revision'] ?? 1));
|
|
$name = $rf['final_filename'] ?? 'Review file';
|
|
$file = $rf['final_file'] ?? '';
|
|
if ($file !== '') {
|
|
$reviewLinks[] = '<a href="' . esc(base_url('exam-drafts/files/final/' . $file)) . '" target="_blank" rel="noopener" class="link-success small">' . esc('Ver' . $revNumber . '_' . $rev . ' ' . $name) . '</a>';
|
|
}
|
|
}
|
|
?>
|
|
<?= !empty($reviewLinks) ? implode(' | ', $reviewLinks) : '' ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
<div class="mt-2">
|
|
<?php $fileInputId = 'review_file_' . (int) ($draft['id'] ?? 0); ?>
|
|
<input type="file" name="final_file" id="<?= esc($fileInputId) ?>" class="form-control form-control-sm" accept="<?= esc($fileAccept) ?>" form="<?= esc($formId) ?>">
|
|
<button type="submit" class="btn btn-sm btn-outline-primary mt-2" form="<?= esc($formId) ?>">Upload review file</button>
|
|
</div>
|
|
</td>
|
|
<td style="min-width: 280px;">
|
|
<?= form_open_multipart($reviewActionUrl, ['class' => 'vstack gap-2', 'id' => $formId]) ?>
|
|
<?= csrf_field() ?>
|
|
<input type="hidden" name="draft_id" value="<?= (int) ($draft['id'] ?? 0) ?>">
|
|
<?php $selectedStatus = ''; ?>
|
|
<select name="review_status" class="form-select form-select-sm" required>
|
|
<option value="" <?= $selectedStatus === '' ? 'selected' : '' ?> disabled>— Select status —</option>
|
|
<option value="accepted" <?= $selectedStatus === 'accepted' ? 'selected' : '' ?>>Accepted</option>
|
|
<option value="legacy" <?= $selectedStatus === 'legacy' ? 'selected' : '' ?>>Legacy</option>
|
|
<option value="canceled" <?= $selectedStatus === 'canceled' ? 'selected' : '' ?>>Canceled</option>
|
|
<option value="rejected" <?= $selectedStatus === 'rejected' ? 'selected' : '' ?>>Rejected</option>
|
|
<option value="review needed" <?= $selectedStatus === 'review needed' ? 'selected' : '' ?>>Review needed</option>
|
|
<option value="under review" <?= $selectedStatus === 'under review' ? 'selected' : '' ?>>Under review</option>
|
|
</select>
|
|
<div class="small js-acceptance-group d-none">
|
|
<span class="d-block mb-1">As is / Minor edits</span>
|
|
<?php $acc = (string) ($draft['acceptance_type'] ?? ''); ?>
|
|
<div class="form-check form-check-inline">
|
|
<input class="form-check-input" type="radio" name="acceptance_type" value="as_is" id="as_is_<?= (int) ($draft['id'] ?? 0) ?>" <?= $acc !== 'minor_edits' ? 'checked' : '' ?>>
|
|
<label class="form-check-label" for="as_is_<?= (int) ($draft['id'] ?? 0) ?>">As is</label>
|
|
</div>
|
|
<div class="form-check form-check-inline">
|
|
<input class="form-check-input" type="radio" name="acceptance_type" value="minor_edits" id="minor_<?= (int) ($draft['id'] ?? 0) ?>" <?= $acc === 'minor_edits' ? 'checked' : '' ?>>
|
|
<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['reviewer_comments'] ?? $draft['admin_comments'] ?? '') ?></textarea>
|
|
<div class="d-flex flex-wrap gap-2">
|
|
<span class="text-muted small js-review-status"></span>
|
|
</div>
|
|
<?= form_close() ?>
|
|
</td>
|
|
<td class="text-nowrap" style="min-width: 220px;">
|
|
<?php
|
|
$pdfFile = $draft['final_pdf_file'] ?? null;
|
|
if (empty($pdfFile) && !empty($draft['final_file']) && strtolower(pathinfo($draft['final_file'], PATHINFO_EXTENSION)) === 'pdf') {
|
|
$pdfFile = $draft['final_file'];
|
|
}
|
|
?>
|
|
<?php if (!empty($pdfFile)): ?>
|
|
<div class="mt-2 d-flex flex-wrap gap-2">
|
|
<a class="btn btn-sm btn-outline-primary" href="<?= base_url('exam-drafts/files/final/' . $pdfFile) ?>" target="_blank" rel="noopener">View</a>
|
|
<a class="btn btn-sm btn-outline-secondary" href="<?= base_url('exam-drafts/files/final/' . $pdfFile) ?>" target="_blank" rel="noopener" download>Download</a>
|
|
</div>
|
|
<?php endif; ?>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tab-pane fade" id="legacy" role="tabpanel" aria-labelledby="legacy-tab">
|
|
<div class="card border-primary-subtle mb-3">
|
|
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<strong>Upload old / legacy exam</strong>
|
|
<div class="small text-muted">Store historic exams as accepted records.</div>
|
|
</div>
|
|
<span class="badge bg-primary-subtle text-primary">Admin only</span>
|
|
</div>
|
|
<div class="card-body">
|
|
<?= form_open_multipart($legacyUploadUrl, ['class' => 'row g-3']) ?>
|
|
<?= csrf_field() ?>
|
|
<div class="col-md-4">
|
|
<label class="form-label small">Class sections</label>
|
|
<select name="class_section_ids[]" class="form-select" multiple required size="6">
|
|
<?php foreach ($classSections as $cs): ?>
|
|
<option value="<?= esc($cs['class_section_id'] ?? '') ?>"><?= esc($cs['class_section_name'] ?? '') ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
<div class="form-text">Hold Ctrl (Windows) or Command (Mac) to select multiple sections.</div>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label small">School year</label>
|
|
<input type="text" name="school_year" class="form-control" value="<?= esc($schoolYear) ?>" placeholder="2025-2026" required>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label small">Semester</label>
|
|
<input type="text" name="semester" class="form-control" value="<?= esc($semester) ?>" placeholder="Fall / Spring" required>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label small">Exam type</label>
|
|
<select name="exam_type" class="form-select">
|
|
<option value="">— Select type —</option>
|
|
<?php foreach ($examTypes as $type): ?>
|
|
<option value="<?= esc($type) ?>"><?= esc($type) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="col-12">
|
|
<label class="form-label small">Upload file</label>
|
|
<input type="file" name="old_exam_file" class="form-control" accept="<?= esc($fileAccept) ?>" required>
|
|
<div class="form-text">Allowed: <?= esc(implode(', ', $allowedExtensions)) ?> • Max <?= number_format($maxUploadBytes / 1024 / 1024, 0) ?> MB</div>
|
|
</div>
|
|
<div class="col-12">
|
|
<button type="submit" class="btn btn-primary">Upload legacy exam</button>
|
|
</div>
|
|
<?= form_close() ?>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card mb-4">
|
|
<div class="card-body">
|
|
<?php if (empty($legacyByClass)): ?>
|
|
<p class="text-muted mb-0">No legacy exams uploaded yet.</p>
|
|
<?php else: ?>
|
|
<?php foreach ($legacyByClass as $group): ?>
|
|
<div class="mb-4">
|
|
<h6 class="mb-2"><?= esc($group['class_section_name'] ?? 'Class') ?></h6>
|
|
<div class="list-group">
|
|
<?php foreach ($group['items'] ?? [] as $item): ?>
|
|
<?php $lst = strtolower((string) ($item['status'] ?? '')); ?>
|
|
<div class="list-group-item d-flex justify-content-between align-items-start flex-wrap gap-2">
|
|
<div class="me-3">
|
|
<div class="fw-semibold"><?= esc($item['draft_title'] ?? 'Legacy exam') ?></div>
|
|
<div class="small text-muted">
|
|
<?= esc($item['exam_type'] ?? 'N/A') ?> •
|
|
<?= esc($item['semester'] ?? '') ?> <?= esc($item['school_year'] ?? '') ?>
|
|
</div>
|
|
<div class="small mt-1"><?= $renderBadge($lst, $statusBadges) ?></div>
|
|
</div>
|
|
<div class="text-end d-flex flex-wrap gap-2 justify-content-end">
|
|
<?php if (!empty($item['final_file'])): ?>
|
|
<a class="btn btn-sm btn-outline-primary" href="<?= base_url('exam-drafts/files/final/' . $item['final_file']) ?>" target="_blank" rel="noopener">View</a>
|
|
<a class="btn btn-sm btn-outline-secondary" href="<?= base_url('exam-drafts/files/final/' . $item['final_file']) ?>" target="_blank" rel="noopener" download>Download</a>
|
|
<?php else: ?>
|
|
<span class="text-muted small">File missing</span>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const submissionsTab = document.getElementById('submissions-tab');
|
|
if (submissionsTab) {
|
|
submissionsTab.addEventListener('click', () => {
|
|
window.location.reload();
|
|
});
|
|
}
|
|
const statusBadges = <?= json_encode($statusBadges, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT) ?>;
|
|
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) {
|
|
const tokenInput = form.querySelector(`input[name="${csrfTokenName}"]`);
|
|
if (tokenInput && tokenInput.value) {
|
|
tokenValue = tokenInput.value;
|
|
}
|
|
}
|
|
if (!tokenValue || !form) {
|
|
return tokenValue;
|
|
}
|
|
const tokenInput = form.querySelector(`input[name="${csrfTokenName}"]`);
|
|
if (tokenInput) {
|
|
tokenInput.value = tokenValue;
|
|
}
|
|
return tokenValue;
|
|
};
|
|
const updateRow = (row) => {
|
|
const statusSelect = row.querySelector('select[name="review_status"]');
|
|
const acceptanceGroup = row.querySelector('.js-acceptance-group');
|
|
if (!statusSelect || !acceptanceGroup) {
|
|
return;
|
|
}
|
|
acceptanceGroup.classList.toggle('d-none', statusSelect.value !== 'accepted');
|
|
};
|
|
|
|
const updateStatusUI = (row) => {
|
|
const statusSelect = row.querySelector('select[name="review_status"]');
|
|
const badge = row.querySelector('.js-status-badge');
|
|
if (!statusSelect || !badge || statusSelect.value === '') {
|
|
return;
|
|
}
|
|
const status = statusSelect.value;
|
|
const badgeData = statusBadges[status] || { label: status, class: 'bg-secondary text-white' };
|
|
badge.textContent = badgeData.label || status;
|
|
badge.className = `badge ${badgeData.class} js-status-badge`;
|
|
if (badgeData.style) {
|
|
badge.setAttribute('style', badgeData.style);
|
|
} else {
|
|
badge.removeAttribute('style');
|
|
}
|
|
badge.dataset.status = status;
|
|
|
|
const acceptanceNote = row.querySelector('.js-acceptance-note');
|
|
if (acceptanceNote) {
|
|
if (status === 'accepted') {
|
|
const acc = row.querySelector('input[name="acceptance_type"]:checked');
|
|
acceptanceNote.textContent = acc && acc.value === 'minor_edits'
|
|
? 'Accepted w/ minor edits'
|
|
: 'Accepted as is';
|
|
} else {
|
|
acceptanceNote.textContent = '';
|
|
}
|
|
}
|
|
};
|
|
|
|
const saveRow = (row, options = {}) => {
|
|
const form = row.querySelector('form');
|
|
if (!form) {
|
|
return;
|
|
}
|
|
const statusSelect = form.querySelector('select[name="review_status"]');
|
|
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);
|
|
}
|
|
if (statusLabel) {
|
|
statusLabel.textContent = 'Saving...';
|
|
}
|
|
fetch(form.action, {
|
|
method: 'POST',
|
|
body: data,
|
|
credentials: 'same-origin',
|
|
headers: {
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
'X-CSRF-TOKEN': csrfToken,
|
|
},
|
|
})
|
|
.then((resp) => {
|
|
if (!resp.ok) {
|
|
throw new Error('Save failed');
|
|
}
|
|
syncCsrfToken(form);
|
|
updateStatusUI(row);
|
|
if (statusLabel) {
|
|
statusLabel.textContent = 'Saved';
|
|
}
|
|
setTimeout(() => {
|
|
if (statusLabel && statusLabel.textContent === 'Saved') {
|
|
statusLabel.textContent = '';
|
|
}
|
|
}, 1500);
|
|
})
|
|
.catch(() => {
|
|
if (statusLabel) {
|
|
statusLabel.textContent = 'Error saving';
|
|
}
|
|
});
|
|
};
|
|
|
|
const debounce = (fn, wait) => {
|
|
let timer;
|
|
return (...args) => {
|
|
clearTimeout(timer);
|
|
timer = setTimeout(() => fn(...args), wait);
|
|
};
|
|
};
|
|
|
|
const debouncedSave = debounce(saveRow, 500);
|
|
|
|
document.querySelectorAll('.exam-drafts-table tbody tr').forEach(updateRow);
|
|
|
|
document.addEventListener('change', (event) => {
|
|
const target = event.target;
|
|
if (!(target instanceof HTMLElement)) {
|
|
return;
|
|
}
|
|
const row = target.closest('tr');
|
|
if (!row) {
|
|
return;
|
|
}
|
|
if (target instanceof HTMLSelectElement && target.name === 'review_status') {
|
|
updateRow(row);
|
|
debouncedSave(row);
|
|
return;
|
|
}
|
|
if (target instanceof HTMLInputElement && target.name === 'acceptance_type') {
|
|
debouncedSave(row);
|
|
}
|
|
});
|
|
|
|
document.addEventListener('input', (event) => {
|
|
const target = event.target;
|
|
if (!(target instanceof HTMLTextAreaElement) || target.name !== 'reviewer_comment') {
|
|
return;
|
|
}
|
|
const row = target.closest('tr');
|
|
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() ?>
|
|
|
|
<?= $this->section('styles') ?>
|
|
<style>
|
|
.exam-drafts-table th,
|
|
.exam-drafts-table td {
|
|
white-space: normal;
|
|
word-break: break-word;
|
|
}
|
|
.exam-drafts-table th {
|
|
min-width: 0;
|
|
white-space: nowrap;
|
|
}
|
|
.exam-drafts-table td {
|
|
max-width: none;
|
|
}
|
|
.exam-drafts-table {
|
|
table-layout: auto;
|
|
width: max-content;
|
|
}
|
|
.exam-drafts-table thead th {
|
|
background-color: #f8f9fa;
|
|
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
|
|
padding-bottom: 0.75rem;
|
|
}
|
|
.exam-drafts-table tbody tr {
|
|
background-color: #fff;
|
|
}
|
|
</style>
|
|
<?= $this->endSection() ?>
|