737 lines
35 KiB
PHP
737 lines
35 KiB
PHP
<?= $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"
|
||
data-confirm-message="Once you submit, you cannot add any scores or comments. Do you want to continue?"
|
||
<?= $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 submitter = event.submitter;
|
||
if (submitter && submitter.id === 'submitScoresLockBtn') {
|
||
const message = submitter.dataset.confirmMessage || 'Submit semester scores?';
|
||
if (!confirm(message)) {
|
||
event.preventDefault();
|
||
return;
|
||
}
|
||
}
|
||
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() ?>
|