add features to family card, scores, invoices and students details
This commit is contained in:
@@ -1074,10 +1074,14 @@ $routes->group('family', ['filter' => 'auth:admin|principal'], static function (
|
||||
$routes->get('', 'View\FamilyAdminController::index');
|
||||
$routes->get('index', 'View\FamilyAdminController::index');
|
||||
$routes->get('search', 'View\FamilyAdminController::search');
|
||||
$routes->get('card', 'View\FamilyAdminController::card');
|
||||
$routes->get('compose-email', 'View\FamilyAdminController::composeEmail');
|
||||
$routes->post('compose-email/send', 'View\FamilyAdminController::sendComposeEmail');
|
||||
});
|
||||
// Teachers and TAs may inspect family/student details from their student lists.
|
||||
// Financial data remains protected inside FamilyAdminController::card().
|
||||
$routes->get('family/card', 'View\FamilyAdminController::card', [
|
||||
'filter' => 'auth:admin|administrator|administrative staff|principal|teacher|teacher_assistant',
|
||||
]);
|
||||
// Convenience alias
|
||||
$routes->get('family', 'View\FamilyAdminController::index', ['filter' => 'auth:admin|principal']);
|
||||
//////////////////////////////////////////////////////////
|
||||
|
||||
@@ -239,6 +239,7 @@ class FamilyAdminController extends BaseController
|
||||
public function card(): ResponseInterface
|
||||
{
|
||||
$db = \Config\Database::connect();
|
||||
$canViewInvoices = $this->canViewFamilyInvoices();
|
||||
$studentId = (int) ($this->request->getGet('student_id') ?? 0);
|
||||
$guardianId = (int) ($this->request->getGet('guardian_id') ?? 0);
|
||||
$familyId = (int) ($this->request->getGet('family_id') ?? 0);
|
||||
@@ -338,9 +339,8 @@ class FamilyAdminController extends BaseController
|
||||
// Hydrate with guardians, students (+grades), invoices, payments
|
||||
$invoiceModel = new \App\Models\InvoiceModel();
|
||||
$paymentModel = new \App\Models\PaymentModel();
|
||||
$studentClassModel = new \App\Models\StudentClassModel();
|
||||
$configModel = new \App\Models\ConfigurationModel();
|
||||
$schoolYear = (string) ($configModel->getConfig('school_year') ?? '');
|
||||
$schoolYear = $this->currentSchoolYearName((string) ($configModel->getConfig('school_year') ?? ''));
|
||||
|
||||
// Guardians
|
||||
$guardians = $db->query(
|
||||
@@ -354,10 +354,11 @@ class FamilyAdminController extends BaseController
|
||||
[$familyId]
|
||||
)->getResultArray();
|
||||
$family['guardians'] = $guardians;
|
||||
$family['can_view_invoices'] = $canViewInvoices;
|
||||
|
||||
// Students
|
||||
$studentsRows = $db->query(
|
||||
"SELECT s.id, s.firstname, s.lastname
|
||||
"SELECT s.*
|
||||
FROM family_students fs
|
||||
JOIN students s ON s.id = fs.student_id
|
||||
WHERE fs.family_id = ?
|
||||
@@ -369,7 +370,7 @@ class FamilyAdminController extends BaseController
|
||||
$studentIds = array_map(static fn(array $row): int => (int) ($row['id'] ?? 0), $studentsRows);
|
||||
if (!in_array($studentId, $studentIds, true)) {
|
||||
$selectedStudent = $db->query(
|
||||
"SELECT id, firstname, lastname
|
||||
"SELECT *
|
||||
FROM students
|
||||
WHERE id = ?
|
||||
LIMIT 1",
|
||||
@@ -383,13 +384,101 @@ class FamilyAdminController extends BaseController
|
||||
}
|
||||
|
||||
if (!empty($studentsRows)) {
|
||||
$studentIds = array_values(array_filter(array_map(
|
||||
static fn(array $row): int => (int) ($row['id'] ?? 0),
|
||||
$studentsRows
|
||||
)));
|
||||
$allergiesByStudent = [];
|
||||
$conditionsByStudent = [];
|
||||
$classAssignmentsByStudent = [];
|
||||
$enrollmentByStudent = [];
|
||||
$scoreHistoryByStudent = [];
|
||||
|
||||
if (!empty($studentIds)) {
|
||||
if ($schoolYear !== '') {
|
||||
$classSectionJoin = 'cs.class_section_id = sc.class_section_id';
|
||||
if ($db->fieldExists('school_year', 'classSection')) {
|
||||
$classSectionJoin .= ' AND (cs.school_year = sc.school_year OR cs.school_year IS NULL)';
|
||||
}
|
||||
|
||||
$classAssignmentRows = $db->table('student_class sc')
|
||||
->select('sc.student_id, cs.class_section_name, c.class_name')
|
||||
->join('classSection cs', $classSectionJoin, 'left')
|
||||
->join('classes c', 'c.id = cs.class_id', 'left')
|
||||
->whereIn('sc.student_id', $studentIds)
|
||||
->where('sc.school_year', $schoolYear)
|
||||
->where('sc.class_section_id IS NOT NULL', null, false)
|
||||
->orderBy('cs.class_section_name', 'ASC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
foreach ($classAssignmentRows as $classAssignmentRow) {
|
||||
$assignmentStudentId = (int) ($classAssignmentRow['student_id'] ?? 0);
|
||||
$className = trim((string) ($classAssignmentRow['class_name'] ?? ''));
|
||||
$sectionName = trim((string) ($classAssignmentRow['class_section_name'] ?? ''));
|
||||
$label = $className !== '' && $sectionName !== '' && strcasecmp($className, $sectionName) !== 0
|
||||
? $className . ' / ' . $sectionName
|
||||
: ($sectionName !== '' ? $sectionName : $className);
|
||||
|
||||
if ($assignmentStudentId > 0 && $label !== '') {
|
||||
$classAssignmentsByStudent[$assignmentStudentId][$label] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$enrollmentRows = $db->table('enrollments e')
|
||||
->select('e.student_id, e.enrollment_status')
|
||||
->whereIn('e.student_id', $studentIds)
|
||||
->where('e.school_year', $schoolYear)
|
||||
->orderBy('e.updated_at', 'DESC')
|
||||
->orderBy('e.enrollment_date', 'DESC')
|
||||
->orderBy('e.id', 'DESC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
foreach ($enrollmentRows as $enrollmentRow) {
|
||||
$enrollmentStudentId = (int) ($enrollmentRow['student_id'] ?? 0);
|
||||
if ($enrollmentStudentId > 0 && !isset($enrollmentByStudent[$enrollmentStudentId])) {
|
||||
$enrollmentByStudent[$enrollmentStudentId] = $enrollmentRow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$allergyRows = $db->table('student_allergies')
|
||||
->select('student_id, allergy')
|
||||
->whereIn('student_id', $studentIds)
|
||||
->orderBy('allergy', 'ASC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
foreach ($allergyRows as $allergyRow) {
|
||||
$allergiesByStudent[(int) ($allergyRow['student_id'] ?? 0)][] = (string) ($allergyRow['allergy'] ?? '');
|
||||
}
|
||||
|
||||
$conditionRows = $db->table('student_medical_conditions')
|
||||
->select('student_id, condition_name')
|
||||
->whereIn('student_id', $studentIds)
|
||||
->orderBy('condition_name', 'ASC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
foreach ($conditionRows as $conditionRow) {
|
||||
$conditionsByStudent[(int) ($conditionRow['student_id'] ?? 0)][] = (string) ($conditionRow['condition_name'] ?? '');
|
||||
}
|
||||
|
||||
$scoreHistoryByStudent = (new \App\Services\StudentScoreHistoryService($db))
|
||||
->forStudents($studentIds);
|
||||
}
|
||||
|
||||
foreach ($studentsRows as &$sr) {
|
||||
$sid = (int) ($sr['id'] ?? 0);
|
||||
$sr['grade'] = $sid ? (string) ($studentClassModel->getClassSectionsByStudentId($sid, $schoolYear) ?? '') : '';
|
||||
$enrollment = $enrollmentByStudent[$sid] ?? [];
|
||||
$sr['grade'] = implode(', ', array_keys($classAssignmentsByStudent[$sid] ?? []));
|
||||
$sr['enrollment_status'] = (string) ($enrollment['enrollment_status'] ?? '');
|
||||
$sr['allergies'] = $allergiesByStudent[$sid] ?? [];
|
||||
$sr['medical_conditions'] = $conditionsByStudent[$sid] ?? [];
|
||||
$sr['score_history'] = $scoreHistoryByStudent[$sid] ?? [];
|
||||
}
|
||||
unset($sr);
|
||||
}
|
||||
$family['students'] = $studentsRows;
|
||||
$family['selected_student_id'] = $studentId;
|
||||
|
||||
// Financials
|
||||
$parentIds = array_map(static fn($g) => (int)($g['user_id'] ?? 0), $guardians);
|
||||
@@ -427,13 +516,21 @@ class FamilyAdminController extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($parentIds)) {
|
||||
if ($canViewInvoices && !empty($parentIds)) {
|
||||
// Invoices
|
||||
$invRows = $db->table('invoices')
|
||||
$invoiceBuilder = $db->table('invoices')
|
||||
->select('id, parent_id, invoice_number, status, total_amount, paid_amount, balance, issue_date, due_date')
|
||||
->whereIn('parent_id', $parentIds)
|
||||
->orderBy('issue_date', 'DESC')
|
||||
->get()->getResultArray();
|
||||
->orderBy('issue_date', 'DESC');
|
||||
if ($schoolYear !== '') {
|
||||
$invoiceBuilder->where('school_year', $schoolYear);
|
||||
}
|
||||
$invRows = $invoiceBuilder->get()->getResultArray();
|
||||
foreach ($invRows as &$invoiceRow) {
|
||||
$invoiceParentId = (int) ($invoiceRow['parent_id'] ?? 0);
|
||||
$invoiceRow['parent_name'] = $gmap[$invoiceParentId] ?? ('Parent #' . $invoiceParentId);
|
||||
}
|
||||
unset($invoiceRow);
|
||||
$family['invoices'] = $invRows;
|
||||
$invoiceMap = [];
|
||||
foreach ($invRows as $ir) {
|
||||
@@ -449,20 +546,43 @@ class FamilyAdminController extends BaseController
|
||||
}
|
||||
|
||||
// Payments
|
||||
$payRows = $db->table('payments p')
|
||||
$paymentBuilder = $db->table('payments p')
|
||||
->select('p.id, p.parent_id, p.invoice_id, p.paid_amount, p.payment_method, p.payment_date, p.status AS payment_status, p.installment_seq, p.number_of_installments, i.invoice_number, i.balance AS invoice_current_balance, i.status AS invoice_status, i.school_year')
|
||||
->join('invoices i', 'i.id = p.invoice_id', 'inner')
|
||||
->whereIn('p.parent_id', $parentIds)
|
||||
->orderBy('p.payment_date', 'DESC')
|
||||
->orderBy('p.id', 'DESC')
|
||||
->limit(10)
|
||||
->get()->getResultArray();
|
||||
->limit(10);
|
||||
if ($schoolYear !== '') {
|
||||
$paymentBuilder->where('i.school_year', $schoolYear);
|
||||
}
|
||||
$payRows = $paymentBuilder->get()->getResultArray();
|
||||
foreach ($payRows as &$paymentRow) {
|
||||
$paymentParentId = (int) ($paymentRow['parent_id'] ?? 0);
|
||||
$paymentRow['parent_name'] = $gmap[$paymentParentId] ?? ('Parent #' . $paymentParentId);
|
||||
}
|
||||
unset($paymentRow);
|
||||
$family['payments'] = $payRows;
|
||||
}
|
||||
|
||||
return service('response')->setBody(view('family/card', ['f' => $family]));
|
||||
}
|
||||
|
||||
private function canViewFamilyInvoices(): bool
|
||||
{
|
||||
$roles = array_map(
|
||||
static fn($role): string => strtolower(trim((string) $role)),
|
||||
array_filter(array_merge((array) session()->get('roles'), [session()->get('role')]))
|
||||
);
|
||||
|
||||
return (bool) array_intersect(array_unique($roles), [
|
||||
'admin',
|
||||
'administrator',
|
||||
'administrative staff',
|
||||
'principal',
|
||||
]);
|
||||
}
|
||||
|
||||
public function composeEmail()
|
||||
{
|
||||
$to = trim((string)$this->request->getGet('to'));
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use CodeIgniter\Database\BaseConnection;
|
||||
|
||||
final class StudentScoreHistoryService
|
||||
{
|
||||
public function __construct(private readonly BaseConnection $db)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Build one score-history row per student and school year.
|
||||
*
|
||||
* @return array<int, array<int, array<string, mixed>>>
|
||||
*/
|
||||
public function forStudents(array $studentIds): array
|
||||
{
|
||||
$studentIds = array_values(array_unique(array_filter(
|
||||
array_map('intval', $studentIds),
|
||||
static fn(int $id): bool => $id > 0
|
||||
)));
|
||||
if ($studentIds === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$history = [];
|
||||
$semesterRows = $this->latestSemesterRows($studentIds);
|
||||
foreach ($semesterRows as $row) {
|
||||
$studentId = (int) ($row['student_id'] ?? 0);
|
||||
$schoolYear = trim((string) ($row['school_year'] ?? ''));
|
||||
$semester = $this->normalizeSemester($row['semester'] ?? '');
|
||||
if ($studentId <= 0 || $schoolYear === '' || !in_array($semester, ['fall', 'spring'], true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$history[$studentId][$schoolYear] ??= $this->emptyYear($schoolYear);
|
||||
if ($semester === 'fall') {
|
||||
$history[$studentId][$schoolYear]['midterm_s1'] = $this->score($row['midterm_exam_score'] ?? null);
|
||||
$history[$studentId][$schoolYear]['ptap_s1'] = $this->score($row['ptap_score'] ?? null);
|
||||
$history[$studentId][$schoolYear]['attendance_s1'] = $this->score($row['attendance_score'] ?? null);
|
||||
$history[$studentId][$schoolYear]['_semester_score_s1'] = $this->score($row['semester_score'] ?? null);
|
||||
} else {
|
||||
$finalScore = $this->score($row['final_exam_score'] ?? null);
|
||||
if ($finalScore === null) {
|
||||
// Some older Spring rows stored the final in this legacy column.
|
||||
$finalScore = $this->score($row['midterm_exam_score'] ?? null);
|
||||
}
|
||||
$history[$studentId][$schoolYear]['final_s2'] = $finalScore;
|
||||
$history[$studentId][$schoolYear]['ptap_s2'] = $this->score($row['ptap_score'] ?? null);
|
||||
$history[$studentId][$schoolYear]['attendance_s2'] = $this->score($row['attendance_score'] ?? null);
|
||||
$history[$studentId][$schoolYear]['_semester_score_s2'] = $this->score($row['semester_score'] ?? null);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->commentRows($studentIds) as $row) {
|
||||
$studentId = (int) ($row['student_id'] ?? 0);
|
||||
$schoolYear = trim((string) ($row['school_year'] ?? ''));
|
||||
$semester = $this->normalizeSemester($row['semester'] ?? '');
|
||||
$type = $this->normalizeCommentType($row['score_type'] ?? '');
|
||||
if ($studentId <= 0 || $schoolYear === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$history[$studentId][$schoolYear] ??= $this->emptyYear($schoolYear);
|
||||
|
||||
$field = match ($semester . ':' . $type) {
|
||||
'fall:midterm' => 'midterm_comment_s1',
|
||||
'fall:ptap' => 'ptap_comment_s1',
|
||||
'fall:attendance' => 'attendance_comment_s1',
|
||||
'spring:final' => 'final_comment_s2',
|
||||
'spring:ptap' => 'ptap_comment_s2',
|
||||
'spring:attendance' => 'attendance_comment_s2',
|
||||
default => null,
|
||||
};
|
||||
if ($field === null || $history[$studentId][$schoolYear][$field] !== '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$comment = trim((string) ($row['comment'] ?? ''));
|
||||
$review = trim((string) ($row['comment_review'] ?? ''));
|
||||
$history[$studentId][$schoolYear][$field] = $type === 'attendance'
|
||||
? ($comment !== '' ? $comment : $review)
|
||||
: ($review !== '' ? $review : $comment);
|
||||
}
|
||||
|
||||
foreach ($this->decisionRows($studentIds) as $row) {
|
||||
$studentId = (int) ($row['student_id'] ?? 0);
|
||||
$schoolYear = trim((string) ($row['school_year'] ?? ''));
|
||||
if ($studentId <= 0 || $schoolYear === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$history[$studentId][$schoolYear] ??= $this->emptyYear($schoolYear);
|
||||
if ($history[$studentId][$schoolYear]['year_score'] === null) {
|
||||
$history[$studentId][$schoolYear]['year_score'] = $this->score($row['year_score'] ?? null);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($history as &$studentHistory) {
|
||||
foreach ($studentHistory as &$year) {
|
||||
if ($year['year_score'] === null) {
|
||||
$semesterScores = array_values(array_filter(
|
||||
[$year['_semester_score_s1'], $year['_semester_score_s2']],
|
||||
static fn($score): bool => $score !== null
|
||||
));
|
||||
if ($semesterScores !== []) {
|
||||
$year['year_score'] = round(array_sum($semesterScores) / count($semesterScores), 1);
|
||||
}
|
||||
}
|
||||
unset($year['_semester_score_s1'], $year['_semester_score_s2']);
|
||||
}
|
||||
unset($year);
|
||||
krsort($studentHistory, SORT_STRING);
|
||||
$studentHistory = array_values($studentHistory);
|
||||
}
|
||||
unset($studentHistory);
|
||||
|
||||
return $history;
|
||||
}
|
||||
|
||||
private function latestSemesterRows(array $studentIds): array
|
||||
{
|
||||
if (!$this->db->tableExists('semester_scores')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$rows = $this->db->table('semester_scores')
|
||||
->select('id, student_id, school_year, semester, midterm_exam_score, final_exam_score, ptap_score, attendance_score, semester_score, updated_at')
|
||||
->whereIn('student_id', $studentIds)
|
||||
->orderBy('school_year', 'DESC')
|
||||
->orderBy('updated_at', 'DESC')
|
||||
->orderBy('id', 'DESC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
$latest = [];
|
||||
foreach ($rows as $row) {
|
||||
$key = (int) ($row['student_id'] ?? 0)
|
||||
. '|' . trim((string) ($row['school_year'] ?? ''))
|
||||
. '|' . $this->normalizeSemester($row['semester'] ?? '');
|
||||
$latest[$key] ??= $row;
|
||||
}
|
||||
|
||||
return array_values($latest);
|
||||
}
|
||||
|
||||
private function commentRows(array $studentIds): array
|
||||
{
|
||||
if (!$this->db->tableExists('score_comments')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$builder = $this->db->table('score_comments')
|
||||
->select('id, student_id, school_year, semester, score_type, comment, comment_review')
|
||||
->whereIn('student_id', $studentIds);
|
||||
if ($this->db->fieldExists('updated_at', 'score_comments')) {
|
||||
$builder->orderBy('updated_at', 'DESC');
|
||||
} elseif ($this->db->fieldExists('created_at', 'score_comments')) {
|
||||
$builder->orderBy('created_at', 'DESC');
|
||||
}
|
||||
|
||||
return $builder->orderBy('id', 'DESC')->get()->getResultArray();
|
||||
}
|
||||
|
||||
private function decisionRows(array $studentIds): array
|
||||
{
|
||||
if (!$this->db->tableExists('student_decisions')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->db->table('student_decisions')
|
||||
->select('id, student_id, school_year, year_score')
|
||||
->whereIn('student_id', $studentIds)
|
||||
->orderBy('school_year', 'DESC')
|
||||
->orderBy('updated_at', 'DESC')
|
||||
->orderBy('id', 'DESC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
}
|
||||
|
||||
private function emptyYear(string $schoolYear): array
|
||||
{
|
||||
return [
|
||||
'school_year' => $schoolYear,
|
||||
'midterm_s1' => null,
|
||||
'midterm_comment_s1' => '',
|
||||
'ptap_s1' => null,
|
||||
'ptap_comment_s1' => '',
|
||||
'attendance_s1' => null,
|
||||
'attendance_comment_s1' => '',
|
||||
'final_s2' => null,
|
||||
'final_comment_s2' => '',
|
||||
'ptap_s2' => null,
|
||||
'ptap_comment_s2' => '',
|
||||
'attendance_s2' => null,
|
||||
'attendance_comment_s2' => '',
|
||||
'year_score' => null,
|
||||
'_semester_score_s1' => null,
|
||||
'_semester_score_s2' => null,
|
||||
];
|
||||
}
|
||||
|
||||
private function normalizeSemester($value): string
|
||||
{
|
||||
$semester = strtolower(trim((string) $value));
|
||||
return match ($semester) {
|
||||
'fall', 'first', 'first semester', 'semester 1', '1' => 'fall',
|
||||
'spring', 'second', 'second semester', 'semester 2', '2' => 'spring',
|
||||
default => $semester,
|
||||
};
|
||||
}
|
||||
|
||||
private function normalizeCommentType($value): string
|
||||
{
|
||||
$type = strtolower(trim((string) $value));
|
||||
$type = trim((string) preg_replace('/[^a-z0-9]+/', '_', $type), '_');
|
||||
|
||||
return match ($type) {
|
||||
'midterm_comment', 'midterm_comments' => 'midterm',
|
||||
'final_comment', 'final_comments' => 'final',
|
||||
'ptap_comment', 'ptap_comments' => 'ptap',
|
||||
'attendance_comment', 'attendance_comments', 'attendence', 'attendence_comment' => 'attendance',
|
||||
default => $type,
|
||||
};
|
||||
}
|
||||
|
||||
private function score($value): ?float
|
||||
{
|
||||
return $value !== null && $value !== '' && is_numeric($value)
|
||||
? round((float) $value, 2)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\App\Config;
|
||||
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
|
||||
final class FamilyCardRouteIntegrityTest extends CIUnitTestCase
|
||||
{
|
||||
public function testFamilyCardAllowsTeachersWhileFamilyManagementRemainsAdministrative(): void
|
||||
{
|
||||
$routes = file_get_contents(ROOTPATH . 'app/Config/Routes.php') ?: '';
|
||||
|
||||
$this->assertStringContainsString(
|
||||
"\$routes->group('family', ['filter' => 'auth:admin|principal']",
|
||||
$routes
|
||||
);
|
||||
$this->assertStringContainsString(
|
||||
"\$routes->get('family/card', 'View\\FamilyAdminController::card'",
|
||||
$routes
|
||||
);
|
||||
$this->assertStringContainsString(
|
||||
"auth:admin|administrator|administrative staff|principal|teacher|teacher_assistant",
|
||||
$routes
|
||||
);
|
||||
$this->assertStringNotContainsString(
|
||||
"\$routes->get('card', 'View\\FamilyAdminController::card');",
|
||||
$routes
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\App\Controllers\View;
|
||||
|
||||
use App\Controllers\View\FamilyAdminController;
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
|
||||
final class FamilyAdminControllerAccessTest extends CIUnitTestCase
|
||||
{
|
||||
protected function tearDown(): void
|
||||
{
|
||||
session()->remove(['role', 'roles']);
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function testTeachersAndAssistantsCannotViewFamilyInvoices(): void
|
||||
{
|
||||
$controller = new FamilyAdminController();
|
||||
$method = new \ReflectionMethod($controller, 'canViewFamilyInvoices');
|
||||
|
||||
session()->set('role', 'teacher');
|
||||
$this->assertFalse($method->invoke($controller));
|
||||
|
||||
session()->set('role', 'teacher_assistant');
|
||||
$this->assertFalse($method->invoke($controller));
|
||||
}
|
||||
|
||||
public function testAdministrativeRolesCanViewFamilyInvoices(): void
|
||||
{
|
||||
$controller = new FamilyAdminController();
|
||||
$method = new \ReflectionMethod($controller, 'canViewFamilyInvoices');
|
||||
|
||||
foreach (['admin', 'administrator', 'administrative staff', 'principal'] as $role) {
|
||||
session()->set('role', $role);
|
||||
$this->assertTrue($method->invoke($controller), $role);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,7 @@ class InventorySchemaNormalizationTest extends CIUnitTestCase
|
||||
{
|
||||
$routes = file_get_contents(ROOTPATH . 'app/Config/Routes.php');
|
||||
|
||||
$this->assertStringContainsString("\$routes->get('card', 'View\\FamilyAdminController::card')", $routes);
|
||||
$this->assertStringContainsString("\$routes->get('family/card', 'View\\FamilyAdminController::card'", $routes);
|
||||
$this->assertStringContainsString("\$routes->get('summary-all', 'View\\InventoryController::summaryAll')", $routes);
|
||||
$this->assertStringContainsString("\$routes->get('/', 'View\\InventoryController::index')", $routes);
|
||||
$this->assertStringContainsString("\$routes->get('(classroom|book|office|kitchen)', 'View\\InventoryController::index/\$1')", $routes);
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\App\Views;
|
||||
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
|
||||
final class FamilyCardViewTest extends CIUnitTestCase
|
||||
{
|
||||
public function testStudentNameExpandsCompleteStudentDetails(): void
|
||||
{
|
||||
helper('time');
|
||||
|
||||
$html = view('family/card', [
|
||||
'f' => [
|
||||
'id' => 9,
|
||||
'household_name' => 'Family of Tester',
|
||||
'guardians' => [],
|
||||
'students' => [[
|
||||
'id' => 12,
|
||||
'school_id' => 'STU-12',
|
||||
'firstname' => 'Test',
|
||||
'lastname' => 'Student',
|
||||
'dob' => '2015-01-02',
|
||||
'age' => 11,
|
||||
'gender' => 'Female',
|
||||
'grade' => '5 / 5-A',
|
||||
'enrollment_status' => 'enrolled',
|
||||
'registration_grade' => '5',
|
||||
'is_active' => 1,
|
||||
'photo_consent' => 1,
|
||||
'registration_date' => '2025-09-02 14:30:00',
|
||||
'year_of_registration' => '2025',
|
||||
'rfid_tag' => 'RF-12',
|
||||
'tuition_paid' => 1,
|
||||
'allergies' => ['Peanut'],
|
||||
'medical_conditions' => ['Asthma'],
|
||||
'score_history' => [[
|
||||
'school_year' => '2025-2026',
|
||||
'midterm_s1' => 88.5,
|
||||
'midterm_comment_s1' => 'Strong first semester.',
|
||||
'ptap_s1' => 91,
|
||||
'ptap_comment_s1' => 'Participates consistently.',
|
||||
'attendance_s1' => 95,
|
||||
'attendance_comment_s1' => 'Excellent attendance.',
|
||||
'final_s2' => 92,
|
||||
'final_comment_s2' => 'Excellent finish.',
|
||||
'ptap_s2' => 94,
|
||||
'ptap_comment_s2' => 'Continued strong participation.',
|
||||
'attendance_s2' => 100,
|
||||
'attendance_comment_s2' => 'Perfect attendance.',
|
||||
'year_score' => 93.25,
|
||||
]],
|
||||
]],
|
||||
'selected_student_id' => 12,
|
||||
'emergency_contacts' => [],
|
||||
'finance_summary' => [],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertStringContainsString('Test Student', $html);
|
||||
$this->assertStringContainsString('data-bs-target="#fc-student-details-9-12"', $html);
|
||||
$this->assertStringContainsString('accordion-collapse collapse show', $html);
|
||||
$this->assertStringContainsString('Student Details', $html);
|
||||
$this->assertStringContainsString('STU-12', $html);
|
||||
$this->assertStringContainsString('5 / 5-A', $html);
|
||||
$this->assertStringContainsString('Enrollment Status', $html);
|
||||
$this->assertStringNotContainsString('Enrollment School Year', $html);
|
||||
$this->assertStringNotContainsString('Enrollment Semester', $html);
|
||||
$this->assertStringContainsString('Peanut', $html);
|
||||
$this->assertStringContainsString('Asthma', $html);
|
||||
$this->assertStringContainsString('Scores History', $html);
|
||||
$this->assertStringContainsString('Midterm Comment S1', $html);
|
||||
$this->assertStringContainsString('Attendance Comment S2', $html);
|
||||
$this->assertStringContainsString('Strong first semester.', $html);
|
||||
$this->assertStringContainsString('93.25', $html);
|
||||
$this->assertStringNotContainsString('data-family-student-id="12"', $html);
|
||||
$this->assertStringNotContainsString('id="fc-tab-fin"', $html);
|
||||
$this->assertStringNotContainsString('Invoices:', $html);
|
||||
}
|
||||
|
||||
public function testAdministrativeFamilyCardDisplaysParentInvoices(): void
|
||||
{
|
||||
helper('time');
|
||||
|
||||
$html = view('family/card', [
|
||||
'f' => [
|
||||
'id' => 10,
|
||||
'household_name' => 'Family of Admin Test',
|
||||
'guardians' => [],
|
||||
'students' => [],
|
||||
'emergency_contacts' => [],
|
||||
'can_view_invoices' => true,
|
||||
'finance_summary' => [
|
||||
'invoices_count' => 1,
|
||||
'total_amount' => 500,
|
||||
'paid_amount' => 200,
|
||||
'balance' => 300,
|
||||
],
|
||||
'invoices' => [[
|
||||
'parent_id' => 88,
|
||||
'parent_name' => 'Parent Person',
|
||||
'invoice_number' => 'INV-2026-001',
|
||||
'status' => 'Partially Paid',
|
||||
'total_amount' => 500,
|
||||
'paid_amount' => 200,
|
||||
'balance' => 300,
|
||||
'issue_date' => '2026-09-01 12:00:00',
|
||||
'due_date' => '2026-09-30 12:00:00',
|
||||
]],
|
||||
'payments' => [],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertStringContainsString('id="fc-tab-fin"', $html);
|
||||
$this->assertStringContainsString('Parent Invoices', $html);
|
||||
$this->assertStringContainsString('Parent Person', $html);
|
||||
$this->assertStringContainsString('INV-2026-001', $html);
|
||||
$this->assertStringContainsString('Invoices: 1', $html);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user