dudi/public/js/sheets.js
Simon Kühn 61d677d811 Fix logout not showing login screen
Symfony's logout responds with a redirect, causing fetch to parse HTML
as JSON and reject — .finally() ensures the UI always transitions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 22:55:22 +02:00

207 lines
9.1 KiB
JavaScript

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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
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').finally(() => { 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 = '<td style="padding:7px 12px">' + escHtml(name) + '</td>'
+ '<td style="padding:7px 12px;color:var(--text2)">' + escHtml(u.email) + '</td>'
+ '<td style="padding:7px 12px;color:var(--text2);font-size:.85em">' + date + '</td>';
body.appendChild(row);
});
showSheet(c, true);
c.querySelector('.au-close').onclick = closeOv;
}).catch(() => showToast(tr('errLoad')));
}