From 2e9a3912805309a2f7081e80cb7094f33d071511 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 10 Mar 2026 16:40:11 -0400 Subject: [PATCH] add stickers logic --- .../Api/Reports/StickersController.php | 80 ++++ .../Stickers/StickerFormDataRequest.php | 30 ++ .../Reports/Stickers/StickerPrintRequest.php | 61 +++ .../StickerStudentsByClassRequest.php | 30 ++ .../Stickers/StickerClassSectionResource.php | 16 + .../Stickers/StickerPresetResource.php | 19 + .../Stickers/StickerStudentResource.php | 19 + .../Stickers/StickerContextService.php | 18 + .../Reports/Stickers/StickerPresetService.php | 23 ++ .../Reports/Stickers/StickerPrintService.php | 255 ++++++++++++ .../Reports/Stickers/StickerQueryService.php | 114 +++++ app/old/StickersController.php | 390 ------------------ routes/api.php | 6 + .../Api/V1/Reports/StickersControllerTest.php | 156 +++++++ .../Stickers/StickerPresetServiceTest.php | 20 + .../Stickers/StickerPrintServiceTest.php | 83 ++++ .../Stickers/StickerQueryServiceTest.php | 117 ++++++ 17 files changed, 1047 insertions(+), 390 deletions(-) create mode 100644 app/Http/Controllers/Api/Reports/StickersController.php create mode 100644 app/Http/Requests/Reports/Stickers/StickerFormDataRequest.php create mode 100644 app/Http/Requests/Reports/Stickers/StickerPrintRequest.php create mode 100644 app/Http/Requests/Reports/Stickers/StickerStudentsByClassRequest.php create mode 100644 app/Http/Resources/Reports/Stickers/StickerClassSectionResource.php create mode 100644 app/Http/Resources/Reports/Stickers/StickerPresetResource.php create mode 100644 app/Http/Resources/Reports/Stickers/StickerStudentResource.php create mode 100644 app/Services/Reports/Stickers/StickerContextService.php create mode 100644 app/Services/Reports/Stickers/StickerPresetService.php create mode 100644 app/Services/Reports/Stickers/StickerPrintService.php create mode 100644 app/Services/Reports/Stickers/StickerQueryService.php delete mode 100644 app/old/StickersController.php create mode 100644 tests/Feature/Api/V1/Reports/StickersControllerTest.php create mode 100644 tests/Unit/Services/Reports/Stickers/StickerPresetServiceTest.php create mode 100644 tests/Unit/Services/Reports/Stickers/StickerPrintServiceTest.php create mode 100644 tests/Unit/Services/Reports/Stickers/StickerQueryServiceTest.php diff --git a/app/Http/Controllers/Api/Reports/StickersController.php b/app/Http/Controllers/Api/Reports/StickersController.php new file mode 100644 index 00000000..003da195 --- /dev/null +++ b/app/Http/Controllers/Api/Reports/StickersController.php @@ -0,0 +1,80 @@ +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') . '"', + ]); + } +} diff --git a/app/Http/Requests/Reports/Stickers/StickerFormDataRequest.php b/app/Http/Requests/Reports/Stickers/StickerFormDataRequest.php new file mode 100644 index 00000000..9644faf2 --- /dev/null +++ b/app/Http/Requests/Reports/Stickers/StickerFormDataRequest.php @@ -0,0 +1,30 @@ +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'], + ]; + } +} diff --git a/app/Http/Requests/Reports/Stickers/StickerPrintRequest.php b/app/Http/Requests/Reports/Stickers/StickerPrintRequest.php new file mode 100644 index 00000000..651ec973 --- /dev/null +++ b/app/Http/Requests/Reports/Stickers/StickerPrintRequest.php @@ -0,0 +1,61 @@ +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.'); + } + }); + } +} diff --git a/app/Http/Requests/Reports/Stickers/StickerStudentsByClassRequest.php b/app/Http/Requests/Reports/Stickers/StickerStudentsByClassRequest.php new file mode 100644 index 00000000..089ff9a0 --- /dev/null +++ b/app/Http/Requests/Reports/Stickers/StickerStudentsByClassRequest.php @@ -0,0 +1,30 @@ +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'], + ]; + } +} diff --git a/app/Http/Resources/Reports/Stickers/StickerClassSectionResource.php b/app/Http/Resources/Reports/Stickers/StickerClassSectionResource.php new file mode 100644 index 00000000..862788f2 --- /dev/null +++ b/app/Http/Resources/Reports/Stickers/StickerClassSectionResource.php @@ -0,0 +1,16 @@ + (int) ($this['class_section_id'] ?? 0), + 'class_section_name' => (string) ($this['class_section_name'] ?? ''), + ]; + } +} diff --git a/app/Http/Resources/Reports/Stickers/StickerPresetResource.php b/app/Http/Resources/Reports/Stickers/StickerPresetResource.php new file mode 100644 index 00000000..9b1dfed9 --- /dev/null +++ b/app/Http/Resources/Reports/Stickers/StickerPresetResource.php @@ -0,0 +1,19 @@ + (string) ($this['value'] ?? ''), + 'label' => (string) ($this['label'] ?? ''), + 'title' => (string) ($this['title'] ?? ''), + 'text' => (string) ($this['text'] ?? ''), + 'per_page' => $this['per_page'] ?? null, + ]; + } +} diff --git a/app/Http/Resources/Reports/Stickers/StickerStudentResource.php b/app/Http/Resources/Reports/Stickers/StickerStudentResource.php new file mode 100644 index 00000000..f745cd5a --- /dev/null +++ b/app/Http/Resources/Reports/Stickers/StickerStudentResource.php @@ -0,0 +1,19 @@ + (int) ($this['id'] ?? 0), + 'firstname' => (string) ($this['firstname'] ?? ''), + 'lastname' => (string) ($this['lastname'] ?? ''), + 'registration_grade' => $this['registration_grade'] ?? null, + 'gender' => $this['gender'] ?? null, + ]; + } +} diff --git a/app/Services/Reports/Stickers/StickerContextService.php b/app/Services/Reports/Stickers/StickerContextService.php new file mode 100644 index 00000000..a10ae983 --- /dev/null +++ b/app/Services/Reports/Stickers/StickerContextService.php @@ -0,0 +1,18 @@ + '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); + } +} diff --git a/app/Services/Reports/Stickers/StickerPrintService.php b/app/Services/Reports/Stickers/StickerPrintService.php new file mode 100644 index 00000000..f4b0ebf9 --- /dev/null +++ b/app/Services/Reports/Stickers/StickerPrintService.php @@ -0,0 +1,255 @@ +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, + ]; + } +} diff --git a/app/Services/Reports/Stickers/StickerQueryService.php b/app/Services/Reports/Stickers/StickerQueryService.php new file mode 100644 index 00000000..a363bacc --- /dev/null +++ b/app/Services/Reports/Stickers/StickerQueryService.php @@ -0,0 +1,114 @@ +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); + })); + } +} diff --git a/app/old/StickersController.php b/app/old/StickersController.php deleted file mode 100644 index 03f350c1..00000000 --- a/app/old/StickersController.php +++ /dev/null @@ -1,390 +0,0 @@ -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); - } -} diff --git a/routes/api.php b/routes/api.php index 8fe03b40..a25e2352 100755 --- a/routes/api.php +++ b/routes/api.php @@ -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']); diff --git a/tests/Feature/Api/V1/Reports/StickersControllerTest.php b/tests/Feature/Api/V1/Reports/StickersControllerTest.php new file mode 100644 index 00000000..f041bfca --- /dev/null +++ b/tests/Feature/Api/V1/Reports/StickersControllerTest.php @@ -0,0 +1,156 @@ +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(), + ]); + } +} diff --git a/tests/Unit/Services/Reports/Stickers/StickerPresetServiceTest.php b/tests/Unit/Services/Reports/Stickers/StickerPresetServiceTest.php new file mode 100644 index 00000000..f1aeaa7c --- /dev/null +++ b/tests/Unit/Services/Reports/Stickers/StickerPresetServiceTest.php @@ -0,0 +1,20 @@ +presets(); + + $this->assertNotEmpty($presets); + $this->assertArrayHasKey('title', $presets[0]); + $this->assertArrayHasKey('text', $presets[0]); + $this->assertArrayHasKey('per_page', $presets[0]); + } +} diff --git a/tests/Unit/Services/Reports/Stickers/StickerPrintServiceTest.php b/tests/Unit/Services/Reports/Stickers/StickerPrintServiceTest.php new file mode 100644 index 00000000..ac3acb0b --- /dev/null +++ b/tests/Unit/Services/Reports/Stickers/StickerPrintServiceTest.php @@ -0,0 +1,83 @@ +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(), + ]); + } +} diff --git a/tests/Unit/Services/Reports/Stickers/StickerQueryServiceTest.php b/tests/Unit/Services/Reports/Stickers/StickerQueryServiceTest.php new file mode 100644 index 00000000..95c949fb --- /dev/null +++ b/tests/Unit/Services/Reports/Stickers/StickerQueryServiceTest.php @@ -0,0 +1,117 @@ +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(), + ]); + } +}