fix family card issue
This commit is contained in:
@@ -2500,6 +2500,10 @@ class ParentController extends BaseController
|
||||
return redirect()->back()->with('error', 'Failed to delete student.');
|
||||
}
|
||||
|
||||
// Keep the normalized household, guardian, contact, and remaining
|
||||
// student membership data aligned after every child removal.
|
||||
(new \App\Services\FamilyDataSyncService($this->db))->syncForParent((int) $parentId);
|
||||
|
||||
// ✅ Check if parent has any students left
|
||||
$remainingStudents = $this->studentModel
|
||||
->where('parent_id', $parentId)
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
final class BackfillMissingFamilyStudentMemberships extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
foreach (['users', 'students', 'families', 'family_guardians', 'family_students'] as $table) {
|
||||
if (! $this->db->tableExists($table)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$students = $this->db->query(
|
||||
"SELECT s.id AS student_id, s.parent_id
|
||||
FROM students s
|
||||
LEFT JOIN family_students fs ON fs.student_id = s.id
|
||||
WHERE fs.id IS NULL
|
||||
AND s.parent_id IS NOT NULL
|
||||
AND s.parent_id > 0
|
||||
ORDER BY s.id"
|
||||
)->getResultArray();
|
||||
|
||||
foreach ($students as $student) {
|
||||
$studentId = (int) ($student['student_id'] ?? 0);
|
||||
$parentId = (int) ($student['parent_id'] ?? 0);
|
||||
if ($studentId <= 0 || $parentId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$family = $this->db->query(
|
||||
"SELECT f.id
|
||||
FROM families f
|
||||
LEFT JOIN family_guardians fg
|
||||
ON fg.family_id = f.id
|
||||
AND fg.user_id = ?
|
||||
WHERE f.family_code = ? OR fg.user_id = ?
|
||||
ORDER BY (f.family_code = ?) DESC, fg.is_primary DESC, f.id ASC
|
||||
LIMIT 1",
|
||||
[$parentId, 'FAM-' . $parentId, $parentId, 'FAM-' . $parentId]
|
||||
)->getRowArray();
|
||||
|
||||
if (empty($family['id'])) {
|
||||
$parent = $this->db->table('users')
|
||||
->select('firstname, lastname')
|
||||
->where('id', $parentId)
|
||||
->get()
|
||||
->getRowArray();
|
||||
$parentName = trim((string) ($parent['firstname'] ?? '') . ' ' . (string) ($parent['lastname'] ?? ''));
|
||||
|
||||
$this->db->table('families')->insert([
|
||||
'family_code' => 'FAM-' . $parentId,
|
||||
'household_name' => $parentName !== '' ? 'Family of ' . $parentName : 'Family of User ' . $parentId,
|
||||
'is_active' => 1,
|
||||
]);
|
||||
$family['id'] = (int) $this->db->insertID();
|
||||
}
|
||||
|
||||
$this->db->query(
|
||||
'INSERT IGNORE INTO family_guardians (family_id, user_id, relation, is_primary, receive_emails, receive_sms) VALUES (?, ?, ?, 1, 1, 0)',
|
||||
[(int) $family['id'], $parentId, 'primary']
|
||||
);
|
||||
$this->db->query(
|
||||
'INSERT IGNORE INTO family_students (family_id, student_id, is_primary_home) VALUES (?, ?, 1)',
|
||||
[(int) $family['id'], $studentId]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
// Data repair is intentionally not reversed: a repaired membership is
|
||||
// indistinguishable from one subsequently confirmed by an administrator.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
/**
|
||||
* Completes the family-link repair for databases that already ran the first
|
||||
* backfill before students without any household record were discovered.
|
||||
*/
|
||||
final class CreateMissingStudentFamilies extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
foreach (['users', 'students', 'families', 'family_guardians', 'family_students'] as $table) {
|
||||
if (! $this->db->tableExists($table)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$students = $this->db->query(
|
||||
"SELECT s.id AS student_id, s.parent_id, u.firstname, u.lastname
|
||||
FROM students s
|
||||
JOIN users u ON u.id = s.parent_id
|
||||
LEFT JOIN family_students fs ON fs.student_id = s.id
|
||||
WHERE fs.id IS NULL
|
||||
AND s.parent_id IS NOT NULL
|
||||
AND s.parent_id > 0
|
||||
ORDER BY s.id"
|
||||
)->getResultArray();
|
||||
|
||||
foreach ($students as $student) {
|
||||
$studentId = (int) ($student['student_id'] ?? 0);
|
||||
$parentId = (int) ($student['parent_id'] ?? 0);
|
||||
if ($studentId <= 0 || $parentId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$family = $this->db->query(
|
||||
"SELECT f.id
|
||||
FROM families f
|
||||
LEFT JOIN family_guardians fg
|
||||
ON fg.family_id = f.id
|
||||
AND fg.user_id = ?
|
||||
WHERE f.family_code = ? OR fg.user_id = ?
|
||||
ORDER BY (f.family_code = ?) DESC, fg.is_primary DESC, f.id ASC
|
||||
LIMIT 1",
|
||||
[$parentId, 'FAM-' . $parentId, $parentId, 'FAM-' . $parentId]
|
||||
)->getRowArray();
|
||||
|
||||
if (empty($family['id'])) {
|
||||
$parentName = trim((string) ($student['firstname'] ?? '') . ' ' . (string) ($student['lastname'] ?? ''));
|
||||
$this->db->table('families')->insert([
|
||||
'family_code' => 'FAM-' . $parentId,
|
||||
'household_name' => $parentName !== '' ? 'Family of ' . $parentName : 'Family of User ' . $parentId,
|
||||
'is_active' => 1,
|
||||
]);
|
||||
$family['id'] = (int) $this->db->insertID();
|
||||
}
|
||||
|
||||
$familyId = (int) ($family['id'] ?? 0);
|
||||
if ($familyId <= 0) {
|
||||
throw new \RuntimeException('Could not create a family for parent ID ' . $parentId . '.');
|
||||
}
|
||||
|
||||
$this->db->query(
|
||||
'INSERT IGNORE INTO family_guardians (family_id, user_id, relation, is_primary, receive_emails, receive_sms) VALUES (?, ?, ?, 1, 1, 0)',
|
||||
[$familyId, $parentId, 'primary']
|
||||
);
|
||||
$this->db->query(
|
||||
'INSERT IGNORE INTO family_students (family_id, student_id, is_primary_home) VALUES (?, ?, 1)',
|
||||
[$familyId, $studentId]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
// This repair is intentionally irreversible for the same reason as the
|
||||
// preceding family-membership backfill.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use CodeIgniter\Database\BaseConnection;
|
||||
|
||||
/**
|
||||
* Keeps the normalized family record aligned with its primary parent and
|
||||
* every current student owned by that parent.
|
||||
*/
|
||||
final class FamilyDataSyncService
|
||||
{
|
||||
public function __construct(private readonly BaseConnection $db)
|
||||
{
|
||||
}
|
||||
|
||||
public function syncForParent(int $parentId): ?int
|
||||
{
|
||||
if ($parentId <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (['users', 'students', 'families', 'family_guardians', 'family_students'] as $table) {
|
||||
if (! $this->db->tableExists($table)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
$parent = $this->db->table('users')
|
||||
->select('id, firstname, lastname, cellphone, address_street, apt, city, state, zip')
|
||||
->where('id', $parentId)
|
||||
->get()
|
||||
->getRowArray();
|
||||
if (! $parent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$familyCode = 'FAM-' . $parentId;
|
||||
$family = $this->db->table('families')
|
||||
->select('id')
|
||||
->where('family_code', $familyCode)
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
if (! $family) {
|
||||
$family = $this->db->table('family_guardians fg')
|
||||
->select('fg.family_id AS id')
|
||||
->join('families f', 'f.id = fg.family_id', 'inner')
|
||||
->where('fg.user_id', $parentId)
|
||||
->orderBy('fg.is_primary', 'DESC')
|
||||
->orderBy('fg.id', 'ASC')
|
||||
->get(1)
|
||||
->getRowArray();
|
||||
}
|
||||
|
||||
$familyData = $this->familyData($parent);
|
||||
if (! $family) {
|
||||
$this->db->table('families')->insert(['family_code' => $familyCode] + $familyData);
|
||||
$familyId = (int) $this->db->insertID();
|
||||
} else {
|
||||
$familyId = (int) ($family['id'] ?? 0);
|
||||
if ($familyId > 0) {
|
||||
$this->db->table('families')->where('id', $familyId)->update($familyData);
|
||||
}
|
||||
}
|
||||
|
||||
if ($familyId <= 0) {
|
||||
throw new \RuntimeException('Family data could not be synchronized for parent ID ' . $parentId . '.');
|
||||
}
|
||||
|
||||
$this->db->query(
|
||||
'INSERT IGNORE INTO family_guardians (family_id, user_id, relation, is_primary, receive_emails, receive_sms) VALUES (?, ?, ?, 1, 1, 0)',
|
||||
[$familyId, $parentId, 'primary']
|
||||
);
|
||||
|
||||
$students = $this->db->table('students')
|
||||
->select('id')
|
||||
->where('parent_id', $parentId)
|
||||
->get()
|
||||
->getResultArray();
|
||||
foreach ($students as $student) {
|
||||
$studentId = (int) ($student['id'] ?? 0);
|
||||
if ($studentId > 0) {
|
||||
$this->db->query(
|
||||
'INSERT IGNORE INTO family_students (family_id, student_id, is_primary_home) VALUES (?, ?, 1)',
|
||||
[$familyId, $studentId]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Normally handled by the foreign key cascade; this also heals older
|
||||
// databases where that constraint was absent.
|
||||
$this->db->query(
|
||||
'DELETE fs FROM family_students fs LEFT JOIN students s ON s.id = fs.student_id WHERE fs.family_id = ? AND s.id IS NULL',
|
||||
[$familyId]
|
||||
);
|
||||
|
||||
return $familyId;
|
||||
}
|
||||
|
||||
private function familyData(array $parent): array
|
||||
{
|
||||
$parentName = trim((string) ($parent['firstname'] ?? '') . ' ' . (string) ($parent['lastname'] ?? ''));
|
||||
$data = [
|
||||
'household_name' => $parentName !== '' ? 'Family of ' . $parentName : 'Family of User ' . (int) $parent['id'],
|
||||
'address_line1' => trim((string) ($parent['address_street'] ?? '')),
|
||||
'address_line2' => trim((string) ($parent['apt'] ?? '')),
|
||||
'city' => trim((string) ($parent['city'] ?? '')),
|
||||
'state' => trim((string) ($parent['state'] ?? '')),
|
||||
'postal_code' => trim((string) ($parent['zip'] ?? '')),
|
||||
'primary_phone' => trim((string) ($parent['cellphone'] ?? '')),
|
||||
'is_active' => 1,
|
||||
];
|
||||
if ($this->db->fieldExists('updated_at', 'families')) {
|
||||
$data['updated_at'] = utc_now();
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ use App\Models\StudentModel;
|
||||
use App\Models\UserModel;
|
||||
use App\Services\PhoneFormatterService;
|
||||
use App\Services\SchoolIdService;
|
||||
use App\Services\FamilyDataSyncService;
|
||||
use CodeIgniter\Database\BaseConnection;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
use DateTime;
|
||||
@@ -296,6 +297,12 @@ class ParentRegistrationService
|
||||
}
|
||||
}
|
||||
|
||||
// Deleting a student cascades the normalized family_students row, but
|
||||
// intentionally leaves the household and guardian records in place.
|
||||
// Always restore that membership when a child is added again (and heal
|
||||
// older registrations that pre-date the normalized family tables).
|
||||
(new FamilyDataSyncService($this->db))->syncForParent($parentId);
|
||||
|
||||
$this->medicalConditionModel->where('student_id', $studentId)->delete();
|
||||
foreach ((array) $conditions as $condition) {
|
||||
$condition = trim((string) $condition);
|
||||
|
||||
@@ -181,7 +181,6 @@ if ($returnUrl === '') {
|
||||
<?php
|
||||
$studentId = (int)($s['id'] ?? 0);
|
||||
$detailId = 'fc-student-details-' . (int)($f['id'] ?? 0) . '-' . $studentId;
|
||||
$isSelected = $selectedStudentId > 0 && $selectedStudentId === $studentId;
|
||||
$allergies = array_values(array_filter(array_map('trim', (array)($s['allergies'] ?? []))));
|
||||
$conditions = array_values(array_filter(array_map('trim', (array)($s['medical_conditions'] ?? []))));
|
||||
?>
|
||||
@@ -189,10 +188,10 @@ if ($returnUrl === '') {
|
||||
<h2 class="accordion-header">
|
||||
<button
|
||||
type="button"
|
||||
class="accordion-button <?= $isSelected ? '' : 'collapsed' ?>"
|
||||
class="accordion-button collapsed"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#<?= esc($detailId) ?>"
|
||||
aria-expanded="<?= $isSelected ? 'true' : 'false' ?>"
|
||||
aria-expanded="false"
|
||||
aria-controls="<?= esc($detailId) ?>"
|
||||
>
|
||||
<span class="fc-name">
|
||||
@@ -206,7 +205,7 @@ if ($returnUrl === '') {
|
||||
|
||||
<div
|
||||
id="<?= esc($detailId) ?>"
|
||||
class="accordion-collapse collapse <?= $isSelected ? 'show' : '' ?>"
|
||||
class="accordion-collapse collapse"
|
||||
data-bs-parent="#fc-students-<?= (int)($f['id'] ?? 0) ?>"
|
||||
>
|
||||
<div class="accordion-body fc-student-details">
|
||||
|
||||
@@ -356,8 +356,20 @@ html, body { overflow-x: hidden; }
|
||||
try {
|
||||
const gid = params && params.guardian_id;
|
||||
const sid = params && params.student_id;
|
||||
if (gid) window.location.href = '<?= site_url('family') ?>' + '?guardian_id=' + encodeURIComponent(gid);
|
||||
else if (sid) window.location.href = '<?= site_url('family') ?>' + '?student_id=' + encodeURIComponent(sid);
|
||||
const familyUrl = new URL('<?= site_url('family') ?>', window.location.origin);
|
||||
const currentPath = window.location.pathname.replace(/\/+$/, '');
|
||||
const familyPath = familyUrl.pathname.replace(/\/+$/, '');
|
||||
const familyIndexPath = familyPath + '/index';
|
||||
if (currentPath !== familyPath && currentPath !== familyIndexPath && (gid || sid)) {
|
||||
familyUrl.searchParams.set(gid ? 'guardian_id' : 'student_id', gid || sid);
|
||||
window.location.href = familyUrl.toString();
|
||||
return;
|
||||
}
|
||||
|
||||
const target = document.getElementById('familyCardContent');
|
||||
if (target) target.innerHTML = '<div class="p-3 text-danger">The family card could not be loaded. Please refresh and try again.</div>';
|
||||
const m = ensureModal();
|
||||
if (m) m.show();
|
||||
} catch(_) {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -614,12 +614,26 @@
|
||||
const m = ensureModal();
|
||||
if (m) m.show();
|
||||
} catch (e) {
|
||||
// Fallback: redirect to /family filtered page
|
||||
try {
|
||||
const gid = params && params.guardian_id;
|
||||
const sid = params && params.student_id;
|
||||
if (gid) window.location.href = '<?= site_url('family') ?>' + '?guardian_id=' + encodeURIComponent(gid);
|
||||
else if (sid) window.location.href = '<?= site_url('family') ?>' + '?student_id=' + encodeURIComponent(sid);
|
||||
const familyUrl = new URL('<?= site_url('family') ?>', window.location.origin);
|
||||
const currentPath = window.location.pathname.replace(/\/+$/, '');
|
||||
const familyPath = familyUrl.pathname.replace(/\/+$/, '');
|
||||
const familyIndexPath = familyPath + '/index';
|
||||
|
||||
// A failed card request used to redirect the family page back to
|
||||
// itself. Its automatic card loader then repeated that forever.
|
||||
if (currentPath !== familyPath && currentPath !== familyIndexPath && (gid || sid)) {
|
||||
familyUrl.searchParams.set(gid ? 'guardian_id' : 'student_id', gid || sid);
|
||||
window.location.href = familyUrl.toString();
|
||||
return;
|
||||
}
|
||||
|
||||
const target = document.getElementById('familyCardContent');
|
||||
if (target) target.innerHTML = '<div class="p-3 text-danger">The family card could not be loaded. Please refresh and try again.</div>';
|
||||
const m = ensureModal();
|
||||
if (m) m.show();
|
||||
} catch(_) {}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user