From 239d35f8ff7716cfddc4bebdd8685870960b6a2f Mon Sep 17 00:00:00 2001 From: root Date: Thu, 21 May 2026 17:00:38 -0400 Subject: [PATCH] fix the event print and certificate qr --- app/Config/Routes.php | 1 + .../View/CertificateController.php | 12 +- app/Controllers/View/EventController.php | 312 +++++++++++++----- .../administrator/events/event_charges.php | 177 ++++------ .../events/event_charges_pdf.php | 207 ++++++++++++ sql_query/IpAttempt.sql | 10 - sql_query/users.sql | 59 ---- 7 files changed, 502 insertions(+), 276 deletions(-) create mode 100644 app/Views/administrator/events/event_charges_pdf.php delete mode 100644 sql_query/IpAttempt.sql delete mode 100644 sql_query/users.sql diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 6b0e361..ba57143 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -545,6 +545,7 @@ $routes->post('payment/event_charges', 'View\EventController::eventUpdate'); // Parent event participation $routes->get('administrator/event-charges', 'View\EventController::eventShow'); +$routes->get('administrator/event-charges/pdf', 'View\EventController::eventChargesPdf'); $routes->post('administrator/event-charges/remove/(:num)', 'View\EventController::removeCharge/$1'); $routes->post('administrator/event-charges/payment/(:num)', 'View\EventController::toggleEventPayment/$1'); $routes->post('administrator/event-charges/waiver/(:num)', 'View\EventController::toggleWaiverStatus/$1'); diff --git a/app/Controllers/View/CertificateController.php b/app/Controllers/View/CertificateController.php index e68575d..8a89ce8 100644 --- a/app/Controllers/View/CertificateController.php +++ b/app/Controllers/View/CertificateController.php @@ -308,11 +308,11 @@ class CertificateController extends BaseController $pdf->Image($imgDir . 'background.png', 280, 176, 291, 340); $pdf->Image($imgDir . 'signature.png', 140, 399, 90, 90); - // ── QR code — small, bottom-right, just above the certificate number + // ── QR code — bottom-right corner: 1.5 cm from bottom, 2.5 cm from right $qrSize = 42; // pt if ($qrFile !== null && file_exists($qrFile)) { - $qrX = $W - 10 - $qrSize; // right-aligned with the cert number text - $qrY = $H - 14 - 3 - $qrSize; // 3 pt gap above the cert number line + $qrX = $W - 70.87 - $qrSize; // 2.5 cm from right edge + $qrY = $H - 42.52 - $qrSize; // 1.5 cm from bottom edge $pdf->Image($qrFile, $qrX, $qrY, $qrSize, $qrSize); } @@ -371,12 +371,12 @@ class CertificateController extends BaseController $pdf->SetXY(106, 492); $pdf->Cell(168, 20, 'Signature', 0, 0, 'C'); - // ── Certificate number — bottom-right, small gray + // ── Certificate number — 1.4 cm from bottom, 2.5 cm from left if ($certNumber !== '') { $pdf->SetFont('helvetica', '', 8); $pdf->SetTextColor(150, 150, 150); - $pdf->SetXY(0, $H - 14); - $pdf->Cell($W - 10, 12, 'Certificate No. ' . $certNumber, 0, 0, 'R'); + $pdf->SetXY(70.87, $H - 39.68); + $pdf->Cell(200, 12, 'Certificate No. ' . $certNumber, 0, 0, 'L'); $pdf->SetTextColor(0, 0, 0); } } diff --git a/app/Controllers/View/EventController.php b/app/Controllers/View/EventController.php index 659a41f..891e901 100644 --- a/app/Controllers/View/EventController.php +++ b/app/Controllers/View/EventController.php @@ -17,9 +17,11 @@ use App\Models\PaymentModel; use App\Models\CalendarModel; use App\Services\EmailService; use Config\Database; -#use ??? use App\Controllers\View\InvoiceController; +use CodeIgniter\HTTP\ResponseInterface; use CodeIgniter\RESTful\ResourceController; +use Dompdf\Dompdf; +use Dompdf\Options; class EventController extends ResourceController { @@ -121,6 +123,194 @@ class EventController extends ResourceController return $fallback; } + private function loadEventChargesListingData(bool $includeParents = true): array + { + $schoolYear = $this->request->getGet('school_year') ?? $this->schoolYear; + $semester = $this->request->getGet('semester') ?? $this->semester; + $filterEventId = (int) ($this->request->getGet('event_id') ?? 0); + $filterParentId = (int) ($this->request->getGet('parent_id') ?? 0); + + $parents = []; + if ($includeParents) { + $parents = $this->userModel->getParents(); + usort($parents, static function (array $a, array $b): int { + $aLabel = trim(preg_replace('/\s+/', ' ', (string) ($a['firstname'] ?? '') . ' ' . (string) ($a['lastname'] ?? ''))); + $bLabel = trim(preg_replace('/\s+/', ' ', (string) ($b['firstname'] ?? '') . ' ' . (string) ($b['lastname'] ?? ''))); + + $nameCmp = strnatcasecmp($aLabel, $bLabel); + if ($nameCmp !== 0) { + return $nameCmp; + } + + return strnatcasecmp((string) ($a['school_id'] ?? ''), (string) ($b['school_id'] ?? '')); + }); + } + + $events = $this->eventModel->getActiveEvents($schoolYear); + + $chargesBuilder = $this->eventChargesModel + ->select('event_charges.*, + users.firstname AS parent_firstname, users.lastname AS parent_lastname, users.cellphone AS parent_cellphone, + students.firstname AS student_firstname, students.lastname AS student_lastname, + events.event_name, events.description AS event_description, events.amount AS event_amount') + ->join('users', 'users.id = event_charges.parent_id', 'left') + ->join('students', 'students.id = event_charges.student_id', 'left') + ->join('events', 'events.id = event_charges.event_id', 'left') + ->where('event_charges.school_year', $schoolYear) + ->where('event_charges.semester', $semester); + + if ($filterEventId > 0) { + $chargesBuilder->where('event_charges.event_id', $filterEventId); + } + + if ($filterParentId > 0) { + $chargesBuilder->where('event_charges.parent_id', $filterParentId); + } + + $charges = $chargesBuilder + ->orderBy('event_charges.created_at', 'DESC') + ->findAll(); + + foreach ($charges as &$charge) { + if (empty($charge['class_section_id']) && !empty($charge['student_id'])) { + $sections = $this->studentClassModel->getClassSectionIdsByStudentId( + (int) $charge['student_id'], + $schoolYear + ); + if (!empty($sections)) { + $charge['class_section_id'] = $sections[0]; + } + } + } + unset($charge); + + $parentBalances = []; + try { + $balanceRows = $this->invoiceModel + ->select('parent_id, COALESCE(SUM(balance),0) AS total_balance') + ->where('school_year', $schoolYear) + ->where('semester', $semester) + ->groupBy('parent_id') + ->findAll(); + + foreach ($balanceRows as $row) { + $parentBalances[(int) ($row['parent_id'] ?? 0)] = (float) ($row['total_balance'] ?? 0.0); + } + } catch (\Throwable $e) { + log_message('error', 'Failed to load parent balances: ' . $e->getMessage()); + } + + $sectionIds = array_unique(array_filter(array_column($charges, 'class_section_id'))); + $classSectionNames = []; + if (!empty($sectionIds)) { + $sections = $this->classSectionModel + ->select('class_section_id, class_section_name') + ->whereIn('class_section_id', $sectionIds) + ->findAll(); + foreach ($sections as $section) { + $classSectionNames[(int) ($section['class_section_id'] ?? 0)] = $section['class_section_name'] ?? ''; + } + } + + return [ + 'charges' => $charges, + 'parents' => $parents, + 'events' => $events, + 'school_year' => $schoolYear, + 'semester' => $semester, + 'filterEventId' => $filterEventId, + 'filterParentId' => $filterParentId, + 'parentBalances' => $parentBalances, + 'classSectionNames' => $classSectionNames, + ]; + } + + private function buildGroupedEventCharges(array $charges): array + { + $grouped = []; + foreach ($charges as $charge) { + $eventId = (int) ($charge['event_id'] ?? 0); + if (!isset($grouped[$eventId])) { + $grouped[$eventId] = [ + 'label' => $charge['event_name'] ?? 'N/A', + 'rows' => [], + ]; + } + $grouped[$eventId]['rows'][] = $charge; + } + + return $grouped; + } + + private function applyGroupedEventChargeRowOrder(array $groupedCharges): array + { + $rowOrder = $this->request->getGet('row_order'); + if (!is_array($rowOrder) || empty($rowOrder)) { + return $groupedCharges; + } + + foreach ($groupedCharges as $eventId => &$group) { + $rawOrder = $rowOrder[$eventId] ?? $rowOrder[(string) $eventId] ?? null; + if (!is_string($rawOrder) || trim($rawOrder) === '') { + continue; + } + + $orderedIds = array_values(array_filter(array_map( + static fn ($value): int => (int) trim((string) $value), + explode(',', $rawOrder) + ))); + if (empty($orderedIds) || empty($group['rows']) || !is_array($group['rows'])) { + continue; + } + + $rowsById = []; + foreach ($group['rows'] as $row) { + $rowsById[(int) ($row['id'] ?? 0)] = $row; + } + + $reorderedRows = []; + foreach ($orderedIds as $chargeId) { + if (isset($rowsById[$chargeId])) { + $reorderedRows[] = $rowsById[$chargeId]; + unset($rowsById[$chargeId]); + } + } + + foreach ($group['rows'] as $row) { + $chargeId = (int) ($row['id'] ?? 0); + if (isset($rowsById[$chargeId])) { + $reorderedRows[] = $row; + unset($rowsById[$chargeId]); + } + } + + $group['rows'] = $reorderedRows; + } + unset($group); + + return $groupedCharges; + } + + private function buildEventChargesPdfFilename(array $data): string + { + $label = 'event_participant_lists'; + if (!empty($data['filterEventId'])) { + foreach (($data['events'] ?? []) as $event) { + if ((int) ($event['id'] ?? 0) === (int) $data['filterEventId']) { + $label = (string) ($event['event_name'] ?? $label); + break; + } + } + } + + $safeLabel = trim((string) preg_replace('/[^A-Za-z0-9]+/', '_', strtolower($label)), '_'); + if ($safeLabel === '') { + $safeLabel = 'event_participant_lists'; + } + + return $safeLabel . '_' . date('Ymd_His') . '.pdf'; + } + public function index() { $eventModel = new EventModel(); @@ -475,85 +665,7 @@ class EventController extends ResourceController // Optionally keep your eventShow / eventUpdate for legacy administrator event charges public function eventShow() { - - $schoolYear = $this->request->getGet('school_year') ?? $this->schoolYear; - $semester = $this->request->getGet('semester') ?? $this->semester; - - $parents = $this->userModel->getParents(); - usort($parents, static function (array $a, array $b): int { - $aLabel = trim(preg_replace('/\s+/', ' ', (string) ($a['firstname'] ?? '') . ' ' . (string) ($a['lastname'] ?? ''))); - $bLabel = trim(preg_replace('/\s+/', ' ', (string) ($b['firstname'] ?? '') . ' ' . (string) ($b['lastname'] ?? ''))); - - $nameCmp = strnatcasecmp($aLabel, $bLabel); - if ($nameCmp !== 0) { - return $nameCmp; - } - - return strnatcasecmp((string) ($a['school_id'] ?? ''), (string) ($b['school_id'] ?? '')); - }); - $events = $this->eventModel->getActiveEvents($schoolYear); - $filterEventId = (int) ($this->request->getGet('event_id') ?? 0); - $filterParentId = (int) ($this->request->getGet('parent_id') ?? 0); - - $chargesBuilder = $this->eventChargesModel - ->select('event_charges.*, - users.firstname AS parent_firstname, users.lastname AS parent_lastname, users.cellphone AS parent_cellphone, - students.firstname AS student_firstname, students.lastname AS student_lastname, - events.event_name, events.description AS event_description, events.amount AS event_amount') - ->join('users', 'users.id = event_charges.parent_id', 'left') - ->join('students', 'students.id = event_charges.student_id', 'left') - ->join('events', 'events.id = event_charges.event_id', 'left') - ->where('event_charges.school_year', $schoolYear) - ->where('event_charges.semester', $semester); - - if ($filterEventId > 0) { - $chargesBuilder->where('event_charges.event_id', $filterEventId); - } - - $charges = $chargesBuilder - ->orderBy('event_charges.created_at', 'DESC') - ->findAll(); - - foreach ($charges as &$charge) { - if (empty($charge['class_section_id']) && !empty($charge['student_id'])) { - $sections = $this->studentClassModel->getClassSectionIdsByStudentId( - (int)$charge['student_id'], - $schoolYear - ); - if (!empty($sections)) { - $charge['class_section_id'] = $sections[0]; - } - } - } - unset($charge); - - $parentBalances = []; - try { - $balanceRows = $this->invoiceModel - ->select('parent_id, COALESCE(SUM(balance),0) AS total_balance') - ->where('school_year', $schoolYear) - ->where('semester', $semester) - ->groupBy('parent_id') - ->findAll(); - - foreach ($balanceRows as $row) { - $parentBalances[(int)($row['parent_id'] ?? 0)] = (float)($row['total_balance'] ?? 0.0); - } - } catch (\Throwable $e) { - log_message('error', 'Failed to load parent balances: ' . $e->getMessage()); - } - - $sectionIds = array_unique(array_filter(array_column($charges, 'class_section_id'))); - $classSectionNames = []; - if (!empty($sectionIds)) { - $sections = $this->classSectionModel - ->select('class_section_id, class_section_name') - ->whereIn('class_section_id', $sectionIds) - ->findAll(); - foreach ($sections as $section) { - $classSectionNames[(int)($section['class_section_id'] ?? 0)] = $section['class_section_name'] ?? ''; - } - } + $data = $this->loadEventChargesListingData(); $semesterOptions = (new EventChargesModel()) ->select('semester') @@ -575,19 +687,37 @@ class EventController extends ResourceController $schoolYearOptions = [$this->schoolYear]; } - return view('administrator/events/event_charges', [ - 'charges' => $charges, - 'parents' => $parents, - 'events' => $events, - 'school_year' => $schoolYear, - 'semester' => $semester, - 'filterEventId' => $filterEventId, - 'filterParentId' => $filterParentId, - 'parentBalances' => $parentBalances, - 'classSectionNames' => $classSectionNames, + return view('administrator/events/event_charges', array_merge($data, [ 'semesterOptions' => $semesterOptions, 'schoolYearOptions' => $schoolYearOptions, - ]); + ])); + } + + public function eventChargesPdf(): ResponseInterface + { + $data = $this->loadEventChargesListingData(false); + $groupedCharges = $this->buildGroupedEventCharges($data['charges']); + $groupedCharges = $this->applyGroupedEventChargeRowOrder($groupedCharges); + $generatedAt = date('m-d-Y h:i A'); + + $html = view('administrator/events/event_charges_pdf', array_merge($data, [ + 'groupedCharges' => $groupedCharges, + 'generatedAt' => $generatedAt, + ])); + + $options = new Options(); + $options->set('isRemoteEnabled', true); + $options->set('defaultFont', 'DejaVu Sans'); + + $dompdf = new Dompdf($options); + $dompdf->loadHtml($html, 'UTF-8'); + $dompdf->setPaper('A4', 'landscape'); + $dompdf->render(); + + return $this->response + ->setHeader('Content-Type', 'application/pdf') + ->setHeader('Content-Disposition', 'inline; filename="' . $this->buildEventChargesPdfFilename($data) . '"') + ->setBody($dompdf->output()); } diff --git a/app/Views/administrator/events/event_charges.php b/app/Views/administrator/events/event_charges.php index cbd530a..db35a95 100644 --- a/app/Views/administrator/events/event_charges.php +++ b/app/Views/administrator/events/event_charges.php @@ -224,23 +224,51 @@
No charges found.
+ $school_year, + 'semester' => $semester, + 'parent_id' => $filterParentId > 0 ? $filterParentId : null, + 'event_id' => $filterEventId > 0 ? $filterEventId : null, + ], static fn ($value) => $value !== null && $value !== ''); + $printAllUrl = site_url('administrator/event-charges/pdf'); + if (!empty($printBaseParams)) { + $printAllUrl .= '?' . http_build_query($printBaseParams); + } + ?>

Click a column header to sort the participants list.

- +
$data): ?> + $school_year, + 'semester' => $semester, + 'parent_id' => $filterParentId > 0 ? $filterParentId : null, + 'event_id' => $eventId, + ], static fn ($value) => $value !== null && $value !== ''); + $singlePrintUrl = site_url('administrator/event-charges/pdf') . '?' . http_build_query($singlePrintParams); + ?>
@@ -287,7 +315,7 @@ . (!empty($returnParams) ? '?' . http_build_query($returnParams) : '') . '#eventTable_' . (int) ($charge['event_id'] ?? 0); ?> - + /g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); - } - function submitExternalParticipant(payload) { const form = document.createElement('form'); form.method = 'post'; @@ -690,89 +709,41 @@ function loadStudentsWithCharges() { ); } - 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; + function getTableRowOrder(table) { + if (!table || !table.tBodies.length) { + return []; } - const style = ` - - `; - printWindow.document.open(); - printWindow.document.write(` - - - - - ${escapeHtml(title)} - ${style} - - -

${escapeHtml(title)}

-

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

- ${contentHtml} -