add features to family card, scores, invoices and students details
Deploy to Shared Hosting / Shared hosting deploy (push) Failing after 49s
Tests / PHPUnit (push) Successful in 1m51s

This commit is contained in:
root
2026-09-09 23:02:16 -04:00
parent 36e8ffe56d
commit ac19226bf0
11 changed files with 822 additions and 38 deletions
+2 -2
View File
@@ -160,13 +160,13 @@ $selectedYear = trim((string)($selectedYear ?? ''));
<tr>
<td><?= esc($student['school_id']) ?></td>
<td>
<a href="#" class="text-decoration-none" data-family-student-id="<?= (int)($student['id'] ?? 0) ?>">
<a href="#" class="text-decoration-none" data-family-student-id="<?= (int)($student['id'] ?? 0) ?>" data-family-school-year="<?= esc($selectedYear) ?>">
<?= esc($student['firstname']) ?>
</a>
<?= student_enrollment_status_button($student, $selectedYear) ?>
</td>
<td>
<a href="#" class="text-decoration-none" data-family-student-id="<?= (int)($student['id'] ?? 0) ?>">
<a href="#" class="text-decoration-none" data-family-student-id="<?= (int)($student['id'] ?? 0) ?>" data-family-school-year="<?= esc($selectedYear) ?>">
<?= esc($student['lastname']) ?>
</a>
</td>
+253 -22
View File
@@ -4,6 +4,41 @@
$gCount = count($f['guardians'] ?? []);
$sCount = count($f['students'] ?? []);
$sum = $f['finance_summary'] ?? ['invoices_count'=>0,'total_amount'=>0,'paid_amount'=>0,'balance'=>0];
$canViewInvoices = !empty($f['can_view_invoices']);
$selectedStudentId = (int)($f['selected_student_id'] ?? 0);
$scoreDefaultStudentId = $selectedStudentId > 0
? $selectedStudentId
: (int)($f['students'][0]['id'] ?? 0);
$displayValue = static function ($value, string $fallback = 'Not provided'): string {
if ($value === null || trim((string)$value) === '') {
return $fallback;
}
return (string)$value;
};
$formatDate = static function ($value, bool $includeTime = false) use ($displayValue): string {
if ($value === null || trim((string)$value) === '') {
return 'Not provided';
}
try {
return $includeTime
? local_datetime((string)$value, 'm-d-Y g:i A')
: (new \DateTime((string)$value))->format('m-d-Y');
} catch (\Throwable $e) {
return $displayValue($value);
}
};
$formatScore = static function ($value): string {
if ($value === null || $value === '' || !is_numeric($value)) {
return '—';
}
return rtrim(rtrim(number_format((float)$value, 2, '.', ''), '0'), '.');
};
// Title: prefer provided household_name unless it's generic like "Family of User 53" or empty.
$titleRaw = trim((string)($f['household_name'] ?? ''));
@@ -41,6 +76,14 @@ if ($returnUrl === '') {
.family-card-root .fc-title { font-size: 1.3rem; letter-spacing: .2px; }
.family-card-root .fc-name { font-size: 1.08rem; font-weight: 600; color: #0b5ed7; }
.family-card-root .fc-name:hover { color: #084298; text-decoration: underline; }
.family-card-root .fc-student-details { background: #f8fafc; border-top: 1px solid #e5e7eb; }
.family-card-root .fc-detail-label { color: #64748b; font-size: .76rem; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; }
.family-card-root .fc-detail-value { color: #12344d; overflow-wrap: anywhere; }
.family-card-root .fc-health-card { border-left: 4px solid #2d89ec !important; }
.family-card-root .fc-score-table th { white-space: normal; min-width: 105px; vertical-align: middle; }
.family-card-root .fc-score-table td { white-space: normal; vertical-align: top; }
.family-card-root .fc-score-table .fc-score-comment { min-width: 220px; max-width: 320px; }
.family-card-root .fc-score-student-nav { flex-wrap: nowrap; overflow-x: auto; }
.family-card-root .fc-badges .badge { background: rgba(255,255,255,.18); color: #fff; font-weight: 500; }
.family-card-root .nav-tabs { padding-left: .5rem; padding-right: .5rem; }
.family-card-root .nav-tabs .nav-link { color: #2161a7; font-weight: 600; }
@@ -92,10 +135,12 @@ if ($returnUrl === '') {
<div class="d-flex flex-wrap gap-2 fc-badges">
<span class="badge">Guardians: <?= (int)$gCount ?></span>
<span class="badge">Students: <?= (int)$sCount ?></span>
<span class="badge">Invoices: <?= (int)($sum['invoices_count'] ?? 0) ?></span>
<span class="badge">
Balance: $<?= number_format((float)($sum['balance'] ?? 0), 2) ?>
</span>
<?php if ($canViewInvoices): ?>
<span class="badge">Invoices: <?= (int)($sum['invoices_count'] ?? 0) ?></span>
<span class="badge">
Balance: $<?= number_format((float)($sum['balance'] ?? 0), 2) ?>
</span>
<?php endif; ?>
</div>
</div>
</div>
@@ -105,15 +150,20 @@ if ($returnUrl === '') {
<li class="nav-item" role="presentation">
<button class="nav-link active" id="fc-tab-overview" data-bs-toggle="tab" data-bs-target="#fc-overview" type="button" role="tab">Students</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="fc-tab-scores" data-bs-toggle="tab" data-bs-target="#fc-scores" type="button" role="tab">Scores History</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="fc-tab-guardians" data-bs-toggle="tab" data-bs-target="#fc-guardians" type="button" role="tab">Parents/Guardians</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="fc-tab-ec" data-bs-toggle="tab" data-bs-target="#fc-ec" type="button" role="tab">Emergency</button>
</li>
<!--li class="nav-item" role="presentation">
<button class="nav-link" id="fc-tab-fin" data-bs-toggle="tab" data-bs-target="#fc-fin" type="button" role="tab">Financials</button>
</li-->
<?php if ($canViewInvoices): ?>
<li class="nav-item" role="presentation">
<button class="nav-link" id="fc-tab-fin" data-bs-toggle="tab" data-bs-target="#fc-fin" type="button" role="tab">Invoices</button>
</li>
<?php endif; ?>
</ul>
<div class="tab-content">
@@ -126,26 +176,202 @@ if ($returnUrl === '') {
<?php if (empty($f['students'])): ?>
<div class="text-muted small">No students linked.</div>
<?php else: ?>
<ul class="list-group">
<div class="accordion" id="fc-students-<?= (int)($f['id'] ?? 0) ?>">
<?php foreach ($f['students'] as $s): ?>
<li class="list-group-item d-flex justify-content-between align-items-center">
<div>
<a href="#" class="text-decoration-none fc-name" data-family-student-id="<?= (int)($s['id'] ?? 0) ?>">
<?= esc(($s['firstname'] ?? '').' '.($s['lastname'] ?? '')) ?>
</a>
<?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'] ?? []))));
?>
<div class="accordion-item">
<h2 class="accordion-header">
<button
type="button"
class="accordion-button <?= $isSelected ? '' : 'collapsed' ?>"
data-bs-toggle="collapse"
data-bs-target="#<?= esc($detailId) ?>"
aria-expanded="<?= $isSelected ? 'true' : 'false' ?>"
aria-controls="<?= esc($detailId) ?>"
>
<span class="fc-name">
<?= esc(trim(($s['firstname'] ?? '').' '.($s['lastname'] ?? ''))) ?>
</span>
<?php if (!empty($s['grade'])): ?>
<span class="badge text-bg-secondary ms-2"><?= esc($s['grade']) ?></span>
<?php endif; ?>
</button>
</h2>
<div
id="<?= esc($detailId) ?>"
class="accordion-collapse collapse <?= $isSelected ? 'show' : '' ?>"
data-bs-parent="#fc-students-<?= (int)($f['id'] ?? 0) ?>"
>
<div class="accordion-body fc-student-details">
<h6 class="mb-3">Student Details</h6>
<div class="row g-3">
<?php
$details = [
'School ID' => $displayValue($s['school_id'] ?? null),
'Date of Birth' => $formatDate($s['dob'] ?? null),
'Age' => $displayValue($s['age'] ?? null),
'Gender' => $displayValue($s['gender'] ?? null),
'Assigned Class / Section' => $displayValue($s['grade'] ?? null),
'Enrollment Status' => $displayValue($s['enrollment_status'] ?? null),
'Registration Grade' => $displayValue($s['registration_grade'] ?? null),
'Status' => ((int)($s['is_active'] ?? 0) === 1) ? 'Active' : 'Inactive',
'Photo Consent' => ((int)($s['photo_consent'] ?? 0) === 1) ? 'Yes' : 'No',
'Registration Date' => $formatDate($s['registration_date'] ?? null, true),
'Year of Registration' => $displayValue($s['year_of_registration'] ?? null),
'RFID Tag' => $displayValue($s['rfid_tag'] ?? null),
'Tuition Paid' => ((int)($s['tuition_paid'] ?? 0) === 1) ? 'Yes' : 'No',
];
?>
<?php foreach ($details as $label => $value): ?>
<div class="col-12 col-sm-6 col-lg-4">
<div class="fc-detail-label"><?= esc($label) ?></div>
<div class="fc-detail-value"><?= esc($value) ?></div>
</div>
<?php endforeach; ?>
</div>
<div class="row g-3 mt-1">
<div class="col-12 col-lg-6">
<div class="card h-100 fc-health-card">
<div class="card-body">
<div class="fc-detail-label mb-2">Medical Conditions</div>
<?php if (empty($conditions)): ?>
<div class="text-muted">None on file</div>
<?php else: ?>
<ul class="mb-0 ps-3">
<?php foreach ($conditions as $condition): ?>
<li><?= esc($condition) ?></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</div>
</div>
</div>
<div class="col-12 col-lg-6">
<div class="card h-100 fc-health-card">
<div class="card-body">
<div class="fc-detail-label mb-2">Allergies</div>
<?php if (empty($allergies)): ?>
<div class="text-muted">None on file</div>
<?php else: ?>
<ul class="mb-0 ps-3">
<?php foreach ($allergies as $allergy): ?>
<li><?= esc($allergy) ?></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</div>
</div>
</div>
</div>
</div>
</div>
<?php if (!empty($s['grade'])): ?>
<span class="badge text-bg-secondary"><?= esc($s['grade']) ?></span>
<?php endif; ?>
</li>
</div>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
<!-- Scores History -->
<div class="tab-pane fade" id="fc-scores" role="tabpanel" aria-labelledby="fc-tab-scores">
<div class="p-3">
<?php if (empty($f['students'])): ?>
<div class="alert alert-light border mb-0">No students linked.</div>
<?php else: ?>
<ul class="nav nav-pills fc-score-student-nav gap-2 mb-3" role="tablist">
<?php foreach ($f['students'] as $s): ?>
<?php
$scoreStudentId = (int)($s['id'] ?? 0);
$scoreStudentActive = $scoreStudentId === $scoreDefaultStudentId;
$scorePaneId = 'fc-score-student-' . (int)($f['id'] ?? 0) . '-' . $scoreStudentId;
?>
<li class="nav-item" role="presentation">
<button
class="nav-link text-nowrap <?= $scoreStudentActive ? 'active' : '' ?>"
data-bs-toggle="pill"
data-bs-target="#<?= esc($scorePaneId) ?>"
type="button"
role="tab"
aria-selected="<?= $scoreStudentActive ? 'true' : 'false' ?>"
>
<?= esc(trim(($s['firstname'] ?? '') . ' ' . ($s['lastname'] ?? ''))) ?>
</button>
</li>
<?php endforeach; ?>
</ul>
<div class="tab-content">
<?php foreach ($f['students'] as $s): ?>
<?php
$scoreStudentId = (int)($s['id'] ?? 0);
$scoreStudentActive = $scoreStudentId === $scoreDefaultStudentId;
$scorePaneId = 'fc-score-student-' . (int)($f['id'] ?? 0) . '-' . $scoreStudentId;
$scoreHistory = (array)($s['score_history'] ?? []);
?>
<div class="tab-pane fade <?= $scoreStudentActive ? 'show active' : '' ?>" id="<?= esc($scorePaneId) ?>" role="tabpanel">
<?php if (empty($scoreHistory)): ?>
<div class="alert alert-light border mb-0">No score history found for this student.</div>
<?php else: ?>
<div class="table-responsive">
<table class="table table-sm table-bordered table-striped align-middle fc-score-table mb-0">
<thead class="table-light">
<tr>
<th>School Year</th>
<th>Midterm S1</th>
<th class="fc-score-comment">Midterm Comment S1</th>
<th>PTAP S1</th>
<th class="fc-score-comment">PTAP Comment S1</th>
<th>Attendance S1</th>
<th class="fc-score-comment">Attendance Comment S1</th>
<th>Final S2</th>
<th class="fc-score-comment">Final Comment S2</th>
<th>PTAP S2</th>
<th class="fc-score-comment">PTAP Comment S2</th>
<th>Attendance S2</th>
<th class="fc-score-comment">Attendance Comment S2</th>
<th>Year Score</th>
</tr>
</thead>
<tbody>
<?php foreach ($scoreHistory as $history): ?>
<tr>
<td data-label="School Year" class="text-nowrap fw-semibold"><?= esc($history['school_year'] ?? '—') ?></td>
<td data-label="Midterm S1"><?= esc($formatScore($history['midterm_s1'] ?? null)) ?></td>
<td data-label="Midterm Comment S1" class="fc-score-comment"><?= esc($displayValue($history['midterm_comment_s1'] ?? null, '—')) ?></td>
<td data-label="PTAP S1"><?= esc($formatScore($history['ptap_s1'] ?? null)) ?></td>
<td data-label="PTAP Comment S1" class="fc-score-comment"><?= esc($displayValue($history['ptap_comment_s1'] ?? null, '—')) ?></td>
<td data-label="Attendance S1"><?= esc($formatScore($history['attendance_s1'] ?? null)) ?></td>
<td data-label="Attendance Comment S1" class="fc-score-comment"><?= esc($displayValue($history['attendance_comment_s1'] ?? null, '—')) ?></td>
<td data-label="Final S2"><?= esc($formatScore($history['final_s2'] ?? null)) ?></td>
<td data-label="Final Comment S2" class="fc-score-comment"><?= esc($displayValue($history['final_comment_s2'] ?? null, '—')) ?></td>
<td data-label="PTAP S2"><?= esc($formatScore($history['ptap_s2'] ?? null)) ?></td>
<td data-label="PTAP Comment S2" class="fc-score-comment"><?= esc($displayValue($history['ptap_comment_s2'] ?? null, '—')) ?></td>
<td data-label="Attendance S2"><?= esc($formatScore($history['attendance_s2'] ?? null)) ?></td>
<td data-label="Attendance Comment S2" class="fc-score-comment"><?= esc($displayValue($history['attendance_comment_s2'] ?? null, '—')) ?></td>
<td data-label="Year Score" class="fw-semibold"><?= esc($formatScore($history['year_score'] ?? null)) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
<!-- Guardians -->
<div class="tab-pane fade" id="fc-guardians" role="tabpanel" aria-labelledby="fc-tab-guardians">
<div class="p-3">
@@ -281,7 +507,8 @@ if ($returnUrl === '') {
</div>
</div>
<!-- Financials -->
<?php if ($canViewInvoices): ?>
<!-- Invoices (administrative roles only) -->
<div class="tab-pane fade" id="fc-fin" role="tabpanel" aria-labelledby="fc-tab-fin">
<div class="p-3">
<div class="row g-3 mb-2">
@@ -313,7 +540,7 @@ if ($returnUrl === '') {
<div class="row g-3">
<div class="col-md-6">
<h6 class="mb-2">Invoices</h6>
<h6 class="mb-2">Parent Invoices</h6>
<?php if (empty($f['invoices'])): ?>
<div class="alert alert-light border">No invoices on record.</div>
<?php else: ?>
@@ -321,18 +548,20 @@ if ($returnUrl === '') {
<table class="table table-sm table-striped table-hover align-middle fc-table-stack">
<thead class="table-light">
<tr>
<th>#</th><th>Status</th><th>Total</th><th>Paid</th><th>Balance</th><th>Date</th>
<th>Parent</th><th>#</th><th>Status</th><th>Total</th><th>Paid</th><th>Balance</th><th>Issued</th><th>Due</th>
</tr>
</thead>
<tbody>
<?php foreach ($f['invoices'] as $iv): ?>
<tr>
<td data-label="Parent"><?= esc($iv['parent_name'] ?? ('Parent #' . (int)($iv['parent_id'] ?? 0))) ?></td>
<td data-label="Invoice"><?= esc($iv['invoice_number']) ?></td>
<td data-label="Status"><?= esc($iv['status']) ?></td>
<td data-label="Total">$<?= number_format((float)($iv['total_amount'] ?? 0), 2) ?></td>
<td data-label="Paid">$<?= number_format((float)($iv['paid_amount'] ?? 0), 2) ?></td>
<td data-label="Balance">$<?= number_format((float)($iv['balance'] ?? 0), 2) ?></td>
<td data-label="Date"><?= esc(!empty($iv['issue_date']) ? local_date($iv['issue_date'], 'm-d-Y') : '') ?></td>
<td data-label="Due"><?= esc(!empty($iv['due_date']) ? local_date($iv['due_date'], 'm-d-Y') : '—') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
@@ -350,13 +579,14 @@ if ($returnUrl === '') {
<table class="table table-sm table-striped table-hover align-middle fc-table-stack">
<thead class="table-light">
<tr>
<th>Invoice</th><th>Amount</th><th>Invoice Balance</th><th>Method</th><th>Date</th><th>Payment Status</th><th>Invoice Status</th>
<th>Parent</th><th>Invoice</th><th>Amount</th><th>Invoice Balance</th><th>Method</th><th>Date</th><th>Payment Status</th><th>Invoice Status</th>
</tr>
</thead>
<tbody>
<?php $imap = $f['invoice_map'] ?? []; ?>
<?php foreach ($f['payments'] as $p): ?>
<tr>
<td data-label="Parent"><?= esc($p['parent_name'] ?? ('Parent #' . (int)($p['parent_id'] ?? 0))) ?></td>
<td data-label="Invoice"><?php $iid = (int)($p['invoice_id'] ?? 0); echo esc($p['invoice_number'] ?? $imap[$iid] ?? ('#'.$iid)); ?></td>
<td data-label="Amount">$<?= number_format((float)($p['paid_amount'] ?? 0), 2) ?></td>
<td data-label="Invoice Balance">$<?= number_format((float)($p['invoice_current_balance'] ?? 0), 2) ?></td>
@@ -374,5 +604,6 @@ if ($returnUrl === '') {
</div>
</div>
</div>
<?php endif; ?>
</div>
</div>
+3
View File
@@ -369,6 +369,7 @@ html, body { overflow-x: hidden; }
let sid = link.getAttribute('data-family-student-id');
let gid = link.getAttribute('data-family-guardian-id');
let fid = link.getAttribute('data-family-id');
let schoolYear = link.getAttribute('data-family-school-year');
if (!sid && !gid && !fid && link.matches('a[href]')) {
try {
const url = new URL(link.getAttribute('href'), window.location.origin);
@@ -378,6 +379,7 @@ html, body { overflow-x: hidden; }
sid = url.searchParams.get('student_id');
gid = url.searchParams.get('guardian_id');
fid = url.searchParams.get('family_id');
schoolYear = url.searchParams.get('school_year');
} catch (_) {
return null;
}
@@ -386,6 +388,7 @@ html, body { overflow-x: hidden; }
if (fid) params.family_id = fid;
else if (sid) params.student_id = sid;
else if (gid) params.guardian_id = gid;
if (schoolYear) params.school_year = schoolYear;
return Object.keys(params).length ? params : null;
}
+3
View File
@@ -632,6 +632,7 @@
let sid = link.getAttribute('data-family-student-id');
let gid = link.getAttribute('data-family-guardian-id');
let fid = link.getAttribute('data-family-id');
let schoolYear = link.getAttribute('data-family-school-year');
if (!sid && !gid && !fid && link.matches('a[href]')) {
try {
const url = new URL(link.getAttribute('href'), window.location.origin);
@@ -641,6 +642,7 @@
sid = url.searchParams.get('student_id');
gid = url.searchParams.get('guardian_id');
fid = url.searchParams.get('family_id');
schoolYear = url.searchParams.get('school_year');
} catch (_) {
return null;
}
@@ -649,6 +651,7 @@
if (fid) params.family_id = fid;
else if (sid) params.student_id = sid;
else if (gid) params.guardian_id = gid;
if (schoolYear) params.school_year = schoolYear;
return Object.keys(params).length ? params : null;
}