Files
flancer/internal/app/web/app.js
jbergner 57f7470310
All checks were successful
release-tag / release-image (push) Successful in 2m6s
Mobile-Update + Bericht
2026-08-14 18:17:38 +02:00

367 lines
27 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const state = {
me: null,
csrf: '',
settings: null,
running: null,
entries: [],
clients: [],
totalCount: 0,
totalDuration: 0,
filter: { q: '', period: 'all', anchor: new Date(), offset: 0, limit: 120 },
timerTick: null,
};
const $ = (s) => document.querySelector(s);
const $$ = (s) => [...document.querySelectorAll(s)];
function applyMobileCompactPreference(enabled = true) {
document.body.classList.toggle('mobile-compact', !!enabled);
const checkbox = $('#setting-mobile-compact');
if (checkbox) checkbox.checked = !!enabled;
syncMobileNav();
}
function switchMobileView(view) {
document.body.classList.toggle('mobile-history', view === 'history');
syncMobileNav();
}
function syncMobileNav() {
const history = document.body.classList.contains('mobile-history');
const track = $('#mobile-nav-track'), overview = $('#mobile-nav-history');
if (track) { track.classList.toggle('active', !history); if (!history) track.setAttribute('aria-current','page'); else track.removeAttribute('aria-current'); }
if (overview) { overview.classList.toggle('active', history); if (history) overview.setAttribute('aria-current','page'); else overview.removeAttribute('aria-current'); }
}
async function api(url, options = {}) {
const headers = { ...(options.headers || {}) };
if (options.body !== undefined && !(options.body instanceof FormData)) headers['Content-Type'] = 'application/json';
if (state.csrf && options.method && !['GET','HEAD'].includes(options.method.toUpperCase())) headers['X-CSRF-Token'] = state.csrf;
const res = await fetch(url, { ...options, headers });
if (res.status === 401) { location.assign('/login'); throw new Error('Nicht angemeldet.'); }
const ct = res.headers.get('content-type') || '';
const body = res.status === 204 ? null : (ct.includes('application/json') ? await res.json().catch(() => ({})) : await res.text());
if (!res.ok) throw new Error(body?.error?.message || `HTTP ${res.status}`);
return body;
}
function toast(msg) {
const el = $('#toast'); el.textContent = msg; el.hidden = false;
clearTimeout(toast._t); toast._t = setTimeout(() => { el.hidden = true; }, 2600);
}
function showError(sel, err) { const el = $(sel); el.textContent = err?.message || String(err); el.hidden = false; }
function hideError(sel) { $(sel).hidden = true; }
function pad(n) { return String(n).padStart(2, '0'); }
function duration(ms, withSeconds = false) {
ms = Math.max(0, ms || 0);
const sec = Math.floor(ms / 1000), h = Math.floor(sec / 3600), m = Math.floor(sec % 3600 / 60), s = sec % 60;
return withSeconds ? `${pad(h)}:${pad(m)}:${pad(s)}` : `${h}:${pad(m)}`;
}
function rounded(ms) {
const mins = Math.max(1, Number(state.settings?.rounding_minutes || 1));
if (mins <= 1) return Math.max(0, ms);
const step = mins * 60000;
return state.settings?.round_up ? Math.ceil(ms / step) * step : Math.round(ms / step) * step;
}
function clock(ms) {
const d = new Date(ms);
return new Intl.DateTimeFormat(state.settings?.language === 'en' ? 'en' : 'de-DE', { hour: '2-digit', minute: '2-digit', hour12: state.settings?.time_format === '12' }).format(d);
}
function localDateTimeValue(ms) {
const d = new Date(ms); const off = d.getTimezoneOffset();
return new Date(d.getTime() - off * 60000).toISOString().slice(0,16);
}
function msFromLocalValue(v) { return v ? new Date(v).getTime() : null; }
function dayKey(ms) { const d = new Date(ms); return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}`; }
function dayTitle(ms) {
const d = new Date(ms), now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
const target = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
if (target === today) return 'Heute';
const yd = new Date(now.getFullYear(), now.getMonth(), now.getDate()); yd.setDate(yd.getDate()-1);
if (target === yd.getTime()) return 'Gestern';
return new Intl.DateTimeFormat('de-DE', { weekday:'long', day:'2-digit', month:'long', year:'numeric' }).format(d);
}
function initials(s) { return (s || '?').trim().split(/\s+/).slice(0,2).map(x=>x[0]?.toUpperCase()||'').join('') || '?'; }
function escapeText(s) { const x=document.createElement('span'); x.textContent=s??''; return x.innerHTML; }
function rangeForFilter() {
const { period, anchor } = state.filter;
if (period === 'all') return { from:0, to:0, label:'Alle Zeiten' };
let from, to;
const d = new Date(anchor); d.setHours(0,0,0,0);
if (period === 'day') { from = d; to = new Date(d); to.setDate(to.getDate()+1); }
if (period === 'week') { const wd = (d.getDay()+6)%7; d.setDate(d.getDate()-wd); from = new Date(d); to = new Date(d); to.setDate(to.getDate()+7); }
if (period === 'month') { from = new Date(d.getFullYear(),d.getMonth(),1); to = new Date(d.getFullYear(),d.getMonth()+1,1); }
if (period === 'year') { from = new Date(d.getFullYear(),0,1); to = new Date(d.getFullYear()+1,0,1); }
let label='';
if (period === 'day') label = new Intl.DateTimeFormat('de-DE',{day:'2-digit',month:'2-digit',year:'numeric'}).format(from);
if (period === 'week') label = `${new Intl.DateTimeFormat('de-DE',{day:'2-digit',month:'2-digit'}).format(from)} ${new Intl.DateTimeFormat('de-DE',{day:'2-digit',month:'2-digit',year:'numeric'}).format(new Date(to.getTime()-1))}`;
if (period === 'month') label = new Intl.DateTimeFormat('de-DE',{month:'long',year:'numeric'}).format(from);
if (period === 'year') label = String(from.getFullYear());
return { from: from.getTime(), to: to.getTime(), label };
}
function stepPeriod(delta) {
const d = new Date(state.filter.anchor), p = state.filter.period;
if (p === 'day') d.setDate(d.getDate()+delta);
if (p === 'week') d.setDate(d.getDate()+7*delta);
if (p === 'month') d.setMonth(d.getMonth()+delta);
if (p === 'year') d.setFullYear(d.getFullYear()+delta);
state.filter.anchor = d; state.filter.offset = 0; refreshEntries();
}
function queryString(includePaging = true) {
const r = rangeForFilter(); const p = new URLSearchParams();
if (state.filter.q) p.set('q', state.filter.q);
if (r.from) p.set('from', r.from); if (r.to) p.set('to', r.to);
if (includePaging) { p.set('limit', state.filter.limit); p.set('offset', state.filter.offset); }
return p.toString();
}
async function init() {
try {
const me = await api('/api/me'); state.me = me.user; state.csrf = me.csrf_token;
const [settings, running, clients] = await Promise.all([api('/api/settings'), api('/api/running'), api('/api/clients')]);
state.settings = settings; state.running = running.entry; state.clients = clients.clients || [];
applyMobileCompactPreference(state.settings.mobile_compact !== false);
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
if ((!state.settings.timezone || state.settings.timezone === 'UTC') && tz && tz !== 'UTC') {
state.settings.timezone = tz;
try { state.settings = await api('/api/settings', { method:'PUT', body:JSON.stringify(state.settings) }); } catch (_) {}
}
renderIdentity(); renderClients(); renderRunning(); fillSettings();
await Promise.all([refreshEntries(), refreshTotals()]);
startTicker();
} catch (e) { toast(e.message); }
}
function renderIdentity() {
$('#user-label').textContent = state.me.display_name || state.me.username;
$('#avatar-text').textContent = initials(state.me.display_name || state.me.username);
}
function renderClients() { $('#client-list').innerHTML = state.clients.map(x => `<option value="${escapeText(x)}"></option>`).join(''); }
function renderRunning() {
const e = state.running;
$('#running-hint').hidden = !e; $('#edit-running').hidden = !e;
$('#timer-button').textContent = e ? '■ Stop' : '▶ Start';
$('#timer-display').classList.toggle('running', !!e);
$('#client-input').disabled = !!e;
if (e) { $('#client-input').value=e.client||''; $('#activity-input').value=e.activity||''; }
updateTicker();
}
function startTicker() { clearInterval(state.timerTick); state.timerTick = setInterval(updateTicker, 1000); updateTicker(); }
function updateTicker() {
const e=state.running, elapsed=e ? Date.now()-e.start_ms : 0;
$('#timer-display').textContent=duration(elapsed,true);
const warn=!!(e && state.settings?.long_run_reminder && elapsed>8*3600000); $('#long-running-warning').hidden=!warn;
}
async function timerToggle() {
try {
if (!state.running) {
const x = await api('/api/entries/start', { method:'POST', body:JSON.stringify({ client:$('#client-input').value, activity:$('#activity-input').value, start_ms:Date.now() }) });
state.running=x; renderRunning(); toast('Timer gestartet.');
} else {
await saveRunningActivity();
await api(`/api/entries/${state.running.id}/stop`, { method:'POST', body:JSON.stringify({ end_ms:Date.now() }) });
state.running=null; $('#client-input').disabled=false; $('#client-input').value=''; $('#activity-input').value=''; renderRunning(); toast('Timer gestoppt.');
await Promise.all([refreshEntries(true),refreshTotals(),refreshClients()]);
}
} catch(e) { toast(e.message); }
}
async function saveRunningActivity() {
if (!state.running) return;
const activity=$('#activity-input').value;
if (activity===state.running.activity) return;
const x=await api(`/api/entries/${state.running.id}`,{method:'PUT',body:JSON.stringify({client:state.running.client,activity,start_ms:state.running.start_ms,end_ms:null})});
state.running=x;
}
async function refreshClients(){const x=await api('/api/clients');state.clients=x.clients||[];renderClients();}
async function refreshTotals() {
const now=new Date(); const today=new Date(now.getFullYear(),now.getMonth(),now.getDate());
const week=new Date(today); week.setDate(week.getDate()-((week.getDay()+6)%7));
try {
const [t,w]=await Promise.all([
api(`/api/entries?from=${today.getTime()}&to=${new Date(today.getFullYear(),today.getMonth(),today.getDate()+1).getTime()}&limit=1&offset=0`),
api(`/api/entries?from=${week.getTime()}&limit=1&offset=0`)
]);
$('#today-total').textContent=duration(t.total_duration_ms);
$('#week-total').textContent=duration(w.total_duration_ms);
$('#header-week').textContent=duration(w.total_duration_ms);
$('#week-badge').hidden=!state.settings.show_week_total;
} catch(e){ console.warn(e); }
}
async function refreshEntries(reset=true) {
if(reset){state.filter.offset=0;state.entries=[];}
const range=rangeForFilter(); $('#period-label').textContent=range.label;
try {
const p=await api('/api/entries?'+queryString(true));
state.totalCount=p.total_count;state.totalDuration=p.total_duration_ms;
state.entries=reset?p.entries:[...state.entries,...p.entries];
renderEntries();
} catch(e){toast(e.message);}
}
function renderEntries() {
$('#result-count').textContent=state.totalCount; $('#result-total').textContent=duration(state.totalDuration);
$('#empty-state').hidden=state.totalCount!==0;
$('#load-more').hidden=state.entries.length>=state.totalCount;
const groups=[]; let current=null;
for(const e of state.entries){const k=dayKey(e.start_ms);if(!current||current.key!==k){current={key:k,start:e.start_ms,items:[]};groups.push(current)}current.items.push(e)}
$('#entries').innerHTML=groups.map(g=>{
const dayTotal=g.items.reduce((n,e)=>n+rounded((e.end_ms||e.start_ms)-e.start_ms),0);
return `<section class="day-group"><div class="day-heading"><strong>${escapeText(dayTitle(g.start))}</strong><span>${duration(dayTotal)}</span></div>${g.items.map(entryHTML).join('')}</section>`;
}).join('');
$$('.entry-row').forEach(el=>el.addEventListener('click',()=>openEntry(el.dataset.id)));
$$('.day-heading').forEach(el=>el.style.position=state.settings.sticky_days?'sticky':'static');
}
function entryHTML(e){return `<button class="entry-row" data-id="${e.id}"><span class="entry-client">${escapeText(e.client||'Kein Kunde')}</span><span class="entry-activity">${escapeText(e.activity||'—')}</span><span class="entry-time">${clock(e.start_ms)}${clock(e.end_ms)}</span><span class="entry-duration">${duration(rounded(e.end_ms-e.start_ms))}</span><span class="entry-chevron"></span></button>`}
function openEntry(id=null, running=false) {
hideError('#entry-error');
let e = null;
if (running) e=state.running; else if(id) e=state.entries.find(x=>x.id===id);
$('#entry-id').value=e?.id||''; $('#edit-client').value=e?.client||''; $('#edit-activity').value=e?.activity||'';
const now=Date.now(); $('#edit-start').value=localDateTimeValue(e?.start_ms||now); $('#edit-end').value=e?.end_ms?localDateTimeValue(e.end_ms):(running?'':localDateTimeValue(now+3600000));
$('#entry-dialog-title').textContent=e?'Eintrag bearbeiten':'Zeit nachtragen'; $('#delete-entry').hidden=!e;
updateEditDuration(); $('#entry-dialog').showModal();
}
function updateEditDuration(){const s=msFromLocalValue($('#edit-start').value),e=msFromLocalValue($('#edit-end').value);$('#edit-duration').textContent=s&&e&&e>=s?duration(rounded(e-s)):''}
async function saveEntry(ev){ev.preventDefault();hideError('#entry-error');const id=$('#entry-id').value;const start=msFromLocalValue($('#edit-start').value),end=msFromLocalValue($('#edit-end').value);if(!start||!end||end<start){showError('#entry-error',new Error('Bitte einen gültigen Start- und Endzeitpunkt wählen.'));return}const body={client:$('#edit-client').value,activity:$('#edit-activity').value,start_ms:start,end_ms:end};try{if(id)await api(`/api/entries/${id}`,{method:'PUT',body:JSON.stringify(body)});else await api('/api/entries',{method:'POST',body:JSON.stringify(body)});$('#entry-dialog').close();if(state.running?.id===id){state.running=null;renderRunning()}await Promise.all([refreshEntries(true),refreshTotals(),refreshClients()]);toast('Eintrag gespeichert.')}catch(e){showError('#entry-error',e)}}
async function deleteEntry(){const id=$('#entry-id').value;if(!id||!confirm('Diesen Eintrag wirklich löschen?'))return;try{await api(`/api/entries/${id}`,{method:'DELETE'});if(state.running?.id===id){state.running=null;renderRunning()}$('#entry-dialog').close();await Promise.all([refreshEntries(true),refreshTotals()]);toast('Eintrag gelöscht.')}catch(e){showError('#entry-error',e)}}
function fillSettings(){const s=state.settings;$('#setting-time-format').value=s.time_format;$('#setting-rounding').value=s.rounding_minutes;$('#setting-round-up').checked=s.round_up;$('#setting-week').checked=s.show_week_total;$('#setting-sticky').checked=s.sticky_days;$('#setting-reminder').checked=s.long_run_reminder;$('#setting-mobile-compact').checked=s.mobile_compact!==false;$('#setting-export-name').value=s.export_name||'';$('#setting-timezone').value=s.timezone||Intl.DateTimeFormat().resolvedOptions().timeZone||'UTC';$('#setting-export-date').checked=s.export_date;$('#admin-section').hidden=state.me.role!=='admin'}
async function openSettings(){fillSettings();hideError('#settings-error');$('#settings-dialog').showModal();if(state.me.role==='admin')await loadUsers()}
async function saveSettings(ev){ev.preventDefault();hideError('#settings-error');const s={language:state.settings.language||'de',time_format:$('#setting-time-format').value,rounding_minutes:Number($('#setting-rounding').value),round_up:$('#setting-round-up').checked,show_week_total:$('#setting-week').checked,sticky_days:$('#setting-sticky').checked,long_run_reminder:$('#setting-reminder').checked,mobile_compact:$('#setting-mobile-compact').checked,export_name:$('#setting-export-name').value,timezone:$('#setting-timezone').value,export_date:$('#setting-export-date').checked};try{state.settings=await api('/api/settings',{method:'PUT',body:JSON.stringify(s)});applyMobileCompactPreference(state.settings.mobile_compact!==false);$('#settings-dialog').close();renderEntries();await refreshTotals();toast('Einstellungen gespeichert.')}catch(e){showError('#settings-error',e)}}
async function changeOwnPassword(ev){
ev.preventDefault(); hideError('#account-password-error');
try {
await api('/api/account/password',{method:'POST',body:JSON.stringify({current_password:$('#current-password').value,new_password:$('#own-new-password').value})});
location.assign('/login');
} catch(e){ showError('#account-password-error',e); }
}
async function loadUsers(){try{const x=await api('/api/admin/users');$('#user-list').innerHTML=x.users.map(u=>`<div class="user-row" data-user-id="${u.id}"><div class="user-meta"><strong>${escapeText(u.display_name||u.username)}</strong><span>${escapeText(u.username)}</span></div><span class="role-badge">${u.role==='admin'?'Admin':'Benutzer'}</span><button type="button" class="btn subtle user-password" ${u.active?'':'disabled'}>Passwort</button><button type="button" class="btn ${u.active?'danger':'subtle'} user-toggle" ${u.id===state.me.id?'disabled':''}>${u.active?'Deaktivieren':'Aktivieren'}</button></div>`).join('');$$('.user-password').forEach(b=>b.addEventListener('click',()=>openPassword(b.closest('.user-row').dataset.userId)));$$('.user-toggle').forEach(b=>b.addEventListener('click',()=>toggleUser(b.closest('.user-row').dataset.userId,b.textContent==='Aktivieren')))}catch(e){showError('#settings-error',e)}}
function openPassword(id){$('#password-user-id').value=id;$('#reset-password').value='';hideError('#password-error');$('#password-dialog').showModal()}
async function toggleUser(id,active){try{await api(`/api/admin/users/${id}`,{method:'PATCH',body:JSON.stringify({active})});await loadUsers();toast(active?'Benutzer aktiviert.':'Benutzer deaktiviert.')}catch(e){showError('#settings-error',e)}}
async function createUser(ev){ev.preventDefault();hideError('#user-error');const body={username:$('#new-username').value,displayName:$('#new-display-name').value,password:$('#new-password').value,role:$('#new-role').value};try{await api('/api/admin/users',{method:'POST',body:JSON.stringify(body)});$('#user-dialog').close();ev.currentTarget.reset();await loadUsers();toast('Benutzer angelegt.')}catch(e){showError('#user-error',e)}}
async function resetPassword(ev){ev.preventDefault();hideError('#password-error');try{await api(`/api/admin/users/${$('#password-user-id').value}/password`,{method:'POST',body:JSON.stringify({password:$('#reset-password').value})});$('#password-dialog').close();toast('Passwort zurückgesetzt.')}catch(e){showError('#password-error',e)}}
function dateInputValue(d) { return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}`; }
function localDayStartMS(value) {
if (!value) return 0;
const [y,m,d]=value.split('-').map(Number);
return new Date(y,m-1,d,0,0,0,0).getTime();
}
function exportParams(includePresentation=true) {
const p=new URLSearchParams();
const scope=$('input[name="export-scope"]:checked')?.value||'view';
if(scope==='view') {
const r=rangeForFilter();
if(state.filter.q) p.set('q',state.filter.q);
if(r.from) p.set('from',r.from);
if(r.to) p.set('to',r.to);
} else {
const from=localDayStartMS($('#export-from').value);
const toStart=localDayStartMS($('#export-to').value);
if(from) p.set('from',from);
if(toStart) { const end=new Date(toStart); end.setDate(end.getDate()+1); p.set('to',end.getTime()); }
}
if(includePresentation) {
if($('#export-sort').value==='asc') p.set('sort','asc');
if($('#export-compact').checked) p.set('compact','1');
}
return p;
}
let exportPreviewSeq=0;
async function updateExportPreview() {
const seq=++exportPreviewSeq;
const scope=$('input[name="export-scope"]:checked')?.value||'view';
$('#export-custom').hidden=scope!=='custom';
if(scope==='view') { $('#export-preview').textContent=`${state.totalCount} Einträge · ${duration(state.totalDuration)}`; return; }
const from=localDayStartMS($('#export-from').value), to=localDayStartMS($('#export-to').value);
if(!from||!to||to<from) { $('#export-preview').textContent='Bitte einen gültigen Zeitraum wählen.'; return; }
try {
const p=exportParams(false); p.set('limit','1'); p.set('offset','0');
const result=await api('/api/entries?'+p.toString());
if(seq===exportPreviewSeq) $('#export-preview').textContent=`${result.total_count} Einträge · ${duration(result.total_duration_ms)}`;
} catch(e) { if(seq===exportPreviewSeq) $('#export-preview').textContent=e.message; }
}
function openExport(){
const now=new Date(), first=new Date(now.getFullYear(),now.getMonth(),1);
if(!$('#export-from').value) $('#export-from').value=dateInputValue(first);
if(!$('#export-to').value) $('#export-to').value=dateInputValue(now);
updateExportPreview(); $('#export-dialog').showModal();
}
function downloadExport(type){
const p=exportParams(true), a=document.createElement('a');
a.href=`/api/export.${type}${p.toString()?'?'+p.toString():''}`; a.click();
}
function serviceFilterPayload(){
const p=exportParams(false);
return {q:p.get('q')||'',from_ms:Number(p.get('from')||0),to_ms:Number(p.get('to')||0)};
}
let servicePreviewSeq=0;
async function updateServicePreview(){
const seq=++servicePreviewSeq, client=$('#service-client').value.trim();
if(!client){$('#service-preview').textContent='Bitte einen Kunden auswählen.';return;}
try{
const p=exportParams(false);p.set('client',client);p.set('limit','1');p.set('offset','0');
const result=await api('/api/entries?'+p.toString());
if(seq===servicePreviewSeq) $('#service-preview').textContent=result.total_count?`${result.total_count} Tätigkeiten · ${duration(result.total_duration_ms)}`:'Keine abgeschlossenen Tätigkeiten für Kunde/Zeitraum.';
}catch(e){if(seq===servicePreviewSeq) $('#service-preview').textContent=e.message;}
}
function openServiceReport(){
hideError('#service-error');
if(($('input[name="export-scope"]:checked')?.value||'view')==='custom'){
const from=localDayStartMS($('#export-from').value),to=localDayStartMS($('#export-to').value);
if(!from||!to||to<from){$('#export-preview').textContent='Bitte einen gültigen Zeitraum wählen.';return;}
}
let client=$('#client-input').value.trim();
const visible=[...new Set(state.entries.map(e=>(e.client||'').trim()).filter(Boolean))];
if(!client&&visible.length===1) client=visible[0];
if(!client&&state.filter.q){const exact=state.clients.find(x=>x.toLowerCase()===state.filter.q.toLowerCase());if(exact)client=exact;}
$('#service-client').value=client;
if(!$('#service-date').value) $('#service-date').value=dateInputValue(new Date());
$('#export-dialog').close();
$('#service-report-dialog').showModal();
updateServicePreview();
}
async function downloadServiceReport(ev){
ev.preventDefault();hideError('#service-error');
const submit=$('#service-report-form button[type="submit"]');submit.disabled=true;
const body={...serviceFilterPayload(),client:$('#service-client').value.trim(),contact:$('#service-contact').value.trim(),location:$('#service-location').value.trim(),order_number:$('#service-order').value.trim(),subject:$('#service-subject').value.trim(),notes:$('#service-notes').value.trim(),place:$('#service-place').value.trim(),report_date:$('#service-date').value};
try{
const res=await fetch('/api/service-report.pdf',{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':state.csrf},body:JSON.stringify(body)});
if(res.status===401){location.assign('/login');return;}
if(!res.ok){const ct=res.headers.get('content-type')||'';const x=ct.includes('application/json')?await res.json().catch(()=>({})):await res.text();throw new Error(x?.error?.message||x||`HTTP ${res.status}`);}
const blob=await res.blob(),url=URL.createObjectURL(blob),a=document.createElement('a');
a.href=url;a.download='service-bericht.pdf';document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),1000);
$('#service-report-dialog').close();toast('Service-Bericht erstellt.');
}catch(e){showError('#service-error',e);}finally{submit.disabled=false;}
}
async function logout() {
if(!confirm('Abmelden?')) return;
try { await api('/api/logout',{method:'POST',body:'{}'}); } finally { location.assign('/login'); }
}
function wireEvents(){
$('#timer-button').addEventListener('click',timerToggle); $('#activity-input').addEventListener('blur',()=>saveRunningActivity().catch(e=>toast(e.message)));
$('#edit-running').addEventListener('click',()=>openEntry(null,true)); $('#add-entry').addEventListener('click',()=>openEntry());
$('#entry-form').addEventListener('submit',saveEntry); $('#delete-entry').addEventListener('click',deleteEntry); $('#edit-start').addEventListener('input',updateEditDuration); $('#edit-end').addEventListener('input',updateEditDuration);
$('#settings-open').addEventListener('click',openSettings); $('#settings-form').addEventListener('submit',saveSettings); $('#change-own-password').addEventListener('click',()=>{hideError('#account-password-error');$('#account-password-form').reset();$('#account-password-dialog').showModal()}); $('#account-password-form').addEventListener('submit',changeOwnPassword); $('#add-user').addEventListener('click',()=>{hideError('#user-error');$('#user-dialog').showModal()}); $('#user-form').addEventListener('submit',createUser); $('#password-form').addEventListener('submit',resetPassword);
$('#export-open').addEventListener('click',openExport); $('#history-export').addEventListener('click',openExport); $('#export-pdf').addEventListener('click',()=>downloadExport('pdf')); $('#export-csv').addEventListener('click',()=>downloadExport('csv')); $('#export-service').addEventListener('click',openServiceReport); $('#service-report-form').addEventListener('submit',downloadServiceReport); let serviceClientTimer; $('#service-client').addEventListener('input',()=>{clearTimeout(serviceClientTimer);serviceClientTimer=setTimeout(updateServicePreview,220)}); $$('input[name="export-scope"]').forEach(x=>x.addEventListener('change',updateExportPreview)); $('#export-from').addEventListener('change',updateExportPreview); $('#export-to').addEventListener('change',updateExportPreview);
$('#account-menu').addEventListener('click',logout); $('#mobile-logout').addEventListener('click',logout);
let searchTimer; $('#search-input').addEventListener('input',e=>{clearTimeout(searchTimer);searchTimer=setTimeout(()=>{state.filter.q=e.target.value.trim();state.filter.offset=0;refreshEntries(true)},220)});
$$('.period-tabs button').forEach(b=>b.addEventListener('click',()=>{$$('.period-tabs button').forEach(x=>x.classList.remove('active'));b.classList.add('active');state.filter.period=b.dataset.period;state.filter.anchor=new Date();state.filter.offset=0;refreshEntries(true)}));
$('#period-prev').addEventListener('click',()=>stepPeriod(-1)); $('#period-next').addEventListener('click',()=>stepPeriod(1));
$('#load-more').addEventListener('click',()=>{state.filter.offset=state.entries.length;refreshEntries(false)});
$$('[data-close]').forEach(b=>b.addEventListener('click',()=>document.getElementById(b.dataset.close).close()));
$$('dialog').forEach(d=>d.addEventListener('click',e=>{const r=d.getBoundingClientRect();if(e.clientX<r.left||e.clientX>r.right||e.clientY<r.top||e.clientY>r.bottom)d.close()}));
$('#mobile-history').addEventListener('click',()=>switchMobileView('history')); $('#mobile-track').addEventListener('click',()=>switchMobileView('track')); $('#mobile-nav-track').addEventListener('click',()=>switchMobileView('track')); $('#mobile-nav-history').addEventListener('click',()=>switchMobileView('history')); $('#mobile-nav-settings').addEventListener('click',openSettings);
}
wireEvents(); init();