AVP-78 Feature to check if parents looked at report card
This commit is contained in:
@@ -8,6 +8,7 @@ use App\Models\ClassSectionModel;
|
||||
use App\Models\CalendarModel;
|
||||
use App\Models\ConfigurationModel;
|
||||
use App\Models\StudentClassModel;
|
||||
use App\Models\SubjectCurriculumModel;
|
||||
use App\Services\SemesterRangeService;
|
||||
use CodeIgniter\Exceptions\PageNotFoundException;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
@@ -21,6 +22,7 @@ class AdminProgressController extends BaseController
|
||||
protected CalendarModel $calendarModel;
|
||||
protected ConfigurationModel $configModel;
|
||||
protected SemesterRangeService $semesterRangeService;
|
||||
protected SubjectCurriculumModel $curriculumModel;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -32,6 +34,7 @@ class AdminProgressController extends BaseController
|
||||
$this->calendarModel = new CalendarModel();
|
||||
$this->configModel = new ConfigurationModel();
|
||||
$this->semesterRangeService = new SemesterRangeService($this->configModel);
|
||||
$this->curriculumModel = new SubjectCurriculumModel();
|
||||
}
|
||||
|
||||
public function index()
|
||||
@@ -107,6 +110,7 @@ class AdminProgressController extends BaseController
|
||||
$filterEnd
|
||||
);
|
||||
$sectionStats = $this->buildSectionSubmissionStats($rows, $activeDatesSet, $expectedDays);
|
||||
$sectionSubjectCounts = $this->buildSectionSubjectCounts($rows);
|
||||
|
||||
return view('admin/class_progress_list', [
|
||||
'reportGroupsBySection' => $reportGroupsBySection,
|
||||
@@ -115,6 +119,7 @@ class AdminProgressController extends BaseController
|
||||
'statusOptions' => ClassProgressController::STATUS_OPTIONS,
|
||||
'subjectSections' => ClassProgressController::SUBJECT_SECTIONS,
|
||||
'sectionStats' => $sectionStats,
|
||||
'sectionSubjectCounts' => $sectionSubjectCounts,
|
||||
'expectedDays' => $expectedDays,
|
||||
]);
|
||||
}
|
||||
@@ -381,50 +386,166 @@ class AdminProgressController extends BaseController
|
||||
|
||||
protected function buildSectionSubmissionStats(array $rows, array $activeDatesSet, int $expectedDays): array
|
||||
{
|
||||
$requiredSubjects = [];
|
||||
foreach (ClassProgressController::SUBJECT_SECTIONS as $section) {
|
||||
$requiredSubjects[] = $section['db_subject'] ?? $section['label'] ?? '';
|
||||
}
|
||||
$requiredSubjects = array_values(array_filter($requiredSubjects));
|
||||
|
||||
$submittedBySection = [];
|
||||
foreach ($rows as $row) {
|
||||
$sectionId = (int) ($row['class_section_id'] ?? 0);
|
||||
$weekStart = (string) ($row['week_start'] ?? '');
|
||||
$subject = (string) ($row['subject'] ?? '');
|
||||
if ($sectionId === 0 || $weekStart === '' || $subject === '') {
|
||||
if ($sectionId === 0 || $weekStart === '') {
|
||||
continue;
|
||||
}
|
||||
if (! empty($activeDatesSet) && empty($activeDatesSet[$weekStart])) {
|
||||
continue;
|
||||
}
|
||||
if (! in_array($subject, $requiredSubjects, true)) {
|
||||
continue;
|
||||
}
|
||||
$submittedBySection[$sectionId][$weekStart][$subject] = true;
|
||||
$submittedBySection[$sectionId][$weekStart] = true;
|
||||
}
|
||||
|
||||
$stats = [];
|
||||
foreach ($submittedBySection as $sectionId => $weeks) {
|
||||
$submitted = 0;
|
||||
foreach ($weeks as $subjects) {
|
||||
$all = true;
|
||||
foreach ($requiredSubjects as $subject) {
|
||||
if (empty($subjects[$subject])) {
|
||||
$all = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($all) {
|
||||
$submitted++;
|
||||
}
|
||||
}
|
||||
$submitted = count($weeks);
|
||||
$stats[$sectionId] = $this->buildSectionStat($submitted, $expectedDays);
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
protected function buildSectionSubjectCounts(array $rows): array
|
||||
{
|
||||
$allowedSubjects = [];
|
||||
foreach (ClassProgressController::SUBJECT_SECTIONS as $section) {
|
||||
$allowedSubjects[] = $section['db_subject'] ?? $section['label'] ?? '';
|
||||
}
|
||||
$allowedSubjects = array_values(array_filter($allowedSubjects));
|
||||
|
||||
$counts = [];
|
||||
$latestWeekBySection = [];
|
||||
$sectionClassMap = [];
|
||||
foreach ($rows as $row) {
|
||||
$sectionId = (int) ($row['class_section_id'] ?? 0);
|
||||
$subject = (string) ($row['subject'] ?? '');
|
||||
$weekStart = (string) ($row['week_start'] ?? '');
|
||||
if ($sectionId === 0 || $subject === '' || $weekStart === '') {
|
||||
continue;
|
||||
}
|
||||
if (! empty($allowedSubjects) && ! in_array($subject, $allowedSubjects, true)) {
|
||||
continue;
|
||||
}
|
||||
if (! isset($sectionClassMap[$sectionId])) {
|
||||
$sectionClassMap[$sectionId] = $this->classSectionModel->getClassId($sectionId);
|
||||
}
|
||||
if (
|
||||
! isset($latestWeekBySection[$sectionId])
|
||||
|| $weekStart > $latestWeekBySection[$sectionId]
|
||||
) {
|
||||
$latestWeekBySection[$sectionId] = $weekStart;
|
||||
}
|
||||
}
|
||||
|
||||
$curriculumChapters = $this->buildCurriculumChapterMap(array_values(array_filter($sectionClassMap)));
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$sectionId = (int) ($row['class_section_id'] ?? 0);
|
||||
$subject = (string) ($row['subject'] ?? '');
|
||||
$weekStart = (string) ($row['week_start'] ?? '');
|
||||
if ($sectionId === 0 || $subject === '' || $weekStart === '') {
|
||||
continue;
|
||||
}
|
||||
if (! empty($allowedSubjects) && ! in_array($subject, $allowedSubjects, true)) {
|
||||
continue;
|
||||
}
|
||||
if (empty($latestWeekBySection[$sectionId]) || $weekStart !== $latestWeekBySection[$sectionId]) {
|
||||
continue;
|
||||
}
|
||||
$subjectSlug = $this->resolveSubjectSlug($subject);
|
||||
$classId = $sectionClassMap[$sectionId] ?? null;
|
||||
$chapterSet = [];
|
||||
if ($classId && $subjectSlug && ! empty($curriculumChapters[$classId][$subjectSlug])) {
|
||||
$chapterSet = $curriculumChapters[$classId][$subjectSlug];
|
||||
}
|
||||
$counts[$sectionId] = ($counts[$sectionId] ?? 0) + $this->countChapterSegments(
|
||||
(string) ($row['unit_title'] ?? ''),
|
||||
$chapterSet
|
||||
);
|
||||
}
|
||||
|
||||
return $counts;
|
||||
}
|
||||
|
||||
protected function buildCurriculumChapterMap(array $classIds): array
|
||||
{
|
||||
$classIds = array_values(array_filter(array_map('intval', $classIds)));
|
||||
if (empty($classIds)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$rows = $this->curriculumModel
|
||||
->whereIn('class_id', $classIds)
|
||||
->findAll();
|
||||
|
||||
$map = [];
|
||||
foreach ($rows as $row) {
|
||||
$classId = (int) ($row['class_id'] ?? 0);
|
||||
$subject = (string) ($row['subject'] ?? '');
|
||||
$chapter = trim((string) ($row['chapter_name'] ?? ''));
|
||||
if ($classId === 0 || $subject === '' || $chapter === '') {
|
||||
continue;
|
||||
}
|
||||
$map[$classId][$subject][$chapter] = true;
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
protected function resolveSubjectSlug(string $subject): ?string
|
||||
{
|
||||
foreach (ClassProgressController::SUBJECT_SECTIONS as $slug => $section) {
|
||||
$dbSubject = (string) ($section['db_subject'] ?? '');
|
||||
$label = (string) ($section['label'] ?? '');
|
||||
if ($subject === $dbSubject || $subject === $label) {
|
||||
return $slug;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function countChapterSegments(string $unitTitle, array $chapterSet): int
|
||||
{
|
||||
$unitTitle = trim($unitTitle);
|
||||
if ($unitTitle === '') {
|
||||
return 1;
|
||||
}
|
||||
$parts = array_filter(array_map('trim', explode(';', $unitTitle)), static fn ($part) => $part !== '');
|
||||
if (! $parts) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
$seen = [];
|
||||
foreach ($parts as $part) {
|
||||
$chapter = $this->extractChapterFromSegment($part);
|
||||
$key = $chapter !== '' ? $chapter : $part;
|
||||
if (! empty($chapterSet) && $chapter !== '' && empty($chapterSet[$chapter])) {
|
||||
$key = $part;
|
||||
}
|
||||
if (isset($seen[$key])) {
|
||||
continue;
|
||||
}
|
||||
$seen[$key] = true;
|
||||
$count++;
|
||||
}
|
||||
|
||||
return $count > 0 ? $count : 1;
|
||||
}
|
||||
|
||||
protected function extractChapterFromSegment(string $segment): string
|
||||
{
|
||||
$segment = trim($segment);
|
||||
if ($segment === '') {
|
||||
return '';
|
||||
}
|
||||
$pos = strrpos($segment, '/');
|
||||
if ($pos === false) {
|
||||
return $segment;
|
||||
}
|
||||
return trim(substr($segment, $pos + 1));
|
||||
}
|
||||
|
||||
protected function buildSectionStat(int $submitted, int $expectedDays): array
|
||||
{
|
||||
$percent = $expectedDays > 0 ? round(($submitted * 100) / $expectedDays, 1) : 0.0;
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Models\ConfigurationModel;
|
||||
use App\Models\ReportCardAcknowledgementModel;
|
||||
use App\Models\StudentModel;
|
||||
use CodeIgniter\Exceptions\PageNotFoundException;
|
||||
use CodeIgniter\Database\BaseConnection;
|
||||
|
||||
class ParentReportCardController extends BaseController
|
||||
{
|
||||
protected ConfigurationModel $configModel;
|
||||
protected ReportCardAcknowledgementModel $ackModel;
|
||||
protected StudentModel $studentModel;
|
||||
protected BaseConnection $db;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
helper(['url', 'form']);
|
||||
$this->db = \Config\Database::connect();
|
||||
$this->configModel = new ConfigurationModel();
|
||||
$this->ackModel = new ReportCardAcknowledgementModel();
|
||||
$this->studentModel = new StudentModel();
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$parentId = $this->resolvePrimaryParentId();
|
||||
if (! $parentId) {
|
||||
return redirect()->back()->with('error', 'Unable to retrieve student data. Please contact support.');
|
||||
}
|
||||
|
||||
$schoolYear = trim((string) ($this->request->getGet('school_year') ?? $this->configModel->getConfig('school_year') ?? ''));
|
||||
$semester = trim((string) ($this->request->getGet('semester') ?? $this->configModel->getConfig('semester') ?? ''));
|
||||
|
||||
$builder = $this->db->table('students s')
|
||||
->select('s.id, s.firstname, s.lastname, cs.class_section_name')
|
||||
->join('student_class sc', 'sc.student_id = s.id', 'left')
|
||||
->join('classSection cs', 'cs.class_section_id = sc.class_section_id', 'left')
|
||||
->where('s.parent_id', $parentId)
|
||||
->orderBy('s.firstname', 'ASC')
|
||||
->orderBy('s.lastname', 'ASC');
|
||||
|
||||
if ($schoolYear !== '') {
|
||||
$builder->where('sc.school_year', $schoolYear);
|
||||
}
|
||||
if ($semester !== '') {
|
||||
$builder->where('sc.semester', $semester);
|
||||
}
|
||||
|
||||
$students = $builder->get()->getResultArray();
|
||||
$studentIds = array_values(array_filter(array_map(static fn ($s) => (int) ($s['id'] ?? 0), $students)));
|
||||
|
||||
$ackMap = [];
|
||||
if (! empty($studentIds)) {
|
||||
$rows = $this->ackModel
|
||||
->where('parent_id', $parentId)
|
||||
->where('school_year', $schoolYear)
|
||||
->where('semester', $semester)
|
||||
->whereIn('student_id', $studentIds)
|
||||
->findAll();
|
||||
foreach ($rows as $row) {
|
||||
$ackMap[(int) $row['student_id']] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
return view('parent/report_cards', [
|
||||
'students' => $students,
|
||||
'ackMap' => $ackMap,
|
||||
'schoolYear' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
]);
|
||||
}
|
||||
|
||||
public function view($studentId)
|
||||
{
|
||||
$parentId = $this->resolvePrimaryParentId();
|
||||
if (! $parentId) {
|
||||
return redirect()->back()->with('error', 'Unable to retrieve student data. Please contact support.');
|
||||
}
|
||||
|
||||
$student = $this->studentModel->find((int) $studentId);
|
||||
if (! $student || (int) ($student['parent_id'] ?? 0) !== $parentId) {
|
||||
throw new PageNotFoundException('Student not found.');
|
||||
}
|
||||
|
||||
$schoolYear = trim((string) ($this->request->getGet('school_year') ?? $this->configModel->getConfig('school_year') ?? ''));
|
||||
$semester = trim((string) ($this->request->getGet('semester') ?? $this->configModel->getConfig('semester') ?? ''));
|
||||
|
||||
$this->touchAcknowledgement($parentId, (int) $studentId, $schoolYear, $semester, [
|
||||
'viewed_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
|
||||
$url = site_url('report-card/student/' . (int) $studentId);
|
||||
$query = [];
|
||||
if ($schoolYear !== '') {
|
||||
$query['school_year'] = $schoolYear;
|
||||
}
|
||||
if ($semester !== '') {
|
||||
$query['semester'] = $semester;
|
||||
}
|
||||
if ($query) {
|
||||
$url .= '?' . http_build_query($query);
|
||||
}
|
||||
|
||||
return redirect()->to($url);
|
||||
}
|
||||
|
||||
public function sign($studentId)
|
||||
{
|
||||
$parentId = $this->resolvePrimaryParentId();
|
||||
if (! $parentId) {
|
||||
return redirect()->back()->with('error', 'Unable to retrieve student data. Please contact support.');
|
||||
}
|
||||
|
||||
$student = $this->studentModel->find((int) $studentId);
|
||||
if (! $student || (int) ($student['parent_id'] ?? 0) !== $parentId) {
|
||||
throw new PageNotFoundException('Student not found.');
|
||||
}
|
||||
|
||||
$name = trim((string) $this->request->getPost('signed_name'));
|
||||
if ($name === '') {
|
||||
return redirect()->back()->with('error', 'Please type your full name to sign.');
|
||||
}
|
||||
|
||||
$schoolYear = trim((string) ($this->configModel->getConfig('school_year') ?? ''));
|
||||
$semester = trim((string) ($this->configModel->getConfig('semester') ?? ''));
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$this->touchAcknowledgement($parentId, (int) $studentId, $schoolYear, $semester, [
|
||||
'viewed_at' => $now,
|
||||
'signed_at' => $now,
|
||||
'signed_name' => $name,
|
||||
'signer_ip' => $this->request->getIPAddress(),
|
||||
]);
|
||||
|
||||
return redirect()->to(site_url('parent/report-cards'))->with('success', 'Report card acknowledged.');
|
||||
}
|
||||
|
||||
protected function resolvePrimaryParentId(): ?int
|
||||
{
|
||||
$parentId = (int) (session()->get('user_id') ?? 0);
|
||||
$userType = (string) ($_SESSION['user_type'] ?? '');
|
||||
if ($userType === 'primary') {
|
||||
return $parentId ?: null;
|
||||
}
|
||||
if ($userType === 'secondary') {
|
||||
$row = $this->db->table('parents')
|
||||
->select('parent_id')
|
||||
->where('secondparent_user_id', $parentId)
|
||||
->get()
|
||||
->getRowArray();
|
||||
return $row ? (int) ($row['parent_id'] ?? 0) : null;
|
||||
}
|
||||
if ($userType === 'tertiary') {
|
||||
$row = $this->db->table('authorized_users')
|
||||
->select('user_id as parent_id')
|
||||
->where('authorized_user_id', $parentId)
|
||||
->get()
|
||||
->getRowArray();
|
||||
return $row ? (int) ($row['parent_id'] ?? 0) : null;
|
||||
}
|
||||
return $parentId ?: null;
|
||||
}
|
||||
|
||||
protected function touchAcknowledgement(
|
||||
int $parentId,
|
||||
int $studentId,
|
||||
string $schoolYear,
|
||||
string $semester,
|
||||
array $values
|
||||
): void {
|
||||
$criteria = [
|
||||
'parent_id' => $parentId,
|
||||
'student_id' => $studentId,
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
];
|
||||
$existing = $this->ackModel->where($criteria)->first();
|
||||
if ($existing) {
|
||||
$this->ackModel->update((int) $existing['id'], $values);
|
||||
return;
|
||||
}
|
||||
$this->ackModel->insert(array_merge($criteria, $values));
|
||||
}
|
||||
}
|
||||
@@ -4,18 +4,21 @@ namespace App\Controllers\View;
|
||||
|
||||
use App\Models\CalendarModel;
|
||||
use App\Models\AttendanceDataModel;
|
||||
use App\Models\ReportCardAcknowledgementModel;
|
||||
use App\Services\SemesterRangeService;
|
||||
|
||||
class ReportCardsController extends PrintablesBaseController
|
||||
{
|
||||
protected $calendarModel;
|
||||
protected $semesterRangeService;
|
||||
protected $ackModel;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->calendarModel = new CalendarModel();
|
||||
$this->semesterRangeService = new SemesterRangeService($this->configModel);
|
||||
$this->ackModel = new ReportCardAcknowledgementModel();
|
||||
}
|
||||
|
||||
public function report_card()
|
||||
@@ -369,6 +372,24 @@ class ReportCardsController extends PrintablesBaseController
|
||||
|
||||
$isNumeric = static fn($v) => $v !== null && $v !== '' && is_numeric($v);
|
||||
|
||||
$ackMap = [];
|
||||
try {
|
||||
$ackRows = $this->ackModel
|
||||
->whereIn('student_id', $studentIds)
|
||||
->where('school_year', $year)
|
||||
->where('semester', $sem)
|
||||
->orderBy('updated_at', 'DESC')
|
||||
->findAll();
|
||||
foreach ($ackRows as $row) {
|
||||
$sid = (int) ($row['student_id'] ?? 0);
|
||||
if ($sid > 0 && ! isset($ackMap[$sid])) {
|
||||
$ackMap[$sid] = $row;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$ackMap = [];
|
||||
}
|
||||
|
||||
$results = [];
|
||||
$completeCount = 0;
|
||||
$warningCount = 0;
|
||||
@@ -471,12 +492,16 @@ class ReportCardsController extends PrintablesBaseController
|
||||
$warningCount++;
|
||||
}
|
||||
|
||||
$ack = $sid > 0 ? ($ackMap[$sid] ?? null) : null;
|
||||
$results[] = [
|
||||
'id' => $sid,
|
||||
'name' => $name !== '' ? $name : 'N/A',
|
||||
'missing' => $missing,
|
||||
'warnings' => $warnings,
|
||||
'complete' => $isComplete,
|
||||
'viewed_at' => $ack['viewed_at'] ?? null,
|
||||
'signed_at' => $ack['signed_at'] ?? null,
|
||||
'signed_name' => $ack['signed_name'] ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -501,6 +526,37 @@ class ReportCardsController extends PrintablesBaseController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* API: report card parent acknowledgement status
|
||||
* GET params: student_id, school_year, semester
|
||||
*/
|
||||
public function reportCardAcknowledgement()
|
||||
{
|
||||
$studentId = (int) ($this->request->getGet('student_id') ?? 0);
|
||||
if ($studentId <= 0) {
|
||||
return $this->response->setJSON(['ok' => false, 'error' => 'Missing student_id'])->setStatusCode(400);
|
||||
}
|
||||
$schoolYear = trim((string) ($this->request->getGet('school_year') ?? $this->schoolYear ?? ''));
|
||||
$semester = trim((string) ($this->request->getGet('semester') ?? $this->semester ?? ''));
|
||||
|
||||
$row = $this->ackModel
|
||||
->where('student_id', $studentId)
|
||||
->where('school_year', $schoolYear)
|
||||
->where('semester', $semester)
|
||||
->orderBy('updated_at', 'DESC')
|
||||
->first();
|
||||
|
||||
return $this->response->setJSON([
|
||||
'ok' => true,
|
||||
'student_id' => $studentId,
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
'viewed_at' => $row['viewed_at'] ?? null,
|
||||
'signed_at' => $row['signed_at'] ?? null,
|
||||
'signed_name' => $row['signed_name'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function fetchStudentsByClass(int $sectionId, ?string $schoolYear)
|
||||
{
|
||||
$b = $this->studentClassModel
|
||||
|
||||
@@ -1378,6 +1378,7 @@ $students = $this->db->table('students')
|
||||
$releaseScores = (strcasecmp($semester, 'Fall') === 0) ? $releaseFall
|
||||
: ((strcasecmp($semester, 'Spring') === 0) ? $releaseSpring : $releaseAny);
|
||||
$scores[$selectedYear][$semester][$studentId] = [
|
||||
'student_id' => $studentId,
|
||||
'school_id' => $student['school_id'],
|
||||
'student_firstname' => $student['firstname'],
|
||||
'student_lastname' => $student['lastname'],
|
||||
|
||||
Reference in New Issue
Block a user