student = model(Student::class); $this->classSection = model(ClassSection::class); $this->config = model(Configuration::class); $this->badgePrintLog = model(BadgePrintLog::class); $this->semesterScore = model(SemesterScore::class); $this->studentClass = model(StudentClass::class); $this->teacherClass = model(TeacherClass::class); $this->user = model(User::class); $this->schoolYear = (string) ($this->config->getConfig('school_year') ?? ''); $this->semester = (string) ($this->config->getConfig('semester') ?? ''); } /** * GET /api/v1/printables/report-card-meta * Get report card metadata for UI hydration */ public function reportCardMeta(): JsonResponse { try { $year = trim((string) ($this->request->getGet('school_year') ?? $this->schoolYear ?? '')); $sem = trim((string) ($this->request->getGet('semester') ?? $this->semester ?? '')); if ($year === '') { // Fallback to latest present in classSection or semester_score $yr = DB::table('classSection') ->select('school_year') ->orderBy('school_year', 'DESC') ->first(); $year = (string) ($yr->school_year ?? ''); } // Build schoolYears from classSection and semester_scores $schoolYears = []; try { $q1 = DB::table('classSection') ->select(DB::raw('DISTINCT school_year')) ->whereNotNull('school_year') ->orderBy('school_year', 'DESC') ->pluck('school_year') ->map(fn($r) => (string) $r) ->filter() ->values() ->toArray(); $schoolYears = $q1; } catch (\Throwable $e) { // ignore } try { $q2 = DB::table('semester_scores') ->select(DB::raw('DISTINCT school_year')) ->whereNotNull('school_year') ->orderBy('school_year', 'DESC') ->pluck('school_year') ->map(fn($r) => (string) $r) ->filter() ->toArray(); foreach ($q2 as $val) { if ($val !== '' && !in_array($val, $schoolYears, true)) { $schoolYears[] = $val; } } } catch (\Throwable $e) { // ignore } rsort($schoolYears); // Semesters available for the selected year $semesters = []; try { $rs = DB::table('semester_scores') ->select(DB::raw('DISTINCT semester')) ->where('school_year', $year) ->whereNotNull('semester') ->orderBy('semester', 'ASC') ->pluck('semester') ->map(fn($r) => (string) $r) ->filter() ->values() ->toArray(); $semesters = $rs; } catch (\Throwable $e) { // ignore } if (empty($semesters)) { // Provide a sensible default list $defaults = array_filter([(string) ($this->semester ?? ''), 'Fall', 'Spring'], fn($v) => $v !== ''); $semesters = array_values(array_unique($defaults)); } if ($sem === '' && !empty($semesters)) { $sem = $semesters[0]; } // Students: prefer those with scores in selected year (+semester if provided); fallback to all $students = []; try { $stuRows = DB::table('semester_scores ss') ->select(DB::raw('DISTINCT s.id, s.firstname, s.lastname')) ->join('students s', 's.id', '=', 'ss.student_id') ->where('ss.school_year', $year) ->where(function ($query) use ($sem) { if ($sem !== '') { $query->where('ss.semester', $sem); } }) ->orderBy('s.lastname', 'ASC') ->get() ->toArray(); if (!empty($stuRows)) { $students = array_map(fn($r) => [ 'id' => (int) $r->id, 'firstname' => (string) ($r->firstname ?? ''), 'lastname' => (string) ($r->lastname ?? ''), ], $stuRows); } } catch (\Throwable $e) { // ignore } if (empty($students)) { $allStudents = $this->student ->select('id', 'firstname', 'lastname') ->orderBy('lastname', 'ASC') ->findAll(); $students = array_map(fn($s) => [ 'id' => (int) ($s['id'] ?? 0), 'firstname' => (string) ($s['firstname'] ?? ''), 'lastname' => (string) ($s['lastname'] ?? ''), ], $allStudents); } // Class sections for selected year (fallback to all) $classSections = $this->classSection ->select('id', 'class_section_id', 'class_section_name', 'school_year') ->where('school_year', $year) ->orderBy('class_section_name', 'ASC') ->findAll(); if (empty($classSections)) { $classSections = $this->classSection ->select('id', 'class_section_id', 'class_section_name', 'school_year') ->orderBy('class_section_name', 'ASC') ->findAll(); } $classSectionsFormatted = array_map(static fn($r) => [ 'id' => (int) ($r['id'] ?? 0), 'class_section_id' => (int) ($r['class_section_id'] ?? ($r['id'] ?? 0)), 'class_section_name' => (string) ($r['class_section_name'] ?? 'Section'), 'school_year' => (string) ($r['school_year'] ?? ''), ], $classSections); return $this->success([ 'ok' => true, 'schoolYears' => $schoolYears, 'selectedYear' => $year, 'semesters' => $semesters, 'selectedSemester' => $sem, 'students' => $students, 'classSections' => $classSectionsFormatted, 'csrf_token' => csrf_token(), 'csrf_hash' => csrf_hash(), ], 'Report card metadata retrieved successfully'); } catch (\Throwable $e) { Log::error('Report card meta error: ' . $e->getMessage()); return $this->respondError('Failed to retrieve report card metadata', Response::HTTP_INTERNAL_SERVER_ERROR); } } /** * GET /api/v1/printables/badge-status * Get badge print status for given users */ public function badgePrintStatus(): JsonResponse { $ids = $this->request->getGet('user_ids'); if (is_string($ids)) { $ids = array_filter(array_map('trim', explode(',', $ids)), 'strlen'); } if (!is_array($ids)) { $ids = $ids ? [$ids] : []; } $ids = array_values(array_unique(array_map(static fn($v) => (int) $v, $ids))); $schoolYear = $this->request->getGet('school_year') ?? $this->schoolYear; try { $status = $this->badgePrintLog->getStatus($ids, is_string($schoolYear) ? $schoolYear : null); } catch (\Throwable $e) { Log::error('Badge print status error: ' . $e->getMessage()); return $this->error('Failed to query status', Response::HTTP_INTERNAL_SERVER_ERROR); } return $this->success([ 'ok' => true, 'data' => $status, 'csrf_token' => csrf_token(), 'csrf_hash' => csrf_hash(), ], 'Badge print status retrieved successfully'); } /** * POST /api/v1/printables/badge-status * Log a set of badge prints explicitly */ public function logBadgePrint(): JsonResponse { $data = $this->payloadData(); if (empty($data)) { return $this->error('Invalid request data', Response::HTTP_BAD_REQUEST); } $ids = $data['user_ids'] ?? []; if (!is_array($ids)) { $ids = $ids ? [$ids] : []; } $ids = array_values(array_unique(array_map(static fn($v) => (int) $v, $ids))); $roles = $data['roles'] ?? []; $class = $data['classes'] ?? []; if (!is_array($roles)) { $roles = []; } if (!is_array($class)) { $class = []; } $schoolYear = $data['school_year'] ?? $this->schoolYear; $actorId = $this->getCurrentUserId(); try { $cnt = $this->badgePrintLog->logPrints( $ids, $actorId, is_string($schoolYear) ? $schoolYear : null, $roles, $class, 1 ); return $this->success([ 'ok' => true, 'inserted' => $cnt, 'csrf_token' => csrf_token(), 'csrf_hash' => csrf_hash(), ], 'Badge prints logged successfully', Response::HTTP_CREATED); } catch (\Throwable $e) { Log::error('Log badge print error: ' . $e->getMessage()); return $this->error('Failed to log badge prints: ' . $e->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR); } } /** * GET /api/v1/printables/students-by-class/{classSectionId} * Get students by class section */ public function getByClass($classSectionId): JsonResponse { try { $students = 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', (int) $classSectionId) ->where('sc.school_year', $this->schoolYear) ->orderBy('s.lastname', 'asc') ->get() ->toArray(); return $this->success($students, 'Students retrieved successfully'); } catch (\Throwable $e) { Log::error('Get students by class error: ' . $e->getMessage()); return $this->respondError('Failed to retrieve students', Response::HTTP_INTERNAL_SERVER_ERROR); } } /** * GET /api/v1/printables/students-by-class/{classSectionId} * Alternative endpoint name for students by class */ public function studentsByClass($classSectionId): JsonResponse { try { $rows = $this->student ->select('students.id', 'students.firstname', 'students.lastname', 'students.gender', 'cs.class_section_name AS registration_grade') ->join('student_class sc', 'sc.student_id', '=', 'students.id', 'inner') ->join('classSection cs', 'cs.class_section_id', '=', 'sc.class_section_id', 'inner') ->where('sc.school_year', $this->schoolYear) ->where('sc.class_section_id', (int) $classSectionId) ->groupBy('students.id', 'students.firstname', 'students.lastname', 'students.gender', 'cs.class_section_name') ->orderBy('students.firstname', 'ASC') ->orderBy('students.lastname', 'ASC') ->findAll(1000); return $this->success($rows, 'Students retrieved successfully'); } catch (\Throwable $e) { Log::error('Students by class error: ' . $e->getMessage()); return $this->respondError('Failed to retrieve students', Response::HTTP_INTERNAL_SERVER_ERROR); } } /** * GET /api/v1/printables/preview-sticker-counts * Preview sticker counts per student */ public function previewStickerCounts(): JsonResponse { try { $classId = (int) ($this->request->getGet('class_id') ?? 0); if ($classId > 0) { $result = $this->getStickerCountsPerStudentForClass($this->schoolYear, $classId, $this->semester); } else { $result = $this->getStickerCountsPerStudent($this->schoolYear, $this->semester); } return $this->success([ 'status' => 'ok', 'data' => $result, ], 'Sticker counts retrieved successfully'); } catch (\Throwable $e) { Log::error('Preview sticker counts error: ' . $e->getMessage()); return $this->respondError('Failed to retrieve sticker counts', Response::HTTP_INTERNAL_SERVER_ERROR); } } /** * Compute sticker counts per student for entire school year */ protected function getStickerCountsPerStudent(string $schoolYear, ?string $semester = null): array { $specialMap = [ 'KG' => 1, '1-A' => 3, '1-B' => 4, '2-A' => 5, '2-B' => 5, '9' => 2, ]; $isUpperBlock = static function (string $g): bool { return (bool) preg_match('/^(3|4|5|6|7|8)(-[AB])?$/', trim($g)); }; $isGrade5 = static function (string $g): bool { return (bool) preg_match('/^5(\-|$)/', trim($g)); }; $builder = DB::table('student_class sc') ->select([ 'sc.student_id', 's.firstname', 's.lastname', 's.is_new', 'cs.class_section_name AS grade_label', ]) ->join('students s', 's.id', '=', 'sc.student_id') ->join('classSection cs', 'cs.class_section_id', '=', 'sc.class_section_id') ->join('classes c', 'c.id', '=', 'cs.class_id') ->where('sc.school_year', $schoolYear); if ($semester !== null && $semester !== '') { $builder->where('sc.semester', $semester); } $rows = $builder->get()->toArray(); // Filter out Youth and empty labels $rows = array_values(array_filter($rows, static function ($r) { $g = trim((string) ($r->grade_label ?? '')); return $g !== '' && strcasecmp($g, 'youth') !== 0 && strncasecmp($g, 'youth-', 6) !== 0; })); $out = []; $totalPrimary = 0; $totalSecondary = 0; foreach ($rows as $r) { $first = trim((string) ($r->firstname ?? '')); $last = trim((string) ($r->lastname ?? '')); $grade = trim((string) ($r->grade_label ?? '')); $isNew = ((int) ($r->is_new ?? 0)) === 1; $primary = 0; $secondary = 0; if (array_key_exists($grade, $specialMap)) { $primary = (int) $specialMap[$grade]; $secondary = 0; } elseif ($isUpperBlock($grade)) { if ($isNew) { $primary = 4; $secondary = 0; } else { $primary = $isGrade5($grade) ? 3 : 2; $secondary = max(0, 4 - $primary); } } else { $primary = 4; $secondary = 0; } $totalPrimary += $primary; $totalSecondary += $secondary; $out[] = [ 'student_id' => (int) $r->student_id, 'firstname' => $first, 'lastname' => $last, 'grade_label' => $grade, 'primary_count' => $primary, 'secondary_count' => $secondary, ]; } return [ 'students' => $out, 'totals' => [ 'primary' => $totalPrimary, 'secondary' => $totalSecondary, 'students' => count($out), ], ]; } /** * Compute sticker counts per student for one class section */ protected function getStickerCountsPerStudentForClass( string $schoolYear, int $classSectionId, ?string $semester = null ): array { $specialMap = [ '1-A' => 3, '1-B' => 4, '2-A' => 5, '2-B' => 5, '9' => 2, ]; $isUpperBlock = static fn($g) => (bool) preg_match('/^(3|4|5|6|7|8)(-[AB])?$/', trim($g)); $isGrade5 = static fn($g) => (bool) preg_match('/^5(\-|$)/', trim($g)); $b = DB::table('student_class sc') ->select('sc.student_id', 's.firstname', 's.lastname', 's.is_new', 'cs.class_section_name AS grade_label') ->join('students s', 's.id', '=', 'sc.student_id') ->join('classSection cs', 'cs.class_section_id', '=', 'sc.class_section_id') ->join('classes c', 'c.id', '=', 'cs.class_id') ->where('sc.school_year', $schoolYear) ->where('sc.class_section_id', $classSectionId); if (!empty($semester)) { $b->where('sc.semester', $semester); } $rows = $b->get()->toArray(); $rows = array_values(array_filter($rows, static function ($r) { $g = trim((string) ($r->grade_label ?? '')); return $g !== '' && strcasecmp($g, 'youth') !== 0 && strncasecmp($g, 'youth-', 6) !== 0; })); $out = []; $tp = 0; $ts = 0; foreach ($rows as $r) { $g = trim((string) ($r->grade_label ?? '')); $isNew = ((int) ($r->is_new ?? 0)) === 1; $p = 0; $s = 0; if (isset($specialMap[$g])) { $p = $specialMap[$g]; } elseif ($isUpperBlock($g)) { if ($isNew) { $p = 4; } else { $p = $isGrade5($g) ? 3 : 2; $s = max(0, 4 - $p); } } else { $p = 4; } $tp += $p; $ts += $s; $out[] = [ 'student_id' => (int) $r->student_id, 'firstname' => trim((string) ($r->firstname ?? '')), 'lastname' => trim((string) ($r->lastname ?? '')), 'grade_label' => $g, 'primary_count' => $p, 'secondary_count' => $s, ]; } return [ 'students' => $out, 'totals' => [ 'primary' => $tp, 'secondary' => $ts, 'students' => count($out), ], ]; } /** * POST /api/v1/printables/badge * Generate staff badge PDF * * Note: This method requires FPDF library. The PDF generation logic from the * CodeIgniter version should be integrated here when FPDF is available. */ public function badge(): Response { // TODO: Implement PDF badge generation using FPDF // This requires the FPDF library to be installed and the drawBadgeInCell method // to be implemented. For now, return an error indicating it's not implemented. return $this->respondError( 'Badge PDF generation is not yet implemented. FPDF library integration required.', Response::HTTP_NOT_IMPLEMENTED ); } /** * POST /api/v1/printables/generate-stickers * Generate student name stickers PDF * * Note: This method requires FPDF library. The PDF generation logic from the * CodeIgniter version should be integrated here when FPDF is available. */ public function generateStickers(): Response { // TODO: Implement PDF sticker generation using FPDF // This requires the FPDF library to be installed. // For now, return an error indicating it's not implemented. return $this->respondError( 'Sticker PDF generation is not yet implemented. FPDF library integration required.', Response::HTTP_NOT_IMPLEMENTED ); } /** * GET /api/v1/printables/report-card/{studentId} * Generate single student report card PDF * * Note: This method requires FPDF library. The PDF generation logic from the * CodeIgniter version should be integrated here when FPDF is available. */ public function generateSingleReport($studentId): Response { // TODO: Implement PDF report card generation using FPDF // This requires the FPDF library to be installed and the formatReportPDF // and prepareStudentReportData methods to be implemented. return $this->respondError( 'Report card PDF generation is not yet implemented. FPDF library integration required.', Response::HTTP_NOT_IMPLEMENTED ); } /** * GET /api/v1/printables/report-card/class/{classSectionId} * Generate class report cards PDF * * Note: This method requires FPDF library. The PDF generation logic from the * CodeIgniter version should be integrated here when FPDF is available. */ public function generateClassReport($classSectionId): Response { // TODO: Implement PDF class report cards generation using FPDF // This requires the FPDF library to be installed. return $this->respondError( 'Class report cards PDF generation is not yet implemented. FPDF library integration required.', Response::HTTP_NOT_IMPLEMENTED ); } }