From 1977a513dff70020fde1913f86f334d194a532d5 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 26 Mar 2026 16:04:03 -0400 Subject: [PATCH] fix student registration --- .../EmergencyContactController.php | 29 +++++++- .../Api/Students/StudentController.php | 71 ++++++++++++++++--- .../Students/StudentUpdateRequest.php | 1 - .../EmergencyContactDirectoryService.php | 16 +++-- .../Students/StudentProfileService.php | 1 - resources/docs/controllers.md | 29 +++++++- resources/docs/openapi.json | 55 ++++++++++++-- routes/api.php | 1 + .../Api/V1/Students/StudentControllerTest.php | 44 ++++++++++++ 9 files changed, 220 insertions(+), 27 deletions(-) diff --git a/app/Http/Controllers/Api/Administrator/EmergencyContactController.php b/app/Http/Controllers/Api/Administrator/EmergencyContactController.php index 8053baca..5c685cf9 100644 --- a/app/Http/Controllers/Api/Administrator/EmergencyContactController.php +++ b/app/Http/Controllers/Api/Administrator/EmergencyContactController.php @@ -10,6 +10,8 @@ use App\Http\Resources\EmergencyContacts\EmergencyContactResource; use App\Services\EmergencyContacts\EmergencyContactCrudService; use App\Services\EmergencyContacts\EmergencyContactDirectoryService; use Illuminate\Http\JsonResponse; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\Validator; class EmergencyContactController extends Controller { @@ -19,9 +21,32 @@ class EmergencyContactController extends Controller ) { } - public function index(): JsonResponse + public function index(Request $request): JsonResponse { - $groups = $this->directoryService->groups(); + $validator = Validator::make($request->query->all(), [ + 'parent_id' => ['nullable', 'integer', 'min:1'], + 'parent_ids' => ['nullable', 'array'], + 'parent_ids.*' => ['integer', 'min:1'], + ]); + + if ($validator->fails()) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $validator->errors(), + ], 422); + } + + $payload = $validator->validated(); + $parentIds = []; + if (!empty($payload['parent_ids'])) { + $parentIds = array_values(array_unique(array_map('intval', $payload['parent_ids']))); + } + if (!empty($payload['parent_id'])) { + $parentIds[] = (int) $payload['parent_id']; + } + $parentIds = array_values(array_unique(array_filter($parentIds))); + + $groups = $this->directoryService->groups($parentIds ?: null); return response()->json([ 'ok' => true, diff --git a/app/Http/Controllers/Api/Students/StudentController.php b/app/Http/Controllers/Api/Students/StudentController.php index 5f8ef45e..9f023f77 100644 --- a/app/Http/Controllers/Api/Students/StudentController.php +++ b/app/Http/Controllers/Api/Students/StudentController.php @@ -30,6 +30,7 @@ use App\Services\Students\StudentScoreCardService; use App\Services\Students\StudentStatusService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Validator; @@ -133,7 +134,15 @@ class StudentController extends BaseApiController public function store(Request $request): JsonResponse { $data = array_merge($request->query->all(), $request->all(), $request->json()->all()); - $validator = Validator::make($data, [ + $parentId = (int) ($data['parent_id'] ?? 0); + $isFirstForParent = $parentId > 0 + && Student::query()->where('parent_id', $parentId)->count() === 0; + if (empty($data['school_id'])) { + $nextId = (int) Student::query()->max('id') + 1; + $data['school_id'] = $this->generateSchoolId($nextId); + } + + $rules = [ 'school_id' => ['nullable', 'string', 'max:50'], 'firstname' => ['required', 'string', 'max:150'], 'lastname' => ['required', 'string', 'max:150'], @@ -147,11 +156,25 @@ class StudentController extends BaseApiController 'parent_id' => ['nullable', 'integer', 'min:1'], 'registration_date' => ['nullable', 'date'], 'tuition_paid' => ['nullable', 'boolean'], - 'rfid_tag' => ['nullable', 'string', 'max:100'], 'semester' => ['nullable', 'string', 'max:50'], 'year_of_registration' => ['nullable', 'integer', 'min:1900'], 'school_year' => ['required', 'string', 'max:50'], - ]); + 'medical_conditions' => ['nullable', 'string'], + 'allergies' => ['nullable', 'string'], + ]; + + if ($isFirstForParent) { + $rules = array_merge($rules, [ + 'name' => ['required', 'string', 'max:150'], + 'cellphone' => ['required', 'string', 'max:30'], + 'email' => ['required', 'email', 'max:150'], + 'relation' => ['nullable', 'string', 'max:80'], + 'semester' => ['nullable', 'string', 'max:50'], + 'school_year' => ['nullable', 'string', 'max:50'], + ]); + } + + $validator = Validator::make($data, $rules); if ($validator->fails()) { return response()->json([ @@ -168,6 +191,34 @@ class StudentController extends BaseApiController $student->save(); } + $healthPayload = []; + if (array_key_exists('medical_conditions', $payload)) { + $healthPayload['medical_conditions'] = $payload['medical_conditions']; + } + if (array_key_exists('allergies', $payload)) { + $healthPayload['allergies'] = $payload['allergies']; + } + if (!empty($healthPayload)) { + $this->profileService->updateStudent($student->id, $healthPayload); + } + + if ($isFirstForParent) { + $contactPayload = [ + 'parent_id' => $parentId, + 'emergency_contact_name' => $payload['name'], + 'cellphone' => $payload['cellphone'], + 'email' => $payload['email'], + 'relation' => $payload['relation'] ?? null, + 'semester' => $payload['semester'] ?? null, + 'school_year' => $payload['school_year'] ?? null, + ]; + if (Schema::hasColumn('emergency_contacts', 'student_id')) { + $contactPayload['student_id'] = $student->id; + } + + EmergencyContact::query()->create($contactPayload); + } + return response()->json(['ok' => true, 'student' => $student], 201); } @@ -178,7 +229,7 @@ class StudentController extends BaseApiController (int) $payload['student_id'], $payload['class_section_id'], (bool) ($payload['is_event_only'] ?? false), - (int) (auth()->id() ?? 0) + (int) (Auth::id() ?? 0) ); $status = $result['ok'] ? 200 : 422; @@ -192,7 +243,7 @@ class StudentController extends BaseApiController $result = $this->assignmentService->removeClass( (int) $payload['student_id'], (int) $payload['class_section_id'], - (int) (auth()->id() ?? 0) + (int) (Auth::id() ?? 0) ); $status = $result['ok'] ? 200 : 422; @@ -218,7 +269,7 @@ class StudentController extends BaseApiController $result = $this->statusService->setActive( (int) $payload['student_id'], (bool) $payload['is_active'], - (int) (auth()->id() ?? 0) + (int) (Auth::id() ?? 0) ); $status = $result['ok'] ? 200 : 422; @@ -234,7 +285,7 @@ class StudentController extends BaseApiController (int) $payload['students_per_section'], $payload['school_year'] ?? null, (int) ($payload['class_section_id'] ?? 0), - (int) (auth()->id() ?? 0) + (int) (Auth::id() ?? 0) ); $status = $result['ok'] ? 200 : 422; @@ -340,7 +391,7 @@ class StudentController extends BaseApiController $studentId, $ids, (bool) ($payload['is_event_only'] ?? false), - (int) (auth()->id() ?? 0) + (int) (Auth::id() ?? 0) ); $status = $result['ok'] ? 200 : 422; @@ -353,7 +404,7 @@ class StudentController extends BaseApiController $result = $this->assignmentService->removeClass( $studentId, $classSectionId, - (int) (auth()->id() ?? 0) + (int) (Auth::id() ?? 0) ); $status = $result['ok'] ? 200 : 422; @@ -380,7 +431,7 @@ class StudentController extends BaseApiController $studentId, [(int) $payload['class_section_id']], false, - (int) (auth()->id() ?? 0) + (int) (Auth::id() ?? 0) ); $status = $result['ok'] ? 200 : 422; diff --git a/app/Http/Requests/Students/StudentUpdateRequest.php b/app/Http/Requests/Students/StudentUpdateRequest.php index 4cb30ea3..6282364f 100644 --- a/app/Http/Requests/Students/StudentUpdateRequest.php +++ b/app/Http/Requests/Students/StudentUpdateRequest.php @@ -22,7 +22,6 @@ class StudentUpdateRequest extends ApiFormRequest 'tuition_paid' => ['nullable', 'boolean'], 'year_of_registration' => ['nullable', 'string'], 'school_year' => ['nullable', 'string'], - 'rfid_tag' => ['nullable', 'string', 'max:100'], 'semester' => ['nullable', 'string'], 'is_new' => ['required', 'boolean'], 'is_active' => ['nullable', 'boolean'], diff --git a/app/Services/EmergencyContacts/EmergencyContactDirectoryService.php b/app/Services/EmergencyContacts/EmergencyContactDirectoryService.php index 10d60b4d..89d8c706 100644 --- a/app/Services/EmergencyContacts/EmergencyContactDirectoryService.php +++ b/app/Services/EmergencyContacts/EmergencyContactDirectoryService.php @@ -11,14 +11,16 @@ use Illuminate\Support\Facades\Schema; class EmergencyContactDirectoryService { - public function groups(): array + public function groups(?array $parentIds = null): array { - $parentIds = EmergencyContact::query() - ->distinct() - ->pluck('parent_id') - ->filter(static fn ($id) => (int) $id > 0) - ->values() - ->all(); + $parentIds = !empty($parentIds) + ? array_values(array_filter(array_map('intval', $parentIds), static fn ($id) => $id > 0)) + : EmergencyContact::query() + ->distinct() + ->pluck('parent_id') + ->filter(static fn ($id) => (int) $id > 0) + ->values() + ->all(); $groups = []; foreach ($parentIds as $parentId) { diff --git a/app/Services/Students/StudentProfileService.php b/app/Services/Students/StudentProfileService.php index 2bff3008..975e895a 100644 --- a/app/Services/Students/StudentProfileService.php +++ b/app/Services/Students/StudentProfileService.php @@ -30,7 +30,6 @@ class StudentProfileService 'tuition_paid' => $payload['tuition_paid'] ?? null, 'year_of_registration' => $payload['year_of_registration'] ?? null, 'school_year' => $payload['school_year'] ?? null, - 'rfid_tag' => $payload['rfid_tag'] ?? null, 'semester' => $payload['semester'] ?? null, 'is_new' => $payload['is_new'] ?? null, 'is_active' => $payload['is_active'] ?? null, diff --git a/resources/docs/controllers.md b/resources/docs/controllers.md index ec2cd3f0..d898303d 100755 --- a/resources/docs/controllers.md +++ b/resources/docs/controllers.md @@ -1779,8 +1779,8 @@ This document lists every API controller alphabetically with any inline route co **Endpoints:** - `GET /api/v1/students` (`index`) – Requires auth. Supports filters (`parent_id`, `parent_ids[]`). Returns list of students for the selected school year. - `GET /api/v1/students/{id}` (`show`) – Requires auth. Returns detailed student profile plus current class section information. -- `POST /api/v1/students` (`store`) – Requires auth. Creates a new student. Validates demographic fields (firstname, lastname, dob, gender, parent_id) and automatically stamps school year, semester, and registration date. -- `PATCH /api/v1/students/{id}` (`update`) – Requires auth. Updates student information. Accepts fields: `school_id`, `firstname`, `lastname`, `dob` (automatically calculates age), `gender`, `registration_grade`, `photo_consent`, `parent_id`, `registration_date`, `tuition_paid`, `year_of_registration`, `school_year`, `rfid_tag`, `semester`, `is_new`. Also supports health information: `medical_conditions` (comma/semicolon/newline separated list), `allergies` (comma/semicolon/newline separated list), `medical_touched`, `allergies_touched` (flags to indicate user interaction). Health lists are normalized, deduplicated, and synced with related tables. Only updates fields that are provided and non-empty. +- `POST /api/v1/students` (`store`) – Requires auth. Creates a new student. Validates demographic fields (firstname, lastname, dob, gender, parent_id). When the parent has no existing students, emergency contact fields are required (`name`, `cellphone`, `email`, optional `relation`) and an `emergency_contacts` record is created. Automatically stamps school year, semester, and registration date. +- `PATCH /api/v1/students/{id}` (`update`) – Requires auth. Updates student information. Accepts fields: `school_id`, `firstname`, `lastname`, `dob` (automatically calculates age), `gender`, `registration_grade`, `photo_consent`, `parent_id`, `registration_date`, `tuition_paid`, `year_of_registration`, `school_year`, `semester`, `is_new`. Also supports health information: `medical_conditions` (comma/semicolon/newline separated list), `allergies` (comma/semicolon/newline separated list), `medical_touched`, `allergies_touched` (flags to indicate user interaction). Health lists are normalized, deduplicated, and synced with related tables. Only updates fields that are provided and non-empty. - `DELETE /api/v1/students/{id}` (`destroy`) – Requires auth. Removes the student record. - `POST /api/v1/students/assign-class` (`assignClassStudent`) – Requires auth. Assigns a student to a class section. Accepts `student_id` and `class_section_id`. Updates `student_class` table, enrollment records, attendance data/records, and all score-related tables (homework, quiz, project, participation, midterm_exam, final_exam, final_score, semester_scores) for the current semester/year. Returns assignment details including attendance and score update statistics. - `POST /api/v1/students/auto-distribute` (`autoDistributeSections`) – Requires auth. Automatically distributes students from promotion queue into lettered sections for a class. Accepts `class_id` (or `class_section_id` to derive class_id), `students_per_section`, and optional `school_year`. Balances male/female distribution across sections. Only considers students with enrollment status 'payment pending' or 'enrolled'. Updates promotion queue and student_class records. Returns summary with distribution details per section (total, male, female counts). @@ -1794,6 +1794,31 @@ This document lists every API controller alphabetically with any inline route co - Health list normalization (removes duplicates, filters placeholders like "none", "n/a") - Transaction-safe operations with rollback on errors +**Student registration payload example:** +```json +{ + "school_year": "2025-2026", + "firstname": "Sara", + "lastname": "Ahmed", + "dob": "2016-03-25", + "age": 8, + "gender": "Female", + "is_active": true, + "registration_grade": "3", + "is_new": true, + "photo_consent": true, + "parent_id": 1, + "registration_date": "2026-03-25", + "tuition_paid": true, + "semester": "Fall", + "year_of_registration": 2026, + "name": "Amina Ahmed", + "cellphone": "5555555555", + "email": "amina.ahmed@example.com", + "relation": "Mother" +} +``` + ## SupplierController **File:** `app/Http/Controllers/Api/SupplierController.php` **Purpose:** CRUD for supplier/vendor records used by purchase orders and inventory management. diff --git a/resources/docs/openapi.json b/resources/docs/openapi.json index 9bf6c8ac..cbf86ad0 100755 --- a/resources/docs/openapi.json +++ b/resources/docs/openapi.json @@ -109,6 +109,12 @@ "type": "integer", "example": 42 }, + "school_year": { + "type": "string" + }, + "school_id": { + "type": "string" + }, "firstname": { "type": "string", "example": "Ahmed" @@ -122,13 +128,53 @@ "format": "date", "example": "2013-04-22" }, + "age": { + "type": "integer" + }, "gender": { "type": "string", "enum": ["male", "female"] }, + "is_active": { + "type": "boolean" + }, + "registration_grade": { + "type": "string" + }, + "is_new": { + "type": "boolean" + }, + "photo_consent": { + "type": "boolean" + }, "parent_id": { "type": "integer" }, + "registration_date": { + "type": "string", + "format": "date" + }, + "tuition_paid": { + "type": "boolean" + }, + "semester": { + "type": "string" + }, + "year_of_registration": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "cellphone": { + "type": "string" + }, + "email": { + "type": "string" + }, + "relation": { + "type": "string" + }, "current_class": { "type": "object", "nullable": true @@ -1747,9 +1793,12 @@ "parent_id": {"type": "integer"}, "registration_date": {"type": "string", "format": "date"}, "tuition_paid": {"type": "boolean"}, - "rfid_tag": {"type": "string"}, "semester": {"type": "string"}, - "year_of_registration": {"type": "integer"} + "year_of_registration": {"type": "integer"}, + "name": {"type": "string"}, + "cellphone": {"type": "string"}, + "email": {"type": "string"}, + "relation": {"type": "string"} } } } @@ -4221,7 +4270,6 @@ "parent_id": {"type": "integer"}, "registration_date": {"type": "string", "format": "date"}, "tuition_paid": {"type": "boolean"}, - "rfid_tag": {"type": "string"}, "semester": {"type": "string"}, "year_of_registration": {"type": "integer"} } @@ -4829,7 +4877,6 @@ "parent_id": {"type": "integer"}, "registration_date": {"type": "string", "format": "date"}, "tuition_paid": {"type": "boolean"}, - "rfid_tag": {"type": "string"}, "semester": {"type": "string"}, "year_of_registration": {"type": "integer"} } diff --git a/routes/api.php b/routes/api.php index 60311d3f..b92c0e8c 100755 --- a/routes/api.php +++ b/routes/api.php @@ -257,6 +257,7 @@ Route::prefix('v1')->group(function () { Route::middleware('auth:api')->group(function () { Route::get('dashboard/route', [DashboardController::class, 'route']); + Route::get('emergency-contacts', [AdministratorEmergencyContactController::class, 'index']); Route::prefix('utilities/phone')->group(function () { Route::post('format', [PhoneFormatterController::class, 'format']); diff --git a/tests/Feature/Api/V1/Students/StudentControllerTest.php b/tests/Feature/Api/V1/Students/StudentControllerTest.php index ccb9d698..d3e2a5d9 100644 --- a/tests/Feature/Api/V1/Students/StudentControllerTest.php +++ b/tests/Feature/Api/V1/Students/StudentControllerTest.php @@ -66,6 +66,50 @@ class StudentControllerTest extends TestCase $this->assertNotEmpty($response->json('rows')); } + public function test_store_creates_emergency_contact_for_first_parent(): void + { + $user = $this->seedUser(); + $parentId = 1234; + + Sanctum::actingAs($user, [], 'api'); + $response = $this->postJson('/api/v1/students', [ + 'school_year' => '2025-2026', + 'firstname' => 'Sara', + 'lastname' => 'Ahmed', + 'dob' => '2016-03-25', + 'age' => 8, + 'gender' => 'Female', + 'is_active' => true, + 'registration_grade' => '3', + 'is_new' => true, + 'photo_consent' => true, + 'parent_id' => $parentId, + 'registration_date' => '2026-03-25', + 'tuition_paid' => true, + 'semester' => 'Fall', + 'year_of_registration' => 2026, + 'name' => 'Amina Ahmed', + 'cellphone' => '5555555555', + 'email' => 'amina.ahmed@example.com', + 'relation' => 'Mother', + ]); + + $response->assertStatus(201); + $response->assertJson(['ok' => true]); + $this->assertDatabaseHas('students', [ + 'parent_id' => $parentId, + 'firstname' => 'Sara', + 'lastname' => 'Ahmed', + ]); + $this->assertDatabaseHas('emergency_contacts', [ + 'parent_id' => $parentId, + 'emergency_contact_name' => 'Amina Ahmed', + 'cellphone' => '5555555555', + 'email' => 'amina.ahmed@example.com', + 'relation' => 'Mother', + ]); + } + private function seedConfig(): void { DB::table('configuration')->insert([