Files
alrahma_sunday_school_api/app/Http/Controllers/Api/StudentController.php
T
2026-03-05 12:29:37 -05:00

905 lines
33 KiB
PHP
Executable File

<?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');
}
}