select('user_id', DB::raw('COUNT(DISTINCT family_id) AS families_count')) ->groupBy('user_id'); $familyStudentMap = DB::table('family_guardians as pfg') ->join('family_students as pfs', 'pfs.family_id', '=', 'pfg.family_id') ->joinSub($this->schoolYearStudentIds($schoolYear), 'pys', function ($join): void { $join->on('pys.student_id', '=', 'pfs.student_id'); }) ->selectRaw('pfg.user_id AS parent_id, pfs.student_id AS student_id'); $directStudentMap = DB::table('students as ps') ->joinSub($this->schoolYearStudentIds($schoolYear), 'dys', function ($join): void { $join->on('dys.student_id', '=', 'ps.id'); }) ->whereNotNull('ps.parent_id') ->selectRaw('ps.parent_id AS parent_id, ps.id AS student_id'); $parentStudentCounts = DB::query() ->fromSub( $familyStudentMap->union($directStudentMap), 'parent_students' ) ->select( 'parent_id', DB::raw('COUNT(DISTINCT student_id) AS selected_year_students_count') ) ->groupBy('parent_id'); $invoiceBalances = DB::table('invoices') ->select('parent_id', DB::raw('COALESCE(SUM(balance), 0) AS balance')) ->whereNotNull('parent_id'); $this->applySchoolYearFilter($invoiceBalances, 'school_year', $schoolYear); $invoiceBalances->groupBy('parent_id'); $query = DB::table('users as u') ->leftJoinSub($familyCounts, 'fc', function ($join): void { $join->on('fc.user_id', '=', 'u.id'); }) ->leftJoinSub($parentStudentCounts, 'psc', function ($join): void { $join->on('psc.parent_id', '=', 'u.id'); }) ->leftJoinSub($invoiceBalances, 'ib', function ($join): void { $join->on('ib.parent_id', '=', 'u.id'); }) ->select( 'u.id', 'u.firstname', 'u.lastname', 'u.email', 'u.cellphone', 'u.status', DB::raw('COALESCE(fc.families_count, 0) AS families_count'), DB::raw('COALESCE(psc.selected_year_students_count, 0) AS selected_year_students_count'), DB::raw('COALESCE(ib.balance, 0) AS balance') ) ->where($this->relevantParentConstraint($schoolYear)) ->orderBy('u.lastname') ->orderBy('u.firstname'); $search = trim($search); if ($search !== '') { $needle = '%'.str_replace(['%', '_'], ['\\%', '\\_'], $search).'%'; $query->where(function ($inner) use ($needle, $schoolYear): void { $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, $schoolYear): void { $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') ->whereRaw("CONCAT_WS(' ', ss.firstname, ss.lastname) LIKE ?", [$needle]); $this->constrainStudentToSchoolYear( $exists, 'ss.id', $schoolYear ); }) ->orWhereExists(function ($exists) use ($needle, $schoolYear): void { $exists->selectRaw('1') ->from('students as direct_search_students') ->whereColumn('direct_search_students.parent_id', 'u.id') ->whereRaw( "CONCAT_WS(' ', direct_search_students.firstname, direct_search_students.lastname) LIKE ?", [$needle] ); $this->constrainStudentToSchoolYear( $exists, 'direct_search_students.id', $schoolYear ); }); }); } 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), '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 { $schoolYearId = $this->schoolYearId($schoolYear); return function ($query) use ($schoolYear, $schoolYearId): void { $query ->whereExists(function ($exists) use ($schoolYear): void { $exists->selectRaw('1') ->from('family_guardians as rfg') ->join('family_students as rfs', 'rfs.family_id', '=', 'rfg.family_id') ->whereColumn('rfg.user_id', 'u.id'); $this->constrainStudentToSchoolYear( $exists, 'rfs.student_id', $schoolYear ); }) ->orWhereExists(function ($exists) use ($schoolYear): void { $exists->selectRaw('1') ->from('students as direct_students') ->whereColumn('direct_students.parent_id', 'u.id'); $this->constrainStudentToSchoolYear( $exists, 'direct_students.id', $schoolYear ); }) ->orWhereExists(function ($exists) use ($schoolYear): void { $exists->selectRaw('1') ->from('invoices as ri') ->whereColumn('ri.parent_id', 'u.id'); $this->applySchoolYearFilter( $exists, 'ri.school_year', $schoolYear ); }); if (! Schema::hasTable('parent_accounts')) { return; } if ($this->isAllSchoolYears($schoolYear)) { $query->orWhereExists(function ($exists): void { $exists->selectRaw('1') ->from('parent_accounts as pa') ->whereColumn('pa.parent_id', 'u.id'); }); } elseif ( $schoolYearId !== null && Schema::hasColumn('parent_accounts', 'school_year_id') ) { $query->orWhereExists(function ($exists) use ($schoolYearId): void { $exists->selectRaw('1') ->from('parent_accounts as pa') ->whereColumn('pa.parent_id', 'u.id') ->where('pa.school_year_id', $schoolYearId); }); } }; } 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): void { $exists->selectRaw('1') ->from('family_students as fs') ->whereColumn('fs.family_id', 'f.id'); $this->constrainStudentToSchoolYear( $exists, 'fs.student_id', $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 { $rowsQuery = DB::table('students as s') ->select('s.id', 's.firstname', 's.lastname', 's.parent_id') ->where(function ($query) use ($parentId): void { $query ->where('s.parent_id', $parentId) ->orWhereExists(function ($exists) use ($parentId): void { $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); }); }); $this->constrainStudentToSchoolYear( $rowsQuery, 's.id', $schoolYear ); $rows = $rowsQuery ->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): array { $parentIds = array_values(array_filter(array_map( static fn ($guardian) => (int) ($guardian['user_id'] ?? 0), $guardians ))); if ($parentIds === []) { return []; } return DB::table('emergency_contacts') ->select( 'id', 'parent_id', 'emergency_contact_name', 'relation', 'cellphone', 'email', 'created_at', 'updated_at' ) ->whereIn('parent_id', $parentIds) ->orderByDesc('updated_at') ->get() ->map(fn ($row) => (array) $row) ->all(); } private function schoolYearStudentIds(string $schoolYear): QueryBuilder { $studentClass = DB::table('student_class') ->select('student_id'); $enrollments = DB::table('enrollments') ->select('student_id'); if (! $this->isAllSchoolYears($schoolYear)) { $studentClass->where('school_year', $schoolYear); $enrollments->where('school_year', $schoolYear); } return $studentClass->union($enrollments); } private function constrainStudentToSchoolYear( QueryBuilder $query, string $studentIdColumn, string $schoolYear ): void { if ($this->isAllSchoolYears($schoolYear)) { return; } $query->where(function ($yearQuery) use ($studentIdColumn, $schoolYear): void { $yearQuery ->whereExists(function ($exists) use ($studentIdColumn, $schoolYear): void { $exists->selectRaw('1') ->from('student_class as year_sc') ->whereColumn('year_sc.student_id', $studentIdColumn) ->where('year_sc.school_year', $schoolYear); }) ->orWhereExists(function ($exists) use ($studentIdColumn, $schoolYear): void { $exists->selectRaw('1') ->from('enrollments as year_e') ->whereColumn('year_e.student_id', $studentIdColumn) ->where('year_e.school_year', $schoolYear); }); }); } private function schoolYearId(string $schoolYear): ?int { if ($this->isAllSchoolYears($schoolYear) || ! Schema::hasTable('school_years')) { return null; } $schoolYear = trim($schoolYear); foreach (['school_year', 'name', 'year', 'label'] as $column) { if (! Schema::hasColumn('school_years', $column)) { continue; } $id = DB::table('school_years') ->where($column, $schoolYear) ->value('id'); if ($id !== null) { return (int) $id; } } return null; } private function applySchoolYearFilter( QueryBuilder $query, string $column, string $schoolYear ): void { if (! $this->isAllSchoolYears($schoolYear)) { $query->where($column, $schoolYear); } } private function isAllSchoolYears(string $schoolYear): bool { $schoolYear = trim($schoolYear); return $schoolYear === '' || strtolower($schoolYear) === 'all'; } }