Files
alrahma_sunday_school/app/Controllers/View/StickersController.php
T
2026-02-10 22:11:06 -05:00

391 lines
18 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Controllers\View;
class StickersController extends PrintablesBaseController
{
public function stickerForm()
{
$data['students'] = $this->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[<student_id>] : 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);
}
}