config = model(Configuration::class); $this->invoice = model(Invoice::class); $this->payment = model(Payment::class); $this->studentClass = model(StudentClass::class); $this->schoolYear = $this->config->getConfig('school_year'); } public function index() { $studentId = (int) ($this->request->getGet('student_id') ?? 0); $guardianId = (int) ($this->request->getGet('guardian_id') ?? 0); try { $data = [ 'student' => null, 'families' => [], 'guardians' => [], 'students' => [], 'search_students' => [], 'search_guardians' => [], ]; // Simple student list for select $data['students'] = DB::table('students') ->select('id', DB::raw("CONCAT(lastname, ', ', firstname) AS name")) ->orderBy('lastname') ->orderBy('firstname') ->get() ->map(fn($row) => (array) $row) ->toArray(); // Preload search datasets $data['search_students'] = DB::table('students') ->select('id', 'firstname', 'lastname') ->orderBy('lastname') ->orderBy('firstname') ->get() ->map(fn($row) => (array) $row) ->toArray(); $data['search_guardians'] = DB::table('family_guardians as fg') ->distinct() ->select('u.id', 'u.firstname', 'u.lastname', 'u.email', 'u.cellphone') ->join('users as u', 'u.id', '=', 'fg.user_id') ->orderBy('u.lastname') ->orderBy('u.firstname') ->get() ->map(fn($row) => (array) $row) ->toArray(); // If a guardian is provided, resolve one of their students if (!$studentId && $guardianId) { $row = DB::table('family_guardians as fg') ->select('s.id as student_id') ->join('family_students as fs', 'fs.family_id', '=', 'fg.family_id') ->join('students as s', 's.id', '=', 'fs.student_id') ->where('fg.user_id', $guardianId) ->orderBy('s.lastname') ->orderBy('s.firstname') ->first(); if (!$row) { $row = DB::table('students as s') ->select('s.id as student_id') ->where('s.parent_id', $guardianId) ->orderBy('s.lastname') ->orderBy('s.firstname') ->first(); } if ($row && !empty($row->student_id)) { $studentId = (int) $row->student_id; } } if ($studentId > 0) { $student = DB::table('students') ->select('id', 'firstname', 'lastname') ->where('id', $studentId) ->first(); if ($student) { $data['student'] = (array) $student; // Fetch all families for this student $families = DB::table('family_students as fs') ->select( 'f.id', 'f.family_code', 'f.household_name', 'f.address_line1', 'f.address_line2', 'f.city', 'f.state', 'f.postal_code', 'f.country', 'f.primary_phone', 'f.preferred_lang', 'f.preferred_contact_method', 'f.is_active', 'fs.is_primary_home' ) ->join('families as f', 'f.id', '=', 'fs.family_id') ->where('fs.student_id', $studentId) ->orderByDesc('fs.is_primary_home') ->orderBy('f.household_name') ->get() ->map(fn($row) => (array) $row) ->toArray(); // Enrich each family with guardians, students, and financials foreach ($families as &$fam) { $fid = (int) $fam['id']; // Guardians $guardians = DB::table('family_guardians as fg') ->select( 'u.id AS user_id', 'u.firstname', 'u.lastname', 'u.email', 'u.cellphone', 'u.address_street', 'u.city', 'u.state', 'u.zip', 'fg.relation', 'fg.is_primary', 'fg.receive_emails', 'fg.receive_sms' ) ->join('users as u', 'u.id', '=', 'fg.user_id') ->where('fg.family_id', $fid) ->orderByDesc('fg.is_primary') ->orderBy('u.lastname') ->orderBy('u.firstname') ->get() ->map(fn($row) => (array) $row) ->toArray(); $fam['guardians'] = $guardians; // Students in this family $studentsRows = DB::table('family_students as fs') ->select('s.id', 's.firstname', 's.lastname') ->join('students as s', 's.id', '=', 'fs.student_id') ->where('fs.family_id', $fid) ->orderBy('s.lastname') ->orderBy('s.firstname') ->get() ->map(fn($row) => (array) $row) ->toArray(); // Enrich with grade label if (!empty($studentsRows)) { foreach ($studentsRows as &$sr) { $sid = (int) ($sr['id'] ?? 0); $sr['grade'] = $sid && $this->schoolYear ? (string) ($this->studentClass->getClassSectionsByStudentId($sid, $this->schoolYear) ?? '') : ''; } unset($sr); } $fam['students'] = $studentsRows; // Financials: invoices and payments for all guardians $parentIds = array_map(static fn($g) => (int) ($g['user_id'] ?? 0), $guardians); $parentIds = array_values(array_filter($parentIds)); $fam['invoices'] = []; $fam['payments'] = []; $fam['finance_summary'] = [ 'invoices_count' => 0, 'total_amount' => 0.0, 'paid_amount' => 0.0, 'balance' => 0.0, ]; if (!empty($parentIds)) { // Invoices $invRows = DB::table('invoices') ->select( 'id', 'parent_id', 'invoice_number', 'status', 'total_amount', 'paid_amount', 'balance', 'issue_date', 'due_date' ) ->whereIn('parent_id', $parentIds) ->orderByDesc('issue_date') ->get() ->map(fn($row) => (array) $row) ->toArray(); $fam['invoices'] = $invRows; // Map invoice id -> number $invoiceMap = []; foreach ($invRows as $ir) { $invoiceMap[(int) $ir['id']] = (string) ($ir['invoice_number'] ?? ''); } $fam['invoice_map'] = $invoiceMap; // Aggregate summary foreach ($invRows as $ir) { $fam['finance_summary']['invoices_count']++; $fam['finance_summary']['total_amount'] += (float) ($ir['total_amount'] ?? 0); $fam['finance_summary']['paid_amount'] += (float) ($ir['paid_amount'] ?? 0); $fam['finance_summary']['balance'] += (float) ($ir['balance'] ?? 0); } // Recent payments (limit 10) $payRows = DB::table('payments') ->select( 'id', 'parent_id', 'invoice_id', 'paid_amount', 'balance', 'payment_method', 'payment_date', 'status' ) ->whereIn('parent_id', $parentIds) ->orderByDesc('payment_date') ->limit(10) ->get() ->map(fn($row) => (array) $row) ->toArray(); $fam['payments'] = $payRows; } } unset($fam); $data['families'] = $families; // Back-compat: also expose guardians of first family if (!empty($families)) { $familyId = (int) $families[0]['id']; $data['guardians'] = DB::table('family_guardians as fg') ->select( 'u.id', 'u.firstname', 'u.lastname', 'u.email', 'fg.relation', 'fg.is_primary', 'fg.receive_emails' ) ->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, 'Family admin data retrieved successfully'); } catch (\Throwable $e) { log_message('error', 'Family admin error: ' . $e->getMessage()); return $this->respondError('Failed to retrieve family admin data', Response::HTTP_INTERNAL_SERVER_ERROR); } } public function search() { $q = trim((string) ($this->request->getGet('q') ?? '')); if ($q === '') { return $this->success(['items' => []], 'Search completed'); } try { $qs = '%' . $q . '%'; // Students suggestions $students = DB::table('students') ->select('id', 'firstname', 'lastname') ->whereRaw("CONCAT_WS(' ', firstname, lastname) LIKE ?", [$qs]) ->orderBy('lastname') ->orderBy('firstname') ->limit(8) ->get() ->map(fn($row) => (array) $row) ->toArray(); // Parents/Guardians suggestions $guardians = DB::table('users') ->select('id', 'firstname', 'lastname', 'email', 'cellphone') ->where(function ($query) use ($qs) { $query->whereRaw("CONCAT_WS(' ', firstname, lastname) LIKE ?", [$qs]) ->orWhere('email', 'like', $qs) ->orWhere('cellphone', 'like', $qs); }) ->orderBy('lastname') ->orderBy('firstname') ->limit(8) ->get() ->map(fn($row) => (array) $row) ->toArray(); $items = []; foreach ($students as $s) { $items[] = [ 'type' => 'student', 'id' => (int) $s['id'], 'label' => trim(($s['firstname'] ?? '') . ' ' . ($s['lastname'] ?? '')), 'sub' => 'Student', ]; } foreach ($guardians as $g) { $items[] = [ 'type' => 'guardian', 'id' => (int) $g['id'], 'label' => trim(($g['firstname'] ?? '') . ' ' . ($g['lastname'] ?? '')), 'sub' => trim(($g['email'] ?? '') . ' ' . ($g['cellphone'] ?? '')), ]; } return $this->success(['items' => $items], 'Search completed successfully'); } catch (\Throwable $e) { log_message('error', 'Family admin search error: ' . $e->getMessage()); return $this->respondError('Failed to perform search', Response::HTTP_INTERNAL_SERVER_ERROR); } } public function card() { $studentId = (int) ($this->request->getGet('student_id') ?? 0); $guardianId = (int) ($this->request->getGet('guardian_id') ?? 0); $familyId = (int) ($this->request->getGet('family_id') ?? 0); try { if (!$familyId) { if ($studentId) { $row = DB::table('family_students as fs') ->select('f.id') ->join('families as f', 'f.id', '=', 'fs.family_id') ->where('fs.student_id', $studentId) ->orderByDesc('fs.is_primary_home') ->orderBy('f.household_name') ->first(); if ($row && !empty($row->id)) { $familyId = (int) $row->id; } } elseif ($guardianId) { // Try via guardians link $row = DB::table('family_guardians as fg') ->select('f.id') ->join('families as f', 'f.id', '=', 'fg.family_id') ->where('fg.user_id', $guardianId) ->orderBy('f.household_name') ->first(); if ($row && !empty($row->id)) { $familyId = (int) $row->id; } else { // Fallback via students.parent_id → family_students $row = DB::table('students as s') ->select('f.id') ->join('family_students as fs', 'fs.student_id', '=', 's.id') ->join('families as f', 'f.id', '=', 'fs.family_id') ->where('s.parent_id', $guardianId) ->orderByDesc('fs.is_primary_home') ->orderBy('f.household_name') ->first(); if ($row && !empty($row->id)) { $familyId = (int) $row->id; } } } } if (!$familyId) { return $this->respondError('Family not found', Response::HTTP_NOT_FOUND); } $family = DB::table('families as f') ->select( 'f.id', 'f.family_code', 'f.household_name', 'f.address_line1', 'f.address_line2', 'f.city', 'f.state', 'f.postal_code', 'f.country', 'f.primary_phone', 'f.preferred_lang', 'f.preferred_contact_method', 'f.is_active' ) ->where('f.id', $familyId) ->first(); if (!$family) { return $this->respondError('Family not found', Response::HTTP_NOT_FOUND); } $family = (array) $family; // Guardians $guardians = DB::table('family_guardians as fg') ->select( 'u.id AS user_id', 'u.firstname', 'u.lastname', 'u.email', 'u.cellphone', 'u.address_street', 'u.city', 'u.state', 'u.zip', '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(); $family['guardians'] = $guardians; // Students $studentsRows = DB::table('family_students as fs') ->select('s.id', 's.firstname', 's.lastname') ->join('students as s', 's.id', '=', 'fs.student_id') ->where('fs.family_id', $familyId) ->orderBy('s.lastname') ->orderBy('s.firstname') ->get() ->map(fn($row) => (array) $row) ->toArray(); if (!empty($studentsRows)) { foreach ($studentsRows as &$sr) { $sid = (int) ($sr['id'] ?? 0); $sr['grade'] = $sid && $this->schoolYear ? (string) ($this->studentClass->getClassSectionsByStudentId($sid, $this->schoolYear) ?? '') : ''; } unset($sr); } $family['students'] = $studentsRows; // Financials $parentIds = array_map(static fn($g) => (int) ($g['user_id'] ?? 0), $guardians); $parentIds = array_values(array_filter($parentIds)); $family['invoices'] = []; $family['payments'] = []; $family['finance_summary'] = [ 'invoices_count' => 0, 'total_amount' => 0.0, 'paid_amount' => 0.0, 'balance' => 0.0, ]; // Emergency contacts $family['emergency_contacts'] = []; if (!empty($parentIds)) { // Map guardian name by user_id $gmap = []; foreach ($guardians as $g) { $gid = (int) ($g['user_id'] ?? 0); if ($gid > 0) { $gmap[$gid] = trim(($g['firstname'] ?? '') . ' ' . ($g['lastname'] ?? '')); } } $ecRows = 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) ->orderByDesc('updated_at') ->get() ->map(fn($row) => (array) $row) ->toArray(); if (!empty($ecRows)) { foreach ($ecRows as &$ec) { $pid = (int) ($ec['parent_id'] ?? 0); $ec['parent_label'] = $gmap[$pid] ?? ('Parent #' . $pid); } unset($ec); $family['emergency_contacts'] = $ecRows; } // Invoices $invRows = DB::table('invoices') ->select( 'id', 'parent_id', 'invoice_number', 'status', 'total_amount', 'paid_amount', 'balance', 'issue_date', 'due_date' ) ->whereIn('parent_id', $parentIds) ->orderByDesc('issue_date') ->get() ->map(fn($row) => (array) $row) ->toArray(); $family['invoices'] = $invRows; $invoiceMap = []; foreach ($invRows as $ir) { $invoiceMap[(int) $ir['id']] = (string) ($ir['invoice_number'] ?? ''); } $family['invoice_map'] = $invoiceMap; foreach ($invRows as $ir) { $family['finance_summary']['invoices_count']++; $family['finance_summary']['total_amount'] += (float) ($ir['total_amount'] ?? 0); $family['finance_summary']['paid_amount'] += (float) ($ir['paid_amount'] ?? 0); $family['finance_summary']['balance'] += (float) ($ir['balance'] ?? 0); } // Payments $payRows = DB::table('payments') ->select( 'id', 'parent_id', 'invoice_id', 'paid_amount', 'balance', 'payment_method', 'payment_date', 'status' ) ->whereIn('parent_id', $parentIds) ->orderByDesc('payment_date') ->limit(10) ->get() ->map(fn($row) => (array) $row) ->toArray(); $family['payments'] = $payRows; } return $this->success(['family' => $family], 'Family card retrieved successfully'); } catch (\Throwable $e) { log_message('error', 'Family card error: ' . $e->getMessage()); return $this->respondError('Failed to retrieve family card', Response::HTTP_INTERNAL_SERVER_ERROR); } } }