'On track', 'slightly_behind' => 'Slightly behind', 'behind' => 'Behind', ]; private const DEFAULT_STATUS = 'on_track'; public const SUBJECT_SECTIONS = [ 'islamic' => [ 'label' => 'Islamic Studies', 'db_subject' => 'Islamic Studies', ], 'quran' => [ 'label' => 'Quran/Arabic', 'db_subject' => 'Quran/Arabic', ], ]; /** Unit column for teacher custom Islamic / Quran rows; must match teacher form JS. Segment: "Custom / {text}". */ public const CUSTOM_UNIT_ROW_LABEL = 'Custom'; protected ClassProgressReportModel $reportModel; protected ClassProgressAttachmentModel $attachmentModel; protected TeacherClassModel $teacherClassModel; protected ConfigurationModel $configModel; protected string $attachmentStoragePath; protected string $attachmentPublicBase; protected SubjectCurriculumModel $curriculumModel; public function __construct() { helper(['form', 'url']); $this->reportModel = new ClassProgressReportModel(); $this->attachmentModel = new ClassProgressAttachmentModel(); $this->teacherClassModel = new TeacherClassModel(); $this->configModel = new ConfigurationModel(); $this->curriculumModel = new SubjectCurriculumModel(); $this->attachmentStoragePath = WRITEPATH . 'uploads/class_material/'; $this->attachmentPublicBase = 'writable/uploads/class_material/'; } public function create() { $teacherId = (int) session()->get('user_id'); $assignments = $this->loadTeacherSections($teacherId); $first = $assignments[0] ?? null; $classSectionId = $first['class_section_id'] ?? null; $classSectionName = $first['class_section_name'] ?? null; $classId = $first['class_id'] ?? null; $sundayOptions = $this->buildSundayOptions(); $defaultWeekStart = $this->pickDefaultWeekStart($sundayOptions); $subjectCurriculum = []; if ($classId) { foreach (self::SUBJECT_SECTIONS as $slug => $section) { $subjectCurriculum[$slug] = $this->curriculumModel->getOptionsForClass((int) $classId, $slug); } } $data = [ 'subjectSections' => self::SUBJECT_SECTIONS, 'subjectCurriculum' => $subjectCurriculum, 'classSectionId' => $classSectionId, 'classSectionName' => $classSectionName, 'classId' => $classId, 'sundayOptions' => $sundayOptions, 'defaultWeekStart' => $defaultWeekStart, ]; return view('teacher/class_progress_submit', $data); } public function store() { $subjectSections = self::SUBJECT_SECTIONS; $rules = [ 'class_section_id' => 'required|integer', 'week_start' => 'required|valid_date[Y-m-d]', 'week_end' => 'required|valid_date[Y-m-d]', 'support_needed' => 'permit_empty|string', 'flags' => 'permit_empty', ]; foreach ($subjectSections as $slug => $section) { $rules["covered_$slug"] = 'required|string'; $rules["homework_$slug"] = 'permit_empty|string'; $rules["unit_{$slug}.*"] = 'permit_empty|string|max_length[120]'; $rules["chapter_{$slug}.*"] = 'permit_empty|string|max_length[120]'; } if (! $this->validate($rules)) { return redirect()->back()->withInput()->with('errors', $this->validator->getErrors()); } if (! $this->hasIslamicUnitSelection()) { return redirect()->back()->withInput()->with('error', 'Please select at least one Islamic Studies unit.'); } $attachmentErrors = $this->validateAttachmentFiles($subjectSections); if (! empty($attachmentErrors)) { return redirect()->back()->withInput()->with('errors', $attachmentErrors); } $weekStart = (string) $this->request->getPost('week_start'); $weekEnd = (string) $this->request->getPost('week_end'); if ($weekStart && ! $weekEnd) { $weekEnd = $this->buildWeekEndFromStart($weekStart); } if ($weekStart && $weekEnd && strtotime($weekEnd) < strtotime($weekStart)) { return redirect()->back()->withInput()->with('error', 'Week end must be the same as or after the week start.'); } $teacherId = (int) session()->get('user_id'); $classSectionId = $this->request->getPost('class_section_id'); $classSectionId = $classSectionId ? (int) $classSectionId : null; if (! $classSectionId) { return redirect()->back()->withInput()->with('error', 'No class assignment found for this report.'); } $confirmOverwrite = (bool) $this->request->getPost('confirm_overwrite'); $existingReports = $this->reportModel ->select('id') ->where('class_section_id', $classSectionId) ->where('week_start', $weekStart) ->where('teacher_id', $teacherId) ->findAll(); if (! $confirmOverwrite && ! empty($existingReports)) { return redirect()->back() ->withInput() ->with('warning', 'A progress report already exists for this week, are you sure you want to override it?') ->with('confirm_overwrite', true); } if ($confirmOverwrite && ! empty($existingReports)) { $existingIds = array_values(array_filter(array_map( static fn (array $row): int => (int) ($row['id'] ?? 0), $existingReports ))); if (! empty($existingIds)) { $this->attachmentModel->whereIn('report_id', $existingIds)->delete(); $this->reportModel->whereIn('id', $existingIds)->delete(); } } $status = self::DEFAULT_STATUS; $reportsCreated = 0; foreach ($subjectSections as $slug => $section) { $covered = trim((string) $this->request->getPost("covered_$slug")); if ($covered === '') { continue; } $homework = trim((string) $this->request->getPost("homework_$slug")); $unitTitle = $this->buildUnitChapterSummary($slug); $data = [ 'teacher_id' => $teacherId, 'class_section_id' => $classSectionId, 'week_start' => $weekStart, 'week_end' => $weekEnd, 'subject' => $section['db_subject'] ?? $section['label'] ?? $slug, 'unit_title' => $unitTitle, 'covered' => $covered, 'homework' => $homework ?: null, 'status' => $status, 'flags_json' => $this->normalizeFlags($this->request->getPost('flags')), ]; $reportId = $this->reportModel->insert($data, true); $attachmentField = "attachment_$slug"; $attachments = $this->request->getFileMultiple($attachmentField) ?? []; $storedAttachments = $this->storeAttachments($reportId, $attachments); if (! empty($storedAttachments)) { $this->attachmentModel->insertBatch($storedAttachments); $this->reportModel->update($reportId, ['attachment_path' => $storedAttachments[0]['file_path']]); } $reportsCreated++; } if ($reportsCreated === 0) { return redirect()->back()->withInput()->with('error', 'Please provide progress for at least one subject.'); } return redirect()->to('teacher/progress/submit')->with('success', 'Progress reports saved.'); } public function history() { $teacherId = (int) session()->get('user_id'); $assignments = $this->loadTeacherSections($teacherId); $selectedSectionId = (int) $this->request->getGet('class_section_id'); $validSectionIds = array_column($assignments, 'class_section_id'); if ($selectedSectionId === 0 && ! empty($validSectionIds)) { $selectedSectionId = $validSectionIds[0]; } if ($selectedSectionId && ! in_array($selectedSectionId, $validSectionIds, true)) { $selectedSectionId = $validSectionIds[0] ?? null; } [$semester, $schoolYear] = $this->resolveCurrentTerm(); $allowedTeacherIds = $this->resolveAssignedTeacherIds($selectedSectionId, $semester, $schoolYear); if (empty($allowedTeacherIds)) { $allowedTeacherIds = [$teacherId]; } $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('teacher_id', $allowedTeacherIds); if ($selectedSectionId) { $builder->where('class_progress_reports.class_section_id', $selectedSectionId); } $rows = $builder ->orderBy('week_start', 'DESC') ->findAll(); $reportGroups = []; foreach ($rows as $row) { $row['status_label'] = self::STATUS_OPTIONS[$row['status']] ?? 'Unknown'; $key = $row['week_start'] ?? ''; if ($key === '') { continue; } if (! isset($reportGroups[$key])) { $reportGroups[$key] = [ 'week_start' => $row['week_start'], 'week_end' => $row['week_end'], 'class_section_name' => $row['class_section_name'] ?? '', 'reports' => [], ]; } $reportGroups[$key]['reports'][$row['subject']] = $row; } $sectionOptions = []; foreach ($assignments as $assignment) { $sectionOptions[$assignment['class_section_id']] = $assignment['class_section_name'] ?? ''; } return view('teacher/class_progress_history', [ 'reportGroups' => $reportGroups, 'subjectSections' => self::SUBJECT_SECTIONS, 'classSectionOptions' => $sectionOptions, 'selectedSectionId' => $selectedSectionId, ]); } public function view($id) { $teacherId = (int) session()->get('user_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') ->where('class_progress_reports.id', (int) $id) ->first(); if (! $row) { throw new PageNotFoundException('Progress report not found.'); } [$semester, $schoolYear] = $this->resolveCurrentTerm(); $allowedTeacherIds = $this->resolveAssignedTeacherIds((int) $row['class_section_id'], $semester, $schoolYear); if (empty($allowedTeacherIds)) { if ($teacherId !== (int) $row['teacher_id']) { throw new PageNotFoundException('Progress report not found.'); } $allowedTeacherIds = [(int) $row['teacher_id']]; } elseif (! in_array($teacherId, $allowedTeacherIds, true)) { throw new PageNotFoundException('Progress report not found.'); } $row['status_label'] = self::STATUS_OPTIONS[$row['status']] ?? 'Unknown'; $weeklyReports = $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('teacher_id', $allowedTeacherIds) ->where('class_progress_reports.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['status_label'] = self::STATUS_OPTIONS[$report['status']] ?? 'Unknown'; $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('teacher/class_progress_view', [ 'row' => $row, 'weeklyReports' => $weeklyReports, 'subjectSections' => self::SUBJECT_SECTIONS, ]); } public function edit($id) { $teacherId = (int) session()->get('user_id'); $row = $this->reportModel ->select('class_progress_reports.*, cs.class_section_name') ->join('classSection cs', 'cs.class_section_id = class_progress_reports.class_section_id', 'left') ->where('class_progress_reports.id', (int) $id) ->first(); if (! $row) { throw new PageNotFoundException('Progress report not found.'); } [$semester, $schoolYear] = $this->resolveCurrentTerm(); $allowedTeacherIds = $this->resolveAssignedTeacherIds((int) $row['class_section_id'], $semester, $schoolYear); if (empty($allowedTeacherIds)) { if ($teacherId !== (int) $row['teacher_id']) { throw new PageNotFoundException('Progress report not found.'); } $allowedTeacherIds = [(int) $row['teacher_id']]; } elseif (! in_array($teacherId, $allowedTeacherIds, true)) { throw new PageNotFoundException('Progress report not found.'); } $weeklyReports = $this->reportModel ->select('class_progress_reports.*') ->whereIn('teacher_id', $allowedTeacherIds) ->where('class_section_id', $row['class_section_id']) ->where('week_start', $row['week_start']) ->orderBy('subject', 'ASC') ->findAll(); $reportMap = []; foreach ($weeklyReports as $report) { $subject = (string) ($report['subject'] ?? ''); if ($subject === '') { continue; } $reportMap[$subject] = $report; } $subjectReports = []; foreach (self::SUBJECT_SECTIONS as $slug => $section) { $subjectName = $section['db_subject'] ?? $section['label'] ?? $slug; $report = $reportMap[$subjectName] ?? null; if (! $report) { continue; } $parsed = $this->parseUnitChapterSummary((string) ($report['unit_title'] ?? '')); $subjectReports[$slug] = [ 'report_id' => (int) ($report['id'] ?? 0), 'covered' => $report['covered'] ?? '', 'homework' => $report['homework'] ?? '', 'unit_title' => $report['unit_title'] ?? '', 'unit_values' => $parsed['units'], 'chapter_values' => $parsed['chapters'], ]; } $assignments = $this->loadTeacherSections($teacherId); $classId = null; $classSectionName = $row['class_section_name'] ?? ''; foreach ($assignments as $assignment) { if ((int) ($assignment['class_section_id'] ?? 0) === (int) $row['class_section_id']) { $classId = $assignment['class_id'] ?? null; $classSectionName = $assignment['class_section_name'] ?? $classSectionName; break; } } $subjectCurriculum = []; if ($classId) { foreach (self::SUBJECT_SECTIONS as $slug => $section) { $subjectCurriculum[$slug] = $this->curriculumModel->getOptionsForClass((int) $classId, $slug); } } $sundayOptions = $this->buildSundayOptions(); if (!in_array($row['week_start'], $sundayOptions, true)) { array_unshift($sundayOptions, $row['week_start']); } return view('teacher/class_progress_submit', [ 'subjectSections' => self::SUBJECT_SECTIONS, 'subjectCurriculum' => $subjectCurriculum, 'classSectionId' => $row['class_section_id'], 'classSectionName' => $classSectionName, 'classId' => $classId, 'sundayOptions' => $sundayOptions, 'defaultWeekStart' => $row['week_start'], 'existingWeekEnd' => $row['week_end'], 'existingReports' => $subjectReports, 'isEdit' => true, 'formAction' => base_url('teacher/progress/update/' . (int) $row['id']), 'submitLabel' => 'Update Progress', ]); } public function update($id) { $teacherId = (int) session()->get('user_id'); $row = $this->reportModel->find((int) $id); if (! $row) { throw new PageNotFoundException('Progress report not found.'); } [$semester, $schoolYear] = $this->resolveCurrentTerm(); $allowedTeacherIds = $this->resolveAssignedTeacherIds((int) $row['class_section_id'], $semester, $schoolYear); if (empty($allowedTeacherIds)) { if ($teacherId !== (int) $row['teacher_id']) { throw new PageNotFoundException('Progress report not found.'); } $allowedTeacherIds = [(int) $row['teacher_id']]; } elseif (! in_array($teacherId, $allowedTeacherIds, true)) { throw new PageNotFoundException('Progress report not found.'); } $subjectSections = self::SUBJECT_SECTIONS; $rules = [ 'class_section_id' => 'required|integer', 'week_start' => 'required|valid_date[Y-m-d]', 'week_end' => 'required|valid_date[Y-m-d]', ]; foreach ($subjectSections as $slug => $section) { $rules["covered_$slug"] = 'required|string'; $rules["homework_$slug"] = 'permit_empty|string'; $rules["unit_{$slug}.*"] = 'permit_empty|string|max_length[120]'; $rules["chapter_{$slug}.*"] = 'permit_empty|string|max_length[120]'; } if (! $this->validate($rules)) { return redirect()->back()->withInput()->with('errors', $this->validator->getErrors()); } if (! $this->hasIslamicUnitSelection()) { return redirect()->back()->withInput()->with('error', 'Please select at least one Islamic Studies unit.'); } $attachmentErrors = $this->validateAttachmentFiles($subjectSections); if (! empty($attachmentErrors)) { return redirect()->back()->withInput()->with('errors', $attachmentErrors); } $weekStart = (string) $this->request->getPost('week_start'); $weekEnd = (string) $this->request->getPost('week_end'); if ($weekStart && ! $weekEnd) { $weekEnd = $this->buildWeekEndFromStart($weekStart); } if ($weekStart && $weekEnd && strtotime($weekEnd) < strtotime($weekStart)) { return redirect()->back()->withInput()->with('error', 'Week end must be the same as or after the week start.'); } $classSectionId = (int) ($row['class_section_id'] ?? 0); if ($classSectionId === 0) { return redirect()->back()->withInput()->with('error', 'No class assignment found for this report.'); } $confirmOverwrite = (bool) $this->request->getPost('confirm_overwrite'); if ($weekStart && $weekStart !== (string) ($row['week_start'] ?? '')) { $conflicts = $this->reportModel ->select('id') ->where('class_section_id', $classSectionId) ->where('week_start', $weekStart) ->where('teacher_id', $teacherId) ->findAll(); if (! $confirmOverwrite && ! empty($conflicts)) { return redirect()->back() ->withInput() ->with('warning', 'A progress report already exists for this week, are you sure you want to override it?') ->with('confirm_overwrite', true); } if ($confirmOverwrite && ! empty($conflicts)) { $conflictIds = array_values(array_filter(array_map( static fn (array $row): int => (int) ($row['id'] ?? 0), $conflicts ))); if (! empty($conflictIds)) { $this->attachmentModel->whereIn('report_id', $conflictIds)->delete(); $this->reportModel->whereIn('id', $conflictIds)->delete(); } } } $weeklyReports = $this->reportModel ->select('class_progress_reports.*') ->whereIn('teacher_id', $allowedTeacherIds) ->where('class_section_id', $classSectionId) ->where('week_start', $row['week_start']) ->orderBy('subject', 'ASC') ->findAll(); $reportMap = []; foreach ($weeklyReports as $report) { $subject = (string) ($report['subject'] ?? ''); if ($subject === '') { continue; } $reportMap[$subject] = $report; } $reportsUpdated = 0; $flagsInput = $this->request->getPost('flags'); foreach ($subjectSections as $slug => $section) { $covered = trim((string) $this->request->getPost("covered_$slug")); if ($covered === '') { continue; } $homework = trim((string) $this->request->getPost("homework_$slug")); $unitTitle = $this->buildUnitChapterSummary($slug); $subjectName = $section['db_subject'] ?? $section['label'] ?? $slug; $existing = $reportMap[$subjectName] ?? null; if ($unitTitle === null && $existing) { $unitTitle = $existing['unit_title'] ?? null; } $data = [ 'class_section_id' => $classSectionId, 'week_start' => $weekStart, 'week_end' => $weekEnd, 'subject' => $subjectName, 'unit_title' => $unitTitle, 'covered' => $covered, 'homework' => $homework ?: null, ]; if ($flagsInput !== null) { $data['flags_json'] = $this->normalizeFlags($flagsInput); } if ($existing) { $this->reportModel->update((int) $existing['id'], $data); $reportId = (int) $existing['id']; } else { $data['teacher_id'] = $teacherId; $data['status'] = self::DEFAULT_STATUS; $reportId = (int) $this->reportModel->insert($data, true); } $attachmentField = "attachment_$slug"; $attachments = $this->request->getFileMultiple($attachmentField) ?? []; $storedAttachments = $this->storeAttachments($reportId, $attachments); if (! empty($storedAttachments)) { $this->attachmentModel->insertBatch($storedAttachments); if (empty($existing['attachment_path'] ?? '')) { $this->reportModel->update($reportId, ['attachment_path' => $storedAttachments[0]['file_path']]); } } $reportsUpdated++; } if ($reportsUpdated === 0) { return redirect()->back()->withInput()->with('error', 'Please provide progress for at least one subject.'); } return redirect()->to('teacher/progress/history')->with('success', 'Progress reports updated.'); } public function attachment($id) { $row = $this->reportModel->find((int)$id); if (! $row || 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.'); } $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 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 validateAttachmentFiles(array $subjectSections): array { $errors = []; foreach ($subjectSections as $slug => $section) { $label = $section['label'] ?? $slug; $field = "attachment_$slug"; $files = $this->request->getFileMultiple($field) ?? []; foreach ($files as $file) { if (! $file || $file->getError() === UPLOAD_ERR_NO_FILE) { continue; } if (! $file->isValid()) { $errors[] = "Invalid attachment uploaded for {$label}."; continue; } if ($file->getSize() > 5 * 1024 * 1024) { $errors[] = "Each attachment for {$label} must be 5MB or smaller."; } $ext = strtolower((string) $file->getClientExtension()); if ($ext === '' || ! in_array($ext, ['pdf', 'jpg', 'jpeg', 'png'], true)) { $errors[] = "Only PDF, JPG, JPEG, or PNG files are allowed for {$label}."; } } } return $errors; } protected function storeAttachments(int $reportId, array $attachments): array { if (empty($attachments)) { return []; } $stored = []; $now = date('Y-m-d H:i:s'); foreach ($attachments as $file) { if (! $file || $file->getError() === UPLOAD_ERR_NO_FILE) { continue; } if (! $file->isValid() || $file->hasMoved()) { continue; } $stored[] = [ 'report_id' => $reportId, 'file_path' => $this->storeAttachment($file), 'original_name' => $file->getClientName(), 'mime_type' => $file->getClientMimeType(), 'file_size' => $file->getSize(), 'created_at' => $now, ]; } return $stored; } 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 loadTeacherSections(int $teacherId): array { $schoolYear = (string) ($this->configModel->getConfig('school_year') ?? ''); $semester = (string) ($this->configModel->getConfig('semester') ?? ''); return $this->teacherClassModel->getClassAssignmentsByUserId($teacherId, $schoolYear, $semester); } protected function normalizeFlags($flags): ?string { $flags = array_values(array_filter((array) $flags, static fn ($item) => $item !== '' && $item !== null)); return $flags ? json_encode($flags) : null; } /** * Split stored unit_title into curriculum lines vs custom teacher-typed subjects ("Custom / …"). * Keep in sync with teacher form, {@see buildUnitChapterSummary()}, and admin views. * * @return array{curriculum: list, custom: list} */ public static function splitUnitTitleForDisplay(string $unitTitle): array { $unitTitle = trim($unitTitle); if ($unitTitle === '') { return ['curriculum' => [], 'custom' => []]; } $segments = preg_split('/\s*;\s*/', $unitTitle, -1, PREG_SPLIT_NO_EMPTY); $curriculum = []; $custom = []; foreach ($segments as $seg) { $seg = trim((string) $seg); if ($seg === '') { continue; } if (preg_match('/^Custom\s*\/\s*(.+)$/iu', $seg, $m)) { $custom[] = trim($m[1]); } else { $curriculum[] = $seg; } } return ['curriculum' => $curriculum, 'custom' => $custom]; } protected function buildUnitChapterSummary(string $slug): ?string { $unitValues = array_map('trim', (array) $this->request->getPost("unit_$slug")); $chapterValues = array_map('trim', (array) $this->request->getPost("chapter_$slug")); $parts = []; $count = max(count($unitValues), count($chapterValues)); for ($i = 0; $i < $count; $i++) { $unit = $unitValues[$i] ?? ''; $chapter = $chapterValues[$i] ?? ''; if ($unit === '' && $chapter === '') { continue; } if (strcasecmp($unit, self::CUSTOM_UNIT_ROW_LABEL) === 0 && $chapter !== '') { $unit = self::CUSTOM_UNIT_ROW_LABEL; } $segment = $unit; if ($chapter !== '') { $segment = $segment !== '' ? $unit . ' / ' . $chapter : $chapter; } if ($segment === '') { continue; } $parts[] = $segment; } if (! $parts) { return null; } $summary = implode(' ; ', $parts); return mb_strlen($summary) > 120 ? mb_substr($summary, 0, 120) : $summary; } protected function hasIslamicUnitSelection(): bool { $unitValues = array_map('trim', (array) $this->request->getPost('unit_islamic')); $chapterValues = array_map('trim', (array) $this->request->getPost('chapter_islamic')); $count = max(count($unitValues), count($chapterValues)); for ($i = 0; $i < $count; $i++) { $unit = $unitValues[$i] ?? ''; $chapter = $chapterValues[$i] ?? ''; if ($unit !== '' || $chapter !== '') { return true; } } return false; } protected function parseUnitChapterSummary(string $summary): array { $summary = trim($summary); if ($summary === '') { return ['units' => [], 'chapters' => []]; } $units = []; $chapters = []; $segments = preg_split('/\s*;\s*/', $summary, -1, PREG_SPLIT_NO_EMPTY); foreach ($segments as $segment) { $segment = trim($segment); if ($segment === '') { continue; } if (preg_match('/^Custom\s*\/\s*(.+)$/iu', $segment, $m)) { $units[] = self::CUSTOM_UNIT_ROW_LABEL; $chapters[] = trim($m[1]); continue; } $parts = preg_split('/\s*\/\s*/', $segment, 2); if (count($parts) === 2) { $u = trim($parts[0]); if (strcasecmp($u, self::CUSTOM_UNIT_ROW_LABEL) === 0) { $u = self::CUSTOM_UNIT_ROW_LABEL; } $units[] = $u; $chapters[] = trim($parts[1]); } else { $units[] = $segment; $chapters[] = ''; } } return ['units' => $units, 'chapters' => $chapters]; } protected function buildSundayOptions(int $count = 12): array { $range = $this->resolveProgressDateRange(); if ($range === null) { return $this->buildUpcomingSundayOptions($count); } [$rangeStart, $rangeEnd] = $range; try { $start = new \DateTime($rangeStart); $end = new \DateTime($rangeEnd); } catch (\Exception $e) { return $this->buildUpcomingSundayOptions($count); } if ($end < $start) { [$start, $end] = [$end, $start]; } $weekday = (int) $start->format('w'); if ($weekday !== 0) { $start->modify('next sunday'); } $options = []; while ($start <= $end) { $options[] = $start->format('Y-m-d'); $start->modify('+7 days'); } return $options; } protected function buildUpcomingSundayOptions(int $count): array { $start = new \DateTime('today'); $weekday = (int) $start->format('w'); if ($weekday !== 0) { $start->modify('next sunday'); } $options = []; for ($i = 0; $i < $count; $i++) { $options[] = $start->format('Y-m-d'); $start->modify('+7 days'); } return $options; } protected function resolveProgressDateRange(): ?array { $schoolYear = (string) ($this->configModel->getConfig('school_year') ?? ''); if ($schoolYear === '') { return null; } $semesterResolver = new SemesterRangeService($this->configModel); $semester = $semesterResolver->normalizeSemester((string) ($this->configModel->getConfig('semester') ?? '')); if ($semester === '') { $semester = $semesterResolver->getSemesterForDate(); } if ($semester !== '') { $range = $semesterResolver->getSemesterRange($schoolYear, $semester); if ($range !== null) { return $range; } } return $semesterResolver->getSchoolYearRange($schoolYear); } protected function pickDefaultWeekStart(array $options): string { if (empty($options)) { return ''; } $today = date('Y-m-d'); $default = ''; foreach ($options as $option) { if ($option <= $today) { $default = $option; continue; } break; } return $default !== '' ? $default : $options[0]; } protected function buildWeekEndFromStart(string $weekStart): string { try { $dt = new \DateTime($weekStart); $dt->modify('+6 days'); return $dt->format('Y-m-d'); } catch (\Exception $e) { return $weekStart; } } protected function storeAttachment($file): string { $this->ensureAttachmentPath(); $name = $file->getRandomName(); $file->move($this->attachmentStoragePath, $name); return $this->attachmentPublicBase . $name; } protected function ensureAttachmentPath(): void { if (! is_dir($this->attachmentStoragePath)) { mkdir($this->attachmentStoragePath, 0755, true); } } protected function resolveCurrentTerm(): array { $schoolYear = (string) ($this->configModel->getConfig('school_year') ?? ''); $semester = (string) ($this->configModel->getConfig('semester') ?? ''); return [$semester, $schoolYear]; } protected function resolveAssignedTeacherIds(?int $classSectionId, string $semester, string $schoolYear): array { if (! $classSectionId || $schoolYear === '') { return []; } $rows = $this->teacherClassModel->assignedForSectionTerm($classSectionId, $semester, $schoolYear); $ids = []; foreach ($rows as $row) { $id = (int) ($row['teacher_id'] ?? 0); if ($id > 0) { $ids[$id] = true; } } return array_keys($ids); } }