recreate project
This commit is contained in:
@@ -0,0 +1,727 @@
|
||||
<?= $this->extend('layout/main_layout') ?>
|
||||
<?= $this->section('content') ?>
|
||||
<?php helper('form'); ?>
|
||||
<div class="w-100 py-3 px-3">
|
||||
<?php if (session()->get('status')): ?>
|
||||
<div class="alert alert-success text-center">
|
||||
<?= session()->get('status') ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (session()->get('error')): ?>
|
||||
<div class="alert alert-danger text-center">
|
||||
<?= esc(session()->get('error')) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($errors = session()->get('errors')): ?>
|
||||
<div class="alert alert-danger">
|
||||
<p class="mb-2 text-center fw-semibold">
|
||||
Errors were found in the PTAP or Midterm/Final comments. Your entries are kept below so you can fix the specific fields and resubmit.
|
||||
</p>
|
||||
<ul class="mb-0">
|
||||
<?php foreach ($errors as $error): ?>
|
||||
<li><?= esc($error) ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<p class="small text-center text-muted mt-3 mb-0">
|
||||
PTAP, Midterm, and Final comments must be 100–350 characters and begin with the student’s first name for validation to pass.
|
||||
</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php $semester = strtolower($activeSemester ?? ''); ?>
|
||||
<?php $focusTarget = $focusTarget ?? null; ?>
|
||||
<?php
|
||||
$classSectionId = (int)($class_section_id ?? 0);
|
||||
$scoresLocked = !empty($scoresLocked);
|
||||
$lockAttr = $scoresLocked ? 'disabled' : '';
|
||||
$displayScore = static function ($value) {
|
||||
return ($value === null || $value === '') ? '' : esc($value);
|
||||
};
|
||||
$scoreClass = static function ($value) {
|
||||
return ($value === null || $value === '') ? 'score-empty' : '';
|
||||
};
|
||||
$trimValue = static function ($value) {
|
||||
return trim((string)($value ?? ''));
|
||||
};
|
||||
$commentClass = static function ($value) use ($trimValue) {
|
||||
return $trimValue($value) === '' ? 'score-empty' : '';
|
||||
};
|
||||
$displayComment = static function ($value) use ($trimValue) {
|
||||
$val = $trimValue($value);
|
||||
return $val === '' ? '' : esc($val);
|
||||
};
|
||||
$preserveCommentClass = static function ($studentId, $scoreType, $existing) use ($commentClass) {
|
||||
$old = old("comments[{$studentId}][{$scoreType}]");
|
||||
return $commentClass($old !== null ? $old : $existing);
|
||||
};
|
||||
$preserveCommentValue = static function ($studentId, $scoreType, $existing) use ($displayComment) {
|
||||
$old = old("comments[{$studentId}][{$scoreType}]");
|
||||
return $displayComment($old !== null ? $old : $existing);
|
||||
};
|
||||
$rawCommentValue = static function ($studentId, $scoreType, $existing) use ($trimValue) {
|
||||
$old = old("comments[{$studentId}][{$scoreType}]");
|
||||
$val = $old !== null ? $old : $existing;
|
||||
return $trimValue($val);
|
||||
};
|
||||
$extractScore = static function ($value) {
|
||||
if (is_array($value)) {
|
||||
return $value[0] ?? '';
|
||||
}
|
||||
return $value;
|
||||
};
|
||||
$scoreEmpty = static function ($value) {
|
||||
return $value === null || $value === '';
|
||||
};
|
||||
?>
|
||||
<form id="gradesForm" action="<?= base_url('/teacher/saveComments') ?>" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="selected_semester" value="<?= esc($selectedSemester ?? ($activeSemester ?? '')) ?>">
|
||||
<input type="hidden" name="school_year" value="<?= esc($schoolYear ?? '') ?>">
|
||||
<div class="text-center mb-3 w-100">
|
||||
<h4 class="text-dark mb-1" style="font-family: Arial, sans-serif;">Students Class Scores</h4>
|
||||
<?php if (!empty($mainsText ?? '')): ?>
|
||||
<h5 class="mb-0" style="font-family: Arial, sans-serif;">Teachers: <?= esc($mainsText) ?></h5>
|
||||
<?php else: ?>
|
||||
<h5 class="mb-0" style="font-family: Arial, sans-serif;">Teacher: <?= esc($teacher_name); ?></h5>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($tasText ?? '')): ?>
|
||||
<h6 class="mb-0 mt-1" style="font-family: Arial, sans-serif;">Teacher Assistants: <?= esc($tasText) ?></h6>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($activeSemester ?? '')): ?>
|
||||
<p class="text-muted mb-0" style="font-size: 0.9rem;">Current semester: <?= esc(ucfirst($activeSemester)) ?></p>
|
||||
<?php endif; ?>
|
||||
<?php if ($scoresLocked): ?>
|
||||
<div class="alert alert-warning mt-3 mb-0">
|
||||
Scores are submitted and locked for this semester.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="mt-3">
|
||||
<a href="<?= base_url('/teacher/scores?choose_semester=1') ?>" class="btn btn-outline-secondary btn-sm">Choose another semester</a>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="class_section_id" value="<?= esc($class_section_id ?? 0) ?>">
|
||||
<div class="table-responsive">
|
||||
<table id="myTable" class="table table-bordered table-striped w-100 scores-table">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>First Name</th>
|
||||
<th>Last Name</th>
|
||||
<th class="score-col">Attendance</th>
|
||||
<th class="score-col">Homework Avg</th>
|
||||
<th class="score-col">Project Avg</th>
|
||||
<th class="score-col">Test/Quiz Avg</th>
|
||||
<th class="score-col">Participation</th>
|
||||
<th class="score-col">PTAP</th>
|
||||
<th class="comment-col">PTAP Comment</th>
|
||||
<th class="comment-col">PTAP Comment Review</th>
|
||||
<?php if ($semester === 'fall'): ?>
|
||||
<th class="score-col">Midterm</th>
|
||||
<th class="comment-col">Midterm Comment</th>
|
||||
<th class="comment-col">Midterm Comment Review</th>
|
||||
<?php endif; ?>
|
||||
<?php if ($semester === 'spring'): ?>
|
||||
<th class="score-col">Final Exam</th>
|
||||
<th class="comment-col">Final Comment</th>
|
||||
<th class="comment-col">Final Comment Review</th>
|
||||
<?php endif; ?>
|
||||
<th class="score-col">Semester Score</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<?php foreach ($students as $student): ?>
|
||||
<?php
|
||||
$studentId = $student['student_id'];
|
||||
$studentName = trim(($student['firstname'] ?? '') . ' ' . ($student['lastname'] ?? ''));
|
||||
$studentName = $studentName !== '' ? $studentName : ('Student #' . $studentId);
|
||||
$missingOk = $student['missing_ok'] ?? [];
|
||||
|
||||
$homeworkVal = $extractScore($student['average_homework'] ?? null);
|
||||
$projectVal = $extractScore($student['average_project'] ?? null);
|
||||
$quizVal = $extractScore($student['average_quiz'] ?? null);
|
||||
$participationVal = $extractScore($student['average_participation'] ?? null);
|
||||
$midtermVal = $semester === 'fall' ? $extractScore($student['midterm'][0] ?? null) : null;
|
||||
$finalVal = $semester === 'spring' ? $extractScore($student['final_exam'][0] ?? null) : null;
|
||||
|
||||
$ptapCommentRaw = $rawCommentValue($studentId, 'ptap', $student['ptap_comment'] ?? '');
|
||||
$midtermCommentRaw = $semester === 'fall'
|
||||
? $rawCommentValue($studentId, 'midterm', $student['midterm_comment'] ?? '')
|
||||
: '';
|
||||
$finalCommentRaw = $semester === 'spring'
|
||||
? $rawCommentValue($studentId, 'final', $student['final_comment'] ?? '')
|
||||
: '';
|
||||
?>
|
||||
<tr data-student-name="<?= esc($studentName) ?>" data-missing-details="<?= esc(json_encode(array_values($student['missing_items'] ?? [])), 'attr') ?>">
|
||||
<td><?= esc($student['firstname']) ?></td>
|
||||
<td><?= esc($student['lastname']) ?></td>
|
||||
<td class="text-center score-col <?= $scoreClass($student['attendance_score'] ?? null) ?>"><?= $displayScore($student['attendance_score'] ?? null) ?></td>
|
||||
<td class="text-center score-col <?= $scoreClass($homeworkVal) ?>" data-score-check="1" data-field-label="Homework Avg" data-field-value="<?= esc($homeworkVal) ?>">
|
||||
<?= $displayScore($homeworkVal) ?>
|
||||
</td>
|
||||
<td class="text-center score-col <?= $scoreClass($projectVal) ?>" data-score-check="1" data-field-label="Project Avg" data-field-value="<?= esc($projectVal) ?>">
|
||||
<?= $displayScore($projectVal) ?>
|
||||
</td>
|
||||
<td class="text-center score-col <?= $scoreClass($quizVal) ?>" data-score-check="1" data-field-label="Test/Quiz Avg" data-field-value="<?= esc($quizVal) ?>">
|
||||
<?= $displayScore($quizVal) ?>
|
||||
</td>
|
||||
<td class="text-center score-col <?= $scoreClass($participationVal) ?>" data-score-check="1" data-field-label="Participation" data-field-value="<?= esc($participationVal) ?>">
|
||||
<?= $displayScore($participationVal) ?>
|
||||
</td>
|
||||
<td class="text-center score-col <?= $scoreClass($student['ptap_score'] ?? null) ?>"><?= $displayScore($student['ptap_score'] ?? null) ?></td>
|
||||
<td class="ptap-comment-cell comment-col">
|
||||
<input type="hidden" name="student_ids[]" value="<?= $studentId ?>">
|
||||
<div class="comment-check" data-field-label="PTAP Comment">
|
||||
<textarea
|
||||
name="comments[<?= $studentId ?>][ptap]"
|
||||
class="form-control auto-expand comment-textarea <?= $preserveCommentClass($studentId, 'ptap', $student['ptap_comment'] ?? '') ?>"
|
||||
data-first-name="<?= esc($student['firstname']) ?>"
|
||||
minlength="100"
|
||||
maxlength="350"
|
||||
placeholder="PTAP Comment"
|
||||
rows="1"
|
||||
style="min-width: 100px; overflow:hidden; resize: none;"
|
||||
spellcheck="true" <?= $lockAttr ?>><?= $preserveCommentValue($studentId, 'ptap', $student['ptap_comment'] ?? '') ?></textarea>
|
||||
<?php $ptapMissingOk = !empty($missingOk['ptap_comment'][0]); ?>
|
||||
<label class="missing-check mt-1 <?= $ptapCommentRaw === '' ? '' : 'd-none' ?>">
|
||||
<input type="checkbox"
|
||||
name="missing_ok[<?= $studentId ?>][ptap_comment]"
|
||||
value="1"
|
||||
class="missing-comment-checkbox"
|
||||
data-field-label="PTAP Comment" <?= $lockAttr ?> <?= $ptapMissingOk ? 'checked' : '' ?>>
|
||||
Missing ok
|
||||
</label>
|
||||
</div>
|
||||
<div class="char-count-wrapper" style="font-size: 0.75rem; text-align: right; color: #6c757d;">
|
||||
<span class="char-count">0</span> / 350
|
||||
</div>
|
||||
<div class="comment-error text-danger small" role="alert" aria-live="polite"></div>
|
||||
</td>
|
||||
|
||||
<td class="comment-review ptap-comment-review-cell comment-col">
|
||||
<span class="<?= $commentClass($student['ptap_comment_review'] ?? '') ?>">
|
||||
<?= $displayComment($student['ptap_comment_review'] ?? '') ?>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<?php if ($semester === 'fall'): ?>
|
||||
<td class="text-center score-col <?= $scoreClass($midtermVal) ?>" data-score-check="1" data-field-label="Midterm" data-field-value="<?= esc($midtermVal) ?>">
|
||||
<?= $displayScore($midtermVal) ?>
|
||||
</td>
|
||||
<td class="midterm-comment-cell comment-col">
|
||||
<div class="comment-check" data-field-label="Midterm Comment">
|
||||
<textarea
|
||||
name="comments[<?= $studentId ?>][midterm]"
|
||||
class="form-control auto-expand comment-textarea <?= $preserveCommentClass($studentId, 'midterm', $student['midterm_comment'] ?? '') ?>"
|
||||
data-first-name="<?= esc($student['firstname']) ?>"
|
||||
minlength="100"
|
||||
maxlength="350"
|
||||
placeholder="Midterm Comment"
|
||||
rows="1"
|
||||
style="min-width: 100px; overflow:hidden; resize: none;"
|
||||
spellcheck="true"
|
||||
autocorrect="on"
|
||||
autocapitalize="sentences"
|
||||
lang="en" <?= $lockAttr ?>><?= $preserveCommentValue($studentId, 'midterm', $student['midterm_comment'] ?? '') ?></textarea>
|
||||
<?php $midtermMissingOk = !empty($missingOk['midterm_comment'][0]); ?>
|
||||
<label class="missing-check mt-1 <?= $midtermCommentRaw === '' ? '' : 'd-none' ?>">
|
||||
<input type="checkbox"
|
||||
name="missing_ok[<?= $studentId ?>][midterm_comment]"
|
||||
value="1"
|
||||
class="missing-comment-checkbox"
|
||||
data-field-label="Midterm Comment" <?= $lockAttr ?> <?= $midtermMissingOk ? 'checked' : '' ?>>
|
||||
Missing ok
|
||||
</label>
|
||||
</div>
|
||||
<div class="char-count-wrapper" style="font-size: 0.75rem; text-align: right; color: #6c757d;">
|
||||
<span class="char-count">0</span> / 350
|
||||
</div>
|
||||
<div class="comment-error text-danger small" role="alert" aria-live="polite"></div>
|
||||
</td>
|
||||
<td class="comment-review midterm-comment-review-cell comment-col">
|
||||
<span class="<?= $commentClass($student['midterm_comment_review'] ?? '') ?>">
|
||||
<?= $displayComment($student['midterm_comment_review'] ?? '') ?>
|
||||
</span>
|
||||
</td>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($semester === 'spring'): ?>
|
||||
<td class="text-center score-col <?= $scoreClass($finalVal) ?>" data-score-check="1" data-field-label="Final Exam" data-field-value="<?= esc($finalVal) ?>">
|
||||
<?= $displayScore($finalVal) ?>
|
||||
</td>
|
||||
<td class="comment-col">
|
||||
<div class="comment-check" data-field-label="Final Comment">
|
||||
<textarea
|
||||
name="comments[<?= $studentId ?>][final]"
|
||||
class="form-control auto-expand comment-textarea <?= $preserveCommentClass($studentId, 'final', $student['final_comment'] ?? '') ?>"
|
||||
data-first-name="<?= esc($student['firstname']) ?>"
|
||||
minlength="100"
|
||||
maxlength="350"
|
||||
placeholder="Final Comment"
|
||||
rows="1"
|
||||
style="min-width: 100px; overflow:hidden; resize: none;"
|
||||
spellcheck="true"
|
||||
autocorrect="on"
|
||||
autocapitalize="sentences"
|
||||
lang="en" <?= $lockAttr ?>><?= $preserveCommentValue($studentId, 'final', $student['final_comment'] ?? '') ?></textarea>
|
||||
<?php $finalMissingOk = !empty($missingOk['final_comment'][0]); ?>
|
||||
<label class="missing-check mt-1 <?= $finalCommentRaw === '' ? '' : 'd-none' ?>">
|
||||
<input type="checkbox"
|
||||
name="missing_ok[<?= $studentId ?>][final_comment]"
|
||||
value="1"
|
||||
class="missing-comment-checkbox"
|
||||
data-field-label="Final Comment" <?= $lockAttr ?> <?= $finalMissingOk ? 'checked' : '' ?>>
|
||||
Missing ok
|
||||
</label>
|
||||
</div>
|
||||
<div class="char-count-wrapper" style="font-size: 0.75rem; text-align: right; color: #6c757d; margin-top: 2px;">
|
||||
<span class="char-count">0</span> / 350
|
||||
</div>
|
||||
<div class="comment-error text-danger small" role="alert" aria-live="polite"></div>
|
||||
</td>
|
||||
<td class="comment-review comment-col">
|
||||
<span class="<?= $commentClass($student['final_comment_review'] ?? '') ?>">
|
||||
<?= $displayComment($student['final_comment_review'] ?? '') ?>
|
||||
</span>
|
||||
</td>
|
||||
<?php endif; ?>
|
||||
<td class="text-center score-col <?= $scoreClass($student['semester_score'] ?? null) ?>"><?= $displayScore($student['semester_score'] ?? null) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="scores-action-bar d-flex flex-wrap gap-2 mt-3">
|
||||
<a href="<?= base_url('/teacher/addHomework?class_section_id=' . $classSectionId) ?>" class="btn btn-secondary">Add Homework</a>
|
||||
<a href="<?= base_url('/teacher/addQuiz?class_section_id=' . $classSectionId) ?>" class="btn btn-secondary">Add Quiz</a>
|
||||
<a href="<?= base_url('/teacher/addProject?class_section_id=' . $classSectionId) ?>" class="btn btn-secondary">Add Project</a>
|
||||
<a href="<?= base_url('/teacher/addParticipation?class_section_id=' . $classSectionId) ?>" class="btn btn-secondary">Add Participation</a>
|
||||
<?php if ($semester === 'fall'): ?>
|
||||
<a href="<?= base_url('/teacher/addMidtermExam?class_section_id=' . $classSectionId) ?>" class="btn btn-secondary">Add Midterm</a>
|
||||
<?php elseif ($semester === 'spring'): ?>
|
||||
<a href="<?= base_url('/teacher/addFinalExam?class_section_id=' . $classSectionId) ?>" class="btn btn-secondary">Add Final</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<button type="submit" class="btn btn-info" <?= $lockAttr ?>>Save Comments</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-outline-danger"
|
||||
id="submitScoresLockBtn"
|
||||
formaction="<?= base_url('/teacher/submit-scores-lock') ?>"
|
||||
formmethod="post"
|
||||
<?= $scoresLocked ? 'disabled' : '' ?>>
|
||||
<?= $scoresLocked ? 'Scores Locked' : 'Submit Semester Scores' ?>
|
||||
</button>
|
||||
</div>
|
||||
<div id="missingWarning" class="alert alert-danger mt-3 d-none" role="alert" aria-live="polite">
|
||||
<div class="fw-semibold mb-2">Please fix the missing items before submitting:</div>
|
||||
<pre class="missing-warning-body mb-0"></pre>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('scripts') ?>
|
||||
<script src="<?= base_url('assets/js/validateScores.js') ?>"></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('input', function(event) {
|
||||
if (event.target.classList.contains('auto-expand')) {
|
||||
event.target.style.height = 'auto';
|
||||
event.target.style.height = Math.max(event.target.scrollHeight, 26) + 'px';
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.querySelectorAll('.auto-expand').forEach(function(textarea) {
|
||||
textarea.style.height = 'auto';
|
||||
textarea.style.height = Math.max(textarea.scrollHeight, 26) + 'px';
|
||||
});
|
||||
|
||||
const form = document.getElementById('gradesForm');
|
||||
if (form) {
|
||||
const csrfInput = form.querySelector('input[type="hidden"][name*="csrf"]');
|
||||
const csrfCookieName = '<?= esc(config('Security')->csrfCookieName ?? (config('Security')->cookieName ?? 'csrf_cookie_name')) ?>';
|
||||
|
||||
const getCookie = function(name) {
|
||||
try {
|
||||
return document.cookie
|
||||
.split(';')
|
||||
.map(function(value) { return value.trim(); })
|
||||
.find(function(value) { return value.indexOf(name + '=') === 0; })
|
||||
?.split('=')[1] || '';
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const syncCsrfToken = function() {
|
||||
if (!csrfInput) return;
|
||||
const fresh = decodeURIComponent(getCookie(csrfCookieName) || '');
|
||||
if (fresh) {
|
||||
csrfInput.value = fresh;
|
||||
}
|
||||
};
|
||||
|
||||
const escapeRegex = function(text) {
|
||||
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
};
|
||||
const getErrorElement = function(field) {
|
||||
return field && field.parentElement ? field.parentElement.querySelector('.comment-error') : null;
|
||||
};
|
||||
const validateStartsWithName = function(field) {
|
||||
const errorEl = getErrorElement(field);
|
||||
if (!errorEl) return true;
|
||||
|
||||
const value = field.value.trim();
|
||||
const firstName = field.dataset.firstName || '';
|
||||
if (!value || !firstName) {
|
||||
errorEl.textContent = '';
|
||||
return true;
|
||||
}
|
||||
|
||||
const startsWithName = new RegExp(`^${escapeRegex(firstName)}\\b`, 'i');
|
||||
if (!startsWithName.test(value)) {
|
||||
errorEl.textContent = `Comment must start with ${firstName}'s first name.`;
|
||||
return false;
|
||||
}
|
||||
|
||||
errorEl.textContent = '';
|
||||
return true;
|
||||
};
|
||||
const shouldAutoSave = function(field) {
|
||||
const value = field.value.trim();
|
||||
if (!value) return false;
|
||||
if (!validateStartsWithName(field)) return false;
|
||||
if (value.length < 100 || value.length > 350) return false;
|
||||
const lastSaved = field.getAttribute('data-last-saved') || '';
|
||||
return value !== lastSaved;
|
||||
};
|
||||
const buildAutoSavePayload = function(field) {
|
||||
const data = new FormData();
|
||||
if (csrfInput) {
|
||||
const csrfValue = decodeURIComponent(getCookie(csrfCookieName) || '') || csrfInput.value;
|
||||
data.append(csrfInput.name, csrfValue);
|
||||
}
|
||||
['selected_semester', 'school_year', 'class_section_id'].forEach(function(name) {
|
||||
const input = form.querySelector('[name="' + name + '"]');
|
||||
if (input) {
|
||||
data.append(name, input.value);
|
||||
}
|
||||
});
|
||||
data.append(field.name, field.value);
|
||||
return data;
|
||||
};
|
||||
const autoSaveField = function(field) {
|
||||
if (!shouldAutoSave(field)) return;
|
||||
const payload = buildAutoSavePayload(field);
|
||||
fetch(form.action, {
|
||||
method: 'POST',
|
||||
body: payload,
|
||||
credentials: 'same-origin'
|
||||
})
|
||||
.then(function() {
|
||||
field.setAttribute('data-last-saved', field.value.trim());
|
||||
syncCsrfToken();
|
||||
})
|
||||
.catch(function() {});
|
||||
};
|
||||
|
||||
form.addEventListener('submit', function(event) {
|
||||
syncCsrfToken();
|
||||
const errors = [];
|
||||
form.querySelectorAll('textarea[data-first-name]').forEach(function(field) {
|
||||
const value = field.value.trim();
|
||||
if (value === '') return;
|
||||
|
||||
const min = 100;
|
||||
const max = 350;
|
||||
const firstName = field.dataset.firstName || '';
|
||||
|
||||
if (value.length < min || value.length > max) {
|
||||
errors.push(`Comment for ${firstName || 'student'} must be between ${min} and ${max} characters.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (firstName) {
|
||||
const startsWithName = new RegExp(`^${escapeRegex(firstName)}\\b`, 'i');
|
||||
if (!startsWithName.test(value)) {
|
||||
errors.push(`Comment for ${firstName} must start with the student's first name.`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (errors.length > 0) {
|
||||
event.preventDefault();
|
||||
alert(errors.join('\\n'));
|
||||
}
|
||||
});
|
||||
|
||||
form.querySelectorAll('textarea.comment-textarea').forEach(function(field) {
|
||||
field.setAttribute('data-last-saved', field.value.trim());
|
||||
field.addEventListener('input', function() {
|
||||
validateStartsWithName(this);
|
||||
});
|
||||
field.addEventListener('blur', function() {
|
||||
validateStartsWithName(this);
|
||||
autoSaveField(this);
|
||||
});
|
||||
});
|
||||
}
|
||||
<?php if (!empty($focusTarget)): ?>
|
||||
(function scrollForFocus() {
|
||||
const focusKey = '<?= esc($focusTarget) ?>';
|
||||
const focusElement = focusKey === 'comments'
|
||||
? document.querySelector('.comment-textarea')
|
||||
: document.getElementById('myTable');
|
||||
if (!focusElement) return;
|
||||
focusElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
if (focusKey === 'comments') {
|
||||
focusElement.focus();
|
||||
}
|
||||
})();
|
||||
<?php endif; ?>
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.comment-review {
|
||||
background-color: #f8f9fa;
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
min-width: 120px;
|
||||
max-width: 200px;
|
||||
white-space: pre-wrap;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
/* Compact table cells */
|
||||
.scores-table td,
|
||||
.scores-table th {
|
||||
padding: 0.2rem 0.35rem;
|
||||
vertical-align: top !important;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.score-col {
|
||||
width: 80px !important;
|
||||
/* Fixed width for score columns */
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.comment-col {
|
||||
min-width: 200px !important;
|
||||
/* Minimum width for comment columns */
|
||||
}
|
||||
|
||||
/* Ensure room above footer on this page and neutralize global 100vh flex */
|
||||
main.container-fluid {
|
||||
display: block !important;
|
||||
min-height: auto !important;
|
||||
/* override global min-height: 100vh */
|
||||
flex: 1 0 auto !important;
|
||||
/* fill remaining height so footer sits at bottom */
|
||||
padding-bottom: 80px;
|
||||
/* room so sticky bar doesn't cover footer */
|
||||
}
|
||||
|
||||
/* Keep action buttons visible and above footer */
|
||||
.scores-action-bar {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
background: #fff;
|
||||
padding: .75rem .5rem;
|
||||
border-top: 1px solid rgba(0, 0, 0, .1);
|
||||
z-index: 10;
|
||||
/* above table, below modals/nav */
|
||||
}
|
||||
|
||||
.score-empty {
|
||||
background-color: #fff3cd !important;
|
||||
box-shadow: inset 0 0 0 1px #ffe8a1;
|
||||
}
|
||||
|
||||
.comment-review .score-empty {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
.missing-check {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 0.75rem;
|
||||
color: #6c757d;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Compact textareas and comment cells; grow only when content expands */
|
||||
.scores-table textarea.form-control.auto-expand {
|
||||
min-height: 26px;
|
||||
height: 26px;
|
||||
line-height: 1.25;
|
||||
padding: 4px 6px;
|
||||
resize: none;
|
||||
margin: 0;
|
||||
display: block;
|
||||
width: 100%;
|
||||
vertical-align: top !important;
|
||||
}
|
||||
|
||||
.ptap-comment-cell,
|
||||
.ptap-comment-review-cell {
|
||||
vertical-align: top !important;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.ptap-comment-cell textarea {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
vertical-align: top !important;
|
||||
}
|
||||
|
||||
.ptap-comment-review-cell span {
|
||||
display: block;
|
||||
vertical-align: top !important;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.midterm-comment-cell,
|
||||
.midterm-comment-review-cell {
|
||||
vertical-align: top !important;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.midterm-comment-cell textarea {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
vertical-align: top !important;
|
||||
}
|
||||
|
||||
.midterm-comment-review-cell span {
|
||||
display: block;
|
||||
vertical-align: top !important;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.scores-table {
|
||||
table-layout: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#missingWarning {
|
||||
font-size: 1rem;
|
||||
}
|
||||
#missingWarning .missing-warning-body {
|
||||
font-size: 1rem;
|
||||
line-height: 1.4;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.10/css/jquery.dataTables.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.10/css/dataTables.bootstrap5.min.css">
|
||||
<script src="https://cdn.datatables.net/1.13.10/js/jquery.dataTables.min.js"></script>
|
||||
<script src="https://cdn.datatables.net/1.13.10/js/dataTables.bootstrap5.min.js"></script>
|
||||
<script>
|
||||
(function ($) {
|
||||
// Order plug-in: treat cell text as number, empty => -Infinity so blanks drop to bottom on desc
|
||||
if ($ && $.fn && $.fn.dataTable && !$.fn.dataTable.ext.order['dom-text-num']) {
|
||||
$.fn.dataTable.ext.order['dom-text-num'] = function (settings, col) {
|
||||
return this.api()
|
||||
.column(col, { order: 'index' })
|
||||
.nodes()
|
||||
.map(function (td) {
|
||||
const raw = (td.textContent || '').trim();
|
||||
const num = parseFloat(raw);
|
||||
return Number.isFinite(num) ? num : -Infinity;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
if (!($ && $.fn && $.fn.DataTable)) return;
|
||||
|
||||
const numericCols = [
|
||||
2, // Attendance
|
||||
3, // Homework Avg
|
||||
4, // Project Avg
|
||||
5, // Test/Quiz Avg
|
||||
6, // Participation
|
||||
7, // PTAP
|
||||
<?php if ($semester === 'fall'): ?>
|
||||
10, // Midterm
|
||||
13 // Semester Score
|
||||
<?php elseif ($semester === 'spring'): ?>
|
||||
10, // Final Exam
|
||||
13 // Semester Score
|
||||
<?php else: ?>
|
||||
10 // Semester Score (no midterm/final columns)
|
||||
<?php endif; ?>
|
||||
];
|
||||
|
||||
// Comment + review columns should stay unsortable
|
||||
const commentCols = [
|
||||
8, 9
|
||||
<?php if ($semester === 'fall'): ?>,11,12<?php endif; ?>
|
||||
<?php if ($semester === 'spring'): ?>,11,12<?php endif; ?>
|
||||
];
|
||||
|
||||
$('#myTable').DataTable({
|
||||
autoWidth: false,
|
||||
scrollX: true,
|
||||
paging: false,
|
||||
info: false,
|
||||
order: [[0, 'asc'], [1, 'asc']],
|
||||
columnDefs: [
|
||||
{ targets: numericCols, orderDataType: 'dom-text-num', type: 'num', orderSequence: ['desc', 'asc'] },
|
||||
{ targets: commentCols, orderable: false, searchable: true },
|
||||
]
|
||||
});
|
||||
});
|
||||
})(jQuery);
|
||||
</script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const textareas = document.querySelectorAll('.comment-textarea');
|
||||
|
||||
/**
|
||||
* Updates the counter for a specific textarea
|
||||
* @param {HTMLElement} el - The textarea element
|
||||
*/
|
||||
function updateCharCount(el) {
|
||||
// Find the counter span within the same table cell
|
||||
const wrapper = el.parentElement.querySelector('.char-count');
|
||||
if (!wrapper) return;
|
||||
|
||||
const currentLength = el.value.trim().length;
|
||||
wrapper.textContent = currentLength;
|
||||
|
||||
// Visual feedback for the 100-character minimum
|
||||
if (currentLength > 0 && currentLength < 100) {
|
||||
wrapper.style.color = '#dc3545'; // Bootstrap Red
|
||||
wrapper.style.fontWeight = 'bold';
|
||||
} else if (currentLength >= 100) {
|
||||
wrapper.style.color = '#198754'; // Bootstrap Green
|
||||
wrapper.style.fontWeight = 'normal';
|
||||
} else {
|
||||
wrapper.style.color = '#6c757d'; // Default Gray
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize and attach listeners
|
||||
textareas.forEach(textarea => {
|
||||
// Run once on load to show current character counts
|
||||
updateCharCount(textarea);
|
||||
|
||||
// Run on every input change
|
||||
textarea.addEventListener('input', function() {
|
||||
updateCharCount(this);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
Reference in New Issue
Block a user