Files
xxx-sphere-web/bin/main/static/konto/einstellungen.html
Mario 87c85b1b17
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
Bugfixes, Dating angefangen
2026-04-01 22:06:46 +02:00

1517 lines
66 KiB
HTML
Raw 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.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Einstellungen xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.settings-page h1 {
font-size: 1.6rem;
margin-bottom: 1.5rem;
}
/* ── Accordion ── */
.settings-section {
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 10px;
margin-bottom: 0.75rem;
overflow: hidden;
}
.settings-section-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
padding: 1rem 1.25rem;
cursor: pointer;
user-select: none;
transition: background 0.15s;
}
.settings-section-header:hover {
background: rgba(255,255,255,0.03);
}
.settings-section-title {
display: flex;
align-items: center;
gap: 0.6rem;
font-size: 1rem;
font-weight: 600;
}
.settings-section-arrow {
font-size: 0.75rem;
color: var(--color-muted);
transition: transform 0.2s;
}
.settings-section.open .settings-section-arrow {
transform: rotate(90deg);
}
.settings-section-body {
display: none;
padding: 0 1.25rem 1.25rem;
border-top: 1px solid var(--color-secondary);
}
.settings-section.open .settings-section-body {
display: block;
}
/* ── Einstellungs-Zeilen ── */
.settings-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.9rem 0;
}
.settings-row-info {
flex: 1;
}
.settings-row-label {
font-size: 0.95rem;
font-weight: 500;
margin-bottom: 0.15rem;
}
.settings-row-desc {
font-size: 0.8rem;
color: var(--color-muted);
}
.settings-row select {
width: auto;
min-width: 150px;
padding: 0.45rem 0.75rem;
margin: 0;
font-size: 0.9rem;
}
/* ── Benachrichtigungen Grid ── */
.notif-grid {
display: grid;
grid-template-columns: 1fr 5rem 5rem;
align-items: center;
}
.notif-grid-row {
display: contents;
}
.notif-grid-row > * {
padding: 0.75rem 0;
}
.notif-grid-row.notif-header > * {
padding-bottom: 0.4rem;
}
.notif-col-check {
display: flex;
justify-content: center;
align-items: center;
}
.no-spinner::-webkit-outer-spin-button,
.no-spinner::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.notif-col-check input[type="checkbox"] {
width: 1.1rem;
height: 1.1rem;
accent-color: var(--color-primary, #c0392b);
cursor: pointer;
}
/* ── Speichern-Feedback (Toast) ── */
.save-toast {
position: fixed;
bottom: 1.5rem;
left: 50%;
transform: translateX(-50%) translateY(2rem);
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-left: 3px solid var(--color-success, #4caf50);
border-radius: 8px;
padding: 0.6rem 1.25rem;
font-size: 0.88rem;
color: var(--color-success, #4caf50);
opacity: 0;
pointer-events: none;
transition: opacity 0.2s, transform 0.2s;
z-index: 500;
white-space: nowrap;
}
.save-toast.visible {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
/* ── Spiel-Einstellungen Check-Items ── */
.spiel-check-group {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
margin-top: 0.35rem;
}
.spiel-check-item {
display: inline-flex;
align-items: center;
gap: 0.4rem;
background: var(--color-secondary);
border: 1px solid transparent;
border-radius: 6px;
padding: 0.35rem 0.65rem;
cursor: pointer;
transition: border-color 0.15s;
user-select: none;
font-size: 0.88rem;
}
.spiel-check-item.is-checked { border-color: var(--color-primary); }
.spiel-check-item input {
accent-color: var(--color-primary);
width: auto;
cursor: pointer;
}
.spiel-subsection {
margin-bottom: 1.25rem;
}
.spiel-subsection-title {
font-size: 1rem;
font-weight: 600;
color: var(--color-text);
margin-bottom: 0.75rem;
padding-bottom: 0.4rem;
border-bottom: 1px solid var(--color-secondary);
}
.spiel-field { margin-bottom: 1rem; }
.spiel-field > .settings-row-label { margin-bottom: 0.2rem; }
/* ── Modal ── */
.modal-backdrop {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.6);
z-index: 200;
align-items: center;
justify-content: center;
}
.modal-backdrop.visible { display: flex; }
.modal {
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 12px;
padding: 2rem;
width: 100%;
max-width: 360px;
box-shadow: 0 8px 32px rgba(0,0,0,0.6);
}
.modal h2 {
color: var(--color-primary);
font-size: 1.2rem;
margin-bottom: 1.25rem;
}
.modal-actions {
display: flex;
gap: 0.75rem;
margin-top: 1.25rem;
}
.modal-actions button { flex: 1; margin-top: 0; }
/* ── Separator ── */
.settings-separator {
border: none;
border-top: 1px solid var(--color-secondary);
margin: 1.25rem 0 0;
}
/* ── Vorschau-Buttons ── */
.preview-row {
padding-top: 1.25rem;
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
align-items: center;
}
.preview-row span {
font-size: 0.85rem;
color: var(--color-muted);
flex-basis: 100%;
margin-bottom: 0.25rem;
}
.preview-row button {
width: auto;
padding: 0.5rem 1.1rem;
margin: 0;
font-size: 0.9rem;
}
</style>
</head>
<body class="app">
<div class="main">
<div class="content">
<h1>⚙️ Einstellungen</h1>
<!-- Grunddaten -->
<div class="settings-section" id="sec-grunddaten">
<div class="settings-section-header" onclick="toggleSection('sec-grunddaten')">
<span class="settings-section-title">👤 Grunddaten</span>
<span class="settings-section-arrow"></span>
</div>
<div class="settings-section-body">
<div class="settings-row">
<div class="settings-row-info">
<div class="settings-row-label">Nickname</div>
<div class="settings-row-desc" id="gd-name"></div>
</div>
<button onclick="openNameDialog()" style="width:auto;padding:0.45rem 1rem;margin:0;font-size:0.9rem;">Ändern</button>
</div>
<div class="settings-row">
<div class="settings-row-info">
<div class="settings-row-label">E-Mail</div>
<div class="settings-row-desc" id="gd-email"></div>
</div>
<button onclick="openEmailDialog()" style="width:auto;padding:0.45rem 1rem;margin:0;font-size:0.9rem;">Ändern</button>
</div>
<div class="settings-row">
<div class="settings-row-info">
<div class="settings-row-label">Geburtsdatum</div>
<div class="settings-row-desc" id="gd-geburtsdatum"></div>
</div>
<button onclick="openGeburtsdatumDialog()" style="width:auto;padding:0.45rem 1rem;margin:0;font-size:0.9rem;">Ändern</button>
</div>
<div class="settings-row">
<div class="settings-row-info">
<div class="settings-row-label" style="color:var(--color-primary);">Konto löschen</div>
<div class="settings-row-desc">Alle Daten werden unwiderruflich gelöscht</div>
</div>
<button onclick="openDeleteDialog()" style="width:auto;padding:0.45rem 1rem;margin:0;font-size:0.9rem;background:transparent;border:1px solid var(--color-secondary);color:var(--color-muted);">Löschen</button>
</div>
</div>
</div>
<!-- Spiel Einstellungen -->
<div class="settings-section" id="sec-spiel">
<div class="settings-section-header" onclick="toggleSection('sec-spiel')">
<span class="settings-section-title">🕹️ Spiel Einstellungen</span>
<span class="settings-section-arrow"></span>
</div>
<div class="settings-section-body">
<!-- BDSM Game -->
<div class="spiel-subsection">
<div class="spiel-subsection-title">BDSM Game</div>
<div class="spiel-field">
<div class="settings-row-label">Spiele mit Geschlecht</div>
<div class="spiel-check-group" id="bdsm-spieltmit">
<label class="spiel-check-item"><input type="checkbox" value="WEIBLICH"> Weiblich</label>
<label class="spiel-check-item"><input type="checkbox" value="DIVERS"> Divers</label>
<label class="spiel-check-item"><input type="checkbox" value="MAENNLICH"> Männlich</label>
</div>
</div>
<div class="spiel-field">
<div class="settings-row-label">Meine Rollen</div>
<div class="spiel-check-group" id="bdsm-rollen">
<label class="spiel-check-item"><input type="checkbox" value="AUFGABE_AKTIV"> Aufgaben aktiv</label>
<label class="spiel-check-item"><input type="checkbox" value="AUFGABE_PASSIV"> Aufgaben passiv</label>
<label class="spiel-check-item"><input type="checkbox" value="BESTRAFUNG_AKTIV"> Bestrafungen aktiv</label>
<label class="spiel-check-item"><input type="checkbox" value="BESTRAFUNG_PASSIV"> Bestrafungen passiv</label>
</div>
</div>
<div class="spiel-field">
<div class="settings-row-label">Was ich einsetze</div>
<div class="spiel-check-group" id="bdsm-werkzeuge">
<label class="spiel-check-item"><input type="checkbox" value="MUND"> Mund</label>
<label class="spiel-check-item"><input type="checkbox" value="VAGINA"> Vagina</label>
<label class="spiel-check-item"><input type="checkbox" value="PENIS"> Penis</label>
<label class="spiel-check-item"><input type="checkbox" value="ANUS"> Anus</label>
<label class="spiel-check-item"><input type="checkbox" value="UMSCHNALLDILDO"> Umschnall-Dildo</label>
</div>
</div>
</div>
</div>
</div>
<!-- Benachrichtigungen -->
<div class="settings-section" id="sec-benachrichtigungen">
<div class="settings-section-header" onclick="toggleSection('sec-benachrichtigungen')">
<span class="settings-section-title">🔔 Benachrichtigungen</span>
<span class="settings-section-arrow"></span>
</div>
<div class="settings-section-body">
<div class="notif-grid">
<!-- Header -->
<div class="notif-grid-row notif-header">
<div></div>
<div class="notif-col-check settings-row-label">In-App</div>
<div class="notif-col-check settings-row-label">E-Mail</div>
</div>
<!-- Einladungen -->
<div class="notif-grid-row">
<div>
<div class="settings-row-label">Einladungen</div>
<div class="settings-row-desc">Einladungen zu Locks und Spielen, Annahmen und Ablehnungen</div>
</div>
<div class="notif-col-check">
<input type="checkbox" id="notif-INVITATION-inApp" onchange="saveNotifications()">
</div>
<div class="notif-col-check">
<input type="checkbox" id="notif-INVITATION-email" onchange="saveNotifications()">
</div>
</div>
<!-- Spielstatus -->
<div class="notif-grid-row">
<div>
<div class="settings-row-label">Spielstatus</div>
<div class="settings-row-desc">Karten, Aufgaben, Verifikationen, Einfrierungen und andere Spielereignisse</div>
</div>
<div class="notif-col-check">
<input type="checkbox" id="notif-GAME_STATE-inApp" onchange="saveNotifications()">
</div>
<div class="notif-col-check">
<input type="checkbox" id="notif-GAME_STATE-email" onchange="saveNotifications()">
</div>
</div>
<!-- Notfall -->
<div class="notif-grid-row">
<div>
<div class="settings-row-label">Notfall</div>
<div class="settings-row-desc">Notfall-Entsperrungen und dringende Meldungen</div>
</div>
<div class="notif-col-check">
<input type="checkbox" id="notif-EMERGENCY-inApp" onchange="saveNotifications()">
</div>
<div class="notif-col-check">
<input type="checkbox" id="notif-EMERGENCY-email" onchange="saveNotifications()">
</div>
</div>
<!-- Freundschaftsanfragen -->
<div class="notif-grid-row">
<div>
<div class="settings-row-label">Freundschaftsanfragen</div>
<div class="settings-row-desc">Neue Freundschaftsanfragen von anderen Nutzern</div>
</div>
<div class="notif-col-check">
<input type="checkbox" id="notif-FRIENDREQUEST-inApp" checked disabled
title="In-App-Benachrichtigungen für Freundschaftsanfragen sind immer aktiv">
</div>
<div class="notif-col-check">
<input type="checkbox" id="notif-FRIENDREQUEST-email" onchange="saveNotifications()">
</div>
</div>
</div>
</div>
</div>
<!-- Abonnements -->
<div class="settings-section" id="sec-abonnements">
<div class="settings-section-header" onclick="toggleSection('sec-abonnements')">
<span class="settings-section-title">⭐ Abonnements</span>
<span class="settings-section-arrow"></span>
</div>
<div class="settings-section-body">
<div class="settings-row">
<div class="settings-row-info">
<div class="settings-row-label">Aktuelles Abo</div>
<div class="settings-row-desc" id="abo-type-desc">Wird geladen…</div>
</div>
<span id="abo-type-badge" style="font-weight:600;"></span>
</div>
<div id="abo-details" style="display:none;">
<div class="settings-row">
<div class="settings-row-info">
<div class="settings-row-label">Gültig bis</div>
</div>
<span id="abo-valid-until" style="font-size:0.9rem;"></span>
</div>
<div class="settings-row">
<div class="settings-row-info">
<div class="settings-row-label">Kündbar ab</div>
</div>
<span id="abo-cancellable-from" style="font-size:0.9rem;"></span>
</div>
</div>
<div style="margin-top:0.75rem;">
<a href="/community/abonnements.html" style="color:var(--color-primary);font-size:0.9rem;">
Abo-Modelle ansehen &amp; Abo abschließen →
</a>
</div>
</div>
</div>
<!-- Dating -->
<div class="settings-section" id="sec-dating">
<div class="settings-section-header" onclick="toggleSection('sec-dating')">
<span class="settings-section-title">♥ Dating</span>
<span class="settings-section-arrow"></span>
</div>
<div class="settings-section-body">
<div class="settings-row">
<div class="settings-row-info">
<div class="settings-row-label">Dating aktivieren</div>
<div class="settings-row-desc">Zeige dein Profil im Dating-Bereich. Ein Standort ist erforderlich.</div>
</div>
<label style="display:flex;align-items:center;gap:0.5rem;cursor:pointer;">
<input type="checkbox" id="datingAktiv" style="width:1.1rem;height:1.1rem;accent-color:var(--color-primary);cursor:pointer;" onchange="onDatingToggle()">
</label>
</div>
<div id="datingStadtRow" style="display:none;">
<div class="settings-row" style="flex-wrap:wrap;gap:0.5rem;">
<div class="settings-row-info">
<div class="settings-row-label">Standort (Stadt)</div>
<div class="settings-row-desc">Wird im Dating-Bereich angezeigt.</div>
</div>
</div>
<div style="display:flex;gap:0.5rem;margin-bottom:0.35rem;position:relative;">
<div style="position:relative;flex:1;display:flex;">
<input type="text" id="datingStadt" placeholder="Stadt suchen und auswählen…" autocomplete="off"
style="flex:1;padding:0.55rem 2rem 0.55rem 0.8rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;outline:none;"
oninput="onStadtInput()">
<button id="datingStadtClear" onclick="clearStadt()" title="Auswahl aufheben"
style="display:none;position:absolute;right:0.4rem;top:50%;transform:translateY(-50%);
background:none;border:none;color:var(--color-muted);font-size:1.1rem;cursor:pointer;padding:0.1rem 0.3rem;margin:0;line-height:1;">×</button>
</div>
<button onclick="detectLocation()" title="Standort ermitteln"
style="white-space:nowrap;padding:0.55rem 0.9rem;margin:0;font-size:0.85rem;">
⌖ Ermitteln
</button>
<ul id="stadtSuggestions" style="display:none;position:absolute;top:100%;left:0;right:3.5rem;
background:var(--color-card);border:1px solid var(--color-secondary);border-radius:6px;
z-index:100;list-style:none;margin:0.2rem 0 0;padding:0;max-height:200px;overflow-y:auto;"></ul>
</div>
<div id="datingLocMsg" style="font-size:0.82rem;color:var(--color-muted);margin-bottom:0.5rem;"></div>
</div>
<div class="settings-row" style="border-top:1px solid var(--color-secondary);padding-top:0.75rem;margin-top:0.25rem;">
<button onclick="saveDating()" id="saveDatingBtn" style="margin:0;padding:0.55rem 1.25rem;font-size:0.9rem;">Speichern</button>
<span id="datingMsg" style="font-size:0.85rem;"></span>
</div>
</div>
</div>
<!-- Datenschutz -->
<div class="settings-section" id="sec-datenschutz">
<div class="settings-section-header" onclick="toggleSection('sec-datenschutz')">
<span class="settings-section-title">🛡️ Datenschutz</span>
<span class="settings-section-arrow"></span>
</div>
<div class="settings-section-body">
<!-- Grunddaten -->
<div class="settings-row">
<div class="settings-row-info">
<div class="settings-row-label">Grunddaten</div>
<div class="settings-row-desc">Alter, Größe, Gewicht, Geschlecht, Neigung, Beziehungsstatus, Beschreibung</div>
</div>
<select id="sv-grunddaten" onchange="doSave()">
<option value="ALLE">Alle</option>
<option value="NUR_FREUNDE">Nur Freunde</option>
</select>
</div>
<!-- Galerie -->
<div class="settings-row">
<div class="settings-row-info">
<div class="settings-row-label">Galerie</div>
<div class="settings-row-desc">Fotos auf dem Profil</div>
</div>
<select id="sv-galerie" onchange="doSave()">
<option value="ALLE">Alle</option>
<option value="NUR_FREUNDE">Nur Freunde</option>
</select>
</div>
<!-- Freunde -->
<div class="settings-row">
<div class="settings-row-info">
<div class="settings-row-label">Freundesliste</div>
<div class="settings-row-desc">Wer kann sehen, wer deine Freunde sind</div>
</div>
<select id="sv-freunde" onchange="doSave()">
<option value="ALLE">Alle</option>
<option value="NUR_FREUNDE">Nur Freunde</option>
</select>
</div>
<!-- Feed -->
<div class="settings-row">
<div class="settings-row-info">
<div class="settings-row-label">Feed / Posts</div>
<div class="settings-row-desc">Posts auf dem Profil-Tab</div>
</div>
<select id="sv-feed" onchange="doSave()">
<option value="ALLE">Alle</option>
<option value="NUR_FREUNDE">Nur Freunde</option>
</select>
</div>
<!-- Pinnwand -->
<div class="settings-row">
<div class="settings-row-info">
<div class="settings-row-label">Pinnwand</div>
<div class="settings-row-desc">Einträge auf der Pinnwand</div>
</div>
<select id="sv-pinnwand" onchange="doSave()">
<option value="ALLE">Alle</option>
<option value="NUR_FREUNDE">Nur Freunde</option>
</select>
</div>
<!-- XP -->
<div class="settings-row">
<div class="settings-row-info">
<div class="settings-row-label">XP-Punkte</div>
<div class="settings-row-desc">Lockee-XP und Keyholder-XP</div>
</div>
<select id="sv-xp" onchange="doSave()">
<option value="ALLE">Alle</option>
<option value="NUR_FREUNDE">Nur Freunde</option>
<option value="NUR_ICH">Nur ich</option>
</select>
</div>
<!-- Lock-Historie -->
<div class="settings-row">
<div class="settings-row-info">
<div class="settings-row-label">Lock-Historie</div>
<div class="settings-row-desc">Abgeschlossene Locks und Keyholder-Aktivitäten</div>
</div>
<select id="sv-lockhistorie" onchange="doSave()">
<option value="ALLE">Alle</option>
<option value="NUR_FREUNDE">Nur Freunde</option>
<option value="NUR_ICH">Nur ich</option>
</select>
</div>
<!-- Vorlieben -->
<div class="settings-row">
<div class="settings-row-info">
<div class="settings-row-label">Vorlieben</div>
<div class="settings-row-desc">Deine Vorlieben und Bewertungen im Profil</div>
</div>
<select id="sv-vorlieben" onchange="doSave()">
<option value="ALLE">Alle</option>
<option value="NUR_FREUNDE">Nur Freunde</option>
<option value="NUR_ICH">Nur ich</option>
</select>
</div>
<!-- Profil bei Veröffentlichungen -->
<div class="settings-row">
<div class="settings-row-info">
<div class="settings-row-label">Profil bei Veröffentlichungen sichtbar</div>
<div class="settings-row-desc">Dein Name wird bei veröffentlichten Lock-Vorlagen angezeigt</div>
</div>
<select id="sv-veroeffentlichungen" onchange="doSave()">
<option value="true">Ja</option>
<option value="false">Nein</option>
</select>
</div>
<hr class="settings-separator">
<!-- Vorschau -->
<div class="preview-row">
<span>Profil-Vorschau wie sieht mein Profil für andere aus?</span>
<button onclick="openPreview('FREUND')">👥 Vorschau als Freund</button>
<button onclick="openPreview('UNBEKANNT')" style="background:var(--color-secondary);color:var(--color-text);">👤 Vorschau als Fremder</button>
</div>
</div>
</div>
<!-- TTLock -->
<div class="settings-section" id="sec-ttlock">
<div class="settings-section-header" onclick="toggleSection('sec-ttlock')">
<span class="settings-section-title">🔒 TTLock</span>
<span class="settings-section-arrow"></span>
</div>
<div class="settings-section-body">
<p style="font-size:0.85rem;color:var(--color-muted);margin:0.75rem 0 1.25rem 0;">
Verknüpfe deinen TTLock-Account, um deine physische Schlüsselbox direkt über das Spiel zu steuern.
</p>
<div style="margin-top:1rem;font-size:0.82rem;">
<a href="/help/ttlock.html" style="color:var(--color-muted);">❓ Hilfe zur TTLock-Integration</a>
</div>
<div class="spiel-field">
<div class="settings-row-label">Benutzername (E-Mail)</div>
<input type="text" id="ttl-username" placeholder="E-Mail-Adresse bei TTLock"
style="margin-top:0.3rem;" autocomplete="off">
</div>
<div class="spiel-field">
<div class="settings-row-label">Passwort <span id="ttl-pw-hint" style="font-size:0.78rem;color:var(--color-muted);font-weight:400;"></span></div>
<input type="password" id="ttl-password" placeholder="Leer lassen = unverändert"
style="margin-top:0.3rem;" autocomplete="new-password"
onfocus="ttlPwFocus()" onblur="ttlPwBlur()">
</div>
<div class="spiel-field">
<div class="settings-row-label">Lock-ID</div>
<input type="number" id="ttl-lockid" placeholder="z.B. 30158446"
style="margin-top:0.3rem;max-width:220px;-moz-appearance:textfield;" min="0"
class="no-spinner">
</div>
<div id="ttl-error" style="font-size:0.82rem;color:var(--color-primary);min-height:1.1em;margin-bottom:0.5rem;"></div>
<!-- Verbindungsstatus -->
<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.75rem;">
<span style="font-size:0.82rem;color:var(--color-muted);">Verbindungsstatus:</span>
<span id="ttl-test-status" style="font-size:0.82rem;font-weight:600;"></span>
</div>
<div style="display:flex;gap:0.6rem;flex-wrap:wrap;align-items:center;">
<button onclick="saveTtlockUserConfig()" style="width:auto;padding:0.5rem 1.25rem;margin:0;">Speichern</button>
<button onclick="testTtlockConnection()" id="ttl-test-btn" style="width:auto;padding:0.5rem 1.25rem;margin:0;background:none;border:1px solid var(--color-secondary);color:var(--color-muted);">🔌 Verbindung testen</button>
<button onclick="ttlockOpenOnce()" id="ttl-open-btn" style="width:auto;padding:0.5rem 1.25rem;margin:0;background:none;border:1px solid var(--color-secondary);color:var(--color-muted);">🔓 Öffnen</button>
</div>
<!-- Test-Ergebnis -->
<div id="ttl-test-result" style="display:none;margin-top:1rem;padding:0.85rem 1rem;border-radius:8px;border:1px solid var(--color-secondary);font-size:0.85rem;"></div>
<!-- Öffnen-Ergebnis -->
<div id="ttl-open-error" style="font-size:0.82rem;color:var(--color-primary);min-height:1.1em;margin-top:0.5rem;"></div>
</div>
</div>
</div>
</div>
<!-- TTLock Lade-Dialog -->
<div class="modal-backdrop" id="ttlLoadingModal">
<div class="modal" style="max-width:320px;text-align:center;padding:2rem 1.5rem;">
<div style="font-size:2rem;margin-bottom:0.75rem;"></div>
<div style="font-weight:600;margin-bottom:0.4rem;">TTLock-Kommunikation läuft…</div>
<div style="font-size:0.85rem;color:var(--color-muted);">Bitte warten, der TTLock-Server wird kontaktiert.</div>
</div>
</div>
<!-- TTLock Öffnen Modal -->
<div class="modal-backdrop" id="ttlOpenModal">
<div class="modal" style="max-width:380px;text-align:center;">
<h2>🔓 Schloss öffnen</h2>
<p style="color:var(--color-muted);font-size:0.85rem;margin-bottom:1rem;">Gib diesen PIN an deinem TTLock-Schloss ein:</p>
<div id="ttl-open-pin" style="font-size:2.2rem;font-weight:700;letter-spacing:0.25em;color:var(--color-primary);margin:0.5rem 0 1.5rem 0;"></div>
<p style="font-size:0.8rem;color:var(--color-muted);margin-bottom:1.25rem;">Nach „OK" wird dieser Code gelöscht und kann nicht mehr verwendet werden.</p>
<div class="modal-actions" style="justify-content:center;">
<button id="ttl-open-ok-btn" class="btn-save" style="min-width:120px;">OK</button>
</div>
</div>
</div>
<!-- Nickname Modal -->
<div class="modal-backdrop" id="nameModal">
<div class="modal">
<h2>Nickname ändern</h2>
<label for="newName">Neuer Nickname</label>
<input type="text" id="newName" placeholder="Neuer Name" autocomplete="off">
<div class="message" id="nameMessage"></div>
<div class="modal-actions">
<button class="secondary" onclick="closeNameDialog()">Abbrechen</button>
<button id="nameConfirmBtn" onclick="saveName()">Speichern</button>
</div>
</div>
</div>
<!-- Geburtsdatum Modal -->
<div class="modal-backdrop" id="geburtsdatumModal">
<div class="modal">
<h2>Geburtsdatum ändern</h2>
<label for="newGeburtsdatum">Neues Geburtsdatum</label>
<input type="date" id="newGeburtsdatum" autocomplete="bday">
<div class="message" id="geburtsdatumMessage"></div>
<div class="modal-actions">
<button class="secondary" onclick="closeGeburtsdatumDialog()">Abbrechen</button>
<button id="geburtsdatumConfirmBtn" onclick="saveGeburtsdatum()">Speichern</button>
</div>
</div>
</div>
<!-- E-Mail Modal -->
<div class="modal-backdrop" id="emailModal">
<div class="modal">
<h2>E-Mail-Adresse ändern</h2>
<label for="newEmail">Neue E-Mail-Adresse</label>
<input type="email" id="newEmail" placeholder="neue@email.de" autocomplete="off">
<div class="message" id="emailMessage"></div>
<div class="modal-actions">
<button class="secondary" onclick="closeEmailDialog()">Abbrechen</button>
<button id="emailConfirmBtn" onclick="requestEmailChange()">Bestätigungsmail senden</button>
</div>
</div>
</div>
<!-- Konto löschen Modal -->
<div class="modal-backdrop" id="deleteModal">
<div class="modal">
<h2>Konto löschen</h2>
<p style="color:var(--color-muted);font-size:0.9rem;margin-bottom:0.75rem;">
Dein Konto sowie alle gespeicherten Aufgaben und Toys werden unwiderruflich gelöscht.
</p>
<p style="color:var(--color-primary);font-size:0.85rem;">Dieser Vorgang kann nicht rückgängig gemacht werden.</p>
<div class="message" id="deleteMessage"></div>
<div class="modal-actions">
<button class="secondary" onclick="closeDeleteDialog()">Abbrechen</button>
<button id="deleteConfirmBtn" onclick="deleteAccount()" style="background:#c0392b;">Konto löschen</button>
</div>
</div>
</div>
<div class="save-toast" id="saveToast">✓ Gespeichert</div>
<script src="/js/icons.js"></script>
<script src="/js/sidebar.js"></script>
<script src="/js/social-sidebar.js"></script>
<script>
let myUserId = null;
let toastTimer = null;
function toggleSection(id) {
const target = document.getElementById(id);
const isOpen = target.classList.contains('open');
document.querySelectorAll('.settings-section').forEach(s => s.classList.remove('open'));
if (!isOpen) target.classList.add('open');
}
// ── Grunddaten laden ──
async function loadGrunddaten() {
const res = await fetch('/login/me');
if (res.status === 401) { window.location.href = '/login.html'; return; }
if (!res.ok) return;
const user = await res.json();
myUserId = user.userId;
document.getElementById('gd-name').textContent = user.name || '—';
document.getElementById('gd-email').textContent = user.email || '—';
document.getElementById('gd-geburtsdatum').textContent = formatGeburtsdatum(user.geburtsdatum);
}
function formatGeburtsdatum(iso) {
if (!iso) return '—';
const [y, m, d] = iso.split('-');
const today = new Date();
const birth = new Date(iso);
const age = today.getFullYear() - birth.getFullYear()
- (today < new Date(today.getFullYear(), birth.getMonth(), birth.getDate()) ? 1 : 0);
return `${d}.${m}.${y} (${age} Jahre)`;
}
// ── Geburtsdatum Dialog ──
function openGeburtsdatumDialog() {
document.getElementById('newGeburtsdatum').value = '';
hideModalMessage('geburtsdatumMessage');
document.getElementById('geburtsdatumModal').classList.add('visible');
document.getElementById('newGeburtsdatum').focus();
}
function closeGeburtsdatumDialog() {
document.getElementById('geburtsdatumModal').classList.remove('visible');
}
async function saveGeburtsdatum() {
const val = document.getElementById('newGeburtsdatum').value;
if (!val) { showModalMessage('geburtsdatumMessage', 'Bitte ein Datum eingeben.', 'error'); return; }
const today = new Date();
const birth = new Date(val);
const age = today.getFullYear() - birth.getFullYear()
- (today < new Date(today.getFullYear(), birth.getMonth(), birth.getDate()) ? 1 : 0);
if (age < 18) {
showModalMessage('geburtsdatumMessage', 'Du musst mindestens 18 Jahre alt sein.', 'error');
return;
}
const btn = document.getElementById('geburtsdatumConfirmBtn');
btn.disabled = true; btn.textContent = 'Wird gespeichert…';
hideModalMessage('geburtsdatumMessage');
try {
const res = await fetch('/user/me/geburtsdatum', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ geburtsdatum: val })
});
if (res.ok) {
document.getElementById('gd-geburtsdatum').textContent = formatGeburtsdatum(val);
closeGeburtsdatumDialog();
showToast();
} else if (res.status === 422) {
showModalMessage('geburtsdatumMessage', 'Du musst mindestens 18 Jahre alt sein.', 'error');
} else {
showModalMessage('geburtsdatumMessage', `Fehler: HTTP ${res.status}`, 'error');
}
} catch (err) {
showModalMessage('geburtsdatumMessage', 'Server nicht erreichbar.', 'error');
console.error(err);
} finally {
btn.disabled = false; btn.textContent = 'Speichern';
}
}
// ── Nickname Dialog ──
function openNameDialog() {
document.getElementById('newName').value = '';
hideModalMessage('nameMessage');
document.getElementById('nameModal').classList.add('visible');
document.getElementById('newName').focus();
}
function closeNameDialog() {
document.getElementById('nameModal').classList.remove('visible');
}
async function saveName() {
const newName = document.getElementById('newName').value.trim();
if (!newName) { showModalMessage('nameMessage', 'Bitte einen Namen eingeben.', 'error'); return; }
const btn = document.getElementById('nameConfirmBtn');
btn.disabled = true; btn.textContent = 'Wird gespeichert…';
hideModalMessage('nameMessage');
try {
const res = await fetch('/user/me/name', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: newName })
});
if (res.ok) {
document.getElementById('gd-name').textContent = newName;
closeNameDialog();
showToast();
} else if (res.status === 409) {
showModalMessage('nameMessage', 'Dieser Nickname ist bereits vergeben.', 'error');
} else {
showModalMessage('nameMessage', `Fehler: HTTP ${res.status}`, 'error');
}
} catch (err) {
showModalMessage('nameMessage', 'Server nicht erreichbar.', 'error');
console.error(err);
} finally {
btn.disabled = false; btn.textContent = 'Speichern';
}
}
// ── E-Mail Dialog ──
function openEmailDialog() {
document.getElementById('newEmail').value = '';
hideModalMessage('emailMessage');
document.getElementById('emailModal').classList.add('visible');
document.getElementById('newEmail').focus();
}
function closeEmailDialog() {
document.getElementById('emailModal').classList.remove('visible');
}
async function requestEmailChange() {
const newEmail = document.getElementById('newEmail').value.trim();
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(newEmail)) {
showModalMessage('emailMessage', 'Bitte eine gültige E-Mail-Adresse eingeben.', 'error');
return;
}
const btn = document.getElementById('emailConfirmBtn');
btn.disabled = true; btn.textContent = 'Wird gesendet…';
hideModalMessage('emailMessage');
try {
const res = await fetch('/email-change', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ newEmail })
});
if (res.status === 202) {
showModalMessage('emailMessage',
'Bestätigungsmail wurde an die neue Adresse gesendet. Bitte bestätige die Änderung über den Link in der E-Mail.',
'success');
btn.disabled = true; btn.textContent = 'Gesendet';
} else if (res.status === 409) {
showModalMessage('emailMessage', 'Diese E-Mail-Adresse ist bereits vergeben.', 'error');
btn.disabled = false; btn.textContent = 'Bestätigungsmail senden';
} else {
showModalMessage('emailMessage', `Fehler: HTTP ${res.status}`, 'error');
btn.disabled = false; btn.textContent = 'Bestätigungsmail senden';
}
} catch (err) {
showModalMessage('emailMessage', 'Server nicht erreichbar.', 'error');
btn.disabled = false; btn.textContent = 'Bestätigungsmail senden';
console.error(err);
}
}
// ── Konto löschen Dialog ──
function openDeleteDialog() {
hideModalMessage('deleteMessage');
document.getElementById('deleteConfirmBtn').disabled = false;
document.getElementById('deleteConfirmBtn').textContent = 'Konto löschen';
document.getElementById('deleteModal').classList.add('visible');
}
function closeDeleteDialog() {
document.getElementById('deleteModal').classList.remove('visible');
}
async function deleteAccount() {
const btn = document.getElementById('deleteConfirmBtn');
btn.disabled = true; btn.textContent = 'Wird gelöscht…';
hideModalMessage('deleteMessage');
try {
const res = await fetch('/user/me', { method: 'DELETE' });
if (res.ok) {
window.location.href = '/login.html?accountDeleted=1';
} else {
showModalMessage('deleteMessage', `Fehler: HTTP ${res.status}`, 'error');
btn.disabled = false; btn.textContent = 'Konto löschen';
}
} catch (err) {
showModalMessage('deleteMessage', 'Server nicht erreichbar.', 'error');
btn.disabled = false; btn.textContent = 'Konto löschen';
console.error(err);
}
}
function showModalMessage(id, text, type) {
const el = document.getElementById(id);
el.textContent = text;
el.className = `message ${type}`;
el.style.display = 'block';
}
function hideModalMessage(id) {
document.getElementById(id).style.display = 'none';
}
// Modal-Backdrop-Klick schließt Modals
['nameModal','geburtsdatumModal','emailModal','deleteModal'].forEach(id => {
document.getElementById(id).addEventListener('click', e => {
if (e.target === document.getElementById(id)) document.getElementById(id).classList.remove('visible');
});
});
document.getElementById('newName').addEventListener('keydown', e => { if (e.key === 'Enter') saveName(); });
document.getElementById('newEmail').addEventListener('keydown', e => { if (e.key === 'Enter') requestEmailChange(); });
// ── Datenschutz laden ──
async function loadPrivacy() {
const meRes = await fetch('/login/me');
if (!meRes.ok) return;
const me = await meRes.json();
myUserId = me.userId;
const profRes = await fetch('/social/users/' + myUserId);
if (!profRes.ok) return;
const profile = await profRes.json();
setValue('sv-grunddaten', profile.sichtbarkeitGrunddaten || 'ALLE');
setValue('sv-galerie', profile.sichtbarkeitGalerie || 'ALLE');
setValue('sv-freunde', profile.sichtbarkeitFreunde || 'ALLE');
setValue('sv-feed', profile.sichtbarkeitFeed || 'ALLE');
setValue('sv-pinnwand', profile.sichtbarkeitPinnwand || 'ALLE');
setValue('sv-xp', profile.sichtbarkeitXp || 'ALLE');
setValue('sv-lockhistorie', profile.sichtbarkeitLockhistorie || 'ALLE');
setValue('sv-vorlieben', profile.sichtbarkeitVorlieben || 'ALLE');
setValue('sv-veroeffentlichungen', profile.profilBeiVeroeffentlichungenSichtbar ? 'true' : 'false');
}
function setValue(id, value) {
const el = document.getElementById(id);
if (el) el.value = value;
}
async function doSave() {
const body = {
sichtbarkeitGrunddaten: document.getElementById('sv-grunddaten').value,
sichtbarkeitGalerie: document.getElementById('sv-galerie').value,
sichtbarkeitFreunde: document.getElementById('sv-freunde').value,
sichtbarkeitFeed: document.getElementById('sv-feed').value,
sichtbarkeitPinnwand: document.getElementById('sv-pinnwand').value,
sichtbarkeitXp: document.getElementById('sv-xp').value,
sichtbarkeitLockhistorie: document.getElementById('sv-lockhistorie').value,
sichtbarkeitVorlieben: document.getElementById('sv-vorlieben').value,
profilBeiVeroeffentlichungenSichtbar: document.getElementById('sv-veroeffentlichungen').value === 'true',
};
const res = await fetch('/user/me/privacy', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (res.ok) showToast();
}
function showToast() {
const t = document.getElementById('saveToast');
t.classList.add('visible');
clearTimeout(toastTimer);
toastTimer = setTimeout(() => t.classList.remove('visible'), 2200);
}
function openPreview(mode) {
if (!myUserId) return;
window.open('/community/benutzer.html?userId=' + myUserId + '&preview=' + mode, '_blank');
}
// ── Benachrichtigungen laden ──
async function loadNotifications() {
const res = await fetch('/user/me/notifications');
if (!res.ok) return;
const data = await res.json();
for (const cause of ['INVITATION', 'GAME_STATE', 'EMERGENCY']) {
const pref = data[cause] || { inApp: true, email: false };
const inApp = document.getElementById('notif-' + cause + '-inApp');
const email = document.getElementById('notif-' + cause + '-email');
if (inApp) inApp.checked = pref.inApp;
if (email) email.checked = pref.email;
}
}
async function saveNotifications() {
const body = {};
for (const cause of ['INVITATION', 'GAME_STATE', 'EMERGENCY']) {
const inApp = document.getElementById('notif-' + cause + '-inApp');
const email = document.getElementById('notif-' + cause + '-email');
body[cause] = {
inApp: inApp ? inApp.checked : true,
email: email ? email.checked : false
};
}
const res = await fetch('/user/me/notifications', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (res.ok) showToast();
}
// Hash-Navigation: Sektion anhand URL-Fragment öffnen
function openSectionFromHash() {
const hash = window.location.hash.replace('#', '');
if (hash) {
const el = document.getElementById(hash);
if (el) el.classList.add('open');
}
}
// ── Spiel Einstellungen BDSM ──
function applyCheckItems(groupId, values) {
document.querySelectorAll(`#${groupId} .spiel-check-item input`).forEach(cb => {
const checked = values.includes(cb.value);
cb.checked = checked;
cb.closest('.spiel-check-item').classList.toggle('is-checked', checked);
});
}
document.querySelectorAll('.spiel-check-item input').forEach(cb => {
cb.addEventListener('change', () => {
cb.closest('.spiel-check-item').classList.toggle('is-checked', cb.checked);
saveBdsmDefaults();
});
});
async function loadBdsmDefaults() {
const res = await fetch('/user/me/bdsm-defaults');
if (!res.ok) return;
const d = await res.json();
applyCheckItems('bdsm-spieltmit', d.spieltMit || []);
applyCheckItems('bdsm-rollen', d.rollen || []);
applyCheckItems('bdsm-werkzeuge', d.werkzeuge || []);
}
async function saveBdsmDefaults() {
const get = groupId => [...document.querySelectorAll(`#${groupId} input:checked`)].map(cb => cb.value);
await fetch('/user/me/bdsm-defaults', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
spieltMit: get('bdsm-spieltmit'),
rollen: get('bdsm-rollen'),
werkzeuge: get('bdsm-werkzeuge'),
})
});
showToast();
}
// ── Abonnement laden ──
async function loadSubscription() {
const res = await fetch('/subscription/me');
if (!res.ok) return;
const data = await res.json();
const type = data.subscriptionType || 'STANDARD';
const labels = { STANDARD: 'Standard', PREMIUM: 'Premium' };
document.getElementById('abo-type-badge').textContent = labels[type] || type;
if (type === 'STANDARD') {
document.getElementById('abo-type-desc').textContent =
'Kein kostenpflichtiges Abo aktiv · bis zu 6 Lock-Vorlagen, 6 Aufgabengruppen (je 50 Aufgaben), 10 Toys';
} else {
document.getElementById('abo-type-desc').textContent = 'Aktives Abo';
document.getElementById('abo-details').style.display = '';
const fmt = d => d ? new Date(d).toLocaleDateString('de-DE') : '';
document.getElementById('abo-valid-until').textContent = fmt(data.validUntil);
document.getElementById('abo-cancellable-from').textContent = fmt(data.cancellableFrom);
}
}
loadGrunddaten();
loadPrivacy();
loadNotifications();
loadBdsmDefaults();
loadSubscription();
loadTtlockUserConfig();
loadDating();
openSectionFromHash();
// ── Dating ──────────────────────────────────────────────────────────────
async function loadDating() {
const res = await fetch('/login/me');
if (!res.ok) return;
const user = await res.json();
document.getElementById('datingAktiv').checked = !!user.datingAktiv;
if (user.datingStadt) {
const input = document.getElementById('datingStadt');
input.value = user.datingStadt;
input.readOnly = true;
document.getElementById('datingStadtClear').style.display = '';
}
if (user.datingLat != null) _datingLat = user.datingLat;
if (user.datingLon != null) _datingLon = user.datingLon;
document.getElementById('datingStadtRow').style.display = user.datingAktiv ? '' : 'none';
}
function onDatingToggle() {
const aktiv = document.getElementById('datingAktiv').checked;
document.getElementById('datingStadtRow').style.display = aktiv ? '' : 'none';
}
let _stadtSuggestTimer = null;
let _datingLat = null;
let _datingLon = null;
function onStadtInput() {
const q = document.getElementById('datingStadt').value.trim();
_datingLat = null;
_datingLon = null;
document.getElementById('datingStadtClear').style.display = 'none';
clearTimeout(_stadtSuggestTimer);
if (q.length < 2) { hideSuggestions(); return; }
_stadtSuggestTimer = setTimeout(() => fetchStadtSuggestions(q), 300);
}
async function fetchStadtSuggestions(q) {
try {
const url = `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(q)}&format=json&addressdetails=1&limit=5&featuretype=city`;
const res = await fetch(url, { headers: { 'Accept-Language': 'de' } });
if (!res.ok) return;
const results = await res.json();
const ul = document.getElementById('stadtSuggestions');
if (!results.length) { hideSuggestions(); return; }
ul.innerHTML = results.map(r => {
const city = r.address.city || r.address.town || r.address.village || r.address.county || r.name;
const country = r.address.country || '';
const label = city + (country ? ', ' + country : '');
return `<li style="padding:0.5rem 0.75rem;cursor:pointer;font-size:0.9rem;border-bottom:1px solid var(--color-secondary);"
onmousedown="selectStadt(event, '${label.replace(/'/g, "\\'")}', ${parseFloat(r.lat)}, ${parseFloat(r.lon)})">${label}</li>`;
}).join('');
ul.style.display = '';
} catch (_) { hideSuggestions(); }
}
function selectStadt(e, label, lat, lon) {
e.preventDefault();
const input = document.getElementById('datingStadt');
input.value = label;
input.readOnly = true;
_datingLat = lat;
_datingLon = lon;
document.getElementById('datingStadtClear').style.display = '';
hideSuggestions();
}
function clearStadt() {
const input = document.getElementById('datingStadt');
input.value = '';
input.readOnly = false;
_datingLat = null;
_datingLon = null;
document.getElementById('datingStadtClear').style.display = 'none';
input.focus();
}
function hideSuggestions() {
document.getElementById('stadtSuggestions').style.display = 'none';
}
document.addEventListener('click', e => {
if (!e.target.closest('#datingStadtRow')) hideSuggestions();
});
async function detectLocation() {
const msgEl = document.getElementById('datingLocMsg');
if (!navigator.geolocation) { msgEl.textContent = 'Geolocation nicht unterstützt.'; return; }
msgEl.textContent = 'Standort wird ermittelt…';
navigator.geolocation.getCurrentPosition(async pos => {
try {
const { latitude, longitude } = pos.coords;
const res = await fetch(
`https://nominatim.openstreetmap.org/reverse?lat=${latitude}&lon=${longitude}&format=json&addressdetails=1`,
{ headers: { 'Accept-Language': 'de' } }
);
if (!res.ok) throw new Error();
const data = await res.json();
const city = data.address.city || data.address.town || data.address.village || data.address.county || '';
const country = data.address.country || '';
const input = document.getElementById('datingStadt');
input.value = city + (country ? ', ' + country : '');
input.readOnly = true;
_datingLat = latitude;
_datingLon = longitude;
document.getElementById('datingStadtClear').style.display = '';
msgEl.textContent = '';
} catch (_) { msgEl.textContent = 'Standort konnte nicht ermittelt werden.'; }
}, () => { msgEl.textContent = 'Zugriff auf Standort verweigert.'; });
}
async function saveDating() {
const aktiv = document.getElementById('datingAktiv').checked;
const stadt = document.getElementById('datingStadt').value.trim();
const msgEl = document.getElementById('datingMsg');
if (aktiv && (!stadt || _datingLat == null || _datingLon == null)) {
msgEl.textContent = 'Bitte eine Stadt aus der Liste auswählen oder per GPS ermitteln.';
msgEl.style.color = 'var(--color-primary)';
return;
}
const btn = document.getElementById('saveDatingBtn');
btn.disabled = true;
try {
const res = await fetch('/user/me/dating', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ datingAktiv: aktiv, datingStadt: stadt || null, datingLat: _datingLat, datingLon: _datingLon })
});
if (res.ok) {
showToast();
msgEl.textContent = '';
} else {
msgEl.textContent = 'Fehler beim Speichern.';
msgEl.style.color = 'var(--color-primary)';
}
} catch (_) {
msgEl.textContent = 'Server nicht erreichbar.';
msgEl.style.color = 'var(--color-primary)';
} finally {
btn.disabled = false;
}
}
// ── TTLock ──────────────────────────────────────────────────────────────
let _ttlPasswordSet = false;
const TTL_PW_DOTS = '••••••';
async function loadTtlockUserConfig() {
const r = await fetch('/user/me/ttlock');
if (!r.ok) return;
const cfg = await r.json();
document.getElementById('ttl-username').value = cfg.username || '';
document.getElementById('ttl-lockid').value = cfg.lockId != null ? cfg.lockId : '';
_ttlPasswordSet = !!cfg.passwordSet;
const pwField = document.getElementById('ttl-password');
if (_ttlPasswordSet) {
pwField.value = TTL_PW_DOTS;
document.getElementById('ttl-pw-hint').textContent = '';
} else {
pwField.value = '';
document.getElementById('ttl-pw-hint').textContent = '(noch nicht gesetzt)';
}
renderTtlockTestStatus(cfg.testSuccessful);
}
function renderTtlockTestStatus(ok) {
const el = document.getElementById('ttl-test-status');
if (ok) {
el.textContent = '✅ Verbindung erfolgreich getestet';
el.style.color = '#27ae60';
} else {
el.textContent = '⚠️ Noch nicht getestet';
el.style.color = 'var(--color-muted)';
}
}
function ttlPwFocus() {
const pwField = document.getElementById('ttl-password');
if (pwField.value === TTL_PW_DOTS) {
pwField.value = '';
document.getElementById('ttl-pw-hint').textContent = '(leer lassen = unverändert)';
}
}
function ttlPwBlur() {
const pwField = document.getElementById('ttl-password');
if (pwField.value === '' && _ttlPasswordSet) {
pwField.value = TTL_PW_DOTS;
document.getElementById('ttl-pw-hint').textContent = '';
}
}
function showTtlockLoading() {
document.getElementById('ttlLoadingModal').classList.add('visible');
}
function hideTtlockLoading() {
document.getElementById('ttlLoadingModal').classList.remove('visible');
}
async function testTtlockConnection() {
const btn = document.getElementById('ttl-test-btn');
const result = document.getElementById('ttl-test-result');
btn.disabled = true;
result.style.display = 'none';
showTtlockLoading();
try {
const r = await fetch('/user/me/ttlock/test');
const data = await r.json();
if (r.ok) {
renderTtlockTestStatus(true);
const battery = data.electricQuantity != null ? `${data.electricQuantity}%` : '';
const state = data.state || '';
result.style.background = 'rgba(39,174,96,0.08)';
result.style.borderColor = '#27ae60';
result.innerHTML = `
<div style="font-weight:700;color:#27ae60;margin-bottom:0.5rem;">✅ Verbindung erfolgreich</div>
<table style="border-collapse:collapse;width:100%;">
<tr><td style="color:var(--color-muted);padding:0.2rem 0.6rem 0.2rem 0;width:40%;">Name</td><td>${escHtml(data.lockName || '')}</td></tr>
<tr><td style="color:var(--color-muted);padding:0.2rem 0.6rem 0.2rem 0;">Alias</td><td>${escHtml(data.lockAlias || '')}</td></tr>
<tr><td style="color:var(--color-muted);padding:0.2rem 0.6rem 0.2rem 0;">Modell</td><td>${escHtml(data.modelNum || '')}</td></tr>
<tr><td style="color:var(--color-muted);padding:0.2rem 0.6rem 0.2rem 0;">Akku</td><td>${escHtml(battery)}</td></tr>
<tr><td style="color:var(--color-muted);padding:0.2rem 0.6rem 0.2rem 0;">Status</td><td>${escHtml(state)}</td></tr>
</table>`;
} else {
const msgs = {
ttlock_not_configured: 'TTLock-Zugangsdaten sind noch nicht gespeichert.',
admin_config_missing: 'Systemkonfiguration für TTLock fehlt (Admin).',
auth_failed: 'Anmeldung bei TTLock fehlgeschlagen Zugangsdaten prüfen.',
lock_detail_failed: `Lock-Abfrage fehlgeschlagen: ${data.message || ''}`,
};
result.style.background = 'rgba(231,76,60,0.08)';
result.style.borderColor = '#e74c3c';
result.innerHTML = `<div style="color:#e74c3c;">❌ ${escHtml(msgs[data.error] || 'Unbekannter Fehler.')}</div>`;
}
} catch {
result.style.background = 'rgba(231,76,60,0.08)';
result.style.borderColor = '#e74c3c';
result.innerHTML = `<div style="color:#e74c3c;">❌ Netzwerkfehler.</div>`;
} finally {
hideTtlockLoading();
}
result.style.display = '';
btn.disabled = false;
btn.textContent = '🔌 Verbindung testen';
}
function escHtml(s) {
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
async function ttlockOpenOnce() {
const btn = document.getElementById('ttl-open-btn');
const errEl = document.getElementById('ttl-open-error');
btn.disabled = true;
errEl.textContent = '';
showTtlockLoading();
try {
const r = await fetch('/user/me/ttlock/open', { method: 'POST' });
const data = await r.json();
if (!r.ok) {
const msgs = {
ttlock_not_configured: 'TTLock-Zugangsdaten sind noch nicht gespeichert.',
admin_config_missing: 'Systemkonfiguration fehlt (Admin).',
auth_failed: 'Anmeldung bei TTLock fehlgeschlagen.',
passcode_failed: 'PIN konnte nicht am Schloss angelegt werden.',
active_lock_exists: 'Du befindest dich in einem aktiven Lock manuelles Öffnen ist nicht möglich.',
};
errEl.textContent = msgs[data.error] || 'Unbekannter Fehler.';
return;
}
const pwdId = data.keyboardPwdId;
document.getElementById('ttl-open-pin').textContent = data.pin;
hideTtlockLoading();
const modal = document.getElementById('ttlOpenModal');
modal.classList.add('visible');
const okBtn = document.getElementById('ttl-open-ok-btn');
const onOk = async () => {
okBtn.removeEventListener('click', onOk);
okBtn.disabled = true;
modal.classList.remove('visible');
showTtlockLoading();
await fetch(`/user/me/ttlock/open/${pwdId}`, { method: 'DELETE' });
hideTtlockLoading();
okBtn.disabled = false;
};
okBtn.addEventListener('click', onOk);
} catch {
hideTtlockLoading();
errEl.textContent = 'Netzwerkfehler.';
} finally {
btn.disabled = false;
btn.textContent = '🔓 Öffnen';
}
}
async function saveTtlockUserConfig() {
const errEl = document.getElementById('ttl-error');
errEl.textContent = '';
const lockIdVal = document.getElementById('ttl-lockid').value.trim();
const pwVal = document.getElementById('ttl-password').value;
const body = {
username: document.getElementById('ttl-username').value.trim(),
password: pwVal === TTL_PW_DOTS ? '' : pwVal,
lockId: lockIdVal !== '' ? parseInt(lockIdVal) : null
};
const r = await fetch('/user/me/ttlock', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (r.ok) {
showToast();
await loadTtlockUserConfig();
} else {
errEl.textContent = 'Fehler beim Speichern.';
}
}
</script>
</body>
</html>