select( 'u.id', 'u.firstname', 'u.lastname', 'u.email', 'u.cellphone', 'u.status', DB::raw('COUNT(DISTINCT fg.family_id) as families_count'), DB::raw('COUNT(DISTINCT s.id) as selected_year_students_count'), DB::raw('COALESCE(SUM(DISTINCT i.balance), 0) as balance') ) ->leftJoin('family_guardians as fg', 'fg.user_id', '=', 'u.id') ->leftJoin('family_students as fs', 'fs.family_id', '=', 'fg.family_id') ->leftJoin('students as s', function ($join) use ($schoolYear) { $join->on('s.id', '=', 'fs.student_id') ->where('s.school_year', '=', $schoolYear); }) ->leftJoin('invoices as i', function ($join) use ($schoolYear) { $join->on('i.parent_id', '=', 'u.id') ->where('i.school_year', '=', $schoolYear); }) ->where($this->relevantParentConstraint($schoolYear)) ->groupBy('u.id', 'u.firstname', 'u.lastname', 'u.email', 'u.cellphone', 'u.status') ->orderBy('u.lastname') ->orderBy('u.firstname'); if ($search !== '') { $needle = '%'.str_replace(['%', '_'], ['\\%', '\\_'], $search).'%'; $query->where(function ($inner) use ($needle) { $inner->whereRaw("CONCAT_WS(' ', u.firstname, u.lastname) LIKE ?", [$needle]) ->orWhere('u.email', 'like', $needle) ->orWhere('u.cellphone', 'like', $needle) ->orWhereExists(function ($exists) use ($needle) { $exists->selectRaw('1') ->from('family_guardians as sfg') ->join('family_students as sfs', 'sfs.family_id', '=', 'sfg.family_id') ->join('students as ss', 'ss.id', '=', 'sfs.student_id') ->whereColumn('sfg.user_id', 'u.id') ->where(function ($student) use ($needle) { $student->whereRaw("CONCAT_WS(' ', ss.firstname, ss.lastname) LIKE ?", [$needle]); }); }); }); } return $query->paginate(max(1, min($perPage, 100))); } public function search(string $schoolYear, string $search, int $limit = 10): array { if (trim($search) === '') { return []; } return $this->index($schoolYear, $search, $limit) ->getCollection() ->map(fn ($row) => $this->parentSummary((array) $row, $schoolYear)) ->all(); } public function show(int $parentId, string $schoolYear): ?array { $parent = DB::table('users') ->select('id', 'firstname', 'lastname', 'email', 'cellphone', 'address_street', 'city', 'state', 'zip', 'status') ->where('id', $parentId) ->first(); if (! $parent) { return null; } $guardians = [ [ 'user_id' => $parentId, 'firstname' => $parent->firstname, 'lastname' => $parent->lastname, ], ]; $finance = $this->finance->loadFinancialsForParents([$parentId], $schoolYear); return [ 'parent' => (array) $parent, 'families' => $this->familiesForParent($parentId, $schoolYear), 'students' => $this->studentsForParent($parentId, $schoolYear), 'emergency_contacts' => $this->emergencyContactsForParents($guardians, $schoolYear), 'invoices' => $finance['invoices'], 'payments' => $finance['payments'], 'finance_summary' => $finance['summary'], ]; } public function updateParent(int $parentId, array $payload): ?array { $allowed = array_intersect_key($payload, array_flip([ 'firstname', 'lastname', 'email', 'cellphone', 'address_street', 'city', 'state', 'zip', 'status', ])); if ($allowed !== []) { $allowed['updated_at'] = now(); DB::table('users')->where('id', $parentId)->update($allowed); } $parent = DB::table('users')->where('id', $parentId)->first(); return $parent ? (array) $parent : null; } public function parentSummary(array $row, string $schoolYear): array { $balance = (float) ($row['balance'] ?? 0); $primaryFamily = DB::table('family_guardians as fg') ->join('families as f', 'f.id', '=', 'fg.family_id') ->where('fg.user_id', (int) ($row['id'] ?? 0)) ->orderByDesc('fg.is_primary') ->orderBy('f.household_name') ->select('f.id', 'f.household_name') ->first(); return [ 'id' => (int) ($row['id'] ?? 0), 'firstname' => $row['firstname'] ?? null, 'lastname' => $row['lastname'] ?? null, 'email' => $row['email'] ?? null, 'cellphone' => $row['cellphone'] ?? null, 'is_active' => strtolower((string) ($row['status'] ?? '')) === 'active', 'families_count' => (int) ($row['families_count'] ?? 0), 'students_count' => (int) ($row['selected_year_students_count'] ?? 0), 'selected_year_students_count' => (int) ($row['selected_year_students_count'] ?? 0), 'balance' => $balance, 'positive_unpaid_balance' => max(0, $balance), 'credit_balance' => $balance < 0 ? abs($balance) : 0.0, 'primary_family' => $primaryFamily ? (array) $primaryFamily : null, 'school_year' => $schoolYear, ]; } private function relevantParentConstraint(string $schoolYear): callable { return function ($query) use ($schoolYear) { $query->whereExists(function ($exists) use ($schoolYear) { $exists->selectRaw('1') ->from('family_guardians as rfg') ->join('family_students as rfs', 'rfs.family_id', '=', 'rfg.family_id') ->join('students as rs', 'rs.id', '=', 'rfs.student_id') ->whereColumn('rfg.user_id', 'u.id') ->where('rs.school_year', $schoolYear); })->orWhereExists(function ($exists) use ($schoolYear) { $exists->selectRaw('1') ->from('students as direct_students') ->whereColumn('direct_students.parent_id', 'u.id') ->where('direct_students.school_year', $schoolYear); })->orWhereExists(function ($exists) use ($schoolYear) { $exists->selectRaw('1') ->from('invoices as ri') ->whereColumn('ri.parent_id', 'u.id') ->where('ri.school_year', $schoolYear); })->orWhereExists(function ($exists) use ($schoolYear) { $exists->selectRaw('1') ->from('parent_accounts as pa') ->whereColumn('pa.parent_id', 'u.id') ->where('pa.school_year', $schoolYear); })->orWhereExists(function ($exists) use ($schoolYear) { $exists->selectRaw('1') ->from('emergency_contacts as ec') ->whereColumn('ec.parent_id', 'u.id') ->where('ec.school_year', $schoolYear); }); }; } private function familiesForParent(int $parentId, string $schoolYear): array { return DB::table('family_guardians as fg') ->join('families as f', 'f.id', '=', 'fg.family_id') ->where('fg.user_id', $parentId) ->whereExists(function ($exists) use ($schoolYear) { $exists->selectRaw('1') ->from('family_students as fs') ->join('students as s', 's.id', '=', 'fs.student_id') ->whereColumn('fs.family_id', 'f.id') ->where('s.school_year', $schoolYear); }) ->orderBy('f.household_name') ->select('f.*', 'fg.relation', 'fg.is_primary', 'fg.receive_emails', 'fg.receive_sms') ->get() ->map(fn ($row) => (array) $row) ->all(); } private function studentsForParent(int $parentId, string $schoolYear): array { $rows = DB::table('students as s') ->select('s.id', 's.firstname', 's.lastname', 's.parent_id') ->where('s.school_year', $schoolYear) ->where(function ($query) use ($parentId) { $query->where('s.parent_id', $parentId) ->orWhereExists(function ($exists) use ($parentId) { $exists->selectRaw('1') ->from('family_guardians as fg') ->join('family_students as fs', 'fs.family_id', '=', 'fg.family_id') ->whereColumn('fs.student_id', 's.id') ->where('fg.user_id', $parentId); }); }) ->orderBy('s.lastname') ->orderBy('s.firstname') ->get() ->map(fn ($row) => (array) $row) ->all(); foreach ($rows as &$row) { $studentId = (int) ($row['id'] ?? 0); $classSectionName = $studentId > 0 ? (string) (StudentClass::getClassSectionsByStudentId($studentId, $schoolYear) ?? '') : ''; $row['class_section_name'] = $classSectionName; $row['grade'] = $classSectionName; } unset($row); return $rows; } private function emergencyContactsForParents(array $guardians, string $schoolYear): array { $parentIds = array_values(array_filter(array_map( static fn ($g) => (int) ($g['user_id'] ?? 0), $guardians ))); if ($parentIds === []) { return []; } return DB::table('emergency_contacts') ->select('id', 'parent_id', 'emergency_contact_name', 'relation', 'cellphone', 'email', 'school_year', 'semester', 'created_at', 'updated_at') ->whereIn('parent_id', $parentIds) ->where('school_year', $schoolYear) ->orderByDesc('updated_at') ->get() ->map(fn ($row) => (array) $row) ->all(); } }