fix issues on examdraft view and event print page
This commit is contained in:
@@ -102,11 +102,9 @@ class ExamDraftController extends BaseController
|
||||
session()->set('class_section_id', $selectedClass);
|
||||
}
|
||||
|
||||
$allDrafts = $this->examDraftModel
|
||||
->select($this->draftSelectColumns())
|
||||
->select('cs.class_section_name')
|
||||
->join('classSection cs', 'cs.class_section_id = exam_drafts.class_section_id', 'left')
|
||||
->where($this->authorIdColumn, $teacherId)
|
||||
$classSectionIds = array_map(static fn($a) => (int) $a['class_section_id'], $assignments);
|
||||
|
||||
$allDrafts = $this->visibleTeacherDraftsQuery($teacherId, $classSectionIds)
|
||||
->orderBy('created_at', 'DESC')
|
||||
->findAll();
|
||||
|
||||
@@ -117,10 +115,10 @@ class ExamDraftController extends BaseController
|
||||
$row['final_pdf_file'] = $pdf;
|
||||
}
|
||||
}
|
||||
$row = $this->attachTeacherDraftContext($row, $teacherId);
|
||||
}
|
||||
unset($row);
|
||||
|
||||
$classSectionIds = array_map(static fn($a) => (int) $a['class_section_id'], $assignments);
|
||||
$legacyExams = [];
|
||||
// Guard against missing schema column in older databases
|
||||
if ($this->hasIsLegacyColumn) {
|
||||
@@ -129,8 +127,9 @@ class ExamDraftController extends BaseController
|
||||
|
||||
$legacyQuery = $this->examDraftModel
|
||||
->select($this->draftSelectColumns())
|
||||
->select('cs.class_section_name')
|
||||
->select('cs.class_section_name, u.firstname AS teacher_first, u.lastname AS teacher_last')
|
||||
->join('classSection cs', 'cs.class_section_id = exam_drafts.class_section_id', 'left')
|
||||
->join('users u', 'u.id = exam_drafts.' . $this->authorIdColumn, 'left')
|
||||
->where('exam_drafts.is_legacy', 1)
|
||||
->where('exam_drafts.status', 'legacy')
|
||||
->where('exam_drafts.final_file IS NOT NULL', null, false);
|
||||
@@ -143,6 +142,11 @@ class ExamDraftController extends BaseController
|
||||
->orderBy('cs.class_section_name', 'ASC')
|
||||
->orderBy('exam_drafts.created_at', 'DESC')
|
||||
->findAll();
|
||||
|
||||
foreach ($legacyExams as &$row) {
|
||||
$row = $this->attachTeacherDraftContext($row, $teacherId);
|
||||
}
|
||||
unset($row);
|
||||
} else {
|
||||
// Legacy column absent, show all drafts and skip legacy tab query
|
||||
$drafts = $allDrafts;
|
||||
@@ -768,10 +772,11 @@ class ExamDraftController extends BaseController
|
||||
return $this->response->setStatusCode(401);
|
||||
}
|
||||
|
||||
$drafts = $this->examDraftModel
|
||||
->select($this->draftSelectColumns())
|
||||
->select('id, status, acceptance_type, updated_at, reviewed_at')
|
||||
->where($this->authorIdColumn, $teacherId)
|
||||
$assignments = $this->teacherClassModel->getClassAssignmentsByUserId($teacherId, $this->schoolYear);
|
||||
$classSectionIds = array_map(static fn($a) => (int) $a['class_section_id'], $assignments);
|
||||
|
||||
$drafts = $this->visibleTeacherDraftsQuery($teacherId, $classSectionIds)
|
||||
->select('exam_drafts.id, exam_drafts.status, exam_drafts.acceptance_type, exam_drafts.updated_at, exam_drafts.reviewed_at')
|
||||
->orderBy('updated_at', 'DESC')
|
||||
->findAll();
|
||||
|
||||
@@ -817,6 +822,58 @@ class ExamDraftController extends BaseController
|
||||
return $validIds[0] ?? 0;
|
||||
}
|
||||
|
||||
private function visibleTeacherDraftsQuery(int $teacherId, array $classSectionIds)
|
||||
{
|
||||
$query = $this->examDraftModel
|
||||
->select($this->draftSelectColumns())
|
||||
->select('cs.class_section_name, u.firstname AS teacher_first, u.lastname AS teacher_last')
|
||||
->join('classSection cs', 'cs.class_section_id = exam_drafts.class_section_id', 'left')
|
||||
->join('users u', 'u.id = exam_drafts.' . $this->authorIdColumn, 'left');
|
||||
|
||||
if (empty($classSectionIds)) {
|
||||
return $query->where('exam_drafts.' . $this->authorIdColumn, $teacherId);
|
||||
}
|
||||
|
||||
$query->groupStart()
|
||||
->where('exam_drafts.' . $this->authorIdColumn, $teacherId)
|
||||
->orGroupStart()
|
||||
->whereIn('exam_drafts.class_section_id', $classSectionIds)
|
||||
->where('exam_drafts.' . $this->authorIdColumn . ' !=', $teacherId)
|
||||
->where('exam_drafts.status !=', 'draft');
|
||||
|
||||
if ($this->schoolYear !== '') {
|
||||
$query->where('exam_drafts.school_year', $this->schoolYear);
|
||||
}
|
||||
if ($this->semester !== '') {
|
||||
$query->where('exam_drafts.semester', $this->semester);
|
||||
}
|
||||
|
||||
$query->groupEnd()
|
||||
->groupEnd();
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
private function attachTeacherDraftContext(array $row, int $viewerId): array
|
||||
{
|
||||
$isOwn = $this->draftTeacherId($row) === $viewerId;
|
||||
$row['is_own_submission'] = $isOwn;
|
||||
$row['teacher_display_name'] = $isOwn ? 'You' : $this->draftTeacherName($row);
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
private function draftTeacherName(array $draft): string
|
||||
{
|
||||
$name = trim(((string) ($draft['teacher_first'] ?? '')) . ' ' . ((string) ($draft['teacher_last'] ?? '')));
|
||||
if ($name !== '') {
|
||||
return $name;
|
||||
}
|
||||
|
||||
$teacherId = $this->draftTeacherId($draft);
|
||||
return $teacherId > 0 ? 'Teacher #' . $teacherId : 'Teacher';
|
||||
}
|
||||
|
||||
private function statusBadgeMap(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -224,6 +224,10 @@ class FilesController extends Controller
|
||||
throw PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
|
||||
if (!$this->canAccessExamDraftFile($name, $subdir)) {
|
||||
return $this->response->setStatusCode(403, 'You are not allowed to access this file.');
|
||||
}
|
||||
|
||||
$mime = 'application/octet-stream';
|
||||
if (function_exists('finfo_open')) {
|
||||
$fi = finfo_open(FILEINFO_MIME_TYPE);
|
||||
@@ -317,4 +321,53 @@ class FilesController extends Controller
|
||||
}
|
||||
return 'teacher_file';
|
||||
}
|
||||
|
||||
private function canAccessExamDraftFile(string $filename, string $subdir): bool
|
||||
{
|
||||
$userId = (int) (session()->get('user_id') ?? 0);
|
||||
if ($userId <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$role = strtolower((string) (session()->get('role') ?? ''));
|
||||
if ($role === 'admin') {
|
||||
return true;
|
||||
}
|
||||
|
||||
$db = Database::connect();
|
||||
$fileColumn = $subdir === 'finals' ? 'final_file' : $this->resolveExamDraftFileColumn($db);
|
||||
|
||||
$draft = $db->table('exam_drafts ed')
|
||||
->select('ed.class_section_id, ed.school_year, ed.teacher_id, ed.author_id')
|
||||
->where('ed.' . $fileColumn, $filename)
|
||||
->limit(1)
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
if (empty($draft)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$authorId = (int) ($draft['teacher_id'] ?? $draft['author_id'] ?? 0);
|
||||
if ($authorId > 0 && $authorId === $userId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$classSectionId = (int) ($draft['class_section_id'] ?? 0);
|
||||
if ($classSectionId <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$assignmentQuery = $db->table('teacher_class')
|
||||
->select('id')
|
||||
->where('teacher_id', $userId)
|
||||
->where('class_section_id', $classSectionId);
|
||||
|
||||
$schoolYear = trim((string) ($draft['school_year'] ?? ''));
|
||||
if ($schoolYear !== '') {
|
||||
$assignmentQuery->where('school_year', $schoolYear);
|
||||
}
|
||||
|
||||
return $assignmentQuery->limit(1)->countAllResults() > 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,54 @@
|
||||
|
||||
<div class="container mt-5">
|
||||
<h3>Event Charges</h3>
|
||||
<style>
|
||||
.event-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
.event-card-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.sortable-header {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
.sortable-header:hover,
|
||||
.sortable-header:focus {
|
||||
color: var(--bs-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
.sortable-header::after {
|
||||
content: '↕';
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.45;
|
||||
}
|
||||
.sortable-header.sorted-asc::after {
|
||||
content: '↑';
|
||||
opacity: 1;
|
||||
}
|
||||
.sortable-header.sorted-desc::after {
|
||||
content: '↓';
|
||||
opacity: 1;
|
||||
}
|
||||
@media print {
|
||||
.no-print {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<?php if (session()->getFlashdata('success')): ?>
|
||||
<div class="alert alert-success"><?= session()->getFlashdata('success') ?></div>
|
||||
@@ -128,9 +176,11 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<?php if (!in_array(strtolower((string)(session()->get('role') ?? '')), ['parent', 'teacher', 'teacher_assistant'], true)): ?>
|
||||
<button type="button" class="btn btn-outline-primary" data-bs-toggle="modal" data-bs-target="#externalParticipantModal">
|
||||
Add non-school participant
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<div class="form-text">After adding participants, click Submit to save them to the database.</div>
|
||||
<div id="externalParticipantsHidden"></div>
|
||||
</div>
|
||||
@@ -160,12 +210,24 @@
|
||||
<?php if (empty($grouped)): ?>
|
||||
<div class="alert alert-info">No charges found.</div>
|
||||
<?php else: ?>
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-3 no-print">
|
||||
<p class="text-muted mb-0">Click a column header to sort the participants list.</p>
|
||||
<button type="button" class="btn btn-outline-secondary" id="printAllEventCharges">
|
||||
Print All Participant Lists
|
||||
</button>
|
||||
</div>
|
||||
<div id="eventChargeCards">
|
||||
<?php foreach ($grouped as $eventId => $data): ?>
|
||||
<?php $eventLabel = $data['label']; ?>
|
||||
<?php $rows = $data['rows']; ?>
|
||||
<div class="card mb-4 event-card" data-event-id="<?= esc($eventId) ?>">
|
||||
<div class="card-header bg-light fw-semibold">
|
||||
<?= esc($eventLabel) ?>
|
||||
<div class="card-header bg-light fw-semibold event-card-header">
|
||||
<span><?= esc($eventLabel) ?></span>
|
||||
<div class="event-card-header-actions no-print">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm print-event-card" data-event-id="<?= esc($eventId) ?>">
|
||||
Print This List
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<?php
|
||||
@@ -173,19 +235,19 @@
|
||||
$totalCharged = 0.0;
|
||||
?>
|
||||
<div class="table-responsive">
|
||||
<table id="eventTable_<?= esc($eventId) ?>" class="table table-bordered table-striped mb-0 no-mgmt-sticky">
|
||||
<table id="eventTable_<?= esc($eventId) ?>" class="table table-bordered table-striped mb-0 no-mgmt-sticky event-charges-table">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Parent Name</th>
|
||||
<th>Student Name</th>
|
||||
<th>External Info</th>
|
||||
<th>Class Section</th>
|
||||
<th>Charged Amount</th>
|
||||
<th>Created</th>
|
||||
<th>Waiver</th>
|
||||
<th>Fees Paid</th>
|
||||
<th>Payment</th>
|
||||
<th><button type="button" class="sortable-header" data-sort-index="0" data-sort-type="number">ID</button></th>
|
||||
<th><button type="button" class="sortable-header" data-sort-index="1" data-sort-type="text">Parent Name</button></th>
|
||||
<th><button type="button" class="sortable-header" data-sort-index="2" data-sort-type="text">Student Name</button></th>
|
||||
<th><button type="button" class="sortable-header" data-sort-index="3" data-sort-type="text">External Info</button></th>
|
||||
<th><button type="button" class="sortable-header" data-sort-index="4" data-sort-type="text">Class Section</button></th>
|
||||
<th><button type="button" class="sortable-header" data-sort-index="5" data-sort-type="number">Charged Amount</button></th>
|
||||
<th><button type="button" class="sortable-header" data-sort-index="6" data-sort-type="date">Created</button></th>
|
||||
<th><button type="button" class="sortable-header" data-sort-index="7" data-sort-type="text">Waiver</button></th>
|
||||
<th><button type="button" class="sortable-header" data-sort-index="8" data-sort-type="text">Fees Paid</button></th>
|
||||
<th><button type="button" class="sortable-header" data-sort-index="9" data-sort-type="text">Payment</button></th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -212,7 +274,7 @@
|
||||
. '#eventTable_' . (int) ($charge['event_id'] ?? 0);
|
||||
?>
|
||||
<tr>
|
||||
<td><?= esc($charge['id']) ?></td>
|
||||
<td data-sort-value="<?= esc((string) ($charge['id'] ?? '')) ?>"><?= esc($charge['id']) ?></td>
|
||||
<?php
|
||||
$studentName = $charge['student_firstname']
|
||||
? trim($charge['student_firstname'] . ' ' . $charge['student_lastname'])
|
||||
@@ -231,25 +293,25 @@
|
||||
$standardParentName = trim($charge['parent_firstname'] . ' ' . $charge['parent_lastname']);
|
||||
$parentColumnName = $externalParentLabel ?: ($standardParentName ?: '—');
|
||||
?>
|
||||
<td><?= esc($parentColumnName) ?></td>
|
||||
<td>
|
||||
<td data-sort-value="<?= esc(strtolower($parentColumnName)) ?>"><?= esc($parentColumnName) ?></td>
|
||||
<td data-sort-value="<?= esc(strtolower($displayName)) ?>">
|
||||
<?= esc($displayName) ?>
|
||||
<?php if ($externalNote): ?>
|
||||
<small class="text-muted d-block"><?= esc($externalNote) ?></small>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<td data-sort-value="<?= esc(strtolower($externalParentPhone ?: '')) ?>">
|
||||
<?php if ($externalParentPhone): ?>
|
||||
<div class="text-muted small"><?= esc($externalParentPhone) ?></div>
|
||||
<?php else: ?>
|
||||
<span class="text-muted small">—</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<td data-sort-value="<?= esc(strtolower((string) ($classSectionNames[$charge['class_section_id'] ?? ''] ?? ''))) ?>">
|
||||
<?= esc($classSectionNames[$charge['class_section_id'] ?? ''] ?? '—') ?>
|
||||
</td>
|
||||
<td>$<?= esc(number_format($charge['event_amount'] ?? 0, 2)) ?></td>
|
||||
<td><?= esc(!empty($charge['created_at']) ? local_datetime($charge['created_at'], 'm-d-Y H:i') : '') ?></td>
|
||||
<td data-sort-value="<?= esc(number_format((float) ($charge['event_amount'] ?? 0), 2, '.', '')) ?>">$<?= esc(number_format($charge['event_amount'] ?? 0, 2)) ?></td>
|
||||
<td data-sort-value="<?= esc((string) ($charge['created_at'] ?? '')) ?>"><?= esc(!empty($charge['created_at']) ? local_datetime($charge['created_at'], 'm-d-Y H:i') : '') ?></td>
|
||||
<?php
|
||||
$feeAmount = (float) ($charge['event_amount'] ?? 0);
|
||||
$hasBalanceRecord = array_key_exists((int)$charge['parent_id'], $parentBalances);
|
||||
@@ -257,7 +319,7 @@
|
||||
$waiverSigned = !empty($charge['waiver_signed']);
|
||||
$feeIsPaid = $isParticipating && ($isEventPaid || ($hasBalanceRecord && $parentBalance <= 0));
|
||||
?>
|
||||
<td class="text-center">
|
||||
<td class="text-center" data-sort-value="<?= $waiverSigned ? 'signed' : 'unsigned' ?>">
|
||||
<form method="post" action="<?= site_url('administrator/event-charges/waiver/' . esc($charge['id'])) ?>" class="d-inline-flex align-items-center">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="return_to" value="<?= esc($returnTo) ?>">
|
||||
@@ -270,14 +332,14 @@
|
||||
</label>
|
||||
</form>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<td class="text-center" data-sort-value="<?= $feeIsPaid ? 'paid' : 'unpaid' ?>">
|
||||
<?php if ($feeIsPaid): ?>
|
||||
<span class="badge bg-success">Paid</span>
|
||||
<?php else: ?>
|
||||
<span class="badge bg-danger">Unpaid</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<td class="text-center" data-sort-value="<?= $isEventPaid ? 'paid' : 'unpaid' ?>">
|
||||
<form method="post" action="<?= site_url('administrator/event-charges/payment/' . esc($charge['id'])) ?>" class="d-inline-flex align-items-center">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="return_to" value="<?= esc($returnTo) ?>">
|
||||
@@ -325,9 +387,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if (!in_array(strtolower((string)(session()->get('role') ?? '')), ['parent', 'teacher', 'teacher_assistant'], true)): ?>
|
||||
<div class="modal fade" id="externalParticipantModal" tabindex="-1" aria-labelledby="externalParticipantModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
@@ -382,6 +446,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
@@ -519,29 +584,29 @@ function loadStudentsWithCharges() {
|
||||
const createdDisplay = new Date().toLocaleString();
|
||||
const $previewRow = $(`
|
||||
<tr class="external-preview table-warning" data-key="${key}" data-event-id="${entry.eventId}">
|
||||
<td>—</td>
|
||||
<td>${escapeHtml(parentNameForRow)}</td>
|
||||
<td>
|
||||
<td data-sort-value="">—</td>
|
||||
<td data-sort-value="${escapeHtml(String(parentNameForRow).toLowerCase())}">${escapeHtml(parentNameForRow)}</td>
|
||||
<td data-sort-value="${escapeHtml(String(`${entry.firstname || ''} ${entry.lastname || ''}`.trim()).toLowerCase())}">
|
||||
<strong>${label}</strong>
|
||||
${noteEsc ? `<div class="text-muted small">${noteEsc}</div>` : ''}
|
||||
</td>
|
||||
<td>
|
||||
<td data-sort-value="${escapeHtml(String(entry.parentPhone || '').toLowerCase())}">
|
||||
${phoneLabel}
|
||||
${phoneLabel ? '' : '<span class="text-muted small">—</span>'}
|
||||
</td>
|
||||
<td>External</td>
|
||||
<td>$${amountDisplay}</td>
|
||||
<td>${createdDisplay}</td>
|
||||
<td class="text-center">
|
||||
<td data-sort-value="external">External</td>
|
||||
<td data-sort-value="${amountDisplay}">$${amountDisplay}</td>
|
||||
<td data-sort-value="${new Date().toISOString()}">${createdDisplay}</td>
|
||||
<td class="text-center" data-sort-value="${waiverSigned ? 'signed' : 'unsigned'}">
|
||||
<div class="form-check d-inline-flex align-items-center gap-2 justify-content-center m-0">
|
||||
<input class="form-check-input external-waiver-toggle" type="checkbox" data-key="${key}" ${waiverSigned ? 'checked' : ''}>
|
||||
<span class="small text-muted external-waiver-label">${waiverSigned ? 'Signed' : 'Unsigned'}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<td class="text-center" data-sort-value="${isPaid ? 'paid' : 'unpaid'}">
|
||||
<span class="badge ${isPaid ? 'bg-success' : 'bg-danger'} external-paid-badge">${isPaid ? 'Paid' : 'Unpaid'}</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<td class="text-center" data-sort-value="${isPaid ? 'paid' : 'unpaid'}">
|
||||
<div class="form-check d-inline-flex align-items-center gap-2 justify-content-center m-0">
|
||||
<input class="form-check-input external-paid-toggle" type="checkbox" data-key="${key}" ${isPaid ? 'checked' : ''}>
|
||||
<span class="small text-muted">Paid</span>
|
||||
@@ -558,6 +623,7 @@ function loadStudentsWithCharges() {
|
||||
</tr>
|
||||
`);
|
||||
$tableBody.append($previewRow);
|
||||
applyActiveSort($tableBody.closest('table').get(0));
|
||||
}
|
||||
|
||||
function persistDrafts() {
|
||||
@@ -648,6 +714,185 @@ function loadStudentsWithCharges() {
|
||||
$('#modalParentEmail').val('');
|
||||
}
|
||||
|
||||
function getSortValue(row, columnIndex, sortType) {
|
||||
const cell = row.children[columnIndex];
|
||||
if (!cell) {
|
||||
return sortType === 'number' ? 0 : '';
|
||||
}
|
||||
const rawValue = cell.getAttribute('data-sort-value');
|
||||
const fallbackValue = (cell.textContent || '').trim();
|
||||
const value = rawValue !== null ? rawValue : fallbackValue;
|
||||
if (sortType === 'number') {
|
||||
const parsed = parseFloat(String(value).replace(/[^0-9.-]/g, ''));
|
||||
return Number.isNaN(parsed) ? 0 : parsed;
|
||||
}
|
||||
if (sortType === 'date') {
|
||||
const time = Date.parse(value);
|
||||
return Number.isNaN(time) ? 0 : time;
|
||||
}
|
||||
return String(value).toLowerCase();
|
||||
}
|
||||
|
||||
function updateSortIndicators(table, columnIndex, direction) {
|
||||
$(table).find('.sortable-header').each(function() {
|
||||
const isCurrent = Number($(this).data('sortIndex')) === Number(columnIndex);
|
||||
$(this)
|
||||
.toggleClass('sorted-asc', isCurrent && direction === 'asc')
|
||||
.toggleClass('sorted-desc', isCurrent && direction === 'desc')
|
||||
.attr('aria-sort', isCurrent ? direction : 'none');
|
||||
});
|
||||
}
|
||||
|
||||
function sortEventTable(table, columnIndex, sortType, direction) {
|
||||
if (!table || !table.tBodies.length) {
|
||||
return;
|
||||
}
|
||||
const tbody = table.tBodies[0];
|
||||
const rows = Array.from(tbody.rows);
|
||||
rows.sort((a, b) => {
|
||||
const first = getSortValue(a, columnIndex, sortType);
|
||||
const second = getSortValue(b, columnIndex, sortType);
|
||||
if (first < second) {
|
||||
return direction === 'asc' ? -1 : 1;
|
||||
}
|
||||
if (first > second) {
|
||||
return direction === 'asc' ? 1 : -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
rows.forEach((row) => tbody.appendChild(row));
|
||||
table.dataset.sortColumn = String(columnIndex);
|
||||
table.dataset.sortType = sortType;
|
||||
table.dataset.sortDirection = direction;
|
||||
updateSortIndicators(table, columnIndex, direction);
|
||||
}
|
||||
|
||||
function applyActiveSort(table) {
|
||||
if (!table || !table.dataset || !table.dataset.sortColumn) {
|
||||
return;
|
||||
}
|
||||
sortEventTable(
|
||||
table,
|
||||
Number(table.dataset.sortColumn),
|
||||
table.dataset.sortType || 'text',
|
||||
table.dataset.sortDirection || 'asc'
|
||||
);
|
||||
}
|
||||
|
||||
function buildPrintableCard(card) {
|
||||
const clone = card.cloneNode(true);
|
||||
clone.querySelectorAll('.no-print').forEach((node) => node.remove());
|
||||
clone.querySelectorAll('.sortable-header').forEach((button) => {
|
||||
const headerText = button.textContent || '';
|
||||
const textNode = document.createTextNode(headerText);
|
||||
button.parentNode.replaceChild(textNode, button);
|
||||
});
|
||||
clone.querySelectorAll('form').forEach((form) => {
|
||||
const cell = form.closest('td');
|
||||
if (!cell) {
|
||||
form.remove();
|
||||
return;
|
||||
}
|
||||
const label = form.querySelector('.form-check-label');
|
||||
const checkbox = form.querySelector('input[type="checkbox"]');
|
||||
const text = label
|
||||
? label.textContent.trim()
|
||||
: (checkbox && checkbox.checked ? 'Paid' : 'Unpaid');
|
||||
cell.textContent = text || '—';
|
||||
});
|
||||
clone.querySelectorAll('tr').forEach((row) => {
|
||||
if (row.children.length > 10) {
|
||||
row.removeChild(row.lastElementChild);
|
||||
}
|
||||
});
|
||||
return clone.outerHTML;
|
||||
}
|
||||
|
||||
function openPrintWindow(title, contentHtml) {
|
||||
const printWindow = window.open('', '_blank', 'noopener,noreferrer,width=1200,height=900');
|
||||
if (!printWindow) {
|
||||
alert('Unable to open the print preview window. Please allow pop-ups for this site.');
|
||||
return;
|
||||
}
|
||||
const style = `
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 24px; color: #212529; }
|
||||
h1 { font-size: 1.5rem; margin-bottom: 0.25rem; }
|
||||
p { margin-top: 0; color: #6c757d; }
|
||||
.card { border: 1px solid #dee2e6; border-radius: 0.5rem; margin-bottom: 1.5rem; overflow: hidden; }
|
||||
.card-header { background: #f8f9fa; padding: 0.9rem 1rem; font-weight: 700; }
|
||||
.table { width: 100%; border-collapse: collapse; }
|
||||
.table th, .table td { border: 1px solid #dee2e6; padding: 0.55rem 0.7rem; text-align: left; vertical-align: top; }
|
||||
.table thead th, .table tfoot th { background: #f8f9fa; }
|
||||
.badge { display: inline-block; padding: 0.2rem 0.45rem; border-radius: 999px; font-size: 0.8rem; font-weight: 700; }
|
||||
.bg-success { background: #198754; color: #fff; }
|
||||
.bg-danger { background: #dc3545; color: #fff; }
|
||||
.bg-secondary { background: #6c757d; color: #fff; }
|
||||
.text-muted { color: #6c757d; }
|
||||
.small { font-size: 0.875rem; }
|
||||
@media print {
|
||||
body { margin: 0; }
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
printWindow.document.open();
|
||||
printWindow.document.write(`
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>${escapeHtml(title)}</title>
|
||||
${style}
|
||||
</head>
|
||||
<body>
|
||||
<h1>${escapeHtml(title)}</h1>
|
||||
<p>Generated on ${escapeHtml(new Date().toLocaleString())}</p>
|
||||
${contentHtml}
|
||||
<script>
|
||||
window.onload = function() {
|
||||
window.print();
|
||||
};
|
||||
<\/script>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
printWindow.document.close();
|
||||
}
|
||||
|
||||
function printEventCards(cards, title) {
|
||||
const printable = cards.map((card) => buildPrintableCard(card)).join('');
|
||||
openPrintWindow(title, printable);
|
||||
}
|
||||
|
||||
$('.sortable-header').on('click', function() {
|
||||
const $button = $(this);
|
||||
const table = $button.closest('table').get(0);
|
||||
const columnIndex = Number($button.data('sortIndex'));
|
||||
const sortType = String($button.data('sortType') || 'text');
|
||||
const currentColumn = Number(table.dataset.sortColumn);
|
||||
const currentDirection = table.dataset.sortDirection || 'asc';
|
||||
const direction = currentColumn === columnIndex && currentDirection === 'asc' ? 'desc' : 'asc';
|
||||
sortEventTable(table, columnIndex, sortType, direction);
|
||||
});
|
||||
|
||||
$('.print-event-card').on('click', function() {
|
||||
const eventId = String($(this).data('eventId'));
|
||||
const card = document.querySelector(`.event-card[data-event-id="${eventId}"]`);
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
const title = $(card).find('.card-header span').first().text().trim() || 'Event Participants';
|
||||
printEventCards([card], `${title} Participant List`);
|
||||
});
|
||||
|
||||
$('#printAllEventCharges').on('click', function() {
|
||||
const cards = Array.from(document.querySelectorAll('.event-card'));
|
||||
if (!cards.length) {
|
||||
return;
|
||||
}
|
||||
printEventCards(cards, 'All Event Participant Lists');
|
||||
});
|
||||
|
||||
$('#externalParticipantSave').on('click', function() {
|
||||
const firstName = capitalizeName($('#modalExternalFirstName').val().trim());
|
||||
const lastName = capitalizeName($('#modalExternalLastName').val().trim());
|
||||
@@ -737,6 +982,10 @@ function loadStudentsWithCharges() {
|
||||
} else {
|
||||
$badge.removeClass('bg-success').addClass('bg-danger').text('Unpaid');
|
||||
}
|
||||
const $row = $(`tr.external-preview[data-key="${key}"]`);
|
||||
$row.children().eq(8).attr('data-sort-value', paid ? 'paid' : 'unpaid');
|
||||
$row.children().eq(9).attr('data-sort-value', paid ? 'paid' : 'unpaid');
|
||||
applyActiveSort($row.closest('table').get(0));
|
||||
});
|
||||
$(document).on('change', '.external-waiver-toggle', function() {
|
||||
const key = $(this).data('key');
|
||||
@@ -758,6 +1007,9 @@ function loadStudentsWithCharges() {
|
||||
} else {
|
||||
$label.text('Unsigned');
|
||||
}
|
||||
const $row = $(`tr.external-preview[data-key="${key}"]`);
|
||||
$row.children().eq(7).attr('data-sort-value', signed ? 'signed' : 'unsigned');
|
||||
applyActiveSort($row.closest('table').get(0));
|
||||
});
|
||||
$('#event-participant-form').on('submit', function() {
|
||||
localStorage.removeItem(storageKey);
|
||||
|
||||
@@ -89,12 +89,14 @@ $maxUploadBytes = $maxUploadBytes ?? (12 * 1024 * 1024);
|
||||
};
|
||||
?>
|
||||
|
||||
<h2 class="h5 mb-3">Your submissions</h2>
|
||||
<h2 class="h5 mb-1">Visible submissions</h2>
|
||||
<p class="text-muted small">Includes your uploads and submitted exam drafts from teachers assigned to the same class-section.</p>
|
||||
<?php if (!empty($drafts)): ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped align-middle teacher-drafts-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Teacher</th>
|
||||
<th>Class</th>
|
||||
<th>Title / type</th>
|
||||
<th>Ver.</th>
|
||||
@@ -111,6 +113,7 @@ $maxUploadBytes = $maxUploadBytes ?? (12 * 1024 * 1024);
|
||||
$badgeHtml = $renderBadge($st, $statusBadges);
|
||||
?>
|
||||
<tr data-draft-id="<?= (int) ($d['id'] ?? 0) ?>">
|
||||
<td><?= esc($d['teacher_display_name'] ?? 'Teacher') ?></td>
|
||||
<td><?= esc($d['class_section_name'] ?? '') ?></td>
|
||||
<td class="title-cell"><?= esc($d['draft_title'] ?? $d['exam_type'] ?? '') ?></td>
|
||||
<td><?= esc((string) ($d['version'] ?? '')) ?></td>
|
||||
@@ -168,6 +171,7 @@ $maxUploadBytes = $maxUploadBytes ?? (12 * 1024 * 1024);
|
||||
<table class="table table-sm table-striped align-middle teacher-drafts-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Teacher</th>
|
||||
<th>Class</th>
|
||||
<th>Title</th>
|
||||
<th>Ver.</th>
|
||||
@@ -183,6 +187,7 @@ $maxUploadBytes = $maxUploadBytes ?? (12 * 1024 * 1024);
|
||||
<?php foreach ($legacyExams as $d): ?>
|
||||
<?php $st = strtolower((string) ($d['status'] ?? '')); ?>
|
||||
<tr>
|
||||
<td><?= esc($d['teacher_display_name'] ?? 'Teacher') ?></td>
|
||||
<td><?= esc($d['class_section_name'] ?? '') ?></td>
|
||||
<td class="title-cell"><?= esc($d['draft_title'] ?? $d['exam_type'] ?? '') ?></td>
|
||||
<td><?= esc((string) ($d['version'] ?? '')) ?></td>
|
||||
|
||||
Reference in New Issue
Block a user