add passed/failed checkbox for non graded courses
This commit is contained in:
+40
-20
@@ -445,7 +445,8 @@ function save() {
|
|||||||
// COMPUTED HELPERS
|
// COMPUTED HELPERS
|
||||||
// ══════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════
|
||||||
function blockMean(blockId) {
|
function blockMean(blockId) {
|
||||||
const graded = S.courses.filter(c => c.blockId === blockId && c.grade != null && c.grade !== '');
|
// passFail courses are excluded from the mean
|
||||||
|
const graded = S.courses.filter(c => c.blockId === blockId && !c.passFail && c.grade != null && c.grade !== '');
|
||||||
if (!graded.length) return null;
|
if (!graded.length) return null;
|
||||||
const sumWeighted = graded.reduce((s, c) => s + +c.grade * (+c.credits || 0), 0);
|
const sumWeighted = graded.reduce((s, c) => s + +c.grade * (+c.credits || 0), 0);
|
||||||
const sumCoef = graded.reduce((s, c) => s + (+c.credits || 0), 0);
|
const sumCoef = graded.reduce((s, c) => s + (+c.credits || 0), 0);
|
||||||
@@ -455,6 +456,7 @@ function blockMean(blockId) {
|
|||||||
|
|
||||||
// status: 'pass' | 'fail' | 'prog'
|
// status: 'pass' | 'fail' | 'prog'
|
||||||
function status(course) {
|
function status(course) {
|
||||||
|
if (course.passFail) return 'pass'; // pass/fail courses always count as passed
|
||||||
if (course.grade == null || course.grade === '') return 'prog';
|
if (course.grade == null || course.grade === '') return 'prog';
|
||||||
if (+course.grade >= 4) return 'pass';
|
if (+course.grade >= 4) return 'pass';
|
||||||
if (course.blockId) {
|
if (course.blockId) {
|
||||||
@@ -475,7 +477,8 @@ function gradeClass(g) {
|
|||||||
return +g >= 5 ? 'g-high' : +g >= 4 ? 'g-mid' : 'g-low';
|
return +g >= 5 ? 'g-high' : +g >= 4 ? 'g-mid' : 'g-low';
|
||||||
}
|
}
|
||||||
|
|
||||||
function fmtGrade(g) {
|
function fmtGrade(g, passFail) {
|
||||||
|
if (passFail) return '<span style="font-size:11px;font-weight:600;color:var(--success)">P/F</span>';
|
||||||
if (g == null || g === '') return '—';
|
if (g == null || g === '') return '—';
|
||||||
const n = +g;
|
const n = +g;
|
||||||
return Number.isInteger(n) ? String(n) : n.toFixed(2).replace(/0+$/, '').replace(/\.$/, '');
|
return Number.isInteger(n) ? String(n) : n.toFixed(2).replace(/0+$/, '').replace(/\.$/, '');
|
||||||
@@ -543,11 +546,14 @@ function renderDash() {
|
|||||||
|
|
||||||
function dashGlobal() {
|
function dashGlobal() {
|
||||||
const all = S.courses;
|
const all = S.courses;
|
||||||
const graded = all.filter(c => c.grade != null && c.grade !== '');
|
// passFail courses excluded from mean, but count for credits/pass rate denominator
|
||||||
|
const graded = all.filter(c => !c.passFail && c.grade != null && c.grade !== '');
|
||||||
const passed = all.filter(c => status(c) === 'pass');
|
const passed = all.filter(c => status(c) === 'pass');
|
||||||
const totCr = all.reduce((s, c) => s + (+c.credits || 0), 0);
|
const totCr = all.reduce((s, c) => s + (+c.credits || 0), 0);
|
||||||
const passCr = passed.reduce((s, c) => s + (+c.credits || 0), 0);
|
const passCr = passed.reduce((s, c) => s + (+c.credits || 0), 0);
|
||||||
const mean = graded.length ? graded.reduce((s, c) => s + +c.grade, 0) / graded.length : null;
|
const sumW = graded.reduce((s, c) => s + +c.grade * (+c.credits || 0), 0);
|
||||||
|
const sumC = graded.reduce((s, c) => s + (+c.credits || 0), 0);
|
||||||
|
const mean = sumC > 0 ? sumW / sumC : null;
|
||||||
const passedGraded = graded.filter(c => status(c) === 'pass').length;
|
const passedGraded = graded.filter(c => status(c) === 'pass').length;
|
||||||
const rate = graded.length ? Math.round(passedGraded / graded.length * 100) : null;
|
const rate = graded.length ? Math.round(passedGraded / graded.length * 100) : null;
|
||||||
|
|
||||||
@@ -568,7 +574,7 @@ function dashGlobal() {
|
|||||||
html += `<tr class="clickable" onclick="showCourseModal('${c.id}')">
|
html += `<tr class="clickable" onclick="showCourseModal('${c.id}')">
|
||||||
<td><div style="font-weight:500">${esc(c.name)}</div><div style="font-size:10px;color:var(--gray-400)">${semName(c.semesterId)}</div></td>
|
<td><div style="font-weight:500">${esc(c.name)}</div><div style="font-size:10px;color:var(--gray-400)">${semName(c.semesterId)}</div></td>
|
||||||
<td>${c.credits || '—'}</td>
|
<td>${c.credits || '—'}</td>
|
||||||
<td class="${gradeClass(c.grade)}">${fmtGrade(c.grade)}</td>
|
<td class="${c.passFail ? '' : gradeClass(c.grade)}">${fmtGrade(c.grade, c.passFail)}</td>
|
||||||
<td>${badge(st)}</td>
|
<td>${badge(st)}</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
});
|
});
|
||||||
@@ -636,8 +642,10 @@ function dashSemesters() {
|
|||||||
const sc = S.courses.filter(c => c.semesterId === sem.id);
|
const sc = S.courses.filter(c => c.semesterId === sem.id);
|
||||||
const totCr = sc.reduce((s, c) => s + (+c.credits || 0), 0);
|
const totCr = sc.reduce((s, c) => s + (+c.credits || 0), 0);
|
||||||
const passCr = sc.filter(c => status(c) === 'pass').reduce((s, c) => s + (+c.credits || 0), 0);
|
const passCr = sc.filter(c => status(c) === 'pass').reduce((s, c) => s + (+c.credits || 0), 0);
|
||||||
const graded = sc.filter(c => c.grade != null && c.grade !== '');
|
const graded = sc.filter(c => !c.passFail && c.grade != null && c.grade !== '');
|
||||||
const mean = graded.length ? graded.reduce((s, c) => s + +c.grade, 0) / graded.length : null;
|
const sumW = graded.reduce((s, c) => s + +c.grade * (+c.credits || 0), 0);
|
||||||
|
const sumC = graded.reduce((s, c) => s + (+c.credits || 0), 0);
|
||||||
|
const mean = sumC > 0 ? sumW / sumC : null;
|
||||||
|
|
||||||
html += `<div class="card"><div class="card-header">
|
html += `<div class="card"><div class="card-header">
|
||||||
<span class="card-title">${esc(sem.name)}</span>
|
<span class="card-title">${esc(sem.name)}</span>
|
||||||
@@ -669,7 +677,7 @@ function coursesMiniTable(courses, showMeta) {
|
|||||||
rows += `<tr class="clickable" onclick="showCourseModal('${c.id}')">
|
rows += `<tr class="clickable" onclick="showCourseModal('${c.id}')">
|
||||||
<td><div style="font-weight:500">${esc(c.name)}</div>${meta ? `<div style="font-size:10px;color:var(--gray-400)">${meta}</div>` : ''}</td>
|
<td><div style="font-weight:500">${esc(c.name)}</div>${meta ? `<div style="font-size:10px;color:var(--gray-400)">${meta}</div>` : ''}</td>
|
||||||
<td>${c.credits || '—'}</td>
|
<td>${c.credits || '—'}</td>
|
||||||
<td class="${gradeClass(c.grade)}">${fmtGrade(c.grade)}</td>
|
<td class="${c.passFail ? '' : gradeClass(c.grade)}">${fmtGrade(c.grade, c.passFail)}</td>
|
||||||
<td>${badge(st)}</td>
|
<td>${badge(st)}</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
});
|
});
|
||||||
@@ -835,6 +843,7 @@ function showCourseModal(id) {
|
|||||||
const curPlan = c ? (c.planId || '') : '';
|
const curPlan = c ? (c.planId || '') : '';
|
||||||
const curBlock = c ? (c.blockId || '') : '';
|
const curBlock = c ? (c.blockId || '') : '';
|
||||||
const blockOpts = buildBlockOpts(curPlan, curBlock);
|
const blockOpts = buildBlockOpts(curPlan, curBlock);
|
||||||
|
const isPF = c ? !!c.passFail : false;
|
||||||
|
|
||||||
openModal(`<div class="modal">
|
openModal(`<div class="modal">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
@@ -850,11 +859,15 @@ function showCourseModal(id) {
|
|||||||
<label>Credits *</label>
|
<label>Credits *</label>
|
||||||
<input id="f-credits" type="number" value="${c ? c.credits : ''}" placeholder="e.g. 3" min="0" step="0.5">
|
<input id="f-credits" type="number" value="${c ? c.credits : ''}" placeholder="e.g. 3" min="0" step="0.5">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group" id="fg-grade" style="display:${isPF ? 'none' : 'block'}">
|
||||||
<label>Grade (1 – 6)</label>
|
<label>Grade (1 – 6)</label>
|
||||||
<input id="f-grade" type="number" value="${c && c.grade != null && c.grade !== '' ? c.grade : ''}" placeholder="e.g. 5.5" min="1" max="6" step="0.25">
|
<input id="f-grade" type="number" value="${c && !isPF && c.grade != null && c.grade !== '' ? c.grade : ''}" placeholder="e.g. 5.5" min="1" max="6" step="0.25">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group" style="display:flex;align-items:center;gap:10px;padding:4px 0">
|
||||||
|
<input type="checkbox" id="f-passfail" style="width:18px;height:18px;cursor:pointer;flex-shrink:0" ${isPF ? 'checked' : ''} onchange="onPassFailChange(this.checked)">
|
||||||
|
<label for="f-passfail" style="text-transform:none;font-size:14px;font-weight:500;color:var(--gray-700);cursor:pointer;letter-spacing:0">Passed (no grade — excluded from means)</label>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Semester</label>
|
<label>Semester</label>
|
||||||
<select id="f-semester">
|
<select id="f-semester">
|
||||||
@@ -895,25 +908,32 @@ function onPlanChange(pid) {
|
|||||||
else { fg.style.display = 'none'; }
|
else { fg.style.display = 'none'; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onPassFailChange(checked) {
|
||||||
|
const fgGrade = document.getElementById('fg-grade');
|
||||||
|
if (fgGrade) fgGrade.style.display = checked ? 'none' : 'block';
|
||||||
|
}
|
||||||
|
|
||||||
function saveCourse(id) {
|
function saveCourse(id) {
|
||||||
const name = document.getElementById('f-name').value.trim();
|
const name = document.getElementById('f-name').value.trim();
|
||||||
const credits = document.getElementById('f-credits').value;
|
const credits = document.getElementById('f-credits').value;
|
||||||
const gradeV = document.getElementById('f-grade').value;
|
const passFail = document.getElementById('f-passfail').checked;
|
||||||
const semId = document.getElementById('f-semester').value;
|
const gradeEl = document.getElementById('f-grade');
|
||||||
const planId = document.getElementById('f-plan').value;
|
const gradeV = (!passFail && gradeEl) ? gradeEl.value : '';
|
||||||
const fg = document.getElementById('fg-block');
|
const semId = document.getElementById('f-semester').value;
|
||||||
const blockId = (fg && fg.style.display !== 'none') ? document.getElementById('f-block').value : '';
|
const planId = document.getElementById('f-plan').value;
|
||||||
|
const fg = document.getElementById('fg-block');
|
||||||
|
const blockId = (fg && fg.style.display !== 'none') ? document.getElementById('f-block').value : '';
|
||||||
|
|
||||||
if (!name) { alert('Please enter a course name.'); return; }
|
if (!name) { alert('Please enter a course name.'); return; }
|
||||||
if (!credits || +credits < 0) { alert('Please enter valid credits.'); return; }
|
if (!credits || +credits < 0) { alert('Please enter valid credits.'); return; }
|
||||||
|
|
||||||
const grade = gradeV !== '' ? parseFloat(gradeV) : null;
|
const grade = (!passFail && gradeV !== '') ? parseFloat(gradeV) : null;
|
||||||
|
|
||||||
if (id) {
|
if (id) {
|
||||||
const i = S.courses.findIndex(c => c.id === id);
|
const i = S.courses.findIndex(c => c.id === id);
|
||||||
if (i >= 0) S.courses[i] = { id, name, credits: +credits, semesterId: semId, grade, planId, blockId };
|
if (i >= 0) S.courses[i] = { id, name, credits: +credits, semesterId: semId, grade, planId, blockId, passFail };
|
||||||
} else {
|
} else {
|
||||||
S.courses.push({ id: uid(), name, credits: +credits, semesterId: semId, grade, planId, blockId });
|
S.courses.push({ id: uid(), name, credits: +credits, semesterId: semId, grade, planId, blockId, passFail });
|
||||||
}
|
}
|
||||||
|
|
||||||
save(); closeModal(); render(page);
|
save(); closeModal(); render(page);
|
||||||
|
|||||||
Reference in New Issue
Block a user