Files
alrahma_sunday_school/app/Controllers/View/FamilyController.php
T
2026-05-16 13:44:12 -04:00

478 lines
18 KiB
PHP
Executable File

<?php
namespace App\Controllers\View;
use App\Controllers\BaseController;
use CodeIgniter\Database\BaseConnection;
// MODELS you should already have (or create tiny ones if not)
use App\Models\FamilyModel;
use App\Models\FamilyGuardianModel;
use App\Models\FamilyStudentModel;
// Your app's models
use App\Models\StudentModel;
use App\Models\UserModel;
/**
* FamilyController
*
* Handles creating/linking Family ←→ Guardians (users) ←→ Students,
* including bootstrap from existing schema where students.parent_id is the
* “first parent”, and a second parent may be present as a users row
* or only as an email (stub user creation).
*/
class FamilyController extends BaseController
{
protected FamilyModel $families;
protected FamilyGuardianModel $guardians;
protected FamilyStudentModel $familyStudents;
protected StudentModel $students;
protected UserModel $users;
protected BaseConnection $db;
public function __construct()
{
$this->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);
}
}