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.'); } $schoolYearContext = $this->resolveSchoolYearContext(); $schoolYear = trim($schoolYearContext->yearName()); $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); } $students = $builder->get()->getResultArray(); $studentIds = array_values(array_filter(array_map(static fn ($s) => (int) ($s['id'] ?? 0), $students))); $ackMap = []; $reportAvailableMap = $this->reportAvailabilityMap($studentIds, $schoolYear, $semester); 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, 'reportAvailableMap' => $reportAvailableMap, 'schoolYear' => $schoolYear, 'semester' => $semester, 'isEditable' => ! $schoolYearContext->isReadonly(), ]); } 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.'); } $schoolYearContext = $this->resolveSchoolYearContext(); $schoolYear = trim($schoolYearContext->yearName()); $semester = trim((string) ($this->request->getGet('semester') ?? $this->configModel->getConfig('semester') ?? '')); if (! $this->reportExists((int) $studentId, $schoolYear, $semester)) { return redirect()->to(site_url('parent/report-cards')) ->with('error', 'No report card exists for the selected school year and semester.'); } if (! $schoolYearContext->isReadonly()) { $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.'); } $schoolYearContext = $this->resolveSchoolYearContext(); try { $this->assertSchoolYearWritable($schoolYearContext); } catch (SchoolYearWriteConflictException $e) { return redirect()->back()->withInput()->with('error', $e->getMessage()); } $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($schoolYearContext->yearName()); $semester = trim((string) ($this->configModel->getConfig('semester') ?? '')); if (! $this->reportExists((int) $studentId, $schoolYear, $semester)) { return redirect()->to(site_url('parent/report-cards')) ->with('error', 'No report card exists for the selected school year and 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 reportAvailabilityMap(array $studentIds, string $schoolYear, string $semester): array { $studentIds = array_values(array_filter(array_map('intval', $studentIds), static fn ($id) => $id > 0)); if (empty($studentIds) || $schoolYear === '') { return []; } $builder = $this->db->table('semester_scores') ->select('student_id') ->whereIn('student_id', $studentIds) ->where('school_year', $schoolYear); $semesterVariants = $this->semesterVariants($semester); if (! empty($semesterVariants)) { $builder->whereIn('semester', $semesterVariants); } $rows = $builder ->groupBy('student_id') ->get() ->getResultArray(); $map = []; foreach ($rows as $row) { $sid = (int) ($row['student_id'] ?? 0); if ($sid > 0) { $map[$sid] = true; } } return $map; } protected function reportExists(int $studentId, string $schoolYear, string $semester): bool { if ($studentId <= 0 || $schoolYear === '') { return false; } $builder = $this->db->table('semester_scores') ->select('id') ->where('student_id', $studentId) ->where('school_year', $schoolYear) ->limit(1); $semesterVariants = $this->semesterVariants($semester); if (! empty($semesterVariants)) { $builder->whereIn('semester', $semesterVariants); } return (bool) $builder->get()->getRowArray(); } protected function semesterVariants(string $semester): array { $raw = trim($semester); if ($raw === '') { return []; } $normalized = strtolower($raw); if ($normalized === 'fall' || $normalized === 'first' || str_contains($normalized, 'fall') || str_contains($normalized, '1')) { $values = ['Fall', 'fall', 'First', 'first', 'Semester 1', 'semester 1', 'Semester1', 'semester1', 'Sem 1', 'sem 1', '1', '01', 'S1', 's1']; } elseif ($normalized === 'spring' || $normalized === 'second' || str_contains($normalized, 'spring') || str_contains($normalized, '2')) { $values = ['Spring', 'spring', 'Second', 'second', 'Semester 2', 'semester 2', 'Semester2', 'semester2', 'Sem 2', 'sem 2', '2', '02', 'S2', 's2']; } else { $values = [$raw]; } $values[] = $raw; return array_values(array_unique($values)); } 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)); } }