improve calendar

This commit is contained in:
2026-07-02 15:25:43 +02:00
parent 1fb32153e3
commit fb87091b78
2 changed files with 150 additions and 78 deletions
+149 -77
View File
@@ -430,10 +430,10 @@
.wk-gline { position: absolute; left: 0; right: 0; border-top: 1px solid var(--gray-100); } .wk-gline { position: absolute; left: 0; right: 0; border-top: 1px solid var(--gray-100); }
.wk-gline-h { border-top-color: var(--gray-200); } .wk-gline-h { border-top-color: var(--gray-200); }
.wk-event { .wk-event {
position: absolute; left: 2px; right: 2px; position: absolute;
border-radius: 4px; font-size: 10px; font-weight: 600; border-radius: 4px; font-size: 10px; font-weight: 600;
padding: 2px 4px; overflow: hidden; cursor: pointer; padding: 2px 4px; overflow: hidden; cursor: pointer;
color: #fff; line-height: 1.3; color: #fff; line-height: 1.3; box-sizing: border-box;
} }
.wk-event:active { filter: brightness(0.85); } .wk-event:active { filter: brightness(0.85); }
.ev-swatch { .ev-swatch {
@@ -944,10 +944,10 @@ function dashPlans() {
</div>`; </div>`;
if (!isCollapsed) { if (!isCollapsed) {
html += roleGroupSection(plan.id, 'Major', 'major', majors, pc, pClr); majors.forEach((blk, i) => { html += planBlockSection(plan.id, blk, pc, majors.length, i); });
html += roleGroupSection(plan.id, 'Minor', 'minor', minors, pc, pClr); minors.forEach((blk, i) => { html += planBlockSection(plan.id, blk, pc, minors.length, i); });
if (settings.showOptional) { if (settings.showOptional) {
html += roleGroupSection(plan.id, 'Optional Studies', 'optional', optionals, pc, pClr); optionals.forEach((blk, i) => { html += planBlockSection(plan.id, blk, pc, optionals.length, i); });
} }
others.forEach(blk => { html += planBlockSection(plan.id, blk, pc, 1, 0); }); others.forEach(blk => { html += planBlockSection(plan.id, blk, pc, 1, 0); });
const nbc = pc.filter(c => !c.blockId); const nbc = pc.filter(c => !c.blockId);
@@ -955,6 +955,11 @@ function dashPlans() {
html += `<div style="margin-top:8px;font-size:11px;color:var(--gray-400);text-transform:uppercase;letter-spacing:.05em">Unassigned courses</div>`; html += `<div style="margin-top:8px;font-size:11px;color:var(--gray-400);text-transform:uppercase;letter-spacing:.05em">Unassigned courses</div>`;
html += coursesMgmtList(nbc); html += coursesMgmtList(nbc);
} }
html += `<div style="display:flex;gap:5px;margin-top:10px;flex-wrap:wrap">
${!majors.length ? `<button class="btn btn-sm btn-secondary" onclick="showMinorModal('${plan.id}','','major')">+ Major</button>` : ''}
<button class="btn btn-sm btn-secondary" onclick="showMinorModal('${plan.id}','','minor')">+ Minor</button>
${settings.showOptional ? `<button class="btn btn-sm btn-secondary" onclick="showMinorModal('${plan.id}','','optional')">+ Optional</button>` : ''}
</div>`;
} }
html += `</div>`; html += `</div>`;
}); });
@@ -1052,6 +1057,90 @@ function planSubBlockSection(planId, blockId, sb, courses, parentColor, inEdit,
const CAL_SVG = `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>`; const CAL_SVG = `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>`;
function semCourseRow(c) {
const st = status(c);
const blk = c.blockId ? S.blocks.find(b => b.id === c.blockId) : null;
const bClr = blk ? blockEffectiveColor(blk) : null;
const borderStyle = bClr ? `border-left:3px solid ${bClr};padding-left:6px;` : '';
const slotsStr = c.slots && c.slots.length
? c.slots.map(s => WK_DAYS[s.day - 1] + ' ' + toHHMM(s.startMin) + '' + toHHMM(s.endMin)).join(' · ')
: '';
const attemptsHtml = c.attempts && c.attempts.length > 1
? c.attempts.map((a, i) => `<span class="${gradeClass(a.grade)}" style="font-size:12px;font-weight:700">${a.grade}${i < c.attempts.length - 1 ? '<span style="color:var(--gray-300);font-weight:400">→</span>' : ''}</span>`).join('')
: `<span class="${gradeClass(c.grade)}" style="font-size:14px;font-weight:700;min-width:28px;text-align:right">${fmtGrade(c.grade, c.passFail)}</span>`;
return `<div style="display:flex;align-items:center;padding:7px 0;border-bottom:1px solid var(--gray-100);gap:8px;${borderStyle}">
<div style="flex:1;min-width:0">
<div style="font-size:13px;font-weight:500;color:var(--gray-800);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${esc(c.name)}</div>
${c.credits ? `<div style="font-size:11px;color:var(--gray-400);margin-top:1px">${c.credits} cr.</div>` : ''}
${slotsStr ? `<div style="font-size:11px;color:${bClr || 'var(--primary)'};margin-top:1px">${slotsStr}</div>` : ''}
</div>
<div style="display:flex;align-items:center;gap:4px;flex-shrink:0">
${badge(st)}
${attemptsHtml}
<button class="btn btn-sm btn-secondary" onclick="showCourseModal('${c.id}')">✎</button>
<button class="btn btn-sm btn-danger" onclick="deleteCourse('${c.id}')">×</button>
</div>
</div>`;
}
function semCoursesByPlan(sc) {
if (!sc.length) return `<div style="color:var(--gray-400);font-size:13px;padding:2px 0 4px">No courses this semester</div>`;
let html = '';
// Plans that have courses in this semester (in S.studyPlans order)
S.studyPlans.forEach(plan => {
const planCrs = sc.filter(c => c.planId === plan.id);
if (!planCrs.length) return;
const pClr = planBaseColor(plan);
html += `<div style="margin-top:10px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:${pClr};padding-bottom:3px;border-bottom:2px solid ${pClr}">${esc(planDisplayName(plan))}</div>`;
// Blocks in plan order: major → minors → optionals → unroled
const ordered = [
...S.blocks.filter(b => b.planId === plan.id && b.role === 'major'),
...S.blocks.filter(b => b.planId === plan.id && b.role === 'minor'),
...S.blocks.filter(b => b.planId === plan.id && b.role === 'optional'),
...S.blocks.filter(b => b.planId === plan.id && !b.role),
].filter(blk => planCrs.some(c => c.blockId === blk.id));
ordered.forEach(blk => {
const blkCrs = planCrs.filter(c => c.blockId === blk.id);
const bClr = blockEffectiveColor(blk);
html += `<div style="border-left:3px solid ${bClr};padding-left:8px;margin-top:8px">`;
html += `<div style="font-size:12px;font-weight:700;color:${bClr};margin-bottom:2px">${esc(blk.name)}</div>`;
// Sub-blocks (in S.subBlocks order)
S.subBlocks.filter(sb => sb.blockId === blk.id && blkCrs.some(c => c.subBlockId === sb.id)).forEach(sb => {
const sbCrs = blkCrs.filter(c => c.subBlockId === sb.id);
const sbClr = subBlockEffectiveColor(sb);
html += `<div style="border-left:2px solid ${sbClr};padding-left:6px;margin-top:5px">`;
html += `<div style="font-size:11px;font-weight:600;color:${sbClr};margin-bottom:2px">${esc(sb.name)}</div>`;
sbCrs.forEach(c => { html += semCourseRow(c); });
html += `</div>`;
});
// Free courses (in block, no sub-block), preserve S.courses order
blkCrs.filter(c => !c.subBlockId).forEach(c => { html += semCourseRow(c); });
html += `</div>`;
});
// Courses in plan but no block
planCrs.filter(c => !c.blockId).forEach(c => { html += semCourseRow(c); });
});
// Courses without a plan
const noPlan = sc.filter(c => !c.planId);
if (noPlan.length) {
if (S.studyPlans.some(p => sc.some(c => c.planId === p.id))) {
html += `<div style="margin-top:10px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--gray-400);padding-bottom:3px;border-bottom:1px solid var(--gray-200)">Sans plan</div>`;
}
noPlan.forEach(c => { html += semCourseRow(c); });
}
return html;
}
function dashSemesters() { function dashSemesters() {
if (!S.semesters.length) return emptyState('📅', 'No semesters yet.<br>Tap + to create one!'); if (!S.semesters.length) return emptyState('📅', 'No semesters yet.<br>Tap + to create one!');
let html = ''; let html = '';
@@ -1077,12 +1166,9 @@ function dashSemesters() {
<button class="btn btn-sm btn-secondary" onclick="showSemesterModal('${sem.id}')" title="Edit semester">${GEAR_SVG}</button> <button class="btn btn-sm btn-secondary" onclick="showSemesterModal('${sem.id}')" title="Edit semester">${GEAR_SVG}</button>
<button class="btn btn-sm btn-danger" onclick="deleteSemester('${sem.id}')">Del</button> <button class="btn btn-sm btn-danger" onclick="deleteSemester('${sem.id}')">Del</button>
</div> </div>
</div>`; </div>
${semCoursesByPlan(sc)}
if (!sc.length) { html += `<div style="color:var(--gray-400);font-size:13px;padding:2px 0 4px">No courses this semester</div>`; } <button class="btn btn-secondary btn-full" style="margin-top:8px;font-size:13px" onclick="showCourseModalPrefilled('','','${sem.id}')">+ Add Course</button>
else html += coursesMgmtList(sc, true);
html += `<button class="btn btn-secondary btn-full" style="margin-top:8px;font-size:13px" onclick="showCourseModalPrefilled('','','${sem.id}')">+ Add Course</button>
</div>`; </div>`;
}); });
return html; return html;
@@ -1334,6 +1420,23 @@ function fromHHMM(str) {
return h * 60 + (m || 0); return h * 60 + (m || 0);
} }
function layoutOverlapping(events) {
if (!events.length) return [];
const sorted = [...events].sort((a, b) => a.startMin - b.startMin || b.endMin - a.endMin);
const colEnds = [];
const assigned = sorted.map(ev => {
let col = colEnds.findIndex(end => end <= ev.startMin);
if (col === -1) col = colEnds.length;
colEnds[col] = ev.endMin;
return { ev, col };
});
return assigned.map(item => {
const peers = assigned.filter(o => o.ev.startMin < item.ev.endMin && o.ev.endMin > item.ev.startMin);
const totalCols = peers.reduce((m, o) => Math.max(m, o.col + 1), 1);
return { ev: item.ev, col: item.col, totalCols };
});
}
function showWeekCalendar(semId) { function showWeekCalendar(semId) {
const sem = S.semesters.find(x => x.id === semId); const sem = S.semesters.find(x => x.id === semId);
if (!sem) return; if (!sem) return;
@@ -1424,17 +1527,21 @@ function renderWeekGrid(semId) {
const dayCols = WK_DAYS.map((_, di) => { const dayCols = WK_DAYS.map((_, di) => {
const dayNum = di + 1; const dayNum = di + 1;
const dayEvents = events.filter(e => e.day === dayNum); const dayEvents = events.filter(e => e.day === dayNum);
const evHtml = dayEvents.map(ev => { const laid = layoutOverlapping(dayEvents);
const evHtml = laid.map(({ ev, col, totalCols }) => {
const t = Math.max(ev.startMin, WK_START); const t = Math.max(ev.startMin, WK_START);
const b = Math.min(ev.endMin, WK_END); const b = Math.min(ev.endMin, WK_END);
if (t >= b) return ''; if (t >= b) return '';
const top = (t - WK_START) * PX_PER_MIN; const top = (t - WK_START) * PX_PER_MIN;
const height = Math.max((b - t) * PX_PER_MIN, 18); const height = Math.max((b - t) * PX_PER_MIN, 18);
const pct = (100 / totalCols).toFixed(2);
const left = `calc(${(col / totalCols * 100).toFixed(2)}% + 2px)`;
const width = `calc(${pct}% - 4px)`;
const loc = ev.location ? `<div style="opacity:.8;font-weight:400;font-size:9px;margin-top:1px">${esc(ev.location)}</div>` : ''; const loc = ev.location ? `<div style="opacity:.8;font-weight:400;font-size:9px;margin-top:1px">${esc(ev.location)}</div>` : '';
const onclick = ev._courseId const onclick = ev._courseId
? `showCourseModal('${ev._courseId}')` ? `showCourseModal('${ev._courseId}')`
: `showEventModal('${ev.semesterId}','${ev.id}')`; : `showEventModal('${ev.semesterId}','${ev.id}')`;
return `<div class="wk-event" style="top:${top}px;height:${height}px;background:${esc(ev.color)}" return `<div class="wk-event" style="top:${top}px;height:${height}px;left:${left};width:${width};background:${esc(ev.color)}"
onclick="${onclick}" title="${esc(ev.title)} ${toHHMM(ev.startMin)}${toHHMM(ev.endMin)}"> onclick="${onclick}" title="${esc(ev.title)} ${toHHMM(ev.startMin)}${toHHMM(ev.endMin)}">
${esc(ev.title)}${height > 30 ? `<br><span style="opacity:.8;font-weight:400">${toHHMM(ev.startMin)}</span>${loc}` : ''} ${esc(ev.title)}${height > 30 ? `<br><span style="opacity:.8;font-weight:400">${toHHMM(ev.startMin)}</span>${loc}` : ''}
</div>`; </div>`;
@@ -1624,48 +1731,11 @@ function overlayClick(e) { if (e.target.id === 'overlay') closeModal(); }
// ══════════════════════════════════════════════════════ // ══════════════════════════════════════════════════════
// COURSE MODAL // COURSE MODAL
// ══════════════════════════════════════════════════════ // ══════════════════════════════════════════════════════
let _slots = []; // working slot list while course modal is open let _slots = []; // working slot list while course modal is open
let _attempts = []; // working attempt list while course modal is open
function renderAttemptsUI() { function onGradeInput(v) {
if (!_attempts.length) return `<div style="font-size:12px;color:var(--gray-400);padding:2px 0">No attempts logged yet</div>`; const fg2 = document.getElementById('fg-grade2');
return _attempts.map((a, i) => { if (fg2) fg2.style.display = (v !== '' && parseFloat(v) < 4) ? 'block' : 'none';
const sem = S.semesters.find(s => s.id === a.semesterId);
const gCls = +a.grade >= 4 ? 'var(--success)' : 'var(--danger)';
const label = sem ? esc(sem.name) : '';
return `<div style="display:flex;align-items:center;gap:6px;padding:5px 0;border-bottom:1px solid var(--gray-100)">
<span style="font-size:11px;font-weight:700;color:var(--gray-400);min-width:18px">#${i+1}</span>
<span style="font-size:14px;font-weight:700;color:${gCls}">${a.grade}</span>
${+a.grade >= 4
? '<span class="badge badge-pass" style="font-size:10px;padding:1px 5px">Passed</span>'
: '<span class="badge badge-fail" style="font-size:10px;padding:1px 5px">Failed</span>'}
${label ? `<span style="font-size:11px;color:var(--gray-500)">${label}</span>` : ''}
<button onclick="removeAttempt(${i})" style="margin-left:auto;background:none;border:none;cursor:pointer;color:var(--gray-400);font-size:16px;line-height:1;padding:0 2px">×</button>
</div>`;
}).join('');
}
function logAttempt() {
const gEl = document.getElementById('f-att-grade');
const sEl = document.getElementById('f-att-sem');
const g = parseFloat(gEl?.value);
if (isNaN(g) || g < 1 || g > 6) { alert('Enter a valid grade between 1 and 6.'); return; }
if (_attempts.length >= 2 && !confirm('You already have 2 attempts logged. Add a third?')) return;
_attempts.push({ id: uid(), grade: g, semesterId: sEl?.value || '' });
// propagate latest grade to the main grade field
const mainG = document.getElementById('f-grade');
if (mainG) mainG.value = g;
if (gEl) gEl.value = '';
const el = document.getElementById('attempts-list');
if (el) el.innerHTML = renderAttemptsUI();
}
function removeAttempt(idx) {
_attempts.splice(idx, 1);
const mainG = document.getElementById('f-grade');
if (mainG && _attempts.length) mainG.value = _attempts[_attempts.length - 1].grade;
const el = document.getElementById('attempts-list');
if (el) el.innerHTML = renderAttemptsUI();
} }
function renderSlotsUI() { function renderSlotsUI() {
@@ -1714,10 +1784,13 @@ function showCourseModal(id, prefill = {}) {
const blockOpts = buildBlockOpts(curPlan, curBlock); const blockOpts = buildBlockOpts(curPlan, curBlock);
const pfRaw = c ? c.passFail : null; const pfRaw = c ? c.passFail : null;
const pfMode = pfRaw === 'passed' || pfRaw === true ? 'passed' : pfRaw === 'failed' ? 'failed' : 'default'; const pfMode = pfRaw === 'passed' || pfRaw === true ? 'passed' : pfRaw === 'failed' ? 'failed' : 'default';
_slots = c && c.slots ? c.slots.map(s => ({ ...s })) : []; _slots = c && c.slots ? c.slots.map(s => ({ ...s })) : [];
_attempts = c && c.attempts ? c.attempts.map(a => ({ ...a })) : []; // Pre-populate grade fields from attempts (fallback to course.grade for legacy courses)
const semOptsAttempt = `<option value="">— none —</option>` + S.semesters.map(s => `<option value="${s.id}">${esc(s.name)}</option>`).join(''); const a0 = c?.attempts?.[0];
const a1 = c?.attempts?.[1];
const g1val = pfMode === 'default' ? (a0 != null ? a0.grade : (c?.grade != null && c.grade !== '' ? c.grade : '')) : '';
const g2val = pfMode === 'default' && a1 != null ? a1.grade : '';
const g2vis = g1val !== '' && parseFloat(g1val) < 4;
openModal(`<div class="modal"> openModal(`<div class="modal">
<div class="modal-header"> <div class="modal-header">
<span class="modal-title">${c ? 'Edit Course' : 'Add Course'}</span> <span class="modal-title">${c ? 'Edit Course' : 'Add Course'}</span>
@@ -1734,7 +1807,11 @@ function showCourseModal(id, prefill = {}) {
</div> </div>
<div class="form-group" id="fg-grade" style="display:${pfMode === 'default' ? 'block' : 'none'}"> <div class="form-group" id="fg-grade" style="display:${pfMode === 'default' ? 'block' : 'none'}">
<label>Grade (1 6)</label> <label>Grade (1 6)</label>
<input id="f-grade" type="number" value="${c && pfMode === 'default' && 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="${g1val}" placeholder="e.g. 5.5" min="1" max="6" step="0.25" oninput="onGradeInput(this.value)">
</div>
<div class="form-group" id="fg-grade2" style="display:${g2vis ? 'block' : 'none'}">
<label>Retake (1 6)</label>
<input id="f-grade-2" type="number" value="${g2val}" placeholder="e.g. 4.5" min="1" max="6" step="0.25">
</div> </div>
</div> </div>
<input type="hidden" id="f-passfail" value="${pfMode}"> <input type="hidden" id="f-passfail" value="${pfMode}">
@@ -1743,21 +1820,6 @@ function showCourseModal(id, prefill = {}) {
<button type="button" class="pf-seg-btn${pfMode==='passed'?' seg-passed':''}" onclick="onPFChange('passed')">Passed</button> <button type="button" class="pf-seg-btn${pfMode==='passed'?' seg-passed':''}" onclick="onPFChange('passed')">Passed</button>
<button type="button" class="pf-seg-btn${pfMode==='failed'?' seg-failed':''}" onclick="onPFChange('failed')">Failed</button> <button type="button" class="pf-seg-btn${pfMode==='failed'?' seg-failed':''}" onclick="onPFChange('failed')">Failed</button>
</div> </div>
<div class="form-group" style="margin-bottom:10px">
<label>Attempt History <span style="font-size:11px;color:var(--gray-400);font-weight:400">(up to 2 allowed · updates grade)</span></label>
<div id="attempts-list" style="margin-bottom:6px">${renderAttemptsUI()}</div>
<div style="display:grid;grid-template-columns:90px 1fr auto;gap:5px;align-items:end">
<div>
<div style="font-size:10px;font-weight:600;color:var(--gray-500);text-transform:uppercase;letter-spacing:.04em;margin-bottom:3px">Grade</div>
<input id="f-att-grade" type="number" min="1" max="6" step="0.25" placeholder="e.g. 3.5" style="width:100%;padding:7px 8px;border:1.5px solid var(--gray-200);border-radius:8px;font-size:14px;background:var(--bg-surface);color:var(--gray-800)">
</div>
<div>
<div style="font-size:10px;font-weight:600;color:var(--gray-500);text-transform:uppercase;letter-spacing:.04em;margin-bottom:3px">Semester</div>
<select id="f-att-sem" style="width:100%;padding:7px 6px;border:1.5px solid var(--gray-200);border-radius:8px;font-size:13px;background:var(--bg-surface);color:var(--gray-800)">${semOptsAttempt}</select>
</div>
<button class="btn btn-primary" style="padding:7px 12px;font-size:16px;line-height:1" onclick="logAttempt()">+</button>
</div>
</div>
<div class="form-group"> <div class="form-group">
<label>Semester</label> <label>Semester</label>
<select id="f-semester"> <select id="f-semester">
@@ -1851,6 +1913,8 @@ function onPFChange(v) {
if (inp) inp.value = v; if (inp) inp.value = v;
const fgGrade = document.getElementById('fg-grade'); const fgGrade = document.getElementById('fg-grade');
if (fgGrade) fgGrade.style.display = v === 'default' ? 'block' : 'none'; if (fgGrade) fgGrade.style.display = v === 'default' ? 'block' : 'none';
const fg2 = document.getElementById('fg-grade2');
if (fg2 && v !== 'default') fg2.style.display = 'none';
document.querySelectorAll('.pf-seg-btn').forEach(btn => { document.querySelectorAll('.pf-seg-btn').forEach(btn => {
btn.className = 'pf-seg-btn'; btn.className = 'pf-seg-btn';
if (btn.textContent.trim().toLowerCase() === v) btn.classList.add('seg-' + v); if (btn.textContent.trim().toLowerCase() === v) btn.classList.add('seg-' + v);
@@ -1863,7 +1927,9 @@ function saveCourse(id) {
const pfVal = document.getElementById('f-passfail').value || 'default'; const pfVal = document.getElementById('f-passfail').value || 'default';
const passFail = pfVal === 'default' ? null : pfVal; const passFail = pfVal === 'default' ? null : pfVal;
const gradeEl = document.getElementById('f-grade'); const gradeEl = document.getElementById('f-grade');
const grade2El = document.getElementById('f-grade-2');
const gradeV = (!passFail && gradeEl) ? gradeEl.value : ''; const gradeV = (!passFail && gradeEl) ? gradeEl.value : '';
const grade2V = (!passFail && grade2El && grade2El.closest('#fg-grade2')?.style.display !== 'none') ? grade2El.value : '';
const semId = document.getElementById('f-semester').value; const semId = document.getElementById('f-semester').value;
const planId = document.getElementById('f-plan').value; const planId = document.getElementById('f-plan').value;
const fg = document.getElementById('fg-block'); const fg = document.getElementById('fg-block');
@@ -1874,7 +1940,14 @@ function saveCourse(id) {
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 = (passFail === null && gradeV !== '') ? parseFloat(gradeV) : null; const grade1 = (passFail === null && gradeV !== '') ? parseFloat(gradeV) : null;
const grade2 = (passFail === null && grade2V !== '') ? parseFloat(grade2V) : null;
const grade = grade2 !== null ? grade2 : grade1;
// Build attempts from the two inline grade fields
const attempts = [];
if (grade1 !== null) attempts.push({ id: uid(), grade: grade1, semesterId: semId });
if (grade2 !== null) attempts.push({ id: uid(), grade: grade2, semesterId: semId });
// Auto-capture the slot picker if user filled it in without clicking "+ Add slot" // Auto-capture the slot picker if user filled it in without clicking "+ Add slot"
const pickerDay = parseInt((document.getElementById('f-slot-day') || {}).value || '1'); const pickerDay = parseInt((document.getElementById('f-slot-day') || {}).value || '1');
@@ -1885,8 +1958,7 @@ function saveCourse(id) {
if (!alreadyIn) _slots.push({ day: pickerDay, startMin: pickerStart, endMin: pickerEnd }); if (!alreadyIn) _slots.push({ day: pickerDay, startMin: pickerStart, endMin: pickerEnd });
} }
const slots = [..._slots]; const slots = [..._slots];
const attempts = [..._attempts];
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, subBlockId, passFail, slots, attempts }; if (i >= 0) S.courses[i] = { id, name, credits: +credits, semesterId: semId, grade, planId, blockId, subBlockId, passFail, slots, attempts };
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE_NAME = 'credittracker-v33'; const CACHE_NAME = 'credittracker-v36';
const ASSETS = [ const ASSETS = [
'./index.html', './index.html',
'./manifest.json', './manifest.json',