recreate project
This commit is contained in:
@@ -0,0 +1,491 @@
|
||||
<?= $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() ?>
|
||||
Reference in New Issue
Block a user