fix student registration
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"}
|
||||
}
|
||||
|
||||
@@ -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']);
|
||||
|
||||
@@ -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([
|
||||
|
||||
Reference in New Issue
Block a user