Files
2026-05-16 13:44:12 -04:00

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() ?>