diff --git a/app/Config/App.php b/app/Config/App.php index 4423270..58df5ce 100644 --- a/app/Config/App.php +++ b/app/Config/App.php @@ -82,7 +82,7 @@ class App extends BaseConfig | DO NOT CHANGE THIS UNLESS YOU FULLY UNDERSTAND THE REPERCUSSIONS!! | */ - public string $permittedURIChars = 'a-z 0-9~%.:_\-'; + public string $permittedURIChars = 'a-z 0-9~%.:_\-,'; /** * -------------------------------------------------------------------------- diff --git a/app/Controllers/ClassProgressController.php b/app/Controllers/ClassProgressController.php index 0a63e2e..859f308 100644 --- a/app/Controllers/ClassProgressController.php +++ b/app/Controllers/ClassProgressController.php @@ -28,6 +28,10 @@ class ClassProgressController extends BaseController '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; @@ -705,6 +709,37 @@ class ClassProgressController extends BaseController 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")); @@ -717,9 +752,12 @@ class ClassProgressController extends BaseController 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 !== '' ? $segment . ' / ' . $chapter : $chapter; + $segment = $segment !== '' ? $unit . ' / ' . $chapter : $chapter; } if ($segment === '') { continue; @@ -730,17 +768,23 @@ class ClassProgressController extends BaseController 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')); - foreach ($unitValues as $value) { - if ($value !== '') { + $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; } @@ -759,9 +803,19 @@ class ClassProgressController extends BaseController 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) { - $units[] = trim($parts[0]); + $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; diff --git a/app/Controllers/View/AdministratorController.php b/app/Controllers/View/AdministratorController.php index 61fb1be..d15eff5 100644 --- a/app/Controllers/View/AdministratorController.php +++ b/app/Controllers/View/AdministratorController.php @@ -763,7 +763,13 @@ class AdministratorController extends BaseController [$progressExpectedWeeks, $progressSubmittedBySection] = $this->buildClassProgressStats($sectionIds); $examDraftCounts = []; - $examDraftDeadline = $this->resolveExamDraftDeadline($semester, $schoolYear); + $examDraftDeadline = $this->resolveTeacherDashboardExamDraftDeadline($semester, $schoolYear); + $examDraftDeadlineConfig = trim((string) ($this->configModel->getConfig('exam_draft_deadline') ?? '')); + $examDraftDeadlineFormatted = ''; + if ($examDraftDeadlineConfig !== '') { + $parsedUi = $this->parseExamDraftDeadlineConfigValue(); + $examDraftDeadlineFormatted = $parsedUi !== null ? $parsedUi->format('M j, Y') : ''; + } $homeworkCounts = []; if (! empty($sectionIds)) { $draftBuilder = $examDrafts @@ -1066,6 +1072,8 @@ class AdministratorController extends BaseController 'notificationHistory' => $historyMap, 'summary' => $summary, 'lowProgressSectionIds' => $lowProgressSectionIds, + 'examDraftDeadlineConfig' => $examDraftDeadlineConfig, + 'examDraftDeadlineFormatted' => $examDraftDeadlineFormatted, ]); } @@ -1299,6 +1307,7 @@ class AdministratorController extends BaseController $progressUrl = site_url('teacher/progress/history'); $examDraftUrl = site_url('teacher/exam-drafts'); $homeworkUrl = site_url('teacher/addHomework'); + $examDraftDeadlineEmailHtml = $this->buildExamDraftDeadlineEmailHtml(); $sentCount = 0; $failCount = 0; @@ -1347,7 +1356,8 @@ class AdministratorController extends BaseController } else { $draftLabel = 'exam draft'; } - $examDraftNote = "

" . ucfirst($draftLabel) . " submissions can be updated at Teacher Exam Drafts.

"; + $examDraftNote = "

" . ucfirst($draftLabel) . " submissions can be updated at Teacher Exam Drafts.

" + . $examDraftDeadlineEmailHtml; } $homeworkNote = ''; if (in_array('homework', $missingItems, true)) { @@ -1485,6 +1495,60 @@ class AdministratorController extends BaseController ]; } + /** + * Exam draft due date for the teacher submissions dashboard: prefers the configuration key + * `exam_draft_deadline` (same as automated reminders); otherwise fall/spring exam deadlines. + */ + private function resolveTeacherDashboardExamDraftDeadline(string $semester, string $schoolYear): ?\DateTimeImmutable + { + $fromExamDraftKey = $this->parseExamDraftDeadlineConfigValue(); + if ($fromExamDraftKey !== null) { + return $fromExamDraftKey; + } + + return $this->resolveExamDraftDeadline($semester, $schoolYear); + } + + /** + * Parses the `exam_draft_deadline` configuration value using the application timezone (midnight that calendar day). + */ + private function parseExamDraftDeadlineConfigValue(): ?\DateTimeImmutable + { + $raw = trim((string) ($this->configModel->getConfig('exam_draft_deadline') ?? '')); + if ($raw === '') { + return null; + } + $tz = new \DateTimeZone(config('App')->appTimezone ?? 'UTC'); + try { + $deadline = new \DateTimeImmutable($raw, $tz); + } catch (\Throwable $e) { + return null; + } + + return $deadline->setTime(0, 0, 0); + } + + /** + * HTML snippet for reminder emails when exam draft is included (deadline from exam_draft_deadline config). + */ + private function buildExamDraftDeadlineEmailHtml(): string + { + $raw = trim((string) ($this->configModel->getConfig('exam_draft_deadline') ?? '')); + if ($raw === '') { + return ''; + } + $parsed = $this->parseExamDraftDeadlineConfigValue(); + $display = $parsed !== null + ? htmlspecialchars($parsed->format('l, F j, Y'), ENT_QUOTES, 'UTF-8') + : htmlspecialchars($raw, ENT_QUOTES, 'UTF-8'); + $rawEsc = htmlspecialchars($raw, ENT_QUOTES, 'UTF-8'); + + return '

Exam draft submission deadline (exam_draft_deadline): ' + . "{$display}" + . ($parsed !== null && $rawEsc !== $display ? " (configured value: {$rawEsc})" : '') + . '.

'; + } + private function resolveExamDraftDeadline(string $semester, string $schoolYear): ?\DateTimeImmutable { $semesterKey = strtolower(trim($semester)); diff --git a/app/Controllers/View/ExamDraftController.php b/app/Controllers/View/ExamDraftController.php index 78879e6..6031752 100644 --- a/app/Controllers/View/ExamDraftController.php +++ b/app/Controllers/View/ExamDraftController.php @@ -6,6 +6,7 @@ use App\Controllers\BaseController; use App\Models\ClassSectionModel; use App\Models\ConfigurationModel; use App\Models\ExamDraftModel; +use App\Models\StudentClassModel; use App\Models\TeacherClassModel; use App\Models\UserModel; use CodeIgniter\HTTP\Files\UploadedFile; @@ -16,6 +17,7 @@ class ExamDraftController extends BaseController protected ExamDraftModel $examDraftModel; protected TeacherClassModel $teacherClassModel; protected ClassSectionModel $classSectionModel; + protected StudentClassModel $studentClassModel; protected UserModel $userModel; protected ConfigurationModel $configModel; @@ -57,6 +59,7 @@ class ExamDraftController extends BaseController $this->examDraftModel = new ExamDraftModel(); $this->teacherClassModel = new TeacherClassModel(); $this->classSectionModel = new ClassSectionModel(); + $this->studentClassModel = new StudentClassModel(); $this->userModel = new UserModel(); $this->configModel = new ConfigurationModel(); $this->db = Database::connect(); @@ -419,6 +422,52 @@ class ExamDraftController extends BaseController } unset($group); + $classSectionsById = []; + foreach ($classSections as $cs) { + $csId = (int) ($cs['class_section_id'] ?? 0); + if ($csId <= 0) { + continue; + } + $classSectionsById[$csId] = $cs; + } + + $studentCounts = $this->studentClassModel->getStudentCountsBySection($this->schoolYear); + $visibleClasses = []; + foreach ($studentCounts as $csId => $count) { + $csId = (int) $csId; + if ($csId <= 0 || $count <= 0) { + continue; + } + $classData = $classSectionsById[$csId] ?? $this->classSectionModel->where('class_section_id', $csId)->first(); + if ($classData === null) { + $classData = [ + 'class_section_id' => $csId, + 'class_section_name' => 'Class ' . $csId, + ]; + } + $classData['student_count'] = $count; + $visibleClasses[$csId] = $classData; + } + uasort($visibleClasses, static fn ($a, $b): int => strcasecmp($a['class_section_name'] ?? '', $b['class_section_name'] ?? '')); + + $classDraftGroups = []; + foreach ($drafts as $draft) { + $cid = (int) ($draft['class_section_id'] ?? 0); + if ($cid <= 0) { + continue; + } + $classDraftGroups[$cid][] = $draft; + } + $newSubmissionClasses = []; + foreach ($classDraftGroups as $cid => $group) { + foreach ($group as $draft) { + if (strtolower((string) ($draft['status'] ?? '')) === 'submitted') { + $newSubmissionClasses[$cid] = true; + break; + } + } + } + return view('administrator/exam_drafts', [ 'drafts' => $drafts, 'statusBadges' => $this->statusBadgeMap(), @@ -430,6 +479,9 @@ class ExamDraftController extends BaseController 'classSections' => $classSections, 'legacyByClass' => $legacyByClass, 'printableDraftIds' => $printableIds, + 'visibleClasses' => $visibleClasses, + 'classDraftGroups' => $classDraftGroups, + 'newSubmissionClasses' => $newSubmissionClasses, ]); } @@ -657,6 +709,9 @@ class ExamDraftController extends BaseController } } } + if ($status === 'legacy') { + $this->prepareLegacyPdfVersion($update, $draft); + } if ($this->hasIsLegacyColumn) { $update['is_legacy'] = strtolower($status) === 'legacy' ? 1 : 0; } @@ -875,7 +930,7 @@ class ExamDraftController extends BaseController $teacherName = 'Teacher #' . $teacherId; } - $class = $this->classSectionModel->find($classSectionId) ?? []; + $class = $this->classSectionModel->where('class_section_id', (int) $classSectionId)->first() ?? []; $className = $class['class_section_name'] ?? ('Class ' . $classSectionId); $subject = 'Exam draft submitted: ' . $className; @@ -1321,4 +1376,44 @@ class ExamDraftController extends BaseController } return $destName; } + + private function prepareLegacyPdfVersion(array &$update, array $draft): void + { + $finalFile = $update['final_file'] ?? $draft['final_file'] ?? null; + if (empty($finalFile)) { + $teacherFile = $this->draftTeacherFile($draft); + if (!empty($teacherFile)) { + $copied = $this->copyDraftToFinal($teacherFile); + if ($copied !== null) { + $finalFile = $copied; + $update['final_file'] = $copied; + $update['final_filename'] = $this->draftTeacherFilename($draft) ?? $teacherFile; + } + } + } + if (empty($finalFile)) { + return; + } + $ext = strtolower(pathinfo($finalFile, PATHINFO_EXTENSION)); + $pdfName = $this->ensurePdfExists($finalFile, $ext); + if ($pdfName === null) { + return; + } + $filename = $update['final_filename'] ?? $draft['final_filename'] ?? ''; + $baseName = ''; + if ($filename !== '') { + $baseName = pathinfo($filename, PATHINFO_FILENAME); + } + if ($baseName === '') { + $baseName = pathinfo($pdfName, PATHINFO_FILENAME); + } + if ($baseName === '') { + $baseName = 'exam'; + } + $update['final_file'] = $pdfName; + $update['final_filename'] = $baseName . '.pdf'; + if ($this->hasFinalPdfColumn) { + $update['final_pdf_file'] = $pdfName; + } + } } diff --git a/app/Controllers/View/LandingPageController.php b/app/Controllers/View/LandingPageController.php index f7a707c..531167e 100644 --- a/app/Controllers/View/LandingPageController.php +++ b/app/Controllers/View/LandingPageController.php @@ -95,6 +95,11 @@ class LandingPageController extends BaseController return null; } + // Teacher class sections only apply to the teacher role; other roles have no teacher_class rows. + if (strtolower(trim((string) $this->getUserRole())) !== 'teacher') { + return null; + } + // Get all class assignments for this teacher in the current term $assignments = $this->teacherClassModel->getClassAssignmentsByUserId( (int)$user_id, @@ -106,7 +111,7 @@ class LandingPageController extends BaseController $ids = array_values(array_filter(array_unique($ids))); if (empty($ids)) { - log_message('error', "No class section found for user ID: $user_id"); + log_message('warning', "No class section found for user ID: $user_id"); return null; } @@ -868,8 +873,12 @@ class LandingPageController extends BaseController protected function getUserRole() { - // Assuming you have a session variable storing the user role - return session()->get('user_role', 'guest'); // Default to guest if not set + $active = session()->get('active_role'); + if ($active !== null && $active !== '') { + return (string) $active; + } + + return (string) (session()->get('role') ?? 'guest'); } protected function getUserRoleFromDatabase($user_id) diff --git a/app/Models/ClassProgressReportModel.php b/app/Models/ClassProgressReportModel.php index f7c0285..d5ad154 100644 --- a/app/Models/ClassProgressReportModel.php +++ b/app/Models/ClassProgressReportModel.php @@ -4,6 +4,15 @@ namespace App\Models; use CodeIgniter\Model; +/** + * Weekly class progress reports (one row per subject per week). + * + * unit_title stores a compact summary of unit/chapter rows: segments joined with " ; ". + * Teacher-entered custom Islamic or Quran topics use the prefix "Custom / {text}" + * (see {@see \App\Controllers\ClassProgressController::CUSTOM_UNIT_ROW_LABEL} and + * {@see \App\Controllers\ClassProgressController::splitUnitTitleForDisplay()}). + * DB column is VARCHAR(120); controller truncates to match. + */ class ClassProgressReportModel extends Model { protected $table = 'class_progress_reports'; @@ -27,7 +36,42 @@ class ClassProgressReportModel extends Model 'flags_json', 'attachment_path', ]; + protected $useTimestamps = true; protected $createdField = 'created_at'; protected $updatedField = 'updated_at'; + + /** + * Validation is performed in {@see \App\Controllers\ClassProgressController} on HTTP input. + * Rules below document schema limits and can be enabled if you set {@see $skipValidation} to false. + */ + protected $skipValidation = true; + + protected $validationRules = [ + 'class_section_id' => 'required|integer', + 'teacher_id' => 'required|integer', + 'week_start' => 'required|valid_date[Y-m-d]', + 'week_end' => 'required|valid_date[Y-m-d]', + 'subject' => 'required|string|max_length[160]', + 'unit_title' => 'permit_empty|string|max_length[120]', + 'covered' => 'permit_empty|string', + 'homework' => 'permit_empty|string', + 'assessment' => 'permit_empty|string', + 'status' => 'permit_empty|in_list[on_track,slightly_behind,behind]', + 'status_notes' => 'permit_empty|string|max_length[200]', + 'class_notes' => 'permit_empty|string', + 'next_week_plan' => 'permit_empty|string', + 'support_needed' => 'permit_empty|string', + 'flags_json' => 'permit_empty|string', + 'attachment_path' => 'permit_empty|string|max_length[255]', + ]; + + protected $validationMessages = [ + 'unit_title' => [ + 'max_length' => 'Unit and chapter summary cannot exceed 120 characters.', + ], + 'subject' => [ + 'max_length' => 'Subject cannot exceed 160 characters.', + ], + ]; } diff --git a/app/Models/TeacherClassModel.php b/app/Models/TeacherClassModel.php index d9be96b..d80db34 100644 --- a/app/Models/TeacherClassModel.php +++ b/app/Models/TeacherClassModel.php @@ -71,7 +71,7 @@ class TeacherClassModel extends Model $builder = $this->db->table('teacher_class tc') ->select([ 'cs.class_section_name', - 'cs.id AS class_section_pk', + 'cs.class_section_id AS class_section_pk', 'cs.class_section_id', 'c.id AS class_id', 'c.class_name', @@ -181,10 +181,10 @@ class TeacherClassModel extends Model { $db = \Config\Database::connect(); - // Adjust table/column names if yours differ (e.g., cs.id vs cs.class_section_id) + // Adjust table/column names if yours differ (e.g., cs.class_section_id vs cs.id) $row = $db->table('classSection cs') ->select('u.id AS teacher_id, u.firstname, u.lastname') - ->join('teacher_class tc', 'tc.class_section_id = cs.id', 'inner') + ->join('teacher_class tc', 'tc.class_section_id = cs.class_section_id', 'inner') ->join('users u', 'u.id = tc.teacher_id', 'inner') ->where('cs.class_section_name', $classSectionName) ->where('tc.school_year', $schoolYear) diff --git a/app/Views/admin/class_progress_list.php b/app/Views/admin/class_progress_list.php index 2fa881a..a6fd88c 100644 --- a/app/Views/admin/class_progress_list.php +++ b/app/Views/admin/class_progress_list.php @@ -214,7 +214,17 @@ -
+
+ + (string) ($report['unit_title'] ?? ''), + 'isQuran' => ($subjectName === 'Quran/Arabic'), + 'compact' => true, + ]) ?> + + No submission + +
diff --git a/app/Views/admin/class_progress_view.php b/app/Views/admin/class_progress_view.php index 62aabeb..f370adc 100644 --- a/app/Views/admin/class_progress_view.php +++ b/app/Views/admin/class_progress_view.php @@ -39,7 +39,6 @@ @@ -51,7 +50,13 @@
No entry submitted for this subject this week.
-
:
+
+ (string) ($report['unit_title'] ?? ''), + 'isQuran' => $isQuran, + 'compact' => false, + ]) ?> +
Materials:
diff --git a/app/Views/admin/partials/class_progress_unit_display.php b/app/Views/admin/partials/class_progress_unit_display.php new file mode 100644 index 0000000..16c2755 --- /dev/null +++ b/app/Views/admin/partials/class_progress_unit_display.php @@ -0,0 +1,48 @@ + + + + - + + +
+ +
+ + +
+ + +
+ + + +
:
+ + +
:
+ + +
:
+ + diff --git a/app/Views/administrator/exam_drafts.php b/app/Views/administrator/exam_drafts.php index 70dab41..da74d90 100644 --- a/app/Views/administrator/exam_drafts.php +++ b/app/Views/administrator/exam_drafts.php @@ -6,6 +6,9 @@ $drafts = $drafts ?? []; $legacyByClass = $legacyByClass ?? []; $classSections = $classSections ?? []; $examTypes = $examTypes ?? []; +$visibleClasses = $visibleClasses ?? []; +$classDraftGroups = $classDraftGroups ?? []; +$newSubmissionClasses = $newSubmissionClasses ?? []; $schoolYear = $schoolYear ?? ''; $semester = $semester ?? ''; $maxUploadBytes = $maxUploadBytes ?? (12 * 1024 * 1024); @@ -56,172 +59,207 @@ $fileAccept = implode(',', array_map(static fn ($x) => '.' . $x, $allowedExtensi
- -

No exam drafts have been submitted yet.

+ +

No classes with enrolled students are available for this term.

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
TeacherClassTitle & AuthorNoteVersionDate-TimeStatusFilesReviewer ActionFinal version
- -
- -
- Author: - -
- - -
- Reviewer: - -
- -
v - $st, 'class' => 'bg-secondary text-white']; - $badgeStyle = !empty($badgeData['style']) ? ' style="' . esc($badgeData['style']) . '"' : ''; - ?> - > - - -
- - - -
- -
- Reviewed - - by +
+ $classInfo): ?> + + +
+

+

- - - -
- - - -
- - No teacher file - - -
- - - -
- - -
- ' . esc('Ver' . $revNumber . '_' . $rev . ' ' . $name) . ''; - } - } - ?> - -
- -
- - -
-
- 'vstack gap-2', 'id' => $formId]) ?> - - - - -
- As is / Minor edits - -
- > - -
-
- > - -
-
- -
- -
- -
- - -
- View - Download -
- -
+ + + + +
+ + +
+
+ +

No submissions yet for this class.

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TeacherClassTitle & AuthorNoteVersionDate-TimeStatusFilesReviewer ActionFinal version
+ +
+ +
+ Author: + +
+ + +
+ Reviewer: + +
+ +
v + $st, 'class' => 'bg-secondary text-white']; + $badgeStyle = !empty($badgeData['style']) ? ' style="' . esc($badgeData['style']) . '"' : ''; + ?> + > + + +
+ + + +
+ +
+ Reviewed + + by + +
+ +
+ + + +
+ + + +
+ + No teacher file + + +
+ + + +
+ + +
+ ' . esc('Ver' . $revNumber . '_' . $rev . ' ' . $name) . ''; + } + } + ?> + +
+ +
+ + + +
+
+ 'vstack gap-2', 'id' => $formId]) ?> + + + + +
+ As is / Minor edits + +
+ > + +
+
+ > + +
+
+ +
+ +
+ +
+ + +
+ View + Download +
+ +
+
+ +
+
+
+
@@ -319,6 +357,12 @@ $fileAccept = implode(',', array_map(static fn ($x) => '.' . $x, $allowedExtensi