625 lines
22 KiB
PHP
Executable File
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
|
|
);
|
|
}
|
|
}
|