add passed/failed checkbox for non graded courses

This commit is contained in:
2026-05-12 00:05:09 +02:00
parent d61f3306ce
commit e4f11df694
+40 -20
View File
@@ -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);