import { state, savePrefs } from './state.js'; import { tr } from './i18n.js'; import { api } from './api.js'; import { tOff, o2d, dTot, fd, fs, editable, now, heuteColor, isCollapsed, toggleCollapse, calc, dcls, dlbl } from './goals.js'; import { tpl, showToast } from './ui.js'; function saveGoal(g) { api('PATCH', 'goals/' + g.id, { name: g.name, unit: g.unit, daily: g.daily, days: g.days, start: g.start, sets: g.sets, }).catch(e => { if (e.status !== 401) showToast(tr('errSave')); }); } function addSet(gid, off) { const g = state.goals.find(x => x.id === gid); if (!g || !editable(g, off)) return; const k = gid + '_' + off, amt = parseInt(state.addAmt[k] || '0', 10); if (amt <= 0) return; if (!g.sets[String(off)]) g.sets[String(off)] = []; g.sets[String(off)].push({ amount: amt, time: off === tOff(g) ? now() : 'โ€”' }); state.addAmt[k] = ''; saveGoal(g); render(); } function remSet(gid, off, idx) { const g = state.goals.find(x => x.id === gid); if (!g || !editable(g, off)) return; g.sets[String(off)].splice(idx, 1); saveGoal(g); render(); } function delGoal(id) { if (!confirm(tr('confirmDelete'))) return; state.goals = state.goals.filter(g => g.id !== id); render(); api('DELETE', 'goals/' + id).catch(() => showToast(tr('errDelete'))); } function selD(gid, off) { const g = state.goals.find(x => x.id === gid); if (!g) return; state.selDay[gid] = state.selDay[gid] === off ? null : off; render(); } function startRen(id) { const g = state.goals.find(x => x.id === id); if (!g) return; state.renamingId = id; state.renameVal = g.name; render(); setTimeout(() => { const el = document.getElementById('ri' + id); if (el) { el.focus(); el.select(); } }, 50); } function commitRen(id) { const g = state.goals.find(x => x.id === id); if (g && state.renameVal.trim()) { g.name = state.renameVal.trim(); saveGoal(g); } state.renamingId = null; render(); } function cancelRen() { state.renamingId = null; render(); } function buildNameWrap(g) { if (state.renamingId === g.id) { const el = tpl('tpl-name-edit'); const inp = el.querySelector('.ren-input'); inp.id = 'ri' + g.id; inp.value = g.name; inp.dataset.g = g.id; return el; } const el = tpl('tpl-name-view'); el.querySelector('.goal-name').textContent = g.name; el.querySelector('.btn-ren').dataset.g = g.id; return el; } function buildPanel(g, off) { const t = tOff(g), sets = g.sets[String(off)] || [], tot = dTot(g, off), ed = editable(g, off); const lbl = off === t ? tr('heute') : off === t - 1 ? tr('gestern') : null; const k = g.id + '_' + off; const el = tpl('tpl-panel'); el.querySelector('.dpanel-title').textContent = (lbl ? lbl + ' โ€” ' : '') + fd(o2d(g, off)); el.querySelector('.dpanel-sub').textContent = tot + ' / ' + g.daily + ' ' + g.unit; const body = el.querySelector('.dpanel-body'); if (sets.length) { for (let i = 0; i < sets.length; i++) { const s = sets[i], row = tpl('tpl-set-row'), span = row.querySelector('span'); if (s.time !== 'โ€”') { const st = document.createElement('span'); st.className = 'stime'; st.textContent = s.time + ' ยท'; span.appendChild(st); span.appendChild(document.createTextNode(' ')); } const strong = document.createElement('strong'); strong.textContent = s.amount; span.appendChild(strong); span.appendChild(document.createTextNode(' ' + g.unit)); const btn = row.querySelector('.sdel'); if (ed) { btn.dataset.g = g.id; btn.dataset.o = off; btn.dataset.i = i; } else { btn.remove(); } body.appendChild(row); } } else { body.appendChild(tpl('tpl-nosets')); } if (ed) { const addRow = tpl('tpl-add-row'); const inp = addRow.querySelector('.num-in'); inp.placeholder = g.daily; inp.value = state.addAmt[k] || ''; inp.dataset.k = k; inp.dataset.g = g.id; inp.dataset.o = off; const abtn = addRow.querySelector('.btn-as'); abtn.dataset.g = g.id; abtn.dataset.o = off; addRow.querySelector('.ulbl').textContent = g.unit; body.appendChild(addRow); } return el; } function buildCard(g) { const c = calc(g), t = c.tOff; const fc = c.surplus > 0 ? 'var(--blue)' : c.st === 0 && c.buf >= 0 ? 'var(--green)' : c.dailyDelta <= 0 ? 'var(--green)' : c.dailyDelta <= g.daily * .2 ? 'var(--amber)' : 'var(--red)'; let bc, bt; const bufStr = (c.buf > 0 ? '+' : '') + c.buf; if (c.ok && c.surplus > 0) { bc = 'b-buf'; bt = bufStr; } else if (c.ok && c.buf >= 0) { bc = 'b-done'; bt = bufStr; } else if (c.dailyDelta <= 0) { bc = 'b-ok'; bt = bufStr; } else if (c.dailyDelta <= g.daily * .2) { bc = 'b-warn'; bt = bufStr; } else { bc = 'b-danger'; bt = bufStr; } let el; if (isCollapsed(g.id)) { el = tpl('tpl-card-collapsed'); if (c.ok) el.classList.add('done'); el.querySelector('.card-hdr').dataset.g = g.id; const bd = el.querySelector('.card-bd'); bd.insertBefore(buildNameWrap(g), bd.firstElementChild); const hc = heuteColor(c.tdone, g.daily); el.querySelector('.m-dr').textContent = c.dr; el.querySelector('.m-end').textContent = fs(c.end); const mH = el.querySelector('.m-heute'); mH.textContent = c.tdone + '/' + g.daily; mH.style.color = hc; el.querySelector('.m-total').textContent = c.done + '/' + c.tot; const badge = el.querySelector('.badge'); badge.className = 'badge ' + bc; badge.textContent = bt; const fill = el.querySelector('.prog-fill'); fill.style.width = c.pct + '%'; fill.style.background = fc; return el; } el = tpl('tpl-card-expanded'); if (c.ok) el.classList.add('done'); el.querySelector('.card-hdr').dataset.g = g.id; const bd = el.querySelector('.card-bd'); bd.insertBefore(buildNameWrap(g), bd.firstElementChild); el.querySelector('.m-dr').textContent = c.dr; el.querySelector('.m-end').textContent = fs(c.end); const badge = el.querySelector('.badge'); badge.className = 'badge ' + bc; badge.textContent = bt; const fill = el.querySelector('.prog-fill'); fill.style.width = c.pct + '%'; fill.style.background = fc; el.querySelector('.pr-done').textContent = c.done + ' ' + g.unit + ' ' + tr('doneLabel'); el.querySelector('.pr-pct').textContent = c.pct + '% ' + tr('ofLabel') + ' ' + c.tot; el.querySelector('.sv-tdone').textContent = c.tdone; el.querySelector('.sv-daily').textContent = g.daily; el.querySelector('.sv-st').textContent = c.st; el.querySelector('.sv-noch').style.color = heuteColor(c.tdone, g.daily); el.querySelectorAll('.sunit').forEach(u => { u.textContent = g.unit; }); const sel = state.selDay[g.id] != null ? state.selDay[g.id] : t; const dotsWrap = el.querySelector('.dots-wrap'); for (let i = 0; i < g.days; i++) { const it = i === t, iy = i === t - 1, is = sel === i, ed = editable(g, i); const dot = tpl('tpl-dot'); dot.className = dcls(g, i) + (is ? ' rs' : it ? ' rt' : iy && t > 0 ? ' ry' : ''); if (i <= t) { dot.dataset.g = g.id; dot.dataset.d = i; } dot.textContent = dlbl(g, i); dotsWrap.appendChild(dot); } if (sel != null) el.insertBefore(buildPanel(g, sel), el.querySelector('.card-foot')); el.querySelector('.btn-del').dataset.g = g.id; return el; } function buildQuickBook() { const active = state.goals.filter(g => { const c = calc(g); return tOff(g) < g.days && (c.buf < 0 || (c.tdone < g.daily && c.buf < g.daily)); }); if (!active.length) return null; const frag = document.createDocumentFragment(); const lbl = document.createElement('div'); lbl.className = 'sec-lbl'; lbl.textContent = tr('qbLabel'); frag.appendChild(lbl); const card = document.createElement('div'); card.className = 'card qb-card'; for (const g of active) { const c = calc(g), k = g.id + '_' + c.tOff; const row = tpl('tpl-qb-row'); row.querySelector('.qb-name').textContent = g.name; const stat = row.querySelector('.qb-stat'); stat.textContent = c.tdone + '/' + g.daily; stat.style.color = heuteColor(c.tdone, g.daily); const inp = row.querySelector('.num-in'); inp.placeholder = g.daily; inp.value = state.addAmt[k] || ''; inp.dataset.k = k; inp.dataset.g = g.id; inp.dataset.o = c.tOff; const btn = row.querySelector('.btn-as'); btn.dataset.g = g.id; btn.dataset.o = c.tOff; card.appendChild(row); } frag.appendChild(card); return frag; } function calcAwards() { let units = 0; for (const g of state.goals) { if (tOff(g) >= g.days) units += Math.floor(g.days / 30); } const gold = Math.floor(units / 25); units %= 25; const silver = Math.floor(units / 5); const bronze = units % 5; return { gold, silver, bronze }; } export function render() { const m = document.getElementById('main'); const frag = document.createDocumentFragment(); if (!state.prefs.hd) { const hint = tpl('tpl-hint'); hint.querySelector('.hclose').onclick = () => { state.prefs.hd = 1; savePrefs(); hint.remove(); }; frag.appendChild(hint); } const aw = calcAwards(); if (aw.gold || aw.silver || aw.bronze) { const awards = document.createElement('div'); awards.className = 'awards'; for (const [emoji, count] of [['๐Ÿฅ‡', aw.gold], ['๐Ÿฅˆ', aw.silver], ['๐Ÿฅ‰', aw.bronze]]) { for (let i = 0; i < count; i++) { const sp = document.createElement('span'); sp.className = 'aw'; sp.textContent = emoji; awards.appendChild(sp); } } frag.appendChild(awards); } if (!state.goals.length) { frag.appendChild(tpl('tpl-empty')); m.innerHTML = ''; m.appendChild(frag); wire(); return; } if (state.userName) { const gr = document.createElement('div'); gr.className = 'greeting'; gr.textContent = tr('hello').replace('{n}', state.userName); frag.appendChild(gr); } const qb = buildQuickBook(); if (qb) frag.appendChild(qb); const open = [], done = []; for (const g of state.goals) { if (calc(g).ok) done.push(g); else open.push(g); } if (open.length) { const sl = document.createElement('div'); sl.className = 'sec-lbl'; sl.textContent = tr('openLabel'); frag.appendChild(sl); for (const g of open) frag.appendChild(buildCard(g)); } if (done.length) { const sl2 = document.createElement('div'); sl2.className = 'sec-lbl'; sl2.textContent = tr('doneToday'); frag.appendChild(sl2); for (const g of done) frag.appendChild(buildCard(g)); } m.innerHTML = ''; m.appendChild(frag); wire(); } function wire() { document.querySelectorAll('.card-hdr[data-g]').forEach(el => { el.onclick = function(e) { if (e.target.classList.contains('btn-ren') || e.target.classList.contains('ren-input')) return; toggleCollapse(this.dataset.g); render(); }; }); document.querySelectorAll('.btn-ren').forEach(b => { b.onclick = function(e) { e.stopPropagation(); startRen(this.dataset.g); }; }); document.querySelectorAll('.ren-input').forEach(inp => { const gid = inp.dataset.g; inp.oninput = function() { state.renameVal = this.value; }; inp.onkeydown = function(e) { if (e.key === 'Enter') commitRen(gid); if (e.key === 'Escape') cancelRen(); }; inp.onblur = function() { commitRen(gid); }; }); document.querySelectorAll('.de, .dl').forEach(d => { d.onclick = function(e) { e.stopPropagation(); selD(this.dataset.g, parseInt(this.dataset.d, 10)); }; }); document.querySelectorAll('.btn-as').forEach(b => { b.onclick = function() { addSet(this.dataset.g, parseInt(this.dataset.o, 10)); }; }); document.querySelectorAll('.num-in').forEach(inp => { const k = inp.dataset.k, g = inp.dataset.g, o = parseInt(inp.dataset.o, 10); inp.oninput = function() { state.addAmt[k] = this.value; }; inp.onkeydown = function(e) { if (e.key === 'Enter') addSet(g, o); }; }); document.querySelectorAll('.sdel').forEach(b => { b.onclick = function() { remSet(this.dataset.g, parseInt(this.dataset.o, 10), parseInt(this.dataset.i, 10)); }; }); document.querySelectorAll('.btn-del').forEach(b => { b.onclick = function() { delGoal(this.dataset.g); }; }); }