Files
xxx-sphere-web/bin/main/static/games/chastity/meine-locks.html
Mario c472093f62
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
Weitere Fehler im Chastity ingame game behoben
2026-04-30 22:52:21 +02:00

1729 lines
97 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>Meine Vorlagen xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<link rel="stylesheet" href="/css/time-picker.css">
<style>
/* ── Liste ── */
.template-list { display:flex; flex-direction:column; gap:0.75rem; margin-top:0.5rem; }
.template-card {
background:var(--color-card); border:1px solid var(--color-secondary);
border-radius:10px; padding:1rem;
}
.template-card-header { display:flex; align-items:flex-start; justify-content:space-between; gap:0.75rem; margin-bottom:0.6rem; }
.template-type-icon {
position:relative; width:2.4rem; height:2.4rem; flex-shrink:0;
display:flex; align-items:center; justify-content:center;
}
.template-type-icon .icon-base { font-size:2rem; line-height:1; }
.template-type-icon img.icon-base { width:2rem; height:2rem; object-fit:contain; }
.template-type-icon .icon-lock {
position:absolute; bottom:-2px; right:-4px;
font-size:1.8rem; line-height:1;
filter: drop-shadow(0 0 2px rgba(0,0,0,0.5));
}
.template-name { font-weight:700; font-size:1rem; }
.template-meta { font-size:0.78rem; color:var(--color-muted); margin-top:0.2rem; line-height:1.5; }
.template-actions { display:flex; gap:0.4rem; flex-shrink:0; }
.template-actions button { margin:0; padding:0.3rem 0.75rem; font-size:0.82rem; width:auto; }
.empty-hint { color:var(--color-muted); font-size:0.9rem; margin-top:0.5rem; }
/* ── Modal ── */
.modal-backdrop {
display:none; position:fixed; inset:0;
background:rgba(0,0,0,0.65); z-index:500;
align-items:flex-start; justify-content:center; overflow-y:auto; padding:2rem 1rem;
}
.modal-backdrop.open { display:flex; }
.modal-box {
background:var(--color-card); border:1px solid var(--color-secondary);
border-radius:14px; padding:1.5rem; box-sizing:border-box;
display:flex; flex-direction:column; gap:0;
}
/* ── Formular ── */
.form-section {
background:var(--color-secondary); border-radius:10px;
padding:1rem 1.1rem; margin-bottom:1rem;
}
.form-section-title {
font-size:0.75rem; font-weight:700; color:var(--color-muted);
text-transform:uppercase; letter-spacing:0.07em; margin-bottom:0.85rem;
}
.form-row { display:flex; flex-direction:column; gap:0.3rem; margin-bottom:0.75rem; }
.form-row:last-child { margin-bottom:0; }
.form-row label { font-size:0.88rem; font-weight:600; color:var(--color-text); }
.form-hint { font-size:0.78rem; color:var(--color-muted); margin-top:0.1rem; }
.form-row input[type="text"],
.form-row input[type="number"],
.form-row select {
width:100%; box-sizing:border-box;
padding:0.65rem 0.9rem;
border:1px solid var(--color-secondary);
border-radius:6px;
background:var(--color-secondary);
color:var(--color-text);
font-size:1rem;
outline:none;
appearance:none;
-webkit-appearance:none;
transition:border-color 0.15s;
}
.form-row select {
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%23cccccc' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
background-repeat:no-repeat;
background-position:right 0.9rem center;
padding-right:2.2rem;
cursor:pointer;
border-color:rgba(255,255,255,0.18);
}
.form-row select:focus { border-color:var(--color-primary); }
.form-row select option { background:var(--color-card); }
.checkbox-row {
display:flex; align-items:center; gap:0.6rem;
margin-bottom:0.6rem; cursor:pointer;
}
.checkbox-row:last-child { margin-bottom:0; }
.checkbox-row input[type="checkbox"] { width:1.1rem; height:1.1rem; flex-shrink:0; accent-color:var(--color-primary); }
.checkbox-row label { font-size:0.9rem; color:var(--color-text); cursor:pointer; user-select:none; }
/* ── Karten-Grid ── */
.cards-grid {
display:grid;
grid-template-columns:repeat(auto-fill, minmax(110px, 1fr));
gap:0.5rem; margin-bottom:0.5rem;
}
.card-count-item {
background:var(--color-card); border-radius:8px; padding:0.6rem 0.5rem;
display:flex; flex-direction:column; align-items:center; gap:0.3rem; text-align:center;
}
.card-count-item img {
width:42px; height:auto; border-radius:4px; display:block;
cursor:pointer; transition:transform 0.15s,opacity 0.15s;
}
.card-count-item img:hover { transform:scale(1.07); opacity:0.85; }
.card-count-item label { font-size:0.72rem; font-weight:600; color:var(--color-text); line-height:1.2; }
.card-range-row {
display:flex; align-items:center; gap:0.25rem;
width:100%; justify-content:center;
}
.range-label { font-size:0.68rem; color:var(--color-muted); width:22px; text-align:right; flex-shrink:0; }
/* ── Stepper ── */
.stepper {
display:flex; align-items:center;
border:1px solid var(--color-muted); border-radius:6px; overflow:hidden; height:26px;
}
.stepper button {
width:22px; height:26px; padding:0; margin:0; border:none; border-radius:0;
background:var(--color-secondary); color:var(--color-text); font-size:0.95rem;
cursor:pointer; flex-shrink:0; display:flex; align-items:center; justify-content:center;
}
.stepper button:hover { background:var(--color-primary); color:#fff; }
.stepper input[type="text"] {
width:28px; height:26px; border:none;
border-left:1px solid var(--color-muted); border-right:1px solid var(--color-muted);
border-radius:0; text-align:center; font-size:0.82rem; padding:0;
background:var(--color-secondary); color:var(--color-text); box-sizing:border-box;
}
.stepper input[type="text"]:focus { outline:none; background:rgba(255,255,255,0.08); }
/* ── Aufgaben ── */
.task-list { display:flex; flex-direction:column; gap:0.5rem; margin-bottom:0.6rem; }
.task-item {
display:flex; flex-direction:column;
background:var(--color-card); border-radius:7px; padding:0.6rem 0.75rem;
}
.task-item-row { display:grid; grid-template-columns:1fr 80px auto; gap:0.4rem; align-items:center; }
.task-title-label { font-size:0.73rem; color:var(--color-muted); margin-bottom:0.1rem; }
.task-item input[type="text"] { width:100%; box-sizing:border-box; }
.task-item input[type="number"] { width:100%; box-sizing:border-box; text-align:center; }
.task-item textarea {
resize:vertical; min-height:56px; margin-top:0.4rem; width:100%; box-sizing:border-box;
padding:0.55rem 0.9rem; border:1px solid var(--color-secondary);
border-radius:6px; background:var(--color-secondary);
color:var(--color-text); font-size:0.88rem; font-family:inherit;
outline:none; transition:border-color 0.2s; line-height:1.45;
}
.task-item textarea:focus { border-color:var(--color-primary); }
/* ── Aufgaben-Accordion ── */
.task-acc-item { background:var(--color-card); border-radius:7px; margin-bottom:0.4rem; overflow:hidden; }
.task-acc-header { display:grid; grid-template-columns:1.2rem 1fr auto; align-items:center; gap:0.5rem; padding:0.5rem 0.6rem; cursor:pointer; }
.task-acc-chevron { font-size:0.7rem; color:var(--color-muted); transition:transform 0.15s; user-select:none; }
.task-acc-item.is-open .task-acc-chevron { transform:rotate(90deg); }
.task-acc-title { width:100%; background:none; border:none; border-bottom:1px solid transparent; color:var(--color-text); font-size:0.92rem; padding:0.2rem 0.3rem; outline:none; transition:border-color 0.15s; }
.task-acc-title:focus { border-bottom-color:var(--color-primary); }
.task-acc-body { display:none; padding:0 0.6rem 0.6rem; border-top:1px solid rgba(255,255,255,0.06); }
.task-acc-item.is-open .task-acc-body { display:block; }
.task-acc-body textarea {
resize:vertical; min-height:56px; width:100%; box-sizing:border-box;
padding:0.55rem 0.9rem; border:1px solid var(--color-secondary);
border-radius:6px; background:var(--color-secondary);
color:var(--color-text); font-size:0.88rem; font-family:inherit;
outline:none; transition:border-color 0.2s; line-height:1.45;
}
.task-acc-body textarea:focus { border-color:var(--color-primary); }
/* ── Spinning-Wheel-Einträge ── */
.wheel-list { display:flex; flex-direction:column; gap:0.5rem; margin-bottom:0.6rem; }
.wheel-item {
display:flex; align-items:center; gap:0.5rem;
background:var(--color-card); border-radius:7px; padding:0.55rem 0.75rem;
flex-wrap:wrap;
border-left: 4px solid transparent; transition: border-color 0.15s;
}
.wheel-item select {
flex:1; min-width:150px; box-sizing:border-box;
padding:0.65rem 0.9rem; padding-right:2.2rem;
border:1px solid var(--color-secondary); border-radius:6px;
background:var(--color-secondary); color:var(--color-text);
font-size:1rem; outline:none;
appearance:none; -webkit-appearance:none;
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%23888' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
background-repeat:no-repeat; background-position:right 0.9rem center;
cursor:pointer; transition:border-color 0.15s;
}
.wheel-item select:focus { border-color:var(--color-primary); }
.wheel-item select option { background:var(--color-card); }
.wheel-item input[type="text"] { flex:1; min-width:120px; box-sizing:border-box; }
.btn-remove { background:none; border:none; color:rgba(200,50,50,0.7); cursor:pointer; font-size:0.95rem; padding:0; margin:0; width:auto; }
.btn-remove:hover { color:#e74c3c; background:none; }
.btn-add {
background:none; border:1px dashed var(--color-muted); color:var(--color-muted);
border-radius:7px; padding:0.4rem; width:100%; cursor:pointer; font-size:0.82rem; margin:0;
}
.btn-add:hover { border-color:var(--color-primary); color:var(--color-primary); background:none; }
.inline-row { display:flex; align-items:center; gap:0.75rem; flex-wrap:wrap; }
.inline-row input[type="number"] { width:90px; }
.inline-row label { font-size:0.88rem; color:var(--color-text); margin:0; }
.hygiene-fields { display:none; }
.field-error-msg { font-size:0.78rem; color:#e74c3c; margin-top:0.2rem; }
.error-msg { color:#e74c3c; font-size:0.85rem; margin-top:0.25rem; display:none; }
.required-star { color:#e74c3c; margin-left:0.15em; }
.modal-footer { display:flex; gap:0.6rem; justify-content:flex-end; margin-top:0.5rem; }
.modal-footer button { width:auto; padding:0.55rem 1.25rem; }
/* ── Karten-Info-Dialog ── */
/* ── Aufgaben-Set Preview ── */
.task-set-preview { margin-top:0.6rem; display:none; }
.task-set-preview-item {
background:var(--color-card); border-radius:6px; padding:0.45rem 0.7rem;
margin-bottom:0.35rem; font-size:0.85rem;
}
.task-set-preview-title { font-weight:600; color:var(--color-text); }
.task-set-preview-meta { font-size:0.75rem; color:var(--color-muted); margin-left:0.5rem; }
.task-set-preview-desc { font-size:0.78rem; color:var(--color-muted); margin-top:0.15rem; }
.card-info-dialog { display:none; position:fixed; inset:0; z-index:600; align-items:center; justify-content:center; }
.card-info-dialog.open { display:flex; }
.card-info-overlay { position:absolute; inset:0; background:rgba(0,0,0,0.55); }
.card-info-box {
position:relative; background:var(--color-card);
border:1px solid var(--color-secondary); border-radius:12px;
padding:1.5rem 1.5rem 1.25rem; max-width:300px; width:90%;
display:flex; flex-direction:column; align-items:center; gap:0.75rem; z-index:1;
}
.card-info-box img { width:80px; height:auto; border-radius:6px; }
.card-info-box h3 { margin:0; font-size:1.05rem; }
.card-info-box p { margin:0; font-size:0.88rem; color:var(--color-muted); text-align:center; line-height:1.5; }
.radio-group { display:flex; flex-direction:row; gap:1.5rem; flex-wrap:wrap; align-items:center; }
.radio-group label { display:flex; align-items:center; gap:0.5rem; cursor:pointer; font-size:0.9rem; margin:0; color:var(--color-text); }
.radio-group input[type="radio"] { width:auto; padding:0; margin:0; }
/* ── Spiel-Set Suche ── */
.gs-dropdown {
display:none; position:absolute; top:100%; left:0; right:0; z-index:200;
background:var(--color-card); border:1px solid var(--color-secondary);
border-radius:6px; max-height:200px; overflow-y:auto;
box-shadow:0 4px 14px rgba(0,0,0,0.35); margin-top:2px;
}
.gs-dropdown-item {
padding:0.55rem 0.9rem; cursor:pointer; font-size:0.9rem;
border-bottom:1px solid rgba(255,255,255,0.05);
}
.gs-dropdown-item:last-child { border-bottom:none; }
.gs-dropdown-item:hover { background:rgba(255,255,255,0.07); }
.gs-item-name { font-weight:600; color:var(--color-text); }
.gs-item-desc { font-size:0.78rem; color:var(--color-muted); margin-top:0.1rem; }
.gs-selected {
display:flex; align-items:center; gap:0.6rem;
background:rgba(255,255,255,0.05); border:1px solid var(--color-secondary);
border-radius:6px; padding:0.45rem 0.75rem; margin-top:0.35rem;
font-size:0.88rem; color:var(--color-text);
}
.gs-selected button {
background:none; border:none; color:var(--color-muted);
cursor:pointer; padding:0; margin:0; font-size:1rem; width:auto; line-height:1;
}
.gs-selected button:hover { color:#e74c3c; background:none; }
/* ── Simulation ── */
.sim-bar-track { background:var(--color-secondary); border-radius:6px; height:8px; overflow:hidden; margin:0.5rem 0 0.25rem; }
.sim-bar-fill { height:100%; background:var(--color-primary); border-radius:6px; transition:width 0.1s linear; width:0%; }
.sim-result { display:flex; gap:1rem; flex-wrap:wrap; margin-top:0.65rem; }
.sim-stat { flex:1; min-width:70px; display:flex; flex-direction:column; align-items:center; background:var(--color-secondary); border-radius:8px; padding:0.5rem 0.75rem; }
.sim-stat-val { font-size:1rem; font-weight:700; color:var(--color-text); }
.sim-stat-lbl { font-size:0.7rem; color:var(--color-muted); margin-top:0.15rem; text-transform:uppercase; letter-spacing:0.05em; }
</style>
</head>
<body class="app">
<div class="main">
<div class="content">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1rem;gap:1rem;flex-wrap:wrap;">
<h1 style="margin:0;">Meine Vorlagen</h1>
<button onclick="openModal()" style="width:auto;padding:0.55rem 1.2rem;">+ Vorlage erstellen</button>
</div>
<div class="template-list" id="templateList"></div>
<p class="empty-hint" id="listEmpty" style="display:none;">Noch keine Vorlagen vorhanden.</p>
<div id="scrollSentinel" style="height:1px;"></div>
<div style="display:flex;align-items:center;justify-content:space-between;margin:2rem 0 1rem;gap:1rem;flex-wrap:wrap;">
<h2 style="margin:0;">Aufgaben-Sets</h2>
<button onclick="openTaskSetModal(null)" style="width:auto;padding:0.55rem 1.2rem;">+ Set anlegen</button>
</div>
<div class="template-list" id="taskSetList"></div>
<p id="taskSetEmpty" style="display:none;color:var(--color-muted);font-size:0.9rem;">Noch keine Aufgaben-Sets vorhanden.</p>
<h2 style="margin:2rem 0 1rem;">Abonnierte Vorlagen</h2>
<div class="template-list" id="subscribedList"></div>
<p id="subscribedEmpty" style="display:none;color:var(--color-muted);font-size:0.9rem;">Keine abonnierten Vorlagen vorhanden.</p>
<div id="subscribedSentinel" style="height:1px;"></div>
</div>
</div>
<!-- Veröffentlichen-Modal -->
<div class="modal-backdrop" id="publishModal" onclick="closePublishModal()">
<div class="modal-box" style="max-width:380px;max-height:none;" onclick="event.stopPropagation()">
<h2 style="margin:0 0 1rem;">Vorlage veröffentlichen</h2>
<p style="font-size:0.88rem;color:var(--color-muted);margin:0 0 1rem;line-height:1.5;">
Die Vorlage wird öffentlich sichtbar und kann von anderen Nutzern abonniert werden.
Wenn du die Veröffentlichung entfernst, werden alle Abonnements gelöscht angefertigte Kopien bleiben erhalten.
</p>
<p style="font-size:0.82rem;color:var(--color-muted);margin:0 0 1.25rem;line-height:1.5;">
Ob dein Name angezeigt wird, hängt von deiner Datenschutzeinstellung
<em>„Profil bei Veröffentlichungen sichtbar"</em> ab.
</p>
<div style="display:flex;gap:0.6rem;justify-content:flex-end;">
<button onclick="closePublishModal()" style="background:none;border:1px solid var(--color-secondary);color:var(--color-muted);width:auto;padding:0.5rem 1.1rem;">Abbrechen</button>
<button id="publishConfirmBtn" onclick="confirmPublish()" style="width:auto;padding:0.5rem 1.25rem;">Veröffentlichen</button>
</div>
</div>
</div>
<!-- Modal -->
<div class="modal-backdrop" id="modalBackdrop">
<div class="modal-box" onclick="event.stopPropagation()">
<h2 id="modalTitle" style="margin:0 0 1.25rem;">Vorlage erstellen</h2>
<!-- Typ-Auswahl (nur beim Erstellen) -->
<div class="form-section" id="sectionTypeSelect">
<div class="form-section-title">Lock-Typ</div>
<div class="radio-group">
<label><input type="radio" name="lockType" value="CARDLOCK" checked onchange="onTypeChange()"> 🃏 Karten-Lock</label>
<label><input type="radio" name="lockType" value="TIMELOCK" onchange="onTypeChange()"> ⏱ Zeit-Lock</label>
</div>
</div>
<!-- Grundeinstellungen (immer) -->
<div class="form-section">
<div class="form-section-title">Grundeinstellungen</div>
<div class="form-row" id="rowName">
<label for="fName">Name<span class="required-star">*</span></label>
<input type="text" id="fName" placeholder="z.B. Wochenend-Lock" maxlength="100" oninput="clearErr('rowName')">
</div>
<div class="checkbox-row">
<input type="checkbox" id="fRequiresVerification" checked>
<label for="fRequiresVerification">Verifikation erforderlich
<span class="form-hint">(tägliche Verifikation für Profildarstellung)</span>
</label>
</div>
</div>
<!-- ══ Karten-Lock Bereich ══ -->
<div id="sectionCardlock">
<div class="form-section">
<div class="form-section-title">Karten-Konfiguration</div>
<div class="cards-grid" id="modalCardsGrid"></div>
<div class="field-error-msg" id="errGreen" style="display:none;margin-bottom:0.5rem;">Grüne Karte Min muss mindestens 1 sein.</div>
<div class="form-row" style="margin-top:0.75rem;">
<label>Karte ziehen alle</label>
<div class="time-picker">
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('pe',-1,'d')"></button><input type="text" id="pe_d" value="0" readonly><button type="button" onclick="tpChange('pe',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('pe',-1,'h')"></button><input type="text" id="pe_h" value="01" readonly><button type="button" onclick="tpChange('pe',1,'h')">+</button></div><span class="tp-label">Stunden</span></div>
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('pe',-1,'m')"></button><input type="text" id="pe_m" value="00" readonly><button type="button" onclick="tpChange('pe',1,'m')">+</button></div><span class="tp-label">Minuten</span></div>
</div>
</div>
<div class="checkbox-row">
<input type="checkbox" id="fAccumulate">
<label for="fAccumulate">Picks akkumulieren <span class="form-hint">(nicht genutzte Züge bleiben erhalten)</span></label>
</div>
<div class="checkbox-row">
<input type="checkbox" id="fShowRemaining">
<label for="fShowRemaining">Art der verbleibenden Karten anzeigen</label>
</div>
</div>
<!-- Aufgaben-Set sichtbar wenn TASK > 0 oder GAME_CARD > 0 -->
<div id="sectionAufgabenSet" style="display:none;">
<div class="form-section">
<div class="form-section-title">Aufgaben-Set</div>
<!-- Task-Karte: Entscheider + internes Set -->
<div id="subCardTaskSet" style="display:none;">
<div style="margin-bottom:0.65rem;">
<div style="font-size:0.75rem;font-weight:700;text-transform:uppercase;letter-spacing:0.07em;color:var(--color-muted);margin-bottom:0.45rem;">Wer entscheidet über die Aufgabe?</div>
<div class="radio-group">
<label><input type="radio" name="modalCardTaskMode" value="RANDOM" checked> Zufall</label>
<label><input type="radio" name="modalCardTaskMode" value="KEYHOLDER" > Keyholder*In</label>
<label><input type="radio" name="modalCardTaskMode" value="COMMUNITY" > Community</label>
</div>
</div>
<div class="form-row" style="margin-bottom:0.5rem;">
<label>Aufgaben-Set <span class="required-star">*</span></label>
<select id="fCardTaskSetId" onchange="onTaskSetChange('card')">
<option value="">Kein Aufgaben-Set</option>
</select>
</div>
<button class="btn-add" type="button" onclick="openTaskSetModal(null,'card')">+ Neues Set anlegen</button>
<div id="cardTaskSetPreview" class="task-set-preview"></div>
</div>
<!-- Spiel-Karte: Chastity-Set + Spieldauer -->
<div id="subGameSet" style="display:none;">
<div class="form-row">
<label>Aufgaben-Set (Chastity) <span class="required-star">*</span></label>
<select id="fGameSetId" onchange="markDirty()">
<option value="">Kein Aufgaben-Set</option>
</select>
<div class="field-error-msg" id="errGameSet" style="display:none;">Bitte ein Aufgaben-Set für Spiel-Karten auswählen.</div>
</div>
<div class="form-row" style="margin-bottom:0;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.4rem;">
<label for="sldGameSpieldauer" style="margin:0;">Spieldauer</label>
<span id="valGameSpieldauer" style="font-size:0.85rem;color:var(--color-muted);">Mittel</span>
</div>
<input type="range" id="sldGameSpieldauer" min="0" max="4" value="2"
oninput="updateGameSpieldauer(this.value)"
style="width:100%;accent-color:var(--color-primary);">
</div>
</div>
</div>
</div>
</div>
<!-- ══ Zeit-Lock Bereich ══ -->
<div id="sectionTimelock" style="display:none;">
<!-- Zeit -->
<div class="form-section">
<div class="form-section-title">Zeit-Einstellungen</div>
<div class="form-row">
<label>Mindestdauer (optional)</label>
<div class="time-picker">
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('tmin',-1,'d')"></button><input type="text" id="tmin_d" value="0" readonly><button type="button" onclick="tpChange('tmin',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('tmin',-1,'h')"></button><input type="text" id="tmin_h" value="00" readonly><button type="button" onclick="tpChange('tmin',1,'h')">+</button></div><span class="tp-label">Stunden</span></div>
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('tmin',-1,'m')"></button><input type="text" id="tmin_m" value="00" readonly><button type="button" onclick="tpChange('tmin',1,'m')">+</button></div><span class="tp-label">Minuten</span></div>
</div>
</div>
<div class="form-row" id="rowMaxTime">
<label>Maximaldauer<span class="required-star">*</span></label>
<div class="time-picker">
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('tmax',-1,'d')"></button><input type="text" id="tmax_d" value="0" readonly><button type="button" onclick="tpChange('tmax',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('tmax',-1,'h')"></button><input type="text" id="tmax_h" value="01" readonly><button type="button" onclick="tpChange('tmax',1,'h')">+</button></div><span class="tp-label">Stunden</span></div>
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('tmax',-1,'m')"></button><input type="text" id="tmax_m" value="00" readonly><button type="button" onclick="tpChange('tmax',1,'m')">+</button></div><span class="tp-label">Minuten</span></div>
</div>
</div>
<div class="checkbox-row">
<input type="checkbox" id="fEndTimeVisible">
<label for="fEndTimeVisible">Endzeit für Lockee sichtbar</label>
</div>
</div>
<!-- Glücksrad -->
<div class="form-section">
<div class="form-section-title">Glücksrad (optional)</div>
<div class="checkbox-row">
<input type="checkbox" id="fSpinToggle" onchange="toggleWheel(this.checked)">
<label for="fSpinToggle">Glücksrad aktivieren</label>
</div>
<div id="wheelFields" style="display:none;">
<div class="form-row" style="margin-top:0.5rem;">
<label>Rad drehen alle</label>
<div class="time-picker">
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('se',-1,'d')"></button><input type="text" id="se_d" value="0" readonly><button type="button" onclick="tpChange('se',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('se',-1,'h')"></button><input type="text" id="se_h" value="01" readonly><button type="button" onclick="tpChange('se',1,'h')">+</button></div><span class="tp-label">Stunden</span></div>
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('se',-1,'m')"></button><input type="text" id="se_m" value="00" readonly><button type="button" onclick="tpChange('se',1,'m')">+</button></div><span class="tp-label">Minuten</span></div>
</div>
</div>
<div class="form-row">
<label>Mindestdrehungen pro Tag (optional)</label>
<div class="inline-row"><input type="number" id="fMinSpins" min="1" placeholder=""> <span style="font-size:0.88rem;color:var(--color-text);">pro Tag</span></div>
</div>
<div style="margin-top:0.5rem;">
<div class="wheel-list" id="wheelList"></div>
<button class="btn-add" onclick="addWheelEntry()">+ Eintrag hinzufügen</button>
</div>
</div>
</div>
<!-- Aufgaben-Timing (nur TimeLock) -->
<div class="form-section">
<div class="form-section-title">Aufgaben-Timing (optional)</div>
<div class="checkbox-row">
<input type="checkbox" id="fTaskTimingToggle" onchange="toggleTaskTiming(this.checked)">
<label for="fTaskTimingToggle">Regelmäßige Aufgaben aktivieren</label>
</div>
<div id="taskTimingFields" style="display:none;">
<div class="form-row" style="margin-top:0.5rem;">
<label>Aufgaben alle</label>
<div class="time-picker">
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('te',-1,'d')"></button><input type="text" id="te_d" value="0" readonly><button type="button" onclick="tpChange('te',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('te',-1,'h')"></button><input type="text" id="te_h" value="08" readonly><button type="button" onclick="tpChange('te',1,'h')">+</button></div><span class="tp-label">Stunden</span></div>
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('te',-1,'m')"></button><input type="text" id="te_m" value="00" readonly><button type="button" onclick="tpChange('te',1,'m')">+</button></div><span class="tp-label">Minuten</span></div>
</div>
</div>
<div class="form-row" style="margin-bottom:0;">
<label>Mindestaufgaben pro Tag (optional)</label>
<div class="inline-row"><input type="number" id="fMinTasks" min="1" placeholder=""> <span style="font-size:0.88rem;color:var(--color-text);">pro Tag</span></div>
</div>
<div style="margin-top:0.85rem;">
<div id="sectionTaskMode" style="margin-bottom:0.65rem;">
<div style="font-size:0.75rem;font-weight:700;text-transform:uppercase;letter-spacing:0.07em;color:var(--color-muted);margin-bottom:0.45rem;">Wer entscheidet über die Aufgaben?</div>
<div class="radio-group">
<label><input type="radio" name="modalTaskMode" value="RANDOM" checked> Zufall</label>
<label><input type="radio" name="modalTaskMode" value="KEYHOLDER" > Keyholder*In</label>
<label><input type="radio" name="modalTaskMode" value="COMMUNITY" > Community</label>
</div>
</div>
<div class="form-row" style="margin-bottom:0.5rem;">
<label>Aufgaben-Set <span class="required-star">*</span></label>
<select id="fTimelockTaskSetId" onchange="onTaskSetChange('timelock')">
<option value="">Kein Aufgaben-Set</option>
</select>
</div>
<button class="btn-add" type="button" onclick="openTaskSetModal(null,'timelock')">+ Neues Set anlegen</button>
<div id="timelockTaskSetPreview" class="task-set-preview"></div>
</div>
</div>
</div>
<!-- Strafmaß -->
<div class="form-section">
<div class="form-section-title">Strafmaß bei Pflichtverletzung (optional)</div>
<div class="form-row">
<label>Strafart</label>
<select id="fPenaltyType" onchange="onPenaltyTypeChange()">
<option value="">Kein Strafmaß</option>
<option value="ADD">Zeit hinzufügen</option>
<option value="FREEZE">Einfrieren</option>
<option value="PILLORY">An den Pranger stellen</option>
</select>
</div>
<div class="form-row" id="rowPenaltyValue" style="display:none;">
<label>Dauer</label>
<div class="time-picker">
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('pv',-1,'d')"></button><input type="text" id="pv_d" value="0" readonly><button type="button" onclick="tpChange('pv',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('pv',-1,'h')"></button><input type="text" id="pv_h" value="01" readonly><button type="button" onclick="tpChange('pv',1,'h')">+</button></div><span class="tp-label">Stunden</span></div>
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('pv',-1,'m')"></button><input type="text" id="pv_m" value="00" readonly><button type="button" onclick="tpChange('pv',1,'m')">+</button></div><span class="tp-label">Minuten</span></div>
</div>
</div>
</div>
</div>
<!-- Hygiene (immer) -->
<div class="form-section">
<div class="form-section-title">Hygiene-Öffnungen (optional)</div>
<div class="checkbox-row">
<input type="checkbox" id="fHygieneToggle" onchange="toggleHygiene(this.checked)">
<label for="fHygieneToggle">Regelmäßige Hygiene-Öffnungen aktivieren</label>
</div>
<div class="hygiene-fields" id="hygieneFields">
<div class="form-row" style="margin-top:0.5rem;">
<label>Hygiene-Öffnung alle</label>
<div class="time-picker">
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('he',-1,'d')"></button><input type="text" id="he_d" value="1" readonly><button type="button" onclick="tpChange('he',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('he',-1,'h')"></button><input type="text" id="he_h" value="00" readonly><button type="button" onclick="tpChange('he',1,'h')">+</button></div><span class="tp-label">Stunden</span></div>
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('he',-1,'m')"></button><input type="text" id="he_m" value="00" readonly><button type="button" onclick="tpChange('he',1,'m')">+</button></div><span class="tp-label">Minuten</span></div>
</div>
</div>
<div class="form-row" style="margin-bottom:0;">
<label>Dauer der Öffnung</label>
<div class="time-picker">
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('hd',-1,'d')"></button><input type="text" id="hd_d" value="0" readonly><button type="button" onclick="tpChange('hd',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('hd',-1,'h')"></button><input type="text" id="hd_h" value="00" readonly><button type="button" onclick="tpChange('hd',1,'h')">+</button></div><span class="tp-label">Stunden</span></div>
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('hd',-1,'m')"></button><input type="text" id="hd_m" value="30" readonly><button type="button" onclick="tpChange('hd',1,'m')">+</button></div><span class="tp-label">Minuten</span></div>
</div>
</div>
</div>
</div>
<!-- Simulation (nur Cardlock) -->
<div class="form-section" id="simSection">
<div class="form-section-title">Simulation</div>
<p style="font-size:0.82rem;color:var(--color-muted);margin:0 0 0.75rem;line-height:1.5;">Simuliert 100 Durchläufe mit der aktuellen Konfiguration und zeigt die erwartete Sperrdauer. Die Simulation basiert auf dem Idealfall, dass jede Karte sofort gezogen wird, sobald sie verfügbar ist die tatsächliche Sperrdauer wird in der Praxis höher ausfallen.</p>
<button type="button" id="simBtn" onclick="runSimulation()" style="width:auto;padding:0.45rem 1.1rem;font-size:0.88rem;">▶ Simulieren</button>
<div id="simRunning" style="display:none;margin-top:0.65rem;">
<div style="font-size:0.8rem;color:var(--color-muted);"><span id="simProgressText">0 von 100</span></div>
<div class="sim-bar-track"><div class="sim-bar-fill" id="simProgressBar"></div></div>
</div>
<div id="simResult" style="display:none;">
<div class="sim-result">
<div class="sim-stat"><span class="sim-stat-val" id="simMin"></span><span class="sim-stat-lbl">Minimum</span></div>
<div class="sim-stat"><span class="sim-stat-val" id="simAvg"></span><span class="sim-stat-lbl">Durchschnitt</span></div>
<div class="sim-stat"><span class="sim-stat-val" id="simMax"></span><span class="sim-stat-lbl">Maximum</span></div>
</div>
</div>
</div>
<div class="error-msg" id="modalError"></div>
<div id="modalDiscardConfirm" style="display:none;background:rgba(231,76,60,0.08);border:1px solid rgba(231,76,60,0.3);border-radius:8px;padding:0.6rem 0.9rem;margin-bottom:0.5rem;display:none;align-items:center;justify-content:space-between;gap:0.75rem;flex-wrap:wrap;">
<span style="font-size:0.88rem;color:#e74c3c;">Ungespeicherte Änderungen verwerfen?</span>
<div style="display:flex;gap:0.5rem;">
<button onclick="cancelDiscard()" style="background:none;border:1px solid var(--color-secondary);color:var(--color-muted);padding:0.3rem 0.8rem;font-size:0.82rem;width:auto;">Weiter bearbeiten</button>
<button onclick="closeModal()" style="background:#c0392b;padding:0.3rem 0.8rem;font-size:0.82rem;width:auto;">Verwerfen</button>
</div>
</div>
<div class="modal-footer">
<button id="modalCancelBtn" onclick="tryCloseModal()" style="background:none;border:1px solid var(--color-secondary);color:var(--color-muted);">Abbrechen</button>
<button id="modalSaveBtn" onclick="saveTemplate()">Speichern</button>
</div>
</div>
</div>
<!-- Karten-Info-Dialog -->
<div class="card-info-dialog" id="cardInfoDialog">
<div class="card-info-overlay" onclick="closeCardInfo()"></div>
<div class="card-info-box">
<img id="cardInfoImg" src="" alt="">
<h3 id="cardInfoTitle"></h3>
<p id="cardInfoDesc"></p>
<button style="width:auto;padding:0.45rem 1.4rem;" onclick="closeCardInfo()">Schließen</button>
</div>
</div>
<!-- Aufgaben-Set Modal -->
<div class="modal-backdrop" id="taskSetModalBackdrop">
<div class="modal-box" style="max-width:900px;" onclick="event.stopPropagation()">
<h2 id="taskSetModalTitle" style="margin:0 0 1.25rem;">Aufgaben-Set erstellen</h2>
<div class="form-section">
<div class="form-section-title">Name</div>
<div class="form-row" id="rowTaskSetName">
<input type="text" id="fTaskSetName" placeholder="z.B. Leichte Aufgaben" maxlength="100">
</div>
</div>
<div class="form-section">
<div class="form-section-title">Aufgaben</div>
<div class="task-list" id="taskSetTaskList"></div>
<button class="btn-add" type="button" onclick="addTaskSetTask()">+ Aufgabe hinzufügen</button>
</div>
<div class="error-msg" id="taskSetError"></div>
<div id="taskSetDiscardConfirm" style="display:none;background:rgba(231,76,60,0.08);border:1px solid rgba(231,76,60,0.3);border-radius:8px;padding:0.6rem 0.9rem;margin-bottom:0.5rem;align-items:center;justify-content:space-between;gap:0.75rem;flex-wrap:wrap;">
<span style="font-size:0.88rem;color:#e74c3c;">Ungespeicherte Änderungen verwerfen?</span>
<div style="display:flex;gap:0.5rem;">
<button onclick="cancelTaskSetDiscard()" style="background:none;border:1px solid var(--color-secondary);color:var(--color-muted);padding:0.3rem 0.8rem;font-size:0.82rem;width:auto;">Weiter bearbeiten</button>
<button onclick="closeTaskSetModal()" style="background:#c0392b;padding:0.3rem 0.8rem;font-size:0.82rem;width:auto;">Verwerfen</button>
</div>
</div>
<div class="modal-footer">
<button onclick="tryCloseTaskSetModal()" style="background:none;border:1px solid var(--color-secondary);color:var(--color-muted);">Abbrechen</button>
<button onclick="saveTaskSet()">Speichern</button>
</div>
</div>
</div>
<script src="/js/card-defs.js"></script>
<script src="/js/card-display.js"></script>
<script src="/js/icons.js"></script>
<script src="/js/nav.js"></script>
<script src="/js/social-sidebar.js"></script>
<script src="/js/time-picker.js"></script>
<script>
function esc(s) { const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; }
// ── State ──
let editId = null;
let editType = null; // 'CARDLOCK' | 'TIMELOCK' beim Bearbeiten fest
let isDirty = false;
let wheelCtr = 0;
// ── Aufgaben-Sets ──
let _taskSets = [];
let _taskSetEditId = null;
let _taskSetTaskCtr = 0;
let _taskSetCallerType = null; // 'card' | 'timelock' | null
let _taskSetIsDirty = false;
let pageNum = 0;
let isLastPage = false;
let isLoading = false;
function fmtMinutes(min) {
if (!min) return '';
const d = Math.floor(min/1440), h = Math.floor((min%1440)/60), m = min%60;
return [d&&d+'T', h&&h+'Std', m&&m+'Min'].filter(Boolean).join(' ') || '0Min';
}
// ── Lock-Typ im Modal ──
function currentModalType() {
return editType || document.querySelector('input[name="lockType"]:checked')?.value || 'CARDLOCK';
}
function onTypeChange() {
const type = currentModalType();
document.getElementById('sectionCardlock').style.display = type === 'CARDLOCK' ? '' : 'none';
document.getElementById('sectionTimelock').style.display = type === 'TIMELOCK' ? '' : 'none';
document.getElementById('simSection').style.display = type === 'CARDLOCK' ? '' : 'none';
}
// ── Karten-Grid ──
function renderCardsGrid(cardCountsMin, cardCountsMax, isEdit) {
const grid = document.getElementById('modalCardsGrid');
grid.innerHTML = '';
CARD_DEFS.forEach(c => {
const minVal = cardCountsMin?.[c.id] ?? (isEdit ? 0 : c.defMin);
const maxVal = cardCountsMax?.[c.id] ?? (isEdit ? 0 : c.defMax);
const item = document.createElement('div');
item.className = 'card-count-item';
item.innerHTML = `
<img src="${c.img}" alt="${c.name}" title="Klicken für Details" onclick="openCardInfo('${c.id}')">
<label>${c.name}</label>
<div class="card-range-row"><span class="range-label">Min</span>${stepperHtml('min_'+c.id, minVal)}</div>
<div class="card-range-row"><span class="range-label">Max</span>${stepperHtml('max_'+c.id, maxVal)}</div>`;
grid.appendChild(item);
});
}
function stepperHtml(id, val) {
return `<div class="stepper">
<button type="button" onclick="stepChange('${id}',-1)"></button>
<input type="text" id="${id}" value="${val}" onchange="stepClamp('${id}')">
<button type="button" onclick="stepChange('${id}',1)">+</button>
</div>`;
}
function stepChange(id, delta) {
const el = document.getElementById(id);
const val = Math.max(0, (parseInt(el.value)||0) + delta);
el.value = val; syncMinMax(id, val);
}
function stepClamp(id) {
const el = document.getElementById(id);
const v = parseInt(el.value);
el.value = isNaN(v)||v<0 ? 0 : v; syncMinMax(id, parseInt(el.value));
}
function syncMinMax(id, val) {
if (id.startsWith('min_')) { const mx = document.getElementById('max_'+id.slice(4)); if (mx && val > (parseInt(mx.value)||0)) mx.value = val; }
else if (id.startsWith('max_')) { const mn = document.getElementById('min_'+id.slice(4)); if (mn && val < (parseInt(mn.value)||0)) mn.value = val; }
if (id === 'min_GAME_CARD' || id === 'max_GAME_CARD') {
if (val > 0) {
// Gegenseitiger Ausschluss: Task-Karten nullen
const minT = document.getElementById('min_TASK'); if (minT) minT.value = 0;
const maxT = document.getElementById('max_TASK'); if (maxT) maxT.value = 0;
checkTaskCardSection();
}
checkGameCardSection();
}
if (id === 'min_TASK' || id === 'max_TASK') {
if (val > 0) {
// Gegenseitiger Ausschluss: Game-Karten nullen
const minG = document.getElementById('min_GAME_CARD'); if (minG) minG.value = 0;
const maxG = document.getElementById('max_GAME_CARD'); if (maxG) maxG.value = 0;
checkGameCardSection();
}
checkTaskCardSection();
}
}
// ── Spiel-Karte: Aufgaben-Set + Spieldauer ──
const GAME_SPIELDAUER = [
{ label: 'Sehr kurz' },
{ label: 'Kurz' },
{ label: 'Mittel' },
{ label: 'Lang' },
{ label: 'Sehr lang' },
];
let _gameGroups = [];
async function loadGameGroups() {
try {
const res = await fetch('/lock-game/groups');
if (!res.ok) return;
_gameGroups = await res.json();
populateGameSetSelect();
} catch(e) { console.error(e); }
}
function populateGameSetSelect() {
const sel = document.getElementById('fGameSetId');
if (!sel) return;
const cur = sel.value;
sel.innerHTML = '<option value="">Kein Aufgaben-Set</option>';
_gameGroups.forEach(g => {
const opt = document.createElement('option');
opt.value = g.gruppenId;
opt.textContent = g.name + (g.beschreibung ? ' ' + g.beschreibung : '');
sel.appendChild(opt);
});
sel.value = cur;
}
function checkGameCardSection() {
const minV = parseInt(document.getElementById('min_GAME_CARD')?.value) || 0;
const maxV = parseInt(document.getElementById('max_GAME_CARD')?.value) || 0;
const hasGame = minV > 0 || maxV > 0;
const sub = document.getElementById('subGameSet');
if (sub) sub.style.display = hasGame ? '' : 'none';
checkAufgabenSetSection();
}
function checkTaskCardSection() {
const minV = parseInt(document.getElementById('min_TASK')?.value) || 0;
const maxV = parseInt(document.getElementById('max_TASK')?.value) || 0;
const hasTask = minV > 0 || maxV > 0;
const sub = document.getElementById('subCardTaskSet');
if (sub) sub.style.display = hasTask ? '' : 'none';
checkAufgabenSetSection();
}
function checkAufgabenSetSection() {
const hasTask = (parseInt(document.getElementById('min_TASK')?.value) || 0) > 0
|| (parseInt(document.getElementById('max_TASK')?.value) || 0) > 0;
const hasGame = (parseInt(document.getElementById('min_GAME_CARD')?.value) || 0) > 0
|| (parseInt(document.getElementById('max_GAME_CARD')?.value) || 0) > 0;
const sec = document.getElementById('sectionAufgabenSet');
if (sec) sec.style.display = (hasTask || hasGame) ? '' : 'none';
}
function updateGameSpieldauer(val) {
document.getElementById('valGameSpieldauer').textContent = GAME_SPIELDAUER[+val].label;
}
// ── Karten-Info ──
function openCardInfo(cardId) {
const c = CARD_DEFS.find(x => x.id === cardId); if (!c) return;
document.getElementById('cardInfoImg').src = c.img;
document.getElementById('cardInfoImg').alt = c.name;
document.getElementById('cardInfoTitle').textContent = c.name;
document.getElementById('cardInfoDesc').textContent = c.desc;
document.getElementById('cardInfoDialog').classList.add('open');
}
function closeCardInfo() { document.getElementById('cardInfoDialog').classList.remove('open'); }
// ── Hygiene ──
function toggleHygiene(on) {
document.getElementById('hygieneFields').style.display = on ? 'block' : 'none';
if (!on) { tpFromMinutes('he', 1440); tpFromMinutes('hd', 30); }
}
// ── Spinning Wheel ──
const WHEEL_TYPES = [
{ value:'ADD_TIME', label:'Zeit hinzufügen', hasInt:true, hasStr:false, intLabel:'Minuten' },
{ value:'REMOVE_TIME', label:'Zeit entfernen', hasInt:true, hasStr:false, intLabel:'Minuten' },
{ value:'FREEZE_TIME', label:'Einfrieren für', hasInt:true, hasStr:false, intLabel:'Minuten' },
{ value:'FREEZE', label:'Einfrieren (unlimitiert)', hasInt:false, hasStr:false },
{ value:'UNFREEZE', label:'Auftauen', hasInt:false, hasStr:false },
{ value:'TASK', label:'Aufgabe zuweisen', hasInt:false, hasStr:false },
{ value:'TEXT', label:'Text anzeigen', hasInt:false, hasStr:true, strLabel:'Text' },
];
const WHEEL_TYPE_COLORS = {
ADD_TIME: '#f39c12',
REMOVE_TIME: '#27ae60',
FREEZE_TIME: '#3498db',
FREEZE: '#e74c3c',
UNFREEZE: '#27ae60',
TASK: '#e6b800',
TEXT: '#3498db',
};
function addWheelEntry(data) {
const id = ++wheelCtr;
const type = data?.type || 'ADD_TIME';
const div = document.createElement('div');
div.className = 'wheel-item';
div.id = 'we-' + id;
div.innerHTML = buildWheelItemHtml(id, type, data?.intVal, data?.stringVal);
document.getElementById('wheelList').appendChild(div);
tpFromMinutes('wt' + id, data?.intVal || 60);
updateWheelFields(id);
}
function buildWheelItemHtml(id, type, intVal, stringVal) {
const opts = WHEEL_TYPES.map(t =>
`<option value="${t.value}" ${t.value===type?'selected':''}>${esc(t.label)}</option>`
).join('');
return `<select onchange="updateWheelFields(${id})">${opts}</select>
<div id="we-tp-${id}" style="display:none;">
<div class="time-picker">
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('wt${id}',-1,'d')"></button><input type="text" id="wt${id}_d" value="0" readonly><button type="button" onclick="tpChange('wt${id}',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('wt${id}',-1,'h')"></button><input type="text" id="wt${id}_h" value="01" readonly><button type="button" onclick="tpChange('wt${id}',1,'h')">+</button></div><span class="tp-label">Stunden</span></div>
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('wt${id}',-1,'m')"></button><input type="text" id="wt${id}_m" value="00" readonly><button type="button" onclick="tpChange('wt${id}',1,'m')">+</button></div><span class="tp-label">Minuten</span></div>
</div>
</div>
<input type="text" id="we-str-${id}" placeholder="Text…" maxlength="200"
value="${esc(stringVal||'')}" style="display:none">
<button class="btn-remove" onclick="removeWheelEntry(${id})" title="Entfernen">✕</button>`;
}
function updateWheelFields(id) {
const sel = document.querySelector(`#we-${id} select`);
if (!sel) return;
const def = WHEEL_TYPES.find(t => t.value === sel.value) || WHEEL_TYPES[0];
document.getElementById('we-tp-' + id).style.display = def.hasInt ? '' : 'none';
document.getElementById('we-str-' + id).style.display = def.hasStr ? '' : 'none';
const item = document.getElementById('we-' + id);
if (item) item.style.borderLeftColor = WHEEL_TYPE_COLORS[sel.value] || 'transparent';
}
function removeWheelEntry(id) {
document.getElementById('we-' + id)?.remove();
}
function toggleWheel(on) {
document.getElementById('wheelFields').style.display = on ? '' : 'none';
if (!on) {
tpFromMinutes('se', 60);
document.getElementById('fMinSpins').value = '';
document.getElementById('wheelList').innerHTML = '';
}
}
function collectWheelEntries() {
return Array.from(document.querySelectorAll('#wheelList .wheel-item')).map(item => {
const id = item.id.replace('we-','');
const sel = item.querySelector('select');
const def = WHEEL_TYPES.find(t => t.value === sel?.value);
const d = parseInt(document.getElementById('wt' + id + '_d')?.value) || 0;
const h = parseInt(document.getElementById('wt' + id + '_h')?.value) || 0;
const m = parseInt(document.getElementById('wt' + id + '_m')?.value) || 0;
const minutes = d * 1440 + h * 60 + m;
return {
type: sel?.value,
intVal: def?.hasInt ? (minutes > 0 ? minutes : null) : null,
stringVal: def?.hasStr ? (document.getElementById('we-str-'+id)?.value||null) : null,
};
});
}
// ── Aufgaben-Timing (TimeLock) ──
function toggleTaskTiming(on) {
document.getElementById('taskTimingFields').style.display = on ? '' : 'none';
if (!on) {
tpFromMinutes('te', 480);
document.getElementById('fMinTasks').value = '';
document.getElementById('modalTaskList').innerHTML = '';
}
}
// ── Strafmaß ──
function onPenaltyTypeChange() {
const type = document.getElementById('fPenaltyType').value;
const needsVal = type === 'ADD' || type === 'FREEZE';
document.getElementById('rowPenaltyValue').style.display = needsVal ? '' : 'none';
}
// ── Aufgaben-Sets: Seite ──
async function loadTaskSets() {
try {
const res = await fetch('/chastity/task-sets');
if (!res.ok) return;
_taskSets = await res.json();
renderTaskSetList();
populateTaskSetSelects();
} catch(e) { console.error(e); }
}
function renderTaskSetList() {
const list = document.getElementById('taskSetList');
list.innerHTML = '';
if (!_taskSets.length) { document.getElementById('taskSetEmpty').style.display = ''; return; }
document.getElementById('taskSetEmpty').style.display = 'none';
_taskSets.forEach(s => appendTaskSetCard(s));
}
function appendTaskSetCard(s) {
const list = document.getElementById('taskSetList');
const card = document.createElement('div');
card.className = 'template-card';
card.style.cursor = 'pointer';
const preview = s.tasks.length
? s.tasks.slice(0,3).map(t => esc(t.title)).join(', ') + (s.tasks.length > 3 ? ' …' : '')
: 'Keine Aufgaben';
card.innerHTML = `
<div class="template-card-header">
<div class="template-type-icon">
<span class="icon-base">📋</span>
</div>
<div style="flex:1;min-width:0;">
<div class="template-name">${esc(s.name)}</div>
<div class="template-meta">${s.tasks.length} Aufgabe(n): ${preview}</div>
</div>
<div class="template-actions">
<button onclick="event.stopPropagation();deleteTaskSet('${s.id}','${esc(s.name)}')" style="background:rgba(231,76,60,0.12);border:1px solid rgba(231,76,60,0.35);color:#e74c3c;width:auto;padding:0.35rem 0.75rem;font-size:0.8rem;">✕ Löschen</button>
</div>
</div>`;
card.addEventListener('click', () => openTaskSetModal(s.id));
list.appendChild(card);
}
// ── Aufgaben-Sets: Modal ──
function openTaskSetModal(id, callerType) {
_taskSetEditId = id || null;
_taskSetCallerType = callerType || null;
_taskSetTaskCtr = 0;
document.getElementById('taskSetTaskList').innerHTML = '';
document.getElementById('taskSetError').style.display = 'none';
document.getElementById('taskSetModalTitle').textContent = id ? 'Aufgaben-Set bearbeiten' : 'Aufgaben-Set erstellen';
if (id) {
const set = _taskSets.find(s => s.id === id);
if (set) { document.getElementById('fTaskSetName').value = set.name; (set.tasks||[]).forEach(t => addTaskSetTask(t)); }
} else {
document.getElementById('fTaskSetName').value = '';
}
document.getElementById('taskSetDiscardConfirm').style.display = 'none';
alignModalToContent();
document.getElementById('taskSetModalBackdrop').classList.add('open');
_taskSetIsDirty = false;
setTimeout(() => {
document.getElementById('taskSetModalBackdrop').querySelectorAll('input, textarea, select').forEach(el => {
el.addEventListener('input', () => { _taskSetIsDirty = true; }, { passive: true });
el.addEventListener('change', () => { _taskSetIsDirty = true; }, { passive: true });
});
}, 0);
document.getElementById('fTaskSetName').focus();
}
function tryCloseTaskSetModal() {
if (_taskSetIsDirty) {
const confirm = document.getElementById('taskSetDiscardConfirm');
confirm.style.display = 'flex';
confirm.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
} else {
closeTaskSetModal();
}
}
function cancelTaskSetDiscard() {
document.getElementById('taskSetDiscardConfirm').style.display = 'none';
}
function closeTaskSetModal() {
document.getElementById('taskSetModalBackdrop').classList.remove('open');
document.getElementById('taskSetDiscardConfirm').style.display = 'none';
_taskSetIsDirty = false;
_taskSetEditId = null; _taskSetCallerType = null;
}
function toggleTaskAccItem(id) {
const target = document.getElementById('ts-' + id);
if (!target) return;
const isOpen = target.classList.contains('is-open');
document.querySelectorAll('#taskSetTaskList .task-acc-item').forEach(el => el.classList.remove('is-open'));
if (!isOpen) target.classList.add('is-open');
}
function addTaskSetTask(data) {
const id = ++_taskSetTaskCtr;
const titleVal = (data?.title || '').replace(/"/g, '&quot;');
const descVal = (data?.description || '').replace(/</g, '&lt;').replace(/>/g, '&gt;');
const minVal = data?.minutes != null ? data.minutes : '';
const div = document.createElement('div');
div.className = 'task-acc-item'; div.id = 'ts-' + id;
div.innerHTML = `
<div class="task-acc-header" onclick="toggleTaskAccItem(${id})">
<span class="task-acc-chevron">▶</span>
<input type="text" class="task-acc-title" placeholder="Aufgabentitel…" maxlength="150" id="ts-title-${id}" value="${titleVal}" onclick="event.stopPropagation()">
<button class="btn-remove" onclick="event.stopPropagation();removeTaskSetTask(${id})" title="Entfernen">✕</button>
</div>
<div class="task-acc-body">
<div class="form-row" style="margin-top:0.5rem;">
<label style="font-size:0.78rem;">Beschreibung (optional)</label>
<textarea placeholder="Beschreibung…" maxlength="600" id="ts-desc-${id}">${descVal}</textarea>
</div>
<div class="form-row" style="margin-bottom:0;">
<label style="font-size:0.78rem;">Dauer (Minuten, optional)</label>
<input type="number" min="1" max="9999" placeholder="Min." id="ts-min-${id}" value="${minVal}">
</div>
</div>`;
document.getElementById('taskSetTaskList').appendChild(div);
_taskSetIsDirty = true;
}
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();
}
function populateTaskSetSelects() {
for (const selId of ['fCardTaskSetId', 'fTimelockTaskSetId']) {
const sel = document.getElementById(selId);
if (!sel) continue;
const cur = sel.value;
sel.innerHTML = '<option value="">Kein Aufgaben-Set</option>';
_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 => `
<div class="task-set-preview-item">
<span class="task-set-preview-title">${esc(t.title)}</span>
${t.minutes ? `<span class="task-set-preview-meta">${t.minutes} Min.</span>` : ''}
${t.description ? `<div class="task-set-preview-desc">${esc(t.description)}</div>` : ''}
</div>`).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) {
const r = document.getElementById(rowId); if (!r) return;
r.classList.add('field-error');
let el = r.querySelector('.field-error-msg');
if (!el) { el = document.createElement('div'); el.className='field-error-msg'; r.appendChild(el); }
el.textContent = msg;
}
function showModalError(msg) {
const el = document.getElementById('modalError');
el.textContent = msg; el.style.display = '';
el.scrollIntoView({ behavior:'smooth', block:'center' });
}
// ── Modal öffnen ──
function alignModalToContent() {
const rect = document.querySelector('.content')?.getBoundingClientRect();
if (!rect) return;
document.getElementById('modalBackdrop').querySelector('.modal-box').style.width = Math.min(rect.width, 800) + 'px';
document.getElementById('taskSetModalBackdrop').querySelector('.modal-box').style.width = Math.min(rect.width, 900) + 'px';
}
function openModal(template) {
editId = template?.templateId || null;
editType = template ? (template._type || null) : null;
document.getElementById('modalTitle').textContent = editId ? 'Vorlage bearbeiten' : 'Vorlage erstellen';
document.getElementById('modalError').style.display = 'none';
document.getElementById('modalSaveBtn').disabled = false;
document.getElementById('fSpinToggle').checked = false;
toggleWheel(false);
document.getElementById('errGreen').style.display = 'none';
wheelCtr = 0;
// Typ-Auswahl: nur beim Erstellen sichtbar
document.getElementById('sectionTypeSelect').style.display = editId ? 'none' : '';
// Typ setzen
const type = editType || 'CARDLOCK';
if (!editId) {
document.querySelector(`input[name="lockType"][value="${type}"]`).checked = true;
}
// Grundeinstellungen
document.getElementById('fName').value = template?.name || '';
document.getElementById('fRequiresVerification').checked = template ? template.requiresVerification : true;
// Sektionen ein/ausblenden
document.getElementById('sectionCardlock').style.display = type === 'CARDLOCK' ? '' : 'none';
document.getElementById('sectionTimelock').style.display = type === 'TIMELOCK' ? '' : 'none';
if (type === 'CARDLOCK') {
renderCardsGrid(template?.cardCountsMin||{}, template?.cardCountsMax||{}, !!template);
tpFromMinutes('pe', template?.pickEveryMinute || 60);
document.getElementById('fAccumulate').checked = template?.accumulatePicks || false;
document.getElementById('fShowRemaining').checked = template?.showRemainingCards || false;
// Task-Karte und Spiel-Karte
checkTaskCardSection();
checkGameCardSection();
const gsi = template?.gameSpieldauerIdx ?? 2;
document.getElementById('sldGameSpieldauer').value = gsi;
updateGameSpieldauer(gsi);
populateGameSetSelect();
document.getElementById('fGameSetId').value = template?.gameSetId || '';
}
if (type === 'TIMELOCK') {
tpFromMinutes('tmin', template?.minTimeInMinutes || 0);
tpFromMinutes('tmax', template?.maxTimeInMinutes || 60);
document.getElementById('fEndTimeVisible').checked = template?.endTimeVisible || false;
// Glücksrad
const hasWheelEntries = !!(template?.spinningWheelEntries?.length);
document.getElementById('fSpinToggle').checked = hasWheelEntries;
toggleWheel(hasWheelEntries);
if (hasWheelEntries) {
template.spinningWheelEntries.forEach(e => addWheelEntry(e));
if (template.spinsEveryMinutes) tpFromMinutes('se', template.spinsEveryMinutes);
document.getElementById('fMinSpins').value = template.minSpinsPerDay || '';
}
// Aufgaben-Timing
const hasTaskTiming = !!template?.taskEveryMinutes;
document.getElementById('fTaskTimingToggle').checked = hasTaskTiming;
toggleTaskTiming(hasTaskTiming);
if (hasTaskTiming) {
tpFromMinutes('te', template.taskEveryMinutes);
document.getElementById('fMinTasks').value = template.minTasksPerDay || '';
}
// Strafmaß
document.getElementById('fPenaltyType').value = template?.penaltyType || '';
tpFromMinutes('pv', template?.penaltyValue || 60);
onPenaltyTypeChange();
}
// Hygiene
const hygieneOn = !!(template?.hygineOpeningEveryMinites);
document.getElementById('fHygieneToggle').checked = hygieneOn;
toggleHygiene(hygieneOn);
if (hygieneOn) { tpFromMinutes('he', template.hygineOpeningEveryMinites); tpFromMinutes('hd', template.hygineOpeningDurationMinutes||30); }
// 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;
// Aufgaben-Set
populateTaskSetSelects();
const taskSetId = template?.taskSetId || '';
document.getElementById('fCardTaskSetId').value = taskSetId;
document.getElementById('fTimelockTaskSetId').value = taskSetId;
onTaskSetChange('card');
onTaskSetChange('timelock');
alignModalToContent();
document.getElementById('modalBackdrop').classList.add('open');
document.getElementById('modalDiscardConfirm').style.display = 'none';
// Dirty-Tracking: erst nach dem nächsten Tick starten, damit die Initialisierung nicht feuert
isDirty = false;
setTimeout(() => {
document.getElementById('modalBackdrop').querySelectorAll('input, textarea, select').forEach(el => {
el.addEventListener('input', markDirty, { passive: true });
el.addEventListener('change', markDirty, { passive: true });
});
}, 0);
document.getElementById('fName').focus();
}
function markDirty() { isDirty = true; }
function tryCloseModal() {
if (isDirty) {
const confirm = document.getElementById('modalDiscardConfirm');
confirm.style.display = 'flex';
confirm.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
} else {
closeModal();
}
}
function cancelDiscard() {
document.getElementById('modalDiscardConfirm').style.display = 'none';
}
function closeModal() {
document.getElementById('modalBackdrop').classList.remove('open');
document.getElementById('modalDiscardConfirm').style.display = 'none';
isDirty = false;
editId = null;
editType = null;
}
document.getElementById('modalBackdrop').addEventListener('click', e => { if (e.target===e.currentTarget) tryCloseModal(); });
document.addEventListener('keydown', e => {
if (e.key !== 'Escape') return;
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(); });
// ── Speichern ──
async function saveTemplate() {
try {
document.getElementById('modalError').style.display = 'none';
clearErr('rowName');
const type = currentModalType();
let firstError = null;
const name = document.getElementById('fName').value.trim();
if (!name) { setErr('rowName','Name ist ein Pflichtfeld.'); firstError = document.getElementById('rowName'); }
else clearErr('rowName');
const hygieneOn = document.getElementById('fHygieneToggle').checked;
const hygieneEvery = hygieneOn ? tpToMinutes('he') : null;
const hygieneDur = hygieneOn ? tpToMinutes('hd') : null;
if (hygieneOn && (!hygieneEvery || hygieneEvery < 1)) {
showModalError('Hygiene-Intervall muss mindestens 1 Minute betragen.');
firstError = firstError || document.getElementById('modalError');
}
if (hygieneOn && (!hygieneDur || hygieneDur < 1)) {
showModalError('Dauer der Hygiene-Öffnung muss mindestens 1 Minute betragen.');
firstError = firstError || document.getElementById('modalError');
}
let body;
if (type === 'CARDLOCK') {
// Min > Max
for (const c of CARD_DEFS) {
const mn = parseInt(document.getElementById('min_'+c.id).value)||0;
const mx = parseInt(document.getElementById('max_'+c.id).value)||0;
if (mn > mx) { showModalError(`Min darf nicht größer als Max sein (${c.name}).`); firstError=firstError||document.getElementById('modalError'); break; }
}
const greenMin = parseInt(document.getElementById('min_GREEN').value)||0;
if (greenMin < 1) {
document.getElementById('errGreen').style.display = '';
document.getElementById('min_GREEN').closest('.card-count-item').style.outline = '2px solid #e74c3c';
firstError = firstError || document.getElementById('errGreen');
} else {
document.getElementById('min_GREEN').closest('.card-count-item').style.outline = '';
}
const pickEvery = tpToMinutes('pe');
if (pickEvery < 1) { showModalError('Kartenzieh-Intervall muss mindestens 1 Minute betragen.'); firstError=firstError||document.getElementById('modalError'); }
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 && !document.getElementById('fCardTaskSetId').value) { showModalError('Bitte ein Aufgaben-Set für die Aufgaben-Karten auswählen.'); 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) {
document.getElementById('errGameSet').style.display = '';
showModalError('Spiel-Karten konfiguriert, aber kein Aufgaben-Set ausgewählt.');
firstError = firstError || document.getElementById('errGameSet');
} else {
document.getElementById('errGameSet').style.display = 'none';
}
if (firstError) { firstError.scrollIntoView({behavior:'smooth',block:'center'}); return; }
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;
});
body = {
name, cardCountsMin, cardCountsMax, pickEveryMinute: pickEvery,
accumulatePicks: document.getElementById('fAccumulate').checked,
showRemainingCards: document.getElementById('fShowRemaining').checked,
hygineOpeningEveryMinites: hygieneEvery,
hygineOpeningDurationMinutes: hygieneDur,
taskSetId: document.getElementById('fCardTaskSetId').value || null,
requiresVerification: document.getElementById('fRequiresVerification').checked,
taskMode: document.querySelector('input[name="modalCardTaskMode"]:checked')?.value||'RANDOM',
gameSetId: hasGameCards ? (document.getElementById('fGameSetId').value || null) : null,
gameSpieldauerIdx: hasGameCards ? (parseInt(document.getElementById('sldGameSpieldauer').value) || 2) : null,
};
} else {
// TimeLock
const maxTime = tpToMinutes('tmax');
if (maxTime < 1) { setErr('rowMaxTime','Maximaldauer muss mindestens 1 Minute betragen.'); firstError=firstError||document.getElementById('rowMaxTime'); }
const minTime = tpToMinutes('tmin');
const hasTaskTiming = document.getElementById('fTaskTimingToggle').checked;
let taskEvery = null, minTasksPerDay = null;
if (hasTaskTiming) {
taskEvery = tpToMinutes('te');
if (taskEvery < 1) { showModalError('Aufgaben-Intervall muss mindestens 1 Minute betragen.'); 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;
}
const wheelEntries = collectWheelEntries();
let spinsEvery = null, minSpinsPerDay = null;
if (wheelEntries.length > 0) {
spinsEvery = tpToMinutes('se');
if (spinsEvery < 1) { showModalError('Glücksrad-Intervall muss mindestens 1 Minute betragen.'); firstError=firstError||document.getElementById('modalError'); }
const ms = parseInt(document.getElementById('fMinSpins').value);
minSpinsPerDay = isNaN(ms)||ms<1 ? null : ms;
}
// Validierung: Aufgaben-Timing + TASK-Wheel-Eintrag schließen sich aus
if (hasTaskTiming && wheelEntries.some(e => e.type === 'TASK')) {
showModalError('Aufgaben-Timing kann nicht mit TASK-Einträgen im Glücksrad kombiniert werden.');
firstError = firstError || document.getElementById('modalError');
}
// Validierung: Unbegrenztes FREEZE ohne UNFREEZE
if (wheelEntries.some(e => e.type === 'FREEZE') && !wheelEntries.some(e => e.type === 'UNFREEZE')) {
showModalError('Das Glücksrad enthält ein unbegrenztes Einfrieren (FREEZE), aber keinen Auftau-Eintrag (UNFREEZE). Bitte einen UNFREEZE-Eintrag hinzufügen.');
firstError = firstError || document.getElementById('modalError');
}
// Validierung: Mindestaufgaben pro Tag (Zeitkollision)
const taskMode = document.querySelector('input[name="modalTaskMode"]:checked')?.value || 'RANDOM';
if (taskEvery && minTasksPerDay) {
const extraPerTask = (taskMode === 'COMMUNITY' || taskMode === 'KEYHOLDER') ? 60 : 0;
const minTaskTime = taskEvery * minTasksPerDay + extraPerTask * minTasksPerDay;
if (minTaskTime > 24 * 60) {
showModalError('Aufgaben-Konfiguration erfordert mehr als 24 Stunden pro Tag bitte Intervall oder Min.-Anzahl reduzieren.');
firstError = firstError || document.getElementById('modalError');
} else if (minTaskTime > 12 * 60) {
showModalError('⚠ Warnung: Aufgaben-Konfiguration erfordert mehr als 12 Stunden pro Tag.');
}
}
// Validierung: Mindestspins pro Tag (Zeitkollision)
if (spinsEvery && minSpinsPerDay) {
const minSpinTime = spinsEvery * minSpinsPerDay;
if (minSpinTime > 24 * 60) {
showModalError('Glücksrad-Konfiguration erfordert mehr als 24 Stunden pro Tag bitte Intervall oder Min.-Anzahl reduzieren.');
firstError = firstError || document.getElementById('modalError');
} else if (minSpinTime > 12 * 60) {
showModalError('⚠ Warnung: Glücksrad-Konfiguration erfordert mehr als 12 Stunden pro Tag.');
}
}
const penaltyType = document.getElementById('fPenaltyType').value || null;
const penaltyMinutes = tpToMinutes('pv');
const penaltyValue = penaltyType && (penaltyType==='ADD'||penaltyType==='FREEZE')
? (penaltyMinutes < 1 ? null : penaltyMinutes) : null;
if (firstError) { firstError.scrollIntoView({behavior:'smooth',block:'center'}); return; }
body = {
name, minTimeInMinutes: minTime||null, maxTimeInMinutes: maxTime,
endTimeVisible: document.getElementById('fEndTimeVisible').checked,
hygineOpeningEveryMinites: hygieneEvery,
hygineOpeningDurationMinutes: hygieneDur,
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',
penaltyType, penaltyValue,
};
}
const base = type === 'CARDLOCK' ? '/cardlock/templates' : '/timelock/templates';
const url = editId ? base+'/'+editId : base;
const method = editId ? 'PUT' : 'POST';
const btn = document.getElementById('modalSaveBtn');
btn.disabled = true;
try {
const res = await fetch(url, { method, headers:{'Content-Type':'application/json'}, body:JSON.stringify(body) });
if (res.ok) { closeModal(); resetList(); }
else { showModalError('Fehler beim Speichern.'); btn.disabled=false; }
} catch(e) { btn.disabled=false; }
} catch(e) {
console.error('saveTemplate exception:', e);
showModalError('Interner Fehler: ' + e.message);
document.getElementById('modalSaveBtn').disabled = false;
}
}
// ── Löschen ──
async function deleteTemplate(type, id, name) {
if (!confirm(`Vorlage „${name}" wirklich löschen?`)) return;
const base = type === 'CARDLOCK' ? '/cardlock/templates' : '/timelock/templates';
const res = await fetch(base+'/'+id, { method:'DELETE' });
if (res.ok||res.status===204) resetList();
}
// ── Liste: Karte anhängen ──
function appendTemplateCard(t) {
const list = document.getElementById('templateList');
const isCard = t.lockType === 'CARDLOCK';
const typeIcon = isCard
? `<img src="img/card.png" class="icon-base" alt="Karten-Lock">`
: `<span class="icon-base">🕐</span>`;
const hygText = t.hygineOpeningEveryMinites
? `alle ${fmtMinutes(t.hygineOpeningEveryMinites)}, ${fmtMinutes(t.hygineOpeningDurationMinutes)} offen`
: 'Keine';
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
? `<span style="font-size:0.7rem;background:rgba(46,204,113,0.15);border:1px solid rgba(46,204,113,0.4);color:#2ecc71;border-radius:5px;padding:0.15rem 0.5rem;margin-left:0.4rem;">🌐 Veröffentlicht</span>`
: '';
const publishBtn = t.published
? `<button onclick="event.stopPropagation();unpublishTemplate('${t.templateId}','${esc(t.name||'')}')" style="background:rgba(46,204,113,0.1);border:1px solid rgba(46,204,113,0.35);color:#2ecc71;width:auto;padding:0.35rem 0.75rem;font-size:0.8rem;">🌐 Entfernen</button>`
: `<button onclick="event.stopPropagation();openPublishModal('${t.templateId}')" style="background:rgba(52,152,219,0.1);border:1px solid rgba(52,152,219,0.35);color:#3498db;width:auto;padding:0.35rem 0.75rem;font-size:0.8rem;">🌐 Veröffentlichen</button>`;
const card = document.createElement('div');
card.className = 'template-card';
card.style.cursor = 'pointer';
card.innerHTML = `
<div class="template-card-header">
<div class="template-type-icon">
${typeIcon}
<span class="icon-lock">🔒</span>
</div>
<div style="flex:1; min-width:0;">
<div class="template-name">${esc(t.name || 'Ohne Namen')}${publishedBadge}</div>
<div class="template-meta">${metaLine}</div>
</div>
<div class="template-actions">
${publishBtn}
<button onclick="event.stopPropagation();deleteTemplate('${t.lockType}','${t.templateId}','${esc(t.name||'')}')" style="background:rgba(231,76,60,0.12);border:1px solid rgba(231,76,60,0.35);color:#e74c3c;">✕ Löschen</button>
</div>
</div>`;
card.addEventListener('click', () => editTemplate(t.templateId));
list.appendChild(card);
}
// ── Templates laden (paged) ──
async function loadNextPage() {
if (isLoading || isLastPage) return;
isLoading = true;
try {
const res = await fetch(`/templates?page=${pageNum}&size=20`);
if (!res.ok) return;
const data = await res.json();
data.content.forEach(t => appendTemplateCard(t));
isLastPage = data.last;
pageNum = data.page + 1;
if (pageNum === 1 && data.content.length === 0) {
document.getElementById('listEmpty').style.display = '';
}
} catch(e) { console.error(e); } finally {
isLoading = false;
}
}
async function resetList() {
pageNum = 0; isLastPage = false; isLoading = false;
document.getElementById('templateList').innerHTML = '';
document.getElementById('listEmpty').style.display = 'none';
await loadTaskSets();
await loadGameGroups();
loadNextPage();
loadSubscribedTemplates();
}
async function editTemplate(id) {
const res = await fetch('/templates/' + id);
if (!res.ok) return;
openModal(await res.json());
}
// ── Abonnierte Vorlagen ──
async function loadSubscribedTemplates() {
try {
const res = await fetch('/templates/subscribed');
if (!res.ok) return;
const list = await res.json();
const el = document.getElementById('subscribedList');
el.innerHTML = '';
if (!list.length) {
document.getElementById('subscribedEmpty').style.display = '';
return;
}
document.getElementById('subscribedEmpty').style.display = 'none';
list.forEach(t => appendSubscribedCard(t));
} catch(e) { console.error(e); }
}
function appendSubscribedCard(t) {
const list = document.getElementById('subscribedList');
const isCard = t.lockType === 'CARDLOCK';
const typeIcon = isCard
? `<img src="img/card.png" class="icon-base" alt="Karten-Lock">`
: `<span class="icon-base">🕐</span>`;
const authorText = t.authorName ? ` · von ${esc(t.authorName)}` : '';
const subsText = `${t.subscriberCount} Abonnent(en)`;
const card = document.createElement('div');
card.className = 'template-card';
card.innerHTML = `
<div class="template-card-header">
<div class="template-type-icon">
${typeIcon}
<span class="icon-lock">🔒</span>
</div>
<div style="flex:1; min-width:0;">
<div class="template-name">${esc(t.name || 'Ohne Namen')}</div>
<div class="template-meta">${isCard ? '🃏 Karten-Lock' : '⏱ Zeit-Lock'}${authorText} · ${subsText}</div>
</div>
<div class="template-actions">
<button onclick="forkTemplate('${t.templateId}')" style="background:rgba(52,152,219,0.1);border:1px solid rgba(52,152,219,0.35);color:#3498db;width:auto;padding:0.35rem 0.75rem;font-size:0.8rem;">📋 Kopie</button>
<button onclick="cancelSubscription('${t.templateId}','${esc(t.name||'')}')" style="background:rgba(231,76,60,0.1);border:1px solid rgba(231,76,60,0.35);color:#e74c3c;width:auto;padding:0.35rem 0.75rem;font-size:0.8rem;">✕ Abo</button>
</div>
</div>`;
list.appendChild(card);
}
async function forkTemplate(id) {
const btn = event.target;
btn.disabled = true;
try {
const res = await fetch(`/templates/${id}/fork`, { method: 'POST' });
if (res.ok) { resetList(); }
else { alert('Kopie konnte nicht erstellt werden.'); btn.disabled = false; }
} catch(e) { btn.disabled = false; }
}
async function cancelSubscription(id, name) {
if (!confirm(`Abonnement von „${name}" wirklich kündigen?`)) return;
const res = await fetch(`/templates/${id}/subscribe`, { method: 'DELETE' });
if (res.ok || res.status === 204) loadSubscribedTemplates();
}
// ── Veröffentlichen ──
let _publishTemplateId = null;
function openPublishModal(id) {
_publishTemplateId = id;
document.getElementById('publishModal').style.display = 'flex';
}
function closePublishModal() {
document.getElementById('publishModal').style.display = 'none';
_publishTemplateId = null;
}
async function confirmPublish() {
if (!_publishTemplateId) return;
const btn = document.getElementById('publishConfirmBtn');
btn.disabled = true;
try {
const res = await fetch(`/templates/${_publishTemplateId}/publish`, { method: 'PATCH' });
if (res.ok || res.status === 204) { closePublishModal(); resetList(); }
else { alert('Fehler beim Veröffentlichen.'); btn.disabled = false; }
} catch(e) { btn.disabled = false; }
}
async function unpublishTemplate(id, name) {
if (!confirm(`Veröffentlichung von „${name}" entfernen? Alle Abonnements werden gelöscht.`)) return;
const res = await fetch(`/templates/${id}/publish`, { method: 'DELETE' });
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();
}, { rootMargin: '200px' });
observer.observe(document.getElementById('scrollSentinel'));
resetList();
</script>
</body>
</html>