diff --git a/index.html b/index.html index 6ad3399..121eaf5 100644 --- a/index.html +++ b/index.html @@ -430,10 +430,10 @@ .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-event { - position: absolute; left: 2px; right: 2px; + position: absolute; border-radius: 4px; font-size: 10px; font-weight: 600; 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); } .ev-swatch { @@ -944,10 +944,10 @@ function dashPlans() { `; if (!isCollapsed) { - html += roleGroupSection(plan.id, 'Major', 'major', majors, pc, pClr); - html += roleGroupSection(plan.id, 'Minor', 'minor', minors, pc, pClr); + majors.forEach((blk, i) => { html += planBlockSection(plan.id, blk, pc, majors.length, i); }); + minors.forEach((blk, i) => { html += planBlockSection(plan.id, blk, pc, minors.length, i); }); 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); }); const nbc = pc.filter(c => !c.blockId); @@ -955,6 +955,11 @@ function dashPlans() { html += `
Unassigned courses
`; html += coursesMgmtList(nbc); } + html += `
+ ${!majors.length ? `` : ''} + + ${settings.showOptional ? `` : ''} +
`; } html += ``; }); @@ -1052,6 +1057,90 @@ function planSubBlockSection(planId, blockId, sb, courses, parentColor, inEdit, const CAL_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) => `${a.grade}${i < c.attempts.length - 1 ? '' : ''}`).join('') + : `${fmtGrade(c.grade, c.passFail)}`; + return `
+
+
${esc(c.name)}
+ ${c.credits ? `
${c.credits} cr.
` : ''} + ${slotsStr ? `
${slotsStr}
` : ''} +
+
+ ${badge(st)} + ${attemptsHtml} + + +
+
`; +} + +function semCoursesByPlan(sc) { + if (!sc.length) return `
No courses this semester
`; + 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 += `
${esc(planDisplayName(plan))}
`; + + // 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 += `
`; + html += `
${esc(blk.name)}
`; + + // 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 += `
`; + html += `
${esc(sb.name)}
`; + sbCrs.forEach(c => { html += semCourseRow(c); }); + html += `
`; + }); + + // Free courses (in block, no sub-block), preserve S.courses order + blkCrs.filter(c => !c.subBlockId).forEach(c => { html += semCourseRow(c); }); + html += `
`; + }); + + // 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 += `
Sans plan
`; + } + noPlan.forEach(c => { html += semCourseRow(c); }); + } + + return html; +} + function dashSemesters() { if (!S.semesters.length) return emptyState('📅', 'No semesters yet.
Tap + to create one!'); let html = ''; @@ -1077,12 +1166,9 @@ function dashSemesters() { - `; - - if (!sc.length) { html += `
No courses this semester
`; } - else html += coursesMgmtList(sc, true); - - html += ` + + ${semCoursesByPlan(sc)} + `; }); return html; @@ -1334,6 +1420,23 @@ function fromHHMM(str) { 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) { const sem = S.semesters.find(x => x.id === semId); if (!sem) return; @@ -1424,17 +1527,21 @@ function renderWeekGrid(semId) { const dayCols = WK_DAYS.map((_, di) => { const dayNum = di + 1; 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 b = Math.min(ev.endMin, WK_END); if (t >= b) return ''; const top = (t - WK_START) * PX_PER_MIN; 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 ? `
${esc(ev.location)}
` : ''; const onclick = ev._courseId ? `showCourseModal('${ev._courseId}')` : `showEventModal('${ev.semesterId}','${ev.id}')`; - return `
${esc(ev.title)}${height > 30 ? `
${toHHMM(ev.startMin)}${loc}` : ''}
`; @@ -1624,48 +1731,11 @@ function overlayClick(e) { if (e.target.id === 'overlay') closeModal(); } // ══════════════════════════════════════════════════════ // COURSE MODAL // ══════════════════════════════════════════════════════ -let _slots = []; // working slot list while course modal is open -let _attempts = []; // working attempt list while course modal is open +let _slots = []; // working slot list while course modal is open -function renderAttemptsUI() { - if (!_attempts.length) return `
No attempts logged yet
`; - return _attempts.map((a, i) => { - 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 `
- #${i+1} - ${a.grade} - ${+a.grade >= 4 - ? 'Passed' - : 'Failed'} - ${label ? `${label}` : ''} - -
`; - }).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 onGradeInput(v) { + const fg2 = document.getElementById('fg-grade2'); + if (fg2) fg2.style.display = (v !== '' && parseFloat(v) < 4) ? 'block' : 'none'; } function renderSlotsUI() { @@ -1714,10 +1784,13 @@ function showCourseModal(id, prefill = {}) { const blockOpts = buildBlockOpts(curPlan, curBlock); const pfRaw = c ? c.passFail : null; const pfMode = pfRaw === 'passed' || pfRaw === true ? 'passed' : pfRaw === 'failed' ? 'failed' : 'default'; - _slots = c && c.slots ? c.slots.map(s => ({ ...s })) : []; - _attempts = c && c.attempts ? c.attempts.map(a => ({ ...a })) : []; - const semOptsAttempt = `` + S.semesters.map(s => ``).join(''); - + _slots = c && c.slots ? c.slots.map(s => ({ ...s })) : []; + // Pre-populate grade fields from attempts (fallback to course.grade for legacy courses) + 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(` @@ -1743,21 +1820,6 @@ function showCourseModal(id, prefill = {}) { -
- -
${renderAttemptsUI()}
-
-
-
Grade
- -
-
-
Semester
- -
- -
-