From 92016f90d0e0b58d704c1f1ed341235f8a3e72fe Mon Sep 17 00:00:00 2001 From: root Date: Tue, 5 May 2026 01:38:20 -0400 Subject: [PATCH] fix issues on examdraft view and event print page --- app/Controllers/View/ExamDraftController.php | 79 ++++- app/Controllers/View/FilesController.php | 53 +++ .../administrator/events/event_charges.php | 318 ++++++++++++++++-- app/Views/teacher/drafts.php | 7 +- 4 files changed, 412 insertions(+), 45 deletions(-) diff --git a/app/Controllers/View/ExamDraftController.php b/app/Controllers/View/ExamDraftController.php index 7575778..4d7ef66 100644 --- a/app/Controllers/View/ExamDraftController.php +++ b/app/Controllers/View/ExamDraftController.php @@ -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 [ diff --git a/app/Controllers/View/FilesController.php b/app/Controllers/View/FilesController.php index ea1462c..17b2b14 100644 --- a/app/Controllers/View/FilesController.php +++ b/app/Controllers/View/FilesController.php @@ -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; + } } diff --git a/app/Views/administrator/events/event_charges.php b/app/Views/administrator/events/event_charges.php index e1a762b..8b659b9 100644 --- a/app/Views/administrator/events/event_charges.php +++ b/app/Views/administrator/events/event_charges.php @@ -3,6 +3,54 @@

Event Charges

+ getFlashdata('success')): ?>
getFlashdata('success') ?>
@@ -128,9 +176,11 @@
+ get('role') ?? '')), ['parent', 'teacher', 'teacher_assistant'], true)): ?> +
After adding participants, click Submit to save them to the database.
@@ -160,12 +210,24 @@
No charges found.
+
+

Click a column header to sort the participants list.

+ +
+
$data): ?>
-
- +
+ +
+ +
- +
- - - - - - - - - - + + + + + + + + + + @@ -212,7 +274,7 @@ . '#eventTable_' . (int) ($charge['event_id'] ?? 0); ?> - + - - + - - - - + + - - - - - - + + - - - - - + + + - - `); $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 = ` + + `; + printWindow.document.open(); + printWindow.document.write(` + + + + + ${escapeHtml(title)} + ${style} + + +

${escapeHtml(title)}

+

Generated on ${escapeHtml(new Date().toLocaleString())}

+ ${contentHtml} +
IDParent NameStudent NameExternal InfoClass SectionCharged AmountCreatedWaiverFees PaidPayment Actions
+ +
+ $$ +
@@ -270,14 +332,14 @@
+ Paid Unpaid +
@@ -325,9 +387,11 @@ + +get('role') ?? '')), ['parent', 'teacher', 'teacher_assistant'], true)): ?> + endSection() ?> @@ -519,29 +584,29 @@ function loadStudentsWithCharges() { const createdDisplay = new Date().toLocaleString(); const $previewRow = $(`
${escapeHtml(parentNameForRow)} + ${escapeHtml(parentNameForRow)} ${label} ${noteEsc ? `
${noteEsc}
` : ''}
+ ${phoneLabel} ${phoneLabel ? '' : ''} External$${amountDisplay}${createdDisplay} + External$${amountDisplay}${createdDisplay}
${waiverSigned ? 'Signed' : 'Unsigned'}
+ ${isPaid ? 'Paid' : 'Unpaid'} +
Paid @@ -558,6 +623,7 @@ function loadStudentsWithCharges() {