AVP-78 Feature to check if parents looked at report card

This commit is contained in:
root
2026-02-27 01:21:39 -05:00
parent fe7b99581d
commit 3cc5546733
37 changed files with 743 additions and 107 deletions
+4
View File
@@ -75,6 +75,7 @@
$subjectSections = $subjectSections ?? [];
$classSections = $classSections ?? [];
$sectionStats = $sectionStats ?? [];
$sectionSubjectCounts = $sectionSubjectCounts ?? [];
$expectedDays = (int) ($expectedDays ?? 0);
?>
<?php if (empty($classSections)): ?>
@@ -90,6 +91,7 @@
$collapseId = 'progress-section-' . $sectionId;
$headingId = 'progress-heading-' . $sectionId;
$stat = $sectionStats[$sectionId] ?? null;
$subjectCount = (int) ($sectionSubjectCounts[$sectionId] ?? 0);
if (! $stat) {
if ($expectedDays === 0) {
$stat = [
@@ -113,12 +115,14 @@
$submissionLabel = $expectedDays > 0
? ('Submitted: ' . (int) $stat['submitted'] . ' / ' . (int) $expectedDays . ' (' . $percentLabel . ')')
: 'Submitted: N/A';
$subjectLabel = 'Subjects: ' . $subjectCount;
?>
<div class="accordion-item mb-2">
<h2 class="accordion-header" id="<?= esc($headingId) ?>">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#<?= esc($collapseId) ?>" aria-expanded="false" aria-controls="<?= esc($collapseId) ?>">
<?= esc($sectionName) ?>
<span class="badge <?= esc($stat['badgeClass']) ?> ms-2"><?= esc($submissionLabel) ?></span>
<span class="badge bg-info text-dark ms-2"><?= esc($subjectLabel) ?></span>
<?php if ($hasReports): ?>
<span class="badge bg-secondary ms-2"><?= count($sectionGroups) ?> weeks</span>
<?php endif; ?>
@@ -121,6 +121,15 @@
</div>
</div>
<!-- Report Cards -->
<div class="col-auto">
<a href="<?= base_url('parent/report-cards') ?>" class="d-inline-block text-decoration-none">
<div class="circle-box bg-dark text-white position-relative" role="button" aria-label="Open report cards">
<h4 class="fw-bold mb-0">Report Cards</h4>
</div>
</a>
</div>
<!-- Enrollment -->
<div class="col-auto">
<div class="circle-box bg-info text-white">
+72
View File
@@ -0,0 +1,72 @@
<?= $this->extend('layout/main_layout') ?>
<?= $this->section('content') ?>
<div class="container my-5">
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-3">
<div>
<h3 class="text-success mb-1" style="font-family: Arial, sans-serif;">Report Cards</h3>
<div class="text-muted small">
<?= esc($schoolYear ?: 'N/A') ?> <?= $semester ? '• ' . esc($semester) . ' Semester' : '' ?>
</div>
</div>
</div>
<?php if (session()->getFlashdata('error')): ?>
<div class="alert alert-danger"><?= esc(session()->getFlashdata('error')) ?></div>
<?php endif; ?>
<?php if (session()->getFlashdata('success')): ?>
<div class="alert alert-success"><?= esc(session()->getFlashdata('success')) ?></div>
<?php endif; ?>
<?php if (empty($students)): ?>
<div class="alert alert-info">No students available for report cards.</div>
<?php else: ?>
<div class="table-responsive">
<table class="table table-striped table-bordered align-middle">
<thead class="table-light">
<tr>
<th>Student</th>
<th>Class Section</th>
<th>Viewed</th>
<th>Signature</th>
<th class="text-end">Action</th>
</tr>
</thead>
<tbody>
<?php foreach ($students as $student): ?>
<?php
$sid = (int) ($student['id'] ?? 0);
$ack = $ackMap[$sid] ?? null;
$viewedAt = $ack['viewed_at'] ?? '';
$signedAt = $ack['signed_at'] ?? '';
$signedName = $ack['signed_name'] ?? '';
?>
<tr>
<td><?= esc(trim(($student['firstname'] ?? '') . ' ' . ($student['lastname'] ?? ''))) ?></td>
<td><?= esc($student['class_section_name'] ?? 'N/A') ?></td>
<td><?= $viewedAt ? esc(local_datetime($viewedAt, 'm-d-Y H:i')) : 'Not viewed' ?></td>
<td>
<?php if ($signedAt): ?>
<?= esc($signedName ?: 'Signed') ?><br>
<small class="text-muted"><?= esc(local_datetime($signedAt, 'm-d-Y H:i')) ?></small>
<?php else: ?>
<span class="text-muted">Not signed</span>
<?php endif; ?>
</td>
<td class="text-end">
<a class="btn btn-sm btn-outline-primary" target="_blank" href="<?= base_url('parent/report-cards/view/' . $sid) ?>">View Report</a>
<?php if (! $signedAt): ?>
<form class="d-inline-flex align-items-center gap-2 ms-2" method="post" action="<?= base_url('parent/report-cards/sign/' . $sid) ?>">
<?= csrf_field() ?>
<input type="text" name="signed_name" class="form-control form-control-sm" placeholder="Full name" required>
<button class="btn btn-sm btn-success" type="submit">Sign</button>
</form>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<?= $this->endSection() ?>
+30 -1
View File
@@ -1,7 +1,9 @@
<?= $this->extend('layout/main_layout') ?>
<?= $this->section('content') ?>
<div class="container my-5">
<h3 class="text-center text-success" style="font-family: Arial, sans-serif;">Scores</h2>
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2">
<h3 class="text-success mb-0" style="font-family: Arial, sans-serif;">Scores</h3>
</div>
</div>
<!-- Filter Form -->
@@ -73,6 +75,7 @@ $showExamScoresBySemester = $showExamScoresBySemester ?? [];
<?php if ($showExamScoresForSemester): ?><th>Exam</th><?php endif; ?>
<?php endif; ?>
<?php if ($showExamScoresForSemester): ?><th>Semester Score</th><?php endif; ?>
<?php if ($showExamScoresForSemester): ?><th>Report Card</th><?php endif; ?>
</tr>
</thead>
<tbody>
@@ -96,6 +99,32 @@ $showExamScoresBySemester = $showExamScoresBySemester ?? [];
<?php if ($showExamScoresForSemester): ?><td><?= esc($student['scores']['midterm_exam']['score'] ?? $student['scores']['final_exam']['score'] ?? '-') ?></td><?php endif; ?>
<?php endif; ?>
<?php if ($showExamScoresForSemester): ?><td><?= esc($student['scores']['semester']['score'] ?? '-') ?></td><?php endif; ?>
<?php if ($showExamScoresForSemester): ?>
<td>
<?php
$reportStudentId = (int) ($student['student_id'] ?? 0);
if ($reportStudentId > 0) {
$reportYear = (string) ($selectedYear ?? '');
$reportSemester = (string) ($semester ?? '');
$reportDate = date('Y-m-d');
$reportUrl = base_url('parent/report-cards/view/' . $reportStudentId);
$query = http_build_query([
'school_year' => $reportYear,
'semester' => $reportSemester,
'report_date' => $reportDate,
]);
if ($query) {
$reportUrl .= '?' . $query;
}
}
?>
<?php if ($reportStudentId > 0): ?>
<a class="btn btn-sm btn-outline-primary" href="<?= esc($reportUrl) ?>" target="_blank" rel="noopener">View</a>
<?php else: ?>
<span class="text-muted">Unavailable</span>
<?php endif; ?>
</td>
<?php endif; ?>
</tr>
<?php if (!empty($student['comment'])): ?>
<tr>
+6
View File
@@ -334,6 +334,12 @@ switch ($role) {
'label' => 'Scores',
'title' => 'Review progressive academic score for the year (homeworks, projects and exams).',
],
[
'href' => base_url('/parent/report-cards'),
'icon' => 'bi-file-earmark-text',
'label' => 'Report Cards',
'title' => 'View and acknowledge student report cards.',
],
[
'href' => base_url('/parent/progress'),
'icon' => 'bi-journal-check',
+44 -1
View File
@@ -48,6 +48,7 @@
<a id="downloadStudentBtn" href="#" class="btn btn-success external-link" target="_blank">Download</a>
</div>
</div>
<div class="text-muted small text-center" id="ackStatus"></div>
</form>
<div class="card shadow-sm mb-3">
@@ -67,10 +68,12 @@
<table class="table table-sm table-bordered align-middle mb-0">
<thead class="table-light">
<tr>
<th style="width: 35%;">Student</th>
<th style="width: 28%;">Student</th>
<th style="width: 15%;">Status</th>
<th>Missing</th>
<th>Warnings</th>
<th style="width: 16%;">Viewed</th>
<th style="width: 16%;">Signed</th>
</tr>
</thead>
<tbody id="completenessBody"></tbody>
@@ -91,6 +94,7 @@
document.addEventListener('DOMContentLoaded', function(){
const API = <?= json_encode(site_url('api/printables/report-card/meta')) ?>;
const COMPLETENESS_API = <?= json_encode(site_url('api/printables/report-card/completeness')) ?>;
const ACK_API = <?= json_encode(site_url('api/printables/report-card/ack')) ?>;
// add cache-busting cb param
const withCB = (u) => u + (u.includes('?') ? '&' : '?') + 'cb=' + Date.now();
@@ -119,6 +123,7 @@ document.addEventListener('DOMContentLoaded', function(){
const $completenessSummary = document.getElementById('completenessSummary');
const $completenessWrap = document.getElementById('completenessTableWrap');
const $completenessBody = document.getElementById('completenessBody');
const $ackStatus = document.getElementById('ackStatus');
function opt(el, val, label) {
const o = document.createElement('option'); o.value = String(val); o.textContent = label; el.appendChild(o);
@@ -157,10 +162,40 @@ document.addEventListener('DOMContentLoaded', function(){
// Classes
$class.innerHTML = '';
(data.classSections || []).forEach(c => opt($class, c.class_section_id || c.id, c.class_section_name));
updateAckStatus();
}
async function updateAckStatus() {
if (! $ackStatus) return;
const sid = $student.value;
const y = $year.value || '';
const s = $sem.value || '';
if (!sid) {
$ackStatus.textContent = '';
return;
}
try {
const url = `${ACK_API}?student_id=${encodeURIComponent(sid)}&school_year=${encodeURIComponent(y)}&semester=${encodeURIComponent(s)}`;
const res = await fetch(url, { credentials: 'same-origin' });
const data = await res.json();
if (!res.ok || data?.ok === false) {
$ackStatus.textContent = 'Parent acknowledgement: unavailable';
return;
}
const viewed = data.viewed_at ? `Viewed: ${data.viewed_at}` : 'Viewed: not yet';
const signed = data.signed_at
? `Signed: ${data.signed_at} (${data.signed_name || 'name missing'})`
: 'Signed: not yet';
$ackStatus.textContent = `Parent acknowledgement — ${viewed} · ${signed}`;
} catch (err) {
$ackStatus.textContent = 'Parent acknowledgement: unavailable';
}
}
$year.addEventListener('change', function(){ loadMeta(this.value, $sem.value).catch(console.error); });
$sem.addEventListener('change', function(){ loadMeta($year.value, this.value).catch(console.error); });
$student.addEventListener('change', function(){ updateAckStatus(); });
$viewS.addEventListener('click', function(){
const sid = $student.value; const y = $year.value; const s=$sem.value;
@@ -254,10 +289,18 @@ document.addEventListener('DOMContentLoaded', function(){
tdMissing.textContent = missing.length ? missing.join(', ') : 'None';
const tdWarnings = document.createElement('td');
tdWarnings.textContent = warningsList.length ? warningsList.join(', ') : 'None';
const tdViewed = document.createElement('td');
tdViewed.textContent = row.viewed_at ? row.viewed_at : 'Not viewed';
const tdSigned = document.createElement('td');
tdSigned.textContent = row.signed_at
? `${row.signed_at}${row.signed_name ? ` (${row.signed_name})` : ''}`
: 'Not signed';
tr.appendChild(tdName);
tr.appendChild(tdStatus);
tr.appendChild(tdMissing);
tr.appendChild(tdWarnings);
tr.appendChild(tdViewed);
tr.appendChild(tdSigned);
$completenessBody.appendChild(tr);
});
$completenessWrap.hidden = false;