Files
alrahma_sunday_school_api/app/Http/Controllers/Api/PrintablesReportsController.php
T
2026-03-05 12:29:37 -05:00

625 lines
22 KiB
PHP
Executable File

<?php
namespace App\Http\Controllers\Api;
use App\Models\BadgePrintLog;
use App\Models\ClassSection;
use App\Models\Configuration;
use App\Models\SemesterScore;
use App\Models\StudentClass;
use App\Models\Student;
use App\Models\TeacherClass;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\StreamedResponse;
class PrintablesReportsController extends BaseApiController
{
protected Student $student;
protected ClassSection $classSection;
protected Configuration $config;
protected BadgePrintLog $badgePrintLog;
protected SemesterScore $semesterScore;
protected StudentClass $studentClass;
protected TeacherClass $teacherClass;
protected User $user;
protected string $schoolYear;
protected string $semester;
public function __construct()
{
parent::__construct();
$this->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
);
}
}