-
-
-
✕
+
-
`;
- const containerId = currentModalType() === 'CARDLOCK' ? 'modalCardTaskList' : 'modalTaskList';
- document.getElementById(containerId).appendChild(div);
- updateTaskModeVisibility();
+
+
+ Beschreibung (optional)
+
+
+
+ Dauer (Minuten, optional)
+
+
+
`;
+ document.getElementById('taskSetTaskList').appendChild(div);
+ _taskSetIsDirty = true;
}
- function removeTask(id) { document.getElementById('mt-'+id)?.remove(); updateTaskModeVisibility(); }
- function updateTaskModeVisibility() {
- const type = currentModalType();
- if (type === 'CARDLOCK') {
- const hasCardTasks = document.querySelectorAll('#modalCardTaskList .task-item').length > 0;
- document.getElementById('sectionCardTaskMode').style.display = hasCardTasks ? '' : 'none';
- }
- // For TimeLock: sectionTaskMode is always visible when taskTimingFields is open
- }
- function collectTasks() {
- return Array.from(document.querySelectorAll('.task-item')).map(item => {
- const id = item.id.replace('mt-','');
- const title = document.getElementById('mt-title-'+id)?.value.trim();
- const desc = document.getElementById('mt-desc-' +id)?.value.trim();
- const mins = parseInt(document.getElementById('mt-min-' +id)?.value);
+
+ function removeTaskSetTask(id) { document.getElementById('ts-'+id)?.remove(); _taskSetIsDirty = true; }
+
+ function collectTaskSetTasks() {
+ return Array.from(document.querySelectorAll('#taskSetTaskList .task-acc-item')).map(item => {
+ const id = item.id.replace('ts-','');
+ const title = document.getElementById('ts-title-'+id)?.value.trim();
+ const desc = document.getElementById('ts-desc-' +id)?.value.trim();
+ const mins = parseInt(document.getElementById('ts-min-' +id)?.value);
return title ? { title, description: desc||null, minutes: isNaN(mins)?null:mins } : null;
}).filter(Boolean);
}
+ async function saveTaskSet() {
+ const name = document.getElementById('fTaskSetName').value.trim();
+ const errEl = document.getElementById('taskSetError');
+ if (!name) { errEl.textContent = 'Name ist ein Pflichtfeld.'; errEl.style.display = ''; return; }
+ const tasks = collectTaskSetTasks();
+ const url = _taskSetEditId ? `/chastity/task-sets/${_taskSetEditId}` : '/chastity/task-sets';
+ const method = _taskSetEditId ? 'PUT' : 'POST';
+ try {
+ const res = await fetch(url, { method, headers:{'Content-Type':'application/json'}, body:JSON.stringify({name, tasks}) });
+ if (!res.ok) { errEl.textContent = 'Fehler beim Speichern.'; errEl.style.display = ''; return; }
+ const saved = await res.json();
+ const caller = _taskSetCallerType;
+ closeTaskSetModal();
+ await loadTaskSets();
+ if (caller) {
+ const sel = document.getElementById(caller === 'card' ? 'fCardTaskSetId' : 'fTimelockTaskSetId');
+ if (sel) { sel.value = saved.id; onTaskSetChange(caller); markDirty(); }
+ }
+ } catch(e) { errEl.textContent = 'Netzwerkfehler.'; errEl.style.display = ''; }
+ }
+
+ async function deleteTaskSet(id, name) {
+ if (!confirm(`Aufgaben-Set „${name}" wirklich löschen?`)) return;
+ const res = await fetch(`/chastity/task-sets/${id}`, { method:'DELETE' });
+ if (res.ok || res.status === 204) await loadTaskSets();
+ }
+
+ const GS_TOOLS = [
+ { value: 'UMSCHNALLDILDO', label: 'Strap-on' },
+ { value: 'MUND', label: 'Oral' },
+ { value: 'ANUS', label: 'Anal' },
+ ];
+ function gsGetChecked(prefix) {
+ return GS_TOOLS.filter(t => document.getElementById(prefix + t.value)?.checked).map(t => t.value);
+ }
+ function gsSetChecked(prefix, values) {
+ GS_TOOLS.forEach(t => {
+ const el = document.getElementById(prefix + t.value);
+ if (el) el.checked = (values || []).includes(t.value);
+ });
+ }
+
+ const GAME_SPIELDAUER = [
+ { label: 'Sehr kurz' },
+ { label: 'Kurz' },
+ { label: 'Mittel' },
+ { label: 'Lang' },
+ { label: 'Sehr lang' },
+ ];
+
+ function populateGameSetSelect() {
+ const sel = document.getElementById('fGameSetId');
+ if (!sel) return;
+ const cur = sel.value;
+ sel.innerHTML = '
Kein Spiel-Set ';
+ _gameSets.forEach(s => {
+ const opt = document.createElement('option');
+ opt.value = s.id; opt.textContent = s.name;
+ sel.appendChild(opt);
+ });
+ sel.value = cur;
+ }
+
+ function onGameSetChange() {
+ const val = document.getElementById('fGameSetId')?.value;
+ document.getElementById('gameSetSpieldauerRow').style.display = val ? '' : 'none';
+ }
+
+ function updateGameSpieldauer(val) {
+ document.getElementById('valGameSpieldauer').textContent = GAME_SPIELDAUER[val]?.label || '';
+ }
+
+ function populateTaskSetSelects() {
+ for (const selId of ['fCardTaskSetId', 'fTimelockTaskSetId']) {
+ const sel = document.getElementById(selId);
+ if (!sel) continue;
+ const cur = sel.value;
+ sel.innerHTML = '
Kein Aufgaben-Set ';
+ _taskSets.forEach(s => {
+ const opt = document.createElement('option');
+ opt.value = s.id; opt.textContent = `${s.name} (${s.tasks.length} Aufgabe${s.tasks.length !== 1 ? 'n' : ''})`;
+ sel.appendChild(opt);
+ });
+ sel.value = cur;
+ }
+ }
+
+ function onTaskSetChange(type) {
+ const selId = type === 'card' ? 'fCardTaskSetId' : 'fTimelockTaskSetId';
+ const previewId = type === 'card' ? 'cardTaskSetPreview' : 'timelockTaskSetPreview';
+ const val = document.getElementById(selId)?.value;
+ const preview = document.getElementById(previewId);
+ if (!preview) return;
+ if (!val) { preview.style.display = 'none'; preview.innerHTML = ''; return; }
+ const set = _taskSets.find(s => s.id === val);
+ if (!set || !set.tasks.length) { preview.style.display = 'none'; preview.innerHTML = ''; return; }
+ preview.style.display = '';
+ preview.innerHTML = set.tasks.map(t => `
+
+
${esc(t.title)}
+ ${t.minutes ? `
${t.minutes} Min. ` : ''}
+ ${t.description ? `
${esc(t.description)}
` : ''}
+
`).join('');
+ }
+
+ // ── Simulation ──
+ async function runSimulation() {
+ const cardCountsMin = {}, cardCountsMax = {};
+ CARD_DEFS.forEach(c => {
+ const mn = parseInt(document.getElementById('min_' + c.id)?.value) || 0;
+ const mx = parseInt(document.getElementById('max_' + c.id)?.value) || 0;
+ if (mn > 0) cardCountsMin[c.id] = mn;
+ if (mx > 0) cardCountsMax[c.id] = mx;
+ });
+
+ const btn = document.getElementById('simBtn');
+ btn.disabled = true;
+ document.getElementById('simRunning').style.display = '';
+ document.getElementById('simResult').style.display = 'none';
+ document.getElementById('simProgressBar').style.width = '0%';
+ document.getElementById('simProgressText').textContent = '0 von 100';
+
+ try {
+ const res = await fetch('/cardlock/templates/simulate', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ cardCountsMin,
+ cardCountsMax,
+ pickEveryMinute: tpToMinutes('pe'),
+ accumulatePicks: document.getElementById('fAccumulate').checked
+ })
+ });
+ if (!res.ok) return;
+
+ const reader = res.body.getReader();
+ const decoder = new TextDecoder();
+ let buffer = '';
+
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+ buffer += decoder.decode(value, { stream: true });
+ let pos;
+ while ((pos = buffer.indexOf('\n\n')) !== -1) {
+ const chunk = buffer.slice(0, pos);
+ buffer = buffer.slice(pos + 2);
+ let eventName = '', data = '';
+ for (const line of chunk.split('\n')) {
+ if (line.startsWith('event:')) eventName = line.slice(6).trim();
+ else if (line.startsWith('data:')) data = line.slice(5).trim();
+ }
+ if (eventName === 'progress') {
+ const p = JSON.parse(data);
+ document.getElementById('simProgressBar').style.width = (p.done / p.total * 100) + '%';
+ document.getElementById('simProgressText').textContent = `${p.done} von ${p.total}`;
+ } else if (eventName === 'result') {
+ const r = JSON.parse(data);
+ document.getElementById('simMin').textContent = fmtMinutes(r.min);
+ document.getElementById('simAvg').textContent = fmtMinutes(r.avg);
+ document.getElementById('simMax').textContent = fmtMinutes(r.max);
+ document.getElementById('simRunning').style.display = 'none';
+ document.getElementById('simResult').style.display = '';
+ }
+ }
+ }
+ } finally {
+ btn.disabled = false;
+ }
+ }
+
// ── Fehler ──
function clearErr(rowId) { const r = document.getElementById(rowId); r?.classList.remove('field-error'); r?.querySelector('.field-error-msg')?.remove(); }
function setErr(rowId, msg) {
@@ -826,8 +1386,8 @@
function alignModalToContent() {
const rect = document.querySelector('.content')?.getBoundingClientRect();
if (!rect) return;
- const box = document.querySelector('.modal-box');
- box.style.width = Math.min(rect.width, 720) + 'px';
+ document.getElementById('modalBackdrop').querySelector('.modal-box').style.width = Math.min(rect.width, 720) + 'px';
+ document.getElementById('taskSetModalBackdrop').querySelector('.modal-box').style.width = Math.min(rect.width, 900) + 'px';
}
function openModal(template) {
@@ -837,11 +1397,10 @@
document.getElementById('modalTitle').textContent = editId ? 'Vorlage bearbeiten' : 'Vorlage erstellen';
document.getElementById('modalError').style.display = 'none';
document.getElementById('modalSaveBtn').disabled = false;
- document.getElementById('modalTaskList').innerHTML = '';
document.getElementById('fSpinToggle').checked = false;
toggleWheel(false);
document.getElementById('errGreen').style.display = 'none';
- taskCtr = 0; wheelCtr = 0;
+ wheelCtr = 0;
// Typ-Auswahl: nur beim Erstellen sichtbar
document.getElementById('sectionTypeSelect').style.display = editId ? 'none' : '';
@@ -903,13 +1462,27 @@
toggleHygiene(hygieneOn);
if (hygieneOn) { tpFromMinutes('he', template.hygineOpeningEveryMinites); tpFromMinutes('hd', template.hygineOpeningDurationMinutes||30); }
- // Aufgaben
- (template?.tasks||[]).forEach(t => addTask(t));
+ // Task mode
const mode = template?.taskMode || template?.taskCardMode || 'RANDOM';
const radioName = type === 'CARDLOCK' ? 'modalCardTaskMode' : 'modalTaskMode';
const radioEl = document.querySelector(`input[name="${radioName}"][value="${mode}"]`);
if (radioEl) radioEl.checked = true;
- updateTaskModeVisibility();
+
+ // Aufgaben-Set
+ populateTaskSetSelects();
+ const taskSetId = template?.taskSetId || '';
+ document.getElementById('fCardTaskSetId').value = taskSetId;
+ document.getElementById('fTimelockTaskSetId').value = taskSetId;
+ onTaskSetChange('card');
+ onTaskSetChange('timelock');
+
+ // Spiel-Set
+ populateGameSetSelect();
+ document.getElementById('fGameSetId').value = template?.gameSetId || '';
+ onGameSetChange();
+ const sdIdx = template?.gameSpieldauerIdx ?? 2;
+ document.getElementById('sldGameSpieldauer').value = sdIdx;
+ updateGameSpieldauer(sdIdx);
alignModalToContent();
document.getElementById('modalBackdrop').classList.add('open');
@@ -953,9 +1526,17 @@
document.getElementById('modalBackdrop').addEventListener('click', e => { if (e.target===e.currentTarget) tryCloseModal(); });
document.addEventListener('keydown', e => {
- if (e.key === 'Escape' && document.getElementById('modalBackdrop').classList.contains('open')) {
- e.preventDefault();
- tryCloseModal();
+ if (e.key !== 'Escape') return;
+ if (document.getElementById('gsItemModal').classList.contains('open')) {
+ e.preventDefault(); closeGsItemModal();
+ } else if (document.getElementById('gsSetModal').classList.contains('open')) {
+ e.preventDefault(); closeGsSetModal();
+ } else if (document.getElementById('gsEditModal').classList.contains('open')) {
+ e.preventDefault(); closeGsEditModal();
+ } else if (document.getElementById('taskSetModalBackdrop').classList.contains('open')) {
+ e.preventDefault(); tryCloseTaskSetModal();
+ } else if (document.getElementById('modalBackdrop').classList.contains('open')) {
+ e.preventDefault(); tryCloseModal();
}
});
window.addEventListener('resize', () => { if (document.getElementById('modalBackdrop').classList.contains('open')) alignModalToContent(); });
@@ -972,7 +1553,6 @@
if (!name) { setErr('rowName','Name ist ein Pflichtfeld.'); firstError = document.getElementById('rowName'); }
else clearErr('rowName');
- const tasks = collectTasks();
const hygieneOn = document.getElementById('fHygieneToggle').checked;
const hygieneEvery = hygieneOn ? tpToMinutes('he') : null;
const hygieneDur = hygieneOn ? tpToMinutes('hd') : null;
@@ -1007,7 +1587,9 @@
const totalMax = CARD_DEFS.reduce((s,c)=>s+(parseInt(document.getElementById('max_'+c.id).value)||0),0);
if (totalMax===0) { showModalError('Das Deck muss mindestens eine Karte enthalten.'); firstError=firstError||document.getElementById('modalError'); }
const hasTaskCards = (parseInt(document.getElementById('min_TASK').value)||0)>0 || (parseInt(document.getElementById('max_TASK').value)||0)>0;
- if (hasTaskCards && tasks.length===0) { showModalError('Aufgaben-Karten konfiguriert, aber keine Aufgaben definiert.'); firstError=firstError||document.getElementById('modalError'); }
+ if (hasTaskCards && !document.getElementById('fCardTaskSetId').value) { showModalError('Aufgaben-Karten konfiguriert, aber kein Aufgaben-Set ausgewählt.'); firstError=firstError||document.getElementById('modalError'); }
+ const hasGameCards = (parseInt(document.getElementById('min_GAME_CARD').value)||0)>0 || (parseInt(document.getElementById('max_GAME_CARD').value)||0)>0;
+ if (hasGameCards && !document.getElementById('fGameSetId').value) { showModalError('Spiel-Karten konfiguriert, aber kein Spiel-Set ausgewählt.'); firstError=firstError||document.getElementById('modalError'); }
if (firstError) { firstError.scrollIntoView({behavior:'smooth',block:'center'}); return; }
@@ -1024,8 +1606,11 @@
showRemainingCards: document.getElementById('fShowRemaining').checked,
hygineOpeningEveryMinites: hygieneEvery,
hygineOpeningDurationMinutes: hygieneDur,
- tasks, requiresVerification: document.getElementById('fRequiresVerification').checked,
+ taskSetId: document.getElementById('fCardTaskSetId').value || null,
+ requiresVerification: document.getElementById('fRequiresVerification').checked,
taskMode: document.querySelector('input[name="modalCardTaskMode"]:checked')?.value||'RANDOM',
+ gameSetId: document.getElementById('fGameSetId').value || null,
+ gameSpieldauerIdx: parseInt(document.getElementById('sldGameSpieldauer').value) || 2,
};
} else {
// TimeLock
@@ -1038,7 +1623,7 @@
if (hasTaskTiming) {
taskEvery = tpToMinutes('te');
if (taskEvery < 1) { showModalError('Aufgaben-Intervall muss mindestens 1 Minute betragen.'); firstError=firstError||document.getElementById('modalError'); }
- if (tasks.length === 0) { showModalError('Aufgaben-Timing aktiviert, aber keine Aufgaben definiert.'); firstError=firstError||document.getElementById('modalError'); }
+ if (!document.getElementById('fTimelockTaskSetId').value) { showModalError('Aufgaben-Timing aktiviert, aber kein Aufgaben-Set ausgewählt.'); firstError=firstError||document.getElementById('modalError'); }
const mt = parseInt(document.getElementById('fMinTasks').value);
minTasksPerDay = isNaN(mt)||mt<1 ? null : mt;
}
@@ -1100,7 +1685,8 @@
endTimeVisible: document.getElementById('fEndTimeVisible').checked,
hygineOpeningEveryMinites: hygieneEvery,
hygineOpeningDurationMinutes: hygieneDur,
- tasks, taskEveryMinutes: taskEvery, minTasksPerDay,
+ taskSetId: document.getElementById('fTimelockTaskSetId').value || null,
+ taskEveryMinutes: taskEvery, minTasksPerDay,
spinningWheelEntries: wheelEntries, spinsEveryMinutes: spinsEvery, minSpinsPerDay,
requiresVerification: document.getElementById('fRequiresVerification').checked,
taskMode: document.querySelector('input[name="modalTaskMode"]:checked')?.value||'RANDOM',
@@ -1145,7 +1731,8 @@
const hygText = t.hygineOpeningEveryMinites
? `alle ${fmtMinutes(t.hygineOpeningEveryMinites)}, ${fmtMinutes(t.hygineOpeningDurationMinutes)} offen`
: 'Keine';
- const metaLine = `Hygiene: ${hygText} · Verif.: ${t.requiresVerification ? 'Ja' : 'Nein'}${t.taskCount ? ' · ' + t.taskCount + ' Aufgabe(n)' : ''}`;
+ const setName = t.taskSetId ? (_taskSets.find(s => s.id === t.taskSetId)?.name || 'Set') : null;
+ const metaLine = `Hygiene: ${hygText} · Verif.: ${t.requiresVerification ? 'Ja' : 'Nein'}${setName ? ' · Set: ' + esc(setName) : ''}`;
const publishedBadge = t.published
? `
🌐 Veröffentlicht `
: '';
@@ -1194,10 +1781,11 @@
}
}
- function resetList() {
+ async function resetList() {
pageNum = 0; isLastPage = false; isLoading = false;
document.getElementById('templateList').innerHTML = '';
document.getElementById('listEmpty').style.display = 'none';
+ await loadTaskSets();
loadNextPage();
loadSubscribedTemplates();
}
@@ -1296,6 +1884,10 @@
if (res.ok || res.status === 204) resetList();
}
+ document.getElementById('taskSetModalBackdrop').addEventListener('click', e => {
+ if (e.target === e.currentTarget) tryCloseTaskSetModal();
+ });
+
// ── IntersectionObserver für Infinite Scroll ──
const observer = new IntersectionObserver(entries => {
if (entries[0].isIntersecting) loadNextPage();
@@ -1303,6 +1895,426 @@
observer.observe(document.getElementById('scrollSentinel'));
resetList();
+ loadGameSets();
+
+ // ════════════════════════════════════════════════
+ // Spiel-Sets
+ // ════════════════════════════════════════════════
+
+ let _gameSets = [];
+ let _gsEditSetId = null; // set being renamed
+ let _gsSetCaller = null; // 'template' when opened from the template modal
+ let _gsOpenSetId = null; // set currently open in the content popup
+ let _gsItemType = null; // 'aufgabe' | 'zeitstrafe' | 'finisher'
+ let _gsItemSetId = null;
+ let _gsItemIdx = null; // null = new, number = editing
+
+ async function loadGameSets() {
+ try {
+ const res = await fetch('/chastity/game-sets');
+ if (!res.ok) return;
+ _gameSets = await res.json();
+ renderGameSetList();
+ populateGameSetSelect();
+ if (_gsOpenSetId) renderGsEditModalContent(_gsOpenSetId);
+ } catch (e) { console.error(e); }
+ }
+
+ function renderGameSetList() {
+ const list = document.getElementById('gameSetList');
+ list.innerHTML = '';
+ document.getElementById('gameSetEmpty').style.display = _gameSets.length ? 'none' : '';
+ document.getElementById('btnNewGameSet').disabled = _gameSets.length >= 5;
+
+ _gameSets.forEach(s => {
+ const aufgaben = s.aufgaben || [];
+ const zeitstrafen = s.zeitstrafen || [];
+ const finisher = s.finisher || [];
+ const levelCounts = [1,2,3,4,5].map(l => aufgaben.filter(a => a.level === l).length);
+
+ const lvlBadges = levelCounts.map((c, i) => {
+ const cls = c >= 3 ? 'gs-badge gs-badge-neutral' : 'gs-badge';
+ return `
L${i+1}: ${c} `;
+ }).join('');
+ const finBadgeCls = finisher.length >= 1 ? 'gs-badge gs-badge-neutral' : 'gs-badge';
+
+ const card = document.createElement('div');
+ card.className = 'gs-card';
+ card.id = 'gscard_' + s.id;
+ card.addEventListener('click', () => openGsEditModal(s.id));
+ card.innerHTML = `
+ `;
+ list.appendChild(card);
+ });
+ }
+
+ function toggleGsListItem(id) {
+ document.getElementById(id)?.classList.toggle('open');
+ }
+
+ // ── Set content popup ──────────────────────────
+
+ function openGsEditModal(setId) {
+ _gsOpenSetId = setId;
+ renderGsEditModalContent(setId);
+ document.getElementById('gsEditModal').classList.add('open');
+ }
+
+ function closeGsEditModal() {
+ document.getElementById('gsEditModal').classList.remove('open');
+ _gsOpenSetId = null;
+ }
+
+ function renderGsEditModalContent(setId) {
+ const container = document.getElementById('gsEditModalContent');
+ if (!container) return;
+ const s = _gameSets.find(x => x.id === setId);
+ if (!s) { closeGsEditModal(); return; }
+ document.getElementById('gsEditModalTitle').textContent = s.name;
+ const aufgaben = s.aufgaben || [];
+ const zeitstrafen = s.zeitstrafen || [];
+ const finisher = s.finisher || [];
+ let html = '';
+ for (let l = 1; l <= 5; l++) {
+ const items = aufgaben.map((a, i) => ({...a, _gi: i})).filter(a => a.level === l);
+ const warnCls = items.length < 3 ? ' gs-sub-warn' : '';
+ const itemsHtml = items.map(a => gsAufgabeRowHtml(s.id, a._gi, a)).join('') ||
+ '
–
';
+ html += `
`;
+ }
+ const zeitHtml = zeitstrafen.map((z, i) => gsZeitstrafeRowHtml(s.id, i, z)).join('') ||
+ '
–
';
+ html += `
`;
+ const finWarnCls = finisher.length < 1 ? ' gs-sub-warn' : '';
+ const finHtml = finisher.map((f, i) => gsFinisherRowHtml(s.id, i, f)).join('') ||
+ '
–
';
+ html += `
`;
+ container.innerHTML = html;
+ }
+
+ // ── Row HTML helpers ───────────────────────────
+
+ function gsAufgabeRowHtml(setId, gi, a) {
+ const toolLabels = (a.benoetigt || []).map(v => GS_TOOLS.find(t => t.value === v)?.label).filter(Boolean);
+ const badges = [
+ a.minutes ? `
${a.minutes} Min. ` : '',
+ ...toolLabels.map(l => `
${l} `),
+ ].join('');
+ const desc = a.description ? `
${esc(a.description)}
` : '';
+ return `
+
+
${esc(a.title)}
+
${badges}
+
+
${desc}
+
+ ✎ Bearbeiten
+ ⧉ Kopie
+ ✕ Löschen
+
+
`;
+ }
+
+ function gsZeitstrafeRowHtml(setId, idx, z) {
+ const timeStr = (z.minMinutes != null ? z.minMinutes : '?') + '–' + (z.maxMinutes != null ? z.maxMinutes : '?') + ' Min.';
+ const sperrtLabels = (z.sperrt || []).map(v => GS_TOOLS.find(t => t.value === v)?.label).filter(Boolean);
+ const badges = [
+ z.level ? `
L${z.level} ` : '',
+ `
${timeStr} `,
+ ...sperrtLabels.map(l => `
🔒 ${l} `),
+ z.releaseText ? `
📝 Aufhebung ` : '',
+ z.tempUnlockBeforeRequired ? `
🔓 Vorher ` : '',
+ z.tempUnlockAfterRequired ? `
🔓 Nachher ` : '',
+ ].join('');
+ const releaseRow = z.releaseText ? `
Bei Aufhebung:
${esc(z.releaseText)}
` : '';
+ const desc = z.description ? `
${esc(z.description)}
` : '';
+ return `
+
+
${esc(z.title)}
+
${badges}
+
+
${desc}${releaseRow}
+
+ ✎ Bearbeiten
+ ⧉ Kopie
+ ✕ Löschen
+
+
`;
+ }
+
+ function gsFinisherRowHtml(setId, idx, f) {
+ const badges = [
+ f.tempUnlockBeforeRequired ? `
🔓 Vorher ` : '',
+ f.tempUnlockAfterRequired ? `
🔓 Nachher ` : '',
+ ].join('');
+ const desc = f.description ? `
${esc(f.description)}
` : '';
+ return `
+
+
${esc(f.title)}
+
${badges}
+
+
${desc}
+
+ ✎ Bearbeiten
+ ⧉ Kopie
+ ✕ Löschen
+
+
`;
+ }
+
+ // ── Set create / rename modal ──────────────────
+
+ function openGsSetModal(id, caller) {
+ _gsEditSetId = id || null;
+ _gsSetCaller = caller || null;
+ document.getElementById('gsSetModalTitle').textContent = id ? 'Spiel-Set umbenennen' : 'Neues Spiel-Set';
+ document.getElementById('gsSetName').value = id ? (_gameSets.find(s => s.id === id)?.name || '') : '';
+ document.getElementById('gsSetError').style.display = 'none';
+ document.getElementById('gsSetModal').classList.add('open');
+ setTimeout(() => document.getElementById('gsSetName').focus(), 50);
+ }
+
+ function closeGsSetModal() {
+ document.getElementById('gsSetModal').classList.remove('open');
+ _gsEditSetId = _gsSetCaller = null;
+ }
+
+ async function saveGsSet() {
+ const name = document.getElementById('gsSetName').value.trim();
+ const errEl = document.getElementById('gsSetError');
+ if (!name) { errEl.textContent = 'Name ist ein Pflichtfeld.'; errEl.style.display = ''; return; }
+ errEl.style.display = 'none';
+ const set = _gsEditSetId ? _gameSets.find(s => s.id === _gsEditSetId) : null;
+ const url = _gsEditSetId ? `/chastity/game-sets/${_gsEditSetId}` : '/chastity/game-sets';
+ const method = _gsEditSetId ? 'PUT' : 'POST';
+ const body = { name,
+ aufgaben: set?.aufgaben || [],
+ zeitstrafen: set?.zeitstrafen || [],
+ finisher: set?.finisher || [] };
+ try {
+ const res = await fetch(url, { method, headers: {'Content-Type':'application/json'}, body: JSON.stringify(body) });
+ if (res.ok) {
+ const saved = await res.json().catch(() => null);
+ const caller = _gsSetCaller;
+ closeGsSetModal();
+ await loadGameSets();
+ if (caller === 'template' && saved?.id) {
+ document.getElementById('fGameSetId').value = saved.id;
+ onGameSetChange();
+ markDirty();
+ }
+ return;
+ }
+ const b = await res.json().catch(() => ({}));
+ errEl.textContent = b.error || 'Fehler.'; errEl.style.display = '';
+ } catch (e) { errEl.textContent = 'Netzwerkfehler.'; errEl.style.display = ''; }
+ }
+
+ async function deleteGameSet(id, name) {
+ if (!confirm(`Spiel-Set „${name}" wirklich löschen?`)) return;
+ const res = await fetch(`/chastity/game-sets/${id}`, { method: 'DELETE' });
+ if (res.ok || res.status === 204) loadGameSets();
+ }
+
+ // ── Item modal ─────────────────────────────────
+
+ function openGsItemModal(type, setId, itemIdx, contextLevel) {
+ _gsItemType = type;
+ _gsItemSetId = setId;
+ _gsItemIdx = itemIdx !== null && itemIdx !== undefined ? itemIdx : null;
+
+ const titles = { aufgabe: 'Aufgabe', zeitstrafe: 'Zeitstrafe', finisher: 'Finisher' };
+ document.getElementById('gsItemModalTitle').textContent =
+ (_gsItemIdx !== null ? 'Bearbeiten: ' : 'Neu: ') + titles[type];
+
+ // Reset fields
+ document.getElementById('gsItemTitle').value = '';
+ document.getElementById('gsItemDesc').value = '';
+ document.getElementById('gsItemMinutes').value = '';
+ document.getElementById('gsItemMinMin').value = '';
+ document.getElementById('gsItemMaxMin').value = '';
+ document.getElementById('gsItemReleaseText').value = '';
+ document.getElementById('gsItemBefore').checked = false;
+ document.getElementById('gsItemAfter').checked = false;
+ document.getElementById('gsItemAufgabeLevel').value = contextLevel || 1;
+ document.getElementById('gsItemZeitstrafeLevel').value = 1;
+ document.getElementById('gsItemError').style.display = 'none';
+ gsSetChecked('gsItemBen_', []);
+ gsSetChecked('gsItemSperr_', []);
+
+ // Show/hide type-specific rows
+ document.getElementById('gsItemAufgabeRow').style.display = type === 'aufgabe' ? '' : 'none';
+ document.getElementById('gsItemBenoetigtRow').style.display = type === 'aufgabe' ? '' : 'none';
+ document.getElementById('gsItemZeitstrafeRow').style.display = type === 'zeitstrafe' ? '' : 'none';
+ document.getElementById('gsItemSperrtRow').style.display = type === 'zeitstrafe' ? '' : 'none';
+ document.getElementById('gsItemUnlockRow').style.display = (type === 'zeitstrafe' || type === 'finisher') ? '' : 'none';
+
+ // Pre-fill when editing
+ if (_gsItemIdx !== null) {
+ const set = _gameSets.find(s => s.id === setId);
+ if (set) {
+ let item;
+ if (type === 'aufgabe') item = set.aufgaben[_gsItemIdx];
+ if (type === 'zeitstrafe') item = set.zeitstrafen[_gsItemIdx];
+ if (type === 'finisher') item = set.finisher[_gsItemIdx];
+ if (item) {
+ document.getElementById('gsItemTitle').value = item.title || '';
+ document.getElementById('gsItemDesc').value = item.description || '';
+ if (type === 'aufgabe') {
+ document.getElementById('gsItemAufgabeLevel').value = item.level || 1;
+ document.getElementById('gsItemMinutes').value = item.minutes || '';
+ gsSetChecked('gsItemBen_', item.benoetigt || []);
+ }
+ if (type === 'zeitstrafe') {
+ document.getElementById('gsItemZeitstrafeLevel').value = item.level || 1;
+ document.getElementById('gsItemMinMin').value = item.minMinutes ?? '';
+ document.getElementById('gsItemMaxMin').value = item.maxMinutes ?? '';
+ document.getElementById('gsItemReleaseText').value = item.releaseText || '';
+ gsSetChecked('gsItemSperr_', item.sperrt || []);
+ }
+ if (type === 'zeitstrafe' || type === 'finisher') {
+ document.getElementById('gsItemBefore').checked = !!item.tempUnlockBeforeRequired;
+ document.getElementById('gsItemAfter').checked = !!item.tempUnlockAfterRequired;
+ }
+ }
+ }
+ }
+
+ document.getElementById('gsItemModal').classList.add('open');
+ setTimeout(() => document.getElementById('gsItemTitle').focus(), 50);
+ }
+
+ function closeGsItemModal() {
+ document.getElementById('gsItemModal').classList.remove('open');
+ _gsItemType = _gsItemSetId = _gsItemIdx = null;
+ }
+
+ async function saveGsItem() {
+ const title = document.getElementById('gsItemTitle').value.trim();
+ const errEl = document.getElementById('gsItemError');
+ if (!title) { errEl.textContent = 'Titel ist ein Pflichtfeld.'; errEl.style.display = ''; return; }
+ errEl.style.display = 'none';
+
+ const set = _gameSets.find(s => s.id === _gsItemSetId);
+ if (!set) return;
+ const updated = {
+ name: set.name,
+ aufgaben: JSON.parse(JSON.stringify(set.aufgaben || [])),
+ zeitstrafen: JSON.parse(JSON.stringify(set.zeitstrafen || [])),
+ finisher: JSON.parse(JSON.stringify(set.finisher || [])),
+ };
+
+ const desc = document.getElementById('gsItemDesc').value.trim() || null;
+ let item;
+ if (_gsItemType === 'aufgabe') {
+ const min = parseInt(document.getElementById('gsItemMinutes').value);
+ const ben = gsGetChecked('gsItemBen_');
+ item = { title, description: desc,
+ level: parseInt(document.getElementById('gsItemAufgabeLevel').value) || 1,
+ minutes: isNaN(min) ? null : min,
+ benoetigt: ben.length ? ben : null };
+ if (_gsItemIdx !== null) updated.aufgaben[_gsItemIdx] = item;
+ else updated.aufgaben.push(item);
+ } else if (_gsItemType === 'zeitstrafe') {
+ const minMin = parseInt(document.getElementById('gsItemMinMin').value);
+ const maxMin = parseInt(document.getElementById('gsItemMaxMin').value);
+ const sperrt = gsGetChecked('gsItemSperr_');
+ const releaseText = document.getElementById('gsItemReleaseText').value.trim() || null;
+ item = { title, description: desc,
+ level: parseInt(document.getElementById('gsItemZeitstrafeLevel').value) || 1,
+ minMinutes: isNaN(minMin) ? null : minMin,
+ maxMinutes: isNaN(maxMin) ? null : maxMin,
+ releaseText,
+ tempUnlockBeforeRequired: document.getElementById('gsItemBefore').checked,
+ tempUnlockAfterRequired: document.getElementById('gsItemAfter').checked,
+ sperrt: sperrt.length ? sperrt : null };
+ if (_gsItemIdx !== null) updated.zeitstrafen[_gsItemIdx] = item;
+ else updated.zeitstrafen.push(item);
+ } else if (_gsItemType === 'finisher') {
+ item = { title, description: desc,
+ tempUnlockBeforeRequired: document.getElementById('gsItemBefore').checked,
+ tempUnlockAfterRequired: document.getElementById('gsItemAfter').checked };
+ if (_gsItemIdx !== null) updated.finisher[_gsItemIdx] = item;
+ else updated.finisher.push(item);
+ }
+
+ try {
+ const res = await fetch(`/chastity/game-sets/${_gsItemSetId}`, {
+ method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify(updated)
+ });
+ if (res.ok) { closeGsItemModal(); await loadGameSets(); }
+ else { const b = await res.json().catch(()=>({})); errEl.textContent = b.error||'Fehler.'; errEl.style.display = ''; }
+ } catch (e) { errEl.textContent = 'Netzwerkfehler.'; errEl.style.display = ''; }
+ }
+
+ async function deleteGsItem(type, setId, idx) {
+ if (!confirm('Eintrag wirklich löschen?')) return;
+ const set = _gameSets.find(s => s.id === setId);
+ if (!set) return;
+ const updated = {
+ name: set.name,
+ aufgaben: JSON.parse(JSON.stringify(set.aufgaben || [])),
+ zeitstrafen: JSON.parse(JSON.stringify(set.zeitstrafen || [])),
+ finisher: JSON.parse(JSON.stringify(set.finisher || [])),
+ };
+ if (type === 'aufgabe') updated.aufgaben.splice(idx, 1);
+ if (type === 'zeitstrafe') updated.zeitstrafen.splice(idx, 1);
+ if (type === 'finisher') updated.finisher.splice(idx, 1);
+ const res = await fetch(`/chastity/game-sets/${setId}`, {
+ method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify(updated)
+ });
+ if (res.ok) loadGameSets();
+ }
+
+ async function duplicateGsItem(type, setId, idx) {
+ const set = _gameSets.find(s => s.id === setId);
+ if (!set) return;
+ const updated = {
+ name: set.name,
+ aufgaben: JSON.parse(JSON.stringify(set.aufgaben || [])),
+ zeitstrafen: JSON.parse(JSON.stringify(set.zeitstrafen || [])),
+ finisher: JSON.parse(JSON.stringify(set.finisher || [])),
+ };
+ if (type === 'aufgabe') updated.aufgaben.splice(idx + 1, 0, JSON.parse(JSON.stringify(set.aufgaben[idx])));
+ if (type === 'zeitstrafe') updated.zeitstrafen.splice(idx + 1, 0, JSON.parse(JSON.stringify(set.zeitstrafen[idx])));
+ if (type === 'finisher') updated.finisher.splice(idx + 1, 0, JSON.parse(JSON.stringify(set.finisher[idx])));
+ const res = await fetch(`/chastity/game-sets/${setId}`, {
+ method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify(updated)
+ });
+ if (res.ok) loadGameSets();
+ }
+
+ document.getElementById('gsSetModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeGsSetModal(); });
+ document.getElementById('gsItemModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeGsItemModal(); });
+ document.getElementById('gsEditModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeGsEditModal(); });