Files
alrahma_sunday_school/app/Views/grading/comments.php
T
2026-02-10 22:11:06 -05:00

492 lines
24 KiB
PHP

<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<?php
$scoresLocked = !empty($scoresLocked);
$lockAttr = $scoresLocked ? 'disabled' : '';
?>
<div class="container-fluid py-4 px-3">
<div class="row">
<div class="col-12">
<div class="text-center mx-auto mb-4" style="max-width: 600px;">
<h2 class="text-dark">Edit Student Comments & Reviews (<?= esc($semester) ?> Semester)</h2>
</div>
<?php if (session()->get('status')): ?>
<div class="alert alert-success"><?= session()->get('status') ?></div>
<?php endif; ?>
<?php if (session()->get('error')): ?>
<div class="alert alert-danger"><?= esc(session()->get('error')) ?></div>
<?php endif; ?>
<?php if ($errors = session()->get('errors')): ?>
<div class="alert alert-danger">
<ul class="mb-0">
<?php foreach ($errors as $error): ?>
<li><?= esc($error) ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<?php if ($scoresLocked): ?>
<div class="alert alert-warning">
Scores are locked for this class. Unlock to edit.
</div>
<?php endif; ?>
<div class="d-flex justify-content-between mt-4 flex-wrap gap-2">
<button type="submit" class="btn btn-success" <?= $lockAttr ?>>
<i class="bi bi-save"></i> Save All Changes
</button>
<a href="<?= base_url('/grading') ?>" class="btn btn-secondary">
<i class="bi bi-x-circle"></i> Back to Scores
</a>
</div>
<br>
<form id="commentsForm" action="<?= base_url('grading/updateComments') ?>" method="post">
<?= csrf_field() ?>
<input type="hidden" name="semester" value="<?= esc($semester) ?>">
<input type="hidden" name="school_year" value="<?= esc($schoolYear) ?>">
<input type="hidden" name="class_section_id" value="<?= esc($classSectionId) ?>">
<input type="hidden" id="proofreadCsrfName" value="<?= csrf_token() ?>">
<input type="hidden" id="proofreadCsrfHash" value="<?= csrf_hash() ?>">
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
<div>
<button type="button" id="proofreadAllBtn" class="btn btn-outline-primary btn-sm" <?= $lockAttr ?>>
<i class="bi bi-magic"></i> Proofread All Reviews
</button>
</div>
<div class="text-muted small" id="proofreadAllStatus"></div>
</div>
<div class="table-responsive w-100">
<table id="commentsTable" class="table table-bordered table-striped w-100 comments-table" data-no-mgmt-sticky>
<thead class="table-light">
<tr>
<th>#</th>
<th>School ID</th>
<th>Student Name</th>
<th>PTAP Comment</th>
<th>PTAP Review</th>
<?php if ($semester === 'Fall'): ?>
<th>Midterm Comment</th>
<th>Midterm Review</th>
<?php elseif ($semester === 'Spring'): ?>
<th>Final Comment</th>
<th>Final Review</th>
<?php endif; ?>
<th>Attendance Comment</th>
</tr>
</thead>
<tbody>
<?php $rowNum = 1; ?>
<?php foreach ($students as $student): ?>
<?php $studentId = $student['student_id']; ?>
<tr>
<td><?= $rowNum++ ?></td>
<td><?= esc($student['school_id']) ?></td>
<td>
<a href="#" class="text-decoration-none" data-family-student-id="<?= (int)$studentId ?>">
<?= esc($student['firstname']) ?> <?= esc($student['lastname']) ?>
</a>
</td>
<!-- PTAP Comment and Review -->
<td>
<?php $ptapCommentId = 'ptap-comment-' . $studentId; ?>
<?php $ptapReviewId = 'ptap-review-' . $studentId; ?>
<textarea
id="<?= esc($ptapCommentId) ?>"
name="comments[<?= $studentId ?>][ptap]"
rows="2"
class="form-control"
data-first-name="<?= esc($student['firstname']) ?>"
minlength="100"
maxlength="350"
placeholder="Enter PTAP comment" <?= $lockAttr ?>><?= esc($comments[$studentId]['ptap']['comment'] ?? '') ?></textarea>
</td>
<td class="ptap-review-cell">
<div class="d-flex gap-2 align-items-start">
<textarea
id="<?= esc($ptapReviewId) ?>"
name="reviews[<?= $studentId ?>][ptap]"
rows="2"
class="form-control review-field"
placeholder="Enter PTAP review" <?= $lockAttr ?>><?= esc($comments[$studentId]['ptap']['comment_review'] ?? '') ?></textarea>
<div class="d-flex flex-column gap-1 align-items-stretch">
<button
type="button"
class="btn btn-outline-secondary btn-sm copy-ptap-btn"
data-copy-source="#<?= esc($ptapCommentId) ?>"
data-copy-target="#<?= esc($ptapReviewId) ?>" <?= $lockAttr ?>>
Paste
</button>
<div class="small text-muted proofread-status" data-status-for="<?= esc($ptapReviewId) ?>"></div>
<ul class="proofread-list small mb-0" data-list-for="<?= esc($ptapReviewId) ?>"></ul>
</div>
</div>
<?php if (isset($comments[$studentId]['ptap']['reviewed_by'])): ?>
<div class="text-muted small mt-1">
Last reviewed by: <?= esc($comments[$studentId]['ptap']['reviewed_by']) ?>
</div>
<?php endif; ?>
</td>
<!-- Conditional Midterm (Fall) or Final (Spring) -->
<?php if ($semester === 'Fall'): ?>
<td>
<?php $midCommentId = 'midterm-comment-' . $studentId; ?>
<?php $midReviewId = 'midterm-review-' . $studentId; ?>
<textarea
id="<?= esc($midCommentId) ?>"
name="comments[<?= $studentId ?>][midterm]"
rows="2"
class="form-control"
data-first-name="<?= esc($student['firstname']) ?>"
minlength="100"
maxlength="350"
placeholder="Enter Midterm comment" <?= $lockAttr ?>><?= esc($comments[$studentId]['midterm']['comment'] ?? '') ?></textarea>
</td>
<td>
<div class="d-flex gap-2 align-items-start">
<textarea
id="<?= esc($midReviewId) ?>"
name="reviews[<?= $studentId ?>][midterm]"
rows="2"
class="form-control review-field"
data-first-name="<?= esc($student['firstname']) ?>"
minlength="100"
maxlength="350"
placeholder="Enter Midterm review" <?= $lockAttr ?>><?= esc($comments[$studentId]['midterm']['comment_review'] ?? '') ?></textarea>
<div class="d-flex flex-column gap-1 align-items-stretch">
<button
type="button"
class="btn btn-outline-secondary btn-sm copy-midterm-btn"
data-copy-source="#<?= esc($midCommentId) ?>"
data-copy-target="#<?= esc($midReviewId) ?>" <?= $lockAttr ?>>
Paste
</button>
<div class="small text-muted proofread-status" data-status-for="<?= esc($midReviewId) ?>"></div>
<ul class="proofread-list small mb-0" data-list-for="<?= esc($midReviewId) ?>"></ul>
</div>
</div>
<?php if (isset($comments[$studentId]['midterm']['reviewed_by'])): ?>
<div class="text-muted small mt-1">
Last reviewed by: <?= esc($comments[$studentId]['midterm']['reviewed_by']) ?>
</div>
<?php endif; ?>
</td>
<?php elseif ($semester === 'Spring'): ?>
<td>
<textarea
name="comments[<?= $studentId ?>][final]"
rows="2"
class="form-control"
data-first-name="<?= esc($student['firstname']) ?>"
minlength="100"
maxlength="350"
placeholder="Enter Final comment" <?= $lockAttr ?>><?= esc($comments[$studentId]['final']['comment'] ?? '') ?></textarea>
</td>
<?php $finalReviewId = 'final-review-' . $studentId; ?>
<td>
<div class="d-flex gap-2 align-items-start">
<textarea
id="<?= esc($finalReviewId) ?>"
name="reviews[<?= $studentId ?>][final]"
rows="2"
class="form-control review-field"
placeholder="Enter Final review" <?= $lockAttr ?>><?= esc($comments[$studentId]['final']['comment_review'] ?? '') ?></textarea>
<div class="d-flex flex-column gap-1 align-items-stretch">
<div class="small text-muted proofread-status" data-status-for="<?= esc($finalReviewId) ?>"></div>
<ul class="proofread-list small mb-0" data-list-for="<?= esc($finalReviewId) ?>"></ul>
</div>
</div>
<?php if (isset($comments[$studentId]['final']['reviewed_by'])): ?>
<div class="text-muted small mt-1">
Last reviewed by: <?= esc($comments[$studentId]['final']['reviewed_by']) ?>
</div>
<?php endif; ?>
</td>
<?php endif; ?>
<!-- Attendance Comment only -->
<td>
<textarea
name="comments[<?= $studentId ?>][attendance]"
rows="2"
class="form-control"
data-first-name="<?= esc($student['firstname']) ?>"
minlength="100"
maxlength="350"
placeholder="Enter Attendance comment" <?= $lockAttr ?>><?= esc($comments[$studentId]['attendance']['comment'] ?? '') ?></textarea>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div class="d-flex justify-content-between mt-4 flex-wrap gap-2">
<button type="submit" class="btn btn-success" <?= $lockAttr ?>>
<i class="bi bi-save"></i> Save All Changes
</button>
<a href="<?= base_url('/grading') ?>" class="btn btn-secondary">
<i class="bi bi-x-circle"></i> Back to Scores
</a>
</div>
</form>
</div>
</div>
</div>
<style>
.review-field {
background-color: #f8f9fa;
border-left: 3px solid #dee2e6;
}
.review-field:focus {
background-color: #fff;
border-left: 3px solid #86b7fe;
}
textarea.form-control {
min-height: 80px;
}
.text-muted small {
font-size: 0.75rem;
}
.comments-table {
width: 100%;
table-layout: auto;
}
.table-responsive {
width: 100%;
overflow-x: auto;
}
/* Stretch table to available space */
.comments-table th,
.comments-table td {
white-space: nowrap;
}
.proofread-list {
list-style: none;
padding-left: 0;
margin-bottom: 0;
white-space: normal;
}
.proofread-status {
min-height: 1.25rem;
white-space: normal;
}
</style>
<?= $this->endSection() ?>
<?= $this->section('scripts') ?>
<script>
window.PROOFREAD_ENDPOINT = "<?= site_url('api/proofread') ?>";
</script>
<script src="<?= base_url('assets/js/proofread.js') ?>"></script>
<script>
// Attach DataTables sorting for comments table
(function ($) {
function initCommentsTable() {
if (!($ && $.fn && $.fn.DataTable)) return;
const $table = $('#commentsTable');
if (!$table.length) return;
$table.DataTable({
paging: false,
info: false,
searching: false,
autoWidth: false,
order: [[2, 'asc']], // Student Name
columnDefs: [
{ targets: 0, orderable: false, searchable: false },
{ targets: 1, className: 'text-nowrap' },
],
fixedHeader: {
header: true,
headerOffset: typeof getFixedHeaderOffset === 'function' ? getFixedHeaderOffset() : 0,
},
drawCallback: function () {
const api = this.api();
api.column(0, { search: 'applied', order: 'applied' }).nodes().each(function (cell, i) {
cell.textContent = i + 1;
});
},
});
}
$(document).ready(initCommentsTable);
})(jQuery);
</script>
<script>
// Auto-expand textareas on load and input
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('textarea').forEach(textarea => {
textarea.style.height = 'auto';
textarea.style.height = (textarea.scrollHeight) + 'px';
textarea.addEventListener('input', function() {
this.style.height = 'auto';
this.style.height = (this.scrollHeight) + 'px';
});
});
const form = document.getElementById('commentsForm');
if (form) {
const escapeRegex = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
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 {
const parts = document.cookie.split(';').map(function(value) { return value.trim(); });
const match = parts.find(function(value) { return value.indexOf(name + '=') === 0; });
return match ? match.split('=')[1] : '';
} catch (e) {
return '';
}
};
const syncCsrfToken = function() {
if (!csrfInput) return;
const fresh = decodeURIComponent(getCookie(csrfCookieName) || '');
if (fresh) csrfInput.value = fresh;
};
const parseFieldName = function(name) {
const match = /^(comments|reviews)\[(\d+)\]\[([^\]]+)\]$/.exec(name || '');
if (!match) return null;
return { group: match[1], studentId: match[2], scoreType: match[3] };
};
const isValidComment = function(field, value) {
if (!value) return false;
if (value.length < 100 || value.length > 350) return false;
const firstName = field.dataset.firstName || '';
if (!firstName) return true;
const startsWithName = new RegExp(`^${escapeRegex(firstName)}\\b`, 'i');
return startsWithName.test(value);
};
const shouldAutoSave = function(field, value) {
if (field.disabled) return false;
const lastSaved = field.getAttribute('data-last-saved') || '';
if (value === lastSaved) return false;
const parsed = parseFieldName(field.name || '');
if (!parsed) return false;
if (parsed.group === 'comments') {
return isValidComment(field, value);
}
return value !== '';
};
const buildAutoSavePayload = function(field) {
const parsed = parseFieldName(field.name || '');
if (!parsed) return null;
const data = new FormData();
if (csrfInput) {
const csrfValue = decodeURIComponent(getCookie(csrfCookieName) || '') || csrfInput.value;
data.append(csrfInput.name, csrfValue);
}
['semester', 'school_year', 'class_section_id'].forEach(function(name) {
const input = form.querySelector('[name="' + name + '"]');
if (input) data.append(name, input.value);
});
if (parsed.group === 'comments') {
data.append(field.name, field.value);
} else if (parsed.group === 'reviews') {
const commentName = `comments[${parsed.studentId}][${parsed.scoreType}]`;
const commentField = form.querySelector(`[name="${commentName}"]`);
data.append(commentName, commentField ? commentField.value : '');
data.append(field.name, field.value);
}
return data;
};
const autoSaveField = function(field) {
const value = field.value.trim();
if (!shouldAutoSave(field, value)) return;
const payload = buildAutoSavePayload(field);
if (!payload) return;
fetch(form.action, {
method: 'POST',
body: payload,
credentials: 'same-origin'
})
.then(function() {
field.setAttribute('data-last-saved', value);
syncCsrfToken();
})
.catch(function() {});
};
form.addEventListener('submit', function(event) {
syncCsrfToken();
const errors = [];
form.querySelectorAll('textarea[data-first-name]').forEach(field => {
const value = field.value.trim();
if (value === '') return;
const min = 100;
const max = 350;
const firstName = field.dataset.firstName || 'student';
if (value.length < min || value.length > max) {
errors.push(`Comment for ${firstName} must be between ${min} and ${max} characters.`);
return;
}
if (field.dataset.firstName) {
const startsWithName = new RegExp(`^${escapeRegex(field.dataset.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[name^="comments["], textarea[name^="reviews["]').forEach(function(field) {
field.setAttribute('data-last-saved', field.value.trim());
field.addEventListener('blur', function() {
autoSaveField(this);
});
});
}
const wireCopyButtons = (selector) => {
document.querySelectorAll(selector).forEach(button => {
button.addEventListener('click', function() {
const source = document.querySelector(this.dataset.copySource || '');
const target = document.querySelector(this.dataset.copyTarget || '');
if (!source || !target) return;
target.value = source.value;
target.style.height = 'auto';
target.style.height = (target.scrollHeight) + 'px';
target.focus();
});
});
};
document.querySelectorAll('.copy-ptap-btn').forEach(button => {
button.addEventListener('click', function() {
const source = document.querySelector(this.dataset.copySource || '');
const target = document.querySelector(this.dataset.copyTarget || '');
if (!source || !target) return;
target.value = source.value;
target.style.height = 'auto';
target.style.height = (target.scrollHeight) + 'px';
target.focus();
});
});
wireCopyButtons('.copy-midterm-btn');
});
</script>
<?= $this->endSection() ?>