studentModel->findAll(); $data['classes'] = $this->classSectionModel->findAll(); $data['stickers'] = $this->stickerPresets(); return view('printables_reports/sticker_form', $data); } public function getByClass($classId) { $builder = $this->db->table('student_class sc') ->select('s.id, s.firstname, s.lastname, s.registration_grade, s.gender') ->join('students s', 's.id = sc.student_id') ->where('sc.class_section_id', $classId) ->where('sc.school_year', $this->schoolYear) ->orderBy('s.lastname', 'asc'); $students = $builder->get()->getResultArray(); return $this->response->setJSON($students); } /** Preset sticker sizes for the view (robust keys) */ private function stickerPresets(): array { $rows = [ ['value' => '100x24.5', 'label' => 'Xsmall - 100x24.5 mm', 'ppg' => 20], ['value' => '60x30', 'label' => 'Small - 60x30 mm', 'ppg' => 24], ['value' => '102x34', 'label' => 'Medium - 102x34 mm', 'ppg' => 14], ['value' => '90x45', 'label' => 'Large - 90x45 mm', 'ppg' => 10], ]; // Add aliases so different views will not break (title/text/per_page) return array_map(static function ($r) { $r['title'] = $r['title'] ?? ($r['label'] ?? ($r['value'] . ' mm')); $r['text'] = $r['text'] ?? $r['title']; $r['per_page'] = $r['per_page'] ?? ($r['ppg'] ?? null); return $r; }, $rows); } /** * Generate name stickers as a PDF. * * Can be used for different sticker sizes/sheets by passing parameters * via function args (from routes) or via GET/POST form fields. * * Accepted request params (POST takes precedence over GET): * - class_id : int * - student_id : int * - print_all : 0|1 * - sticker_size : string like "102x34" (accepts upper/lower case X or the unicode multiply sign) * - sticker_width : number (mm) * - sticker_height : number (mm) * - stickers_per_page : int (optional cap; grid still derived from size/margins) * - page_size : "A4" (default) | "Letter" | any FPDF size token * - orientation : "P" (default) | "L" * - margin_left : number (mm) * - margin_top : number (mm) * - margin_right : number (mm; defaults to margin_left) * - margin_bottom : number (mm; default 10) * - gap_x : number (mm; horizontal gap between stickers) * - gap_y : number (mm; vertical gap between stickers) * - copies[] : int per-student copies in batch mode * - single_copies : int copies for single student mode */ public function generateStickers( ?int $studentId = null, ?float $stickerWidthArg = null, ?float $stickerHeightArg = null, ?int $stickersPerPageArg = null, ?string $pageSizeArg = null, ?string $orientationArg = null, ?float $marginLeftArg = null, ?float $marginTopArg = null, ?float $marginRightArg = null, ?float $marginBottomArg = null, ?float $gapXArg = null, ?float $gapYArg = null ) { $request = service('request'); $method = strtolower($request->getMethod()); $classId = $request->getPost('class_id') ?: $request->getGet('class_id'); $studentId = $studentId ?? ($request->getPost('student_id') ?: $request->getGet('student_id')); // Safety cast $classId = $classId !== null ? (int) $classId : null; $studentId = $studentId !== null ? (int) $studentId : null; // Sanity log for school year if (empty($this->schoolYear)) { log_message('error', 'generateStickers(): $this->schoolYear is empty. Load it from ConfigurationModel.'); } else { log_message('debug', 'generateStickers(): schoolYear=' . $this->schoolYear); } // Load classes that actually have students in this school year $classes = $this->classSectionModel ->select('classSection.class_section_id, classSection.class_section_name') ->join('student_class', 'student_class.class_section_id = classSection.class_section_id', 'inner') ->where('student_class.school_year', $this->schoolYear) ->groupBy('classSection.class_section_id, classSection.class_section_name') ->orderBy('classSection.class_section_name', 'ASC') ->findAll(); // ---------- GET: render form ---------- if ($method === 'get') { $students = []; if (!empty($classId)) { $students = $this->fetchStudentsByClass((int) $classId, $this->schoolYear); } else { $students = $this->studentModel ->select('students.id, students.firstname, students.lastname') ->join('student_class', 'student_class.student_id = students.id', 'inner') ->where('student_class.school_year', $this->schoolYear) ->groupBy('students.id, students.firstname, students.lastname') ->orderBy('students.firstname', 'ASC') ->orderBy('students.lastname', 'ASC') ->findAll(500); } // Presets (now with robust keys expected by the view) $presetRows = [ ['value' => '60x30', 'label' => 'Small - 60x30 mm', 'ppg' => 24], ['value' => '102x34', 'label' => 'Medium - 102x34 mm', 'ppg' => 14], ['value' => '90x45', 'label' => 'Large - 90x45 mm', 'ppg' => 10], ]; // Normalize to include 'title', 'text', and 'per_page' so views using any of those will not error. $stickers = array_map(static function (array $r): array { $r['title'] = $r['title'] ?? ($r['label'] ?? ($r['value'] . ' mm')); $r['text'] = $r['text'] ?? $r['title']; $r['per_page'] = $r['per_page'] ?? ($r['ppg'] ?? null); return $r; }, $presetRows); return view('printables_reports/sticker_form', [ 'classes' => $classes, 'students' => $students, 'stickers' => $stickers, ]); } // ---------- POST: build student list ---------- $printAll = (int) ($request->getPost('print_all') ?? $request->getGet('print_all') ?? 0) === 1; $students = []; if (!empty($studentId)) { $student = $this->studentModel->find((int) $studentId); if (!$student) { return 'Student not found.'; } $students = [$student]; } elseif (!empty($classId)) { $students = $this->fetchStudentsByClass((int) $classId, $this->schoolYear); if (empty($students)) { return 'No students found in this class.'; } } elseif ($printAll) { $students = $this->studentModel ->select('students.id, students.firstname, students.lastname, sc.class_section_id, cs.class_section_name, c.id AS class_id') ->join('student_class sc', 'sc.student_id = students.id', 'inner') ->join('classSection cs', 'cs.class_section_id = sc.class_section_id', 'inner') ->join('classes c', 'c.id = cs.class_id', 'inner') ->where('sc.school_year', $this->schoolYear) ->groupBy('students.id, students.firstname, students.lastname, sc.class_section_id, cs.class_section_name, c.id') ->orderBy('c.id', 'ASC') ->orderBy('cs.class_section_name', 'ASC') ->orderBy('students.firstname', 'ASC') ->orderBy('students.lastname', 'ASC') ->findAll(5000); // Exclude Youth and KG $students = array_values(array_filter($students, static function ($s) { $g = trim((string) ($s['class_section_name'] ?? '')); return $g !== '' && !preg_match('/^(youth|kg)(?:\b|-)/i', $g); })); if (empty($students)) { return 'No students found for this school year.'; } } else { return 'Please select a student or class.'; } // ---- Expand list according to per-student copy counts ---- $copiesPosted = $request->getPost('copies'); // array|null $singleCopies = (int) ($request->getPost('single_copies') ?? $request->getGet('single_copies') ?? 1); if ($singleCopies < 0) $singleCopies = 0; $printList = []; if (!empty($studentId)) { $qty = max(0, $singleCopies); for ($k = 0; $k < $qty; $k++) { $printList[] = $students[0]; } } else { $hasCopies = is_array($copiesPosted); $defaultQty = $hasCopies ? 0 : 1; // if copies[] present, only print IDs provided foreach ($students as $stu) { $id = (int) ($stu['id'] ?? 0); $qty = $defaultQty; if ($hasCopies && array_key_exists((string)$id, $copiesPosted)) { $qty = (int) $copiesPosted[(string)$id]; } $qty = max(0, $qty); for ($k = 0; $k < $qty; $k++) { $printList[] = $stu; } } } if (empty($printList)) { return 'Nothing to print. All quantities are zero.'; } // ---------- Layout parameters (request/args/defaults) ---------- $sizeStrReq = trim((string) ($request->getPost('sticker_size') ?? $request->getGet('sticker_size') ?? '')); $stickerWidth = $stickerWidthArg ?? (float) ($request->getPost('sticker_width') ?? $request->getGet('sticker_width') ?? 0); $stickerHeight = $stickerHeightArg ?? (float) ($request->getPost('sticker_height') ?? $request->getGet('sticker_height') ?? 0); // parse size string if provided (supports decimals; accepts x/X/unicode multiply) if (($stickerWidth <= 0 || $stickerHeight <= 0) && $sizeStrReq !== '') { if (preg_match('/^\\s*(\\d+(?:\\.\\d+)?)\\s*[xX\\x{00D7}]\\s*(\\d+(?:\\.\\d+)?)\\s*$/u', $sizeStrReq, $m)) { $stickerWidth = (float) $m[1]; $stickerHeight = (float) $m[2]; } } // fallbacks to controller properties or sane defaults (mm) if ($stickerWidth <= 0) { $stickerWidth = isset($this->stickerWidth) ? (float)$this->stickerWidth : 102.0; } if ($stickerHeight <= 0) { $stickerHeight = isset($this->stickerHeight) ? (float)$this->stickerHeight : 34.0; } $stickersPerPageReq = $stickersPerPageArg ?? (int) ($request->getPost('stickers_per_page') ?? $request->getGet('stickers_per_page') ?? 0); $pageSize = $pageSizeArg ?? (string) ($request->getPost('page_size') ?? $request->getGet('page_size') ?? 'A4'); $orientation = $orientationArg ?? (string) ($request->getPost('orientation') ?? $request->getGet('orientation') ?? 'P'); $marginLeftProvided = $marginLeftArg !== null || $request->getPost('margin_left') !== null || $request->getGet('margin_left') !== null; $marginTopProvided = $marginTopArg !== null || $request->getPost('margin_top') !== null || $request->getGet('margin_top') !== null; $marginRightProvided = $marginRightArg !== null || $request->getPost('margin_right') !== null || $request->getGet('margin_right') !== null; $marginBottomProvided = $marginBottomArg !== null || $request->getPost('margin_bottom') !== null || $request->getGet('margin_bottom') !== null; $gapXProvided = $gapXArg !== null || $request->getPost('gap_x') !== null || $request->getGet('gap_x') !== null; $gapYProvided = $gapYArg !== null || $request->getPost('gap_y') !== null || $request->getGet('gap_y') !== null; $marginLeft = $marginLeftArg ?? (float) ($request->getPost('margin_left') ?? $request->getGet('margin_left') ?? ($this->marginL ?? 6)); $marginTop = $marginTopArg ?? (float) ($request->getPost('margin_top') ?? $request->getGet('margin_top') ?? ($this->marginT ?? 6)); $marginRight = $marginRightArg ?? (float) ($request->getPost('margin_right') ?? $request->getGet('margin_right') ?? $marginLeft); $marginBottom = $marginBottomArg ?? (float) ($request->getPost('margin_bottom') ?? $request->getGet('margin_bottom') ?? 10); $gapX = $gapXArg ?? (float) ($request->getPost('gap_x') ?? $request->getGet('gap_x') ?? ($this->gapX ?? 0)); $gapY = $gapYArg ?? (float) ($request->getPost('gap_y') ?? $request->getGet('gap_y') ?? ($this->gapY ?? 0)); // If using the 100x24.5 preset (small labels) and nothing custom was provided, // default to tight margins and zero gaps to fit 20 per page (2 columns * 10+ rows). $normalizedSize = strtolower(str_replace(['×', ' '], ['x', ''], $sizeStrReq)); $isXSmallPreset = $normalizedSize === '100x24.5' || (abs($stickerWidth - 100.0) < 0.6 && abs($stickerHeight - 24.5) < 0.6); if ($isXSmallPreset) { if (!$marginLeftProvided) { $marginLeft = 4.0; if (!$marginRightProvided) { $marginRight = 4.0; } } if (!$marginRightProvided && $marginRight < 4.0) { $marginRight = 4.0; } if (!$gapXProvided) { $gapX = 0.0; } if (!$gapYProvided) { $gapY = 0.0; } } // ---------- FPDF setup ---------- $pdf = new \FPDF($orientation, 'mm', $pageSize); $pdf->SetMargins($marginLeft, $marginTop, $marginRight); $pdf->SetAutoPageBreak(true, $marginBottom); $pdf->AddPage(); $pdf->SetFont('Arial', '', 12); // Printable area $pageW = (float) $pdf->GetPageWidth(); $pageH = (float) $pdf->GetPageHeight(); $usableW = max(0.0, $pageW - ($marginLeft + $marginRight)); $usableH = max(0.0, $pageH - ($marginTop + $marginBottom)); // Compute columns & rows from physical dimensions and gaps. $cols = (int) floor(($usableW + max(0.0, $gapX)) / (max(0.1, $stickerWidth) + max(0.0, $gapX))); $rows = (int) floor(($usableH + max(0.0, $gapY)) / (max(0.1, $stickerHeight) + max(0.0, $gapY))); $cols = max(1, $cols); $rows = max(1, $rows); // Optional cap: stickers_per_page $capacity = $cols * $rows; if ($stickersPerPageReq > 0 && $stickersPerPageReq < $capacity) { $rows = (int) ceil($stickersPerPageReq / $cols); $maxRowsFit = (int) floor(($usableH + max(0.0, $gapY)) / (max(0.1, $stickerHeight) + max(0.0, $gapY))); $rows = max(1, min($rows, max(1, $maxRowsFit))); $capacity = $cols * $rows; } // Starting positions $xStart = $marginLeft; $yStart = $marginTop; $x = $xStart; $y = $yStart; $pt2mm = static function (float $pt): float { return $pt * 0.352778; }; // Draw loop $i = 0; $perPage = $capacity; foreach ($printList as $stu) { if ($i > 0 && $i % $perPage === 0) { $pdf->AddPage(); $x = $xStart; $y = $yStart; } // Background sticker rectangle (optional; fill white) $pdf->SetFillColor(255, 255, 255); $pdf->Rect($x, $y, $stickerWidth, $stickerHeight, 'F'); // Centered name with dynamic font size $fontFamily = 'Arial'; $fontStyle = 'B'; $maxPt = 16; $minPt = 8; $hPad = 2; $fullName = trim(($stu['firstname'] ?? '') . ' ' . ($stu['lastname'] ?? '')) ?: 'Student'; $bestPt = $maxPt; $textW = 0.0; while ($bestPt >= $minPt) { $pdf->SetFont($fontFamily, $fontStyle, $bestPt); $textW = (float) $pdf->GetStringWidth($fullName); if ($textW <= ($stickerWidth - 2 * $hPad)) break; $bestPt -= 0.5; } if ($bestPt < $minPt) { $bestPt = $minPt; $pdf->SetFont($fontFamily, $fontStyle, $bestPt); $textW = (float) $pdf->GetStringWidth($fullName); } $textH = $pt2mm($bestPt); $cellX = $x + (($stickerWidth - $textW) / 2); $cellY = $y + (($stickerHeight - $textH) / 2); $pdf->SetXY($cellX, $cellY); $pdf->Cell($textW, $textH, $fullName, 0, 0, 'L'); // Advance cell position within the grid $i++; $posInRow = ($i - 1) % $cols; if ($posInRow === ($cols - 1)) { $x = $xStart; $y += $stickerHeight + $gapY; } else { $x += $stickerWidth + $gapX; } } // Output $pdfContent = $pdf->Output('S'); return $this->response ->setHeader('Content-Type', 'application/pdf') ->setHeader('Content-Disposition', 'inline; filename="Student_Stickers.pdf"') ->setHeader('Cache-Control', 'private, max-age=0, must-revalidate') ->setHeader('Pragma', 'public') ->setHeader('Content-Length', (string) strlen($pdfContent)) ->setBody($pdfContent); } }