exam draft fix

This commit is contained in:
root
2026-04-11 15:33:12 -04:00
parent 89e95b7851
commit 7bc999643a
13 changed files with 582 additions and 180 deletions
+1 -1
View File
@@ -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~%.:_\-,';
/**
* --------------------------------------------------------------------------
+58 -4
View File
@@ -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<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
{
$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;
@@ -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 = "<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 = '';
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
{
$semesterKey = strtolower(trim($semester));
+96 -1
View File
@@ -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;
}
}
}
+12 -3
View File
@@ -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)
+44
View File
@@ -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.',
],
];
}
+3 -3
View File
@@ -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)
+11 -1
View File
@@ -214,7 +214,17 @@
<strong class="small mb-0"><?= esc($section['label'] ?? $subjectName) ?></strong>
<span class="badge <?= $badgeClass ?>"><?= esc($statusTag) ?></span>
</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>
<?php endforeach; ?>
</div>
+7 -2
View File
@@ -39,7 +39,6 @@
<?php
$subjectName = $section['db_subject'] ?? $section['label'] ?? $slug;
$isQuran = $subjectName === 'Quran/Arabic';
$unitLabel = $isQuran ? 'Surah / Custom Arabic' : 'Unit / Chapter';
$homeworkLabel = $isQuran ? 'Arabic Practice / Homework' : 'Assigned Homework';
$report = $reportsBySubject[$subjectName] ?? null;
?>
@@ -51,7 +50,13 @@
<?php if (! $report): ?>
<div class="text-muted">No entry submitted for this subject this week.</div>
<?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'])): ?>
<div class="mb-2"><strong>Materials:</strong> <?= esc($report['materials']) ?></div>
<?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; ?>
+47 -3
View File
@@ -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,8 +59,37 @@ $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="card mb-4">
<div class="card-body">
<?php if (empty($drafts)): ?>
<p class="text-muted mb-0">No exam drafts have been submitted yet.</p>
<?php if (empty($visibleClasses)): ?>
<p class="text-muted mb-0">No classes with enrolled students are available for this term.</p>
<?php else: ?>
<div class="accordion exam-drafts-accordion" id="examDraftsAccordion">
<?php foreach ($visibleClasses as $classSectionId => $classInfo): ?>
<?php $classDraftsForSection = $classDraftGroups[$classSectionId] ?? []; ?>
<?php $collapseId = 'classDraftGroup_' . $classSectionId; ?>
<div class="accordion-item mb-3">
<h2 class="accordion-header" id="heading_<?= esc($collapseId) ?>">
<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) ?>">
<div class="d-flex flex-column flex-lg-row w-100 justify-content-between gap-2">
<div>
<strong><?= esc($classInfo['class_section_name'] ?? ('Class ' . $classSectionId)) ?></strong>
<div class="small text-muted">
<?= esc($classInfo['student_count'] ?? 0) ?> students · <?= esc(count($classDraftsForSection)) ?> submissions
<?php if (!empty($newSubmissionClasses[$classSectionId])): ?>
<span class="badge bg-info text-dark ms-2">New submission</span>
<?php endif; ?>
</div>
</div>
<span class="badge bg-secondary align-self-start">
<?= esc(count($classDraftsForSection)) ?>
<?= count($classDraftsForSection) === 1 ? 'submission' : 'submissions' ?>
</span>
</div>
</button>
</h2>
<div id="<?= esc($collapseId) ?>" class="accordion-collapse collapse" aria-labelledby="heading_<?= esc($collapseId) ?>">
<div class="accordion-body pt-0 px-3">
<?php if (empty($classDraftsForSection)): ?>
<p class="text-muted mb-0">No submissions yet for this class.</p>
<?php else: ?>
<div class="table-responsive">
<table class="table table-striped table-bordered align-middle mb-0 exam-drafts-table no-mgmt-sticky">
@@ -75,7 +107,7 @@ $fileAccept = implode(',', array_map(static fn ($x) => '.' . $x, $allowedExtensi
</tr>
</thead>
<tbody>
<?php foreach ($drafts as $draft): ?>
<?php foreach ($classDraftsForSection as $draft): ?>
<?php
$teacherName = trim(($draft['teacher_first'] ?? '') . ' ' . ($draft['teacher_last'] ?? ''));
if ($teacherName === '') {
@@ -227,6 +259,12 @@ $fileAccept = implode(',', array_map(static fn ($x) => '.' . $x, $allowedExtensi
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
</div>
<div class="tab-pane fade" id="legacy" role="tabpanel" aria-labelledby="legacy-tab">
<div class="card border-primary-subtle mb-3">
@@ -319,6 +357,12 @@ $fileAccept = implode(',', array_map(static fn ($x) => '.' . $x, $allowedExtensi
</div>
<script>
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 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([
@@ -121,6 +121,21 @@
<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>
</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 class="text-center">
Homework
+15 -1
View File
@@ -183,6 +183,14 @@
</div>
<div class="form-text small text-muted">Add a custom Surah or Arabic target.</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; ?>
</div>
</div>
@@ -385,6 +393,9 @@
if (isQuran) {
return isCustom ? 'Custom' : 'Surah';
}
if (subject === 'islamic' && isCustom) {
return 'Custom';
}
return parts.join(' ');
};
@@ -445,7 +456,10 @@
if (!input) return;
const customValue = input.value.trim();
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);
input.value = '';
hideMenus();