Files
xxx-sphere-web/bin/main/static/games/vanilla/aufgaben.html
Mario ec1409820b
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
Weiter an der Oberfläche getüftelt
2026-04-08 22:50:10 +02:00

1773 lines
91 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>Aufgaben Vanilla xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
/* ── Section ── */
.section + .section { margin-top: 2.5rem; }
.section-header {
display: flex; align-items: center; justify-content: space-between;
margin-bottom: 0.6rem; padding-bottom: 0.5rem;
border-bottom: 1px solid var(--color-secondary);
}
.section-title { font-size: 1.1rem; font-weight: 600; color: var(--color-primary); margin: 0; }
.section-actions { display: flex; align-items: center; gap: 0.5rem; }
/* ── Buttons ── */
.btn-add {
display: flex; align-items: center; gap: 0.4rem;
background: var(--color-primary); color: #fff;
border: none; border-radius: 6px; padding: 0.4rem 0.85rem;
font-size: 0.85rem; font-weight: 600; cursor: pointer; transition: background 0.15s;
}
.btn-add:hover { background: #c73652; }
.btn-action {
background: var(--color-secondary); color: var(--color-text);
border: none; border-radius: 6px; padding: 0.4rem 0.85rem;
font-size: 0.85rem; font-weight: 600; cursor: pointer;
transition: background 0.15s, color 0.15s, opacity 0.15s;
}
.btn-action:disabled { opacity: 0.35; cursor: default; }
.btn-action:not(:disabled):hover { background: var(--color-primary); color: #fff; }
.btn-action-danger:not(:disabled):hover { background: rgba(233,69,96,0.18); color: var(--color-primary); }
.btn-sub-add {
background: none; border: 1px solid var(--color-secondary); border-radius: 5px;
color: var(--color-muted); font-size: 0.75rem; padding: 0.15rem 0.5rem;
cursor: pointer; transition: border-color 0.15s, color 0.15s;
}
.btn-sub-add:hover { border-color: var(--color-primary); color: var(--color-primary); }
.action-error { font-size: 0.82rem; color: var(--color-primary); min-height: 1.1em; margin-bottom: 0.4rem; }
/* ── Paging ── */
.paging {
display: flex; align-items: center; justify-content: center;
gap: 0.75rem; margin-top: 1rem;
}
.paging button {
background: var(--color-secondary); color: var(--color-text);
border: none; border-radius: 6px; padding: 0.4rem 0.9rem;
font-size: 0.85rem; cursor: pointer; transition: background 0.15s;
}
.paging button:hover:not(:disabled) { background: var(--color-primary); }
.paging button:disabled { opacity: 0.35; cursor: default; }
.paging .page-info { font-size: 0.85rem; color: var(--color-muted); }
.empty, .loading { color: var(--color-muted); font-size: 0.9rem; padding: 0.75rem 0; }
/* ── Gruppe card ── */
.gruppe-list { display: flex; flex-direction: column; gap: 0.75rem; }
.gruppe-card {
background: var(--color-card); border: 1px solid var(--color-secondary);
border-radius: 10px; overflow: hidden; transition: border-color 0.15s;
}
.gruppe-card.selected { border-color: var(--color-primary); background: rgba(233,69,96,0.05); }
.gruppe-header {
display: flex; align-items: center; gap: 0.9rem;
padding: 0.85rem 1rem; cursor: pointer; user-select: none;
}
.gruppe-img {
width: 48px; height: 48px; border-radius: 7px;
object-fit: cover; flex-shrink: 0;
}
.gruppe-img-placeholder {
width: 48px; height: 48px; border-radius: 7px;
background: var(--color-secondary);
display: flex; align-items: center; justify-content: center;
font-size: 1.3rem; flex-shrink: 0; color: var(--color-muted);
}
.gruppe-meta { flex: 1; min-width: 0; }
.gruppe-name {
font-size: 0.95rem; font-weight: 600; color: var(--color-text);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.gruppe-info { font-size: 0.75rem; color: var(--color-muted); margin-top: 0.2rem; }
.gruppe-badges { display: flex; gap: 0.3rem; margin-top: 0.25rem; flex-wrap: wrap; }
.gruppe-badge {
font-size: 0.65rem; padding: 0.1rem 0.4rem; border-radius: 20px;
background: rgba(255,255,255,0.07); color: var(--color-muted);
}
.gruppe-badge-private { background: rgba(233,69,96,0.15); color: var(--color-primary); }
.gruppe-badge-public { background: rgba(46,204,113,0.15); color: var(--color-success); }
.gruppe-toggle { font-size: 0.75rem; color: var(--color-muted); flex-shrink: 0; transition: transform 0.2s; }
.gruppe-card.open .gruppe-toggle { transform: rotate(90deg); }
/* ── Gruppe body ── */
.gruppe-body { border-top: 1px solid var(--color-secondary); padding: 1rem 1rem 0.75rem; }
.gruppe-desc { font-size: 0.82rem; color: var(--color-muted); margin-bottom: 0.85rem; line-height: 1.5; }
.sub-section + .sub-section { margin-top: 0.85rem; }
.sub-section-header {
display: flex; align-items: center; justify-content: space-between;
margin-bottom: 0.4rem;
}
.sub-section-title {
font-size: 0.72rem; font-weight: 700; letter-spacing: 0.06em;
text-transform: uppercase; color: var(--color-primary);
}
/* ── Items ── */
.item-list { display: flex; flex-direction: column; gap: 0.3rem; }
.item {
border-radius: 6px; background: var(--color-secondary);
overflow: hidden;
}
.item-row {
display: flex; align-items: center; justify-content: space-between;
gap: 0.75rem; padding: 0.35rem 0.6rem;
cursor: pointer; user-select: none;
transition: background 0.12s;
}
.item-row:hover { background: rgba(255,255,255,0.04); }
.item.open .item-row { background: rgba(233,69,96,0.08); }
.item-text {
color: var(--color-text); flex: 1; min-width: 0; font-size: 0.82rem;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.item-badges { display: flex; gap: 0.35rem; flex-shrink: 0; }
.badge {
font-size: 0.7rem; padding: 0.1rem 0.45rem; border-radius: 20px;
background: rgba(233,69,96,0.15); color: var(--color-primary); white-space: nowrap;
}
.badge-neutral { background: rgba(255,255,255,0.07); color: var(--color-muted); }
/* ── Item detail ── */
.item-detail {
display: none; padding: 0.5rem 0.6rem 0.6rem; border-top: 1px solid rgba(255,255,255,0.06);
font-size: 0.8rem; color: var(--color-muted); line-height: 1.55;
}
.item.open .item-detail { display: block; }
.item-detail-text { margin-bottom: 0.4rem; color: var(--color-text); white-space: pre-wrap; }
.item-detail-row { display: flex; gap: 0.4rem; flex-wrap: wrap; align-items: center; margin-top: 0.25rem; }
.item-detail-label { font-size: 0.72rem; color: var(--color-muted); }
.item-detail-chip {
font-size: 0.7rem; padding: 0.1rem 0.5rem; border-radius: 20px;
background: rgba(255,255,255,0.07); color: var(--color-text);
}
.item-detail-chip-toy {
background: rgba(233,69,96,0.12); color: var(--color-primary);
}
.sub-empty { font-size: 0.78rem; color: var(--color-muted); padding: 0.2rem 0; }
/* ── Gruppe-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.open { display: flex; }
.modal {
background: var(--color-card); border: 1px solid var(--color-secondary);
border-radius: 12px; padding: 2rem; width: 100%; max-width: 460px;
box-shadow: 0 12px 40px rgba(0,0,0,0.6); max-height: 90vh; overflow-y: auto;
}
.modal h2 { color: var(--color-primary); font-size: 1.1rem; margin-bottom: 1.25rem; }
.modal label {
display: block; font-size: 0.8rem; color: #aaa;
margin-top: 1rem; margin-bottom: 0.3rem;
}
.modal input[type="text"],
.modal input[type="number"],
.modal textarea {
width: 100%; padding: 0.6rem 0.85rem;
border: 1px solid var(--color-secondary); border-radius: 6px;
background: var(--color-secondary); color: var(--color-text);
font-size: 0.95rem; outline: none; transition: border-color 0.2s; resize: vertical;
box-sizing: border-box;
}
.modal input[type="text"]:focus,
.modal input[type="number"]:focus,
.modal textarea:focus { border-color: var(--color-primary); }
.modal input[type="file"] { font-size: 0.85rem; color: var(--color-muted); margin-top: 0.25rem; }
.modal-check {
display: flex; align-items: center; gap: 0.6rem;
margin-top: 1rem; font-size: 0.9rem; cursor: pointer;
}
.modal-check input[type="checkbox"] { accent-color: var(--color-primary); width: 16px; height: 16px; }
.modal-actions {
display: flex; justify-content: flex-end; gap: 0.75rem; margin-top: 1.5rem;
}
.modal-actions .btn-cancel {
background: var(--color-secondary); color: var(--color-text);
border: none; border-radius: 6px; padding: 0.55rem 1.1rem;
font-size: 0.9rem; cursor: pointer; transition: background 0.15s;
}
.modal-actions .btn-cancel:hover { background: #1a4a8a; }
.modal-actions .btn-save {
background: var(--color-primary); color: #fff;
border: none; border-radius: 6px; padding: 0.55rem 1.1rem;
font-size: 0.9rem; font-weight: 600; cursor: pointer; transition: background 0.15s;
}
.modal-actions .btn-save:hover { background: #c73652; }
.modal-actions .btn-save:disabled { opacity: 0.5; cursor: default; }
.modal-error { color: var(--color-primary); font-size: 0.82rem; margin-top: 0.75rem; display: none; }
/* ── Placeholder-Hint ── */
.label-with-hint { display: flex; align-items: center; gap: 0.4rem; }
.btn-hint {
background: none; border: 1px solid rgba(136,136,136,0.4); border-radius: 50%;
color: var(--color-muted); font-size: 0.7rem; font-style: italic; font-weight: 700;
width: 16px; height: 16px; line-height: 1; padding: 0;
cursor: pointer; flex-shrink: 0; transition: border-color 0.15s, color 0.15s;
}
.btn-hint:hover { border-color: var(--color-primary); color: var(--color-primary); }
.placeholder-hint {
background: rgba(255,255,255,0.04); border: 1px solid rgba(136,136,136,0.25);
border-radius: 6px; padding: 0.5rem 0.7rem; margin-bottom: 0.3rem;
font-size: 0.78rem; color: var(--color-muted); line-height: 1.6;
}
.placeholder-hint code {
background: rgba(233,69,96,0.12); color: var(--color-primary);
border-radius: 3px; padding: 0.05rem 0.3rem; font-size: 0.75rem;
}
#iTextAC { position: fixed; z-index: 9999; background: var(--color-surface, #1e1e2e);
border: 1px solid var(--color-border, #444); border-radius: 6px;
box-shadow: 0 4px 14px rgba(0,0,0,.5); display: none; overflow: hidden; min-width: 180px; max-height: 280px; overflow-y: auto; }
.ac-item { padding: 0.45rem 0.9rem; cursor: pointer; font-size: 0.88rem;
font-family: monospace; color: var(--color-text, #cdd6f4); user-select: none; }
.ac-item:hover, .ac-item-active { background: var(--color-primary, #cba6f7); color: #1e1e2e; }
.ac-separator { padding: 0.2rem 0.7rem; font-size: 0.72rem; color: var(--color-muted);
text-transform: uppercase; letter-spacing: 0.05em; background: rgba(255,255,255,0.04);
pointer-events: none; border-top: 1px solid rgba(136,136,136,0.2); margin-top: 2px; }
/* ── Item-Add-Modal extra ── */
.modal-two-col { display: flex; gap: 0.75rem; }
.modal-two-col > * { flex: 1; }
.werkzeug-checks {
display: flex; flex-wrap: nowrap;
gap: 0.25rem; margin-top: 0.5rem;
}
.werkzeug-check {
flex: 1; display: flex; flex-direction: column;
align-items: center; gap: 0.3rem;
font-size: 0.73rem; cursor: pointer;
text-align: center; line-height: 1.2;
}
.werkzeug-check input[type="checkbox"] {
accent-color: var(--color-primary);
width: 15px; height: 15px; flex-shrink: 0;
}
.item-action-btns { display: flex; gap: 0.4rem; margin-top: 0.5rem; justify-content: space-between; }
.btn-item-edit {
background: none; border: 1px solid rgba(136,136,136,0.45); border-radius: 5px;
color: var(--color-muted); font-size: 0.75rem; padding: 0.2rem 0.6rem;
cursor: pointer; transition: border-color 0.15s, color 0.15s;
}
.btn-item-edit:hover { border-color: var(--color-text); color: var(--color-text); }
.btn-item-delete {
background: none; border: 1px solid rgba(233,69,96,0.4); border-radius: 5px;
color: var(--color-primary); font-size: 0.75rem; padding: 0.2rem 0.6rem;
cursor: pointer; transition: background 0.15s;
}
.btn-item-delete:hover { background: rgba(233,69,96,0.15); }
/* ── Toy-Auswahl (item modal) ── */
.selected-toys-row {
display: flex; flex-wrap: wrap; gap: 0.35rem; margin-top: 0.4rem; min-height: 1.2rem;
}
.sel-toy-chip {
display: inline-flex; align-items: center; gap: 0.3rem;
padding: 0.18rem 0.5rem 0.18rem 0.55rem; border-radius: 20px;
font-size: 0.78rem; border: 1px solid rgba(233,69,96,0.5);
background: rgba(233,69,96,0.1); color: var(--color-text);
}
.sel-toy-chip img { width: 16px; height: 16px; border-radius: 3px; object-fit: cover; flex-shrink: 0; }
.sel-toy-chip .toy-remove {
background: none; border: none; color: var(--color-muted); cursor: pointer;
padding: 0; font-size: 0.75rem; line-height: 1; margin-left: 0.1rem;
}
.sel-toy-chip .toy-remove:hover { color: var(--color-primary); }
.btn-toy-add {
background: none; border: 1px dashed rgba(136,136,136,0.5); border-radius: 20px;
color: var(--color-muted); font-size: 0.78rem; padding: 0.18rem 0.7rem;
cursor: pointer; margin-top: 0.4rem; transition: border-color 0.15s, color 0.15s;
}
.btn-toy-add:hover { border-color: var(--color-primary); color: var(--color-primary); }
/* ── Toy-Suche (inline) ── */
.toy-search-input {
width: 100%; box-sizing: border-box; padding: 0.45rem 0.7rem;
background: var(--color-secondary); border: 1px solid rgba(136,136,136,0.3);
border-radius: 6px; color: var(--color-text); font-size: 0.9rem; margin-bottom: 0.75rem;
}
.toy-search-input:focus { outline: none; border-color: var(--color-primary); }
.toy-search-results {
display: flex; flex-wrap: wrap; gap: 0.35rem; max-height: 240px;
overflow-y: auto; padding-right: 2px;
}
.toy-result-chip {
display: inline-flex; align-items: center; gap: 0.3rem;
padding: 0.2rem 0.6rem; border-radius: 20px; cursor: pointer;
font-size: 0.8rem; border: 1px solid var(--color-secondary);
background: var(--color-secondary); color: var(--color-muted);
transition: border-color 0.15s, background 0.15s, color 0.15s;
user-select: none;
}
.toy-result-chip:hover { border-color: var(--color-primary); color: var(--color-text); }
.toy-result-chip.selected {
border-color: var(--color-primary); background: rgba(233,69,96,0.15); color: var(--color-primary);
}
.toy-result-chip img { width: 18px; height: 18px; border-radius: 3px; object-fit: cover; flex-shrink: 0; }
/* ── Publish Modal ── */
.publish-warning {
background: rgba(233,69,96,0.08); border: 1px solid rgba(233,69,96,0.3);
border-radius: 8px; padding: 0.85rem 1rem; font-size: 0.85rem;
color: var(--color-text); line-height: 1.55; margin-bottom: 1rem;
}
.publish-warning strong { color: var(--color-primary); }
.publish-confirm-check {
display: flex; align-items: flex-start; gap: 0.65rem;
font-size: 0.85rem; line-height: 1.5; cursor: pointer; margin-top: 0.5rem;
}
.publish-confirm-check input[type="checkbox"] {
accent-color: var(--color-primary); width: 16px; height: 16px;
flex-shrink: 0; margin-top: 0.15rem;
}
</style>
</head>
<body class="app">
<!-- Gruppe-Modal -->
<div class="modal-backdrop" id="confirmModal">
<div class="modal" style="max-width:420px;">
<h2 id="confirmModalTitle">Bestätigung</h2>
<p id="confirmModalText" style="color:var(--color-text);margin-bottom:1.25rem;line-height:1.5;"></p>
<div class="modal-actions">
<button class="btn-cancel" id="confirmModalCancel">Abbrechen</button>
<button class="btn-save" id="confirmModalOk" style="background:var(--color-danger,#e74c3c);">Löschen</button>
</div>
</div>
</div>
<div class="modal-backdrop" id="gruppeModal">
<div class="modal">
<h2 id="modalTitle">Neue Aufgabengruppe</h2>
<label for="gName">Name *</label>
<input type="text" id="gName" maxlength="100" placeholder="Gruppenname">
<label for="gDesc">Beschreibung</label>
<textarea id="gDesc" rows="3" maxlength="1000" placeholder="Kurze Beschreibung…"></textarea>
<label>Bild (optional)</label>
<div id="gCurrentImgWrap" style="display:none; align-items:center; gap:0.5rem; margin-bottom:0.4rem;">
<img id="gCurrentImg" style="max-width:64px; max-height:64px; border-radius:6px;" src="" alt="">
<span style="font-size:0.78rem; color:var(--color-muted);">Aktuelles Bild neues wählen zum Ersetzen</span>
</div>
<input type="file" id="gBild" accept="image/*">
<label id="gPublicLabel" style="display:none;">
<span class="modal-check">
<input type="checkbox" id="gPublic">
Gruppe veröffentlichen (für alle sichtbar)
</span>
</label>
<div class="modal-error" id="modalError"></div>
<div class="modal-actions">
<button class="btn-cancel" id="cancelBtn">Abbrechen</button>
<button class="btn-save" id="saveBtn">Speichern</button>
</div>
</div>
</div>
<!-- Veröffentlichen-Modal -->
<div class="modal-backdrop" id="publishModal">
<div class="modal">
<h2 style="color:var(--color-primary);">Gruppe veröffentlichen</h2>
<div class="publish-warning">
<strong>Achtung:</strong> Wenn du diese Gruppe veröffentlichst, können <strong>alle anderen Nutzer</strong>
sie sehen und eine eigene Kopie anlegen. Dieser Vorgang kann nicht automatisch rückgängig gemacht werden
du kannst die Gruppe danach nur noch über „Bearbeiten" wieder auf privat stellen.
</div>
<label class="publish-confirm-check" id="publishCheckLabel">
<input type="checkbox" id="publishConfirmCb">
<span>Ich bestätige, dass ich ausschließlich eigene Bilder und kein urheberrechtlich geschütztes Bildmaterial in dieser Gruppe verwende.</span>
</label>
<div class="modal-error" id="publishError" style="display:none;"></div>
<div class="modal-actions">
<button class="btn-cancel" id="publishCancelBtn">Abbrechen</button>
<button class="btn-save" id="publishConfirmBtn" disabled>Veröffentlichen</button>
</div>
</div>
</div>
<!-- Item-Hinzufügen-Modal -->
<div class="modal-backdrop" id="itemModal">
<div class="modal">
<h2 id="itemModalTitle">Aufgabe hinzufügen</h2>
<label for="iKurzText">Kurzbezeichnung *</label>
<input type="text" id="iKurzText" maxlength="200" placeholder="Kurzer Name">
<label class="label-with-hint">
<span>Beschreibung *</span>
<button type="button" class="btn-hint" onclick="togglePlaceholderHint()" title="Platzhalter-Hilfe">i</button>
</label>
<div id="iPlaceholderHint" style="display:none;">
<div class="placeholder-hint">
In Texten können Platzhalter verwendet werden:<br>
<code>{AKTIV}</code> Name des aktiven Parts<br>
<code>{PASSIV}</code> Name des passiven Parts
</div>
</div>
<textarea id="iText" rows="4" maxlength="4000" placeholder="Ausführliche Beschreibung…"></textarea>
<div id="iTextAC"></div>
<!-- Finisher: Geschlecht -->
<div id="iGeschlechtRow">
<label>Geschlecht der Person die kommt *</label>
<div style="display:flex; gap:1.5rem; margin-top:0.5rem;" id="iGeschlecht">
<label style="display:flex; align-items:center; gap:0.4rem; font-size:0.85rem; cursor:pointer;">
<input type="radio" name="iGeschlechtRadio" value="WEIBLICH" style="accent-color:var(--color-primary);">
Weiblich
</label>
<label style="display:flex; align-items:center; gap:0.4rem; font-size:0.85rem; cursor:pointer;">
<input type="radio" name="iGeschlechtRadio" value="DIVERS" style="accent-color:var(--color-primary);">
Divers
</label>
<label style="display:flex; align-items:center; gap:0.4rem; font-size:0.85rem; cursor:pointer;">
<input type="radio" name="iGeschlechtRadio" value="MAENNLICH" style="accent-color:var(--color-primary);">
Männlich
</label>
</div>
</div>
<!-- Aufgabe / Strafe: Level + Sekunden -->
<div id="iLevelRow">
<label for="iLevel">Level *</label>
<input type="number" id="iLevel" min="1" max="5" placeholder="15">
<label>Dauer (Sekunden)</label>
<div class="modal-two-col">
<div>
<label for="iSekVon" style="margin-top:0;">Von</label>
<input type="number" id="iSekVon" min="0" placeholder="z. B. 30">
</div>
<div>
<label for="iSekBis" style="margin-top:0;">Bis</label>
<input type="number" id="iSekBis" min="0" placeholder="z. B. 120">
</div>
</div>
</div>
<!-- Aufgabe / Strafe: Werkzeuge aktiv/passiv -->
<div id="iWerkzeugAktivRow">
<label>Benötigt (aktiv)</label>
<div class="werkzeug-checks" id="iWerkzeugAktiv">
<label class="werkzeug-check"><span>Mund</span><input type="checkbox" value="MUND"></label>
<label class="werkzeug-check"><span>Vagina</span><input type="checkbox" value="VAGINA"></label>
<label class="werkzeug-check"><span>Penis</span><input type="checkbox" value="PENIS"></label>
<label class="werkzeug-check"><span>Anus</span><input type="checkbox" value="ANUS"></label>
<label class="werkzeug-check"><span>Umschnall-Dildo</span><input type="checkbox" value="UMSCHNALLDILDO"></label>
</div>
</div>
<div id="iWerkzeugPassivRow">
<label>Benötigt (passiv)</label>
<div class="werkzeug-checks" id="iWerkzeugPassiv">
<label class="werkzeug-check"><span>Mund</span><input type="checkbox" value="MUND"></label>
<label class="werkzeug-check"><span>Vagina</span><input type="checkbox" value="VAGINA"></label>
<label class="werkzeug-check"><span>Penis</span><input type="checkbox" value="PENIS"></label>
<label class="werkzeug-check"><span>Anus</span><input type="checkbox" value="ANUS"></label>
<label class="werkzeug-check"><span>Umschnall-Dildo</span><input type="checkbox" value="UMSCHNALLDILDO"></label>
</div>
</div>
<!-- Finisher: Werkzeuge (eigene Labels, kein Umschnalldildo bei aktiv) -->
<div id="iWerkzeugFinisherAktivRow">
<label>Benötigt (Person die kommt)</label>
<div class="werkzeug-checks" id="iWerkzeugFinisherAktiv">
<label class="werkzeug-check"><span>Mund</span><input type="checkbox" value="MUND"></label>
<label class="werkzeug-check"><span>Vagina</span><input type="checkbox" value="VAGINA"></label>
<label class="werkzeug-check"><span>Penis</span><input type="checkbox" value="PENIS"></label>
<label class="werkzeug-check"><span>Anus</span><input type="checkbox" value="ANUS"></label>
</div>
</div>
<div id="iWerkzeugFinisherPassivRow">
<label>Benötigt (Person die zum Kommen bringt)</label>
<div class="werkzeug-checks" id="iWerkzeugFinisherPassiv">
<label class="werkzeug-check"><span>Mund</span><input type="checkbox" value="MUND"></label>
<label class="werkzeug-check"><span>Vagina</span><input type="checkbox" value="VAGINA"></label>
<label class="werkzeug-check"><span>Penis</span><input type="checkbox" value="PENIS"></label>
<label class="werkzeug-check"><span>Anus</span><input type="checkbox" value="ANUS"></label>
<label class="werkzeug-check"><span>Umschnall-Dildo</span><input type="checkbox" value="UMSCHNALLDILDO"></label>
</div>
</div>
<!-- Zeitstrafe: Minuten + SperreFuer + releaseText -->
<div id="iMinutenRow">
<label>Dauer (Minuten) *</label>
<div class="modal-two-col">
<div>
<label for="iMinVon" style="margin-top:0;">Von *</label>
<input type="number" id="iMinVon" min="0" placeholder="z. B. 5">
</div>
<div>
<label for="iMinBis" style="margin-top:0;">Bis</label>
<input type="number" id="iMinBis" min="0" placeholder="z. B. 30">
</div>
</div>
</div>
<div id="iSperreFuerRow">
<label>Sperrt *</label>
<div class="werkzeug-checks" id="iSperreFuer">
<label class="werkzeug-check"><span>Mund</span><input type="checkbox" value="MUND"></label>
<label class="werkzeug-check"><span>Vagina</span><input type="checkbox" value="VAGINA"></label>
<label class="werkzeug-check"><span>Penis</span><input type="checkbox" value="PENIS"></label>
<label class="werkzeug-check"><span>Anus</span><input type="checkbox" value="ANUS"></label>
</div>
</div>
<div id="iReleaseTextRow">
<label for="iReleaseText">Text bei Aufhebung</label>
<textarea id="iReleaseText" rows="2" maxlength="2000" placeholder="Text der angezeigt wird, wenn die Sperre endet…"></textarea>
</div>
<!-- Toys -->
<label>Benötigte Toys (optional)</label>
<div class="selected-toys-row" id="iSelectedToys"></div>
<button class="btn-toy-add" type="button" id="iToyAddBtn">+ Toy hinzufügen</button>
<div id="iToySearchArea" style="display:none; margin-top:0.5rem;">
<input class="toy-search-input" type="text" id="toySearchInput" placeholder="Name filtern…" autocomplete="off">
<div class="toy-search-results" id="toySearchResults"></div>
<div id="toySearchEmpty" style="font-size:0.82rem; color:var(--color-muted); display:none; margin-top:0.4rem;">Keine Toys gefunden.</div>
</div>
<div class="modal-error" id="itemModalError"></div>
<div class="modal-actions">
<button class="btn-cancel" id="itemCancelBtn">Abbrechen</button>
<button class="btn-save" id="itemSaveBtn">Speichern</button>
</div>
</div>
</div>
<div class="main">
<div class="content">
<!-- Meine Aufgabengruppen -->
<div class="section">
<div class="section-header">
<h2 class="section-title">Meine Aufgabengruppen</h2>
<div class="section-actions">
<button class="btn-action" id="publishBtn" disabled>⊙ Veröffentlichen</button>
<button class="btn-action" id="editBtn" disabled>✎ Bearbeiten</button>
<button class="btn-action btn-action-danger" id="deleteBtn" disabled>✕ Löschen</button>
<button class="btn-add" id="openCreateBtn">+ Neu</button>
</div>
</div>
<div class="action-error" id="userActionError"></div>
<div id="userLoading" class="loading">Wird geladen…</div>
<div class="gruppe-list" id="userList"></div>
<div class="paging" id="userPaging" style="display:none;">
<button id="userPrev"> Zurück</button>
<span class="page-info" id="userPageInfo"></span>
<button id="userNext">Weiter </button>
</div>
</div>
<!-- Abonnierte Aufgabengruppen -->
<div class="section">
<div class="section-header">
<h2 class="section-title">Abonnierte Aufgabengruppen</h2>
<div class="section-actions">
<button class="btn-action" id="unsubBtn" disabled>♥ Abonnement kündigen</button>
<button class="btn-action" id="aboCopyBtn" disabled>⊕ In meine Gruppen kopieren</button>
</div>
</div>
<div class="action-error" id="aboActionError"></div>
<div id="aboLoading" class="loading">Wird geladen…</div>
<div class="gruppe-list" id="aboList"></div>
<div class="paging" id="aboPaging" style="display:none;">
<button id="aboPrev"> Zurück</button>
<span class="page-info" id="aboPageInfo"></span>
<button id="aboNext">Weiter </button>
</div>
</div>
<!-- System-Aufgabengruppen -->
<div class="section">
<div class="section-header">
<h2 class="section-title">System-Aufgabengruppen</h2>
<div class="section-actions">
<button class="btn-action" id="copyBtn" disabled>⊕ In meine Gruppen kopieren</button>
</div>
</div>
<div class="action-error" id="systemActionError"></div>
<div id="systemLoading" class="loading">Wird geladen…</div>
<div class="gruppe-list" id="systemList"></div>
<div class="paging" id="systemPaging" style="display:none;">
<button id="systemPrev"> Zurück</button>
<span class="page-info" id="systemPageInfo"></span>
<button id="systemNext">Weiter </button>
</div>
</div>
</div>
</div>
<script>
// ── API-URL-Hilfsfunktion ──
function apiUrl(path) {
return '/vanilla' + path;
}
const PAGE_SIZE = 5;
let userPage = 0, userTotalPages = 1;
let aboPage = 0, aboTotalPages = 1;
let systemPage = 0, systemTotalPages = 1;
// ── Pending expand after reload ──
let pendingExpandId = null;
let pendingExpandType = null;
// ── Auth ──
fetch('/login/me')
.then(r => { if (r.status === 401) { window.location.href = '/login.html'; return null; } return r.ok ? r.json() : null; })
.then(user => { if (!user) return; loadUserGruppen(); loadAboGruppen(); loadSystemGruppen(); })
.catch(() => { window.location.href = '/login.html'; });
// ── Load ──
function loadUserGruppen() {
resetSelection();
document.getElementById('userLoading').style.display = 'block';
fetch(apiUrl(`/gruppe/list/user`) + `?page=${userPage}&size=${PAGE_SIZE}`)
.then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); })
.then(data => {
console.log('[aufgaben] user gruppen:', data);
userTotalPages = data.totalPages || 1;
try { renderGruppen('userList', data.content, 'user'); } catch(e) { console.error('[aufgaben] renderGruppen user Fehler:', e); throw e; }
updatePaging('userPaging', 'userPrev', 'userNext', 'userPageInfo', userPage, userTotalPages);
document.getElementById('userLoading').style.display = 'none';
reapplyPendingExpand();
})
.catch(err => { console.error('[aufgaben] Fehler user gruppen:', err); document.getElementById('userLoading').textContent = 'Fehler beim Laden: ' + err.message; });
}
function loadSystemGruppen() {
resetSelection();
document.getElementById('systemLoading').style.display = 'block';
fetch(apiUrl(`/gruppe/list/system`) + `?page=${systemPage}&size=${PAGE_SIZE}`)
.then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); })
.then(data => {
console.log('[aufgaben] system gruppen:', data);
systemTotalPages = data.totalPages || 1;
try { renderGruppen('systemList', data.content, 'system'); } catch(e) { console.error('[aufgaben] renderGruppen system Fehler:', e); throw e; }
updatePaging('systemPaging', 'systemPrev', 'systemNext', 'systemPageInfo', systemPage, systemTotalPages);
document.getElementById('systemLoading').style.display = 'none';
reapplyPendingExpand();
})
.catch(err => { console.error('[aufgaben] Fehler system gruppen:', err); document.getElementById('systemLoading').textContent = 'Fehler beim Laden: ' + err.message; });
}
function loadAboGruppen() {
document.getElementById('aboLoading').style.display = 'block';
fetch(apiUrl(`/abo/list`) + `?page=${aboPage}&size=${PAGE_SIZE}`)
.then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); })
.then(data => {
console.log('[aufgaben] abo gruppen:', data);
aboTotalPages = data.totalPages || 1;
renderGruppen('aboList', data.content, 'abo');
updatePaging('aboPaging', 'aboPrev', 'aboNext', 'aboPageInfo', aboPage, aboTotalPages);
document.getElementById('aboLoading').style.display = 'none';
reapplyPendingExpand();
})
.catch(err => { console.error('[aufgaben] Fehler abo gruppen:', err); document.getElementById('aboLoading').textContent = 'Fehler beim Laden: ' + err.message; });
}
function reapplyPendingExpand() {
if (!pendingExpandId) return;
const card = document.getElementById('gruppe-' + pendingExpandId);
if (!card) return;
const id = pendingExpandId;
const type = pendingExpandType;
pendingExpandId = pendingExpandType = null;
selectedGruppeId = id;
selectedGruppeType = type;
expandGruppe(id);
updateButtons(type);
}
// ── Render Gruppen ──
function renderGruppen(listId, gruppen, type) {
const list = document.getElementById(listId);
if (!gruppen || gruppen.length === 0) {
list.innerHTML = '<p class="empty">Keine Einträge vorhanden.</p>';
return;
}
list.innerHTML = gruppen.map(g => {
_gruppeData[g.gruppenId] = g;
const aufgabenCount = (g.aufgaben || []).length;
const strafeCount = (g.strafen || []).length;
const sperreCount = (g.sperren || []).length;
const finisherCount = (g.finisher || []).length;
const counts = [
aufgabenCount ? `${aufgabenCount} Aufgabe${aufgabenCount !== 1 ? 'n' : ''}` : '',
finisherCount ? `${finisherCount} Finisher` : ''
].filter(Boolean).join(' · ');
const badges = [];
if (g.relevanz != null) badges.push(`<span class="gruppe-badge">Relevanz ${esc(String(g.relevanz))}</span>`);
if (g.privateGruppe) badges.push(`<span class="gruppe-badge gruppe-badge-private">Privat</span>`);
else badges.push(`<span class="gruppe-badge gruppe-badge-public">Öffentlich</span>`);
if (type === 'user' && g.subscriberCount > 0) badges.push(`<span class="gruppe-badge">♥ ${g.subscriberCount} Abo${g.subscriberCount !== 1 ? 's' : ''}</span>`);
return `
<div class="gruppe-card" id="gruppe-${esc(g.gruppenId)}">
<div class="gruppe-header" onclick="selectAndToggle('${esc(g.gruppenId)}', '${type}')">
${g.bild
? `<img class="gruppe-img" src="data:image/png;base64,${g.bild}" alt="${esc(g.name)}">`
: `<div class="gruppe-img-placeholder">✓</div>`}
<div class="gruppe-meta">
<div class="gruppe-name">${esc(g.name)}</div>
<div class="gruppe-info">${g.von ? esc(g.von) + (counts ? ' · ' : '') : ''}${counts || 'Keine Einträge'}</div>
${badges.length ? `<div class="gruppe-badges">${badges.join('')}</div>` : ''}
</div>
<span class="gruppe-toggle">▶</span>
</div>
<div class="gruppe-body" id="body-${esc(g.gruppenId)}" style="display:none;">
${g.beschreibung ? `<div class="gruppe-desc">${esc(g.beschreibung)}</div>` : ''}
${renderSubSection('Aufgaben', sortByLevelThenName(g.aufgaben || []), 'aufgabe', renderAufgabe, g.gruppenId, type)}
${renderSubSection('Finisher', sortByGeschlecht(g.finisher || []), 'finisher', renderFinisher, g.gruppenId, type)}
</div>
</div>`;
}).join('');
// Offenes Item tracking
openItemId = null;
}
// ── Sub-sections ──
function renderSubSection(title, items, kind, renderFn, gruppenId, type) {
const showAdd = type === 'user' && (kind !== 'strafe' && kind !== 'zeitstrafe');
const addBtn = showAdd
? `<button class="btn-sub-add" onclick="openItemModal('${esc(gruppenId)}','${kind}')">+ ${title.replace('en','').replace('fen','fe')}</button>`
: '';
return `<div class="sub-section">
<div class="sub-section-header">
<span class="sub-section-title">${esc(title)} (${items.length})</span>
${addBtn}
</div>
${items.length === 0
? '<div class="sub-empty">Keine Einträge</div>'
: `<div class="item-list">${items.map(item => renderFn(item, type, gruppenId)).join('')}</div>`}
</div>`;
}
// ── Gruppen- und Item-Datencache ──
const _gruppeData = {};
const _itemData = {};
// ── Item renderers ──
const WERKZEUG_LABEL = {
MUND: 'Mund', VAGINA: 'Vagina', PENIS: 'Penis',
ANUS: 'Anus', UMSCHNALLDILDO: 'Umschnall-Dildo'
};
function werkzeugChips(list) {
if (!list || list.length === 0) return '';
return list.map(w => `<span class="item-detail-chip">${esc(WERKZEUG_LABEL[w] || w)}</span>`).join('');
}
function toyChips(list) {
if (!list || list.length === 0) return '';
return list.map(t => `<span class="item-detail-chip item-detail-chip-toy">${esc(t.name || t)}</span>`).join('');
}
function renderAufgabe(a, type, gruppenId) {
_itemData[a.aufgabeId] = { ...a, _kind: 'aufgabe', _gruppenId: gruppenId };
const badges = [];
const zeit = formatSek(a.sekundenVon, a.sekundenBis);
if (zeit) badges.push(`<span class="badge badge-neutral">${esc(zeit)}</span>`);
if (a.level != null) badges.push(`<span class="badge">Level ${esc(String(a.level))}</span>`);
const detailRows = [];
if (a.text) detailRows.push(`<div class="item-detail-text">${esc(a.text)}</div>`);
if (a.benoetigtAktiv && a.benoetigtAktiv.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Aktiv:</span>${werkzeugChips(a.benoetigtAktiv)}</div>`);
if (a.benoetigtPassiv && a.benoetigtPassiv.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Passiv:</span>${werkzeugChips(a.benoetigtPassiv)}</div>`);
if (a.benoetigteToys && a.benoetigteToys.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Toys:</span>${toyChips(a.benoetigteToys)}</div>`);
const actionBtns = type === 'user' ? `
<div class="item-action-btns">
<button class="btn-item-edit" onclick="openEditItemModal('${esc(a.aufgabeId)}',event)">✎ Bearbeiten</button>
<button class="btn-item-delete" onclick="deleteItem('aufgabe','${esc(a.aufgabeId)}','${esc(gruppenId)}',event)">✕ Löschen</button>
</div>` : '';
return `<div class="item" id="item-${esc(a.aufgabeId)}">
<div class="item-row" onclick="toggleItem('${esc(a.aufgabeId)}')">
<span class="item-text">${esc(a.kurzText)}</span>
${badges.length ? `<span class="item-badges">${badges.join('')}</span>` : ''}
</div>
${(detailRows.length || actionBtns) ? `<div class="item-detail">${detailRows.join('')}${actionBtns}</div>` : ''}
</div>`;
}
function renderStrafe(s, type, gruppenId) {
_itemData[s.strafeId] = { ...s, _kind: 'strafe', _gruppenId: gruppenId };
const badges = [];
const zeit = formatSek(s.sekundenVon, s.sekundenBis);
if (zeit) badges.push(`<span class="badge badge-neutral">${esc(zeit)}</span>`);
if (s.level != null) badges.push(`<span class="badge">Level ${esc(String(s.level))}</span>`);
const detailRows = [];
if (s.text) detailRows.push(`<div class="item-detail-text">${esc(s.text)}</div>`);
if (s.benoetigtAktiv && s.benoetigtAktiv.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Aktiv:</span>${werkzeugChips(s.benoetigtAktiv)}</div>`);
if (s.benoetigtPassiv && s.benoetigtPassiv.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Passiv:</span>${werkzeugChips(s.benoetigtPassiv)}</div>`);
if (s.benoetigteToys && s.benoetigteToys.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Toys:</span>${toyChips(s.benoetigteToys)}</div>`);
const actionBtns = type === 'user' ? `
<div class="item-action-btns">
<button class="btn-item-edit" onclick="openEditItemModal('${esc(s.strafeId)}',event)">✎ Bearbeiten</button>
<button class="btn-item-delete" onclick="deleteItem('strafe','${esc(s.strafeId)}','${esc(gruppenId)}',event)">✕ Löschen</button>
</div>` : '';
return `<div class="item" id="item-${esc(s.strafeId)}">
<div class="item-row" onclick="toggleItem('${esc(s.strafeId)}')">
<span class="item-text">${esc(s.kurzText)}</span>
${badges.length ? `<span class="item-badges">${badges.join('')}</span>` : ''}
</div>
${(detailRows.length || actionBtns) ? `<div class="item-detail">${detailRows.join('')}${actionBtns}</div>` : ''}
</div>`;
}
function renderZeitstrafe(z, type, gruppenId) {
_itemData[z.sperreId] = { ...z, _kind: 'zeitstrafe', _gruppenId: gruppenId };
const badges = [];
const zeit = formatMin(z.minutenVon, z.minutenBis);
if (zeit) badges.push(`<span class="badge badge-neutral">${esc(zeit)}</span>`);
const detailRows = [];
if (z.text) detailRows.push(`<div class="item-detail-text">${esc(z.text)}</div>`);
if (z.releaseText) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Bei Aufhebung:</span><span style="font-size:0.78rem; color:var(--color-text);">${esc(z.releaseText)}</span></div>`);
if (z.sperreFuer && z.sperreFuer.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Sperrt:</span>${werkzeugChips(z.sperreFuer)}</div>`);
if (z.benoetigteToys && z.benoetigteToys.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Toys:</span>${toyChips(z.benoetigteToys)}</div>`);
const actionBtns = type === 'user' ? `
<div class="item-action-btns">
<button class="btn-item-edit" onclick="openEditItemModal('${esc(z.sperreId)}',event)">✎ Bearbeiten</button>
<button class="btn-item-delete" onclick="deleteItem('zeitstrafe','${esc(z.sperreId)}','${esc(gruppenId)}',event)">✕ Löschen</button>
</div>` : '';
return `<div class="item" id="item-${esc(z.sperreId)}">
<div class="item-row" onclick="toggleItem('${esc(z.sperreId)}')">
<span class="item-text">${esc(z.kurzText)}</span>
${badges.length ? `<span class="item-badges">${badges.join('')}</span>` : ''}
</div>
${(detailRows.length || actionBtns) ? `<div class="item-detail">${detailRows.join('')}${actionBtns}</div>` : ''}
</div>`;
}
const GESCHLECHT_LABEL = { WEIBLICH: 'Weiblich', DIVERS: 'Divers', MAENNLICH: 'Männlich' };
function renderFinisher(f, type, gruppenId) {
_itemData[f.finisherId] = { ...f, _kind: 'finisher', _gruppenId: gruppenId };
const badges = [];
if (f.geschlecht) badges.push(`<span class="badge badge-neutral">${esc(GESCHLECHT_LABEL[f.geschlecht] || f.geschlecht)}</span>`);
const detailRows = [];
if (f.text) detailRows.push(`<div class="item-detail-text">${esc(f.text)}</div>`);
if (f.benoetigtAktiv && f.benoetigtAktiv.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Aktiv:</span>${werkzeugChips(f.benoetigtAktiv)}</div>`);
if (f.benoetigtPassiv && f.benoetigtPassiv.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Passiv:</span>${werkzeugChips(f.benoetigtPassiv)}</div>`);
if (f.benoetigteToys && f.benoetigteToys.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Toys:</span>${toyChips(f.benoetigteToys)}</div>`);
const actionBtns = type === 'user' ? `
<div class="item-action-btns">
<button class="btn-item-edit" onclick="openEditItemModal('${esc(f.finisherId)}',event)">✎ Bearbeiten</button>
<button class="btn-item-delete" onclick="deleteItem('finisher','${esc(f.finisherId)}','${esc(gruppenId)}',event)">✕ Löschen</button>
</div>` : '';
return `<div class="item" id="item-${esc(f.finisherId)}">
<div class="item-row" onclick="toggleItem('${esc(f.finisherId)}')">
<span class="item-text">${esc(f.kurzText)}</span>
${badges.length ? `<span class="item-badges">${badges.join('')}</span>` : ''}
</div>
${(detailRows.length || actionBtns) ? `<div class="item-detail">${detailRows.join('')}${actionBtns}</div>` : ''}
</div>`;
}
// ── Item toggle (detail panel) ──
let openItemId = null;
function toggleItem(itemId) {
if (openItemId === itemId) {
document.getElementById('item-' + itemId).classList.remove('open');
openItemId = null;
return;
}
if (openItemId) {
const prev = document.getElementById('item-' + openItemId);
if (prev) prev.classList.remove('open');
}
const el = document.getElementById('item-' + itemId);
if (el) el.classList.add('open');
openItemId = itemId;
}
// ── Item löschen ──
const ITEM_DELETE_URL = {
aufgabe: apiUrl('/aufgabe'),
strafe: null,
zeitstrafe: null,
finisher: apiUrl('/finisher')
};
const ITEM_DELETE_FIELD = { aufgabe: 'aufgabeId', strafe: 'strafeId', zeitstrafe: 'sperreId', finisher: 'finisherId' };
function deleteItem(kind, itemId, gruppenId, event) {
event.stopPropagation();
if (!confirm('Eintrag wirklich löschen?')) return;
const deleteUrl = ITEM_DELETE_URL[kind];
if (!deleteUrl) return;
const body = { [ITEM_DELETE_FIELD[kind]]: itemId };
fetch(deleteUrl, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
}).then(r => {
if (r.ok || r.status === 202) {
openItemId = null;
pendingExpandId = gruppenId;
pendingExpandType = 'user';
loadUserGruppen();
} else {
document.getElementById('userActionError').textContent = 'Fehler beim Löschen (HTTP ' + r.status + ').';
}
}).catch(() => {
document.getElementById('userActionError').textContent = 'Verbindungsfehler.';
});
}
// ── Sort ──
function sortByLevelThenName(items) {
return items.slice().sort((a, b) => {
const la = a.level ?? 999, lb = b.level ?? 999;
if (la !== lb) return la - lb;
return (a.kurzText || '').localeCompare(b.kurzText || '', 'de');
});
}
const GESCHLECHT_ORDER = { WEIBLICH: 0, DIVERS: 1, MAENNLICH: 2 };
function sortByGeschlecht(items) {
return items.slice().sort((a, b) => {
const ga = GESCHLECHT_ORDER[a.geschlecht] ?? 99, gb = GESCHLECHT_ORDER[b.geschlecht] ?? 99;
if (ga !== gb) return ga - gb;
return (a.kurzText || '').localeCompare(b.kurzText || '', 'de');
});
}
function sortByName(items) {
return items.slice().sort((a, b) => (a.kurzText || '').localeCompare(b.kurzText || '', 'de'));
}
// ── Time formatting ──
function formatSek(von, bis) {
if (von != null && bis != null) return `${von}${bis} s`;
if (von != null) return `ab ${von} s`;
if (bis != null) return `bis ${bis} s`;
return '';
}
function formatMin(von, bis) {
if (von != null && bis != null) return `${von}${bis} min`;
if (von != null) return `ab ${von} min`;
if (bis != null) return `bis ${bis} min`;
return '';
}
// ── Selektion (listenübergreifend) ──
let selectedGruppeId = null;
let selectedGruppeType = null;
function selectAndToggle(gruppenId, type) {
if (selectedGruppeId === gruppenId) {
collapseGruppe(gruppenId);
selectedGruppeId = selectedGruppeType = null;
updateButtons(null);
return;
}
if (selectedGruppeId) collapseGruppe(selectedGruppeId);
selectedGruppeId = gruppenId;
selectedGruppeType = type;
expandGruppe(gruppenId);
updateButtons(type);
document.getElementById('userActionError').textContent = '';
document.getElementById('aboActionError').textContent = '';
document.getElementById('systemActionError').textContent = '';
}
function expandGruppe(id) {
const card = document.getElementById('gruppe-' + id);
const body = document.getElementById('body-' + id);
if (!card) return;
card.classList.add('selected', 'open');
body.style.display = 'block';
}
function collapseGruppe(id) {
const card = document.getElementById('gruppe-' + id);
const body = document.getElementById('body-' + id);
if (!card) return;
card.classList.remove('selected', 'open');
body.style.display = 'none';
}
function updateButtons(type) {
const isUser = type === 'user';
const isSystem = type === 'system';
const isAbo = type === 'abo';
const isPrivate = isUser && selectedGruppeId && _gruppeData[selectedGruppeId]?.privateGruppe;
document.getElementById('publishBtn').disabled = !isPrivate;
document.getElementById('editBtn').disabled = !isUser;
document.getElementById('deleteBtn').disabled = !isUser;
document.getElementById('copyBtn').disabled = !isSystem;
document.getElementById('aboCopyBtn').disabled = !isAbo;
document.getElementById('unsubBtn').disabled = !isAbo;
}
function resetSelection() {
if (selectedGruppeId) collapseGruppe(selectedGruppeId);
selectedGruppeId = selectedGruppeType = null;
updateButtons(null);
document.getElementById('userActionError').textContent = '';
document.getElementById('aboActionError').textContent = '';
document.getElementById('systemActionError').textContent = '';
}
// ── Paging ──
function updatePaging(pagingId, prevId, nextId, infoId, current, total) {
const el = document.getElementById(pagingId);
if (total <= 1) { el.style.display = 'none'; return; }
el.style.display = 'flex';
document.getElementById(prevId).disabled = current === 0;
document.getElementById(nextId).disabled = current >= total - 1;
document.getElementById(infoId).textContent = `Seite ${current + 1} von ${total}`;
}
document.getElementById('userPrev').addEventListener('click', () => { if (userPage > 0) { userPage--; loadUserGruppen(); } });
document.getElementById('userNext').addEventListener('click', () => { if (userPage < userTotalPages - 1) { userPage++; loadUserGruppen(); } });
document.getElementById('aboPrev').addEventListener('click', () => { if (aboPage > 0) { aboPage--; loadAboGruppen(); } });
document.getElementById('aboNext').addEventListener('click', () => { if (aboPage < aboTotalPages - 1) { aboPage++; loadAboGruppen(); } });
document.getElementById('systemPrev').addEventListener('click', () => { if (systemPage > 0) { systemPage--; loadSystemGruppen(); } });
document.getElementById('systemNext').addEventListener('click', () => { if (systemPage < systemTotalPages - 1) { systemPage++; loadSystemGruppen(); } });
// ── Gruppe-Modal ──
const gruppeModal = document.getElementById('gruppeModal');
const saveBtn = document.getElementById('saveBtn');
let currentEditId = null;
function openModal(editId) {
currentEditId = editId || null;
document.getElementById('modalError').style.display = 'none';
document.getElementById('gBild').value = '';
if (currentEditId) {
fetch(apiUrl(`/gruppe/${currentEditId}`))
.then(r => r.ok ? r.json() : null)
.then(g => {
if (!g) return;
document.getElementById('modalTitle').textContent = 'Aufgabengruppe bearbeiten';
document.getElementById('gName').value = g.name || '';
document.getElementById('gDesc').value = g.beschreibung || '';
const pubCb = document.getElementById('gPublic');
pubCb.checked = !g.privateGruppe;
pubCb.disabled = g.privateGruppe; // Veröffentlichen nur über den Veröffentlichen-Button
document.getElementById('gPublicLabel').style.display = 'block';
const imgWrap = document.getElementById('gCurrentImgWrap');
if (g.bild) {
document.getElementById('gCurrentImg').src = 'data:image/png;base64,' + g.bild;
imgWrap.style.display = 'flex';
} else {
imgWrap.style.display = 'none';
}
gruppeModal.classList.add('open');
document.getElementById('gName').focus();
})
.catch(() => alert('Fehler beim Laden der Gruppe.'));
} else {
document.getElementById('modalTitle').textContent = 'Neue Aufgabengruppe';
document.getElementById('gName').value = '';
document.getElementById('gDesc').value = '';
document.getElementById('gPublic').checked = false;
document.getElementById('gPublicLabel').style.display = 'none';
document.getElementById('gCurrentImgWrap').style.display = 'none';
gruppeModal.classList.add('open');
document.getElementById('gName').focus();
}
}
function closeGruppeModal() {
gruppeModal.classList.remove('open');
document.getElementById('gPublic').disabled = false;
}
document.getElementById('openCreateBtn').addEventListener('click', () => openModal(null));
document.getElementById('editBtn').addEventListener('click', () => { if (selectedGruppeId && selectedGruppeType === 'user') openModal(selectedGruppeId); });
document.getElementById('cancelBtn').addEventListener('click', closeGruppeModal);
gruppeModal.addEventListener('click', e => { if (e.target === gruppeModal) closeGruppeModal(); });
saveBtn.addEventListener('click', async () => {
const name = document.getElementById('gName').value.trim();
if (!name) { showModalError('Bitte einen Namen eingeben.'); return; }
saveBtn.disabled = true;
saveBtn.textContent = 'Speichert…';
let bildBase64 = null;
const fileInput = document.getElementById('gBild');
if (fileInput.files.length > 0) {
bildBase64 = await toBase64(fileInput.files[0]);
}
const isEdit = currentEditId != null;
const payload = {
name,
beschreibung: document.getElementById('gDesc').value.trim() || null,
privateGruppe: isEdit ? !document.getElementById('gPublic').checked : true,
bild: bildBase64
};
fetch(isEdit ? apiUrl(`/gruppe/${currentEditId}`) : apiUrl('/gruppe'), {
method: isEdit ? 'PUT' : 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
.then(r => {
if (r.ok || r.status === 201) {
closeGruppeModal();
if (isEdit) {
pendingExpandId = currentEditId;
pendingExpandType = 'user';
}
userPage = 0;
loadUserGruppen();
} else if (r.status === 409) {
showModalError('Limit erreicht: maximal 10 eigene Aufgabengruppen möglich.');
} else {
showModalError('Fehler beim Speichern (HTTP ' + r.status + ').');
}
})
.catch(() => showModalError('Verbindungsfehler.'))
.finally(() => { saveBtn.disabled = false; saveBtn.textContent = 'Speichern'; });
});
function showModalError(msg) {
const el = document.getElementById('modalError');
el.textContent = msg; el.style.display = 'block';
}
// ── Delete ──
document.getElementById('deleteBtn').addEventListener('click', () => {
if (!selectedGruppeId || selectedGruppeType !== 'user') return;
openConfirmModal('Aufgabengruppe und alle enthaltenen Aufgaben, Strafen und Zeitstrafen wirklich löschen?', () => {
const btn = document.getElementById('deleteBtn');
btn.disabled = true;
fetch(apiUrl(`/gruppe/${selectedGruppeId}`), { method: 'DELETE' })
.then(r => {
if (r.ok || r.status === 202) {
userPage = 0;
loadUserGruppen();
} else if (r.status === 403) {
document.getElementById('userActionError').textContent = 'Keine Berechtigung.';
btn.disabled = false;
} else {
document.getElementById('userActionError').textContent = 'Fehler beim Löschen.';
btn.disabled = false;
}
})
.catch(() => { document.getElementById('userActionError').textContent = 'Verbindungsfehler.'; btn.disabled = false; });
});
});
// ── Copy ──
document.getElementById('copyBtn').addEventListener('click', () => {
if (!selectedGruppeId || selectedGruppeType !== 'system') return;
const btn = document.getElementById('copyBtn');
btn.disabled = true;
fetch(apiUrl(`/gruppe/copy/${selectedGruppeId}`), { method: 'POST' })
.then(r => {
if (r.ok || r.status === 201) {
userPage = 0;
loadUserGruppen();
document.getElementById('systemActionError').textContent = '';
} else {
document.getElementById('systemActionError').textContent = 'Fehler beim Kopieren (HTTP ' + r.status + ').';
btn.disabled = false;
}
})
.catch(() => { document.getElementById('systemActionError').textContent = 'Verbindungsfehler.'; btn.disabled = false; });
});
// ── Copy Abo-Gruppe ──
document.getElementById('aboCopyBtn').addEventListener('click', () => {
if (!selectedGruppeId || selectedGruppeType !== 'abo') return;
const btn = document.getElementById('aboCopyBtn');
btn.disabled = true;
fetch(apiUrl(`/gruppe/copy/${selectedGruppeId}`), { method: 'POST' })
.then(r => {
if (r.ok || r.status === 201) {
userPage = 0;
loadUserGruppen();
document.getElementById('aboActionError').textContent = '';
} else if (r.status === 409) {
document.getElementById('aboActionError').textContent = 'Limit erreicht: maximal 10 eigene Aufgabengruppen möglich.';
btn.disabled = false;
} else {
document.getElementById('aboActionError').textContent = 'Fehler beim Kopieren (HTTP ' + r.status + ').';
btn.disabled = false;
}
})
.catch(() => { document.getElementById('aboActionError').textContent = 'Verbindungsfehler.'; btn.disabled = false; });
});
// ── Unsubscribe ──
document.getElementById('unsubBtn').addEventListener('click', () => {
if (!selectedGruppeId || selectedGruppeType !== 'abo') return;
if (!confirm('Abonnement dieser Gruppe wirklich kündigen?')) return;
const btn = document.getElementById('unsubBtn');
btn.disabled = true;
fetch(apiUrl(`/abo/${selectedGruppeId}`), { method: 'DELETE' })
.then(r => {
if (r.ok || r.status === 202) {
resetSelection();
aboPage = 0;
loadAboGruppen();
} else {
document.getElementById('aboActionError').textContent = 'Fehler beim Kündigen (HTTP ' + r.status + ').';
btn.disabled = false;
}
})
.catch(() => { document.getElementById('aboActionError').textContent = 'Verbindungsfehler.'; btn.disabled = false; });
});
// ── Toy-Auswahl ──
let _selectedToys = []; // [{toyId, name, bild}]
let _allAvailableToys = null; // cache
function _loadAvailableToys() {
if (_allAvailableToys !== null) return Promise.resolve(_allAvailableToys);
return fetch(apiUrl('/toy/available'))
.then(r => r.json())
.then(toys => { _allAvailableToys = toys || []; return _allAvailableToys; });
}
function renderSelectedToys() {
const container = document.getElementById('iSelectedToys');
if (_selectedToys.length === 0) { container.innerHTML = ''; return; }
container.innerHTML = _selectedToys.map(t => {
const img = t.bild ? `<img src="data:image/png;base64,${t.bild}" alt="">` : '';
return `<span class="sel-toy-chip">${img}${esc(t.name)}<button class="toy-remove" type="button" onclick="removeToy('${esc(t.toyId)}')" title="Entfernen">✕</button></span>`;
}).join('');
}
function removeToy(toyId) {
_selectedToys = _selectedToys.filter(t => t.toyId !== toyId);
renderSelectedToys();
// update search modal chips if open
const chip = document.querySelector(`#toySearchResults .toy-result-chip[data-id="${toyId}"]`);
if (chip) chip.classList.remove('selected');
}
// Toy-Suche (inline)
document.getElementById('iToyAddBtn').addEventListener('click', toggleToySearch);
document.getElementById('toySearchInput').addEventListener('input', renderToySearchResults);
function toggleToySearch() {
const area = document.getElementById('iToySearchArea');
if (area.style.display === 'none') {
area.style.display = 'block';
document.getElementById('toySearchInput').value = '';
document.getElementById('toySearchResults').innerHTML = '';
document.getElementById('toySearchEmpty').style.display = 'none';
document.getElementById('iToyAddBtn').textContent = '▲ Suche schließen';
_loadAvailableToys()
.then(() => renderToySearchResults())
.catch(() => {
document.getElementById('toySearchResults').innerHTML =
'<span style="font-size:0.82rem;color:var(--color-muted)">Fehler beim Laden.</span>';
});
document.getElementById('toySearchInput').focus();
} else {
closeToySearch();
}
}
function closeToySearch() {
document.getElementById('iToySearchArea').style.display = 'none';
document.getElementById('iToyAddBtn').textContent = '+ Toy hinzufügen';
}
function renderToySearchResults() {
const query = document.getElementById('toySearchInput').value.trim().toLowerCase();
const results = document.getElementById('toySearchResults');
const empty = document.getElementById('toySearchEmpty');
if (!_allAvailableToys) return;
const filtered = query
? _allAvailableToys.filter(t => t.name.toLowerCase().includes(query))
: _allAvailableToys;
if (filtered.length === 0) {
results.innerHTML = '';
empty.style.display = 'block';
return;
}
empty.style.display = 'none';
const selectedIds = new Set(_selectedToys.map(t => t.toyId));
results.innerHTML = filtered.map(t => {
const sel = selectedIds.has(t.toyId) ? ' selected' : '';
const img = t.bild ? `<img src="data:image/png;base64,${t.bild}" alt="">` : '';
return `<span class="toy-result-chip${sel}" data-id="${esc(t.toyId)}" onclick="toggleToyFromSearch('${esc(t.toyId)}')">${img}${esc(t.name)}</span>`;
}).join('');
}
function toggleToyFromSearch(toyId) {
const toy = (_allAvailableToys || []).find(t => t.toyId === toyId);
if (!toy) return;
const idx = _selectedToys.findIndex(t => t.toyId === toyId);
if (idx >= 0) {
_selectedToys.splice(idx, 1);
} else {
_selectedToys.push({ toyId: toy.toyId, name: toy.name, bild: toy.bild });
}
renderSelectedToys();
renderToySearchResults();
}
function _initToys(preSelected) {
// preSelected: [{toyId, name, bild}]
_selectedToys = preSelected || [];
renderSelectedToys();
}
// ── Item-Hinzufügen-Modal ──
const itemModal = document.getElementById('itemModal');
const itemSaveBtn = document.getElementById('itemSaveBtn');
let currentItemGruppeId = null;
let currentItemKind = null; // 'aufgabe' | 'strafe' | 'zeitstrafe'
let currentItemEditId = null; // null = neu, sonst ID des zu bearbeitenden Items
const ITEM_TITLES_NEW = { aufgabe: 'Aufgabe hinzufügen', strafe: 'Strafe hinzufügen', zeitstrafe: 'Zeitstrafe hinzufügen', finisher: 'Finisher hinzufügen' };
const ITEM_TITLES_EDIT = { aufgabe: 'Aufgabe bearbeiten', strafe: 'Strafe bearbeiten', zeitstrafe: 'Zeitstrafe bearbeiten', finisher: 'Finisher bearbeiten' };
function _setupItemModal(kind) {
const isZeit = kind === 'zeitstrafe';
const isFinisher = kind === 'finisher';
document.querySelector('#iPlaceholderHint .placeholder-hint').innerHTML =
isFinisher
? 'In Texten können Platzhalter verwendet werden:<br>' +
'<code>{AKTIV}</code> Name der Person die kommt<br>' +
'<code>{PASSIV}</code> Name der Person die zum Kommen bringt'
: 'In Texten können Platzhalter verwendet werden:<br>' +
'<code>{AKTIV}</code> Name des aktiven Parts<br>' +
'<code>{PASSIV}</code> Name des passiven Parts';
document.getElementById('iGeschlechtRow').style.display = isFinisher ? 'block' : 'none';
document.getElementById('iLevelRow').style.display = (!isZeit && !isFinisher) ? 'block' : 'none';
document.getElementById('iWerkzeugAktivRow').style.display = (!isZeit && !isFinisher) ? 'block' : 'none';
document.getElementById('iWerkzeugPassivRow').style.display = (!isZeit && !isFinisher) ? 'block' : 'none';
document.getElementById('iWerkzeugFinisherAktivRow').style.display = isFinisher ? 'block' : 'none';
document.getElementById('iWerkzeugFinisherPassivRow').style.display = isFinisher ? 'block' : 'none';
document.getElementById('iMinutenRow').style.display = isZeit ? 'block' : 'none';
document.getElementById('iSperreFuerRow').style.display = isZeit ? 'block' : 'none';
document.getElementById('iReleaseTextRow').style.display = isZeit ? 'block' : 'none';
}
function _resetItemFields() {
document.getElementById('iKurzText').value = '';
document.getElementById('iText').value = '';
document.getElementById('iLevel').value = '';
document.getElementById('iSekVon').value = '';
document.getElementById('iSekBis').value = '';
document.getElementById('iMinVon').value = '';
document.getElementById('iMinBis').value = '';
document.getElementById('iReleaseText').value = '';
document.querySelectorAll('#iWerkzeugAktiv input').forEach(cb => cb.checked = false);
document.querySelectorAll('#iWerkzeugPassiv input').forEach(cb => cb.checked = false);
document.querySelectorAll('#iWerkzeugFinisherAktiv input').forEach(cb => cb.checked = false);
document.querySelectorAll('#iWerkzeugFinisherPassiv input').forEach(cb => cb.checked = false);
document.querySelectorAll('#iSperreFuer input').forEach(cb => cb.checked = false);
document.querySelectorAll('#iGeschlecht input').forEach(rb => rb.checked = false);
_selectedToys = [];
renderSelectedToys();
document.getElementById('itemModalError').style.display = 'none';
}
function openItemModal(gruppenId, kind) {
currentItemGruppeId = gruppenId;
currentItemKind = kind;
currentItemEditId = null;
document.getElementById('itemModalTitle').textContent = ITEM_TITLES_NEW[kind];
_resetItemFields();
_setupItemModal(kind);
_initToys([]);
itemModal.classList.add('open');
document.getElementById('iKurzText').focus();
}
function openEditItemModal(itemId, event) {
event.stopPropagation();
const d = _itemData[itemId];
if (!d) return;
currentItemGruppeId = d._gruppenId;
currentItemKind = d._kind;
currentItemEditId = itemId;
document.getElementById('itemModalTitle').textContent = ITEM_TITLES_EDIT[d._kind];
_resetItemFields();
_setupItemModal(d._kind);
document.getElementById('iKurzText').value = d.kurzText || '';
document.getElementById('iText').value = d.text || '';
if (d._kind === 'aufgabe' || d._kind === 'strafe') {
document.getElementById('iLevel').value = d.level != null ? d.level : '';
document.getElementById('iSekVon').value = d.sekundenVon != null ? d.sekundenVon : '';
document.getElementById('iSekBis').value = d.sekundenBis != null ? d.sekundenBis : '';
(d.benoetigtAktiv || []).forEach(w => { const cb = document.querySelector(`#iWerkzeugAktiv input[value="${w}"]`); if (cb) cb.checked = true; });
(d.benoetigtPassiv || []).forEach(w => { const cb = document.querySelector(`#iWerkzeugPassiv input[value="${w}"]`); if (cb) cb.checked = true; });
} else if (d._kind === 'finisher') {
(d.benoetigtAktiv || []).forEach(w => { const cb = document.querySelector(`#iWerkzeugFinisherAktiv input[value="${w}"]`); if (cb) cb.checked = true; });
(d.benoetigtPassiv || []).forEach(w => { const cb = document.querySelector(`#iWerkzeugFinisherPassiv input[value="${w}"]`); if (cb) cb.checked = true; });
if (d.geschlecht) {
const rb = document.querySelector(`#iGeschlecht input[value="${d.geschlecht}"]`);
if (rb) rb.checked = true;
}
} else {
document.getElementById('iMinVon').value = d.minutenVon != null ? d.minutenVon : '';
document.getElementById('iMinBis').value = d.minutenBis != null ? d.minutenBis : '';
document.getElementById('iReleaseText').value = d.releaseText || '';
(d.sperreFuer || []).forEach(w => { const cb = document.querySelector(`#iSperreFuer input[value="${w}"]`); if (cb) cb.checked = true; });
}
const preSelected = (d.benoetigteToys || []).filter(t => t.toyId);
_initToys(preSelected);
itemModal.classList.add('open');
document.getElementById('iKurzText').focus();
}
function closeItemModal() { itemModal.classList.remove('open'); closeToySearch(); document.getElementById('iPlaceholderHint').style.display = 'none'; document.getElementById('iTextAC').style.display = 'none'; }
function togglePlaceholderHint() {
const el = document.getElementById('iPlaceholderHint');
el.style.display = el.style.display === 'none' ? 'block' : 'none';
}
(function() {
const STATIC = ['{AKTIV}', '{PASSIV}'];
const ta = document.getElementById('iText');
const ac = document.getElementById('iTextAC');
let _allToys = [];
let _items = [];
let _wordStart = 0;
let activeIdx = -1;
function currentWord() {
const pos = ta.selectionStart;
let start = pos;
while (start > 0 && !/\s/.test(ta.value[start - 1])) start--;
return { word: ta.value.slice(start, pos).toLowerCase(), start };
}
function buildItems(filter) {
const f = filter || '';
_items = STATIC.filter(s => !f || s.toLowerCase().includes(f)).map(s => ({ label: s, insert: s }));
const toys = _allToys.filter(t => !f || t.name.toLowerCase().includes(f));
if (toys.length) {
_items.push({ separator: true, label: 'Toys' });
toys.forEach(t => _items.push({ label: t.name, insert: t.name, toyId: t.toyId }));
}
}
function selectables() { return _items.map((it, i) => it.separator ? null : i).filter(i => i !== null); }
function renderItems() {
ac.innerHTML = '';
activeIdx = -1;
_items.forEach((item, i) => {
if (item.separator) {
const sep = document.createElement('div');
sep.className = 'ac-separator';
sep.textContent = item.label;
ac.appendChild(sep);
} else {
const div = document.createElement('div');
div.className = 'ac-item';
div.dataset.idx = String(i);
div.textContent = item.label;
div.addEventListener('mousedown', e => { e.preventDefault(); doInsert(item); });
div.addEventListener('mouseover', () => setActive(i));
ac.appendChild(div);
}
});
const s = selectables();
if (s.length) { setActive(s[0]); ac.style.display = 'block'; } else hideAC();
}
function showAC(toys) {
_allToys = toys || [];
const { word, start } = currentWord();
_wordStart = start;
buildItems(word);
const rect = ta.getBoundingClientRect();
ac.style.left = rect.left + 'px';
ac.style.top = (rect.bottom + 4) + 'px';
renderItems();
}
function hideAC() { ac.style.display = 'none'; activeIdx = -1; }
function setActive(i) {
activeIdx = i;
let activeEl = null;
ac.querySelectorAll('.ac-item').forEach(el => {
const on = parseInt(el.dataset.idx) === i;
el.classList.toggle('ac-item-active', on);
if (on) activeEl = el;
});
if (activeEl) activeEl.scrollIntoView({ block: 'nearest' });
}
function doInsert(item) {
const end = ta.selectionStart;
ta.value = ta.value.slice(0, _wordStart) + item.insert + ta.value.slice(end);
ta.selectionStart = ta.selectionEnd = _wordStart + item.insert.length;
ta.focus();
if (item.toyId) {
const already = (_selectedToys || []).find(t => t.toyId === item.toyId);
if (!already) toggleToyFromSearch(item.toyId);
}
hideAC();
}
ta.addEventListener('keydown', e => {
if (e.ctrlKey && e.code === 'Space') {
e.preventDefault();
_loadAvailableToys().then(toys => showAC(toys)).catch(() => showAC([]));
return;
}
if (ac.style.display !== 'block') return;
if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); hideAC(); return; }
const s = selectables();
if (!s.length) return;
const pos = s.indexOf(activeIdx);
if (e.key === 'ArrowDown') { e.preventDefault(); setActive(s[(pos + 1) % s.length]); }
else if (e.key === 'ArrowUp') { e.preventDefault(); setActive(s[(pos - 1 + s.length) % s.length]); }
else if (e.key === 'Enter' || e.key === 'Tab') {
e.preventDefault();
const item = _items[activeIdx];
if (item && !item.separator) doInsert(item);
}
});
ta.addEventListener('input', () => {
if (ac.style.display !== 'block') return;
const { word, start } = currentWord();
_wordStart = start;
buildItems(word);
renderItems();
});
document.addEventListener('mousedown', e => { if (!ac.contains(e.target) && e.target !== ta) hideAC(); });
})();
document.getElementById('itemCancelBtn').addEventListener('click', closeItemModal);
itemModal.addEventListener('click', e => { if (e.target === itemModal) closeItemModal(); });
itemSaveBtn.addEventListener('click', async () => {
const kurzText = document.getElementById('iKurzText').value.trim();
const text = document.getElementById('iText').value.trim();
if (!kurzText) { showItemError('Bitte eine Kurzbezeichnung eingeben.'); return; }
if (!text) { showItemError('Bitte eine Beschreibung eingeben.'); return; }
const kind = currentItemKind;
const isEdit = currentItemEditId !== null;
let url, method, payload;
if (kind === 'aufgabe' || kind === 'strafe') {
const levelVal = document.getElementById('iLevel').value.trim();
if (!levelVal) { showItemError('Bitte ein Level angeben.'); return; }
const level = parseInt(levelVal, 10);
if (isNaN(level) || level < 1 || level > 5) { showItemError('Level muss zwischen 1 und 5 liegen.'); return; }
const sekVon = document.getElementById('iSekVon').value.trim();
const sekBis = document.getElementById('iSekBis').value.trim();
payload = {
kurzText, text, level,
gruppeId: isEdit ? undefined : currentItemGruppeId,
sekundenVon: sekVon ? parseInt(sekVon, 10) : null,
sekundenBis: sekBis ? parseInt(sekBis, 10) : null,
benoetigtAktiv: checkedValues('iWerkzeugAktiv'),
benoetigtPassiv: checkedValues('iWerkzeugPassiv'),
benoetigteToys: _selectedToys.map(t => ({ toyId: t.toyId }))
};
const base = kind === 'aufgabe' ? apiUrl('/aufgabe') : '/strafe';
url = isEdit ? `${base}/${currentItemEditId}` : base;
method = isEdit ? 'PUT' : 'POST';
} else if (kind === 'finisher') {
const geschlecht = document.querySelector('#iGeschlecht input:checked')?.value;
if (!geschlecht) { showItemError('Bitte ein Geschlecht auswählen.'); return; }
payload = {
kurzText, text, geschlecht,
gruppeId: isEdit ? undefined : currentItemGruppeId,
benoetigtAktiv: checkedValues('iWerkzeugFinisherAktiv'),
benoetigtPassiv: checkedValues('iWerkzeugFinisherPassiv'),
benoetigteToys: _selectedToys.map(t => ({ toyId: t.toyId }))
};
url = isEdit ? apiUrl(`/finisher/${currentItemEditId}`) : apiUrl('/finisher');
method = isEdit ? 'PUT' : 'POST';
} else {
const minVon = document.getElementById('iMinVon').value.trim();
if (!minVon) { showItemError('Bitte eine Mindestdauer in Minuten angeben.'); return; }
const sperreFuer = checkedValues('iSperreFuer');
if (sperreFuer.length === 0) { showItemError('Bitte mindestens ein Werkzeug für die Sperre auswählen.'); return; }
const minBis = document.getElementById('iMinBis').value.trim();
payload = {
kurzText, text,
gruppeId: isEdit ? undefined : currentItemGruppeId,
minutenVon: parseInt(minVon, 10),
minutenBis: minBis ? parseInt(minBis, 10) : null,
releaseText: document.getElementById('iReleaseText').value.trim() || null,
sperreFuer,
benoetigteToys: _selectedToys.map(t => ({ toyId: t.toyId }))
};
url = isEdit ? `/sperre/${currentItemEditId}` : '/sperre'; // BDSM-only
method = isEdit ? 'PUT' : 'POST';
}
itemSaveBtn.disabled = true;
itemSaveBtn.textContent = 'Speichert…';
fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
.then(r => {
if (r.ok || r.status === 201) {
closeItemModal();
pendingExpandId = currentItemGruppeId;
pendingExpandType = 'user';
userPage = 0;
loadUserGruppen();
} else if (r.status === 409) {
showItemError('Limit erreicht: maximal 100 Einträge pro Gruppe möglich.');
} else {
showItemError('Fehler beim Speichern (HTTP ' + r.status + ').');
}
})
.catch(() => showItemError('Verbindungsfehler.'))
.finally(() => { itemSaveBtn.disabled = false; itemSaveBtn.textContent = 'Speichern'; });
});
function showItemError(msg) {
const el = document.getElementById('itemModalError');
el.textContent = msg; el.style.display = 'block';
}
function checkedValues(containerId) {
return Array.from(document.querySelectorAll(`#${containerId} input:checked`)).map(cb => cb.value);
}
// ── Image scaling ──
function toBase64(file) {
const MAX = 128;
return new Promise((resolve, reject) => {
const img = new Image();
const url = URL.createObjectURL(file);
img.onload = () => {
URL.revokeObjectURL(url);
let w = img.naturalWidth, h = img.naturalHeight;
if (w > MAX || h > MAX) {
if (w >= h) { h = Math.max(1, Math.round(MAX * h / w)); w = MAX; }
else { w = Math.max(1, Math.round(MAX * w / h)); h = MAX; }
}
const canvas = document.createElement('canvas');
canvas.width = w; canvas.height = h;
canvas.getContext('2d').drawImage(img, 0, 0, w, h);
resolve(canvas.toDataURL('image/png').split(',')[1]);
};
img.onerror = reject;
img.src = url;
});
}
// ── XSS ──
function esc(str) {
if (str == null) return '';
return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// ── Publish-Modal ──
const publishModal = document.getElementById('publishModal');
const publishConfirmBtn = document.getElementById('publishConfirmBtn');
const publishConfirmCb = document.getElementById('publishConfirmCb');
function openPublishModal() {
if (!selectedGruppeId || !_gruppeData[selectedGruppeId]?.privateGruppe) return;
publishConfirmCb.checked = false;
publishConfirmBtn.disabled = true;
document.getElementById('publishError').style.display = 'none';
publishModal.classList.add('open');
}
function closePublishModal() {
publishModal.classList.remove('open');
}
publishConfirmCb.addEventListener('change', () => {
publishConfirmBtn.disabled = !publishConfirmCb.checked;
});
document.getElementById('publishCancelBtn').addEventListener('click', closePublishModal);
publishModal.addEventListener('click', e => { if (e.target === publishModal) closePublishModal(); });
document.getElementById('publishBtn').addEventListener('click', openPublishModal);
publishConfirmBtn.addEventListener('click', () => {
const g = _gruppeData[selectedGruppeId];
if (!g) return;
publishConfirmBtn.disabled = true;
publishConfirmBtn.textContent = 'Wird veröffentlicht…';
fetch(apiUrl(`/gruppe/${selectedGruppeId}`), {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: g.name,
beschreibung: g.beschreibung || null,
privateGruppe: false,
bild: g.bild || null
})
})
.then(r => {
if (r.ok) {
closePublishModal();
pendingExpandId = selectedGruppeId;
pendingExpandType = 'user';
userPage = 0;
loadUserGruppen();
} else {
const errEl = document.getElementById('publishError');
errEl.textContent = 'Fehler beim Veröffentlichen (HTTP ' + r.status + ').';
errEl.style.display = 'block';
publishConfirmBtn.disabled = false;
}
})
.catch(() => {
const errEl = document.getElementById('publishError');
errEl.textContent = 'Verbindungsfehler.';
errEl.style.display = 'block';
publishConfirmBtn.disabled = false;
})
.finally(() => { publishConfirmBtn.textContent = 'Veröffentlichen'; });
});
// ── ESC schließt alle Modals ──
document.addEventListener('keydown', e => {
if (e.key !== 'Escape') return;
if (publishModal.classList.contains('open')) { closePublishModal(); return; }
if (gruppeModal.classList.contains('open')) { closeGruppeModal(); return; }
if (itemModal.classList.contains('open')) { closeItemModal(); return; }
if (confirmModal.classList.contains('open')) { closeConfirmModal(); return; }
});
const confirmModal = document.getElementById('confirmModal');
document.getElementById('confirmModalCancel').addEventListener('click', closeConfirmModal);
confirmModal.addEventListener('click', e => { if (e.target === confirmModal) closeConfirmModal(); });
function closeConfirmModal() { confirmModal.classList.remove('open'); }
function openConfirmModal(text, onConfirm) {
document.getElementById('confirmModalText').textContent = text;
const okBtn = document.getElementById('confirmModalOk');
const handler = () => { okBtn.removeEventListener('click', handler); closeConfirmModal(); onConfirm(); };
okBtn.removeEventListener('click', handler);
okBtn.addEventListener('click', handler);
confirmModal.classList.add('open');
}
</script>
<script src="/js/icons.js"></script>
<script src="/js/nav.js"></script>
</body>
</html>