481 lines
19 KiB
PHP
481 lines
19 KiB
PHP
<?= $this->extend('layout/management_layout') ?>
|
|
<?= $this->section('content') ?>
|
|
|
|
<?php
|
|
// This page is Fall semester only.
|
|
// Do not expose Whole Year mode here.
|
|
$semester = 'fall';
|
|
$actionSemester = 'fall';
|
|
|
|
$schoolYear = $schoolYear ?? '';
|
|
$schoolYears = $schoolYears ?? [];
|
|
|
|
if (empty($schoolYears) && $schoolYear !== '') {
|
|
$schoolYears = [$schoolYear];
|
|
}
|
|
?>
|
|
|
|
<div class="container-fluid">
|
|
<div class="wrapper below-sixty-wrapper">
|
|
<h2 class="text-center mt-4 mb-4 below-sixty-title">Below 60 Summary</h2>
|
|
|
|
<!-- School year filter only -->
|
|
<div class="card shadow-sm mb-3">
|
|
<div class="card-body py-3">
|
|
<form method="get"
|
|
action="<?= site_url('grading/below-60') ?>"
|
|
class="row g-2 align-items-end justify-content-center">
|
|
|
|
<input type="hidden" name="semester" value="fall">
|
|
|
|
<div class="col-12 col-sm-auto">
|
|
<label class="form-label mb-1 small fw-semibold">School Year</label>
|
|
|
|
<?php if (!empty($schoolYears)): ?>
|
|
<select name="school_year"
|
|
class="form-select form-select-sm"
|
|
style="min-width:160px;">
|
|
<?php foreach ($schoolYears as $yr): ?>
|
|
<option value="<?= esc($yr) ?>" <?= (string)$yr === (string)$schoolYear ? 'selected' : '' ?>>
|
|
<?= esc($yr) ?>
|
|
</option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
<?php else: ?>
|
|
<input type="text"
|
|
name="school_year"
|
|
class="form-control form-control-sm"
|
|
style="min-width:160px;"
|
|
value="<?= esc($schoolYear) ?>"
|
|
placeholder="2025-2026">
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<div class="col-12 col-sm-auto">
|
|
<button type="submit" class="btn btn-sm btn-primary">
|
|
<i class="bi bi-funnel me-1"></i>Apply
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
|
|
<div class="text-muted">
|
|
Fall • <?= esc($schoolYear) ?>
|
|
</div>
|
|
|
|
<div class="d-flex gap-2">
|
|
<?php if (!empty($canViewGrading)): ?>
|
|
<a class="btn btn-outline-secondary btn-sm" href="<?= base_url('grading') ?>">
|
|
Back to Grading
|
|
</a>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
|
|
<?php
|
|
$displayScore = function ($value) {
|
|
if ($value === null || $value === '') {
|
|
return '—';
|
|
}
|
|
|
|
if (is_numeric($value)) {
|
|
return esc(number_format((float)$value, 2, '.', ''));
|
|
}
|
|
|
|
return esc($value);
|
|
};
|
|
?>
|
|
|
|
<?php if (empty($rows)): ?>
|
|
<div class="alert alert-success text-center d-inline-block">
|
|
No students below 60 for Fall in <?= esc($schoolYear) ?>.
|
|
</div>
|
|
<?php else: ?>
|
|
<div class="table-responsive below-sixty-table">
|
|
<table class="table table-bordered table-striped align-middle w-100 no-mgmt-sticky below-sixty-dt"
|
|
data-no-mgmt-sticky
|
|
data-no-dt-fixedheader>
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>Student Name</th>
|
|
<th>Section</th>
|
|
<th class="text-center">Fall Score</th>
|
|
<th>Status</th>
|
|
<th>Email Parent</th>
|
|
<th>Schedule Meeting</th>
|
|
</tr>
|
|
</thead>
|
|
|
|
<tbody>
|
|
<?php foreach ($rows as $row): ?>
|
|
<?php
|
|
// Fall-only page: use semester_score.
|
|
$scoreRaw = $row['semester_score'] ?? null;
|
|
$scoreVal = is_numeric($scoreRaw) ? (float)$scoreRaw : null;
|
|
|
|
$scoreClass = '';
|
|
|
|
if ($scoreVal !== null) {
|
|
if ($scoreVal < 50) {
|
|
$scoreClass = 'grade-red';
|
|
} elseif ($scoreVal < 60) {
|
|
$scoreClass = 'grade-orange';
|
|
}
|
|
}
|
|
|
|
$studentName = trim((string)($row['firstname'] ?? '') . ' ' . (string)($row['lastname'] ?? ''));
|
|
$studentLabel = $studentName !== '' ? $studentName : 'N/A';
|
|
$isClosed = ($row['status'] ?? 'Open') === 'Closed';
|
|
?>
|
|
|
|
<tr class="<?= esc($scoreClass) ?>">
|
|
<td><?= esc($studentLabel) ?></td>
|
|
<td><?= esc($row['class_section_name'] ?? '—') ?></td>
|
|
|
|
<td class="text-center">
|
|
<div class="fw-semibold"><?= $displayScore($scoreRaw) ?></div>
|
|
|
|
<button type="button"
|
|
class="btn btn-outline-secondary btn-xs mt-1 btn-show-details"
|
|
style="font-size:0.72rem;padding:1px 7px;"
|
|
data-student-id="<?= (int)($row['student_id'] ?? 0) ?>"
|
|
data-student-name="<?= esc($studentLabel) ?>"
|
|
data-school-year="<?= esc((string)$schoolYear) ?>">
|
|
Details
|
|
</button>
|
|
</td>
|
|
|
|
<td class="text-center">
|
|
<form method="post"
|
|
action="<?= site_url('grading/below-60/status') ?>"
|
|
class="d-flex align-items-center gap-2 justify-content-center">
|
|
<?= csrf_field() ?>
|
|
|
|
<input type="hidden"
|
|
name="student_id"
|
|
value="<?= esc((string)($row['student_id'] ?? '')) ?>">
|
|
|
|
<input type="hidden"
|
|
name="semester"
|
|
value="fall">
|
|
|
|
<input type="hidden"
|
|
name="school_year"
|
|
value="<?= esc((string)$schoolYear) ?>">
|
|
|
|
<select name="status"
|
|
class="form-select form-select-sm"
|
|
style="width:110px;">
|
|
<option value="Open" <?= ($row['status'] ?? 'Open') === 'Open' ? 'selected' : '' ?>>
|
|
Open
|
|
</option>
|
|
<option value="Closed" <?= ($row['status'] ?? '') === 'Closed' ? 'selected' : '' ?>>
|
|
Closed
|
|
</option>
|
|
</select>
|
|
|
|
<input type="text"
|
|
name="note"
|
|
class="form-control form-control-sm"
|
|
style="width:140px;"
|
|
placeholder="Note (optional)"
|
|
value="<?= esc((string)($row['note'] ?? '')) ?>">
|
|
|
|
<button type="submit" class="btn btn-sm btn-outline-secondary">
|
|
Update
|
|
</button>
|
|
</form>
|
|
</td>
|
|
|
|
<td class="text-center">
|
|
<?php if ($isClosed): ?>
|
|
<button type="button" class="btn btn-sm btn-secondary" disabled>
|
|
Send Email
|
|
</button>
|
|
<?php else: ?>
|
|
<a class="btn btn-sm btn-outline-primary"
|
|
href="<?= site_url('grading/below-60/email/edit?' . http_build_query([
|
|
'student_id' => (int)($row['student_id'] ?? 0),
|
|
'semester' => 'fall',
|
|
'school_year' => (string)$schoolYear,
|
|
])) ?>">
|
|
Send Email
|
|
</a>
|
|
<?php endif; ?>
|
|
</td>
|
|
|
|
<td class="text-center">
|
|
<?php if ($isClosed): ?>
|
|
<button class="btn btn-sm btn-secondary" disabled>
|
|
Schedule
|
|
</button>
|
|
<?php else: ?>
|
|
<a class="btn btn-sm btn-outline-secondary"
|
|
href="<?= site_url('grading/below-60/schedule?' . http_build_query([
|
|
'student_id' => (int)($row['student_id'] ?? 0),
|
|
'semester' => 'fall',
|
|
'school_year' => (string)$schoolYear,
|
|
])) ?>">
|
|
Schedule
|
|
</a>
|
|
<?php endif; ?>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Score details modal -->
|
|
<div class="modal fade" id="detailsModal" tabindex="-1" aria-labelledby="detailsModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="detailsModalLabel">Score Details</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
|
|
<div class="modal-body" id="detailsModalBody">
|
|
<div class="text-center py-4">
|
|
<div class="spinner-border text-primary" role="status"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal-footer">
|
|
<button type="button"
|
|
class="btn btn-outline-secondary"
|
|
data-bs-dismiss="modal">
|
|
Close
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<?= $this->endSection() ?>
|
|
|
|
<?= $this->section('scripts') ?>
|
|
<style>
|
|
.grade-orange td {
|
|
background: #fff3cd !important;
|
|
color: #8a5d00;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.grade-red td {
|
|
background: #f8d7da !important;
|
|
color: #842029;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.below-sixty-wrapper {
|
|
padding-top: 0.75rem;
|
|
}
|
|
|
|
.below-sixty-title {
|
|
position: relative;
|
|
z-index: 1;
|
|
margin-bottom: 1.25rem;
|
|
}
|
|
|
|
.below-sixty-table {
|
|
margin-top: 0.75rem;
|
|
}
|
|
|
|
.below-sixty-dt td {
|
|
vertical-align: top;
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
(function () {
|
|
if (window.$ && $.fn && $.fn.DataTable) {
|
|
$(function () {
|
|
const table = $('.below-sixty-dt');
|
|
|
|
if (!table.length) return;
|
|
|
|
try {
|
|
table.DataTable({
|
|
order: [[2, 'asc']],
|
|
pageLength: 100,
|
|
lengthMenu: [10, 25, 50, 100, 200],
|
|
columnDefs: [
|
|
{ orderable: false, targets: [3, 4, 5] }
|
|
]
|
|
});
|
|
} catch (_) {}
|
|
});
|
|
}
|
|
|
|
const detailsModal = document.getElementById('detailsModal');
|
|
const detailsModalBody = document.getElementById('detailsModalBody');
|
|
const detailsModalTitle = document.getElementById('detailsModalLabel');
|
|
|
|
const SCORE_LABELS = {
|
|
homework_avg: 'Homework Avg',
|
|
project_avg: 'Project Avg',
|
|
participation_score: 'Participation',
|
|
test_avg: 'Test Avg',
|
|
ptap_score: 'PTAP Score',
|
|
attendance_score: 'Attendance',
|
|
midterm_exam_score: 'Midterm Score',
|
|
final_exam_score: 'Final Exam',
|
|
semester_score: 'Semester Score'
|
|
};
|
|
|
|
const COMMENT_TYPE_LABELS = {
|
|
general: 'General',
|
|
attendance: 'Attendance',
|
|
attendance_comment: 'Attendance',
|
|
midterm: 'Midterm',
|
|
final: 'Final Exam',
|
|
ptap: 'PTAP'
|
|
};
|
|
|
|
function fmtScore(value) {
|
|
if (value === null || value === '' || value === undefined) return '—';
|
|
|
|
const parsed = parseFloat(value);
|
|
|
|
return isNaN(parsed) ? value : parsed.toFixed(2);
|
|
}
|
|
|
|
function esc(str) {
|
|
return String(str)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"');
|
|
}
|
|
|
|
function buildDetailsHtml(semesters) {
|
|
if (!semesters || semesters.length === 0) {
|
|
return '<div class="alert alert-warning mb-0">No score data found.</div>';
|
|
}
|
|
|
|
let html = '';
|
|
|
|
semesters.forEach(function (sem) {
|
|
html += '<h6 class="fw-bold mt-3 mb-2">' + esc(sem.semester || '') + ' Semester';
|
|
|
|
if (sem.class_section_name) {
|
|
html += ' <span class="text-muted fw-normal fs-6">— ' + esc(sem.class_section_name) + '</span>';
|
|
}
|
|
|
|
html += '</h6>';
|
|
|
|
html += '<table class="table table-sm table-bordered mb-2">';
|
|
html += '<thead class="table-light"><tr><th>Item</th><th class="text-center">Score</th></tr></thead><tbody>';
|
|
|
|
let hasScoreRow = false;
|
|
|
|
Object.entries(SCORE_LABELS).forEach(function (entry) {
|
|
const key = entry[0];
|
|
const label = entry[1];
|
|
const value = sem[key];
|
|
|
|
if (value === null || value === '' || value === undefined) return;
|
|
|
|
const bold = key === 'semester_score' ? ' fw-bold' : '';
|
|
|
|
html += '<tr><td>' + esc(label) + '</td><td class="text-center' + bold + '">' + esc(fmtScore(value)) + '</td></tr>';
|
|
hasScoreRow = true;
|
|
});
|
|
|
|
if (!hasScoreRow) {
|
|
html += '<tr><td colspan="2" class="text-muted">No scores recorded.</td></tr>';
|
|
}
|
|
|
|
html += '</tbody></table>';
|
|
|
|
const comments = sem.comments || {};
|
|
|
|
const commentEntries = Object.entries(comments).filter(function (entry) {
|
|
return entry[1] && String(entry[1]).trim();
|
|
});
|
|
|
|
const seen = {};
|
|
const deduped = [];
|
|
|
|
commentEntries.forEach(function (entry) {
|
|
const type = entry[0];
|
|
const text = String(entry[1]);
|
|
const label = COMMENT_TYPE_LABELS[type] || type;
|
|
const key = label + '|' + text.trim();
|
|
|
|
if (!seen[key]) {
|
|
seen[key] = true;
|
|
deduped.push([label, text]);
|
|
}
|
|
});
|
|
|
|
if (deduped.length > 0) {
|
|
html += '<div class="mb-3">';
|
|
html += '<p class="fw-semibold mb-1" style="font-size:0.9rem;">Comments</p>';
|
|
|
|
deduped.forEach(function (entry) {
|
|
const label = entry[0];
|
|
const text = entry[1];
|
|
|
|
html += '<div class="mb-2 p-2 bg-light rounded border-start border-3 border-secondary">';
|
|
html += '<span class="badge bg-secondary me-1" style="font-size:0.7rem;">' + esc(label) + '</span>';
|
|
html += '<span class="text-dark" style="font-size:0.9rem;">' + esc(text).replace(/\n/g, '<br>') + '</span>';
|
|
html += '</div>';
|
|
});
|
|
|
|
html += '</div>';
|
|
}
|
|
});
|
|
|
|
return html;
|
|
}
|
|
|
|
if (detailsModal) {
|
|
document.addEventListener('click', function (e) {
|
|
const btn = e.target.closest('.btn-show-details');
|
|
|
|
if (!btn) return;
|
|
|
|
const studentId = btn.dataset.studentId;
|
|
const studentName = btn.dataset.studentName;
|
|
const schoolYear = btn.dataset.schoolYear;
|
|
|
|
detailsModalTitle.textContent = studentName + ' — Score Details';
|
|
detailsModalBody.innerHTML = '<div class="text-center py-4"><div class="spinner-border text-primary" role="status"></div></div>';
|
|
|
|
bootstrap.Modal.getOrCreateInstance(detailsModal).show();
|
|
|
|
const url = '<?= site_url('grading/below-60/decisions/student-details') ?>'
|
|
+ '?student_id=' + encodeURIComponent(studentId)
|
|
+ '&school_year=' + encodeURIComponent(schoolYear);
|
|
|
|
fetch(url, {
|
|
headers: {
|
|
'X-Requested-With': 'XMLHttpRequest'
|
|
}
|
|
})
|
|
.then(function (response) {
|
|
return response.json();
|
|
})
|
|
.then(function (data) {
|
|
if (data.error) {
|
|
detailsModalBody.innerHTML = '<div class="alert alert-danger">' + esc(data.error) + '</div>';
|
|
return;
|
|
}
|
|
|
|
detailsModalBody.innerHTML = buildDetailsHtml(data.semesters);
|
|
})
|
|
.catch(function () {
|
|
detailsModalBody.innerHTML = '<div class="alert alert-danger">Failed to load details.</div>';
|
|
});
|
|
});
|
|
}
|
|
})();
|
|
</script>
|
|
<?= $this->endSection() ?>
|