import { api } from './api.js'; import { tr, setLocale, LOCALE, ldoc } from './i18n.js'; import { state } from './state.js'; import { tpl, showSheet, closeOv, showToast, updateHeader } from './ui.js'; import { render } from './render.js'; import { showChangePassword, showLogin } from './auth.js'; export function escHtml(s) { return String(s).replace(/&/g, '&').replace(//g, '>'); } export function openNew() { const c = tpl('tpl-new-goal'); showSheet(c, true); const name = c.querySelector('.ng-name'), unit = c.querySelector('.ng-unit'); const daily = c.querySelector('.ng-daily'), weekly = c.querySelector('.ng-weekly'); const days = c.querySelector('.ng-days'), sub = c.querySelector('.ng-sub'); daily.addEventListener('input', () => { if (daily.value) weekly.value = Math.round(parseFloat(daily.value) * 7 * 100) / 100; }); weekly.addEventListener('input', () => { if (weekly.value) daily.value = Math.round(parseFloat(weekly.value) / 7 * 100) / 100; }); setTimeout(() => name.focus(), 50); c.querySelector('.ng-can').onclick = closeOv; sub.onclick = () => { const nv = (name.value || '').trim(), uv = (unit.value || '').trim() || tr('unitDefault'); const dv = parseFloat(daily.value) || 1, dyv = parseInt(days.value, 10) || 30; if (!nv) { name.focus(); return; } sub.disabled = true; api('POST', 'goals', { name: nv, unit: uv, daily: dv, days: dyv, start: state.TODAY.toISOString() }) .then(r => { state.goals.push({ id: r.id, name: r.name, unit: r.unit, daily: r.daily, days: r.days, start: r.start, sets: r.sets || {} }); closeOv(); render(); }) .catch(e => { sub.disabled = false; if (e.status !== 401) showToast(tr('errCreate')); }); }; // closes sub.onclick } // closes openNew export function openData() { const c = tpl('tpl-data-menu'); showSheet(c, true); c.querySelector('.dm-cls').onclick = closeOv; c.querySelector('.dm-name').onclick = () => { const nc = tpl('tpl-change-name'); showSheet(nc, true); const inp = nc.querySelector('.cn-name'), errEl = nc.querySelector('.login-err'), sub = nc.querySelector('.cn-sub'); inp.value = state.userName; setTimeout(() => { inp.focus(); inp.select(); }, 50); nc.querySelector('.cn-can').onclick = closeOv; sub.onclick = () => { const nv = inp.value.trim(); if (!nv) { errEl.textContent = tr('errNameEmpty'); errEl.style.display = ''; return; } sub.disabled = true; sub.textContent = '…'; api('PATCH', 'me', { name: nv }) .then(r => { state.userName = r.name; closeOv(); render(); showToast(tr('nameSaved')); }) .catch(() => { sub.disabled = false; sub.textContent = tr('save'); showToast(tr('errNameSave')); }); }; }; c.querySelector('.dm-cpw').onclick = () => { closeOv(); showChangePassword(); }; c.querySelector('.dm-lgout').onclick = () => { api('POST', 'logout').then(() => { state.goals = []; closeOv(); render(); showLogin(); }); }; const adminBtn = c.querySelector('.dm-admin'); if (state.isAdmin) { adminBtn.style.display = ''; adminBtn.onclick = () => { closeOv(); openAdmin(); }; } c.querySelector('.dm-inv').onclick = () => { const ic = tpl('tpl-invite-form'); showSheet(ic, true); const invName = ic.querySelector('.inv-name'); setTimeout(() => invName.focus(), 50); ic.querySelector('.inv-cancel').onclick = closeOv; ic.querySelector('.inv-gen').onclick = function() { const note = invName.value.trim(), btn = this; btn.disabled = true; btn.textContent = '…'; api('POST', 'invite', { note }) .then(res => { const lc = tpl('tpl-invite-link'); lc.querySelector('.stitle').textContent = tr('inviteLinkTitle') + (note ? ' — ' + note : ''); const urlInp = lc.querySelector('.il-url'); urlInp.value = res.url; showSheet(lc, true); lc.querySelector('.il-close').onclick = closeOv; lc.querySelector('.il-copy').onclick = () => { navigator.clipboard.writeText(res.url).then(() => { showToast(tr('linkCopied')); closeOv(); }); }; setTimeout(() => urlInp.select(), 50); }) .catch(err => { btn.disabled = false; btn.textContent = tr('generateLink'); showToast(err.message || tr('errGenerate')); }); }; }; c.querySelector('.dm-invlist').onclick = () => { api('GET', 'invites').then(list => { const statusLabel = { pending: tr('statusPending'), used: tr('statusUsed'), expired: tr('statusExpired') }; const statusColor = { pending: 'var(--amber)', used: 'var(--green)', expired: 'var(--red)' }; const lc = tpl('tpl-invite-list'); const body = lc.querySelector('.dpanel-body'); if (!list.length) { const empty = document.createElement('div'); empty.className = 'nosets'; empty.style.padding = '16px'; empty.textContent = tr('noInvites'); body.appendChild(empty); } else { for (const inv of list) { const label = inv.note || new Date(inv.created_at).toLocaleDateString(ldoc(), { day: 'numeric', month: 'short', year: 'numeric' }); const detail = inv.used_by_email ? (tr('acceptedBy') + ' ' + inv.used_by_email) : (inv.status === 'pending' ? tr('expiresAt') + ' ' + new Date(inv.expires_at).toLocaleDateString(ldoc(), { day: 'numeric', month: 'short' }) : ''); const row = tpl('tpl-invite-row'); row.querySelector('.ir-label').textContent = label; if (detail) row.querySelector('.ir-detail').textContent = ' ' + detail; const st = row.querySelector('.ir-status'); st.textContent = statusLabel[inv.status]; st.style.color = statusColor[inv.status]; if (inv.url) { const cp = row.querySelector('.ir-copy'); cp.style.display = ''; cp.onclick = () => { navigator.clipboard.writeText(inv.url).then(() => { showToast(tr('linkCopied')); }); }; } body.appendChild(row); } } showSheet(lc, true); lc.querySelector('.il-close').onclick = closeOv; }).catch(() => showToast(tr('errLoad'))); }; c.querySelector('.dm-exp').onclick = () => { const blob = new Blob( [JSON.stringify({ goals: state.goals, at: new Date().toISOString() }, null, 2)], { type: 'application/json' } ); const url = URL.createObjectURL(blob), a = document.createElement('a'); a.href = url; a.download = 'dudi-backup.json'; a.click(); URL.revokeObjectURL(url); closeOv(); }; c.querySelector('.dm-imp').onclick = () => { const inp = document.createElement('input'); inp.type = 'file'; inp.accept = '.json'; inp.onchange = e => { const f = e.target.files[0]; if (!f) return; const r = new FileReader(); r.onload = ev => { try { const p = JSON.parse(ev.target.result); if (!p.goals || !Array.isArray(p.goals)) throw new Error(tr('invalidFormat')); if (!confirm(tr('confirmImport').replace('{n}', p.goals.length))) return; const promises = p.goals.map(g => api('POST', 'goals', { name: g.name, unit: g.unit, daily: g.daily, days: g.days, start: g.start, sets: g.sets || {} }) .then(r => { state.goals.push({ id: r.id, name: r.name, unit: r.unit, daily: r.daily, days: r.days, start: r.start, sets: r.sets || {} }); }) ); Promise.all(promises).then(() => { closeOv(); render(); alert(tr('importDone').replace('{n}', p.goals.length)); }); } catch (err) { alert(err.message); } }; r.readAsText(f); }; inp.click(); }; c.querySelectorAll('.btn-lang').forEach(b => { if (b.dataset.lang === LOCALE) b.classList.add('active'); b.onclick = function() { const lang = this.dataset.lang; setLocale(lang, true); api('PATCH', 'me', { locale: lang }).catch(() => {}); render(); updateHeader(); closeOv(); }; }); c.querySelector('.dm-clr').onclick = () => { if (!confirm(tr('confirmClear'))) return; const ids = state.goals.map(g => g.id); state.goals = []; render(); Promise.all(ids.map(id => api('DELETE', 'goals/' + id))).catch(() => showToast(tr('errDelete'))); closeOv(); }; } export function openAdmin() { api('GET', 'admin/users').then(rows => { const c = tpl('tpl-admin-users'); const body = c.querySelector('.au-body'); rows.forEach(u => { const row = document.createElement('tr'); row.style.borderBottom = '1px solid var(--border)'; const name = u.username || '—'; const date = new Date(u.registered * 1000).toLocaleDateString(ldoc(), { day: 'numeric', month: 'short', year: 'numeric' }); row.innerHTML = '