add projet

This commit is contained in:
root
2026-03-05 12:29:37 -05:00
parent 8d1eef8ba8
commit 23b7db1107
9109 changed files with 1106501 additions and 73 deletions
+767
View File
@@ -0,0 +1,767 @@
<?php
namespace App\Http\Controllers\Api;
use App\Models\AuthorizedUser;
use App\Models\ClassSection;
use App\Models\Configuration;
use App\Models\EmergencyContact;
use App\Models\Enrollment;
use App\Models\EventCharges;
use App\Models\Event;
use App\Models\Invoice;
use App\Models\ParentAttendanceReport;
use App\Models\PromotionQueue;
use App\Models\Refund;
use App\Models\StudentAllergy;
use App\Models\StudentClass;
use App\Models\StudentMedicalCondition;
use App\Models\Student;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpFoundation\Response;
use Throwable;
class ParentController extends BaseApiController
{
protected User $user;
protected Student $student;
protected StudentClass $studentClass;
protected Enrollment $enrollment;
protected Configuration $config;
protected EventCharges $charges;
protected Event $event;
protected StudentMedicalCondition $medicalCondition;
protected StudentAllergy $allergy;
protected Refund $refund;
protected Invoice $invoice;
protected ClassSection $classSection;
protected PromotionQueue $promotion;
protected EmergencyContact $emergencyContact;
protected AuthorizedUser $authorizedUser;
public function __construct()
{
parent::__construct();
$this->user = model(User::class);
$this->student = model(Student::class);
$this->studentClass = model(StudentClass::class);
$this->enrollment = model(Enrollment::class);
$this->config = model(Configuration::class);
$this->charges = model(EventCharges::class);
$this->event = model(Event::class);
$this->medicalCondition = model(StudentMedicalCondition::class);
$this->allergy = model(StudentAllergy::class);
$this->refund = model(Refund::class);
$this->invoice = model(Invoice::class);
$this->classSection = model(ClassSection::class);
$this->promotion = model(PromotionQueue::class);
$this->emergencyContact = model(EmergencyContact::class);
$this->authorizedUser = model(AuthorizedUser::class);
}
public function index()
{
$page = max(1, (int) ($this->request->getGet('page') ?? 1));
$perPage = min(100, max(1, (int) ($this->request->getGet('per_page') ?? 20)));
$query = $this->user->newQuery()
->select('users.*')
->join('user_roles', 'user_roles.user_id', '=', 'users.id')
->join('roles', 'roles.id', '=', 'user_roles.role_id')
->where('roles.name', 'parent')
->groupBy('users.id');
$result = $this->paginate($query, $page, $perPage);
foreach ($result['data'] as &$parent) {
unset($parent['password'], $parent['token']);
}
return $this->success($result, 'Parents retrieved successfully');
}
public function enrollClassesData($id)
{
try {
$parentId = (int) $id;
$parent = $this->user->find($parentId);
if (!$parent) {
return $this->error('Parent not found', Response::HTTP_NOT_FOUND);
}
$schoolYear = (string) ($this->config->getConfig('school_year'));
$semester = (string) ($this->config->getConfig('semester'));
$lastDayOfRegistration = (string) ($this->config->getConfig('enrollment_deadline'));
$schoolStartDate = (string) ($this->config->getConfig('school_start_date'));
$withdrawalDeadline = (string) ($this->config->getConfig('refund_deadline'));
$selectedYear = (string) ($this->request->getGet('school_year') ?? $schoolYear);
$isEditable = ($selectedYear === $schoolYear);
$students = DB::table('students')
->where('parent_id', $parentId)
->get()
->map(fn($row) => (array) $row)
->toArray();
$statusMap = [
'admission under review' => 'admission under review',
'payment pending' => 'payment pending',
'enrolled' => 'enrolled',
'withdraw under review' => 'withdraw under review',
'refund pending' => 'refund pending',
'withdrawn' => 'withdrawn',
'denied' => 'denied',
'waitlist' => 'waitlist',
];
foreach ($students as &$student) {
$studentId = (int) ($student['id'] ?? 0);
$classRow = DB::table('student_class')
->select('class_section_id')
->where('student_id', $studentId)
->where('school_year', $selectedYear)
->first();
$classSectionId = $classRow->class_section_id ?? null;
$student['class_section'] = $classSectionId
? ($this->classSection->getClassSectionNameBySectionId($classSectionId) ?? 'Class not Assigned')
: 'Class not Assigned';
$enr = DB::table('enrollments')
->select('enrollment_status', 'admission_status', 'is_withdrawn')
->where('student_id', $studentId)
->where('school_year', $selectedYear)
->orderByDesc('updated_at')
->first();
if ($enr && isset($enr->admission_status)) {
$student['admission_status'] = $enr->admission_status;
if ($enr->admission_status === 'denied') {
$student['enrollment_status'] = 'denied';
} else {
$est = $enr->enrollment_status ?? '';
$student['enrollment_status'] = $statusMap[$est] ?? 'not enrolled';
}
} else {
$student['admission_status'] = null;
$student['enrollment_status'] = 'not enrolled';
}
$student['disable_enroll'] = in_array(
$student['enrollment_status'],
['admission under review', 'payment pending', 'enrolled', 'withdraw under review', 'denied'],
true
);
}
unset($student);
$schoolYears = DB::table('enrollments')
->select('school_year')
->distinct()
->orderByDesc('school_year')
->get()
->map(fn($row) => (array) $row)
->toArray();
if (empty($schoolYears)) {
$schoolYears[] = ['school_year' => $schoolYear];
}
return $this->success([
'students' => $students,
'schoolYears' => $schoolYears,
'selectedYear' => $selectedYear,
'isEditable' => $isEditable,
'withdrawalDeadline' => $withdrawalDeadline,
'lastDayOfRegistration'=> $lastDayOfRegistration,
'schoolStartDate' => $schoolStartDate,
], 'Enroll classes data');
} catch (\Throwable $e) {
log_message('error', 'enrollClassesData error: ' . $e->getMessage());
return $this->error('Server error', Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
public function updateEnrollments($id)
{
$parentId = (int) $id;
$parent = $this->user->find($parentId);
if (!$parent) {
return $this->error('Parent not found', Response::HTTP_NOT_FOUND);
}
$payload = $this->payloadData();
$enroll = array_map('intval', (array) ($payload['enroll'] ?? []));
$withdraw = array_map('intval', (array) ($payload['withdraw'] ?? []));
if (empty($enroll) && empty($withdraw)) {
return $this->error('No students selected for enrollment or withdrawal.', Response::HTTP_UNPROCESSABLE_ENTITY);
}
$result = $this->processEnrollmentUpdate($parentId, $enroll, $withdraw);
return $this->success($result, 'Enrollment updates processed');
}
public function emergencyContacts($id)
{
$parentId = (int) $id;
$rows = $this->emergencyContact->getEmergencyContactsByParentId($parentId);
return $this->success($rows, 'Emergency contacts retrieved');
}
public function saveEmergencyContact($id)
{
$parentId = (int) $id;
$data = $this->payloadData();
$payload = [
'parent_id' => $parentId,
'emergency_contact_name' => trim((string) ($data['emergency_contact_name'] ?? '')),
'cellphone' => trim((string) ($data['cellphone'] ?? '')),
'email' => trim((string) ($data['email'] ?? '')) ?: null,
'relation' => trim((string) ($data['relation'] ?? '')) ?: null,
'semester' => (string) $this->config->getConfig('semester'),
'school_year' => (string) $this->config->getConfig('school_year'),
];
if ($payload['emergency_contact_name'] === '' || $payload['cellphone'] === '') {
return $this->error('Name and cellphone are required', Response::HTTP_UNPROCESSABLE_ENTITY);
}
$idToUpdate = isset($data['id']) ? (int) $data['id'] : 0;
if ($idToUpdate > 0) {
$row = $this->emergencyContact->find($idToUpdate);
if (!$row || (int) $row['parent_id'] !== $parentId) {
return $this->error('Contact not found', Response::HTTP_NOT_FOUND);
}
$this->emergencyContact->update($idToUpdate, $payload);
return $this->success(['id' => $idToUpdate], 'Emergency contact updated');
}
$newId = $this->emergencyContact->insert($payload);
if (!$newId) {
return $this->error('Failed to save emergency contact', Response::HTTP_INTERNAL_SERVER_ERROR);
}
return $this->success(['id' => $newId], 'Emergency contact created');
}
public function authorizedUsers($id)
{
$parentId = (int) $id;
$rows = $this->authorizedUser->newQuery()
->where('user_id', $parentId)
->orderByDesc('created_at')
->get()
->toArray();
return $this->success($rows, 'Authorized users retrieved');
}
public function addAuthorizedUser($id)
{
$parentId = (int) $id;
$data = $this->payloadData();
$payload = [
'user_id' => $parentId,
'firstname' => trim((string) ($data['firstname'] ?? '')),
'lastname' => trim((string) ($data['lastname'] ?? '')),
'email' => trim((string) ($data['email'] ?? '')),
'phone_number' => trim((string) ($data['phone_number'] ?? '')),
'relation_to_user'=> trim((string) ($data['relation_to_user'] ?? '')),
'status' => 'Pending',
];
if ($payload['firstname'] === '' || $payload['lastname'] === '' || $payload['email'] === '' || $payload['phone_number'] === '') {
return $this->error('Missing required fields', Response::HTTP_UNPROCESSABLE_ENTITY);
}
if (!$this->authorizedUser->insert($payload)) {
return $this->error('Failed to create authorized user', Response::HTTP_INTERNAL_SERVER_ERROR, $this->authorizedUser->errors());
}
return $this->success([], 'Authorized user request created');
}
public function createStudent($id)
{
$parentId = (int) $id;
$data = $this->payloadData();
$allowed = [
'firstname', 'lastname', 'dob', 'age', 'gender', 'registration_grade', 'photo_consent', 'rfid_tag',
];
$payload = array_intersect_key($data, array_flip($allowed));
$payload['parent_id'] = $parentId;
$payload['is_new'] = isset($data['is_new']) ? (int) $data['is_new'] : 1;
$payload['school_year'] = (string) $this->config->getConfig('school_year');
$payload['semester'] = (string) $this->config->getConfig('semester');
$payload['registration_date'] = Carbon::now('UTC')->format('Y-m-d');
if (empty($payload['firstname']) || empty($payload['lastname']) || empty($payload['gender'])) {
return $this->error('firstname, lastname, and gender are required', Response::HTTP_UNPROCESSABLE_ENTITY);
}
$student = $this->student->create($payload);
if (!$student) {
return $this->error('Failed to create student', Response::HTTP_INTERNAL_SERVER_ERROR, $this->student->errors());
}
return $this->success(['id' => $student->id], 'Student created');
}
public function updateStudent($id, $studentId)
{
$parentId = (int) $id;
$studentId = (int) $studentId;
$row = $this->student->find($studentId);
if (!$row || (int) $row['parent_id'] !== $parentId) {
return $this->error('Student not found', Response::HTTP_NOT_FOUND);
}
$data = $this->payloadData();
$allowed = ['firstname', 'lastname', 'dob', 'age', 'gender', 'registration_grade', 'photo_consent', 'rfid_tag'];
$payload = array_intersect_key($data, array_flip($allowed));
if (empty($payload)) {
return $this->error('No valid fields to update', Response::HTTP_UNPROCESSABLE_ENTITY);
}
$ok = $this->student->update($studentId, $payload);
if (!$ok) {
return $this->error('Failed to update student', Response::HTTP_INTERNAL_SERVER_ERROR, $this->student->errors());
}
return $this->success([], 'Student updated');
}
public function deleteStudent($id, $studentId)
{
$parentId = (int) $id;
$studentId = (int) $studentId;
$row = $this->student->find($studentId);
if (!$row || (int) $row['parent_id'] !== $parentId) {
return $this->error('Student not found', Response::HTTP_NOT_FOUND);
}
$ok = $this->student->delete($studentId);
if (!$ok) {
return $this->error('Failed to delete student', Response::HTTP_INTERNAL_SERVER_ERROR);
}
return $this->success([], 'Student deleted');
}
private function processEnrollmentUpdate(int $parentId, array $enroll, array $withdraw): array
{
$schoolYear = (string) $this->config->getConfig('school_year');
$semester = (string) $this->config->getConfig('semester');
$lastDayOfRegistration = (string) $this->config->getConfig('enrollment_deadline');
$today = Carbon::now('UTC')->format('Y-m-d');
if ($lastDayOfRegistration && $today > date('Y-m-d', strtotime($lastDayOfRegistration))) {
if (!empty($enroll)) {
return [
'enrolled' => [],
'withdrawn' => [],
'messages' => ['Enrollment deadline has passed. New enrollments are not allowed.'],
];
}
}
$enrolled = [];
$withdrawn = [];
$messages = [];
foreach ($enroll as $studentId) {
$studentId = (int) $studentId;
if ($studentId <= 0) {
continue;
}
$existing = $this->enrollment->newQuery()
->where('student_id', $studentId)
->where('school_year', $schoolYear)
->where('semester', $semester)
->first();
if ($existing) {
if ((int) ($existing['is_withdrawn'] ?? 0) === 1) {
$ok = $this->enrollment->update((int) $existing['id'], [
'is_withdrawn' => 0,
'withdrawal_date' => null,
'enrollment_status' => 'payment pending',
'updated_at' => Carbon::now('UTC')->toDateTimeString(),
]);
if ($ok) {
$enrolled[] = $studentId;
$messages[] = "Re-enrolled student {$studentId}.";
}
} else {
$messages[] = "Student {$studentId} already enrolled.";
}
} else {
$ok = $this->enrollment->insert([
'student_id' => $studentId,
'parent_id' => $parentId,
'school_year' => $schoolYear,
'semester' => $semester,
'enrollment_date' => $today,
'is_withdrawn' => 0,
'enrollment_status' => 'admission under review',
'admission_status' => 'pending',
'created_at' => Carbon::now('UTC')->toDateTimeString(),
]);
if ($ok) {
$enrolled[] = $studentId;
$messages[] = "Enrolled student {$studentId}.";
}
}
$this->applyPromotionAssignment($studentId, $schoolYear);
}
foreach ($withdraw as $studentId) {
$studentId = (int) $studentId;
if ($studentId <= 0) {
continue;
}
$enrollment = $this->enrollment->newQuery()
->where('student_id', $studentId)
->where('school_year', $schoolYear)
->where('semester', $semester)
->where('is_withdrawn', 0)
->first();
if ($enrollment) {
$ok = $this->enrollment->update((int) $enrollment['id'], [
'withdrawal_date' => $today,
'enrollment_status' => 'withdraw under review',
'updated_at' => Carbon::now('UTC')->toDateTimeString(),
]);
if ($ok) {
$withdrawn[] = $studentId;
$messages[] = "Withdrawn student {$studentId}.";
}
$invoice = $this->invoice->newQuery()
->where('parent_id', $parentId)
->where('school_year', $schoolYear)
->orderByDesc('created_at')
->first();
if ($invoice) {
$existingRefund = $this->refund->newQuery()
->where('parent_id', $parentId)
->where('invoice_id', $invoice['id'])
->where('school_year', $schoolYear)
->first();
if ($existingRefund) {
$this->refund->update((int) $existingRefund['id'], [
'reason' => 'Withdrawal under review for student ID ' . $studentId,
'note' => null,
'updated_by' => $this->getCurrentUserId(),
]);
} else {
$this->refund->insert([
'parent_id' => $parentId,
'invoice_id' => $invoice['id'],
'requested_at' => Carbon::now('UTC')->toDateTimeString(),
'school_year' => $schoolYear,
'status' => 'Pending review',
'reason' => 'Withdrawal under review for student ID ' . $studentId,
'request' => 'new',
'semester' => $semester,
'refund_paid_amount' => 0.0,
]);
}
}
}
}
return [
'enrolled' => $enrolled,
'withdrawn' => $withdrawn,
'messages' => $messages,
];
}
private function applyPromotionAssignment(int $studentId, string $year): void
{
try {
$row = $this->promotion->newQuery()
->where('student_id', $studentId)
->where('school_year_to', $year)
->first();
if (!$row) {
return;
}
$targetSectionId = (int) ($row['to_class_section_id'] ?? 0);
if ($targetSectionId <= 0) {
$base = $this->classSection->getBaseSectionByClassId((int) $row['to_class_id']);
if (!$base) {
return;
}
$targetSectionId = (int) ($base['class_section_id'] ?? 0);
}
if ($targetSectionId <= 0) {
return;
}
$semester = (string) $this->config->getConfig('semester');
$exists = $this->studentClass->newQuery()
->where('student_id', $studentId)
->where('school_year', $year)
->where('semester', $semester)
->first();
$payload = [
'student_id' => $studentId,
'class_section_id' => $targetSectionId,
'school_year' => $year,
'semester' => $semester,
'updated_by' => $this->getCurrentUserId(),
'updated_at' => Carbon::now('UTC')->toDateTimeString(),
];
if ($exists) {
$this->studentClass->update((int) $exists['id'], $payload);
} else {
$payload['created_at'] = Carbon::now('UTC')->toDateTimeString();
$this->studentClass->insert($payload);
}
$this->promotion->update((int) $row['id'], [
'status' => 'applied',
'updated_at' => Carbon::now('UTC')->toDateTimeString(),
'updated_by' => $this->getCurrentUserId(),
]);
} catch (\Throwable $e) {
log_message('error', 'applyPromotionAssignment (API) failed: ' . $e->getMessage());
}
}
public function show($id = null)
{
$parent = $this->user->newQuery()
->select('users.*')
->join('user_roles', 'user_roles.user_id', '=', 'users.id')
->join('roles', 'roles.id', '=', 'user_roles.role_id')
->where('roles.name', 'parent')
->where('users.id', $id)
->first();
if (!$parent) {
return $this->error('Parent not found', Response::HTTP_NOT_FOUND);
}
unset($parent['password'], $parent['token']);
return $this->success($parent, 'Parent retrieved successfully');
}
public function getStudents($id = null)
{
$parent = $this->user->find($id);
if (!$parent) {
return $this->error('Parent not found', Response::HTTP_NOT_FOUND);
}
$students = $this->student->newQuery()
->where('parent_id', $id)
->get()
->map(fn($row) => (array) $row)
->toArray();
foreach ($students as &$student) {
$classInfo = DB::table('student_class')
->select('class_section.*', 'classes.name as class_name')
->join('class_section', 'class_section.id', '=', 'student_class.class_section_id', 'left')
->join('classes', 'classes.id', '=', 'class_section.class_id', 'left')
->where('student_class.student_id', $student['id'])
->orderByDesc('student_class.school_year')
->first();
$student['current_class'] = $classInfo ? (array) $classInfo : null;
$student['medical_conditions'] = $this->medicalCondition
->where('student_id', $student['id'])
->findColumn('condition_name') ?? [];
$student['allergies'] = $this->allergy
->where('student_id', $student['id'])
->findColumn('allergy') ?? [];
}
return $this->success($students, 'Students retrieved successfully');
}
public function attendance($id)
{
try {
$year = $this->request->getGet('school_year') ?? $this->config->getConfig('school_year');
$records = DB::table('attendance_data')
->select('students.firstname', 'students.lastname', 'attendance_data.date', 'attendance_data.status', 'attendance_data.reason')
->join('students', 'students.id', '=', 'attendance_data.student_id')
->where('students.parent_id', $id)
->where('attendance_data.school_year', $year)
->orderByDesc('attendance_data.date')
->get()
->map(fn($row) => (array) $row)
->toArray();
return $this->success($records, 'Attendance data retrieved successfully');
} catch (Throwable $e) {
return $this->error('Error fetching attendance: ' . $e->getMessage());
}
}
public function payments($id)
{
$invoices = DB::table('invoices')
->where('parent_id', $id)
->orderByDesc('created_at')
->get()
->map(fn($row) => (array) $row)
->toArray();
return $this->success($invoices, 'Payments retrieved successfully');
}
public function enrollments($id)
{
$schoolYear = $this->request->getGet('school_year') ?? $this->config->getConfig('school_year');
$students = $this->student->where('parent_id', $id)->findAll();
$results = [];
foreach ($students as $student) {
$enrollment = $this->enrollment->newQuery()
->where('student_id', $student['id'])
->where('school_year', $schoolYear)
->orderByDesc('created_at')
->first();
$student['enrollment'] = $enrollment ?? [
'enrollment_status' => 'not enrolled',
'admission_status' => null,
];
$results[] = $student;
}
return $this->success($results, 'Enrollment data retrieved successfully');
}
public function enroll($id)
{
try {
$data = $this->payloadData();
if (empty($data['student_ids']) || !is_array($data['student_ids'])) {
return $this->error('Invalid student_ids payload', Response::HTTP_BAD_REQUEST);
}
$parentId = (int) $id;
$result = $this->processEnrollmentUpdate($parentId, array_map('intval', $data['student_ids']), []);
return $this->success($result, 'Enrollment submitted successfully');
} catch (Throwable $e) {
return $this->error('Failed to enroll: ' . $e->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
public function events($id)
{
$schoolYear = $this->config->getConfig('school_year');
$semester = $this->config->getConfig('semester');
$events = $this->event->getActiveEvents($schoolYear, $semester);
$charges = $this->charges->getChargesWithEventInfo($id, $schoolYear);
$chargeMap = [];
foreach ($charges as $charge) {
$chargeMap[$charge['student_id'] . ':' . $charge['event_id']] = $charge;
}
return $this->success([
'events' => $events,
'charges' => $chargeMap,
], 'Event participation data retrieved');
}
public function updateParticipation($id)
{
try {
$payload = $this->payloadData();
$participations = $payload['participation'] ?? [];
$schoolYear = $this->config->getConfig('school_year');
$semester = $this->config->getConfig('semester');
foreach ($participations as $key => $value) {
[$studentId, $eventId] = explode(':', $key);
$existing = $this->charges->where([
'parent_id' => $id,
'student_id' => $studentId,
'event_id' => $eventId,
])->first();
if ($value === 'no') {
if ($existing) {
$this->charges->delete($existing['id']);
}
continue;
}
$event = $this->event->getEvent($eventId, $schoolYear);
if ($existing) {
$this->charges->update($existing['id'], ['participation' => $value]);
} else {
$this->charges->insert([
'parent_id' => $id,
'student_id' => $studentId,
'event_id' => $eventId,
'participation' => $value,
'charged' => $event['amount'],
'school_year' => $schoolYear,
'semester' => $semester,
]);
}
}
return $this->success([], 'Participation updated successfully');
} catch (Throwable $e) {
return $this->error('Failed to update participation: ' . $e->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
public function profile($id)
{
$user = $this->user->find($id);
if (!$user) {
return $this->error('Parent not found', Response::HTTP_NOT_FOUND);
}
unset($user['password'], $user['token']);
return $this->success($user, 'Profile retrieved successfully');
}
public function updateProfile($id)
{
try {
$data = $this->payloadData();
if (!$data) {
return $this->error('Invalid JSON payload', Response::HTTP_BAD_REQUEST);
}
$allowed = ['firstname', 'lastname', 'cellphone', 'address_street', 'city', 'state', 'zip'];
$update = array_intersect_key($data, array_flip($allowed));
if (empty($update)) {
return $this->error('No valid fields to update', Response::HTTP_BAD_REQUEST);
}
$this->user->update($id, $update);
return $this->success([], 'Profile updated successfully');
} catch (Throwable $e) {
return $this->error('Failed to update profile: ' . $e->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
}