add projet
This commit is contained in:
+904
@@ -0,0 +1,904 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Models\Student;
|
||||
use App\Models\StudentClass;
|
||||
use App\Models\Configuration;
|
||||
use App\Models\ClassSection;
|
||||
use App\Models\Enrollment;
|
||||
use App\Models\EmergencyContact;
|
||||
use App\Models\StudentAllergy;
|
||||
use App\Models\StudentMedicalCondition;
|
||||
use App\Models\PromotionQueue;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class StudentController extends BaseApiController
|
||||
{
|
||||
protected Student $student;
|
||||
protected StudentClass $studentClass;
|
||||
protected Configuration $config;
|
||||
protected ClassSection $classSection;
|
||||
protected Enrollment $enrollment;
|
||||
protected EmergencyContact $emergencyContact;
|
||||
protected StudentAllergy $allergy;
|
||||
protected StudentMedicalCondition $condition;
|
||||
protected PromotionQueue $promotionQueue;
|
||||
protected string $schoolYear;
|
||||
protected string $semester;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->student = model(Student::class);
|
||||
$this->studentClass = model(StudentClass::class);
|
||||
$this->config = model(Configuration::class);
|
||||
$this->classSection = model(ClassSection::class);
|
||||
$this->enrollment = model(Enrollment::class);
|
||||
$this->emergencyContact = model(EmergencyContact::class);
|
||||
$this->allergy = model(StudentAllergy::class);
|
||||
$this->condition = model(StudentMedicalCondition::class);
|
||||
$this->promotionQueue = model(PromotionQueue::class);
|
||||
$this->schoolYear = (string) ($this->config->getConfig('school_year') ?? '');
|
||||
$this->semester = (string) ($this->config->getConfig('semester') ?? '');
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$page = max(1, (int) ($this->request->getGet('page') ?? 1));
|
||||
$perPage = min(100, max(1, (int) ($this->request->getGet('per_page') ?? 20)));
|
||||
$parentId = $this->request->getGet('parent_id');
|
||||
$classId = $this->request->getGet('class_id');
|
||||
|
||||
$query = $this->student->newQuery();
|
||||
|
||||
if (!empty($parentId)) {
|
||||
$query->where('parent_id', $parentId);
|
||||
}
|
||||
|
||||
if (!empty($classId)) {
|
||||
$studentIds = $this->studentClass->newQuery()
|
||||
->where('class_section_id', $classId)
|
||||
->pluck('student_id')
|
||||
->toArray();
|
||||
|
||||
if (!empty($studentIds)) {
|
||||
$query->whereIn('id', $studentIds);
|
||||
} else {
|
||||
return $this->success([
|
||||
'data' => [],
|
||||
'pagination' => [
|
||||
'current_page' => $page,
|
||||
'per_page' => $perPage,
|
||||
'total' => 0,
|
||||
'total_pages' => 0,
|
||||
],
|
||||
], 'No students found');
|
||||
}
|
||||
}
|
||||
|
||||
$result = $this->paginate($query, $page, $perPage);
|
||||
|
||||
$schoolYear = $this->config->getConfig('school_year');
|
||||
foreach ($result['data'] as &$student) {
|
||||
$student['current_class'] = $this->studentClass->newQuery()
|
||||
->select([
|
||||
'cs.*',
|
||||
'c.class_name as class_name',
|
||||
])
|
||||
->from('student_class as sc')
|
||||
->join('class_sections as cs', 'cs.id', '=', 'sc.class_section_id')
|
||||
->join('classes as c', 'c.id', '=', 'cs.class_id')
|
||||
->where('sc.student_id', $student['id'])
|
||||
->where('sc.school_year', $schoolYear)
|
||||
->where('sc.semester', $this->config->getConfig('semester'))
|
||||
->where('cs.school_year', $schoolYear)
|
||||
->where('cs.semester', $this->config->getConfig('semester'))
|
||||
->first();
|
||||
}
|
||||
|
||||
return $this->success($result, 'Students retrieved successfully');
|
||||
}
|
||||
|
||||
public function show($id = null)
|
||||
{
|
||||
$student = $this->student->find($id);
|
||||
if (!$student) {
|
||||
return $this->respondError('Student not found', Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
$schoolYear = $this->config->getConfig('school_year');
|
||||
$student['current_class'] = $this->studentClass->newQuery()
|
||||
->select([
|
||||
'cs.*',
|
||||
'c.class_name as class_name',
|
||||
])
|
||||
->from('student_class as sc')
|
||||
->join('class_sections as cs', 'cs.id', '=', 'sc.class_section_id')
|
||||
->join('classes as c', 'c.id', '=', 'cs.class_id')
|
||||
->where('sc.student_id', $id)
|
||||
->where('sc.school_year', $schoolYear)
|
||||
->where('sc.semester', $this->config->getConfig('semester'))
|
||||
->where('cs.school_year', $schoolYear)
|
||||
->where('cs.semester', $this->config->getConfig('semester'))
|
||||
->first();
|
||||
|
||||
return $this->success($student, 'Student retrieved successfully');
|
||||
}
|
||||
|
||||
public function store()
|
||||
{
|
||||
$data = $this->payloadData();
|
||||
if (empty($data)) {
|
||||
return $this->respondError('Invalid request data', Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$rules = [
|
||||
'firstname' => 'required|min:2|max:100',
|
||||
'lastname' => 'required|min:2|max:100',
|
||||
'dob' => 'required|date',
|
||||
'gender' => 'required|in:male,female',
|
||||
'parent_id' => 'required|integer',
|
||||
];
|
||||
|
||||
$errors = $this->validateRequest($data, $rules);
|
||||
if (!empty($errors)) {
|
||||
return $this->respondValidationError($errors);
|
||||
}
|
||||
|
||||
$data['school_year'] = $this->config->getConfig('school_year');
|
||||
$data['semester'] = $this->config->getConfig('semester');
|
||||
$data['registration_date'] = now()->toDateTimeString();
|
||||
|
||||
try {
|
||||
$student = $this->student->create($data);
|
||||
return $this->success($student->toArray(), 'Student created successfully', Response::HTTP_CREATED);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Student creation error: ' . $e->getMessage());
|
||||
return $this->respondError('Failed to create student', Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
public function update($id = null)
|
||||
{
|
||||
$student = $this->student->find($id);
|
||||
if (!$student) {
|
||||
return $this->respondError('Student not found', Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
$data = $this->payloadData();
|
||||
if (empty($data)) {
|
||||
return $this->respondError('Invalid request data', Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
// Build student data payload
|
||||
$studentData = [];
|
||||
$allowedFields = [
|
||||
'school_id', 'firstname', 'lastname', 'dob', 'age', 'gender',
|
||||
'registration_grade', 'photo_consent', 'parent_id', 'registration_date',
|
||||
'tuition_paid', 'year_of_registration', 'school_year', 'rfid_tag',
|
||||
'semester', 'is_new'
|
||||
];
|
||||
|
||||
foreach ($allowedFields as $field) {
|
||||
if (array_key_exists($field, $data) && $data[$field] !== null && $data[$field] !== '') {
|
||||
$value = $data[$field];
|
||||
// Apply title case to names
|
||||
if (in_array($field, ['firstname', 'lastname'])) {
|
||||
$value = $this->titleCase((string) $value);
|
||||
}
|
||||
$studentData[$field] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle DOB and age calculation
|
||||
if (isset($data['dob'])) {
|
||||
try {
|
||||
$dob = Carbon::parse($data['dob']);
|
||||
$studentData['dob'] = $dob->format('Y-m-d');
|
||||
$studentData['age'] = $dob->age;
|
||||
} catch (\Throwable $e) {
|
||||
DB::rollBack();
|
||||
return $this->respondError('Invalid date of birth format', Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle registration_date
|
||||
if (isset($data['registration_date']) && $data['registration_date'] !== '') {
|
||||
try {
|
||||
$regDate = Carbon::parse($data['registration_date']);
|
||||
$studentData['registration_date'] = $regDate->utc()->format('Y-m-d H:i:00');
|
||||
} catch (\Throwable $e) {
|
||||
// Ignore invalid dates
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize boolean fields
|
||||
if (isset($data['photo_consent'])) {
|
||||
$studentData['photo_consent'] = (int) ($data['photo_consent'] === '1' || $data['photo_consent'] === true);
|
||||
}
|
||||
if (isset($data['tuition_paid'])) {
|
||||
$studentData['tuition_paid'] = (int) ($data['tuition_paid'] === '1' || $data['tuition_paid'] === true);
|
||||
}
|
||||
if (isset($data['is_new'])) {
|
||||
$studentData['is_new'] = (int) ($data['is_new'] === '1' || $data['is_new'] === true);
|
||||
}
|
||||
|
||||
// Update student if there are changes
|
||||
if (!empty($studentData)) {
|
||||
$this->student->where('id', $id)->update($studentData);
|
||||
}
|
||||
|
||||
// Handle health lists if provided
|
||||
$rawPost = $data;
|
||||
if (isset($rawPost['medical_conditions']) || isset($rawPost['medical_touched'])) {
|
||||
$medTouched = ($rawPost['medical_touched'] ?? '0') === '1';
|
||||
$medConditions = $rawPost['medical_conditions'] ?? '';
|
||||
if ($medTouched || (isset($rawPost['medical_conditions']) && trim($medConditions) !== '')) {
|
||||
$normConditions = $this->normalizeHealthList($medConditions, 100);
|
||||
$this->syncHealthList($id, $this->condition, 'condition_name', $normConditions);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($rawPost['allergies']) || isset($rawPost['allergies_touched'])) {
|
||||
$allTouched = ($rawPost['allergies_touched'] ?? '0') === '1';
|
||||
$allergies = $rawPost['allergies'] ?? '';
|
||||
if ($allTouched || (isset($rawPost['allergies']) && trim($allergies) !== '')) {
|
||||
$normAllergies = $this->normalizeHealthList($allergies, 100);
|
||||
$this->syncHealthList($id, $this->allergy, 'allergy', $normAllergies);
|
||||
}
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
|
||||
$updated = $this->student->find($id);
|
||||
return $this->success($updated, 'Student updated successfully');
|
||||
} catch (\Throwable $e) {
|
||||
DB::rollBack();
|
||||
Log::error('Student update error: ' . $e->getMessage());
|
||||
return $this->respondError('Failed to update student: ' . $e->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
public function destroy($id = null)
|
||||
{
|
||||
$student = $this->student->find($id);
|
||||
if (!$student) {
|
||||
return $this->respondError('Student not found', Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->student->delete($id);
|
||||
return $this->respondDeleted(null, 'Student deleted successfully');
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Student deletion error: ' . $e->getMessage());
|
||||
return $this->respondError('Failed to delete student', Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/students/assign-class
|
||||
* Assign a student to a class section
|
||||
*/
|
||||
public function assignClassStudent(): JsonResponse
|
||||
{
|
||||
$data = $this->payloadData();
|
||||
$studentId = (int) ($data['student_id'] ?? 0);
|
||||
$classSectionId = (int) ($data['class_section_id'] ?? 0);
|
||||
$userId = $this->getCurrentUserId();
|
||||
$now = utc_now();
|
||||
|
||||
// Validate input
|
||||
if (!$studentId || !$classSectionId) {
|
||||
return $this->error('Missing required data (student/class section).', Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
// Check student
|
||||
$student = $this->student->find($studentId);
|
||||
if (!$student) {
|
||||
return $this->error('Student not found.', Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
// Robust section lookup: allow id OR class_section_id
|
||||
$section = $this->classSection->newQuery()
|
||||
->where(function ($q) use ($classSectionId) {
|
||||
$q->where('id', $classSectionId)
|
||||
->orWhere('class_section_id', $classSectionId);
|
||||
})
|
||||
->first();
|
||||
|
||||
if (!$section) {
|
||||
return $this->error('Class/Section not found.', Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
$displayName = $this->formatClassSectionDisplayName($section, $classSectionId);
|
||||
|
||||
// Derive parent class_id from the section row
|
||||
$parentClassId = (int) ($section['class_id'] ?? 0);
|
||||
if (!$parentClassId) {
|
||||
$row = DB::table('classSection')
|
||||
->select('class_id')
|
||||
->where(function ($q) use ($section, $classSectionId) {
|
||||
$q->where('id', $section['id'] ?? $classSectionId)
|
||||
->orWhere('class_section_id', $classSectionId);
|
||||
})
|
||||
->first();
|
||||
|
||||
if ($row && isset($row->class_id)) {
|
||||
$parentClassId = (int) $row->class_id;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
// Upsert student_class
|
||||
$payload = [
|
||||
'student_id' => $studentId,
|
||||
'class_section_id' => $classSectionId,
|
||||
'semester' => $this->semester,
|
||||
'school_year' => $this->schoolYear,
|
||||
'updated_by' => $userId,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
|
||||
$existing = $this->studentClass->newQuery()
|
||||
->where('student_id', $studentId)
|
||||
->where('semester', $this->semester)
|
||||
->where('school_year', $this->schoolYear)
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
$this->studentClass->where('id', $existing['id'])->update($payload);
|
||||
} else {
|
||||
$payload['created_at'] = $now;
|
||||
$this->studentClass->insert($payload);
|
||||
}
|
||||
|
||||
// Update enrollment for current term (if exists)
|
||||
$enroll = $this->enrollment->newQuery()
|
||||
->where('student_id', $studentId)
|
||||
->where('school_year', $this->schoolYear)
|
||||
->where('semester', $this->semester)
|
||||
->first();
|
||||
|
||||
if ($enroll) {
|
||||
$this->enrollment->where('id', $enroll['id'])->update([
|
||||
'class_section_id' => $classSectionId,
|
||||
'enrollment_status' => 'payment pending',
|
||||
'admission_status' => 'accepted',
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
}
|
||||
|
||||
// Re-tag attendance rows
|
||||
$attnStats = $this->updateStudentAttendanceSection(
|
||||
$studentId,
|
||||
$classSectionId,
|
||||
$parentClassId ?: 0,
|
||||
$this->semester,
|
||||
$this->schoolYear,
|
||||
$userId
|
||||
);
|
||||
|
||||
// Re-tag score rows
|
||||
$scoreStats = $this->updateStudentScoresSection(
|
||||
$studentId,
|
||||
$classSectionId,
|
||||
$this->semester,
|
||||
$this->schoolYear,
|
||||
$userId
|
||||
);
|
||||
|
||||
DB::commit();
|
||||
|
||||
return $this->success([
|
||||
'student_id' => $studentId,
|
||||
'class_section_id' => $classSectionId,
|
||||
'class_section_name' => $displayName,
|
||||
'attendance_updates' => $attnStats,
|
||||
'score_updates' => $scoreStats,
|
||||
], 'Assignment saved.');
|
||||
} catch (\Throwable $e) {
|
||||
DB::rollBack();
|
||||
Log::error('Unable to assign class: ' . $e->getMessage());
|
||||
return $this->error('Unable to assign class: ' . $e->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/students/auto-distribute
|
||||
* Auto distribute students into lettered sections for a class
|
||||
*/
|
||||
public function autoDistributeSections(): JsonResponse
|
||||
{
|
||||
$data = $this->payloadData();
|
||||
$classId = (int) ($data['class_id'] ?? 0);
|
||||
$classSectionId = (int) ($data['class_section_id'] ?? 0);
|
||||
$perSec = (int) ($data['students_per_section'] ?? 0);
|
||||
$year = trim((string) ($data['school_year'] ?? $this->schoolYear));
|
||||
|
||||
if ($classId <= 0 && $classSectionId > 0) {
|
||||
$cid = $this->classSection->getClassId($classSectionId);
|
||||
$classId = (int) ($cid ?? 0);
|
||||
}
|
||||
|
||||
if ($classId <= 0 || $perSec <= 0) {
|
||||
return $this->error('Invalid class_id or students_per_section.', Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
try {
|
||||
// Candidates from promotion queue
|
||||
$cands = $this->promotionQueue->newQuery()
|
||||
->select('promotion_queue.*', 'students.gender')
|
||||
->join('students', 'students.id', '=', 'promotion_queue.student_id', 'left')
|
||||
->where('promotion_queue.to_class_id', $classId)
|
||||
->where('promotion_queue.school_year_to', $year)
|
||||
->whereIn('promotion_queue.status', ['queued', 'assigned'])
|
||||
->get()
|
||||
->toArray();
|
||||
|
||||
if (empty($cands)) {
|
||||
return $this->error('No students found in promotion queue for selected class/year.', Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
// Filter to those with enrollment
|
||||
$studentIds = array_map(fn($r) => (int) $r['student_id'], $cands);
|
||||
$enrolledIds = [];
|
||||
|
||||
if (!empty($studentIds)) {
|
||||
$rows = DB::table('enrollments')
|
||||
->select('student_id')
|
||||
->whereIn('student_id', $studentIds)
|
||||
->where('school_year', $year)
|
||||
->whereIn('enrollment_status', ['payment pending', 'enrolled'])
|
||||
->groupBy('student_id')
|
||||
->get()
|
||||
->toArray();
|
||||
|
||||
$enrolledIds = array_map(fn($r) => (int) $r->student_id, $rows);
|
||||
}
|
||||
|
||||
$cands = array_values(array_filter($cands, fn($r) => in_array((int) $r['student_id'], $enrolledIds, true)));
|
||||
|
||||
if (empty($cands)) {
|
||||
return $this->error('No eligible enrolled students found to distribute.', Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$total = count($cands);
|
||||
$sectionsNeeded = (int) ceil($total / $perSec);
|
||||
|
||||
// Fetch lettered sections
|
||||
$letters = $this->classSection->getLetterSectionsByClassId($classId);
|
||||
|
||||
if (empty($letters)) {
|
||||
return $this->error('No lettered sections found for the selected class.', Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (count($letters) < $sectionsNeeded) {
|
||||
return $this->error('Not enough sections available. Needed: ' . $sectionsNeeded . ', available: ' . count($letters), Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$letters = array_slice($letters, 0, $sectionsNeeded);
|
||||
|
||||
// Prepare buckets
|
||||
$buckets = [];
|
||||
foreach ($letters as $idx => $sec) {
|
||||
$buckets[$idx] = [
|
||||
'class_section_id' => (int) $sec['class_section_id'],
|
||||
'assigned' => [],
|
||||
'male' => 0,
|
||||
'female' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
// Split by gender
|
||||
$males = [];
|
||||
$females = [];
|
||||
foreach ($cands as $r) {
|
||||
$g = strtolower((string) ($r['gender'] ?? ''));
|
||||
if ($g === 'female') {
|
||||
$females[] = $r;
|
||||
} else {
|
||||
$males[] = $r;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to pick next bucket
|
||||
$pickBucket = function (string $gender) use (&$buckets, $perSec): ?int {
|
||||
$bestIdx = null;
|
||||
$bestCnt = PHP_INT_MAX;
|
||||
foreach ($buckets as $i => $b) {
|
||||
if (count($b['assigned']) >= $perSec) {
|
||||
continue;
|
||||
}
|
||||
$cnt = ($gender === 'female') ? $b['female'] : $b['male'];
|
||||
if ($cnt < $bestCnt) {
|
||||
$bestCnt = $cnt;
|
||||
$bestIdx = $i;
|
||||
}
|
||||
}
|
||||
return $bestIdx;
|
||||
};
|
||||
|
||||
// Assign males then females
|
||||
foreach ($males as $r) {
|
||||
$bi = $pickBucket('male');
|
||||
if ($bi === null) {
|
||||
break;
|
||||
}
|
||||
$buckets[$bi]['assigned'][] = (int) $r['student_id'];
|
||||
$buckets[$bi]['male']++;
|
||||
}
|
||||
|
||||
foreach ($females as $r) {
|
||||
$bi = $pickBucket('female');
|
||||
if ($bi === null) {
|
||||
break;
|
||||
}
|
||||
$buckets[$bi]['assigned'][] = (int) $r['student_id'];
|
||||
$buckets[$bi]['female']++;
|
||||
}
|
||||
|
||||
// Persist assignments
|
||||
$promoIdsBySid = [];
|
||||
foreach ($cands as $r) {
|
||||
$promoIdsBySid[(int) $r['student_id']] = (int) $r['id'];
|
||||
}
|
||||
|
||||
$updatedBy = $this->getCurrentUserId();
|
||||
$now = utc_now();
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
foreach ($buckets as $b) {
|
||||
$secId = (int) $b['class_section_id'];
|
||||
foreach ($b['assigned'] as $sid) {
|
||||
// Update promotion queue
|
||||
if (isset($promoIdsBySid[$sid])) {
|
||||
$this->promotionQueue->where('id', $promoIdsBySid[$sid])->update([
|
||||
'to_class_section_id' => $secId,
|
||||
'status' => 'assigned',
|
||||
'updated_by' => $updatedBy,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
}
|
||||
|
||||
// Upsert student_class
|
||||
$exists = $this->studentClass->newQuery()
|
||||
->where('student_id', $sid)
|
||||
->where('school_year', $year)
|
||||
->where('semester', $this->semester)
|
||||
->first();
|
||||
|
||||
$payload = [
|
||||
'student_id' => $sid,
|
||||
'class_section_id' => $secId,
|
||||
'school_year' => $year,
|
||||
'semester' => $this->semester,
|
||||
'updated_by' => $updatedBy,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
|
||||
if ($exists) {
|
||||
$this->studentClass->where('id', $exists['id'])->update($payload);
|
||||
} else {
|
||||
$payload['created_at'] = $now;
|
||||
$this->studentClass->insert($payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
|
||||
// Build summary
|
||||
$nameById = [];
|
||||
foreach ($letters as $secRow) {
|
||||
$nameById[(int) $secRow['class_section_id']] = (string) ($secRow['class_section_name'] ?? '');
|
||||
}
|
||||
|
||||
$summary = [];
|
||||
foreach ($buckets as $b) {
|
||||
$secId = (int) $b['class_section_id'];
|
||||
$summary[] = [
|
||||
'class_section_id' => $secId,
|
||||
'class_section_name' => $nameById[$secId] ?? (string) $secId,
|
||||
'total' => count($b['assigned']),
|
||||
'male' => $b['male'],
|
||||
'female' => $b['female'],
|
||||
];
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'sections' => $summary,
|
||||
], 'Auto distribution completed.');
|
||||
} catch (\Throwable $e) {
|
||||
DB::rollBack();
|
||||
Log::error('Auto distribution failed: ' . $e->getMessage());
|
||||
return $this->error('Auto distribution failed: ' . $e->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/students/promotion-totals
|
||||
* Return totals per base class for promotion_queue in selected year
|
||||
*/
|
||||
public function promotionTotalsApi(): JsonResponse
|
||||
{
|
||||
try {
|
||||
$year = trim((string) ($this->laravelRequest->query('school_year') ?? $this->schoolYear));
|
||||
|
||||
// Fetch base sections
|
||||
$bases = $this->classSection->newQuery()
|
||||
->whereRaw("class_section_name NOT LIKE '%-%'")
|
||||
->orderBy('class_id', 'ASC')
|
||||
->get()
|
||||
->toArray();
|
||||
|
||||
$wanted = [];
|
||||
foreach ($bases as $r) {
|
||||
$nameRaw = (string) ($r['class_section_name'] ?? '');
|
||||
$name = strtolower($nameRaw);
|
||||
if ($name === 'kg' || $name === 'youth') {
|
||||
$wanted[] = $r;
|
||||
continue;
|
||||
}
|
||||
if (ctype_digit($name)) {
|
||||
$num = (int) $name;
|
||||
if ($num >= 1 && $num <= 9) {
|
||||
$wanted[] = $r;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$out = [];
|
||||
foreach ($wanted as $r) {
|
||||
$classId = (int) $r['class_id'];
|
||||
|
||||
$cands = DB::table('promotion_queue as pq')
|
||||
->select('pq.student_id')
|
||||
->join('enrollments as e', function ($join) use ($year) {
|
||||
$join->on('e.student_id', '=', 'pq.student_id')
|
||||
->where('e.school_year', $year);
|
||||
}, 'left')
|
||||
->where('pq.to_class_id', $classId)
|
||||
->where('pq.school_year_to', $year)
|
||||
->whereIn('pq.status', ['queued', 'assigned', 'applied'])
|
||||
->whereIn('e.enrollment_status', ['payment pending', 'enrolled'])
|
||||
->groupBy('pq.student_id')
|
||||
->get()
|
||||
->toArray();
|
||||
|
||||
$out[] = [
|
||||
'class_id' => $classId,
|
||||
'class_section_id' => (int) ($r['class_section_id'] ?? 0),
|
||||
'class_section_name' => (string) ($r['class_section_name'] ?? ''),
|
||||
'total' => count($cands),
|
||||
];
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'year' => $year,
|
||||
'rows' => $out,
|
||||
], 'Promotion totals retrieved');
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Promotion totals error: ' . $e->getMessage());
|
||||
return $this->error($e->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update attendance tables for a student's new class section
|
||||
*/
|
||||
private function updateStudentAttendanceSection(
|
||||
int $studentId,
|
||||
int $newClassSectionId,
|
||||
int $newClassId,
|
||||
string $semester,
|
||||
string $schoolYear,
|
||||
?int $modifiedBy = null
|
||||
): array {
|
||||
$now = utc_now();
|
||||
|
||||
// Update attendance_data
|
||||
$dataUpdated = DB::table('attendance_data')
|
||||
->where('student_id', $studentId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->update([
|
||||
'class_section_id' => $newClassSectionId,
|
||||
'class_id' => $newClassId,
|
||||
'updated_at' => $now,
|
||||
'modified_by' => $modifiedBy,
|
||||
]);
|
||||
|
||||
// Update attendance_record
|
||||
$recUpdated = DB::table('attendance_record')
|
||||
->where('student_id', $studentId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->update([
|
||||
'class_section_id' => $newClassSectionId,
|
||||
'updated_at' => $now,
|
||||
'modified_by' => $modifiedBy,
|
||||
]);
|
||||
|
||||
return [
|
||||
'attendance_data_updated' => $dataUpdated,
|
||||
'attendance_record_updated' => $recUpdated,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update all score-related tables for a student to the new class_section_id
|
||||
*/
|
||||
private function updateStudentScoresSection(
|
||||
int $studentId,
|
||||
int $newClassSectionId,
|
||||
string $semester,
|
||||
string $schoolYear,
|
||||
?int $modifiedBy = null
|
||||
): array {
|
||||
$now = utc_now();
|
||||
$tables = [
|
||||
'homework',
|
||||
'quiz',
|
||||
'project',
|
||||
'participation',
|
||||
'midterm_exam',
|
||||
'final_exam',
|
||||
'final_score',
|
||||
'semester_scores',
|
||||
];
|
||||
|
||||
$results = [];
|
||||
foreach ($tables as $tbl) {
|
||||
$updated = DB::table($tbl)
|
||||
->where('student_id', $studentId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->update([
|
||||
'class_section_id' => $newClassSectionId,
|
||||
'updated_at' => $now,
|
||||
'updated_by' => $modifiedBy,
|
||||
]);
|
||||
$results[$tbl . '_updated'] = $updated;
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a display name from section row
|
||||
*/
|
||||
private function formatClassSectionDisplayName(array $row, int $fallbackId): string
|
||||
{
|
||||
if (!empty($row['class_section_name'])) {
|
||||
return (string) $row['class_section_name'];
|
||||
}
|
||||
if (!empty($row['section_name'])) {
|
||||
return (string) $row['section_name'];
|
||||
}
|
||||
if (!empty($row['name'])) {
|
||||
return (string) $row['name'];
|
||||
}
|
||||
if (!empty($row['title'])) {
|
||||
return (string) $row['title'];
|
||||
}
|
||||
|
||||
$grade = $row['grade_name'] ?? $row['grade'] ?? $row['class_name'] ?? null;
|
||||
$letter = $row['section'] ?? $row['section_letter'] ?? $row['letter'] ?? null;
|
||||
|
||||
if ($grade && $letter) {
|
||||
return "{$grade} - {$letter}";
|
||||
}
|
||||
if ($grade) {
|
||||
return (string) $grade;
|
||||
}
|
||||
|
||||
return 'Section #' . $fallbackId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a comma/semicolon/newline list
|
||||
*/
|
||||
private function normalizeHealthList(?string $s, int $maxLen = 100): array
|
||||
{
|
||||
$s = (string) $s;
|
||||
if ($s === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$parts = preg_split('/[,\n;]+/u', $s, -1, PREG_SPLIT_NO_EMPTY) ?: [];
|
||||
$outAssoc = [];
|
||||
|
||||
foreach ($parts as $p) {
|
||||
$v = trim(preg_replace('/\s+/u', ' ', $p));
|
||||
if ($v === '') {
|
||||
continue;
|
||||
}
|
||||
if (preg_match('/^(none|n\/a|na|null|nil|no)$/i', $v)) {
|
||||
continue;
|
||||
}
|
||||
$v = mb_substr($v, 0, $maxLen, 'UTF-8');
|
||||
$outAssoc[mb_strtolower($v, 'UTF-8')] = $v;
|
||||
}
|
||||
|
||||
return array_values($outAssoc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Diff-sync helper for health lists
|
||||
*/
|
||||
private function syncHealthList(int $studentId, $model, string $field, array $newValues): void
|
||||
{
|
||||
// Fetch current values
|
||||
$rows = $model->newQuery()->where('student_id', $studentId)->select("id, {$field}")->get()->toArray();
|
||||
$current = [];
|
||||
$byId = [];
|
||||
|
||||
foreach ($rows as $r) {
|
||||
$val = (string) ($r[$field] ?? '');
|
||||
$key = mb_strtolower(trim($val), 'UTF-8');
|
||||
if ($key === '') {
|
||||
continue;
|
||||
}
|
||||
$current[$key] = $val;
|
||||
$byId[$key] = (int) $r['id'];
|
||||
}
|
||||
|
||||
// Build new set
|
||||
$incoming = [];
|
||||
foreach ($newValues as $v) {
|
||||
$key = mb_strtolower(trim($v), 'UTF-8');
|
||||
if ($key === '') {
|
||||
continue;
|
||||
}
|
||||
$incoming[$key] = $v;
|
||||
}
|
||||
|
||||
// Compute diffs
|
||||
$toDeleteKeys = array_diff(array_keys($current), array_keys($incoming));
|
||||
$toInsertKeys = array_diff(array_keys($incoming), array_keys($current));
|
||||
|
||||
// Delete removed
|
||||
if (!empty($toDeleteKeys)) {
|
||||
$ids = array_map(fn($k) => $byId[$k], $toDeleteKeys);
|
||||
if (!empty($ids)) {
|
||||
$model->newQuery()->whereIn('id', $ids)->delete();
|
||||
}
|
||||
}
|
||||
|
||||
// Insert new
|
||||
if (!empty($toInsertKeys)) {
|
||||
$batch = [];
|
||||
foreach ($toInsertKeys as $k) {
|
||||
$batch[] = [
|
||||
'student_id' => $studentId,
|
||||
$field => $incoming[$k],
|
||||
];
|
||||
}
|
||||
if (!empty($batch)) {
|
||||
$model->newQuery()->insert($batch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert string to title case
|
||||
*/
|
||||
private function titleCase(string $s): string
|
||||
{
|
||||
$s = strip_tags($s);
|
||||
$s = mb_strtolower($s ?: '', 'UTF-8');
|
||||
return mb_convert_case($s, MB_CASE_TITLE, 'UTF-8');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user