families = new FamilyModel(); $this->guardians = new FamilyGuardianModel(); $this->familyStudents = new FamilyStudentModel(); $this->students = new StudentModel(); $this->users = new UserModel(); $this->db = \Config\Database::connect(); } /* ========================================================= * SECTION A — APIs your UI uses * =======================================================*/ // GET /api/students/{id}/families public function familiesByStudent(int $studentId) { $rows = $this->db->query( "SELECT f.*, fs.is_primary_home FROM family_students fs JOIN families f ON f.id = fs.family_id WHERE fs.student_id = ? AND f.is_active = 1 ORDER BY fs.is_primary_home DESC, f.household_name", [$studentId] )->getResultArray(); return $this->response->setJSON(['data' => $rows]); } // GET /api/families/{id}/guardians public function guardiansByFamily(int $familyId) { $rows = $this->db->query( "SELECT u.id AS user_id, u.firstname, u.lastname, u.email, 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", [$familyId] )->getResultArray(); return $this->response->setJSON(['data' => $rows]); } /* ========================================================= * SECTION B — Bootstrap & Linking helpers * =======================================================*/ // POST /families/bootstrap // Creates/ensures families for every student with parent_id, // links the student to that family, and (optionally) links a second parent. public function bootstrap() { // Optionally restrict this to admins // if (! $this->userCan('families.bootstrap')) return $this->deny(); // Guard: ensure required tables exist foreach (['families','family_students','family_guardians'] as $t) { if (! $this->db->tableExists($t)) { return $this->response->setStatusCode(500)->setJSON([ 'status' => 'error', 'message' => "Missing required table '{$t}'. Run migrations: php spark migrate", ]); } } $this->db->transStart(); // 1) Get all distinct primary parents from students.parent_id $primaryParents = $this->db->query( "SELECT DISTINCT parent_id FROM students WHERE parent_id IS NOT NULL" )->getResultArray(); $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 = $this->db->query( "SELECT id FROM students WHERE parent_id = ?", [$primaryUserId] )->getResultArray(); 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); } $this->db->transComplete(); $payload = [ 'status' => $this->db->transStatus() ? 'ok' : 'error', 'families_created' => $created, 'students_linked' => $linkedStudents, 'guardians_linked' => $linkedGuardians, ]; // If accessed via GET (browser), redirect back with flash if (strtolower($this->request->getMethod()) === 'get') { $msg = json_encode($payload); return redirect()->to('/family')->with('message', "Bootstrap result: {$msg}"); } return $this->response->setJSON($payload); } // POST /families/attach-second-by-user // body: student_id, user_id, relation='secondary' public function attachSecondByUser() { $studentId = (int) $this->request->getPost('student_id'); $userId = (int) $this->request->getPost('user_id'); $relation = (string) ($this->request->getPost('relation') ?? 'secondary'); if (!$studentId || !$userId) return $this->bad('student_id and user_id required'); $familyId = $this->familyIdFromStudentPrimary($studentId); if (!$familyId) return $this->bad('No primary family found for this student'); $rows = $this->attachGuardianUser($familyId, $userId, $relation, false, 1, 0); return $this->response->setJSON(['status' => 'ok', 'attached' => $rows, 'family_id' => $familyId]); } // POST /families/attach-second-by-email // body: student_id, email, firstname, lastname, relation='secondary' // Creates a stub user if email not in users, then links. public function attachSecondByEmail() { $studentId = (int) $this->request->getPost('student_id'); $email = trim((string)$this->request->getPost('email')); $first = trim((string)$this->request->getPost('firstname')); $last = trim((string)$this->request->getPost('lastname')); $relation = (string) ($this->request->getPost('relation') ?? 'secondary'); if (!$studentId || !$email) return $this->bad('student_id and email required'); $familyId = $this->familyIdFromStudentPrimary($studentId); if (!$familyId) return $this->bad('No primary family found for this student'); $user = $this->users->where('email', $email)->first(); if (!$user) { // Create a minimal/stub user; adjust fields to your schema $this->users->insert([ 'firstname' => $first ?: '', 'lastname' => $last ?: '', 'email' => $email, 'status' => 'Active', 'user_type' => 'secondary', // align with secondary guardian role ]); $userId = (int) $this->users->getInsertID(); } else { $userId = (int) $user['id']; } $rows = $this->attachGuardianUser($familyId, $userId, $relation, false, 1, 0); return $this->response->setJSON(['status' => 'ok', 'attached' => $rows, 'family_id' => $familyId, 'user_id' => $userId]); } /* ========================================================= * SECTION C — Maintenance actions * =======================================================*/ // POST /families/set-primary-home // body: family_id, student_id, is_primary_home (0/1) public function setPrimaryHome() { $familyId = (int) $this->request->getPost('family_id'); $studentId = (int) $this->request->getPost('student_id'); $flag = (int) $this->request->getPost('is_primary_home'); if (!$familyId || !$studentId) return $this->bad('family_id and student_id required'); $this->familyStudents ->where(['family_id' => $familyId, 'student_id' => $studentId]) ->set(['is_primary_home' => $flag ? 1 : 0]) ->update(); return $this->response->setJSON(['status' => 'ok']); } // POST /families/set-guardian-flags // body: family_id, user_id, receive_emails(0/1), is_primary(0/1), receive_sms(0/1) public function setGuardianFlags() { $familyId = (int) $this->request->getPost('family_id'); $userId = (int) $this->request->getPost('user_id'); if (!$familyId || !$userId) return $this->bad('family_id and user_id required'); $data = []; foreach (['receive_emails', 'is_primary', 'receive_sms', 'relation'] as $k) { if (null !== $this->request->getPost($k)) { $val = $this->request->getPost($k); $data[$k] = in_array($k, ['receive_emails', 'is_primary', 'receive_sms']) ? (int)$val : (string)$val; } } if (!$data) return $this->bad('No flags provided'); $this->guardians->where(['family_id' => $familyId, 'user_id' => $userId])->set($data)->update(); return $this->response->setJSON(['status' => 'ok']); } // POST /families/unlink-guardian // body: family_id, user_id public function unlinkGuardian() { $familyId = (int) $this->request->getPost('family_id'); $userId = (int) $this->request->getPost('user_id'); if (!$familyId || !$userId) return $this->bad('family_id and user_id required'); $this->guardians->where(['family_id' => $familyId, 'user_id' => $userId])->delete(); return $this->response->setJSON(['status' => 'ok']); } // POST /families/unlink-student // body: family_id, student_id public function unlinkStudent() { $familyId = (int) $this->request->getPost('family_id'); $studentId = (int) $this->request->getPost('student_id'); if (!$familyId || !$studentId) return $this->bad('family_id and student_id required'); $this->familyStudents->where(['family_id' => $familyId, 'student_id' => $studentId])->delete(); return $this->response->setJSON(['status' => 'ok']); } /* ========================================================= * SECTION D — Private helpers * =======================================================*/ // Ensure there is exactly one family per primary parent (users.id) // Returns $familyId and increments $created by reference if newly created. protected function ensureFamilyForPrimaryParent(int $primaryUserId, int &$createdCounter): int { $code = 'FAM-' . $primaryUserId; $row = $this->families->where('family_code', $code)->first(); if ($row) return (int)$row['id']; $this->families->insert([ 'family_code' => $code, 'household_name' => 'Family of User ' . $primaryUserId, 'is_active' => 1, ]); $createdCounter++; return (int)$this->families->getInsertID(); } // Attach a student to a family (idempotent; returns rows affected >0 if inserted) protected function attachStudentToFamily(int $studentId, int $familyId): int { // INSERT IGNORE pattern $sql = "INSERT IGNORE INTO family_students (family_id, student_id, is_primary_home) VALUES (?, ?, 1)"; $this->db->query($sql, [$familyId, $studentId]); return $this->db->affectedRows(); } // 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 { $sql = "INSERT IGNORE INTO family_guardians (family_id, user_id, relation, is_primary, receive_emails, receive_sms) VALUES (?, ?, ?, ?, ?, ?)"; $this->db->query($sql, [$familyId, $userId, $relation, $isPrimary ? 1 : 0, $receiveEmails, $receiveSms]); return $this->db->affectedRows(); } // Find the “primary” family for a student = family created from parent_id protected function familyIdFromStudentPrimary(int $studentId): ?int { // 1) Get primary parent id for the student $st = $this->students->select('parent_id')->find($studentId); if (!$st || empty($st['parent_id'])) return null; // 2) The canonical family uses code FAM-{parent_id} $code = 'FAM-' . (int)$st['parent_id']; $row = $this->families->select('id')->where('family_code', $code)->first(); if ($row) return (int)$row['id']; // If not found, fall back to any family linked already $row = $this->db->query( "SELECT family_id FROM family_students WHERE student_id = ? ORDER BY is_primary_home DESC LIMIT 1", [$studentId] )->getRowArray(); return $row ? (int)$row['family_id'] : null; } // Simple 400 protected function bad(string $msg) { return $this->response->setStatusCode(400)->setJSON(['status' => 'error', 'message' => $msg]); } // Example permission check hook (plug your own logic) protected function userCan(string $perm): bool { $perms = (array) (session('permissions') ?? []); return in_array($perm, $perms, true); } protected function deny() { return redirect()->to('/access_denied')->setStatusCode(403); } // Legacy import helper: map secondary parents from legacy 'parents' table public function importSecondParentsFromLegacy() { // Protect this route (e.g., admin only) via filter in routes $L = [ 'table' => 'parents', 'firstparent_id' => 'firstparent_id', // users.id of primary (first) parent 'second_user_id' => 'secondparent_id', // users.id of second parent (optional) 'second_email' => 'secondparent_email', 'second_firstname' => 'secondparent_firstname', 'second_lastname' => 'secondparent_lastname', ]; if (!$this->db->tableExists($L['table'])) { return $this->response->setJSON(['status' => 'error', 'message' => "Legacy table '{$L['table']}' not found"]); } foreach (['families','family_students','family_guardians'] as $t) { if (! $this->db->tableExists($t)) { return $this->response->setStatusCode(500)->setJSON([ 'status' => 'error', 'message' => "Missing required table '{$t}'. Run migrations: php spark migrate", ]); } } $rows = $this->db->table($L['table']) ->select( "{$L['firstparent_id']} AS first_id, " . "{$L['second_user_id']} AS second_id, " . "{$L['second_email']} AS email, " . "{$L['second_firstname']} AS firstname, " . "{$L['second_lastname']} AS lastname", false ) ->groupStart() ->where("{$L['second_user_id']} !=", 0) ->orWhere("{$L['second_email']} !=", '') ->groupEnd() ->get()->getResultArray(); $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->families->select('id')->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->users->where('email', $email)->first(); if (!$user) { $this->users->insert([ 'firstname' => $r['firstname'] ?? '', 'lastname' => $r['lastname'] ?? '', 'email' => $email, 'status' => 'Active', 'user_type' => 'secondary', ]); $userId = (int)$this->users->getInsertID(); $createdUsers++; } else { $userId = (int)$user['id']; } } // Ensure all students for this primary parent are linked to this family $stuRows = $this->db->query("SELECT id FROM students WHERE parent_id = ?", [$firstId])->getResultArray(); 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); } $payload = [ 'status' => 'ok', 'created_users' => $createdUsers, 'guardians_linked' => $linked, 'skipped' => $skipped, 'total_source' => count($rows), ]; if (strtolower($this->request->getMethod()) === 'get') { $msg = json_encode($payload); return redirect()->to('/family')->with('message', "Import legacy result: {$msg}"); } return $this->response->setJSON($payload); } }