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

652 lines
23 KiB
PHP
Executable File

<?php
namespace App\Http\Controllers\Api;
use App\Models\FamilyGuardian;
use App\Models\Family;
use App\Models\FamilyStudent;
use App\Models\Student;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpFoundation\Response;
class FamilyController extends BaseApiController
{
protected Family $family;
protected FamilyStudent $familyStudent;
protected FamilyGuardian $familyGuardian;
protected Student $student;
protected User $user;
public function __construct()
{
parent::__construct();
$this->family = model(Family::class);
$this->familyStudent = model(FamilyStudent::class);
$this->familyGuardian = model(FamilyGuardian::class);
$this->student = model(Student::class);
$this->user = model(User::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)));
$result = $this->paginate($this->family->newQuery()->orderBy('id'), $page, $perPage);
foreach ($result['data'] as &$family) {
$family['students'] = $this->familyStudent
->where('family_id', $family['id'])
->findAll();
$family['guardians'] = $this->familyGuardian
->where('family_id', $family['id'])
->findAll();
}
return $this->success($result, 'Families retrieved successfully');
}
public function show($id = null)
{
$family = $this->family->find($id);
if (!$family) {
return $this->respondError('Family not found', Response::HTTP_NOT_FOUND);
}
$family['students'] = $this->familyStudent->where('family_id', $id)->findAll();
$family['guardians'] = $this->familyGuardian->where('family_id', $id)->findAll();
return $this->success($family, 'Family retrieved successfully');
}
public function getByStudent($id = null)
{
$familyIds = $this->familyStudent->where('student_id', $id)->findColumn('family_id');
if (empty($familyIds)) {
return $this->success([], 'No families found for this student');
}
$families = $this->family->whereIn('id', $familyIds)->get();
$payload = [];
foreach ($families as $family) {
$family->students = $this->familyStudent->where('family_id', $family->id)->findAll();
$family->guardians = $this->familyGuardian->where('family_id', $family->id)->findAll();
$payload[] = $family;
}
return $this->success($payload, 'Families retrieved successfully');
}
public function getGuardians($id = null)
{
$family = $this->family->find($id);
if (!$family) {
return $this->respondError('Family not found', Response::HTTP_NOT_FOUND);
}
$guardians = $this->familyGuardian->where('family_id', $id)->findAll();
return $this->success($guardians, 'Guardians retrieved successfully');
}
/**
* Get families for a student
* GET /api/v1/families/by-student/{student_id}
*/
public function familiesByStudent(int $studentId)
{
$rows = DB::table('family_students as fs')
->select('f.*', 'fs.is_primary_home')
->join('families as f', 'f.id', '=', 'fs.family_id')
->where('fs.student_id', $studentId)
->where('f.is_active', 1)
->orderByDesc('fs.is_primary_home')
->orderBy('f.household_name')
->get()
->map(fn($row) => (array) $row)
->toArray();
return $this->success(['data' => $rows], 'Families retrieved successfully');
}
/**
* Get guardians for a family
* GET /api/v1/families/{family_id}/guardians
*/
public function guardiansByFamily(int $familyId)
{
$rows = DB::table('family_guardians as fg')
->select(
'u.id AS user_id', 'u.firstname', 'u.lastname', 'u.email',
'fg.relation', 'fg.is_primary', 'fg.receive_emails', 'fg.receive_sms'
)
->join('users as u', 'u.id', '=', 'fg.user_id')
->where('fg.family_id', $familyId)
->orderByDesc('fg.is_primary')
->orderBy('u.lastname')
->orderBy('u.firstname')
->get()
->map(fn($row) => (array) $row)
->toArray();
return $this->success(['data' => $rows], 'Guardians retrieved successfully');
}
/**
* Bootstrap families from existing students.parent_id
* POST /api/v1/families/bootstrap
*/
public function bootstrap()
{
// Guard: ensure required tables exist
foreach (['families', 'family_students', 'family_guardians'] as $t) {
if (!DB::getSchemaBuilder()->hasTable($t)) {
return $this->respondError(
"Missing required table '{$t}'. Run migrations.",
Response::HTTP_INTERNAL_SERVER_ERROR
);
}
}
try {
DB::beginTransaction();
// Get all distinct primary parents from students.parent_id
$primaryParents = DB::table('students')
->select('parent_id')
->whereNotNull('parent_id')
->distinct()
->get()
->map(fn($row) => (array) $row)
->toArray();
$created = 0;
$linkedStudents = 0;
$linkedGuardians = 0;
foreach ($primaryParents as $row) {
$primaryUserId = (int) $row['parent_id'];
if ($primaryUserId <= 0) continue;
$familyId = $this->ensureFamilyForPrimaryParent($primaryUserId, $created);
// Link all students of this primary parent
$students = DB::table('students')
->select('id')
->where('parent_id', $primaryUserId)
->get()
->map(fn($row) => (array) $row)
->toArray();
foreach ($students as $s) {
$linkedStudents += $this->attachStudentToFamily((int) $s['id'], $familyId);
}
// Ensure primary parent is guardian on that family
$linkedGuardians += $this->attachGuardianUser($familyId, $primaryUserId, 'primary', true, 1, 0);
}
DB::commit();
return $this->success([
'families_created' => $created,
'students_linked' => $linkedStudents,
'guardians_linked' => $linkedGuardians,
], 'Bootstrap completed successfully');
} catch (\Throwable $e) {
DB::rollBack();
log_message('error', 'Family bootstrap error: ' . $e->getMessage());
return $this->respondError('Failed to bootstrap families: ' . $e->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Attach second parent by user ID
* POST /api/v1/families/attach-second-by-user
*/
public function attachSecondByUser()
{
$data = $this->payloadData();
$studentId = (int) ($data['student_id'] ?? 0);
$userId = (int) ($data['user_id'] ?? 0);
$relation = (string) ($data['relation'] ?? 'secondary');
if (!$studentId || !$userId) {
return $this->respondError('student_id and user_id required', Response::HTTP_BAD_REQUEST);
}
$familyId = $this->familyIdFromStudentPrimary($studentId);
if (!$familyId) {
return $this->respondError('No primary family found for this student', Response::HTTP_NOT_FOUND);
}
$rows = $this->attachGuardianUser($familyId, $userId, $relation, false, 1, 0);
return $this->success([
'attached' => $rows,
'family_id' => $familyId,
], 'Guardian attached successfully');
}
/**
* Attach second parent by email (creates user if needed)
* POST /api/v1/families/attach-second-by-email
*/
public function attachSecondByEmail()
{
$data = $this->payloadData();
$studentId = (int) ($data['student_id'] ?? 0);
$email = trim((string) ($data['email'] ?? ''));
$first = trim((string) ($data['firstname'] ?? ''));
$last = trim((string) ($data['lastname'] ?? ''));
$relation = (string) ($data['relation'] ?? 'secondary');
if (!$studentId || !$email) {
return $this->respondError('student_id and email required', Response::HTTP_BAD_REQUEST);
}
$familyId = $this->familyIdFromStudentPrimary($studentId);
if (!$familyId) {
return $this->respondError('No primary family found for this student', Response::HTTP_NOT_FOUND);
}
$user = $this->user->where('email', $email)->first();
if (!$user) {
// Create a minimal/stub user
$newUser = $this->user->create([
'firstname' => $first ?: '',
'lastname' => $last ?: '',
'email' => $email,
'status' => 'Active',
'user_type' => 'secondary',
]);
if (!$newUser || !isset($newUser['id'])) {
return $this->respondError('Failed to create user', Response::HTTP_INTERNAL_SERVER_ERROR);
}
$userId = (int) $newUser['id'];
} else {
$userId = (int) $user['id'];
}
$rows = $this->attachGuardianUser($familyId, $userId, $relation, false, 1, 0);
return $this->success([
'attached' => $rows,
'family_id' => $familyId,
'user_id' => $userId,
], 'Guardian attached successfully');
}
/**
* Set primary home flag for a student-family relationship
* POST /api/v1/families/set-primary-home
*/
public function setPrimaryHome()
{
$data = $this->payloadData();
$familyId = (int) ($data['family_id'] ?? 0);
$studentId = (int) ($data['student_id'] ?? 0);
$flag = (int) ($data['is_primary_home'] ?? 0);
if (!$familyId || !$studentId) {
return $this->respondError('family_id and student_id required', Response::HTTP_BAD_REQUEST);
}
try {
$this->familyStudent
->where(['family_id' => $familyId, 'student_id' => $studentId])
->update(['is_primary_home' => $flag ? 1 : 0]);
return $this->success(null, 'Primary home flag updated successfully');
} catch (\Throwable $e) {
log_message('error', 'Set primary home error: ' . $e->getMessage());
return $this->respondError('Failed to update primary home flag', Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Set guardian flags (receive_emails, is_primary, receive_sms, relation)
* POST /api/v1/families/set-guardian-flags
*/
public function setGuardianFlags()
{
$data = $this->payloadData();
$familyId = (int) ($data['family_id'] ?? 0);
$userId = (int) ($data['user_id'] ?? 0);
if (!$familyId || !$userId) {
return $this->respondError('family_id and user_id required', Response::HTTP_BAD_REQUEST);
}
$updateData = [];
foreach (['receive_emails', 'is_primary', 'receive_sms', 'relation'] as $k) {
if (isset($data[$k])) {
$val = $data[$k];
$updateData[$k] = in_array($k, ['receive_emails', 'is_primary', 'receive_sms'])
? (int) $val
: (string) $val;
}
}
if (empty($updateData)) {
return $this->respondError('No flags provided', Response::HTTP_BAD_REQUEST);
}
try {
$this->familyGuardian
->where(['family_id' => $familyId, 'user_id' => $userId])
->update($updateData);
return $this->success(null, 'Guardian flags updated successfully');
} catch (\Throwable $e) {
log_message('error', 'Set guardian flags error: ' . $e->getMessage());
return $this->respondError('Failed to update guardian flags', Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Unlink a guardian from a family
* POST /api/v1/families/unlink-guardian
*/
public function unlinkGuardian()
{
$data = $this->payloadData();
$familyId = (int) ($data['family_id'] ?? 0);
$userId = (int) ($data['user_id'] ?? 0);
if (!$familyId || !$userId) {
return $this->respondError('family_id and user_id required', Response::HTTP_BAD_REQUEST);
}
try {
$this->familyGuardian
->where(['family_id' => $familyId, 'user_id' => $userId])
->delete();
return $this->success(null, 'Guardian unlinked successfully');
} catch (\Throwable $e) {
log_message('error', 'Unlink guardian error: ' . $e->getMessage());
return $this->respondError('Failed to unlink guardian', Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Unlink a student from a family
* POST /api/v1/families/unlink-student
*/
public function unlinkStudent()
{
$data = $this->payloadData();
$familyId = (int) ($data['family_id'] ?? 0);
$studentId = (int) ($data['student_id'] ?? 0);
if (!$familyId || !$studentId) {
return $this->respondError('family_id and student_id required', Response::HTTP_BAD_REQUEST);
}
try {
$this->familyStudent
->where(['family_id' => $familyId, 'student_id' => $studentId])
->delete();
return $this->success(null, 'Student unlinked successfully');
} catch (\Throwable $e) {
log_message('error', 'Unlink student error: ' . $e->getMessage());
return $this->respondError('Failed to unlink student', Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Import second parents from legacy 'parents' table
* POST /api/v1/families/import-second-parents
*/
public function importSecondParentsFromLegacy()
{
$L = [
'table' => 'parents',
'firstparent_id' => 'firstparent_id',
'second_user_id' => 'secondparent_id',
'second_email' => 'secondparent_email',
'second_firstname' => 'secondparent_firstname',
'second_lastname' => 'secondparent_lastname',
];
if (!DB::getSchemaBuilder()->hasTable($L['table'])) {
return $this->respondError("Legacy table '{$L['table']}' not found", Response::HTTP_NOT_FOUND);
}
foreach (['families', 'family_students', 'family_guardians'] as $t) {
if (!DB::getSchemaBuilder()->hasTable($t)) {
return $this->respondError(
"Missing required table '{$t}'. Run migrations.",
Response::HTTP_INTERNAL_SERVER_ERROR
);
}
}
try {
$rows = DB::table($L['table'])
->select(
DB::raw("{$L['firstparent_id']} AS first_id"),
DB::raw("{$L['second_user_id']} AS second_id"),
DB::raw("{$L['second_email']} AS email"),
DB::raw("{$L['second_firstname']} AS firstname"),
DB::raw("{$L['second_lastname']} AS lastname")
)
->where(function ($query) use ($L) {
$query->where($L['second_user_id'], '!=', 0)
->orWhere($L['second_email'], '!=', '');
})
->get()
->map(fn($row) => (array) $row)
->toArray();
$createdUsers = 0;
$linked = 0;
$skipped = 0;
foreach ($rows as $r) {
$firstId = (int) ($r['first_id'] ?? 0);
$secondId = (int) ($r['second_id'] ?? 0);
$email = trim((string) ($r['email'] ?? ''));
if (!$firstId || (!$secondId && $email === '')) {
$skipped++;
continue;
}
// Ensure/find family by primary parent
$code = 'FAM-' . $firstId;
$fam = $this->family->where('family_code', $code)->first();
if ($fam) {
$familyId = (int) $fam['id'];
} else {
$tmp = 0; // counter sink
$familyId = $this->ensureFamilyForPrimaryParent($firstId, $tmp);
}
// Resolve/create user
$userId = 0;
if ($secondId > 0) {
$userId = $secondId;
} else {
$user = $this->user->where('email', $email)->first();
if (!$user) {
$newUser = $this->user->create([
'firstname' => $r['firstname'] ?? '',
'lastname' => $r['lastname'] ?? '',
'email' => $email,
'status' => 'Active',
'user_type' => 'secondary',
]);
if ($newUser && isset($newUser['id'])) {
$userId = (int) $newUser['id'];
$createdUsers++;
} else {
$userId = 0;
}
} else {
$userId = (int) $user['id'];
}
}
if (!$userId) {
$skipped++;
continue;
}
// Ensure all students for this primary parent are linked to this family
$stuRows = DB::table('students')
->select('id')
->where('parent_id', $firstId)
->get()
->map(fn($row) => (array) $row)
->toArray();
foreach ($stuRows as $sr) {
$sid = (int) ($sr['id'] ?? 0);
if ($sid > 0) {
$this->attachStudentToFamily($sid, $familyId);
}
}
// Link as guardian (idempotent)
$linked += $this->attachGuardianUser($familyId, $userId, 'guardian', false, 1, 0);
}
return $this->success([
'created_users' => $createdUsers,
'guardians_linked' => $linked,
'skipped' => $skipped,
'total_source' => count($rows),
], 'Legacy import completed successfully');
} catch (\Throwable $e) {
log_message('error', 'Import second parents error: ' . $e->getMessage());
return $this->respondError('Failed to import second parents: ' . $e->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/* =========================================================
* Private helpers
* =======================================================*/
/**
* Ensure there is exactly one family per primary parent (users.id)
* Returns $familyId and increments $createdCounter by reference if newly created.
*/
protected function ensureFamilyForPrimaryParent(int $primaryUserId, int &$createdCounter): int
{
$code = 'FAM-' . $primaryUserId;
$row = $this->family->where('family_code', $code)->first();
if ($row) {
return (int) $row['id'];
}
$family = $this->family->create([
'family_code' => $code,
'household_name' => 'Family of User ' . $primaryUserId,
'is_active' => 1,
]);
if ($family && isset($family['id'])) {
$createdCounter++;
return (int) $family['id'];
}
// If create failed, try to find it again (race condition)
$row = $this->family->where('family_code', $code)->first();
if ($row) {
return (int) $row['id'];
}
throw new \Exception('Failed to create family for primary parent ' . $primaryUserId);
}
/**
* Attach a student to a family (idempotent; returns rows affected >0 if inserted)
*/
protected function attachStudentToFamily(int $studentId, int $familyId): int
{
// Check if already exists
$exists = $this->familyStudent
->where('family_id', $familyId)
->where('student_id', $studentId)
->first();
if ($exists) {
return 0;
}
// Insert
$result = $this->familyStudent->create([
'family_id' => $familyId,
'student_id' => $studentId,
'is_primary_home' => 1,
]);
return ($result && isset($result['id'])) ? 1 : 0;
}
/**
* Attach a guardian user to family (idempotent)
*/
protected function attachGuardianUser(
int $familyId,
int $userId,
string $relation = 'primary',
bool $isPrimary = false,
int $receiveEmails = 1,
int $receiveSms = 0
): int {
// Check if already exists
$exists = $this->familyGuardian
->where('family_id', $familyId)
->where('user_id', $userId)
->first();
if ($exists) {
return 0;
}
// Insert
$result = $this->familyGuardian->create([
'family_id' => $familyId,
'user_id' => $userId,
'relation' => $relation,
'is_primary' => $isPrimary ? 1 : 0,
'receive_emails' => $receiveEmails,
'receive_sms' => $receiveSms,
]);
return ($result && isset($result['id'])) ? 1 : 0;
}
/**
* Find the "primary" family for a student = family created from parent_id
*/
protected function familyIdFromStudentPrimary(int $studentId): ?int
{
// Get primary parent id for the student
$st = $this->student->select('parent_id')->find($studentId);
if (!$st || empty($st['parent_id'])) {
return null;
}
// The canonical family uses code FAM-{parent_id}
$code = 'FAM-' . (int) $st['parent_id'];
$row = $this->family->select('id')->where('family_code', $code)->first();
if ($row) {
return (int) $row['id'];
}
// If not found, fall back to any family linked already
$row = DB::table('family_students')
->select('family_id')
->where('student_id', $studentId)
->orderByDesc('is_primary_home')
->first();
return $row ? (int) $row->family_id : null;
}
}