2026-04-29 13:40:57 +00:00
|
|
|
var TODAY = new Date(); TODAY.setHours(0,0,0,0);
|
|
|
|
|
var goals = [], prefs, selDay = {}, addAmt = {}, renamingId = null, renameVal = '', collapsed = {};
|
2026-05-01 08:06:14 +00:00
|
|
|
var userName = '', isAdmin = false;
|
2026-04-29 13:40:57 +00:00
|
|
|
|
2026-04-30 11:34:41 +00:00
|
|
|
var STRINGS = {
|
|
|
|
|
de: {
|
|
|
|
|
hint:'Menü → "Zum Startbildschirm" für App-Icon',
|
|
|
|
|
emptyLine1:'Noch keine Ziele.',emptyLine2:'Tippe auf + um zu starten.',
|
|
|
|
|
noEntry:'Noch kein Eintrag',log:'Eintragen',
|
|
|
|
|
noch:'Noch',dAbbr:'T',endet:'endet',todayShort:'heute',total:'total',
|
|
|
|
|
todayHeading:'Heute',done2:'Gemacht',dailyGoal:'Tagesziel',remaining:'Noch',
|
|
|
|
|
history:'Verlauf — heute & gestern bearbeitbar',
|
|
|
|
|
legBuf:'Puffer',legDone:'Erreicht',legPartial:'Teilweise',legMissed:'Verpasst',
|
|
|
|
|
delGoal:'Ziel löschen',
|
|
|
|
|
qbLabel:'Quick-Buchen',openLabel:'Offen',doneToday:'Heute erledigt',
|
|
|
|
|
hello:'Hallo {n}!',
|
|
|
|
|
confirmDelete:'Ziel wirklich löschen?',errDelete:'Fehler beim Löschen',errSave:'Speicherfehler',
|
|
|
|
|
loginTitle:'Anmelden',emailLabel:'E-Mail',passwordLabel:'Passwort',loginBtn:'Anmelden',
|
|
|
|
|
forgotPw:'Passwort vergessen?',
|
|
|
|
|
loginErrEmpty:'Bitte E-Mail und Passwort eingeben',
|
|
|
|
|
loginErrWrong:'Falsche E-Mail oder Passwort',loginErrRate:'Zu viele Versuche',loginErrConn:'Verbindungsfehler',
|
|
|
|
|
forgotTitle:'Passwort vergessen',forgotSub:'Wir schicken dir einen Reset-Link',
|
|
|
|
|
sendLink:'Link senden',back:'Zurück',
|
|
|
|
|
emailSentTitle:'E-Mail gesendet',
|
|
|
|
|
emailSentSub:'Falls die Adresse bekannt ist, erhältst du in Kürze einen Reset-Link.',
|
|
|
|
|
ok:'OK',
|
|
|
|
|
resetTitle:'Neues Passwort',newPwLabel:'Neues Passwort',setPw:'Passwort setzen',min8:'mind. 8 Zeichen',
|
|
|
|
|
pwChangedTitle:'Passwort geändert',pwChangedSub:'Du kannst dich jetzt anmelden.',
|
|
|
|
|
changeNameTitle:'Name ändern',yourName:'Dein Name',save:'Speichern',cancel:'Abbrechen',
|
|
|
|
|
errNameEmpty:'Name darf nicht leer sein',errNameSave:'Fehler beim Speichern',nameSaved:'Name gespeichert',
|
|
|
|
|
changePwTitle:'Passwort ändern',currentPwLabel:'Aktuelles Passwort',
|
|
|
|
|
newPwConfLabel:'Neues Passwort bestätigen',changePwBtn:'Ändern',
|
|
|
|
|
errPwMismatch:'Die neuen Passwörter stimmen nicht überein',pwChanged:'Passwort geändert',
|
|
|
|
|
registerTitle:'Konto erstellen',registerSub:'Du wurdest eingeladen',
|
|
|
|
|
namePlaceholder:'Wie sollen wir dich nennen?',pwConfLabel:'Passwort bestätigen',
|
|
|
|
|
pwPlaceholder:'Passwort wiederholen',registerBtn:'Registrieren',
|
|
|
|
|
errPwMismatch2:'Passwörter stimmen nicht überein',errFillAll:'Bitte alle Felder ausfüllen',
|
|
|
|
|
newGoalTitle:'Neues Ziel',exerciseLabel:'Übung / Gewohnheit',exercisePlaceholder:'Liegestütz, Plank …',
|
|
|
|
|
unitLabel:'Einheit',unitDefault:'Stück',daysLabel:'Dauer in Tagen',
|
|
|
|
|
dailyLabel:'Tagesziel',weeklyLabel:'Wochenziel',startGoal:'Ziel starten',errCreate:'Fehler beim Erstellen',
|
|
|
|
|
dataMenuTitle:'Daten verwalten',dataMenuSub:'Export, Import und Backup',
|
|
|
|
|
exportLabel:'Exportieren',exportSub:'Alle Ziele als JSON-Datei speichern',
|
|
|
|
|
importLabel:'Importieren',importSub:'Backup laden oder zusammenführen',
|
|
|
|
|
inviteLabel:'Freund einladen',inviteSub:'Einladungslink generieren',
|
|
|
|
|
inviteListLabel:'Meine Einladungen',inviteListSub:'Status aller gesendeten Einladungen',
|
|
|
|
|
changeName:'Name ändern',changePw:'Passwort ändern',logout:'Abmelden',close:'Schließen',
|
|
|
|
|
clearAll:'Alle Daten löschen',clearAllSub:'Kann nicht rückgängig gemacht werden',
|
|
|
|
|
confirmClear:'Alle Daten löschen?',
|
|
|
|
|
inviteFormTitle:'Freund einladen',inviteFormSub:'Link gilt 7 Tage und kann nur einmal verwendet werden',
|
|
|
|
|
inviteNameLabel:'Name (für deine Übersicht)',inviteNamePlaceholder:'z.B. Max',
|
|
|
|
|
generateLink:'Link generieren',inviteLinkTitle:'Einladungslink',
|
|
|
|
|
copyLink:'Link kopieren',linkCopied:'Link kopiert!',errGenerate:'Fehler beim Generieren',
|
|
|
|
|
noInvites:'Noch keine Einladungen verschickt',
|
|
|
|
|
statusPending:'Ausstehend',statusUsed:'Angenommen',statusExpired:'Abgelaufen',
|
|
|
|
|
errLoad:'Fehler beim Laden',
|
|
|
|
|
confirmImport:'{n} Ziel(e) importieren?',importDone:'{n} Ziel(e) importiert.',invalidFormat:'Ungültiges Format',
|
|
|
|
|
linkLabel:'Link',doneLabel:'gemacht',ofLabel:'von',
|
|
|
|
|
gestern:'Gestern',heute:'Heute',
|
|
|
|
|
expiresAt:'läuft ab:',acceptedBy:'→',
|
2026-05-01 08:06:14 +00:00
|
|
|
adminLabel:'Nutzer',adminColName:'Name',adminColEmail:'E-Mail',adminColRegistered:'Registriert',
|
2026-04-30 11:34:41 +00:00
|
|
|
},
|
|
|
|
|
en: {
|
|
|
|
|
hint:'Menu → "Add to Home Screen" for app icon',
|
|
|
|
|
emptyLine1:'No goals yet.',emptyLine2:'Tap + to get started.',
|
|
|
|
|
noEntry:'No entries yet',log:'Log',
|
|
|
|
|
noch:'Left',dAbbr:'d',endet:'ends',todayShort:'today',total:'total',
|
|
|
|
|
todayHeading:'Today',done2:'Done',dailyGoal:'Daily goal',remaining:'Left',
|
|
|
|
|
history:'History — today & yesterday editable',
|
|
|
|
|
legBuf:'Buffer',legDone:'Reached',legPartial:'Partial',legMissed:'Missed',
|
|
|
|
|
delGoal:'Delete goal',
|
|
|
|
|
qbLabel:'Quick-log',openLabel:'Open',doneToday:'Done today',
|
|
|
|
|
hello:'Hello {n}!',
|
|
|
|
|
confirmDelete:'Really delete this goal?',errDelete:'Delete failed',errSave:'Save failed',
|
|
|
|
|
loginTitle:'Sign in',emailLabel:'E-Mail',passwordLabel:'Password',loginBtn:'Sign in',
|
|
|
|
|
forgotPw:'Forgot password?',
|
|
|
|
|
loginErrEmpty:'Please enter email and password',
|
|
|
|
|
loginErrWrong:'Wrong email or password',loginErrRate:'Too many attempts',loginErrConn:'Connection error',
|
|
|
|
|
forgotTitle:'Forgot password',forgotSub:"We'll send you a reset link",
|
|
|
|
|
sendLink:'Send link',back:'Back',
|
|
|
|
|
emailSentTitle:'Email sent',
|
|
|
|
|
emailSentSub:"If the address is registered, you'll receive a reset link shortly.",
|
|
|
|
|
ok:'OK',
|
|
|
|
|
resetTitle:'New password',newPwLabel:'New password',setPw:'Set password',min8:'at least 8 characters',
|
|
|
|
|
pwChangedTitle:'Password changed',pwChangedSub:'You can now sign in.',
|
|
|
|
|
changeNameTitle:'Change name',yourName:'Your name',save:'Save',cancel:'Cancel',
|
|
|
|
|
errNameEmpty:'Name cannot be empty',errNameSave:'Save failed',nameSaved:'Name saved',
|
|
|
|
|
changePwTitle:'Change password',currentPwLabel:'Current password',
|
|
|
|
|
newPwConfLabel:'Confirm new password',changePwBtn:'Change',
|
|
|
|
|
errPwMismatch:"The new passwords don't match",pwChanged:'Password changed',
|
|
|
|
|
registerTitle:'Create account',registerSub:"You've been invited",
|
|
|
|
|
namePlaceholder:'What should we call you?',pwConfLabel:'Confirm password',
|
|
|
|
|
pwPlaceholder:'Repeat password',registerBtn:'Register',
|
|
|
|
|
errPwMismatch2:"Passwords don't match",errFillAll:'Please fill in all fields',
|
|
|
|
|
newGoalTitle:'New goal',exerciseLabel:'Exercise / Habit',exercisePlaceholder:'Push-ups, Plank …',
|
|
|
|
|
unitLabel:'Unit',unitDefault:'reps',daysLabel:'Duration (days)',
|
|
|
|
|
dailyLabel:'Daily goal',weeklyLabel:'Weekly goal',startGoal:'Start goal',errCreate:'Create failed',
|
|
|
|
|
dataMenuTitle:'Manage data',dataMenuSub:'Export, Import and Backup',
|
|
|
|
|
exportLabel:'Export',exportSub:'Save all goals as JSON file',
|
|
|
|
|
importLabel:'Import',importSub:'Load or merge backup',
|
|
|
|
|
inviteLabel:'Invite friend',inviteSub:'Generate invite link',
|
|
|
|
|
inviteListLabel:'My invitations',inviteListSub:'Status of all sent invitations',
|
|
|
|
|
changeName:'Change name',changePw:'Change password',logout:'Sign out',close:'Close',
|
|
|
|
|
clearAll:'Delete all data',clearAllSub:'Cannot be undone',
|
|
|
|
|
confirmClear:'Delete all data?',
|
|
|
|
|
inviteFormTitle:'Invite friend',inviteFormSub:'Link valid 7 days, single use',
|
|
|
|
|
inviteNameLabel:'Name (for your reference)',inviteNamePlaceholder:'e.g. Max',
|
|
|
|
|
generateLink:'Generate link',inviteLinkTitle:'Invite link',
|
|
|
|
|
copyLink:'Copy link',linkCopied:'Link copied!',errGenerate:'Generate failed',
|
|
|
|
|
noInvites:'No invitations sent yet',
|
|
|
|
|
statusPending:'Pending',statusUsed:'Accepted',statusExpired:'Expired',
|
|
|
|
|
errLoad:'Load failed',
|
|
|
|
|
confirmImport:'Import {n} goal(s)?',importDone:'{n} goal(s) imported.',invalidFormat:'Invalid format',
|
|
|
|
|
linkLabel:'Link',doneLabel:'done',ofLabel:'of',
|
|
|
|
|
gestern:'Yesterday',heute:'Today',
|
|
|
|
|
expiresAt:'expires:',acceptedBy:'→',
|
2026-05-01 08:06:14 +00:00
|
|
|
adminLabel:'Users',adminColName:'Name',adminColEmail:'Email',adminColRegistered:'Registered',
|
2026-04-30 11:34:41 +00:00
|
|
|
},
|
|
|
|
|
pl: {
|
|
|
|
|
hint:'Menu → "Dodaj do ekranu głównego" aby zainstalować',
|
|
|
|
|
emptyLine1:'Brak celów.',emptyLine2:'Dotknij +, aby zacząć.',
|
|
|
|
|
noEntry:'Brak wpisów',log:'Zapisz',
|
|
|
|
|
noch:'Jeszcze',dAbbr:'d',endet:'kończy',todayShort:'dziś',total:'łącznie',
|
|
|
|
|
todayHeading:'Dziś',done2:'Wykonano',dailyGoal:'Cel dzienny',remaining:'Pozostało',
|
|
|
|
|
history:'Historia — dziś i wczoraj edytowalne',
|
|
|
|
|
legBuf:'Zapas',legDone:'Osiągnięto',legPartial:'Częściowo',legMissed:'Pominięto',
|
|
|
|
|
delGoal:'Usuń cel',
|
|
|
|
|
qbLabel:'Szybki wpis',openLabel:'Otwarte',doneToday:'Dziś ukończone',
|
|
|
|
|
hello:'Cześć {n}!',
|
|
|
|
|
confirmDelete:'Na pewno usunąć cel?',errDelete:'Błąd podczas usuwania',errSave:'Błąd zapisu',
|
|
|
|
|
loginTitle:'Zaloguj się',emailLabel:'E-Mail',passwordLabel:'Hasło',loginBtn:'Zaloguj',
|
|
|
|
|
forgotPw:'Zapomniałeś hasła?',
|
|
|
|
|
loginErrEmpty:'Podaj e-mail i hasło',
|
|
|
|
|
loginErrWrong:'Błędny e-mail lub hasło',loginErrRate:'Zbyt wiele prób',loginErrConn:'Błąd połączenia',
|
|
|
|
|
forgotTitle:'Zapomniane hasło',forgotSub:'Wyślemy Ci link do resetu',
|
|
|
|
|
sendLink:'Wyślij link',back:'Wróć',
|
|
|
|
|
emailSentTitle:'E-mail wysłany',
|
|
|
|
|
emailSentSub:'Jeśli adres jest zarejestrowany, wkrótce otrzymasz link resetujący.',
|
|
|
|
|
ok:'OK',
|
|
|
|
|
resetTitle:'Nowe hasło',newPwLabel:'Nowe hasło',setPw:'Ustaw hasło',min8:'min. 8 znaków',
|
|
|
|
|
pwChangedTitle:'Hasło zmienione',pwChangedSub:'Możesz się teraz zalogować.',
|
|
|
|
|
changeNameTitle:'Zmień nazwę',yourName:'Twoje imię',save:'Zapisz',cancel:'Anuluj',
|
|
|
|
|
errNameEmpty:'Imię nie może być puste',errNameSave:'Błąd zapisu',nameSaved:'Imię zapisano',
|
|
|
|
|
changePwTitle:'Zmień hasło',currentPwLabel:'Aktualne hasło',
|
|
|
|
|
newPwConfLabel:'Potwierdź nowe hasło',changePwBtn:'Zmień',
|
|
|
|
|
errPwMismatch:'Nowe hasła się nie zgadzają',pwChanged:'Hasło zmienione',
|
|
|
|
|
registerTitle:'Utwórz konto',registerSub:'Zostałeś zaproszony',
|
|
|
|
|
namePlaceholder:'Jak mamy Cię nazywać?',pwConfLabel:'Potwierdź hasło',
|
|
|
|
|
pwPlaceholder:'Powtórz hasło',registerBtn:'Zarejestruj',
|
|
|
|
|
errPwMismatch2:'Hasła się nie zgadzają',errFillAll:'Wypełnij wszystkie pola',
|
|
|
|
|
newGoalTitle:'Nowy cel',exerciseLabel:'Ćwiczenie / Nawyk',exercisePlaceholder:'Pompki, Plank …',
|
|
|
|
|
unitLabel:'Jednostka',unitDefault:'szt.',daysLabel:'Czas trwania (dni)',
|
|
|
|
|
dailyLabel:'Cel dzienny',weeklyLabel:'Cel tygodniowy',startGoal:'Rozpocznij cel',errCreate:'Błąd tworzenia',
|
|
|
|
|
dataMenuTitle:'Zarządzaj danymi',dataMenuSub:'Eksport, import i kopia zapasowa',
|
|
|
|
|
exportLabel:'Eksportuj',exportSub:'Zapisz wszystkie cele jako plik JSON',
|
|
|
|
|
importLabel:'Importuj',importSub:'Załaduj lub połącz kopię zapasową',
|
|
|
|
|
inviteLabel:'Zaproś znajomego',inviteSub:'Wygeneruj link zaproszenia',
|
|
|
|
|
inviteListLabel:'Moje zaproszenia',inviteListSub:'Status wszystkich wysłanych zaproszeń',
|
|
|
|
|
changeName:'Zmień nazwę',changePw:'Zmień hasło',logout:'Wyloguj',close:'Zamknij',
|
|
|
|
|
clearAll:'Usuń wszystkie dane',clearAllSub:'Nie można cofnąć',
|
|
|
|
|
confirmClear:'Usunąć wszystkie dane?',
|
|
|
|
|
inviteFormTitle:'Zaproś znajomego',inviteFormSub:'Link ważny 7 dni, jednorazowy',
|
|
|
|
|
inviteNameLabel:'Nazwa (dla Twojej ewidencji)',inviteNamePlaceholder:'np. Max',
|
|
|
|
|
generateLink:'Wygeneruj link',inviteLinkTitle:'Link zaproszenia',
|
|
|
|
|
copyLink:'Kopiuj link',linkCopied:'Link skopiowany!',errGenerate:'Błąd generowania',
|
|
|
|
|
noInvites:'Brak wysłanych zaproszeń',
|
|
|
|
|
statusPending:'Oczekujący',statusUsed:'Zaakceptowany',statusExpired:'Wygasły',
|
|
|
|
|
errLoad:'Błąd ładowania',
|
|
|
|
|
confirmImport:'Importować {n} cel(e)?',importDone:'Zaimportowano {n} cel(e).',invalidFormat:'Nieprawidłowy format',
|
|
|
|
|
linkLabel:'Link',doneLabel:'wykonano',ofLabel:'z',
|
|
|
|
|
gestern:'Wczoraj',heute:'Dziś',
|
|
|
|
|
expiresAt:'wygasa:',acceptedBy:'→',
|
2026-05-01 08:06:14 +00:00
|
|
|
adminLabel:'Użytkownicy',adminColName:'Nazwa',adminColEmail:'E-mail',adminColRegistered:'Rejestracja',
|
2026-04-30 11:34:41 +00:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var LOCALE = 'de';
|
|
|
|
|
|
|
|
|
|
function tr(key){ return (STRINGS[LOCALE]||STRINGS.de)[key]||STRINGS.de[key]||key; }
|
|
|
|
|
function ldoc(){ return LOCALE==='de'?'de-DE':LOCALE==='pl'?'pl-PL':'en-GB'; }
|
|
|
|
|
|
|
|
|
|
function setLocale(lang, save){
|
|
|
|
|
LOCALE = lang;
|
|
|
|
|
if(save){
|
|
|
|
|
api('PATCH','me',{locale:lang}).catch(function(){});
|
|
|
|
|
localStorage.setItem('zt_locale',lang);
|
|
|
|
|
}
|
|
|
|
|
render(); updateHeader();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 13:40:57 +00:00
|
|
|
function loadPref(k,def){ try{ return JSON.parse(localStorage.getItem(k)||def); }catch(e){ return JSON.parse(def); } }
|
|
|
|
|
function saveP(){ localStorage.setItem('zt_p',JSON.stringify(prefs)); }
|
|
|
|
|
prefs = loadPref('zt_p','{}');
|
|
|
|
|
|
|
|
|
|
function tOff(g){ return Math.round((TODAY - new Date(g.start))/86400000); }
|
|
|
|
|
function o2d(g,i){ var d=new Date(new Date(g.start).getTime()+i*86400000); d.setHours(0,0,0,0); return d; }
|
|
|
|
|
function dTot(g,o){ return (g.sets[String(o)]||[]).reduce(function(a,b){return a+b.amount;},0); }
|
2026-04-30 11:34:41 +00:00
|
|
|
function fd(d){ return d.toLocaleDateString(ldoc(),{weekday:'short',day:'numeric',month:'short'}); }
|
|
|
|
|
function fs(d){ return d.toLocaleDateString(ldoc(),{day:'numeric',month:'short'}); }
|
2026-04-29 13:40:57 +00:00
|
|
|
function editable(g,o){ var t=tOff(g); return o===t||o===t-1; }
|
|
|
|
|
function now(){ var n=new Date(); return String(n.getHours()).padStart(2,'0')+':'+String(n.getMinutes()).padStart(2,'0'); }
|
|
|
|
|
|
|
|
|
|
function heuteColor(tdone,daily){
|
|
|
|
|
if(tdone===0) return 'var(--red)';
|
|
|
|
|
if(tdone>=daily*1.1) return 'var(--blue)';
|
|
|
|
|
if(tdone>=daily) return 'var(--green)';
|
|
|
|
|
return 'var(--amber)';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isCollapsed(id){ return collapsed[id]!==false; }
|
|
|
|
|
function toggleCollapse(id){
|
|
|
|
|
var wasCollapsed=isCollapsed(id);
|
|
|
|
|
collapsed[id]=!wasCollapsed;
|
|
|
|
|
if(wasCollapsed){
|
|
|
|
|
var g=goals.filter(function(x){return x.id===id;})[0];
|
|
|
|
|
if(g) selDay[id]=tOff(g);
|
|
|
|
|
}
|
|
|
|
|
render();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function calc(g){
|
|
|
|
|
var t=tOff(g), tot=g.daily*g.days;
|
|
|
|
|
var dr=Math.max(0,g.days-t-1);
|
|
|
|
|
var sd=new Date(g.start); sd.setHours(0,0,0,0);
|
|
|
|
|
var end=new Date(sd.getTime()+g.days*86400000);
|
|
|
|
|
var past=0;
|
|
|
|
|
for(var i=0;i<Math.min(t,g.days);i++) past+=dTot(g,i);
|
|
|
|
|
var tdone=dTot(g,t), tot2=past+tdone;
|
|
|
|
|
var dl=dr+1;
|
|
|
|
|
var remaining=Math.max(0,tot-past);
|
|
|
|
|
var pd=Math.ceil(remaining/Math.max(1,dl));
|
|
|
|
|
var st=Math.max(0,pd-tdone);
|
|
|
|
|
var expectedPast=Math.min(t,g.days)*g.daily;
|
|
|
|
|
var buf=(past-expectedPast)+Math.max(0,tdone-g.daily);
|
|
|
|
|
var deficit=Math.min(0,buf);
|
|
|
|
|
var surplus=Math.max(0,buf);
|
|
|
|
|
var dailyDelta=pd-g.daily;
|
|
|
|
|
var pct=Math.min(100,Math.round((tot2/tot)*100));
|
|
|
|
|
return{tot:tot,tOff:t,end:end,dr:dr,done:tot2,tdone:tdone,pd:pd,st:st,buf:buf,deficit:deficit,surplus:surplus,dailyDelta:dailyDelta,net:tdone-pd,pct:pct,ok:tdone>=pd};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function dcls(g,i){
|
|
|
|
|
var t=tOff(g); if(i>t) return 'dot df';
|
|
|
|
|
var v=dTot(g,i);
|
|
|
|
|
var c=v===0?'dot dm':v>=g.daily*1.1?'dot db':v>=g.daily?'dot dd':'dot dp';
|
|
|
|
|
return c+(editable(g,i)?' de':' dl');
|
|
|
|
|
}
|
|
|
|
|
function dlbl(g,i){
|
|
|
|
|
var t=tOff(g); if(i>t) return String(i+1);
|
|
|
|
|
var v=dTot(g,i);
|
|
|
|
|
if(v===0) return '✕'; if(v>=g.daily*1.1) return '+'; if(v>=g.daily) return '✓';
|
|
|
|
|
return Math.round(v/g.daily*100)+'%';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── API ──────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
function api(method, path, body){
|
|
|
|
|
var opts = {method:method, credentials:'include', headers:{'Content-Type':'application/json'}};
|
|
|
|
|
if(body) opts.body = JSON.stringify(body);
|
|
|
|
|
return fetch('api/' + path, opts).then(function(res){
|
|
|
|
|
return res.json().then(function(data){
|
|
|
|
|
if(!res.ok){ var e=new Error(data.error||'Fehler'); e.status=res.status; throw e; }
|
|
|
|
|
return data;
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function loadGoals(){
|
|
|
|
|
return api('GET','goals').then(function(data){ return data; });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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(function(e){
|
|
|
|
|
if(e.status===401){ showLogin(); }
|
2026-04-30 11:34:41 +00:00
|
|
|
else showToast(tr('errSave'));
|
2026-04-29 13:40:57 +00:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Toast ─────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
function showToast(msg){
|
|
|
|
|
var t=document.createElement('div'); t.className='toast'; t.textContent=msg;
|
|
|
|
|
document.body.appendChild(t); setTimeout(function(){t.remove();},3000);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Goal-Aktionen ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
function addSet(gid,off){
|
|
|
|
|
var g=goals.filter(function(x){return x.id===gid;})[0];
|
|
|
|
|
if(!g||!editable(g,off)) return;
|
|
|
|
|
var k=gid+'_'+off, amt=parseInt(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():'—'});
|
|
|
|
|
addAmt[k]=''; saveGoal(g); render();
|
|
|
|
|
}
|
|
|
|
|
function remSet(gid,off,idx){
|
|
|
|
|
var g=goals.filter(function(x){return x.id===gid;})[0];
|
|
|
|
|
if(!g||!editable(g,off)) return;
|
|
|
|
|
g.sets[String(off)].splice(idx,1); saveGoal(g); render();
|
|
|
|
|
}
|
|
|
|
|
function delGoal(id){
|
2026-04-30 11:34:41 +00:00
|
|
|
if(!confirm(tr('confirmDelete'))) return;
|
2026-04-29 13:40:57 +00:00
|
|
|
goals=goals.filter(function(g){return g.id!==id;});
|
|
|
|
|
render();
|
2026-04-30 11:34:41 +00:00
|
|
|
api('DELETE','goals/'+id).catch(function(){ showToast(tr('errDelete')); });
|
2026-04-29 13:40:57 +00:00
|
|
|
}
|
|
|
|
|
function selD(gid,off){
|
|
|
|
|
var g=goals.filter(function(x){return x.id===gid;})[0];
|
|
|
|
|
if(!g||!editable(g,off)) return;
|
|
|
|
|
selDay[gid]=selDay[gid]===off?null:off; render();
|
|
|
|
|
}
|
|
|
|
|
function startRen(id){
|
|
|
|
|
var g=goals.filter(function(x){return x.id===id;})[0]; if(!g) return;
|
|
|
|
|
renamingId=id; renameVal=g.name; render();
|
|
|
|
|
setTimeout(function(){ var el=document.getElementById('ri'+id); if(el){el.focus();el.select();} },50);
|
|
|
|
|
}
|
|
|
|
|
function commitRen(id){
|
|
|
|
|
var g=goals.filter(function(x){return x.id===id;})[0];
|
|
|
|
|
if(g&&renameVal.trim()){g.name=renameVal.trim(); saveGoal(g);}
|
|
|
|
|
renamingId=null; render();
|
|
|
|
|
}
|
|
|
|
|
function cancelRen(){ renamingId=null; render(); }
|
|
|
|
|
|
|
|
|
|
// ── Template-Helper ───────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
function tpl(id){
|
2026-04-30 11:34:41 +00:00
|
|
|
var el=document.getElementById(id).content.cloneNode(true).firstElementChild;
|
|
|
|
|
el.querySelectorAll('[data-t]').forEach(function(n){ n.textContent=tr(n.dataset.t); });
|
|
|
|
|
el.querySelectorAll('[data-ph]').forEach(function(n){ n.placeholder=tr(n.dataset.ph); });
|
|
|
|
|
el.querySelectorAll('[data-val]').forEach(function(n){ n.value=tr(n.dataset.val); });
|
|
|
|
|
return el;
|
2026-04-29 13:40:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Overlays ──────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
var OV_CSS='display:flex;position:fixed;inset:0;z-index:200;background:rgba(0,0,0,.5);align-items:flex-end;justify-content:center;animation:fi .2s ease';
|
|
|
|
|
|
|
|
|
|
function closeOv(){
|
|
|
|
|
var o=document.getElementById('ov');
|
|
|
|
|
o.style.display='none';
|
|
|
|
|
o.innerHTML='';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function showSheet(content, dismissable){
|
|
|
|
|
var o=document.getElementById('ov');
|
|
|
|
|
o.style.cssText=OV_CSS;
|
|
|
|
|
var sheet=tpl('tpl-sheet');
|
|
|
|
|
sheet.appendChild(content);
|
|
|
|
|
o.innerHTML='';
|
|
|
|
|
o.appendChild(sheet);
|
|
|
|
|
o.onclick=dismissable!==false?function(e){if(e.target===o)closeOv();}:null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Login ─────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
function showLogin(err){
|
|
|
|
|
var c=tpl('tpl-login');
|
|
|
|
|
if(err){ var e=c.querySelector('.login-err'); e.textContent=err; e.style.display=''; }
|
|
|
|
|
showSheet(c,false);
|
|
|
|
|
var email=c.querySelector('.lf-email'), pass=c.querySelector('.lf-pass'), sub=c.querySelector('.lf-sub');
|
|
|
|
|
setTimeout(function(){email.focus();},50);
|
|
|
|
|
email.onkeydown=function(e){if(e.key==='Enter')pass.focus();};
|
|
|
|
|
pass.onkeydown=function(e){if(e.key==='Enter')sub.click();};
|
|
|
|
|
c.querySelector('.lf-fgt').onclick=function(){showForgotPassword();};
|
|
|
|
|
sub.onclick=function(){
|
|
|
|
|
var ev=email.value.trim(), pv=pass.value;
|
2026-04-30 11:34:41 +00:00
|
|
|
if(!ev||!pv){ var errEl=c.querySelector('.login-err'); errEl.textContent=tr('loginErrEmpty'); errEl.style.display=''; return; }
|
2026-04-29 13:40:57 +00:00
|
|
|
sub.disabled=true; sub.textContent='…';
|
|
|
|
|
api('POST','login',{email:ev,password:pv})
|
|
|
|
|
.then(function(){ return loadGoals(); })
|
|
|
|
|
.then(function(g){ goals=g; closeOv(); render(); })
|
|
|
|
|
.catch(function(err){
|
2026-04-30 11:34:41 +00:00
|
|
|
sub.disabled=false; sub.textContent=tr('loginBtn');
|
|
|
|
|
showLogin(err.status===401?tr('loginErrWrong'):err.status===429?tr('loginErrRate'):tr('loginErrConn'));
|
2026-04-29 13:40:57 +00:00
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Passwort vergessen ────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
function showForgotPassword(){
|
|
|
|
|
var c=tpl('tpl-forgot-pw');
|
|
|
|
|
showSheet(c,false);
|
|
|
|
|
var email=c.querySelector('.fp-email'), errEl=c.querySelector('.login-err'), sub=c.querySelector('.fp-sub');
|
|
|
|
|
setTimeout(function(){email.focus();},50);
|
|
|
|
|
c.querySelector('.fp-back').onclick=function(){showLogin();};
|
|
|
|
|
sub.onclick=function(){
|
|
|
|
|
var ev=email.value.trim(); if(!ev) return;
|
|
|
|
|
sub.disabled=true; sub.textContent='…';
|
|
|
|
|
api('POST','reset-request',{email:ev})
|
|
|
|
|
.then(function(){
|
|
|
|
|
var conf=tpl('tpl-email-sent');
|
|
|
|
|
conf.querySelector('.es-ok').onclick=function(){showLogin();};
|
|
|
|
|
showSheet(conf,false);
|
|
|
|
|
})
|
|
|
|
|
.catch(function(err){
|
2026-04-30 11:34:41 +00:00
|
|
|
sub.disabled=false; sub.textContent=tr('sendLink');
|
2026-04-29 13:40:57 +00:00
|
|
|
errEl.textContent=err.message||'Fehler'; errEl.style.display='';
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Passwort zurücksetzen ─────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
function showResetPassword(selector,token){
|
|
|
|
|
var c=tpl('tpl-reset-pw');
|
|
|
|
|
showSheet(c,false);
|
|
|
|
|
var pass=c.querySelector('.rp-pass'), errEl=c.querySelector('.login-err'), sub=c.querySelector('.rp-sub');
|
|
|
|
|
setTimeout(function(){pass.focus();},50);
|
|
|
|
|
sub.onclick=function(){
|
|
|
|
|
var pv=pass.value; if(!pv) return;
|
|
|
|
|
sub.disabled=true; sub.textContent='…';
|
|
|
|
|
api('POST','reset-password',{selector:selector,token:token,password:pv})
|
|
|
|
|
.then(function(){
|
|
|
|
|
var conf=tpl('tpl-pw-changed');
|
|
|
|
|
conf.querySelector('.pc-ok').onclick=function(){showLogin();};
|
|
|
|
|
showSheet(conf,false);
|
|
|
|
|
})
|
|
|
|
|
.catch(function(err){
|
2026-04-30 11:34:41 +00:00
|
|
|
sub.disabled=false; sub.textContent=tr('setPw');
|
2026-04-29 13:40:57 +00:00
|
|
|
errEl.textContent=err.message||'Fehler'; errEl.style.display='';
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Passwort ändern ───────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
function showChangePassword(){
|
|
|
|
|
var c=tpl('tpl-change-pw');
|
|
|
|
|
showSheet(c,true);
|
|
|
|
|
var oldP=c.querySelector('.cp-old'), newP=c.querySelector('.cp-new'), newP2=c.querySelector('.cp-new2');
|
|
|
|
|
var errEl=c.querySelector('.login-err'), sub=c.querySelector('.cp-sub');
|
|
|
|
|
setTimeout(function(){oldP.focus();},50);
|
|
|
|
|
c.querySelector('.cp-can').onclick=closeOv;
|
|
|
|
|
sub.onclick=function(){
|
|
|
|
|
var o=oldP.value, n=newP.value, n2=newP2.value;
|
|
|
|
|
if(!o||!n||!n2) return;
|
2026-04-30 11:34:41 +00:00
|
|
|
if(n!==n2){ errEl.textContent=tr('errPwMismatch'); errEl.style.display=''; return; }
|
2026-04-29 13:40:57 +00:00
|
|
|
sub.disabled=true; sub.textContent='…';
|
|
|
|
|
api('POST','change-password',{old_password:o,new_password:n})
|
2026-04-30 11:34:41 +00:00
|
|
|
.then(function(){ showToast(tr('pwChanged')); closeOv(); })
|
2026-04-29 13:40:57 +00:00
|
|
|
.catch(function(err){
|
2026-04-30 11:34:41 +00:00
|
|
|
sub.disabled=false; sub.textContent=tr('changePwBtn');
|
2026-04-29 13:40:57 +00:00
|
|
|
errEl.textContent=err.message||'Fehler'; errEl.style.display='';
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Registrierung ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
function showRegister(token){
|
|
|
|
|
var c=tpl('tpl-register');
|
|
|
|
|
showSheet(c,false);
|
|
|
|
|
var nameInp=c.querySelector('.rg-name'), email=c.querySelector('.rg-email');
|
|
|
|
|
var pass=c.querySelector('.rg-pass'), pass2=c.querySelector('.rg-pass2');
|
|
|
|
|
var errEl=c.querySelector('.login-err'), sub=c.querySelector('.rg-sub');
|
|
|
|
|
setTimeout(function(){nameInp.focus();},50);
|
|
|
|
|
nameInp.onkeydown=function(e){if(e.key==='Enter')email.focus();};
|
|
|
|
|
email.onkeydown=function(e){if(e.key==='Enter')pass.focus();};
|
|
|
|
|
pass.onkeydown=function(e){if(e.key==='Enter')pass2.focus();};
|
|
|
|
|
pass2.onkeydown=function(e){if(e.key==='Enter')sub.click();};
|
2026-04-30 11:34:41 +00:00
|
|
|
function checkMatch(){ if(pass2.value&&pass.value!==pass2.value){ errEl.textContent=tr('errPwMismatch2'); errEl.style.display=''; } else { errEl.style.display='none'; } }
|
2026-04-29 13:40:57 +00:00
|
|
|
pass.oninput=checkMatch; pass2.oninput=checkMatch;
|
|
|
|
|
sub.onclick=function(){
|
|
|
|
|
var nv=nameInp.value.trim(), ev=email.value.trim(), pv=pass.value;
|
2026-04-30 11:34:41 +00:00
|
|
|
if(!nv||!ev||!pv){ errEl.textContent=tr('errFillAll'); errEl.style.display=''; return; }
|
|
|
|
|
if(pv!==pass2.value){ errEl.textContent=tr('errPwMismatch2'); errEl.style.display=''; return; }
|
2026-04-29 13:40:57 +00:00
|
|
|
sub.disabled=true; sub.textContent='…';
|
|
|
|
|
api('POST','register',{name:nv,email:ev,password:pv,token:token})
|
|
|
|
|
.then(function(r){ userName=r.name||''; return loadGoals(); })
|
|
|
|
|
.then(function(g){ goals=g; closeOv(); updateHeader(); render(); })
|
|
|
|
|
.catch(function(err){
|
2026-04-30 11:34:41 +00:00
|
|
|
sub.disabled=false; sub.textContent=tr('registerBtn');
|
2026-04-29 13:40:57 +00:00
|
|
|
errEl.textContent=err.message||'Fehler'; errEl.style.display='';
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Neues Ziel ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
function openNew(){
|
|
|
|
|
var c=tpl('tpl-new-goal');
|
|
|
|
|
showSheet(c,true);
|
|
|
|
|
var name=c.querySelector('.ng-name'), unit=c.querySelector('.ng-unit');
|
2026-04-30 09:08:23 +00:00
|
|
|
var daily=c.querySelector('.ng-daily'), weekly=c.querySelector('.ng-weekly');
|
|
|
|
|
var days=c.querySelector('.ng-days'), sub=c.querySelector('.ng-sub');
|
|
|
|
|
daily.addEventListener('input',function(){
|
|
|
|
|
if(daily.value) weekly.value=Math.round(parseFloat(daily.value)*7*100)/100;
|
|
|
|
|
});
|
|
|
|
|
weekly.addEventListener('input',function(){
|
|
|
|
|
if(weekly.value) daily.value=Math.round(parseFloat(weekly.value)/7*100)/100;
|
|
|
|
|
});
|
2026-04-29 13:40:57 +00:00
|
|
|
setTimeout(function(){name.focus();},50);
|
|
|
|
|
c.querySelector('.ng-can').onclick=closeOv;
|
|
|
|
|
sub.onclick=function(){
|
2026-04-30 11:34:41 +00:00
|
|
|
var nv=(name.value||'').trim(), uv=(unit.value||'').trim()||tr('unitDefault');
|
2026-04-30 09:08:23 +00:00
|
|
|
var dv=parseFloat(daily.value)||1, dyv=parseInt(days.value,10)||30;
|
2026-04-29 13:40:57 +00:00
|
|
|
if(!nv){ name.focus(); return; }
|
|
|
|
|
sub.disabled=true;
|
|
|
|
|
api('POST','goals',{name:nv,unit:uv,daily:dv,days:dyv,start:TODAY.toISOString()})
|
|
|
|
|
.then(function(r){
|
|
|
|
|
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(function(e){
|
|
|
|
|
sub.disabled=false;
|
|
|
|
|
if(e.status===401){ closeOv(); showLogin(); }
|
2026-04-30 11:34:41 +00:00
|
|
|
else showToast(tr('errCreate'));
|
2026-04-29 13:40:57 +00:00
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Daten-Menü ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
function openData(){
|
|
|
|
|
var c=tpl('tpl-data-menu');
|
|
|
|
|
showSheet(c,true);
|
|
|
|
|
c.querySelector('.dm-cls').onclick=closeOv;
|
|
|
|
|
c.querySelector('.dm-name').onclick=function(){
|
|
|
|
|
var nc=tpl('tpl-change-name');
|
|
|
|
|
showSheet(nc,true);
|
|
|
|
|
var inp=nc.querySelector('.cn-name'), errEl=nc.querySelector('.login-err'), sub=nc.querySelector('.cn-sub');
|
|
|
|
|
inp.value=userName;
|
|
|
|
|
setTimeout(function(){inp.focus();inp.select();},50);
|
|
|
|
|
nc.querySelector('.cn-can').onclick=closeOv;
|
|
|
|
|
sub.onclick=function(){
|
|
|
|
|
var nv=inp.value.trim();
|
2026-04-30 11:34:41 +00:00
|
|
|
if(!nv){ errEl.textContent=tr('errNameEmpty'); errEl.style.display=''; return; }
|
2026-04-29 13:40:57 +00:00
|
|
|
sub.disabled=true; sub.textContent='…';
|
|
|
|
|
api('PATCH','me',{name:nv})
|
2026-04-30 11:34:41 +00:00
|
|
|
.then(function(r){ userName=r.name; closeOv(); render(); showToast(tr('nameSaved')); })
|
|
|
|
|
.catch(function(){ sub.disabled=false; sub.textContent=tr('save'); showToast(tr('errNameSave')); });
|
2026-04-29 13:40:57 +00:00
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
c.querySelector('.dm-cpw').onclick=function(){ closeOv(); showChangePassword(); };
|
|
|
|
|
c.querySelector('.dm-lgout').onclick=function(){
|
|
|
|
|
api('POST','logout').then(function(){ goals=[]; closeOv(); render(); showLogin(); });
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-01 08:06:14 +00:00
|
|
|
var adminBtn=c.querySelector('.dm-admin');
|
|
|
|
|
if(isAdmin){ adminBtn.style.display=''; adminBtn.onclick=function(){ closeOv(); openAdmin(); }; }
|
|
|
|
|
|
2026-04-29 13:40:57 +00:00
|
|
|
c.querySelector('.dm-inv').onclick=function(){
|
|
|
|
|
var ic=tpl('tpl-invite-form');
|
|
|
|
|
showSheet(ic,true);
|
|
|
|
|
var invName=ic.querySelector('.inv-name');
|
|
|
|
|
setTimeout(function(){invName.focus();},50);
|
|
|
|
|
ic.querySelector('.inv-cancel').onclick=closeOv;
|
|
|
|
|
ic.querySelector('.inv-gen').onclick=function(){
|
|
|
|
|
var note=invName.value.trim(), btn=this;
|
|
|
|
|
btn.disabled=true; btn.textContent='…';
|
|
|
|
|
api('POST','invite',{note:note}).then(function(res){
|
|
|
|
|
var lc=tpl('tpl-invite-link');
|
2026-04-30 11:34:41 +00:00
|
|
|
lc.querySelector('.stitle').textContent=tr('inviteLinkTitle')+(note?' — '+note:'');
|
2026-04-29 13:40:57 +00:00
|
|
|
var urlInp=lc.querySelector('.il-url');
|
|
|
|
|
urlInp.value=res.url;
|
|
|
|
|
showSheet(lc,true);
|
|
|
|
|
lc.querySelector('.il-close').onclick=closeOv;
|
|
|
|
|
lc.querySelector('.il-copy').onclick=function(){
|
2026-04-30 11:34:41 +00:00
|
|
|
navigator.clipboard.writeText(res.url).then(function(){ showToast(tr('linkCopied')); closeOv(); });
|
2026-04-29 13:40:57 +00:00
|
|
|
};
|
|
|
|
|
setTimeout(function(){urlInp.select();},50);
|
2026-05-01 08:22:16 +00:00
|
|
|
}).catch(function(err){
|
2026-04-30 11:34:41 +00:00
|
|
|
btn.disabled=false; btn.textContent=tr('generateLink');
|
2026-05-01 08:22:16 +00:00
|
|
|
showToast(err.message||tr('errGenerate'));
|
2026-04-29 13:40:57 +00:00
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
c.querySelector('.dm-invlist').onclick=function(){
|
|
|
|
|
api('GET','invites').then(function(list){
|
2026-04-30 11:34:41 +00:00
|
|
|
var statusLabel={'pending':tr('statusPending'),'used':tr('statusUsed'),'expired':tr('statusExpired')};
|
2026-04-29 13:40:57 +00:00
|
|
|
var statusColor={'pending':'var(--amber)','used':'var(--green)','expired':'var(--red)'};
|
|
|
|
|
var lc=tpl('tpl-invite-list');
|
|
|
|
|
var body=lc.querySelector('.dpanel-body');
|
|
|
|
|
if(!list.length){
|
|
|
|
|
var empty=document.createElement('div');
|
|
|
|
|
empty.className='nosets'; empty.style.padding='16px';
|
2026-04-30 11:34:41 +00:00
|
|
|
empty.textContent=tr('noInvites');
|
2026-04-29 13:40:57 +00:00
|
|
|
body.appendChild(empty);
|
|
|
|
|
} else {
|
|
|
|
|
for(var i=0;i<list.length;i++){
|
|
|
|
|
var inv=list[i];
|
2026-04-30 11:34:41 +00:00
|
|
|
var label=inv.note||new Date(inv.created_at).toLocaleDateString(ldoc(),{day:'numeric',month:'short',year:'numeric'});
|
|
|
|
|
var 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'}):'');
|
2026-04-29 13:40:57 +00:00
|
|
|
var row=tpl('tpl-invite-row');
|
|
|
|
|
row.querySelector('.ir-label').textContent=label;
|
|
|
|
|
if(detail) row.querySelector('.ir-detail').textContent=' '+detail;
|
|
|
|
|
var st=row.querySelector('.ir-status');
|
|
|
|
|
st.textContent=statusLabel[inv.status]; st.style.color=statusColor[inv.status];
|
|
|
|
|
if(inv.url){
|
|
|
|
|
var cp=row.querySelector('.ir-copy'); cp.style.display='';
|
2026-04-30 11:34:41 +00:00
|
|
|
cp.onclick=function(url){ return function(){ navigator.clipboard.writeText(url).then(function(){ showToast(tr('linkCopied')); }); }; }(inv.url);
|
2026-04-29 13:40:57 +00:00
|
|
|
}
|
|
|
|
|
body.appendChild(row);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
showSheet(lc,true);
|
|
|
|
|
lc.querySelector('.il-close').onclick=closeOv;
|
2026-04-30 11:34:41 +00:00
|
|
|
}).catch(function(){ showToast(tr('errLoad')); });
|
2026-04-29 13:40:57 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
c.querySelector('.dm-exp').onclick=function(){
|
|
|
|
|
var blob=new Blob([JSON.stringify({goals:goals,at:new Date().toISOString()},null,2)],{type:'application/json'});
|
|
|
|
|
var 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=function(){
|
|
|
|
|
var inp=document.createElement('input'); inp.type='file'; inp.accept='.json';
|
|
|
|
|
inp.onchange=function(e){
|
|
|
|
|
var f=e.target.files[0]; if(!f) return;
|
|
|
|
|
var r=new FileReader(); r.onload=function(ev){
|
|
|
|
|
try{
|
|
|
|
|
var p=JSON.parse(ev.target.result);
|
2026-04-30 11:34:41 +00:00
|
|
|
if(!p.goals||!Array.isArray(p.goals)) throw new Error(tr('invalidFormat'));
|
|
|
|
|
if(!confirm(tr('confirmImport').replace('{n}',p.goals.length))) return;
|
2026-04-29 13:40:57 +00:00
|
|
|
var promises=p.goals.map(function(g){
|
|
|
|
|
return api('POST','goals',{name:g.name,unit:g.unit,daily:g.daily,days:g.days,start:g.start,sets:g.sets||{}})
|
|
|
|
|
.then(function(r){ goals.push({id:r.id,name:r.name,unit:r.unit,daily:r.daily,days:r.days,start:r.start,sets:r.sets||{}}); });
|
|
|
|
|
});
|
2026-04-30 11:34:41 +00:00
|
|
|
Promise.all(promises).then(function(){ closeOv(); render(); alert(tr('importDone').replace('{n}',p.goals.length)); });
|
|
|
|
|
}catch(err){ alert(err.message); }
|
2026-04-29 13:40:57 +00:00
|
|
|
}; r.readAsText(f);
|
|
|
|
|
}; inp.click();
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-30 11:34:41 +00:00
|
|
|
c.querySelectorAll('.btn-lang').forEach(function(b){
|
|
|
|
|
if(b.dataset.lang===LOCALE) b.classList.add('active');
|
|
|
|
|
b.onclick=function(){ setLocale(this.dataset.lang,true); closeOv(); };
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-29 13:40:57 +00:00
|
|
|
c.querySelector('.dm-clr').onclick=function(){
|
2026-04-30 11:34:41 +00:00
|
|
|
if(!confirm(tr('confirmClear'))) return;
|
2026-04-29 13:40:57 +00:00
|
|
|
var ids=goals.map(function(g){return g.id;}); goals=[]; render();
|
|
|
|
|
Promise.all(ids.map(function(id){return api('DELETE','goals/'+id);}))
|
2026-04-30 11:34:41 +00:00
|
|
|
.catch(function(){ showToast(tr('errDelete')); });
|
2026-04-29 13:40:57 +00:00
|
|
|
closeOv();
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-01 08:06:14 +00:00
|
|
|
// ── Admin ─────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
function openAdmin(){
|
|
|
|
|
api('GET','admin/users').then(function(rows){
|
|
|
|
|
var c=tpl('tpl-admin-users');
|
|
|
|
|
var body=c.querySelector('.au-body');
|
|
|
|
|
rows.forEach(function(u){
|
|
|
|
|
var row=document.createElement('tr');
|
|
|
|
|
row.style.borderBottom='1px solid var(--border)';
|
|
|
|
|
var name=u.username||'—';
|
|
|
|
|
var 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(function(){ showToast(tr('errLoad')); });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function escHtml(s){ return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
|
|
|
|
|
2026-04-29 13:40:57 +00:00
|
|
|
// ── Card-Bausteine ────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
function buildNameWrap(g){
|
|
|
|
|
if(renamingId===g.id){
|
|
|
|
|
var el=tpl('tpl-name-edit');
|
|
|
|
|
var inp=el.querySelector('.ren-input');
|
|
|
|
|
inp.id='ri'+g.id; inp.value=g.name; inp.dataset.g=g.id;
|
|
|
|
|
return el;
|
|
|
|
|
}
|
|
|
|
|
var 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){
|
|
|
|
|
var t=tOff(g), sets=g.sets[String(off)]||[], tot=dTot(g,off);
|
2026-04-30 11:34:41 +00:00
|
|
|
var lbl=off===t?tr('heute'):tr('gestern'), k=g.id+'_'+off;
|
2026-04-29 13:40:57 +00:00
|
|
|
var el=tpl('tpl-panel');
|
|
|
|
|
el.querySelector('.dpanel-title').textContent=lbl+' — '+fd(o2d(g,off));
|
|
|
|
|
el.querySelector('.dpanel-sub').textContent=tot+' / '+g.daily+' '+g.unit;
|
|
|
|
|
var body=el.querySelector('.dpanel-body');
|
|
|
|
|
if(sets.length){
|
|
|
|
|
for(var i=0;i<sets.length;i++){
|
|
|
|
|
var s=sets[i], row=tpl('tpl-set-row'), span=row.querySelector('span');
|
|
|
|
|
if(s.time!=='—'){
|
|
|
|
|
var st=document.createElement('span'); st.className='stime'; st.textContent=s.time+' ·';
|
|
|
|
|
span.appendChild(st); span.appendChild(document.createTextNode(' '));
|
|
|
|
|
}
|
|
|
|
|
var strong=document.createElement('strong'); strong.textContent=s.amount;
|
|
|
|
|
span.appendChild(strong); span.appendChild(document.createTextNode(' '+g.unit));
|
|
|
|
|
var 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'));
|
|
|
|
|
}
|
|
|
|
|
var addRow=tpl('tpl-add-row');
|
|
|
|
|
var inp=addRow.querySelector('.num-in');
|
|
|
|
|
inp.placeholder=g.daily; inp.value=addAmt[k]||''; inp.dataset.k=k; inp.dataset.g=g.id; inp.dataset.o=off;
|
|
|
|
|
var 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){
|
|
|
|
|
var c=calc(g), t=c.tOff;
|
|
|
|
|
var 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)';
|
|
|
|
|
var bc,bt,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;}
|
|
|
|
|
|
|
|
|
|
var 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;
|
|
|
|
|
var bd=el.querySelector('.card-bd');
|
|
|
|
|
bd.insertBefore(buildNameWrap(g),bd.firstElementChild);
|
|
|
|
|
var hc=heuteColor(c.tdone,g.daily);
|
|
|
|
|
el.querySelector('.m-dr').textContent=c.dr;
|
|
|
|
|
el.querySelector('.m-end').textContent=fs(c.end);
|
|
|
|
|
var mH=el.querySelector('.m-heute'); mH.textContent=c.tdone+'/'+g.daily; mH.style.color=hc;
|
|
|
|
|
el.querySelector('.m-total').textContent=c.done+'/'+c.tot;
|
|
|
|
|
var badge=el.querySelector('.badge'); badge.className='badge '+bc; badge.textContent=bt;
|
|
|
|
|
var 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;
|
|
|
|
|
var 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);
|
|
|
|
|
var badge=el.querySelector('.badge'); badge.className='badge '+bc; badge.textContent=bt;
|
|
|
|
|
var fill=el.querySelector('.prog-fill'); fill.style.width=c.pct+'%'; fill.style.background=fc;
|
2026-04-30 11:34:41 +00:00
|
|
|
el.querySelector('.pr-done').textContent=c.done+' '+g.unit+' '+tr('doneLabel');
|
|
|
|
|
el.querySelector('.pr-pct').textContent=c.pct+'% '+tr('ofLabel')+' '+c.tot;
|
2026-04-29 13:40:57 +00:00
|
|
|
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(function(u){ u.textContent=g.unit; });
|
|
|
|
|
|
|
|
|
|
var sel=selDay[g.id]!=null?selDay[g.id]:t;
|
|
|
|
|
var dotsWrap=el.querySelector('.dots-wrap');
|
|
|
|
|
for(var i=0;i<g.days;i++){
|
|
|
|
|
var it=i===t, iy=i===t-1, is=sel===i, ed=editable(g,i);
|
|
|
|
|
var 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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Quick-Buchen ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
function buildQuickBook(){
|
2026-05-01 08:26:05 +00:00
|
|
|
var active=goals.filter(function(g){ var c=calc(g); return tOff(g)<g.days&&(c.buf<0||(c.tdone<g.daily&&c.buf<g.daily)); });
|
2026-04-29 13:40:57 +00:00
|
|
|
if(!active.length) return null;
|
|
|
|
|
var frag=document.createDocumentFragment();
|
2026-04-30 11:34:41 +00:00
|
|
|
var lbl=document.createElement('div'); lbl.className='sec-lbl'; lbl.textContent=tr('qbLabel');
|
2026-04-29 13:40:57 +00:00
|
|
|
frag.appendChild(lbl);
|
|
|
|
|
var card=document.createElement('div'); card.className='card qb-card';
|
|
|
|
|
for(var i=0;i<active.length;i++){
|
|
|
|
|
var g=active[i], c=calc(g), k=g.id+'_'+c.tOff;
|
|
|
|
|
var row=tpl('tpl-qb-row');
|
|
|
|
|
row.querySelector('.qb-name').textContent=g.name;
|
|
|
|
|
var stat=row.querySelector('.qb-stat'); stat.textContent=c.tdone+'/'+g.daily; stat.style.color=heuteColor(c.tdone,g.daily);
|
|
|
|
|
var inp=row.querySelector('.num-in');
|
|
|
|
|
inp.placeholder=g.daily; inp.value=addAmt[k]||''; inp.dataset.k=k; inp.dataset.g=g.id; inp.dataset.o=c.tOff;
|
|
|
|
|
var btn=row.querySelector('.btn-as'); btn.dataset.g=g.id; btn.dataset.o=c.tOff;
|
|
|
|
|
card.appendChild(row);
|
|
|
|
|
}
|
|
|
|
|
frag.appendChild(card);
|
|
|
|
|
return frag;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Render ────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
function calcAwards(){
|
|
|
|
|
var units=0;
|
|
|
|
|
for(var i=0;i<goals.length;i++){
|
|
|
|
|
var g=goals[i];
|
|
|
|
|
if(tOff(g)>=g.days) units+=Math.floor(g.days/30);
|
|
|
|
|
}
|
|
|
|
|
var gold=Math.floor(units/25); units%=25;
|
|
|
|
|
var silver=Math.floor(units/5); var bronze=units%5;
|
|
|
|
|
return{gold:gold,silver:silver,bronze:bronze};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function render(){
|
|
|
|
|
var m=document.getElementById('main');
|
|
|
|
|
var frag=document.createDocumentFragment();
|
|
|
|
|
|
|
|
|
|
if(!prefs.hd){
|
|
|
|
|
var hint=tpl('tpl-hint');
|
|
|
|
|
hint.querySelector('.hclose').onclick=function(){ prefs.hd=1; saveP(); hint.remove(); };
|
|
|
|
|
frag.appendChild(hint);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var aw=calcAwards();
|
|
|
|
|
if(aw.gold||aw.silver||aw.bronze){
|
|
|
|
|
var awards=document.createElement('div'); awards.className='awards';
|
|
|
|
|
var medals=[['🥇',aw.gold],['🥈',aw.silver],['🥉',aw.bronze]];
|
|
|
|
|
for(var mi=0;mi<medals.length;mi++){
|
|
|
|
|
for(var ai=0;ai<medals[mi][1];ai++){
|
|
|
|
|
var sp=document.createElement('span'); sp.className='aw'; sp.textContent=medals[mi][0];
|
|
|
|
|
awards.appendChild(sp);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
frag.appendChild(awards);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(!goals.length){
|
|
|
|
|
frag.appendChild(tpl('tpl-empty'));
|
|
|
|
|
m.innerHTML=''; m.appendChild(frag); wire(); return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(userName){
|
2026-04-30 11:34:41 +00:00
|
|
|
var gr=document.createElement('div'); gr.className='greeting'; gr.textContent=tr('hello').replace('{n}',userName);
|
2026-04-29 13:40:57 +00:00
|
|
|
frag.appendChild(gr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var qb=buildQuickBook(); if(qb) frag.appendChild(qb);
|
|
|
|
|
|
|
|
|
|
var open=[],done=[];
|
|
|
|
|
for(var gi=0;gi<goals.length;gi++){
|
|
|
|
|
var g=goals[gi], c=calc(g);
|
|
|
|
|
if(c.ok) done.push(g); else open.push(g);
|
|
|
|
|
}
|
|
|
|
|
if(open.length){
|
2026-04-30 11:34:41 +00:00
|
|
|
var sl=document.createElement('div'); sl.className='sec-lbl'; sl.textContent=tr('openLabel');
|
2026-04-29 13:40:57 +00:00
|
|
|
frag.appendChild(sl);
|
|
|
|
|
for(var i=0;i<open.length;i++) frag.appendChild(buildCard(open[i]));
|
|
|
|
|
}
|
|
|
|
|
if(done.length){
|
2026-04-30 11:34:41 +00:00
|
|
|
var sl2=document.createElement('div'); sl2.className='sec-lbl'; sl2.textContent=tr('doneToday');
|
2026-04-29 13:40:57 +00:00
|
|
|
frag.appendChild(sl2);
|
|
|
|
|
for(var j=0;j<done.length;j++) frag.appendChild(buildCard(done[j]));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m.innerHTML=''; m.appendChild(frag); wire();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function wire(){
|
|
|
|
|
document.querySelectorAll('.card-hdr[data-g]').forEach(function(el){
|
|
|
|
|
el.onclick=function(e){
|
|
|
|
|
if(e.target.classList.contains('btn-ren')||e.target.classList.contains('ren-input')) return;
|
|
|
|
|
toggleCollapse(this.dataset.g);
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
document.querySelectorAll('.btn-ren').forEach(function(b){
|
|
|
|
|
b.onclick=function(e){e.stopPropagation();startRen(this.dataset.g);};
|
|
|
|
|
});
|
|
|
|
|
document.querySelectorAll('.ren-input').forEach(function(inp){
|
|
|
|
|
var gid=inp.dataset.g;
|
|
|
|
|
inp.oninput=function(){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(function(d){
|
|
|
|
|
d.onclick=function(e){e.stopPropagation();selD(this.dataset.g,parseInt(this.dataset.d,10));};
|
|
|
|
|
});
|
|
|
|
|
document.querySelectorAll('.btn-as').forEach(function(b){
|
|
|
|
|
b.onclick=function(){addSet(this.dataset.g,parseInt(this.dataset.o,10));};
|
|
|
|
|
});
|
|
|
|
|
document.querySelectorAll('.num-in').forEach(function(inp){
|
|
|
|
|
var k=inp.dataset.k, g=inp.dataset.g, o=parseInt(inp.dataset.o,10);
|
|
|
|
|
inp.oninput=function(){addAmt[k]=this.value;};
|
|
|
|
|
inp.onkeydown=function(e){if(e.key==='Enter')addSet(g,o);};
|
|
|
|
|
});
|
|
|
|
|
document.querySelectorAll('.sdel').forEach(function(b){
|
|
|
|
|
b.onclick=function(){remSet(this.dataset.g,parseInt(this.dataset.o,10),parseInt(this.dataset.i,10));};
|
|
|
|
|
});
|
|
|
|
|
document.querySelectorAll('.btn-del').forEach(function(b){
|
|
|
|
|
b.onclick=function(){delGoal(this.dataset.g);};
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Start ─────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
function updateHeader(){
|
2026-04-30 11:34:41 +00:00
|
|
|
document.getElementById('tlbl').textContent=TODAY.toLocaleDateString(ldoc(),{weekday:'long',day:'numeric',month:'long'});
|
2026-04-29 13:40:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
document.getElementById('btnNew').onclick=openNew;
|
|
|
|
|
document.getElementById('btnData').onclick=openData;
|
|
|
|
|
updateHeader();
|
|
|
|
|
|
|
|
|
|
var _qs=new URLSearchParams(window.location.search);
|
|
|
|
|
var inviteToken=_qs.get('invite');
|
|
|
|
|
var resetSelector=_qs.get('reset_selector');
|
|
|
|
|
var resetToken=_qs.get('reset_token');
|
|
|
|
|
if(inviteToken||resetSelector) history.replaceState(null,'',location.pathname);
|
|
|
|
|
|
2026-04-30 11:34:41 +00:00
|
|
|
function applyLocale(userLocale){
|
|
|
|
|
var lang = userLocale || localStorage.getItem('zt_locale');
|
|
|
|
|
if(!lang){
|
|
|
|
|
var nav = (navigator.language||'').slice(0,2).toLowerCase();
|
|
|
|
|
if(STRINGS[nav]) lang = nav;
|
|
|
|
|
}
|
|
|
|
|
if(lang && STRINGS[lang]) LOCALE = lang;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 13:40:57 +00:00
|
|
|
if(resetSelector&&resetToken){
|
2026-04-30 11:34:41 +00:00
|
|
|
applyLocale(null); render(); showResetPassword(resetSelector,resetToken);
|
2026-04-29 13:40:57 +00:00
|
|
|
} else {
|
|
|
|
|
api('GET','me')
|
2026-05-01 08:06:14 +00:00
|
|
|
.then(function(r){ userName=r.name||''; isAdmin=r.is_admin||false; applyLocale(r.locale); updateHeader(); return loadGoals(); })
|
2026-04-29 13:40:57 +00:00
|
|
|
.then(function(g){ goals=g; render(); })
|
|
|
|
|
.catch(function(){
|
2026-04-30 11:34:41 +00:00
|
|
|
applyLocale(null); render();
|
2026-04-29 13:40:57 +00:00
|
|
|
if(inviteToken){ showRegister(inviteToken); }
|
|
|
|
|
else { showLogin(); }
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function scheduleMidnight(){
|
|
|
|
|
var n=new Date();
|
|
|
|
|
var ms=new Date(n.getFullYear(),n.getMonth(),n.getDate()+1,0,0,5).getTime()-n.getTime();
|
|
|
|
|
setTimeout(function(){
|
|
|
|
|
TODAY=new Date();TODAY.setHours(0,0,0,0);selDay={};collapsed={};
|
|
|
|
|
updateHeader();render();scheduleMidnight();
|
|
|
|
|
},ms);
|
|
|
|
|
}
|
|
|
|
|
scheduleMidnight();
|
|
|
|
|
|
|
|
|
|
document.addEventListener('visibilitychange',function(){
|
|
|
|
|
if(document.visibilityState==='visible'){
|
|
|
|
|
var n=new Date();n.setHours(0,0,0,0);
|
|
|
|
|
if(n.getTime()!==TODAY.getTime()){TODAY=n;selDay={};collapsed={};render();scheduleMidnight();}
|
|
|
|
|
loadGoals().then(function(g){goals=g;render();}).catch(function(){});
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-04-30 08:35:29 +00:00
|
|
|
|
2026-04-30 09:00:54 +00:00
|
|
|
var sw = (function(){
|
2026-04-30 08:35:29 +00:00
|
|
|
var swEl = document.getElementById('sw');
|
|
|
|
|
var state = 0; // 0=stopped, 1=running, 2=paused
|
|
|
|
|
var start = 0, elapsed = 0, raf = null;
|
|
|
|
|
|
2026-04-30 09:00:54 +00:00
|
|
|
function getMs(){ return state === 1 ? elapsed + (Date.now() - start) : elapsed; }
|
|
|
|
|
|
|
|
|
|
function updateFillBtns(){
|
|
|
|
|
var show = getMs() >= 1000;
|
|
|
|
|
document.querySelectorAll('.btn-sw-fill').forEach(function(b){ b.style.display = show ? '' : 'none'; });
|
2026-04-30 08:35:29 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-30 09:00:54 +00:00
|
|
|
function fmt(ms){ return (ms / 1000).toFixed(2) + 's'; }
|
|
|
|
|
|
2026-04-30 08:35:29 +00:00
|
|
|
function tick(){
|
|
|
|
|
swEl.textContent = fmt(Date.now() - start + elapsed);
|
2026-04-30 09:00:54 +00:00
|
|
|
updateFillBtns();
|
2026-04-30 08:35:29 +00:00
|
|
|
raf = requestAnimationFrame(tick);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
swEl.addEventListener('click', function(){
|
|
|
|
|
if(state === 0){
|
|
|
|
|
start = Date.now(); elapsed = 0;
|
|
|
|
|
swEl.classList.add('running');
|
|
|
|
|
state = 1; tick();
|
|
|
|
|
} else if(state === 1){
|
|
|
|
|
cancelAnimationFrame(raf);
|
|
|
|
|
elapsed += Date.now() - start;
|
|
|
|
|
swEl.textContent = fmt(elapsed);
|
|
|
|
|
swEl.classList.remove('running');
|
2026-04-30 09:00:54 +00:00
|
|
|
state = 2; updateFillBtns();
|
2026-04-30 08:35:29 +00:00
|
|
|
} else {
|
|
|
|
|
cancelAnimationFrame(raf);
|
|
|
|
|
elapsed = 0; swEl.textContent = '0.00s';
|
|
|
|
|
swEl.classList.remove('running');
|
2026-04-30 09:00:54 +00:00
|
|
|
state = 0; updateFillBtns();
|
2026-04-30 08:35:29 +00:00
|
|
|
}
|
|
|
|
|
});
|
2026-04-30 09:00:54 +00:00
|
|
|
|
|
|
|
|
document.addEventListener('click', function(e){
|
|
|
|
|
if(!e.target.classList.contains('btn-sw-fill')) return;
|
|
|
|
|
var inp = e.target.closest('.add-row, .qb-row').querySelector('.num-in');
|
2026-05-01 08:23:41 +00:00
|
|
|
if(inp){ inp.value = Math.floor(getMs() / 1000); inp.dispatchEvent(new Event('input')); }
|
2026-04-30 09:00:54 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return { getMs: getMs };
|
2026-04-30 08:35:29 +00:00
|
|
|
})();
|