dudi/public/js/render.js

287 lines
12 KiB
JavaScript
Raw Normal View History

2026-05-08 09:46:43 +00:00
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 || !editable(g, off)) 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);
const lbl = off === t ? tr('heute') : tr('gestern'), k = g.id + '_' + off;
const el = tpl('tpl-panel');
el.querySelector('.dpanel-title').textContent = 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');
btn.dataset.g = g.id; btn.dataset.o = off; btn.dataset.i = i;
body.appendChild(row);
}
} else {
body.appendChild(tpl('tpl-nosets'));
}
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 ? '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) { 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 (ed) { 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').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); };
});
}