Weiter am Taskgame gebastelt
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled

This commit is contained in:
2026-05-02 23:10:41 +02:00
parent c472093f62
commit ca0e933d95
76 changed files with 987 additions and 288 deletions

View File

@@ -263,6 +263,12 @@
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-copy {
background: none; border: 1px solid rgba(100,160,255,0.4); 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-copy:hover { border-color: rgba(100,160,255,0.9); 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;
@@ -533,17 +539,11 @@
</div>
</div>
<div id="iTempUnlockRow">
<label>Temporäre Öffnungen</label>
<div style="display:flex; flex-direction:column; gap:0.5rem; margin-top:0.5rem;">
<label style="display:flex; align-items:center; gap:0.6rem; font-size:0.85rem; cursor:pointer;">
<input type="checkbox" id="iTempUnlockBefore" style="accent-color:var(--color-primary); width:1rem; height:1rem;">
Temporäre Öffnung <em>vor</em> der Zeitstrafe erforderlich
</label>
<label style="display:flex; align-items:center; gap:0.6rem; font-size:0.85rem; cursor:pointer;">
<input type="checkbox" id="iTempUnlockAfter" style="accent-color:var(--color-primary); width:1rem; height:1rem;">
Temporäre Öffnung <em>nach</em> der Zeitstrafe erforderlich
</label>
</div>
<label class="toggle-switch" style="display:flex; align-items:center; gap:0.75rem; cursor:pointer; margin-top:0.25rem;">
<input type="checkbox" id="iTempUnlockRequired">
<span class="toggle-track"></span>
<span style="font-size:0.9rem;">Temporäre Öffnung erforderlich</span>
</label>
</div>
<div id="iReleaseTextRow">
<label for="iReleaseText">Text bei Aufhebung</label>
@@ -765,7 +765,7 @@
${g.beschreibung ? `<div class="gruppe-desc">${esc(g.beschreibung)}</div>` : ''}
${renderSubSection('Aufgaben', sortByLevelThenName(g.aufgaben || []), 'aufgabe', renderAufgabe, g.gruppenId, type)}
${g.availableIn !== 'CHASTITY_ONLY' ? renderSubSection('Strafen', sortByLevelThenName(g.strafen || []), 'strafe', renderStrafe, g.gruppenId, type) : ''}
${renderSubSection('Zeitstrafen',sortByName(g.sperren || []), 'zeitstrafe',renderZeitstrafe, g.gruppenId, type)}
${renderSubSection('Zeitstrafen',sortByLevelThenName(g.sperren || []), 'zeitstrafe',renderZeitstrafe, g.gruppenId, type)}
${renderSubSection('Finisher', sortByGeschlecht(g.finisher || []), 'finisher', renderFinisher, g.gruppenId, type)}
</div>
</div>`;
@@ -814,6 +814,7 @@
function renderAufgabe(a, type, gruppenId) {
_itemData[a.aufgabeId] = { ...a, _kind: 'aufgabe', _gruppenId: gruppenId };
const badges = [];
(a.benoetigteToys || []).forEach(t => badges.push(`<span class="item-detail-chip item-detail-chip-toy">${esc(t.name || t)}</span>`));
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>`);
@@ -826,6 +827,7 @@
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-copy" onclick="duplicateItem('aufgabe','${esc(a.aufgabeId)}','${esc(gruppenId)}',event)">⧉ Duplizieren</button>
<button class="btn-item-delete" onclick="deleteItem('aufgabe','${esc(a.aufgabeId)}','${esc(gruppenId)}',event)">✕ Löschen</button>
</div>` : '';
@@ -841,6 +843,7 @@
function renderStrafe(s, type, gruppenId) {
_itemData[s.strafeId] = { ...s, _kind: 'strafe', _gruppenId: gruppenId };
const badges = [];
(s.benoetigteToys || []).forEach(t => badges.push(`<span class="item-detail-chip item-detail-chip-toy">${esc(t.name || t)}</span>`));
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>`);
@@ -853,6 +856,7 @@
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-copy" onclick="duplicateItem('strafe','${esc(s.strafeId)}','${esc(gruppenId)}',event)">⧉ Duplizieren</button>
<button class="btn-item-delete" onclick="deleteItem('strafe','${esc(s.strafeId)}','${esc(gruppenId)}',event)">✕ Löschen</button>
</div>` : '';
@@ -868,8 +872,10 @@
function renderZeitstrafe(z, type, gruppenId) {
_itemData[z.sperreId] = { ...z, _kind: 'zeitstrafe', _gruppenId: gruppenId };
const badges = [];
(z.benoetigteToys || []).forEach(t => badges.push(`<span class="item-detail-chip item-detail-chip-toy">${esc(t.name || t)}</span>`));
const zeit = formatMin(z.minutenVon, z.minutenBis);
if (zeit) badges.push(`<span class="badge badge-neutral">${esc(zeit)}</span>`);
if (z.level != null) badges.push(`<span class="badge">Level ${esc(String(z.level))}</span>`);
const detailRows = [];
if (z.text) detailRows.push(`<div class="item-detail-text">${esc(z.text)}</div>`);
@@ -879,6 +885,7 @@
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-copy" onclick="duplicateItem('zeitstrafe','${esc(z.sperreId)}','${esc(gruppenId)}',event)">⧉ Duplizieren</button>
<button class="btn-item-delete" onclick="deleteItem('zeitstrafe','${esc(z.sperreId)}','${esc(gruppenId)}',event)">✕ Löschen</button>
</div>` : '';
@@ -896,6 +903,7 @@
function renderFinisher(f, type, gruppenId) {
_itemData[f.finisherId] = { ...f, _kind: 'finisher', _gruppenId: gruppenId };
const badges = [];
(f.benoetigteToys || []).forEach(t => badges.push(`<span class="item-detail-chip item-detail-chip-toy">${esc(t.name || t)}</span>`));
if (f.geschlecht) badges.push(`<span class="badge badge-neutral">${esc(GESCHLECHT_LABEL[f.geschlecht] || f.geschlecht)}</span>`);
const detailRows = [];
@@ -906,6 +914,7 @@
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-copy" onclick="duplicateItem('finisher','${esc(f.finisherId)}','${esc(gruppenId)}',event)">⧉ Duplizieren</button>
<button class="btn-item-delete" onclick="deleteItem('finisher','${esc(f.finisherId)}','${esc(gruppenId)}',event)">✕ Löschen</button>
</div>` : '';
@@ -944,10 +953,36 @@
finisher: apiUrl('/finisher')
};
const ITEM_DELETE_FIELD = { aufgabe: 'aufgabeId', strafe: 'strafeId', zeitstrafe: 'sperreId', finisher: 'finisherId' };
const ITEM_COPY_URL = {
aufgabe: apiUrl('/aufgabe/copy'),
strafe: '/strafe/copy',
zeitstrafe: '/sperre/copy',
finisher: apiUrl('/finisher/copy')
};
function duplicateItem(kind, itemId, gruppenId, event) {
event.stopPropagation();
const copyUrl = ITEM_COPY_URL[kind];
if (!copyUrl) return;
fetch(`${copyUrl}/${itemId}`, { method: 'POST' }).then(r => {
if (r.ok) {
pendingExpandId = gruppenId;
pendingExpandType = 'user';
_notifyOnLoad = true; loadUserGruppen();
} else {
document.getElementById('userActionError').textContent = 'Fehler beim Duplizieren (HTTP ' + r.status + ').';
}
}).catch(() => {
document.getElementById('userActionError').textContent = 'Verbindungsfehler.';
});
}
function deleteItem(kind, itemId, gruppenId, event) {
event.stopPropagation();
if (!confirm('Eintrag wirklich löschen?')) return;
openConfirmModal('Eintrag wirklich löschen?', () => _doDeleteItem(kind, itemId, gruppenId));
}
function _doDeleteItem(kind, itemId, gruppenId) {
const deleteUrl = ITEM_DELETE_URL[kind];
if (!deleteUrl) return;
const body = { [ITEM_DELETE_FIELD[kind]]: itemId };
@@ -1430,7 +1465,7 @@
const lbl = document.querySelector(`#iSperreFuer input[value="${v}"]`)?.closest('label');
if (lbl) lbl.style.display = isChastity ? 'none' : '';
});
document.getElementById('iTempUnlockRow').style.display = (isZeit && isChastity) ? 'block' : 'none';
document.getElementById('iTempUnlockRow').style.display = ((isZeit || isFinisher) && isChastity) ? 'block' : 'none';
document.getElementById('iReleaseTextRow').style.display = isZeit ? 'block' : 'none';
}
@@ -1449,8 +1484,7 @@
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);
document.getElementById('iTempUnlockBefore').checked = false;
document.getElementById('iTempUnlockAfter').checked = false;
document.getElementById('iTempUnlockRequired').checked = false;
_selectedToys = [];
renderSelectedToys();
document.getElementById('itemModalError').style.display = 'none';
@@ -1495,6 +1529,9 @@
const rb = document.querySelector(`#iGeschlecht input[value="${d.geschlecht}"]`);
if (rb) rb.checked = true;
}
if (_isChastityMode) {
document.getElementById('iTempUnlockRequired').checked = d.tempUnlockRequired === true;
}
} else {
document.getElementById('iMinVon').value = d.minutenVon != null ? d.minutenVon : '';
document.getElementById('iMinBis').value = d.minutenBis != null ? d.minutenBis : '';
@@ -1502,8 +1539,7 @@
(d.sperreFuer || []).forEach(w => { const cb = document.querySelector(`#iSperreFuer input[value="${w}"]`); if (cb) cb.checked = true; });
if (_isChastityMode) {
document.getElementById('iLevel').value = d.level != null ? d.level : '';
document.getElementById('iTempUnlockBefore').checked = d.tempUnlockBeforeRequired === true;
document.getElementById('iTempUnlockAfter').checked = d.tempUnlockAfterRequired === true;
document.getElementById('iTempUnlockRequired').checked = d.tempUnlockRequired === true;
}
}
@@ -1666,11 +1702,12 @@
if (!_isChastityMode && !geschlecht) { showItemError('Bitte ein Geschlecht auswählen.'); return; }
payload = {
kurzText, text,
geschlecht: geschlecht || null,
gruppeId: isEdit ? undefined : currentItemGruppeId,
benoetigtAktiv: _isChastityMode ? [] : checkedValues('iWerkzeugFinisherAktiv'),
benoetigtPassiv: _isChastityMode ? [] : checkedValues('iWerkzeugFinisherPassiv'),
benoetigteToys: _selectedToys.map(t => ({ toyId: t.toyId }))
geschlecht: geschlecht || null,
gruppeId: isEdit ? undefined : currentItemGruppeId,
benoetigtAktiv: _isChastityMode ? [] : checkedValues('iWerkzeugFinisherAktiv'),
benoetigtPassiv: _isChastityMode ? [] : checkedValues('iWerkzeugFinisherPassiv'),
benoetigteToys: _selectedToys.map(t => ({ toyId: t.toyId })),
tempUnlockRequired: _isChastityMode ? document.getElementById('iTempUnlockRequired').checked : null
};
url = isEdit ? apiUrl(`/finisher/${currentItemEditId}`) : apiUrl('/finisher');
method = isEdit ? 'PUT' : 'POST';
@@ -1698,8 +1735,7 @@
releaseText: document.getElementById('iReleaseText').value.trim() || null,
sperreFuer,
level: zeitLevel,
tempUnlockBeforeRequired: _isChastityMode ? document.getElementById('iTempUnlockBefore').checked : null,
tempUnlockAfterRequired: _isChastityMode ? document.getElementById('iTempUnlockAfter').checked : null,
tempUnlockRequired: _isChastityMode ? document.getElementById('iTempUnlockRequired').checked : null,
benoetigteToys: _selectedToys.map(t => ({ toyId: t.toyId }))
};
url = isEdit ? `/sperre/${currentItemEditId}` : '/sperre'; // BDSM-only