add stickers logic
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Reports;
|
||||
|
||||
use App\Http\Controllers\Api\BaseApiController;
|
||||
use App\Http\Requests\Reports\Stickers\StickerFormDataRequest;
|
||||
use App\Http\Requests\Reports\Stickers\StickerPrintRequest;
|
||||
use App\Http\Requests\Reports\Stickers\StickerStudentsByClassRequest;
|
||||
use App\Http\Resources\Reports\Stickers\StickerClassSectionResource;
|
||||
use App\Http\Resources\Reports\Stickers\StickerPresetResource;
|
||||
use App\Http\Resources\Reports\Stickers\StickerStudentResource;
|
||||
use App\Services\Reports\Stickers\StickerPresetService;
|
||||
use App\Services\Reports\Stickers\StickerPrintService;
|
||||
use App\Services\Reports\Stickers\StickerQueryService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class StickersController extends BaseApiController
|
||||
{
|
||||
public function __construct(
|
||||
private StickerQueryService $queryService,
|
||||
private StickerPresetService $presetService,
|
||||
private StickerPrintService $printService
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function formData(StickerFormDataRequest $request): JsonResponse
|
||||
{
|
||||
$payload = $request->validated();
|
||||
$schoolYear = $this->queryService->resolveSchoolYear($payload['school_year'] ?? null);
|
||||
$classSectionId = (int) ($payload['class_section_id'] ?? 0);
|
||||
|
||||
$classes = $this->queryService->listClassesWithStudents($schoolYear);
|
||||
$students = $classSectionId > 0
|
||||
? $this->queryService->listStudentsByClass($classSectionId, $schoolYear)
|
||||
: $this->queryService->listStudentsForYear($schoolYear);
|
||||
|
||||
$presets = $this->presetService->presets();
|
||||
|
||||
return $this->success([
|
||||
'classes' => StickerClassSectionResource::collection($classes),
|
||||
'students' => StickerStudentResource::collection($students),
|
||||
'presets' => StickerPresetResource::collection($presets),
|
||||
]);
|
||||
}
|
||||
|
||||
public function studentsByClass(StickerStudentsByClassRequest $request): JsonResponse
|
||||
{
|
||||
$payload = $request->validated();
|
||||
$schoolYear = $this->queryService->resolveSchoolYear($payload['school_year'] ?? null);
|
||||
$classSectionId = (int) ($payload['class_section_id'] ?? 0);
|
||||
|
||||
$students = $this->queryService->listStudentsByClass($classSectionId, $schoolYear);
|
||||
|
||||
return $this->success([
|
||||
'students' => StickerStudentResource::collection($students),
|
||||
]);
|
||||
}
|
||||
|
||||
public function print(StickerPrintRequest $request)
|
||||
{
|
||||
try {
|
||||
$result = $this->printService->generate($request->validated());
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Sticker print failed: ' . $e->getMessage());
|
||||
return $this->error('Failed to generate stickers.', Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
if (!($result['ok'] ?? false)) {
|
||||
return $this->error($result['message'] ?? 'Unable to generate stickers.', Response::HTTP_UNPROCESSABLE_ENTITY);
|
||||
}
|
||||
|
||||
return response($result['pdf'], 200, [
|
||||
'Content-Type' => 'application/pdf',
|
||||
'Content-Disposition' => 'inline; filename="' . ($result['filename'] ?? 'Student_Stickers.pdf') . '"',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Reports\Stickers;
|
||||
|
||||
use App\Http\Requests\ApiFormRequest;
|
||||
|
||||
class StickerFormDataRequest extends ApiFormRequest
|
||||
{
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
parent::prepareForValidation();
|
||||
|
||||
if (!$this->has('class_section_id') && $this->has('class_id')) {
|
||||
$this->merge(['class_section_id' => $this->input('class_id')]);
|
||||
}
|
||||
}
|
||||
|
||||
public function authorize(): bool
|
||||
{
|
||||
return auth()->check();
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'school_year' => ['nullable', 'string', 'max:20'],
|
||||
'class_section_id' => ['nullable', 'integer', 'min:1'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Reports\Stickers;
|
||||
|
||||
use App\Http\Requests\ApiFormRequest;
|
||||
use Illuminate\Validation\Validator;
|
||||
|
||||
class StickerPrintRequest extends ApiFormRequest
|
||||
{
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
parent::prepareForValidation();
|
||||
|
||||
if (!$this->has('class_section_id') && $this->has('class_id')) {
|
||||
$this->merge(['class_section_id' => $this->input('class_id')]);
|
||||
}
|
||||
}
|
||||
|
||||
public function authorize(): bool
|
||||
{
|
||||
return auth()->check();
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'school_year' => ['nullable', 'string', 'max:20'],
|
||||
'student_id' => ['nullable', 'integer', 'min:1'],
|
||||
'class_section_id' => ['nullable', 'integer', 'min:1'],
|
||||
'print_all' => ['nullable', 'boolean'],
|
||||
'sticker_size' => ['nullable', 'string', 'max:20'],
|
||||
'sticker_width' => ['nullable', 'numeric', 'min:1'],
|
||||
'sticker_height' => ['nullable', 'numeric', 'min:1'],
|
||||
'stickers_per_page' => ['nullable', 'integer', 'min:1'],
|
||||
'page_size' => ['nullable', 'string', 'max:20'],
|
||||
'orientation' => ['nullable', 'string', 'max:2'],
|
||||
'margin_left' => ['nullable', 'numeric', 'min:0'],
|
||||
'margin_top' => ['nullable', 'numeric', 'min:0'],
|
||||
'margin_right' => ['nullable', 'numeric', 'min:0'],
|
||||
'margin_bottom' => ['nullable', 'numeric', 'min:0'],
|
||||
'gap_x' => ['nullable', 'numeric', 'min:0'],
|
||||
'gap_y' => ['nullable', 'numeric', 'min:0'],
|
||||
'copies' => ['nullable', 'array'],
|
||||
'copies.*' => ['integer', 'min:0'],
|
||||
'single_copies' => ['nullable', 'integer', 'min:0'],
|
||||
];
|
||||
}
|
||||
|
||||
public function withValidator(Validator $validator): void
|
||||
{
|
||||
$validator->after(function (Validator $validator) {
|
||||
$hasStudent = $this->filled('student_id');
|
||||
$hasClass = $this->filled('class_section_id');
|
||||
$printAll = (bool) $this->input('print_all', false);
|
||||
|
||||
if (!$hasStudent && !$hasClass && !$printAll) {
|
||||
$validator->errors()->add('selection', 'Please select a student or class, or set print_all.');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Reports\Stickers;
|
||||
|
||||
use App\Http\Requests\ApiFormRequest;
|
||||
|
||||
class StickerStudentsByClassRequest extends ApiFormRequest
|
||||
{
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
parent::prepareForValidation();
|
||||
|
||||
if (!$this->has('class_section_id') && $this->has('class_id')) {
|
||||
$this->merge(['class_section_id' => $this->input('class_id')]);
|
||||
}
|
||||
}
|
||||
|
||||
public function authorize(): bool
|
||||
{
|
||||
return auth()->check();
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'class_section_id' => ['required', 'integer', 'min:1'],
|
||||
'school_year' => ['nullable', 'string', 'max:20'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources\Reports\Stickers;
|
||||
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class StickerClassSectionResource extends JsonResource
|
||||
{
|
||||
public function toArray($request): array
|
||||
{
|
||||
return [
|
||||
'class_section_id' => (int) ($this['class_section_id'] ?? 0),
|
||||
'class_section_name' => (string) ($this['class_section_name'] ?? ''),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources\Reports\Stickers;
|
||||
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class StickerPresetResource extends JsonResource
|
||||
{
|
||||
public function toArray($request): array
|
||||
{
|
||||
return [
|
||||
'value' => (string) ($this['value'] ?? ''),
|
||||
'label' => (string) ($this['label'] ?? ''),
|
||||
'title' => (string) ($this['title'] ?? ''),
|
||||
'text' => (string) ($this['text'] ?? ''),
|
||||
'per_page' => $this['per_page'] ?? null,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources\Reports\Stickers;
|
||||
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class StickerStudentResource extends JsonResource
|
||||
{
|
||||
public function toArray($request): array
|
||||
{
|
||||
return [
|
||||
'id' => (int) ($this['id'] ?? 0),
|
||||
'firstname' => (string) ($this['firstname'] ?? ''),
|
||||
'lastname' => (string) ($this['lastname'] ?? ''),
|
||||
'registration_grade' => $this['registration_grade'] ?? null,
|
||||
'gender' => $this['gender'] ?? null,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Reports\Stickers;
|
||||
|
||||
use App\Models\Configuration;
|
||||
|
||||
class StickerContextService
|
||||
{
|
||||
public function schoolYear(?string $override = null): string
|
||||
{
|
||||
$override = trim((string) $override);
|
||||
if ($override !== '') {
|
||||
return $override;
|
||||
}
|
||||
|
||||
return trim((string) (Configuration::getConfig('school_year') ?? ''));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Reports\Stickers;
|
||||
|
||||
class StickerPresetService
|
||||
{
|
||||
public function presets(): 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],
|
||||
];
|
||||
|
||||
return array_map(static function (array $row): array {
|
||||
$row['title'] = $row['title'] ?? ($row['label'] ?? ($row['value'] . ' mm'));
|
||||
$row['text'] = $row['text'] ?? $row['title'];
|
||||
$row['per_page'] = $row['per_page'] ?? ($row['ppg'] ?? null);
|
||||
return $row;
|
||||
}, $rows);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Reports\Stickers;
|
||||
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class StickerPrintService
|
||||
{
|
||||
public function __construct(private StickerQueryService $queryService)
|
||||
{
|
||||
}
|
||||
|
||||
public function generate(array $payload): array
|
||||
{
|
||||
$schoolYear = $this->queryService->resolveSchoolYear($payload['school_year'] ?? null);
|
||||
if ($schoolYear === '') {
|
||||
Log::error('Sticker print requested without school year.');
|
||||
}
|
||||
|
||||
$studentId = (int) ($payload['student_id'] ?? 0);
|
||||
$classSectionId = (int) ($payload['class_section_id'] ?? $payload['class_id'] ?? 0);
|
||||
$printAll = (bool) ($payload['print_all'] ?? false);
|
||||
|
||||
if ($studentId > 0) {
|
||||
$student = $this->queryService->findStudent($studentId);
|
||||
if (!$student) {
|
||||
return ['ok' => false, 'message' => 'Student not found.'];
|
||||
}
|
||||
$students = [$student];
|
||||
} elseif ($classSectionId > 0) {
|
||||
$students = $this->queryService->listStudentsByClass($classSectionId, $schoolYear);
|
||||
if (empty($students)) {
|
||||
return ['ok' => false, 'message' => 'No students found in this class.'];
|
||||
}
|
||||
} elseif ($printAll) {
|
||||
$students = $this->queryService->listStudentsForPrintAll($schoolYear);
|
||||
if (empty($students)) {
|
||||
return ['ok' => false, 'message' => 'No students found for this school year.'];
|
||||
}
|
||||
} else {
|
||||
return ['ok' => false, 'message' => 'Please select a student or class.'];
|
||||
}
|
||||
|
||||
$printList = $this->expandCopies($students, $payload, $studentId > 0);
|
||||
if (empty($printList)) {
|
||||
return ['ok' => false, 'message' => 'Nothing to print. All quantities are zero.'];
|
||||
}
|
||||
|
||||
$layout = $this->buildLayout($payload);
|
||||
|
||||
$pdf = new \FPDF($layout['orientation'], 'mm', $layout['page_size']);
|
||||
$pdf->SetMargins($layout['margin_left'], $layout['margin_top'], $layout['margin_right']);
|
||||
$pdf->SetAutoPageBreak(true, $layout['margin_bottom']);
|
||||
$pdf->AddPage();
|
||||
$pdf->SetFont('Arial', '', 12);
|
||||
|
||||
$pageW = (float) $pdf->GetPageWidth();
|
||||
$pageH = (float) $pdf->GetPageHeight();
|
||||
|
||||
$usableW = max(0.0, $pageW - ($layout['margin_left'] + $layout['margin_right']));
|
||||
$usableH = max(0.0, $pageH - ($layout['margin_top'] + $layout['margin_bottom']));
|
||||
|
||||
$cols = (int) floor(($usableW + max(0.0, $layout['gap_x'])) / (max(0.1, $layout['sticker_width']) + max(0.0, $layout['gap_x'])));
|
||||
$rows = (int) floor(($usableH + max(0.0, $layout['gap_y'])) / (max(0.1, $layout['sticker_height']) + max(0.0, $layout['gap_y'])));
|
||||
|
||||
$cols = max(1, $cols);
|
||||
$rows = max(1, $rows);
|
||||
|
||||
$capacity = $cols * $rows;
|
||||
if ($layout['stickers_per_page'] > 0 && $layout['stickers_per_page'] < $capacity) {
|
||||
$rows = (int) ceil($layout['stickers_per_page'] / $cols);
|
||||
$maxRowsFit = (int) floor(($usableH + max(0.0, $layout['gap_y'])) / (max(0.1, $layout['sticker_height']) + max(0.0, $layout['gap_y'])));
|
||||
$rows = max(1, min($rows, max(1, $maxRowsFit)));
|
||||
$capacity = $cols * $rows;
|
||||
}
|
||||
|
||||
$xStart = $layout['margin_left'];
|
||||
$yStart = $layout['margin_top'];
|
||||
$x = $xStart;
|
||||
$y = $yStart;
|
||||
|
||||
$ptToMm = static function (float $pt): float {
|
||||
return $pt * 0.352778;
|
||||
};
|
||||
|
||||
$i = 0;
|
||||
$perPage = $capacity;
|
||||
|
||||
foreach ($printList as $student) {
|
||||
if ($i > 0 && $i % $perPage === 0) {
|
||||
$pdf->AddPage();
|
||||
$x = $xStart;
|
||||
$y = $yStart;
|
||||
}
|
||||
|
||||
$pdf->SetFillColor(255, 255, 255);
|
||||
$pdf->Rect($x, $y, $layout['sticker_width'], $layout['sticker_height'], 'F');
|
||||
|
||||
$fontFamily = 'Arial';
|
||||
$fontStyle = 'B';
|
||||
$maxPt = 16;
|
||||
$minPt = 8;
|
||||
$hPad = 2;
|
||||
|
||||
$fullName = trim(($student['firstname'] ?? '') . ' ' . ($student['lastname'] ?? '')) ?: 'Student';
|
||||
|
||||
$bestPt = $maxPt;
|
||||
$textW = 0.0;
|
||||
while ($bestPt >= $minPt) {
|
||||
$pdf->SetFont($fontFamily, $fontStyle, $bestPt);
|
||||
$textW = (float) $pdf->GetStringWidth($fullName);
|
||||
if ($textW <= ($layout['sticker_width'] - 2 * $hPad)) {
|
||||
break;
|
||||
}
|
||||
$bestPt -= 0.5;
|
||||
}
|
||||
if ($bestPt < $minPt) {
|
||||
$bestPt = $minPt;
|
||||
$pdf->SetFont($fontFamily, $fontStyle, $bestPt);
|
||||
$textW = (float) $pdf->GetStringWidth($fullName);
|
||||
}
|
||||
$textH = $ptToMm($bestPt);
|
||||
|
||||
$cellX = $x + (($layout['sticker_width'] - $textW) / 2);
|
||||
$cellY = $y + (($layout['sticker_height'] - $textH) / 2);
|
||||
|
||||
$pdf->SetXY($cellX, $cellY);
|
||||
$pdf->Cell($textW, $textH, $fullName, 0, 0, 'L');
|
||||
|
||||
$i++;
|
||||
$posInRow = ($i - 1) % $cols;
|
||||
if ($posInRow === ($cols - 1)) {
|
||||
$x = $xStart;
|
||||
$y += $layout['sticker_height'] + $layout['gap_y'];
|
||||
} else {
|
||||
$x += $layout['sticker_width'] + $layout['gap_x'];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'pdf' => $pdf->Output('S'),
|
||||
'filename' => 'Student_Stickers.pdf',
|
||||
];
|
||||
}
|
||||
|
||||
private function expandCopies(array $students, array $payload, bool $singleStudentMode): array
|
||||
{
|
||||
$copiesPosted = $payload['copies'] ?? null;
|
||||
$singleCopies = (int) ($payload['single_copies'] ?? 1);
|
||||
if ($singleCopies < 0) {
|
||||
$singleCopies = 0;
|
||||
}
|
||||
|
||||
$printList = [];
|
||||
if ($singleStudentMode) {
|
||||
$qty = max(0, $singleCopies);
|
||||
for ($k = 0; $k < $qty; $k++) {
|
||||
$printList[] = $students[0];
|
||||
}
|
||||
return $printList;
|
||||
}
|
||||
|
||||
$hasCopies = is_array($copiesPosted);
|
||||
$defaultQty = $hasCopies ? 0 : 1;
|
||||
foreach ($students as $student) {
|
||||
$id = (int) ($student['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[] = $student;
|
||||
}
|
||||
}
|
||||
|
||||
return $printList;
|
||||
}
|
||||
|
||||
private function buildLayout(array $payload): array
|
||||
{
|
||||
$sizeStrReq = trim((string) ($payload['sticker_size'] ?? ''));
|
||||
$stickerWidth = (float) ($payload['sticker_width'] ?? 0);
|
||||
$stickerHeight = (float) ($payload['sticker_height'] ?? 0);
|
||||
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
||||
if ($stickerWidth <= 0) {
|
||||
$stickerWidth = 102.0;
|
||||
}
|
||||
if ($stickerHeight <= 0) {
|
||||
$stickerHeight = 34.0;
|
||||
}
|
||||
|
||||
$stickersPerPage = (int) ($payload['stickers_per_page'] ?? 0);
|
||||
$pageSize = (string) ($payload['page_size'] ?? 'A4');
|
||||
$orientation = (string) ($payload['orientation'] ?? 'P');
|
||||
|
||||
$marginLeftProvided = array_key_exists('margin_left', $payload);
|
||||
$marginTopProvided = array_key_exists('margin_top', $payload);
|
||||
$marginRightProvided = array_key_exists('margin_right', $payload);
|
||||
$marginBottomProvided = array_key_exists('margin_bottom', $payload);
|
||||
$gapXProvided = array_key_exists('gap_x', $payload);
|
||||
$gapYProvided = array_key_exists('gap_y', $payload);
|
||||
|
||||
$marginLeft = (float) ($payload['margin_left'] ?? 6);
|
||||
$marginTop = (float) ($payload['margin_top'] ?? 6);
|
||||
$marginRight = (float) ($payload['margin_right'] ?? $marginLeft);
|
||||
$marginBottom = (float) ($payload['margin_bottom'] ?? 10);
|
||||
$gapX = (float) ($payload['gap_x'] ?? 0);
|
||||
$gapY = (float) ($payload['gap_y'] ?? 0);
|
||||
|
||||
$normalizedSize = strtolower(str_replace(["\x{00D7}", ' '], ['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;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'sticker_width' => $stickerWidth,
|
||||
'sticker_height' => $stickerHeight,
|
||||
'stickers_per_page' => $stickersPerPage,
|
||||
'page_size' => $pageSize,
|
||||
'orientation' => $orientation,
|
||||
'margin_left' => $marginLeft,
|
||||
'margin_top' => $marginTop,
|
||||
'margin_right' => $marginRight,
|
||||
'margin_bottom' => $marginBottom,
|
||||
'gap_x' => $gapX,
|
||||
'gap_y' => $gapY,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Reports\Stickers;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class StickerQueryService
|
||||
{
|
||||
public function __construct(private StickerContextService $context)
|
||||
{
|
||||
}
|
||||
|
||||
public function resolveSchoolYear(?string $schoolYear): string
|
||||
{
|
||||
return $this->context->schoolYear($schoolYear);
|
||||
}
|
||||
|
||||
public function listClassesWithStudents(string $schoolYear): array
|
||||
{
|
||||
return DB::table('classSection')
|
||||
->select('classSection.class_section_id', 'classSection.class_section_name')
|
||||
->join('student_class', 'student_class.class_section_id', '=', 'classSection.class_section_id')
|
||||
->where('student_class.school_year', $schoolYear)
|
||||
->groupBy('classSection.class_section_id', 'classSection.class_section_name')
|
||||
->orderBy('classSection.class_section_name', 'ASC')
|
||||
->get()
|
||||
->map(fn ($row) => (array) $row)
|
||||
->all();
|
||||
}
|
||||
|
||||
public function listStudentsByClass(int $classSectionId, string $schoolYear): array
|
||||
{
|
||||
return DB::table('student_class as sc')
|
||||
->select('s.id', 's.firstname', 's.lastname', 's.registration_grade', 's.gender')
|
||||
->join('students as s', 's.id', '=', 'sc.student_id')
|
||||
->where('sc.class_section_id', $classSectionId)
|
||||
->where('sc.school_year', $schoolYear)
|
||||
->orderBy('s.lastname', 'asc')
|
||||
->get()
|
||||
->map(fn ($row) => (array) $row)
|
||||
->all();
|
||||
}
|
||||
|
||||
public function listStudentsForYear(string $schoolYear, int $limit = 500): array
|
||||
{
|
||||
return DB::table('students')
|
||||
->select('students.id', 'students.firstname', 'students.lastname')
|
||||
->join('student_class', 'student_class.student_id', '=', 'students.id')
|
||||
->where('student_class.school_year', $schoolYear)
|
||||
->groupBy('students.id', 'students.firstname', 'students.lastname')
|
||||
->orderBy('students.firstname', 'ASC')
|
||||
->orderBy('students.lastname', 'ASC')
|
||||
->limit($limit)
|
||||
->get()
|
||||
->map(fn ($row) => (array) $row)
|
||||
->all();
|
||||
}
|
||||
|
||||
public function listStudentsForPrintAll(string $schoolYear, int $limit = 5000): array
|
||||
{
|
||||
$rows = DB::table('students')
|
||||
->select(
|
||||
'students.id',
|
||||
'students.firstname',
|
||||
'students.lastname',
|
||||
'sc.class_section_id',
|
||||
'cs.class_section_name',
|
||||
'c.id as class_id'
|
||||
)
|
||||
->join('student_class as sc', 'sc.student_id', '=', 'students.id')
|
||||
->join('classSection as cs', 'cs.class_section_id', '=', 'sc.class_section_id')
|
||||
->join('classes as c', 'c.id', '=', 'cs.class_id')
|
||||
->where('sc.school_year', $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')
|
||||
->limit($limit)
|
||||
->get()
|
||||
->map(fn ($row) => (array) $row)
|
||||
->all();
|
||||
|
||||
return $this->filterExcludedSections($rows);
|
||||
}
|
||||
|
||||
public function findStudent(int $studentId): ?array
|
||||
{
|
||||
$row = DB::table('students')
|
||||
->select('id', 'firstname', 'lastname')
|
||||
->where('id', $studentId)
|
||||
->first();
|
||||
|
||||
return $row ? (array) $row : null;
|
||||
}
|
||||
|
||||
private function filterExcludedSections(array $students): array
|
||||
{
|
||||
return array_values(array_filter($students, static function (array $student): bool {
|
||||
$sectionName = trim((string) ($student['class_section_name'] ?? ''));
|
||||
if ($sectionName === '') {
|
||||
return false;
|
||||
}
|
||||
return !preg_match('/^(youth|kg)(?:\b|-)/i', $sectionName);
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -1,390 +0,0 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,7 @@ use App\Http\Controllers\Api\Settings\EventController;
|
||||
use App\Http\Controllers\Api\Family\FamilyAdminController;
|
||||
use App\Http\Controllers\Api\Family\FamilyController;
|
||||
use App\Http\Controllers\Api\Reports\FilesController;
|
||||
use App\Http\Controllers\Api\Reports\StickersController;
|
||||
use App\Http\Controllers\Api\Scores\FinalController;
|
||||
use App\Http\Controllers\Api\Finance\FinancialController;
|
||||
use App\Http\Controllers\Api\System\FlagController;
|
||||
@@ -397,6 +398,11 @@ Route::prefix('v1')->group(function () {
|
||||
Route::get('logs', [SlipPrinterController::class, 'logs']);
|
||||
Route::post('reprint', [SlipPrinterController::class, 'reprint']);
|
||||
});
|
||||
Route::prefix('reports/stickers')->group(function () {
|
||||
Route::get('form-data', [StickersController::class, 'formData']);
|
||||
Route::get('students', [StickersController::class, 'studentsByClass']);
|
||||
Route::post('print', [StickersController::class, 'print']);
|
||||
});
|
||||
|
||||
Route::prefix('subjects/curriculum')->group(function () {
|
||||
Route::get('/', [SubjectCurriculumApiController::class, 'index']);
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Reports;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\TestCase;
|
||||
|
||||
class StickersControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_form_data_returns_classes_students_and_presets(): void
|
||||
{
|
||||
$this->seedConfig();
|
||||
$user = $this->seedUser();
|
||||
$this->seedClassAndStudent();
|
||||
|
||||
Sanctum::actingAs($user);
|
||||
$response = $this->getJson('/api/v1/reports/stickers/form-data?school_year=2025-2026');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJsonPath('status', true);
|
||||
$this->assertNotEmpty($response->json('data.classes'));
|
||||
$this->assertNotEmpty($response->json('data.students'));
|
||||
$this->assertNotEmpty($response->json('data.presets'));
|
||||
$this->assertArrayHasKey('value', $response->json('data.presets.0'));
|
||||
}
|
||||
|
||||
public function test_students_by_class_returns_students(): void
|
||||
{
|
||||
$this->seedConfig();
|
||||
$user = $this->seedUser();
|
||||
$this->seedClassAndStudent();
|
||||
|
||||
Sanctum::actingAs($user);
|
||||
$response = $this->getJson('/api/v1/reports/stickers/students?class_section_id=200&school_year=2025-2026');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJsonPath('status', true);
|
||||
$this->assertNotEmpty($response->json('data.students'));
|
||||
}
|
||||
|
||||
public function test_print_returns_pdf(): void
|
||||
{
|
||||
$this->seedConfig();
|
||||
$user = $this->seedUser();
|
||||
$this->seedClassAndStudent();
|
||||
|
||||
Sanctum::actingAs($user);
|
||||
$response = $this->postJson('/api/v1/reports/stickers/print', [
|
||||
'student_id' => 100,
|
||||
'school_year' => '2025-2026',
|
||||
'sticker_size' => '102x34',
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
$this->assertSame('application/pdf', $response->headers->get('content-type'));
|
||||
}
|
||||
|
||||
public function test_print_validation_rejects_missing_selection(): void
|
||||
{
|
||||
$this->seedConfig();
|
||||
$user = $this->seedUser();
|
||||
|
||||
Sanctum::actingAs($user);
|
||||
$response = $this->postJson('/api/v1/reports/stickers/print', [
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
$response->assertStatus(422);
|
||||
$response->assertJsonPath('message', 'Validation failed.');
|
||||
}
|
||||
|
||||
public function test_requires_authentication(): void
|
||||
{
|
||||
$response = $this->getJson('/api/v1/reports/stickers/form-data');
|
||||
|
||||
$response->assertStatus(401);
|
||||
}
|
||||
|
||||
private function seedConfig(): void
|
||||
{
|
||||
DB::table('configuration')->insert([
|
||||
['config_key' => 'school_year', 'config_value' => '2025-2026'],
|
||||
['config_key' => 'semester', 'config_value' => 'Fall'],
|
||||
]);
|
||||
}
|
||||
|
||||
private function seedUser(): User
|
||||
{
|
||||
$userId = DB::table('users')->insertGetId([
|
||||
'firstname' => 'Admin',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '9999999999',
|
||||
'email' => 'stickers@example.com',
|
||||
'address_street' => '123 Street',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'password' => bcrypt('password'),
|
||||
'user_type' => 'primary',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'status' => 'Active',
|
||||
]);
|
||||
|
||||
return User::query()->findOrFail($userId);
|
||||
}
|
||||
|
||||
private function seedClassAndStudent(): void
|
||||
{
|
||||
DB::table('classes')->insert([
|
||||
'id' => 1,
|
||||
'class_name' => 'Grade 1',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
DB::table('classSection')->insert([
|
||||
'class_section_id' => 200,
|
||||
'class_section_name' => 'Grade 1',
|
||||
'class_id' => 1,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
DB::table('students')->insert([
|
||||
'id' => 100,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Student',
|
||||
'lastname' => 'One',
|
||||
'parent_id' => 10,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'is_active' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
DB::table('student_class')->insert([
|
||||
'student_id' => 100,
|
||||
'class_section_id' => 200,
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
'is_event_only' => 0,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\Reports\Stickers;
|
||||
|
||||
use App\Services\Reports\Stickers\StickerPresetService;
|
||||
use Tests\TestCase;
|
||||
|
||||
class StickerPresetServiceTest extends TestCase
|
||||
{
|
||||
public function test_presets_include_alias_fields(): void
|
||||
{
|
||||
$service = new StickerPresetService();
|
||||
$presets = $service->presets();
|
||||
|
||||
$this->assertNotEmpty($presets);
|
||||
$this->assertArrayHasKey('title', $presets[0]);
|
||||
$this->assertArrayHasKey('text', $presets[0]);
|
||||
$this->assertArrayHasKey('per_page', $presets[0]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\Reports\Stickers;
|
||||
|
||||
use App\Services\Reports\Stickers\StickerContextService;
|
||||
use App\Services\Reports\Stickers\StickerPrintService;
|
||||
use App\Services\Reports\Stickers\StickerQueryService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
class StickerPrintServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_generate_returns_pdf_content(): void
|
||||
{
|
||||
$this->seedData();
|
||||
$service = new StickerPrintService(new StickerQueryService(new StickerContextService()));
|
||||
|
||||
$result = $service->generate([
|
||||
'student_id' => 100,
|
||||
'school_year' => '2025-2026',
|
||||
'sticker_size' => '102x34',
|
||||
]);
|
||||
|
||||
$this->assertTrue($result['ok']);
|
||||
$this->assertNotEmpty($result['pdf']);
|
||||
}
|
||||
|
||||
public function test_generate_returns_error_when_no_students(): void
|
||||
{
|
||||
$service = new StickerPrintService(new StickerQueryService(new StickerContextService()));
|
||||
|
||||
$result = $service->generate([
|
||||
'class_section_id' => 999,
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
$this->assertSame('No students found in this class.', $result['message']);
|
||||
}
|
||||
|
||||
private function seedData(): void
|
||||
{
|
||||
DB::table('classes')->insert([
|
||||
['id' => 1, 'class_name' => 'Grade 1', 'created_at' => now(), 'updated_at' => now()],
|
||||
]);
|
||||
|
||||
DB::table('classSection')->insert([
|
||||
'class_section_id' => 200,
|
||||
'class_section_name' => 'Grade 1',
|
||||
'class_id' => 1,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
DB::table('students')->insert([
|
||||
'id' => 100,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Student',
|
||||
'lastname' => 'One',
|
||||
'parent_id' => 10,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'is_active' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
DB::table('student_class')->insert([
|
||||
'student_id' => 100,
|
||||
'class_section_id' => 200,
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
'is_event_only' => 0,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\Reports\Stickers;
|
||||
|
||||
use App\Services\Reports\Stickers\StickerContextService;
|
||||
use App\Services\Reports\Stickers\StickerQueryService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
class StickerQueryServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_list_students_by_class_returns_rows(): void
|
||||
{
|
||||
$this->seedData();
|
||||
$service = new StickerQueryService(new StickerContextService());
|
||||
|
||||
$students = $service->listStudentsByClass(200, '2025-2026');
|
||||
|
||||
$this->assertCount(1, $students);
|
||||
$this->assertSame(100, $students[0]['id']);
|
||||
}
|
||||
|
||||
public function test_list_students_for_print_all_excludes_youth_and_kg(): void
|
||||
{
|
||||
$this->seedData();
|
||||
$this->seedYouthClass();
|
||||
|
||||
$service = new StickerQueryService(new StickerContextService());
|
||||
$students = $service->listStudentsForPrintAll('2025-2026');
|
||||
|
||||
$this->assertCount(1, $students);
|
||||
$this->assertSame('Grade 1', $students[0]['class_section_name']);
|
||||
}
|
||||
|
||||
private function seedData(): void
|
||||
{
|
||||
DB::table('classes')->insert([
|
||||
['id' => 1, 'class_name' => 'Grade 1', 'created_at' => now(), 'updated_at' => now()],
|
||||
]);
|
||||
|
||||
DB::table('classSection')->insert([
|
||||
'class_section_id' => 200,
|
||||
'class_section_name' => 'Grade 1',
|
||||
'class_id' => 1,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
DB::table('students')->insert([
|
||||
'id' => 100,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Student',
|
||||
'lastname' => 'One',
|
||||
'parent_id' => 10,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'is_active' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
DB::table('student_class')->insert([
|
||||
'student_id' => 100,
|
||||
'class_section_id' => 200,
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
'is_event_only' => 0,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function seedYouthClass(): void
|
||||
{
|
||||
DB::table('classes')->insert([
|
||||
['id' => 2, 'class_name' => 'Youth', 'created_at' => now(), 'updated_at' => now()],
|
||||
]);
|
||||
|
||||
DB::table('classSection')->insert([
|
||||
'class_section_id' => 201,
|
||||
'class_section_name' => 'Youth',
|
||||
'class_id' => 2,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
DB::table('students')->insert([
|
||||
'id' => 101,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Youth',
|
||||
'lastname' => 'Student',
|
||||
'parent_id' => 11,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'is_active' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
DB::table('student_class')->insert([
|
||||
'student_id' => 101,
|
||||
'class_section_id' => 201,
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
'is_event_only' => 0,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user