exam draft fix
This commit is contained in:
+1
-1
@@ -82,7 +82,7 @@ class App extends BaseConfig
|
|||||||
| DO NOT CHANGE THIS UNLESS YOU FULLY UNDERSTAND THE REPERCUSSIONS!!
|
| DO NOT CHANGE THIS UNLESS YOU FULLY UNDERSTAND THE REPERCUSSIONS!!
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
public string $permittedURIChars = 'a-z 0-9~%.:_\-';
|
public string $permittedURIChars = 'a-z 0-9~%.:_\-,';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* --------------------------------------------------------------------------
|
* --------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ class ClassProgressController extends BaseController
|
|||||||
'db_subject' => '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 ClassProgressReportModel $reportModel;
|
||||||
protected ClassProgressAttachmentModel $attachmentModel;
|
protected ClassProgressAttachmentModel $attachmentModel;
|
||||||
protected TeacherClassModel $teacherClassModel;
|
protected TeacherClassModel $teacherClassModel;
|
||||||
@@ -705,6 +709,37 @@ class ClassProgressController extends BaseController
|
|||||||
return $flags ? json_encode($flags) : 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<string>, custom: list<string>}
|
||||||
|
*/
|
||||||
|
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
|
protected function buildUnitChapterSummary(string $slug): ?string
|
||||||
{
|
{
|
||||||
$unitValues = array_map('trim', (array) $this->request->getPost("unit_$slug"));
|
$unitValues = array_map('trim', (array) $this->request->getPost("unit_$slug"));
|
||||||
@@ -717,9 +752,12 @@ class ClassProgressController extends BaseController
|
|||||||
if ($unit === '' && $chapter === '') {
|
if ($unit === '' && $chapter === '') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (strcasecmp($unit, self::CUSTOM_UNIT_ROW_LABEL) === 0 && $chapter !== '') {
|
||||||
|
$unit = self::CUSTOM_UNIT_ROW_LABEL;
|
||||||
|
}
|
||||||
$segment = $unit;
|
$segment = $unit;
|
||||||
if ($chapter !== '') {
|
if ($chapter !== '') {
|
||||||
$segment = $segment !== '' ? $segment . ' / ' . $chapter : $chapter;
|
$segment = $segment !== '' ? $unit . ' / ' . $chapter : $chapter;
|
||||||
}
|
}
|
||||||
if ($segment === '') {
|
if ($segment === '') {
|
||||||
continue;
|
continue;
|
||||||
@@ -730,17 +768,23 @@ class ClassProgressController extends BaseController
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
$summary = implode(' ; ', $parts);
|
$summary = implode(' ; ', $parts);
|
||||||
|
|
||||||
return mb_strlen($summary) > 120 ? mb_substr($summary, 0, 120) : $summary;
|
return mb_strlen($summary) > 120 ? mb_substr($summary, 0, 120) : $summary;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function hasIslamicUnitSelection(): bool
|
protected function hasIslamicUnitSelection(): bool
|
||||||
{
|
{
|
||||||
$unitValues = array_map('trim', (array) $this->request->getPost('unit_islamic'));
|
$unitValues = array_map('trim', (array) $this->request->getPost('unit_islamic'));
|
||||||
foreach ($unitValues as $value) {
|
$chapterValues = array_map('trim', (array) $this->request->getPost('chapter_islamic'));
|
||||||
if ($value !== '') {
|
$count = max(count($unitValues), count($chapterValues));
|
||||||
|
for ($i = 0; $i < $count; $i++) {
|
||||||
|
$unit = $unitValues[$i] ?? '';
|
||||||
|
$chapter = $chapterValues[$i] ?? '';
|
||||||
|
if ($unit !== '' || $chapter !== '') {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -759,9 +803,19 @@ class ClassProgressController extends BaseController
|
|||||||
if ($segment === '') {
|
if ($segment === '') {
|
||||||
continue;
|
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);
|
$parts = preg_split('/\s*\/\s*/', $segment, 2);
|
||||||
if (count($parts) === 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]);
|
$chapters[] = trim($parts[1]);
|
||||||
} else {
|
} else {
|
||||||
$units[] = $segment;
|
$units[] = $segment;
|
||||||
|
|||||||
@@ -763,7 +763,13 @@ class AdministratorController extends BaseController
|
|||||||
|
|
||||||
[$progressExpectedWeeks, $progressSubmittedBySection] = $this->buildClassProgressStats($sectionIds);
|
[$progressExpectedWeeks, $progressSubmittedBySection] = $this->buildClassProgressStats($sectionIds);
|
||||||
$examDraftCounts = [];
|
$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 = [];
|
$homeworkCounts = [];
|
||||||
if (! empty($sectionIds)) {
|
if (! empty($sectionIds)) {
|
||||||
$draftBuilder = $examDrafts
|
$draftBuilder = $examDrafts
|
||||||
@@ -1066,6 +1072,8 @@ class AdministratorController extends BaseController
|
|||||||
'notificationHistory' => $historyMap,
|
'notificationHistory' => $historyMap,
|
||||||
'summary' => $summary,
|
'summary' => $summary,
|
||||||
'lowProgressSectionIds' => $lowProgressSectionIds,
|
'lowProgressSectionIds' => $lowProgressSectionIds,
|
||||||
|
'examDraftDeadlineConfig' => $examDraftDeadlineConfig,
|
||||||
|
'examDraftDeadlineFormatted' => $examDraftDeadlineFormatted,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1299,6 +1307,7 @@ class AdministratorController extends BaseController
|
|||||||
$progressUrl = site_url('teacher/progress/history');
|
$progressUrl = site_url('teacher/progress/history');
|
||||||
$examDraftUrl = site_url('teacher/exam-drafts');
|
$examDraftUrl = site_url('teacher/exam-drafts');
|
||||||
$homeworkUrl = site_url('teacher/addHomework');
|
$homeworkUrl = site_url('teacher/addHomework');
|
||||||
|
$examDraftDeadlineEmailHtml = $this->buildExamDraftDeadlineEmailHtml();
|
||||||
$sentCount = 0;
|
$sentCount = 0;
|
||||||
$failCount = 0;
|
$failCount = 0;
|
||||||
|
|
||||||
@@ -1347,7 +1356,8 @@ class AdministratorController extends BaseController
|
|||||||
} else {
|
} else {
|
||||||
$draftLabel = 'exam draft';
|
$draftLabel = 'exam draft';
|
||||||
}
|
}
|
||||||
$examDraftNote = "<p>" . ucfirst($draftLabel) . " submissions can be updated at <a href=\"{$examDraftUrl}\">Teacher Exam Drafts</a>.</p>";
|
$examDraftNote = "<p>" . ucfirst($draftLabel) . " submissions can be updated at <a href=\"{$examDraftUrl}\">Teacher Exam Drafts</a>.</p>"
|
||||||
|
. $examDraftDeadlineEmailHtml;
|
||||||
}
|
}
|
||||||
$homeworkNote = '';
|
$homeworkNote = '';
|
||||||
if (in_array('homework', $missingItems, true)) {
|
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 '<p><strong>Exam draft submission deadline</strong> (<code>exam_draft_deadline</code>): '
|
||||||
|
. "<strong>{$display}</strong>"
|
||||||
|
. ($parsed !== null && $rawEsc !== $display ? " <span style=\"color:#555;\">(configured value: {$rawEsc})</span>" : '')
|
||||||
|
. '.</p>';
|
||||||
|
}
|
||||||
|
|
||||||
private function resolveExamDraftDeadline(string $semester, string $schoolYear): ?\DateTimeImmutable
|
private function resolveExamDraftDeadline(string $semester, string $schoolYear): ?\DateTimeImmutable
|
||||||
{
|
{
|
||||||
$semesterKey = strtolower(trim($semester));
|
$semesterKey = strtolower(trim($semester));
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use App\Controllers\BaseController;
|
|||||||
use App\Models\ClassSectionModel;
|
use App\Models\ClassSectionModel;
|
||||||
use App\Models\ConfigurationModel;
|
use App\Models\ConfigurationModel;
|
||||||
use App\Models\ExamDraftModel;
|
use App\Models\ExamDraftModel;
|
||||||
|
use App\Models\StudentClassModel;
|
||||||
use App\Models\TeacherClassModel;
|
use App\Models\TeacherClassModel;
|
||||||
use App\Models\UserModel;
|
use App\Models\UserModel;
|
||||||
use CodeIgniter\HTTP\Files\UploadedFile;
|
use CodeIgniter\HTTP\Files\UploadedFile;
|
||||||
@@ -16,6 +17,7 @@ class ExamDraftController extends BaseController
|
|||||||
protected ExamDraftModel $examDraftModel;
|
protected ExamDraftModel $examDraftModel;
|
||||||
protected TeacherClassModel $teacherClassModel;
|
protected TeacherClassModel $teacherClassModel;
|
||||||
protected ClassSectionModel $classSectionModel;
|
protected ClassSectionModel $classSectionModel;
|
||||||
|
protected StudentClassModel $studentClassModel;
|
||||||
protected UserModel $userModel;
|
protected UserModel $userModel;
|
||||||
protected ConfigurationModel $configModel;
|
protected ConfigurationModel $configModel;
|
||||||
|
|
||||||
@@ -57,6 +59,7 @@ class ExamDraftController extends BaseController
|
|||||||
$this->examDraftModel = new ExamDraftModel();
|
$this->examDraftModel = new ExamDraftModel();
|
||||||
$this->teacherClassModel = new TeacherClassModel();
|
$this->teacherClassModel = new TeacherClassModel();
|
||||||
$this->classSectionModel = new ClassSectionModel();
|
$this->classSectionModel = new ClassSectionModel();
|
||||||
|
$this->studentClassModel = new StudentClassModel();
|
||||||
$this->userModel = new UserModel();
|
$this->userModel = new UserModel();
|
||||||
$this->configModel = new ConfigurationModel();
|
$this->configModel = new ConfigurationModel();
|
||||||
$this->db = Database::connect();
|
$this->db = Database::connect();
|
||||||
@@ -419,6 +422,52 @@ class ExamDraftController extends BaseController
|
|||||||
}
|
}
|
||||||
unset($group);
|
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', [
|
return view('administrator/exam_drafts', [
|
||||||
'drafts' => $drafts,
|
'drafts' => $drafts,
|
||||||
'statusBadges' => $this->statusBadgeMap(),
|
'statusBadges' => $this->statusBadgeMap(),
|
||||||
@@ -430,6 +479,9 @@ class ExamDraftController extends BaseController
|
|||||||
'classSections' => $classSections,
|
'classSections' => $classSections,
|
||||||
'legacyByClass' => $legacyByClass,
|
'legacyByClass' => $legacyByClass,
|
||||||
'printableDraftIds' => $printableIds,
|
'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) {
|
if ($this->hasIsLegacyColumn) {
|
||||||
$update['is_legacy'] = strtolower($status) === 'legacy' ? 1 : 0;
|
$update['is_legacy'] = strtolower($status) === 'legacy' ? 1 : 0;
|
||||||
}
|
}
|
||||||
@@ -875,7 +930,7 @@ class ExamDraftController extends BaseController
|
|||||||
$teacherName = 'Teacher #' . $teacherId;
|
$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);
|
$className = $class['class_section_name'] ?? ('Class ' . $classSectionId);
|
||||||
|
|
||||||
$subject = 'Exam draft submitted: ' . $className;
|
$subject = 'Exam draft submitted: ' . $className;
|
||||||
@@ -1321,4 +1376,44 @@ class ExamDraftController extends BaseController
|
|||||||
}
|
}
|
||||||
return $destName;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,6 +95,11 @@ class LandingPageController extends BaseController
|
|||||||
return null;
|
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
|
// Get all class assignments for this teacher in the current term
|
||||||
$assignments = $this->teacherClassModel->getClassAssignmentsByUserId(
|
$assignments = $this->teacherClassModel->getClassAssignmentsByUserId(
|
||||||
(int)$user_id,
|
(int)$user_id,
|
||||||
@@ -106,7 +111,7 @@ class LandingPageController extends BaseController
|
|||||||
$ids = array_values(array_filter(array_unique($ids)));
|
$ids = array_values(array_filter(array_unique($ids)));
|
||||||
|
|
||||||
if (empty($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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -868,8 +873,12 @@ class LandingPageController extends BaseController
|
|||||||
|
|
||||||
protected function getUserRole()
|
protected function getUserRole()
|
||||||
{
|
{
|
||||||
// Assuming you have a session variable storing the user role
|
$active = session()->get('active_role');
|
||||||
return session()->get('user_role', 'guest'); // Default to guest if not set
|
if ($active !== null && $active !== '') {
|
||||||
|
return (string) $active;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string) (session()->get('role') ?? 'guest');
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getUserRoleFromDatabase($user_id)
|
protected function getUserRoleFromDatabase($user_id)
|
||||||
|
|||||||
@@ -4,6 +4,15 @@ namespace App\Models;
|
|||||||
|
|
||||||
use CodeIgniter\Model;
|
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
|
class ClassProgressReportModel extends Model
|
||||||
{
|
{
|
||||||
protected $table = 'class_progress_reports';
|
protected $table = 'class_progress_reports';
|
||||||
@@ -27,7 +36,42 @@ class ClassProgressReportModel extends Model
|
|||||||
'flags_json',
|
'flags_json',
|
||||||
'attachment_path',
|
'attachment_path',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $useTimestamps = true;
|
protected $useTimestamps = true;
|
||||||
protected $createdField = 'created_at';
|
protected $createdField = 'created_at';
|
||||||
protected $updatedField = 'updated_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.',
|
||||||
|
],
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ class TeacherClassModel extends Model
|
|||||||
$builder = $this->db->table('teacher_class tc')
|
$builder = $this->db->table('teacher_class tc')
|
||||||
->select([
|
->select([
|
||||||
'cs.class_section_name',
|
'cs.class_section_name',
|
||||||
'cs.id AS class_section_pk',
|
'cs.class_section_id AS class_section_pk',
|
||||||
'cs.class_section_id',
|
'cs.class_section_id',
|
||||||
'c.id AS class_id',
|
'c.id AS class_id',
|
||||||
'c.class_name',
|
'c.class_name',
|
||||||
@@ -181,10 +181,10 @@ class TeacherClassModel extends Model
|
|||||||
{
|
{
|
||||||
$db = \Config\Database::connect();
|
$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')
|
$row = $db->table('classSection cs')
|
||||||
->select('u.id AS teacher_id, u.firstname, u.lastname')
|
->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')
|
->join('users u', 'u.id = tc.teacher_id', 'inner')
|
||||||
->where('cs.class_section_name', $classSectionName)
|
->where('cs.class_section_name', $classSectionName)
|
||||||
->where('tc.school_year', $schoolYear)
|
->where('tc.school_year', $schoolYear)
|
||||||
|
|||||||
@@ -214,7 +214,17 @@
|
|||||||
<strong class="small mb-0"><?= esc($section['label'] ?? $subjectName) ?></strong>
|
<strong class="small mb-0"><?= esc($section['label'] ?? $subjectName) ?></strong>
|
||||||
<span class="badge <?= $badgeClass ?>"><?= esc($statusTag) ?></span>
|
<span class="badge <?= $badgeClass ?>"><?= esc($statusTag) ?></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="small text-muted"><?= $report ? esc($report['unit_title'] ?: '-') : 'No submission' ?></div>
|
<div class="small">
|
||||||
|
<?php if ($report): ?>
|
||||||
|
<?= view('admin/partials/class_progress_unit_display', [
|
||||||
|
'unitTitle' => (string) ($report['unit_title'] ?? ''),
|
||||||
|
'isQuran' => ($subjectName === 'Quran/Arabic'),
|
||||||
|
'compact' => true,
|
||||||
|
]) ?>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="text-muted">No submission</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -39,7 +39,6 @@
|
|||||||
<?php
|
<?php
|
||||||
$subjectName = $section['db_subject'] ?? $section['label'] ?? $slug;
|
$subjectName = $section['db_subject'] ?? $section['label'] ?? $slug;
|
||||||
$isQuran = $subjectName === 'Quran/Arabic';
|
$isQuran = $subjectName === 'Quran/Arabic';
|
||||||
$unitLabel = $isQuran ? 'Surah / Custom Arabic' : 'Unit / Chapter';
|
|
||||||
$homeworkLabel = $isQuran ? 'Arabic Practice / Homework' : 'Assigned Homework';
|
$homeworkLabel = $isQuran ? 'Arabic Practice / Homework' : 'Assigned Homework';
|
||||||
$report = $reportsBySubject[$subjectName] ?? null;
|
$report = $reportsBySubject[$subjectName] ?? null;
|
||||||
?>
|
?>
|
||||||
@@ -51,7 +50,13 @@
|
|||||||
<?php if (! $report): ?>
|
<?php if (! $report): ?>
|
||||||
<div class="text-muted">No entry submitted for this subject this week.</div>
|
<div class="text-muted">No entry submitted for this subject this week.</div>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<div class="mb-2"><strong><?= $unitLabel ?>:</strong> <?= esc($report['unit_title'] ?: '-') ?></div>
|
<div class="mb-2">
|
||||||
|
<?= view('admin/partials/class_progress_unit_display', [
|
||||||
|
'unitTitle' => (string) ($report['unit_title'] ?? ''),
|
||||||
|
'isQuran' => $isQuran,
|
||||||
|
'compact' => false,
|
||||||
|
]) ?>
|
||||||
|
</div>
|
||||||
<?php if (!empty($report['materials'])): ?>
|
<?php if (!empty($report['materials'])): ?>
|
||||||
<div class="mb-2"><strong>Materials:</strong> <?= esc($report['materials']) ?></div>
|
<div class="mb-2"><strong>Materials:</strong> <?= esc($report['materials']) ?></div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Renders class progress unit_title with explicit curriculum vs custom subject lines.
|
||||||
|
*
|
||||||
|
* @var string $unitTitle
|
||||||
|
* @var bool $isQuran
|
||||||
|
* @var bool $compact If true, tighter layout for the list accordion.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use App\Controllers\ClassProgressController;
|
||||||
|
|
||||||
|
$unitTitle = trim((string) ($unitTitle ?? ''));
|
||||||
|
$compact = ! empty($compact);
|
||||||
|
$isQuran = ! empty($isQuran);
|
||||||
|
|
||||||
|
$split = ClassProgressController::splitUnitTitleForDisplay($unitTitle);
|
||||||
|
$curriculumLines = $split['curriculum'];
|
||||||
|
$customTopics = $split['custom'];
|
||||||
|
|
||||||
|
$labelCurriculum = $isQuran ? 'Surah / curriculum' : 'Unit / chapter';
|
||||||
|
$labelCustom = $isQuran ? 'Custom Surah / Arabic' : 'Custom subject(s)';
|
||||||
|
?>
|
||||||
|
|
||||||
|
<?php if ($unitTitle === ''): ?>
|
||||||
|
<span class="text-muted">-</span>
|
||||||
|
<?php elseif ($compact): ?>
|
||||||
|
<?php if (! empty($customTopics)): ?>
|
||||||
|
<div class="small">
|
||||||
|
<span class="badge text-bg-info me-1"><?= $isQuran ? 'Custom' : 'Custom subject' ?></span><?= esc(implode(', ', $customTopics)) ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (! empty($curriculumLines)): ?>
|
||||||
|
<div class="small <?= ! empty($customTopics) ? 'text-muted mt-1' : 'text-muted' ?>"><?= esc(implode(' · ', $curriculumLines)) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (empty($customTopics) && empty($curriculumLines)): ?>
|
||||||
|
<div class="small text-muted"><?= esc($unitTitle) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php if (! empty($curriculumLines)): ?>
|
||||||
|
<div class="mb-2"><strong><?= esc($labelCurriculum) ?>:</strong> <?= esc(implode(' ; ', $curriculumLines)) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (! empty($customTopics)): ?>
|
||||||
|
<div class="mb-2"><strong><?= esc($labelCustom) ?>:</strong> <?= esc(implode(' ; ', $customTopics)) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (empty($curriculumLines) && empty($customTopics)): ?>
|
||||||
|
<div class="mb-2"><strong><?= esc($labelCurriculum) ?>:</strong> <?= esc($unitTitle) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
@@ -6,6 +6,9 @@ $drafts = $drafts ?? [];
|
|||||||
$legacyByClass = $legacyByClass ?? [];
|
$legacyByClass = $legacyByClass ?? [];
|
||||||
$classSections = $classSections ?? [];
|
$classSections = $classSections ?? [];
|
||||||
$examTypes = $examTypes ?? [];
|
$examTypes = $examTypes ?? [];
|
||||||
|
$visibleClasses = $visibleClasses ?? [];
|
||||||
|
$classDraftGroups = $classDraftGroups ?? [];
|
||||||
|
$newSubmissionClasses = $newSubmissionClasses ?? [];
|
||||||
$schoolYear = $schoolYear ?? '';
|
$schoolYear = $schoolYear ?? '';
|
||||||
$semester = $semester ?? '';
|
$semester = $semester ?? '';
|
||||||
$maxUploadBytes = $maxUploadBytes ?? (12 * 1024 * 1024);
|
$maxUploadBytes = $maxUploadBytes ?? (12 * 1024 * 1024);
|
||||||
@@ -56,172 +59,207 @@ $fileAccept = implode(',', array_map(static fn ($x) => '.' . $x, $allowedExtensi
|
|||||||
<div class="tab-pane fade show active" id="submissions" role="tabpanel" aria-labelledby="submissions-tab">
|
<div class="tab-pane fade show active" id="submissions" role="tabpanel" aria-labelledby="submissions-tab">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<?php if (empty($drafts)): ?>
|
<?php if (empty($visibleClasses)): ?>
|
||||||
<p class="text-muted mb-0">No exam drafts have been submitted yet.</p>
|
<p class="text-muted mb-0">No classes with enrolled students are available for this term.</p>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<div class="table-responsive">
|
<div class="accordion exam-drafts-accordion" id="examDraftsAccordion">
|
||||||
<table class="table table-striped table-bordered align-middle mb-0 exam-drafts-table no-mgmt-sticky">
|
<?php foreach ($visibleClasses as $classSectionId => $classInfo): ?>
|
||||||
<thead class="table-light">
|
<?php $classDraftsForSection = $classDraftGroups[$classSectionId] ?? []; ?>
|
||||||
<tr>
|
<?php $collapseId = 'classDraftGroup_' . $classSectionId; ?>
|
||||||
<th>Teacher</th>
|
<div class="accordion-item mb-3">
|
||||||
<th>Class</th>
|
<h2 class="accordion-header" id="heading_<?= esc($collapseId) ?>">
|
||||||
<th>Title & AuthorNote</th>
|
<button class="accordion-button collapsed px-3" type="button" data-bs-toggle="collapse" data-bs-target="#<?= esc($collapseId) ?>" aria-expanded="false" aria-controls="<?= esc($collapseId) ?>">
|
||||||
<th>Version</th>
|
<div class="d-flex flex-column flex-lg-row w-100 justify-content-between gap-2">
|
||||||
<th>Date-Time</th>
|
<div>
|
||||||
<th>Status</th>
|
<strong><?= esc($classInfo['class_section_name'] ?? ('Class ' . $classSectionId)) ?></strong>
|
||||||
<th>Files</th>
|
<div class="small text-muted">
|
||||||
<th>Reviewer Action</th>
|
<?= esc($classInfo['student_count'] ?? 0) ?> students · <?= esc(count($classDraftsForSection)) ?> submissions
|
||||||
<th>Final version</th>
|
<?php if (!empty($newSubmissionClasses[$classSectionId])): ?>
|
||||||
</tr>
|
<span class="badge bg-info text-dark ms-2">New submission</span>
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php foreach ($drafts as $draft): ?>
|
|
||||||
<?php
|
|
||||||
$teacherName = trim(($draft['teacher_first'] ?? '') . ' ' . ($draft['teacher_last'] ?? ''));
|
|
||||||
if ($teacherName === '') {
|
|
||||||
$teacherName = ($draft['admin_id'] ?? null) === ($draft['teacher_id'] ?? null)
|
|
||||||
? 'Admin upload'
|
|
||||||
: 'User #' . ($draft['teacher_id'] ?? 'N/A');
|
|
||||||
}
|
|
||||||
$st = strtolower((string) ($draft['status'] ?? ''));
|
|
||||||
$adminName = trim(($draft['admin_first'] ?? '') . ' ' . ($draft['admin_last'] ?? ''));
|
|
||||||
$authorNote = $draft['author_comment'] ?? $draft['description'] ?? '';
|
|
||||||
$reviewerNote = $draft['reviewer_comment'] ?? $draft['admin_comments'] ?? '';
|
|
||||||
?>
|
|
||||||
<tr>
|
|
||||||
<td><?= esc($teacherName) ?></td>
|
|
||||||
<td><?= esc($draft['class_section_name'] ?? 'Unknown') ?></td>
|
|
||||||
<td>
|
|
||||||
<strong><?= esc($draft['draft_title'] ?? 'Untitled') ?></strong>
|
|
||||||
<div class="small text-muted"><?= esc($draft['exam_type'] ?? 'N/A') ?></div>
|
|
||||||
<?php if ($authorNote !== ''): ?>
|
|
||||||
<div class="small mt-1">
|
|
||||||
<span class="text-muted fw-semibold">Author:</span>
|
|
||||||
<?= esc($authorNote) ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if ($reviewerNote !== ''): ?>
|
|
||||||
<div class="small mt-1 border-top pt-1">
|
|
||||||
<span class="text-muted fw-semibold">Reviewer:</span>
|
|
||||||
<?= esc($reviewerNote) ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</td>
|
|
||||||
<td class="text-nowrap">v<?= esc((string) ($draft['version'] ?? 1)) ?></td>
|
|
||||||
<td class="text-nowrap"><?= esc(!empty($draft['created_at']) ? local_datetime($draft['created_at'], 'M j, Y g:i A') : '—') ?></td>
|
|
||||||
<td>
|
|
||||||
<?php
|
|
||||||
$badgeData = $statusBadges[$st] ?? ['label' => $st, 'class' => 'bg-secondary text-white'];
|
|
||||||
$badgeStyle = !empty($badgeData['style']) ? ' style="' . esc($badgeData['style']) . '"' : '';
|
|
||||||
?>
|
|
||||||
<span class="badge <?= esc($badgeData['class']) ?> js-status-badge" data-status="<?= esc($st) ?>"<?= $badgeStyle ?>>
|
|
||||||
<?= esc($badgeData['label']) ?>
|
|
||||||
</span>
|
|
||||||
<div class="small text-muted mt-1 js-acceptance-note">
|
|
||||||
<?php if ($st === 'accepted' && !empty($draft['acceptance_type'])): ?>
|
|
||||||
<?= $draft['acceptance_type'] === 'minor_edits' ? 'Accepted w/ minor edits' : 'Accepted as is' ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
<?php if (!empty($draft['reviewed_at'])): ?>
|
|
||||||
<div class="small text-muted mt-1">
|
|
||||||
Reviewed <?= esc(local_datetime($draft['reviewed_at'], 'M j, Y g:i A')) ?>
|
|
||||||
<?php if ($adminName !== ''): ?>
|
|
||||||
by <?= esc($adminName) ?>
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<?php $formId = 'review_form_' . (int) ($draft['id'] ?? 0); ?>
|
|
||||||
<?php $revNumber = max(1, (int) ($draft['version'] ?? 1)); ?>
|
|
||||||
<?php if (!empty($draft['teacher_file'])): ?>
|
|
||||||
<div>
|
|
||||||
<a href="<?= base_url('exam-drafts/files/teacher/' . $draft['teacher_file']) ?>" target="_blank" rel="noopener">
|
|
||||||
<?= esc('Ver' . $revNumber . ' ' . ($draft['teacher_filename'] ?? 'Submitted draft')) ?>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<?php else: ?>
|
|
||||||
<span class="text-muted small">No teacher file</span>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if (!empty($draft['final_file']) && strtolower((string) ($draft['status'] ?? '')) === 'accepted'): ?>
|
|
||||||
<div class="mt-2">
|
|
||||||
<a href="<?= base_url('exam-drafts/files/final/' . $draft['final_file']) ?>" target="_blank" rel="noopener" class="link-success small">
|
|
||||||
<?= esc('Final ' . ($draft['final_filename'] ?? 'Final draft')) ?>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if (!empty($draft['review_files'])): ?>
|
|
||||||
<div class="mt-2">
|
|
||||||
<?php
|
|
||||||
$reviewLinks = [];
|
|
||||||
foreach ($draft['review_files'] as $rf) {
|
|
||||||
$rev = max(1, (int) ($rf['review_revision'] ?? 1));
|
|
||||||
$name = $rf['final_filename'] ?? 'Review file';
|
|
||||||
$file = $rf['final_file'] ?? '';
|
|
||||||
if ($file !== '') {
|
|
||||||
$reviewLinks[] = '<a href="' . esc(base_url('exam-drafts/files/final/' . $file)) . '" target="_blank" rel="noopener" class="link-success small">' . esc('Ver' . $revNumber . '_' . $rev . ' ' . $name) . '</a>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<?= !empty($reviewLinks) ? implode(' | ', $reviewLinks) : '' ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
<div class="mt-2">
|
|
||||||
<?php $fileInputId = 'review_file_' . (int) ($draft['id'] ?? 0); ?>
|
|
||||||
<input type="file" name="final_file" id="<?= esc($fileInputId) ?>" class="form-control form-control-sm" accept="<?= esc($fileAccept) ?>" form="<?= esc($formId) ?>">
|
|
||||||
<button type="submit" class="btn btn-sm btn-outline-primary mt-2" form="<?= esc($formId) ?>">Upload review file</button>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
<span class="badge bg-secondary align-self-start">
|
||||||
<td style="min-width: 280px;">
|
<?= esc(count($classDraftsForSection)) ?>
|
||||||
<?= form_open_multipart(base_url('administrator/exam-drafts/review'), ['class' => 'vstack gap-2', 'id' => $formId]) ?>
|
<?= count($classDraftsForSection) === 1 ? 'submission' : 'submissions' ?>
|
||||||
<?= csrf_field() ?>
|
</span>
|
||||||
<input type="hidden" name="draft_id" value="<?= (int) ($draft['id'] ?? 0) ?>">
|
</div>
|
||||||
<?php $selectedStatus = ''; ?>
|
</button>
|
||||||
<select name="review_status" class="form-select form-select-sm" required>
|
</h2>
|
||||||
<option value="" <?= $selectedStatus === '' ? 'selected' : '' ?> disabled>— Select status —</option>
|
<div id="<?= esc($collapseId) ?>" class="accordion-collapse collapse" aria-labelledby="heading_<?= esc($collapseId) ?>">
|
||||||
<option value="accepted" <?= $selectedStatus === 'accepted' ? 'selected' : '' ?>>Accepted</option>
|
<div class="accordion-body pt-0 px-3">
|
||||||
<option value="legacy" <?= $selectedStatus === 'legacy' ? 'selected' : '' ?>>Legacy</option>
|
<?php if (empty($classDraftsForSection)): ?>
|
||||||
<option value="canceled" <?= $selectedStatus === 'canceled' ? 'selected' : '' ?>>Canceled</option>
|
<p class="text-muted mb-0">No submissions yet for this class.</p>
|
||||||
<option value="rejected" <?= $selectedStatus === 'rejected' ? 'selected' : '' ?>>Rejected</option>
|
<?php else: ?>
|
||||||
<option value="review needed" <?= $selectedStatus === 'review needed' ? 'selected' : '' ?>>Review needed</option>
|
<div class="table-responsive">
|
||||||
<option value="under review" <?= $selectedStatus === 'under review' ? 'selected' : '' ?>>Under review</option>
|
<table class="table table-striped table-bordered align-middle mb-0 exam-drafts-table no-mgmt-sticky">
|
||||||
</select>
|
<thead class="table-light">
|
||||||
<div class="small js-acceptance-group d-none">
|
<tr>
|
||||||
<span class="d-block mb-1">As is / Minor edits</span>
|
<th>Teacher</th>
|
||||||
<?php $acc = (string) ($draft['acceptance_type'] ?? ''); ?>
|
<th>Class</th>
|
||||||
<div class="form-check form-check-inline">
|
<th>Title & AuthorNote</th>
|
||||||
<input class="form-check-input" type="radio" name="acceptance_type" value="as_is" id="as_is_<?= (int) ($draft['id'] ?? 0) ?>" <?= $acc !== 'minor_edits' ? 'checked' : '' ?>>
|
<th>Version</th>
|
||||||
<label class="form-check-label" for="as_is_<?= (int) ($draft['id'] ?? 0) ?>">As is</label>
|
<th>Date-Time</th>
|
||||||
</div>
|
<th>Status</th>
|
||||||
<div class="form-check form-check-inline">
|
<th>Files</th>
|
||||||
<input class="form-check-input" type="radio" name="acceptance_type" value="minor_edits" id="minor_<?= (int) ($draft['id'] ?? 0) ?>" <?= $acc === 'minor_edits' ? 'checked' : '' ?>>
|
<th>Reviewer Action</th>
|
||||||
<label class="form-check-label" for="minor_<?= (int) ($draft['id'] ?? 0) ?>">Minor edits</label>
|
<th>Final version</th>
|
||||||
</div>
|
</tr>
|
||||||
</div>
|
</thead>
|
||||||
<textarea name="reviewer_comment" class="form-control form-control-sm js-review-comment" rows="3" placeholder="Feedback for the teacher"><?= esc($draft['reviewer_comment'] ?? $draft['reviewer_comments'] ?? $draft['admin_comments'] ?? '') ?></textarea>
|
<tbody>
|
||||||
<div class="d-flex flex-wrap gap-2">
|
<?php foreach ($classDraftsForSection as $draft): ?>
|
||||||
<span class="text-muted small js-review-status"></span>
|
<?php
|
||||||
</div>
|
$teacherName = trim(($draft['teacher_first'] ?? '') . ' ' . ($draft['teacher_last'] ?? ''));
|
||||||
<?= form_close() ?>
|
if ($teacherName === '') {
|
||||||
</td>
|
$teacherName = ($draft['admin_id'] ?? null) === ($draft['teacher_id'] ?? null)
|
||||||
<td class="text-nowrap" style="min-width: 220px;">
|
? 'Admin upload'
|
||||||
<?php
|
: 'User #' . ($draft['teacher_id'] ?? 'N/A');
|
||||||
$pdfFile = $draft['final_pdf_file'] ?? null;
|
}
|
||||||
if (empty($pdfFile) && !empty($draft['final_file']) && strtolower(pathinfo($draft['final_file'], PATHINFO_EXTENSION)) === 'pdf') {
|
$st = strtolower((string) ($draft['status'] ?? ''));
|
||||||
$pdfFile = $draft['final_file'];
|
$adminName = trim(($draft['admin_first'] ?? '') . ' ' . ($draft['admin_last'] ?? ''));
|
||||||
}
|
$authorNote = $draft['author_comment'] ?? $draft['description'] ?? '';
|
||||||
?>
|
$reviewerNote = $draft['reviewer_comment'] ?? $draft['admin_comments'] ?? '';
|
||||||
<?php if (!empty($pdfFile)): ?>
|
?>
|
||||||
<div class="mt-2 d-flex flex-wrap gap-2">
|
<tr>
|
||||||
<a class="btn btn-sm btn-outline-primary" href="<?= base_url('exam-drafts/files/final/' . $pdfFile) ?>" target="_blank" rel="noopener">View</a>
|
<td><?= esc($teacherName) ?></td>
|
||||||
<a class="btn btn-sm btn-outline-secondary" href="<?= base_url('exam-drafts/files/final/' . $pdfFile) ?>" target="_blank" rel="noopener" download>Download</a>
|
<td><?= esc($draft['class_section_name'] ?? 'Unknown') ?></td>
|
||||||
</div>
|
<td>
|
||||||
<?php endif; ?>
|
<strong><?= esc($draft['draft_title'] ?? 'Untitled') ?></strong>
|
||||||
</td>
|
<div class="small text-muted"><?= esc($draft['exam_type'] ?? 'N/A') ?></div>
|
||||||
</tr>
|
<?php if ($authorNote !== ''): ?>
|
||||||
<?php endforeach; ?>
|
<div class="small mt-1">
|
||||||
</tbody>
|
<span class="text-muted fw-semibold">Author:</span>
|
||||||
</table>
|
<?= esc($authorNote) ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($reviewerNote !== ''): ?>
|
||||||
|
<div class="small mt-1 border-top pt-1">
|
||||||
|
<span class="text-muted fw-semibold">Reviewer:</span>
|
||||||
|
<?= esc($reviewerNote) ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td class="text-nowrap">v<?= esc((string) ($draft['version'] ?? 1)) ?></td>
|
||||||
|
<td class="text-nowrap"><?= esc(!empty($draft['created_at']) ? local_datetime($draft['created_at'], 'M j, Y g:i A') : '—') ?></td>
|
||||||
|
<td>
|
||||||
|
<?php
|
||||||
|
$badgeData = $statusBadges[$st] ?? ['label' => $st, 'class' => 'bg-secondary text-white'];
|
||||||
|
$badgeStyle = !empty($badgeData['style']) ? ' style="' . esc($badgeData['style']) . '"' : '';
|
||||||
|
?>
|
||||||
|
<span class="badge <?= esc($badgeData['class']) ?> js-status-badge" data-status="<?= esc($st) ?>"<?= $badgeStyle ?>>
|
||||||
|
<?= esc($badgeData['label']) ?>
|
||||||
|
</span>
|
||||||
|
<div class="small text-muted mt-1 js-acceptance-note">
|
||||||
|
<?php if ($st === 'accepted' && !empty($draft['acceptance_type'])): ?>
|
||||||
|
<?= $draft['acceptance_type'] === 'minor_edits' ? 'Accepted w/ minor edits' : 'Accepted as is' ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php if (!empty($draft['reviewed_at'])): ?>
|
||||||
|
<div class="small text-muted mt-1">
|
||||||
|
Reviewed <?= esc(local_datetime($draft['reviewed_at'], 'M j, Y g:i A')) ?>
|
||||||
|
<?php if ($adminName !== ''): ?>
|
||||||
|
by <?= esc($adminName) ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<?php $formId = 'review_form_' . (int) ($draft['id'] ?? 0); ?>
|
||||||
|
<?php $revNumber = max(1, (int) ($draft['version'] ?? 1)); ?>
|
||||||
|
<?php if (!empty($draft['teacher_file'])): ?>
|
||||||
|
<div>
|
||||||
|
<a href="<?= base_url('exam-drafts/files/teacher/' . $draft['teacher_file']) ?>" target="_blank" rel="noopener">
|
||||||
|
<?= esc('Ver' . $revNumber . ' ' . ($draft['teacher_filename'] ?? 'Submitted draft')) ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="text-muted small">No teacher file</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (!empty($draft['final_file']) && strtolower((string) ($draft['status'] ?? '')) === 'accepted'): ?>
|
||||||
|
<div class="mt-2">
|
||||||
|
<a href="<?= base_url('exam-drafts/files/final/' . $draft['final_file']) ?>" target="_blank" rel="noopener" class="link-success small">
|
||||||
|
<?= esc('Final ' . ($draft['final_filename'] ?? 'Final draft')) ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (!empty($draft['review_files'])): ?>
|
||||||
|
<div class="mt-2">
|
||||||
|
<?php
|
||||||
|
$reviewLinks = [];
|
||||||
|
foreach ($draft['review_files'] as $rf) {
|
||||||
|
$rev = max(1, (int) ($rf['review_revision'] ?? 1));
|
||||||
|
$name = $rf['final_filename'] ?? 'Review file';
|
||||||
|
$file = $rf['final_file'] ?? '';
|
||||||
|
if ($file !== '') {
|
||||||
|
$reviewLinks[] = '<a href="' . esc(base_url('exam-drafts/files/final/' . $file)) . '" target="_blank" rel="noopener" class="link-success small">' . esc('Ver' . $revNumber . '_' . $rev . ' ' . $name) . '</a>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<?= !empty($reviewLinks) ? implode(' | ', $reviewLinks) : '' ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<div class="mt-2">
|
||||||
|
<?php $fileInputId = 'review_file_' . (int) ($draft['id'] ?? 0); ?>
|
||||||
|
<input type="file" name="final_file" id="<?= esc($fileInputId) ?>" class="form-control form-control-sm" accept="<?= esc($fileAccept) ?>" form="<?= esc($formId) ?>">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-primary mt-2" form="<?= esc($formId) ?>">Upload review file</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td style="min-width: 280px;">
|
||||||
|
<?= form_open_multipart(base_url('administrator/exam-drafts/review'), ['class' => 'vstack gap-2', 'id' => $formId]) ?>
|
||||||
|
<?= csrf_field() ?>
|
||||||
|
<input type="hidden" name="draft_id" value="<?= (int) ($draft['id'] ?? 0) ?>">
|
||||||
|
<?php $selectedStatus = ''; ?>
|
||||||
|
<select name="review_status" class="form-select form-select-sm" required>
|
||||||
|
<option value="" <?= $selectedStatus === '' ? 'selected' : '' ?> disabled>— Select status —</option>
|
||||||
|
<option value="accepted" <?= $selectedStatus === 'accepted' ? 'selected' : '' ?>>Accepted</option>
|
||||||
|
<option value="legacy" <?= $selectedStatus === 'legacy' ? 'selected' : '' ?>>Legacy</option>
|
||||||
|
<option value="canceled" <?= $selectedStatus === 'canceled' ? 'selected' : '' ?>>Canceled</option>
|
||||||
|
<option value="rejected" <?= $selectedStatus === 'rejected' ? 'selected' : '' ?>>Rejected</option>
|
||||||
|
<option value="review needed" <?= $selectedStatus === 'review needed' ? 'selected' : '' ?>>Review needed</option>
|
||||||
|
<option value="under review" <?= $selectedStatus === 'under review' ? 'selected' : '' ?>>Under review</option>
|
||||||
|
</select>
|
||||||
|
<div class="small js-acceptance-group d-none">
|
||||||
|
<span class="d-block mb-1">As is / Minor edits</span>
|
||||||
|
<?php $acc = (string) ($draft['acceptance_type'] ?? ''); ?>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input class="form-check-input" type="radio" name="acceptance_type" value="as_is" id="as_is_<?= (int) ($draft['id'] ?? 0) ?>" <?= $acc !== 'minor_edits' ? 'checked' : '' ?>>
|
||||||
|
<label class="form-check-label" for="as_is_<?= (int) ($draft['id'] ?? 0) ?>">As is</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input class="form-check-input" type="radio" name="acceptance_type" value="minor_edits" id="minor_<?= (int) ($draft['id'] ?? 0) ?>" <?= $acc === 'minor_edits' ? 'checked' : '' ?>>
|
||||||
|
<label class="form-check-label" for="minor_<?= (int) ($draft['id'] ?? 0) ?>">Minor edits</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<textarea name="reviewer_comment" class="form-control form-control-sm js-review-comment" rows="3" placeholder="Feedback for the teacher"><?= esc($draft['reviewer_comment'] ?? $draft['reviewer_comments'] ?? $draft['admin_comments'] ?? '') ?></textarea>
|
||||||
|
<div class="d-flex flex-wrap gap-2">
|
||||||
|
<span class="text-muted small js-review-status"></span>
|
||||||
|
</div>
|
||||||
|
<?= form_close() ?>
|
||||||
|
</td>
|
||||||
|
<td class="text-nowrap" style="min-width: 220px;">
|
||||||
|
<?php
|
||||||
|
$pdfFile = $draft['final_pdf_file'] ?? null;
|
||||||
|
if (empty($pdfFile) && !empty($draft['final_file']) && strtolower(pathinfo($draft['final_file'], PATHINFO_EXTENSION)) === 'pdf') {
|
||||||
|
$pdfFile = $draft['final_file'];
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<?php if (!empty($pdfFile)): ?>
|
||||||
|
<div class="mt-2 d-flex flex-wrap gap-2">
|
||||||
|
<a class="btn btn-sm btn-outline-primary" href="<?= base_url('exam-drafts/files/final/' . $pdfFile) ?>" target="_blank" rel="noopener">View</a>
|
||||||
|
<a class="btn btn-sm btn-outline-secondary" href="<?= base_url('exam-drafts/files/final/' . $pdfFile) ?>" target="_blank" rel="noopener" download>Download</a>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
@@ -319,6 +357,12 @@ $fileAccept = implode(',', array_map(static fn ($x) => '.' . $x, $allowedExtensi
|
|||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const submissionsTab = document.getElementById('submissions-tab');
|
||||||
|
if (submissionsTab) {
|
||||||
|
submissionsTab.addEventListener('click', () => {
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
}
|
||||||
const statusBadges = <?= json_encode($statusBadges, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT) ?>;
|
const statusBadges = <?= json_encode($statusBadges, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT) ?>;
|
||||||
const csrfTokenName = <?= json_encode(csrf_token(), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT) ?>;
|
const csrfTokenName = <?= json_encode(csrf_token(), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT) ?>;
|
||||||
const csrfCookieNames = <?= json_encode(array_values(array_filter([
|
const csrfCookieNames = <?= json_encode(array_values(array_filter([
|
||||||
|
|||||||
@@ -121,6 +121,21 @@
|
|||||||
<input class="form-check-input" type="checkbox" name="notify_exam_draft" id="notifyExamDraft" value="1">
|
<input class="form-check-input" type="checkbox" name="notify_exam_draft" id="notifyExamDraft" value="1">
|
||||||
<label class="form-check-label small" for="notifyExamDraft">Include</label>
|
<label class="form-check-label small" for="notifyExamDraft">Include</label>
|
||||||
</div>
|
</div>
|
||||||
|
<?php
|
||||||
|
$examDraftDeadlineConfig = $examDraftDeadlineConfig ?? '';
|
||||||
|
$examDraftDeadlineFormatted = $examDraftDeadlineFormatted ?? '';
|
||||||
|
?>
|
||||||
|
<div class="small fw-normal text-muted mt-2 text-wrap">
|
||||||
|
<span class="text-uppercase" style="font-size:0.65rem;">exam_draft_deadline</span><br>
|
||||||
|
<?php if ($examDraftDeadlineConfig !== ''): ?>
|
||||||
|
<?= esc($examDraftDeadlineConfig) ?>
|
||||||
|
<?php if ($examDraftDeadlineFormatted !== ''): ?>
|
||||||
|
<span class="text-muted"> → <?= esc($examDraftDeadlineFormatted) ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="text-warning">Not set</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th class="text-center">
|
<th class="text-center">
|
||||||
Homework
|
Homework
|
||||||
|
|||||||
@@ -183,6 +183,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-text small text-muted">Add a custom Surah or Arabic target.</div>
|
<div class="form-text small text-muted">Add a custom Surah or Arabic target.</div>
|
||||||
</div>
|
</div>
|
||||||
|
<?php elseif ($slug === 'islamic'): ?>
|
||||||
|
<div class="px-2 py-2 border-top">
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<input type="text" class="form-control" placeholder="Type subject or topic" data-custom-input data-subject="<?= esc($slug) ?>">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" data-custom-entry data-subject="<?= esc($slug) ?>">Add</button>
|
||||||
|
</div>
|
||||||
|
<div class="form-text small text-muted">Add a subject or unit not listed above (e.g. Seerah, Fiqh, Akhlaq).</div>
|
||||||
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -385,6 +393,9 @@
|
|||||||
if (isQuran) {
|
if (isQuran) {
|
||||||
return isCustom ? 'Custom' : 'Surah';
|
return isCustom ? 'Custom' : 'Surah';
|
||||||
}
|
}
|
||||||
|
if (subject === 'islamic' && isCustom) {
|
||||||
|
return 'Custom';
|
||||||
|
}
|
||||||
return parts.join(' – ');
|
return parts.join(' – ');
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -445,7 +456,10 @@
|
|||||||
if (!input) return;
|
if (!input) return;
|
||||||
const customValue = input.value.trim();
|
const customValue = input.value.trim();
|
||||||
if (customValue === '') return;
|
if (customValue === '') return;
|
||||||
const unitValue = buildUnitDisplay(subject, '', '', { isQuran: subject === 'quran', isCustom: true });
|
const unitValue = buildUnitDisplay(subject, '', '', {
|
||||||
|
isQuran: subject === 'quran',
|
||||||
|
isCustom: subject === 'quran' || subject === 'islamic',
|
||||||
|
});
|
||||||
appendUnitChapterRow(subject, unitValue, customValue);
|
appendUnitChapterRow(subject, unitValue, customValue);
|
||||||
input.value = '';
|
input.value = '';
|
||||||
hideMenus();
|
hideMenus();
|
||||||
|
|||||||
Reference in New Issue
Block a user