reportModel = new ClassProgressReportModel(); $this->attachmentModel = new ClassProgressAttachmentModel(); $this->enrollmentModel = new EnrollmentModel(); $this->db = Database::connect(); } public function index() { $students = $this->getParentStudents(); $sectionIds = array_values(array_unique(array_filter(array_map( static fn (array $student): int => (int) ($student['class_section_id'] ?? 0), $students )))); $subjectSections = ClassProgressController::SUBJECT_SECTIONS; $rows = []; if (! empty($sectionIds)) { $builder = $this->reportModel ->select('class_progress_reports.*, cs.class_section_name, CONCAT(IFNULL(u.firstname, ""), " ", IFNULL(u.lastname, "")) AS teacher_name') ->join('classSection cs', 'cs.class_section_id = class_progress_reports.class_section_id', 'left') ->join('users u', 'u.id = class_progress_reports.teacher_id', 'left') ->whereIn('class_progress_reports.class_section_id', $sectionIds); $rows = $builder ->orderBy('week_start', 'DESC') ->findAll(); } $studentReportGroups = []; foreach ($students as $student) { $studentId = (int) ($student['student_id'] ?? 0); if ($studentId === 0) { continue; } $classSectionId = (int) ($student['class_section_id'] ?? 0); $studentRows = $classSectionId ? array_values(array_filter( $rows, static fn (array $row): bool => (int) ($row['class_section_id'] ?? 0) === $classSectionId )) : []; $studentReportGroups[$studentId] = $this->groupReportsByWeek($studentRows); } return view('parent/class_progress_list', [ 'students' => $students, 'studentReportGroups' => $studentReportGroups, 'subjectSections' => $subjectSections, 'hasStudents' => ! empty($students), ]); } public function view($id) { $row = $this->reportModel ->select('class_progress_reports.*, cs.class_section_name, CONCAT(IFNULL(u.firstname, ""), " ", IFNULL(u.lastname, "")) AS teacher_name') ->join('classSection cs', 'cs.class_section_id = class_progress_reports.class_section_id', 'left') ->join('users u', 'u.id = class_progress_reports.teacher_id', 'left') ->find((int) $id); if (! $row || ! $this->isSectionAccessible($row['class_section_id'] ?? null)) { throw new PageNotFoundException('Progress report not found.'); } $row['status_label'] = ClassProgressController::STATUS_OPTIONS[$row['status']] ?? 'Unknown'; $row['flags'] = $this->decodeFlags($row['flags_json']); $weeklyReports = $this->reportModel ->select('class_progress_reports.*') ->where('class_section_id', $row['class_section_id']) ->where('week_start', $row['week_start']) ->orderBy('subject', 'ASC') ->findAll(); $attachmentMap = $this->loadAttachmentsForReports(array_column($weeklyReports, 'id')); foreach ($weeklyReports as &$report) { $report['attachments'] = $attachmentMap[$report['id']] ?? []; if (empty($report['attachments']) && ! empty($report['attachment_path'])) { $report['attachments'][] = [ 'id' => $report['id'], 'name' => basename((string) $report['attachment_path']), 'legacy' => true, ]; } } return view('parent/class_progress_view', [ 'row' => $row, 'weeklyReports' => $weeklyReports, 'subjectSections' => ClassProgressController::SUBJECT_SECTIONS, ]); } public function attachment($id) { $row = $this->reportModel->find((int) $id); if (! $row || ! $this->isSectionAccessible($row['class_section_id'] ?? null) || empty($row['attachment_path'])) { throw new PageNotFoundException('Attachment not found.'); } $file = $this->resolveAttachmentFile($row); if (! $file) { throw new PageNotFoundException('Attachment missing.'); } return $this->response->download($file, null)->setFileName(basename($file)); } public function attachmentFile($id) { $attachment = $this->attachmentModel->find((int) $id); if (! $attachment || empty($attachment['file_path'])) { throw new PageNotFoundException('Attachment not found.'); } $report = $this->reportModel->find((int) ($attachment['report_id'] ?? 0)); if (! $report || ! $this->isSectionAccessible($report['class_section_id'] ?? null)) { throw new PageNotFoundException('Attachment not found.'); } $file = $this->resolveAttachmentPath($attachment['file_path']); if (! $file) { throw new PageNotFoundException('Attachment missing.'); } $downloadName = $attachment['original_name'] ?: basename($file); return $this->response->download($file, null)->setFileName($downloadName); } protected function getParentSectionIds(): array { if ($this->parentSectionIds !== null) { return $this->parentSectionIds; } $parentId = (int) session()->get('user_id'); if ($parentId === 0) { $this->parentSectionIds = []; return $this->parentSectionIds; } $rows = $this->enrollmentModel ->select('class_section_id') ->where('parent_id', $parentId) ->where('is_withdrawn', 0) ->groupBy('class_section_id') ->findAll(); $ids = []; foreach ($rows as $row) { $classSectionId = (int) ($row['class_section_id'] ?? 0); if ($classSectionId > 0) { $ids[] = $classSectionId; } } $this->parentSectionIds = array_values(array_unique($ids)); return $this->parentSectionIds; } protected function buildSectionOptions(array $sectionIds): array { if (empty($sectionIds)) { return []; } $rows = $this->db->table('classSection') ->select('class_section_id, class_section_name') ->whereIn('class_section_id', $sectionIds) ->orderBy('class_section_name', 'ASC') ->get() ->getResultArray(); $options = []; foreach ($rows as $row) { $options[(int) ($row['class_section_id'] ?? 0)] = $row['class_section_name'] ?? 'Unknown section'; } return $options; } protected function getParentStudents(): array { $parentId = (int) session()->get('user_id'); if ($parentId === 0) { return []; } $rows = $this->db->table('enrollments e') ->select('e.student_id, e.class_section_id, e.updated_at, e.created_at, s.firstname, s.lastname, cs.class_section_name') ->join('students s', 's.id = e.student_id') ->join('classSection cs', 'cs.class_section_id = e.class_section_id', 'left') ->where('e.parent_id', $parentId) ->where('e.is_withdrawn', 0) ->orderBy('e.updated_at', 'DESC') ->orderBy('e.created_at', 'DESC') ->get() ->getResultArray(); $students = []; foreach ($rows as $row) { $studentId = (int) ($row['student_id'] ?? 0); if ($studentId === 0 || isset($students[$studentId])) { continue; } $students[$studentId] = $row; } return array_values($students); } protected function groupReportsByWeek(array $rows): array { $reportGroups = []; foreach ($rows as $row) { $row['status_label'] = ClassProgressController::STATUS_OPTIONS[$row['status']] ?? 'Unknown'; $weekStart = $row['week_start'] ?? ''; $sectionId = (int) ($row['class_section_id'] ?? 0); if ($weekStart === '' || $sectionId === 0) { continue; } $key = $weekStart . ':' . $sectionId; if (! isset($reportGroups[$key])) { $reportGroups[$key] = [ 'week_start' => $row['week_start'] ?? '', 'week_end' => $row['week_end'] ?? '', 'class_section_name' => $row['class_section_name'] ?? '', 'class_section_id' => $sectionId, 'reports' => [], ]; } $reportGroups[$key]['reports'][$row['subject']] = $row; } return $reportGroups; } protected function isSectionAccessible(?int $classSectionId): bool { if (! $classSectionId) { return false; } return in_array((int) $classSectionId, $this->getParentSectionIds(), true); } protected function resolveAttachmentFile(array $row): ?string { $path = trim((string) ($row['attachment_path'] ?? '')); if ($path === '') { return null; } return $this->resolveAttachmentPath($path); } protected function resolveAttachmentPath(string $path): ?string { $relative = preg_replace('#^writable/uploads/#', '', $path); $absolute = WRITEPATH . 'uploads/' . ltrim($relative, '/'); return is_file($absolute) ? $absolute : null; } protected function loadAttachmentsForReports(array $reportIds): array { $reportIds = array_values(array_filter(array_map('intval', $reportIds))); if (empty($reportIds)) { return []; } $rows = $this->attachmentModel ->whereIn('report_id', $reportIds) ->orderBy('id', 'ASC') ->findAll(); $map = []; foreach ($rows as $row) { $reportId = (int) ($row['report_id'] ?? 0); if ($reportId === 0) { continue; } $map[$reportId][] = [ 'id' => (int) $row['id'], 'name' => $row['original_name'] ?: basename((string) $row['file_path']), ]; } return $map; } protected function decodeFlags(?string $json): array { if (! $json) { return []; } $flags = json_decode($json, true); return is_array($flags) ? $flags : []; } }