request->getGet('student_id') ?? 0); $guardianId = (int) ($this->request->getGet('guardian_id') ?? 0); $data = ['student' => null, 'families' => [], 'guardians' => [], 'students' => []]; // Simple student list for select $data['students'] = $db->query("SELECT id, CONCAT(lastname, ', ', firstname) AS name FROM students ORDER BY lastname, firstname")->getResultArray(); // Preload search datasets (students + guardians/parents) $data['searchStudents'] = $db->query("SELECT id, firstname, lastname FROM students ORDER BY lastname, firstname")->getResultArray(); $data['searchGuardians'] = $db->query( "SELECT DISTINCT u.id, u.firstname, u.lastname, u.email, u.cellphone FROM family_guardians fg JOIN users u ON u.id = fg.user_id ORDER BY u.lastname, u.firstname" )->getResultArray(); // If a guardian is provided, resolve one of their students and redirect to use existing flow if (!$studentId && $guardianId) { $row = $db->query( "SELECT s.id AS student_id FROM family_guardians fg JOIN family_students fs ON fs.family_id = fg.family_id JOIN students s ON s.id = fs.student_id WHERE fg.user_id = ? ORDER BY s.lastname, s.firstname LIMIT 1", [$guardianId] )->getRowArray(); if (!$row) { $row = $db->query( "SELECT s.id AS student_id FROM students s WHERE s.parent_id = ? ORDER BY s.lastname, s.firstname LIMIT 1", [$guardianId] )->getRowArray(); } if ($row && !empty($row['student_id'])) { return redirect()->to(site_url('family?student_id='.(int)$row['student_id'])); } } if ($studentId) { $data['student'] = $db->query("SELECT id, firstname, lastname FROM students WHERE id = ?", [$studentId])->getRowArray(); // Fetch all families for this student with extended fields $families = $db->query( "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 FROM family_students fs JOIN families f ON f.id = fs.family_id WHERE fs.student_id = ? ORDER BY fs.is_primary_home DESC, f.household_name", [$studentId] )->getResultArray(); // Enrich each family with guardians, students, and financials $invoiceModel = new InvoiceModel(); $paymentModel = new PaymentModel(); $studentClassModel = new StudentClassModel(); $configModel = new ConfigurationModel(); $schoolYear = (string) ($configModel->getConfig('school_year') ?? ''); foreach ($families as &$fam) { $fid = (int) $fam['id']; // Guardians (with contact info) $guardians = $db->query( "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 FROM family_guardians fg JOIN users u ON u.id = fg.user_id WHERE fg.family_id = ? ORDER BY fg.is_primary DESC, u.lastname, u.firstname", [$fid] )->getResultArray(); $fam['guardians'] = $guardians; // Students in this family $studentsRows = $db->query( "SELECT s.id, s.firstname, s.lastname FROM family_students fs JOIN students s ON s.id = fs.student_id WHERE fs.family_id = ? ORDER BY s.lastname, s.firstname", [$fid] )->getResultArray(); // Enrich with grade label if available if (!empty($studentsRows)) { foreach ($studentsRows as &$sr) { $sid = (int) ($sr['id'] ?? 0); $sr['grade'] = $sid ? (string) ($studentClassModel->getClassSectionsByStudentId($sid, $schoolYear) ?? '') : ''; } unset($sr); } $fam['students'] = $studentsRows; // Financials: invoices and payments for all guardians (by user_id) $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) ->orderBy('issue_date', 'DESC') ->get()->getResultArray(); $fam['invoices'] = $invRows; // Map invoice id -> number for payments table $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) ->orderBy('payment_date', 'DESC') ->limit(10) ->get()->getResultArray(); $fam['payments'] = $payRows; } } unset($fam); $data['families'] = $families; // Back-compat: also expose guardians of first family for old UI pieces if (!empty($families)) { $familyId = (int)$families[0]['id']; $data['guardians'] = $db->query( "SELECT u.id, u.firstname, u.lastname, u.email, fg.relation, fg.is_primary, fg.receive_emails FROM family_guardians fg JOIN users u ON u.id = fg.user_id WHERE fg.family_id = ? ORDER BY fg.is_primary DESC, u.lastname, u.firstname", [$familyId] )->getResultArray(); } } return service('response')->setBody(view('family/index', $data)); } // GET /family/search?q=.. public function search(): ResponseInterface { $db = \Config\Database::connect(); $q = trim((string)($this->request->getGet('q') ?? '')); if ($q === '') { return $this->response->setJSON(['items' => []]); } $qs = '%' . $db->escapeLikeString($q) . '%'; // Students suggestions $students = $db->query( "SELECT id, firstname, lastname FROM students WHERE CONCAT_WS(' ', firstname, lastname) LIKE ? ORDER BY lastname, firstname LIMIT 8", [$qs] )->getResultArray(); // Parents/Guardians suggestions (users) $guardians = $db->query( "SELECT id, firstname, lastname, email, cellphone FROM users WHERE ( CONCAT_WS(' ', firstname, lastname) LIKE ? OR email LIKE ? OR cellphone LIKE ? ) ORDER BY lastname, firstname LIMIT 8", [$qs, $qs, $qs] )->getResultArray(); $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->response->setJSON(['items' => $items]); } // GET /family/card?student_id=.. | guardian_id=.. | family_id=.. public function card(): ResponseInterface { $db = \Config\Database::connect(); $studentId = (int) ($this->request->getGet('student_id') ?? 0); $guardianId = (int) ($this->request->getGet('guardian_id') ?? 0); $familyId = (int) ($this->request->getGet('family_id') ?? 0); if (!$familyId) { if ($studentId) { $row = $db->query( "SELECT f.id FROM family_students fs JOIN families f ON f.id = fs.family_id WHERE fs.student_id = ? ORDER BY fs.is_primary_home DESC, f.household_name LIMIT 1", [$studentId] )->getRowArray(); if (!empty($row['id'])) $familyId = (int) $row['id']; } elseif ($guardianId) { // 1) Try via guardians link $row = $db->query( "SELECT f.id FROM family_guardians fg JOIN families f ON f.id = fg.family_id WHERE fg.user_id = ? ORDER BY f.household_name LIMIT 1", [$guardianId] )->getRowArray(); if (!empty($row['id'])) { $familyId = (int) $row['id']; } else { // 2) Fallback via students.parent_id → family_students $row = $db->query( "SELECT f.id FROM students s JOIN family_students fs ON fs.student_id = s.id JOIN families f ON f.id = fs.family_id WHERE s.parent_id = ? ORDER BY fs.is_primary_home DESC, f.household_name LIMIT 1", [$guardianId] )->getRowArray(); if (!empty($row['id'])) $familyId = (int) $row['id']; } } } if (!$familyId) { return $this->response->setStatusCode(404)->setBody('