391 lines
18 KiB
PHP
391 lines
18 KiB
PHP
<?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);
|
||
}
|
||
}
|