Files
alrahma_sunday_school/app/Views/family/index.php
T
2026-02-10 22:11:06 -05:00

472 lines
23 KiB
PHP

<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<div class="container-fluid my-4">
<?php if (session()->getFlashdata('message')): ?>
<div class="alert alert-info">
<?= esc(session()->getFlashdata('message')) ?>
</div>
<?php endif; ?>
<h2 class="text-center mt-4 mb-3">Family Management</h2>
<div class="row mb-3 justify-content-center">
<div class="col-12 col-md-8 col-lg-6 position-relative mx-auto">
<label for="familySearch" class="form-label">Search (parent, student, phone, email)</label>
<input type="search" class="form-control" id="familySearch" placeholder="Type to filter…" autocomplete="off" aria-autocomplete="list" aria-haspopup="listbox">
<div id="familySearchSuggest" class="list-group position-absolute w-100 d-none" style="z-index:1030; max-height:260px; overflow:auto;"></div>
</div>
</div>
<div id="familyNoResults" class="alert alert-warning d-none">No matches found.</div>
<!-- Student dropdown removed; use the search bar above to find families -->
<?php if ($student): ?>
<div class="card mb-3">
<div class="card-header">Student</div>
<div class="card-body">
<strong><?= esc($student['firstname'].' '.$student['lastname']) ?></strong>
</div>
</div>
<?php endif; ?>
<?php if (empty($families)): ?>
<div class="alert alert-warning">No families found.</div>
<?php else: ?>
<?php foreach ($families as $f): ?>
<?php
$parts = [];
$parts[] = (string)($f['household_name'] ?? '');
$parts[] = (string)($f['family_code'] ?? '');
$parts[] = (string)($f['primary_phone'] ?? '');
$parts[] = (string)($f['preferred_lang'] ?? '');
$parts[] = (string)($f['preferred_contact_method'] ?? '');
$parts[] = (string)($f['address_line1'] ?? '');
$parts[] = (string)($f['address_line2'] ?? '');
$parts[] = (string)($f['city'] ?? '');
$parts[] = (string)($f['state'] ?? '');
$parts[] = (string)($f['postal_code'] ?? '');
$parts[] = (string)($f['country'] ?? '');
foreach (($f['guardians'] ?? []) as $gg) {
$parts[] = trim((string)($gg['firstname'] ?? '').' '.(string)($gg['lastname'] ?? ''));
$parts[] = (string)($gg['email'] ?? '');
$parts[] = (string)($gg['cellphone'] ?? '');
$parts[] = (string)($gg['relation'] ?? '');
$parts[] = (string)($gg['address_street'] ?? '');
$parts[] = (string)($gg['city'] ?? '');
$parts[] = (string)($gg['state'] ?? '');
$parts[] = (string)($gg['zip'] ?? '');
}
foreach (($f['students'] ?? []) as $ss) {
$parts[] = trim((string)($ss['firstname'] ?? '').' '.(string)($ss['lastname'] ?? ''));
$parts[] = (string)($ss['grade'] ?? '');
}
$searchBlob = strtolower(trim(implode(' ', array_filter($parts, fn($x)=>trim((string)$x) !== ''))));
?>
<div class="card mb-3 family-card" data-search="<?= esc($searchBlob) ?>">
<div class="card-header">
<div class="d-flex flex-wrap justify-content-between align-items-center">
<div class="mb-2 mb-sm-0">
<strong><?= esc($f['household_name'] ?: ('Family #'.$f['id'])) ?></strong>
<?php if (!empty($f['is_primary_home'] ?? 0)): ?><span class="badge bg-primary ms-2">Primary</span><?php endif; ?>
<span class="badge bg-light text-dark ms-2">Code: <?= esc($f['family_code']) ?></span>
</div>
<?php
$gCount = count($f['guardians'] ?? []);
$sCount = count($f['students'] ?? []);
$sum = $f['finance_summary'] ?? ['invoices_count'=>0,'total_amount'=>0,'paid_amount'=>0,'balance'=>0];
?>
<div class="text-end">
<span class="badge bg-secondary me-1">Guardians: <?= (int)$gCount ?></span>
<span class="badge bg-secondary me-1">Students: <?= (int)$sCount ?></span>
<span class="badge bg-secondary me-1">Invoices: <?= (int)($sum['invoices_count'] ?? 0) ?></span>
<span class="badge <?= ((float)($sum['balance'] ?? 0) > 0) ? 'bg-danger' : 'bg-success' ?>">Balance: $<?= number_format((float)($sum['balance'] ?? 0), 2) ?></span>
</div>
</div>
</div>
<div class="card-body">
<div class="row g-2">
<!-- Family Details -->
<div class="col-lg-4 order-lg-2">
<?php
$__vals = [
$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'] ?? ''
];
$__hasDetails = false;
foreach ($__vals as $__v) { if (trim((string)$__v) !== '') { $__hasDetails = true; break; } }
?>
<?php if ($__hasDetails): ?>
<h6 class="mb-2">Family Details</h6>
<div class="small text-muted">Address</div>
<div><?= esc($f['address_line1'] ?? '') ?> <?= esc($f['address_line2'] ?? '') ?></div>
<div><?= esc($f['city'] ?? '') ?> <?= esc($f['state'] ?? '') ?> <?= esc($f['postal_code'] ?? '') ?></div>
<?php if (!empty($f['country'])): ?><div><?= esc($f['country']) ?></div><?php endif; ?>
<div class="mt-2"><span class="text-muted small">Phone:</span> <?= esc($f['primary_phone'] ?? '—') ?></div>
<div class="mt-1"><span class="text-muted small">Lang:</span> <?= esc($f['preferred_lang'] ?? '—') ?></div>
<div class="mt-1"><span class="text-muted small">Contact:</span> <?= esc($f['preferred_contact_method'] ?? '—') ?></div>
<div class="mt-1"><span class="text-muted small">Active:</span> <?= !empty($f['is_active']) ? 'Yes' : 'No' ?></div>
<?php endif; ?>
<div class="mt-3">
<h6 class="mb-2">Students</h6>
<?php if (empty($f['students'])): ?>
<div class="text-muted small">No students linked.</div>
<?php else: ?>
<ul class="list-group students-list">
<?php foreach ($f['students'] as $s): ?>
<li class="list-group-item student-item">
<a href="#" class="text-decoration-none" data-family-student-id="<?= (int)($s['id'] ?? 0) ?>">
<?= esc($s['firstname'].' '.$s['lastname']) ?>
</a>
<?php if (!empty($s['grade'])): ?>
<span class="text-muted small">• <?= esc($s['grade']) ?></span>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</div>
</div>
<!-- Guardians -->
<div class="col-lg-8 order-lg-1">
<div class="d-flex justify-content-between align-items-center mb-2">
<h6 class="mb-0">Guardians</h6>
<span class="text-muted small">Total: <?= count($f['guardians'] ?? []) ?></span>
</div>
<?php if (empty($f['guardians'])): ?>
<div class="alert alert-light border">No guardians yet.</div>
<?php else: ?>
<div class="table-responsive">
<table class="table table-sm align-middle guardians-table">
<thead class="table-light">
<tr>
<th style="min-width: 220px;">Guardian</th>
<th style="min-width: 220px;">Contact</th>
<th style="min-width: 260px;">Address</th>
<th style="min-width: 140px;">Prefs</th>
</tr>
</thead>
<tbody>
<?php foreach ($f['guardians'] as $g): ?>
<tr class="guard-row">
<td>
<div>
<strong>
<a href="#" class="text-decoration-none" data-family-guardian-id="<?= (int)($g['user_id'] ?? 0) ?>">
<?= esc($g['firstname'].' '.$g['lastname']) ?>
</a>
</strong>
<?php if (!empty($g['is_primary'])): ?>
<span class="badge text-bg-primary ms-1"><i class="bi bi-star-fill"></i> Primary</span>
<?php endif; ?>
</div>
<?php
$relRaw = (string)($g['relation'] ?? '');
$rel = strtolower(trim($relRaw));
if ($rel === 'guardian') {
$relLabel = 'second parent';
} elseif ($rel === 'primary') {
$relLabel = 'first parent';
} else {
$relLabel = $relRaw;
}
?>
<div class="text-muted small">Relation: <?= esc($relLabel) ?></div>
</td>
<td class="small">
<div>
<i class="bi bi-envelope me-1 text-muted"></i>
<?php if (!empty($g['email'])): ?>
<a href="mailto:<?= esc($g['email']) ?>"><?= esc($g['email']) ?></a>
<?php else: ?>
<span class="text-muted">—</span>
<?php endif; ?>
</div>
<div class="mt-1">
<i class="bi bi-telephone me-1 text-muted"></i>
<?php if (!empty($g['cellphone'])): ?>
<a href="tel:<?= esc($g['cellphone']) ?>"><?= esc($g['cellphone']) ?></a>
<?php else: ?>
<span class="text-muted">—</span>
<?php endif; ?>
</div>
</td>
<td class="small">
<i class="bi bi-geo-alt me-1 text-muted"></i>
<?php
$addrParts = array_filter([
$g['address_street'] ?? '',
trim(($g['city'] ?? '').' '.($g['state'] ?? '')),
$g['zip'] ?? ''
], fn($x)=>trim((string)$x) !== '');
?>
<?= esc(implode(', ', $addrParts) ?: '—') ?>
</td>
<td>
<?php if (!empty($g['receive_emails'])): ?>
<span class="badge text-bg-success"><i class="bi bi-envelope-fill"></i> Email</span>
<?php else: ?>
<span class="badge text-bg-secondary"><i class="bi bi-envelope"></i> Email</span>
<?php endif; ?>
<?php if (!empty($g['receive_sms'])): ?>
<span class="badge text-bg-info ms-1"><i class="bi bi-chat-dots-fill"></i> SMS</span>
<?php else: ?>
<span class="badge text-bg-secondary ms-1"><i class="bi bi-chat-dots"></i> SMS</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
<!-- Add Second Parent by Email section hidden/removed per request -->
</div>
<!-- Invoices & Payments side-by-side -->
<?php $hasInvoices = !empty($f['invoices']); $hasPayments = !empty($f['payments']); $half = ($hasInvoices && $hasPayments); ?>
<?php if ($hasInvoices): ?>
<div class="<?= $half ? 'col-lg-6' : 'col-12' ?>">
<h6 class="mb-2">Invoices</h6>
<?php if (!empty($f['invoices'])): ?>
<div class="table-responsive">
<table class="table table-sm table-striped table-hover align-middle">
<thead class="table-light">
<tr>
<th>#</th><th>Status</th><th>Total</th><th>Paid</th><th>Balance</th><th>Date</th>
</tr>
</thead>
<tbody>
<?php foreach ($f['invoices'] as $iv): ?>
<tr>
<td><?= esc($iv['invoice_number']) ?></td>
<td><?= esc($iv['status']) ?></td>
<td>$<?= number_format((float)$iv['total_amount'], 2) ?></td>
<td>$<?= number_format((float)$iv['paid_amount'], 2) ?></td>
<td>$<?= number_format((float)$iv['balance'], 2) ?></td>
<td><?= esc(!empty($iv['issue_date']) ? local_date($iv['issue_date'], 'm-d-Y') : '') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if ($hasPayments): ?>
<div class="<?= $half ? 'col-lg-6' : 'col-12' ?>">
<h6 class="mb-2">Recent Payments</h6>
<?php if (!empty($f['payments'])): ?>
<div class="table-responsive">
<table class="table table-sm table-striped table-hover align-middle">
<thead class="table-light">
<tr>
<th>Invoice</th><th>Amount</th><th>Balance</th><th>Method</th><th>Date</th><th>Status</th>
</tr>
</thead>
<tbody>
<?php $imap = $f['invoice_map'] ?? []; ?>
<?php foreach ($f['payments'] as $p): ?>
<tr>
<td><?php $iid = (int)($p['invoice_id'] ?? 0); echo esc($imap[$iid] ?? ('#'.$iid)); ?></td>
<td>$<?= number_format((float)($p['paid_amount'] ?? 0), 2) ?></td>
<td>$<?= number_format((float)($p['balance'] ?? 0), 2) ?></td>
<td><?= esc($p['payment_method'] ?? '') ?></td>
<td><?= esc(!empty($p['payment_date']) ? local_date($p['payment_date'], 'm-d-Y') : '') ?></td>
<td><?= esc($p['status'] ?? '') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
<!-- Maintenance section hidden per request -->
</div>
<?= $this->endSection() ?>
<?= $this->section('scripts') ?>
<script>
(function() {
const input = document.getElementById('familySearch');
if (!input) return;
const cards = Array.from(document.querySelectorAll('.family-card'));
const noRes = document.getElementById('familyNoResults');
const suggest = document.getElementById('familySearchSuggest');
const normalize = s => (s||'').toString().toLowerCase();
const debounce = (fn, wait=200) => { let t; return (...args)=>{ clearTimeout(t); t=setTimeout(()=>fn(...args), wait); }; };
// Preloaded datasets for suggestions (students + guardians)
const STATIC_ITEMS = [
<?php foreach (($searchStudents ?? []) as $st):
$sid = (int)($st['id'] ?? 0);
$sf = trim((string)($st['firstname'] ?? ''));
$sl = trim((string)($st['lastname'] ?? ''));
$slab= trim($sf.' '.$sl);
?>{type:'student', id:<?= $sid ?>, label:<?= json_encode($slab) ?>, sub:'Student'},
<?php endforeach; ?>
<?php foreach (($searchGuardians ?? []) as $gu):
$gid = (int)($gu['id'] ?? 0);
$gf = trim((string)($gu['firstname'] ?? ''));
$gl = trim((string)($gu['lastname'] ?? ''));
$gel = trim((string)($gu['email'] ?? ''));
$gph = trim((string)($gu['cellphone'] ?? ''));
$glab= trim($gf.' '.$gl);
$gsub= trim($gel.' '.$gph);
?>{type:'guardian', id:<?= $gid ?>, label:<?= json_encode($glab) ?>, sub:<?= json_encode($gsub) ?>},
<?php endforeach; ?>
];
const SEARCH_URLS = [
'<?= site_url('family/search') ?>',
'<?= base_url('family/search') ?>',
'/family/search'
];
let lastController = 0;
async function fetchSuggestions(q){
const ctrlId = ++lastController;
try {
let resp;
for (const u of SEARCH_URLS) {
try {
resp = await fetch(u + '?q=' + encodeURIComponent(q), {headers: {'Accept':'application/json'}});
if (resp && resp.ok) break;
} catch(_) { /* try next */ }
}
let items = [];
if (resp && resp.ok) {
const data = await resp.json();
if (ctrlId !== lastController) return; // outdated
items = (data && Array.isArray(data.items)) ? data.items : [];
}
// Fallback: use preloaded static dataset if API unavailable/empty
if (!items.length) {
const qn = normalize(q);
items = STATIC_ITEMS.filter(it => {
const l = normalize(it.label);
const s = normalize(it.sub || '');
return l.includes(qn) || s.includes(qn);
}).slice(0, 12);
}
suggest.innerHTML = '';
if (items.length === 0) { suggest.classList.add('d-none'); return; }
items.slice(0, 8).forEach(it => {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'list-group-item list-group-item-action';
btn.innerHTML = `<div class="d-flex justify-content-between"><span>${(it.label||'').replace(/</g,'&lt;')}</span><span class="text-muted small">${(it.sub||'').replace(/</g,'&lt;')}</span></div>`;
btn.addEventListener('click', () => {
// Redirect to show only the selected family
if (it.type === 'student') {
window.location.href = '<?= site_url('family') ?>' + '?student_id=' + encodeURIComponent(it.id);
} else if (it.type === 'guardian') {
window.location.href = '<?= site_url('family') ?>' + '?guardian_id=' + encodeURIComponent(it.id);
}
});
suggest.appendChild(btn);
});
suggest.classList.remove('d-none');
} catch (e) {
// ignore
}
}
function applyFilter() {
const q = normalize(input.value.trim());
let anyVisible = false;
cards.forEach(card => {
// Filter guardians
const gRows = card.querySelectorAll('.guardians-table tbody tr');
let gVisible = 0;
gRows.forEach(tr => {
const t = normalize(tr.textContent);
const match = q === '' || t.includes(q);
tr.style.display = match ? '' : 'none';
if (match) gVisible++;
});
// Filter students
const sItems = card.querySelectorAll('.students-list li');
let sVisible = 0;
sItems.forEach(li => {
const t = normalize(li.textContent);
const match = q === '' || t.includes(q);
li.style.display = match ? '' : 'none';
if (match) sVisible++;
});
// Also allow match against card's data-search blob
const blob = normalize(card.dataset.search || '');
const show = (q === '') ? true : (gVisible > 0 || sVisible > 0 || blob.includes(q));
card.style.display = show ? '' : 'none';
if (show) anyVisible = true;
});
if (noRes) noRes.classList.toggle('d-none', anyVisible || input.value.trim() === '');
const qval = input.value.trim();
if (qval.length >= 1) debouncedSuggest(qval); else if (suggest) suggest.classList.add('d-none');
}
const debouncedSuggest = debounce(fetchSuggestions, 150);
input.addEventListener('input', applyFilter);
input.addEventListener('focus', () => { const v=input.value.trim(); if (v.length>=1) debouncedSuggest(v); });
input.addEventListener('keydown', (e)=>{
if (e.key === 'Enter' && suggest && !suggest.classList.contains('d-none')) {
const first = suggest.querySelector('.list-group-item');
if (first) { e.preventDefault(); first.click(); }
}
});
// If Enter with no visible suggestion: try best match from student dropdown
input.addEventListener('keydown', (e)=>{
if (e.key !== 'Enter' || (suggest && !suggest.classList.contains('d-none'))) return;
const sel = document.querySelector('select[name="student_id"]');
if (!sel) return;
const qn = normalize(input.value.trim());
if (!qn) return;
const opts = Array.from(sel.options).filter(o => !!o.value);
let best = null;
for (const o of opts) {
const txt = o.text || '';
const t = normalize(txt);
let hit = t.includes(qn);
if (!hit && txt.indexOf(',') !== -1) {
const parts = txt.split(',');
const last = normalize(parts[0] || '');
const first = normalize(parts[1] || '');
const fl = (first.trim() + ' ' + last.trim()).trim();
const lf = (last.trim() + ' ' + first.trim()).trim();
hit = fl.includes(qn) || lf.includes(qn);
}
if (hit) { best = o; break; }
}
if (best) {
e.preventDefault();
window.location.href = '<?= site_url('family') ?>' + '?student_id=' + encodeURIComponent(best.value);
}
});
// Hide suggestions on outside click
document.addEventListener('click', (e) => {
if (!suggest) return;
if (e.target === input || suggest.contains(e.target)) return;
suggest.classList.add('d-none');
});
// Initial draw (no filter)
applyFilter();
})();
</script>
<?= $this->endSection() ?>